From 5209620cc4bf32af1c4b021d20e3234fda3864e5 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Sun, 1 Dec 2024 03:18:13 +0530 Subject: [PATCH 01/39] AppDetails: Move action buttons to top of page Signed-off-by: Aayush Gupta --- .../model/{Enumerations.kt => AccountType.kt} | 9 - .../custom/layouts/button/ActionButton.kt | 109 ----- .../view/ui/details/AppDetailsFragment.kt | 378 ++++++++++-------- app/src/main/res/drawable/bg_bottomsheet.xml | 29 -- app/src/main/res/drawable/bg_cancel.xml | 25 -- .../main/res/drawable/bg_rounded_outlined.xml | 27 -- .../res/drawable/bg_rounded_transparent.xml | 5 - app/src/main/res/drawable/bg_sheet.xml | 27 -- .../res/drawable/filter_chip_background.xml | 12 - app/src/main/res/layout/fragment_details.xml | 4 - .../main/res/layout/layout_details_app.xml | 62 ++- .../res/layout/layout_details_install.xml | 137 ------- .../main/res/layout/view_action_button.xml | 68 ---- app/src/main/res/values/attrs.xml | 8 +- 14 files changed, 263 insertions(+), 637 deletions(-) rename app/src/main/java/com/aurora/store/data/model/{Enumerations.kt => AccountType.kt} (89%) delete mode 100644 app/src/main/java/com/aurora/store/view/custom/layouts/button/ActionButton.kt delete mode 100644 app/src/main/res/drawable/bg_bottomsheet.xml delete mode 100644 app/src/main/res/drawable/bg_cancel.xml delete mode 100644 app/src/main/res/drawable/bg_rounded_outlined.xml delete mode 100644 app/src/main/res/drawable/bg_rounded_transparent.xml delete mode 100644 app/src/main/res/drawable/bg_sheet.xml delete mode 100644 app/src/main/res/drawable/filter_chip_background.xml delete mode 100644 app/src/main/res/layout/layout_details_install.xml delete mode 100644 app/src/main/res/layout/view_action_button.xml diff --git a/app/src/main/java/com/aurora/store/data/model/Enumerations.kt b/app/src/main/java/com/aurora/store/data/model/AccountType.kt similarity index 89% rename from app/src/main/java/com/aurora/store/data/model/Enumerations.kt rename to app/src/main/java/com/aurora/store/data/model/AccountType.kt index 9cb35a6c4..a3f59acc4 100644 --- a/app/src/main/java/com/aurora/store/data/model/Enumerations.kt +++ b/app/src/main/java/com/aurora/store/data/model/AccountType.kt @@ -23,12 +23,3 @@ enum class AccountType { ANONYMOUS, GOOGLE } - -enum class State { - IDLE, - QUEUED, - PROGRESS, - COMPLETE, - CANCELED, - INSTALLING, -} 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 deleted file mode 100644 index a77395be2..000000000 --- a/app/src/main/java/com/aurora/store/view/custom/layouts/button/ActionButton.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.custom.layouts.button - -import android.content.Context -import android.content.res.ColorStateList -import android.util.AttributeSet -import android.widget.RelativeLayout -import androidx.core.content.ContextCompat -import com.aurora.store.R -import com.aurora.store.data.model.State -import com.aurora.store.databinding.ViewActionButtonBinding - -class ActionButton : RelativeLayout { - - private lateinit var binding: ViewActionButtonBinding - - constructor(context: Context) : super(context) { - init(context, null) - } - - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { - init(context, attrs) - } - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) { - init(context, attrs) - } - - private fun init(context: Context, attrs: AttributeSet?) { - val view = inflate(context, R.layout.view_action_button, this) - binding = ViewActionButtonBinding.bind(view) - - val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ActionButton) - val btnTxt = typedArray.getString(R.styleable.ActionButton_btnActionText) - - val btnTxtColor = typedArray.getResourceId( - R.styleable.ActionButton_btnActionTextColor, - R.color.colorWhite - ) - - val stateIcon = typedArray.getResourceId( - R.styleable.ActionButton_btnActionIcon, - R.drawable.ic_check - ) - - val stateColor = ContextCompat.getColor(context, btnTxtColor) - - binding.btn.text = btnTxt - binding.btn.setTextColor(stateColor) - binding.img.setImageDrawable(ContextCompat.getDrawable(context, stateIcon)) - binding.img.imageTintList = ColorStateList.valueOf(stateColor) - - typedArray.recycle() - } - - fun setText(text: String) { - binding.viewFlipper.displayedChild = 0 - binding.btn.text = text - } - - fun setText(text: Int) { - binding.viewFlipper.displayedChild = 0 - binding.btn.text = ContextCompat.getString(context, text) - } - - fun setButtonState(enabled: Boolean = true) { - binding.btn.isEnabled = enabled - } - - fun updateState(state: State) { - val displayChild = when (state) { - State.PROGRESS -> 1 - State.COMPLETE -> 2 - else -> 0 - } - - if (binding.viewFlipper.displayedChild != displayChild) { - binding.viewFlipper.displayedChild = displayChild - - if (displayChild == 2) updateState(State.IDLE) - } - } - - fun addOnClickListener(onClickListener: OnClickListener?) { - binding.btn.setOnClickListener(onClickListener) - } -} 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 7a2b96c83..84c1bc998 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 @@ -20,33 +20,40 @@ package com.aurora.store.view.ui.details +import android.animation.ObjectAnimator import android.content.ActivityNotFoundException import android.content.Intent +import android.graphics.drawable.Drawable import android.net.Uri import android.os.Bundle import android.provider.Settings import android.view.View import android.view.ViewGroup -import android.widget.LinearLayout +import android.view.animation.AccelerateDecelerateInterpolator import android.widget.RelativeLayout import android.widget.Toast import androidx.core.content.ContextCompat import androidx.core.text.HtmlCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import coil3.asDrawable import coil3.load import coil3.request.placeholder import coil3.request.transformations +import coil3.transform.CircleCropTransformation import coil3.transform.RoundedCornersTransformation 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 import com.aurora.extensions.share @@ -66,7 +73,6 @@ import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.installer.AppInstaller import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.model.PermissionType -import com.aurora.store.data.model.State import com.aurora.store.data.model.ViewState import com.aurora.store.data.model.ViewState.Loading.getDataAs import com.aurora.store.data.providers.AuthProvider @@ -75,6 +81,8 @@ import com.aurora.store.util.CertUtil import com.aurora.store.util.CommonUtil import com.aurora.store.util.PackageUtil import com.aurora.store.util.Preferences +import com.aurora.store.util.Preferences.PREFERENCE_SIMILAR +import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED import com.aurora.store.util.ShortcutManagerUtil import com.aurora.store.view.custom.RatingView import com.aurora.store.view.epoxy.controller.DetailsCarouselController @@ -85,8 +93,6 @@ import com.aurora.store.view.epoxy.views.details.ScreenshotViewModel_ import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.viewmodel.details.AppDetailsViewModel import com.aurora.store.viewmodel.details.DetailsClusterViewModel -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter @@ -106,8 +112,8 @@ class AppDetailsFragment : BaseFragment() { @Inject lateinit var authProvider: AuthProvider - private lateinit var bottomSheetBehavior: BottomSheetBehavior private lateinit var app: App + private lateinit var iconDrawable: Drawable private var streamBundle: StreamBundle? = StreamBundle() @@ -117,11 +123,19 @@ class AppDetailsFragment : BaseFragment() { private var isUpdatable: Boolean = false private var uninstallActionEnabled = false + private val tags = mutableListOf() + + private val isExtendedUpdateEnabled: Boolean + get() = Preferences.getBoolean(requireContext(), PREFERENCE_UPDATES_EXTENDED) + private val showSimilarApps: Boolean + get() = Preferences.getBoolean(requireContext(), PREFERENCE_SIMILAR) + private fun onEvent(event: Event) { when (event) { is InstallerEvent.Installed -> { if (app.packageName == event.packageName) { - attachActions() + checkAndSetupInstall() + transformIcon(false) binding.layoutDetailsToolbar.toolbar.menu.apply { findItem(R.id.action_home_screen)?.isVisible = ShortcutManagerUtil.canPinShortcut(requireContext(), app.packageName) @@ -133,7 +147,8 @@ class AppDetailsFragment : BaseFragment() { is InstallerEvent.Uninstalled -> { if (app.packageName == event.packageName) { - attachActions() + checkAndSetupInstall() + transformIcon(false) binding.layoutDetailsToolbar.toolbar.menu.apply { findItem(R.id.action_home_screen)?.isVisible = false findItem(R.id.action_uninstall)?.isVisible = false @@ -164,8 +179,7 @@ class AppDetailsFragment : BaseFragment() { is InstallerEvent.Installing -> { if (event.packageName == app.packageName) { - attachActions() - updateActionState(State.INSTALLING) + checkAndSetupInstall() } } @@ -178,23 +192,6 @@ class AppDetailsFragment : BaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // Adjust margins for edgeToEdge display - ViewCompat.setOnApplyWindowInsetsListener(binding.layoutDetailsDev.root) { v, w -> - val insets = w.getInsets(WindowInsetsCompat.Type.navigationBars()) - v.updateLayoutParams { - bottomMargin += insets.bottom - } - WindowInsetsCompat.CONSUMED - } - - ViewCompat.setOnApplyWindowInsetsListener(binding.layoutDetailsInstall.viewFlipper) { v, w -> - val insets = w.getInsets(WindowInsetsCompat.Type.navigationBars()) - v.updateLayoutParams { - bottomMargin += insets.bottom - } - WindowInsetsCompat.CONSUMED - } - if (args.app != null) { app = args.app!! inflatePartialApp() @@ -232,17 +229,20 @@ class AppDetailsFragment : BaseFragment() { download?.let { downloadStatus = it.downloadStatus - if (it.isFinished) flip(0) else flip(1) when (it.downloadStatus) { DownloadStatus.QUEUED -> { updateProgress(it.progress) } DownloadStatus.DOWNLOADING -> { + updateSecondaryAction(true) updateProgress(it.progress, it.speed, it.timeRemaining) } - else -> {} + else -> { + transformIcon(false) + updateSecondaryAction(false) + } } } } @@ -368,12 +368,6 @@ class AppDetailsFragment : BaseFragment() { } } - binding.layoutDetailsInstall.progressDownload.clipToOutline = true - binding.layoutDetailsInstall.imgCancel.setOnClickListener { - viewModel.cancelDownload(app) - if (downloadStatus != DownloadStatus.DOWNLOADING) flip(0) - } - viewLifecycleOwner.lifecycleScope.launch { AuroraApp.events.busEvent.collect { onEvent(it) } } @@ -387,11 +381,6 @@ class AppDetailsFragment : BaseFragment() { super.onResume() } - private fun attachActions() { - flip(0) - checkAndSetupInstall() - } - private fun attachToolbar() { binding.layoutDetailsToolbar.toolbar.apply { elevation = 0f @@ -475,6 +464,9 @@ class AppDetailsFragment : BaseFragment() { imgIcon.load(app.iconArtwork.url) { placeholder(R.drawable.bg_placeholder) transformations(RoundedCornersTransformation(32F)) + listener { _, result -> + result.image.asDrawable(resources).let { iconDrawable = it } + } } txtLine1.text = app.displayName @@ -488,7 +480,7 @@ class AppDetailsFragment : BaseFragment() { txtLine3.text = ("${app.versionName} (${app.versionCode})") packageName.text = app.packageName - val tags = mutableListOf() + if (app.isFree) tags.add(getString(R.string.details_free)) else @@ -503,40 +495,6 @@ class AppDetailsFragment : BaseFragment() { } } - private fun attachBottomSheet() { - binding.layoutDetailsInstall.apply { - viewFlipper.setInAnimation(requireContext(), R.anim.fade_in) - viewFlipper.setOutAnimation(requireContext(), R.anim.fade_out) - } - - bottomSheetBehavior = BottomSheetBehavior.from(binding.layoutDetailsInstall.bottomSheet) - bottomSheetBehavior.isDraggable = false - - bottomSheetBehavior.addBottomSheetCallback(object : BottomSheetCallback() { - override fun onStateChanged(bottomSheet: View, newState: Int) { - if (newState == BottomSheetBehavior.STATE_EXPANDED) { - bottomSheetBehavior.setDraggable(true) - } else if (newState == BottomSheetBehavior.STATE_COLLAPSED) { - bottomSheetBehavior.isDraggable = false - } - } - - override fun onSlide(bottomSheet: View, slideOffset: Float) {} - }) - } - - private fun updateActionState(state: State) { - runOnUiThread { - binding.layoutDetailsInstall.btnDownload.apply { - updateState(state) - if (state == State.INSTALLING) { - setButtonState(false) - setText(R.string.action_installing) - } - } - } - } - private fun openApp() { val intent = PackageUtil.getLaunchIntent(requireContext(), app.packageName) if (intent != null) { @@ -552,28 +510,24 @@ class AppDetailsFragment : BaseFragment() { private fun startDownload() { when (downloadStatus) { DownloadStatus.DOWNLOADING -> { - flip(1) toast("Already downloading") } else -> { - flip(1) purchase() } } } private fun purchase() { - bottomSheetBehavior.isHideable = false - bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED - updateActionState(State.PROGRESS) - if (app.fileList.requiresObbDir()) { if (permissionProvider.isGranted(PermissionType.STORAGE_MANAGER)) { viewModel.download(app) } else { permissionProvider.request(PermissionType.STORAGE_MANAGER) { - if (it) viewModel.download(app) else flip(0) + if (it) viewModel.download(app) else { + // TODO: Ask for permission again or redirect to Permission Manager + } } } } else { @@ -583,108 +537,191 @@ class AppDetailsFragment : BaseFragment() { private fun updateProgress(progress: Int, speed: Long = -1, timeRemaining: Long = -1) { runOnUiThread { + updatePrimaryAction(false) + updateSecondaryAction(true) + if (progress == 100) { - binding.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing)) + transformIcon(false) + binding.layoutDetailsApp.apply { + txtLine3.text = ("${app.versionName} (${app.versionCode})") + txtLine4.text = tags.joinToString(separator = " • ") + } return@runOnUiThread } - binding.layoutDetailsInstall.apply { - txtProgressPercent.text = ("${progress}%") - progressDownload.apply { - this.progress = progress - isIndeterminate = progress < 1 + 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) } - txtEta.text = CommonUtil.getETAString(requireContext(), timeRemaining) - txtSpeed.text = CommonUtil.getDownloadSpeedString(requireContext(), speed) } } } - private fun checkAndSetupInstall() { - app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) - - runOnUiThread { - binding.layoutDetailsInstall.btnDownload.let { btn -> - btn.setButtonState(true) - if (app.isInstalled) { - val isExtendedUpdateEnabled = Preferences.getBoolean( - requireContext(), Preferences.PREFERENCE_UPDATES_EXTENDED - ) - val needsExtendedUpdate = !app.certificateSetList.any { - it.certificateSet in CertUtil.getEncodedCertificateHashes( - requireContext(), app.packageName - ) - } - isUpdatable = PackageUtil.isUpdatable( - requireContext(), - app.packageName, - app.versionCode.toLong() - ) - - val installedVersion = - PackageUtil.getInstalledVersion(requireContext(), app.packageName) - - if (isUpdatable && !needsExtendedUpdate || isUpdatable && isExtendedUpdateEnabled) { - binding.layoutDetailsApp.txtLine3.text = - ("$installedVersion ➔ ${app.versionName} (${app.versionCode})") - btn.setText(R.string.action_update) - btn.addOnClickListener { - if (app.versionCode == 0) { - toast(R.string.toast_app_unavailable) - } else { - startDownload() - } - } - } else { - binding.layoutDetailsApp.txtLine3.text = installedVersion - btn.setText(R.string.action_open) - btn.addOnClickListener { openApp() } - } - if (!uninstallActionEnabled) { - binding.layoutDetailsToolbar.toolbar.invalidateMenu() - } - } else { - if (downloadStatus in DownloadStatus.running) { - flip(1) - } else if (app.isFree) { - btn.setText(R.string.action_install) - } else { - btn.setText(app.price) - } - - btn.addOnClickListener { - if (!permissionProvider.isGranted(PermissionType.INSTALL_UNKNOWN_APPS)) { - permissionProvider.request(PermissionType.INSTALL_UNKNOWN_APPS) { - if (it) { - btn.setText(R.string.download_metadata) - startDownload() - } - } - } else if (authProvider.isAnonymous && !app.isFree) { - toast(R.string.toast_purchase_blocked) - } else if (app.versionCode == 0) { - toast(R.string.toast_app_unavailable) - } else { - btn.setText(R.string.download_metadata) - startDownload() - } - } - - if (uninstallActionEnabled) { - binding.layoutDetailsToolbar.toolbar.invalidateMenu() - } - } + 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 flip(nextView: Int) { + private fun updateSecondaryAction(enabled: Boolean = false) { runOnUiThread { - val displayChild = binding.layoutDetailsInstall.viewFlipper.displayedChild - if (displayChild != nextView) { - binding.layoutDetailsInstall.viewFlipper.displayedChild = nextView - if (nextView == 0) checkAndSetupInstall() + 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) { + val scaleFactor = if (ongoing) 0.75f else 1f + val isDownloadVisible = binding.layoutDetailsApp.progressDownload.isShown + + // Avoids flickering when the download is in progress + if (isDownloadVisible && scaleFactor != 1f) + return + + if (!isDownloadVisible && scaleFactor == 1f) + return + + if (scaleFactor == 1f) { + binding.layoutDetailsApp.progressDownload.invisible() + } else { + binding.layoutDetailsApp.progressDownload.show() + } + + val scale = listOf( + ObjectAnimator.ofFloat(binding.layoutDetailsApp.imgIcon, "scaleX", scaleFactor), + ObjectAnimator.ofFloat(binding.layoutDetailsApp.imgIcon, "scaleY", scaleFactor) + ) + + scale.forEach { animation -> + animation.apply { + interpolator = AccelerateDecelerateInterpolator() + duration = 250 + start() + } + } + + iconDrawable?.let { + binding.layoutDetailsApp.imgIcon.load(it) { + transformations( + if (scaleFactor == 1f) + RoundedCornersTransformation(8.px.toFloat()) + else + CircleCropTransformation() + ) + } + } + } + + @Synchronized + private fun checkAndSetupInstall() { + runOnUiThread { + app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) + + if (app.isInstalled) { + val needsExtendedUpdate = !app.certificateSetList.any { + it.certificateSet in CertUtil.getEncodedCertificateHashes( + requireContext(), + app.packageName + ) + } + + isUpdatable = PackageUtil.isUpdatable( + requireContext(), + app.packageName, + app.versionCode.toLong() + ) + + 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 (!uninstallActionEnabled) { + binding.layoutDetailsToolbar.toolbar.invalidateMenu() + } + } 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() + } + } + + if (uninstallActionEnabled) { + binding.layoutDetailsToolbar.toolbar.invalidateMenu() + } } } } @@ -692,8 +729,7 @@ class AppDetailsFragment : BaseFragment() { private fun inflatePartialApp() { if (::app.isInitialized) { attachHeader() - attachBottomSheet() - attachActions() + checkAndSetupInstall() } } @@ -717,7 +753,7 @@ class AppDetailsFragment : BaseFragment() { inflateBetaSubscription(app) } - if (Preferences.getBoolean(requireContext(), Preferences.PREFERENCE_SIMILAR)) { + if (showSimilarApps) { inflateAppStream(app) } } diff --git a/app/src/main/res/drawable/bg_bottomsheet.xml b/app/src/main/res/drawable/bg_bottomsheet.xml deleted file mode 100644 index aa2832c4a..000000000 --- a/app/src/main/res/drawable/bg_bottomsheet.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_cancel.xml b/app/src/main/res/drawable/bg_cancel.xml deleted file mode 100644 index c5d625d38..000000000 --- a/app/src/main/res/drawable/bg_cancel.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/bg_rounded_outlined.xml b/app/src/main/res/drawable/bg_rounded_outlined.xml deleted file mode 100644 index 41c3e6a78..000000000 --- a/app/src/main/res/drawable/bg_rounded_outlined.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/bg_rounded_transparent.xml b/app/src/main/res/drawable/bg_rounded_transparent.xml deleted file mode 100644 index 49c544924..000000000 --- a/app/src/main/res/drawable/bg_rounded_transparent.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/bg_sheet.xml b/app/src/main/res/drawable/bg_sheet.xml deleted file mode 100644 index 29774bd74..000000000 --- a/app/src/main/res/drawable/bg_sheet.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/filter_chip_background.xml b/app/src/main/res/drawable/filter_chip_background.xml deleted file mode 100644 index 78ec47234..000000000 --- a/app/src/main/res/drawable/filter_chip_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml index 77ae231ef..21ff5af44 100644 --- a/app/src/main/res/layout/fragment_details.xml +++ b/app/src/main/res/layout/fragment_details.xml @@ -119,8 +119,4 @@ - - diff --git a/app/src/main/res/layout/layout_details_app.xml b/app/src/main/res/layout/layout_details_app.xml index 9c908cf0c..0b00e301f 100644 --- a/app/src/main/res/layout/layout_details_app.xml +++ b/app/src/main/res/layout/layout_details_app.xml @@ -18,6 +18,7 @@ --> - + + + + + + + @@ -84,4 +102,34 @@ android:layout_alignEnd="@id/txt_line1" android:textAppearance="@style/TextAppearance.Aurora.Line2" tools:text="Free" /> - \ No newline at end of file + + + + + + + + + diff --git a/app/src/main/res/layout/layout_details_install.xml b/app/src/main/res/layout/layout_details_install.xml deleted file mode 100644 index fcb8fc683..000000000 --- a/app/src/main/res/layout/layout_details_install.xml +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/view_action_button.xml b/app/src/main/res/layout/view_action_button.xml deleted file mode 100644 index 8c0177610..000000000 --- a/app/src/main/res/layout/view_action_button.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 17f8959e7..abde5e933 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -28,12 +28,6 @@ - - - - - - @@ -48,4 +42,4 @@ - \ No newline at end of file + From faa417d8cd36488bd5bf7235b702c83e6704f5a5 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Wed, 4 Dec 2024 19:32:19 +0530 Subject: [PATCH 02/39] AppDetails: Tweak tags UI --- .../view/ui/details/AppDetailsFragment.kt | 6 +- .../res/layout/layout_details_description.xml | 89 +++++++------------ app/src/main/res/layout/sheet_filter.xml | 6 +- app/src/main/res/values-night/themes.xml | 2 +- app/src/main/res/values/themes.xml | 11 ++- 5 files changed, 45 insertions(+), 69 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 84c1bc998..1b259f7db 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 @@ -28,16 +28,12 @@ import android.net.Uri import android.os.Bundle import android.provider.Settings import android.view.View -import android.view.ViewGroup import android.view.animation.AccelerateDecelerateInterpolator import android.widget.RelativeLayout import android.widget.Toast import androidx.core.content.ContextCompat import androidx.core.text.HtmlCompat -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController @@ -123,7 +119,7 @@ class AppDetailsFragment : BaseFragment() { private var isUpdatable: Boolean = false private var uninstallActionEnabled = false - private val tags = mutableListOf() + private val tags = mutableSetOf() private val isExtendedUpdateEnabled: Boolean get() = Preferences.getBoolean(requireContext(), PREFERENCE_UPDATES_EXTENDED) diff --git a/app/src/main/res/layout/layout_details_description.xml b/app/src/main/res/layout/layout_details_description.xml index 434963328..6b3b1f0d2 100644 --- a/app/src/main/res/layout/layout_details_description.xml +++ b/app/src/main/res/layout/layout_details_description.xml @@ -33,72 +33,45 @@ android:layout_height="wrap_content" android:orientation="vertical"> - + android:paddingStart="@dimen/padding_medium" + android:paddingEnd="@dimen/padding_medium" + app:selectionRequired="false" + app:singleLine="true"> - + app:chipIcon="@drawable/ic_star" + tools:text="3.5" /> - + - + - - - - - + + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index b12707032..c70d8103a 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -2,7 +2,7 @@ diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 01191ab05..fcebd0b0f 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -20,7 +20,7 @@ - From a152f4acb90d411ecdb825bd371e93531da3533d Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Wed, 4 Dec 2024 19:56:48 +0530 Subject: [PATCH 03/39] TopCharts: Update chip stype to M3 --- app/src/main/res/layout/fragment_top_chart.xml | 8 ++++---- app/src/main/res/values/themes.xml | 11 ++++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/src/main/res/layout/fragment_top_chart.xml b/app/src/main/res/layout/fragment_top_chart.xml index b23743fe9..22bc4327c 100644 --- a/app/src/main/res/layout/fragment_top_chart.xml +++ b/app/src/main/res/layout/fragment_top_chart.xml @@ -42,14 +42,14 @@ @@ -57,14 +57,14 @@ diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index fcebd0b0f..b6ed8cd79 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -49,10 +49,15 @@ + + From 52c1acf6d4a26bc065a39486a8357dd344ad6cff Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Wed, 4 Dec 2024 20:06:19 +0530 Subject: [PATCH 04/39] TopCharts: Chip style update --- app/src/main/res/layout/fragment_top_chart.xml | 3 +-- app/src/main/res/values/themes.xml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/fragment_top_chart.xml b/app/src/main/res/layout/fragment_top_chart.xml index 22bc4327c..418e97b82 100644 --- a/app/src/main/res/layout/fragment_top_chart.xml +++ b/app/src/main/res/layout/fragment_top_chart.xml @@ -33,8 +33,7 @@ android:id="@+id/top_tab_group" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:paddingStart="@dimen/padding_normal" - android:paddingEnd="@dimen/padding_normal" + android:padding="@dimen/padding_medium" app:chipSpacingHorizontal="@dimen/margin_small" app:selectionRequired="true" app:singleLine="true" diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index b6ed8cd79..2736c408e 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -50,7 +50,7 @@ From bd231a1d7b7621131ebc72728ddeedac7a984995 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Wed, 4 Dec 2024 20:12:06 +0530 Subject: [PATCH 05/39] AppDetails: Do not animate icon, if there is no icon --- .../com/aurora/store/view/ui/details/AppDetailsFragment.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 1b259f7db..85b50ec8a 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 @@ -586,6 +586,8 @@ class AppDetailsFragment : BaseFragment() { } 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 @@ -615,7 +617,7 @@ class AppDetailsFragment : BaseFragment() { } } - iconDrawable?.let { + iconDrawable.let { binding.layoutDetailsApp.imgIcon.load(it) { transformations( if (scaleFactor == 1f) From c2f6f6d9d41715a85f0290eeee7aa3e661cdd37f Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Wed, 4 Dec 2024 20:18:29 +0530 Subject: [PATCH 06/39] AppDetails: Make tags scrollable --- .../res/layout/layout_details_description.xml | 71 ++++++++++--------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/app/src/main/res/layout/layout_details_description.xml b/app/src/main/res/layout/layout_details_description.xml index 6b3b1f0d2..fb5269118 100644 --- a/app/src/main/res/layout/layout_details_description.xml +++ b/app/src/main/res/layout/layout_details_description.xml @@ -33,45 +33,52 @@ android:layout_height="wrap_content" android:orientation="vertical"> - + android:scrollbars="none"> - + android:paddingStart="@dimen/padding_medium" + android:paddingEnd="@dimen/padding_medium" + app:selectionRequired="false" + app:singleLine="true"> - + - + - - + + + + + + Date: Thu, 5 Dec 2024 19:25:16 +0530 Subject: [PATCH 07/39] InstalledApps: Minor tweak --- .../com/aurora/store/data/model/MinimalApp.kt | 35 ++++++++++++++++--- ...PackageInfoView.kt => InstalledAppView.kt} | 17 ++++----- .../store/view/ui/all/AppsGamesFragment.kt | 19 +++++----- .../store/viewmodel/all/InstalledViewModel.kt | 4 ++- 4 files changed, 52 insertions(+), 23 deletions(-) rename app/src/main/java/com/aurora/store/view/epoxy/views/{PackageInfoView.kt => InstalledAppView.kt} (75%) diff --git a/app/src/main/java/com/aurora/store/data/model/MinimalApp.kt b/app/src/main/java/com/aurora/store/data/model/MinimalApp.kt index 28d1914c0..a3e0a648c 100644 --- a/app/src/main/java/com/aurora/store/data/model/MinimalApp.kt +++ b/app/src/main/java/com/aurora/store/data/model/MinimalApp.kt @@ -2,34 +2,61 @@ package com.aurora.store.data.model import android.content.Context import android.content.pm.PackageInfo +import android.graphics.Bitmap +import android.graphics.drawable.Drawable import android.os.Parcelable import androidx.core.content.pm.PackageInfoCompat import com.aurora.gplayapi.data.models.App import com.aurora.store.data.room.update.Update +import com.aurora.store.util.PackageUtil +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Parcelize data class MinimalApp( val packageName: String, + val versionName: String, val versionCode: Int, - val displayName: String + val displayName: String, + @IgnoredOnParcel + val icon: Bitmap? = null ) : Parcelable { companion object { fun fromApp(app: App): MinimalApp { - return MinimalApp(app.packageName, app.versionCode, app.displayName) + return MinimalApp( + app.packageName, + app.versionName, + app.versionCode, + app.displayName + ) + } + + fun toApp(minimalApp: MinimalApp): App { + return App(minimalApp.packageName).apply { + versionName = minimalApp.versionName ?: "" + versionCode = minimalApp.versionCode + displayName = minimalApp.displayName + } } fun fromUpdate(update: Update): MinimalApp { - return MinimalApp(update.packageName, update.versionCode, update.displayName) + return MinimalApp( + update.packageName, + update.versionName, + update.versionCode, + update.displayName + ) } fun fromPackageInfo(context: Context, packageInfo: PackageInfo): MinimalApp { return MinimalApp( packageInfo.packageName, + packageInfo.versionName ?: "", PackageInfoCompat.getLongVersionCode(packageInfo).toInt(), - packageInfo.applicationInfo!!.loadLabel(context.packageManager).toString() + packageInfo.applicationInfo!!.loadLabel(context.packageManager).toString(), + PackageUtil.getIconForPackage(context, packageInfo.packageName) ) } } diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/PackageInfoView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/InstalledAppView.kt similarity index 75% rename from app/src/main/java/com/aurora/store/view/epoxy/views/PackageInfoView.kt rename to app/src/main/java/com/aurora/store/view/epoxy/views/InstalledAppView.kt index 37a7cd8ba..efaa492c4 100644 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/PackageInfoView.kt +++ b/app/src/main/java/com/aurora/store/view/epoxy/views/InstalledAppView.kt @@ -20,9 +20,7 @@ package com.aurora.store.view.epoxy.views import android.content.Context -import android.content.pm.PackageInfo import android.util.AttributeSet -import androidx.core.content.pm.PackageInfoCompat import coil3.load import coil3.request.placeholder import coil3.request.transformations @@ -31,30 +29,29 @@ import com.airbnb.epoxy.CallbackProp import com.airbnb.epoxy.ModelProp import com.airbnb.epoxy.ModelView import com.aurora.store.R +import com.aurora.store.data.model.MinimalApp import com.aurora.store.databinding.ViewPackageBinding -import com.aurora.store.util.PackageUtil @ModelView( autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT, baseModelClass = BaseModel::class ) -class PackageInfoView @JvmOverloads constructor( +class InstalledAppView @JvmOverloads constructor( context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : BaseView(context, attrs, defStyleAttr) { @ModelProp(options = [ModelProp.Option.IgnoreRequireHashCode]) - fun packageInfo(packageInfo: PackageInfo) { - val appInfo = packageInfo.applicationInfo!! - binding.imgIcon.load(PackageUtil.getIconForPackage(context, appInfo.packageName)) { + fun packageInfo(app: MinimalApp) { + binding.imgIcon.load(app.icon) { placeholder(R.drawable.bg_placeholder) transformations(RoundedCornersTransformation(25F)) } - binding.txtLine1.text = appInfo.loadLabel(context.packageManager) - binding.txtLine2.text = appInfo.packageName - binding.txtLine3.text = ("${packageInfo.versionName}.${PackageInfoCompat.getLongVersionCode(packageInfo)}") + binding.txtLine1.text = app.displayName + binding.txtLine2.text = app.packageName + binding.txtLine3.text = ("${app.versionName} (${app.versionCode})") } @CallbackProp diff --git a/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt b/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt index f69d46304..3f4565833 100644 --- a/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt @@ -19,7 +19,6 @@ package com.aurora.store.view.ui.all -import android.content.pm.PackageInfo import android.os.Bundle import android.text.Editable import android.text.TextWatcher @@ -32,7 +31,7 @@ import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.model.MinimalApp import com.aurora.store.databinding.FragmentGenericWithSearchBinding import com.aurora.store.view.epoxy.views.HeaderViewModel_ -import com.aurora.store.view.epoxy.views.PackageInfoViewModel_ +import com.aurora.store.view.epoxy.views.InstalledAppViewModel_ import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_ import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.viewmodel.all.InstalledViewModel @@ -79,8 +78,7 @@ class AppsGamesFragment : BaseFragment() { updateController(viewModel.packages.value) } else { val filteredPackages = viewModel.packages.value?.filter { - it.applicationInfo!!.loadLabel(requireContext().packageManager) - .contains(s, true) || it.packageName.contains(s, true) + it.displayName.contains(s, true) || it.packageName.contains(s, true) } updateController(filteredPackages) } @@ -98,7 +96,7 @@ class AppsGamesFragment : BaseFragment() { } } - private fun updateController(packages: List?) { + private fun updateController(packages: List?) { binding.recycler.withModels { setFilterDuplicates(true) if (packages == null) { @@ -116,12 +114,17 @@ class AppsGamesFragment : BaseFragment() { ) packages.forEach { app -> add( - PackageInfoViewModel_() + InstalledAppViewModel_() .id(app.packageName.hashCode()) .packageInfo(app) - .click { _ -> openDetailsFragment(app.packageName) } + .click { _ -> + openDetailsFragment( + app.packageName, + MinimalApp.toApp(app) + ) + } .longClick { _ -> - openAppMenuSheet(MinimalApp.fromPackageInfo(requireContext(), app)) + openAppMenuSheet(app) false } ) diff --git a/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt index d16ddfa01..87f11a56e 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt @@ -24,6 +24,7 @@ import android.content.pm.PackageInfo import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.aurora.store.data.model.MinimalApp import com.aurora.store.util.PackageUtil import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -40,7 +41,7 @@ class InstalledViewModel @Inject constructor( private val TAG = InstalledViewModel::class.java.simpleName - private val _packages = MutableStateFlow?>(null) + private val _packages = MutableStateFlow?>(null) val packages = _packages.asStateFlow() init { @@ -51,6 +52,7 @@ class InstalledViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { try { _packages.value = PackageUtil.getAllValidPackages(context) + .map { MinimalApp.fromPackageInfo(context, it) } } catch (exception: Exception) { Log.e(TAG, "Failed to fetch apps", exception) } From d3d30100a0e51ba521611e3a8dd51e0379c64570 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Thu, 5 Dec 2024 20:25:06 +0530 Subject: [PATCH 08/39] Blacklist: Add support for import & export --- .../view/ui/commons/BlacklistFragment.kt | 75 ++++++++++++++++++- .../view/ui/commons/FavouriteFragment.kt | 8 +- .../store/viewmodel/all/BlacklistViewModel.kt | 36 +++++++++ app/src/main/res/drawable/ic_menu.xml | 9 +++ app/src/main/res/values-night/themes.xml | 1 + app/src/main/res/values/strings.xml | 6 ++ app/src/main/res/values/themes.xml | 11 ++- 7 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 app/src/main/res/drawable/ic_menu.xml diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt index 7a74c5d59..978a1de75 100644 --- a/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt @@ -20,14 +20,23 @@ package com.aurora.store.view.ui.commons import android.content.pm.PackageInfo +import android.net.Uri import android.os.Bundle import android.text.Editable import android.text.TextWatcher +import android.view.ContextThemeWrapper +import android.view.MenuInflater +import android.view.MenuItem import android.view.View +import android.widget.PopupMenu +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.content.res.AppCompatResources import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import com.aurora.extensions.toast import com.aurora.store.AuroraApp +import com.aurora.store.R import com.aurora.store.data.event.BusEvent import com.aurora.store.databinding.FragmentGenericWithSearchBinding import com.aurora.store.view.epoxy.views.BlackListViewModel_ @@ -35,12 +44,23 @@ import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_ import com.aurora.store.viewmodel.all.BlacklistViewModel import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch +import java.util.Calendar @AndroidEntryPoint class BlacklistFragment : BaseFragment() { private val viewModel: BlacklistViewModel by viewModels() + private val mimeType = "application/json" + private val startForDocumentImport = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { + if (it != null) importBlacklist(it) else toast(R.string.toast_black_import_failed) + } + private val startForDocumentExport = + registerForActivityResult(ActivityResultContracts.CreateDocument(mimeType)) { + if (it != null) exportBlacklist(it) else toast(R.string.toast_black_export_failed) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -53,7 +73,15 @@ class BlacklistFragment : BaseFragment() { // Toolbar binding.layoutToolbarNative.apply { imgActionPrimary.visibility = View.VISIBLE - imgActionSecondary.visibility = View.GONE + imgActionSecondary.apply { + visibility = View.VISIBLE + setImageDrawable( + AppCompatResources.getDrawable(requireContext(), R.drawable.ic_menu) + ) + setOnClickListener { + showMenu(it) + } + } imgActionPrimary.setOnClickListener { viewModel.blacklistProvider.blacklist = viewModel.selected @@ -79,7 +107,8 @@ class BlacklistFragment : BaseFragment() { start: Int, count: Int, after: Int - ) {} + ) { + } }) } } @@ -89,6 +118,37 @@ class BlacklistFragment : BaseFragment() { viewModel.blacklistProvider.blacklist = viewModel.selected } + private fun showMenu(anchor: View) { + val popupMenu = PopupMenu( + ContextThemeWrapper( + requireContext(), + R.style.AppTheme_PopupMenu + ), anchor + ) + + val inflater: MenuInflater = popupMenu.menuInflater + inflater.inflate(R.menu.menu_import_export, popupMenu.menu) + + popupMenu.setOnMenuItemClickListener { menuItem: MenuItem -> + when (menuItem.itemId) { + R.id.action_import -> { + startForDocumentImport.launch(arrayOf(mimeType)) + true + } + + R.id.action_export -> { + startForDocumentExport.launch( + "aurora_store_blacklist_${Calendar.getInstance().time.time}.json" + ) + true + } + + else -> false + } + } + popupMenu.show() + } + private fun updateController(packages: List?) { binding.recycler.withModels { setFilterDuplicates(true) @@ -125,4 +185,15 @@ class BlacklistFragment : BaseFragment() { } } } + + private fun importBlacklist(uri: Uri) { + viewModel.importBlacklist(requireContext(), uri) + binding.recycler.requestModelBuild() + toast(R.string.toast_black_import_success) + } + + private fun exportBlacklist(uri: Uri) { + viewModel.exportBlacklist(requireContext(), uri) + toast(R.string.toast_black_export_success) + } } diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/FavouriteFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/FavouriteFragment.kt index c0df585f7..202368320 100644 --- a/app/src/main/java/com/aurora/store/view/ui/commons/FavouriteFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/commons/FavouriteFragment.kt @@ -45,11 +45,11 @@ class FavouriteFragment : BaseFragment() { private val mimeType = "application/json" private val startForDocumentImport = registerForActivityResult(ActivityResultContracts.OpenDocument()) { - if (it != null) importDeviceConfig(it) else toast(R.string.toast_fav_import_failed) + if (it != null) importFavourites(it) else toast(R.string.toast_fav_import_failed) } private val startForDocumentExport = registerForActivityResult(ActivityResultContracts.CreateDocument(mimeType)) { - if (it != null) exportDeviceConfig(it) else toast(R.string.toast_fav_export_failed) + if (it != null) exportFavourites(it) else toast(R.string.toast_fav_export_failed) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -111,13 +111,13 @@ class FavouriteFragment : BaseFragment() { } } - private fun importDeviceConfig(uri: Uri) { + private fun importFavourites(uri: Uri) { viewModel.importFavourites(requireContext(), uri) binding.recycler.requestModelBuild() toast(R.string.toast_fav_import_success) } - private fun exportDeviceConfig(uri: Uri) { + private fun exportFavourites(uri: Uri) { viewModel.exportFavourites(requireContext(), uri) toast(R.string.toast_fav_export_success) } diff --git a/app/src/main/java/com/aurora/store/viewmodel/all/BlacklistViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/all/BlacklistViewModel.kt index 6a363afea..6a7d65ca8 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/all/BlacklistViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/all/BlacklistViewModel.kt @@ -21,11 +21,14 @@ package com.aurora.store.viewmodel.all import android.content.Context import android.content.pm.PackageInfo +import android.net.Uri import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.store.data.providers.BlacklistProvider import com.aurora.store.util.PackageUtil +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers @@ -37,6 +40,7 @@ import javax.inject.Inject @HiltViewModel class BlacklistViewModel @Inject constructor( val blacklistProvider: BlacklistProvider, + val gson: Gson, @ApplicationContext private val context: Context ) : ViewModel() { @@ -60,4 +64,36 @@ class BlacklistViewModel @Inject constructor( } } } + + fun importBlacklist(context: Context, uri: Uri) { + viewModelScope.launch(Dispatchers.IO) { + try { + context.contentResolver.openInputStream(uri)?.use { + val importedSet: MutableSet = gson.fromJson( + it.bufferedReader().readText(), + object : TypeToken?>() {}.type + ) + + val knownSet = blacklistProvider.blacklist + knownSet.addAll(importedSet) + + selected = knownSet + } + } catch (exception: Exception) { + Log.e(TAG, "Failed to import blacklist", exception) + } + } + } + + fun exportBlacklist(context: Context, uri: Uri) { + viewModelScope.launch(Dispatchers.IO) { + try { + context.contentResolver.openOutputStream(uri)?.use { + it.write(gson.toJson(blacklistProvider.blacklist).encodeToByteArray()) + } + } catch (exception: Exception) { + Log.e(TAG, "Failed to export blacklist", exception) + } + } + } } diff --git a/app/src/main/res/drawable/ic_menu.xml b/app/src/main/res/drawable/ic_menu.xml new file mode 100644 index 000000000..966e8e94b --- /dev/null +++ b/app/src/main/res/drawable/ic_menu.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index c70d8103a..b8ecd72e1 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -5,5 +5,6 @@ @style/Chip.Filter @style/AppTheme.PreferenceThemeOverlay @color/colorTransparent + @style/AppTheme.PopupMenu diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8c931f503..aa0a3c1a4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -437,6 +437,12 @@ Favourites imported! Favourites exported! + + Failed to import blacklist! + Failed to export blacklist! + Blacklist imported! + Blacklist exported! + File Exporter Hold on, exporting your file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 2736c408e..e4795a322 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -24,6 +24,7 @@ @style/AppTheme.PreferenceThemeOverlay @style/AppTheme.BottomSheetStyle @color/colorTransparent + @style/AppTheme.PopupMenu + + From 852d9bdb85b4dd5dcba6ed0a116613cfcbc7cc13 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Thu, 5 Dec 2024 22:00:49 +0530 Subject: [PATCH 09/39] Blacklist: Add select & remove all action --- .../view/ui/commons/BlacklistFragment.kt | 14 +++++++- .../store/viewmodel/all/BlacklistViewModel.kt | 8 +++++ app/src/main/res/menu/menu_blacklist.xml | 33 +++++++++++++++++++ app/src/main/res/values/strings.xml | 2 ++ 4 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/menu/menu_blacklist.xml diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt index 978a1de75..f72a71461 100644 --- a/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt @@ -127,7 +127,7 @@ class BlacklistFragment : BaseFragment() { ) val inflater: MenuInflater = popupMenu.menuInflater - inflater.inflate(R.menu.menu_import_export, popupMenu.menu) + inflater.inflate(R.menu.menu_blacklist, popupMenu.menu) popupMenu.setOnMenuItemClickListener { menuItem: MenuItem -> when (menuItem.itemId) { @@ -143,6 +143,18 @@ class BlacklistFragment : BaseFragment() { true } + R.id.action_select_all -> { + viewModel.selectAll() + binding.recycler.requestModelBuild() + true + } + + R.id.action_remove_all -> { + viewModel.removeAll() + binding.recycler.requestModelBuild() + true + } + else -> false } } diff --git a/app/src/main/java/com/aurora/store/viewmodel/all/BlacklistViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/all/BlacklistViewModel.kt index 6a7d65ca8..acea39901 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/all/BlacklistViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/all/BlacklistViewModel.kt @@ -65,6 +65,14 @@ class BlacklistViewModel @Inject constructor( } } + fun selectAll() { + selected.addAll(packages.value?.map { it.packageName } ?: emptyList()) + } + + fun removeAll() { + selected.clear() + } + fun importBlacklist(context: Context, uri: Uri) { viewModelScope.launch(Dispatchers.IO) { try { diff --git a/app/src/main/res/menu/menu_blacklist.xml b/app/src/main/res/menu/menu_blacklist.xml new file mode 100644 index 000000000..072968921 --- /dev/null +++ b/app/src/main/res/menu/menu_blacklist.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aa0a3c1a4..bd7c0c961 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -85,10 +85,12 @@ "Pause" "Pending" "Post" + "Remove all" Request new analysis "Restart" "Resume" "Search" + "Select all" "Share" "Uninstall" "Successfully uninstalled" From 279be8af683fd454c85cadc46bdb8e4828e80150 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Thu, 5 Dec 2024 23:25:07 +0530 Subject: [PATCH 10/39] Offer selfupdate for nightlies --- .../aurora/store/data/work/UpdateWorker.kt | 52 ++++++++++++++++--- .../java/com/aurora/store/util/Preferences.kt | 2 + 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt b/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt index fac761fa3..7e162be20 100644 --- a/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt +++ b/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt @@ -29,6 +29,7 @@ import com.aurora.store.util.NotificationUtil import com.aurora.store.util.PackageUtil import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_AUTO +import com.aurora.store.util.save import com.google.gson.Gson import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -155,13 +156,25 @@ class UpdateWorker @AssistedInject constructor( val filteredPackages = if (isAuroraOnlyFilterEnabled) { packages.filter { CertUtil.isAuroraStoreApp(appContext, it.packageName) } } else { - packages.filterNot { if (isFDroidFilterEnabled) CertUtil.isFDroidApp(appContext, it.packageName) else false } + packages.filterNot { + if (isFDroidFilterEnabled) + CertUtil.isFDroidApp(appContext, it.packageName) + else + false + } } - val updates = appDetailsHelper.getAppByPackageName(filteredPackages.map { it.packageName }) - .filter { it.displayName.isNotEmpty() } - .filter { PackageUtil.isUpdatable(appContext, it.packageName, it.versionCode.toLong()) } - .toMutableList() + val updates = + appDetailsHelper.getAppByPackageName(filteredPackages.map { it.packageName }) + .filter { it.displayName.isNotEmpty() } + .filter { + PackageUtil.isUpdatable( + appContext, + it.packageName, + it.versionCode.toLong() + ) + } + .toMutableList() if (canSelfUpdate) getSelfUpdate()?.let { updates.add(it) } @@ -191,13 +204,30 @@ class UpdateWorker @AssistedInject constructor( SelfUpdate::class.java ) + val lastSelfUpdate = gson.fromJson( + Preferences.getString( + appContext, + Preferences.PREFERENCE_SELF_UPDATE, + "{}" + ), + SelfUpdate::class.java + ) + + if (lastSelfUpdate.versionCode == 0) { + Log.i(TAG, "No old self-updates entry found, saving current!") + saveSelfUpdate(selfUpdate) + return@withContext null + } + val isUpdate = when (BuildType.CURRENT) { - BuildType.NIGHTLY, + BuildType.NIGHTLY -> selfUpdate.timestamp > lastSelfUpdate.timestamp BuildType.RELEASE -> selfUpdate.versionCode > BuildConfig.VERSION_CODE else -> false } if (isUpdate) { + saveSelfUpdate(selfUpdate) + if (CertUtil.isFDroidApp(appContext, BuildConfig.APPLICATION_ID)) { if (selfUpdate.fdroidBuild.isNotEmpty()) { return@withContext SelfUpdate.toApp(selfUpdate, appContext) @@ -208,6 +238,9 @@ class UpdateWorker @AssistedInject constructor( Log.e(TAG, "Update file is missing!") return@withContext null } + } else { + Log.i(TAG, "No self-updates found!") + return@withContext null } } catch (exception: Exception) { Log.e(TAG, "Failed to check self-updates", exception) @@ -219,6 +252,13 @@ class UpdateWorker @AssistedInject constructor( } } + private fun saveSelfUpdate(selfUpdate: SelfUpdate) { + appContext.save( + Preferences.PREFERENCE_SELF_UPDATE, + gson.toJson(selfUpdate) + ) + } + private fun notifyUpdates(updates: List) { with(appContext.getSystemService()!!) { notify( diff --git a/app/src/main/java/com/aurora/store/util/Preferences.kt b/app/src/main/java/com/aurora/store/util/Preferences.kt index d35871cbf..0026bae7c 100644 --- a/app/src/main/java/com/aurora/store/util/Preferences.kt +++ b/app/src/main/java/com/aurora/store/util/Preferences.kt @@ -55,6 +55,8 @@ object Preferences { const val PREFERENCE_UPDATES_AUTO = "PREFERENCE_UPDATES_AUTO" const val PREFERENCE_UPDATES_CHECK_INTERVAL = "PREFERENCE_UPDATES_CHECK_INTERVAL" + const val PREFERENCE_SELF_UPDATE = "PREFERENCE_SELF_UPDATE" + const val PREFERENCE_MIGRATION_VERSION = "PREFERENCE_MIGRATION_VERSION" private var prefs: SharedPreferences? = null From b4d89d0658749ef8d7339a52b7743f586de1e8d8 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Sat, 28 Sep 2024 10:59:17 +0100 Subject: [PATCH 11/39] Add support for fetching authTokens from microG Signed-off-by: Aayush Gupta --- app/src/main/AndroidManifest.xml | 4 ++ .../com/aurora/store/data/work/AuthWorker.kt | 62 ++++++++++++++-- .../java/com/aurora/store/util/CertUtil.kt | 5 ++ .../java/com/aurora/store/util/PackageUtil.kt | 12 ++++ .../store/view/ui/splash/SplashFragment.kt | 70 ++++++++++++++++++- 5 files changed, 146 insertions(+), 7 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 98d0c8135..e4a34b0fa 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,6 +46,10 @@ + + diff --git a/app/src/main/java/com/aurora/store/data/work/AuthWorker.kt b/app/src/main/java/com/aurora/store/data/work/AuthWorker.kt index b62cda05d..3ce970815 100644 --- a/app/src/main/java/com/aurora/store/data/work/AuthWorker.kt +++ b/app/src/main/java/com/aurora/store/data/work/AuthWorker.kt @@ -1,7 +1,13 @@ package com.aurora.store.data.work +import android.accounts.Account +import android.accounts.AccountManager import android.content.Context +import android.os.Handler +import android.os.Looper +import android.util.Base64 import android.util.Log +import androidx.core.os.bundleOf import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters @@ -10,8 +16,16 @@ import com.aurora.gplayapi.helpers.AuthHelper import com.aurora.store.data.model.AccountType import com.aurora.store.data.providers.AccountProvider import com.aurora.store.data.providers.AuthProvider +import com.aurora.store.util.CertUtil.GOOGLE_ACCOUNT_TYPE +import com.aurora.store.util.CertUtil.GOOGLE_PLAY_AUTH_TOKEN_TYPE +import com.aurora.store.util.CertUtil.GOOGLE_PLAY_CERT +import com.aurora.store.util.CertUtil.GOOGLE_PLAY_PACKAGE_NAME import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.runBlocking /** * Worker to refresh [AuthData] in background @@ -26,6 +40,8 @@ open class AuthWorker @AssistedInject constructor( private val TAG = AuthWorker::class.java.simpleName + private val authToken: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 1) + override suspend fun doWork(): Result { if (!AccountProvider.isLoggedIn(appContext)) { Log.i(TAG, "User has logged out!") @@ -42,14 +58,48 @@ open class AuthWorker @AssistedInject constructor( val accountType = AccountProvider.getAccountType(appContext) val authData = when (accountType) { AccountType.GOOGLE -> { - authProvider.buildGoogleAuthData( - AccountProvider.getLoginEmail(appContext)!!, - AccountProvider.getLoginToken(appContext)!!.first, - AccountProvider.getLoginToken(appContext)!!.second - ).getOrThrow() + val email = AccountProvider.getLoginEmail(appContext)!! + val token = AccountProvider.getLoginToken(appContext)!!.first + val tokenType = AccountProvider.getLoginToken(appContext)!!.second + + if (tokenType == AuthHelper.Token.AAS) { + Log.i(TAG, "Refreshing AuthData for personal account") + authProvider.buildGoogleAuthData(email, token, tokenType).getOrThrow() + } else { + /* + * We are working with AuthToken here. The only scenario when we will have + * AuthToken and Google login is when the user used microG to login into + * Aurora Store. In this case, we use system's AccountManager to request credentials. + */ + Log.i(TAG, "Refreshing AuthData for personal account using AccountManager") + AccountManager.get(appContext) + .getAuthToken( + Account(email, GOOGLE_ACCOUNT_TYPE), + GOOGLE_PLAY_AUTH_TOKEN_TYPE, + bundleOf( + "overridePackage" to GOOGLE_PLAY_PACKAGE_NAME, + "overrideCertificate" to Base64.decode(GOOGLE_PLAY_CERT, Base64.DEFAULT) + ), + true, + { + authToken.tryEmit(it.result.getString(AccountManager.KEY_AUTHTOKEN)) + }, + Handler(Looper.getMainLooper()) + ) + runBlocking { + authProvider.buildGoogleAuthData( + email, + authToken.take(1).first()!!, + tokenType + ).getOrThrow() + } + } } - AccountType.ANONYMOUS -> authProvider.buildAnonymousAuthData().getOrThrow() + AccountType.ANONYMOUS -> { + Log.i(TAG, "Refreshing AuthData for anonymous account") + authProvider.buildAnonymousAuthData().getOrThrow() + } } require(verifyAndSaveAuth(authData, accountType) != null) diff --git a/app/src/main/java/com/aurora/store/util/CertUtil.kt b/app/src/main/java/com/aurora/store/util/CertUtil.kt index aa8d35d9b..445dc2dbd 100644 --- a/app/src/main/java/com/aurora/store/util/CertUtil.kt +++ b/app/src/main/java/com/aurora/store/util/CertUtil.kt @@ -37,6 +37,11 @@ object CertUtil { private val TAG = "CertUtil" + const val GOOGLE_ACCOUNT_TYPE = "com.google" + const val GOOGLE_PLAY_AUTH_TOKEN_TYPE = "oauth2:https://www.googleapis.com/auth/googleplay" + const val GOOGLE_PLAY_PACKAGE_NAME = "com.android.vending" + const val GOOGLE_PLAY_CERT = "MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK" + fun isFDroidApp(context: Context, packageName: String): Boolean { return isInstalledByFDroid(context, packageName) || isSignedByFDroid(context, packageName) } diff --git a/app/src/main/java/com/aurora/store/util/PackageUtil.kt b/app/src/main/java/com/aurora/store/util/PackageUtil.kt index 540f06b23..40e8c618a 100644 --- a/app/src/main/java/com/aurora/store/util/PackageUtil.kt +++ b/app/src/main/java/com/aurora/store/util/PackageUtil.kt @@ -46,6 +46,10 @@ object PackageUtil { private const val TAG = "PackageUtil" + private const val PACKAGE_NAME_MICRO_G = "com.google.android.gms" + private const val VERSION_CODE_MICRO_G = 240913402 + private const val VERSION_CODE_MICRO_G_HUAWEI = 240913007 + fun getAllValidPackages(context: Context): List { val sharedLibs = context.packageManager.systemSharedLibraryNames ?: emptyArray() return context.packageManager.getInstalledPackages(PackageManager.GET_META_DATA) @@ -57,6 +61,14 @@ object PackageUtil { } } + fun hasSupportedMicroG(context: Context): Boolean { + return if (isHuawei) { + isInstalled(context, PACKAGE_NAME_MICRO_G, VERSION_CODE_MICRO_G_HUAWEI) + } else { + isInstalled(context, PACKAGE_NAME_MICRO_G, VERSION_CODE_MICRO_G) + } + } + fun isInstalled(context: Context, packageName: String): Boolean { return try { getPackageInfo(context, packageName, PackageManager.GET_META_DATA) diff --git a/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt b/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt index 5ed9e279f..fbc7a0979 100644 --- a/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt @@ -19,19 +19,34 @@ package com.aurora.store.view.ui.splash +import android.accounts.Account +import android.accounts.AccountManager import android.content.Intent import android.net.UrlQuerySanitizer import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Base64 +import android.util.Log import android.view.View import androidx.core.view.isVisible +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.os.bundleOf import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.aurora.extensions.hide +import com.aurora.extensions.isMAndAbove import com.aurora.extensions.show +import com.aurora.gplayapi.helpers.AuthHelper import com.aurora.store.R import com.aurora.store.data.model.AuthState import com.aurora.store.databinding.FragmentSplashBinding +import com.aurora.store.util.CertUtil.GOOGLE_ACCOUNT_TYPE +import com.aurora.store.util.CertUtil.GOOGLE_PLAY_AUTH_TOKEN_TYPE +import com.aurora.store.util.CertUtil.GOOGLE_PLAY_CERT +import com.aurora.store.util.CertUtil.GOOGLE_PLAY_PACKAGE_NAME +import com.aurora.store.util.PackageUtil import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB import com.aurora.store.util.Preferences.PREFERENCE_INTRO @@ -44,8 +59,20 @@ import kotlinx.coroutines.launch @AndroidEntryPoint class SplashFragment : BaseFragment() { + private val TAG = SplashFragment::class.java.simpleName + private val viewModel: AuthViewModel by activityViewModels() + private val startForAccount = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + val accountName = it.data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) + if (!accountName.isNullOrBlank()) { + requestAuthTokenForGoogle(accountName) + } else { + findNavController().navigate(R.id.googleFragment) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -176,7 +203,22 @@ class SplashFragment : BaseFragment() { binding.btnGoogle.addOnClickListener { if (viewModel.authState.value != AuthState.Fetching) { binding.btnGoogle.updateProgress(true) - findNavController().navigate(R.id.googleFragment) + if (isMAndAbove && PackageUtil.hasSupportedMicroG(requireContext())) { + Log.i(TAG, "Found supported microG, trying to request credentials") + val accountIntent = AccountManager.newChooseAccountIntent( + null, + null, + arrayOf(GOOGLE_ACCOUNT_TYPE), + null, + null, + null, + null + ) + startForAccount.launch(accountIntent) + } else { + findNavController().navigate(R.id.googleFragment) + } + } } } @@ -221,4 +263,30 @@ class SplashFragment : BaseFragment() { requireArguments().getString("packageName") ?: "" } } + + private fun requestAuthTokenForGoogle(accountName: String) { + try { + AccountManager.get(requireContext()) + .getAuthToken( + Account(accountName, GOOGLE_ACCOUNT_TYPE), + GOOGLE_PLAY_AUTH_TOKEN_TYPE, + bundleOf( + "overridePackage" to GOOGLE_PLAY_PACKAGE_NAME, + "overrideCertificate" to Base64.decode(GOOGLE_PLAY_CERT, Base64.DEFAULT) + ), + requireActivity(), + { + viewModel.buildGoogleAuthData( + accountName, + it.result.getString(AccountManager.KEY_AUTHTOKEN)!!, + AuthHelper.Token.AUTH + ) + }, + Handler(Looper.getMainLooper()) + ) + } catch (exception: Exception) { + Log.e(TAG, "Failed to get authToken for Google login") + findNavController().navigate(R.id.googleFragment) + } + } } From baa954eb20ddd454814dc93f598dd6296535d484 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Sat, 14 Dec 2024 13:13:33 +0530 Subject: [PATCH 12/39] Do not show account selection dialog, if there is only 1 account --- app/src/main/AndroidManifest.xml | 2 ++ .../store/view/ui/splash/SplashFragment.kt | 25 ++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e4a34b0fa..e0f150b58 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,6 +46,8 @@ + + diff --git a/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt b/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt index fbc7a0979..0ed6598f1 100644 --- a/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt @@ -29,14 +29,15 @@ import android.os.Looper import android.util.Base64 import android.util.Log import android.view.View -import androidx.core.view.isVisible import androidx.activity.result.contract.ActivityResultContracts import androidx.core.os.bundleOf +import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.aurora.extensions.hide import com.aurora.extensions.isMAndAbove +import com.aurora.extensions.runOnUiThread import com.aurora.extensions.show import com.aurora.gplayapi.helpers.AuthHelper import com.aurora.store.R @@ -69,7 +70,7 @@ class SplashFragment : BaseFragment() { if (!accountName.isNullOrBlank()) { requestAuthTokenForGoogle(accountName) } else { - findNavController().navigate(R.id.googleFragment) + runOnUiThread { resetActions() } } } @@ -204,6 +205,14 @@ class SplashFragment : BaseFragment() { if (viewModel.authState.value != AuthState.Fetching) { binding.btnGoogle.updateProgress(true) if (isMAndAbove && PackageUtil.hasSupportedMicroG(requireContext())) { + val accounts = fetchGoogleAccounts() + + // Do not show selection dialog if there is only one account available + if (accounts.isNotEmpty() && accounts.size == 1) { + requestAuthTokenForGoogle(accounts.first().name) + return@addOnClickListener + } + Log.i(TAG, "Found supported microG, trying to request credentials") val accountIntent = AccountManager.newChooseAccountIntent( null, @@ -218,7 +227,6 @@ class SplashFragment : BaseFragment() { } else { findNavController().navigate(R.id.googleFragment) } - } } } @@ -236,7 +244,8 @@ class SplashFragment : BaseFragment() { } private fun navigateToDefaultTab() { - val defaultDestination = Preferences.getInteger(requireContext(), PREFERENCE_DEFAULT_SELECTED_TAB) + val defaultDestination = + Preferences.getInteger(requireContext(), PREFERENCE_DEFAULT_SELECTED_TAB) val directions = when (requireArguments().getInt("destinationId", defaultDestination)) { R.id.updatesFragment -> { @@ -264,6 +273,11 @@ class SplashFragment : BaseFragment() { } } + private fun fetchGoogleAccounts(): Array { + val accountManager = AccountManager.get(requireContext()) + return accountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE) + } + private fun requestAuthTokenForGoogle(accountName: String) { try { AccountManager.get(requireContext()) @@ -278,7 +292,7 @@ class SplashFragment : BaseFragment() { { viewModel.buildGoogleAuthData( accountName, - it.result.getString(AccountManager.KEY_AUTHTOKEN)!!, + it.result.getString(AccountManager.KEY_AUTHTOKEN) ?: "", AuthHelper.Token.AUTH ) }, @@ -286,7 +300,6 @@ class SplashFragment : BaseFragment() { ) } catch (exception: Exception) { Log.e(TAG, "Failed to get authToken for Google login") - findNavController().navigate(R.id.googleFragment) } } } From acbb0f1a8c7b47ed0ac57fe7f4d9067960e31531 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Sat, 14 Dec 2024 13:38:19 +0530 Subject: [PATCH 13/39] SplashFragment: Use null safe requireActivity() --- .../com/aurora/store/view/ui/splash/SplashFragment.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt b/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt index 0ed6598f1..cc61641d3 100644 --- a/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt @@ -257,16 +257,16 @@ class SplashFragment : BaseFragment() { 2 -> SplashFragmentDirections.actionSplashFragmentToUpdatesFragment() else -> SplashFragmentDirections.actionSplashFragmentToNavigationApps() } - activity?.viewModelStore?.clear() // Clear ViewModelStore to avoid bugs with logout + requireActivity().viewModelStore.clear() // Clear ViewModelStore to avoid bugs with logout findNavController().navigate(directions) } private fun getPackageName(): String { // Navigation component cannot handle market scheme as its missing a valid host - return if (activity?.intent != null && activity?.intent?.scheme == "market") { + return if (requireActivity().intent != null && requireActivity().intent.scheme == "market") { requireActivity().intent.data!!.getQueryParameter("id") ?: "" - } else if (activity?.intent != null && activity?.intent?.action == Intent.ACTION_SEND) { - val clipData = activity?.intent?.getStringExtra(Intent.EXTRA_TEXT) ?: "" + } else if (requireActivity().intent != null && requireActivity().intent.action == Intent.ACTION_SEND) { + val clipData = requireActivity().intent.getStringExtra(Intent.EXTRA_TEXT) ?: "" UrlQuerySanitizer(clipData).getValue("id") ?: "" } else { requireArguments().getString("packageName") ?: "" From d2145962d83121781fd6081e2de0aeab20ca88a3 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Sat, 14 Dec 2024 15:45:02 +0530 Subject: [PATCH 14/39] Add a check to ensure GMS is not from Google --- .../java/com/aurora/store/util/CertUtil.kt | 53 ++++++++++++++++++- .../java/com/aurora/store/util/PackageUtil.kt | 5 ++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/aurora/store/util/CertUtil.kt b/app/src/main/java/com/aurora/store/util/CertUtil.kt index 445dc2dbd..309935d5a 100644 --- a/app/src/main/java/com/aurora/store/util/CertUtil.kt +++ b/app/src/main/java/com/aurora/store/util/CertUtil.kt @@ -32,6 +32,7 @@ import com.aurora.store.data.model.Algorithm import com.aurora.store.util.PackageUtil.getPackageInfo import java.security.MessageDigest import java.security.cert.X509Certificate +import javax.security.auth.x500.X500Principal object CertUtil { @@ -40,7 +41,17 @@ object CertUtil { const val GOOGLE_ACCOUNT_TYPE = "com.google" const val GOOGLE_PLAY_AUTH_TOKEN_TYPE = "oauth2:https://www.googleapis.com/auth/googleplay" const val GOOGLE_PLAY_PACKAGE_NAME = "com.android.vending" - const val GOOGLE_PLAY_CERT = "MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK" + const val GOOGLE_PLAY_CERT = + "MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK" + + // Keep this list updated as & when new signatures are added. + private val knownGMSSignatures = listOf( + "bd32424203e0fb25f36b57e5aa356f9bdd1da998", + "38918a453d07199354f8b19af05ec6562ced5788,", + "2169eddb5fbb1fdf241c262681024692c4fc1ecb", + "58e1c4133f7441ec3d2c270270a14802da47ba0e", + "4f87463a1ae6f7d71b2c0b0658845790236dba42" + ) fun isFDroidApp(context: Context, packageName: String): Boolean { return isInstalledByFDroid(context, packageName) || isSignedByFDroid(context, packageName) @@ -89,6 +100,31 @@ object CertUtil { } } + fun isGoogleGMS(context: Context, packageName: String): Boolean { + return try { + getX509Certificates(context, packageName).any { certificate -> + val signatureHash = extractSHA1Fingerprint(certificate) + + if (knownGMSSignatures.contains(signatureHash)) return true + + // Follow heuristics to determine if the app is signed by Google, just to ensure we don't miss any. + listOf( + certificate.issuerX500Principal, + certificate.subjectX500Principal + ).any { + val map = parseX500Principal(it) + map["O"] == "Google LLC" || map["O"] == "Google Inc." + && map["L"] == "Mountain View" + && map["ST"] == "California" + && map["C"] == "US" + } + } + } catch (exception: Exception) { + Log.e(TAG, "Failed to check signing cert for $packageName") + false + } + } + private fun isInstalledByFDroid(context: Context, packageName: String): Boolean { val fdroidPackages = listOf( "org.fdroid.basic", "org.fdroid.fdroid", "org.fdroid.fdroid.privileged" @@ -125,4 +161,19 @@ object CertUtil { getPackageInfo(context, packageName, PackageManager.GET_SIGNATURES) } } + + private fun extractSHA1Fingerprint(certificate: X509Certificate): String { + val messageDigest = MessageDigest.getInstance(Algorithm.SHA1.value) + messageDigest.update(certificate.encoded) + return messageDigest.digest() + .joinToString("") { byte -> String.format("%02x", byte) } + .lowercase() + } + + private fun parseX500Principal(principal: X500Principal): Map { + return principal.name.split(",").associate { + val (left, right) = it.split("=") + left.trim() to right.trim() + } + } } diff --git a/app/src/main/java/com/aurora/store/util/PackageUtil.kt b/app/src/main/java/com/aurora/store/util/PackageUtil.kt index 40e8c618a..631b92f3e 100644 --- a/app/src/main/java/com/aurora/store/util/PackageUtil.kt +++ b/app/src/main/java/com/aurora/store/util/PackageUtil.kt @@ -62,6 +62,11 @@ object PackageUtil { } fun hasSupportedMicroG(context: Context): Boolean { + val isGoogle = CertUtil.isGoogleGMS(context, PACKAGE_NAME_MICRO_G) + + // Do not check for MicroG if Google Play Services is installed + if (isGoogle) return false + return if (isHuawei) { isInstalled(context, PACKAGE_NAME_MICRO_G, VERSION_CODE_MICRO_G_HUAWEI) } else { From c7fefb12c7954252155c4a53bc441dff4dcc8bf0 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Sat, 14 Dec 2024 23:29:02 +0530 Subject: [PATCH 15/39] AppDetails: Fix user review + minor cleanup --- app/src/main/AndroidManifest.xml | 3 +- .../view/ui/details/AppDetailsFragment.kt | 212 +++++++++--------- .../viewmodel/details/AppDetailsViewModel.kt | 50 ++++- .../main/res/layout/layout_details_app.xml | 12 +- .../main/res/layout/layout_details_beta.xml | 9 +- .../res/layout/layout_details_privacy.xml | 14 +- .../main/res/layout/layout_details_review.xml | 58 ++--- gradle/libs.versions.toml | 2 +- 8 files changed, 193 insertions(+), 167 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e0f150b58..499e4fbbe 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -87,7 +87,8 @@ + android:exported="true" + android:windowSoftInputMode="adjustPan"> 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 85b50ec8a..33c528607 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 @@ -188,18 +188,18 @@ class AppDetailsFragment : BaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - if (args.app != null) { - app = args.app!! - inflatePartialApp() - } else { - app = App(args.packageName) + app = args.app ?: App(args.packageName) + app.apply { + // Check whether app is installed or not + isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) + uninstallActionEnabled = isInstalled } - // Toolbar - attachToolbar() + // Show the basic app details, while the rest of the data is being fetched + updateAppHeader(app) - // Check whether app is installed or not - app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) + // Toolbar + attachToolbar(app) // App Details viewModel.fetchAppDetails(app.packageName) @@ -208,11 +208,27 @@ class AppDetailsFragment : BaseFragment() { viewModel.app.collect { if (it.packageName.isNotBlank()) { app = it - inflatePartialApp() // Re-inflate the app details, as web data may vary. - inflateExtraDetails(app) + + // App User Review + // We can not fetch it outside of this block, as we need the testing program status + if (!authProvider.isAnonymous && app.isInstalled) { + viewModel.fetchUserAppReview(app) + } + + updateAppHeader(app) // Re-inflate the app details, as web data may vary. + updateExtraDetails(app) + + // Fetch App Reviews viewModel.fetchAppReviews(app.packageName) + + // Fetch Data Safety Report + viewModel.fetchAppDataSafetyReport(app.packageName) + + // Fetch Exodus Privacy Report + viewModel.fetchAppReport(app.packageName) } else { - toast("Failed to fetch app details") + toast(getString(R.string.status_unavailable)) + // TODO: Redirect to App Unavailable Fragment } } } @@ -257,12 +273,7 @@ class AppDetailsFragment : BaseFragment() { viewLifecycleOwner.lifecycleScope.launch { viewModel.userReview.collect { if (it.commentId.isNotEmpty()) { - binding.layoutDetailsReview.userStars.rating = it.rating.toFloat() - Toast.makeText( - requireContext(), - getString(R.string.toast_rated_success), - Toast.LENGTH_SHORT - ).show() + runOnUiThread { updateUserReview(it) } } else { Toast.makeText( requireContext(), @@ -377,10 +388,11 @@ class AppDetailsFragment : BaseFragment() { super.onResume() } - private fun attachToolbar() { + private fun attachToolbar(app: App) { binding.layoutDetailsToolbar.toolbar.apply { elevation = 0f navigationIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_arrow_back) + setNavigationOnClickListener { if (isExternal) { activity?.finish() @@ -389,8 +401,22 @@ class AppDetailsFragment : BaseFragment() { } } + // Inflate Menu inflateMenu(R.menu.menu_details) + // Adjust Menu Items + menu.let { + it.findItem(R.id.action_home_screen)?.isVisible = + app.isInstalled && ShortcutManagerUtil.canPinShortcut( + requireContext(), + app.packageName + ) + it.findItem(R.id.menu_download_manual)?.isVisible = !app.isInstalled + it.findItem(R.id.action_uninstall)?.isVisible = app.isInstalled + it.findItem(R.id.menu_app_settings)?.isVisible = app.isInstalled + } + + // Set Menu Item Clicks setOnMenuItemClickListener { when (it.itemId) { R.id.action_home_screen -> { @@ -436,26 +462,13 @@ class AppDetailsFragment : BaseFragment() { requireContext().browse("${Constants.SHARE_URL}${app.packageName}") } } + true } - - if (::app.isInitialized) { - app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) - - menu?.findItem(R.id.action_home_screen)?.isVisible = - app.isInstalled && ShortcutManagerUtil.canPinShortcut( - requireContext(), - app.packageName - ) - - menu?.findItem(R.id.action_uninstall)?.isVisible = app.isInstalled - menu?.findItem(R.id.menu_app_settings)?.isVisible = app.isInstalled - uninstallActionEnabled = app.isInstalled - } } } - private fun attachHeader() { + private fun updateAppHeader(app: App) { binding.layoutDetailsApp.apply { imgIcon.load(app.iconArtwork.url) { placeholder(R.drawable.bg_placeholder) @@ -465,27 +478,20 @@ class AppDetailsFragment : BaseFragment() { } } + packageName.text = app.packageName txtLine1.text = app.displayName txtLine2.text = app.developerName + txtLine3.text = ("${app.versionName} (${app.versionCode})") + txtLine2.setOnClickListener { findNavController().navigate( AppDetailsFragmentDirections .actionAppDetailsFragmentToDevAppsFragment(app.developerName) ) } - txtLine3.text = ("${app.versionName} (${app.versionCode})") - packageName.text = app.packageName - - if (app.isFree) - tags.add(getString(R.string.details_free)) - else - tags.add(getString(R.string.details_paid)) - - if (app.containsAds) - tags.add(getString(R.string.details_contains_ads)) - else - tags.add(getString(R.string.details_no_ads)) + 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))) txtLine4.text = tags.joinToString(separator = " • ") } @@ -724,40 +730,34 @@ class AppDetailsFragment : BaseFragment() { } } - private fun inflatePartialApp() { - if (::app.isInitialized) { - attachHeader() - checkAndSetupInstall() - } - } + private fun updateExtraDetails(app: App) { + binding.viewFlipper.displayedChild = 1 - private fun inflateExtraDetails(app: App?) { - app?.let { - binding.viewFlipper.displayedChild = 1 - inflateAppDescription(app) - inflateAppRatingAndReviews(app) - inflateAppDevInfo(app) - inflateAppDataSafety(app) - inflateAppPrivacy(app) - inflateAppPermission(app) + updateAppDescription(app) + updateAppRatingAndReviews(app) + updateAppDevInfo(app) + updateAppPermission(app) - if (!authProvider.isAnonymous) { - app.testingProgram?.let { - if (it.isAvailable && it.isSubscribed) { - binding.layoutDetailsApp.txtLine1.text = it.displayName - } + // Allow users to handle beta subscriptions, if logged in by own account. + if (!authProvider.isAnonymous) { + // Update app name to the testing program name, if subscribed + app.testingProgram?.let { + if (it.isAvailable && it.isSubscribed) { + binding.layoutDetailsApp.txtLine1.text = it.displayName } - - inflateBetaSubscription(app) } - if (showSimilarApps) { - inflateAppStream(app) - } + updateBetaSubscription(app) } + + if (showSimilarApps) { + updateAppStream(app) + } + + checkAndSetupInstall() } - private fun inflateAppDescription(app: App) { + private fun updateAppDescription(app: App) { binding.layoutDetailDescription.apply { val installs = CommonUtil.addDiPrefix(app.installs) @@ -814,10 +814,16 @@ class AppDetailsFragment : BaseFragment() { } } - private fun inflateAppRatingAndReviews(app: App) { + private fun updateAppRatingAndReviews(app: App) { binding.layoutDetailsReview.apply { - averageRating.text = app.rating.average.toString() - txtReviewCount.text = app.rating.abbreviatedLabel + headerRatingReviews.addClickListener { + findNavController().navigate( + AppDetailsFragmentDirections.actionAppDetailsFragmentToDetailsReviewFragment( + app.displayName, + app.packageName + ) + ) + } var totalStars = 0L totalStars += app.rating.oneStar @@ -837,42 +843,34 @@ class AppDetailsFragment : BaseFragment() { averageRating.text = String.format(Locale.getDefault(), "%.1f", app.rating.average) txtReviewCount.text = app.rating.abbreviatedLabel + } + } - layoutUserReview.visibility = - if (authProvider.isAnonymous) View.GONE else View.VISIBLE + private fun updateUserReview(review: Review) { + binding.layoutDetailsReview.apply { + layoutUserReview.visibility = View.VISIBLE + inputTitle.setText(review.title) + inputReview.setText(review.comment) + userStars.rating = review.rating.toFloat() - btnPostReview.setOnClickListener { - if (authProvider.isAnonymous) { - toast(R.string.toast_anonymous_restriction) - } else { - addOrUpdateReview(app, Review().apply { - title = inputTitle.text.toString() - rating = userStars.rating.toInt() - comment = inputReview.text.toString() - }) - } - } - - headerRatingReviews.addClickListener { - findNavController().navigate( - AppDetailsFragmentDirections.actionAppDetailsFragmentToDetailsReviewFragment( - app.displayName, - app.packageName + if (!authProvider.isAnonymous && app.isInstalled) { + btnPostReview.setOnClickListener { + addOrUpdateReview( + app, + Review().apply { + title = inputTitle.text.toString() + rating = userStars.rating.toInt() + comment = inputReview.text.toString() + } ) - ) + } + } else { + layoutUserReview.visibility = View.GONE } } } - private fun inflateAppDataSafety(app: App) { - viewModel.fetchAppDataSafetyReport(app.packageName) - } - - private fun inflateAppPrivacy(app: App) { - viewModel.fetchAppReport(app.packageName) - } - - private fun inflateAppDevInfo(app: App) { + private fun updateAppDevInfo(app: App) { binding.layoutDetailsDev.apply { if (app.developerAddress.isNotEmpty()) { devAddress.apply { @@ -902,7 +900,7 @@ class AppDetailsFragment : BaseFragment() { } } - private fun inflateBetaSubscription(app: App) { + private fun updateBetaSubscription(app: App) { binding.layoutDetailsBeta.apply { app.testingProgram?.let { betaProgram -> if (betaProgram.isAvailable) { @@ -933,7 +931,7 @@ class AppDetailsFragment : BaseFragment() { } } - private fun inflateAppStream(app: App) { + private fun updateAppStream(app: App) { app.detailsStreamUrl?.let { val carouselController = DetailsCarouselController(object : GenericCarouselController.Callbacks { @@ -980,7 +978,7 @@ class AppDetailsFragment : BaseFragment() { } } - private fun inflateAppPermission(app: App) { + private fun updateAppPermission(app: App) { binding.layoutDetailsPermissions.apply { headerPermission.addClickListener { if (app.permissions.isNotEmpty()) { 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 469b9e33d..a9aa2f057 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 @@ -1,9 +1,11 @@ package com.aurora.store.viewmodel.details +import android.content.Context import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.Constants +import com.aurora.extensions.toast import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.Review import com.aurora.gplayapi.data.models.details.TestingProgramStatus @@ -12,13 +14,16 @@ import com.aurora.gplayapi.helpers.ReviewsHelper import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper import com.aurora.gplayapi.network.IHttpClient import com.aurora.store.BuildConfig +import com.aurora.store.R import com.aurora.store.data.helper.DownloadHelper import com.aurora.store.data.model.ExodusReport import com.aurora.store.data.model.Report import com.aurora.store.data.room.favourite.Favourite import com.aurora.store.data.room.favourite.FavouriteDao +import com.aurora.store.util.PackageUtil import com.google.gson.GsonBuilder import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -32,6 +37,7 @@ import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport @HiltViewModel class AppDetailsViewModel @Inject constructor( + @ApplicationContext private val context: Context, private val appDetailsHelper: AppDetailsHelper, private val reviewsHelper: ReviewsHelper, private val webDataSafetyHelper: WebDataSafetyHelper, @@ -77,7 +83,9 @@ class AppDetailsViewModel @Inject constructor( checkFavourite(packageName) val app: App = appStash.getOrPut(packageName) { - appDetailsHelper.getAppByPackageName(packageName) + appDetailsHelper.getAppByPackageName(packageName).apply { + isInstalled = PackageUtil.isInstalled(context, packageName) + } } _app.emit(app) @@ -148,20 +156,42 @@ class AppDetailsViewModel @Inject constructor( } } + fun fetchUserAppReview(app: App) { + viewModelScope.launch(Dispatchers.IO) { + try { + val stashedUserReview = userReviewStash[app.packageName] + if (stashedUserReview != null) { + _userReview.emit(stashedUserReview) + return@launch + } + + val isTesting = app.testingProgram?.isSubscribed ?: false + val userReview = reviewsHelper.getUserReview(app.packageName, isTesting) + + if (userReview != null) { + userReviewStash[app.packageName] = userReview + _userReview.emit(userReview) + } + } catch (exception: Exception) { + Log.e(TAG, "Failed to fetch user review", exception) + } + } + } + fun postAppReview(packageName: String, review: Review, isBeta: Boolean) { viewModelScope.launch(Dispatchers.IO) { try { - val userReview = userReviewStash.getOrPut(packageName) { - reviewsHelper.addOrEditReview( - packageName, - review.title, - review.comment, - review.rating, - isBeta - ) - } + val userReview = reviewsHelper.addOrEditReview( + packageName, + review.title, + review.comment, + review.rating, + isBeta + ) if (userReview != null) { + context.toast(R.string.toast_rated_success) + userReviewStash[packageName] = userReview _userReview.emit(userReview) } } catch (exception: Exception) { diff --git a/app/src/main/res/layout/layout_details_app.xml b/app/src/main/res/layout/layout_details_app.xml index 0b00e301f..189b4a3a7 100644 --- a/app/src/main/res/layout/layout_details_app.xml +++ b/app/src/main/res/layout/layout_details_app.xml @@ -29,13 +29,13 @@ + android:layout_width="@dimen/icon_size_large" + android:layout_height="@dimen/icon_size_large"> @@ -120,6 +120,7 @@ android:backgroundTint="?colorError" android:textColor="?colorOnError" android:visibility="gone" + app:cornerRadius="@dimen/radius_small" tools:text="@string/action_cancel" tools:visibility="visible" /> @@ -129,6 +130,7 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/margin_small" android:layout_weight="1" + app:cornerRadius="@dimen/radius_small" tools:text="@string/action_install" /> diff --git a/app/src/main/res/layout/layout_details_beta.xml b/app/src/main/res/layout/layout_details_beta.xml index 9e961d51b..f423cef5f 100644 --- a/app/src/main/res/layout/layout_details_beta.xml +++ b/app/src/main/res/layout/layout_details_beta.xml @@ -22,9 +22,11 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:divider="@drawable/divider" android:orientation="vertical" android:paddingStart="@dimen/padding_small" android:paddingEnd="@dimen/padding_small" + android:showDividers="middle" android:visibility="gone" tools:visibility="visible"> @@ -40,7 +42,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" - android:layout_marginTop="@dimen/margin_small" android:orientation="vertical"> diff --git a/app/src/main/res/layout/layout_details_privacy.xml b/app/src/main/res/layout/layout_details_privacy.xml index 0d757003b..00dc5b3b7 100644 --- a/app/src/main/res/layout/layout_details_privacy.xml +++ b/app/src/main/res/layout/layout_details_privacy.xml @@ -19,7 +19,7 @@ ~ --> - - - + diff --git a/app/src/main/res/layout/layout_details_review.xml b/app/src/main/res/layout/layout_details_review.xml index 26d752bb1..5ee028729 100644 --- a/app/src/main/res/layout/layout_details_review.xml +++ b/app/src/main/res/layout/layout_details_review.xml @@ -38,8 +38,9 @@ android:id="@+id/layout_user_review" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_gravity="center_horizontal" - android:orientation="vertical"> + android:orientation="vertical" + android:visibility="gone" + tools:visibility="visible"> - + android:layout_height="wrap_content"> - + + + + android:layout_height="wrap_content"> + + + + app:cornerRadius="@dimen/radius_small" /> diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4376bf31e..c7f568f88 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ composeBom = "2024.11.00" coreVersion = "1.15.0" epoxyVersion = "5.1.4" espressoVersion = "3.6.1" -gplayapiVersion = "3.4.3" +gplayapiVersion = "3.4.4" gsonVersion = "2.11.0" hiddenapibypassVersion = "4.3" hiltVersion = "2.53" From 5e40f0cb53b82179c98ae158ae46bad2ed8eabdd Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Sun, 15 Dec 2024 00:06:46 +0530 Subject: [PATCH 16/39] AppDetails: Update header layout to better show unknown apps --- .../main/java/com/aurora/extensions/View.kt | 11 +++++++ .../java/com/aurora/store/util/PackageUtil.kt | 17 ++++++++++ .../view/ui/details/AppDetailsFragment.kt | 32 ++++++++++++------- .../main/res/layout/layout_details_app.xml | 3 +- 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/aurora/extensions/View.kt b/app/src/main/java/com/aurora/extensions/View.kt index c7ca5629c..5296bfd12 100644 --- a/app/src/main/java/com/aurora/extensions/View.kt +++ b/app/src/main/java/com/aurora/extensions/View.kt @@ -21,6 +21,7 @@ package com.aurora.extensions import android.view.View import android.view.inputmethod.InputMethodManager +import android.widget.TextView import androidx.core.content.getSystemService fun View.isVisible() = visibility == View.VISIBLE @@ -51,3 +52,13 @@ fun View.hideKeyboard(): Boolean { } return false } + + +fun TextView.updateText(text: String?) { + if (text.isNullOrEmpty()) { + hide() + } else { + show() + this.text = text + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/util/PackageUtil.kt b/app/src/main/java/com/aurora/store/util/PackageUtil.kt index 631b92f3e..e3491cc3b 100644 --- a/app/src/main/java/com/aurora/store/util/PackageUtil.kt +++ b/app/src/main/java/com/aurora/store/util/PackageUtil.kt @@ -27,11 +27,13 @@ import android.content.pm.PackageManager import android.content.pm.PackageManager.PackageInfoFlags import android.content.pm.SharedLibraryInfo import android.graphics.Bitmap +import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build import android.provider.Settings import android.util.Log import androidx.annotation.RequiresApi +import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.pm.PackageInfoCompat import androidx.core.graphics.drawable.toBitmap import com.aurora.extensions.isHuawei @@ -40,6 +42,7 @@ import com.aurora.extensions.isPAndAbove import com.aurora.extensions.isTAndAbove import com.aurora.extensions.isValidApp import com.aurora.store.BuildConfig +import com.aurora.store.R import java.util.Locale object PackageUtil { @@ -235,6 +238,20 @@ object PackageUtil { } } + fun getIconDrawableForPackage(context: Context, packageName: String): Drawable? { + val placeholder = AppCompatResources.getDrawable(context, R.drawable.bg_placeholder) + + return try { + val packageInfo = context.packageManager.getPackageInfo(packageName, 0) + val applicationInfo = packageInfo.applicationInfo ?: return placeholder + + applicationInfo.loadIcon(context.packageManager) + } catch (exception: Exception) { + Log.e(TAG, "Failed to get icon for package!", exception) + placeholder + } + } + private fun getAllSharedLibraries(context: Context, flags: Int = 0): List { return if (isTAndAbove) { context.packageManager.getSharedLibraries(PackageInfoFlags.of(flags.toLong())) 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 33c528607..31808e92c 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 @@ -40,7 +40,7 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import coil3.asDrawable import coil3.load -import coil3.request.placeholder +import coil3.request.error import coil3.request.transformations import coil3.transform.CircleCropTransformation import coil3.transform.RoundedCornersTransformation @@ -55,6 +55,7 @@ import com.aurora.extensions.runOnUiThread import com.aurora.extensions.share import com.aurora.extensions.show import com.aurora.extensions.toast +import com.aurora.extensions.updateText import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.Review import com.aurora.gplayapi.data.models.StreamBundle @@ -196,7 +197,7 @@ class AppDetailsFragment : BaseFragment() { } // Show the basic app details, while the rest of the data is being fetched - updateAppHeader(app) + updateAppHeader(app, false) // Toolbar attachToolbar(app) @@ -468,20 +469,25 @@ class AppDetailsFragment : BaseFragment() { } } - private fun updateAppHeader(app: App) { + private fun updateAppHeader(app: App, isFullApp: Boolean = true) { binding.layoutDetailsApp.apply { + val fallbackDrawable = if (isFullApp) + ContextCompat.getDrawable(requireContext(), R.drawable.bg_placeholder) + else + PackageUtil.getIconDrawableForPackage(requireContext(), app.packageName) + imgIcon.load(app.iconArtwork.url) { - placeholder(R.drawable.bg_placeholder) + error(fallbackDrawable) transformations(RoundedCornersTransformation(32F)) listener { _, result -> result.image.asDrawable(resources).let { iconDrawable = it } } } - packageName.text = app.packageName - txtLine1.text = app.displayName - txtLine2.text = app.developerName - txtLine3.text = ("${app.versionName} (${app.versionCode})") + packageName.updateText(app.packageName) + txtLine1.updateText(app.displayName) + txtLine2.updateText(app.developerName) + txtLine3.updateText(("${app.versionName} (${app.versionCode})")) txtLine2.setOnClickListener { findNavController().navigate( @@ -490,10 +496,12 @@ class AppDetailsFragment : BaseFragment() { ) } - 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))) - - txtLine4.text = tags.joinToString(separator = " • ") + // 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))) + txtLine4.updateText(tags.joinToString(separator = " • ")) + } } } diff --git a/app/src/main/res/layout/layout_details_app.xml b/app/src/main/res/layout/layout_details_app.xml index 189b4a3a7..9f64b69ad 100644 --- a/app/src/main/res/layout/layout_details_app.xml +++ b/app/src/main/res/layout/layout_details_app.xml @@ -107,6 +107,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/txt_line4" + android:layout_marginTop="@dimen/margin_medium" android:divider="@drawable/divider" android:orientation="horizontal" android:showDividers="middle"> @@ -115,7 +116,6 @@ android:id="@+id/btn_secondary_action" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/margin_small" android:layout_weight="1" android:backgroundTint="?colorError" android:textColor="?colorOnError" @@ -128,7 +128,6 @@ android:id="@+id/btn_primary_action" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/margin_small" android:layout_weight="1" app:cornerRadius="@dimen/radius_small" tools:text="@string/action_install" /> From 59a2ae5e0878b35cffc25b172ef3ea31016f26bf Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Sun, 15 Dec 2024 00:16:58 +0530 Subject: [PATCH 17/39] AppDetails: Add reviews by latest --- .../aurora/store/view/ui/details/DetailsReviewFragment.kt | 1 + app/src/main/res/layout/fragment_details_review.xml | 7 +++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 9 insertions(+) diff --git a/app/src/main/java/com/aurora/store/view/ui/details/DetailsReviewFragment.kt b/app/src/main/java/com/aurora/store/view/ui/details/DetailsReviewFragment.kt index 7313ffb19..38b288c11 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/DetailsReviewFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/DetailsReviewFragment.kt @@ -91,6 +91,7 @@ class DetailsReviewFragment : BaseFragment() { binding.chipGroup.setOnCheckedStateChangeListener { _, checkedIds -> when (checkedIds[0]) { R.id.filter_review_all -> filter = Review.Filter.ALL + R.id.filter_newest_first -> filter = Review.Filter.NEWEST R.id.filter_review_critical -> filter = Review.Filter.CRITICAL R.id.filter_review_positive -> filter = Review.Filter.POSITIVE R.id.filter_review_five -> filter = Review.Filter.FIVE diff --git a/app/src/main/res/layout/fragment_details_review.xml b/app/src/main/res/layout/fragment_details_review.xml index 56077d621..4390aa1d9 100644 --- a/app/src/main/res/layout/fragment_details_review.xml +++ b/app/src/main/res/layout/fragment_details_review.xml @@ -53,6 +53,13 @@ android:layout_height="wrap_content" android:text="@string/filter_review_all" /> + + known trackers(s) found in "View report" "All" + "Latest" "Critical" "Five" "Four" From a8dca26add947fc3831594acafefa0a4cbff96ad Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Sun, 15 Dec 2024 00:17:43 +0530 Subject: [PATCH 18/39] AppDetails: Check icon itself instead of source --- .../java/com/aurora/store/view/ui/details/AppDetailsFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 31808e92c..d117e4090 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 @@ -471,7 +471,7 @@ class AppDetailsFragment : BaseFragment() { private fun updateAppHeader(app: App, isFullApp: Boolean = true) { binding.layoutDetailsApp.apply { - val fallbackDrawable = if (isFullApp) + val fallbackDrawable = if (app.iconArtwork.url.isNotBlank()) ContextCompat.getDrawable(requireContext(), R.drawable.bg_placeholder) else PackageUtil.getIconDrawableForPackage(requireContext(), app.packageName) From 9b0f106c3c2a9bbe09d53a5e2ba866b7198f0f96 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Mon, 16 Dec 2024 20:03:56 +0530 Subject: [PATCH 19/39] AppDetails: Always offer manual downloads --- .../java/com/aurora/store/view/ui/details/AppDetailsFragment.kt | 1 - 1 file changed, 1 deletion(-) 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 d117e4090..b127c149f 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 @@ -412,7 +412,6 @@ class AppDetailsFragment : BaseFragment() { requireContext(), app.packageName ) - it.findItem(R.id.menu_download_manual)?.isVisible = !app.isInstalled it.findItem(R.id.action_uninstall)?.isVisible = app.isInstalled it.findItem(R.id.menu_app_settings)?.isVisible = app.isInstalled } From dce752a0ff263328c9d36bc543cf25846fcf6a39 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Mon, 16 Dec 2024 20:20:19 +0530 Subject: [PATCH 20/39] AppDetails: Update toolbar again once full app is fetched --- .../aurora/store/view/ui/details/AppDetailsFragment.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 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 b127c149f..3646a1bc7 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 @@ -200,7 +200,7 @@ class AppDetailsFragment : BaseFragment() { updateAppHeader(app, false) // Toolbar - attachToolbar(app) + updateToolbar(app) // App Details viewModel.fetchAppDetails(app.packageName) @@ -216,6 +216,7 @@ class AppDetailsFragment : BaseFragment() { viewModel.fetchUserAppReview(app) } + updateToolbar(app) updateAppHeader(app) // Re-inflate the app details, as web data may vary. updateExtraDetails(app) @@ -389,7 +390,7 @@ class AppDetailsFragment : BaseFragment() { super.onResume() } - private fun attachToolbar(app: App) { + private fun updateToolbar(app: App) { binding.layoutDetailsToolbar.toolbar.apply { elevation = 0f navigationIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_arrow_back) @@ -402,6 +403,9 @@ class AppDetailsFragment : BaseFragment() { } } + // Clear Menu, so that it clears the previous menu items before inflating new ones + menu.clear() + // Inflate Menu inflateMenu(R.menu.menu_details) From 110848a0ba41ad0147ae55d47c4a72261870f1de Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Mon, 16 Dec 2024 20:40:06 +0530 Subject: [PATCH 21/39] AppsGamesFragment: show only what is available on PlayStore --- .../store/view/ui/all/AppsGamesFragment.kt | 24 +++++++++++-------- .../store/viewmodel/all/InstalledViewModel.kt | 20 +++++++++++----- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt b/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt index 3f4565833..e391a10b0 100644 --- a/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt @@ -26,12 +26,13 @@ import android.view.View import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import com.aurora.gplayapi.data.models.App import com.aurora.store.AuroraApp import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.model.MinimalApp import com.aurora.store.databinding.FragmentGenericWithSearchBinding import com.aurora.store.view.epoxy.views.HeaderViewModel_ -import com.aurora.store.view.epoxy.views.InstalledAppViewModel_ +import com.aurora.store.view.epoxy.views.app.AppListViewModel_ import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_ import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.viewmodel.all.InstalledViewModel @@ -47,7 +48,7 @@ class AppsGamesFragment : BaseFragment() { super.onViewCreated(view, savedInstanceState) viewLifecycleOwner.lifecycleScope.launch { - viewModel.packages.collect { + viewModel.apps.collect { updateController(it) } } @@ -75,9 +76,9 @@ class AppsGamesFragment : BaseFragment() { inputSearch.addTextChangedListener(object : TextWatcher { override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { if (s.isNullOrEmpty()) { - updateController(viewModel.packages.value) + updateController(viewModel.apps.value) } else { - val filteredPackages = viewModel.packages.value?.filter { + val filteredPackages = viewModel.apps.value?.filter { it.displayName.contains(s, true) || it.packageName.contains(s, true) } updateController(filteredPackages) @@ -96,7 +97,7 @@ class AppsGamesFragment : BaseFragment() { } } - private fun updateController(packages: List?) { + private fun updateController(packages: List?) { binding.recycler.withModels { setFilterDuplicates(true) if (packages == null) { @@ -114,17 +115,21 @@ class AppsGamesFragment : BaseFragment() { ) packages.forEach { app -> add( - InstalledAppViewModel_() + AppListViewModel_() .id(app.packageName.hashCode()) - .packageInfo(app) + .app(app) .click { _ -> openDetailsFragment( app.packageName, - MinimalApp.toApp(app) + app ) } .longClick { _ -> - openAppMenuSheet(app) + openAppMenuSheet( + MinimalApp.fromApp( + app + ) + ) false } ) @@ -132,5 +137,4 @@ class AppsGamesFragment : BaseFragment() { } } } - } diff --git a/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt index 87f11a56e..fb78774a5 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt @@ -20,11 +20,11 @@ package com.aurora.store.viewmodel.all import android.content.Context -import android.content.pm.PackageInfo import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.aurora.store.data.model.MinimalApp +import com.aurora.gplayapi.data.models.App +import com.aurora.gplayapi.helpers.web.WebAppDetailsHelper import com.aurora.store.util.PackageUtil import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -41,8 +41,8 @@ class InstalledViewModel @Inject constructor( private val TAG = InstalledViewModel::class.java.simpleName - private val _packages = MutableStateFlow?>(null) - val packages = _packages.asStateFlow() + private val _apps = MutableStateFlow?>(null) + val apps = _apps.asStateFlow() init { fetchApps() @@ -51,8 +51,16 @@ class InstalledViewModel @Inject constructor( fun fetchApps() { viewModelScope.launch(Dispatchers.IO) { try { - _packages.value = PackageUtil.getAllValidPackages(context) - .map { MinimalApp.fromPackageInfo(context, it) } + val packages = PackageUtil.getAllValidPackages(context) + + // Divide the list of packages into chunks of 100 & fetch app details + // 50 is a safe number to avoid hitting the rate limit or package size limit + val chunkedPackages = packages.chunked(50) + val allApps = chunkedPackages.flatMap { chunk -> + WebAppDetailsHelper().getAppDetails(chunk.map { it.packageName }) + } + + _apps.emit(allApps) } catch (exception: Exception) { Log.e(TAG, "Failed to fetch apps", exception) } From cf1d709bf58c6fd4fad289265270e3b924a545b3 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Mon, 16 Dec 2024 20:44:38 +0530 Subject: [PATCH 22/39] AppsGamesFragment: lets filter out blacklisted apps --- .../com/aurora/store/viewmodel/all/InstalledViewModel.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt index fb78774a5..442953a96 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.helpers.web.WebAppDetailsHelper +import com.aurora.store.data.providers.BlacklistProvider import com.aurora.store.util.PackageUtil import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -36,7 +37,8 @@ import javax.inject.Inject @HiltViewModel class InstalledViewModel @Inject constructor( - @ApplicationContext private val context: Context + @ApplicationContext private val context: Context, + private val blacklistProvider: BlacklistProvider, ) : ViewModel() { private val TAG = InstalledViewModel::class.java.simpleName @@ -52,6 +54,7 @@ class InstalledViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { try { val packages = PackageUtil.getAllValidPackages(context) + .filterNot { blacklistProvider.isBlacklisted(it.packageName) } // Divide the list of packages into chunks of 100 & fetch app details // 50 is a safe number to avoid hitting the rate limit or package size limit From fbeee2300cfe64628f795ac818e9ae93eb8f890a Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Mon, 16 Dec 2024 21:16:58 +0530 Subject: [PATCH 23/39] CertifitacePinning: Add toggle to enable/disable - Enabled by default - Could be useful for people using VPN based ad blockers - Option to make app usable, if at all Google's root certs gets changed/updated --- .../store/data/network/OkHttpClientModule.kt | 49 ++++++++++++++----- .../java/com/aurora/store/util/Preferences.kt | 1 + .../view/ui/onboarding/OnboardingFragment.kt | 3 ++ app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/preferences_network.xml | 6 +++ 5 files changed, 50 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/aurora/store/data/network/OkHttpClientModule.kt b/app/src/main/java/com/aurora/store/data/network/OkHttpClientModule.kt index a04e93312..ba79cddee 100644 --- a/app/src/main/java/com/aurora/store/data/network/OkHttpClientModule.kt +++ b/app/src/main/java/com/aurora/store/data/network/OkHttpClientModule.kt @@ -26,6 +26,7 @@ import com.aurora.store.R import com.aurora.store.data.model.Algorithm import com.aurora.store.data.model.ProxyInfo import com.aurora.store.util.Preferences +import com.aurora.store.util.Preferences.PREFERENCE_CERTIFICATE_PINNING_ENABLED import com.aurora.store.util.Preferences.PREFERENCE_PROXY_ENABLED import com.aurora.store.util.Preferences.PREFERENCE_PROXY_INFO import com.google.gson.Gson @@ -34,10 +35,10 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import java.io.ByteArrayInputStream -import java.io.InputStream import okhttp3.CertificatePinner import okhttp3.OkHttpClient +import java.io.ByteArrayInputStream +import java.io.InputStream import java.net.Authenticator import java.net.InetSocketAddress import java.net.PasswordAuthentication @@ -59,17 +60,33 @@ object OkHttpClientModule { @Provides @Singleton - fun providesOkHttpClientInstance(certPinner: CertificatePinner, proxy: Proxy?): OkHttpClient { - return OkHttpClient().newBuilder() + fun providesOkHttpClientInstance( + @ApplicationContext context: Context, + certPinner: CertificatePinner, + proxy: Proxy? + ): OkHttpClient { + val isCertPinningEnabled = Preferences.getBoolean( + context, + PREFERENCE_CERTIFICATE_PINNING_ENABLED, + true + ) + + val builder = OkHttpClient().newBuilder() .proxy(proxy) - .certificatePinner(certPinner) .connectTimeout(25, TimeUnit.SECONDS) .readTimeout(25, TimeUnit.SECONDS) .writeTimeout(25, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .followRedirects(true) .followSslRedirects(true) - .build() + + if (isCertPinningEnabled) { + builder.certificatePinner(certPinner) + } else { + Log.i(TAG, "Certificate pinning is disabled") + } + + return builder.build() } @Provides @@ -79,12 +96,21 @@ object OkHttpClientModule { val googleRootCerts = getGoogleRootCertHashes(context).map { "sha256/$it" } .toTypedArray() - return CertificatePinner.Builder() + return CertificatePinner.Builder() .add("*.googleapis.com", *googleRootCerts) .add("*.google.com", *googleRootCerts) - .add("auroraoss.com", "sha256/mEflZT5enoR1FuXLgYYGqnVEoZvmf9c2bVBpiOjYQ0c=") // GTS Root R4 - .add("*.exodus-privacy.eu.org", "sha256/C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M=") // ISRG Root X1 - .add("gitlab.com", "sha256/x4QzPSC810K5/cMjb05Qm4k3Bw5zBn4lTdO/nEW/Td4=") // USERTrust RSA Certification Authority + .add( + "auroraoss.com", + "sha256/mEflZT5enoR1FuXLgYYGqnVEoZvmf9c2bVBpiOjYQ0c=" + ) // GTS Root R4 + .add( + "*.exodus-privacy.eu.org", + "sha256/C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M=" + ) // ISRG Root X1 + .add( + "gitlab.com", + "sha256/x4QzPSC810K5/cMjb05Qm4k3Bw5zBn4lTdO/nEW/Td4=" + ) // USERTrust RSA Certification Authority .build() } @@ -121,7 +147,8 @@ object OkHttpClientModule { private fun getGoogleRootCertHashes(context: Context): List { return try { - val certs = getX509Certificates(context.resources.openRawResource(R.raw.google_roots_ca)) + val certs = + getX509Certificates(context.resources.openRawResource(R.raw.google_roots_ca)) certs.map { val messageDigest = MessageDigest.getInstance(Algorithm.SHA256.value) messageDigest.update(it.publicKey.encoded) diff --git a/app/src/main/java/com/aurora/store/util/Preferences.kt b/app/src/main/java/com/aurora/store/util/Preferences.kt index 0026bae7c..c00d3ffc8 100644 --- a/app/src/main/java/com/aurora/store/util/Preferences.kt +++ b/app/src/main/java/com/aurora/store/util/Preferences.kt @@ -47,6 +47,7 @@ object Preferences { const val PREFERENCE_PROXY_URL = "PREFERENCE_PROXY_URL" const val PREFERENCE_PROXY_INFO = "PREFERENCE_PROXY_INFO" const val PREFERENCE_PROXY_ENABLED = "PREFERENCE_PROXY_ENABLED" + const val PREFERENCE_CERTIFICATE_PINNING_ENABLED = "PREFERENCE_CERTIFICATE_PINNING_ENABLED" const val PREFERENCE_DISPENSER_URLS = "PREFERENCE_DISPENSER_URLS" const val PREFERENCE_VENDING_VERSION = "PREFERENCE_VENDING_VERSION" diff --git a/app/src/main/java/com/aurora/store/view/ui/onboarding/OnboardingFragment.kt b/app/src/main/java/com/aurora/store/view/ui/onboarding/OnboardingFragment.kt index 43d458aa8..33b5aa97d 100644 --- a/app/src/main/java/com/aurora/store/view/ui/onboarding/OnboardingFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/onboarding/OnboardingFragment.kt @@ -31,6 +31,7 @@ import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import com.aurora.Constants import com.aurora.extensions.areNotificationsEnabled import com.aurora.extensions.isIgnoringBatteryOptimizations +import com.aurora.store.BuildConfig import com.aurora.store.R import com.aurora.store.data.helper.UpdateHelper import com.aurora.store.data.model.UpdateMode @@ -40,6 +41,7 @@ import com.aurora.store.util.CertUtil import com.aurora.store.util.PackageUtil import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences.PREFERENCE_AUTO_DELETE +import com.aurora.store.util.Preferences.PREFERENCE_CERTIFICATE_PINNING_ENABLED import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB import com.aurora.store.util.Preferences.PREFERENCE_DISPENSER_URLS @@ -169,6 +171,7 @@ class OnboardingFragment : BaseFragment() { if (!CertUtil.isAppGalleryApp(requireContext(), requireContext().packageName)) { save(PREFERENCE_DISPENSER_URLS, setOf(Constants.URL_DISPENSER)) } + save(PREFERENCE_CERTIFICATE_PINNING_ENABLED, !BuildConfig.DEBUG) save(PREFERENCE_VENDING_VERSION, 0) /*Customization*/ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 72d6baf6b..83c1a8697 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -231,6 +231,8 @@ Proxy "Enable proxy" "Allow all traffic from app to go through the proxy" + "Enable certificate pinning" + "Locks the app to trust only specific server certificates, preventing connections to untrusted or compromised servers." "Proxy URL" Enter a valid proxy URL to pass all data through the proxy. "Customization" diff --git a/app/src/main/res/xml/preferences_network.xml b/app/src/main/res/xml/preferences_network.xml index e0f4856d7..8feabd5d3 100644 --- a/app/src/main/res/xml/preferences_network.xml +++ b/app/src/main/res/xml/preferences_network.xml @@ -44,6 +44,12 @@ app:singleLineTitle="false" app:title="@string/pref_common_extra" /> + + Date: Mon, 16 Dec 2024 22:07:47 +0530 Subject: [PATCH 24/39] AppsGamesFragment: Allow exporting list as favourites - This will allow isers to export there current installed app & then do bulk import as favourites - Probably allowing a bulk install in favourites later would be a great addition --- app/src/main/java/com/aurora/Constants.kt | 5 +- .../store/data/room/favourite/Favourite.kt | 13 ++++ .../store/view/ui/all/AppsGamesFragment.kt | 60 ++++++++++++++++++- .../view/ui/commons/BlacklistFragment.kt | 6 +- .../view/ui/commons/FavouriteFragment.kt | 6 +- .../store/viewmodel/all/FavouriteViewModel.kt | 29 ++++++--- .../store/viewmodel/all/InstalledViewModel.kt | 20 +++++++ .../viewmodel/details/AppDetailsViewModel.kt | 4 +- 8 files changed, 125 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/aurora/Constants.kt b/app/src/main/java/com/aurora/Constants.kt index f691fe06a..16732cfe3 100644 --- a/app/src/main/java/com/aurora/Constants.kt +++ b/app/src/main/java/com/aurora/Constants.kt @@ -35,7 +35,8 @@ object Constants { const val SHARE_URL = "https://play.google.com/store/apps/details?id=" const val UPDATE_URL_STABLE = "https://gitlab.com/AuroraOSS/AuroraStore/raw/master/updates.json" - const val UPDATE_URL_NIGHTLY = "https://auroraoss.com/downloads/AuroraStore/Feeds/nightly_feed.json" + const val UPDATE_URL_NIGHTLY = + "https://auroraoss.com/downloads/AuroraStore/Feeds/nightly_feed.json" const val NOTIFICATION_CHANNEL_EXPORT = "NOTIFICATION_CHANNEL_EXPORT" const val NOTIFICATION_CHANNEL_INSTALL = "NOTIFICATION_CHANNEL_INSTALL" @@ -56,4 +57,6 @@ object Constants { const val PAGE_TYPE = "PAGE_TYPE" const val TOP_CHART_TYPE = "TOP_CHART_TYPE" const val TOP_CHART_CATEGORY = "TOP_CHART_CATEGORY" + + const val JSON_MIME_TYPE = "application/json" } diff --git a/app/src/main/java/com/aurora/store/data/room/favourite/Favourite.kt b/app/src/main/java/com/aurora/store/data/room/favourite/Favourite.kt index 4ab8da49b..34765092b 100644 --- a/app/src/main/java/com/aurora/store/data/room/favourite/Favourite.kt +++ b/app/src/main/java/com/aurora/store/data/room/favourite/Favourite.kt @@ -3,6 +3,7 @@ package com.aurora.store.data.room.favourite import android.os.Parcelable import androidx.room.Entity import androidx.room.PrimaryKey +import com.aurora.gplayapi.data.models.App import kotlinx.parcelize.Parcelize @Parcelize @@ -16,6 +17,18 @@ data class Favourite( val mode: Mode ) : Parcelable { + companion object { + fun fromApp(app: App, mode: Mode): Favourite { + return Favourite( + packageName = app.packageName, + displayName = app.displayName, + iconURL = app.iconArtwork.url, + added = System.currentTimeMillis(), + mode = mode + ) + } + } + enum class Mode { MANUAL, IMPORT diff --git a/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt b/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt index e391a10b0..db9d20e2e 100644 --- a/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt @@ -19,15 +19,25 @@ package com.aurora.store.view.ui.all +import android.net.Uri import android.os.Bundle import android.text.Editable import android.text.TextWatcher +import android.view.ContextThemeWrapper +import android.view.MenuInflater +import android.view.MenuItem import android.view.View +import android.widget.PopupMenu +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.content.res.AppCompatResources import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import com.aurora.Constants +import com.aurora.extensions.toast import com.aurora.gplayapi.data.models.App import com.aurora.store.AuroraApp +import com.aurora.store.R import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.model.MinimalApp import com.aurora.store.databinding.FragmentGenericWithSearchBinding @@ -38,12 +48,18 @@ import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.viewmodel.all.InstalledViewModel import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch +import java.util.Calendar @AndroidEntryPoint class AppsGamesFragment : BaseFragment() { private val viewModel: InstalledViewModel by viewModels() + private val startForDocumentExport = + registerForActivityResult(ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE)) { + if (it != null) exportInstalledApps(it) else toast(R.string.toast_fav_export_failed) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -69,9 +85,17 @@ class AppsGamesFragment : BaseFragment() { // Toolbar binding.layoutToolbarNative.apply { imgActionPrimary.visibility = View.VISIBLE - imgActionSecondary.visibility = View.GONE + imgActionSecondary.visibility = View.VISIBLE imgActionPrimary.setOnClickListener { findNavController().navigateUp() } + imgActionSecondary.apply { + setImageDrawable( + AppCompatResources.getDrawable(requireContext(), R.drawable.ic_menu) + ) + setOnClickListener { + showMenu(it) + } + } inputSearch.addTextChangedListener(object : TextWatcher { override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { @@ -97,6 +121,35 @@ class AppsGamesFragment : BaseFragment() { } } + private fun showMenu(anchor: View) { + val popupMenu = PopupMenu( + ContextThemeWrapper( + requireContext(), + R.style.AppTheme_PopupMenu + ), anchor + ) + + val inflater: MenuInflater = popupMenu.menuInflater + inflater.inflate(R.menu.menu_import_export, popupMenu.menu) + + popupMenu.menu.removeItem(R.id.action_import) + + popupMenu.setOnMenuItemClickListener { menuItem: MenuItem -> + when (menuItem.itemId) { + R.id.action_export -> { + startForDocumentExport.launch( + "aurora_store_apps_${Calendar.getInstance().time.time}.json" + ) + true + } + + else -> false + } + } + + popupMenu.show() + } + private fun updateController(packages: List?) { binding.recycler.withModels { setFilterDuplicates(true) @@ -137,4 +190,9 @@ class AppsGamesFragment : BaseFragment() { } } } + + private fun exportInstalledApps(uri: Uri) { + viewModel.exportApps(requireContext(), uri) + toast(R.string.toast_fav_export_success) + } } diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt index f72a71461..df0758433 100644 --- a/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt @@ -34,6 +34,7 @@ import androidx.appcompat.content.res.AppCompatResources import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import com.aurora.Constants import com.aurora.extensions.toast import com.aurora.store.AuroraApp import com.aurora.store.R @@ -51,13 +52,12 @@ class BlacklistFragment : BaseFragment() { private val viewModel: BlacklistViewModel by viewModels() - private val mimeType = "application/json" private val startForDocumentImport = registerForActivityResult(ActivityResultContracts.OpenDocument()) { if (it != null) importBlacklist(it) else toast(R.string.toast_black_import_failed) } private val startForDocumentExport = - registerForActivityResult(ActivityResultContracts.CreateDocument(mimeType)) { + registerForActivityResult(ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE)) { if (it != null) exportBlacklist(it) else toast(R.string.toast_black_export_failed) } @@ -132,7 +132,7 @@ class BlacklistFragment : BaseFragment() { popupMenu.setOnMenuItemClickListener { menuItem: MenuItem -> when (menuItem.itemId) { R.id.action_import -> { - startForDocumentImport.launch(arrayOf(mimeType)) + startForDocumentImport.launch(arrayOf(Constants.JSON_MIME_TYPE)) true } diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/FavouriteFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/FavouriteFragment.kt index 202368320..29651dc9a 100644 --- a/app/src/main/java/com/aurora/store/view/ui/commons/FavouriteFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/commons/FavouriteFragment.kt @@ -26,6 +26,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import com.aurora.Constants import com.aurora.extensions.toast import com.aurora.store.R import com.aurora.store.data.room.favourite.Favourite @@ -42,13 +43,12 @@ import java.util.Calendar class FavouriteFragment : BaseFragment() { private val viewModel: FavouriteViewModel by viewModels() - private val mimeType = "application/json" private val startForDocumentImport = registerForActivityResult(ActivityResultContracts.OpenDocument()) { if (it != null) importFavourites(it) else toast(R.string.toast_fav_import_failed) } private val startForDocumentExport = - registerForActivityResult(ActivityResultContracts.CreateDocument(mimeType)) { + registerForActivityResult(ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE)) { if (it != null) exportFavourites(it) else toast(R.string.toast_fav_export_failed) } @@ -65,7 +65,7 @@ class FavouriteFragment : BaseFragment() { binding.toolbar.apply { setOnMenuItemClickListener { when (it.itemId) { - R.id.action_import -> startForDocumentImport.launch(arrayOf(mimeType)) + R.id.action_import -> startForDocumentImport.launch(arrayOf(Constants.JSON_MIME_TYPE)) R.id.action_export -> { startForDocumentExport.launch( "aurora_store_favourites_${Calendar.getInstance().time.time}.json" diff --git a/app/src/main/java/com/aurora/store/viewmodel/all/FavouriteViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/all/FavouriteViewModel.kt index 87ae60bea..09a42246e 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/all/FavouriteViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/all/FavouriteViewModel.kt @@ -21,6 +21,7 @@ package com.aurora.store.viewmodel.all import android.content.Context import android.net.Uri +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.store.data.room.favourite.Favourite @@ -39,26 +40,38 @@ class FavouriteViewModel @Inject constructor( private val favouriteDao: FavouriteDao, private val gson: Gson ) : ViewModel() { + private val TAG = FavouriteViewModel::class.java.simpleName val favouritesList = favouriteDao.favourites() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) fun importFavourites(context: Context, uri: Uri) { viewModelScope.launch(Dispatchers.IO) { - context.contentResolver.openInputStream(uri)?.use { - val importExport = - gson.fromJson(it.bufferedReader().readText(), ImportExport::class.java) - favouriteDao.insertAll( - importExport.favourites.map { fav -> fav.copy(mode = Favourite.Mode.IMPORT) } - ) + try { + context.contentResolver.openInputStream(uri)?.use { + val importExport = gson.fromJson( + it.bufferedReader().readText(), + ImportExport::class.java + ) + + favouriteDao.insertAll( + importExport.favourites.map { fav -> fav.copy(mode = Favourite.Mode.IMPORT) } + ) + } + } catch (exception: Exception) { + Log.e(TAG, "Failed to import favourites", exception) } } } fun exportFavourites(context: Context, uri: Uri) { viewModelScope.launch(Dispatchers.IO) { - context.contentResolver.openOutputStream(uri)?.use { - it.write(gson.toJson(ImportExport(favouritesList.value!!)).encodeToByteArray()) + try { + context.contentResolver.openOutputStream(uri)?.use { + it.write(gson.toJson(ImportExport(favouritesList.value!!)).encodeToByteArray()) + } + } catch (exception: Exception) { + Log.e(TAG, "Failed to export favourites", exception) } } } diff --git a/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt index 442953a96..73353fee6 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt @@ -20,13 +20,17 @@ package com.aurora.store.viewmodel.all import android.content.Context +import android.net.Uri import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.helpers.web.WebAppDetailsHelper import com.aurora.store.data.providers.BlacklistProvider +import com.aurora.store.data.room.favourite.Favourite +import com.aurora.store.data.room.favourite.ImportExport import com.aurora.store.util.PackageUtil +import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers @@ -39,6 +43,7 @@ import javax.inject.Inject class InstalledViewModel @Inject constructor( @ApplicationContext private val context: Context, private val blacklistProvider: BlacklistProvider, + private val gson: Gson ) : ViewModel() { private val TAG = InstalledViewModel::class.java.simpleName @@ -69,4 +74,19 @@ class InstalledViewModel @Inject constructor( } } } + + fun exportApps(context: Context, uri: Uri) { + viewModelScope.launch(Dispatchers.IO) { + try { + val favourites: List = apps.value!!.map { app -> + Favourite.fromApp(app, Favourite.Mode.IMPORT) + } + context.contentResolver.openOutputStream(uri)?.use { + it.write(gson.toJson(ImportExport(favourites)).encodeToByteArray()) + } + } catch (exception: Exception) { + Log.e(TAG, "Failed to installed apps", exception) + } + } + } } 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 a9aa2f057..367e61074 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 @@ -236,8 +236,8 @@ class AppDetailsViewModel @Inject constructor( private fun getLatestExodusReport(packageName: String): Report? { val headers: MutableMap = mutableMapOf() - headers["Content-Type"] = "application/json" - headers["Accept"] = "application/json" + headers["Content-Type"] = Constants.JSON_MIME_TYPE + headers["Accept"] = Constants.JSON_MIME_TYPE headers["Authorization"] = "Token ${BuildConfig.EXODUS_API_KEY}" val url = Constants.EXODUS_SEARCH_URL + packageName From dc20fbd67ca96df7a57b4c43cc80b3cdff65fd1e Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Mon, 16 Dec 2024 22:20:40 +0530 Subject: [PATCH 25/39] AppDetails: Do not hide textviews --- app/src/main/java/com/aurora/extensions/View.kt | 11 ----------- .../store/view/ui/details/AppDetailsFragment.kt | 11 +++++------ 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/aurora/extensions/View.kt b/app/src/main/java/com/aurora/extensions/View.kt index 5296bfd12..c7ca5629c 100644 --- a/app/src/main/java/com/aurora/extensions/View.kt +++ b/app/src/main/java/com/aurora/extensions/View.kt @@ -21,7 +21,6 @@ package com.aurora.extensions import android.view.View import android.view.inputmethod.InputMethodManager -import android.widget.TextView import androidx.core.content.getSystemService fun View.isVisible() = visibility == View.VISIBLE @@ -52,13 +51,3 @@ fun View.hideKeyboard(): Boolean { } return false } - - -fun TextView.updateText(text: String?) { - if (text.isNullOrEmpty()) { - hide() - } else { - show() - this.text = text - } -} \ No newline at end of file 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 3646a1bc7..2c44fbb7b 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 @@ -55,7 +55,6 @@ import com.aurora.extensions.runOnUiThread import com.aurora.extensions.share import com.aurora.extensions.show import com.aurora.extensions.toast -import com.aurora.extensions.updateText import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.Review import com.aurora.gplayapi.data.models.StreamBundle @@ -487,10 +486,10 @@ class AppDetailsFragment : BaseFragment() { } } - packageName.updateText(app.packageName) - txtLine1.updateText(app.displayName) - txtLine2.updateText(app.developerName) - txtLine3.updateText(("${app.versionName} (${app.versionCode})")) + packageName.text = app.packageName + txtLine1.text = app.displayName + txtLine2.text = app.developerName + txtLine3.text = ("${app.versionName} (${app.versionCode})") txtLine2.setOnClickListener { findNavController().navigate( @@ -503,7 +502,7 @@ class AppDetailsFragment : BaseFragment() { 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))) - txtLine4.updateText(tags.joinToString(separator = " • ")) + txtLine4.text = tags.joinToString(separator = " • ") } } } From d03e87d02cf291898534d86b78eceb97f9de8e53 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Mon, 16 Dec 2024 22:27:47 +0530 Subject: [PATCH 26/39] Favourites: Minor improvements --- .../com/aurora/store/data/room/favourite/Favourite.kt | 9 +++++++++ .../aurora/store/view/ui/commons/FavouriteFragment.kt | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/aurora/store/data/room/favourite/Favourite.kt b/app/src/main/java/com/aurora/store/data/room/favourite/Favourite.kt index 34765092b..b71017988 100644 --- a/app/src/main/java/com/aurora/store/data/room/favourite/Favourite.kt +++ b/app/src/main/java/com/aurora/store/data/room/favourite/Favourite.kt @@ -4,6 +4,7 @@ import android.os.Parcelable import androidx.room.Entity import androidx.room.PrimaryKey import com.aurora.gplayapi.data.models.App +import com.aurora.gplayapi.data.models.Artwork import kotlinx.parcelize.Parcelize @Parcelize @@ -27,6 +28,14 @@ data class Favourite( mode = mode ) } + + fun Favourite.toApp(): App { + return App( + packageName = packageName, + displayName = displayName, + iconArtwork = Artwork(url = iconURL) + ) + } } enum class Mode { diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/FavouriteFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/FavouriteFragment.kt index 29651dc9a..5a6b01c30 100644 --- a/app/src/main/java/com/aurora/store/view/ui/commons/FavouriteFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/commons/FavouriteFragment.kt @@ -30,6 +30,7 @@ import com.aurora.Constants import com.aurora.extensions.toast import com.aurora.store.R import com.aurora.store.data.room.favourite.Favourite +import com.aurora.store.data.room.favourite.Favourite.Companion.toApp import com.aurora.store.databinding.FragmentFavouriteBinding import com.aurora.store.view.epoxy.views.FavouriteViewModel_ import com.aurora.store.view.epoxy.views.app.NoAppViewModel_ @@ -103,7 +104,7 @@ class FavouriteFragment : BaseFragment() { FavouriteViewModel_() .id(it.packageName.hashCode()) .favourite(it) - .onClick { _ -> openDetailsFragment(it.packageName) } + .onClick { _ -> openDetailsFragment(it.packageName, it.toApp()) } .onFavourite { _ -> viewModel.removeFavourite(it.packageName) } ) } From d85bcfd2accfa4c653e5f8ee4199a06962a3c719 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Mon, 16 Dec 2024 22:58:37 +0530 Subject: [PATCH 27/39] AppDetails: Do not clear menu instead don not inflate if not required --- .../view/ui/details/AppDetailsFragment.kt | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 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 2c44fbb7b..b9bba59dd 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 @@ -117,7 +117,6 @@ class AppDetailsFragment : BaseFragment() { private var downloadStatus = DownloadStatus.UNAVAILABLE private var isUpdatable: Boolean = false - private var uninstallActionEnabled = false private val tags = mutableSetOf() @@ -192,7 +191,6 @@ class AppDetailsFragment : BaseFragment() { app.apply { // Check whether app is installed or not isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) - uninstallActionEnabled = isInstalled } // Show the basic app details, while the rest of the data is being fetched @@ -227,6 +225,8 @@ class AppDetailsFragment : BaseFragment() { // Fetch Exodus Privacy Report viewModel.fetchAppReport(app.packageName) + + } else { toast(getString(R.string.status_unavailable)) // TODO: Redirect to App Unavailable Fragment @@ -402,11 +402,10 @@ class AppDetailsFragment : BaseFragment() { } } - // Clear Menu, so that it clears the previous menu items before inflating new ones - menu.clear() - - // Inflate Menu - inflateMenu(R.menu.menu_details) + if (menu.size() == 0) { + // Inflate Menu only if it is not already inflated + inflateMenu(R.menu.menu_details) + } // Adjust Menu Items menu.let { @@ -697,10 +696,6 @@ class AppDetailsFragment : BaseFragment() { } } } - - if (!uninstallActionEnabled) { - binding.layoutDetailsToolbar.toolbar.invalidateMenu() - } } else { if (downloadStatus in DownloadStatus.running) { updateProgress(-1) @@ -732,10 +727,6 @@ class AppDetailsFragment : BaseFragment() { startDownload() } } - - if (uninstallActionEnabled) { - binding.layoutDetailsToolbar.toolbar.invalidateMenu() - } } } } From 5d7d7c5ffa1e9df8692402334500a6981b446193 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Mon, 16 Dec 2024 23:12:56 +0530 Subject: [PATCH 28/39] AppDetails: Show warning if app in unavailable for the device --- .../store/view/ui/details/AppDetailsFragment.kt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 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 b9bba59dd..1a257a882 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 @@ -217,6 +217,10 @@ class AppDetailsFragment : BaseFragment() { updateAppHeader(app) // Re-inflate the app details, as web data may vary. updateExtraDetails(app) + if (app.versionCode == 0) { + warnAppUnavailable(app) + } + // Fetch App Reviews viewModel.fetchAppReviews(app.packageName) @@ -225,8 +229,6 @@ class AppDetailsFragment : BaseFragment() { // Fetch Exodus Privacy Report viewModel.fetchAppReport(app.packageName) - - } else { toast(getString(R.string.status_unavailable)) // TODO: Redirect to App Unavailable Fragment @@ -1032,6 +1034,13 @@ class AppDetailsFragment : BaseFragment() { } } + private fun warnAppUnavailable(app: App) { + AuroraApp.events.send(InstallerEvent.Failed(app.packageName).apply { + error = getString(R.string.status_unavailable) + extra = getString(R.string.toast_app_unavailable) + }) + } + /* App Review Helpers */ private fun addAvgReviews(number: Int, max: Long, rating: Long): RelativeLayout { From 3ec134279fa4eb72f27d6e6b40e1e7b1147f0206 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Fri, 6 Dec 2024 12:58:27 +0800 Subject: [PATCH 29/39] view_toolbar_native: Switch to actual toolbar everywhere Signed-off-by: Aayush Gupta --- .../view/ui/commons/CategoryBrowseFragment.kt | 5 +-- .../commons/ExpandedStreamBrowseFragment.kt | 7 ++-- .../view/ui/commons/StreamBrowseFragment.kt | 5 +-- .../view/ui/details/AppDetailsFragment.kt | 12 +++---- .../view/ui/details/DetailsExodusFragment.kt | 4 +-- .../store/view/ui/details/DevAppsFragment.kt | 5 +-- .../view/ui/downloads/DownloadFragment.kt | 7 +--- .../store/view/ui/splash/SplashFragment.kt | 8 ++--- .../store/view/ui/spoof/SpoofFragment.kt | 11 ++----- .../main/res/layout-land/fragment_splash.xml | 10 ++++-- app/src/main/res/layout/fragment_details.xml | 11 +++++-- app/src/main/res/layout/fragment_download.xml | 14 +++++--- .../layout/fragment_generic_with_toolbar.xml | 14 +++++--- app/src/main/res/layout/fragment_splash.xml | 10 ++++-- ...eric_with_pager.xml => fragment_spoof.xml} | 16 ++++++--- app/src/main/res/layout/settings_activity.xml | 32 ------------------ .../main/res/layout/view_toolbar_native.xml | 33 ------------------- .../main/res/navigation/mobile_navigation.xml | 4 +-- 18 files changed, 72 insertions(+), 136 deletions(-) rename app/src/main/res/layout/{fragment_generic_with_pager.xml => fragment_spoof.xml} (78%) delete mode 100644 app/src/main/res/layout/settings_activity.xml delete mode 100644 app/src/main/res/layout/view_toolbar_native.xml diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/CategoryBrowseFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/CategoryBrowseFragment.kt index bb466bd7c..7fc2cb8e4 100644 --- a/app/src/main/java/com/aurora/store/view/ui/commons/CategoryBrowseFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/commons/CategoryBrowseFragment.kt @@ -21,7 +21,6 @@ package com.aurora.store.view.ui.commons import android.os.Bundle import android.view.View -import androidx.core.content.ContextCompat import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs @@ -30,7 +29,6 @@ import com.aurora.gplayapi.data.models.StreamBundle import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.helpers.contracts.StreamContract import com.aurora.gplayapi.utils.CategoryUtil -import com.aurora.store.R import com.aurora.store.data.model.ViewState import com.aurora.store.data.model.ViewState.Loading.getDataAs import com.aurora.store.databinding.FragmentGenericWithToolbarBinding @@ -57,9 +55,8 @@ class CategoryBrowseFragment : BaseFragment() val genericCarouselController = CategoryCarouselController(this) // Toolbar - binding.layoutToolbarNative.toolbar.apply { + binding.toolbar.apply { title = args.title - navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back) setNavigationOnClickListener { findNavController().navigateUp() } } diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/ExpandedStreamBrowseFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/ExpandedStreamBrowseFragment.kt index 2841e30d3..c1be4599d 100644 --- a/app/src/main/java/com/aurora/store/view/ui/commons/ExpandedStreamBrowseFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/commons/ExpandedStreamBrowseFragment.kt @@ -21,13 +21,11 @@ package com.aurora.store.view.ui.commons import android.os.Bundle import android.view.View -import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.airbnb.epoxy.EpoxyModel import com.aurora.gplayapi.data.models.StreamCluster -import com.aurora.store.R import com.aurora.store.databinding.FragmentGenericWithToolbarBinding import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener import com.aurora.store.view.epoxy.groups.CarouselHorizontalModel_ @@ -51,9 +49,8 @@ class ExpandedStreamBrowseFragment : BaseFragment() { streamCluster = args.cluster // Toolbar - binding.layoutToolbarNative.toolbar.apply { + binding.toolbar.apply { title = streamCluster.clusterTitle - navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back) setNavigationOnClickListener { findNavController().navigateUp() } } 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 1a257a882..f201fe68e 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 @@ -131,7 +131,7 @@ class AppDetailsFragment : BaseFragment() { if (app.packageName == event.packageName) { checkAndSetupInstall() transformIcon(false) - binding.layoutDetailsToolbar.toolbar.menu.apply { + binding.toolbar.menu.apply { findItem(R.id.action_home_screen)?.isVisible = ShortcutManagerUtil.canPinShortcut(requireContext(), app.packageName) findItem(R.id.action_uninstall)?.isVisible = true @@ -144,7 +144,7 @@ class AppDetailsFragment : BaseFragment() { if (app.packageName == event.packageName) { checkAndSetupInstall() transformIcon(false) - binding.layoutDetailsToolbar.toolbar.menu.apply { + 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 @@ -360,12 +360,10 @@ class AppDetailsFragment : BaseFragment() { viewLifecycleOwner.lifecycleScope.launch { viewModel.favourite.collect { if (it) { - binding.layoutDetailsToolbar.toolbar.menu - ?.findItem(R.id.action_favourite) + binding.toolbar.menu?.findItem(R.id.action_favourite) ?.setIcon(R.drawable.ic_favorite_checked) } else { - binding.layoutDetailsToolbar.toolbar.menu - ?.findItem(R.id.action_favourite) + binding.toolbar.menu?.findItem(R.id.action_favourite) ?.setIcon(R.drawable.ic_favorite_unchecked) } } @@ -392,7 +390,7 @@ class AppDetailsFragment : BaseFragment() { } private fun updateToolbar(app: App) { - binding.layoutDetailsToolbar.toolbar.apply { + binding.toolbar.apply { elevation = 0f navigationIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_arrow_back) diff --git a/app/src/main/java/com/aurora/store/view/ui/details/DetailsExodusFragment.kt b/app/src/main/java/com/aurora/store/view/ui/details/DetailsExodusFragment.kt index 9e4fc6aa1..a3be33f54 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/DetailsExodusFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/DetailsExodusFragment.kt @@ -21,7 +21,6 @@ package com.aurora.store.view.ui.details import android.os.Bundle import android.view.View -import androidx.core.content.ContextCompat import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.aurora.Constants @@ -49,9 +48,8 @@ class DetailsExodusFragment : BaseFragment() super.onViewCreated(view, savedInstanceState) // Toolbar - binding.layoutToolbarNative.toolbar.apply { + binding.toolbar.apply { title = args.displayName - navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back) setNavigationOnClickListener { findNavController().navigateUp() } } diff --git a/app/src/main/java/com/aurora/store/view/ui/details/DevAppsFragment.kt b/app/src/main/java/com/aurora/store/view/ui/details/DevAppsFragment.kt index eee29f42b..2de10d2c9 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/DevAppsFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/DevAppsFragment.kt @@ -21,12 +21,10 @@ package com.aurora.store.view.ui.details import android.os.Bundle import android.view.View -import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.aurora.gplayapi.data.models.SearchBundle -import com.aurora.store.R import com.aurora.store.databinding.FragmentGenericWithToolbarBinding import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener import com.aurora.store.view.epoxy.views.AppProgressViewModel_ @@ -49,9 +47,8 @@ class DevAppsFragment : BaseFragment() { } // Toolbar - binding.layoutToolbarNative.toolbar.apply { + binding.toolbar.apply { title = args.developerName - navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back) setNavigationOnClickListener { findNavController().navigateUp() } } diff --git a/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadFragment.kt b/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadFragment.kt index 146242f74..ea292f41e 100644 --- a/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadFragment.kt @@ -22,7 +22,6 @@ package com.aurora.store.view.ui.downloads import android.os.Bundle import android.text.format.DateUtils import android.view.View -import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.aurora.Constants.GITLAB_URL @@ -54,11 +53,7 @@ class DownloadFragment : BaseFragment() { super.onViewCreated(view, savedInstanceState) // Toolbar - binding.layoutToolbarAction.toolbar.apply { - elevation = 0f - title = getString(R.string.title_download_manager) - navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back) - inflateMenu(R.menu.menu_download_main) + binding.toolbar.apply { setNavigationOnClickListener { findNavController().navigateUp() } setOnMenuItemClickListener { when (it.itemId) { diff --git a/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt b/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt index cc61641d3..c2c3b1f24 100644 --- a/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt @@ -85,9 +85,7 @@ class SplashFragment : BaseFragment() { } // Toolbar - binding.layoutToolbarAction.toolbar.apply { - elevation = 0f - inflateMenu(R.menu.menu_splash) + binding.toolbar.apply { setOnMenuItemClickListener { when (it.itemId) { R.id.menu_blacklist_manager -> { @@ -186,10 +184,10 @@ class SplashFragment : BaseFragment() { private fun updateActionLayout(isVisible: Boolean) { if (isVisible) { binding.layoutAction.show() - binding.layoutToolbarAction.toolbar.visibility = View.VISIBLE + binding.toolbar.visibility = View.VISIBLE } else { binding.layoutAction.hide() - binding.layoutToolbarAction.toolbar.visibility = View.GONE + binding.toolbar.visibility = View.GONE } } diff --git a/app/src/main/java/com/aurora/store/view/ui/spoof/SpoofFragment.kt b/app/src/main/java/com/aurora/store/view/ui/spoof/SpoofFragment.kt index 16df22231..b7410ad3c 100644 --- a/app/src/main/java/com/aurora/store/view/ui/spoof/SpoofFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/spoof/SpoofFragment.kt @@ -25,7 +25,6 @@ import android.os.Bundle import android.util.Log import android.view.View import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle @@ -34,7 +33,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter import com.aurora.extensions.toast import com.aurora.store.R import com.aurora.store.data.providers.NativeDeviceInfoProvider -import com.aurora.store.databinding.FragmentGenericWithPagerBinding +import com.aurora.store.databinding.FragmentSpoofBinding import com.aurora.store.util.PathUtil import com.aurora.store.view.ui.commons.BaseFragment import com.google.android.material.tabs.TabLayout @@ -42,7 +41,7 @@ import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class SpoofFragment : BaseFragment() { +class SpoofFragment : BaseFragment() { private val TAG = SpoofFragment::class.java.simpleName // Android is weird, even if export device config with proper mime type, it will refuse to open @@ -63,11 +62,7 @@ class SpoofFragment : BaseFragment() { super.onViewCreated(view, savedInstanceState) // Toolbar - binding.layoutActionToolbar.toolbar.apply { - elevation = 0f - title = getString(R.string.title_spoof_manager) - navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back) - inflateMenu(R.menu.menu_import_export) + binding.toolbar.apply { setNavigationOnClickListener { findNavController().navigateUp() } setOnMenuItemClickListener { when (it.itemId) { diff --git a/app/src/main/res/layout-land/fragment_splash.xml b/app/src/main/res/layout-land/fragment_splash.xml index 36c580854..1dd28284d 100644 --- a/app/src/main/res/layout-land/fragment_splash.xml +++ b/app/src/main/res/layout-land/fragment_splash.xml @@ -28,9 +28,13 @@ android:weightSum="2" tools:context=".view.ui.splash.SplashFragment"> - + - + - + - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_generic_with_toolbar.xml b/app/src/main/res/layout/fragment_generic_with_toolbar.xml index ed2126f86..af5f55429 100644 --- a/app/src/main/res/layout/fragment_generic_with_toolbar.xml +++ b/app/src/main/res/layout/fragment_generic_with_toolbar.xml @@ -23,17 +23,21 @@ android:layout_height="match_parent" android:orientation="vertical"> - + - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_splash.xml b/app/src/main/res/layout/fragment_splash.xml index d93b2240b..1b81ecfc6 100644 --- a/app/src/main/res/layout/fragment_splash.xml +++ b/app/src/main/res/layout/fragment_splash.xml @@ -28,9 +28,13 @@ android:weightSum="2" tools:context=".view.ui.splash.SplashFragment"> - + - + - \ No newline at end of file + diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml deleted file mode 100644 index 15898ab7e..000000000 --- a/app/src/main/res/layout/settings_activity.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_toolbar_native.xml b/app/src/main/res/layout/view_toolbar_native.xml deleted file mode 100644 index 266e9115b..000000000 --- a/app/src/main/res/layout/view_toolbar_native.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 249a94b0f..c69f436b5 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -71,12 +71,12 @@ android:id="@+id/appsGamesFragment" android:name="com.aurora.store.view.ui.all.AppsGamesFragment" android:label="@string/title_apps_games" - tools:layout="@layout/fragment_generic_with_pager" /> + tools:layout="@layout/fragment_generic_with_search" /> + tools:layout="@layout/fragment_spoof" /> Date: Thu, 12 Dec 2024 18:53:29 +0700 Subject: [PATCH 30/39] view_toolbar_search: Switch to native toolbar for search Signed-off-by: Aayush Gupta --- .../store/view/ui/all/AppsGamesFragment.kt | 98 +++++-------- .../view/ui/commons/BlacklistFragment.kt | 130 +++++++----------- .../view/ui/search/SearchResultsFragment.kt | 87 +++++------- .../ui/search/SearchSuggestionFragment.kt | 48 +++---- app/src/main/res/drawable/ic_menu.xml | 9 -- .../layout/fragment_generic_with_search.xml | 27 +++- .../res/layout/fragment_search_result.xml | 26 +++- .../res/layout/fragment_search_suggestion.xml | 29 +++- .../main/res/layout/view_toolbar_search.xml | 77 ----------- app/src/main/res/menu/menu_search.xml | 15 ++ app/src/main/res/values-night/themes.xml | 1 - app/src/main/res/values/themes.xml | 7 - 12 files changed, 222 insertions(+), 332 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_menu.xml delete mode 100644 app/src/main/res/layout/view_toolbar_search.xml create mode 100644 app/src/main/res/menu/menu_search.xml diff --git a/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt b/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt index db9d20e2e..1c4cb7bcf 100644 --- a/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt @@ -23,13 +23,8 @@ import android.net.Uri import android.os.Bundle import android.text.Editable import android.text.TextWatcher -import android.view.ContextThemeWrapper -import android.view.MenuInflater -import android.view.MenuItem import android.view.View -import android.widget.PopupMenu import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.content.res.AppCompatResources import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController @@ -83,71 +78,44 @@ class AppsGamesFragment : BaseFragment() { } // Toolbar - binding.layoutToolbarNative.apply { - imgActionPrimary.visibility = View.VISIBLE - imgActionSecondary.visibility = View.VISIBLE - - imgActionPrimary.setOnClickListener { findNavController().navigateUp() } - imgActionSecondary.apply { - setImageDrawable( - AppCompatResources.getDrawable(requireContext(), R.drawable.ic_menu) - ) - setOnClickListener { - showMenu(it) - } - } - - inputSearch.addTextChangedListener(object : TextWatcher { - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - if (s.isNullOrEmpty()) { - updateController(viewModel.apps.value) - } else { - val filteredPackages = viewModel.apps.value?.filter { - it.displayName.contains(s, true) || it.packageName.contains(s, true) - } - updateController(filteredPackages) + binding.toolbar.apply { + inflateMenu(R.menu.menu_import_export) + setNavigationOnClickListener { findNavController().navigateUp() } + setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_export -> { + startForDocumentExport.launch( + "aurora_store_apps_${Calendar.getInstance().time.time}.json" + ) + true } + + else -> false } - - override fun afterTextChanged(s: Editable?) {} - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int - ) { - } - }) - } - } - - private fun showMenu(anchor: View) { - val popupMenu = PopupMenu( - ContextThemeWrapper( - requireContext(), - R.style.AppTheme_PopupMenu - ), anchor - ) - - val inflater: MenuInflater = popupMenu.menuInflater - inflater.inflate(R.menu.menu_import_export, popupMenu.menu) - - popupMenu.menu.removeItem(R.id.action_import) - - popupMenu.setOnMenuItemClickListener { menuItem: MenuItem -> - when (menuItem.itemId) { - R.id.action_export -> { - startForDocumentExport.launch( - "aurora_store_apps_${Calendar.getInstance().time.time}.json" - ) - true - } - - else -> false } } - popupMenu.show() + binding.searchBar.addTextChangedListener(object : TextWatcher { + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + if (s.isNullOrEmpty()) { + updateController(viewModel.apps.value) + } else { + val filteredPackages = viewModel.apps.value?.filter { + it.displayName.contains(s, true) || it.packageName.contains(s, true) + } + updateController(filteredPackages) + } + } + + override fun afterTextChanged(s: Editable?) {} + override fun beforeTextChanged( + s: CharSequence?, + start: Int, + count: Int, + after: Int + ) { + } + }) } private fun updateController(packages: List?) { diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt index df0758433..35a251b74 100644 --- a/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt @@ -24,13 +24,8 @@ import android.net.Uri import android.os.Bundle import android.text.Editable import android.text.TextWatcher -import android.view.ContextThemeWrapper -import android.view.MenuInflater -import android.view.MenuItem import android.view.View -import android.widget.PopupMenu import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.content.res.AppCompatResources import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController @@ -71,46 +66,62 @@ class BlacklistFragment : BaseFragment() { } // Toolbar - binding.layoutToolbarNative.apply { - imgActionPrimary.visibility = View.VISIBLE - imgActionSecondary.apply { - visibility = View.VISIBLE - setImageDrawable( - AppCompatResources.getDrawable(requireContext(), R.drawable.ic_menu) - ) - setOnClickListener { - showMenu(it) - } - } - - imgActionPrimary.setOnClickListener { + binding.toolbar.apply { + inflateMenu(R.menu.menu_blacklist) + setNavigationOnClickListener { viewModel.blacklistProvider.blacklist = viewModel.selected findNavController().navigateUp() } + setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_import -> { + startForDocumentImport.launch(arrayOf(Constants.JSON_MIME_TYPE)) + } - inputSearch.addTextChangedListener(object : TextWatcher { - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - if (s.isNullOrEmpty()) { - updateController(viewModel.packages.value) - } else { - val filteredPackages = viewModel.packages.value?.filter { - it.applicationInfo!!.loadLabel(requireContext().packageManager) - .contains(s, true) || it.packageName.contains(s, true) - } - updateController(filteredPackages) + R.id.action_export -> { + startForDocumentExport.launch( + "aurora_store_apps_${Calendar.getInstance().time.time}.json" + ) + } + + R.id.action_select_all -> { + viewModel.selectAll() + binding.recycler.requestModelBuild() + true + } + + R.id.action_remove_all -> { + viewModel.removeAll() + binding.recycler.requestModelBuild() + true } } - - override fun afterTextChanged(s: Editable?) {} - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int - ) { - } - }) + true + } } + + binding.searchBar.addTextChangedListener(object : TextWatcher { + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + if (s.isNullOrEmpty()) { + updateController(viewModel.packages.value) + } else { + val filteredPackages = viewModel.packages.value?.filter { + it.applicationInfo!!.loadLabel(requireContext().packageManager) + .contains(s, true) || it.packageName.contains(s, true) + } + updateController(filteredPackages) + } + } + + override fun afterTextChanged(s: Editable?) {} + override fun beforeTextChanged( + s: CharSequence?, + start: Int, + count: Int, + after: Int + ) { + } + }) } override fun onPause() { @@ -118,49 +129,6 @@ class BlacklistFragment : BaseFragment() { viewModel.blacklistProvider.blacklist = viewModel.selected } - private fun showMenu(anchor: View) { - val popupMenu = PopupMenu( - ContextThemeWrapper( - requireContext(), - R.style.AppTheme_PopupMenu - ), anchor - ) - - val inflater: MenuInflater = popupMenu.menuInflater - inflater.inflate(R.menu.menu_blacklist, popupMenu.menu) - - popupMenu.setOnMenuItemClickListener { menuItem: MenuItem -> - when (menuItem.itemId) { - R.id.action_import -> { - startForDocumentImport.launch(arrayOf(Constants.JSON_MIME_TYPE)) - true - } - - R.id.action_export -> { - startForDocumentExport.launch( - "aurora_store_blacklist_${Calendar.getInstance().time.time}.json" - ) - true - } - - R.id.action_select_all -> { - viewModel.selectAll() - binding.recycler.requestModelBuild() - true - } - - R.id.action_remove_all -> { - viewModel.removeAll() - binding.recycler.requestModelBuild() - true - } - - else -> false - } - } - popupMenu.show() - } - private fun updateController(packages: List?) { binding.recycler.withModels { setFilterDuplicates(true) diff --git a/app/src/main/java/com/aurora/store/view/ui/search/SearchResultsFragment.kt b/app/src/main/java/com/aurora/store/view/ui/search/SearchResultsFragment.kt index f14385df2..599541918 100644 --- a/app/src/main/java/com/aurora/store/view/ui/search/SearchResultsFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/search/SearchResultsFragment.kt @@ -50,7 +50,6 @@ import com.aurora.store.view.epoxy.views.app.NoAppViewModel_ import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_ import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.viewmodel.search.SearchResultViewModel -import com.google.android.material.textfield.TextInputEditText import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -59,8 +58,6 @@ class SearchResultsFragment : BaseFragment(), private val viewModel: SearchResultViewModel by viewModels() - private lateinit var searchView: TextInputEditText - private lateinit var sharedPreferences: SharedPreferences private var query: String? = null @@ -84,20 +81,20 @@ class SearchResultsFragment : BaseFragment(), sharedPreferences.registerOnSharedPreferenceChangeListener(this) // Toolbar - binding.layoutViewToolbar.apply { - searchView = inputSearch - imgActionPrimary.setOnClickListener { + binding.toolbar.apply { + setNavigationOnClickListener { + binding.searchBar.hideKeyboard() findNavController().navigateUp() } - imgActionSecondary.setOnClickListener { - findNavController().navigate(R.id.downloadFragment) - } - clearButton.apply { - visibility = if (query.isNullOrBlank()) View.GONE else View.VISIBLE - setOnClickListener { - searchView.text?.clear() - searchView.showKeyboard() + setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_clear -> { + binding.searchBar.text?.clear() + binding.searchBar.showKeyboard() + } + R.id.action_download -> findNavController().navigate(R.id.downloadFragment) } + true } } @@ -186,30 +183,29 @@ class SearchResultsFragment : BaseFragment(), } } } else { - binding.recycler - .withModels { - setFilterDuplicates(true) + binding.recycler.withModels { + setFilterDuplicates(true) - filteredAppList.forEach { app -> - add( - AppListViewModel_() - .id(app.id) - .app(app) - .click(View.OnClickListener { - searchView.hideKeyboard() - openDetailsFragment(app.packageName, app) - }) - ) - } - - if (searchBundle.subBundles.isNotEmpty()) { - add( - AppProgressViewModel_() - .id("progress") - ) - } + filteredAppList.forEach { app -> + add( + AppListViewModel_() + .id(app.id) + .app(app) + .click(View.OnClickListener { + binding.searchBar.hideKeyboard() + openDetailsFragment(app.packageName, app) + }) + ) } + if (searchBundle.subBundles.isNotEmpty()) { + add( + AppProgressViewModel_() + .id("progress") + ) + } + } + binding.recycler.adapter?.let { if (it.itemCount < 10) { viewModel.next(searchBundle.subBundles) @@ -219,28 +215,22 @@ class SearchResultsFragment : BaseFragment(), } private fun attachSearch() { - searchView.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { - - } + binding.searchBar.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - if (s.isNotEmpty()) { - binding.layoutViewToolbar.clearButton.visibility = View.VISIBLE - } else { - binding.layoutViewToolbar.clearButton.visibility = View.GONE - } + binding.toolbar.menu.findItem(R.id.action_clear)?.isVisible = s.isNotBlank() } override fun afterTextChanged(s: Editable) {} }) - searchView.setOnEditorActionListener { _: TextView?, actionId: Int, _: KeyEvent? -> + binding.searchBar.setOnEditorActionListener { _: TextView?, actionId: Int, _: KeyEvent? -> if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == KeyEvent.ACTION_DOWN || actionId == KeyEvent.KEYCODE_ENTER ) { - query = searchView.text.toString() + query = binding.searchBar.text.toString() query?.let { requireArguments().putString("query", it) queryViewModel(it) @@ -252,8 +242,8 @@ class SearchResultsFragment : BaseFragment(), } private fun updateQuery(query: String) { - searchView.text = Editable.Factory.getInstance().newEditable(query) - searchView.setSelection(query.length) + binding.searchBar.text = Editable.Factory.getInstance().newEditable(query) + binding.searchBar.setSelection(query.length) queryViewModel(query) } @@ -277,7 +267,6 @@ class SearchResultsFragment : BaseFragment(), .toList() } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { if (key == PREFERENCE_FILTER) query?.let { queryViewModel(it) } } diff --git a/app/src/main/java/com/aurora/store/view/ui/search/SearchSuggestionFragment.kt b/app/src/main/java/com/aurora/store/view/ui/search/SearchSuggestionFragment.kt index 1cc4fb5b1..ebebe7013 100644 --- a/app/src/main/java/com/aurora/store/view/ui/search/SearchSuggestionFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/search/SearchSuggestionFragment.kt @@ -37,7 +37,6 @@ import com.aurora.store.databinding.FragmentSearchSuggestionBinding import com.aurora.store.view.epoxy.views.SearchSuggestionViewModel_ import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.viewmodel.search.SearchSuggestionViewModel -import com.google.android.material.textfield.TextInputEditText import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -47,26 +46,21 @@ class SearchSuggestionFragment : BaseFragment() private val viewModel: SearchSuggestionViewModel by viewModels() - private lateinit var searchView: TextInputEditText - - private var query: String = String() - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // Toolbar - binding.layoutToolbarSearch.apply { - searchView = inputSearch - imgActionPrimary.setOnClickListener { - searchView.hideKeyboard() + binding.toolbar.apply { + setNavigationOnClickListener { + binding.searchBar.hideKeyboard() findNavController().navigateUp() } - imgActionSecondary.setOnClickListener { - findNavController().navigate(R.id.downloadFragment) - } - clearButton.apply { - visibility = if (query.isBlank()) View.GONE else View.VISIBLE - setOnClickListener { searchView.text?.clear() } + setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_clear -> binding.searchBar.text?.clear() + R.id.action_download -> findNavController().navigate(R.id.downloadFragment) + } + true } } @@ -81,9 +75,7 @@ class SearchSuggestionFragment : BaseFragment() override fun onResume() { super.onResume() - if (::searchView.isInitialized) { - searchView.showKeyboard() - } + binding.searchBar.showKeyboard() } private fun updateController(searchSuggestions: List) { @@ -98,7 +90,7 @@ class SearchSuggestionFragment : BaseFragment() updateQuery(it.title) } .click { _ -> - searchView.hideKeyboard() + binding.searchBar.hideKeyboard() search(it.title) } ) @@ -107,32 +99,30 @@ class SearchSuggestionFragment : BaseFragment() } private fun setupSearch() { - searchView.addTextChangedListener(object : TextWatcher { + binding.searchBar.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { if (s.isNotEmpty()) { - query = s.toString() + val query = s.toString() if (query.isNotEmpty()) { viewModel.observeStreamBundles(query) } - binding.layoutToolbarSearch.clearButton.visibility = View.VISIBLE - } else { - binding.layoutToolbarSearch.clearButton.visibility = View.GONE } + binding.toolbar.menu.findItem(R.id.action_clear)?.isVisible = s.isNotBlank() } override fun afterTextChanged(s: Editable) {} }) - searchView.setOnEditorActionListener { _: TextView?, actionId: Int, _: KeyEvent? -> + binding.searchBar.setOnEditorActionListener { _: TextView?, actionId: Int, _: KeyEvent? -> if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == KeyEvent.ACTION_DOWN || actionId == KeyEvent.KEYCODE_ENTER ) { - query = searchView.text.toString() + val query = binding.searchBar.text.toString() if (query.isNotEmpty()) { - searchView.hideKeyboard() + binding.searchBar.hideKeyboard() search(query) return@setOnEditorActionListener true } @@ -142,8 +132,8 @@ class SearchSuggestionFragment : BaseFragment() } private fun updateQuery(query: String) { - searchView.text = Editable.Factory.getInstance().newEditable(query) - searchView.setSelection(query.length) + binding.searchBar.text = Editable.Factory.getInstance().newEditable(query) + binding.searchBar.setSelection(query.length) } private fun search(query: String) { diff --git a/app/src/main/res/drawable/ic_menu.xml b/app/src/main/res/drawable/ic_menu.xml deleted file mode 100644 index 966e8e94b..000000000 --- a/app/src/main/res/drawable/ic_menu.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/layout/fragment_generic_with_search.xml b/app/src/main/res/layout/fragment_generic_with_search.xml index fdba04491..9289fbf15 100644 --- a/app/src/main/res/layout/fragment_generic_with_search.xml +++ b/app/src/main/res/layout/fragment_generic_with_search.xml @@ -24,18 +24,35 @@ android:layout_height="match_parent" android:orientation="vertical"> - + + + + - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_search_result.xml b/app/src/main/res/layout/fragment_search_result.xml index 80cf9a3f1..6f96a2055 100644 --- a/app/src/main/res/layout/fragment_search_result.xml +++ b/app/src/main/res/layout/fragment_search_result.xml @@ -30,15 +30,33 @@ android:layout_height="match_parent" android:orientation="vertical"> - + + + + + android:layout_below="@id/toolbar" /> - + + + + + android:layout_below="@id/toolbar" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/view_toolbar_search.xml b/app/src/main/res/layout/view_toolbar_search.xml deleted file mode 100644 index 86d75ec3e..000000000 --- a/app/src/main/res/layout/view_toolbar_search.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/menu/menu_search.xml b/app/src/main/res/menu/menu_search.xml new file mode 100644 index 000000000..fd45127de --- /dev/null +++ b/app/src/main/res/menu/menu_search.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index b8ecd72e1..c70d8103a 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -5,6 +5,5 @@ @style/Chip.Filter @style/AppTheme.PreferenceThemeOverlay @color/colorTransparent - @style/AppTheme.PopupMenu diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index e4795a322..cb5ebd377 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -24,7 +24,6 @@ @style/AppTheme.PreferenceThemeOverlay @style/AppTheme.BottomSheetStyle @color/colorTransparent - @style/AppTheme.PopupMenu - - -