AppDetailsFragment: Download apps using DownloadWorker

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2023-10-28 18:29:17 +05:30
parent cbeda8cc9f
commit f4226ac9ef
2 changed files with 102 additions and 325 deletions

View File

@@ -129,4 +129,8 @@ class ActionButton : RelativeLayout {
fun addOnClickListener(onClickListener: OnClickListener?) {
B.btn.setOnClickListener(onClickListener)
}
fun addOnLongClickListener(onLongClickListener: OnLongClickListener?) {
B.btn.setOnLongClickListener(onLongClickListener)
}
}

View File

@@ -22,15 +22,12 @@ package com.aurora.store.view.ui.details
import android.Manifest
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.IBinder
import android.provider.Settings
import android.view.View
import android.widget.LinearLayout
@@ -44,6 +41,8 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.work.WorkInfo
import androidx.work.WorkManager
import coil.load
import coil.transform.RoundedCornersTransformation
import com.airbnb.epoxy.EpoxyRecyclerView
@@ -65,15 +64,13 @@ import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.R
import com.aurora.store.State
import com.aurora.store.data.ViewState
import com.aurora.store.data.downloader.DownloadManager
import com.aurora.store.data.downloader.getGroupId
import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.installer.RootInstaller
import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.data.service.AppMetadataStatusListener
import com.aurora.store.data.service.UpdateService
import com.aurora.store.data.work.DownloadWorker
import com.aurora.store.databinding.FragmentDetailsBinding
import com.aurora.store.databinding.LayoutDetailsBetaBinding
import com.aurora.store.databinding.LayoutDetailsDescriptionBinding
@@ -81,7 +78,6 @@ import com.aurora.store.databinding.LayoutDetailsDevBinding
import com.aurora.store.databinding.LayoutDetailsPermissionsBinding
import com.aurora.store.databinding.LayoutDetailsReviewBinding
import com.aurora.store.util.CommonUtil
import com.aurora.store.util.Log
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.PathUtil
import com.aurora.store.util.Preferences
@@ -98,14 +94,6 @@ import com.aurora.store.viewmodel.details.DetailsClusterViewModel
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tonyodev.fetch2.AbstractFetchGroupListener
import com.tonyodev.fetch2.Download
import com.tonyodev.fetch2.Error
import com.tonyodev.fetch2.Fetch
import com.tonyodev.fetch2.FetchGroup
import com.tonyodev.fetch2.FetchGroupListener
import com.tonyodev.fetch2.Status
import com.tonyodev.fetch2core.DownloadBlock
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
@@ -113,6 +101,7 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.io.File
import java.util.Locale
import kotlin.io.path.pathString
@AndroidEntryPoint
class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
@@ -130,64 +119,29 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
private val startForStorageManagerResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (isRAndAbove() && Environment.isExternalStorageManager()) {
updateApp(app)
DownloadWorker.enqueueApp(requireContext(), app)
} else {
toast(R.string.permissions_denied)
}
}
private val startForPermissions =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) updateApp(app) else toast(R.string.permissions_denied)
if (it) {
DownloadWorker.enqueueApp(requireContext(), app)
} else {
toast(R.string.permissions_denied)
}
}
private lateinit var authData: AuthData
private lateinit var app: App
private var fetch: Fetch? = null
private var downloadManager: DownloadManager? = null
private var attachToServiceCalled = false
private var updateService: UpdateService? = null
private var pendingAddListeners = true
private var serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
updateService = (binder as UpdateService.UpdateServiceBinder).getUpdateService()
if (::fetchGroupListener.isInitialized && ::appMetadataListener.isInitialized && pendingAddListeners) {
updateService!!.registerFetchListener(fetchGroupListener)
// appMetadataListener needs to be initialized after the fetchGroupListener
updateService!!.registerAppMetadataListener(appMetadataListener)
pendingAddListeners = false
}
if (listOfActionsWhenServiceAttaches.isNotEmpty()) {
val iterator = listOfActionsWhenServiceAttaches.iterator()
while (iterator.hasNext()) {
val next = iterator.next()
next.run()
iterator.remove()
}
}
}
override fun onServiceDisconnected(name: ComponentName) {
updateService = null
attachToServiceCalled = false
pendingAddListeners = true
}
}
private lateinit var fetchGroupListener: FetchGroupListener
private lateinit var appMetadataListener: AppMetadataStatusListener
private lateinit var completionMarker: File
private lateinit var inProgressMarker: File
private var isExternal = false
private var isNone = false
private var status = Status.NONE
private var downloadStatus = DownloadStatus.UNAVAILABLE
private var isUpdatable: Boolean = false
private var autoDownload: Boolean = false
private var downloadOnly: Boolean = false
private var uninstallActionEnabled = false
val listOfActionsWhenServiceAttaches = ArrayList<Runnable>()
override fun onStart() {
super.onStart()
EventBus.getDefault().register(this)
@@ -283,6 +237,38 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
}
}
// Downloads
binding.layoutDetailsInstall.imgCancel.setOnClickListener {
DownloadWorker.cancelDownload(it.context, app)
if (downloadStatus != DownloadStatus.DOWNLOADING) flip(0)
}
val uniqueWorkName = "${DownloadWorker.DOWNLOAD_WORKER}/${app.packageName}"
WorkManager.getInstance(view.context)
.getWorkInfosForUniqueWorkLiveData(uniqueWorkName)
.observe(viewLifecycleOwner) { workList ->
workList.getOrNull(0)?.let {
if (it.state.isFinished) flip(0) else flip(1)
when (it.state) {
WorkInfo.State.ENQUEUED,
WorkInfo.State.RUNNING -> {
downloadStatus = DownloadStatus.DOWNLOADING
updateProgress(
it.progress.getInt(DownloadWorker.DOWNLOAD_PROGRESS, 0),
it.progress.getLong(DownloadWorker.DOWNLOAD_SPEED, -1),
it.progress.getLong(DownloadWorker.DOWNLOAD_TIME, -1)
)
}
WorkInfo.State.FAILED -> downloadStatus = DownloadStatus.FAILED
WorkInfo.State.CANCELLED -> downloadStatus = DownloadStatus.CANCELLED
WorkInfo.State.SUCCEEDED -> downloadStatus = DownloadStatus.COMPLETED
else -> {}
}
}
}
// Reviews
viewLifecycleOwner.lifecycleScope.launch {
viewModel.reviews.collect {
@@ -434,7 +420,6 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
}
override fun onResume() {
getUpdateServiceInstance()
checkAndSetupInstall()
super.onResume()
}
@@ -459,34 +444,24 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
}
}
private fun verifyAndInstall(files: List<Download>) {
if (downloadOnly)
return
var filesExist = true
files.forEach { download ->
filesExist = filesExist && File(download.file).exists()
}
if (filesExist)
install(files)
else
purchase()
}
@Synchronized
private fun install(files: List<Download>) {
updateActionState(State.IDLE)
private fun install() {
val files = File(
PathUtil.getAppDownloadDir(
requireContext(),
app.packageName,
app.versionCode
).pathString
).listFiles() ?: return
val apkFiles = files.filter { it.file.endsWith(".apk") }
val apkFiles = files.filter { it.path.endsWith(".apk") }
val preferredInstaller =
Preferences.getInteger(requireContext(), Preferences.PREFERENCE_INSTALLER_ID)
if (apkFiles.size > 1 && preferredInstaller == 1) {
showDialog(R.string.title_installer, R.string.dialog_desc_native_split)
} else {
viewModel.install(requireContext(), app.packageName, apkFiles.map { it.file })
viewModel.install(requireContext(), app.packageName, apkFiles)
runOnUiThread {
binding.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing))
@@ -515,16 +490,10 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
}
}
private fun attachWhiteListStatus() {
}
private fun inflatePartialApp() {
if (::app.isInitialized) {
attachWhiteListStatus()
attachHeader()
attachBottomSheet()
attachFetch()
attachActions()
if (autoDownload) {
@@ -592,21 +561,29 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
}
@Synchronized
private fun startDownload() {
when (status) {
Status.PAUSED -> {
fetch?.resumeGroup(app.getGroupId(requireContext()))
}
Status.DOWNLOADING -> {
private fun startDownload(forceDownload: Boolean = false) {
when (downloadStatus) {
DownloadStatus.DOWNLOADING -> {
flip(1)
toast("Already downloading")
}
Status.COMPLETED -> {
fetch?.getFetchGroup(app.getGroupId(requireContext())) {
verifyAndInstall(it.downloads)
DownloadStatus.COMPLETED -> {
if (forceDownload) {
purchase()
return
}
// Check if files are still present, else proceed to re-download the app
val files = File(
PathUtil.getAppDownloadDir(
requireContext(),
app.packageName,
app.versionCode
).pathString
).listFiles()
if (files?.isNotEmpty() == true) install() else purchase()
}
else -> {
@@ -627,7 +604,7 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
)
} else {
updateApp(app)
DownloadWorker.enqueueApp(requireContext(), app)
}
} else {
if (ContextCompat.checkSelfPermission(
@@ -635,42 +612,23 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
) {
updateApp(app)
DownloadWorker.enqueueApp(requireContext(), app)
} else {
startForPermissions.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}
} else {
updateApp(app)
DownloadWorker.enqueueApp(requireContext(), app)
}
}
private fun updateApp(app: App) {
if (updateService == null) {
listOfActionsWhenServiceAttaches.add {
updateService?.updateApp(app, true)
}
getUpdateServiceInstance()
} else {
updateService?.updateApp(app, true)
}
}
private fun updateProgress(
fetchGroup: FetchGroup,
etaInMilliSeconds: Long,
downloadedBytesPerSecond: Long
) {
private fun updateProgress(progress: Int, speed: Long = -1, timeRemaining: Long = -1) {
runOnUiThread {
val progress = if (fetchGroup.groupDownloadProgress > 0)
fetchGroup.groupDownloadProgress
else
0
if (progress == 100) {
binding.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing))
return@runOnUiThread
}
binding.layoutDetailsInstall.apply {
txtProgressPercent.text = ("${progress}%")
@@ -680,31 +638,8 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
progressDownload.progress = progress
}
txtEta.text = CommonUtil.getETAString(
requireContext(),
etaInMilliSeconds
)
txtSpeed.text =
CommonUtil.getDownloadSpeedString(
requireContext(),
downloadedBytesPerSecond
)
}
}
}
private fun expandBottomSheet(message: String?) {
runOnUiThread {
bottomSheetBehavior.isHideable = false
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
with(binding.layoutDetailsInstall) {
txtPurchaseError.text = message
btnDownload.updateState(State.IDLE)
if (app.isFree)
btnDownload.setText(R.string.action_install)
else
btnDownload.setText(app.price)
txtEta.text = CommonUtil.getETAString(requireContext(), timeRemaining)
txtSpeed.text = CommonUtil.getDownloadSpeedString(requireContext(), speed)
}
}
}
@@ -713,6 +648,8 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName)
runOnUiThread {
app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName)
binding.layoutDetailsInstall.btnDownload.let { btn ->
if (app.isInstalled) {
isUpdatable = PackageUtil.isUpdatable(
@@ -729,6 +666,10 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
("$installedVersion${app.versionName} (${app.versionCode})")
btn.setText(R.string.action_update)
btn.addOnClickListener { startDownload() }
btn.addOnLongClickListener {
startDownload(true)
true
}
} else {
binding.layoutDetailsApp.txtLine3.text = installedVersion
btn.setText(R.string.action_open)
@@ -738,7 +679,10 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
binding.layoutDetailsToolbar.toolbar.invalidateMenu()
}
} else {
if (app.isFree) {
val downloading = downloadStatus == DownloadStatus.DOWNLOADING
if (DownloadWorker.isEnqueued(app.packageName) && !downloading) {
flip(1)
} else if (app.isFree) {
btn.setText(R.string.action_install)
} else {
btn.setText(app.price)
@@ -752,6 +696,15 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
startDownload()
}
}
btn.addOnLongClickListener {
if (authData.isAnonymous && !app.isFree) {
toast(R.string.toast_purchase_blocked)
} else {
btn.setText(R.string.download_metadata)
startDownload(true)
}
true
}
if (uninstallActionEnabled) {
binding.layoutDetailsToolbar.toolbar.invalidateMenu()
}
@@ -771,186 +724,6 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
}
}
private fun attachFetch() {
if (fetch == null) {
downloadManager = DownloadManager.with(requireContext())
fetch = downloadManager!!.fetch
}
fetch?.getFetchGroup(app.getGroupId(requireContext())) { fetchGroup: FetchGroup ->
if (fetchGroup.groupDownloadProgress == 100 && fetchGroup.completedDownloads.isNotEmpty()) {
status = Status.COMPLETED
} else if (downloadManager?.isDownloading(fetchGroup) == true) {
status = Status.DOWNLOADING
flip(1)
} else if (downloadManager?.isCanceled(fetchGroup) == true) {
status = Status.CANCELLED
} else if (fetchGroup.pausedDownloads.isNotEmpty()) {
status = Status.PAUSED
} else {
status = Status.NONE
}
}
fetchGroupListener = object : AbstractFetchGroupListener() {
override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (groupId == app.getGroupId(requireContext())) {
status = download.status
}
}
override fun onStarted(
groupId: Int,
download: Download,
downloadBlocks: List<DownloadBlock>,
totalBlocks: Int,
fetchGroup: FetchGroup
) {
if (groupId == app.getGroupId(requireContext())) {
status = download.status
flip(1)
val pkgDir = PathUtil.getPackageDirectory(requireContext(), app.packageName)
completionMarker =
File("$pkgDir/.${app.versionCode}.download-complete")
inProgressMarker =
File("$pkgDir/.${app.versionCode}.download-in-progress")
if (completionMarker.exists())
completionMarker.delete()
inProgressMarker.createNewFile()
}
}
override fun onResumed(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (groupId == app.getGroupId(requireContext())) {
status = download.status
flip(1)
inProgressMarker.parentFile?.mkdirs()
inProgressMarker.createNewFile()
}
}
override fun onPaused(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (groupId == app.getGroupId(requireContext())) {
status = download.status
flip(0)
}
}
override fun onProgress(
groupId: Int,
download: Download,
etaInMilliSeconds: Long,
downloadedBytesPerSecond: Long,
fetchGroup: FetchGroup
) {
if (groupId == app.getGroupId(requireContext())) {
updateProgress(fetchGroup, etaInMilliSeconds, downloadedBytesPerSecond)
Log.i(
"${app.displayName} : ${download.file} -> Progress : %d",
fetchGroup.groupDownloadProgress
)
}
}
override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (groupId == app.getGroupId(requireContext()) && fetchGroup.groupDownloadProgress == 100) {
status = download.status
flip(0)
updateProgress(fetchGroup, -1, -1)
try {
inProgressMarker.delete()
completionMarker.createNewFile()
} catch (ex: Exception) {
ex.printStackTrace()
}
}
}
override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (groupId == app.getGroupId(requireContext())) {
status = download.status
flip(0)
inProgressMarker.delete()
}
}
override fun onError(
groupId: Int,
download: Download,
error: Error,
throwable: Throwable?,
fetchGroup: FetchGroup
) {
if (groupId == app.getGroupId(requireContext())) {
status = download.status
flip(0)
inProgressMarker.delete()
}
}
}
appMetadataListener = object : AppMetadataStatusListener {
override fun onAppMetadataStatusError(reason: String, app: App) {
if (app.packageName == this@AppDetailsFragment.app.packageName) {
updateActionState(State.IDLE)
expandBottomSheet(reason)
}
}
}
getUpdateServiceInstance()
binding.layoutDetailsInstall.imgCancel.setOnClickListener {
fetch?.cancelGroup(
app.getGroupId(requireContext())
)
}
if (updateService != null) {
pendingAddListeners = false
updateService!!.registerFetchListener(fetchGroupListener)
// appMetadataListener needs to be initialized after the fetchGroupListener
updateService!!.registerAppMetadataListener(appMetadataListener)
} else {
pendingAddListeners = true
}
}
private fun getUpdateServiceInstance() {
if (updateService == null && !attachToServiceCalled) {
attachToServiceCalled = true
val intent = Intent(requireContext(), UpdateService::class.java)
activity?.startService(intent)
activity?.bindService(
intent,
serviceConnection,
0
)
}
}
override fun onPause() {
if (updateService != null) {
updateService = null
attachToServiceCalled = false
pendingAddListeners = true
activity?.unbindService(serviceConnection)
}
super.onPause()
}
override fun onDestroy() {
super.onDestroy()
if (updateService != null) {
updateService = null
attachToServiceCalled = false
pendingAddListeners = true
activity?.unbindService(serviceConnection)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null