diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5e29853e5..c4e52a7a3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,6 +23,10 @@ val lastCommitHash = providers.exec { commandLine("git", "rev-parse", "--short", "HEAD") }.standardOutput.asText.map { it.trim() } +val lastCommitTimestamp = providers.exec { + commandLine("git", "log", "-1", "--format=%ct") +}.standardOutput.asText.map { it.trim() } + java { toolchain { languageVersion = JavaLanguageVersion.of(21) @@ -69,6 +73,7 @@ configure { testInstrumentationRunnerArguments["disableAnalytics"] = "true" buildConfigField("String", "EXODUS_API_KEY", "\"bbe6ebae4ad45a9cbacb17d69739799b8df2c7ae\"") + buildConfigField("long", "BUILD_TIMESTAMP", "${lastCommitTimestamp.get()}L") missingDimensionStrategy("device", "vanilla") } diff --git a/app/src/main/java/com/aurora/Constants.kt b/app/src/main/java/com/aurora/Constants.kt index 09ca67e34..29e9d0ce6 100644 --- a/app/src/main/java/com/aurora/Constants.kt +++ b/app/src/main/java/com/aurora/Constants.kt @@ -37,7 +37,8 @@ object Constants { 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 = "https://auroraoss.com/downloads/AuroraStore/Feeds/nightly_feed.json" diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/updates/UpdatesPreferenceScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/updates/UpdatesPreferenceScreen.kt index 9ff5b97cb..e99c3bceb 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/preferences/updates/UpdatesPreferenceScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/updates/UpdatesPreferenceScreen.kt @@ -55,6 +55,7 @@ import com.aurora.store.compose.navigation.Destination import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.compose.ui.preferences.network.SingleChoiceDialog 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.PREFERENCES_UPDATES_RESTRICTIONS_BATTERY 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_FDROID 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_CHECK_INTERVAL import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED @@ -79,6 +81,7 @@ fun UpdatesPreferenceScreen( onScheduleAutomatedCheck = { viewModel.updateHelper.scheduleAutomatedCheck() }, onUpdateAutomatedCheck = { viewModel.updateHelper.updateAutomatedCheck() }, onCheckUpdatesNow = { viewModel.updateHelper.checkUpdatesNow() }, + onDeleteSelfUpdate = { viewModel.updateHelper.deleteSelfUpdate() }, onNavigateTo = onNavigateTo ) } @@ -89,6 +92,7 @@ private fun ScreenContent( onScheduleAutomatedCheck: () -> Unit = {}, onUpdateAutomatedCheck: () -> Unit = {}, onCheckUpdatesNow: () -> Unit = {}, + onDeleteSelfUpdate: () -> Unit = {}, onNavigateTo: (Destination) -> Unit = {} ) { val context = LocalContext.current @@ -109,6 +113,10 @@ private fun ScreenContent( var updatesExtended by remember { 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. 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 + ) + } + ) + } + } } } } diff --git a/app/src/main/java/com/aurora/store/compose/ui/updates/UpdatesScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/updates/UpdatesScreen.kt index 50e4f67d8..e562b1ef2 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/updates/UpdatesScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/updates/UpdatesScreen.kt @@ -59,19 +59,21 @@ fun UpdatesScreen( } val groupedUpdates = remember(updateMap) { + val self = mutableListOf>() val main = mutableListOf>() val approval = mutableListOf>() val incompatible = mutableListOf>() updateMap?.entries?.forEach { entry -> when { + entry.key.isSelfUpdate(context) -> self += entry entry.key.isIncompatible -> incompatible += entry entry.key.requiresOwnershipTransfer(context) -> approval += 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 approvalAnyActive = approvalEntries.any { it.value.isActive() } @@ -110,6 +112,29 @@ fun UpdatesScreen( 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()) { item(key = "header_main") { val title = "${mainEntries.size} " + stringResource( 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 bb7f27db1..a4f92ed5b 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 @@ -14,8 +14,10 @@ import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import com.aurora.extensions.TAG import com.aurora.store.AuroraApp +import com.aurora.store.BuildConfig import com.aurora.store.data.event.BusEvent 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.room.update.IgnoredUpdate import com.aurora.store.data.room.update.IgnoredUpdateDao @@ -185,6 +187,14 @@ class UpdateHelper @Inject constructor( 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. */ @@ -243,6 +253,13 @@ 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) { + 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 37e4a4e2a..4a589214f 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 @@ -1,20 +1,6 @@ /* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * 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 . - * + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later */ 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.EncodedCertificateSet import com.aurora.gplayapi.data.models.PlayFile -import com.aurora.store.BuildConfig import com.aurora.store.R import com.aurora.store.util.CertUtil import kotlinx.serialization.SerialName 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 data class SelfUpdate( - @SerialName("version_name") var versionName: String = String(), - @SerialName("version_code") var versionCode: Long = 0, - @SerialName("aurora_build") var auroraBuild: String = String(), - @SerialName("fdroid_build") var fdroidBuild: String = String(), - @SerialName("updated_on") var updatedOn: String = String(), - val changelog: String = String(), - val size: Long = 0L, - val timestamp: Long = 0L + @SerialName("version_name") + val versionName: String = "", + @SerialName("version_code") + val versionCodeRaw: String = "0", + @SerialName("download_url") + val downloadUrl: String = "", + val changelog: String = "", + @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 { - private const val BASE_URL = "https://gitlab.com/AuroraOSS/AuroraStore/-/raw/master" + val versionCode: Long get() = versionCodeRaw.toLongOrNull() ?: 0L + 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 - val icon = "fastlane/metadata/android/en-US/images/icon.png" - - val downloadURL = if (CertUtil.isFDroidApp(context, BuildConfig.APPLICATION_ID)) { - selfUpdate.fdroidBuild - } else { - selfUpdate.auroraBuild - } - - return App( - packageName = context.packageName, - versionCode = selfUpdate.versionCode, - versionName = selfUpdate.versionName, - changes = selfUpdate.changelog, - size = selfUpdate.size, - updatedOn = selfUpdate.updatedOn, - displayName = context.getString(R.string.app_name), - developerName = "Rahul Kumar Patel", - iconArtwork = Artwork(url = "$BASE_URL/$icon"), - fileList = mutableListOf( - PlayFile( - 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() + /** + * Maps the feed entry onto a regular [App] so it can flow through the normal + * update pipeline ([com.aurora.store.data.room.update.Update.fromApp] → + * download → install). The certificate set is the currently installed app's own + * hashes so [com.aurora.store.data.room.update.Update.hasValidCert] holds and the + * update is never filtered out as untrusted. + */ + fun toApp(context: Context): App = App( + packageName = context.packageName, + versionCode = versionCode, + versionName = versionName, + changes = changelog, + size = size, + updatedOn = updatedOn, + displayName = context.getString(R.string.app_name), + developerName = "Rahul Kumar Patel", + iconArtwork = Artwork(url = ICON_URL), + fileList = mutableListOf( + PlayFile( + name = "${context.packageName}.apk", + url = downloadUrl, + size = size ) - } + ), + 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" } } diff --git a/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt b/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt index f32a14527..7275ff99f 100644 --- a/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt +++ b/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt @@ -15,7 +15,6 @@ import com.aurora.extensions.isHyperOS import com.aurora.extensions.isIgnoringBatteryOptimizations import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.helpers.AppDetailsHelper -import com.aurora.gplayapi.network.IHttpClient import com.aurora.store.BuildConfig import com.aurora.store.data.helper.DownloadHelper 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.SelfUpdate 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.AuthProvider 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.PackageUtil 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 dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -41,18 +42,22 @@ import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json /** - * A worker to check for updates for installed apps based on saved authentication data, - * filters and the auto-updates mode selected by the user. The repeat interval - * is configurable by the user, defaulting to 3 hours with a flex time of 30 minutes. + * A worker that drives periodic app-update checks. The repeat interval 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. * @see AuthWorker */ @HiltWorker class UpdateWorker @AssistedInject constructor( + private val httpClient: HttpClient, private val json: Json, private val blacklistProvider: BlacklistProvider, - private val httpClient: IHttpClient, private val updateDao: UpdateDao, private val downloadHelper: DownloadHelper, private val authProvider: AuthProvider, @@ -61,11 +66,18 @@ class UpdateWorker @AssistedInject constructor( @Assisted workerParams: WorkerParameters ) : 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) && - BuildType.CURRENT != BuildType.DEBUG + /** + * `true` when the build supports self-update ([PackageUtil.isSelfUpdateSupported]) + * 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 get() = Preferences.getBoolean(context, Preferences.PREFERENCE_FILTER_AURORA_ONLY, false) @@ -124,7 +136,8 @@ class UpdateWorker @AssistedInject constructor( 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 .filter { it.hasValidCert } .filterNot { it.isSelfUpdate(context) } @@ -152,7 +165,7 @@ class UpdateWorker @AssistedInject constructor( } override suspend fun getForegroundInfo(): ForegroundInfo = ForegroundInfo( - notificationID, + NOTIFICATION_ID, NotificationUtil.getUpdateNotification(context) ) @@ -206,7 +219,18 @@ class UpdateWorker @AssistedInject constructor( .filter { PackageUtil.isUpdatable(context, it.packageName, it.versionCode) } .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 { 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? { - return withContext(Dispatchers.IO) { - val updateUrl = when (BuildType.CURRENT) { - BuildType.RELEASE -> Constants.UPDATE_URL_STABLE - - BuildType.NIGHTLY -> Constants.UPDATE_URL_NIGHTLY - - 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(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) + private suspend fun getSelfUpdate(): App? = withContext(Dispatchers.IO) { + val updateUrl = when (BuildType.CURRENT) { + BuildType.RELEASE -> Constants.UPDATE_URL_VANILLA + BuildType.NIGHTLY -> Constants.UPDATE_URL_NIGHTLY + else -> { + Log.i(TAG, "Self-updates are not available for this build!") return@withContext null } - - Log.i(TAG, "No self-updates found!") - return@withContext null } + + try { + val selfUpdate = httpClient.call(updateUrl).use { + json.decodeFromString(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) { with(context.getSystemService()!!) { notify( - notificationID, + NOTIFICATION_ID, NotificationUtil.getUpdateNotification(context, updates) ) } diff --git a/app/src/main/java/com/aurora/store/util/PackageUtil.kt b/app/src/main/java/com/aurora/store/util/PackageUtil.kt index 75ad1b36c..19f7f7617 100644 --- a/app/src/main/java/com/aurora/store/util/PackageUtil.kt +++ b/app/src/main/java/com/aurora/store/util/PackageUtil.kt @@ -36,6 +36,8 @@ import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.pm.PackageInfoCompat import androidx.core.graphics.drawable.toBitmap 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_GMS 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.store.BuildConfig import com.aurora.store.R +import com.aurora.store.data.model.BuildType import java.util.Locale 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 * Considering the following: diff --git a/app/src/main/java/com/aurora/store/util/Preferences.kt b/app/src/main/java/com/aurora/store/util/Preferences.kt index 5af10d467..58b2c13e8 100644 --- a/app/src/main/java/com/aurora/store/util/Preferences.kt +++ b/app/src/main/java/com/aurora/store/util/Preferences.kt @@ -74,6 +74,8 @@ object Preferences { const val PREFERENCE_MIGRATION_VERSION = "PREFERENCE_MIGRATION_VERSION" + const val PREFERENCE_SELF_UPDATE_ENABLED = "PREFERENCE_SELF_UPDATE_ENABLED" + private var prefs: SharedPreferences? = null fun getPrefs(context: Context): SharedPreferences = when (BuildConfig.FLAVOR) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0c0f9cdae..a4e2f87ae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -228,6 +228,8 @@ "Similar and related apps" "Show updates that may fail" "Display updates for incompatible or disabled apps that may fail to install." + "Self-update" + "Offer new Aurora Store builds in the Updates tab." "Filter apps from other sources." "Do not check for updates for apps installed from sources outside Aurora Store" "Filter apps from other sources" @@ -317,6 +319,8 @@ Enables background app download update available updates available + Self-update + A newer build of Aurora Store is available Incompatible updates These system app updates cannot be installed on this OS Updates requiring approval