From bd55b157e663bd0011b4adb0f576b0f5c5c59949 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Wed, 13 Dec 2023 18:04:24 +0530 Subject: [PATCH] DownloadFragment: Refactor to work with new downloads logic Signed-off-by: Aayush Gupta --- .../store/data/room/download/Download.kt | 5 +- .../aurora/store/util/DownloadWorkerUtil.kt | 32 +++- .../store/view/epoxy/views/DownloadView.kt | 52 ++----- .../view/ui/downloads/DownloadFragment.kt | 141 +++--------------- .../store/view/ui/sheets/DownloadMenuSheet.kt | 72 +++++---- app/src/main/res/layout/view_download.xml | 40 +---- app/src/main/res/menu/menu_download_main.xml | 11 +- .../main/res/menu/menu_download_single.xml | 8 +- .../main/res/navigation/mobile_navigation.xml | 4 +- 9 files changed, 116 insertions(+), 249 deletions(-) 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 728f1f6ac..386a604fa 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 @@ -1,10 +1,13 @@ package com.aurora.store.data.room.download +import android.os.Parcelable import androidx.room.Entity import androidx.room.PrimaryKey import com.aurora.gplayapi.data.models.App import com.aurora.store.data.model.DownloadStatus +import kotlinx.parcelize.Parcelize +@Parcelize @Entity(tableName = "download") data class Download( @PrimaryKey val packageName: String, @@ -21,7 +24,7 @@ data class Download( var timeRemaining: Long, var totalFiles: Int, var downloadedFiles: Int -) { +) : Parcelable { val isFinished get() = status in DownloadStatus.finished companion object { 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 d867462a3..7a45230f5 100644 --- a/app/src/main/java/com/aurora/store/util/DownloadWorkerUtil.kt +++ b/app/src/main/java/com/aurora/store/util/DownloadWorkerUtil.kt @@ -21,6 +21,8 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.deleteRecursively @OptIn(DelicateCoroutinesApi::class) class DownloadWorkerUtil @Inject constructor( @@ -42,6 +44,8 @@ class DownloadWorkerUtil @Inject constructor( val downloadsList = downloadDao.downloads() .stateIn(GlobalScope, SharingStarted.WhileSubscribed(), emptyList()) + private val TAG = DownloadWorkerUtil::class.java.simpleName + fun init() { // Run cleanup for last finished download and drop it database GlobalScope.launch { @@ -68,13 +72,35 @@ class DownloadWorkerUtil @Inject constructor( downloadDao.insert(Download.fromApp(app)) } - fun cancelDownload(packageName: String) { + suspend fun cancelDownload(packageName: String) { + Log.i(TAG, "Cancelling download for $packageName") WorkManager.getInstance(context).cancelAllWorkByTag("$PACKAGE_NAME:$packageName") + downloadsList.value + .find { it.packageName == packageName && it.status == DownloadStatus.QUEUED } + ?.let { downloadDao.update(it.copy(status = DownloadStatus.CANCELLED)) } } - fun cancelAll(context: Context, downloads: Boolean = true, updates: Boolean = true) { - val workManager = WorkManager.getInstance(context) + @OptIn(ExperimentalPathApi::class) + suspend fun clearDownload(packageName: String, versionCode: Int) { + Log.i(TAG, "Clearing downloads for $packageName ($versionCode)") + downloadDao.delete(packageName) + PathUtil.getAppDownloadDir(context, packageName, versionCode) + .deleteRecursively() + } + suspend fun clearFinishedDownloads() { + downloadsList.value.filter { it.isFinished }.forEach { + clearDownload(it.packageName, it.versionCode) + } + } + + suspend fun cancelAll(downloads: Boolean = true, updates: Boolean = true) { + // Cancel all enqueued downloads first to avoid triggering re-download + downloadsList.value.filter { it.status == DownloadStatus.QUEUED }.forEach { + downloadDao.update(it.copy(status = DownloadStatus.CANCELLED)) + } + + val workManager = WorkManager.getInstance(context) if (downloads) workManager.cancelAllWorkByTag(DOWNLOAD_APP) if (updates) workManager.cancelAllWorkByTag(DOWNLOAD_UPDATE) } diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/DownloadView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/DownloadView.kt index 0607f3c25..d53c9766a 100644 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/DownloadView.kt +++ b/app/src/main/java/com/aurora/store/view/epoxy/views/DownloadView.kt @@ -27,17 +27,14 @@ import coil.transform.RoundedCornersTransformation import com.airbnb.epoxy.CallbackProp import com.airbnb.epoxy.ModelProp import com.airbnb.epoxy.ModelView -import com.aurora.Constants -import com.aurora.gplayapi.data.models.App import com.aurora.store.R -import com.aurora.store.data.model.DownloadFile +import com.aurora.store.data.model.DownloadStatus +import com.aurora.store.data.room.download.Download import com.aurora.store.databinding.ViewDownloadBinding import com.aurora.store.util.CommonUtil.getDownloadSpeedString import com.aurora.store.util.CommonUtil.getETAString -import com.aurora.store.util.CommonUtil.humanReadableByteValue import com.google.gson.Gson import com.google.gson.GsonBuilder -import com.tonyodev.fetch2.Status import java.lang.reflect.Modifier import java.util.Locale @@ -49,10 +46,6 @@ class DownloadView : RelativeLayout { private lateinit var B: ViewDownloadBinding - private val gson: Gson = GsonBuilder() - .excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT) - .create() - constructor(context: Context?) : super(context) { init(context) } @@ -75,18 +68,12 @@ class DownloadView : RelativeLayout { } @ModelProp - fun download(downloadFile: DownloadFile) { - val download = downloadFile.download - val extras = download.extras.getString(Constants.STRING_EXTRA, "{}") - val app = gson.fromJson(extras, App::class.java) - - app?.let { - B.imgDownload.load(app.iconArtwork.url) { - placeholder(R.drawable.bg_placeholder) - transformations(RoundedCornersTransformation(32F)) - } - B.txtTitle.text = app.displayName + fun download(download: Download) { + B.imgDownload.load(download.iconURL) { + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(32F)) } + B.txtTitle.text = download.displayName B.txtStatus.text = download.status.name .lowercase(Locale.getDefault()) @@ -98,33 +85,24 @@ class DownloadView : RelativeLayout { } } - B.txtSize.text = StringBuilder() - .append(humanReadableByteValue(download.downloaded, true)) - .append("/") - .append(humanReadableByteValue(download.total, true)) - - val file = download.file - B.txtFile.text = file.substring(file.lastIndexOf("/") + 1) - - var progress = download.progress - if (progress == -1) { - progress = 0 + B.progressDownload.apply { + progress = download.progress + isIndeterminate = download.progress <= 0 && !download.isFinished } + B.txtProgress.text = ("${download.progress}%") - B.progressDownload.progress = progress - B.txtProgress.text = ("$progress%") - - B.txtEta.text = getETAString(context, download.etaInMilliSeconds) + B.txtEta.text = getETAString(context, download.timeRemaining) B.txtSpeed.text = getDownloadSpeedString( context, - download.downloadedBytesPerSecond + download.speed ) when (download.status) { - Status.DOWNLOADING, Status.QUEUED, Status.ADDED -> { + DownloadStatus.DOWNLOADING, DownloadStatus.QUEUED -> { B.txtSpeed.visibility = VISIBLE B.txtEta.visibility = VISIBLE } + else -> { B.txtSpeed.visibility = INVISIBLE B.txtEta.visibility = INVISIBLE diff --git a/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadFragment.kt b/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadFragment.kt index 78e765ff7..4de5020b1 100644 --- a/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadFragment.kt @@ -22,24 +22,21 @@ package com.aurora.store.view.ui.downloads import android.os.Bundle import android.view.View import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.aurora.store.MobileNavigationDirections import com.aurora.store.R -import com.aurora.store.data.downloader.DownloadManager -import com.aurora.store.data.model.DownloadFile +import com.aurora.store.data.room.download.Download import com.aurora.store.databinding.FragmentDownloadBinding -import com.aurora.store.util.Preferences +import com.aurora.store.util.DownloadWorkerUtil import com.aurora.store.view.epoxy.views.DownloadViewModel_ import com.aurora.store.view.epoxy.views.app.NoAppViewModel_ import com.aurora.store.view.ui.commons.BaseFragment -import com.tonyodev.fetch2.AbstractFetchListener -import com.tonyodev.fetch2.BuildConfig -import com.tonyodev.fetch2.Download -import com.tonyodev.fetch2.Error -import com.tonyodev.fetch2.Fetch -import com.tonyodev.fetch2.FetchListener -import com.tonyodev.fetch2.Status import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch @AndroidEntryPoint class DownloadFragment : BaseFragment(R.layout.fragment_download) { @@ -48,53 +45,8 @@ class DownloadFragment : BaseFragment(R.layout.fragment_download) { private val binding: FragmentDownloadBinding get() = _binding!! - private lateinit var fetch: Fetch - - private var fetchListener: FetchListener = object : AbstractFetchListener() { - override fun onAdded(download: Download) { - updateDownloadsList() - } - - override fun onQueued(download: Download, waitingOnNetwork: Boolean) { - updateDownloadsList() - } - - override fun onCompleted(download: Download) { - updateDownloadsList() - } - - override fun onError(download: Download, error: Error, throwable: Throwable?) { - updateDownloadsList() - } - - override fun onProgress( - download: Download, - etaInMilliSeconds: Long, - downloadedBytesPerSecond: Long - ) { - updateDownloadsList() - } - - override fun onPaused(download: Download) { - updateDownloadsList() - } - - override fun onResumed(download: Download) { - updateDownloadsList() - } - - override fun onCancelled(download: Download) { - updateDownloadsList() - } - - override fun onRemoved(download: Download) { - updateDownloadsList() - } - - override fun onDeleted(download: Download) { - updateDownloadsList() - } - } + @Inject + lateinit var downloadWorkerUtil: DownloadWorkerUtil override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -110,73 +62,32 @@ class DownloadFragment : BaseFragment(R.layout.fragment_download) { setNavigationOnClickListener { findNavController().navigateUp() } setOnMenuItemClickListener { when (it.itemId) { - R.id.action_pause_all -> { - fetch.pauseAll() - } - - R.id.action_resume_all -> { - fetch.resumeAll() - } - R.id.action_cancel_all -> { - fetch.cancelAll() + viewLifecycleOwner.lifecycleScope.launch(NonCancellable) { + downloadWorkerUtil.cancelAll() + } } - R.id.action_clear_completed -> { - fetch.removeAllWithStatus(Status.COMPLETED) - Preferences.getPrefs(view.context).edit() - .remove(Preferences.PREFERENCE_UNIQUE_GROUP_IDS).apply() - } - - R.id.action_force_clear_all -> { - fetch.deleteAll() - Preferences.getPrefs(view.context).edit() - .remove(Preferences.PREFERENCE_UNIQUE_GROUP_IDS).apply() + viewLifecycleOwner.lifecycleScope.launch(NonCancellable) { + downloadWorkerUtil.clearFinishedDownloads() + } } } true } } - fetch = DownloadManager.with(view.context).fetch - updateDownloadsList() - - binding.swipeRefreshLayout.setOnRefreshListener { - updateDownloadsList() + viewLifecycleOwner.lifecycleScope.launch { + downloadWorkerUtil.downloadsList.collectLatest { updateController(it) } } } - override fun onResume() { - super.onResume() - if (::fetch.isInitialized) - fetch.addListener(fetchListener) - } - - override fun onPause() { - if (::fetch.isInitialized) - fetch.removeListener(fetchListener) - super.onPause() - } - override fun onDestroyView() { - if (::fetch.isInitialized) fetch.removeListener(fetchListener) super.onDestroyView() _binding = null } - private fun updateDownloadsList() { - if (::fetch.isInitialized) - fetch.getDownloads { downloads -> - updateController( - downloads - .filter { it.id != BuildConfig.APPLICATION_ID.hashCode() } - .sortedWith { o1, o2 -> o2.created.compareTo(o1.created) } - .map { DownloadFile(it) } - ) - } - } - - private fun updateController(downloads: List) { + private fun updateController(downloads: List) { binding.recycler.withModels { if (downloads.isEmpty()) { add( @@ -188,12 +99,12 @@ class DownloadFragment : BaseFragment(R.layout.fragment_download) { downloads.forEach { add( DownloadViewModel_() - .id(it.download.id, it.download.progress, it.download.status.value) + .id(it.packageName) .download(it) - .click { _ -> openDetailsActivity(it) } + .click { _ -> openDetailsFragment(it.packageName) } .longClick { _ -> openDownloadMenuSheet(it) - false + true } ) } @@ -202,15 +113,9 @@ class DownloadFragment : BaseFragment(R.layout.fragment_download) { binding.swipeRefreshLayout.isRefreshing = false } - private fun openDetailsActivity(downloadFile: DownloadFile) { + private fun openDownloadMenuSheet(download: Download) { findNavController().navigate( - MobileNavigationDirections.actionGlobalAppDetailsFragment(downloadFile.download.tag!!) - ) - } - - private fun openDownloadMenuSheet(downloadFile: DownloadFile) { - findNavController().navigate( - DownloadFragmentDirections.actionDownloadFragmentToDownloadMenuSheet(downloadFile) + DownloadFragmentDirections.actionDownloadFragmentToDownloadMenuSheet(download) ) } } diff --git a/app/src/main/java/com/aurora/store/view/ui/sheets/DownloadMenuSheet.kt b/app/src/main/java/com/aurora/store/view/ui/sheets/DownloadMenuSheet.kt index e372dcd19..268a3159e 100644 --- a/app/src/main/java/com/aurora/store/view/ui/sheets/DownloadMenuSheet.kt +++ b/app/src/main/java/com/aurora/store/view/ui/sheets/DownloadMenuSheet.kt @@ -23,79 +23,72 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.navArgs import com.aurora.extensions.copyToClipBoard import com.aurora.extensions.toast import com.aurora.store.R import com.aurora.store.data.downloader.DownloadManager +import com.aurora.store.data.model.DownloadStatus import com.aurora.store.databinding.SheetDownloadMenuBinding +import com.aurora.store.util.DownloadWorkerUtil import com.tonyodev.fetch2.Fetch import com.tonyodev.fetch2.Status import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.launch import kotlin.properties.Delegates @AndroidEntryPoint class DownloadMenuSheet : BaseBottomSheet() { - private lateinit var B: SheetDownloadMenuBinding - private lateinit var fetch: Fetch + private var _binding: SheetDownloadMenuBinding? = null + private val binding get() = _binding!! private val args: DownloadMenuSheetArgs by navArgs() - private var status by Delegates.notNull() + private val playStoreURL = "https://play.google.com/store/apps/details?id=" + + @Inject + lateinit var downloadWorkerUtil: DownloadWorkerUtil override fun onCreateContentView( inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle? ): View { - B = SheetDownloadMenuBinding.inflate(layoutInflater) - return B.root + _binding = SheetDownloadMenuBinding.inflate(layoutInflater) + return binding.root } override fun onContentViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - fetch = DownloadManager - .with(requireContext()) - .getFetchInstance() - - status = args.downloadFile.download.status.value - attachNavigation() - } - - private fun attachNavigation() { - with(B.navigationView) { - if (status == Status.PAUSED.value || status == Status.COMPLETED.value || status == Status.CANCELLED.value) { - menu.findItem(R.id.action_pause).isVisible = false - } - - if (status == Status.DOWNLOADING.value || status == Status.COMPLETED.value || status == Status.QUEUED.value) { - menu.findItem(R.id.action_resume).isVisible = false - } - - if (status == Status.COMPLETED.value || status == Status.CANCELLED.value) { - menu.findItem(R.id.action_cancel).isVisible = false - } + with(binding.navigationView) { + menu.findItem(R.id.action_cancel).isVisible = !args.download.isFinished + menu.findItem(R.id.action_clear).isVisible = args.download.isFinished setNavigationItemSelectedListener { item -> when (item.itemId) { R.id.action_copy -> { - requireContext().copyToClipBoard(args.downloadFile.download.url) + requireContext().copyToClipBoard( + "${playStoreURL}${args.download.packageName}" + ) requireContext().toast(requireContext().getString(R.string.toast_clipboard_copied)) } - R.id.action_pause -> { - fetch.pause(args.downloadFile.download.id) - } - R.id.action_resume -> if (status == Status.FAILED.value || status == Status.CANCELLED.value) { - fetch.retry(args.downloadFile.download.id) - } else { - fetch.resume(args.downloadFile.download.id) - } R.id.action_cancel -> { - fetch.cancel(args.downloadFile.download.id) + findViewTreeLifecycleOwner()?.lifecycleScope?.launch(NonCancellable) { + downloadWorkerUtil.cancelDownload(args.download.packageName) + } } R.id.action_clear -> { - fetch.delete(args.downloadFile.download.id) + findViewTreeLifecycleOwner()?.lifecycleScope?.launch(NonCancellable) { + downloadWorkerUtil.clearDownload( + args.download.packageName, + args.download.versionCode + ) + } } } dismissAllowingStateLoss() @@ -103,4 +96,9 @@ class DownloadMenuSheet : BaseBottomSheet() { } } } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } } diff --git a/app/src/main/res/layout/view_download.xml b/app/src/main/res/layout/view_download.xml index d1e798707..5853c0bf8 100644 --- a/app/src/main/res/layout/view_download.xml +++ b/app/src/main/res/layout/view_download.xml @@ -51,43 +51,15 @@ android:textAlignment="viewStart" tools:text="App Name" /> - - - - - - - + android:textAlignment="viewStart" + tools:text="Status" /> - - - - \ No newline at end of file + diff --git a/app/src/main/res/menu/menu_download_single.xml b/app/src/main/res/menu/menu_download_single.xml index 9b8b52913..3285ce692 100644 --- a/app/src/main/res/menu/menu_download_single.xml +++ b/app/src/main/res/menu/menu_download_single.xml @@ -21,16 +21,10 @@ - - - \ No newline at end of file + diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index fc0222340..c8b25ed9d 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -432,8 +432,8 @@ android:label="DownloadMenuSheet" tools:layout="@layout/sheet_download_menu"> + android:name="download" + app:argType="com.aurora.store.data.room.download.Download" />