AppDetailsFragment: Improvements to action buttons

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2024-12-09 11:06:26 +08:00
parent 41cf4d56ff
commit 733ed0acc2
3 changed files with 158 additions and 215 deletions

View File

@@ -48,7 +48,6 @@ import com.aurora.Constants
import com.aurora.Constants.EXODUS_SUBMIT_PAGE import com.aurora.Constants.EXODUS_SUBMIT_PAGE
import com.aurora.extensions.browse import com.aurora.extensions.browse
import com.aurora.extensions.hide import com.aurora.extensions.hide
import com.aurora.extensions.invisible
import com.aurora.extensions.px import com.aurora.extensions.px
import com.aurora.extensions.requiresObbDir import com.aurora.extensions.requiresObbDir
import com.aurora.extensions.runOnUiThread 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.AppDetailsViewModel
import com.aurora.store.viewmodel.details.DetailsClusterViewModel import com.aurora.store.viewmodel.details.DetailsClusterViewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@@ -115,11 +115,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
private val isExternal get() = activity?.intent?.action != Intent.ACTION_MAIN private val isExternal get() = activity?.intent?.action != Intent.ACTION_MAIN
private var downloadStatus = DownloadStatus.UNAVAILABLE
private var isUpdatable: Boolean = false
private val tags = mutableSetOf<String>()
private val isExtendedUpdateEnabled: Boolean private val isExtendedUpdateEnabled: Boolean
get() = Preferences.getBoolean(requireContext(), PREFERENCE_UPDATES_EXTENDED) get() = Preferences.getBoolean(requireContext(), PREFERENCE_UPDATES_EXTENDED)
private val showSimilarApps: Boolean private val showSimilarApps: Boolean
@@ -130,25 +125,12 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
is InstallerEvent.Installed -> { is InstallerEvent.Installed -> {
if (app.packageName == event.packageName) { if (app.packageName == event.packageName) {
checkAndSetupInstall() 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 -> { is InstallerEvent.Uninstalled -> {
if (app.packageName == event.packageName) { if (app.packageName == event.packageName) {
checkAndSetupInstall() 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<FragmentDetailsBinding>() {
} }
} }
viewLifecycleOwner.lifecycleScope.launch { viewModel.download.filterNotNull().onEach {
viewModel.downloadsList when (it.downloadStatus) {
.filter { list -> list.any { it.packageName == app.packageName } } DownloadStatus.QUEUED,
.collectLatest { downloadsList -> DownloadStatus.DOWNLOADING -> {
val download = downloadsList.find { it.packageName == app.packageName } updateProgress(it.progress)
download?.let { binding.layoutDetailsApp.btnPrimaryAction.apply {
downloadStatus = it.downloadStatus isEnabled = false
text = getString(R.string.action_open)
when (it.downloadStatus) { setOnClickListener { openApp() }
DownloadStatus.QUEUED -> { }
updateProgress(it.progress) binding.layoutDetailsApp.btnSecondaryAction.apply {
} text = getString(R.string.action_cancel)
setOnClickListener { viewModel.cancelDownload(app) }
DownloadStatus.DOWNLOADING -> {
updateSecondaryAction(true)
updateProgress(it.progress, it.speed, it.timeRemaining)
}
else -> {
transformIcon(false)
updateSecondaryAction(false)
}
}
} }
} }
}
else -> checkAndSetupInstall()
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
// Reviews // Reviews
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
@@ -499,8 +474,20 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
// Do not show tags for web apps or unknown apps // Do not show tags for web apps or unknown apps
if (isFullApp) { if (isFullApp) {
tags.add(getString((if (app.isFree) R.string.details_free else R.string.details_paid))) val tags = mutableSetOf<String>().apply {
tags.add(getString((if (app.containsAds) R.string.details_contains_ads else R.string.details_no_ads))) 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 = "") txtLine4.text = tags.joinToString(separator = "")
} }
} }
@@ -517,19 +504,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
} }
@Synchronized
private fun startDownload() {
when (downloadStatus) {
DownloadStatus.DOWNLOADING -> {
toast("Already downloading")
}
else -> {
purchase()
}
}
}
private fun purchase() { private fun purchase() {
if (app.fileList.requiresObbDir()) { if (app.fileList.requiresObbDir()) {
if (permissionProvider.isGranted(PermissionType.STORAGE_MANAGER)) { if (permissionProvider.isGranted(PermissionType.STORAGE_MANAGER)) {
@@ -546,82 +520,33 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
} }
private fun updateProgress(progress: Int, speed: Long = -1, timeRemaining: Long = -1) { private fun updateProgress(progress: Int) {
runOnUiThread { // No need to update progress if it is already 100% / completed
updatePrimaryAction(false) transformIcon(progress != 100)
updateSecondaryAction(true) if (progress == 100) return
if (progress == 100) { binding.layoutDetailsApp.apply {
transformIcon(false) progressDownload.progress = progress
binding.layoutDetailsApp.apply { progressDownload.isIndeterminate = progress < 1
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)
}
}
} }
} }
private fun transformIcon(ongoing: Boolean = false) { private fun transformIcon(ongoing: Boolean = false) {
if (::iconDrawable.isInitialized.not()) return if (::iconDrawable.isInitialized.not()) return
val scaleFactor = if (ongoing) 0.75f else 1f val imgIcon = binding.layoutDetailsApp.imgIcon
val isDownloadVisible = binding.layoutDetailsApp.progressDownload.isShown val progressDownload = binding.layoutDetailsApp.progressDownload
// Avoids flickering when the download is in progress // Avoids flickering when the download is in progress
if (isDownloadVisible && scaleFactor != 1f) if (progressDownload.isShown && ongoing) return
return if (!progressDownload.isShown && !ongoing) return
if (!isDownloadVisible && scaleFactor == 1f) binding.layoutDetailsApp.progressDownload.isVisible = ongoing
return
if (scaleFactor == 1f) {
binding.layoutDetailsApp.progressDownload.invisible()
} else {
binding.layoutDetailsApp.progressDownload.show()
}
val scaleFactor = if (ongoing) 0.75f else 1f
val scale = listOf( val scale = listOf(
ObjectAnimator.ofFloat(binding.layoutDetailsApp.imgIcon, "scaleX", scaleFactor), ObjectAnimator.ofFloat(imgIcon, "scaleX", scaleFactor),
ObjectAnimator.ofFloat(binding.layoutDetailsApp.imgIcon, "scaleY", scaleFactor) ObjectAnimator.ofFloat(imgIcon, "scaleY", scaleFactor)
) )
scale.forEach { animation -> scale.forEach { animation ->
@@ -632,103 +557,118 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
} }
iconDrawable.let { imgIcon.load(iconDrawable) {
binding.layoutDetailsApp.imgIcon.load(it) { transformations(
transformations( if (ongoing) {
if (scaleFactor == 1f) CircleCropTransformation()
RoundedCornersTransformation(8.px.toFloat()) } else {
else RoundedCornersTransformation(8.px.toFloat())
CircleCropTransformation() }
) )
}
} }
} }
@Synchronized
private fun checkAndSetupInstall() { private fun checkAndSetupInstall() {
runOnUiThread { app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName)
app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName)
if (app.isInstalled) { // Setup primary and secondary action buttons
val needsExtendedUpdate = !app.certificateSetList.any { binding.layoutDetailsApp.btnPrimaryAction.isEnabled = true
it.certificateSet in CertUtil.getEncodedCertificateHashes( binding.layoutDetailsApp.btnPrimaryAction.isEnabled = true
requireContext(),
app.packageName
)
}
isUpdatable = PackageUtil.isUpdatable( if (app.isInstalled) {
requireContext(), val isUpdatable = PackageUtil.isUpdatable(requireContext(), app.packageName, app.versionCode.toLong())
app.packageName, val hasValidCert = app.certificateSetList.any {
app.versionCode.toLong() it.certificateSet in CertUtil.getEncodedCertificateHashes(requireContext(), app.packageName)
) }
val installedVersion = PackageUtil.getInstalledVersion( if ((isUpdatable && hasValidCert) || (isUpdatable && isExtendedUpdateEnabled)) {
requireContext(), binding.layoutDetailsApp.btnPrimaryAction.apply {
app.packageName text = getString(R.string.action_update)
) setOnClickListener {
if (app.versionCode == 0) {
if ((isUpdatable && !needsExtendedUpdate) || (isUpdatable && isExtendedUpdateEnabled)) { toast(R.string.toast_app_unavailable)
binding.layoutDetailsApp.apply { setText(R.string.status_unavailable)
txtLine3.text = } else {
("$installedVersion${app.versionName} (${app.versionCode})") purchase()
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() }
} }
} }
} }
} else { } else {
if (downloadStatus in DownloadStatus.running) { binding.layoutDetailsApp.apply {
updateProgress(-1) txtLine3.text = PackageUtil.getInstalledVersion(requireContext(), app.packageName)
} else if (app.isFree) { btnPrimaryAction.apply {
binding.layoutDetailsApp.btnPrimaryAction.setText(R.string.action_install) setText(R.string.action_open)
} else { setOnClickListener { openApp() }
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.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) { private fun updateExtraDetails(app: App) {

View File

@@ -13,6 +13,7 @@ import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.gplayapi.helpers.ReviewsHelper import com.aurora.gplayapi.helpers.ReviewsHelper
import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper
import com.aurora.gplayapi.network.IHttpClient import com.aurora.gplayapi.network.IHttpClient
import com.aurora.store.AuroraApp
import com.aurora.store.BuildConfig import com.aurora.store.BuildConfig
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.helper.DownloadHelper import com.aurora.store.data.helper.DownloadHelper
@@ -27,8 +28,11 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.json.JSONObject import org.json.JSONObject
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
@@ -75,7 +79,10 @@ class AppDetailsViewModel @Inject constructor(
private val _favourite = MutableStateFlow<Boolean>(false) private val _favourite = MutableStateFlow<Boolean>(false)
val favourite = _favourite.asStateFlow() 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) { fun fetchAppDetails(packageName: String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {

View File

@@ -110,26 +110,22 @@
android:layout_marginTop="@dimen/margin_medium" android:layout_marginTop="@dimen/margin_medium"
android:divider="@drawable/divider" android:divider="@drawable/divider"
android:orientation="horizontal" android:orientation="horizontal"
android:showDividers="middle"> android:showDividers="middle"
android:weightSum="2">
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btn_secondary_action" android:id="@+id/btn_secondary_action"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:backgroundTint="?colorError" tools:text="@string/title_manual_download" />
android:textColor="?colorOnError"
android:visibility="gone"
app:cornerRadius="@dimen/radius_small"
tools:text="@string/action_cancel"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btn_primary_action" android:id="@+id/btn_primary_action"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
app:cornerRadius="@dimen/radius_small"
tools:text="@string/action_install" /> tools:text="@string/action_install" />
</LinearLayout> </LinearLayout>