Initial support for caching updates

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2024-07-16 16:31:18 +07:00
parent bec1bea05a
commit 8b341c088c
18 changed files with 312 additions and 157 deletions

View File

@@ -33,19 +33,26 @@ import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController
import com.aurora.extensions.accentColor import com.aurora.extensions.accentColor
import com.aurora.extensions.applyThemeAccent import com.aurora.extensions.applyThemeAccent
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.providers.NetworkProvider import com.aurora.store.data.providers.NetworkProvider
import com.aurora.store.databinding.ActivityMainBinding import com.aurora.store.databinding.ActivityMainBinding
import com.aurora.store.util.AppUtil
import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB
import com.aurora.store.view.ui.sheets.NetworkDialogSheet import com.aurora.store.view.ui.sheets.NetworkDialogSheet
import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationBarView
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@Inject
lateinit var appUtil: AppUtil
private lateinit var B: ActivityMainBinding private lateinit var B: ActivityMainBinding
private lateinit var navController: NavController private lateinit var navController: NavController
@@ -129,6 +136,26 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
// Updates
lifecycleScope.launch {
AuroraApp.events.installerEvent.collectLatest {
when (it) {
is InstallerEvent.Installed -> appUtil.deleteUpdate(it.packageName)
is InstallerEvent.Uninstalled -> appUtil.deleteUpdate(it.packageName)
else -> {}
}
}
}
lifecycleScope.launch {
appUtil.updates.collectLatest { list ->
(B.navView as NavigationBarView).getOrCreateBadge(R.id.updatesFragment).apply {
isVisible = !list.isNullOrEmpty()
number = list?.size ?: 0
}
}
}
} }
private fun isIntroDone(): Boolean { private fun isIntroDone(): Boolean {

View File

@@ -8,10 +8,17 @@ import com.aurora.store.data.room.download.DownloadConverter
import com.aurora.store.data.room.download.DownloadDao import com.aurora.store.data.room.download.DownloadDao
import com.aurora.store.data.room.favourites.Favourite import com.aurora.store.data.room.favourites.Favourite
import com.aurora.store.data.room.favourites.FavouriteDao import com.aurora.store.data.room.favourites.FavouriteDao
import com.aurora.store.data.room.update.Update
import com.aurora.store.data.room.update.UpdateDao
@Database(entities = [Download::class, Favourite::class], version = 2, exportSchema = false) @Database(
entities = [Download::class, Favourite::class, Update::class],
version = 3,
exportSchema = false
)
@TypeConverters(DownloadConverter::class) @TypeConverters(DownloadConverter::class)
abstract class AuroraDatabase : RoomDatabase() { abstract class AuroraDatabase : RoomDatabase() {
abstract fun downloadDao(): DownloadDao abstract fun downloadDao(): DownloadDao
abstract fun favouriteDao(): FavouriteDao abstract fun favouriteDao(): FavouriteDao
abstract fun updateDao(): UpdateDao
} }

View File

@@ -5,6 +5,7 @@ import androidx.room.Room
import com.aurora.store.data.room.download.DownloadConverter import com.aurora.store.data.room.download.DownloadConverter
import com.aurora.store.data.room.download.DownloadDao import com.aurora.store.data.room.download.DownloadDao
import com.aurora.store.data.room.favourites.FavouriteDao import com.aurora.store.data.room.favourites.FavouriteDao
import com.aurora.store.data.room.update.UpdateDao
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@@ -39,4 +40,9 @@ object RoomModule {
fun providesFavouriteDao(auroraDatabase: AuroraDatabase): FavouriteDao { fun providesFavouriteDao(auroraDatabase: AuroraDatabase): FavouriteDao {
return auroraDatabase.favouriteDao() return auroraDatabase.favouriteDao()
} }
@Provides
fun providesUpdateDao(auroraDatabase: AuroraDatabase): UpdateDao {
return auroraDatabase.updateDao()
}
} }

View File

@@ -6,6 +6,7 @@ import androidx.room.PrimaryKey
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.File import com.aurora.gplayapi.data.models.File
import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.room.update.Update
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@@ -52,5 +53,26 @@ data class Download(
app.dependencies.dependentLibraries.map { SharedLib.fromApp(it) } app.dependencies.dependentLibraries.map { SharedLib.fromApp(it) }
) )
} }
fun fromUpdate(update: Update): Download {
return Download(
update.packageName,
update.versionCode,
update.offerType,
true,
update.displayName,
update.iconURL,
update.size,
update.id,
DownloadStatus.QUEUED,
0,
0L,
0L,
0,
0,
update.fileList,
update.sharedLibs
)
}
} }
} }

View File

@@ -0,0 +1,67 @@
package com.aurora.store.data.room.update
import android.content.Context
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.aurora.Constants
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.File
import com.aurora.store.data.room.download.SharedLib
import com.aurora.store.util.CertUtil
import com.aurora.store.util.PackageUtil
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(tableName = "update")
data class Update(
@PrimaryKey
val packageName: String,
val versionCode: Int,
val versionName: String,
val displayName: String,
val iconURL: String,
val changelog: String,
val id: Int,
val developerName: String,
val size: Long,
val updatedOn: String,
val hasValidCert: Boolean,
val offerType: Int,
var fileList: List<File>,
val sharedLibs: List<SharedLib>
) : Parcelable {
companion object {
fun fromApp(context: Context, app: App): Update {
return Update(
app.packageName,
app.versionCode,
app.versionName,
app.displayName,
app.iconArtwork.url,
app.changes,
app.id,
app.developerName,
app.size,
app.updatedOn,
app.certificateSetList.any {
it.certificateSet in CertUtil.getEncodedCertificateHashes(
context, app.packageName
)
},
app.offerType,
app.fileList.filterNot { it.url.isBlank() },
app.dependencies.dependentLibraries.map { SharedLib.fromApp(it) }
)
}
}
fun isSelfUpdate(): Boolean {
return packageName == Constants.APP_ID
}
fun isInstalled(context: Context): Boolean {
return PackageUtil.isInstalled(context, packageName)
}
}

View File

@@ -0,0 +1,30 @@
package com.aurora.store.data.room.update
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow
@Dao
interface UpdateDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(updates: List<Update>)
@Query("SELECT * FROM `update` ORDER BY displayName ASC")
fun updates(): Flow<List<Update>>
@Query("DELETE FROM `update` WHERE packageName = :packageName")
suspend fun delete(packageName: String)
@Query("DELETE FROM `update`")
suspend fun deleteAll()
@Transaction
suspend fun insertUpdates(updates: List<Update>) {
deleteAll()
insertAll(updates)
}
}

View File

@@ -27,7 +27,6 @@ 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.aurora.store.util.Preferences.PREFERENCE_UPDATES_CHECK_INTERVAL
import com.aurora.store.util.save import com.aurora.store.util.save
import com.google.gson.Gson
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -42,8 +41,8 @@ import java.util.concurrent.TimeUnit.MINUTES
*/ */
@HiltWorker @HiltWorker
class UpdateWorker @AssistedInject constructor( class UpdateWorker @AssistedInject constructor(
private val appUtil: AppUtil,
private val downloadWorkerUtil: DownloadWorkerUtil, private val downloadWorkerUtil: DownloadWorkerUtil,
private val gson: Gson,
private val authProvider: AuthProvider, private val authProvider: AuthProvider,
@Assisted private val appContext: Context, @Assisted private val appContext: Context,
@Assisted workerParams: WorkerParameters @Assisted workerParams: WorkerParameters
@@ -128,13 +127,7 @@ class UpdateWorker @AssistedInject constructor(
} }
try { try {
val updatesList = AppUtil.getUpdatableApps( val updatesList = appUtil.checkUpdates().filterNot { it.isSelfUpdate() }
context = appContext,
authData = authProvider.authData!!,
gson = gson,
verifyCert = true,
selfUpdate = false
)
if (updatesList.isNotEmpty()) { if (updatesList.isNotEmpty()) {
if (autoUpdatesMode == 1) { if (autoUpdatesMode == 1) {
@@ -150,7 +143,7 @@ class UpdateWorker @AssistedInject constructor(
if (list.isEmpty()) return@let if (list.isEmpty()) return@let
Log.i(TAG, "Found auto-update enabled apps, updating!") Log.i(TAG, "Found auto-update enabled apps, updating!")
list.forEach { downloadWorkerUtil.enqueueApp(it) } list.forEach { downloadWorkerUtil.enqueueUpdate(it) }
} }
// Notify about remaining apps (if any) // Notify about remaining apps (if any)

View File

@@ -6,30 +6,47 @@ import android.util.Log
import androidx.core.content.pm.PackageInfoCompat import androidx.core.content.pm.PackageInfoCompat
import com.aurora.Constants import com.aurora.Constants
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.store.AuroraApp
import com.aurora.store.BuildConfig import com.aurora.store.BuildConfig
import com.aurora.store.data.model.SelfUpdate import com.aurora.store.data.model.SelfUpdate
import com.aurora.store.data.network.HttpClient import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.data.providers.BlacklistProvider 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.google.gson.Gson import com.google.gson.Gson
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject
object AppUtil { class AppUtil @Inject constructor(
private val gson: Gson,
private val authProvider: AuthProvider,
private val updateDao: UpdateDao,
@ApplicationContext private val context: Context
) {
private const val TAG = "AppUtil" private val TAG = AppUtil::class.java.simpleName
private const val RELEASE = "release" private val RELEASE = "release"
suspend fun getUpdatableApps( private val isExtendedUpdateEnabled get() = Preferences.getBoolean(
context: Context, context, Preferences.PREFERENCE_UPDATES_EXTENDED
authData: AuthData, )
gson: Gson, val updates = updateDao.updates()
verifyCert: Boolean, .map { list -> list.filter { it.isInstalled(context) } }
selfUpdate: Boolean = true .map { list -> if (!isExtendedUpdateEnabled) list.filter { it.hasValidCert } else list }
): List<App> { .stateIn(AuroraApp.scope, SharingStarted.WhileSubscribed(), null)
suspend fun checkUpdates(): List<Update> {
Log.i(TAG, "Checking for updates")
val packageInfoMap = PackageUtil.getPackageInfoMap(context) val packageInfoMap = PackageUtil.getPackageInfoMap(context)
val appUpdatesList = getFilteredInstalledApps(context, authData, packageInfoMap).filter { val appUpdatesList = getFilteredInstalledApps(packageInfoMap).filter {
val packageInfo = packageInfoMap[it.packageName] val packageInfo = packageInfoMap[it.packageName]
if (packageInfo != null) { if (packageInfo != null) {
it.versionCode.toLong() > PackageInfoCompat.getLongVersionCode(packageInfo) it.versionCode.toLong() > PackageInfoCompat.getLongVersionCode(packageInfo)
@@ -38,33 +55,26 @@ object AppUtil {
} }
}.toMutableList() }.toMutableList()
val verifiedUpdatesList = if (verifyCert) { if (canSelfUpdate(context)) {
appUpdatesList.filter { app -> getSelfUpdate(context, gson)?.let { appUpdatesList.add(it) }
app.certificateSetList.any {
it.certificateSet in CertUtil.getEncodedCertificateHashes(
context, app.packageName
)
}
}.toMutableList()
} else {
appUpdatesList
} }
if (canSelfUpdate(context) && selfUpdate) { return appUpdatesList.map { Update.fromApp(context, it) }.also {
getSelfUpdate(context, gson)?.let { verifiedUpdatesList.add(it) } // Cache the updates into the database
updateDao.insertUpdates(it)
} }
}
return verifiedUpdatesList suspend fun deleteUpdate(packageName: String) {
updateDao.delete(packageName)
} }
suspend fun getFilteredInstalledApps( suspend fun getFilteredInstalledApps(
context: Context,
authData: AuthData,
packageInfoMap: MutableMap<String, PackageInfo>? = null packageInfoMap: MutableMap<String, PackageInfo>? = null
): List<App> { ): List<App> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val blackList = BlacklistProvider.with(context).getBlackList() val blackList = BlacklistProvider.with(context).getBlackList()
val appDetailsHelper = AppDetailsHelper(authData) val appDetailsHelper = AppDetailsHelper(authProvider.authData!!)
.using(HttpClient.getPreferredClient(context)) .using(HttpClient.getPreferredClient(context))
(packageInfoMap ?: PackageUtil.getPackageInfoMap(context)).keys.let { packages -> (packageInfoMap ?: PackageUtil.getPackageInfoMap(context)).keys.let { packages ->

View File

@@ -12,6 +12,7 @@ import com.aurora.store.AuroraApp
import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.room.download.Download import com.aurora.store.data.room.download.Download
import com.aurora.store.data.room.download.DownloadDao import com.aurora.store.data.room.download.DownloadDao
import com.aurora.store.data.room.update.Update
import com.aurora.store.data.work.DownloadWorker import com.aurora.store.data.work.DownloadWorker
import com.google.gson.Gson import com.google.gson.Gson
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
@@ -86,6 +87,14 @@ class DownloadWorkerUtil @Inject constructor(
downloadDao.insert(Download.fromApp(app)) downloadDao.insert(Download.fromApp(app))
} }
/**
* Enqueues an update for download & install
* @param update [Update] to download
*/
suspend fun enqueueUpdate(update: Update) {
downloadDao.insert(Download.fromUpdate(update))
}
/** /**
* Cancels the download for the given package * Cancels the download for the given package
* @param packageName Name of the package to cancel download * @param packageName Name of the package to cancel download

View File

@@ -25,6 +25,7 @@ import com.aurora.store.R
import com.aurora.store.data.activity.InstallActivity import com.aurora.store.data.activity.InstallActivity
import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.room.download.Download import com.aurora.store.data.room.download.Download
import com.aurora.store.data.room.update.Update
import java.util.UUID import java.util.UUID
import com.aurora.store.data.room.download.Download as AuroraDownload import com.aurora.store.data.room.download.Download as AuroraDownload
@@ -185,7 +186,7 @@ object NotificationUtil {
.build() .build()
} }
fun getUpdateNotification(context: Context, updatesList: List<App>): 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)
.setDestination(R.id.updatesFragment) .setDestination(R.id.updatesFragment)

View File

@@ -41,6 +41,7 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.room.download.Download import com.aurora.store.data.room.download.Download
import com.aurora.store.data.room.update.Update
import com.aurora.store.databinding.ViewAppUpdateBinding import com.aurora.store.databinding.ViewAppUpdateBinding
import com.aurora.store.util.CommonUtil import com.aurora.store.util.CommonUtil
import com.aurora.store.view.epoxy.views.BaseModel import com.aurora.store.view.epoxy.views.BaseModel
@@ -59,11 +60,11 @@ class AppUpdateView @JvmOverloads constructor(
private val cornersTransformation = RoundedCornersTransformation(8.px.toFloat()) private val cornersTransformation = RoundedCornersTransformation(8.px.toFloat())
@ModelProp @ModelProp
fun app(app: App) { fun update(update: Update) {
/*Inflate App details*/ /*Inflate App details*/
with(app) { with(update) {
binding.txtLine1.text = displayName binding.txtLine1.text = displayName
binding.imgIcon.load(iconArtwork.url) { binding.imgIcon.load(iconURL) {
placeholder(R.drawable.bg_placeholder) placeholder(R.drawable.bg_placeholder)
transformations(cornersTransformation) transformations(cornersTransformation)
listener { _, result -> listener { _, result ->
@@ -74,9 +75,9 @@ class AppUpdateView @JvmOverloads constructor(
binding.txtLine2.text = developerName binding.txtLine2.text = developerName
binding.txtLine3.text = ("${CommonUtil.addSiPrefix(size)}$updatedOn") binding.txtLine3.text = ("${CommonUtil.addSiPrefix(size)}$updatedOn")
binding.txtLine4.text = ("$versionName (${versionCode})") binding.txtLine4.text = ("$versionName (${versionCode})")
binding.txtChangelog.text = if (changes.isNotEmpty()) binding.txtChangelog.text = if (changelog.isNotEmpty())
HtmlCompat.fromHtml( HtmlCompat.fromHtml(
changes, changelog,
HtmlCompat.FROM_HTML_OPTION_USE_CSS_COLORS HtmlCompat.FROM_HTML_OPTION_USE_CSS_COLORS
) )
else else

View File

@@ -22,13 +22,14 @@ package com.aurora.store.view.epoxy.views.app
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView import com.airbnb.epoxy.ModelView
import com.aurora.store.databinding.ViewNoAppBinding import com.aurora.store.databinding.ViewNoAppBinding
import com.aurora.store.view.epoxy.views.BaseModel import com.aurora.store.view.epoxy.views.BaseModel
import com.aurora.store.view.epoxy.views.BaseView import com.aurora.store.view.epoxy.views.BaseView
@ModelView( @ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_MATCH_HEIGHT, autoLayout = ModelView.Size.MATCH_WIDTH_MATCH_HEIGHT,
baseModelClass = BaseModel::class baseModelClass = BaseModel::class
@@ -50,4 +51,19 @@ class NoAppView @JvmOverloads constructor(
binding.img.setImageDrawable(ContextCompat.getDrawable(context, icon)) binding.img.setImageDrawable(ContextCompat.getDrawable(context, icon))
} }
} }
@ModelProp
fun showAction(visibility: Boolean) {
binding.button.isVisible = visibility
}
@ModelProp
fun actionMessage(message: String) {
binding.button.text = message
}
@CallbackProp
fun actionCallback(viewOnClickListener: OnClickListener?) {
binding.button.setOnClickListener(viewOnClickListener)
}
} }

View File

@@ -26,8 +26,6 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.store.AuroraApp import com.aurora.store.AuroraApp
import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.Event
import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.databinding.FragmentAppsBinding import com.aurora.store.databinding.FragmentAppsBinding
import com.aurora.store.view.epoxy.views.HeaderViewModel_ import com.aurora.store.view.epoxy.views.HeaderViewModel_
@@ -51,20 +49,6 @@ class InstalledAppsFragment : BaseFragment<FragmentAppsBinding>() {
} }
} }
private fun onEvent(event: Event) {
when (event) {
is InstallerEvent.Installed,
is InstallerEvent.Uninstalled,
is BusEvent.Blacklisted -> {
viewModel.fetchApps()
}
else -> {
Log.i(TAG, "Got an unhandled event: $event")
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -75,7 +59,18 @@ class InstalledAppsFragment : BaseFragment<FragmentAppsBinding>() {
} }
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
AuroraApp.events.busEvent.collect { onEvent(it) } AuroraApp.events.installerEvent.collect {
when (it) {
is InstallerEvent.Installed,
is InstallerEvent.Uninstalled -> {
viewModel.fetchApps()
}
else -> {
Log.i(TAG, "Got an unhandled event: $it")
}
}
}
} }
} }

View File

@@ -26,20 +26,17 @@ import android.os.Environment
import android.view.View import android.view.View
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.aurora.Constants import com.aurora.Constants
import com.aurora.extensions.browse import com.aurora.extensions.browse
import com.aurora.extensions.isRAndAbove import com.aurora.extensions.isRAndAbove
import com.aurora.extensions.toast import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App
import com.aurora.store.AuroraApp
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.event.Event
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.room.download.Download import com.aurora.store.data.room.download.Download
import com.aurora.store.data.room.update.Update
import com.aurora.store.databinding.FragmentUpdatesBinding import com.aurora.store.databinding.FragmentUpdatesBinding
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
import com.aurora.store.util.PathUtil import com.aurora.store.util.PathUtil
@@ -57,14 +54,14 @@ import kotlinx.coroutines.launch
@AndroidEntryPoint @AndroidEntryPoint
class UpdatesFragment : BaseFragment<FragmentUpdatesBinding>() { class UpdatesFragment : BaseFragment<FragmentUpdatesBinding>() {
private lateinit var app: App private lateinit var update: Update
private val viewModel: UpdatesViewModel by activityViewModels() private val viewModel: UpdatesViewModel by viewModels()
private val startForStorageManagerResult = private val startForStorageManagerResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (isRAndAbove() && Environment.isExternalStorageManager()) { if (isRAndAbove() && Environment.isExternalStorageManager()) {
viewModel.download(app) viewModel.download(update)
} else { } else {
toast(R.string.permissions_denied) toast(R.string.permissions_denied)
} }
@@ -72,7 +69,7 @@ class UpdatesFragment : BaseFragment<FragmentUpdatesBinding>() {
private val startForPermissions = private val startForPermissions =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { perm -> registerForActivityResult(ActivityResultContracts.RequestPermission()) { perm ->
if (perm) viewModel.download(app) else toast(R.string.permissions_denied) if (perm) viewModel.download(update) else toast(R.string.permissions_denied)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -102,7 +99,6 @@ class UpdatesFragment : BaseFragment<FragmentUpdatesBinding>() {
} }
}.collectLatest { map -> }.collectLatest { map ->
updateController(map) updateController(map)
binding.swipeRefreshLayout.isRefreshing = false
viewModel.updateAllEnqueued = viewModel.updateAllEnqueued =
map?.values?.all { it?.isRunning == true } ?: false map?.values?.all { it?.isRunning == true } ?: false
@@ -139,26 +135,19 @@ class UpdatesFragment : BaseFragment<FragmentUpdatesBinding>() {
} }
} }
viewLifecycleOwner.lifecycleScope.launch {
viewModel.fetchingUpdates.collect {
binding.swipeRefreshLayout.isRefreshing = it
if (it && viewModel.updates.value.isNullOrEmpty()) updateController(null)
}
}
binding.swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
viewModel.fetchUpdates() viewModel.fetchUpdates()
} }
viewLifecycleOwner.lifecycleScope.launch {
AuroraApp.events.busEvent.collect { onEvent(it) }
}
} }
private fun onEvent(event: Event) { private fun updateController(appList: Map<Update, Download?>?) {
when (event) {
is InstallerEvent.Installed, is InstallerEvent.Uninstalled -> {
viewModel.fetchUpdates()
}
else -> {}
}
}
private fun updateController(appList: Map<App, Download?>?) {
binding.recycler.withModels { binding.recycler.withModels {
setFilterDuplicates(true) setFilterDuplicates(true)
if (appList == null) { if (appList == null) {
@@ -175,6 +164,9 @@ class UpdatesFragment : BaseFragment<FragmentUpdatesBinding>() {
.id("no_update") .id("no_update")
.icon(R.drawable.ic_updates) .icon(R.drawable.ic_updates)
.message(getString(R.string.details_no_updates)) .message(getString(R.string.details_no_updates))
.showAction(true)
.actionMessage(getString(R.string.check_updates))
.actionCallback { _ -> viewModel.fetchUpdates() }
) )
} else { } else {
add( add(
@@ -193,25 +185,21 @@ class UpdatesFragment : BaseFragment<FragmentUpdatesBinding>() {
} }
) )
for ((app, download) in appList) { for ((update, download) in appList) {
add( add(
AppUpdateViewModel_() AppUpdateViewModel_()
.id(app.packageName) .id(update.packageName)
.app(app) .update(update)
.download(download) .download(download)
.click { _ -> .click { _ ->
if (app.packageName == Constants.APP_ID) { if (update.packageName == Constants.APP_ID) {
requireContext().browse(Constants.GITLAB_URL) requireContext().browse(Constants.GITLAB_URL)
} else { } else {
openDetailsFragment(app.packageName, app) openDetailsFragment(update.packageName)
} }
} }
.longClick { _ -> .positiveAction { _ -> updateSingle(update) }
openAppMenuSheet(app) .negativeAction { _ -> cancelSingle(update) }
false
}
.positiveAction { _ -> updateSingle(app) }
.negativeAction { _ -> cancelSingle(app) }
) )
} }
} }
@@ -219,18 +207,18 @@ class UpdatesFragment : BaseFragment<FragmentUpdatesBinding>() {
} }
} }
private fun updateSingle(app: App, updateAll: Boolean = false) { private fun updateSingle(update: Update, updateAll: Boolean = false) {
this.app = app this.update = update
viewModel.updateAllEnqueued = updateAll viewModel.updateAllEnqueued = updateAll
if (PathUtil.needsStorageManagerPerm(app.fileList)) { if (PathUtil.needsStorageManagerPerm(update.fileList)) {
if (isRAndAbove()) { if (isRAndAbove()) {
if (!Environment.isExternalStorageManager()) { if (!Environment.isExternalStorageManager()) {
startForStorageManagerResult.launch( startForStorageManagerResult.launch(
PackageUtil.getStorageManagerIntent(requireContext()) PackageUtil.getStorageManagerIntent(requireContext())
) )
} else { } else {
viewModel.download(app) viewModel.download(update)
} }
} else { } else {
if (ContextCompat.checkSelfPermission( if (ContextCompat.checkSelfPermission(
@@ -238,18 +226,18 @@ class UpdatesFragment : BaseFragment<FragmentUpdatesBinding>() {
Manifest.permission.WRITE_EXTERNAL_STORAGE Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED ) == PackageManager.PERMISSION_GRANTED
) { ) {
viewModel.download(app) viewModel.download(update)
} else { } else {
startForPermissions.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) startForPermissions.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
} }
} }
} else { } else {
viewModel.download(app) viewModel.download(update)
} }
} }
private fun cancelSingle(app: App) { private fun cancelSingle(update: Update) {
viewModel.cancelDownload(app) viewModel.cancelDownload(update.packageName)
} }
private fun cancelAll() { private fun cancelAll() {

View File

@@ -19,16 +19,13 @@
package com.aurora.store.viewmodel.all package com.aurora.store.viewmodel.all
import android.content.Context
import android.util.Log import android.util.Log
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.util.AppUtil import com.aurora.store.util.AppUtil
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -37,10 +34,7 @@ import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class InstalledViewModel @Inject constructor( class InstalledViewModel @Inject constructor(private val appUtil: AppUtil) : ViewModel() {
@ApplicationContext private val context: Context,
private val authProvider: AuthProvider
) : ViewModel() {
private val TAG = InstalledViewModel::class.java.simpleName private val TAG = InstalledViewModel::class.java.simpleName
@@ -56,7 +50,7 @@ class InstalledViewModel @Inject constructor(
fun fetchApps() { fun fetchApps() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val apps = AppUtil.getFilteredInstalledApps(context, authProvider.authData!!) val apps = appUtil.getFilteredInstalledApps()
_installedApps.emit(apps.sortedBy { it.displayName.lowercase(Locale.getDefault()) }) _installedApps.emit(apps.sortedBy { it.displayName.lowercase(Locale.getDefault()) })
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, "Failed to get installed apps", exception) Log.e(TAG, "Failed to get installed apps", exception)

View File

@@ -20,76 +20,52 @@
package com.aurora.store.viewmodel.all package com.aurora.store.viewmodel.all
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.App import com.aurora.store.data.room.update.Update
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.util.AppUtil import com.aurora.store.util.AppUtil
import com.aurora.store.util.DownloadWorkerUtil import com.aurora.store.util.DownloadWorkerUtil
import com.aurora.store.util.Preferences
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@SuppressLint("StaticFieldLeak") // false positive, see https://github.com/google/dagger/issues/3253 @SuppressLint("StaticFieldLeak") // false positive, see https://github.com/google/dagger/issues/3253
class UpdatesViewModel @Inject constructor( class UpdatesViewModel @Inject constructor(
@ApplicationContext private val context: Context, private val appUtil: AppUtil,
private val gson: Gson,
private val downloadWorkerUtil: DownloadWorkerUtil, private val downloadWorkerUtil: DownloadWorkerUtil,
private val authProvider: AuthProvider
) : ViewModel() { ) : ViewModel() {
private val TAG = UpdatesViewModel::class.java.simpleName private val TAG = UpdatesViewModel::class.java.simpleName
var updateAllEnqueued: Boolean = false var updateAllEnqueued: Boolean = false
private val _updates = MutableStateFlow<List<App>?>(null)
val updates = _updates.asStateFlow()
val downloadsList get() = downloadWorkerUtil.downloadsList val downloadsList get() = downloadWorkerUtil.downloadsList
val updates get() = appUtil.updates
init { private val _fetchingUpdates = MutableStateFlow(false)
fetchUpdates() val fetchingUpdates = _fetchingUpdates.asStateFlow()
}
fun fetchUpdates() { fun fetchUpdates() {
_fetchingUpdates.value = true
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
supervisorScope { try {
try { appUtil.checkUpdates()
val isExtendedUpdateEnabled = Preferences.getBoolean( } catch (exception: Exception) {
context, Preferences.PREFERENCE_UPDATES_EXTENDED Log.d(TAG, "Failed to get updates", exception)
)
val updates = AppUtil.getUpdatableApps(
context,
authProvider.authData!!,
gson,
!isExtendedUpdateEnabled
).sortedBy { it.displayName.lowercase(Locale.getDefault()) }
_updates.emit(updates)
} catch (exception: Exception) {
Log.d(TAG, "Failed to get updates", exception)
}
} }
} }.invokeOnCompletion { _fetchingUpdates.value = false }
} }
fun download(app: App) { fun download(update: Update) {
viewModelScope.launch { downloadWorkerUtil.enqueueApp(app) } viewModelScope.launch { downloadWorkerUtil.enqueueUpdate(update) }
} }
fun cancelDownload(app: App) { fun cancelDownload(packageName: String) {
viewModelScope.launch { downloadWorkerUtil.cancelDownload(app.packageName) } viewModelScope.launch { downloadWorkerUtil.cancelDownload(packageName) }
} }
fun cancelAll() { fun cancelAll() {

View File

@@ -28,7 +28,8 @@
android:layout_width="@dimen/icon_size" android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size" android:layout_height="@dimen/icon_size"
android:layout_centerInParent="true" android:layout_centerInParent="true"
app:tint="?android:textColorPrimary" /> app:tint="?android:textColorPrimary"
tools:src="@drawable/ic_updates" />
<TextView <TextView
android:id="@+id/txt" android:id="@+id/txt"
@@ -36,7 +37,16 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/img" android:layout_below="@id/img"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:layout_marginTop="@dimen/margin_small" android:layout_marginVertical="@dimen/margin_small"
android:textAppearance="@style/TextAppearance.Aurora.Line1" android:textAppearance="@style/TextAppearance.Aurora.Line1"
tools:text="No updates available" /> tools:text="No updates available" />
</RelativeLayout>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/txt"
android:layout_centerInParent="true"
android:visibility="gone"
tools:text="@string/check_updates" />
</RelativeLayout>

View File

@@ -469,4 +469,7 @@
<!-- LogoutDialog --> <!-- LogoutDialog -->
<string name="action_logout_confirmation_title">Log out?</string> <string name="action_logout_confirmation_title">Log out?</string>
<string name="action_logout_confirmation_message">Are you sure you want to log out?</string> <string name="action_logout_confirmation_message">Are you sure you want to log out?</string>
<!-- UpdatesFragment -->
<string name="check_updates">Check updates</string>
</resources> </resources>