compose: spoof: Initial migration to compose

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-12-07 20:24:40 +08:00
parent bb9debaec2
commit 28e93b0af7
18 changed files with 562 additions and 527 deletions

View File

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

View File

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

View File

@@ -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.onboarding.OnboardingScreen
import com.aurora.store.compose.ui.search.SearchScreen
import com.aurora.store.compose.ui.spoof.SpoofScreen
/**
* Navigation display for compose screens
@@ -37,6 +38,16 @@ import com.aurora.store.compose.ui.search.SearchScreen
fun NavDisplay(startDestination: NavKey) {
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
val activity = LocalActivity.current
fun onNavigateUp() {
@@ -95,16 +106,6 @@ fun NavDisplay(startDestination: NavKey) {
}
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(
onNavigateUp = ::onNavigateUp,
onNavigateToSplash = { activity?.startActivity(splashIntent) }
@@ -127,6 +128,13 @@ fun NavDisplay(startDestination: NavKey) {
entry<Screen.Onboarding> {
OnboardingScreen()
}
entry<Screen.Spoof> {
SpoofScreen(
onNavigateUp = ::onNavigateUp,
onNavigateToSplash = { activity?.startActivity(splashIntent) }
)
}
}
)
}

View File

@@ -51,4 +51,7 @@ sealed class Screen : NavKey, Parcelable {
@Serializable
data object Onboarding : Screen()
@Serializable
data object Spoof : Screen()
}

View File

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

View File

@@ -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() }
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -438,10 +438,10 @@ class MoreDialogFragment : DialogFragment() {
icon = R.drawable.ic_favorite_unchecked,
screen = Screen.Favourite
),
ViewOption(
ComposeOption(
title = R.string.title_spoof_manager,
icon = R.drawable.ic_spoof,
destinationID = R.id.spoofFragment
screen = Screen.Spoof
)
)
}

View File

@@ -83,9 +83,7 @@ abstract class BaseFlavouredSplashFragment : BaseFragment<FragmentSplashBinding>
requireContext().navigate(Screen.Blacklist)
}
R.id.menu_spoof_manager -> {
findNavController().navigate(R.id.spoofFragment)
}
R.id.menu_spoof_manager -> requireContext().navigate(Screen.Spoof)
R.id.menu_settings -> {
findNavController().navigate(R.id.settingsFragment)

View File

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

View File

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

View File

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

View File

@@ -21,39 +21,30 @@ class SpoofViewModel @Inject constructor(
val defaultLocale: Locale = Locale.getDefault()
val defaultProperties = NativeDeviceInfoProvider.getNativeDeviceProperties(context)
private var currentDevice = spoofProvider.deviceProperties.getProperty("UserReadableName")
private var currentLocale = spoofProvider.locale
private val _currentLocale = MutableStateFlow(spoofProvider.locale)
val currentLocale = _currentLocale.asStateFlow()
private val _availableLocales: MutableStateFlow<List<Locale>> = MutableStateFlow(
spoofProvider.availableSpoofLocales
)
private val _availableLocales = MutableStateFlow(spoofProvider.availableSpoofLocales)
val availableLocales = _availableLocales.asStateFlow()
private val _availableDevices: MutableStateFlow<List<Properties>> = MutableStateFlow(
spoofProvider.availableSpoofDeviceProperties
)
private val _currentDevice = MutableStateFlow(spoofProvider.deviceProperties)
val currentDevice = _currentDevice.asStateFlow()
private val _availableDevices = MutableStateFlow(spoofProvider.availableSpoofDeviceProperties)
val availableDevices = _availableDevices.asStateFlow()
fun isDeviceSelected(properties: Properties): Boolean {
return currentDevice == properties.getProperty("UserReadableName")
}
fun onDeviceSelected(properties: Properties) {
currentDevice = properties.getProperty("UserReadableName")
_currentDevice.value = properties
if (currentDevice == defaultProperties.getProperty("UserReadableName")) {
if (currentDevice == defaultProperties) {
spoofProvider.removeSpoofDeviceProperties()
} else {
spoofProvider.setSpoofDeviceProperties(properties)
}
}
fun isLocaleSelected(locale: Locale): Boolean {
return currentLocale == locale
}
fun onLocaleSelected(locale: Locale) {
currentLocale = locale
_currentLocale.value = locale
if (currentLocale == defaultLocale) {
spoofProvider.removeSpoofLocale()

View File

@@ -61,11 +61,6 @@
android:name="com.aurora.store.view.ui.all.AppsGamesFragment"
android:label="@string/title_apps_games"
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
android:id="@+id/settingsFragment"
android:name="com.aurora.store.view.ui.preferences.SettingsFragment"

View File

@@ -470,6 +470,7 @@
<string name="checking_updates">Checking for updates</string>
<!-- ForceRestartDialog -->
<string name="force_restart_snack">Restart to apply changes?</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>