Implement "Update all" apps button

This commit is contained in:
Torsten Grote
2025-10-07 14:52:14 -03:00
parent f8d83cd39c
commit d02da86eaa
9 changed files with 60 additions and 3 deletions

View File

@@ -38,10 +38,20 @@ data class InstallNotificationState(
*/
val isInstallingSomeApp: Boolean = apps.any { it.category == AppStateCategory.INSTALLING }
/**
* Returns true if *all* apps being installed are updates to existing apps.
*/
private val isUpdatingApps: Boolean = apps.all { it.currentVersionName != null }
fun getTitle(context: Context): String {
val titleRes = if (isUpdatingApps) {
R.plurals.notification_updating_title
} else {
R.plurals.notification_installing_title
}
val numActiveApps: Int = apps.count { it.category != AppStateCategory.INSTALLED }
val installTitle = context.resources.getQuantityString(
R.plurals.notification_installing_title,
titleRes,
numActiveApps,
numActiveApps,
)

View File

@@ -113,6 +113,7 @@ fun Main(onListeningForIntent: () -> Unit = {}) {
myAppsViewModel.myAppsModel.collectAsStateWithLifecycle().value
override fun refresh() = myAppsViewModel.refresh()
override fun updateAll() = myAppsViewModel.updateAll()
override fun changeSortOrder(sort: AppListSortOrder) =
myAppsViewModel.changeSortOrder(sort)

View File

@@ -21,6 +21,7 @@ data class InstallingAppItem(
}
data class AppUpdateItem(
val repoId: Long,
override val packageName: String,
override val name: String,
val installedVersionName: String,

View File

@@ -215,7 +215,7 @@ fun MyApps(
.weight(1f),
)
Button(
onClick = {},
onClick = myAppsInfo::updateAll,
modifier = Modifier.padding(end = 16.dp),
) {
Text(stringResource(R.string.update_all))
@@ -356,6 +356,7 @@ fun MyAppsPreview() {
)
)
val app1 = AppUpdateItem(
repoId = 1,
packageName = "B1",
name = "App Update 123",
installedVersionName = "1.0.1",
@@ -363,6 +364,7 @@ fun MyAppsPreview() {
whatsNew = "This is new, all is new, nothing old.",
)
val app2 = AppUpdateItem(
repoId = 2,
packageName = "B2",
name = Names.randomName,
installedVersionName = "3.0.1",

View File

@@ -6,6 +6,7 @@ import org.fdroid.install.InstallState
interface MyAppsInfo {
val model: MyAppsModel
fun refresh()
fun updateAll()
fun changeSortOrder(sort: AppListSortOrder)
fun search(query: String)
fun confirmAppInstall(packageName: String, state: InstallState.UserConfirmationNeeded)

View File

@@ -81,6 +81,10 @@ class MyAppsViewModel @Inject constructor(
installedAppsLiveData.removeObserver(installedAppsObserver)
}
fun updateAll() {
updatesManager.updateAll()
}
fun search(query: String) {
searchQuery.value = query
}

View File

@@ -103,6 +103,7 @@ fun UpdatableAppRow(
@Composable
fun UpdatableAppRowPreview() {
val app1 = AppUpdateItem(
repoId = 1,
packageName = "A",
name = "App Update 123",
installedVersionName = "1.0.1",
@@ -110,6 +111,7 @@ fun UpdatableAppRowPreview() {
whatsNew = "This is new, all is new, nothing old.",
)
val app2 = AppUpdateItem(
repoId = 2,
packageName = "B",
name = "App Update 456",
installedVersionName = "1.0.1",

View File

@@ -210,6 +210,7 @@ fun getAppListInfo(model: AppListModel) = object : AppListInfo {
fun getMyAppsInfo(model: MyAppsModel): MyAppsInfo = object : MyAppsInfo {
override val model = model
override fun refresh() {}
override fun updateAll() {}
override fun changeSortOrder(sort: AppListSortOrder) {}
override fun search(query: String) {}
override fun confirmAppInstall(

View File

@@ -5,20 +5,28 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import mu.KotlinLogging
import org.fdroid.database.AppVersion
import org.fdroid.database.DbUpdateChecker
import org.fdroid.database.FDroidDatabase
import org.fdroid.download.getDownloadRequest
import org.fdroid.index.RepoManager
import org.fdroid.install.AppInstallManager
import org.fdroid.ui.apps.AppUpdateItem
import org.fdroid.utils.IoDispatcher
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.min
@Singleton
class UpdatesManager @Inject constructor(
private val db: FDroidDatabase,
private val dbUpdateChecker: DbUpdateChecker,
@IoDispatcher private val coroutineScope: CoroutineScope,
private val repoManager: RepoManager,
private val appInstallManager: AppInstallManager,
@param:IoDispatcher private val coroutineScope: CoroutineScope,
) {
private val log = KotlinLogging.logger { }
@@ -38,6 +46,7 @@ class UpdatesManager @Inject constructor(
log.info { "Checking for updates..." }
dbUpdateChecker.getUpdatableApps(onlyFromPreferredRepo = true).map { update ->
AppUpdateItem(
repoId = update.repoId,
packageName = update.packageName,
name = update.name ?: "Unknown app",
installedVersionName = update.installedVersionName,
@@ -55,4 +64,30 @@ class UpdatesManager @Inject constructor(
_updates.value = updates
_numUpdates.value = updates.size
}
fun updateAll() {
val concurrencyLimit = min(Runtime.getRuntime().availableProcessors(), 8)
val semaphore = Semaphore(concurrencyLimit)
updates.value?.forEach { update ->
// launch a new co-routine for each app to update
coroutineScope.launch {
// suspend here until we get a permit from the semaphore (there's free workers)
semaphore.withPermit {
updateApp(update)
}
}
}
}
private suspend fun updateApp(update: AppUpdateItem) {
val app = db.getAppDao().getApp(update.repoId, update.packageName) ?: return
appInstallManager.install(
appMetadata = app.metadata,
// we know this is true, because we set this above in loadUpdates()
version = update.update as AppVersion,
currentVersionName = update.installedVersionName,
repo = repoManager.getRepository(update.repoId) ?: return,
iconDownloadRequest = update.iconDownloadRequest
)
}
}