DownloadFragment: Refactor to work with new downloads logic

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2023-12-13 18:04:24 +05:30
parent 711c26af42
commit bd55b157e6
9 changed files with 116 additions and 249 deletions

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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<DownloadFile>) {
private fun updateController(downloads: List<Download>) {
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)
)
}
}

View File

@@ -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<Int>()
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
}
}

View File

@@ -51,43 +51,15 @@
android:textAlignment="viewStart"
tools:text="App Name" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:weightSum="2">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_status"
style="@style/AuroraTextStyle.Line2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
tools:text="Status" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_size"
style="@style/AuroraTextStyle.Line2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewEnd"
tools:text="33 MB" />
</LinearLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_file"
style="@style/AuroraTextStyle.Line3"
android:layout_width="match_parent"
android:layout_height="17dp"
android:id="@+id/txt_status"
style="@style/AuroraTextStyle.Line2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
tools:text="File path" />
android:textAlignment="viewStart"
tools:text="Status" />
<ProgressBar
android:id="@+id/progress_download"

View File

@@ -18,19 +18,10 @@
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_pause_all"
android:title="@string/download_pause_all" />
<item
android:id="@+id/action_resume_all"
android:title="@string/download_resume_all" />
<item
android:id="@+id/action_cancel_all"
android:title="@string/download_cancel_all" />
<item
android:id="@+id/action_clear_completed"
android:title="@string/download_clear_completed" />
<item
android:id="@+id/action_force_clear_all"
android:title="@string/download_force_clear_all" />
</menu>
</menu>

View File

@@ -21,16 +21,10 @@
<item
android:id="@+id/action_copy"
android:title="@string/action_copy_link" />
<item
android:id="@+id/action_pause"
android:title="@string/action_pause" />
<item
android:id="@+id/action_resume"
android:title="@string/action_resume" />
<item
android:id="@+id/action_cancel"
android:title="@string/action_cancel" />
<item
android:id="@+id/action_clear"
android:title="@string/action_clear" />
</menu>
</menu>

View File

@@ -432,8 +432,8 @@
android:label="DownloadMenuSheet"
tools:layout="@layout/sheet_download_menu">
<argument
android:name="downloadFile"
app:argType="com.aurora.store.data.model.DownloadFile" />
android:name="download"
app:argType="com.aurora.store.data.room.download.Download" />
</dialog>
<dialog
android:id="@+id/deviceMiuiSheet"