compose: spoof: Initial migration to compose
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import com.aurora.store.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable to display device details for spoofing in a list
|
||||||
|
* @param modifier The modifier to be applied to the composable
|
||||||
|
* @param userReadableName Name of the device, obtained through `UserReadableName` property
|
||||||
|
* @param manufacturer Name of the device manufacturer, obtained through `Build.MANUFACTURER` property
|
||||||
|
* @param androidVersionSdk Android version on the device, obtained through `Build.VERSION.SDK_INT` property
|
||||||
|
* @param platforms Platforms supported on the device, obtained through `Platforms` property
|
||||||
|
* @param isChecked If the device is selected
|
||||||
|
* @param onClick Callback when the composable is clicked
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun DeviceListItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
userReadableName: String,
|
||||||
|
manufacturer: String,
|
||||||
|
androidVersionSdk: String,
|
||||||
|
platforms: String,
|
||||||
|
isChecked: Boolean = false,
|
||||||
|
onClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = { if (!isChecked) onClick() })
|
||||||
|
.padding(dimensionResource(R.dimen.padding_small)),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1F)) {
|
||||||
|
Text(
|
||||||
|
text = userReadableName,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.spoof_property, manufacturer, androidVersionSdk),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = platforms.replace(",\\s*".toRegex(), ", "),
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Checkbox(checked = isChecked, onCheckedChange = { if (!isChecked) onClick() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun DeviceListItemPreview() {
|
||||||
|
DeviceListItem(
|
||||||
|
userReadableName = "Google Pixel 7a",
|
||||||
|
manufacturer = "Google",
|
||||||
|
androidVersionSdk = "33",
|
||||||
|
platforms = "arm64-v8a",
|
||||||
|
isChecked = true
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import com.aurora.store.R
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable to display locale details in a list
|
||||||
|
* @param modifier The modifier to be applied to the composable
|
||||||
|
* @param displayName Display name of the locale
|
||||||
|
* @param displayLanguage Display name of the language in the locale
|
||||||
|
* @param isChecked Whether the locale is checked/selected
|
||||||
|
* @param onClick Callback when the composable is clicked
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun LocaleListItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
displayName: String,
|
||||||
|
displayLanguage: String,
|
||||||
|
isChecked: Boolean = false,
|
||||||
|
onClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = { if (!isChecked) onClick() })
|
||||||
|
.padding(dimensionResource(R.dimen.padding_small)),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1F)) {
|
||||||
|
Text(
|
||||||
|
text = displayName,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = displayLanguage,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Checkbox(checked = isChecked, onCheckedChange = { if (!isChecked) onClick() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun LocaleListItemPreview() {
|
||||||
|
LocaleListItem(
|
||||||
|
displayName = Locale.JAPANESE.displayName,
|
||||||
|
displayLanguage = Locale.JAPAN.getDisplayLanguage(Locale.JAPAN),
|
||||||
|
isChecked = true
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import com.aurora.store.compose.ui.downloads.DownloadsScreen
|
|||||||
import com.aurora.store.compose.ui.favourite.FavouriteScreen
|
import com.aurora.store.compose.ui.favourite.FavouriteScreen
|
||||||
import com.aurora.store.compose.ui.onboarding.OnboardingScreen
|
import com.aurora.store.compose.ui.onboarding.OnboardingScreen
|
||||||
import com.aurora.store.compose.ui.search.SearchScreen
|
import com.aurora.store.compose.ui.search.SearchScreen
|
||||||
|
import com.aurora.store.compose.ui.spoof.SpoofScreen
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigation display for compose screens
|
* Navigation display for compose screens
|
||||||
@@ -37,6 +38,16 @@ import com.aurora.store.compose.ui.search.SearchScreen
|
|||||||
fun NavDisplay(startDestination: NavKey) {
|
fun NavDisplay(startDestination: NavKey) {
|
||||||
val backstack = rememberNavBackStack(startDestination)
|
val backstack = rememberNavBackStack(startDestination)
|
||||||
|
|
||||||
|
// TODO: Rework when migrating splash fragment to compose
|
||||||
|
val splashIntent = NavDeepLinkBuilder(LocalContext.current)
|
||||||
|
.setGraph(R.navigation.mobile_navigation)
|
||||||
|
.setDestination(R.id.splashFragment)
|
||||||
|
.setComponentName(MainActivity::class.java)
|
||||||
|
.createTaskStackBuilder()
|
||||||
|
.intents
|
||||||
|
.first()
|
||||||
|
.apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) }
|
||||||
|
|
||||||
// TODO: Drop this logic once everything is in compose
|
// TODO: Drop this logic once everything is in compose
|
||||||
val activity = LocalActivity.current
|
val activity = LocalActivity.current
|
||||||
fun onNavigateUp() {
|
fun onNavigateUp() {
|
||||||
@@ -95,16 +106,6 @@ fun NavDisplay(startDestination: NavKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
entry<Screen.Accounts> {
|
entry<Screen.Accounts> {
|
||||||
// TODO: Rework when migrating splash fragment to compose
|
|
||||||
val splashIntent = NavDeepLinkBuilder(LocalContext.current)
|
|
||||||
.setGraph(R.navigation.mobile_navigation)
|
|
||||||
.setDestination(R.id.splashFragment)
|
|
||||||
.setComponentName(MainActivity::class.java)
|
|
||||||
.createTaskStackBuilder()
|
|
||||||
.intents
|
|
||||||
.first()
|
|
||||||
.apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) }
|
|
||||||
|
|
||||||
AccountsScreen(
|
AccountsScreen(
|
||||||
onNavigateUp = ::onNavigateUp,
|
onNavigateUp = ::onNavigateUp,
|
||||||
onNavigateToSplash = { activity?.startActivity(splashIntent) }
|
onNavigateToSplash = { activity?.startActivity(splashIntent) }
|
||||||
@@ -127,6 +128,13 @@ fun NavDisplay(startDestination: NavKey) {
|
|||||||
entry<Screen.Onboarding> {
|
entry<Screen.Onboarding> {
|
||||||
OnboardingScreen()
|
OnboardingScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry<Screen.Spoof> {
|
||||||
|
SpoofScreen(
|
||||||
|
onNavigateUp = ::onNavigateUp,
|
||||||
|
onNavigateToSplash = { activity?.startActivity(splashIntent) }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,4 +51,7 @@ sealed class Screen : NavKey, Parcelable {
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Onboarding : Screen()
|
data object Onboarding : Screen()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Spoof : Screen()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.spoof
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composable.TextDividerComposable
|
||||||
|
import com.aurora.store.compose.composable.DeviceListItem
|
||||||
|
import com.aurora.store.compose.preview.PreviewTemplate
|
||||||
|
import com.aurora.store.viewmodel.spoof.SpoofViewModel
|
||||||
|
import java.util.Properties
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DevicePage(
|
||||||
|
onRequestNavigateToSplash: () -> Unit,
|
||||||
|
viewModel: SpoofViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val availableDevices by viewModel.availableDevices.collectAsStateWithLifecycle()
|
||||||
|
val currentDevice by viewModel.currentDevice.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
PageContent(
|
||||||
|
devices = availableDevices,
|
||||||
|
defaultDevice = viewModel.defaultProperties,
|
||||||
|
isDeviceSelected = { device -> device == currentDevice },
|
||||||
|
onDeviceSelected = { properties ->
|
||||||
|
viewModel.onDeviceSelected(properties)
|
||||||
|
onRequestNavigateToSplash()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PageContent(
|
||||||
|
defaultDevice: Properties = Properties(),
|
||||||
|
devices: List<Properties> = emptyList(),
|
||||||
|
isDeviceSelected: (properties: Properties) -> Boolean = { false },
|
||||||
|
onDeviceSelected: (properties: Properties) -> Unit = {}
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall))
|
||||||
|
) {
|
||||||
|
stickyHeader {
|
||||||
|
Surface(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
TextDividerComposable(
|
||||||
|
title = stringResource(R.string.default_spoof)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
DeviceListItem(
|
||||||
|
userReadableName = defaultDevice.getProperty("UserReadableName"),
|
||||||
|
manufacturer = defaultDevice.getProperty("Build.MANUFACTURER"),
|
||||||
|
androidVersionSdk = defaultDevice.getProperty("Build.VERSION.SDK_INT"),
|
||||||
|
platforms = defaultDevice.getProperty("Platforms"),
|
||||||
|
isChecked = isDeviceSelected(defaultDevice),
|
||||||
|
onClick = { onDeviceSelected(defaultDevice) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
stickyHeader {
|
||||||
|
Surface(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
TextDividerComposable(
|
||||||
|
title = stringResource(R.string.available_spoof)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items(items = devices, key = { device -> device.getProperty("Build.PRODUCT") }) { device ->
|
||||||
|
DeviceListItem(
|
||||||
|
userReadableName = device.getProperty("UserReadableName"),
|
||||||
|
manufacturer = device.getProperty("Build.MANUFACTURER"),
|
||||||
|
androidVersionSdk = device.getProperty("Build.VERSION.SDK_INT"),
|
||||||
|
platforms = device.getProperty("Platforms"),
|
||||||
|
isChecked = isDeviceSelected(device),
|
||||||
|
onClick = { onDeviceSelected(device) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun DevicePagePreview() {
|
||||||
|
fun getDevice(): Properties {
|
||||||
|
return Properties().apply {
|
||||||
|
setProperty("UserReadableName", "Google Pixel 9a")
|
||||||
|
setProperty("Build.VERSION.SDK_INT", "35")
|
||||||
|
setProperty("Build.MANUFACTURER", "Google")
|
||||||
|
setProperty("Platforms", "arm64-v8a")
|
||||||
|
setProperty("Build.PRODUCT", Random.nextInt().toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PreviewTemplate {
|
||||||
|
val defaultDevice = getDevice()
|
||||||
|
PageContent(
|
||||||
|
defaultDevice = defaultDevice,
|
||||||
|
devices = List(10) { getDevice() },
|
||||||
|
isDeviceSelected = { device -> defaultDevice == device }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.spoof
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composable.TextDividerComposable
|
||||||
|
import com.aurora.store.compose.composable.LocaleListItem
|
||||||
|
import com.aurora.store.compose.preview.PreviewTemplate
|
||||||
|
import com.aurora.store.viewmodel.spoof.SpoofViewModel
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LocalePage(
|
||||||
|
onRequestNavigateToSplash: () -> Unit,
|
||||||
|
viewModel: SpoofViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
|
val availableLocales by viewModel.availableLocales.collectAsStateWithLifecycle()
|
||||||
|
val currentLocale by viewModel.currentLocale.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
PageContent(
|
||||||
|
defaultLocale = viewModel.defaultLocale,
|
||||||
|
locales = availableLocales,
|
||||||
|
isLocaleSelected = { locale -> currentLocale == locale },
|
||||||
|
onLocaleSelected = { locale ->
|
||||||
|
viewModel.onLocaleSelected(locale)
|
||||||
|
onRequestNavigateToSplash()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PageContent(
|
||||||
|
defaultLocale: Locale = Locale.getDefault(),
|
||||||
|
locales: List<Locale> = emptyList(),
|
||||||
|
isLocaleSelected: (locale: Locale) -> Boolean = { false },
|
||||||
|
onLocaleSelected: (locale: Locale) -> Unit = {},
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall))
|
||||||
|
) {
|
||||||
|
stickyHeader {
|
||||||
|
Surface(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
TextDividerComposable(
|
||||||
|
title = stringResource(R.string.default_spoof)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
LocaleListItem(
|
||||||
|
displayName = defaultLocale.displayName,
|
||||||
|
displayLanguage = defaultLocale.getDisplayLanguage(defaultLocale),
|
||||||
|
isChecked = isLocaleSelected(defaultLocale),
|
||||||
|
onClick = { onLocaleSelected(defaultLocale) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
stickyHeader {
|
||||||
|
Surface(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
TextDividerComposable(
|
||||||
|
title = stringResource(R.string.available_spoof)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items(items = locales, key = { locale -> locale.hashCode() }) { locale ->
|
||||||
|
LocaleListItem(
|
||||||
|
displayName = locale.displayName,
|
||||||
|
displayLanguage = locale.getDisplayLanguage(locale),
|
||||||
|
isChecked = isLocaleSelected(locale),
|
||||||
|
onClick = { onLocaleSelected(locale) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun LocalePagePreview() {
|
||||||
|
PreviewTemplate {
|
||||||
|
PageContent(
|
||||||
|
locales = Locale.getAvailableLocales().toList().filter { it.displayName.isNotBlank() },
|
||||||
|
isLocaleSelected = { locale -> locale == Locale.getDefault() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.spoof
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SecondaryTabRow
|
||||||
|
import androidx.compose.material3.SnackbarDuration
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.SnackbarResult
|
||||||
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.util.fastForEachIndexed
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
|
import com.aurora.store.compose.ui.spoof.navigation.SpoofPage
|
||||||
|
import com.aurora.store.data.providers.AccountProvider
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SpoofScreen(onNavigateUp: () -> Unit, onNavigateToSplash: () -> Unit) {
|
||||||
|
ScreenContent(
|
||||||
|
onNavigateUp = onNavigateUp,
|
||||||
|
onNavigateToSplash = onNavigateToSplash
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ScreenContent(
|
||||||
|
pages: List<SpoofPage> = listOf(SpoofPage.DEVICE, SpoofPage.LOCALE),
|
||||||
|
onNavigateUp: () -> Unit = {},
|
||||||
|
onNavigateToSplash: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val pagerState = rememberPagerState { pages.size }
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val snackBarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
fun onRequestNavigateToSplash() {
|
||||||
|
coroutineScope.launch {
|
||||||
|
val result = snackBarHostState.showSnackbar(
|
||||||
|
message = context.getString(R.string.force_restart_snack),
|
||||||
|
actionLabel = context.getString(R.string.action_restart),
|
||||||
|
duration = SnackbarDuration.Indefinite
|
||||||
|
)
|
||||||
|
when (result) {
|
||||||
|
SnackbarResult.ActionPerformed -> {
|
||||||
|
AccountProvider.logout(context)
|
||||||
|
onNavigateToSplash()
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
snackbarHost = {
|
||||||
|
SnackbarHost(hostState = snackBarHostState)
|
||||||
|
},
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = stringResource(R.string.title_spoof_manager),
|
||||||
|
onNavigateUp = onNavigateUp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
SecondaryTabRow(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
selectedTabIndex = pagerState.currentPage
|
||||||
|
) {
|
||||||
|
pages.fastForEachIndexed { index, _ ->
|
||||||
|
Tab(
|
||||||
|
selected = pagerState.currentPage == index,
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
pagerState.animateScrollToPage(index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(id = pages[index].localized))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalPager(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
state = pagerState,
|
||||||
|
verticalAlignment = Alignment.Top
|
||||||
|
) { page ->
|
||||||
|
when (pages[page]) {
|
||||||
|
SpoofPage.DEVICE -> DevicePage(
|
||||||
|
onRequestNavigateToSplash = ::onRequestNavigateToSplash
|
||||||
|
)
|
||||||
|
|
||||||
|
SpoofPage.LOCALE -> LocalePage(
|
||||||
|
onRequestNavigateToSplash = ::onRequestNavigateToSplash
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.spoof.navigation
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import com.aurora.store.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pages that are shown in SpoofScreen
|
||||||
|
*/
|
||||||
|
enum class SpoofPage(@StringRes val localized: Int) {
|
||||||
|
DEVICE(R.string.title_device),
|
||||||
|
LOCALE(R.string.title_language)
|
||||||
|
}
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
* Aurora Store
|
|
||||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
|
||||||
*
|
|
||||||
* Aurora Store is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 2 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Aurora Store is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aurora.store.view.epoxy.views.preference
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.widget.CompoundButton
|
|
||||||
import com.airbnb.epoxy.CallbackProp
|
|
||||||
import com.airbnb.epoxy.ModelProp
|
|
||||||
import com.airbnb.epoxy.ModelView
|
|
||||||
import com.aurora.store.R
|
|
||||||
import com.aurora.store.databinding.ViewDeviceBinding
|
|
||||||
import com.aurora.store.view.epoxy.views.BaseModel
|
|
||||||
import com.aurora.store.view.epoxy.views.BaseView
|
|
||||||
import java.util.Properties
|
|
||||||
|
|
||||||
@ModelView(
|
|
||||||
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
|
|
||||||
baseModelClass = BaseModel::class
|
|
||||||
)
|
|
||||||
class DeviceView @JvmOverloads constructor(
|
|
||||||
context: Context?,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0
|
|
||||||
) : BaseView<ViewDeviceBinding>(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
@ModelProp
|
|
||||||
fun properties(properties: Properties) {
|
|
||||||
binding.line1.text = properties.getProperty("UserReadableName")
|
|
||||||
binding.line2.text = resources.getString(
|
|
||||||
R.string.spoof_property,
|
|
||||||
properties.getProperty("Build.MANUFACTURER"),
|
|
||||||
properties.getProperty("Build.VERSION.SDK_INT")
|
|
||||||
)
|
|
||||||
binding.line3.text = properties.getProperty("Platforms")
|
|
||||||
}
|
|
||||||
|
|
||||||
@ModelProp
|
|
||||||
fun markChecked(isChecked: Boolean) {
|
|
||||||
binding.checkbox.isChecked = isChecked
|
|
||||||
binding.checkbox.isEnabled = !isChecked
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallbackProp
|
|
||||||
fun checked(onCheckedChangeListener: CompoundButton.OnCheckedChangeListener?) {
|
|
||||||
binding.checkbox.setOnCheckedChangeListener(onCheckedChangeListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* Aurora Store
|
|
||||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
|
||||||
*
|
|
||||||
* Aurora Store is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 2 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Aurora Store is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aurora.store.view.epoxy.views.preference
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.widget.CompoundButton
|
|
||||||
import com.airbnb.epoxy.CallbackProp
|
|
||||||
import com.airbnb.epoxy.ModelProp
|
|
||||||
import com.airbnb.epoxy.ModelView
|
|
||||||
import com.aurora.store.databinding.ViewLocaleBinding
|
|
||||||
import com.aurora.store.view.epoxy.views.BaseModel
|
|
||||||
import com.aurora.store.view.epoxy.views.BaseView
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
@ModelView(
|
|
||||||
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
|
|
||||||
baseModelClass = BaseModel::class
|
|
||||||
)
|
|
||||||
class LocaleView @JvmOverloads constructor(
|
|
||||||
context: Context?,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0
|
|
||||||
) : BaseView<ViewLocaleBinding>(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
@ModelProp
|
|
||||||
fun locale(locale: Locale) {
|
|
||||||
binding.line1.text = locale.displayName
|
|
||||||
binding.line2.text = locale.getDisplayLanguage(locale)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ModelProp
|
|
||||||
fun markChecked(isChecked: Boolean) {
|
|
||||||
binding.checkbox.isChecked = isChecked
|
|
||||||
binding.checkbox.isEnabled = !isChecked
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallbackProp
|
|
||||||
fun checked(onCheckedChangeListener: CompoundButton.OnCheckedChangeListener?) {
|
|
||||||
binding.checkbox.setOnCheckedChangeListener(onCheckedChangeListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -438,10 +438,10 @@ class MoreDialogFragment : DialogFragment() {
|
|||||||
icon = R.drawable.ic_favorite_unchecked,
|
icon = R.drawable.ic_favorite_unchecked,
|
||||||
screen = Screen.Favourite
|
screen = Screen.Favourite
|
||||||
),
|
),
|
||||||
ViewOption(
|
ComposeOption(
|
||||||
title = R.string.title_spoof_manager,
|
title = R.string.title_spoof_manager,
|
||||||
icon = R.drawable.ic_spoof,
|
icon = R.drawable.ic_spoof,
|
||||||
destinationID = R.id.spoofFragment
|
screen = Screen.Spoof
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,9 +83,7 @@ abstract class BaseFlavouredSplashFragment : BaseFragment<FragmentSplashBinding>
|
|||||||
requireContext().navigate(Screen.Blacklist)
|
requireContext().navigate(Screen.Blacklist)
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.menu_spoof_manager -> {
|
R.id.menu_spoof_manager -> requireContext().navigate(Screen.Spoof)
|
||||||
findNavController().navigate(R.id.spoofFragment)
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.menu_settings -> {
|
R.id.menu_settings -> {
|
||||||
findNavController().navigate(R.id.settingsFragment)
|
findNavController().navigate(R.id.settingsFragment)
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
/*
|
|
||||||
* Aurora Store
|
|
||||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
|
||||||
*
|
|
||||||
* Aurora Store is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 2 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Aurora Store is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aurora.store.view.ui.spoof
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import com.aurora.store.R
|
|
||||||
import com.aurora.store.data.providers.AccountProvider
|
|
||||||
import com.aurora.store.databinding.FragmentGenericRecyclerBinding
|
|
||||||
import com.aurora.store.view.epoxy.views.TextDividerViewModel_
|
|
||||||
import com.aurora.store.view.epoxy.views.preference.DeviceViewModel_
|
|
||||||
import com.aurora.store.view.ui.commons.BaseFragment
|
|
||||||
import com.aurora.store.viewmodel.spoof.SpoofViewModel
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.Properties
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class DeviceSpoofFragment : BaseFragment<FragmentGenericRecyclerBinding>() {
|
|
||||||
|
|
||||||
private val viewModel: SpoofViewModel by viewModels()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
fun newInstance(): DeviceSpoofFragment {
|
|
||||||
return DeviceSpoofFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
||||||
viewModel.availableDevices.collect { updateController(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateController(devices: List<Properties>) {
|
|
||||||
binding.recycler.withModels {
|
|
||||||
setFilterDuplicates(true)
|
|
||||||
|
|
||||||
add(
|
|
||||||
TextDividerViewModel_()
|
|
||||||
.id("default_divider")
|
|
||||||
.title(getString(R.string.default_spoof))
|
|
||||||
)
|
|
||||||
|
|
||||||
add(
|
|
||||||
DeviceViewModel_()
|
|
||||||
.id(viewModel.defaultProperties.hashCode())
|
|
||||||
.markChecked(viewModel.isDeviceSelected(viewModel.defaultProperties))
|
|
||||||
.checked { _, checked ->
|
|
||||||
if (checked) {
|
|
||||||
viewModel.onDeviceSelected(viewModel.defaultProperties)
|
|
||||||
requestModelBuild()
|
|
||||||
AccountProvider.logout(requireContext())
|
|
||||||
findNavController().navigate(R.id.forceRestartDialog)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.properties(viewModel.defaultProperties)
|
|
||||||
)
|
|
||||||
|
|
||||||
add(
|
|
||||||
TextDividerViewModel_()
|
|
||||||
.id("available_divider")
|
|
||||||
.title(getString(R.string.available_spoof))
|
|
||||||
)
|
|
||||||
|
|
||||||
devices.forEach {
|
|
||||||
add(
|
|
||||||
DeviceViewModel_()
|
|
||||||
.id(it.hashCode())
|
|
||||||
.markChecked(viewModel.isDeviceSelected(it))
|
|
||||||
.checked { _, checked ->
|
|
||||||
if (checked) {
|
|
||||||
viewModel.onDeviceSelected(it)
|
|
||||||
requestModelBuild()
|
|
||||||
AccountProvider.logout(requireContext())
|
|
||||||
findNavController().navigate(R.id.forceRestartDialog)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.properties(it)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
/*
|
|
||||||
* Aurora Store
|
|
||||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
|
||||||
*
|
|
||||||
* Aurora Store is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 2 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Aurora Store is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aurora.store.view.ui.spoof
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import com.aurora.store.R
|
|
||||||
import com.aurora.store.data.providers.AccountProvider
|
|
||||||
import com.aurora.store.databinding.FragmentGenericRecyclerBinding
|
|
||||||
import com.aurora.store.view.epoxy.views.TextDividerViewModel_
|
|
||||||
import com.aurora.store.view.epoxy.views.preference.LocaleViewModel_
|
|
||||||
import com.aurora.store.view.ui.commons.BaseFragment
|
|
||||||
import com.aurora.store.viewmodel.spoof.SpoofViewModel
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class LocaleSpoofFragment : BaseFragment<FragmentGenericRecyclerBinding>() {
|
|
||||||
|
|
||||||
private val viewModel: SpoofViewModel by viewModels()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
fun newInstance(): LocaleSpoofFragment {
|
|
||||||
return LocaleSpoofFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewModel.availableLocales.collect {
|
|
||||||
updateController(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateController(locales: List<Locale>) {
|
|
||||||
binding.recycler.withModels {
|
|
||||||
setFilterDuplicates(true)
|
|
||||||
|
|
||||||
add(
|
|
||||||
TextDividerViewModel_()
|
|
||||||
.id("default_divider")
|
|
||||||
.title(getString(R.string.default_spoof))
|
|
||||||
)
|
|
||||||
|
|
||||||
add(
|
|
||||||
LocaleViewModel_()
|
|
||||||
.id(viewModel.defaultLocale.language)
|
|
||||||
.markChecked(viewModel.isLocaleSelected(viewModel.defaultLocale))
|
|
||||||
.checked { _, checked ->
|
|
||||||
if (checked) {
|
|
||||||
viewModel.onLocaleSelected(viewModel.defaultLocale)
|
|
||||||
requestModelBuild()
|
|
||||||
AccountProvider.logout(requireContext())
|
|
||||||
findNavController().navigate(R.id.forceRestartDialog)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.locale(viewModel.defaultLocale)
|
|
||||||
)
|
|
||||||
|
|
||||||
add(
|
|
||||||
TextDividerViewModel_()
|
|
||||||
.id("available_divider")
|
|
||||||
.title(getString(R.string.available_spoof))
|
|
||||||
)
|
|
||||||
|
|
||||||
locales.forEach {
|
|
||||||
add(
|
|
||||||
LocaleViewModel_()
|
|
||||||
.id(it.language)
|
|
||||||
.markChecked(viewModel.spoofProvider.locale == it)
|
|
||||||
.checked { _, checked ->
|
|
||||||
if (checked) {
|
|
||||||
viewModel.onLocaleSelected(it)
|
|
||||||
requestModelBuild()
|
|
||||||
AccountProvider.logout(requireContext())
|
|
||||||
findNavController().navigate(R.id.forceRestartDialog)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.locale(it)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
/*
|
|
||||||
* Aurora Store
|
|
||||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
|
||||||
*
|
|
||||||
* Aurora Store is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 2 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Aurora Store is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aurora.store.view.ui.spoof
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.View
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
import com.aurora.extensions.toast
|
|
||||||
import com.aurora.store.R
|
|
||||||
import com.aurora.store.data.providers.NativeDeviceInfoProvider
|
|
||||||
import com.aurora.store.databinding.FragmentSpoofBinding
|
|
||||||
import com.aurora.store.util.PathUtil
|
|
||||||
import com.aurora.store.view.ui.commons.BaseFragment
|
|
||||||
import com.google.android.material.tabs.TabLayout
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class SpoofFragment : BaseFragment<FragmentSpoofBinding>() {
|
|
||||||
private val TAG = SpoofFragment::class.java.simpleName
|
|
||||||
|
|
||||||
// Android is weird, even if export device config with proper mime type, it will refuse to open
|
|
||||||
// it again with same mime type
|
|
||||||
private val importMimeType = "application/octet-stream"
|
|
||||||
private val exportMimeType = "text/x-java-properties"
|
|
||||||
|
|
||||||
private val startForDocumentImport =
|
|
||||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) {
|
|
||||||
if (it != null) importDeviceConfig(it) else toast(R.string.toast_import_failed)
|
|
||||||
}
|
|
||||||
private val startForDocumentExport =
|
|
||||||
registerForActivityResult(ActivityResultContracts.CreateDocument(exportMimeType)) {
|
|
||||||
if (it != null) exportDeviceConfig(it) else toast(R.string.toast_export_failed)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
// Toolbar
|
|
||||||
binding.toolbar.apply {
|
|
||||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
|
||||||
setOnMenuItemClickListener {
|
|
||||||
when (it.itemId) {
|
|
||||||
R.id.action_import -> {
|
|
||||||
startForDocumentImport.launch(arrayOf(importMimeType))
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.action_export -> {
|
|
||||||
startForDocumentExport
|
|
||||||
.launch("aurora_store_${Build.BRAND}_${Build.DEVICE}.properties")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ViewPager
|
|
||||||
binding.pager.adapter = ViewPagerAdapter(childFragmentManager, viewLifecycleOwner.lifecycle)
|
|
||||||
|
|
||||||
TabLayoutMediator(
|
|
||||||
binding.tabLayout,
|
|
||||||
binding.pager,
|
|
||||||
true
|
|
||||||
) { tab: TabLayout.Tab, position: Int ->
|
|
||||||
when (position) {
|
|
||||||
0 -> tab.text = getString(R.string.title_device)
|
|
||||||
1 -> tab.text = getString(R.string.title_language)
|
|
||||||
else -> {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.attach()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
binding.pager.adapter = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importDeviceConfig(uri: Uri) {
|
|
||||||
try {
|
|
||||||
requireContext().contentResolver?.openInputStream(uri)?.use { input ->
|
|
||||||
PathUtil.getNewEmptySpoofConfig(requireContext()).outputStream().use {
|
|
||||||
input.copyTo(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toast(R.string.toast_import_success)
|
|
||||||
activity?.recreate()
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.e(TAG, "Failed to import device config", exception)
|
|
||||||
toast(R.string.toast_import_failed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun exportDeviceConfig(uri: Uri) {
|
|
||||||
try {
|
|
||||||
NativeDeviceInfoProvider.getNativeDeviceProperties(requireContext(), true)
|
|
||||||
.store(requireContext().contentResolver?.openOutputStream(uri), "DEVICE_CONFIG")
|
|
||||||
toast(R.string.toast_export_success)
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.e(TAG, "Failed to export device config", exception)
|
|
||||||
toast(R.string.toast_export_failed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class ViewPagerAdapter(fragment: FragmentManager, lifecycle: Lifecycle) :
|
|
||||||
FragmentStateAdapter(fragment, lifecycle) {
|
|
||||||
override fun createFragment(position: Int): Fragment {
|
|
||||||
return when (position) {
|
|
||||||
0 -> DeviceSpoofFragment.newInstance()
|
|
||||||
1 -> LocaleSpoofFragment.newInstance()
|
|
||||||
else -> Fragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,39 +21,30 @@ class SpoofViewModel @Inject constructor(
|
|||||||
val defaultLocale: Locale = Locale.getDefault()
|
val defaultLocale: Locale = Locale.getDefault()
|
||||||
val defaultProperties = NativeDeviceInfoProvider.getNativeDeviceProperties(context)
|
val defaultProperties = NativeDeviceInfoProvider.getNativeDeviceProperties(context)
|
||||||
|
|
||||||
private var currentDevice = spoofProvider.deviceProperties.getProperty("UserReadableName")
|
private val _currentLocale = MutableStateFlow(spoofProvider.locale)
|
||||||
private var currentLocale = spoofProvider.locale
|
val currentLocale = _currentLocale.asStateFlow()
|
||||||
|
|
||||||
private val _availableLocales: MutableStateFlow<List<Locale>> = MutableStateFlow(
|
private val _availableLocales = MutableStateFlow(spoofProvider.availableSpoofLocales)
|
||||||
spoofProvider.availableSpoofLocales
|
|
||||||
)
|
|
||||||
val availableLocales = _availableLocales.asStateFlow()
|
val availableLocales = _availableLocales.asStateFlow()
|
||||||
|
|
||||||
private val _availableDevices: MutableStateFlow<List<Properties>> = MutableStateFlow(
|
private val _currentDevice = MutableStateFlow(spoofProvider.deviceProperties)
|
||||||
spoofProvider.availableSpoofDeviceProperties
|
val currentDevice = _currentDevice.asStateFlow()
|
||||||
)
|
|
||||||
|
private val _availableDevices = MutableStateFlow(spoofProvider.availableSpoofDeviceProperties)
|
||||||
val availableDevices = _availableDevices.asStateFlow()
|
val availableDevices = _availableDevices.asStateFlow()
|
||||||
|
|
||||||
fun isDeviceSelected(properties: Properties): Boolean {
|
|
||||||
return currentDevice == properties.getProperty("UserReadableName")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDeviceSelected(properties: Properties) {
|
fun onDeviceSelected(properties: Properties) {
|
||||||
currentDevice = properties.getProperty("UserReadableName")
|
_currentDevice.value = properties
|
||||||
|
|
||||||
if (currentDevice == defaultProperties.getProperty("UserReadableName")) {
|
if (currentDevice == defaultProperties) {
|
||||||
spoofProvider.removeSpoofDeviceProperties()
|
spoofProvider.removeSpoofDeviceProperties()
|
||||||
} else {
|
} else {
|
||||||
spoofProvider.setSpoofDeviceProperties(properties)
|
spoofProvider.setSpoofDeviceProperties(properties)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isLocaleSelected(locale: Locale): Boolean {
|
|
||||||
return currentLocale == locale
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onLocaleSelected(locale: Locale) {
|
fun onLocaleSelected(locale: Locale) {
|
||||||
currentLocale = locale
|
_currentLocale.value = locale
|
||||||
|
|
||||||
if (currentLocale == defaultLocale) {
|
if (currentLocale == defaultLocale) {
|
||||||
spoofProvider.removeSpoofLocale()
|
spoofProvider.removeSpoofLocale()
|
||||||
|
|||||||
@@ -61,11 +61,6 @@
|
|||||||
android:name="com.aurora.store.view.ui.all.AppsGamesFragment"
|
android:name="com.aurora.store.view.ui.all.AppsGamesFragment"
|
||||||
android:label="@string/title_apps_games"
|
android:label="@string/title_apps_games"
|
||||||
tools:layout="@layout/fragment_generic_with_search" />
|
tools:layout="@layout/fragment_generic_with_search" />
|
||||||
<fragment
|
|
||||||
android:id="@+id/spoofFragment"
|
|
||||||
android:name="com.aurora.store.view.ui.spoof.SpoofFragment"
|
|
||||||
android:label="@string/title_spoof_manager"
|
|
||||||
tools:layout="@layout/fragment_spoof" />
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/settingsFragment"
|
android:id="@+id/settingsFragment"
|
||||||
android:name="com.aurora.store.view.ui.preferences.SettingsFragment"
|
android:name="com.aurora.store.view.ui.preferences.SettingsFragment"
|
||||||
|
|||||||
@@ -470,6 +470,7 @@
|
|||||||
<string name="checking_updates">Checking for updates</string>
|
<string name="checking_updates">Checking for updates</string>
|
||||||
|
|
||||||
<!-- ForceRestartDialog -->
|
<!-- ForceRestartDialog -->
|
||||||
|
<string name="force_restart_snack">Restart to apply changes?</string>
|
||||||
<string name="force_restart_title">Restart Aurora Store</string>
|
<string name="force_restart_title">Restart Aurora Store</string>
|
||||||
<string name="force_restart_summary">Aurora Store needs to be restarted to apply the newly changed settings</string>
|
<string name="force_restart_summary">Aurora Store needs to be restarted to apply the newly changed settings</string>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user