diff --git a/app/src/main/java/com/aurora/store/data/helper/DownloadHelper.kt b/app/src/main/java/com/aurora/store/data/helper/DownloadHelper.kt index af3aaffeb..8c54d7560 100644 --- a/app/src/main/java/com/aurora/store/data/helper/DownloadHelper.kt +++ b/app/src/main/java/com/aurora/store/data/helper/DownloadHelper.kt @@ -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) { + 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: diff --git a/app/src/main/java/com/aurora/store/data/helper/UpdateHelper.kt b/app/src/main/java/com/aurora/store/data/helper/UpdateHelper.kt index a4f92ed5b..8e6ed1ca9 100644 --- a/app/src/main/java/com/aurora/store/data/helper/UpdateHelper.kt +++ b/app/src/main/java/com/aurora/store/data/helper/UpdateHelper.kt @@ -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) } diff --git a/app/src/main/java/com/aurora/store/data/model/SelfUpdate.kt b/app/src/main/java/com/aurora/store/data/model/SelfUpdate.kt index 4a589214f..19760fb4b 100644 --- a/app/src/main/java/com/aurora/store/data/model/SelfUpdate.kt +++ b/app/src/main/java/com/aurora/store/data/model/SelfUpdate.kt @@ -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" - } } diff --git a/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt b/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt index b1a3934f8..d9b07b676 100644 --- a/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt +++ b/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt @@ -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