diff --git a/app/src/main/java/com/aurora/store/view/custom/layouts/button/ActionButton.kt b/app/src/main/java/com/aurora/store/view/custom/layouts/button/ActionButton.kt index c944e6025..b1a24bff9 100644 --- a/app/src/main/java/com/aurora/store/view/custom/layouts/button/ActionButton.kt +++ b/app/src/main/java/com/aurora/store/view/custom/layouts/button/ActionButton.kt @@ -129,4 +129,8 @@ class ActionButton : RelativeLayout { fun addOnClickListener(onClickListener: OnClickListener?) { B.btn.setOnClickListener(onClickListener) } + + fun addOnLongClickListener(onLongClickListener: OnLongClickListener?) { + B.btn.setOnLongClickListener(onLongClickListener) + } } diff --git a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt index 2d89e8662..8c479741c 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt @@ -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() - 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) { - 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) { - 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, - 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