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:
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user