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

@@ -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"

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

View File

@@ -59,19 +59,21 @@ fun UpdatesScreen(
}
val groupedUpdates = remember(updateMap) {
val self = mutableListOf<Map.Entry<Update, Download?>>()
val main = mutableListOf<Map.Entry<Update, Download?>>()
val approval = mutableListOf<Map.Entry<Update, Download?>>()
val incompatible = mutableListOf<Map.Entry<Update, Download?>>()
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(

View File

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

View File

@@ -1,20 +1,6 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* 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/>.
*
* 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"
}
}

View File

@@ -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<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)
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<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>) {
with(context.getSystemService<NotificationManager>()!!) {
notify(
notificationID,
NOTIFICATION_ID,
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.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:

View File

@@ -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) {

View File

@@ -228,6 +228,8 @@
<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_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_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>
@@ -317,6 +319,8 @@
<string name="app_updater_service_notif_text">Enables background app download</string>
<string name="update_available">update 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_desc">These system app updates cannot be installed on this OS</string>
<string name="updates_approval_header">Updates requiring approval</string>