diff --git a/app/src/main/java/com/aurora/store/compose/composable/DeviceListItem.kt b/app/src/main/java/com/aurora/store/compose/composable/DeviceListItem.kt new file mode 100644 index 000000000..0fb05fbd8 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composable/DeviceListItem.kt @@ -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 + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/composable/LocaleListItem.kt b/app/src/main/java/com/aurora/store/compose/composable/LocaleListItem.kt new file mode 100644 index 000000000..cebc413d8 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composable/LocaleListItem.kt @@ -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 + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt b/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt index 4f9ee5374..df528f12a 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt @@ -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 { - // 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 { OnboardingScreen() } + + entry { + SpoofScreen( + onNavigateUp = ::onNavigateUp, + onNavigateToSplash = { activity?.startActivity(splashIntent) } + ) + } } ) } diff --git a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt index 21cdaf7a1..e64a88362 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt @@ -51,4 +51,7 @@ sealed class Screen : NavKey, Parcelable { @Serializable data object Onboarding : Screen() + + @Serializable + data object Spoof : Screen() } diff --git a/app/src/main/java/com/aurora/store/compose/ui/spoof/DevicePage.kt b/app/src/main/java/com/aurora/store/compose/ui/spoof/DevicePage.kt new file mode 100644 index 000000000..0a5de1d8b --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/spoof/DevicePage.kt @@ -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 = 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 } + ) + } +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/spoof/LocalePage.kt b/app/src/main/java/com/aurora/store/compose/ui/spoof/LocalePage.kt new file mode 100644 index 000000000..c08841054 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/spoof/LocalePage.kt @@ -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 = 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() } + ) + } +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/spoof/SpoofScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/spoof/SpoofScreen.kt new file mode 100644 index 000000000..0612714ac --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/spoof/SpoofScreen.kt @@ -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 = 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 + ) + } + } + } + } +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/spoof/navigation/SpoofPage.kt b/app/src/main/java/com/aurora/store/compose/ui/spoof/navigation/SpoofPage.kt new file mode 100644 index 000000000..d7f2f5ce0 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/spoof/navigation/SpoofPage.kt @@ -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) +} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/preference/DeviceView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/preference/DeviceView.kt deleted file mode 100644 index 3932c458e..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/preference/DeviceView.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * 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 . - * - */ - -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(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) - } -} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/preference/LocaleView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/preference/LocaleView.kt deleted file mode 100644 index c8525f5b7..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/preference/LocaleView.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * 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 . - * - */ - -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(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) - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/MoreDialogFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/MoreDialogFragment.kt index 0320806fb..3112c0456 100644 --- a/app/src/main/java/com/aurora/store/view/ui/commons/MoreDialogFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/commons/MoreDialogFragment.kt @@ -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 ) ) } diff --git a/app/src/main/java/com/aurora/store/view/ui/splash/BaseFlavouredSplashFragment.kt b/app/src/main/java/com/aurora/store/view/ui/splash/BaseFlavouredSplashFragment.kt index e0ec92113..74590b7dc 100644 --- a/app/src/main/java/com/aurora/store/view/ui/splash/BaseFlavouredSplashFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/splash/BaseFlavouredSplashFragment.kt @@ -83,9 +83,7 @@ abstract class BaseFlavouredSplashFragment : BaseFragment 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) diff --git a/app/src/main/java/com/aurora/store/view/ui/spoof/DeviceSpoofFragment.kt b/app/src/main/java/com/aurora/store/view/ui/spoof/DeviceSpoofFragment.kt deleted file mode 100644 index d1485e098..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/spoof/DeviceSpoofFragment.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * 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 . - * - */ - -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() { - - 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) { - 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) - ) - } - } - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/spoof/LocaleSpoofFragment.kt b/app/src/main/java/com/aurora/store/view/ui/spoof/LocaleSpoofFragment.kt deleted file mode 100644 index 696369d96..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/spoof/LocaleSpoofFragment.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * 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 . - * - */ - -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() { - - 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) { - 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) - ) - } - } - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/spoof/SpoofFragment.kt b/app/src/main/java/com/aurora/store/view/ui/spoof/SpoofFragment.kt deleted file mode 100644 index b7410ad3c..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/spoof/SpoofFragment.kt +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * 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 . - * - */ - -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() { - 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 - } - } -} diff --git a/app/src/main/java/com/aurora/store/viewmodel/spoof/SpoofViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/spoof/SpoofViewModel.kt index 7990be8a5..cde38a242 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/spoof/SpoofViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/spoof/SpoofViewModel.kt @@ -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> = MutableStateFlow( - spoofProvider.availableSpoofLocales - ) + private val _availableLocales = MutableStateFlow(spoofProvider.availableSpoofLocales) val availableLocales = _availableLocales.asStateFlow() - private val _availableDevices: MutableStateFlow> = 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() diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 8b5d7a54e..6a5d5ead3 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -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" /> - Checking for updates + Restart to apply changes? Restart Aurora Store Aurora Store needs to be restarted to apply the newly changed settings