Merge branch 'dev' into 'master'

Merge Dev

See merge request AuroraOSS/AuroraStore!358
This commit is contained in:
Rahul Patel
2024-07-29 22:05:06 +00:00
17 changed files with 294 additions and 49 deletions

View File

@@ -19,11 +19,14 @@
package com.aurora.extensions package com.aurora.extensions
import android.graphics.Color
import android.text.format.DateFormat import android.text.format.DateFormat
import androidx.annotation.ColorInt
import java.io.PrintWriter import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
import java.util.Calendar import java.util.Calendar
import java.util.Locale import java.util.Locale
import javax.annotation.Nullable
fun Long.toDate(): String { fun Long.toDate(): String {
val calendar = Calendar.getInstance(Locale.getDefault()) val calendar = Calendar.getInstance(Locale.getDefault())
@@ -42,4 +45,51 @@ fun Throwable.stackTraceToString(): String {
fun isValidPackageName(packageName: String): Boolean { fun isValidPackageName(packageName: String): Boolean {
val packageRegex = "^[a-zA-Z][a-zA-Z0-9_]*(\\.[a-zA-Z][a-zA-Z0-9_]*)*$".toRegex() val packageRegex = "^[a-zA-Z][a-zA-Z0-9_]*(\\.[a-zA-Z][a-zA-Z0-9_]*)*$".toRegex()
return packageName.matches(packageRegex) return packageName.matches(packageRegex)
}
/**
* Computes a darker color from the given color.
* @param color The color to darken.
* @param factor The factor to darken the color by.
* - higher factor values will result in a lighter color.
* @return The darker color.
*/
fun darkenColor(@ColorInt color: Int, factor: Float = 0.25f): Int {
val a = Color.alpha(color)
val r = (Color.red(color) * factor).coerceIn(0f, 255f).toInt()
val g = (Color.green(color) * factor).coerceIn(0f, 255f).toInt()
val b = (Color.blue(color) * factor).coerceIn(0f, 255f).toInt()
return Color.argb(a, r, g, b)
}
/**
* Computes a lighter color from the given color.
* @param color The color to lighten.
* @param factor The factor to lighten the color by.
* - higher factor values will result in a lighter color.
* @param alpha The alpha value to use for the lighter color.
* @return The lighter color.
*/
fun lightenColor(@ColorInt color: Int, factor: Float = 0.5f, @Nullable alpha: Int? = null): Int {
val a = alpha ?: Color.alpha(color)
val r = (Color.red(color) + (255 - Color.red(color)) * factor).toInt()
val g = (Color.green(color) + (255 - Color.green(color)) * factor).toInt()
val b = (Color.blue(color) + (255 - Color.blue(color)) * factor).toInt()
return Color.argb(a, r, g, b)
}
/**
* Computes a contrasting color from the given color.
* @param color The color to contrast.
* @return The contrasting color.
*/
fun contrastingColor(@ColorInt color: Int): Int {
val red = Color.red(color)
val green = Color.green(color)
val blue = Color.blue(color)
val yiq = ((red * 299) + (green * 587) + (blue * 114)) / 1000
return if (yiq >= 128) Color.BLACK else Color.WHITE
} }

View File

@@ -22,6 +22,8 @@ package com.aurora.store.view.custom.layouts
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.isVisible
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.databinding.ViewDevInfoBinding import com.aurora.store.databinding.ViewDevInfoBinding
@@ -29,6 +31,16 @@ class DevInfoLayout : RelativeLayout {
private lateinit var binding: ViewDevInfoBinding private lateinit var binding: ViewDevInfoBinding
val icon: AppCompatImageView get() = binding.img
var title: String
get() = binding.txtTitle.text.toString()
set(value) = setTxtTitle(value)
var subTitle: String?
get() = binding.txtSubtitle.text.toString()
set(value) = setTxtSubtitle(value)
constructor(context: Context) : super(context) { constructor(context: Context) : super(context) {
init(context, null) init(context, null)
} }
@@ -64,8 +76,15 @@ class DevInfoLayout : RelativeLayout {
typedArray.recycle() typedArray.recycle()
} }
fun setTxtTitle(text: String?) {
binding.txtTitle.text = text
binding.txtTitle.isVisible = text != null
invalidate()
}
fun setTxtSubtitle(text: String?) { fun setTxtSubtitle(text: String?) {
binding.txtSubtitle.text = text binding.txtSubtitle.text = text
binding.txtSubtitle.isVisible = text != null
invalidate() invalidate()
} }
} }

View File

@@ -20,9 +20,13 @@
package com.aurora.store.view.custom.layouts.button package com.aurora.store.view.custom.layouts.button
import android.content.Context import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.RelativeLayout import android.widget.RelativeLayout
import com.aurora.extensions.accentColor
import com.aurora.extensions.darkenColor
import com.aurora.extensions.getString import com.aurora.extensions.getString
import com.aurora.extensions.lightenColor
import com.aurora.extensions.runOnUiThread import com.aurora.extensions.runOnUiThread
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.model.DownloadStatus
@@ -51,6 +55,15 @@ class UpdateButton : RelativeLayout {
private fun init(context: Context) { private fun init(context: Context) {
val view = inflate(context, R.layout.view_update_button, this) val view = inflate(context, R.layout.view_update_button, this)
binding = ViewUpdateButtonBinding.bind(view) binding = ViewUpdateButtonBinding.bind(view)
// Apply primaryColor tint to all buttons with alpha
val alphaAccent = lightenColor(context.accentColor(), alpha = 200)
binding.btnPositive.backgroundTintList = ColorStateList.valueOf(alphaAccent)
binding.btnNegative.backgroundTintList = ColorStateList.valueOf(alphaAccent)
val textColor = darkenColor(context.accentColor())
binding.btnPositive.setTextColor(textColor)
binding.btnNegative.setTextColor(textColor)
} }
fun setText(text: String) { fun setText(text: String) {
@@ -65,9 +78,8 @@ class UpdateButton : RelativeLayout {
fun updateState(downloadStatus: DownloadStatus) { fun updateState(downloadStatus: DownloadStatus) {
val displayChild = when (downloadStatus) { val displayChild = when (downloadStatus) {
DownloadStatus.QUEUED, DownloadStatus.QUEUED -> 1
DownloadStatus.DOWNLOADING -> 2 DownloadStatus.DOWNLOADING -> 2
else -> 0 else -> 0
} }

View File

@@ -25,6 +25,8 @@ import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView import com.airbnb.epoxy.ModelView
import com.airbnb.epoxy.OnViewRecycled import com.airbnb.epoxy.OnViewRecycled
import com.aurora.extensions.accentColor
import com.aurora.extensions.contrastingColor
import com.aurora.store.databinding.ViewHeaderUpdateBinding import com.aurora.store.databinding.ViewHeaderUpdateBinding
@@ -46,6 +48,7 @@ class UpdateHeaderView @JvmOverloads constructor(
@ModelProp @ModelProp
fun action(action: String) { fun action(action: String) {
binding.btnAction.text = action binding.btnAction.text = action
binding.btnAction.setTextColor(contrastingColor(context.accentColor()))
} }
@CallbackProp @CallbackProp

View File

@@ -26,6 +26,8 @@ import androidx.core.view.isVisible
import com.airbnb.epoxy.CallbackProp import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView import com.airbnb.epoxy.ModelView
import com.aurora.extensions.accentColor
import com.aurora.extensions.contrastingColor
import com.aurora.store.databinding.ViewNoAppBinding import com.aurora.store.databinding.ViewNoAppBinding
import com.aurora.store.view.epoxy.views.BaseModel import com.aurora.store.view.epoxy.views.BaseModel
import com.aurora.store.view.epoxy.views.BaseView import com.aurora.store.view.epoxy.views.BaseView
@@ -62,6 +64,7 @@ class NoAppView @JvmOverloads constructor(
@ModelProp @ModelProp
fun actionMessage(message: String = String()) { fun actionMessage(message: String = String()) {
binding.button.text = message binding.button.text = message
binding.button.setTextColor(contrastingColor(context.accentColor()))
} }
@JvmOverloads @JvmOverloads

View File

@@ -6,6 +6,7 @@ import android.os.Bundle
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -33,6 +34,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -50,7 +52,11 @@ import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest import coil.request.ImageRequest
import com.aurora.Constants import com.aurora.Constants
import com.aurora.Constants.URL_TOS import com.aurora.Constants.URL_TOS
import com.aurora.extensions.accentColor
import com.aurora.extensions.browse import com.aurora.extensions.browse
import com.aurora.extensions.darkenColor
import com.aurora.extensions.getStyledAttributeColor
import com.aurora.extensions.lightenColor
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.view.theme.AuroraTheme import com.aurora.store.view.theme.AuroraTheme
@@ -64,6 +70,11 @@ class MoreDialogFragment : DialogFragment() {
@Inject @Inject
lateinit var authProvider: AuthProvider lateinit var authProvider: AuthProvider
private var primaryColor: Color = Color.White
private var onPrimaryColor: Color = Color.Black
private var secondaryColor: Color = Color.White
private var onSecondaryColor: Color = Color.Black
private data class Option( private data class Option(
@StringRes val title: Int, @StringRes val title: Int,
@DrawableRes val icon: Int, @DrawableRes val icon: Int,
@@ -81,30 +92,37 @@ class MoreDialogFragment : DialogFragment() {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent { setContent {
AuroraTheme { AuroraTheme {
val backgroundColor = if (isSystemInDarkTheme()) { if (isSystemInDarkTheme()) {
MaterialTheme.colorScheme.onSecondary primaryColor = Color(darkenColor(requireContext().accentColor(), 0.25f))
onPrimaryColor = Color(lightenColor(primaryColor.toArgb()))
secondaryColor = Color(darkenColor(requireContext().accentColor(), 0.15f))
onSecondaryColor = Color(lightenColor(primaryColor.toArgb()))
} else { } else {
MaterialTheme.colorScheme.surfaceVariant primaryColor = Color(lightenColor(requireContext().accentColor(), 0.85f))
onPrimaryColor = Color(darkenColor(primaryColor.toArgb()))
secondaryColor = Color(lightenColor(requireContext().accentColor(), 0.95f))
onSecondaryColor = Color(darkenColor(primaryColor.toArgb()))
} }
val onBackgroundColor = if (isSystemInDarkTheme()) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.onSecondary
}
val tintColor = if (isSystemInDarkTheme()) Color.White else Color.Black
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(color = backgroundColor) .background(color = primaryColor)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(10.dp), .padding(10.dp),
verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.CenterVertically) verticalArrangement = Arrangement.spacedBy(
5.dp,
Alignment.CenterVertically
)
) { ) {
AppBar(tintColor = tintColor) AppBar(
AccountHeader(backgroundColor = onBackgroundColor) backgroundColor = primaryColor,
onBackgroundColor = onPrimaryColor
)
AccountHeader(
backgroundColor = secondaryColor,
onBackgroundColor = onSecondaryColor
)
Column( Column(
modifier = Modifier modifier = Modifier
.clip( .clip(
@@ -115,17 +133,22 @@ class MoreDialogFragment : DialogFragment() {
bottomEnd = 20.dp bottomEnd = 20.dp
) )
) )
.background(color = onBackgroundColor) .background(color = secondaryColor)
) { ) {
getOptions().fastForEach { option -> OptionItem(option = option) } getOptions().fastForEach { option ->
OptionItem(
option = option,
tintColor = onSecondaryColor
)
}
} }
getExtraOptions().fastForEach { option -> getExtraOptions().fastForEach { option ->
OptionItem( OptionItem(
option = option, option = option,
tintColor = tintColor tintColor = onPrimaryColor
) )
} }
Footer(tintColor) Footer(onPrimaryColor)
} }
} }
} }
@@ -133,20 +156,20 @@ class MoreDialogFragment : DialogFragment() {
} }
@Composable @Composable
fun AppBar(tintColor: Color) { fun AppBar(backgroundColor: Color = Color.Transparent, onBackgroundColor: Color) {
Box(contentAlignment = Alignment.CenterEnd) { Box(contentAlignment = Alignment.CenterEnd) {
Text( Text(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.app_name), text = stringResource(id = R.string.app_name),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = tintColor, color = onBackgroundColor,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
IconButton(onClick = { findNavController().navigateUp() }) { IconButton(onClick = { findNavController().navigateUp() }) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_cancel), painter = painterResource(id = R.drawable.ic_cancel),
contentDescription = stringResource(id = R.string.action_cancel), contentDescription = stringResource(id = R.string.action_cancel),
tint = tintColor tint = onBackgroundColor
) )
} }
} }
@@ -157,11 +180,12 @@ class MoreDialogFragment : DialogFragment() {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.CenterHorizontally) horizontalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterHorizontally)
) { ) {
TextButton(onClick = { requireContext().browse(Constants.URL_POLICY) }) { TextButton(onClick = { requireContext().browse(Constants.URL_POLICY) }) {
Text( Text(
text = stringResource(id = R.string.privacy_policy_title), text = stringResource(id = R.string.privacy_policy_title),
fontWeight = FontWeight.Light,
color = tintColor color = tintColor
) )
} }
@@ -169,6 +193,7 @@ class MoreDialogFragment : DialogFragment() {
TextButton(onClick = { requireContext().browse(URL_TOS) }) { TextButton(onClick = { requireContext().browse(URL_TOS) }) {
Text( Text(
text = stringResource(id = R.string.menu_terms), text = stringResource(id = R.string.menu_terms),
fontWeight = FontWeight.Light,
color = tintColor color = tintColor
) )
} }
@@ -176,13 +201,16 @@ class MoreDialogFragment : DialogFragment() {
} }
@Composable @Composable
private fun AccountHeader(backgroundColor: Color) { private fun AccountHeader(backgroundColor: Color, onBackgroundColor: Color = Color.White) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip( .clip(
RoundedCornerShape( RoundedCornerShape(
topStart = 20.dp, topEnd = 20.dp, bottomStart = 5.dp, bottomEnd = 5.dp topStart = 20.dp,
topEnd = 20.dp,
bottomStart = 5.dp,
bottomEnd = 5.dp
) )
) )
.background(color = backgroundColor) .background(color = backgroundColor)
@@ -205,7 +233,7 @@ class MoreDialogFragment : DialogFragment() {
contentDescription = stringResource(id = R.string.title_account_manager), contentDescription = stringResource(id = R.string.title_account_manager),
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = Modifier modifier = Modifier
.requiredSize(48.dp) .requiredSize(36.dp)
.clip(CircleShape) .clip(CircleShape)
) )
Column( Column(
@@ -213,24 +241,32 @@ class MoreDialogFragment : DialogFragment() {
horizontalAlignment = Alignment.Start horizontalAlignment = Alignment.Start
) { ) {
Text( Text(
text = if (authProvider.isAnonymous) "anonymous" else authProvider.authData!!.userProfile!!.name, text = if (authProvider.isAnonymous) "Anonymous" else authProvider.authData!!.userProfile!!.name,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.Normal,
fontSize = 16.sp fontSize = 16.sp,
color = onBackgroundColor
) )
Text( Text(
text = if (authProvider.isAnonymous) "anonymous@gmail.com" else authProvider.authData!!.userProfile!!.email, text = if (authProvider.isAnonymous) "anonymous@gmail.com" else authProvider.authData!!.userProfile!!.email,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Light,
fontSize = 14.sp fontSize = 14.sp,
color = onBackgroundColor
) )
} }
} }
OutlinedButton( OutlinedButton(
onClick = { findNavController().navigate(R.id.accountFragment) }, onClick = { findNavController().navigate(R.id.accountFragment) },
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(12.dp),
border = BorderStroke(
1.dp,
Color(requireContext().getStyledAttributeColor(androidx.appcompat.R.attr.colorControlHighlight))
),
modifier = Modifier.fillMaxWidth()
) { ) {
Text( Text(
text = stringResource(id = R.string.manage_account), text = stringResource(id = R.string.manage_account),
color = Color.Black color = onBackgroundColor,
fontWeight = FontWeight.Medium
) )
} }
} }

View File

@@ -39,7 +39,9 @@ import coil.load
import coil.transform.RoundedCornersTransformation import coil.transform.RoundedCornersTransformation
import com.aurora.Constants import com.aurora.Constants
import com.aurora.Constants.EXODUS_SUBMIT_PAGE import com.aurora.Constants.EXODUS_SUBMIT_PAGE
import com.aurora.extensions.accentColor
import com.aurora.extensions.browse import com.aurora.extensions.browse
import com.aurora.extensions.contrastingColor
import com.aurora.extensions.getString import com.aurora.extensions.getString
import com.aurora.extensions.hide import com.aurora.extensions.hide
import com.aurora.extensions.runOnUiThread import com.aurora.extensions.runOnUiThread
@@ -51,6 +53,7 @@ import com.aurora.gplayapi.data.models.File
import com.aurora.gplayapi.data.models.Review import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.data.models.StreamBundle import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.data.models.datasafety.EntryType
import com.aurora.store.AppStreamStash import com.aurora.store.AppStreamStash
import com.aurora.store.AuroraApp import com.aurora.store.AuroraApp
import com.aurora.store.PermissionType import com.aurora.store.PermissionType
@@ -89,6 +92,7 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
@AndroidEntryPoint @AndroidEntryPoint
class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() { class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
@@ -264,6 +268,11 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
} }
// Data Safety Report
viewLifecycleOwner.lifecycleScope.launch {
viewModel.dataSafetyReport.collect { updateDataSafetyViews(it) }
}
// Exodus Privacy Report // Exodus Privacy Report
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewModel.exodusReport.collect { report -> viewModel.exodusReport.collect { report ->
@@ -310,7 +319,10 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewModel.testingProgramStatus.collect { viewModel.testingProgramStatus.collect {
if (it != null) { if (it != null) {
binding.layoutDetailsBeta.btnBetaAction.isEnabled = true binding.layoutDetailsBeta.btnBetaAction.apply {
isEnabled = true
setTextColor(contrastingColor(requireContext().accentColor()))
}
if (it.subscribed) { if (it.subscribed) {
updateBetaActions(true) updateBetaActions(true)
} }
@@ -342,8 +354,11 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
// Misc Bindings // Misc Bindings
binding.layoutDetailsPrivacy.btnRequestAnalysis.setOnClickListener { binding.layoutDetailsPrivacy.btnRequestAnalysis.apply {
it.context.browse("${EXODUS_SUBMIT_PAGE}${app.packageName}") setOnClickListener {
it.context.browse("${EXODUS_SUBMIT_PAGE}${app.packageName}")
}
setTextColor(contrastingColor(requireContext().accentColor()))
} }
binding.layoutDetailsInstall.progressDownload.clipToOutline = true binding.layoutDetailsInstall.progressDownload.clipToOutline = true
binding.layoutDetailsInstall.imgCancel.setOnClickListener { binding.layoutDetailsInstall.imgCancel.setOnClickListener {
@@ -687,6 +702,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
inflateAppDescription(app) inflateAppDescription(app)
inflateAppRatingAndReviews(app) inflateAppRatingAndReviews(app)
inflateAppDevInfo(app) inflateAppDevInfo(app)
inflateAppDataSafety(app)
inflateAppPrivacy(app) inflateAppPrivacy(app)
inflateAppPermission(app) inflateAppPermission(app)
@@ -814,6 +830,10 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
} }
private fun inflateAppDataSafety(app: App) {
viewModel.fetchAppDataSafetyReport(app.packageName)
}
private fun inflateAppPrivacy(app: App) { private fun inflateAppPrivacy(app: App) {
viewModel.fetchAppReport(app.packageName) viewModel.fetchAppReport(app.packageName)
} }
@@ -953,6 +973,32 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
} }
private fun updateDataSafetyViews(report: DataSafetyReport) {
report.entries.groupBy { it.type }.forEach { (type, entries) ->
when (type) {
EntryType.DATA_COLLECTED -> {
binding.layoutDetailsDataSafety.dataCollect.title = HtmlCompat.fromHtml(
entries.first().description,
HtmlCompat.FROM_HTML_MODE_COMPACT
).toString()
binding.layoutDetailsDataSafety.dataCollect.subTitle =
entries.first().subEntries.joinToString(", ") { it.name }.ifBlank { null }
}
EntryType.DATA_SHARED -> {
binding.layoutDetailsDataSafety.dataShare.title = HtmlCompat.fromHtml(
entries.first().description,
HtmlCompat.FROM_HTML_MODE_COMPACT
).toString()
binding.layoutDetailsDataSafety.dataShare.subTitle =
entries.first().subEntries.joinToString(", ") { it.name }.ifBlank { null }
}
else -> {}
}
}
}
/* App Review Helpers */ /* App Review Helpers */
private fun addAvgReviews(number: Int, max: Long, rating: Long): RelativeLayout { private fun addAvgReviews(number: Int, max: Long, rating: Long): RelativeLayout {

View File

@@ -9,6 +9,7 @@ import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.data.models.details.TestingProgramStatus import com.aurora.gplayapi.data.models.details.TestingProgramStatus
import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.gplayapi.helpers.ReviewsHelper import com.aurora.gplayapi.helpers.ReviewsHelper
import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper
import com.aurora.store.data.model.ExodusReport import com.aurora.store.data.model.ExodusReport
import com.aurora.store.data.model.Report import com.aurora.store.data.model.Report
import com.aurora.store.data.network.HttpClient import com.aurora.store.data.network.HttpClient
@@ -28,6 +29,7 @@ import kotlinx.coroutines.launch
import org.json.JSONObject import org.json.JSONObject
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
import javax.inject.Inject import javax.inject.Inject
import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
@HiltViewModel @HiltViewModel
class AppDetailsViewModel @Inject constructor( class AppDetailsViewModel @Inject constructor(
@@ -58,6 +60,10 @@ class AppDetailsViewModel @Inject constructor(
private val _userReview = MutableSharedFlow<Review>() private val _userReview = MutableSharedFlow<Review>()
val userReview = _userReview.asSharedFlow() val userReview = _userReview.asSharedFlow()
private val dataSafetyReportStash = mutableMapOf<String, DataSafetyReport>()
private val _dataSafetyReport = MutableSharedFlow<DataSafetyReport>()
val dataSafetyReport = _dataSafetyReport.asSharedFlow()
private val exodusReportStash = mutableMapOf<String, Report?>() private val exodusReportStash = mutableMapOf<String, Report?>()
private val _exodusReport = MutableSharedFlow<Report?>() private val _exodusReport = MutableSharedFlow<Report?>()
val exodusReport = _exodusReport.asSharedFlow() val exodusReport = _exodusReport.asSharedFlow()
@@ -103,6 +109,21 @@ class AppDetailsViewModel @Inject constructor(
} }
} }
fun fetchAppDataSafetyReport(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
val report = dataSafetyReportStash.getOrPut(packageName) {
WebDataSafetyHelper()
.using(httpClient)
.fetch(packageName)
}
_dataSafetyReport.emit(report)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch data safety report", exception)
}
}
}
fun fetchAppReport(packageName: String) { fun fetchAppReport(packageName: String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M260,800Q169,800 104.5,737Q40,674 40,583Q40,505 87,444Q134,383 210,366Q235,274 310,217Q385,160 480,160Q597,160 678.5,241.5Q760,323 760,440L760,440L760,440Q829,448 874.5,499.5Q920,551 920,620Q920,695 867.5,747.5Q815,800 740,800L520,800Q487,800 463.5,776.5Q440,753 440,720L440,514L376,576L320,520L480,360L640,520L584,576L520,514L520,720Q520,720 520,720Q520,720 520,720L740,720Q782,720 811,691Q840,662 840,620Q840,578 811,549Q782,520 740,520L680,520L680,440Q680,357 621.5,298.5Q563,240 480,240Q397,240 338.5,298.5Q280,357 280,440L260,440Q202,440 161,481Q120,522 120,580Q120,638 161,679Q202,720 260,720L360,720L360,800L260,800ZM480,520L480,520L480,520Q480,520 480,520Q480,520 480,520L480,520L480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520L480,520L480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520L480,520Z"/>
</vector>

View File

@@ -103,6 +103,10 @@
android:id="@+id/layout_details_permissions" android:id="@+id/layout_details_permissions"
layout="@layout/layout_details_permissions" /> layout="@layout/layout_details_permissions" />
<include
android:id="@+id/layout_details_data_safety"
layout="@layout/layout_details_data_safety" />
<include <include
android:id="@+id/layout_details_privacy" android:id="@+id/layout_details_privacy"
layout="@layout/layout_details_privacy" /> layout="@layout/layout_details_privacy" />

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
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">
<com.aurora.store.view.custom.layouts.ActionHeaderLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:headerSubtitle="@string/details_data_safety_subtitle"
app:headerTitle="@string/details_data_safety_title" />
<com.aurora.store.view.custom.layouts.DevInfoLayout
android:id="@+id/data_collect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:imgIcon="@drawable/ic_cloud_upload"
tools:txtSubtitle="Personal info, Financial info and 3 others"
tools:txtTitle="This app may collect these data types" />
<com.aurora.store.view.custom.layouts.DevInfoLayout
android:id="@+id/data_share"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:imgIcon="@drawable/ic_share"
tools:txtSubtitle="Learn more about how developers declare sharing"
tools:txtTitle="No data shared with third parties" />
</LinearLayout>

View File

@@ -106,6 +106,7 @@
android:layout_alignTop="@id/txt_line1" android:layout_alignTop="@id/txt_line1"
android:layout_toStartOf="@id/btn_action" android:layout_toStartOf="@id/btn_action"
android:contentDescription="@string/details_changelog" android:contentDescription="@string/details_changelog"
app:iconTint="?colorControlNormal"
app:icon="@drawable/ic_arrow_down" /> app:icon="@drawable/ic_arrow_down" />
<com.aurora.store.view.custom.layouts.button.UpdateButton <com.aurora.store.view.custom.layouts.button.UpdateButton

View File

@@ -45,5 +45,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:text="@string/action_update_all" /> android:text="@string/action_update_all"
android:textAppearance="@style/TextAppearance.Aurora.Line1" />
</RelativeLayout> </RelativeLayout>

View File

@@ -41,12 +41,13 @@
android:textAppearance="@style/TextAppearance.Aurora.Line1" android:textAppearance="@style/TextAppearance.Aurora.Line1"
tools:text="No updates available" /> tools:text="No updates available" />
<Button <com.google.android.material.button.MaterialButton
android:id="@+id/button" android:id="@+id/button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/txt" android:layout_below="@id/txt"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:visibility="gone" android:visibility="gone"
tools:text="@string/check_updates" /> tools:text="@string/check_updates"
tools:visibility="visible" />
</RelativeLayout> </RelativeLayout>

View File

@@ -25,14 +25,16 @@
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btn" android:id="@+id/btn"
style="@style/Widget.Material3.Button.IconButton.Outlined" style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:textColor="?colorControlNormal"
app:iconPadding="@dimen/padding_normal" app:iconPadding="@dimen/padding_normal"
app:iconTint="?colorAccent" app:iconTint="?colorControlNormal"
app:strokeColor="?colorAccent" app:strokeColor="?colorControlHighlight"
app:strokeWidth="1dp"
tools:icon="@drawable/ic_anonymous" tools:icon="@drawable/ic_anonymous"
tools:text="Button Name" /> tools:text="Button Name" />

View File

@@ -36,6 +36,7 @@
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btnPositive" android:id="@+id/btnPositive"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/action_update" android:text="@string/action_update"
@@ -48,6 +49,7 @@
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btnQueued" android:id="@+id/btnQueued"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:enabled="false" android:enabled="false"
@@ -61,15 +63,11 @@
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btnNegative" android:id="@+id/btnNegative"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/action_cancel" android:text="@string/action_cancel"
android:textAppearance="@style/TextAppearance.Aurora.Line1" /> android:textAppearance="@style/TextAppearance.Aurora.Line1" />
</RelativeLayout> </RelativeLayout>
</ViewFlipper> </ViewFlipper>
</RelativeLayout> </RelativeLayout>

View File

@@ -127,6 +127,8 @@
<string name="details_no_apps_on_sale">"No apps found on sale"</string> <string name="details_no_apps_on_sale">"No apps found on sale"</string>
<string name="details_paid">"Paid"</string> <string name="details_paid">"Paid"</string>
<string name="details_permission">"Permission"</string> <string name="details_permission">"Permission"</string>
<string name="details_data_safety_title">Data safety</string>
<string name="details_data_safety_subtitle">Data privacy and security practices declared by developer</string>
<string name="details_privacy">"Privacy"</string> <string name="details_privacy">"Privacy"</string>
<string name="details_ratings">"Rating and reviews"</string> <string name="details_ratings">"Rating and reviews"</string>
<string name="details_ratings_title_hint">"Review title"</string> <string name="details_ratings_title_hint">"Review title"</string>