Surface self-updates through the regular updates list

Fold Aurora Store's self-update back into the existing update pipeline
instead of a dedicated sheet/viewmodel/helper. The feed entry is mapped
to a regular App and added to the update list, reusing the standard
download + install path; it is never auto-installed silently.

- Add a dedicated "Self-update" section in the Updates tab with the
  app-details navigation disabled (it's served from the Aurora OSS feed).
- Gate eligibility via PackageUtil.isSelfUpdateSupported(): vanilla/preload
  flavors, not debug, not F-Droid (huawei excluded by flavor).
- Add a build-aware Settings toggle as the opt-out, removing the row
  immediately when disabled.
- Exempt nightly self-updates from deleteInvalidUpdates (static version
  code) and drop any stale self-update row when none is offered, so a
  phantom update doesn't linger after a self-install.
This commit is contained in:
Rahul Patel
2026-05-29 23:28:09 +05:30
parent 71f6538d46
commit d13d50e442
10 changed files with 239 additions and 121 deletions

View File

@@ -23,6 +23,10 @@ val lastCommitHash = providers.exec {
commandLine("git", "rev-parse", "--short", "HEAD") commandLine("git", "rev-parse", "--short", "HEAD")
}.standardOutput.asText.map { it.trim() } }.standardOutput.asText.map { it.trim() }
val lastCommitTimestamp = providers.exec {
commandLine("git", "log", "-1", "--format=%ct")
}.standardOutput.asText.map { it.trim() }
java { java {
toolchain { toolchain {
languageVersion = JavaLanguageVersion.of(21) languageVersion = JavaLanguageVersion.of(21)
@@ -69,6 +73,7 @@ configure<ApplicationExtension> {
testInstrumentationRunnerArguments["disableAnalytics"] = "true" testInstrumentationRunnerArguments["disableAnalytics"] = "true"
buildConfigField("String", "EXODUS_API_KEY", "\"bbe6ebae4ad45a9cbacb17d69739799b8df2c7ae\"") buildConfigField("String", "EXODUS_API_KEY", "\"bbe6ebae4ad45a9cbacb17d69739799b8df2c7ae\"")
buildConfigField("long", "BUILD_TIMESTAMP", "${lastCommitTimestamp.get()}L")
missingDimensionStrategy("device", "vanilla") missingDimensionStrategy("device", "vanilla")
} }

View File

@@ -37,7 +37,8 @@ object Constants {
const val SHARE_URL = "https://play.google.com/store/apps/details?id=" const val SHARE_URL = "https://play.google.com/store/apps/details?id="
const val UPDATE_URL_STABLE = "https://gitlab.com/AuroraOSS/AuroraStore/raw/master/updates.json" const val UPDATE_URL_VANILLA =
"https://auroraoss.com/downloads/AuroraStore/Feeds/release_feed.json"
const val UPDATE_URL_NIGHTLY = const val UPDATE_URL_NIGHTLY =
"https://auroraoss.com/downloads/AuroraStore/Feeds/nightly_feed.json" "https://auroraoss.com/downloads/AuroraStore/Feeds/nightly_feed.json"

View File

@@ -55,6 +55,7 @@ import com.aurora.store.compose.navigation.Destination
import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider
import com.aurora.store.compose.ui.preferences.network.SingleChoiceDialog import com.aurora.store.compose.ui.preferences.network.SingleChoiceDialog
import com.aurora.store.data.model.UpdateMode import com.aurora.store.data.model.UpdateMode
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences 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_BATTERY
import com.aurora.store.util.Preferences.PREFERENCES_UPDATES_RESTRICTIONS_IDLE import com.aurora.store.util.Preferences.PREFERENCES_UPDATES_RESTRICTIONS_IDLE
@@ -62,6 +63,7 @@ import com.aurora.store.util.Preferences.PREFERENCES_UPDATES_RESTRICTIONS_METERE
import com.aurora.store.util.Preferences.PREFERENCE_FILTER_AURORA_ONLY import com.aurora.store.util.Preferences.PREFERENCE_FILTER_AURORA_ONLY
import com.aurora.store.util.Preferences.PREFERENCE_FILTER_FDROID import com.aurora.store.util.Preferences.PREFERENCE_FILTER_FDROID
import com.aurora.store.util.Preferences.PREFERENCE_FILTER_INSTALLERS import com.aurora.store.util.Preferences.PREFERENCE_FILTER_INSTALLERS
import com.aurora.store.util.Preferences.PREFERENCE_SELF_UPDATE_ENABLED
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_AUTO import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_AUTO
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_CHECK_INTERVAL import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_CHECK_INTERVAL
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED
@@ -79,6 +81,7 @@ fun UpdatesPreferenceScreen(
onScheduleAutomatedCheck = { viewModel.updateHelper.scheduleAutomatedCheck() }, onScheduleAutomatedCheck = { viewModel.updateHelper.scheduleAutomatedCheck() },
onUpdateAutomatedCheck = { viewModel.updateHelper.updateAutomatedCheck() }, onUpdateAutomatedCheck = { viewModel.updateHelper.updateAutomatedCheck() },
onCheckUpdatesNow = { viewModel.updateHelper.checkUpdatesNow() }, onCheckUpdatesNow = { viewModel.updateHelper.checkUpdatesNow() },
onDeleteSelfUpdate = { viewModel.updateHelper.deleteSelfUpdate() },
onNavigateTo = onNavigateTo onNavigateTo = onNavigateTo
) )
} }
@@ -89,6 +92,7 @@ private fun ScreenContent(
onScheduleAutomatedCheck: () -> Unit = {}, onScheduleAutomatedCheck: () -> Unit = {},
onUpdateAutomatedCheck: () -> Unit = {}, onUpdateAutomatedCheck: () -> Unit = {},
onCheckUpdatesNow: () -> Unit = {}, onCheckUpdatesNow: () -> Unit = {},
onDeleteSelfUpdate: () -> Unit = {},
onNavigateTo: (Destination) -> Unit = {} onNavigateTo: (Destination) -> Unit = {}
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -109,6 +113,10 @@ private fun ScreenContent(
var updatesExtended by remember { var updatesExtended by remember {
mutableStateOf(Preferences.getBoolean(context, PREFERENCE_UPDATES_EXTENDED)) mutableStateOf(Preferences.getBoolean(context, PREFERENCE_UPDATES_EXTENDED))
} }
val selfUpdateSupported = remember { PackageUtil.isSelfUpdateSupported(context) }
var selfUpdateEnabled by remember {
mutableStateOf(Preferences.getBoolean(context, PREFERENCE_SELF_UPDATE_ENABLED, true))
}
// Re-read source-filter prefs after returning from SourceFiltersScreen. // Re-read source-filter prefs after returning from SourceFiltersScreen.
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
@@ -335,6 +343,30 @@ private fun ScreenContent(
} }
) )
} }
if (selfUpdateSupported) {
item {
fun onSelfUpdateChanged(enabled: Boolean) {
selfUpdateEnabled = enabled
context.save(PREFERENCE_SELF_UPDATE_ENABLED, enabled)
if (enabled) onCheckUpdatesNow() else onDeleteSelfUpdate()
}
ListItem(
modifier = Modifier.clickable {
onSelfUpdateChanged(!selfUpdateEnabled)
},
headlineContent = { Text(stringResource(R.string.pref_self_update)) },
supportingContent = {
Text(stringResource(R.string.pref_self_update_desc))
},
trailingContent = {
Switch(
checked = selfUpdateEnabled,
onCheckedChange = ::onSelfUpdateChanged
)
}
)
}
}
} }
} }
} }

View File

@@ -59,19 +59,21 @@ fun UpdatesScreen(
} }
val groupedUpdates = remember(updateMap) { val groupedUpdates = remember(updateMap) {
val self = mutableListOf<Map.Entry<Update, Download?>>()
val main = mutableListOf<Map.Entry<Update, Download?>>() val main = mutableListOf<Map.Entry<Update, Download?>>()
val approval = mutableListOf<Map.Entry<Update, Download?>>() val approval = mutableListOf<Map.Entry<Update, Download?>>()
val incompatible = mutableListOf<Map.Entry<Update, Download?>>() val incompatible = mutableListOf<Map.Entry<Update, Download?>>()
updateMap?.entries?.forEach { entry -> updateMap?.entries?.forEach { entry ->
when { when {
entry.key.isSelfUpdate(context) -> self += entry
entry.key.isIncompatible -> incompatible += entry entry.key.isIncompatible -> incompatible += entry
entry.key.requiresOwnershipTransfer(context) -> approval += entry entry.key.requiresOwnershipTransfer(context) -> approval += entry
else -> main += entry else -> main += entry
} }
} }
Triple(main.toList(), approval.toList(), incompatible.toList()) listOf(self.toList(), main.toList(), approval.toList(), incompatible.toList())
} }
val (mainEntries, approvalEntries, incompatibleEntries) = groupedUpdates val (selfEntries, mainEntries, approvalEntries, incompatibleEntries) = groupedUpdates
val mainAnyActive = mainEntries.any { it.value.isActive() } val mainAnyActive = mainEntries.any { it.value.isActive() }
val approvalAnyActive = approvalEntries.any { it.value.isActive() } val approvalAnyActive = approvalEntries.any { it.value.isActive() }
@@ -110,6 +112,29 @@ fun UpdatesScreen(
dimensionResource(R.dimen.spacing_medium) dimensionResource(R.dimen.spacing_medium)
) )
) { ) {
if (selfEntries.isNotEmpty()) {
item(key = "header_self") {
SectionHeader(
title = stringResource(R.string.updates_self_header),
subtitle = stringResource(R.string.updates_self_desc)
)
}
items(
items = selfEntries,
key = { "self-${it.key.packageName}" }
) { (update, download) ->
AppUpdateItem(
update = update,
download = download,
// Served from the Aurora OSS feed, not Play — there is
// no app details page to open.
onClick = {},
onUpdate = { onRequestUpdate(update) },
onCancel = { onCancelUpdate(update.packageName) }
)
}
}
if (mainEntries.isNotEmpty()) { if (mainEntries.isNotEmpty()) {
item(key = "header_main") { item(key = "header_main") {
val title = "${mainEntries.size} " + stringResource( val title = "${mainEntries.size} " + stringResource(

View File

@@ -14,8 +14,10 @@ import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import com.aurora.extensions.TAG import com.aurora.extensions.TAG
import com.aurora.store.AuroraApp import com.aurora.store.AuroraApp
import com.aurora.store.BuildConfig
import com.aurora.store.data.event.BusEvent import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.model.BuildType
import com.aurora.store.data.model.UpdateMode import com.aurora.store.data.model.UpdateMode
import com.aurora.store.data.room.update.IgnoredUpdate import com.aurora.store.data.room.update.IgnoredUpdate
import com.aurora.store.data.room.update.IgnoredUpdateDao import com.aurora.store.data.room.update.IgnoredUpdateDao
@@ -185,6 +187,14 @@ class UpdateHelper @Inject constructor(
updateDao.deleteAll() updateDao.deleteAll()
} }
/**
* Immediately drops Aurora Store's own update row, e.g. when the user turns the
* self-update preference off. The periodic check won't re-add it while disabled.
*/
fun deleteSelfUpdate() {
AuroraApp.scope.launch { deleteUpdate(BuildConfig.APPLICATION_ID) }
}
/** /**
* Hide all future updates for [packageName] until [unignore] is called. * Hide all future updates for [packageName] until [unignore] is called.
*/ */
@@ -243,6 +253,13 @@ class UpdateHelper @Inject constructor(
private suspend fun deleteInvalidUpdates() { private suspend fun deleteInvalidUpdates() {
updateDao.updates().firstOrNull()?.forEach { update -> 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) {
return@forEach
}
if (!update.isInstalled(context) || update.isUpToDate(context)) { if (!update.isInstalled(context) || update.isUpToDate(context)) {
deleteUpdate(update.packageName) deleteUpdate(update.packageName)
} }

View File

@@ -1,20 +1,6 @@
/* /*
* Aurora Store * SPDX-FileCopyrightText: 2026 Aurora OSS
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com> * SPDX-License-Identifier: GPL-3.0-or-later
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
package com.aurora.store.data.model package com.aurora.store.data.model
@@ -24,62 +10,79 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Artwork import com.aurora.gplayapi.data.models.Artwork
import com.aurora.gplayapi.data.models.EncodedCertificateSet import com.aurora.gplayapi.data.models.EncodedCertificateSet
import com.aurora.gplayapi.data.models.PlayFile import com.aurora.gplayapi.data.models.PlayFile
import com.aurora.store.BuildConfig
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.util.CertUtil import com.aurora.store.util.CertUtil
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
/**
* Self-update feed entry returned by `release_feed.json` (vanilla release) and
* `nightly_feed.json` (nightly), both served from the Aurora OSS server.
*
* The producer encodes numeric fields as JSON strings, so the raw fields below stay
* `String` and we expose typed `Long` accessors that tolerate blank values. Decoding
* with `Long`s would otherwise blow up on the first request.
*/
@Serializable @Serializable
data class SelfUpdate( data class SelfUpdate(
@SerialName("version_name") var versionName: String = String(), @SerialName("version_name")
@SerialName("version_code") var versionCode: Long = 0, val versionName: String = "",
@SerialName("aurora_build") var auroraBuild: String = String(), @SerialName("version_code")
@SerialName("fdroid_build") var fdroidBuild: String = String(), val versionCodeRaw: String = "0",
@SerialName("updated_on") var updatedOn: String = String(), @SerialName("download_url")
val changelog: String = String(), val downloadUrl: String = "",
val size: Long = 0L, val changelog: String = "",
val timestamp: Long = 0L @SerialName("size")
val sizeRaw: String = "0",
@SerialName("last_commit")
val lastCommit: String = "",
@SerialName("updated_on")
val updatedOn: String = "",
@SerialName("timestamp")
val timestampRaw: String = "0"
) { ) {
companion object { val versionCode: Long get() = versionCodeRaw.toLongOrNull() ?: 0L
private const val BASE_URL = "https://gitlab.com/AuroraOSS/AuroraStore/-/raw/master" val size: Long get() = sizeRaw.toLongOrNull() ?: 0L
val timestamp: Long get() = timestampRaw.toLongOrNull() ?: 0L
fun toApp(selfUpdate: SelfUpdate, context: Context): App { /**
// Keep paths updated with fastlane data on project * Maps the feed entry onto a regular [App] so it can flow through the normal
val icon = "fastlane/metadata/android/en-US/images/icon.png" * update pipeline ([com.aurora.store.data.room.update.Update.fromApp] →
* download → install). The certificate set is the currently installed app's own
val downloadURL = if (CertUtil.isFDroidApp(context, BuildConfig.APPLICATION_ID)) { * hashes so [com.aurora.store.data.room.update.Update.hasValidCert] holds and the
selfUpdate.fdroidBuild * update is never filtered out as untrusted.
} else { */
selfUpdate.auroraBuild fun toApp(context: Context): App = App(
} packageName = context.packageName,
versionCode = versionCode,
return App( versionName = versionName,
packageName = context.packageName, changes = changelog,
versionCode = selfUpdate.versionCode, size = size,
versionName = selfUpdate.versionName, updatedOn = updatedOn,
changes = selfUpdate.changelog, displayName = context.getString(R.string.app_name),
size = selfUpdate.size, developerName = "Rahul Kumar Patel",
updatedOn = selfUpdate.updatedOn, iconArtwork = Artwork(url = ICON_URL),
displayName = context.getString(R.string.app_name), fileList = mutableListOf(
developerName = "Rahul Kumar Patel", PlayFile(
iconArtwork = Artwork(url = "$BASE_URL/$icon"), name = "${context.packageName}.apk",
fileList = mutableListOf( url = downloadUrl,
PlayFile( size = size
name = "${context.packageName}.apk",
url = downloadURL,
size = selfUpdate.size
)
),
isFree = true,
isInstalled = true,
certificateSetList = CertUtil.getEncodedCertificateHashes(
context,
context.packageName
).map {
EncodedCertificateSet(certificateSet = it, sha256 = String())
}.toMutableList()
) )
} ),
isFree = true,
isInstalled = true,
certificateSetList = CertUtil.getEncodedCertificateHashes(
context,
context.packageName
).map {
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

@@ -15,7 +15,6 @@ import com.aurora.extensions.isHyperOS
import com.aurora.extensions.isIgnoringBatteryOptimizations import com.aurora.extensions.isIgnoringBatteryOptimizations
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.gplayapi.network.IHttpClient
import com.aurora.store.BuildConfig import com.aurora.store.BuildConfig
import com.aurora.store.data.helper.DownloadHelper import com.aurora.store.data.helper.DownloadHelper
import com.aurora.store.data.helper.UpdateHelper import com.aurora.store.data.helper.UpdateHelper
@@ -23,6 +22,7 @@ import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.model.BuildType import com.aurora.store.data.model.BuildType
import com.aurora.store.data.model.SelfUpdate import com.aurora.store.data.model.SelfUpdate
import com.aurora.store.data.model.UpdateMode import com.aurora.store.data.model.UpdateMode
import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AccountProvider import com.aurora.store.data.providers.AccountProvider
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.data.providers.BlacklistProvider import com.aurora.store.data.providers.BlacklistProvider
@@ -32,6 +32,7 @@ import com.aurora.store.util.CertUtil
import com.aurora.store.util.NotificationUtil import com.aurora.store.util.NotificationUtil
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_SELF_UPDATE_ENABLED
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_AUTO import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_AUTO
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@@ -41,18 +42,22 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
/** /**
* A worker to check for updates for installed apps based on saved authentication data, * A worker that drives periodic app-update checks. The repeat interval is configurable
* filters and the auto-updates mode selected by the user. The repeat interval * by the user, defaulting to 3 hours with a flex time of 30 minutes.
* is configurable by the user, defaulting to 3 hours with a flex time of 30 minutes. *
* Aurora Store's own update is fetched from the bundled release/nightly feed and added
* to the regular update list (see [getSelfUpdate]); from there it reuses the standard
* download + install pipeline. It is never auto-installed silently — the user triggers
* it from the Updates tab.
* *
* Avoid using this worker directly and prefer using [UpdateHelper] instead. * Avoid using this worker directly and prefer using [UpdateHelper] instead.
* @see AuthWorker * @see AuthWorker
*/ */
@HiltWorker @HiltWorker
class UpdateWorker @AssistedInject constructor( class UpdateWorker @AssistedInject constructor(
private val httpClient: HttpClient,
private val json: Json, private val json: Json,
private val blacklistProvider: BlacklistProvider, private val blacklistProvider: BlacklistProvider,
private val httpClient: IHttpClient,
private val updateDao: UpdateDao, private val updateDao: UpdateDao,
private val downloadHelper: DownloadHelper, private val downloadHelper: DownloadHelper,
private val authProvider: AuthProvider, private val authProvider: AuthProvider,
@@ -61,11 +66,18 @@ class UpdateWorker @AssistedInject constructor(
@Assisted workerParams: WorkerParameters @Assisted workerParams: WorkerParameters
) : AuthWorker(authProvider, context, workerParams) { ) : AuthWorker(authProvider, context, workerParams) {
private val notificationID = 100 companion object {
private const val NOTIFICATION_ID = 100
}
private val canSelfUpdate = !CertUtil.isFDroidApp(context, BuildConfig.APPLICATION_ID) && /**
!CertUtil.isAppGalleryApp(context, BuildConfig.APPLICATION_ID) && * `true` when the build supports self-update ([PackageUtil.isSelfUpdateSupported])
BuildType.CURRENT != BuildType.DEBUG * and the user hasn't opted out via the Settings toggle. Read each check so flipping
* the preference takes effect on the next run.
*/
private val canSelfUpdate: Boolean
get() = PackageUtil.isSelfUpdateSupported(context) &&
Preferences.getBoolean(context, PREFERENCE_SELF_UPDATE_ENABLED, true)
private val isAuroraOnlyFilterEnabled: Boolean private val isAuroraOnlyFilterEnabled: Boolean
get() = Preferences.getBoolean(context, Preferences.PREFERENCE_FILTER_AURORA_ONLY, false) get() = Preferences.getBoolean(context, Preferences.PREFERENCE_FILTER_AURORA_ONLY, false)
@@ -124,7 +136,8 @@ class UpdateWorker @AssistedInject constructor(
return Result.success() return Result.success()
} }
// Clean the update list to prepare for installing // Clean the update list to prepare for installing. Aurora Store's own update
// is never installed silently — the user triggers it from the Updates tab.
val filteredUpdates = updates val filteredUpdates = updates
.filter { it.hasValidCert } .filter { it.hasValidCert }
.filterNot { it.isSelfUpdate(context) } .filterNot { it.isSelfUpdate(context) }
@@ -152,7 +165,7 @@ class UpdateWorker @AssistedInject constructor(
} }
override suspend fun getForegroundInfo(): ForegroundInfo = ForegroundInfo( override suspend fun getForegroundInfo(): ForegroundInfo = ForegroundInfo(
notificationID, NOTIFICATION_ID,
NotificationUtil.getUpdateNotification(context) NotificationUtil.getUpdateNotification(context)
) )
@@ -206,7 +219,18 @@ class UpdateWorker @AssistedInject constructor(
.filter { PackageUtil.isUpdatable(context, it.packageName, it.versionCode) } .filter { PackageUtil.isUpdatable(context, it.packageName, it.versionCode) }
.toMutableList() .toMutableList()
if (canSelfUpdate) getSelfUpdate()?.let { updates.add(it) } // Aurora Store's own update comes from the feed, not Play. When one is
// offered, add it; otherwise drop any stale self-update row. This is the
// cleanup path for the row (nightly self-updates are exempt from
// deleteInvalidUpdates, and the install event isn't delivered reliably when
// the app replaces itself), so a previously shown self-update doesn't linger
// after we've already updated to it.
val selfUpdate = if (canSelfUpdate) getSelfUpdate() else null
if (selfUpdate != null) {
updates.add(selfUpdate)
} else {
updateDao.delete(context.packageName)
}
return@withContext updates.map { return@withContext updates.map {
Update.fromApp( Update.fromApp(
@@ -219,58 +243,47 @@ class UpdateWorker @AssistedInject constructor(
} }
/** /**
* Checks and returns updates for Aurora Store if available * Fetches Aurora Store's own update from the bundled release/nightly feed and maps
* it onto an [App] so it joins the regular update list. Nightly version codes never
* bump, so newness is decided by the build timestamp there; release uses the version
* code. Best-effort: any failure logs and yields no update.
*/ */
private suspend fun getSelfUpdate(): App? { private suspend fun getSelfUpdate(): App? = withContext(Dispatchers.IO) {
return withContext(Dispatchers.IO) { val updateUrl = when (BuildType.CURRENT) {
val updateUrl = when (BuildType.CURRENT) { BuildType.RELEASE -> Constants.UPDATE_URL_VANILLA
BuildType.RELEASE -> Constants.UPDATE_URL_STABLE BuildType.NIGHTLY -> Constants.UPDATE_URL_NIGHTLY
else -> {
BuildType.NIGHTLY -> Constants.UPDATE_URL_NIGHTLY Log.i(TAG, "Self-updates are not available for this build!")
else -> {
Log.i(TAG, "Self-updates are not available for this build!")
return@withContext null
}
}
try {
val response = httpClient.get(updateUrl, mapOf())
val selfUpdate = json.decodeFromString<SelfUpdate>(String(response.responseBytes))
val isUpdate = when (BuildType.CURRENT) {
BuildType.NIGHTLY,
BuildType.RELEASE -> selfUpdate.versionCode > BuildConfig.VERSION_CODE
else -> false
}
if (isUpdate) {
if (CertUtil.isFDroidApp(context, BuildConfig.APPLICATION_ID)) {
if (selfUpdate.fdroidBuild.isNotEmpty()) {
return@withContext SelfUpdate.toApp(selfUpdate, context)
}
} else if (selfUpdate.auroraBuild.isNotEmpty()) {
return@withContext SelfUpdate.toApp(selfUpdate, context)
} else {
Log.e(TAG, "Update file is missing!")
return@withContext null
}
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to check self-updates", exception)
return@withContext null return@withContext null
} }
Log.i(TAG, "No self-updates found!")
return@withContext null
} }
try {
val selfUpdate = httpClient.call(updateUrl).use {
json.decodeFromString<SelfUpdate>(it.body.string())
}
val isNewer = when (BuildType.CURRENT) {
BuildType.RELEASE -> selfUpdate.versionCode > BuildConfig.VERSION_CODE
BuildType.NIGHTLY -> selfUpdate.timestamp > BuildConfig.BUILD_TIMESTAMP
else -> false
}
if (isNewer && selfUpdate.downloadUrl.isNotBlank()) {
return@withContext selfUpdate.toApp(context)
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to check self-updates", exception)
}
Log.i(TAG, "No self-updates found!")
return@withContext null
} }
private fun notifyUpdates(updates: List<Update>) { private fun notifyUpdates(updates: List<Update>) {
with(context.getSystemService<NotificationManager>()!!) { with(context.getSystemService<NotificationManager>()!!) {
notify( notify(
notificationID, NOTIFICATION_ID,
NotificationUtil.getUpdateNotification(context, updates) NotificationUtil.getUpdateNotification(context, updates)
) )
} }

View File

@@ -36,6 +36,8 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.pm.PackageInfoCompat import androidx.core.content.pm.PackageInfoCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.core.net.toUri import androidx.core.net.toUri
import com.aurora.Constants.FLAVOUR_PRELOAD
import com.aurora.Constants.FLAVOUR_VANILLA
import com.aurora.Constants.PACKAGE_NAME_APP_GALLERY import com.aurora.Constants.PACKAGE_NAME_APP_GALLERY
import com.aurora.Constants.PACKAGE_NAME_GMS import com.aurora.Constants.PACKAGE_NAME_GMS
import com.aurora.Constants.PACKAGE_NAME_PLAY_STORE import com.aurora.Constants.PACKAGE_NAME_PLAY_STORE
@@ -48,6 +50,7 @@ import com.aurora.extensions.isVAndAbove
import com.aurora.extensions.isValidApp import com.aurora.extensions.isValidApp
import com.aurora.store.BuildConfig import com.aurora.store.BuildConfig
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.model.BuildType
import java.util.Locale import java.util.Locale
object PackageUtil { object PackageUtil {
@@ -184,6 +187,19 @@ object PackageUtil {
} }
} }
/**
* Build-level eligibility for Aurora Store's self-update: vanilla / preload flavors
* only, never debug, and never an F-Droid-signed build. Huawei is excluded by the
* flavor check. The user-facing toggle gates this further at runtime.
*/
fun isSelfUpdateSupported(context: Context): Boolean {
val flavorEligible = BuildConfig.FLAVOR == FLAVOUR_VANILLA ||
BuildConfig.FLAVOR == FLAVOUR_PRELOAD
return flavorEligible &&
BuildType.CURRENT != BuildType.DEBUG &&
!CertUtil.isFDroidApp(context, BuildConfig.APPLICATION_ID)
}
/** /**
* Confirm if MicroG bundle is installed * Confirm if MicroG bundle is installed
* Considering the following: * Considering the following:

View File

@@ -74,6 +74,8 @@ object Preferences {
const val PREFERENCE_MIGRATION_VERSION = "PREFERENCE_MIGRATION_VERSION" const val PREFERENCE_MIGRATION_VERSION = "PREFERENCE_MIGRATION_VERSION"
const val PREFERENCE_SELF_UPDATE_ENABLED = "PREFERENCE_SELF_UPDATE_ENABLED"
private var prefs: SharedPreferences? = null private var prefs: SharedPreferences? = null
fun getPrefs(context: Context): SharedPreferences = when (BuildConfig.FLAVOR) { fun getPrefs(context: Context): SharedPreferences = when (BuildConfig.FLAVOR) {

View File

@@ -228,6 +228,8 @@
<string name="pref_ui_similar_apps">"Similar and related apps"</string> <string name="pref_ui_similar_apps">"Similar and related apps"</string>
<string name="pref_updates_incompatible">"Show updates that may fail"</string> <string name="pref_updates_incompatible">"Show updates that may fail"</string>
<string name="pref_updates_incompatible_desc">"Display updates for incompatible or disabled apps that may fail to install."</string> <string name="pref_updates_incompatible_desc">"Display updates for incompatible or disabled apps that may fail to install."</string>
<string name="pref_self_update">"Self-update"</string>
<string name="pref_self_update_desc">"Offer new Aurora Store builds in the Updates tab."</string>
<string name="pref_aurora_only">"Filter apps from other sources."</string> <string name="pref_aurora_only">"Filter apps from other sources."</string>
<string name="pref_aurora_only_desc">"Do not check for updates for apps installed from sources outside Aurora Store"</string> <string name="pref_aurora_only_desc">"Do not check for updates for apps installed from sources outside Aurora Store"</string>
<string name="pref_source_filters_title">"Filter apps from other sources"</string> <string name="pref_source_filters_title">"Filter apps from other sources"</string>
@@ -317,6 +319,8 @@
<string name="app_updater_service_notif_text">Enables background app download</string> <string name="app_updater_service_notif_text">Enables background app download</string>
<string name="update_available">update available</string> <string name="update_available">update available</string>
<string name="updates_available">updates available</string> <string name="updates_available">updates available</string>
<string name="updates_self_header">Self-update</string>
<string name="updates_self_desc">A newer build of Aurora Store is available</string>
<string name="updates_incompatible_header">Incompatible updates</string> <string name="updates_incompatible_header">Incompatible updates</string>
<string name="updates_incompatible_desc">These system app updates cannot be installed on this OS</string> <string name="updates_incompatible_desc">These system app updates cannot be installed on this OS</string>
<string name="updates_approval_header">Updates requiring approval</string> <string name="updates_approval_header">Updates requiring approval</string>