compose: spoof: Migrate spoof menu to compose

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-12-08 15:07:02 +08:00
parent 28e93b0af7
commit 3c01dd28fa
6 changed files with 174 additions and 58 deletions

View File

@@ -5,6 +5,10 @@
package com.aurora.store.compose.ui.spoof
import android.net.Uri
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -27,17 +31,29 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.util.fastForEachIndexed
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.aurora.Constants
import com.aurora.extensions.toast
import com.aurora.store.R
import com.aurora.store.compose.composable.TopAppBar
import com.aurora.store.compose.ui.spoof.menu.MenuItem
import com.aurora.store.compose.ui.spoof.menu.SpoofMenu
import com.aurora.store.compose.ui.spoof.navigation.SpoofPage
import com.aurora.store.data.providers.AccountProvider
import com.aurora.store.viewmodel.spoof.SpoofViewModel
import kotlinx.coroutines.launch
@Composable
fun SpoofScreen(onNavigateUp: () -> Unit, onNavigateToSplash: () -> Unit) {
fun SpoofScreen(
onNavigateUp: () -> Unit,
onNavigateToSplash: () -> Unit,
viewModel: SpoofViewModel = hiltViewModel(),
) {
ScreenContent(
onNavigateUp = onNavigateUp,
onNavigateToSplash = onNavigateToSplash
onNavigateToSplash = onNavigateToSplash,
onDeviceSpoofImport = { uri -> viewModel.importDeviceSpoof(uri) },
onDeviceSpoofExport = { uri -> viewModel.exportDeviceSpoof(uri) }
)
}
@@ -45,13 +61,36 @@ fun SpoofScreen(onNavigateUp: () -> Unit, onNavigateToSplash: () -> Unit) {
private fun ScreenContent(
pages: List<SpoofPage> = listOf(SpoofPage.DEVICE, SpoofPage.LOCALE),
onNavigateUp: () -> Unit = {},
onNavigateToSplash: () -> Unit = {}
onNavigateToSplash: () -> Unit = {},
onDeviceSpoofImport: (uri: Uri) -> Unit = {},
onDeviceSpoofExport: (uri: Uri) -> Unit = {}
) {
val context = LocalContext.current
val pagerState = rememberPagerState { pages.size }
val coroutineScope = rememberCoroutineScope()
val snackBarHostState = remember { SnackbarHostState() }
val docImportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(),
onResult = {
if (it != null) {
onDeviceSpoofImport(it)
} else {
context.toast(R.string.toast_import_failed)
}
}
)
val docExportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument(Constants.PROPERTIES_EXPORT_MIME_TYPE),
onResult = {
if (it != null) {
onDeviceSpoofExport(it)
} else {
context.toast(R.string.toast_export_failed)
}
}
)
fun onRequestNavigateToSplash() {
coroutineScope.launch {
val result = snackBarHostState.showSnackbar(
@@ -64,11 +103,29 @@ private fun ScreenContent(
AccountProvider.logout(context)
onNavigateToSplash()
}
else -> Unit
}
}
}
@Composable
fun SetupMenu() {
SpoofMenu { menuItem ->
when (menuItem) {
MenuItem.IMPORT -> {
docImportLauncher.launch(arrayOf(Constants.PROPERTIES_IMPORT_MIME_TYPE))
}
MenuItem.EXPORT -> {
docExportLauncher.launch(
"aurora_store_${Build.BRAND}_${Build.DEVICE}.properties"
)
}
}
}
}
Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackBarHostState)
@@ -76,7 +133,8 @@ private fun ScreenContent(
topBar = {
TopAppBar(
title = stringResource(R.string.title_spoof_manager),
onNavigateUp = onNavigateUp
onNavigateUp = onNavigateUp,
actions = { SetupMenu() }
)
}
) { paddingValues ->

View File

@@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.spoof.menu
enum class MenuItem {
IMPORT,
EXPORT
}

View File

@@ -0,0 +1,70 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.spoof.menu
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R
import com.aurora.store.compose.preview.PreviewTemplate
/**
* Menu for the blacklist screen
* @param modifier The modifier to be applied to the composable
* @param onMenuItemClicked Callback when a menu item has been clicked
* @see MenuItem
*/
@Composable
fun SpoofMenu(
modifier: Modifier = Modifier,
isExpanded: Boolean = false,
onMenuItemClicked: (menuItem: MenuItem) -> Unit = {}
) {
var expanded by remember { mutableStateOf(isExpanded) }
fun onClick(menuItem: MenuItem) {
onMenuItemClicked(menuItem)
expanded = false
}
Box(modifier = modifier) {
IconButton(onClick = { expanded = true }) {
Icon(
painter = painterResource(R.drawable.ic_more_vert),
contentDescription = stringResource(R.string.menu)
)
}
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_import)) },
onClick = { onClick(MenuItem.IMPORT) }
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_export)) },
onClick = { onClick(MenuItem.EXPORT) }
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun SpoofMenuPreview() {
PreviewTemplate {
SpoofMenu(isExpanded = true)
}
}

View File

@@ -1,9 +1,12 @@
package com.aurora.store.viewmodel.spoof
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.lifecycle.ViewModel
import com.aurora.store.data.providers.NativeDeviceInfoProvider
import com.aurora.store.data.providers.SpoofProvider
import com.aurora.store.util.PathUtil
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
@@ -14,9 +17,11 @@ import javax.inject.Inject
@HiltViewModel
class SpoofViewModel @Inject constructor(
val spoofProvider: SpoofProvider,
private val spoofProvider: SpoofProvider,
@ApplicationContext private val context: Context
): ViewModel() {
) : ViewModel() {
private val TAG = SpoofViewModel::class.java.simpleName
val defaultLocale: Locale = Locale.getDefault()
val defaultProperties = NativeDeviceInfoProvider.getNativeDeviceProperties(context)
@@ -52,4 +57,26 @@ class SpoofViewModel @Inject constructor(
spoofProvider.setSpoofLocale(locale)
}
}
fun importDeviceSpoof(uri: Uri) {
try {
context.contentResolver?.openInputStream(uri)?.use { input ->
PathUtil.getNewEmptySpoofConfig(context).outputStream().use {
input.copyTo(it)
}
}
_availableDevices.value = spoofProvider.availableDeviceProperties
} catch (exception: Exception) {
Log.e(TAG, "Failed to import device config", exception)
}
}
fun exportDeviceSpoof(uri: Uri) {
try {
NativeDeviceInfoProvider.getNativeDeviceProperties(context, true)
.store(context.contentResolver?.openOutputStream(uri), "DEVICE_CONFIG")
} catch (exception: Exception) {
Log.e(TAG, "Failed to export device config", exception)
}
}
}