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")
}
}