Update: Migrate ignoring updates logic to Extras

This commit is contained in:
machiav3lli 2022-06-23 00:52:53 +02:00
parent bed6c203a1
commit 9941354bcb
6 changed files with 129 additions and 119 deletions

View File

@ -15,7 +15,6 @@ import coil.ImageLoader
import coil.ImageLoaderFactory import coil.ImageLoaderFactory
import com.looker.droidify.content.Cache import com.looker.droidify.content.Cache
import com.looker.droidify.content.Preferences import com.looker.droidify.content.Preferences
import com.looker.droidify.content.ProductPreferences
import com.looker.droidify.database.DatabaseX import com.looker.droidify.database.DatabaseX
import com.looker.droidify.index.RepositoryUpdater import com.looker.droidify.index.RepositoryUpdater
import com.looker.droidify.network.CoilDownloader import com.looker.droidify.network.CoilDownloader
@ -45,7 +44,6 @@ class MainApplication : Application(), ImageLoaderFactory {
db = DatabaseX.getInstance(applicationContext) db = DatabaseX.getInstance(applicationContext)
Preferences.init(this) Preferences.init(this)
ProductPreferences.init(this)
RepositoryUpdater.init(this) RepositoryUpdater.init(this)
listenApplications() listenApplications()
listenPreferences() listenPreferences()

View File

@ -38,8 +38,6 @@ import com.looker.droidify.ROW_UPDATED
import com.looker.droidify.ROW_VERSION_CODE import com.looker.droidify.ROW_VERSION_CODE
import com.looker.droidify.TABLE_CATEGORY import com.looker.droidify.TABLE_CATEGORY
import com.looker.droidify.TABLE_CATEGORY_NAME 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
import com.looker.droidify.TABLE_EXTRAS_NAME import com.looker.droidify.TABLE_EXTRAS_NAME
import com.looker.droidify.TABLE_INSTALLED import com.looker.droidify.TABLE_INSTALLED
@ -198,9 +196,9 @@ interface ProductDao : BaseDao<Product> {
// Select the return fields // Select the return fields
builder += """SELECT $TABLE_PRODUCT.rowid AS $ROW_ID, $TABLE_PRODUCT.$ROW_REPOSITORY_ID, 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_PACKAGE_NAME, $TABLE_PRODUCT.$ROW_LABEL,
$TABLE_PRODUCT.$ROW_SUMMARY, $TABLE_PRODUCT.$ROW_DESCRIPTION, $TABLE_PRODUCT.$ROW_SUMMARY, $TABLE_PRODUCT.$ROW_DESCRIPTION,
(COALESCE($TABLE_IGNORED.$ROW_VERSION_CODE, -1) NOT IN (0, $TABLE_PRODUCT.$ROW_VERSION_CODE) AND (COALESCE($TABLE_EXTRAS.$ROW_IGNORED_VERSION, -1) != $TABLE_PRODUCT.$ROW_VERSION_CODE AND
$TABLE_PRODUCT.$ROW_COMPATIBLE != 0 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 $TABLE_PRODUCT.$ROW_VERSION_CODE > COALESCE($TABLE_INSTALLED.$ROW_VERSION_CODE, 0xffffffff) AND
$signatureMatches) AS $ROW_CAN_UPDATE, $TABLE_PRODUCT.$ROW_ICON, $signatureMatches) AS $ROW_CAN_UPDATE, $TABLE_PRODUCT.$ROW_ICON,
$TABLE_PRODUCT.$ROW_METADATA_ICON, $TABLE_PRODUCT.$ROW_RELEASES, $TABLE_PRODUCT.$ROW_CATEGORIES, $TABLE_PRODUCT.$ROW_METADATA_ICON, $TABLE_PRODUCT.$ROW_RELEASES, $TABLE_PRODUCT.$ROW_CATEGORIES,
@ -227,9 +225,9 @@ interface ProductDao : BaseDao<Product> {
builder += """JOIN $TABLE_REPOSITORY_NAME AS $TABLE_REPOSITORY builder += """JOIN $TABLE_REPOSITORY_NAME AS $TABLE_REPOSITORY
ON $TABLE_PRODUCT.$ROW_REPOSITORY_ID = $TABLE_REPOSITORY.$ROW_ID""" ON $TABLE_PRODUCT.$ROW_REPOSITORY_ID = $TABLE_REPOSITORY.$ROW_ID"""
// Merge the matching locks // Merge the matching extras
builder += """LEFT JOIN $TABLE_IGNORED_NAME AS $TABLE_IGNORED builder += """LEFT JOIN $TABLE_EXTRAS_NAME AS $TABLE_EXTRAS
ON $TABLE_PRODUCT.$ROW_PACKAGE_NAME = $TABLE_IGNORED.$ROW_PACKAGE_NAME""" ON $TABLE_PRODUCT.$ROW_PACKAGE_NAME = $TABLE_EXTRAS.$ROW_PACKAGE_NAME"""
// Merge the matching installed // Merge the matching installed
if (!installed && !updates) builder += "LEFT" if (!installed && !updates) builder += "LEFT"

View File

@ -7,7 +7,7 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import com.looker.droidify.database.entity.Category import com.looker.droidify.database.entity.Category
import com.looker.droidify.database.entity.CategoryTemp 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.Installed
import com.looker.droidify.database.entity.Product import com.looker.droidify.database.entity.Product
import com.looker.droidify.database.entity.ProductTemp import com.looker.droidify.database.entity.ProductTemp
@ -27,8 +27,8 @@ import kotlinx.coroutines.launch
Category::class, Category::class,
CategoryTemp::class, CategoryTemp::class,
Installed::class, Installed::class,
Ignored::class Extras::class
], version = 7 ], version = 8
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class DatabaseX : RoomDatabase() { abstract class DatabaseX : RoomDatabase() {
@ -39,7 +39,7 @@ abstract class DatabaseX : RoomDatabase() {
abstract val categoryDao: CategoryDao abstract val categoryDao: CategoryDao
abstract val categoryTempDao: CategoryTempDao abstract val categoryTempDao: CategoryTempDao
abstract val installedDao: InstalledDao abstract val installedDao: InstalledDao
abstract val lockDao: LockDao abstract val extrasDao: ExtrasDao
companion object { companion object {
@Volatile @Volatile

View File

@ -3,12 +3,10 @@ package com.looker.droidify.ui.compose.utils
import android.net.Uri import android.net.Uri
import com.looker.droidify.database.entity.Release import com.looker.droidify.database.entity.Release
import com.looker.droidify.entity.ActionState import com.looker.droidify.entity.ActionState
import com.looker.droidify.entity.ProductPreference
import com.looker.droidify.entity.Screenshot import com.looker.droidify.entity.Screenshot
interface Callbacks { interface Callbacks {
fun onActionClick(action: ActionState?) fun onActionClick(action: ActionState?)
fun onPreferenceChanged(preference: ProductPreference)
fun onPermissionsClick(group: String?, permissions: List<String>) fun onPermissionsClick(group: String?, permissions: List<String>)
fun onScreenshotClick(screenshot: Screenshot) fun onScreenshotClick(screenshot: Screenshot)
fun onReleaseClick(release: Release) fun onReleaseClick(release: Release)

View File

@ -41,13 +41,12 @@ import com.looker.droidify.RELEASE_STATE_INSTALLED
import com.looker.droidify.RELEASE_STATE_NONE import com.looker.droidify.RELEASE_STATE_NONE
import com.looker.droidify.RELEASE_STATE_SUGGESTED import com.looker.droidify.RELEASE_STATE_SUGGESTED
import com.looker.droidify.content.Preferences 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.database.entity.Release
import com.looker.droidify.entity.ActionState import com.looker.droidify.entity.ActionState
import com.looker.droidify.entity.AntiFeature import com.looker.droidify.entity.AntiFeature
import com.looker.droidify.entity.DonateType import com.looker.droidify.entity.DonateType
import com.looker.droidify.entity.DownloadState import com.looker.droidify.entity.DownloadState
import com.looker.droidify.entity.ProductPreference
import com.looker.droidify.entity.Screenshot import com.looker.droidify.entity.Screenshot
import com.looker.droidify.installer.AppInstaller import com.looker.droidify.installer.AppInstaller
import com.looker.droidify.network.CoilDownloader import com.looker.droidify.network.CoilDownloader
@ -144,9 +143,7 @@ class AppSheetX() : FullscreenBottomSheetDialogFragment(), Callbacks {
override fun setupLayout() { override fun setupLayout() {
viewModel._productRepos.observe(this) { viewModel._productRepos.observe(this) {
lifecycleScope.launch { viewModel.updateActions()
updateButtons()
}
} }
} }
@ -159,86 +156,6 @@ class AppSheetX() : FullscreenBottomSheetDialogFragment(), Callbacks {
downloadConnection.unbind(requireContext()) 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<ActionState>()
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?) { private suspend fun updateDownloadState(downloadState: DownloadService.State?) {
val state = when (downloadState) { val state = when (downloadState) {
is DownloadService.State.Pending -> DownloadState.Pending is DownloadService.State.Pending -> DownloadState.Pending
@ -250,8 +167,8 @@ class AppSheetX() : FullscreenBottomSheetDialogFragment(), Callbacks {
else -> null else -> null
} }
viewModel.downloadState.value = state viewModel.downloadState.value = state
updateButtons()
if (downloadState is DownloadService.State.Success && isResumed && !rootInstallerEnabled) { if (downloadState is DownloadService.State.Success && isResumed && !rootInstallerEnabled) {
viewModel.updateActions()
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
AppInstaller.getInstance(context)?.defaultInstaller?.install(downloadState.release.cacheFileName) AppInstaller.getInstance(context)?.defaultInstaller?.install(downloadState.release.cacheFileName)
} }
@ -325,10 +242,6 @@ class AppSheetX() : FullscreenBottomSheetDialogFragment(), Callbacks {
startActivity(Intent.createChooser(shareIntent, "Where to Send?")) startActivity(Intent.createChooser(shareIntent, "Where to Send?"))
} }
override fun onPreferenceChanged(preference: ProductPreference) {
lifecycleScope.launch { updateButtons(preference) }
}
override fun onPermissionsClick(group: String?, permissions: List<String>) { override fun onPermissionsClick(group: String?, permissions: List<String>) {
MessageDialog(MessageDialog.Message.Permissions(group, permissions)).show( MessageDialog(MessageDialog.Message.Permissions(group, permissions)).show(
childFragmentManager childFragmentManager
@ -422,6 +335,7 @@ class AppSheetX() : FullscreenBottomSheetDialogFragment(), Callbacks {
val mainAction by viewModel.mainAction.observeAsState(if (installed == null) ActionState.Install else ActionState.Launch) 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 actions by viewModel.actions.observeAsState() // TODO add rest actions to UI
val secondaryAction by viewModel.secondaryAction.observeAsState() val secondaryAction by viewModel.secondaryAction.observeAsState()
val extras by viewModel.extras.observeAsState(Extras(packageName))
val productRepos = products?.mapNotNull { product -> val productRepos = products?.mapNotNull { product ->
repos?.firstOrNull { it.id == product.repositoryId } repos?.firstOrNull { it.id == product.repositoryId }
?.let { Pair(product, it) } ?.let { Pair(product, it) }
@ -515,27 +429,23 @@ class AppSheetX() : FullscreenBottomSheetDialogFragment(), Callbacks {
item { item {
AnimatedVisibility(visible = product.canUpdate(installed)) { AnimatedVisibility(visible = product.canUpdate(installed)) {
SwitchPreference(text = stringResource(id = R.string.ignore_this_update), SwitchPreference(text = stringResource(id = R.string.ignore_this_update),
initSelected = ProductPreferences[product.packageName].ignoreVersionCode == product.versionCode, initSelected = extras?.ignoredVersion == product.versionCode,
onCheckedChanged = { onCheckedChanged = {
ProductPreferences[product.packageName].let { viewModel.setIgnoredVersion(
it.copy( product.packageName,
ignoreVersionCode = if (it) product.versionCode else 0
if (it.ignoreVersionCode == product.versionCode) 0 else product.versionCode )
) viewModel.updateActions()
}
}) })
} }
} }
item { item {
AnimatedVisibility(visible = installed != null) { AnimatedVisibility(visible = installed != null) {
SwitchPreference(text = stringResource(id = R.string.ignore_all_updates), SwitchPreference(text = stringResource(id = R.string.ignore_all_updates),
initSelected = ProductPreferences[product.packageName].ignoreVersionCode == product.versionCode, initSelected = extras?.ignoreUpdates == true,
onCheckedChanged = { onCheckedChanged = {
ProductPreferences[product.packageName].let { viewModel.setIgnoreUpdates(product.packageName, it)
it.copy( viewModel.updateActions()
ignoreUpdates = !it.ignoreUpdates
)
}
}) })
} }
} }

View File

@ -4,12 +4,18 @@ import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.looker.droidify.database.DatabaseX 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.Installed
import com.looker.droidify.database.entity.Product import com.looker.droidify.database.entity.Product
import com.looker.droidify.database.entity.Repository import com.looker.droidify.database.entity.Repository
import com.looker.droidify.entity.ActionState import com.looker.droidify.entity.ActionState
import com.looker.droidify.entity.DownloadState 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() { 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<ActionState>() val mainAction = MutableLiveData<ActionState>()
val actions = MutableLiveData<Set<ActionState>>() val actions = MutableLiveData<Set<ActionState>>()
val secondaryAction = MutableLiveData<ActionState>() val secondaryAction = MutableLiveData<ActionState>()
val extras = MediatorLiveData<Extras>()
init { init {
products.addSource(db.productDao.getLive(packageName)) { products.setValue(it.filterNotNull()) } products.addSource(db.productDao.getLive(packageName)) { products.setValue(it.filterNotNull()) }
repositories.addSource(db.repositoryDao.allLive, repositories::setValue) repositories.addSource(db.repositoryDao.allLive, repositories::setValue)
installedItem.addSource(db.installedDao.getLive(packageName), installedItem::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<ActionState>()
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 { class Factory(val db: DatabaseX, val packageName: String) : ViewModelProvider.Factory {