SpoofProvider: Refactor and adjust a bit

* Show default device on top under different category
* Remove spoof configuration when default is selected

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2024-11-19 13:37:26 +07:00
parent a26a4953c8
commit 84cd577a3c
12 changed files with 229 additions and 186 deletions

View File

@@ -36,22 +36,22 @@ class SpoofProviderTest {
@Test
fun testSpoofingDeviceLocale() {
assertThat(spoofProvider.isLocaleSpoofEnabled()).isFalse()
assertThat(spoofProvider.isLocaleSpoofEnabled).isFalse()
spoofProvider.setSpoofLocale(Locale.JAPAN)
assertThat(spoofProvider.isLocaleSpoofEnabled()).isTrue()
assertThat(spoofProvider.getSpoofLocale() == Locale.JAPAN).isTrue()
assertThat(spoofProvider.isLocaleSpoofEnabled).isTrue()
assertThat(spoofProvider.locale == Locale.JAPAN).isTrue()
}
@Test
fun testSpoofingDeviceProperties() {
assertThat(spoofProvider.isDeviceSpoofEnabled()).isFalse()
assertThat(spoofProvider.isDeviceSpoofEnabled).isFalse()
val properties = Properties().apply {
setProperty("UserReadableName", "Test")
}
spoofProvider.setSpoofDeviceProperties(properties)
assertThat(spoofProvider.isDeviceSpoofEnabled()).isTrue()
assertThat(spoofProvider.getSpoofDeviceProperties() == properties).isTrue()
assertThat(spoofProvider.isDeviceSpoofEnabled).isTrue()
assertThat(spoofProvider.deviceProperties == properties).isTrue()
}
}

View File

@@ -32,13 +32,10 @@ import com.aurora.store.data.model.Auth
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_AUTH_DATA
import com.aurora.store.util.Preferences.PREFERENCE_DISPENSER_URLS
import com.aurora.store.util.Preferences.PREFERENCE_VENDING_VERSION
import com.google.gson.Gson
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.Locale
import java.util.Properties
import javax.inject.Inject
import javax.inject.Singleton
@@ -72,24 +69,6 @@ class AuthProvider @Inject constructor(
val isAnonymous: Boolean
get() = AccountProvider.getAccountType(context) == AccountType.ANONYMOUS
private val properties: Properties
get() {
val currentProperties = if (spoofProvider.isDeviceSpoofEnabled()) {
spoofProvider.getSpoofDeviceProperties()
} else {
NativeDeviceInfoProvider(context).getNativeDeviceProperties()
}
setVendingVersion(currentProperties)
return currentProperties
}
private val locale: Locale
get() = if (spoofProvider.isLocaleSpoofEnabled()) {
spoofProvider.getSpoofLocale()
} else {
Locale.getDefault()
}
/**
* Checks whether saved AuthData is valid or not
*/
@@ -124,8 +103,8 @@ class AuthProvider @Inject constructor(
email = email,
token = token,
tokenType = tokenType,
properties = properties,
locale = locale,
properties = spoofProvider.deviceProperties,
locale = spoofProvider.locale,
)
)
} catch (exception: Exception) {
@@ -153,8 +132,8 @@ class AuthProvider @Inject constructor(
token = auth.auth,
tokenType = AuthHelper.Token.AUTH,
isAnonymous = true,
properties = properties,
locale = locale
properties = spoofProvider.deviceProperties,
locale = spoofProvider.locale
)
)
} catch (exception: Exception) {
@@ -178,18 +157,6 @@ class AuthProvider @Inject constructor(
Preferences.remove(context, PREFERENCE_AUTH_DATA)
}
private fun setVendingVersion(currentProperties: Properties) {
val vendingVersionIndex = Preferences.getInteger(context, PREFERENCE_VENDING_VERSION)
if (vendingVersionIndex > 0) {
val resources = context.resources
val versionCodes = resources.getStringArray(R.array.pref_vending_version_codes)
val versionStrings = resources.getStringArray(R.array.pref_vending_version)
currentProperties.setProperty("Vending.version", versionCodes[vendingVersionIndex])
currentProperties.setProperty("Vending.versionString", versionStrings[vendingVersionIndex])
}
}
@Throws(Exception::class)
private fun throwError(playResponse: PlayResponse, context: Context) {
when (playResponse.code) {

View File

@@ -26,12 +26,12 @@ import androidx.core.content.getSystemService
import com.aurora.extensions.isHuawei
import java.util.Properties
class NativeDeviceInfoProvider(val context: Context) {
object NativeDeviceInfoProvider {
fun getNativeDeviceProperties(isExport: Boolean = false): Properties {
fun getNativeDeviceProperties(context: Context, isExport: Boolean = false): Properties {
val properties = Properties().apply {
//Build Props
setProperty("UserReadableName", "${Build.DEVICE}-default")
setProperty("UserReadableName", "${Build.MANUFACTURER} ${Build.MODEL}")
setProperty("Build.HARDWARE", Build.HARDWARE)
setProperty(
"Build.RADIO",
@@ -71,11 +71,11 @@ class NativeDeviceInfoProvider(val context: Context) {
//Supported Platforms
setProperty("Platforms", Build.SUPPORTED_ABIS.joinToString(separator = ","))
//Supported Features
setProperty("Features", getFeatures().joinToString(separator = ","))
setProperty("Features", getFeatures(context).joinToString(separator = ","))
//Shared Locales
setProperty("Locales", getLocales().joinToString(separator = ","))
setProperty("Locales", getLocales(context).joinToString(separator = ","))
//Shared Libraries
setProperty("SharedLibraries", getSharedLibraries().joinToString(separator = ","))
setProperty("SharedLibraries", getSharedLibraries(context).joinToString(separator = ","))
//GL Extensions
val activityManager = context.getSystemService<ActivityManager>()
setProperty(
@@ -104,27 +104,25 @@ class NativeDeviceInfoProvider(val context: Context) {
setProperty("SimOperator", "38")
}
if (isHuawei && !isExport)
stripHuaweiProperties(properties)
if (isHuawei && !isExport) stripHuaweiProperties(properties)
return properties
}
private fun getFeatures(): List<String> {
private fun getFeatures(context: Context): List<String> {
return context
.packageManager
.systemAvailableFeatures
?.mapNotNull { it.name } ?: emptyList()
.mapNotNull { it.name }
}
private fun getLocales(): List<String> {
private fun getLocales(context: Context): List<String> {
return context
.assets
.locales
.mapNotNull { it.replace("-", "_") }
}
private fun getSharedLibraries(): List<String> {
private fun getSharedLibraries(context: Context): List<String> {
return context
.packageManager
.systemSharedLibraryNames

View File

@@ -23,7 +23,6 @@ import android.content.Context
import android.util.Log
import com.aurora.store.BuildConfig
import com.aurora.store.util.PathUtil
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
@@ -31,22 +30,26 @@ import java.io.IOException
import java.util.Properties
import java.util.jar.JarEntry
import java.util.jar.JarFile
import javax.inject.Inject
import javax.inject.Singleton
/**
* Provider class to work with device spoof configs imported by users & shipped by GPlayAPI library
*
* Do not use this class directly. Consider using [SpoofProvider] instead.
*/
@Singleton
class SpoofDeviceProvider @Inject constructor(@ApplicationContext val context: Context) {
open class SpoofDeviceProvider(private val context: Context) {
private val TAG = SpoofDeviceProvider::class.java.simpleName
private val SUFFIX = ".properties"
val availableDevice: List<Properties>
val availableDeviceProperties: MutableList<Properties>
get() {
val propertiesList: MutableList<Properties> = ArrayList()
propertiesList.add(0, NativeDeviceInfoProvider(context).getNativeDeviceProperties())
propertiesList.addAll(spoofDevicesFromApk)
propertiesList.addAll(spoofDevicesFromUser)
propertiesList.sortBy { it.getProperty("UserReadableName") }
return propertiesList
}

View File

@@ -20,7 +20,9 @@
package com.aurora.store.data.providers
import android.content.Context
import com.aurora.store.R
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_VENDING_VERSION
import com.google.gson.Gson
import dagger.hilt.android.qualifiers.ApplicationContext
import java.util.Locale
@@ -28,50 +30,65 @@ import java.util.Properties
import javax.inject.Inject
import javax.inject.Singleton
/**
* Provider class to work with device and locale spoofs
*/
@Singleton
class SpoofProvider @Inject constructor(
private val gson: Gson,
@ApplicationContext val context: Context,
private val gson: Gson
) {
) : SpoofDeviceProvider(context) {
companion object {
const val LOCALE_SPOOF_ENABLED = "LOCALE_SPOOF_ENABLED"
const val LOCALE_SPOOF_LANG = "LOCALE_SPOOF_LANG"
const val LOCALE_SPOOF_COUNTRY = "LOCALE_SPOOF_COUNTRY"
private const val LOCALE_SPOOF_ENABLED = "LOCALE_SPOOF_ENABLED"
private const val LOCALE_SPOOF_LANG = "LOCALE_SPOOF_LANG"
private const val LOCALE_SPOOF_COUNTRY = "LOCALE_SPOOF_COUNTRY"
const val DEVICE_SPOOF_ENABLED = "DEVICE_SPOOF_ENABLED"
const val DEVICE_SPOOF_PROPERTIES = "DEVICE_SPOOF_PROPERTIES"
private const val DEVICE_SPOOF_ENABLED = "DEVICE_SPOOF_ENABLED"
private const val DEVICE_SPOOF_PROPERTIES = "DEVICE_SPOOF_PROPERTIES"
}
fun isLocaleSpoofEnabled(): Boolean {
return Preferences.getBoolean(context, LOCALE_SPOOF_ENABLED)
val availableSpoofDeviceProperties get() = availableDeviceProperties
val availableSpoofLocales = Locale.getAvailableLocales().toMutableList().apply {
remove(Locale.getDefault())
sortBy { it.displayName }
}
fun isDeviceSpoofEnabled(): Boolean {
return Preferences.getBoolean(context, DEVICE_SPOOF_ENABLED)
}
val deviceProperties: Properties
get() {
val currentProperties = if (isDeviceSpoofEnabled) {
spoofDeviceProperties
} else {
NativeDeviceInfoProvider.getNativeDeviceProperties(context)
}
setVendingVersion(currentProperties)
return currentProperties
}
fun getSpoofLocale(): Locale {
return if (isLocaleSpoofEnabled()) {
Locale(
Preferences.getString(context, LOCALE_SPOOF_LANG),
Preferences.getString(context, LOCALE_SPOOF_COUNTRY)
)
val locale: Locale
get() = if (isLocaleSpoofEnabled) {
spoofLocale
} else {
Locale.getDefault()
}
}
fun getSpoofDeviceProperties(): Properties {
return if (isDeviceSpoofEnabled()) {
return gson.fromJson(
Preferences.getString(context, DEVICE_SPOOF_PROPERTIES),
Properties::class.java
)
} else {
Properties()
}
}
val isLocaleSpoofEnabled: Boolean
get() = Preferences.getBoolean(context, LOCALE_SPOOF_ENABLED)
val isDeviceSpoofEnabled: Boolean
get() = Preferences.getBoolean(context, DEVICE_SPOOF_ENABLED)
private val spoofLocale: Locale
get() = Locale(
Preferences.getString(context, LOCALE_SPOOF_LANG),
Preferences.getString(context, LOCALE_SPOOF_COUNTRY)
)
private val spoofDeviceProperties: Properties
get() = gson.fromJson(
Preferences.getString(context, DEVICE_SPOOF_PROPERTIES),
Properties::class.java
)
fun setSpoofLocale(locale: Locale) {
Preferences.putBoolean(context, LOCALE_SPOOF_ENABLED, true)
@@ -94,4 +111,16 @@ class SpoofProvider @Inject constructor(
Preferences.remove(context, DEVICE_SPOOF_ENABLED)
Preferences.remove(context, DEVICE_SPOOF_PROPERTIES)
}
private fun setVendingVersion(currentProperties: Properties) {
val vendingVersionIndex = Preferences.getInteger(context, PREFERENCE_VENDING_VERSION)
if (vendingVersionIndex > 0) {
val resources = context.resources
val versionCodes = resources.getStringArray(R.array.pref_vending_version_codes)
val versionStrings = resources.getStringArray(R.array.pref_vending_version)
currentProperties.setProperty("Vending.version", versionCodes[vendingVersionIndex])
currentProperties.setProperty("Vending.versionString", versionStrings[vendingVersionIndex])
}
}
}

View File

@@ -55,6 +55,7 @@ class DeviceView @JvmOverloads constructor(
@ModelProp
fun markChecked(isChecked: Boolean) {
binding.checkbox.isChecked = isChecked
binding.checkbox.isEnabled = !isChecked
}
@CallbackProp

View File

@@ -49,6 +49,7 @@ class LocaleView @JvmOverloads constructor(
@ModelProp
fun markChecked(isChecked: Boolean) {
binding.checkbox.isChecked = isChecked
binding.checkbox.isEnabled = !isChecked
}
@CallbackProp

View File

@@ -25,83 +25,85 @@ 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.extensions.toast
import com.aurora.store.R
import com.aurora.store.data.providers.NativeDeviceInfoProvider
import com.aurora.store.data.providers.SpoofProvider
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
import javax.inject.Inject
@AndroidEntryPoint
class DeviceSpoofFragment : BaseFragment<FragmentGenericRecyclerBinding>() {
private val viewModel: SpoofViewModel by viewModels()
@Inject
lateinit var spoofProvider: SpoofProvider
private lateinit var properties: Properties
companion object {
@JvmStatic
fun newInstance(): DeviceSpoofFragment {
return DeviceSpoofFragment().apply {
}
return DeviceSpoofFragment()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
properties = if (spoofProvider.isDeviceSpoofEnabled()) {
spoofProvider.getSpoofDeviceProperties()
} else {
NativeDeviceInfoProvider(requireContext()).getNativeDeviceProperties()
}
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.availableDevices.collect { updateController(it) }
}
}
viewModel.fetchAvailableDevices()
}
private fun updateController(locales: List<Properties>) {
private fun updateController(devices: List<Properties>) {
binding.recycler.withModels {
setFilterDuplicates(true)
locales
.sortedBy { it.getProperty("UserReadableName") }
.forEach {
add(
DeviceViewModel_()
.id(it.hashCode())
.markChecked(
properties.getProperty("UserReadableName") == it.getProperty(
"UserReadableName"
)
)
.checked { _, checked ->
if (checked) {
properties = it
saveSelection(it)
requestModelBuild()
}
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()
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()
findNavController().navigate(R.id.forceRestartDialog)
}
.properties(it)
)
}
}
.properties(it)
)
}
}
}
private fun saveSelection(properties: Properties) {
requireContext().toast(R.string.spoof_apply)
spoofProvider.setSpoofDeviceProperties(properties)
}
}

View File

@@ -20,68 +20,83 @@
package com.aurora.store.view.ui.spoof
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.aurora.extensions.toast
import com.aurora.store.R
import com.aurora.store.data.providers.SpoofProvider
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
import javax.inject.Inject
@AndroidEntryPoint
class LocaleSpoofFragment : BaseFragment<FragmentGenericRecyclerBinding>() {
private val TAG = LocaleSpoofFragment::class.java.simpleName
@Inject
lateinit var spoofProvider: SpoofProvider
private lateinit var locale: Locale
private val viewModel: SpoofViewModel by viewModels()
companion object {
@JvmStatic
fun newInstance(): LocaleSpoofFragment {
return LocaleSpoofFragment().apply {
}
return LocaleSpoofFragment()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
locale = if (spoofProvider.isLocaleSpoofEnabled()) {
spoofProvider.getSpoofLocale()
} else {
Locale.getDefault()
}
try {
updateController(fetchAvailableLocales())
} catch (exception: Exception) {
Log.e(TAG, "Could not get available locales", exception)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.availableLocales.collect {
updateController(it)
}
}
}
private fun updateController(locales: List<Locale>) {
binding.recycler.withModels {
setFilterDuplicates(true)
locales
.sortedBy { it.displayName }
.forEach {
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()
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(locale == it)
.markChecked(viewModel.spoofProvider.locale == it)
.checked { _, checked ->
if (checked) {
locale = it
saveSelection(it)
viewModel.onLocaleSelected(it)
requestModelBuild()
findNavController().navigate(R.id.forceRestartDialog)
}
}
.locale(it)
@@ -89,17 +104,4 @@ class LocaleSpoofFragment : BaseFragment<FragmentGenericRecyclerBinding>() {
}
}
}
private fun fetchAvailableLocales(): List<Locale> {
val locales = Locale.getAvailableLocales()
val localeList: MutableList<Locale> = ArrayList()
localeList.addAll(locales)
localeList.add(0, Locale.getDefault())
return localeList
}
private fun saveSelection(locale: Locale) {
requireContext().toast(R.string.spoof_apply)
spoofProvider.setSpoofLocale(locale)
}
}

View File

@@ -123,8 +123,7 @@ class SpoofFragment : BaseFragment<FragmentGenericWithPagerBinding>() {
private fun exportDeviceConfig(uri: Uri) {
try {
NativeDeviceInfoProvider(requireContext())
.getNativeDeviceProperties(true)
NativeDeviceInfoProvider.getNativeDeviceProperties(requireContext(), true)
.store(requireContext().contentResolver?.openOutputStream(uri), "DEVICE_CONFIG")
toast(R.string.toast_export_success)
} catch (exception: Exception) {

View File

@@ -1,27 +1,64 @@
package com.aurora.store.viewmodel.spoof
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.store.data.providers.SpoofDeviceProvider
import com.aurora.store.data.providers.NativeDeviceInfoProvider
import com.aurora.store.data.providers.SpoofProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.util.Locale
import java.util.Properties
import javax.inject.Inject
@HiltViewModel
class SpoofViewModel @Inject constructor(
private val spoofDeviceProvider: SpoofDeviceProvider
val spoofProvider: SpoofProvider,
@ApplicationContext private val context: Context
): ViewModel() {
private val _availableDevices: MutableStateFlow<List<Properties>> = MutableStateFlow(emptyList())
val defaultLocale: Locale = Locale.getDefault()
val defaultProperties = NativeDeviceInfoProvider.getNativeDeviceProperties(context)
private var currentDevice = spoofProvider.deviceProperties.getProperty("UserReadableName")
private var currentLocale = spoofProvider.locale
private val _availableLocales: MutableStateFlow<List<Locale>> = MutableStateFlow(
spoofProvider.availableSpoofLocales
)
val availableLocales = _availableLocales.asStateFlow()
private val _availableDevices: MutableStateFlow<List<Properties>> = MutableStateFlow(
spoofProvider.availableSpoofDeviceProperties
)
val availableDevices = _availableDevices.asStateFlow()
fun fetchAvailableDevices() {
viewModelScope.launch(Dispatchers.IO) {
_availableDevices.value = spoofDeviceProvider.availableDevice
fun isDeviceSelected(properties: Properties): Boolean {
return currentDevice == properties.getProperty("UserReadableName")
}
fun onDeviceSelected(properties: Properties) {
currentDevice = properties.getProperty("UserReadableName")
if (currentDevice == defaultProperties.getProperty("UserReadableName")) {
spoofProvider.removeSpoofDeviceProperties()
} else {
spoofProvider.setSpoofDeviceProperties(properties)
}
}
fun isLocaleSelected(locale: Locale): Boolean {
return currentLocale == locale
}
fun onLocaleSelected(locale: Locale) {
currentLocale = locale
if (currentLocale == defaultLocale) {
spoofProvider.removeSpoofLocale()
} else {
spoofProvider.setSpoofLocale(locale)
}
}
}

View File

@@ -467,4 +467,8 @@
<!-- UnarchivePackageReceiver -->
<string name="authentication_required_title">Authentication required</string>
<string name="authentication_required_unarchive">Please log in to your Google Play account to unarchive the app!</string>
<!-- SpoofFragment -->
<string name="default_spoof">Default</string>
<string name="available_spoof">Available</string>
</resources>