From ddafdd6e8b540c1d29d004cb809d08bfe19065b9 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 30 Dec 2025 10:49:03 -0300 Subject: [PATCH] Only show one app confirmation dialog at a time --- .../main/kotlin/org/fdroid/install/InstallState.kt | 12 ++++++++++++ app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt | 12 ++---------- app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt | 3 ++- .../kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt | 6 ++++++ 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/install/InstallState.kt b/app/src/main/kotlin/org/fdroid/install/InstallState.kt index 1fd743dda..fda917fad 100644 --- a/app/src/main/kotlin/org/fdroid/install/InstallState.kt +++ b/app/src/main/kotlin/org/fdroid/install/InstallState.kt @@ -20,6 +20,7 @@ sealed class InstallState(val showProgress: Boolean) { val version: AppVersion, val repo: Repository, override val sessionId: Int, + override val creationTimeMillis: Long = System.currentTimeMillis(), override val intent: PendingIntent, ) : InstallConfirmationState() { override val name: String = state.name @@ -67,6 +68,7 @@ sealed class InstallState(val showProgress: Boolean) { override val iconDownloadRequest: DownloadRequest?, override val sessionId: Int, override val intent: PendingIntent, + override val creationTimeMillis: Long, val progress: Float, ) : InstallConfirmationState() { constructor( @@ -82,6 +84,7 @@ sealed class InstallState(val showProgress: Boolean) { iconDownloadRequest = state.iconDownloadRequest, sessionId = sessionId, intent = intent, + creationTimeMillis = System.currentTimeMillis(), progress = progress ) } @@ -127,5 +130,14 @@ sealed class InstallStateWithInfo(showProgress: Boolean) : InstallState(showProg sealed class InstallConfirmationState() : InstallStateWithInfo(true) { abstract val sessionId: Int + + /** + * The epoch time in milliseconds when this state was created. + * This is used to get a stable ordering on apps that require user confirmation. + * The reason this is needed is that we can only show a single confirmation dialog at a time. + * If we show more than one, the second one gets silently swallowed by the system + * and we don't receive any feedback, so installation process of several apps gets stuck. + */ + abstract val creationTimeMillis: Long abstract val intent: PendingIntent } diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt index 1383f0973..08585a76b 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt @@ -25,7 +25,6 @@ import androidx.compose.material3.TopAppBarDefaults.enterAlwaysScrollBehavior import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -59,18 +58,11 @@ fun MyApps( modifier: Modifier = Modifier, ) { val myAppsModel = myAppsInfo.model - val appToConfirm by remember(myAppsInfo.model.installingApps) { - derivedStateOf { - myAppsInfo.model.installingApps.find { app -> - app.installState is InstallConfirmationState - } - } - } // Ask user to confirm appToConfirm whenever it changes and we are in STARTED state. // In tests, waiting for RESUME didn't work, because the LaunchedEffect ran before. val lifecycleOwner = LocalLifecycleOwner.current - LaunchedEffect(appToConfirm) { - val app = appToConfirm + LaunchedEffect(myAppsModel.appToConfirm) { + val app = myAppsModel.appToConfirm if (app != null && lifecycleOwner.lifecycle.currentState.isAtLeast(STARTED)) { val state = app.installState as InstallConfirmationState myAppsInfo.confirmAppInstall(app.packageName, state) diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt index 84a75b0a0..f3b1d54fe 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt @@ -14,8 +14,9 @@ interface MyAppsInfo { } data class MyAppsModel( - val installingApps: List, + val appToConfirm: InstallingAppItem? = null, val appUpdates: List? = null, + val installingApps: List, val appsWithIssue: List? = null, val installedApps: List? = null, val sortOrder: AppListSortOrder = AppListSortOrder.NAME, diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt index f729ba0f7..b7e9f9d05 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import org.fdroid.database.AppListSortOrder import org.fdroid.download.NetworkState +import org.fdroid.install.InstallConfirmationState import org.fdroid.install.InstallState import org.fdroid.install.InstallStateWithInfo import org.fdroid.ui.utils.normalize @@ -87,6 +88,11 @@ fun MyAppsPresenter( } } ?: run { updateBytes = null } return MyAppsModel( + appToConfirm = installingApps.filter { + it.installState is InstallConfirmationState + }.minByOrNull { + (it.installState as InstallConfirmationState).creationTimeMillis + }, installingApps = installingApps.sort(sortOrder), appUpdates = updates?.sort(sortOrder), appsWithIssue = withIssues?.sort(sortOrder),