Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand Down