Profile Manager: Add configuration features

This commit is contained in:
Marvin W 2022-01-22 21:44:05 +01:00
parent 3bbae67fda
commit 6e21b52bfe
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
6 changed files with 389 additions and 35 deletions

View File

@ -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<String, String>): Map<String, String> {
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<String, String>()
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, "<data key=\"$key\" value=\"$value\" />")
}
}
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)
}

View File

@ -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<String?>?, 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<String?>?, 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)
}
}

View File

@ -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<String>
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 {

View File

@ -11,12 +11,18 @@
android:title="Device profile">
<ListPreference
android:key="pref_device_profile"
android:persistent="false"
android:title="Select profile"
tools:summary="Automatic (Google Pixel 3, Android 11)" />
<Preference
android:key="pref_device_profile_import"
android:summary="Import device profile from file"
android:title="Import profile" />
android:title="Import custom profile" />
<Preference
android:enabled="false"
android:key="pref_device_serial"
android:title="Serial"
tools:summary="123456" />
</PreferenceCategory>
<PreferenceCategory
android:key="prefcat_device_registration_status"

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2021, microG Project Team
~ SPDX-FileCopyrightText: 2021 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<profile name="Google Nexus 5X (Android 8.1.0)" product="bullhead" sdk="27" id="bullhead_27">
<profile name="Google Nexus 5X (Android 8.1.0)" product="bullhead" sdk="27" id="bullhead_27" auto="true">
<!-- Data from OPM3.171019.016, Mar 2018 -->
<data key="Build.BOARD" value="bullhead" />
<data key="Build.BOOTLOADER" value="BHZ31b" />
@ -27,6 +27,7 @@
<data key="Build.VERSION.CODENAME" value="REL" />
<data key="Build.VERSION.INCREMENTAL" value="6d95f5a143" />
<data key="Build.VERSION.RELEASE" value="8.1.0" />
<data key="Build.VERSION.SECURITY_PATCH" value="2021-10-05" />
<data key="Build.VERSION.SDK" value="27" />
<data key="Build.VERSION.SDK_INT" value="27" />
<data key="Build.SUPPORTED_ABIS" value="arm64-v8a,armeabi-v7a,armeabi" />

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<profile name="Motorola Moto G (LineageOS 14.1)" product="lineage_falcon" sdk="25" id="lineage_falcon_25" auto="true">
<!-- Data from LineageOS 14.1-20190207-NIGHTLY-falcon, Feb 2019 -->
<data key="Build.BOARD" value="MSM8226" />
<data key="Build.BOOTLOADER" value="0x4118" />
<data key="Build.BRAND" value="motorola" />
<data key="Build.CPU_ABI" value="armeabi-v7a" />
<data key="Build.CPU_ABI2" value="armeabi" />
<data key="Build.DEVICE" value="falcon_umts" />
<data key="Build.DISPLAY" value="lineage_falcon-userdebug 7.1.2 NJH47F f4535aec29" />
<data key="Build.FINGERPRINT" value="motorola/falcon_retuglb/falcon_umts:5.1/LPB23.13-58/58:user/release-keys" />
<data key="Build.HARDWARE" value="qcom" />
<data key="Build.HOST" value="lineage-runner" />
<data key="Build.ID" value="NJH47F" />
<data key="Build.MANUFACTURER" value="motorola" />
<data key="Build.MODEL" value="Moto G" />
<data key="Build.PRODUCT" value="lineage_falcon" />
<data key="Build.RADIO" value="unknown" />
<data key="Build.TAGS" value="release-keys" />
<data key="Build.TIME" value="1549548437000" />
<data key="Build.TYPE" value="user" />
<data key="Build.USER" value="gitlab-runner" />
<data key="Build.VERSION.CODENAME" value="REL" />
<data key="Build.VERSION.INCREMENTAL" value="f4535aec29" />
<data key="Build.VERSION.RELEASE" value="7.1.2" />
<data key="Build.VERSION.SECURITY_PATCH" value="2019-01-05" />
<data key="Build.VERSION.SDK" value="25" />
<data key="Build.VERSION.SDK_INT" value="25" />
<data key="Build.SUPPORTED_ABIS" value="armeabi-v7a,armeabi" />
<serial template="TA9290XXXX" />
</profile>