diff --git a/src/main/kotlin/com/looker/droidify/MainApplication.kt b/src/main/kotlin/com/looker/droidify/MainApplication.kt index 330044ea..0979e717 100644 --- a/src/main/kotlin/com/looker/droidify/MainApplication.kt +++ b/src/main/kotlin/com/looker/droidify/MainApplication.kt @@ -15,7 +15,6 @@ import coil.ImageLoader import coil.ImageLoaderFactory import com.looker.droidify.content.Cache import com.looker.droidify.content.Preferences -import com.looker.droidify.content.ProductPreferences import com.looker.droidify.database.DatabaseX import com.looker.droidify.index.RepositoryUpdater import com.looker.droidify.network.CoilDownloader @@ -45,7 +44,6 @@ class MainApplication : Application(), ImageLoaderFactory { db = DatabaseX.getInstance(applicationContext) Preferences.init(this) - ProductPreferences.init(this) RepositoryUpdater.init(this) listenApplications() listenPreferences() diff --git a/src/main/kotlin/com/looker/droidify/database/DAOs.kt b/src/main/kotlin/com/looker/droidify/database/DAOs.kt index 4f928a3d..db3d2d01 100644 --- a/src/main/kotlin/com/looker/droidify/database/DAOs.kt +++ b/src/main/kotlin/com/looker/droidify/database/DAOs.kt @@ -38,8 +38,6 @@ import com.looker.droidify.ROW_UPDATED import com.looker.droidify.ROW_VERSION_CODE import com.looker.droidify.TABLE_CATEGORY import com.looker.droidify.TABLE_CATEGORY_NAME -import com.looker.droidify.TABLE_IGNORED -import com.looker.droidify.TABLE_IGNORED_NAME import com.looker.droidify.TABLE_EXTRAS import com.looker.droidify.TABLE_EXTRAS_NAME import com.looker.droidify.TABLE_INSTALLED @@ -198,9 +196,9 @@ interface ProductDao : BaseDao { // Select the return fields builder += """SELECT $TABLE_PRODUCT.rowid AS $ROW_ID, $TABLE_PRODUCT.$ROW_REPOSITORY_ID, $TABLE_PRODUCT.$ROW_PACKAGE_NAME, $TABLE_PRODUCT.$ROW_LABEL, - $TABLE_PRODUCT.$ROW_SUMMARY, $TABLE_PRODUCT.$ROW_DESCRIPTION, - (COALESCE($TABLE_IGNORED.$ROW_VERSION_CODE, -1) NOT IN (0, $TABLE_PRODUCT.$ROW_VERSION_CODE) AND - $TABLE_PRODUCT.$ROW_COMPATIBLE != 0 AND + $TABLE_PRODUCT.$ROW_SUMMARY, $TABLE_PRODUCT.$ROW_DESCRIPTION, + (COALESCE($TABLE_EXTRAS.$ROW_IGNORED_VERSION, -1) != $TABLE_PRODUCT.$ROW_VERSION_CODE AND + COALESCE($TABLE_EXTRAS.$ROW_IGNORE_UPDATES, 0) = 0 AND $TABLE_PRODUCT.$ROW_COMPATIBLE != 0 AND $TABLE_PRODUCT.$ROW_VERSION_CODE > COALESCE($TABLE_INSTALLED.$ROW_VERSION_CODE, 0xffffffff) AND $signatureMatches) AS $ROW_CAN_UPDATE, $TABLE_PRODUCT.$ROW_ICON, $TABLE_PRODUCT.$ROW_METADATA_ICON, $TABLE_PRODUCT.$ROW_RELEASES, $TABLE_PRODUCT.$ROW_CATEGORIES, @@ -227,9 +225,9 @@ interface ProductDao : BaseDao { builder += """JOIN $TABLE_REPOSITORY_NAME AS $TABLE_REPOSITORY ON $TABLE_PRODUCT.$ROW_REPOSITORY_ID = $TABLE_REPOSITORY.$ROW_ID""" - // Merge the matching locks - builder += """LEFT JOIN $TABLE_IGNORED_NAME AS $TABLE_IGNORED - ON $TABLE_PRODUCT.$ROW_PACKAGE_NAME = $TABLE_IGNORED.$ROW_PACKAGE_NAME""" + // Merge the matching extras + builder += """LEFT JOIN $TABLE_EXTRAS_NAME AS $TABLE_EXTRAS + ON $TABLE_PRODUCT.$ROW_PACKAGE_NAME = $TABLE_EXTRAS.$ROW_PACKAGE_NAME""" // Merge the matching installed if (!installed && !updates) builder += "LEFT" diff --git a/src/main/kotlin/com/looker/droidify/database/DatabaseX.kt b/src/main/kotlin/com/looker/droidify/database/DatabaseX.kt index 0a2100bd..9126181b 100644 --- a/src/main/kotlin/com/looker/droidify/database/DatabaseX.kt +++ b/src/main/kotlin/com/looker/droidify/database/DatabaseX.kt @@ -7,7 +7,7 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverters import com.looker.droidify.database.entity.Category import com.looker.droidify.database.entity.CategoryTemp -import com.looker.droidify.database.entity.Ignored +import com.looker.droidify.database.entity.Extras import com.looker.droidify.database.entity.Installed import com.looker.droidify.database.entity.Product import com.looker.droidify.database.entity.ProductTemp @@ -27,8 +27,8 @@ import kotlinx.coroutines.launch Category::class, CategoryTemp::class, Installed::class, - Ignored::class - ], version = 7 + Extras::class + ], version = 8 ) @TypeConverters(Converters::class) abstract class DatabaseX : RoomDatabase() { @@ -39,7 +39,7 @@ abstract class DatabaseX : RoomDatabase() { abstract val categoryDao: CategoryDao abstract val categoryTempDao: CategoryTempDao abstract val installedDao: InstalledDao - abstract val lockDao: LockDao + abstract val extrasDao: ExtrasDao companion object { @Volatile diff --git a/src/main/kotlin/com/looker/droidify/ui/compose/utils/Callbacks.kt b/src/main/kotlin/com/looker/droidify/ui/compose/utils/Callbacks.kt index 4ddf9f30..ac8bedee 100644 --- a/src/main/kotlin/com/looker/droidify/ui/compose/utils/Callbacks.kt +++ b/src/main/kotlin/com/looker/droidify/ui/compose/utils/Callbacks.kt @@ -3,12 +3,10 @@ package com.looker.droidify.ui.compose.utils import android.net.Uri import com.looker.droidify.database.entity.Release import com.looker.droidify.entity.ActionState -import com.looker.droidify.entity.ProductPreference import com.looker.droidify.entity.Screenshot interface Callbacks { fun onActionClick(action: ActionState?) - fun onPreferenceChanged(preference: ProductPreference) fun onPermissionsClick(group: String?, permissions: List) fun onScreenshotClick(screenshot: Screenshot) fun onReleaseClick(release: Release) diff --git a/src/main/kotlin/com/looker/droidify/ui/fragments/AppSheetX.kt b/src/main/kotlin/com/looker/droidify/ui/fragments/AppSheetX.kt index 0839e91a..ad0317dd 100644 --- a/src/main/kotlin/com/looker/droidify/ui/fragments/AppSheetX.kt +++ b/src/main/kotlin/com/looker/droidify/ui/fragments/AppSheetX.kt @@ -41,13 +41,12 @@ import com.looker.droidify.RELEASE_STATE_INSTALLED import com.looker.droidify.RELEASE_STATE_NONE import com.looker.droidify.RELEASE_STATE_SUGGESTED import com.looker.droidify.content.Preferences -import com.looker.droidify.content.ProductPreferences +import com.looker.droidify.database.entity.Extras import com.looker.droidify.database.entity.Release import com.looker.droidify.entity.ActionState import com.looker.droidify.entity.AntiFeature import com.looker.droidify.entity.DonateType import com.looker.droidify.entity.DownloadState -import com.looker.droidify.entity.ProductPreference import com.looker.droidify.entity.Screenshot import com.looker.droidify.installer.AppInstaller import com.looker.droidify.network.CoilDownloader @@ -144,9 +143,7 @@ class AppSheetX() : FullscreenBottomSheetDialogFragment(), Callbacks { override fun setupLayout() { viewModel._productRepos.observe(this) { - lifecycleScope.launch { - updateButtons() - } + viewModel.updateActions() } } @@ -159,86 +156,6 @@ class AppSheetX() : FullscreenBottomSheetDialogFragment(), Callbacks { downloadConnection.unbind(requireContext()) } - private suspend fun updateButtons() { - updateButtons(ProductPreferences[packageName]) - } - - // TODO rename to updateActions - private suspend fun updateButtons(preference: ProductPreference) = - withContext(Dispatchers.Default) { - val installed = viewModel.installedItem.value - val productRepos = viewModel.productRepos - val product = findSuggestedProduct(productRepos, installed) { it.first }?.first - val compatible = product != null && product.selectedReleases.firstOrNull() - .let { it != null && it.incompatibilities.isEmpty() } - val canInstall = product != null && installed == null && compatible - val canUpdate = - product != null && compatible && product.canUpdate(installed) && - !preference.shouldIgnoreUpdate(product.versionCode) - val canUninstall = product != null && installed != null && !installed.isSystem - val canLaunch = - product != null && installed != null && installed.launcherActivities.isNotEmpty() - val canShare = product != null && productRepos[0].second.name == "F-Droid" - - val actions = mutableSetOf() - launch { - if (canInstall) { - actions += ActionState.Install - } - } - launch { - if (canUpdate) { - actions += ActionState.Update - } - } - launch { - if (canLaunch) { - actions += ActionState.Launch - } - } - launch { - if (installed != null) { - actions += ActionState.Details - } - } - launch { - if (canUninstall) { - actions += ActionState.Uninstall - } - } - launch { - if (canShare) { - actions += ActionState.Share - } - } - // TODO prioritize actions set and choose the first for main others for extra actions - val primaryAction = when { - canUpdate -> ActionState.Update - canLaunch -> ActionState.Launch - canInstall -> ActionState.Install - canShare -> ActionState.Share - else -> null - } - val secondaryAction = when { - primaryAction != ActionState.Share && canShare -> ActionState.Share - primaryAction != ActionState.Launch && canLaunch -> ActionState.Launch - installed != null && canUninstall -> ActionState.Uninstall - else -> null - } - - withContext(Dispatchers.Main) { - viewModel.actions.value = actions - - if (viewModel.downloadState.value != null && viewModel.mainAction.value?.textId != viewModel.downloadState.value?.textId) - viewModel.downloadState.value?.let { - viewModel.mainAction.value = ActionState.Cancel(it.textId) - } - else if (viewModel.downloadState.value == null) // && viewModel.mainAction.value != primaryAction) - viewModel.mainAction.value = primaryAction - viewModel.secondaryAction.value = secondaryAction - } - } - private suspend fun updateDownloadState(downloadState: DownloadService.State?) { val state = when (downloadState) { is DownloadService.State.Pending -> DownloadState.Pending @@ -250,8 +167,8 @@ class AppSheetX() : FullscreenBottomSheetDialogFragment(), Callbacks { else -> null } viewModel.downloadState.value = state - updateButtons() if (downloadState is DownloadService.State.Success && isResumed && !rootInstallerEnabled) { + viewModel.updateActions() withContext(Dispatchers.Default) { AppInstaller.getInstance(context)?.defaultInstaller?.install(downloadState.release.cacheFileName) } @@ -325,10 +242,6 @@ class AppSheetX() : FullscreenBottomSheetDialogFragment(), Callbacks { startActivity(Intent.createChooser(shareIntent, "Where to Send?")) } - override fun onPreferenceChanged(preference: ProductPreference) { - lifecycleScope.launch { updateButtons(preference) } - } - override fun onPermissionsClick(group: String?, permissions: List) { MessageDialog(MessageDialog.Message.Permissions(group, permissions)).show( childFragmentManager @@ -422,6 +335,7 @@ class AppSheetX() : FullscreenBottomSheetDialogFragment(), Callbacks { val mainAction by viewModel.mainAction.observeAsState(if (installed == null) ActionState.Install else ActionState.Launch) val actions by viewModel.actions.observeAsState() // TODO add rest actions to UI val secondaryAction by viewModel.secondaryAction.observeAsState() + val extras by viewModel.extras.observeAsState(Extras(packageName)) val productRepos = products?.mapNotNull { product -> repos?.firstOrNull { it.id == product.repositoryId } ?.let { Pair(product, it) } @@ -515,27 +429,23 @@ class AppSheetX() : FullscreenBottomSheetDialogFragment(), Callbacks { item { AnimatedVisibility(visible = product.canUpdate(installed)) { SwitchPreference(text = stringResource(id = R.string.ignore_this_update), - initSelected = ProductPreferences[product.packageName].ignoreVersionCode == product.versionCode, + initSelected = extras?.ignoredVersion == product.versionCode, onCheckedChanged = { - ProductPreferences[product.packageName].let { - it.copy( - ignoreVersionCode = - if (it.ignoreVersionCode == product.versionCode) 0 else product.versionCode - ) - } + viewModel.setIgnoredVersion( + product.packageName, + if (it) product.versionCode else 0 + ) + viewModel.updateActions() }) } } item { AnimatedVisibility(visible = installed != null) { SwitchPreference(text = stringResource(id = R.string.ignore_all_updates), - initSelected = ProductPreferences[product.packageName].ignoreVersionCode == product.versionCode, + initSelected = extras?.ignoreUpdates == true, onCheckedChanged = { - ProductPreferences[product.packageName].let { - it.copy( - ignoreUpdates = !it.ignoreUpdates - ) - } + viewModel.setIgnoreUpdates(product.packageName, it) + viewModel.updateActions() }) } } diff --git a/src/main/kotlin/com/looker/droidify/ui/viewmodels/AppViewModelX.kt b/src/main/kotlin/com/looker/droidify/ui/viewmodels/AppViewModelX.kt index 20849b7c..02e6c741 100644 --- a/src/main/kotlin/com/looker/droidify/ui/viewmodels/AppViewModelX.kt +++ b/src/main/kotlin/com/looker/droidify/ui/viewmodels/AppViewModelX.kt @@ -4,12 +4,18 @@ import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope import com.looker.droidify.database.DatabaseX +import com.looker.droidify.database.entity.Extras import com.looker.droidify.database.entity.Installed import com.looker.droidify.database.entity.Product import com.looker.droidify.database.entity.Repository import com.looker.droidify.entity.ActionState import com.looker.droidify.entity.DownloadState +import com.looker.droidify.utility.findSuggestedProduct +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class AppViewModelX(val db: DatabaseX, val packageName: String) : ViewModel() { @@ -26,11 +32,111 @@ class AppViewModelX(val db: DatabaseX, val packageName: String) : ViewModel() { val mainAction = MutableLiveData() val actions = MutableLiveData>() val secondaryAction = MutableLiveData() + val extras = MediatorLiveData() init { products.addSource(db.productDao.getLive(packageName)) { products.setValue(it.filterNotNull()) } repositories.addSource(db.repositoryDao.allLive, repositories::setValue) installedItem.addSource(db.installedDao.getLive(packageName), installedItem::setValue) + extras.addSource(db.extrasDao.getLive(packageName), extras::setValue) + } + + fun updateActions() { + viewModelScope.launch { + updateUI() + } + } + + private suspend fun updateUI() { + withContext(Dispatchers.IO) { + val installed = installedItem.value + val productRepos = productRepos + val product = findSuggestedProduct(productRepos, installed) { it.first }?.first + val compatible = product != null && product.selectedReleases.firstOrNull() + .let { it != null && it.incompatibilities.isEmpty() } + val canInstall = product != null && installed == null && compatible + val canUpdate = + product != null && compatible && product.canUpdate(installed) && + !shouldIgnore(product.versionCode) + val canUninstall = product != null && installed != null && !installed.isSystem + val canLaunch = + product != null && installed != null && installed.launcherActivities.isNotEmpty() + val canShare = product != null && productRepos[0].second.name == "F-Droid" + val bookmarked = extras.value?.favorite + + val actions = mutableSetOf() + launch { + if (canInstall) actions += ActionState.Install + if (canUpdate) actions += ActionState.Update + if (canLaunch) actions += ActionState.Launch + if (installed != null) actions += ActionState.Details + if (canUninstall) actions += ActionState.Uninstall + if (canShare) actions += ActionState.Share + if (bookmarked == true) actions += ActionState.Bookmarked + else actions += ActionState.Bookmark + } + val primaryAction = when { + canUpdate -> ActionState.Update + canLaunch -> ActionState.Launch + canInstall -> ActionState.Install + canShare -> ActionState.Share + else -> null + } + val secondaryAction = when { + primaryAction != ActionState.Share && canShare -> ActionState.Share + primaryAction != ActionState.Launch && canLaunch -> ActionState.Launch + installed != null && canUninstall -> ActionState.Uninstall + else -> null + } + + withContext(Dispatchers.Main) { + this@AppViewModelX.actions.value = actions + + if (downloadState.value != null && mainAction.value?.textId != downloadState.value?.textId) + downloadState.value?.let { + mainAction.value = ActionState.Cancel(it.textId) + } + else if (downloadState.value == null) + mainAction.value = primaryAction + this@AppViewModelX.secondaryAction.value = secondaryAction + } + } + } + + fun shouldIgnore(appVersionCode: Long): Boolean = + extras.value?.ignoredVersion == appVersionCode && extras.value?.ignoreUpdates != false + + fun setIgnoredVersion(packageName: String, versionCode: Long) { + viewModelScope.launch { + saveIgnoredVersion(packageName, versionCode) + } + } + + private suspend fun saveIgnoredVersion(packageName: String, versionCode: Long) { + withContext(Dispatchers.IO) { + val oldValue = db.extrasDao[packageName] + if (oldValue != null) db.extrasDao + .insertReplace(oldValue.copy(ignoredVersion = versionCode)) + else db.extrasDao + .insertReplace(Extras(packageName, ignoredVersion = versionCode)) + } + } + + + fun setIgnoreUpdates(packageName: String, setBoolean: Boolean) { + viewModelScope.launch { + saveIgnoreUpdates(packageName, setBoolean) + } + } + + private suspend fun saveIgnoreUpdates(packageName: String, setBoolean: Boolean) { + withContext(Dispatchers.IO) { + val oldValue = db.extrasDao[packageName] + if (oldValue != null) db.extrasDao + .insertReplace(oldValue.copy(ignoreUpdates = setBoolean)) + else db.extrasDao + .insertReplace(Extras(packageName, ignoreUpdates = setBoolean)) + } } class Factory(val db: DatabaseX, val packageName: String) : ViewModelProvider.Factory {