Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit 6e21b52b authored by Marvin W.'s avatar Marvin W. 🐿️
Browse files

Profile Manager: Add configuration features

parent 3bbae67f
Loading
Loading
Loading
Loading
+118 −31
Original line number Diff line number Diff line
@@ -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,9 +151,7 @@ object ProfileManager {

        // From profile
        try {
            val profileResId = getProfileResId(context, profile)
            if (profileResId != 0) {
                context.resources.getXml(profileResId).use {
            getProfileXml(context, profile)?.use {
                var next = it.next()
                while (next != XmlPullParser.END_DOCUMENT) {
                    when (next) {
@@ -112,24 +162,23 @@ object ProfileManager {
                    next = it.next()
                }
            }
            }
        } catch (e: Exception) {
            Log.w(TAG, e)
        }

        // 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)
            }
+127 −0
Original line number Diff line number Diff line
/*
 * 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)
    }
}
+98 −1
Original line number Diff line number Diff line
@@ -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 {
+7 −1
Original line number Diff line number Diff line
@@ -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"
+3 −2
Original line number Diff line number Diff line
<?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" />
Loading