From 33d3fe71b5e75d7cb3cb918e00bcf2ef991f1e84 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 2 May 2026 09:43:23 +0500 Subject: [PATCH] fix: resolve stuck 'preparing to install' after self-update (#478) After GitHub Store updates itself, Android does not deliver ACTION_PACKAGE_REPLACED to the app's own receivers, leaving isPendingInstall = true permanently in the database. Changes: - GithubStoreApp: registerSelfAsInstalledApp() now detects and resolves a stale isPendingInstall flag on every cold start by querying the system PackageManager. - PackageEventReceiver: handle ACTION_MY_PACKAGE_REPLACED (the Android-sanctioned broadcast for self-updates) with a fallback to context.packageName since it carries no data URI. - AndroidManifest: add a dedicated intent-filter for MY_PACKAGE_REPLACED (no data scheme required). Closes #478 Co-Authored-By: Oz --- .../src/androidMain/AndroidManifest.xml | 4 ++ .../rainxch/githubstore/app/GithubStoreApp.kt | 44 ++++++++++++++++++- .../data/services/PackageEventReceiver.kt | 12 ++++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 09d159ec..c7297029 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -150,6 +150,10 @@ + + + + diff --git a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt index e8581840..0368d4ea 100644 --- a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt +++ b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt @@ -145,7 +145,16 @@ class GithubStoreApp : Application() { val selfPackageName = packageName val existing = repo.getAppByPackage(selfPackageName) - if (existing != null) return@launch + if (existing != null) { + // After a self-update the old process is killed before + // ACTION_PACKAGE_REPLACED can be delivered to our own + // receiver, so isPendingInstall stays true. Resolve it + // here at the earliest startup opportunity. + if (existing.isPendingInstall) { + resolveSelfPendingInstall(existing, repo) + } + return@launch + } val packageMonitor = get() val systemInfo = packageMonitor.getInstalledPackageInfo(selfPackageName) @@ -199,6 +208,39 @@ class GithubStoreApp : Application() { } } + /** + * Resolves a stale `isPendingInstall` flag for the app's own + * database row. Called on every cold start when the row exists + * and still has the flag set — the typical scenario after a + * successful self-update where the broadcast path missed. + */ + private suspend fun resolveSelfPendingInstall( + existing: InstalledApp, + repo: InstalledAppsRepository, + ) { + try { + val packageMonitor = get() + val systemInfo = packageMonitor.getInstalledPackageInfo(packageName) + if (systemInfo != null) { + val latestVersionCode = existing.latestVersionCode ?: 0L + repo.updateApp( + existing.copy( + isPendingInstall = false, + installedVersionName = systemInfo.versionName, + installedVersionCode = systemInfo.versionCode, + isUpdateAvailable = latestVersionCode > systemInfo.versionCode, + ), + ) + Logger.i { "Resolved self-update pending install: ${systemInfo.versionName} (code=${systemInfo.versionCode})" } + } else { + repo.updatePendingStatus(packageName, false) + Logger.i { "Resolved self-update pending install (no system info)" } + } + } catch (e: Exception) { + Logger.e(e) { "Failed to resolve self-update pending install" } + } + } + companion object { private const val SELF_REPO_ID = 1101281251L private const val SELF_SHA256_FINGERPRINT = diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt index 75bbf523..85dd61e3 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt @@ -86,7 +86,15 @@ class PackageEventReceiver() : context: Context?, intent: Intent?, ) { - val packageName = intent?.data?.schemeSpecificPart ?: return + // MY_PACKAGE_REPLACED has no data URI — the target is the + // receiving app itself. Fall back to the context's package name. + val packageName = intent?.data?.schemeSpecificPart + ?: if (intent?.action == Intent.ACTION_MY_PACKAGE_REPLACED) { + context?.packageName + } else { + null + } + ?: return Logger.d { "PackageEventReceiver: ${intent.action} for $packageName" } @@ -94,6 +102,7 @@ class PackageEventReceiver() : when (intent.action) { Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_REPLACED, + Intent.ACTION_MY_PACKAGE_REPLACED, -> { scope.launch { onPackageInstalled(packageName) } } @@ -357,6 +366,7 @@ class PackageEventReceiver() : addAction(Intent.ACTION_PACKAGE_ADDED) addAction(Intent.ACTION_PACKAGE_REPLACED) addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED) + addAction(Intent.ACTION_MY_PACKAGE_REPLACED) addDataScheme("package") } }