From 733ed0acc27ca4c09cedc4d6f28bdc99c7fc7295 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Mon, 9 Dec 2024 11:06:26 +0800 Subject: [PATCH] AppDetailsFragment: Improvements to action buttons Signed-off-by: Aayush Gupta --- .../view/ui/details/AppDetailsFragment.kt | 352 ++++++++---------- .../viewmodel/details/AppDetailsViewModel.kt | 9 +- .../main/res/layout/layout_details_app.xml | 12 +- 3 files changed, 158 insertions(+), 215 deletions(-) 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 f201fe68e..1d43aa8f9 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 @@ -48,7 +48,6 @@ import com.aurora.Constants import com.aurora.Constants.EXODUS_SUBMIT_PAGE import com.aurora.extensions.browse import com.aurora.extensions.hide -import com.aurora.extensions.invisible import com.aurora.extensions.px import com.aurora.extensions.requiresObbDir import com.aurora.extensions.runOnUiThread @@ -90,8 +89,9 @@ import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.viewmodel.details.AppDetailsViewModel import com.aurora.store.viewmodel.details.DetailsClusterViewModel import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import java.util.Locale import javax.inject.Inject @@ -115,11 +115,6 @@ class AppDetailsFragment : BaseFragment() { private val isExternal get() = activity?.intent?.action != Intent.ACTION_MAIN - private var downloadStatus = DownloadStatus.UNAVAILABLE - private var isUpdatable: Boolean = false - - private val tags = mutableSetOf() - private val isExtendedUpdateEnabled: Boolean get() = Preferences.getBoolean(requireContext(), PREFERENCE_UPDATES_EXTENDED) private val showSimilarApps: Boolean @@ -130,25 +125,12 @@ class AppDetailsFragment : BaseFragment() { is InstallerEvent.Installed -> { if (app.packageName == event.packageName) { checkAndSetupInstall() - transformIcon(false) - binding.toolbar.menu.apply { - findItem(R.id.action_home_screen)?.isVisible = - ShortcutManagerUtil.canPinShortcut(requireContext(), app.packageName) - findItem(R.id.action_uninstall)?.isVisible = true - findItem(R.id.menu_app_settings)?.isVisible = true - } } } is InstallerEvent.Uninstalled -> { if (app.packageName == event.packageName) { checkAndSetupInstall() - transformIcon(false) - binding.toolbar.menu.apply { - findItem(R.id.action_home_screen)?.isVisible = false - findItem(R.id.action_uninstall)?.isVisible = false - findItem(R.id.menu_app_settings)?.isVisible = false - } } } @@ -236,32 +218,25 @@ class AppDetailsFragment : BaseFragment() { } } - viewLifecycleOwner.lifecycleScope.launch { - viewModel.downloadsList - .filter { list -> list.any { it.packageName == app.packageName } } - .collectLatest { downloadsList -> - val download = downloadsList.find { it.packageName == app.packageName } - download?.let { - downloadStatus = it.downloadStatus - - when (it.downloadStatus) { - DownloadStatus.QUEUED -> { - updateProgress(it.progress) - } - - DownloadStatus.DOWNLOADING -> { - updateSecondaryAction(true) - updateProgress(it.progress, it.speed, it.timeRemaining) - } - - else -> { - transformIcon(false) - updateSecondaryAction(false) - } - } + viewModel.download.filterNotNull().onEach { + when (it.downloadStatus) { + DownloadStatus.QUEUED, + DownloadStatus.DOWNLOADING -> { + updateProgress(it.progress) + binding.layoutDetailsApp.btnPrimaryAction.apply { + isEnabled = false + text = getString(R.string.action_open) + setOnClickListener { openApp() } + } + binding.layoutDetailsApp.btnSecondaryAction.apply { + text = getString(R.string.action_cancel) + setOnClickListener { viewModel.cancelDownload(app) } } } - } + + else -> checkAndSetupInstall() + } + }.launchIn(viewLifecycleOwner.lifecycleScope) // Reviews viewLifecycleOwner.lifecycleScope.launch { @@ -499,8 +474,20 @@ class AppDetailsFragment : BaseFragment() { // Do not show tags for web apps or unknown apps if (isFullApp) { - tags.add(getString((if (app.isFree) R.string.details_free else R.string.details_paid))) - tags.add(getString((if (app.containsAds) R.string.details_contains_ads else R.string.details_no_ads))) + val tags = mutableSetOf().apply { + if (app.isFree) { + add(getString(R.string.details_free)) + } else { + add(getString(R.string.details_paid)) + } + + if (app.containsAds) { + add(getString(R.string.details_contains_ads)) + } else { + add(getString(R.string.details_no_ads)) + } + } + txtLine4.text = tags.joinToString(separator = " • ") } } @@ -517,19 +504,6 @@ class AppDetailsFragment : BaseFragment() { } } - @Synchronized - private fun startDownload() { - when (downloadStatus) { - DownloadStatus.DOWNLOADING -> { - toast("Already downloading") - } - - else -> { - purchase() - } - } - } - private fun purchase() { if (app.fileList.requiresObbDir()) { if (permissionProvider.isGranted(PermissionType.STORAGE_MANAGER)) { @@ -546,82 +520,33 @@ class AppDetailsFragment : BaseFragment() { } } - private fun updateProgress(progress: Int, speed: Long = -1, timeRemaining: Long = -1) { - runOnUiThread { - updatePrimaryAction(false) - updateSecondaryAction(true) + private fun updateProgress(progress: Int) { + // No need to update progress if it is already 100% / completed + transformIcon(progress != 100) + if (progress == 100) return - if (progress == 100) { - transformIcon(false) - binding.layoutDetailsApp.apply { - txtLine3.text = ("${app.versionName} (${app.versionCode})") - txtLine4.text = tags.joinToString(separator = " • ") - } - return@runOnUiThread - } - - transformIcon(true) - binding.layoutDetailsApp.apply { - if (progress < 1) { - progressDownload.isIndeterminate = true - } else { - progressDownload.isIndeterminate = false - progressDownload.progress = progress - txtLine3.text = CommonUtil.getETAString(requireContext(), timeRemaining) - txtLine4.text = CommonUtil.getDownloadSpeedString(requireContext(), speed) - } - } - } - } - - private fun updatePrimaryAction(enabled: Boolean = false) { - binding.layoutDetailsApp.btnPrimaryAction.apply { - isEnabled = enabled - text = if (app.isInstalled) { - getString(R.string.action_open) - } else { - getString(R.string.action_install) - } - } - } - - @Synchronized - private fun updateSecondaryAction(enabled: Boolean = false) { - runOnUiThread { - binding.layoutDetailsApp.btnSecondaryAction.apply { - isEnabled = enabled - isVisible = enabled - text = getString(R.string.action_cancel) - setOnClickListener { - viewModel.cancelDownload(app) - updatePrimaryAction(true) - } - } + binding.layoutDetailsApp.apply { + progressDownload.progress = progress + progressDownload.isIndeterminate = progress < 1 } } private fun transformIcon(ongoing: Boolean = false) { if (::iconDrawable.isInitialized.not()) return - val scaleFactor = if (ongoing) 0.75f else 1f - val isDownloadVisible = binding.layoutDetailsApp.progressDownload.isShown + val imgIcon = binding.layoutDetailsApp.imgIcon + val progressDownload = binding.layoutDetailsApp.progressDownload // Avoids flickering when the download is in progress - if (isDownloadVisible && scaleFactor != 1f) - return + if (progressDownload.isShown && ongoing) return + if (!progressDownload.isShown && !ongoing) return - if (!isDownloadVisible && scaleFactor == 1f) - return - - if (scaleFactor == 1f) { - binding.layoutDetailsApp.progressDownload.invisible() - } else { - binding.layoutDetailsApp.progressDownload.show() - } + binding.layoutDetailsApp.progressDownload.isVisible = ongoing + val scaleFactor = if (ongoing) 0.75f else 1f val scale = listOf( - ObjectAnimator.ofFloat(binding.layoutDetailsApp.imgIcon, "scaleX", scaleFactor), - ObjectAnimator.ofFloat(binding.layoutDetailsApp.imgIcon, "scaleY", scaleFactor) + ObjectAnimator.ofFloat(imgIcon, "scaleX", scaleFactor), + ObjectAnimator.ofFloat(imgIcon, "scaleY", scaleFactor) ) scale.forEach { animation -> @@ -632,103 +557,118 @@ class AppDetailsFragment : BaseFragment() { } } - iconDrawable.let { - binding.layoutDetailsApp.imgIcon.load(it) { - transformations( - if (scaleFactor == 1f) - RoundedCornersTransformation(8.px.toFloat()) - else - CircleCropTransformation() - ) - } + imgIcon.load(iconDrawable) { + transformations( + if (ongoing) { + CircleCropTransformation() + } else { + RoundedCornersTransformation(8.px.toFloat()) + } + ) } } - @Synchronized private fun checkAndSetupInstall() { - runOnUiThread { - app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) + app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) - if (app.isInstalled) { - val needsExtendedUpdate = !app.certificateSetList.any { - it.certificateSet in CertUtil.getEncodedCertificateHashes( - requireContext(), - app.packageName - ) - } + // Setup primary and secondary action buttons + binding.layoutDetailsApp.btnPrimaryAction.isEnabled = true + binding.layoutDetailsApp.btnPrimaryAction.isEnabled = true - isUpdatable = PackageUtil.isUpdatable( - requireContext(), - app.packageName, - app.versionCode.toLong() - ) + if (app.isInstalled) { + val isUpdatable = PackageUtil.isUpdatable(requireContext(), app.packageName, app.versionCode.toLong()) + val hasValidCert = app.certificateSetList.any { + it.certificateSet in CertUtil.getEncodedCertificateHashes(requireContext(), app.packageName) + } - val installedVersion = PackageUtil.getInstalledVersion( - requireContext(), - app.packageName - ) - - if ((isUpdatable && !needsExtendedUpdate) || (isUpdatable && isExtendedUpdateEnabled)) { - binding.layoutDetailsApp.apply { - txtLine3.text = - ("$installedVersion ➔ ${app.versionName} (${app.versionCode})") - txtLine4.text = tags.joinToString(separator = " • ") - btnPrimaryAction.apply { - isEnabled = true - setText(R.string.action_update) - setOnClickListener { - if (app.versionCode == 0) { - toast(R.string.toast_app_unavailable) - btnPrimaryAction.setText(R.string.status_unavailable) - } else { - startDownload() - } - } - } - } - } else { - binding.layoutDetailsApp.apply { - txtLine3.text = installedVersion - btnPrimaryAction.apply { - isEnabled = true - setText(R.string.action_open) - setOnClickListener { openApp() } + if ((isUpdatable && hasValidCert) || (isUpdatable && isExtendedUpdateEnabled)) { + binding.layoutDetailsApp.btnPrimaryAction.apply { + text = getString(R.string.action_update) + setOnClickListener { + if (app.versionCode == 0) { + toast(R.string.toast_app_unavailable) + setText(R.string.status_unavailable) + } else { + purchase() } } } } else { - if (downloadStatus in DownloadStatus.running) { - updateProgress(-1) - } else if (app.isFree) { - binding.layoutDetailsApp.btnPrimaryAction.setText(R.string.action_install) - } else { - binding.layoutDetailsApp.btnPrimaryAction.text = app.price - } - - binding.layoutDetailsApp.btnPrimaryAction.setOnClickListener { - if (authProvider.isAnonymous && !app.isFree) { - toast(R.string.toast_purchase_blocked) - return@setOnClickListener - } else if (app.versionCode == 0) { - toast(R.string.toast_app_unavailable) - return@setOnClickListener - } - - if (!permissionProvider.isGranted(PermissionType.INSTALL_UNKNOWN_APPS)) { - permissionProvider.request(PermissionType.INSTALL_UNKNOWN_APPS) { - if (it) { - startDownload() - } else { - toast(R.string.permissions_denied) - // TODO: Warn & redirect to Permission Manager - } - } - } else { - startDownload() + binding.layoutDetailsApp.apply { + txtLine3.text = PackageUtil.getInstalledVersion(requireContext(), app.packageName) + btnPrimaryAction.apply { + setText(R.string.action_open) + setOnClickListener { openApp() } } } } + + binding.layoutDetailsApp.btnSecondaryAction.apply { + text = getString(R.string.action_uninstall) + setOnClickListener { + AppInstaller.uninstall(requireContext(), app.packageName) + } + } + } else { + if (app.isFree) { + binding.layoutDetailsApp.btnPrimaryAction.setText(R.string.action_install) + } else { + binding.layoutDetailsApp.btnPrimaryAction.text = app.price + } + + binding.layoutDetailsApp.btnPrimaryAction.setOnClickListener { + if (authProvider.isAnonymous && !app.isFree) { + toast(R.string.toast_purchase_blocked) + return@setOnClickListener + } else if (app.versionCode == 0) { + toast(R.string.toast_app_unavailable) + return@setOnClickListener + } + + if (!permissionProvider.isGranted(PermissionType.INSTALL_UNKNOWN_APPS)) { + permissionProvider.request(PermissionType.INSTALL_UNKNOWN_APPS) { + if (it) { + purchase() + } else { + toast(R.string.permissions_denied) + } + } + } else { + purchase() + } + } + + binding.layoutDetailsApp.btnSecondaryAction.apply { + text = getString(R.string.title_manual_download) + setOnClickListener { + findNavController().navigate( + AppDetailsFragmentDirections + .actionAppDetailsFragmentToManualDownloadSheet(app) + ) + } + } } + + // Lay out the toolbar again + binding.toolbar.invalidateMenu() + + if (app.isInstalled) { + binding.toolbar.menu.apply { + findItem(R.id.action_home_screen)?.isVisible = + ShortcutManagerUtil.canPinShortcut(requireContext(), app.packageName) + findItem(R.id.action_uninstall)?.isVisible = true + findItem(R.id.menu_app_settings)?.isVisible = true + } + } else { + binding.toolbar.menu.apply { + findItem(R.id.action_home_screen)?.isVisible = false + findItem(R.id.action_uninstall)?.isVisible = false + findItem(R.id.menu_app_settings)?.isVisible = false + } + } + + // Restore icon and progress + updateProgress(100) } private fun updateExtraDetails(app: App) { diff --git a/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt index 367e61074..5e60fee9e 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt @@ -13,6 +13,7 @@ import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.ReviewsHelper import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper import com.aurora.gplayapi.network.IHttpClient +import com.aurora.store.AuroraApp import com.aurora.store.BuildConfig import com.aurora.store.R import com.aurora.store.data.helper.DownloadHelper @@ -27,8 +28,11 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.json.JSONObject import java.lang.reflect.Modifier @@ -75,7 +79,10 @@ class AppDetailsViewModel @Inject constructor( private val _favourite = MutableStateFlow(false) val favourite = _favourite.asStateFlow() - val downloadsList get() = downloadHelper.downloadsList + val download = combine(app, downloadHelper.downloadsList) { a, list -> + if (a.packageName.isBlank()) return@combine null + list.find { d -> d.packageName == a.packageName } + }.stateIn(viewModelScope, SharingStarted.Eagerly, null) fun fetchAppDetails(packageName: String) { viewModelScope.launch(Dispatchers.IO) { diff --git a/app/src/main/res/layout/layout_details_app.xml b/app/src/main/res/layout/layout_details_app.xml index 9f64b69ad..d04bb6b16 100644 --- a/app/src/main/res/layout/layout_details_app.xml +++ b/app/src/main/res/layout/layout_details_app.xml @@ -110,26 +110,22 @@ android:layout_marginTop="@dimen/margin_medium" android:divider="@drawable/divider" android:orientation="horizontal" - android:showDividers="middle"> + android:showDividers="middle" + android:weightSum="2"> + tools:text="@string/title_manual_download" />