Clean up stale self-update state on launch

The install event isn't delivered when the app replaces its own APK, so a
self-update could linger in the Updates list as "installing forever" after the
update completed and the app restarted.

- UpdateHelper: clear a stale self-update row on launch, flavor-aware — nightly
  by its commit-tagged version name (the version code is static), release/preload
  by the version code.
- DownloadHelper: finalize a self-update download stuck in INSTALLING (the commit
  killed the process before it could advance) to INSTALLED on launch.
This commit is contained in:
Rahul Patel
2026-06-01 03:07:40 +05:30
parent ce04b859fd
commit 4a0c1101f4
4 changed files with 54 additions and 17 deletions

View File

@@ -73,13 +73,31 @@ class DownloadHelper @Inject constructor(
*/
fun init() {
AuroraApp.scope.launch {
cancelFailedDownloads(downloadDao.downloads().firstOrNull() ?: emptyList())
val downloads = downloadDao.downloads().firstOrNull() ?: emptyList()
cancelFailedDownloads(downloads)
finalizeStaleSelfUpdate(downloads)
}.invokeOnCompletion {
observeDownloads()
observeInstalls()
}
}
/**
* Finalizes a self-update download left dangling in [DownloadStatus.INSTALLING]. Replacing
* the app's own APK kills the process before the installer's "installed" event can advance
* the row, so on the next launch it would otherwise show as installing forever. Reaching
* INSTALLING means the install was already committed, so mark it installed; if it actually
* failed, the periodic update check re-offers and re-enqueues it.
*/
private suspend fun finalizeStaleSelfUpdate(downloads: List<Download>) {
downloads.firstOrNull {
it.packageName == context.packageName && it.status == DownloadStatus.INSTALLING
}?.let {
Log.i(TAG, "Finalizing stale self-update install for ${it.packageName}")
downloadDao.updateStatus(it.packageName, DownloadStatus.INSTALLED)
}
}
/**
* Advances a download row through the installer phase so its history reflects whether the
* app actually installed, not just that the bytes finished downloading:

View File

@@ -24,6 +24,7 @@ import com.aurora.store.data.room.update.IgnoredUpdateDao
import com.aurora.store.data.room.update.Update
import com.aurora.store.data.room.update.UpdateDao
import com.aurora.store.data.work.UpdateWorker
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCES_UPDATES_RESTRICTIONS_BATTERY
import com.aurora.store.util.Preferences.PREFERENCES_UPDATES_RESTRICTIONS_IDLE
@@ -253,13 +254,23 @@ class UpdateHelper @Inject constructor(
private suspend fun deleteInvalidUpdates() {
updateDao.updates().firstOrNull()?.forEach { update ->
// Nightly builds share a static version code, so the generic "up to date"
// check would wrongly drop a freshly found self-update. The worker owns the
// self-update lifecycle (re-checked by build timestamp); cleanup happens via
// the install event instead.
if (update.isSelfUpdate(context) && BuildType.CURRENT == BuildType.NIGHTLY) {
if (update.isSelfUpdate(context)) {
// Self-updates need a flavor-aware staleness check. Release/preload bump the
// version code, so the normal up-to-date check works. Nightly reuses a static
// version code (isUpToDate is always true there), so fall back to the
// commit-tagged version name. Without this the row lingers until the next
// update check, since the install event isn't delivered when the app replaces
// itself.
val alreadyInstalled = if (BuildType.CURRENT == BuildType.NIGHTLY) {
PackageUtil.getInstalledVersionName(context, update.packageName) ==
update.versionName
} else {
update.isUpToDate(context)
}
if (alreadyInstalled) deleteUpdate(update.packageName)
return@forEach
}
if (!update.isInstalled(context) || update.isUpToDate(context)) {
deleteUpdate(update.packageName)
}

View File

@@ -31,6 +31,12 @@ data class SelfUpdate(
val versionCodeRaw: String = "0",
@SerialName("download_url")
val downloadUrl: String = "",
@SerialName("icon_url")
val iconUrl: String = "",
@SerialName("sha1")
val sha1: String = "",
@SerialName("sha256")
val sha256: String = "",
val changelog: String = "",
@SerialName("size")
val sizeRaw: String = "0",
@@ -61,12 +67,14 @@ data class SelfUpdate(
updatedOn = updatedOn,
displayName = context.getString(R.string.app_name),
developerName = "Rahul Kumar Patel",
iconArtwork = Artwork(url = ICON_URL),
iconArtwork = Artwork(url = iconUrl),
fileList = mutableListOf(
PlayFile(
name = "${context.packageName}.apk",
url = downloadUrl,
size = size
size = size,
sha1 = sha1,
sha256 = sha256
)
),
isFree = true,
@@ -78,11 +86,4 @@ data class SelfUpdate(
EncodedCertificateSet(certificateSet = it, sha256 = String())
}.toMutableList()
)
companion object {
// Kept in sync with the fastlane metadata on the project.
private const val ICON_URL =
"https://gitlab.com/AuroraOSS/AuroraStore/-/raw/master/" +
"fastlane/metadata/android/en-US/images/icon.png"
}
}

View File

@@ -130,14 +130,21 @@ class DownloadWorker @AssistedInject constructor(
// Fetch required data for download
try {
download = downloadDao.getDownload(inputData.getString(DownloadHelper.PACKAGE_NAME)!!)
} catch (exception: Exception) {
return onFailure(exception)
}
// The icon only decorates the progress notification. Fetching it is best-effort: a
// failure (unreachable/pinned host, non-HTTP url, undecodable image) must never abort
// the download itself.
try {
val response = (httpClient as HttpClient).call(download.iconURL).body
val bitmap = BitmapFactory.decodeStream(
withContext(Dispatchers.IO) { response.byteStream() }
)
icon = bitmap.scale(96, 96)
icon = bitmap?.scale(96, 96)
} catch (exception: Exception) {
return onFailure(exception)
Log.w(TAG, "Failed to fetch icon for ${download.packageName}", exception)
}
// Set work/service to foreground on < Android 12.0