SelfUpdateWorker: Move self-updates logic into dedicated worker

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2023-12-21 13:16:51 +05:30
parent 59309783b1
commit a796c4c381
14 changed files with 209 additions and 333 deletions

View File

@@ -19,6 +19,8 @@
package com.aurora.extensions
import com.aurora.gplayapi.data.models.File
fun <T> MutableList<T>.flushAndAdd(list: List<T>) {
clear()
addAll(list)
@@ -28,3 +30,7 @@ fun <T> MutableSet<T>.flushAndAdd(list: Set<T>) {
clear()
addAll(list)
}
fun List<File>.requiresObbDir(): Boolean {
return this.any { it.type == File.FileType.OBB || it.type == File.FileType.PATCH }
}

View File

@@ -37,7 +37,6 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
@@ -60,16 +59,12 @@ import com.aurora.extensions.isMAndAbove
import com.aurora.extensions.isRAndAbove
import com.aurora.extensions.toast
import com.aurora.store.data.model.NetworkStatus
import com.aurora.store.data.model.SelfUpdate
import com.aurora.store.data.providers.NetworkProvider
import com.aurora.store.databinding.ActivityMainBinding
import com.aurora.store.util.Log
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB
import com.aurora.store.util.Preferences.PREFERENCE_SELF_UPDATE
import com.aurora.store.view.ui.sheets.NetworkDialogSheet
import com.aurora.store.view.ui.sheets.SelfUpdateSheet
import com.aurora.store.viewmodel.MainViewModel
import com.google.android.material.bottomnavigation.BottomNavigationView
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@@ -81,8 +76,6 @@ class MainActivity : AppCompatActivity() {
private lateinit var navController: NavController
private lateinit var appConfig: AppBarConfiguration
private val viewModel: MainViewModel by viewModels()
private val startForPermissions =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (!it) toast(R.string.permissions_denied)
@@ -154,20 +147,6 @@ class MainActivity : AppCompatActivity() {
}
}
/* Check self update only for stable release, skip debug & nightlies*/
if (Preferences.getBoolean(this, PREFERENCE_SELF_UPDATE)) {
if (BuildConfig.APPLICATION_ID == Constants.APP_ID) viewModel.checkSelfUpdate(this)
this.lifecycleScope.launch {
viewModel.selfUpdateAvailable.collect {
if (it != null) {
showUpdatesSheet(it)
} else {
Log.i("No Aurora Store update available")
}
}
}
}
// Handle quick exit from back actions
val defaultTab = when (Preferences.getInteger(this, PREFERENCE_DEFAULT_SELECTED_TAB)) {
1 -> R.id.gamesContainerFragment
@@ -296,14 +275,6 @@ class MainActivity : AppCompatActivity() {
}
}
private fun showUpdatesSheet(selfUpdate: SelfUpdate) {
if (!supportFragmentManager.isDestroyed) {
val sheet = SelfUpdateSheet.newInstance(selfUpdate)
sheet.isCancelable = false
sheet.show(supportFragmentManager, SelfUpdateSheet.TAG)
}
}
private fun isIntroDone(): Boolean {
return Preferences.getBoolean(this@MainActivity, Preferences.PREFERENCE_INTRO)
}

View File

@@ -19,21 +19,53 @@
package com.aurora.store.data.model
import android.content.Context
import com.aurora.Constants
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Artwork
import com.aurora.gplayapi.data.models.File
import com.aurora.store.BuildConfig
import com.aurora.store.R
import com.aurora.store.util.CertUtil
import com.google.gson.annotations.SerializedName
class SelfUpdate {
@SerializedName("version_name")
var versionName: String = String()
data class SelfUpdate(
@SerializedName("version_name") var versionName: String = String(),
@SerializedName("version_code") var versionCode: Int = 0,
@SerializedName("aurora_build") var auroraBuild: String = String(),
@SerializedName("fdroid_build") var fdroidBuild: String = String(),
val changelog: String = String(),
val size: Long = 0L
) {
companion object {
private const val BASE_URL = "https://gitlab.com/AuroraOSS/AuroraStore/-/raw/master"
@SerializedName("version_code")
var versionCode: Int = 0
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"
@SerializedName("aurora_build")
var auroraBuild: String = String()
val downloadURL = if (CertUtil.isFDroidApp(context, BuildConfig.APPLICATION_ID)) {
selfUpdate.fdroidBuild
} else {
selfUpdate.auroraBuild
}
@SerializedName("fdroid_build")
var fdroidBuild: String = String()
@SerializedName("changelog")
var changelog: String = String()
}
return App(
packageName = Constants.APP_ID,
versionCode = selfUpdate.versionCode,
versionName = selfUpdate.versionName,
changes = selfUpdate.changelog,
size = selfUpdate.size,
displayName = context.getString(R.string.app_name),
iconArtwork = Artwork(url = "$BASE_URL/$icon"),
fileList = mutableListOf(
File(
name = "${Constants.APP_ID}.apk",
url = downloadURL,
size = selfUpdate.size
)
)
)
}
}
}

View File

@@ -4,6 +4,7 @@ import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.File
import com.aurora.store.data.model.DownloadStatus
import kotlinx.parcelize.Parcelize
@@ -24,6 +25,7 @@ data class Download(
var timeRemaining: Long,
var totalFiles: Int,
var downloadedFiles: Int,
var fileList: List<File>,
val sharedLibs: List<SharedLib>
) : Parcelable {
val isFinished get() = status in DownloadStatus.finished
@@ -45,6 +47,7 @@ data class Download(
0L,
0,
0,
app.fileList.filterNot { it.url.isBlank() },
app.dependencies.dependentLibraries.map { SharedLib.fromApp(it) }
)
}

View File

@@ -1,6 +1,7 @@
package com.aurora.store.data.room.download
import androidx.room.TypeConverter
import com.aurora.gplayapi.data.models.File
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
@@ -16,4 +17,15 @@ object DownloadConverter {
fun fromSharedLibList(list: List<SharedLib>): String {
return Gson().toJson(list)
}
@TypeConverter
fun toGPlayFileList(string: String): List<File> {
val listType = object : TypeToken<List<File>>() {}.type
return Gson().fromJson(string, listType)
}
@TypeConverter
fun fromGPlayFileList(list: List<File>): String {
return Gson().toJson(list)
}
}

View File

@@ -2,16 +2,22 @@ package com.aurora.store.data.room.download
import android.os.Parcelable
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.File
import kotlinx.parcelize.Parcelize
@Parcelize
data class SharedLib(
val packageName: String,
val versionCode: Int
val versionCode: Int,
var fileList: List<File>
) : Parcelable {
companion object {
fun fromApp(app: App): SharedLib {
return SharedLib(app.packageName, app.versionCode)
return SharedLib(
app.packageName,
app.versionCode,
app.fileList.filterNot { it.url.isBlank() }
)
}
}
}

View File

@@ -13,6 +13,7 @@ import androidx.work.WorkerParameters
import com.aurora.Constants
import com.aurora.extensions.copyTo
import com.aurora.extensions.isQAndAbove
import com.aurora.extensions.requiresObbDir
import com.aurora.gplayapi.helpers.PurchaseHelper
import com.aurora.store.data.model.DownloadInfo
import com.aurora.store.data.model.DownloadStatus
@@ -80,9 +81,10 @@ class DownloadWorker @AssistedInject constructor(
appContext.getSystemService(Service.NOTIFICATION_SERVICE) as NotificationManager
// Bail out if file list is empty
val files =
download.fileList = download.fileList.ifEmpty {
purchaseHelper.purchase(download.packageName, download.versionCode, download.offerType)
if (files.isEmpty()) {
}
if (download.fileList.isEmpty()) {
Log.i(TAG, "Nothing to download!")
notifyStatus(DownloadStatus.FAILED)
return Result.failure()
@@ -90,7 +92,7 @@ class DownloadWorker @AssistedInject constructor(
// Create dirs & generate download request for files and shared libs (if any)
PathUtil.getAppDownloadDir(appContext, download.packageName, download.versionCode).mkdirs()
if (files.any { it.type == GPlayFile.FileType.OBB || it.type == GPlayFile.FileType.PATCH }) {
if (download.fileList.requiresObbDir()) {
PathUtil.getObbDownloadDir(download.packageName).mkdirs()
}
@@ -104,11 +106,13 @@ class DownloadWorker @AssistedInject constructor(
download.versionCode,
it.packageName
).mkdirs()
val libs = purchaseHelper.purchase(it.packageName, it.versionCode, 0)
requestList.addAll(getDownloadRequest(libs, it.packageName))
it.fileList = it.fileList.ifEmpty {
purchaseHelper.purchase(it.packageName, it.versionCode, 0)
}
requestList.addAll(getDownloadRequest(it.fileList, it.packageName))
}
}
requestList.addAll(getDownloadRequest(files, null))
requestList.addAll(getDownloadRequest(download.fileList, null))
// Update data for notification
download.totalFiles = requestList.size

View File

@@ -0,0 +1,101 @@
package com.aurora.store.data.work
import android.content.Context
import android.util.Log
import androidx.hilt.work.HiltWorker
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy.KEEP
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.aurora.Constants
import com.aurora.extensions.isMAndAbove
import com.aurora.store.BuildConfig
import com.aurora.store.data.model.SelfUpdate
import com.aurora.store.data.network.HttpClient
import com.aurora.store.util.CertUtil
import com.aurora.store.util.DownloadWorkerUtil
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_SELF_UPDATE
import com.google.gson.Gson
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import java.util.concurrent.TimeUnit.HOURS
import java.util.concurrent.TimeUnit.MINUTES
@HiltWorker
class SelfUpdateWorker @AssistedInject constructor(
private val downloadWorkerUtil: DownloadWorkerUtil,
private val gson: Gson,
@Assisted private val appContext: Context,
@Assisted workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
companion object {
private const val TAG = "SelfUpdateWorker"
private const val SELF_UPDATE_WORKER = "SELF_UPDATE_WORKER"
fun cancelAutomatedCheck(context: Context) {
Log.i(TAG, "Cancelling periodic self-updates!")
WorkManager.getInstance(context).cancelUniqueWork(SELF_UPDATE_WORKER)
}
fun scheduleAutomatedCheck(context: Context) {
Log.i(TAG, "Scheduling periodic self-updates!")
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresBatteryNotLow(true)
if (isMAndAbove()) constraints.setRequiresDeviceIdle(true)
val workRequest = PeriodicWorkRequestBuilder<UpdateWorker>(
repeatInterval = 24,
repeatIntervalTimeUnit = HOURS,
flexTimeInterval = 30,
flexTimeIntervalUnit = MINUTES
).setConstraints(constraints.build()).build()
val workManager = WorkManager.getInstance(context)
workManager.enqueueUniquePeriodicWork(SELF_UPDATE_WORKER, KEEP, workRequest)
}
}
private val TAG = SelfUpdateWorker::class.java.simpleName
private val RELEASE = "release"
override suspend fun doWork(): Result {
@Suppress("KotlinConstantConditions") // False-positive for build type always not being release
if (BuildConfig.BUILD_TYPE != RELEASE && Preferences.getBoolean(appContext, PREFERENCE_SELF_UPDATE)) {
Log.e(TAG, "Either build type is not release or self-update check is disabled")
return Result.failure()
}
try {
val response = HttpClient.getPreferredClient(appContext)
.get(Constants.UPDATE_URL, mapOf())
val selfUpdate = gson.fromJson(String(response.responseBytes), SelfUpdate::class.java)
if (selfUpdate.versionCode > BuildConfig.VERSION_CODE) {
if (CertUtil.isFDroidApp(appContext, BuildConfig.APPLICATION_ID)) {
if (selfUpdate.fdroidBuild.isNotEmpty()) {
downloadWorkerUtil.enqueueApp(SelfUpdate.toApp(selfUpdate, appContext))
}
} else if (selfUpdate.auroraBuild.isNotEmpty()) {
downloadWorkerUtil.enqueueApp(SelfUpdate.toApp(selfUpdate, appContext))
} else {
Log.e(TAG, "Update file is missing!")
return Result.failure()
}
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to check Aurora Store updates", exception)
return Result.failure()
}
Log.i(TAG, "No updates found")
return Result.success()
}
}

View File

@@ -24,6 +24,8 @@ import android.view.View
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.aurora.Constants
import com.aurora.extensions.browse
import com.aurora.store.MobileNavigationDirections
import com.aurora.store.R
import com.aurora.store.data.room.download.Download
@@ -48,6 +50,8 @@ class DownloadFragment : BaseFragment(R.layout.fragment_download) {
@Inject
lateinit var downloadWorkerUtil: DownloadWorkerUtil
private val AURORA_STORE_URL = "https://gitlab.com/AuroraOSS/AuroraStore"
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -101,7 +105,13 @@ class DownloadFragment : BaseFragment(R.layout.fragment_download) {
DownloadViewModel_()
.id(it.packageName)
.download(it)
.click { _ -> openDetailsFragment(it.packageName) }
.click { _ ->
if (it.packageName == Constants.APP_ID) {
requireContext().browse(AURORA_STORE_URL)
} else {
openDetailsFragment(it.packageName)
}
}
.longClick { _ ->
openDownloadMenuSheet(it)
true

View File

@@ -30,6 +30,7 @@ import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.aurora.extensions.isSAndAbove
import com.aurora.store.BuildConfig
import com.aurora.store.R
import com.aurora.store.data.work.SelfUpdateWorker
import com.aurora.store.data.work.UpdateWorker
import com.aurora.store.databinding.FragmentOnboardingBinding
import com.aurora.store.util.CertUtil
@@ -63,6 +64,7 @@ import com.aurora.store.util.Preferences.PREFERENCE_VENDING_VERSION
import com.aurora.store.util.save
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
import kotlin.properties.Delegates
@AndroidEntryPoint
class OnboardingFragment : Fragment(R.layout.fragment_onboarding) {
@@ -70,6 +72,7 @@ class OnboardingFragment : Fragment(R.layout.fragment_onboarding) {
private var _binding: FragmentOnboardingBinding? = null
private val binding get() = _binding!!
private var shouldSelfUpdate by Delegates.notNull<Boolean>()
private var lastPosition = 0
internal class PagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
@@ -95,6 +98,8 @@ class OnboardingFragment : Fragment(R.layout.fragment_onboarding) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentOnboardingBinding.bind(view)
shouldSelfUpdate = CertUtil.isFDroidApp(requireContext(), BuildConfig.APPLICATION_ID)
val isDefaultPrefLoaded = Preferences.getBoolean(requireContext(), PREFERENCE_DEFAULT)
if (!isDefaultPrefLoaded) {
save(PREFERENCE_DEFAULT, true)
@@ -144,6 +149,7 @@ class OnboardingFragment : Fragment(R.layout.fragment_onboarding) {
binding.btnForward.setOnClickListener {
save(PREFERENCE_INTRO, true)
UpdateWorker.scheduleAutomatedCheck(requireContext())
if (shouldSelfUpdate) SelfUpdateWorker.scheduleAutomatedCheck(requireContext())
findNavController().navigate(
OnboardingFragmentDirections.actionOnboardingFragmentToSplashFragment()
)
@@ -192,9 +198,6 @@ class OnboardingFragment : Fragment(R.layout.fragment_onboarding) {
save(PREFERENCE_UPDATES_EXTENDED, false)
save(PREFERENCE_UPDATES_AUTO, 2)
save(PREFERENCE_UPDATES_CHECK_INTERVAL, 3)
save(
PREFERENCE_SELF_UPDATE,
CertUtil.isFDroidApp(requireContext(), BuildConfig.APPLICATION_ID)
)
save(PREFERENCE_SELF_UPDATE, shouldSelfUpdate)
}
}

View File

@@ -30,6 +30,7 @@ import androidx.preference.SeekBarPreference
import androidx.preference.SwitchPreferenceCompat
import com.aurora.store.BuildConfig
import com.aurora.store.R
import com.aurora.store.data.work.SelfUpdateWorker
import com.aurora.store.data.work.UpdateWorker
import com.aurora.store.util.CertUtil
import com.aurora.store.util.Preferences
@@ -55,6 +56,11 @@ class UpdatesPreference : PreferenceFragmentCompat() {
}
it.setOnPreferenceChangeListener { _, newValue ->
if (newValue.toString().toBoolean()) {
SelfUpdateWorker.scheduleAutomatedCheck(requireContext())
} else {
SelfUpdateWorker.cancelAutomatedCheck(requireContext())
}
save(PREFERENCE_SELF_UPDATE, newValue.toString().toBoolean())
true
}

View File

@@ -1,103 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2019, 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/>.
*
*
*/
package com.aurora.store.view.ui.sheets
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.aurora.Constants
import com.aurora.store.R
import com.aurora.store.data.model.SelfUpdate
import com.aurora.store.data.service.SelfUpdateService
import com.aurora.store.databinding.SheetSelfUpdateBinding
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class SelfUpdateSheet : BaseBottomSheet() {
private lateinit var B: SheetSelfUpdateBinding
private lateinit var selfUpdate: SelfUpdate
companion object {
const val TAG = "ManualDownloadSheet"
@JvmStatic
fun newInstance(
selfUpdate: SelfUpdate
): SelfUpdateSheet {
return SelfUpdateSheet().apply {
arguments = Bundle().apply {
putString(Constants.STRING_EXTRA, gson.toJson(selfUpdate))
}
}
}
}
override fun onCreateContentView(
inflater: LayoutInflater,
container: ViewGroup,
savedInstanceState: Bundle?
): View {
B = SheetSelfUpdateBinding.inflate(inflater)
return B.root
}
override fun onContentViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val bundle = arguments
bundle?.let {
val rawUpdate = bundle.getString(Constants.STRING_EXTRA, "{}")
selfUpdate = gson.fromJson(rawUpdate, SelfUpdate::class.java)
if (selfUpdate.versionName.isNotEmpty()) {
inflateData()
attachActions()
} else {
dismissAllowingStateLoss()
}
}
}
private fun inflateData() {
B.txtLine2.text = ("${selfUpdate.versionName} (${selfUpdate.versionCode})")
val messages: String = if (selfUpdate.changelog.isEmpty())
getString(R.string.details_changelog_unavailable)
else
selfUpdate.changelog
B.txtChangelog.text = messages.trim()
}
private fun attachActions() {
B.btnPrimary.setOnClickListener {
val intent = Intent(requireContext(), SelfUpdateService::class.java)
intent.putExtra(Constants.STRING_EXTRA, gson.toJson(selfUpdate))
requireContext().startService(intent)
dismissAllowingStateLoss()
}
B.btnSecondary.setOnClickListener {
dismissAllowingStateLoss()
}
}
}

View File

@@ -1,56 +0,0 @@
package com.aurora.store.viewmodel
import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.Constants
import com.aurora.store.BuildConfig
import com.aurora.store.data.model.SelfUpdate
import com.aurora.store.data.network.HttpClient
import com.aurora.store.util.CertUtil
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import java.lang.reflect.Modifier
class MainViewModel : ViewModel() {
private val TAG = MainViewModel::class.java.simpleName
private val _selfUpdateAvailable = MutableSharedFlow<SelfUpdate?>()
val selfUpdateAvailable = _selfUpdateAvailable.asSharedFlow()
fun checkSelfUpdate(context: Context) {
Log.i(TAG, "Checking for Aurora Store updates")
viewModelScope.launch(Dispatchers.IO) {
try {
val gson: Gson =
GsonBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT).create()
val response = HttpClient.getPreferredClient(context).get(Constants.UPDATE_URL, mapOf())
val selfUpdate =
gson.fromJson(String(response.responseBytes), SelfUpdate::class.java)
if (selfUpdate.versionCode > BuildConfig.VERSION_CODE) {
if (CertUtil.isFDroidApp(context, BuildConfig.APPLICATION_ID)) {
if (selfUpdate.fdroidBuild.isNotEmpty()) {
_selfUpdateAvailable.emit(selfUpdate)
}
} else if (selfUpdate.auroraBuild.isNotEmpty()) {
_selfUpdateAvailable.emit(selfUpdate)
} else {
_selfUpdateAvailable.emit(null)
}
} else {
_selfUpdateAvailable.emit(null)
}
} catch (exception: Exception) {
_selfUpdateAvailable.emit(null)
Log.d(TAG, "Failed to check Aurora Store updates", exception)
}
}
}
}