diff --git a/next/src/main/kotlin/org/fdroid/install/InstallNotificationState.kt b/next/src/main/kotlin/org/fdroid/install/InstallNotificationState.kt index 24bee438f..6719c4bc3 100644 --- a/next/src/main/kotlin/org/fdroid/install/InstallNotificationState.kt +++ b/next/src/main/kotlin/org/fdroid/install/InstallNotificationState.kt @@ -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, ) diff --git a/next/src/main/kotlin/org/fdroid/ui/Main.kt b/next/src/main/kotlin/org/fdroid/ui/Main.kt index 75a84cc34..8eddc1394 100644 --- a/next/src/main/kotlin/org/fdroid/ui/Main.kt +++ b/next/src/main/kotlin/org/fdroid/ui/Main.kt @@ -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) diff --git a/next/src/main/kotlin/org/fdroid/ui/apps/MyAppItem.kt b/next/src/main/kotlin/org/fdroid/ui/apps/MyAppItem.kt index 2e4d86ede..4200d8657 100644 --- a/next/src/main/kotlin/org/fdroid/ui/apps/MyAppItem.kt +++ b/next/src/main/kotlin/org/fdroid/ui/apps/MyAppItem.kt @@ -21,6 +21,7 @@ data class InstallingAppItem( } data class AppUpdateItem( + val repoId: Long, override val packageName: String, override val name: String, val installedVersionName: String, diff --git a/next/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt b/next/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt index fdcbe0477..5ac43c812 100644 --- a/next/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt +++ b/next/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt @@ -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", diff --git a/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt b/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt index c34c22756..0feca5b7e 100644 --- a/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt +++ b/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt @@ -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) diff --git a/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt b/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt index 78c125572..18a950b95 100644 --- a/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt +++ b/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt @@ -81,6 +81,10 @@ class MyAppsViewModel @Inject constructor( installedAppsLiveData.removeObserver(installedAppsObserver) } + fun updateAll() { + updatesManager.updateAll() + } + fun search(query: String) { searchQuery.value = query } diff --git a/next/src/main/kotlin/org/fdroid/ui/apps/UpdatableAppRow.kt b/next/src/main/kotlin/org/fdroid/ui/apps/UpdatableAppRow.kt index bd8abf8d4..9fdbcb6fc 100644 --- a/next/src/main/kotlin/org/fdroid/ui/apps/UpdatableAppRow.kt +++ b/next/src/main/kotlin/org/fdroid/ui/apps/UpdatableAppRow.kt @@ -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", diff --git a/next/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt b/next/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt index 44dca49a8..c14f40aa6 100644 --- a/next/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt +++ b/next/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt @@ -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( diff --git a/next/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt b/next/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt index 39938eb14..b984f5846 100644 --- a/next/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt +++ b/next/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt @@ -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 + ) + } }