compose: exodus: Add back button to request new anaylsis

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-12-03 13:29:02 +08:00
parent 9d405ce20a
commit 6767ed7ea7
4 changed files with 115 additions and 43 deletions

View File

@@ -393,7 +393,7 @@ private fun ScreenContentApp(
Privacy( Privacy(
report = exodusReport, report = exodusReport,
onNavigateToDetailsExodus = if (!exodusReport?.trackers.isNullOrEmpty()) { onNavigateToDetailsExodus = if (exodusReport?.id != -1) {
{ showExtraPane(ExtraScreen.Exodus) } { showExtraPane(ExtraScreen.Exodus) }
} else { } else {
null null

View File

@@ -9,31 +9,38 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.aurora.Constants.EXODUS_REPORT_URL import com.aurora.Constants.EXODUS_REPORT_URL
import com.aurora.Constants.EXODUS_SUBMIT_PAGE
import com.aurora.extensions.adaptiveNavigationIcon import com.aurora.extensions.adaptiveNavigationIcon
import com.aurora.extensions.browse import com.aurora.extensions.browse
import com.aurora.extensions.isWindowCompact import com.aurora.extensions.isWindowCompact
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.composable.Error
import com.aurora.store.compose.composable.Header import com.aurora.store.compose.composable.Header
import com.aurora.store.compose.composable.TopAppBar import com.aurora.store.compose.composable.TopAppBar
import com.aurora.store.compose.composable.details.ExodusListItem import com.aurora.store.compose.composable.details.ExodusListItem
import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.AppPreviewProvider
import com.aurora.store.compose.preview.PreviewTemplate import com.aurora.store.compose.preview.PreviewTemplate
import com.aurora.store.data.model.ExodusTracker import com.aurora.store.data.model.ExodusTracker
import com.aurora.store.data.model.Report
import com.aurora.store.viewmodel.details.AppDetailsViewModel import com.aurora.store.viewmodel.details.AppDetailsViewModel
import com.aurora.store.viewmodel.details.ExodusViewModel import com.aurora.store.viewmodel.details.ExodusViewModel
@@ -42,39 +49,52 @@ fun ExodusScreen(
packageName: String, packageName: String,
onNavigateUp: () -> Unit, onNavigateUp: () -> Unit,
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName), appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
exodusViewModel: ExodusViewModel = hiltViewModel( exodusViewModel: ExodusViewModel = hiltViewModel(key = "$packageName/exodus"),
key = "$packageName/exodus",
creationCallback = { factory: ExodusViewModel.Factory ->
factory.create(appDetailsViewModel.exodusReport.value!!)
}
),
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo() windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) { ) {
val context = LocalContext.current
val app by appDetailsViewModel.app.collectAsStateWithLifecycle() val app by appDetailsViewModel.app.collectAsStateWithLifecycle()
val exodusReport by appDetailsViewModel.exodusReport.collectAsStateWithLifecycle() val report by appDetailsViewModel.exodusReport.collectAsStateWithLifecycle()
val trackers by exodusViewModel.trackers.collectAsStateWithLifecycle() val trackers by exodusViewModel.trackers.collectAsStateWithLifecycle()
LaunchedEffect(key1 = Unit) {
report?.let { exodusViewModel.getExodusTrackersFromReport(it) }
}
val topAppBarTitle = when { val topAppBarTitle = when {
windowAdaptiveInfo.isWindowCompact -> app!!.displayName windowAdaptiveInfo.isWindowCompact -> app!!.displayName
else -> stringResource(R.string.details_privacy) else -> stringResource(R.string.details_privacy)
} }
ScreenContent( when (report) {
topAppBarTitle = topAppBarTitle, null -> {
id = exodusReport!!.id, ScreenContentError(
version = exodusReport!!.version, topAppBarTitle = topAppBarTitle,
trackers = trackers, onNavigateUp = onNavigateUp,
onNavigateUp = onNavigateUp, onRequestAnalysis = { context.browse("${EXODUS_SUBMIT_PAGE}${app!!.packageName}") }
) )
}
else -> {
ScreenContentReport(
topAppBarTitle = topAppBarTitle,
report = report,
trackers = trackers,
onNavigateUp = onNavigateUp,
onRequestAnalysis = { context.browse("${EXODUS_SUBMIT_PAGE}${app!!.packageName}") }
)
}
}
} }
@Composable @Composable
private fun ScreenContent( private fun ScreenContentReport(
topAppBarTitle: String? = null, topAppBarTitle: String? = null,
id: Int = -1, report: Report? = null,
version: String = String(),
trackers: List<ExodusTracker> = emptyList(), trackers: List<ExodusTracker> = emptyList(),
onNavigateUp: () -> Unit = {}, onNavigateUp: () -> Unit = {},
onRequestAnalysis: () -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo() windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -86,6 +106,14 @@ private fun ScreenContent(
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon, navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
onNavigateUp = onNavigateUp onNavigateUp = onNavigateUp
) )
},
floatingActionButton = {
FloatingActionButton(onClick = onRequestAnalysis) {
Icon(
painter = painterResource(R.drawable.ic_scan),
contentDescription = stringResource(R.string.action_request_analysis)
)
}
} }
) { paddingValues -> ) { paddingValues ->
LazyColumn( LazyColumn(
@@ -96,9 +124,17 @@ private fun ScreenContent(
) { ) {
stickyHeader { stickyHeader {
Header( Header(
title = stringResource(R.string.exodus_report_trackers, trackers.size, version), title = if (report?.trackers.isNullOrEmpty()) {
stringResource(R.string.exodus_no_tracker)
} else {
stringResource(
R.string.exodus_report_trackers,
report.trackers.size,
report.version
)
},
subtitle = stringResource(R.string.exodus_view_report), subtitle = stringResource(R.string.exodus_view_report),
onClick = { context.browse(EXODUS_REPORT_URL + id) } onClick = { context.browse(EXODUS_REPORT_URL + report!!.id) }
) )
} }
@@ -109,13 +145,47 @@ private fun ScreenContent(
} }
} }
@Preview /**
* Composable to display errors related to fetching app details
*/
@Composable @Composable
private fun ExodusScreenPreview(@PreviewParameter(AppPreviewProvider::class) app: App) { private fun ScreenContentError(
PreviewTemplate { topAppBarTitle: String? = null,
ScreenContent( onNavigateUp: () -> Unit = {},
topAppBarTitle = app.displayName, onRequestAnalysis: () -> Unit = {},
version = app.versionName windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
Scaffold(
topBar = {
TopAppBar(
title = topAppBarTitle,
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
onNavigateUp = onNavigateUp
)
},
) { paddingValues ->
Error(
modifier = Modifier.padding(paddingValues),
painter = painterResource(R.drawable.ic_disclaimer),
message = stringResource(R.string.failed_to_fetch_report),
actionMessage = stringResource(R.string.action_request_analysis),
onAction = onRequestAnalysis
) )
} }
} }
@Preview
@Composable
private fun ExodusScreenPreviewReport(@PreviewParameter(AppPreviewProvider::class) app: App) {
PreviewTemplate {
ScreenContentReport(topAppBarTitle = app.displayName)
}
}
@Preview
@Composable
private fun ExodusScreenPreviewError(@PreviewParameter(AppPreviewProvider::class) app: App) {
PreviewTemplate {
ScreenContentError(topAppBarTitle = app.displayName)
}
}

View File

@@ -9,37 +9,25 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aurora.store.data.model.ExodusTracker import com.aurora.store.data.model.ExodusTracker
import com.aurora.store.data.model.Report import com.aurora.store.data.model.Report
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.json.JSONObject import org.json.JSONObject
import javax.inject.Inject
@HiltViewModel(assistedFactory = ExodusViewModel.Factory::class) @HiltViewModel
class ExodusViewModel @AssistedInject constructor( class ExodusViewModel @Inject constructor(
@Assisted private val exodusReport: Report,
private val exodusTrackers: JSONObject private val exodusTrackers: JSONObject
) : ViewModel() { ) : ViewModel() {
@AssistedFactory
interface Factory {
fun create(exodusReport: Report): ExodusViewModel
}
private val _trackers = MutableStateFlow<List<ExodusTracker>>(emptyList()) private val _trackers = MutableStateFlow<List<ExodusTracker>>(emptyList())
val trackers = _trackers.asStateFlow() val trackers = _trackers.asStateFlow()
init { fun getExodusTrackersFromReport(report: Report) {
getExodusTrackersFromReport()
}
private fun getExodusTrackersFromReport() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val trackerObjects = exodusReport.trackers.map { val trackerObjects = report.trackers.map {
exodusTrackers.getJSONObject(it.toString()) exodusTrackers.getJSONObject(it.toString())
}.toList() }.toList()

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M240,880Q207,880 183.5,856.5Q160,833 160,800L160,680L240,680L240,800Q240,800 240,800Q240,800 240,800L720,800Q720,800 720,800Q720,800 720,800L720,680L800,680L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM160,440L160,160Q160,127 183.5,103.5Q207,80 240,80L560,80L800,320L800,440L720,440L720,360L520,360L520,160L240,160Q240,160 240,160Q240,160 240,160L240,440L160,440ZM40,600L40,520L920,520L920,600L40,600ZM480,440L480,440L480,440L480,440L480,440Q480,440 480,440Q480,440 480,440ZM480,680L480,680Q480,680 480,680Q480,680 480,680L480,680Q480,680 480,680Q480,680 480,680L480,680Z"/>
</vector>