From 6fdf62b670dd17dbedde1d960a61db99855276f2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 18:04:13 +0000 Subject: [PATCH 001/124] chore(deps): update dependency gradle to v8.12.1 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cea7a793..e18bc253 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 5d1ef5e289383b3bca8b6e8b3ca4f074a32b1735 Mon Sep 17 00:00:00 2001 From: Lucas <55422065+lucasmz-dev@users.noreply.github.com> Date: Sat, 25 Jan 2025 12:01:37 -0300 Subject: [PATCH 002/124] Remove Bromite repo and add Cromite's Bromite has been abandoned and is insecure, Cromite is the closest thing. --- .../com/looker/droidify/model/Repository.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/com/looker/droidify/model/Repository.kt b/app/src/main/kotlin/com/looker/droidify/model/Repository.kt index f19eeef6..6897b69d 100644 --- a/app/src/main/kotlin/com/looker/droidify/model/Repository.kt +++ b/app/src/main/kotlin/com/looker/droidify/model/Repository.kt @@ -150,14 +150,6 @@ data class Repository( " by Netsyms Technologies.", fingerprint = "2581BA7B32D3AB443180C4087CAB6A7E8FB258D3A6E98870ECB3C675E4D64489" ), - defaultRepository( - address = "https://fdroid.bromite.org/fdroid/repo", - name = "Bromite", - description = "The official repository for Bromite. " + - "Bromite is a Chromium with ad blocking and enhanced p" + - "rivacy.", - fingerprint = "E1EE5CD076D7B0DC84CB2B45FB78B86DF2EB39A3B6C56BA3DC292A5E0C3B9504" - ), defaultRepository( address = "https://molly.im/fdroid/foss/fdroid/repo", name = "Molly", @@ -412,6 +404,14 @@ data class Repository( description = "The official repository for Total Commander", fingerprint = "3576596CECDD70488D61CFD90799A49B7FFD26A81A8FEF1BADEC88D069FA72C1" ), + defaultRepository( + address = "https://www.cromite.org/fdroid/repo", + name = "Cromite", + description = "The official repository for Cromite. " + + "Cromite is a Chromium fork based on Bromite with " + + "built-in support for ad blocking and an eye for privacy.", + fingerprint = "49F37E74DEE483DCA2B991334FB5A0200787430D0B5F9A783DD5F13695E9517B" + ) ) } } From 467ca8569a1647171315dc25b94ad8ec9059b000 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Sat, 15 Feb 2025 12:14:10 +0530 Subject: [PATCH 003/124] imp: Skip signature check while syncing Show update notification if skip signature is enabled Closes #255 Signed-off-by: LooKeR --- .../com/looker/droidify/database/Database.kt | 15 +++++++----- .../looker/droidify/service/SyncService.kt | 24 +++++++++++-------- .../droidify/ui/appList/AppListViewModel.kt | 14 +++++++---- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/app/src/main/kotlin/com/looker/droidify/database/Database.kt b/app/src/main/kotlin/com/looker/droidify/database/Database.kt index 119526f3..8c6ae7ee 100644 --- a/app/src/main/kotlin/com/looker/droidify/database/Database.kt +++ b/app/src/main/kotlin/com/looker/droidify/database/Database.kt @@ -577,11 +577,12 @@ object Database { .map { get(packageName, null) } .flowOn(Dispatchers.IO) - suspend fun getUpdates(): List = withContext(Dispatchers.IO) { + suspend fun getUpdates(skipSignatureCheck: Boolean): List = withContext(Dispatchers.IO) { query( installed = true, updates = true, searchQuery = "", + skipSignatureCheck = skipSignatureCheck, section = ProductItem.Section.All, order = SortOrder.NAME, signal = null @@ -592,11 +593,11 @@ object Database { } } - fun getUpdatesStream(): Flow> = flowOf(Unit) + fun getUpdatesStream(skipSignatureCheck: Boolean): Flow> = flowOf(Unit) .onCompletion { if (it == null) emitAll(flowCollection(Subject.Products)) } // Crashes due to immediate retrieval of data? .onEach { delay(50) } - .map { getUpdates() } + .map { getUpdates(skipSignatureCheck) } .flowOn(Dispatchers.IO) fun get(packageName: String, signal: CancellationSignal?): List { @@ -631,6 +632,7 @@ object Database { fun query( installed: Boolean, updates: Boolean, + skipSignatureCheck: Boolean = false, searchQuery: String, section: ProductItem.Section, order: SortOrder, @@ -638,9 +640,10 @@ object Database { ): Cursor { val builder = QueryBuilder() - val signatureMatches = """installed.${Schema.Installed.ROW_SIGNATURE} IS NOT NULL AND - product.${Schema.Product.ROW_SIGNATURES} LIKE ('%.' || installed.${Schema.Installed.ROW_SIGNATURE} || '.%') AND - product.${Schema.Product.ROW_SIGNATURES} != ''""" + val signatureMatches = if (skipSignatureCheck) "1" + else """installed.${Schema.Installed.ROW_SIGNATURE} IS NOT NULL AND + product.${Schema.Product.ROW_SIGNATURES} LIKE ('%.' || installed.${Schema.Installed.ROW_SIGNATURE} || '.%') AND + product.${Schema.Product.ROW_SIGNATURES} != ''""" builder += """SELECT product.rowid AS _id, product.${Schema.Product.ROW_REPOSITORY_ID}, product.${Schema.Product.ROW_PACKAGE_NAME}, product.${Schema.Product.ROW_NAME}, diff --git a/app/src/main/kotlin/com/looker/droidify/service/SyncService.kt b/app/src/main/kotlin/com/looker/droidify/service/SyncService.kt index f94f103a..3442bf39 100644 --- a/app/src/main/kotlin/com/looker/droidify/service/SyncService.kt +++ b/app/src/main/kotlin/com/looker/droidify/service/SyncService.kt @@ -22,7 +22,6 @@ import com.looker.droidify.utility.common.extension.getColorFromAttr import com.looker.droidify.utility.common.extension.notificationManager import com.looker.droidify.utility.common.extension.startServiceCompat import com.looker.droidify.utility.common.extension.stopForegroundCompat -import com.looker.droidify.utility.common.log import com.looker.droidify.utility.common.result.Result import com.looker.droidify.utility.common.sdkAbove import com.looker.droidify.datastore.SettingsRepository @@ -53,6 +52,7 @@ import kotlinx.coroutines.withContext import java.lang.ref.WeakReference import javax.inject.Inject import com.looker.droidify.R +import kotlinx.coroutines.FlowPreview import android.R as AndroidR import com.looker.droidify.R.string as stringRes import com.looker.droidify.R.style as styleRes @@ -145,7 +145,8 @@ class SyncService : ConnectionService() { } suspend fun updateAllApps() { - updateAllAppsInternal() + val skipSignature = settingsRepository.getInitial().ignoreSignature + updateAllAppsInternal(skipSignature) } fun setUpdateNotificationBlocker(fragment: Fragment?) { @@ -198,6 +199,7 @@ class SyncService : ConnectionService() { private val binder = Binder() override fun onBind(intent: Intent): Binder = binder + @OptIn(FlowPreview::class) override fun onCreate() { super.onCreate() @@ -389,7 +391,8 @@ class SyncService : ConnectionService() { handleUpdates( hasUpdates = hasUpdates, notifyUpdates = setting.notifyUpdate, - autoUpdate = setting.autoUpdate + autoUpdate = setting.autoUpdate, + skipSignature = setting.ignoreSignature, ) } } @@ -470,7 +473,8 @@ class SyncService : ConnectionService() { private suspend fun handleUpdates( hasUpdates: Boolean, notifyUpdates: Boolean, - autoUpdate: Boolean + autoUpdate: Boolean, + skipSignature: Boolean, ) { try { if (!hasUpdates) { @@ -481,15 +485,16 @@ class SyncService : ConnectionService() { return } val blocked = updateNotificationBlockerFragment?.get()?.isAdded == true - val updates = Database.ProductAdapter.getUpdates() + val updates = Database.ProductAdapter.getUpdates(skipSignature) if (!blocked && updates.isNotEmpty()) { if (notifyUpdates) displayUpdatesNotification(updates) - if (autoUpdate) updateAllAppsInternal() + if (autoUpdate) updateAllAppsInternal(skipSignature) } handleUpdates( hasUpdates = false, notifyUpdates = notifyUpdates, - autoUpdate = autoUpdate + autoUpdate = autoUpdate, + skipSignature = skipSignature, ) } finally { withContext(NonCancellable) { @@ -499,10 +504,9 @@ class SyncService : ConnectionService() { } } - private suspend fun updateAllAppsInternal() { - log("Check Running", "Syncing") + private suspend fun updateAllAppsInternal(skipSignature: Boolean) { Database.ProductAdapter - .getUpdates() + .getUpdates(skipSignature) // Update Droid-ify the last .sortedBy { if (it.packageName == packageName) 1 else -1 } .map { diff --git a/app/src/main/kotlin/com/looker/droidify/ui/appList/AppListViewModel.kt b/app/src/main/kotlin/com/looker/droidify/ui/appList/AppListViewModel.kt index 076a2084..63304e0a 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/appList/AppListViewModel.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/appList/AppListViewModel.kt @@ -15,7 +15,9 @@ import com.looker.droidify.service.SyncService import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -25,14 +27,18 @@ class AppListViewModel settingsRepository: SettingsRepository ) : ViewModel() { + private val skipSignatureStream = settingsRepository.get { ignoreSignature } + val reposStream = Database.RepositoryAdapter .getAllStream() .asStateFlow(emptyList()) - val showUpdateAllButton = Database.ProductAdapter - .getUpdatesStream() - .map { it.isNotEmpty() } - .asStateFlow(false) + @OptIn(ExperimentalCoroutinesApi::class) + val showUpdateAllButton = skipSignatureStream.flatMapConcat { skip -> + Database.ProductAdapter + .getUpdatesStream(skip) + .map { it.isNotEmpty() } + }.asStateFlow(false) val sortOrderFlow = settingsRepository.get { sortOrder } .asStateFlow(SortOrder.UPDATED) From 1b60198309e83432a1a7fc3d698d302dc7fcb693 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Sat, 15 Feb 2025 12:30:40 +0530 Subject: [PATCH 004/124] fix: False background warning message Fixes the message of missing background access in settings page Closes #712 Signed-off-by: LooKeR --- .../droidify/ui/settings/SettingsFragment.kt | 42 ++++++++----------- .../droidify/ui/settings/SettingsViewModel.kt | 25 +++++------ 2 files changed, 29 insertions(+), 38 deletions(-) diff --git a/app/src/main/kotlin/com/looker/droidify/ui/settings/SettingsFragment.kt b/app/src/main/kotlin/com/looker/droidify/ui/settings/SettingsFragment.kt index 9f44d849..fd1b9e7c 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/settings/SettingsFragment.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/settings/SettingsFragment.kt @@ -23,13 +23,11 @@ import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText -import com.looker.droidify.utility.common.SdkCheck -import com.looker.droidify.utility.common.extension.getColorFromAttr -import com.looker.droidify.utility.common.extension.homeAsUp -import com.looker.droidify.utility.common.extension.systemBarsPadding -import com.looker.droidify.utility.common.extension.updateAsMutable -import com.looker.droidify.utility.common.isIgnoreBatteryEnabled -import com.looker.droidify.utility.common.requestBatteryFreedom +import com.looker.droidify.BuildConfig +import com.looker.droidify.R +import com.looker.droidify.databinding.EnumTypeBinding +import com.looker.droidify.databinding.SettingsPageBinding +import com.looker.droidify.databinding.SwitchTypeBinding import com.looker.droidify.datastore.Settings import com.looker.droidify.datastore.extension.autoSyncName import com.looker.droidify.datastore.extension.installerName @@ -40,20 +38,21 @@ import com.looker.droidify.datastore.model.AutoSync import com.looker.droidify.datastore.model.InstallerType import com.looker.droidify.datastore.model.ProxyType import com.looker.droidify.datastore.model.Theme -import com.looker.droidify.BuildConfig -import com.looker.droidify.databinding.EnumTypeBinding -import com.looker.droidify.databinding.SettingsPageBinding -import com.looker.droidify.databinding.SwitchTypeBinding +import com.looker.droidify.utility.common.SdkCheck +import com.looker.droidify.utility.common.extension.getColorFromAttr +import com.looker.droidify.utility.common.extension.homeAsUp +import com.looker.droidify.utility.common.extension.systemBarsPadding +import com.looker.droidify.utility.common.extension.updateAsMutable +import com.looker.droidify.utility.common.isIgnoreBatteryEnabled +import com.looker.droidify.utility.common.requestBatteryFreedom import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import java.util.Locale import kotlin.time.Duration import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours import com.google.android.material.R as MaterialR -import com.looker.droidify.R @AndroidEntryPoint class SettingsFragment : Fragment() { @@ -119,9 +118,7 @@ class SettingsFragment : Fragment() { ): View { _binding = SettingsPageBinding.inflate(inflater, container, false) binding.nestedScrollView.systemBarsPadding() - if (requireContext().isIgnoreBatteryEnabled()) { - viewModel.allowBackground() - } + viewModel.toggleBackgroundAccess(requireContext().isIgnoreBatteryEnabled()) val toolbar = binding.toolbar toolbar.navigationIcon = toolbar.context.homeAsUp toolbar.setNavigationOnClickListener { activity?.onBackPressedDispatcher?.onBackPressed() } @@ -283,6 +280,7 @@ class SettingsFragment : Fragment() { exportRepos.title.text = getString(R.string.export_repos_title) exportRepos.content.text = getString(R.string.export_repos_DESC) + allowBackgroundWork.root.isVisible = false allowBackgroundWork.title.text = getString(R.string.require_background_access) allowBackgroundWork.content.text = getString(R.string.require_background_access_DESC) @@ -315,8 +313,8 @@ class SettingsFragment : Fragment() { launch { viewModel.settingsFlow.collect { setting -> updateSettings(setting) - binding.allowBackgroundWork.root.isVisible = !viewModel.backgroundTask.first() - && setting.autoSync != AutoSync.NEVER + binding.allowBackgroundWork.root.isVisible = + !viewModel.isBackgroundAllowed && setting.autoSync != AutoSync.NEVER } } } @@ -326,9 +324,7 @@ class SettingsFragment : Fragment() { override fun onResume() { super.onResume() - if (requireContext().isIgnoreBatteryEnabled()) { - viewModel.allowBackground() - } + viewModel.toggleBackgroundAccess(requireContext().isIgnoreBatteryEnabled()) } override fun onDestroyView() { @@ -376,9 +372,7 @@ class SettingsFragment : Fragment() { } allowBackgroundWork.root.setOnClickListener { requireContext().requestBatteryFreedom() - if (requireContext().isIgnoreBatteryEnabled()) { - viewModel.allowBackground() - } + viewModel.toggleBackgroundAccess(requireContext().isIgnoreBatteryEnabled()) } creditFoxy.root.setOnClickListener { openLink(FOXY_DROID_URL) diff --git a/app/src/main/kotlin/com/looker/droidify/ui/settings/SettingsViewModel.kt b/app/src/main/kotlin/com/looker/droidify/ui/settings/SettingsViewModel.kt index 77d81b34..76456e91 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/settings/SettingsViewModel.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/settings/SettingsViewModel.kt @@ -7,35 +7,34 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.core.os.LocaleListCompat import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.looker.droidify.R +import com.looker.droidify.database.Database +import com.looker.droidify.database.RepositoryExporter import com.looker.droidify.datastore.Settings import com.looker.droidify.datastore.SettingsRepository import com.looker.droidify.datastore.get import com.looker.droidify.datastore.model.AutoSync import com.looker.droidify.datastore.model.InstallerType -import com.looker.droidify.datastore.model.InstallerType.* +import com.looker.droidify.datastore.model.InstallerType.ROOT +import com.looker.droidify.datastore.model.InstallerType.SHIZUKU import com.looker.droidify.datastore.model.ProxyType import com.looker.droidify.datastore.model.Theme -import com.looker.droidify.database.Database -import com.looker.droidify.database.RepositoryExporter -import com.looker.droidify.work.CleanUpWorker import com.looker.droidify.installer.installers.isMagiskGranted import com.looker.droidify.installer.installers.isShizukuAlive import com.looker.droidify.installer.installers.isShizukuGranted import com.looker.droidify.installer.installers.isShizukuInstalled import com.looker.droidify.installer.installers.requestPermissionListener +import com.looker.droidify.work.CleanUpWorker import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import java.util.Locale import javax.inject.Inject import kotlin.time.Duration -import com.looker.droidify.R @HiltViewModel class SettingsViewModel @@ -49,8 +48,8 @@ class SettingsViewModel } val settingsFlow get() = settingsRepository.data - private val _backgroundTask = MutableStateFlow(false) - val backgroundTask = _backgroundTask.asStateFlow() + var isBackgroundAllowed = true + private set private val _snackbarStringId = MutableSharedFlow() val snackbarStringId = _snackbarStringId.asSharedFlow() @@ -59,10 +58,8 @@ class SettingsViewModel fun getInitialSetting(block: Settings.() -> T): Flow = initialSetting.map { it.block() } - fun allowBackground() { - viewModelScope.launch { - _backgroundTask.emit(true) - } + fun toggleBackgroundAccess(enable: Boolean) { + isBackgroundAllowed = enable } fun setLanguage(language: String) { @@ -172,7 +169,7 @@ class SettingsViewModel } else if (isShizukuGranted()) { settingsRepository.setInstallerType(installerType) } else if (!isShizukuGranted()) { - if(requestPermissionListener()) { + if (requestPermissionListener()) { settingsRepository.setInstallerType(installerType) } } From 8435c8f7eada3c2b0b12b9b40dffee3557ba517f Mon Sep 17 00:00:00 2001 From: LooKeR Date: Sat, 15 Feb 2025 13:27:09 +0530 Subject: [PATCH 005/124] imp: Add sync progress in home page Add a progress bar which shows up when syncing Signed-off-by: LooKeR --- .../looker/droidify/service/SyncService.kt | 20 ++++++++-- .../droidify/ui/tabsFragment/TabsFragment.kt | 39 ++++++++++++++----- app/src/main/res/layout/tabs_toolbar.xml | 10 +++-- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/app/src/main/kotlin/com/looker/droidify/service/SyncService.kt b/app/src/main/kotlin/com/looker/droidify/service/SyncService.kt index 3442bf39..18afd196 100644 --- a/app/src/main/kotlin/com/looker/droidify/service/SyncService.kt +++ b/app/src/main/kotlin/com/looker/droidify/service/SyncService.kt @@ -53,6 +53,7 @@ import java.lang.ref.WeakReference import javax.inject.Inject import com.looker.droidify.R import kotlinx.coroutines.FlowPreview +import kotlin.math.roundToInt import android.R as AndroidR import com.looker.droidify.R.string as stringRes import com.looker.droidify.R.style as styleRes @@ -69,15 +70,16 @@ class SyncService : ConnectionService() { private const val MAX_UPDATE_NOTIFICATION = 5 private const val ACTION_CANCEL = "${BuildConfig.APPLICATION_ID}.intent.action.CANCEL" - private val syncState = MutableSharedFlow() + val syncState = MutableSharedFlow() } @Inject lateinit var settingsRepository: SettingsRepository sealed class State(val name: String) { - data class Connecting(val appName: String) : State(appName) - data class Syncing( + class Connecting(appName: String) : State(appName) + + class Syncing( val appName: String, val stage: RepositoryUpdater.Stage, val read: DataSize, @@ -85,6 +87,18 @@ class SyncService : ConnectionService() { ) : State(appName) data object Finish : State("") + + val progress: Int + get() = when (this) { + is Connecting -> Int.MIN_VALUE + Finish -> Int.MAX_VALUE + is Syncing -> when(stage) { + RepositoryUpdater.Stage.DOWNLOAD -> ((read percentBy total) * 0.4F).roundToInt() + RepositoryUpdater.Stage.PROCESS -> 50 + RepositoryUpdater.Stage.MERGE -> 75 + RepositoryUpdater.Stage.COMMIT -> 90 + } + } } private class Task(val repositoryId: Long, val manual: Boolean) diff --git a/app/src/main/kotlin/com/looker/droidify/ui/tabsFragment/TabsFragment.kt b/app/src/main/kotlin/com/looker/droidify/ui/tabsFragment/TabsFragment.kt index e675a9c4..cc217e08 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/tabsFragment/TabsFragment.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/tabsFragment/TabsFragment.kt @@ -14,6 +14,7 @@ import android.widget.FrameLayout import android.widget.TextView import androidx.activity.OnBackPressedCallback import androidx.appcompat.widget.SearchView +import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels @@ -28,21 +29,21 @@ import com.google.android.material.elevation.SurfaceColors import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel import com.google.android.material.tabs.TabLayoutMediator +import com.looker.droidify.R +import com.looker.droidify.databinding.TabsToolbarBinding +import com.looker.droidify.datastore.extension.sortOrderName +import com.looker.droidify.datastore.model.SortOrder +import com.looker.droidify.model.ProductItem +import com.looker.droidify.service.Connection +import com.looker.droidify.service.SyncService +import com.looker.droidify.ui.ScreenFragment +import com.looker.droidify.ui.appList.AppListFragment import com.looker.droidify.utility.common.device.Huawei import com.looker.droidify.utility.common.extension.dp import com.looker.droidify.utility.common.extension.getMutatedIcon import com.looker.droidify.utility.common.extension.selectableBackground import com.looker.droidify.utility.common.extension.systemBarsPadding import com.looker.droidify.utility.common.sdkAbove -import com.looker.droidify.datastore.extension.sortOrderName -import com.looker.droidify.datastore.model.SortOrder -import com.looker.droidify.R -import com.looker.droidify.databinding.TabsToolbarBinding -import com.looker.droidify.model.ProductItem -import com.looker.droidify.service.Connection -import com.looker.droidify.service.SyncService -import com.looker.droidify.ui.ScreenFragment -import com.looker.droidify.ui.appList.AppListFragment import com.looker.droidify.utility.extension.resources.sizeScaled import com.looker.droidify.utility.extension.screenActivity import com.looker.droidify.widget.DividerConfiguration @@ -203,7 +204,6 @@ class TabsFragment : ScreenFragment() { syncRepositoriesMenuItem = add(0, 0, 0, stringRes.sync_repositories) .setIcon(toolbar.context.getMutatedIcon(R.drawable.ic_sync)) .setOnMenuItemClickListener { -// SyncWorker.startSyncWork(requireContext()) syncConnection.binder?.sync(SyncService.SyncRequest.MANUAL) true } @@ -297,6 +297,25 @@ class TabsFragment : ScreenFragment() { onBackPressedCallback?.isEnabled = it != BackAction.None } } + launch { + SyncService.syncState.collect { + when (it) { + is SyncService.State.Connecting -> { + tabsBinding.syncState.isVisible = true + tabsBinding.syncState.isIndeterminate = true + } + + SyncService.State.Finish -> { + tabsBinding.syncState.isGone = true + } + + is SyncService.State.Syncing -> { + tabsBinding.syncState.isVisible = true + tabsBinding.syncState.setProgressCompat(it.progress, true) + } + } + } + } } } diff --git a/app/src/main/res/layout/tabs_toolbar.xml b/app/src/main/res/layout/tabs_toolbar.xml index 8f77d5cf..21facbdc 100644 --- a/app/src/main/res/layout/tabs_toolbar.xml +++ b/app/src/main/res/layout/tabs_toolbar.xml @@ -44,8 +44,12 @@ android:layout_marginEnd="20dp" android:src="@drawable/ic_arrow_down" tools:ignore="ContentDescription" /> - - - \ No newline at end of file + + + From e63325cef87c47bcc52400de20aee8316797b3c6 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Sat, 15 Feb 2025 13:27:25 +0530 Subject: [PATCH 006/124] imp: Animate app download progress bar Signed-off-by: LooKeR --- .../looker/droidify/ui/appDetail/AppDetailAdapter.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailAdapter.kt b/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailAdapter.kt index 4f3a2056..435a8006 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailAdapter.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailAdapter.kt @@ -56,6 +56,7 @@ import com.looker.droidify.model.Release import com.looker.droidify.model.Repository import com.looker.droidify.model.findSuggested import com.looker.droidify.network.DataSize +import com.looker.droidify.network.percentBy import com.looker.droidify.utility.PackageItemResolver import com.looker.droidify.utility.common.extension.authentication import com.looker.droidify.utility.common.extension.copyToClipboard @@ -1365,12 +1366,10 @@ class AppDetailAdapter(private val callbacks: Callbacks) : ) holder.progress.isIndeterminate = status.total == null if (status.total != null) { - holder.progress.progress = - ( - holder.progress.max.toFloat() * - status.read.value / - status.total.value - ).roundToInt() + holder.progress.setProgressCompat( + status.read.value percentBy status.total.value, + true + ) } } From 85d02c0b4ad83f73c19565babbeb7c9dd2808c77 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Sat, 15 Feb 2025 13:27:44 +0530 Subject: [PATCH 007/124] ref: Inline back actions function Signed-off-by: LooKeR --- .../droidify/ui/tabsFragment/TabsViewModel.kt | 43 +++++++------------ 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/app/src/main/kotlin/com/looker/droidify/ui/tabsFragment/TabsViewModel.kt b/app/src/main/kotlin/com/looker/droidify/ui/tabsFragment/TabsViewModel.kt index 0957d3f8..0efcc771 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/tabsFragment/TabsViewModel.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/tabsFragment/TabsViewModel.kt @@ -3,19 +3,19 @@ package com.looker.droidify.ui.tabsFragment import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.looker.droidify.utility.common.extension.asStateFlow +import com.looker.droidify.database.Database import com.looker.droidify.datastore.SettingsRepository import com.looker.droidify.datastore.get import com.looker.droidify.datastore.model.SortOrder import com.looker.droidify.model.ProductItem -import com.looker.droidify.database.Database import com.looker.droidify.ui.tabsFragment.TabsFragment.BackAction +import com.looker.droidify.utility.common.extension.asStateFlow import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow -import javax.inject.Inject import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel class TabsViewModel @Inject constructor( @@ -57,7 +57,18 @@ class TabsViewModel @Inject constructor( val showSections = MutableStateFlow(false) - val backAction = combine(currentSection, isSearchActionItemExpanded, showSections, ::calcBackAction).asStateFlow(BackAction.None) + val backAction = combine( + currentSection, + isSearchActionItemExpanded, + showSections + ) { currentSection, isSearchActionItemExpanded, showSections -> + when { + currentSection != ProductItem.Section.All -> BackAction.ProductAll + isSearchActionItemExpanded -> BackAction.CollapseSearchView + showSections -> BackAction.HideSections + else -> BackAction.None + } + }.asStateFlow(BackAction.None) fun setSection(section: ProductItem.Section) { savedStateHandle[STATE_SECTION] = section @@ -69,30 +80,6 @@ class TabsViewModel @Inject constructor( } } - private fun calcBackAction( - currentSection: ProductItem.Section, - isSearchActionItemExpanded: Boolean, - showSections: Boolean, - ): BackAction { - return when { - currentSection != ProductItem.Section.All -> { - BackAction.ProductAll - } - - isSearchActionItemExpanded -> { - BackAction.CollapseSearchView - } - - showSections -> { - BackAction.HideSections - } - - else -> { - BackAction.None - } - } - } - companion object { private const val STATE_SECTION = "section" } From 8daeeb5bf93ca19f1142cc46310d6c1d1899a2ee Mon Sep 17 00:00:00 2001 From: LooKeR Date: Sat, 15 Feb 2025 13:51:00 +0530 Subject: [PATCH 008/124] imp: Show a Installing notification when app is installing Closes #906 Signed-off-by: LooKeR --- .../com/looker/droidify/installer/InstallManager.kt | 9 +++++++++ .../com/looker/droidify/installer/model/InstallItem.kt | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/looker/droidify/installer/InstallManager.kt b/app/src/main/kotlin/com/looker/droidify/installer/InstallManager.kt index 8420c544..e6d6a87f 100644 --- a/app/src/main/kotlin/com/looker/droidify/installer/InstallManager.kt +++ b/app/src/main/kotlin/com/looker/droidify/installer/InstallManager.kt @@ -16,6 +16,8 @@ import com.looker.droidify.installer.installers.session.SessionInstaller import com.looker.droidify.installer.installers.shizuku.ShizukuInstaller import com.looker.droidify.installer.model.InstallItem import com.looker.droidify.installer.model.InstallState +import com.looker.droidify.installer.notification.createInstallNotification +import com.looker.droidify.installer.notification.installNotification import com.looker.droidify.installer.notification.removeInstallNotification import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel @@ -87,6 +89,13 @@ class InstallManager( }.consumeEach { item -> if (state.value.containsKey(item.packageName)) { updateState { put(item.packageName, InstallState.Installing) } + context.notificationManager?.installNotification( + packageName = item.packageName.name, + notification = context.createInstallNotification( + appName = item.packageName.name, + state = InstallState.Installing, + ) + ) val success = installer.use { it.install(item) } diff --git a/app/src/main/kotlin/com/looker/droidify/installer/model/InstallItem.kt b/app/src/main/kotlin/com/looker/droidify/installer/model/InstallItem.kt index 0c604daa..8f4ce263 100644 --- a/app/src/main/kotlin/com/looker/droidify/installer/model/InstallItem.kt +++ b/app/src/main/kotlin/com/looker/droidify/installer/model/InstallItem.kt @@ -3,7 +3,7 @@ package com.looker.droidify.installer.model import com.looker.droidify.domain.model.PackageName import com.looker.droidify.domain.model.toPackageName -data class InstallItem( +class InstallItem( val packageName: PackageName, val installFileName: String ) From 93a277653ac53a98fad78acfdc96da9c0d9a86bb Mon Sep 17 00:00:00 2001 From: LooKeR Date: Sat, 15 Feb 2025 15:00:25 +0530 Subject: [PATCH 009/124] rem: Flattr Id parsing Flattr is no longer available Signed-off-by: LooKeR --- .../looker/droidify/index/IndexV1Parser.kt | 3 -- .../com/looker/droidify/model/Product.kt | 50 ------------------- .../droidify/ui/appDetail/AppDetailAdapter.kt | 6 --- .../serialization/ProductSerialization.kt | 7 --- 4 files changed, 66 deletions(-) diff --git a/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt b/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt index eff4cb2d..b39a85ff 100644 --- a/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt +++ b/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt @@ -125,7 +125,6 @@ object IndexV1Parser { Product.Donate.Regular::class, Product.Donate.Bitcoin::class, Product.Donate.Litecoin::class, - Product.Donate.Flattr::class, Product.Donate.Liberapay::class, Product.Donate.OpenCollective::class ) @@ -236,7 +235,6 @@ object IndexV1Parser { private const val KEY_PRODUCT_LICENSE = "license" private const val KEY_PRODUCT_DONATE = "donate" private const val KEY_PRODUCT_BITCOIN = "bitcoin" - private const val KEY_PRODUCT_FLATTRID = "flattrID" private const val KEY_PRODUCT_LIBERAPAYID = "liberapayID" private const val KEY_PRODUCT_OPENCOLLECTIVE = "openCollective" private const val KEY_PRODUCT_LOCALIZED = "localized" @@ -295,7 +293,6 @@ object IndexV1Parser { key.string(KEY_PRODUCT_DONATE) -> donates += Product.Donate.Regular(valueAsString) key.string(KEY_PRODUCT_BITCOIN) -> donates += Product.Donate.Bitcoin(valueAsString) - key.string(KEY_PRODUCT_FLATTRID) -> donates += Product.Donate.Flattr(valueAsString) key.string(KEY_PRODUCT_LIBERAPAYID) -> donates += Product.Donate.Liberapay( valueAsString ) diff --git a/app/src/main/kotlin/com/looker/droidify/model/Product.kt b/app/src/main/kotlin/com/looker/droidify/model/Product.kt index 99978389..0083808a 100644 --- a/app/src/main/kotlin/com/looker/droidify/model/Product.kt +++ b/app/src/main/kotlin/com/looker/droidify/model/Product.kt @@ -1,9 +1,5 @@ package com.looker.droidify.model -import com.looker.droidify.domain.model.App -import com.looker.droidify.domain.model.Donation -import com.looker.droidify.domain.model.Screenshots - data class Product( var repositoryId: Long, val packageName: String, @@ -34,7 +30,6 @@ data class Product( data class Regular(val url: String) : Donate() data class Bitcoin(val address: String) : Donate() data class Litecoin(val address: String) : Donate() - data class Flattr(val id: String) : Donate() data class Liberapay(val id: String) : Donate() data class OpenCollective(val id: String) : Donate() } @@ -116,48 +111,3 @@ fun List>.findSuggested( } ) ) -// -//fun App.toProduct() = Product( -// packageName = metadata.packageName.name, -// name = metadata.name, -// summary = metadata.summary, -// description = metadata.description, -// whatsNew = metadata.whatsNew, -// icon = metadata.icon, -// metadataIcon = "", -// author = Product.Author( -// name = author.name, -// email = author.email, -// web = author.web, -// ), -// source = links.sourceCode, -// changelog = links.changelog, -// web = links.webSite, -// tracker = links.issueTracker, -// added = metadata.added, -// updated = metadata.lastUpdated, -// suggestedVersionCode = metadata.suggestedVersionCode, -// categories = categories, -// antiFeatures = metadata.antiFeatures, -// licenses = listOf(metadata.license), -// donates = donation.toLegacy(), -// screenshots = screenshots.toLegacy(), -// releases = packages -//) - -fun Donation.toLegacy() = buildList { - regularUrl?.let { add(Product.Donate.Regular(it)) } - bitcoinAddress?.let { add(Product.Donate.Bitcoin(it)) } - flattrId?.let { add(Product.Donate.Flattr(it)) } - liteCoinAddress?.let { add(Product.Donate.Litecoin(it)) } - openCollectiveId?.let { add(Product.Donate.OpenCollective(it)) } - librePayId?.let { add(Product.Donate.Liberapay(it)) } -} - -fun Screenshots.toLegacy() = buildList { - phone.forEach { add(Product.Screenshot("en-US", Product.Screenshot.Type.PHONE, it)) } - sevenInch.forEach { add(Product.Screenshot("en-US", Product.Screenshot.Type.SMALL_TABLET, it)) } - tenInch.forEach { add(Product.Screenshot("en-US", Product.Screenshot.Type.LARGE_TABLET, it)) } - tv.forEach { add(Product.Screenshot("en-US", Product.Screenshot.Type.PHONE, it)) } - wear.forEach { add(Product.Screenshot("en-US", Product.Screenshot.Type.PHONE, it)) } -} diff --git a/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailAdapter.kt b/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailAdapter.kt index 435a8006..30cf106b 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailAdapter.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailAdapter.kt @@ -314,7 +314,6 @@ class AppDetailAdapter(private val callbacks: Callbacks) : is Product.Donate.Regular -> drawableRes.ic_donate is Product.Donate.Bitcoin -> drawableRes.ic_donate_bitcoin is Product.Donate.Litecoin -> drawableRes.ic_donate_litecoin - is Product.Donate.Flattr -> drawableRes.ic_donate_flattr is Product.Donate.Liberapay -> drawableRes.ic_donate_liberapay is Product.Donate.OpenCollective -> drawableRes.ic_donate_opencollective } @@ -323,7 +322,6 @@ class AppDetailAdapter(private val callbacks: Callbacks) : is Product.Donate.Regular -> context.getString(stringRes.website) is Product.Donate.Bitcoin -> "Bitcoin" is Product.Donate.Litecoin -> "Litecoin" - is Product.Donate.Flattr -> "Flattr" is Product.Donate.Liberapay -> "Liberapay" is Product.Donate.OpenCollective -> "Open Collective" } @@ -332,10 +330,6 @@ class AppDetailAdapter(private val callbacks: Callbacks) : is Product.Donate.Regular -> Uri.parse(donate.url) is Product.Donate.Bitcoin -> Uri.parse("bitcoin:${donate.address}") is Product.Donate.Litecoin -> Uri.parse("litecoin:${donate.address}") - is Product.Donate.Flattr -> Uri.parse( - "https://flattr.com/thing/${donate.id}" - ) - is Product.Donate.Liberapay -> Uri.parse( "https://liberapay.com/~${donate.id}" ) diff --git a/app/src/main/kotlin/com/looker/droidify/utility/serialization/ProductSerialization.kt b/app/src/main/kotlin/com/looker/droidify/utility/serialization/ProductSerialization.kt index 33640b96..99f1fa69 100644 --- a/app/src/main/kotlin/com/looker/droidify/utility/serialization/ProductSerialization.kt +++ b/app/src/main/kotlin/com/looker/droidify/utility/serialization/ProductSerialization.kt @@ -53,11 +53,6 @@ fun Product.serialize(generator: JsonGenerator) { writeStringField(ADDRESS, it.address) } - is Product.Donate.Flattr -> { - writeStringField(TYPE, DONATION_FLATTR) - writeStringField(ID, it.id) - } - is Product.Donate.Liberapay -> { writeStringField(TYPE, DONATION_LIBERAPAY) writeStringField(ID, it.id) @@ -149,7 +144,6 @@ fun JsonParser.product(): Product { DONATION_EMPTY -> Product.Donate.Regular(url) DONATION_BITCOIN -> Product.Donate.Bitcoin(address) DONATION_LITECOIN -> Product.Donate.Litecoin(address) - DONATION_FLATTR -> Product.Donate.Flattr(id) DONATION_LIBERAPAY -> Product.Donate.Liberapay(id) DONATION_OPENCOLLECTIVE -> Product.Donate.OpenCollective(id) else -> null @@ -243,6 +237,5 @@ private const val KEY_EMPTY = "" private const val DONATION_EMPTY = "" private const val DONATION_BITCOIN = "bitcoin" private const val DONATION_LITECOIN = "litecoin" -private const val DONATION_FLATTR = "flattr" private const val DONATION_LIBERAPAY = "liberapay" private const val DONATION_OPENCOLLECTIVE = "openCollective" From ddfb2132adacd56b1ffd5f956c5c4c8d623e5986 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Sat, 15 Feb 2025 15:22:27 +0530 Subject: [PATCH 010/124] imp: Parse wear and tv screenshots Closes #914 Signed-off-by: LooKeR --- .../looker/droidify/index/IndexV1Parser.kt | 88 +++++++++---------- .../com/looker/droidify/model/Product.kt | 6 +- 2 files changed, 49 insertions(+), 45 deletions(-) diff --git a/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt b/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt index b39a85ff..0e854b79 100644 --- a/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt +++ b/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt @@ -5,16 +5,21 @@ import androidx.core.os.ConfigurationCompat.getLocales import androidx.core.os.LocaleListCompat import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonToken -import com.looker.droidify.utility.common.SdkCheck import com.looker.core.common.extension.Json import com.looker.core.common.extension.collectDistinctNotEmptyStrings import com.looker.core.common.extension.collectNotNull import com.looker.core.common.extension.forEach import com.looker.core.common.extension.forEachKey import com.looker.core.common.extension.illegal -import com.looker.droidify.utility.common.nullIfEmpty import com.looker.droidify.model.Product +import com.looker.droidify.model.Product.Screenshot.Type.LARGE_TABLET +import com.looker.droidify.model.Product.Screenshot.Type.PHONE +import com.looker.droidify.model.Product.Screenshot.Type.SMALL_TABLET +import com.looker.droidify.model.Product.Screenshot.Type.TV +import com.looker.droidify.model.Product.Screenshot.Type.WEAR import com.looker.droidify.model.Release +import com.looker.droidify.utility.common.SdkCheck +import com.looker.droidify.utility.common.nullIfEmpty import java.io.InputStream object IndexV1Parser { @@ -34,7 +39,9 @@ object IndexV1Parser { private class Screenshots( val phone: List, val smallTablet: List, - val largeTablet: List + val largeTablet: List, + val wear: List, + val tv: List, ) private class Localized( @@ -90,10 +97,9 @@ object IndexV1Parser { } private fun Map.find(callback: (String, Localized) -> T?): T? { - return getAndCall("en-US", callback) ?: getAndCall("en_US", callback) ?: getAndCall( - "en", - callback - ) + return getAndCall("en-US", callback) + ?: getAndCall("en_US", callback) + ?: getAndCall("en", callback) } private fun Map.findLocalized(callback: (Localized) -> T?): T? { @@ -239,9 +245,11 @@ object IndexV1Parser { private const val KEY_PRODUCT_OPENCOLLECTIVE = "openCollective" private const val KEY_PRODUCT_LOCALIZED = "localized" private const val KEY_PRODUCT_WHATSNEW = "whatsNew" - private const val KEY_PRODUCT_PHONESCREENSHOTS = "phoneScreenshots" - private const val KEY_PRODUCT_SEVENINCHSCREENSHOTS = "sevenInchScreenshots" - private const val KEY_PRODUCT_TENINCHSCREENSHOTS = "tenInchScreenshots" + private const val KEY_PRODUCT_PHONE_SCREENSHOTS = "phoneScreenshots" + private const val KEY_PRODUCT_SEVEN_INCH_SCREENSHOTS = "sevenInchScreenshots" + private const val KEY_PRODUCT_TEN_INCH_SCREENSHOTS = "tenInchScreenshots" + private const val KEY_PRODUCT_WEAR_SCREENSHOTS = "wearScreenshots" + private const val KEY_PRODUCT_TV_SCREENSHOTS = "tvScreenshots" private fun JsonParser.parseProduct(repositoryId: Long): Product { var packageName = "" @@ -312,6 +320,8 @@ object IndexV1Parser { var phone = emptyList() var smallTablet = emptyList() var largeTablet = emptyList() + var wear = emptyList() + var tv = emptyList() forEachKey { when { it.string(KEY_PRODUCT_NAME) -> name = valueAsString @@ -319,29 +329,34 @@ object IndexV1Parser { it.string(KEY_PRODUCT_DESCRIPTION) -> description = valueAsString it.string(KEY_PRODUCT_WHATSNEW) -> whatsNew = valueAsString it.string(KEY_PRODUCT_ICON) -> metadataIcon = valueAsString - it.array(KEY_PRODUCT_PHONESCREENSHOTS) -> - phone = - collectDistinctNotEmptyStrings() + it.array(KEY_PRODUCT_PHONE_SCREENSHOTS) -> + phone = collectDistinctNotEmptyStrings() - it.array(KEY_PRODUCT_SEVENINCHSCREENSHOTS) -> - smallTablet = - collectDistinctNotEmptyStrings() + it.array(KEY_PRODUCT_SEVEN_INCH_SCREENSHOTS) -> + smallTablet = collectDistinctNotEmptyStrings() - it.array(KEY_PRODUCT_TENINCHSCREENSHOTS) -> - largeTablet = - collectDistinctNotEmptyStrings() + it.array(KEY_PRODUCT_TEN_INCH_SCREENSHOTS) -> + largeTablet = collectDistinctNotEmptyStrings() + + it.array(KEY_PRODUCT_WEAR_SCREENSHOTS) -> + wear = collectDistinctNotEmptyStrings() + + it.array(KEY_PRODUCT_TV_SCREENSHOTS) -> + tv = collectDistinctNotEmptyStrings() else -> skipChildren() } } val screenshots = - if (sequenceOf( + if (arrayOf( phone, smallTablet, - largeTablet + largeTablet, + wear, + tv, ).any { it.isNotEmpty() } ) { - Screenshots(phone, smallTablet, largeTablet) + Screenshots(phone, smallTablet, largeTablet, wear, tv) } else { null } @@ -374,28 +389,13 @@ object IndexV1Parser { } val screenshotPairs = localizedMap.find { key, localized -> localized.screenshots?.let { Pair(key, it) } } - val screenshots = screenshotPairs - ?.let { (key, screenshots) -> - screenshots.phone.asSequence() - .map { Product.Screenshot(key, Product.Screenshot.Type.PHONE, it) } + - screenshots.smallTablet.asSequence() - .map { - Product.Screenshot( - key, - Product.Screenshot.Type.SMALL_TABLET, - it - ) - } + - screenshots.largeTablet.asSequence() - .map { - Product.Screenshot( - key, - Product.Screenshot.Type.LARGE_TABLET, - it - ) - } - } - .orEmpty().toList() + val screenshots = screenshotPairs?.let { (key, screenshots) -> + screenshots.phone.map { Product.Screenshot(key, PHONE, it) } + + screenshots.smallTablet.map { Product.Screenshot(key, SMALL_TABLET, it) } + + screenshots.largeTablet.map { Product.Screenshot(key, LARGE_TABLET, it) } + + screenshots.wear.map { Product.Screenshot(key, WEAR, it) } + + screenshots.tv.map { Product.Screenshot(key, TV, it) } + }.orEmpty() return Product( repositoryId = repositoryId, packageName = packageName, diff --git a/app/src/main/kotlin/com/looker/droidify/model/Product.kt b/app/src/main/kotlin/com/looker/droidify/model/Product.kt index 0083808a..39ee4990 100644 --- a/app/src/main/kotlin/com/looker/droidify/model/Product.kt +++ b/app/src/main/kotlin/com/looker/droidify/model/Product.kt @@ -38,7 +38,9 @@ data class Product( enum class Type(val jsonName: String) { PHONE("phone"), SMALL_TABLET("smallTablet"), - LARGE_TABLET("largeTablet") + LARGE_TABLET("largeTablet"), + WEAR("wear"), + TV("tv") } val identifier: String @@ -52,6 +54,8 @@ data class Product( Type.PHONE -> "phoneScreenshots" Type.SMALL_TABLET -> "sevenInchScreenshots" Type.LARGE_TABLET -> "tenInchScreenshots" + Type.WEAR -> "wearScreenshots" + Type.TV -> "tvScreenshots" } return "${repository.address}/$packageName/$locale/$phoneType/$path" } From 3b4dc2ef65f284cb948b19844b1f1e879937a04c Mon Sep 17 00:00:00 2001 From: LooKeR Date: Sat, 15 Feb 2025 23:58:32 +0530 Subject: [PATCH 011/124] fix: Parsing of Liberapay and Litecoin Liberapay Id and Litecoin id are now properly parsed Closes #915 Signed-off-by: LooKeR --- .../looker/droidify/index/IndexV1Parser.kt | 32 ++++++++++--------- .../droidify/ui/appDetail/AppDetailAdapter.kt | 2 +- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt b/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt index 0e854b79..548c24b0 100644 --- a/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt +++ b/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt @@ -12,6 +12,11 @@ import com.looker.core.common.extension.forEach import com.looker.core.common.extension.forEachKey import com.looker.core.common.extension.illegal import com.looker.droidify.model.Product +import com.looker.droidify.model.Product.Donate.Bitcoin +import com.looker.droidify.model.Product.Donate.Liberapay +import com.looker.droidify.model.Product.Donate.Litecoin +import com.looker.droidify.model.Product.Donate.OpenCollective +import com.looker.droidify.model.Product.Donate.Regular import com.looker.droidify.model.Product.Screenshot.Type.LARGE_TABLET import com.looker.droidify.model.Product.Screenshot.Type.PHONE import com.looker.droidify.model.Product.Screenshot.Type.SMALL_TABLET @@ -128,11 +133,11 @@ object IndexV1Parser { internal object DonateComparator : Comparator { private val classes = listOf( - Product.Donate.Regular::class, - Product.Donate.Bitcoin::class, - Product.Donate.Litecoin::class, - Product.Donate.Liberapay::class, - Product.Donate.OpenCollective::class + Regular::class, + Bitcoin::class, + Litecoin::class, + Liberapay::class, + OpenCollective::class ) override fun compare(donate1: Product.Donate, donate2: Product.Donate): Int { @@ -241,7 +246,8 @@ object IndexV1Parser { private const val KEY_PRODUCT_LICENSE = "license" private const val KEY_PRODUCT_DONATE = "donate" private const val KEY_PRODUCT_BITCOIN = "bitcoin" - private const val KEY_PRODUCT_LIBERAPAYID = "liberapayID" + private const val KEY_PRODUCT_LIBERAPAYID = "liberapay" + private const val KEY_PRODUCT_LITECOIN = "litecoin" private const val KEY_PRODUCT_OPENCOLLECTIVE = "openCollective" private const val KEY_PRODUCT_LOCALIZED = "localized" private const val KEY_PRODUCT_WHATSNEW = "whatsNew" @@ -299,15 +305,11 @@ object IndexV1Parser { key.string(KEY_PRODUCT_LICENSE) -> licenses += valueAsString.split(',') .filter { it.isNotEmpty() } - key.string(KEY_PRODUCT_DONATE) -> donates += Product.Donate.Regular(valueAsString) - key.string(KEY_PRODUCT_BITCOIN) -> donates += Product.Donate.Bitcoin(valueAsString) - key.string(KEY_PRODUCT_LIBERAPAYID) -> donates += Product.Donate.Liberapay( - valueAsString - ) - - key.string(KEY_PRODUCT_OPENCOLLECTIVE) -> donates += Product.Donate.OpenCollective( - valueAsString - ) + key.string(KEY_PRODUCT_DONATE) -> donates += Regular(valueAsString) + key.string(KEY_PRODUCT_BITCOIN) -> donates += Bitcoin(valueAsString) + key.string(KEY_PRODUCT_LIBERAPAYID) -> donates += Liberapay(valueAsString) + key.string(KEY_PRODUCT_LITECOIN) -> donates += Litecoin(valueAsString) + key.string(KEY_PRODUCT_OPENCOLLECTIVE) -> donates += OpenCollective(valueAsString) key.dictionary(KEY_PRODUCT_LOCALIZED) -> forEachKey { localizedKey -> if (localizedKey.token == JsonToken.START_OBJECT) { diff --git a/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailAdapter.kt b/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailAdapter.kt index 30cf106b..4c5483ba 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailAdapter.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailAdapter.kt @@ -331,7 +331,7 @@ class AppDetailAdapter(private val callbacks: Callbacks) : is Product.Donate.Bitcoin -> Uri.parse("bitcoin:${donate.address}") is Product.Donate.Litecoin -> Uri.parse("litecoin:${donate.address}") is Product.Donate.Liberapay -> Uri.parse( - "https://liberapay.com/~${donate.id}" + "https://liberapay.com/${donate.id}" ) is Product.Donate.OpenCollective -> Uri.parse( From 6639ae4ce0fc672e0f5d85a2112b3027308612b0 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Sat, 15 Feb 2025 23:58:55 +0530 Subject: [PATCH 012/124] rem: Unused icons Signed-off-by: LooKeR --- app/src/main/res/drawable/ic_donate_flattr.xml | 17 ----------------- app/src/main/res/drawable/ic_gitlab.xml | 9 --------- app/src/main/res/drawable/ic_kde.xml | 9 --------- 3 files changed, 35 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_donate_flattr.xml delete mode 100644 app/src/main/res/drawable/ic_gitlab.xml delete mode 100644 app/src/main/res/drawable/ic_kde.xml diff --git a/app/src/main/res/drawable/ic_donate_flattr.xml b/app/src/main/res/drawable/ic_donate_flattr.xml deleted file mode 100644 index 62de4a6a..00000000 --- a/app/src/main/res/drawable/ic_donate_flattr.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ic_gitlab.xml b/app/src/main/res/drawable/ic_gitlab.xml deleted file mode 100644 index a4089ea0..00000000 --- a/app/src/main/res/drawable/ic_gitlab.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_kde.xml b/app/src/main/res/drawable/ic_kde.xml deleted file mode 100644 index ffd419f3..00000000 --- a/app/src/main/res/drawable/ic_kde.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - From ae9a248460f9aa5acb89b80facda812563602739 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Sun, 16 Feb 2025 00:31:31 +0530 Subject: [PATCH 013/124] add: Video Parsing Shows video in screenshots, UI is not perfect but works for now Closes #921 Signed-off-by: LooKeR --- .../looker/droidify/index/IndexV1Parser.kt | 35 +++--- .../com/looker/droidify/model/Product.kt | 14 ++- .../ui/appDetail/AppDetailFragment.kt | 2 +- .../ui/appDetail/ScreenshotsAdapter.kt | 113 +++++++++++++----- .../utility/common/extension/Context.kt | 27 ++++- .../droidify/utility/common/extension/View.kt | 8 +- app/src/main/res/drawable/ic_video.xml | 10 ++ app/src/main/res/layout/video_button.xml | 14 +++ app/src/main/res/values/strings.xml | 1 + .../droidify/network/KtorDownloaderTest.kt | 2 +- 10 files changed, 169 insertions(+), 57 deletions(-) create mode 100644 app/src/main/res/drawable/ic_video.xml create mode 100644 app/src/main/res/layout/video_button.xml diff --git a/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt b/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt index 548c24b0..88da124d 100644 --- a/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt +++ b/app/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt @@ -21,6 +21,7 @@ import com.looker.droidify.model.Product.Screenshot.Type.LARGE_TABLET import com.looker.droidify.model.Product.Screenshot.Type.PHONE import com.looker.droidify.model.Product.Screenshot.Type.SMALL_TABLET import com.looker.droidify.model.Product.Screenshot.Type.TV +import com.looker.droidify.model.Product.Screenshot.Type.VIDEO import com.looker.droidify.model.Product.Screenshot.Type.WEAR import com.looker.droidify.model.Release import com.looker.droidify.utility.common.SdkCheck @@ -42,6 +43,7 @@ object IndexV1Parser { } private class Screenshots( + val video: List, val phone: List, val smallTablet: List, val largeTablet: List, @@ -256,6 +258,7 @@ object IndexV1Parser { private const val KEY_PRODUCT_TEN_INCH_SCREENSHOTS = "tenInchScreenshots" private const val KEY_PRODUCT_WEAR_SCREENSHOTS = "wearScreenshots" private const val KEY_PRODUCT_TV_SCREENSHOTS = "tvScreenshots" + private const val KEY_PRODUCT_VIDEO = "video" private fun JsonParser.parseProduct(repositoryId: Long): Product { var packageName = "" @@ -324,6 +327,7 @@ object IndexV1Parser { var largeTablet = emptyList() var wear = emptyList() var tv = emptyList() + var video = emptyList() forEachKey { when { it.string(KEY_PRODUCT_NAME) -> name = valueAsString @@ -331,6 +335,7 @@ object IndexV1Parser { it.string(KEY_PRODUCT_DESCRIPTION) -> description = valueAsString it.string(KEY_PRODUCT_WHATSNEW) -> whatsNew = valueAsString it.string(KEY_PRODUCT_ICON) -> metadataIcon = valueAsString + it.string(KEY_PRODUCT_VIDEO) -> video = listOf(valueAsString) it.array(KEY_PRODUCT_PHONE_SCREENSHOTS) -> phone = collectDistinctNotEmptyStrings() @@ -349,26 +354,23 @@ object IndexV1Parser { else -> skipChildren() } } + val isScreenshotEmpty = + arrayOf(video, phone, smallTablet, largeTablet, wear, tv) + .any { it.isNotEmpty() } val screenshots = - if (arrayOf( - phone, - smallTablet, - largeTablet, - wear, - tv, - ).any { it.isNotEmpty() } - ) { - Screenshots(phone, smallTablet, largeTablet, wear, tv) + if (isScreenshotEmpty) { + Screenshots(video, phone, smallTablet, largeTablet, wear, tv) } else { null } localizedMap[locale] = Localized( - name, - summary, - description, - whatsNew, - metadataIcon.nullIfEmpty()?.let { "$locale/$it" }.orEmpty(), - screenshots + name = name, + summary = summary, + description = description, + whatsNew = whatsNew, + metadataIcon = metadataIcon.nullIfEmpty()?.let { "$locale/$it" } + .orEmpty(), + screenshots = screenshots, ) } else { skipChildren() @@ -392,7 +394,8 @@ object IndexV1Parser { val screenshotPairs = localizedMap.find { key, localized -> localized.screenshots?.let { Pair(key, it) } } val screenshots = screenshotPairs?.let { (key, screenshots) -> - screenshots.phone.map { Product.Screenshot(key, PHONE, it) } + + screenshots.video.map { Product.Screenshot(key, VIDEO, it) } + + screenshots.phone.map { Product.Screenshot(key, PHONE, it) } + screenshots.smallTablet.map { Product.Screenshot(key, SMALL_TABLET, it) } + screenshots.largeTablet.map { Product.Screenshot(key, LARGE_TABLET, it) } + screenshots.wear.map { Product.Screenshot(key, WEAR, it) } + diff --git a/app/src/main/kotlin/com/looker/droidify/model/Product.kt b/app/src/main/kotlin/com/looker/droidify/model/Product.kt index 39ee4990..8271eee6 100644 --- a/app/src/main/kotlin/com/looker/droidify/model/Product.kt +++ b/app/src/main/kotlin/com/looker/droidify/model/Product.kt @@ -1,5 +1,11 @@ package com.looker.droidify.model +import android.content.Context +import com.looker.droidify.utility.common.extension.camera +import com.looker.droidify.utility.common.extension.getColorFromAttr +import com.looker.droidify.utility.common.extension.videoPlaceHolder +import com.google.android.material.R as MaterialR + data class Product( var repositoryId: Long, val packageName: String, @@ -36,6 +42,7 @@ data class Product( class Screenshot(val locale: String, val type: Type, val path: String) { enum class Type(val jsonName: String) { + VIDEO("video"), PHONE("phone"), SMALL_TABLET("smallTablet"), LARGE_TABLET("largeTablet"), @@ -47,15 +54,20 @@ data class Product( get() = "$locale.${type.name}.$path" fun url( + context: Context, repository: Repository, packageName: String - ): String { + ): Any { + if (type == Type.VIDEO) return context.videoPlaceHolder.apply { + setTintList(context.getColorFromAttr(MaterialR.attr.colorOnSurfaceInverse)) + } val phoneType = when (type) { Type.PHONE -> "phoneScreenshots" Type.SMALL_TABLET -> "sevenInchScreenshots" Type.LARGE_TABLET -> "tenInchScreenshots" Type.WEAR -> "wearScreenshots" Type.TV -> "tvScreenshots" + Type.VIDEO -> error("Should not be here, video url already returned") } return "${repository.address}/$packageName/$locale/$phoneType/$path" } diff --git a/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailFragment.kt b/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailFragment.kt index ab17d868..90673feb 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailFragment.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/appDetail/AppDetailFragment.kt @@ -456,7 +456,7 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks { val position = screenshots.indexOfFirst { screenshot.identifier == it.identifier } StfalconImageViewer .Builder(context, screenshots) { view, current -> - view.load(current.url(product.second, viewModel.packageName)) + view.load(current.url(requireContext(), product.second, viewModel.packageName)) } .withStartPosition(position) .show() diff --git a/app/src/main/kotlin/com/looker/droidify/ui/appDetail/ScreenshotsAdapter.kt b/app/src/main/kotlin/com/looker/droidify/ui/appDetail/ScreenshotsAdapter.kt index a44e590a..d8de146e 100644 --- a/app/src/main/kotlin/com/looker/droidify/ui/appDetail/ScreenshotsAdapter.kt +++ b/app/src/main/kotlin/com/looker/droidify/ui/appDetail/ScreenshotsAdapter.kt @@ -12,28 +12,48 @@ import coil.load import coil.size.Dimension import coil.size.Scale import com.google.android.material.imageview.ShapeableImageView +import com.looker.droidify.databinding.VideoButtonBinding +import com.looker.droidify.graphics.PaddingDrawable +import com.looker.droidify.model.Product +import com.looker.droidify.model.Repository import com.looker.droidify.utility.common.extension.aspectRatio import com.looker.droidify.utility.common.extension.authentication import com.looker.droidify.utility.common.extension.camera import com.looker.droidify.utility.common.extension.dp import com.looker.droidify.utility.common.extension.dpToPx import com.looker.droidify.utility.common.extension.getColorFromAttr +import com.looker.droidify.utility.common.extension.layoutInflater +import com.looker.droidify.utility.common.extension.openLink import com.looker.droidify.utility.common.extension.selectableBackground -import com.looker.droidify.graphics.PaddingDrawable -import com.looker.droidify.model.Product -import com.looker.droidify.model.Repository import com.looker.droidify.widget.StableRecyclerAdapter import com.google.android.material.R as MaterialR import com.looker.droidify.R.dimen as dimenRes class ScreenshotsAdapter(private val onClick: (Product.Screenshot, ImageView) -> Unit) : StableRecyclerAdapter() { - enum class ViewType { SCREENSHOT } + enum class ViewType { SCREENSHOT, VIDEO } - private val items = mutableListOf() + private val items = mutableListOf() - private class ViewHolder(context: Context) : - RecyclerView.ViewHolder(FrameLayout(context)) { + class VideoViewHolder( + binding: VideoButtonBinding, + ) : RecyclerView.ViewHolder(binding.root) { + val button = binding.videoButton + + init { + with(button) { + layoutParams = RecyclerView.LayoutParams( + RecyclerView.LayoutParams.WRAP_CONTENT, + 150.dp, + ) + } + } + } + + + private class ScreenshotViewHolder( + context: Context + ) : RecyclerView.ViewHolder(FrameLayout(context)) { val image: ShapeableImageView = object : ShapeableImageView(context) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) @@ -78,64 +98,91 @@ class ScreenshotsAdapter(private val onClick: (Product.Screenshot, ImageView) -> screenshots: List ) { items.clear() - items += screenshots.map { Item.ScreenshotItem(repository, packageName, it) } + items += screenshots.map { + when (it.type) { + Product.Screenshot.Type.VIDEO -> Item.VideoItem(it.path) + else -> Item.ScreenshotItem(repository, packageName, it) + } + } notifyItemRangeInserted(0, screenshots.size) } - override val viewTypeClass: Class - get() = ViewType::class.java - - override fun getItemEnumViewType(position: Int): ViewType { - return ViewType.SCREENSHOT - } + override val viewTypeClass: Class get() = ViewType::class.java + override fun getItemCount(): Int = items.size + override fun getItemEnumViewType(position: Int) = items[position].viewType + override fun getItemDescriptor(position: Int): String = items[position].descriptor override fun onCreateViewHolder( parent: ViewGroup, viewType: ViewType ): RecyclerView.ViewHolder { - return ViewHolder(parent.context).apply { - image.setOnClickListener { - onClick( - items[absoluteAdapterPosition].screenshot, - it as ImageView - ) + return when (viewType) { + ViewType.VIDEO -> VideoViewHolder(VideoButtonBinding.inflate(parent.context.layoutInflater)) + ViewType.SCREENSHOT -> ScreenshotViewHolder(parent.context).apply { + image.setOnClickListener { + val item = items[absoluteAdapterPosition] as Item.ScreenshotItem + onClick(item.screenshot, it as ImageView) + } } } } - override fun getItemDescriptor(position: Int): String = items[position].descriptor - override fun getItemCount(): Int = items.size - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - holder as ViewHolder val item = items[position] - with(holder.image) { - load(item.screenshot.url(item.repository, item.packageName)) { - size(Dimension.Undefined, Dimension(150.dp.dpToPx.toInt())) - scale(Scale.FIT) - placeholder(holder.placeholder) - error(holder.placeholder) - authentication(item.repository.authentication) + when (getItemEnumViewType(position)) { + ViewType.SCREENSHOT -> { + holder as ScreenshotViewHolder + item as Item.ScreenshotItem + with(holder.image) { + load(item.screenshot.url(context, item.repository, item.packageName)) { + size(Dimension.Undefined, Dimension(150.dp.dpToPx.toInt())) + scale(Scale.FIT) + placeholder(holder.placeholder) + error(holder.placeholder) + authentication(item.repository.authentication) + } + } + } + + ViewType.VIDEO -> { + holder as VideoViewHolder + item as Item.VideoItem + holder.button.setOnClickListener { + it.context?.openLink(item.videoUrl) + } } } } override fun onViewRecycled(holder: RecyclerView.ViewHolder) { super.onViewRecycled(holder) - holder as ViewHolder - holder.image.dispose() + if (holder is ScreenshotViewHolder) holder.image.dispose() } private sealed class Item { abstract val descriptor: String + abstract val viewType: ViewType class ScreenshotItem( val repository: Repository, val packageName: String, val screenshot: Product.Screenshot ) : Item() { + override val viewType: ViewType + get() = ViewType.SCREENSHOT + override val descriptor: String get() = "screenshot.${repository.id}.${screenshot.identifier}" } + + class VideoItem( + val videoUrl: String, + ) : Item() { + override val viewType: ViewType + get() = ViewType.VIDEO + + override val descriptor: String + get() = "video" + } } } diff --git a/app/src/main/kotlin/com/looker/droidify/utility/common/extension/Context.kt b/app/src/main/kotlin/com/looker/droidify/utility/common/extension/Context.kt index 1bd318e0..f4ea0bb0 100644 --- a/app/src/main/kotlin/com/looker/droidify/utility/common/extension/Context.kt +++ b/app/src/main/kotlin/com/looker/droidify/utility/common/extension/Context.kt @@ -5,6 +5,7 @@ import android.app.job.JobScheduler import android.content.ClipData import android.content.ClipboardManager import android.content.Context +import android.content.Intent import android.content.res.ColorStateList import android.graphics.drawable.Drawable import android.net.ConnectivityManager @@ -15,6 +16,7 @@ import androidx.annotation.DrawableRes import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.core.content.getSystemService +import androidx.core.net.toUri import com.looker.droidify.R inline val Context.clipboardManager: ClipboardManager? @@ -39,6 +41,13 @@ fun Context.copyToClipboard(clip: String) { clipboardManager?.setPrimaryClip(ClipData.newPlainText(null, clip)) } +fun Context.openLink(url: String) { + val intent = intent(Intent.ACTION_VIEW) { + setData(url.toUri()) + } + startActivity(intent) +} + val Context.corneredBackground: Drawable get() = getDrawableCompat(R.drawable.background_border) @@ -57,6 +66,9 @@ val Context.selectableBackground: Drawable val Context.camera: Drawable get() = getDrawableCompat(R.drawable.ic_image) +val Context.videoPlaceHolder: Drawable + get() = getDrawableCompat(R.drawable.ic_video) + val Context.aspectRatio: Float get() = with(resources.displayMetrics) { (heightPixels / widthPixels).toFloat() @@ -75,14 +87,21 @@ private fun Context.getDrawableFromAttr(attrResId: Int): Drawable { } fun Context.getDrawableCompat(@DrawableRes resId: Int = R.drawable.background_border): Drawable = - requireNotNull(AppCompatResources.getDrawable(this, resId)) { "Cannot find drawable, ID: $resId" } + requireNotNull( + AppCompatResources.getDrawable( + this, + resId + ) + ) { "Cannot find drawable, ID: $resId" } fun Context.getColorFromAttr(@AttrRes attrResId: Int): ColorStateList { val typedArray = obtainStyledAttributes(intArrayOf(attrResId)) - val (colorStateList, resId) = try { - Pair(typedArray.getColorStateList(0), typedArray.getResourceId(0, 0)) + return try { + typedArray.getColorStateList(0) ?: run { + val resourceId = typedArray.getResourceId(0, 0) + ContextCompat.getColorStateList(this, resourceId)!! + } } finally { typedArray.recycle() } - return colorStateList ?: ContextCompat.getColorStateList(this, resId)!! } diff --git a/app/src/main/kotlin/com/looker/droidify/utility/common/extension/View.kt b/app/src/main/kotlin/com/looker/droidify/utility/common/extension/View.kt index 2935c1f4..305168b1 100644 --- a/app/src/main/kotlin/com/looker/droidify/utility/common/extension/View.kt +++ b/app/src/main/kotlin/com/looker/droidify/utility/common/extension/View.kt @@ -1,5 +1,6 @@ package com.looker.droidify.utility.common.extension +import android.content.Context import android.util.TypedValue import android.view.LayoutInflater import android.view.View @@ -18,7 +19,9 @@ import kotlin.math.min import kotlin.math.roundToInt fun ImageRequest.Builder.authentication(base64: String) { - addHeader("Authorization", base64) + if (base64.isNotEmpty()) { + addHeader("Authorization", base64) + } } fun TextView.setTextSizeScaled(size: Int) { @@ -26,6 +29,9 @@ fun TextView.setTextSizeScaled(size: Int) { setTextSize(TypedValue.COMPLEX_UNIT_PX, realSize.toFloat()) } +val Context.layoutInflater: LayoutInflater + get() = LayoutInflater.from(this) + fun ViewGroup.inflate(layoutResId: Int): View { return LayoutInflater.from(context).inflate(layoutResId, this, false) } diff --git a/app/src/main/res/drawable/ic_video.xml b/app/src/main/res/drawable/ic_video.xml new file mode 100644 index 00000000..9959ba37 --- /dev/null +++ b/app/src/main/res/drawable/ic_video.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/video_button.xml b/app/src/main/res/layout/video_button.xml new file mode 100644 index 00000000..190da49c --- /dev/null +++ b/app/src/main/res/layout/video_button.xml @@ -0,0 +1,14 @@ + +