Rework updates check & install setup

Merge updates checking logic into UpdateWorker and move trigger
methods to UpdateHelper class, similar to how DownloadHelper works.

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2024-10-19 16:16:00 +05:30
parent 2e98e33847
commit 0eb58a4e9e
16 changed files with 348 additions and 277 deletions

View File

@@ -23,8 +23,6 @@ object Constants {
const val PARCEL_DOWNLOAD = "PARCEL_DOWNLOAD" const val PARCEL_DOWNLOAD = "PARCEL_DOWNLOAD"
const val APP_ID = "com.aurora.store"
const val URL_TOS = "https://play.google.com/about/play-terms/" const val URL_TOS = "https://play.google.com/about/play-terms/"
const val URL_LICENSE = "https://gitlab.com/AuroraOSS/AuroraStore/blob/master/LICENSE" const val URL_LICENSE = "https://gitlab.com/AuroraOSS/AuroraStore/blob/master/LICENSE"
const val URL_DISCLAIMER = "https://gitlab.com/AuroraOSS/AuroraStore/blob/master/DISCLAIMER.md" const val URL_DISCLAIMER = "https://gitlab.com/AuroraOSS/AuroraStore/blob/master/DISCLAIMER.md"

View File

@@ -34,8 +34,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.FloatingWindow import androidx.navigation.FloatingWindow
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController
import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.model.NetworkStatus import com.aurora.store.data.model.NetworkStatus
import com.aurora.store.data.receiver.MigrationReceiver import com.aurora.store.data.receiver.MigrationReceiver
import com.aurora.store.databinding.ActivityMainBinding import com.aurora.store.databinding.ActivityMainBinding
@@ -149,22 +147,6 @@ class MainActivity : AppCompatActivity() {
} }
// Updates // Updates
lifecycleScope.launch {
AuroraApp.events.installerEvent.collect {
when (it) {
is InstallerEvent.Installed -> updateHelper.deleteUpdate(it.packageName)
is InstallerEvent.Uninstalled -> updateHelper.deleteUpdate(it.packageName)
else -> {}
}
}
}
lifecycleScope.launch {
AuroraApp.events.busEvent.collect {
if (it is BusEvent.Blacklisted) updateHelper.deleteUpdate(it.packageName)
}
}
lifecycleScope.launch { lifecycleScope.launch {
updateHelper.updates.collectLatest { list -> updateHelper.updates.collectLatest { list ->
B.navView.getOrCreateBadge(R.id.updatesFragment).apply { B.navView.getOrCreateBadge(R.id.updatesFragment).apply {

View File

@@ -1,156 +1,168 @@
package com.aurora.store.data.helper package com.aurora.store.data.helper
import android.content.Context import android.content.Context
import android.content.pm.PackageInfo
import android.util.Log import android.util.Log
import androidx.core.content.pm.PackageInfoCompat import androidx.work.Constraints
import com.aurora.Constants import androidx.work.Data
import com.aurora.gplayapi.data.models.App import androidx.work.ExistingPeriodicWorkPolicy
import com.aurora.gplayapi.helpers.AppDetailsHelper import androidx.work.ExistingWorkPolicy
import com.aurora.gplayapi.network.IHttpClient import androidx.work.NetworkType
import androidx.work.OutOfQuotaPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import com.aurora.extensions.isMAndAbove
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.model.SelfUpdate import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.model.UpdateMode
import com.aurora.store.data.providers.BlacklistProvider
import com.aurora.store.data.room.update.Update
import com.aurora.store.data.room.update.UpdateDao import com.aurora.store.data.room.update.UpdateDao
import com.aurora.store.util.CertUtil import com.aurora.store.data.work.UpdateWorker
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences
import com.google.gson.Gson import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_CHECK_INTERVAL
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import javax.inject.Inject
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit.HOURS
import java.util.concurrent.TimeUnit.MINUTES
import javax.inject.Inject
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
/**
* Helper class to work with the [UpdateWorker].
*/
class UpdateHelper @Inject constructor( class UpdateHelper @Inject constructor(
private val gson: Gson,
private val authProvider: AuthProvider,
private val updateDao: UpdateDao, private val updateDao: UpdateDao,
private val blacklistProvider: BlacklistProvider,
private val httpClient: IHttpClient,
@ApplicationContext private val context: Context @ApplicationContext private val context: Context
) { ) {
companion object {
const val UPDATE_MODE = "UPDATE_MODE"
private const val UPDATE_WORKER = "UPDATE_WORKER"
private const val EXPEDITED_UPDATE_WORKER = "EXPEDITED_UPDATE_WORKER"
}
private val TAG = UpdateHelper::class.java.simpleName private val TAG = UpdateHelper::class.java.simpleName
private val RELEASE = "release" private val isExtendedUpdateEnabled
private val NIGHTLY = "nightly" get() = Preferences.getBoolean(context, Preferences.PREFERENCE_UPDATES_EXTENDED)
private val isExtendedUpdateEnabled get() = Preferences.getBoolean(
context, Preferences.PREFERENCE_UPDATES_EXTENDED
)
val updates = updateDao.updates() val updates = updateDao.updates()
.map { list -> if (!isExtendedUpdateEnabled) list.filter { it.hasValidCert } else list } .map { list -> if (!isExtendedUpdateEnabled) list.filter { it.hasValidCert } else list }
.stateIn(AuroraApp.scope, SharingStarted.WhileSubscribed(), null) .stateIn(AuroraApp.scope, SharingStarted.WhileSubscribed(), null)
init { init {
AuroraApp.scope.launch { AuroraApp.scope.launch {
updateDao.updates().firstOrNull()?.forEach { update -> deleteInvalidUpdates()
if (!update.isInstalled(context) || update.isUpToDate(context)) { }.invokeOnCompletion {
deleteUpdate(update.packageName) observeUpdates()
} }
}
private fun observeUpdates() {
AuroraApp.events.installerEvent.onEach {
when (it) {
is InstallerEvent.Installed -> deleteUpdate(it.packageName)
is InstallerEvent.Uninstalled -> deleteUpdate(it.packageName)
else -> {}
} }
} }.launchIn(AuroraApp.scope)
AuroraApp.events.busEvent.onEach {
if (it is BusEvent.Blacklisted) deleteUpdate(it.packageName)
}.launchIn(AuroraApp.scope)
} }
suspend fun checkUpdates(): List<Update> { /**
Log.i(TAG, "Checking for updates") * Checks for updates using an expedited worker
val packageInfoMap = PackageUtil.getPackageInfoMap(context) */
val appUpdatesList = getFilteredInstalledApps(packageInfoMap) fun checkUpdatesNow() {
.filter { val inputData = Data.Builder()
val packageInfo = packageInfoMap[it.packageName] .putInt(UPDATE_MODE, UpdateMode.CHECK_ONLY.ordinal)
if (packageInfo != null) { .build()
it.versionCode.toLong() > PackageInfoCompat.getLongVersionCode(packageInfo)
} else {
false
}
}.toMutableList()
if (canSelfUpdate(context)) { val work = OneTimeWorkRequestBuilder<UpdateWorker>()
getSelfUpdate(context, gson)?.let { appUpdatesList.add(it) } .addTag(EXPEDITED_UPDATE_WORKER)
} .setExpedited(OutOfQuotaPolicy.DROP_WORK_REQUEST)
.setInputData(inputData)
.build()
return appUpdatesList.map { Update.fromApp(context, it) }.also { WorkManager.getInstance(context)
// Cache the updates into the database .enqueueUniqueWork(EXPEDITED_UPDATE_WORKER, ExistingWorkPolicy.KEEP, work)
updateDao.insertUpdates(it)
}
} }
suspend fun deleteUpdate(packageName: String) { /**
* Delete update for a package from the database
* @param packageName Name of the package
*/
private suspend fun deleteUpdate(packageName: String) {
updateDao.delete(packageName) updateDao.delete(packageName)
} }
private suspend fun getFilteredInstalledApps( /**
packageInfoMap: MutableMap<String, PackageInfo>? = null * Cancels the automated updates check
): List<App> { * @see [UpdateWorker]
return withContext(Dispatchers.IO) { */
val appDetailsHelper = AppDetailsHelper(authProvider.authData!!) fun cancelAutomatedCheck() {
.using(httpClient) Log.i(TAG, "Cancelling periodic app updates!")
WorkManager.getInstance(context).cancelUniqueWork(UPDATE_WORKER)
(packageInfoMap ?: PackageUtil.getPackageInfoMap(context)).keys.let { packages ->
val filtersPackages = packages.filter { !blacklistProvider.isBlacklisted(it) }
appDetailsHelper.getAppByPackageName(filtersPackages)
.filter { it.displayName.isNotEmpty() }
.map { it.isInstalled = true; it }
}
}
} }
private fun canSelfUpdate(context: Context): Boolean { /**
return !CertUtil.isFDroidApp(context, BuildConfig.APPLICATION_ID) && * Schedules the automated updates check
!CertUtil.isAppGalleryApp(context, BuildConfig.APPLICATION_ID) * @see [UpdateWorker]
*/
fun scheduleAutomatedCheck() {
Log.i(TAG,"Scheduling periodic app updates!")
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
UPDATE_WORKER,
ExistingPeriodicWorkPolicy.KEEP,
getAutoUpdateWork()
)
} }
private suspend fun getSelfUpdate(context: Context, gson: Gson): App? { /**
return withContext(Dispatchers.IO) { * Updates the automated updates check to reconsider the new user preferences
@Suppress("KotlinConstantConditions") // False-positive for build type always not being release * @see [UpdateWorker]
val updateUrl = when (BuildConfig.BUILD_TYPE) { */
RELEASE -> Constants.UPDATE_URL_STABLE fun updateAutomatedCheck() {
NIGHTLY -> Constants.UPDATE_URL_NIGHTLY Log.i(TAG,"Updating periodic app updates!")
else -> { WorkManager.getInstance(context).updateWork(getAutoUpdateWork())
Log.i(TAG, "Self-updates are not available for this build!") }
return@withContext null
} private fun getAutoUpdateWork(): PeriodicWorkRequest {
val updateCheckInterval = Preferences.getInteger(
context,
PREFERENCE_UPDATES_CHECK_INTERVAL,
3
).toLong()
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresBatteryNotLow(true)
if (isMAndAbove()) constraints.setRequiresDeviceIdle(true)
return PeriodicWorkRequestBuilder<UpdateWorker>(
repeatInterval = updateCheckInterval,
repeatIntervalTimeUnit = HOURS,
flexTimeInterval = 30,
flexTimeIntervalUnit = MINUTES
).setConstraints(constraints.build()).build()
}
private suspend fun deleteInvalidUpdates() {
updateDao.updates().firstOrNull()?.forEach { update ->
if (!update.isInstalled(context) || update.isUpToDate(context)) {
deleteUpdate(update.packageName)
} }
try {
val response = httpClient.get(updateUrl, mapOf())
val selfUpdate =
gson.fromJson(String(response.responseBytes), SelfUpdate::class.java)
val isUpdate = when (BuildConfig.BUILD_TYPE) {
RELEASE -> selfUpdate.versionCode > BuildConfig.VERSION_CODE
NIGHTLY -> selfUpdate.timestamp > BuildConfig.TIMESTAMP
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
}
Log.i(TAG, "No self-updates found!")
return@withContext null
} }
} }
} }

View File

@@ -0,0 +1,7 @@
package com.aurora.store.data.model
enum class BuildType {
RELEASE,
NIGHTLY,
DEBUG
}

View File

@@ -53,7 +53,7 @@ data class SelfUpdate(
} }
return App( return App(
packageName = Constants.APP_ID, packageName = context.packageName,
versionCode = selfUpdate.versionCode, versionCode = selfUpdate.versionCode,
versionName = selfUpdate.versionName, versionName = selfUpdate.versionName,
changes = selfUpdate.changelog, changes = selfUpdate.changelog,
@@ -64,7 +64,7 @@ data class SelfUpdate(
iconArtwork = Artwork(url = "$BASE_URL/$icon"), iconArtwork = Artwork(url = "$BASE_URL/$icon"),
fileList = mutableListOf( fileList = mutableListOf(
File( File(
name = "${Constants.APP_ID}.apk", name = "${context.packageName}.apk",
url = downloadURL, url = downloadURL,
size = selfUpdate.size size = selfUpdate.size
) )

View File

@@ -0,0 +1,7 @@
package com.aurora.store.data.model
enum class UpdateMode {
DISABLED,
CHECK_ONLY,
CHECK_AND_INSTALL
}

View File

@@ -59,8 +59,8 @@ data class Update(
} }
} }
fun isSelfUpdate(): Boolean { fun isSelfUpdate(context: Context): Boolean {
return packageName == Constants.APP_ID return packageName == context.packageName
} }
fun isInstalled(context: Context): Boolean { fun isInstalled(context: Context): Boolean {

View File

@@ -4,179 +4,220 @@ import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.content.pm.PackageInfoCompat
import androidx.hilt.work.HiltWorker import androidx.hilt.work.HiltWorker
import androidx.work.Constraints
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy.KEEP import androidx.work.ForegroundInfo
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.aurora.Constants
import com.aurora.extensions.isIgnoringBatteryOptimizations import com.aurora.extensions.isIgnoringBatteryOptimizations
import com.aurora.extensions.isMAndAbove 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.installer.AppInstaller import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.data.helper.UpdateHelper
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.model.BuildType
import com.aurora.store.data.model.SelfUpdate
import com.aurora.store.data.model.UpdateMode
import com.aurora.store.data.providers.AccountProvider
import com.aurora.store.data.providers.BlacklistProvider
import com.aurora.store.data.room.update.Update
import com.aurora.store.data.room.update.UpdateDao
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.Preferences import com.aurora.store.util.Preferences
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.google.gson.Gson
import com.aurora.store.util.save
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.concurrent.TimeUnit.HOURS
import java.util.concurrent.TimeUnit.MINUTES
/** /**
* A periodic worker to check for updates for installed apps based on * A worker to check for updates for installed apps based on
* filters and the auto-updates mode selected by the user. The repeat interval * 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. * is configurable by the user, defaulting to 3 hours with a flex time of 30 minutes.
*
* Avoid using this worker directly and prefer using [UpdateHelper] instead.
*/ */
@HiltWorker @HiltWorker
class UpdateWorker @AssistedInject constructor( class UpdateWorker @AssistedInject constructor(
private val updateHelper: UpdateHelper, private val gson: Gson,
private val blacklistProvider: BlacklistProvider,
private val httpClient: IHttpClient,
private val updateDao: UpdateDao,
private val downloadHelper: DownloadHelper, private val downloadHelper: DownloadHelper,
private val authProvider: AuthProvider, private val authProvider: AuthProvider,
@Assisted private val appContext: Context, @Assisted private val appContext: Context,
@Assisted workerParams: WorkerParameters @Assisted workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) { ) : CoroutineWorker(appContext, workerParams) {
companion object { private val TAG = UpdateWorker::class.java.simpleName
private const val TAG = "UpdateWorker"
private const val UPDATE_WORKER = "UPDATE_WORKER"
/**
* Cancels the automated updates check
* @param context Current [Context]
* @see [UpdateWorker]
*/
fun cancelAutomatedCheck(context: Context) {
Log.i(TAG, "Cancelling periodic app updates!")
WorkManager.getInstance(context).cancelUniqueWork(UPDATE_WORKER)
}
/**
* Schedules the automated updates check
* @param context Current [Context]
* @see [UpdateWorker]
*/
fun scheduleAutomatedCheck(context: Context) {
Log.i(TAG,"Scheduling periodic app updates!")
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(UPDATE_WORKER, KEEP, buildUpdateWork(context))
}
/**
* Updates the automated updates check to reconsider the new user preferences
* @param context Current [Context]
* @see [UpdateWorker]
*/
fun updateAutomatedCheck(context: Context) {
Log.i(TAG,"Updating periodic app updates!")
WorkManager.getInstance(context).updateWork(buildUpdateWork(context))
}
private fun buildUpdateWork(context: Context): PeriodicWorkRequest {
val updateCheckInterval = Preferences.getInteger(
context,
PREFERENCE_UPDATES_CHECK_INTERVAL,
3
).toLong()
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresBatteryNotLow(true)
if (isMAndAbove()) constraints.setRequiresDeviceIdle(true)
return PeriodicWorkRequestBuilder<UpdateWorker>(
repeatInterval = updateCheckInterval,
repeatIntervalTimeUnit = HOURS,
flexTimeInterval = 30,
flexTimeIntervalUnit = MINUTES
).setConstraints(constraints.build()).build()
}
}
private val notificationID = 100 private val notificationID = 100
@Suppress("KotlinConstantConditions")
private val buildType = when (BuildConfig.BUILD_TYPE) {
"release" -> BuildType.RELEASE
"nightly" -> BuildType.NIGHTLY
else -> BuildType.DEBUG
}
private val canSelfUpdate = !CertUtil.isFDroidApp(appContext, BuildConfig.APPLICATION_ID) &&
!CertUtil.isAppGalleryApp(appContext, BuildConfig.APPLICATION_ID) &&
buildType != BuildType.DEBUG
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
Log.i(TAG, "Checking for app updates") Log.i(TAG, "Checking for app updates")
val updateMode = UpdateMode.entries[inputData.getInt(
UpdateHelper.UPDATE_MODE,
Preferences.getInteger(appContext, PREFERENCE_UPDATES_AUTO, 3)
)]
val autoUpdatesMode = Preferences.getInteger(appContext, PREFERENCE_UPDATES_AUTO, 3) if (updateMode == UpdateMode.DISABLED || !AccountProvider.isLoggedIn(appContext)) {
val notificationManager = appContext.getSystemService<NotificationManager>() Log.i(TAG, "Updates are disabled, bailing out!")
// Exit if auto-updates is turned off in settings
if (autoUpdatesMode == 0) {
Log.i(TAG, "Auto-updates is disabled, bailing out!")
return Result.failure() return Result.failure()
} }
withContext(Dispatchers.IO) { if (!authProvider.isSavedAuthDataValid()) {
if (!authProvider.isSavedAuthDataValid()) { Log.i(TAG, "AuthData is not valid, retrying later!")
Log.i(TAG, "AuthData is not valid, retrying later!") return Result.retry()
return@withContext Result.retry() }
try {
val updates = checkUpdates()
if (updates.isEmpty()) {
Log.i(TAG, "No updates found!")
return Result.success()
}
// Notify and exit if we are only checking for updates or if battery optimizations are enabled
if (updateMode == UpdateMode.CHECK_ONLY || !appContext.isIgnoringBatteryOptimizations()) {
Log.i(TAG, "Found updates, notifying!")
notifyUpdates(updates)
return Result.success()
}
// Clean the update list to prepare for installing
val filteredUpdates = updates
.filter { it.hasValidCert }
.filterNot { it.isSelfUpdate(appContext) }
.partition {
AppInstaller.canInstallSilently(appContext, it.packageName, it.targetSdk)
}
// Notify about apps that cannot be auto-updated
notifyUpdates(filteredUpdates.second)
// Trigger download for apps if they can be auto-updated
filteredUpdates.first.forEach { downloadHelper.enqueueUpdate(it) }
return Result.success()
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch updates", exception)
return Result.failure()
}
}
override suspend fun getForegroundInfo(): ForegroundInfo {
return ForegroundInfo(
notificationID,
NotificationUtil.getUpdateNotification(appContext)
)
}
/**
* Checks and returns updates for all possible apps
*/
private suspend fun checkUpdates(): List<Update> {
return withContext(Dispatchers.IO) {
val packageInfoMap = PackageUtil.getPackageInfoMap(appContext)
val appDetailsHelper = AppDetailsHelper(authProvider.authData!!)
.using(httpClient)
val updates = packageInfoMap.keys.let { packages ->
val filteredPackages = packages.filter { !blacklistProvider.isBlacklisted(it) }
appDetailsHelper.getAppByPackageName(filteredPackages)
.filter { it.displayName.isNotEmpty() }
.map { it.isInstalled = true; it }
}.filter {
val packageInfo = packageInfoMap[it.packageName]
if (packageInfo != null) {
it.versionCode.toLong() > PackageInfoCompat.getLongVersionCode(packageInfo)
} else {
false
}
}.toMutableList()
if (canSelfUpdate) getSelfUpdate()?.let { updates.add(it) }
return@withContext updates.map { Update.fromApp(appContext, it) }.also {
// Cache the updates into the database
updateDao.insertUpdates(it)
}
}
}
/**
* Checks and returns updates for Aurora Store if available
*/
private suspend fun getSelfUpdate(): App? {
return withContext(Dispatchers.IO) {
val updateUrl = when (buildType) {
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 { try {
val updatesList = updateHelper.checkUpdates() val response = httpClient.get(updateUrl, mapOf())
.filter { it.hasValidCert } val selfUpdate = gson.fromJson(
.filterNot { it.isSelfUpdate() } String(response.responseBytes),
SelfUpdate::class.java
)
if (updatesList.isNotEmpty()) { val isUpdate = when (buildType) {
if (autoUpdatesMode == 1) { BuildType.RELEASE -> selfUpdate.versionCode > BuildConfig.VERSION_CODE
Log.i(TAG, "Found updates, notifying!") BuildType.NIGHTLY -> selfUpdate.timestamp > BuildConfig.TIMESTAMP
notificationManager!!.notify( else -> false
notificationID, }
NotificationUtil.getUpdateNotification(appContext, updatesList)
) if (isUpdate) {
} else { if (CertUtil.isFDroidApp(appContext, BuildConfig.APPLICATION_ID)) {
if (appContext.isIgnoringBatteryOptimizations()) { if (selfUpdate.fdroidBuild.isNotEmpty()) {
// Trigger download for apps if they can be auto-updated (if any) return@withContext SelfUpdate.toApp(selfUpdate, appContext)
updatesList.filter { }
AppInstaller.canInstallSilently(appContext, it.packageName, it.targetSdk) } else if (selfUpdate.auroraBuild.isNotEmpty()) {
}.let { list -> return@withContext SelfUpdate.toApp(selfUpdate, appContext)
if (list.isEmpty()) return@let } else {
Log.e(TAG, "Update file is missing!")
Log.i(TAG, "Found auto-update enabled apps, updating!") return@withContext null
list.forEach { downloadHelper.enqueueUpdate(it) } }
}
// Notify about remaining apps (if any)
updatesList.filterNot {
AppInstaller.canInstallSilently(appContext, it.packageName, it.targetSdk)
}.let { list ->
if (list.isEmpty()) return@let
Log.i(TAG, "Found apps that cannot be auto-updated, notifying!")
notificationManager!!.notify(
notificationID,
NotificationUtil.getUpdateNotification(appContext, list)
)
}
} else {
// Fallback to notification if battery optimizations are enabled
Log.i(TAG, "Found updates, but battery optimizations are enabled!")
notificationManager!!.notify(
notificationID,
NotificationUtil.getUpdateNotification(appContext, updatesList)
)
appContext.save(PREFERENCE_UPDATES_AUTO, 1)
}
}
} else {
Log.i(TAG, "No updates found!")
} }
return@withContext Result.success()
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, "Failed to fetch updates", exception) Log.e(TAG, "Failed to check self-updates", exception)
return@withContext Result.failure() return@withContext null
} }
Log.i(TAG, "No self-updates found!")
return@withContext null
}
}
private fun notifyUpdates(updates: List<Update>) {
with(appContext.getSystemService<NotificationManager>()!!) {
notify(
notificationID,
NotificationUtil.getUpdateNotification(appContext, updates)
)
} }
return Result.success()
} }
} }

View File

@@ -190,6 +190,14 @@ object NotificationUtil {
.build() .build()
} }
fun getUpdateNotification(context: Context): Notification {
return NotificationCompat.Builder(context, Constants.NOTIFICATION_CHANNEL_UPDATES)
.setSmallIcon(R.drawable.ic_updates)
.setContentTitle(context.getString(R.string.checking_updates))
.setOngoing(true)
.build()
}
fun getUpdateNotification(context: Context, updatesList: List<Update>): Notification { fun getUpdateNotification(context: Context, updatesList: List<Update>): Notification {
val contentIntent = NavDeepLinkBuilder(context) val contentIntent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.mobile_navigation) .setGraph(R.navigation.mobile_navigation)
@@ -261,6 +269,7 @@ object NotificationUtil {
.setSmallIcon(R.drawable.ic_file_copy) .setSmallIcon(R.drawable.ic_file_copy)
.setContentTitle(context.getString(R.string.export_app_title)) .setContentTitle(context.getString(R.string.export_app_title))
.setContentText(context.getString(R.string.export_app_summary)) .setContentText(context.getString(R.string.export_app_summary))
.setOngoing(true)
.build() .build()
} }

View File

@@ -105,7 +105,7 @@ class DownloadFragment : BaseFragment<FragmentDownloadBinding>() {
.id(it.packageName) .id(it.packageName)
.download(it) .download(it)
.click { _ -> .click { _ ->
if (it.packageName == Constants.APP_ID) { if (it.packageName == requireContext().packageName) {
requireContext().browse(GITLAB_URL) requireContext().browse(GITLAB_URL)
} else { } else {
openDetailsFragment(it.packageName) openDetailsFragment(it.packageName)

View File

@@ -33,8 +33,8 @@ import com.aurora.Constants
import com.aurora.extensions.areNotificationsEnabled import com.aurora.extensions.areNotificationsEnabled
import com.aurora.extensions.isIgnoringBatteryOptimizations import com.aurora.extensions.isIgnoringBatteryOptimizations
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.helper.UpdateHelper
import com.aurora.store.data.work.CacheWorker import com.aurora.store.data.work.CacheWorker
import com.aurora.store.data.work.UpdateWorker
import com.aurora.store.databinding.FragmentOnboardingBinding import com.aurora.store.databinding.FragmentOnboardingBinding
import com.aurora.store.util.CertUtil import com.aurora.store.util.CertUtil
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
@@ -59,10 +59,14 @@ import com.aurora.store.util.save
import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.view.ui.commons.BaseFragment
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class OnboardingFragment : BaseFragment<FragmentOnboardingBinding>() { class OnboardingFragment : BaseFragment<FragmentOnboardingBinding>() {
@Inject
lateinit var updateHelper: UpdateHelper
private var lastPosition = 0 private var lastPosition = 0
internal class PagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : internal class PagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
@@ -190,6 +194,6 @@ class OnboardingFragment : BaseFragment<FragmentOnboardingBinding>() {
else -> 0 // Disable else -> 0 // Disable
} }
save(PREFERENCE_UPDATES_AUTO, updateMode) save(PREFERENCE_UPDATES_AUTO, updateMode)
UpdateWorker.scheduleAutomatedCheck(requireContext()) updateHelper.scheduleAutomatedCheck()
} }
} }

View File

@@ -28,14 +28,18 @@ import androidx.preference.SeekBarPreference
import com.aurora.extensions.isIgnoringBatteryOptimizations import com.aurora.extensions.isIgnoringBatteryOptimizations
import com.aurora.store.MobileNavigationDirections import com.aurora.store.MobileNavigationDirections
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.work.UpdateWorker import com.aurora.store.data.helper.UpdateHelper
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 dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class UpdatesPreference : BasePreferenceFragment() { class UpdatesPreference : BasePreferenceFragment() {
@Inject
lateinit var updateHelper: UpdateHelper
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences_updates, rootKey) setPreferencesFromResource(R.xml.preferences_updates, rootKey)
@@ -43,11 +47,11 @@ class UpdatesPreference : BasePreferenceFragment() {
?.setOnPreferenceChangeListener { _, newValue -> ?.setOnPreferenceChangeListener { _, newValue ->
val value = newValue.toString().toInt() val value = newValue.toString().toInt()
when (value) { when (value) {
0 -> UpdateWorker.cancelAutomatedCheck(requireContext()) 0 -> updateHelper.cancelAutomatedCheck()
1 -> UpdateWorker.scheduleAutomatedCheck(requireContext()) 1 -> updateHelper.scheduleAutomatedCheck()
else -> { else -> {
if (requireContext().isIgnoringBatteryOptimizations()) { if (requireContext().isIgnoringBatteryOptimizations()) {
UpdateWorker.scheduleAutomatedCheck(requireContext()) updateHelper.scheduleAutomatedCheck()
return@setOnPreferenceChangeListener true return@setOnPreferenceChangeListener true
} else { } else {
findNavController().navigate( findNavController().navigate(
@@ -61,7 +65,7 @@ class UpdatesPreference : BasePreferenceFragment() {
findPreference<SeekBarPreference>(PREFERENCE_UPDATES_CHECK_INTERVAL) findPreference<SeekBarPreference>(PREFERENCE_UPDATES_CHECK_INTERVAL)
?.setOnPreferenceChangeListener { _, _ -> ?.setOnPreferenceChangeListener { _, _ ->
UpdateWorker.updateAutomatedCheck(requireContext()) updateHelper.updateAutomatedCheck()
true true
} }
} }

View File

@@ -7,14 +7,20 @@ import com.aurora.extensions.isIgnoringBatteryOptimizations
import com.aurora.extensions.toast import com.aurora.extensions.toast
import com.aurora.store.PermissionType import com.aurora.store.PermissionType
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.helper.UpdateHelper
import com.aurora.store.data.providers.PermissionProvider import com.aurora.store.data.providers.PermissionProvider
import com.aurora.store.data.work.UpdateWorker
import com.aurora.store.databinding.SheetDozeWarningBinding import com.aurora.store.databinding.SheetDozeWarningBinding
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_AUTO import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_AUTO
import com.aurora.store.util.save import com.aurora.store.util.save
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class DozeWarningSheet : BaseDialogSheet<SheetDozeWarningBinding>() { class DozeWarningSheet : BaseDialogSheet<SheetDozeWarningBinding>() {
@Inject
lateinit var updateHelper: UpdateHelper
private val args: DozeWarningSheetArgs by navArgs() private val args: DozeWarningSheetArgs by navArgs()
private lateinit var permissionProvider: PermissionProvider private lateinit var permissionProvider: PermissionProvider
@@ -26,7 +32,7 @@ class DozeWarningSheet : BaseDialogSheet<SheetDozeWarningBinding>() {
if (requireContext().isIgnoringBatteryOptimizations()) { if (requireContext().isIgnoringBatteryOptimizations()) {
if (args.enableAutoUpdate) { if (args.enableAutoUpdate) {
requireContext().save(PREFERENCE_UPDATES_AUTO, 2) requireContext().save(PREFERENCE_UPDATES_AUTO, 2)
UpdateWorker.scheduleAutomatedCheck(requireContext()) updateHelper.scheduleAutomatedCheck()
} }
toast(R.string.toast_permission_granted) toast(R.string.toast_permission_granted)
activity?.recreate() activity?.recreate()

View File

@@ -169,7 +169,7 @@ class UpdatesFragment : BaseFragment<FragmentUpdatesBinding>() {
.update(update) .update(update)
.download(download) .download(download)
.click { _ -> .click { _ ->
if (update.packageName == Constants.APP_ID) { if (update.packageName == requireContext().packageName) {
requireContext().browse(Constants.GITLAB_URL) requireContext().browse(Constants.GITLAB_URL)
} else { } else {
openDetailsFragment(update.packageName) openDetailsFragment(update.packageName)

View File

@@ -51,7 +51,7 @@ class UpdatesViewModel @Inject constructor(
_fetchingUpdates.value = true _fetchingUpdates.value = true
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
updateHelper.checkUpdates() updateHelper.checkUpdatesNow()
} catch (exception: Exception) { } catch (exception: Exception) {
Log.d(TAG, "Failed to get updates", exception) Log.d(TAG, "Failed to get updates", exception)
} }

View File

@@ -454,6 +454,7 @@
<!-- UpdatesFragment --> <!-- UpdatesFragment -->
<string name="check_updates">Check for updates</string> <string name="check_updates">Check for updates</string>
<string name="checking_updates">Checking for updates</string>
<!-- ForceRestartDialog --> <!-- ForceRestartDialog -->
<string name="force_restart_title">Restart Aurora Store</string> <string name="force_restart_title">Restart Aurora Store</string>