diff --git a/app/src/main/java/com/aurora/store/MainActivity.kt b/app/src/main/java/com/aurora/store/MainActivity.kt index 3b05f1185..105826ab8 100644 --- a/app/src/main/java/com/aurora/store/MainActivity.kt +++ b/app/src/main/java/com/aurora/store/MainActivity.kt @@ -33,19 +33,26 @@ import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController import com.aurora.extensions.accentColor import com.aurora.extensions.applyThemeAccent +import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.model.NetworkStatus import com.aurora.store.data.providers.NetworkProvider import com.aurora.store.databinding.ActivityMainBinding +import com.aurora.store.util.AppUtil import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB import com.aurora.store.view.ui.sheets.NetworkDialogSheet import com.google.android.material.navigation.NavigationBarView import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import javax.inject.Inject @AndroidEntryPoint class MainActivity : AppCompatActivity() { + @Inject + lateinit var appUtil: AppUtil + private lateinit var B: ActivityMainBinding 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 { diff --git a/app/src/main/java/com/aurora/store/data/room/AuroraDatabase.kt b/app/src/main/java/com/aurora/store/data/room/AuroraDatabase.kt index 9b2ae86f4..6b45452f5 100644 --- a/app/src/main/java/com/aurora/store/data/room/AuroraDatabase.kt +++ b/app/src/main/java/com/aurora/store/data/room/AuroraDatabase.kt @@ -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.favourites.Favourite 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) abstract class AuroraDatabase : RoomDatabase() { abstract fun downloadDao(): DownloadDao abstract fun favouriteDao(): FavouriteDao + abstract fun updateDao(): UpdateDao } diff --git a/app/src/main/java/com/aurora/store/data/room/RoomModule.kt b/app/src/main/java/com/aurora/store/data/room/RoomModule.kt index 6a96fb15e..1d51d7a03 100644 --- a/app/src/main/java/com/aurora/store/data/room/RoomModule.kt +++ b/app/src/main/java/com/aurora/store/data/room/RoomModule.kt @@ -5,6 +5,7 @@ import androidx.room.Room import com.aurora.store.data.room.download.DownloadConverter import com.aurora.store.data.room.download.DownloadDao import com.aurora.store.data.room.favourites.FavouriteDao +import com.aurora.store.data.room.update.UpdateDao import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -39,4 +40,9 @@ object RoomModule { fun providesFavouriteDao(auroraDatabase: AuroraDatabase): FavouriteDao { return auroraDatabase.favouriteDao() } + + @Provides + fun providesUpdateDao(auroraDatabase: AuroraDatabase): UpdateDao { + return auroraDatabase.updateDao() + } } diff --git a/app/src/main/java/com/aurora/store/data/room/download/Download.kt b/app/src/main/java/com/aurora/store/data/room/download/Download.kt index 14047b072..5ee316f96 100644 --- a/app/src/main/java/com/aurora/store/data/room/download/Download.kt +++ b/app/src/main/java/com/aurora/store/data/room/download/Download.kt @@ -6,6 +6,7 @@ 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 com.aurora.store.data.room.update.Update import kotlinx.parcelize.Parcelize @Parcelize @@ -52,5 +53,26 @@ data class Download( 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 + ) + } } } diff --git a/app/src/main/java/com/aurora/store/data/room/update/Update.kt b/app/src/main/java/com/aurora/store/data/room/update/Update.kt new file mode 100644 index 000000000..99f0778f5 --- /dev/null +++ b/app/src/main/java/com/aurora/store/data/room/update/Update.kt @@ -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, + val sharedLibs: List +) : 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) + } +} diff --git a/app/src/main/java/com/aurora/store/data/room/update/UpdateDao.kt b/app/src/main/java/com/aurora/store/data/room/update/UpdateDao.kt new file mode 100644 index 000000000..50ae13778 --- /dev/null +++ b/app/src/main/java/com/aurora/store/data/room/update/UpdateDao.kt @@ -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) + + @Query("SELECT * FROM `update` ORDER BY displayName ASC") + fun updates(): Flow> + + @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) { + deleteAll() + insertAll(updates) + } +} diff --git a/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt b/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt index eab51916d..7ca459595 100644 --- a/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt +++ b/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt @@ -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_CHECK_INTERVAL import com.aurora.store.util.save -import com.google.gson.Gson import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.Dispatchers @@ -42,8 +41,8 @@ import java.util.concurrent.TimeUnit.MINUTES */ @HiltWorker class UpdateWorker @AssistedInject constructor( + private val appUtil: AppUtil, private val downloadWorkerUtil: DownloadWorkerUtil, - private val gson: Gson, private val authProvider: AuthProvider, @Assisted private val appContext: Context, @Assisted workerParams: WorkerParameters @@ -128,13 +127,7 @@ class UpdateWorker @AssistedInject constructor( } try { - val updatesList = AppUtil.getUpdatableApps( - context = appContext, - authData = authProvider.authData!!, - gson = gson, - verifyCert = true, - selfUpdate = false - ) + val updatesList = appUtil.checkUpdates().filterNot { it.isSelfUpdate() } if (updatesList.isNotEmpty()) { if (autoUpdatesMode == 1) { @@ -150,7 +143,7 @@ class UpdateWorker @AssistedInject constructor( if (list.isEmpty()) return@let 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) diff --git a/app/src/main/java/com/aurora/store/util/AppUtil.kt b/app/src/main/java/com/aurora/store/util/AppUtil.kt index 6e5743837..e00cfc73c 100644 --- a/app/src/main/java/com/aurora/store/util/AppUtil.kt +++ b/app/src/main/java/com/aurora/store/util/AppUtil.kt @@ -6,30 +6,47 @@ import android.util.Log import androidx.core.content.pm.PackageInfoCompat import com.aurora.Constants import com.aurora.gplayapi.data.models.App -import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.helpers.AppDetailsHelper +import com.aurora.store.AuroraApp import com.aurora.store.BuildConfig import com.aurora.store.data.model.SelfUpdate 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.room.update.Update +import com.aurora.store.data.room.update.UpdateDao import com.google.gson.Gson +import dagger.hilt.android.qualifiers.ApplicationContext 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 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 const val RELEASE = "release" + private val TAG = AppUtil::class.java.simpleName + private val RELEASE = "release" - suspend fun getUpdatableApps( - context: Context, - authData: AuthData, - gson: Gson, - verifyCert: Boolean, - selfUpdate: Boolean = true - ): List { + private val isExtendedUpdateEnabled get() = Preferences.getBoolean( + context, Preferences.PREFERENCE_UPDATES_EXTENDED + ) + val updates = updateDao.updates() + .map { list -> list.filter { it.isInstalled(context) } } + .map { list -> if (!isExtendedUpdateEnabled) list.filter { it.hasValidCert } else list } + .stateIn(AuroraApp.scope, SharingStarted.WhileSubscribed(), null) + + suspend fun checkUpdates(): List { + Log.i(TAG, "Checking for updates") val packageInfoMap = PackageUtil.getPackageInfoMap(context) - val appUpdatesList = getFilteredInstalledApps(context, authData, packageInfoMap).filter { + val appUpdatesList = getFilteredInstalledApps(packageInfoMap).filter { val packageInfo = packageInfoMap[it.packageName] if (packageInfo != null) { it.versionCode.toLong() > PackageInfoCompat.getLongVersionCode(packageInfo) @@ -38,33 +55,26 @@ object AppUtil { } }.toMutableList() - val verifiedUpdatesList = if (verifyCert) { - appUpdatesList.filter { app -> - app.certificateSetList.any { - it.certificateSet in CertUtil.getEncodedCertificateHashes( - context, app.packageName - ) - } - }.toMutableList() - } else { - appUpdatesList + if (canSelfUpdate(context)) { + getSelfUpdate(context, gson)?.let { appUpdatesList.add(it) } } - if (canSelfUpdate(context) && selfUpdate) { - getSelfUpdate(context, gson)?.let { verifiedUpdatesList.add(it) } + return appUpdatesList.map { Update.fromApp(context, it) }.also { + // Cache the updates into the database + updateDao.insertUpdates(it) } + } - return verifiedUpdatesList + suspend fun deleteUpdate(packageName: String) { + updateDao.delete(packageName) } suspend fun getFilteredInstalledApps( - context: Context, - authData: AuthData, packageInfoMap: MutableMap? = null ): List { return withContext(Dispatchers.IO) { val blackList = BlacklistProvider.with(context).getBlackList() - val appDetailsHelper = AppDetailsHelper(authData) + val appDetailsHelper = AppDetailsHelper(authProvider.authData!!) .using(HttpClient.getPreferredClient(context)) (packageInfoMap ?: PackageUtil.getPackageInfoMap(context)).keys.let { packages -> diff --git a/app/src/main/java/com/aurora/store/util/DownloadWorkerUtil.kt b/app/src/main/java/com/aurora/store/util/DownloadWorkerUtil.kt index 0103d52dd..269d1e60b 100644 --- a/app/src/main/java/com/aurora/store/util/DownloadWorkerUtil.kt +++ b/app/src/main/java/com/aurora/store/util/DownloadWorkerUtil.kt @@ -12,6 +12,7 @@ import com.aurora.store.AuroraApp import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.room.download.Download 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.google.gson.Gson import dagger.hilt.android.qualifiers.ApplicationContext @@ -86,6 +87,14 @@ class DownloadWorkerUtil @Inject constructor( 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 * @param packageName Name of the package to cancel download diff --git a/app/src/main/java/com/aurora/store/util/NotificationUtil.kt b/app/src/main/java/com/aurora/store/util/NotificationUtil.kt index fb4b390f4..984fbe80e 100644 --- a/app/src/main/java/com/aurora/store/util/NotificationUtil.kt +++ b/app/src/main/java/com/aurora/store/util/NotificationUtil.kt @@ -25,6 +25,7 @@ import com.aurora.store.R import com.aurora.store.data.activity.InstallActivity import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.room.download.Download +import com.aurora.store.data.room.update.Update import java.util.UUID import com.aurora.store.data.room.download.Download as AuroraDownload @@ -185,7 +186,7 @@ object NotificationUtil { .build() } - fun getUpdateNotification(context: Context, updatesList: List): Notification { + fun getUpdateNotification(context: Context, updatesList: List): Notification { val contentIntent = NavDeepLinkBuilder(context) .setGraph(R.navigation.mobile_navigation) .setDestination(R.id.updatesFragment) diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/app/AppUpdateView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/app/AppUpdateView.kt index e79cc9779..215a69f96 100644 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/app/AppUpdateView.kt +++ b/app/src/main/java/com/aurora/store/view/epoxy/views/app/AppUpdateView.kt @@ -41,6 +41,7 @@ import com.aurora.gplayapi.data.models.App import com.aurora.store.R import com.aurora.store.data.model.DownloadStatus 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.util.CommonUtil import com.aurora.store.view.epoxy.views.BaseModel @@ -59,11 +60,11 @@ class AppUpdateView @JvmOverloads constructor( private val cornersTransformation = RoundedCornersTransformation(8.px.toFloat()) @ModelProp - fun app(app: App) { + fun update(update: Update) { /*Inflate App details*/ - with(app) { + with(update) { binding.txtLine1.text = displayName - binding.imgIcon.load(iconArtwork.url) { + binding.imgIcon.load(iconURL) { placeholder(R.drawable.bg_placeholder) transformations(cornersTransformation) listener { _, result -> @@ -74,9 +75,9 @@ class AppUpdateView @JvmOverloads constructor( binding.txtLine2.text = developerName binding.txtLine3.text = ("${CommonUtil.addSiPrefix(size)} • $updatedOn") binding.txtLine4.text = ("$versionName (${versionCode})") - binding.txtChangelog.text = if (changes.isNotEmpty()) + binding.txtChangelog.text = if (changelog.isNotEmpty()) HtmlCompat.fromHtml( - changes, + changelog, HtmlCompat.FROM_HTML_OPTION_USE_CSS_COLORS ) else diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/app/NoAppView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/app/NoAppView.kt index 1272a977e..0d7a029ce 100644 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/app/NoAppView.kt +++ b/app/src/main/java/com/aurora/store/view/epoxy/views/app/NoAppView.kt @@ -22,13 +22,14 @@ package com.aurora.store.view.epoxy.views.app import android.content.Context import android.util.AttributeSet import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import com.airbnb.epoxy.CallbackProp import com.airbnb.epoxy.ModelProp import com.airbnb.epoxy.ModelView import com.aurora.store.databinding.ViewNoAppBinding import com.aurora.store.view.epoxy.views.BaseModel import com.aurora.store.view.epoxy.views.BaseView - @ModelView( autoLayout = ModelView.Size.MATCH_WIDTH_MATCH_HEIGHT, baseModelClass = BaseModel::class @@ -50,4 +51,19 @@ class NoAppView @JvmOverloads constructor( 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) + } } diff --git a/app/src/main/java/com/aurora/store/view/ui/all/InstalledAppsFragment.kt b/app/src/main/java/com/aurora/store/view/ui/all/InstalledAppsFragment.kt index 6074f5bf2..5d148ac3c 100644 --- a/app/src/main/java/com/aurora/store/view/ui/all/InstalledAppsFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/all/InstalledAppsFragment.kt @@ -26,8 +26,6 @@ import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import com.aurora.gplayapi.data.models.App 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.databinding.FragmentAppsBinding import com.aurora.store.view.epoxy.views.HeaderViewModel_ @@ -51,20 +49,6 @@ class InstalledAppsFragment : BaseFragment() { } } - 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?) { super.onViewCreated(view, savedInstanceState) @@ -75,7 +59,18 @@ class InstalledAppsFragment : BaseFragment() { } 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") + } + } + } } } diff --git a/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt b/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt index b733e260f..1a7bfb898 100644 --- a/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt @@ -26,20 +26,17 @@ import android.os.Environment import android.view.View import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat -import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.aurora.Constants import com.aurora.extensions.browse import com.aurora.extensions.isRAndAbove 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.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.update.Update import com.aurora.store.databinding.FragmentUpdatesBinding import com.aurora.store.util.PackageUtil import com.aurora.store.util.PathUtil @@ -57,14 +54,14 @@ import kotlinx.coroutines.launch @AndroidEntryPoint class UpdatesFragment : BaseFragment() { - 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 = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (isRAndAbove() && Environment.isExternalStorageManager()) { - viewModel.download(app) + viewModel.download(update) } else { toast(R.string.permissions_denied) } @@ -72,7 +69,7 @@ class UpdatesFragment : BaseFragment() { private val startForPermissions = 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?) { @@ -102,7 +99,6 @@ class UpdatesFragment : BaseFragment() { } }.collectLatest { map -> updateController(map) - binding.swipeRefreshLayout.isRefreshing = false viewModel.updateAllEnqueued = map?.values?.all { it?.isRunning == true } ?: false @@ -139,26 +135,19 @@ class UpdatesFragment : BaseFragment() { } } + viewLifecycleOwner.lifecycleScope.launch { + viewModel.fetchingUpdates.collect { + binding.swipeRefreshLayout.isRefreshing = it + if (it && viewModel.updates.value.isNullOrEmpty()) updateController(null) + } + } + binding.swipeRefreshLayout.setOnRefreshListener { viewModel.fetchUpdates() } - - viewLifecycleOwner.lifecycleScope.launch { - AuroraApp.events.busEvent.collect { onEvent(it) } - } } - private fun onEvent(event: Event) { - when (event) { - is InstallerEvent.Installed, is InstallerEvent.Uninstalled -> { - viewModel.fetchUpdates() - } - - else -> {} - } - } - - private fun updateController(appList: Map?) { + private fun updateController(appList: Map?) { binding.recycler.withModels { setFilterDuplicates(true) if (appList == null) { @@ -175,6 +164,9 @@ class UpdatesFragment : BaseFragment() { .id("no_update") .icon(R.drawable.ic_updates) .message(getString(R.string.details_no_updates)) + .showAction(true) + .actionMessage(getString(R.string.check_updates)) + .actionCallback { _ -> viewModel.fetchUpdates() } ) } else { add( @@ -193,25 +185,21 @@ class UpdatesFragment : BaseFragment() { } ) - for ((app, download) in appList) { + for ((update, download) in appList) { add( AppUpdateViewModel_() - .id(app.packageName) - .app(app) + .id(update.packageName) + .update(update) .download(download) .click { _ -> - if (app.packageName == Constants.APP_ID) { + if (update.packageName == Constants.APP_ID) { requireContext().browse(Constants.GITLAB_URL) } else { - openDetailsFragment(app.packageName, app) + openDetailsFragment(update.packageName) } } - .longClick { _ -> - openAppMenuSheet(app) - false - } - .positiveAction { _ -> updateSingle(app) } - .negativeAction { _ -> cancelSingle(app) } + .positiveAction { _ -> updateSingle(update) } + .negativeAction { _ -> cancelSingle(update) } ) } } @@ -219,18 +207,18 @@ class UpdatesFragment : BaseFragment() { } } - private fun updateSingle(app: App, updateAll: Boolean = false) { - this.app = app + private fun updateSingle(update: Update, updateAll: Boolean = false) { + this.update = update viewModel.updateAllEnqueued = updateAll - if (PathUtil.needsStorageManagerPerm(app.fileList)) { + if (PathUtil.needsStorageManagerPerm(update.fileList)) { if (isRAndAbove()) { if (!Environment.isExternalStorageManager()) { startForStorageManagerResult.launch( PackageUtil.getStorageManagerIntent(requireContext()) ) } else { - viewModel.download(app) + viewModel.download(update) } } else { if (ContextCompat.checkSelfPermission( @@ -238,18 +226,18 @@ class UpdatesFragment : BaseFragment() { Manifest.permission.WRITE_EXTERNAL_STORAGE ) == PackageManager.PERMISSION_GRANTED ) { - viewModel.download(app) + viewModel.download(update) } else { startForPermissions.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) } } } else { - viewModel.download(app) + viewModel.download(update) } } - private fun cancelSingle(app: App) { - viewModel.cancelDownload(app) + private fun cancelSingle(update: Update) { + viewModel.cancelDownload(update.packageName) } private fun cancelAll() { diff --git a/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt index ff7fc379f..429bb0d98 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt @@ -19,16 +19,13 @@ package com.aurora.store.viewmodel.all -import android.content.Context import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.App -import com.aurora.store.data.providers.AuthProvider import com.aurora.store.util.AppUtil import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -37,10 +34,7 @@ import java.util.Locale import javax.inject.Inject @HiltViewModel -class InstalledViewModel @Inject constructor( - @ApplicationContext private val context: Context, - private val authProvider: AuthProvider -) : ViewModel() { +class InstalledViewModel @Inject constructor(private val appUtil: AppUtil) : ViewModel() { private val TAG = InstalledViewModel::class.java.simpleName @@ -56,7 +50,7 @@ class InstalledViewModel @Inject constructor( fun fetchApps() { viewModelScope.launch(Dispatchers.IO) { try { - val apps = AppUtil.getFilteredInstalledApps(context, authProvider.authData!!) + val apps = appUtil.getFilteredInstalledApps() _installedApps.emit(apps.sortedBy { it.displayName.lowercase(Locale.getDefault()) }) } catch (exception: Exception) { Log.e(TAG, "Failed to get installed apps", exception) diff --git a/app/src/main/java/com/aurora/store/viewmodel/all/UpdatesViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/all/UpdatesViewModel.kt index dca03376d..5ff212300 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/all/UpdatesViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/all/UpdatesViewModel.kt @@ -20,76 +20,52 @@ package com.aurora.store.viewmodel.all import android.annotation.SuppressLint -import android.content.Context import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.aurora.gplayapi.data.models.App -import com.aurora.store.data.providers.AuthProvider +import com.aurora.store.data.room.update.Update import com.aurora.store.util.AppUtil 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.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.supervisorScope -import java.util.Locale import javax.inject.Inject @HiltViewModel @SuppressLint("StaticFieldLeak") // false positive, see https://github.com/google/dagger/issues/3253 class UpdatesViewModel @Inject constructor( - @ApplicationContext private val context: Context, - private val gson: Gson, + private val appUtil: AppUtil, private val downloadWorkerUtil: DownloadWorkerUtil, - private val authProvider: AuthProvider ) : ViewModel() { private val TAG = UpdatesViewModel::class.java.simpleName var updateAllEnqueued: Boolean = false - private val _updates = MutableStateFlow?>(null) - val updates = _updates.asStateFlow() - val downloadsList get() = downloadWorkerUtil.downloadsList + val updates get() = appUtil.updates - init { - fetchUpdates() - } + private val _fetchingUpdates = MutableStateFlow(false) + val fetchingUpdates = _fetchingUpdates.asStateFlow() fun fetchUpdates() { + _fetchingUpdates.value = true viewModelScope.launch(Dispatchers.IO) { - supervisorScope { - try { - val isExtendedUpdateEnabled = Preferences.getBoolean( - context, Preferences.PREFERENCE_UPDATES_EXTENDED - ) - - 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) - } + try { + appUtil.checkUpdates() + } catch (exception: Exception) { + Log.d(TAG, "Failed to get updates", exception) } - } + }.invokeOnCompletion { _fetchingUpdates.value = false } } - fun download(app: App) { - viewModelScope.launch { downloadWorkerUtil.enqueueApp(app) } + fun download(update: Update) { + viewModelScope.launch { downloadWorkerUtil.enqueueUpdate(update) } } - fun cancelDownload(app: App) { - viewModelScope.launch { downloadWorkerUtil.cancelDownload(app.packageName) } + fun cancelDownload(packageName: String) { + viewModelScope.launch { downloadWorkerUtil.cancelDownload(packageName) } } fun cancelAll() { diff --git a/app/src/main/res/layout/view_no_app.xml b/app/src/main/res/layout/view_no_app.xml index 7cc47a1ed..94d809208 100644 --- a/app/src/main/res/layout/view_no_app.xml +++ b/app/src/main/res/layout/view_no_app.xml @@ -28,7 +28,8 @@ android:layout_width="@dimen/icon_size" android:layout_height="@dimen/icon_size" android:layout_centerInParent="true" - app:tint="?android:textColorPrimary" /> + app:tint="?android:textColorPrimary" + tools:src="@drawable/ic_updates" /> - \ No newline at end of file + +