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

Commit f411ed37 authored by Jacky Wang's avatar Jacky Wang
Browse files

[Catalyst] Support flags to get preference graph

Bug: 373895596
Flag: EXEMPT library
Test: manual
Change-Id: Ie4f3c7b1f7543c444cbc66877014e3346aa7a3e4
parent 2fdf339d
Loading
Loading
Loading
Loading
+7 −3
Original line number Diff line number Diff line
@@ -42,7 +42,7 @@ abstract class GetPreferenceGraphApiHandler(
        callingUid: Int,
        request: GetPreferenceGraphRequest,
    ): PreferenceGraphProto {
        val builder = PreferenceGraphBuilder.of(application, request)
        val builder = PreferenceGraphBuilder.of(application, myUid, callingUid, request)
        if (request.screenKeys.isEmpty()) {
            for (key in PreferenceScreenRegistry.preferenceScreens.keys) {
                builder.addPreferenceScreenFromRegistry(key)
@@ -68,16 +68,18 @@ constructor(
    val screenKeys: Set<String> = setOf(),
    val visitedScreens: Set<String> = setOf(),
    val locale: Locale? = null,
    val includeValue: Boolean = true,
    val flags: Int = PreferenceGetterFlags.ALL,
    val includeValue: Boolean = true, // TODO: clean up
    val includeValueDescriptor: Boolean = true,
)

object GetPreferenceGraphRequestCodec : MessageCodec<GetPreferenceGraphRequest> {
    override fun encode(data: GetPreferenceGraphRequest): Bundle =
        Bundle(3).apply {
        Bundle(4).apply {
            putStringArray(KEY_SCREEN_KEYS, data.screenKeys.toTypedArray())
            putStringArray(KEY_VISITED_KEYS, data.visitedScreens.toTypedArray())
            putString(KEY_LOCALE, data.locale?.toLanguageTag())
            putInt(KEY_FLAGS, data.flags)
        }

    override fun decode(data: Bundle): GetPreferenceGraphRequest {
@@ -88,12 +90,14 @@ object GetPreferenceGraphRequestCodec : MessageCodec<GetPreferenceGraphRequest>
            screenKeys.toSet(),
            visitedScreens.toSet(),
            data.getString(KEY_LOCALE).toLocale(),
            data.getInt(KEY_FLAGS),
        )
    }

    private const val KEY_SCREEN_KEYS = "k"
    private const val KEY_VISITED_KEYS = "v"
    private const val KEY_LOCALE = "l"
    private const val KEY_FLAGS = "f"
}

object PreferenceGraphProtoCodec : MessageCodec<PreferenceGraphProto> {
+31 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settingslib.graph

/** Flags for preference getter operation. */
object PreferenceGetterFlags {
    const val VALUE = 1 shl 0
    const val VALUE_DESCRIPTOR = 1 shl 1
    const val METADATA = 1 shl 2
    const val ALL = (1 shl 3) - 1

    fun Int.includeValue() = (this and VALUE) != 0

    fun Int.includeValueDescriptor() = (this and VALUE_DESCRIPTOR) != 0

    fun Int.includeMetadata() = (this and METADATA) != 0
}
+138 −99
Original line number Diff line number Diff line
@@ -31,6 +31,9 @@ import androidx.preference.Preference
import androidx.preference.PreferenceGroup
import androidx.preference.PreferenceScreen
import androidx.preference.TwoStatePreference
import com.android.settingslib.graph.PreferenceGetterFlags.includeMetadata
import com.android.settingslib.graph.PreferenceGetterFlags.includeValue
import com.android.settingslib.graph.PreferenceGetterFlags.includeValueDescriptor
import com.android.settingslib.graph.proto.PreferenceGraphProto
import com.android.settingslib.graph.proto.PreferenceGroupProto
import com.android.settingslib.graph.proto.PreferenceProto
@@ -41,7 +44,6 @@ import com.android.settingslib.metadata.BooleanValue
import com.android.settingslib.metadata.PersistentPreference
import com.android.settingslib.metadata.PreferenceAvailabilityProvider
import com.android.settingslib.metadata.PreferenceHierarchy
import com.android.settingslib.metadata.PreferenceHierarchyNode
import com.android.settingslib.metadata.PreferenceMetadata
import com.android.settingslib.metadata.PreferenceRestrictionProvider
import com.android.settingslib.metadata.PreferenceScreenBindingKeyProvider
@@ -50,6 +52,7 @@ import com.android.settingslib.metadata.PreferenceScreenRegistry
import com.android.settingslib.metadata.PreferenceSummaryProvider
import com.android.settingslib.metadata.PreferenceTitleProvider
import com.android.settingslib.metadata.RangeValue
import com.android.settingslib.metadata.ReadWritePermit
import com.android.settingslib.preference.PreferenceScreenFactory
import com.android.settingslib.preference.PreferenceScreenProvider
import java.util.Locale
@@ -60,14 +63,17 @@ private const val TAG = "PreferenceGraphBuilder"

/** Builder of preference graph. */
class PreferenceGraphBuilder
private constructor(private val context: Context, private val request: GetPreferenceGraphRequest) {
private constructor(
    private val context: Context,
    private val myUid: Int,
    private val callingUid: Int,
    private val request: GetPreferenceGraphRequest,
) {
    private val preferenceScreenFactory by lazy {
        PreferenceScreenFactory(context.ofLocale(request.locale))
    }
    private val builder by lazy { PreferenceGraphProto.newBuilder() }
    private val visitedScreens = mutableSetOf<String>().apply { addAll(request.visitedScreens) }
    private val includeValue = request.includeValue
    private val includeValueDescriptor = request.includeValueDescriptor

    private suspend fun init() {
        for (key in request.screenKeys) {
@@ -229,7 +235,7 @@ private constructor(private val context: Context, private val request: GetPrefer
        enabled = isEnabled
        available = isVisible
        persistent = isPersistent
        if (includeValue && isPersistent && this@toProto is TwoStatePreference) {
        if (request.flags.includeValue() && isPersistent && this@toProto is TwoStatePreference) {
            value = preferenceValueProto { booleanValue = this@toProto.isChecked }
        }
        this@toProto.fragment.toActionTarget(preferenceExtras)?.let {
@@ -243,14 +249,14 @@ private constructor(private val context: Context, private val request: GetPrefer
        screenMetadata: PreferenceScreenMetadata,
        isRoot: Boolean,
    ): PreferenceGroupProto = preferenceGroupProto {
        preference = toProto(screenMetadata, this@toProto, isRoot)
        preference = toProto(screenMetadata, this@toProto.metadata, isRoot)
        forEachAsync {
            addPreferences(
                preferenceOrGroupProto {
                    if (it is PreferenceHierarchy) {
                        group = it.toProto(screenMetadata, false)
                    } else {
                        preference = toProto(screenMetadata, it, false)
                        preference = toProto(screenMetadata, it.metadata, false)
                    }
                }
            )
@@ -259,12 +265,103 @@ private constructor(private val context: Context, private val request: GetPrefer

    private suspend fun toProto(
        screenMetadata: PreferenceScreenMetadata,
        node: PreferenceHierarchyNode,
        metadata: PreferenceMetadata,
        isRoot: Boolean,
    ) =
        metadata.toProto(context, myUid, callingUid, screenMetadata, isRoot, request.flags).also {
            if (metadata is PreferenceScreenMetadata) {
                @Suppress("CheckReturnValue") addPreferenceScreenMetadata(metadata)
            }
            metadata.intent(context)?.resolveActivity(context.packageManager)?.let {
                if (it.packageName == context.packageName) {
                    add(it.className)
                }
            }
        }

    private suspend fun String?.toActionTarget(extras: Bundle?): ActionTarget? {
        if (this.isNullOrEmpty()) return null
        try {
            val fragmentClass = context.classLoader.loadClass(this)
            if (Fragment::class.java.isAssignableFrom(fragmentClass)) {
                @Suppress("UNCHECKED_CAST")
                return (fragmentClass as Class<out Fragment>).toActionTarget(extras)
            }
        } catch (e: Exception) {
            Log.e(TAG, "Cannot loadClass $this", e)
        }
        return null
    }

    private suspend fun Class<out Fragment>.toActionTarget(extras: Bundle?): ActionTarget? {
        if (
            !PreferenceScreenProvider::class.java.isAssignableFrom(this) &&
                !PreferenceScreenBindingKeyProvider::class.java.isAssignableFrom(this)
        ) {
            return null
        }
        val fragment =
            withContext(Dispatchers.Main) {
                return@withContext try {
                    newInstance().apply { arguments = extras }
                } catch (e: Exception) {
                    Log.e(TAG, "Fail to instantiate fragment ${this@toActionTarget}", e)
                    null
                }
            }
        if (fragment is PreferenceScreenBindingKeyProvider) {
            val screenKey = fragment.getPreferenceScreenBindingKey(context)
            if (screenKey != null && addPreferenceScreenFromRegistry(screenKey)) {
                return actionTargetProto { key = screenKey }
            }
        }
        if (fragment is PreferenceScreenProvider) {
            try {
                val screen = fragment.createPreferenceScreen(preferenceScreenFactory)
                val screenKey = screen?.key
                if (!screenKey.isNullOrEmpty()) {
                    @Suppress("CheckReturnValue")
                    addPreferenceScreen(screenKey) { screen.toProto(null) }
                    return actionTargetProto { key = screenKey }
                }
            } catch (e: Exception) {
                Log.e(TAG, "Fail to createPreferenceScreen for $fragment", e)
            }
        }
        return null
    }

    private suspend fun Intent.toActionTarget() =
        toActionTarget(context).also {
            resolveActivity(context.packageManager)?.let {
                if (it.packageName == context.packageName) {
                    add(it.className)
                }
            }
        }

    companion object {
        suspend fun of(
            context: Context,
            myUid: Int,
            callingUid: Int,
            request: GetPreferenceGraphRequest,
        ) = PreferenceGraphBuilder(context, myUid, callingUid, request).also { it.init() }
    }
}

fun PreferenceMetadata.toProto(
    context: Context,
    myUid: Int,
    callingUid: Int,
    screenMetadata: PreferenceScreenMetadata,
    isRoot: Boolean,
    flags: Int,
) = preferenceProto {
        val metadata = node.metadata
    val metadata = this@toProto
    key = metadata.key
        metadata.getTitleTextProto(isRoot)?.let { title = it }
    if (flags.includeMetadata()) {
        metadata.getTitleTextProto(context, isRoot)?.let { title = it }
        if (metadata.summary != 0) {
            summary = textProto { resourceId = metadata.summary }
        } else {
@@ -285,9 +382,19 @@ private constructor(private val context: Context, private val request: GetPrefer
        if (metadata is PreferenceRestrictionProvider) {
            restricted = metadata.isRestricted(context)
        }
        metadata.intent(context)?.let { actionTarget = it.toActionTarget(context) }
        screenMetadata.getLaunchIntent(context, metadata)?.let { launchIntent = it.toProto() }
    }
    persistent = metadata.isPersistent(context)
    if (persistent) {
            if (includeValue && metadata is PersistentPreference<*>) {
        if (
            flags.includeValue() &&
                enabled &&
                (!hasAvailable() || available) &&
                (!hasRestricted() || !restricted) &&
                metadata is PersistentPreference<*> &&
                metadata.getReadPermit(context, myUid, callingUid) == ReadWritePermit.ALLOW
        ) {
            value = preferenceValueProto {
                when (metadata) {
                    is BooleanValue ->
@@ -305,7 +412,7 @@ private constructor(private val context: Context, private val request: GetPrefer
                }
            }
        }
            if (includeValueDescriptor) {
        if (flags.includeValueDescriptor()) {
            valueDescriptor = preferenceValueDescriptorProto {
                when (metadata) {
                    is BooleanValue -> booleanType = true
@@ -319,14 +426,9 @@ private constructor(private val context: Context, private val request: GetPrefer
            }
        }
    }
        if (metadata is PreferenceScreenMetadata) {
            @Suppress("CheckReturnValue") addPreferenceScreenMetadata(metadata)
        }
        metadata.intent(context)?.let { actionTarget = it.toActionTarget() }
        screenMetadata.getLaunchIntent(context, metadata)?.let { launchIntent = it.toProto() }
}

    private fun PreferenceMetadata.getTitleTextProto(isRoot: Boolean): TextProto? {
private fun PreferenceMetadata.getTitleTextProto(context: Context, isRoot: Boolean): TextProto? {
    if (isRoot && this is PreferenceScreenMetadata) {
        val titleRes = screenTitle
        if (titleRes != 0) {
@@ -347,76 +449,13 @@ private constructor(private val context: Context, private val request: GetPrefer
    }
}

    private suspend fun String?.toActionTarget(extras: Bundle?): ActionTarget? {
        if (this.isNullOrEmpty()) return null
        try {
            val fragmentClass = context.classLoader.loadClass(this)
            if (Fragment::class.java.isAssignableFrom(fragmentClass)) {
                @Suppress("UNCHECKED_CAST")
                return (fragmentClass as Class<out Fragment>).toActionTarget(extras)
            }
        } catch (e: Exception) {
            Log.e(TAG, "Cannot loadClass $this", e)
        }
        return null
    }

    private suspend fun Class<out Fragment>.toActionTarget(extras: Bundle?): ActionTarget? {
        if (
            !PreferenceScreenProvider::class.java.isAssignableFrom(this) &&
                !PreferenceScreenBindingKeyProvider::class.java.isAssignableFrom(this)
        ) {
            return null
        }
        val fragment =
            withContext(Dispatchers.Main) {
                return@withContext try {
                    newInstance().apply { arguments = extras }
                } catch (e: Exception) {
                    Log.e(TAG, "Fail to instantiate fragment ${this@toActionTarget}", e)
                    null
                }
            }
        if (fragment is PreferenceScreenBindingKeyProvider) {
            val screenKey = fragment.getPreferenceScreenBindingKey(context)
            if (screenKey != null && addPreferenceScreenFromRegistry(screenKey)) {
                return actionTargetProto { key = screenKey }
            }
        }
        if (fragment is PreferenceScreenProvider) {
            try {
                val screen = fragment.createPreferenceScreen(preferenceScreenFactory)
                val screenKey = screen?.key
                if (!screenKey.isNullOrEmpty()) {
                    @Suppress("CheckReturnValue")
                    addPreferenceScreen(screenKey) { screen.toProto(null) }
                    return actionTargetProto { key = screenKey }
                }
            } catch (e: Exception) {
                Log.e(TAG, "Fail to createPreferenceScreen for $fragment", e)
            }
        }
        return null
    }

    private suspend fun Intent.toActionTarget(): ActionTarget {
private fun Intent.toActionTarget(context: Context): ActionTarget {
    if (component?.packageName == "") {
        setClassName(context, component!!.className)
    }
        resolveActivity(context.packageManager)?.let {
            if (it.packageName == context.packageName) {
                add(it.className)
            }
        }
    return actionTargetProto { intent = toProto() }
}

    companion object {
        suspend fun of(context: Context, request: GetPreferenceGraphRequest) =
            PreferenceGraphBuilder(context, request).also { it.init() }
    }
}

@SuppressLint("AppBundleLocaleChanges")
internal fun Context.ofLocale(locale: Locale?): Context {
    if (locale == null) return this