From 6e21b52bfe0c841514a7b13fd1467d0fd07f7c86 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sat, 22 Jan 2022 21:44:05 +0100 Subject: [PATCH] Profile Manager: Add configuration features --- .../org/microg/gms/profile/ProfileManager.kt | 149 ++++++++++++++---- .../microg/gms/utils/FileXmlResourceParser.kt | 127 +++++++++++++++ .../DeviceRegistrationPreferencesFragment.kt | 99 +++++++++++- .../xml/preferences_device_registration.xml | 8 +- .../src/main/res/xml/profile_bullhead_27.xml | 5 +- .../res/xml/profile_lineage_falcon_25.xml | 36 +++++ 6 files changed, 389 insertions(+), 35 deletions(-) create mode 100644 play-services-base-core/src/main/kotlin/org/microg/gms/utils/FileXmlResourceParser.kt create mode 100644 play-services-core/src/main/res/xml/profile_lineage_falcon_25.xml diff --git a/play-services-base-core/src/main/kotlin/org/microg/gms/profile/ProfileManager.kt b/play-services-base-core/src/main/kotlin/org/microg/gms/profile/ProfileManager.kt index 89251a9b..f14f14ad 100644 --- a/play-services-base-core/src/main/kotlin/org/microg/gms/profile/ProfileManager.kt +++ b/play-services-base-core/src/main/kotlin/org/microg/gms/profile/ProfileManager.kt @@ -7,10 +7,13 @@ package org.microg.gms.profile import android.annotation.SuppressLint import android.content.Context +import android.content.res.XmlResourceParser import android.util.Log import org.microg.gms.settings.SettingsContract import org.microg.gms.settings.SettingsContract.Profile +import org.microg.gms.utils.FileXmlResourceParser import org.xmlpull.v1.XmlPullParser +import java.io.File import java.util.* import kotlin.random.Random @@ -19,18 +22,67 @@ object ProfileManager { const val PROFILE_REAL = "real" const val PROFILE_AUTO = "auto" const val PROFILE_NATIVE = "native" + const val PROFILE_USER = "user" + const val PROFILE_SYSTEM = "system" - private var initialized = false + private var activeProfile: String? = null - private fun getProfileFromSettings(context: Context) = SettingsContract.getSettings(context, Profile.getContentUri(context), arrayOf(Profile.PROFILE)) { it.getString(0) } - private fun getAutoProfile(context: Context): String { + private fun getUserProfileFile(context: Context): File = File(context.filesDir, "device_profile.xml") + private fun getSystemProfileFile(context: Context): File = File("/system/etc/microg_device_profile.xml") + private fun getProfileResId(context: Context, profile: String) = context.resources.getIdentifier("${context.packageName}:xml/profile_$profile".toLowerCase(Locale.US), null, null) + + fun getConfiguredProfile(context: Context): String = SettingsContract.getSettings(context, Profile.getContentUri(context), arrayOf(Profile.PROFILE)) { it.getString(0) } ?: PROFILE_AUTO + + fun getAutoProfile(context: Context): String { + if (hasProfile(context, PROFILE_SYSTEM) && isAutoProfile(context, PROFILE_SYSTEM)) return PROFILE_SYSTEM val profile = "${android.os.Build.PRODUCT}_${android.os.Build.VERSION.SDK_INT}" - if (hasProfile(context, profile)) return profile + if (hasProfile(context, profile) && isAutoProfile(context, profile)) return profile return PROFILE_NATIVE } - private fun getProfileResId(context: Context, profile: String) = context.resources.getIdentifier("${context.packageName}:xml/profile_$profile".toLowerCase(Locale.US), null, null) - private fun hasProfile(context: Context, profile: String): Boolean = getProfileResId(context, profile) != 0 + fun hasProfile(context: Context, profile: String): Boolean = when (profile) { + PROFILE_AUTO -> hasProfile(context, getAutoProfile(context)) + PROFILE_NATIVE, PROFILE_REAL -> true + PROFILE_USER -> getUserProfileFile(context).exists() + PROFILE_SYSTEM -> getSystemProfileFile(context).exists() + else -> getProfileResId(context, profile) != 0 + } + + private fun getProfileXml(context: Context, profile: String): XmlResourceParser? = kotlin.runCatching { + when (profile) { + PROFILE_AUTO -> getProfileXml(context, getAutoProfile(context)) + PROFILE_NATIVE, PROFILE_REAL -> null + PROFILE_USER -> FileXmlResourceParser(getUserProfileFile(context)) + PROFILE_SYSTEM -> FileXmlResourceParser(getSystemProfileFile(context)) + else -> { + val profileResId = getProfileResId(context, profile) + if (profileResId == 0) return@runCatching null + context.resources.getXml(profileResId) + } + } + }.getOrNull() + + fun isAutoProfile(context: Context, profile: String): Boolean = kotlin.runCatching { + when (profile) { + PROFILE_AUTO -> false + PROFILE_REAL -> false + PROFILE_NATIVE -> true + else -> getProfileXml(context, profile)?.use { + var next = it.next() + while (next != XmlPullParser.END_DOCUMENT) { + when (next) { + XmlPullParser.START_TAG -> when (it.name) { + "profile" -> { + return@use it.getAttributeBooleanValue(null, "auto", false) + } + } + } + next = it.next() + } + } == true + } + }.getOrDefault(false) + private fun getProfileData(context: Context, profile: String, realData: Map): Map { try { if (profile in listOf(PROFILE_REAL, PROFILE_NATIVE)) return realData @@ -38,7 +90,7 @@ object ProfileManager { if (profileResId == 0) return realData val resultData = mutableMapOf() resultData.putAll(realData) - context.resources.getXml(profileResId).use { + getProfileXml(context, profile)?.use { var next = it.next() while (next != XmlPullParser.END_DOCUMENT) { when (next) { @@ -61,7 +113,7 @@ object ProfileManager { } } - private fun getActiveProfile(context: Context) = getProfileFromSettings(context).let { if (it != PROFILE_AUTO) it else getAutoProfile(context) } + private fun getProfile(context: Context) = getConfiguredProfile(context).let { if (it != PROFILE_AUTO) it else getAutoProfile(context) } private fun getSerialFromSettings(context: Context): String? = SettingsContract.getSettings(context, Profile.getContentUri(context), arrayOf(Profile.SERIAL)) { it.getString(0) } private fun saveSerial(context: Context, serial: String) = SettingsContract.setSettings(context, Profile.getContentUri(context)) { put(Profile.SERIAL, serial) } @@ -99,18 +151,15 @@ object ProfileManager { // From profile try { - val profileResId = getProfileResId(context, profile) - if (profileResId != 0) { - context.resources.getXml(profileResId).use { - var next = it.next() - while (next != XmlPullParser.END_DOCUMENT) { - when (next) { - XmlPullParser.START_TAG -> when (it.name) { - "serial" -> return it.getAttributeValue(null, "template") - } + getProfileXml(context, profile)?.use { + var next = it.next() + while (next != XmlPullParser.END_DOCUMENT) { + when (next) { + XmlPullParser.START_TAG -> when (it.name) { + "serial" -> return it.getAttributeValue(null, "template") } - next = it.next() } + next = it.next() } } } catch (e: Exception) { @@ -118,18 +167,18 @@ object ProfileManager { } // Fallback - return "008741A0B2C4D6E8" + return randomSerial("008741A0B2C4D6E8") } @SuppressLint("MissingPermission") - private fun getEffectiveProfileSerial(context: Context, profile: String): String { - getSerialFromSettings(context)?.let { return it } + fun getSerial(context: Context, profile: String = getProfile(context), local: Boolean = false): String { + if (!local) getSerialFromSettings(context)?.let { return it } val serialTemplate = getProfileSerialTemplate(context, profile) val serial = when { profile == PROFILE_REAL && serialTemplate != android.os.Build.UNKNOWN -> serialTemplate else -> randomSerial(serialTemplate) } - saveSerial(context, serial) + if (!local) saveSerial(context, serial) return serial } @@ -210,29 +259,67 @@ object ProfileManager { } } - private fun applyProfile(context: Context, profile: String) { - val profileData = getProfileData(context, profile, getRealData()) ?: getRealData() + private fun applyProfile(context: Context, profile: String, serial: String = getSerial(context, profile)) { + val profileData = getProfileData(context, profile, getRealData()) + if (Log.isLoggable(TAG, Log.VERBOSE)) { + for ((key, value) in profileData) { + Log.v(TAG, "") + } + } applyProfileData(profileData) - Build.SERIAL = getEffectiveProfileSerial(context, profile) + Build.SERIAL = serial Log.d(TAG, "Using Serial ${Build.SERIAL}") + activeProfile = profile + } + + fun getProfileName(context: Context, profile: String): String? = getProfileName { getProfileXml(context, profile) } + + private fun getProfileName(parserCreator: () -> XmlResourceParser?): String? = parserCreator()?.use { + var next = it.next() + while (next != XmlPullParser.END_DOCUMENT) { + when (next) { + XmlPullParser.START_TAG -> when (it.name) { + "profile" -> { + return@use it.getAttributeValue(null, "name") + } + } + } + next = it.next() + } + null } fun setProfile(context: Context, profile: String?) { + val changed = getProfile(context) != profile + val newProfile = profile ?: PROFILE_AUTO + val newSerial = if (changed) getSerial(context, newProfile, true) else getSerial(context) SettingsContract.setSettings(context, Profile.getContentUri(context)) { - put(Profile.PROFILE, profile) - put(Profile.SERIAL, null as String?) + put(Profile.PROFILE, newProfile) + if (changed) put(Profile.SERIAL, newSerial) + } + if (changed && activeProfile != null) applyProfile(context, newProfile, newSerial) + } + + fun importUserProfile(context: Context, file: File): Boolean { + val profileName = getProfileName { FileXmlResourceParser(file) } ?: return false + try { + Log.d(TAG, "Importing user profile '$profileName'") + file.copyTo(getUserProfileFile(context)) + if (activeProfile == PROFILE_USER) applyProfile(context, PROFILE_USER) + return true + } catch (e: Exception) { + Log.w(TAG, e) + return false } - applyProfile(context, profile ?: PROFILE_AUTO) } @JvmStatic fun ensureInitialized(context: Context) { synchronized(this) { - if (initialized) return try { - val profile = getActiveProfile(context) + val profile = getProfile(context) + if (activeProfile == profile) return applyProfile(context, profile) - initialized = true } catch (e: Exception) { Log.w(TAG, e) } diff --git a/play-services-base-core/src/main/kotlin/org/microg/gms/utils/FileXmlResourceParser.kt b/play-services-base-core/src/main/kotlin/org/microg/gms/utils/FileXmlResourceParser.kt new file mode 100644 index 00000000..689d0f46 --- /dev/null +++ b/play-services-base-core/src/main/kotlin/org/microg/gms/utils/FileXmlResourceParser.kt @@ -0,0 +1,127 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.utils + +import android.content.res.XmlResourceParser +import android.util.Xml +import org.xmlpull.v1.XmlPullParser +import java.io.Closeable +import java.io.File +import java.io.FileReader +import java.io.Reader + +class FileXmlResourceParser(private val reader: Reader, private val parser: XmlPullParser = Xml.newPullParser()) : + XmlResourceParser, + XmlPullParser by parser, + Closeable by reader { + constructor(file: File) : this(FileReader(file)) + + init { + parser.setInput(reader) + } + + override fun getAttributeNameResource(index: Int): Int { + return 0 + } + + override fun getAttributeListValue( + namespace: String?, attribute: String?, + options: Array?, defaultValue: Int + ): Int { + val s = getAttributeValue(namespace, attribute) + return s?.toInt() ?: defaultValue + } + + override fun getAttributeBooleanValue( + namespace: String?, attribute: String?, + defaultValue: Boolean + ): Boolean { + + val s = getAttributeValue(namespace, attribute) + return s?.toBooleanStrictOrNull() ?: defaultValue + } + + override fun getAttributeResourceValue( + namespace: String?, attribute: String?, + defaultValue: Int + ): Int { + val s = getAttributeValue(namespace, attribute) + return s?.toInt() ?: defaultValue + } + + override fun getAttributeIntValue( + namespace: String?, attribute: String?, + defaultValue: Int + ): Int { + val s = getAttributeValue(namespace, attribute) + return s?.toInt() ?: defaultValue + } + + override fun getAttributeUnsignedIntValue( + namespace: String?, attribute: String?, + defaultValue: Int + ): Int { + val s = getAttributeValue(namespace, attribute) + return s?.toInt() ?: defaultValue + } + + override fun getAttributeFloatValue( + namespace: String?, attribute: String?, + defaultValue: Float + ): Float { + val s = getAttributeValue(namespace, attribute) + return s?.toFloat() ?: defaultValue + } + + override fun getAttributeListValue( + index: Int, + options: Array?, defaultValue: Int + ): Int { + val s = getAttributeValue(index) + return s?.toInt() ?: defaultValue + } + + override fun getAttributeBooleanValue(index: Int, defaultValue: Boolean): Boolean { + val s = getAttributeValue(index) + return s?.toBooleanStrictOrNull() ?: defaultValue + } + + override fun getAttributeResourceValue(index: Int, defaultValue: Int): Int { + val s = getAttributeValue(index) + return s?.toInt() ?: defaultValue + } + + override fun getAttributeIntValue(index: Int, defaultValue: Int): Int { + val s = getAttributeValue(index) + return s?.toInt() ?: defaultValue + } + + override fun getAttributeUnsignedIntValue(index: Int, defaultValue: Int): Int { + val s = getAttributeValue(index) + return s?.toInt() ?: defaultValue + } + + override fun getAttributeFloatValue(index: Int, defaultValue: Float): Float { + val s = getAttributeValue(index) + return s?.toFloat() ?: defaultValue + } + + override fun getIdAttribute(): String? { + return getAttributeValue(null, "id") + } + + override fun getClassAttribute(): String? { + return getAttributeValue(null, "class") + } + + override fun getIdAttributeResourceValue(defaultValue: Int): Int { + return getAttributeResourceValue(null, "id", defaultValue) + } + + override fun getStyleAttribute(): Int { + return getAttributeResourceValue(null, "style", 0) + } +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/DeviceRegistrationPreferencesFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/DeviceRegistrationPreferencesFragment.kt index 6d27eed6..ced09539 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/DeviceRegistrationPreferencesFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/DeviceRegistrationPreferencesFragment.kt @@ -5,31 +5,122 @@ package org.microg.gms.ui +import android.net.Uri import android.os.Bundle import android.os.Handler import android.text.format.DateUtils +import android.util.Log +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.lifecycle.lifecycleScope +import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat import com.google.android.gms.R import org.microg.gms.checkin.getCheckinServiceInfo +import org.microg.gms.profile.ProfileManager +import org.microg.gms.profile.ProfileManager.PROFILE_AUTO +import org.microg.gms.profile.ProfileManager.PROFILE_NATIVE +import org.microg.gms.profile.ProfileManager.PROFILE_REAL +import org.microg.gms.profile.ProfileManager.PROFILE_SYSTEM +import org.microg.gms.profile.ProfileManager.PROFILE_USER +import java.io.File +import java.io.FileOutputStream class DeviceRegistrationPreferencesFragment : PreferenceFragmentCompat() { + private lateinit var deviceProfile: ListPreference + private lateinit var importProfile: Preference + private lateinit var serial: Preference private lateinit var statusCategory: PreferenceCategory private lateinit var status: Preference private lateinit var androidId: Preference private val handler = Handler() private val updateRunnable = Runnable { updateStatus() } + private lateinit var profileFileImport: ActivityResultLauncher + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + profileFileImport = registerForActivityResult(ActivityResultContracts.GetContent(), this::onFileSelected) + } + + private fun onFileSelected(uri: Uri?) { + if (uri == null) return + try { + val context = requireContext() + val file = File.createTempFile("profile_", ".xml", context.cacheDir) + context.contentResolver.openInputStream(uri)?.use { inputStream -> + FileOutputStream(file).use { inputStream.copyTo(it) } + } + val success = ProfileManager.importUserProfile(context, file) + file.delete() + if (success && ProfileManager.isAutoProfile(context, PROFILE_USER)) { + ProfileManager.setProfile(context, PROFILE_USER) + } + updateStatus() + } catch (e: Exception) { + Log.w(TAG, e) + } + } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.preferences_device_registration) } override fun onBindPreferences() { + deviceProfile = preferenceScreen.findPreference("pref_device_profile") ?: deviceProfile + importProfile = preferenceScreen.findPreference("pref_device_profile_import") ?: importProfile + serial = preferenceScreen.findPreference("pref_device_serial") ?: serial statusCategory = preferenceScreen.findPreference("prefcat_device_registration_status") ?: statusCategory status = preferenceScreen.findPreference("pref_device_registration_status") ?: status androidId = preferenceScreen.findPreference("pref_device_registration_android_id") ?: androidId + + deviceProfile.setOnPreferenceChangeListener { _, newValue -> + ProfileManager.setProfile(requireContext(), newValue as String? ?: PROFILE_AUTO) + updateStatus() + true + } + importProfile.setOnPreferenceClickListener { + profileFileImport.launch("text/xml") + true + } + } + + private fun configureProfilePreference() { + val context = requireContext() + val configuredProfile = ProfileManager.getConfiguredProfile(context) + val autoProfile = ProfileManager.getAutoProfile(context) + val autoProfileName = when (autoProfile) { + PROFILE_NATIVE -> "Native" + PROFILE_REAL -> "Real" + else -> ProfileManager.getProfileName(context, autoProfile) + } + val profiles = + mutableListOf(PROFILE_AUTO, PROFILE_NATIVE, PROFILE_REAL) + val profileNames = mutableListOf("Automatic: $autoProfileName", "Native", "Real") + if (ProfileManager.hasProfile(context, PROFILE_SYSTEM)) { + profiles.add(PROFILE_SYSTEM) + profileNames.add("System: ${ProfileManager.getProfileName(context, PROFILE_SYSTEM)}") + } + if (ProfileManager.hasProfile(context, PROFILE_USER)) { + profiles.add(PROFILE_USER) + profileNames.add("Custom: ${ProfileManager.getProfileName(context, PROFILE_USER)}") + } + for (profile in R.xml::class.java.declaredFields.map { it.name } + .filter { it.startsWith("profile_") } + .map { it.substring(8) } + .sorted()) { + val profileName = ProfileManager.getProfileName(context, profile) + if (profileName != null) { + profiles.add(profile) + profileNames.add(profileName) + } + } + deviceProfile.entryValues = profiles.toTypedArray() + deviceProfile.entries = profileNames.toTypedArray() + deviceProfile.value = configuredProfile + deviceProfile.summary = + profiles.indexOf(configuredProfile).takeIf { it >= 0 }?.let { profileNames[it] } ?: "Unknown" } override fun onResume() { @@ -43,13 +134,19 @@ class DeviceRegistrationPreferencesFragment : PreferenceFragmentCompat() { } private fun updateStatus() { + handler.removeCallbacks(updateRunnable) handler.postDelayed(updateRunnable, UPDATE_INTERVAL) val appContext = requireContext().applicationContext lifecycleScope.launchWhenResumed { + configureProfilePreference() + serial.summary = ProfileManager.getSerial(appContext) val serviceInfo = getCheckinServiceInfo(appContext) statusCategory.isVisible = serviceInfo.configuration.enabled if (serviceInfo.lastCheckin > 0) { - status.summary = getString(R.string.checkin_last_registration, DateUtils.getRelativeTimeSpanString(serviceInfo.lastCheckin, System.currentTimeMillis(), 0)) + status.summary = getString( + R.string.checkin_last_registration, + DateUtils.getRelativeTimeSpanString(serviceInfo.lastCheckin, System.currentTimeMillis(), 0) + ) androidId.isVisible = true androidId.summary = serviceInfo.androidId.toString(16) } else { diff --git a/play-services-core/src/main/res/xml/preferences_device_registration.xml b/play-services-core/src/main/res/xml/preferences_device_registration.xml index 6c82e63c..4ad1fae0 100644 --- a/play-services-core/src/main/res/xml/preferences_device_registration.xml +++ b/play-services-core/src/main/res/xml/preferences_device_registration.xml @@ -11,12 +11,18 @@ android:title="Device profile"> + android:title="Import custom profile" /> + - + @@ -27,6 +27,7 @@ + diff --git a/play-services-core/src/main/res/xml/profile_lineage_falcon_25.xml b/play-services-core/src/main/res/xml/profile_lineage_falcon_25.xml new file mode 100644 index 00000000..57beb5d4 --- /dev/null +++ b/play-services-core/src/main/res/xml/profile_lineage_falcon_25.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +