AppDetails: Fix user review + minor cleanup

This commit is contained in:
Rahul Patel
2024-12-14 23:29:02 +05:30
parent d2145962d8
commit c7fefb12c7
8 changed files with 193 additions and 167 deletions

View File

@@ -87,7 +87,8 @@
<activity
android:name=".MainActivity"
android:exported="true">
android:exported="true"
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@@ -188,18 +188,18 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
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<FragmentDetailsBinding>() {
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<FragmentDetailsBinding>() {
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<FragmentDetailsBinding>() {
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<FragmentDetailsBinding>() {
}
}
// 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<FragmentDetailsBinding>() {
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<FragmentDetailsBinding>() {
}
}
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<FragmentDetailsBinding>() {
}
}
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<FragmentDetailsBinding>() {
}
}
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<FragmentDetailsBinding>() {
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<FragmentDetailsBinding>() {
}
}
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<FragmentDetailsBinding>() {
}
}
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<FragmentDetailsBinding>() {
}
}
private fun inflateAppPermission(app: App) {
private fun updateAppPermission(app: App) {
binding.layoutDetailsPermissions.apply {
headerPermission.addClickListener {
if (app.permissions.isNotEmpty()) {

View File

@@ -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) {

View File

@@ -29,13 +29,13 @@
<RelativeLayout
android:id="@+id/img_icon_layout"
android:layout_width="@dimen/icon_size_medium"
android:layout_height="@dimen/icon_size_medium">
android:layout_width="@dimen/icon_size_large"
android:layout_height="@dimen/icon_size_large">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/img_icon"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:layout_width="@dimen/icon_size_large"
android:layout_height="@dimen/icon_size_large"
tools:src="@drawable/bg_placeholder" />
<com.google.android.material.progressindicator.CircularProgressIndicator
@@ -44,7 +44,7 @@
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:visibility="gone"
app:indicatorSize="@dimen/icon_size_medium"
app:indicatorSize="@dimen/icon_size_large"
app:trackThickness="3dp"
tools:progress="40" />
</RelativeLayout>
@@ -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" />
</LinearLayout>

View File

@@ -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">
<androidx.appcompat.widget.AppCompatImageView
@@ -61,11 +62,9 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_beta_Action"
android:layout_width="wrap_content"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:insetTop="@dimen/margin_xsmall"
android:insetBottom="@dimen/margin_xsmall"
android:minWidth="128dp"
android:text="@string/action_join"
app:cornerRadius="@dimen/radius_small" />
</LinearLayout>

View File

@@ -19,7 +19,7 @@
~
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/exodus_card"
android:layout_width="match_parent"
@@ -41,20 +41,14 @@
android:id="@+id/txt_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/header_privacy"
android:text="@string/exodus_progress"
android:textAppearance="@style/TextAppearance.Aurora.Line1" />
<com.google.android.material.button.MaterialButton
style="@style/Widget.Material3.Button.TonalButton"
android:id="@+id/btn_request_analysis"
style="@style/Widget.Material3.Button"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/txt_status"
android:layout_gravity="center_vertical"
android:insetTop="@dimen/padding_small"
android:insetBottom="@dimen/padding_xsmall"
android:text="@string/action_request_analysis"
app:cornerRadius="@dimen/margin_small" />
</RelativeLayout>
</LinearLayout>

View File

@@ -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">
<RatingBar
android:id="@+id/user_stars"
@@ -55,38 +56,39 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="@dimen/margin_normal"
android:divider="@drawable/divider"
android:orientation="vertical"
android:showDividers="middle">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_title"
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="42dp"
android:background="@drawable/bg_changelog"
android:gravity="center_vertical|center_horizontal"
android:hint="@string/details_ratings_title_hint"
android:imeOptions="flagNoExtractUi|actionDone"
android:inputType="text"
android:paddingStart="@dimen/padding_large"
android:paddingEnd="@dimen/padding_normal"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Aurora.Line2" />
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_review"
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:hint="@string/details_ratings_title_hint"
android:inputType="text"
android:textAppearance="@style/TextAppearance.Aurora.Line2" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="42dp"
android:background="@drawable/bg_changelog"
android:gravity="center_vertical|center_horizontal"
android:hint="@string/details_think_this_app"
android:imeOptions="flagNoExtractUi|actionDone"
android:inputType="text"
android:paddingStart="@dimen/padding_large"
android:paddingEnd="@dimen/padding_normal"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Aurora.Line2" />
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_review"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:hint="@string/details_think_this_app"
android:inputType="textMultiLine"
android:textAppearance="@style/TextAppearance.Aurora.Line2" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_post_review"
@@ -94,7 +96,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/action_post"
app:cornerRadius="@dimen/margin_small" />
app:cornerRadius="@dimen/radius_small" />
</LinearLayout>
</LinearLayout>

View File

@@ -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"