From 7ac198821bcd70896d55ca23318479fc56a46db6 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 2 May 2026 21:57:28 +0500 Subject: [PATCH] fix: don't mark app installed when Shizuku falls back to system installer --- .../services/DefaultDownloadOrchestrator.kt | 50 +++++++++++++---- .../domain/system/DownloadOrchestrator.kt | 18 ++++++ .../details/presentation/DetailsViewModel.kt | 55 +++++++++++++++---- 3 files changed, 100 insertions(+), 23 deletions(-) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/DefaultDownloadOrchestrator.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/DefaultDownloadOrchestrator.kt index 5d80f0ff7..ae117ffd2 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/DefaultDownloadOrchestrator.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/DefaultDownloadOrchestrator.kt @@ -310,19 +310,45 @@ class DefaultDownloadOrchestrator( val ext = spec.asset.name.substringAfterLast('.', "").lowercase() try { installer.ensurePermissionsOrThrow(ext) - installer.install(filePath, ext) - // Successful install — clear any pending file path on - // the row (if it was set from a prior aborted attempt) - // and move the orchestrator entry to Completed. - try { - installedAppsRepository.setPendingInstallFilePath(spec.packageName, null) - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - Logger.w(e) { "Orchestrator: failed to clear pending install path" } + val outcome = installer.install(filePath, ext) + when (outcome) { + InstallOutcome.COMPLETED -> { + // Genuine silent install (Shizuku/Sui). Clear any + // pending file path on the row (if set by a prior + // aborted attempt) and move the entry to Completed. + try { + installedAppsRepository.setPendingInstallFilePath(spec.packageName, null) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Logger.w(e) { "Orchestrator: failed to clear pending install path" } + } + pendingInstallNotifier.clearPending(spec.packageName) + updateEntry(spec.packageName) { + it.copy(stage = DownloadStage.Completed, installOutcome = outcome) + } + } + + InstallOutcome.DELEGATED_TO_SYSTEM -> { + // The install was *handed off* to the system + // installer (e.g. Shizuku binder went away and the + // wrapper fell back to the standard installer). The + // user has not confirmed yet — they may cancel the + // system prompt. Do NOT mark this as a real install. + // Keep the file parked so the foreground VM can re- + // run validation if needed and the apps row can show + // a "ready to install" state. The eventual + // PACKAGE_ADDED broadcast (or its absence on cancel) + // is what flips `isPendingInstall` correctly. + Logger.i { + "Orchestrator: AlwaysInstall path returned DELEGATED_TO_SYSTEM " + + "for ${spec.packageName}; Completed-with-pending so DB row stays pending" + } + updateEntry(spec.packageName) { + it.copy(stage = DownloadStage.Completed, installOutcome = outcome) + } + } } - pendingInstallNotifier.clearPending(spec.packageName) - updateEntry(spec.packageName) { it.copy(stage = DownloadStage.Completed) } } catch (e: CancellationException) { throw e } catch (t: Throwable) { diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/DownloadOrchestrator.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/DownloadOrchestrator.kt index 355936bd6..60d7ea116 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/DownloadOrchestrator.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/DownloadOrchestrator.kt @@ -212,6 +212,24 @@ data class OrchestratedDownload( val totalBytes: Long? = null, /** Error message if [stage] is [DownloadStage.Failed]. */ val errorMessage: String? = null, + /** + * Outcome reported by the platform installer when the orchestrator + * itself ran the install (the [InstallPolicy.AlwaysInstall] path). + * + * `null` for everything before the orchestrator finishes calling + * `installer.install()` and for entries that never went through the + * orchestrator's own install (i.e. [InstallPolicy.InstallWhileForeground] + * and [InstallPolicy.DeferUntilUserAction] — those hand the file off + * to the foreground ViewModel which captures the outcome itself). + * + * Consumers observing [DownloadStage.Completed] must use this to + * decide whether to persist the row as actually-installed + * (`COMPLETED`) or as pending-confirmation (`DELEGATED_TO_SYSTEM` — + * e.g. Shizuku falling back to the system installer because the + * binder went away). Treating every `Completed` as a real install + * silently marks rows installed on cancelled prompts. + */ + val installOutcome: InstallOutcome? = null, ) /** diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt index 9e3fe38c7..8a3c2143c 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt @@ -1756,19 +1756,39 @@ class DetailsViewModel( } OrchestratorStage.Completed -> { - // Shizuku/AlwaysInstall path: orchestrator - // installed silently. Persist the DB row here — - // PackageEventReceiver only patches existing rows - // and would skip a fresh Shizuku install. + // AlwaysInstall path: orchestrator finished its + // own install attempt. The actual outcome may be + // COMPLETED (genuine silent install — Shizuku/Sui) + // or DELEGATED_TO_SYSTEM (Shizuku fell back to the + // system installer because the binder went away). + // + // Persist the DB row here — PackageEventReceiver + // only patches existing rows and would skip a + // fresh row entirely. The row's `isPendingInstall` + // flag is driven by the actual outcome so a + // delegated-but-unconfirmed install never gets + // marked installed (cancelled prompts no longer + // leave the row falsely flipped to installed). + val resolvedOutcome = entry.installOutcome ?: InstallOutcome.COMPLETED + val isCompleted = resolvedOutcome == InstallOutcome.COMPLETED + _state.value = _state.value.copy(downloadStage = DownloadStage.IDLE) currentAssetName = null appendLog( assetName = assetName, size = sizeBytes, tag = releaseTag, - result = if (isUpdate) LogResult.Updated else LogResult.Installed, + result = when { + !isCompleted -> LogResult.Downloaded + isUpdate -> LogResult.Updated + else -> LogResult.Installed + }, ) - _state.value.repository?.id?.let { telemetryRepository.recordInstallSucceeded(it) } + if (isCompleted) { + _state.value.repository?.id?.let { + telemetryRepository.recordInstallSucceeded(it) + } + } if (platform == Platform.ANDROID) { val filePath = entry.filePath @@ -1787,22 +1807,35 @@ class DetailsViewModel( assetSize = sizeBytes, releaseTag = releaseTag, isUpdate = isUpdate, - installOutcome = InstallOutcome.COMPLETED, + installOutcome = resolvedOutcome, ) } else { logger.warn( - "Shizuku install completed but APK validation failed: $validation", + "Orchestrator install settled (outcome=$resolvedOutcome) " + + "but APK validation failed: $validation", ) } }.onFailure { t -> - logger.error("Failed to persist Shizuku install: ${t.message}") + logger.error("Failed to persist orchestrator install: ${t.message}") } } else { - logger.warn("Shizuku install completed but filePath is null; DB not updated") + logger.warn( + "Orchestrator install settled (outcome=$resolvedOutcome) " + + "but filePath is null; DB not updated", + ) } } - downloadOrchestrator.dismiss(packageKey) + // Only dismiss when the install was actually + // confirmed. For DELEGATED outcomes we keep the + // entry in the orchestrator so the apps row can + // still surface the parked file (one-tap retry on + // user-cancelled prompts) until PackageEventReceiver + // confirms the install or the stale-pending sweep + // cleans it up. + if (isCompleted) { + downloadOrchestrator.dismiss(packageKey) + } return@collect }