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"