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

Commit 7c99f9bd authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "[Catalyst] Support settings search" into main

parents d8240117 321c1360
Loading
Loading
Loading
Loading
+22 −4
Original line number Diff line number Diff line
@@ -108,12 +108,30 @@ interface PreferenceMetadata {
    fun tags(context: Context): Array<String> = arrayOf()

    /**
     * Returns if preference is indexable, default value is `true`.
     * Returns if preference is indexable for settings search.
     *
     * Return `false` only when the preference is always unavailable on current device. If it is
     * conditional available, override [PreferenceAvailabilityProvider].
     * Return `false` only when the preference is unavailable for indexing on current device. If it
     * is available on condition, override [PreferenceAvailabilityProvider].
     *
     * Note: If a [PreferenceScreenMetadata.isIndexable] returns `false`, all the preferences on the
     * screen are not indexable.
     */
    fun isIndexable(context: Context): Boolean =
        when (this) {
            is PreferenceGroup -> getPreferenceTitle(context)?.isNotEmpty() == true
            else -> true
        }

    /**
     * Returns if the preference is available on condition, which indicates its availability could
     * be changed at runtime and should not be cached (e.g. for indexing).
     *
     * [PreferenceAvailabilityProvider] subclass returns `true` by default. For [PreferenceMetadata]
     * that are generated programmatically should also return `true` even it does not implement
     * [PreferenceAvailabilityProvider].
     */
    fun isIndexable(context: Context): Boolean = true
    val isAvailableOnCondition: Boolean
        get() = this is PreferenceAvailabilityProvider

    /**
     * Returns if preference is enabled.
+6 −0
Original line number Diff line number Diff line
@@ -55,6 +55,9 @@ interface PreferenceScreenMetadata : PreferenceMetadata {
    val description: Int
        @StringRes get() = 0

    /** Returns if the flag (e.g. for rollout) is enabled on current screen. */
    fun isFlagEnabled(context: Context): Boolean = true

    /** Returns dynamic screen title, use [screenTitle] whenever possible. */
    fun getScreenTitle(context: Context): CharSequence? = null

@@ -86,6 +89,9 @@ interface PreferenceScreenMetadata : PreferenceMetadata {
    /**
     * Returns the [Intent] to show current preference screen.
     *
     * NOTE: Always provide action for the returned intent. Otherwise, SettingsIntelligence starts
     * intent with com.android.settings.SEARCH_RESULT_TRAMPOLINE action instead of given activity.
     *
     * @param metadata the preference to locate when show the screen
     */
    fun getLaunchIntent(context: Context, metadata: PreferenceMetadata?): Intent? = null
+198 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.metadata

import android.content.Context
import android.database.MatrixCursor
import android.os.SystemClock
import android.provider.SearchIndexablesContract
import android.provider.SearchIndexablesProvider
import android.util.Log

/**
 * [SearchIndexablesProvider] to generate indexing data consistently for preference screens that are
 * built on top of Catalyst framework.
 *
 * A screen is qualified as indexable only when:
 * - [PreferenceScreenMetadata.hasCompleteHierarchy] is true: hybrid mode is not supported to avoid
 *   potential conflict.
 * - **AND** [PreferenceScreenMetadata.isIndexable] is true.
 * - **AND** [PreferenceScreenMetadata.isFlagEnabled] is true.
 *
 * The strategy to provide indexing data is:
 * - A preference is treated dynamic (and handled by [queryDynamicRawData]) only when:
 *     - it or any of its parent implements [PreferenceAvailabilityProvider]
 *     - **OR** it has dynamic content (i.e. implements [PreferenceTitleProvider] and
 *       [PreferenceIconProvider])
 * - A preference is treated static (and handled by [queryRawData]) only when:
 *     - it and all of its parents do no implement [PreferenceAvailabilityProvider]
 *     - **AND** it does not contain any dynamic content
 *
 * With this strategy, [queryNonIndexableKeys] simply returns an empty data.
 *
 * Nevertheless, it is possible to design other strategy. For example, a preference implements
 * [PreferenceAvailabilityProvider] and does not contain any dynamic content can be treated as
 * static. Then it will reduce the data returned by [queryDynamicRawData] but need more time to
 * traversal all the screens for [queryNonIndexableKeys].
 */
abstract class PreferenceSearchIndexablesProvider : SearchIndexablesProvider() {

    /**
     * Returns if Catalyst indexable provider is enabled.
     *
     * This is mainly for flagging purpose.
     */
    abstract val isCatalystSearchEnabled: Boolean

    override fun queryDynamicRawData(projection: Array<out String>?): MatrixCursor {
        val cursor = MatrixCursor(SearchIndexablesContract.INDEXABLES_RAW_COLUMNS)
        if (!isCatalystSearchEnabled) return cursor
        val start = SystemClock.elapsedRealtime()
        val context = requireContext()
        context.visitPreferenceScreen { preferenceScreenMetadata ->
            val screenTitle = preferenceScreenMetadata.getPreferenceScreenTitle(context)
            fun PreferenceHierarchyNode.visitRecursively(isParentAvailableOnCondition: Boolean) {
                if (!metadata.isAvailable(context)) return
                val isAvailableOnCondition =
                    isParentAvailableOnCondition || metadata.isAvailableOnCondition
                if (
                    metadata.isIndexable(context) &&
                        (isAvailableOnCondition || metadata.isDynamic) &&
                        !metadata.isScreenEntryPoint(preferenceScreenMetadata)
                ) {
                    metadata
                        .toRawColumnValues(context, preferenceScreenMetadata, screenTitle)
                        ?.let { cursor.addRow(it) }
                }
                (this as? PreferenceHierarchy)?.forEach {
                    it.visitRecursively(isAvailableOnCondition)
                }
            }
            preferenceScreenMetadata.getPreferenceHierarchy(context).visitRecursively(false)
        }
        Log.d(TAG, "dynamicRawData: ${cursor.count} in ${SystemClock.elapsedRealtime() - start}ms")
        return cursor
    }

    override fun queryRawData(projection: Array<String>?): MatrixCursor {
        val cursor = MatrixCursor(SearchIndexablesContract.INDEXABLES_RAW_COLUMNS)
        if (!isCatalystSearchEnabled) return cursor
        val start = SystemClock.elapsedRealtime()
        val context = requireContext()
        context.visitPreferenceScreen { preferenceScreenMetadata ->
            val screenTitle = preferenceScreenMetadata.getPreferenceScreenTitle(context)
            fun PreferenceHierarchyNode.visitRecursively() {
                if (metadata.isAvailableOnCondition) return
                if (
                    metadata.isIndexable(context) &&
                        !metadata.isDynamic &&
                        !metadata.isScreenEntryPoint(preferenceScreenMetadata)
                ) {
                    metadata
                        .toRawColumnValues(context, preferenceScreenMetadata, screenTitle)
                        ?.let { cursor.addRow(it) }
                }
                (this as? PreferenceHierarchy)?.forEach { it.visitRecursively() }
            }
            preferenceScreenMetadata.getPreferenceHierarchy(context).visitRecursively()
        }
        Log.d(TAG, "rawData: ${cursor.count} in ${SystemClock.elapsedRealtime() - start}ms")
        return cursor
    }

    override fun queryNonIndexableKeys(projection: Array<String>?) =
        // Just return empty as queryRawData ignores conditional available preferences recursively
        MatrixCursor(SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS)

    private fun Context.visitPreferenceScreen(action: (PreferenceScreenMetadata) -> Unit) {
        PreferenceScreenRegistry.preferenceScreenMetadataFactories.forEach { _, factory ->
            // parameterized screen is not supported because there is no way to provide arguments
            if (factory is PreferenceScreenMetadataParameterizedFactory) return@forEach
            val preferenceScreenMetadata = factory.create(this)
            if (
                preferenceScreenMetadata.hasCompleteHierarchy() &&
                    preferenceScreenMetadata.isIndexable(this) &&
                    preferenceScreenMetadata.isFlagEnabled(this)
            ) {
                action(preferenceScreenMetadata)
            }
        }
    }

    private fun PreferenceMetadata.toRawColumnValues(
        context: Context,
        preferenceScreenMetadata: PreferenceScreenMetadata,
        screenTitle: CharSequence?,
    ): Array<Any?>? {
        val intent = preferenceScreenMetadata.getLaunchIntent(context, this) ?: return null
        val columnValues = arrayOfNulls<Any>(SearchIndexablesContract.INDEXABLES_RAW_COLUMNS.size)

        columnValues[SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE] = getPreferenceTitle(context)
        columnValues[SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS] = getKeywords(context)
        columnValues[SearchIndexablesContract.COLUMN_INDEX_RAW_SCREEN_TITLE] = screenTitle
        val iconResId = getPreferenceIcon(context)
        if (iconResId != 0) {
            columnValues[SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID] = iconResId
        }
        columnValues[SearchIndexablesContract.COLUMN_INDEX_RAW_KEY] =
            "$PREFIX${preferenceScreenMetadata.key}/$key"

        columnValues[SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_ACTION] = intent.action
        intent.component?.let {
            columnValues[SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE] =
                it.packageName
            columnValues[SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS] =
                it.className
        }
        return columnValues
    }

    private fun PreferenceMetadata.getKeywords(context: Context) =
        if (keywords != 0) context.getString(keywords) else null

    private fun PreferenceMetadata.isAvailable(context: Context) =
        (this as? PreferenceAvailabilityProvider)?.isAvailable(context) != false

    /**
     * Returns if the preference has dynamic content.
     *
     * Dynamic summary is not taken into account because it is not used by settings search now.
     */
    private val PreferenceMetadata.isDynamic: Boolean
        get() = this is PreferenceTitleProvider || this is PreferenceIconProvider

    /**
     * Returns if the preference is an entry point of another screen.
     *
     * If true, the preference will be excluded to avoid duplication on search result.
     */
    private fun PreferenceMetadata.isScreenEntryPoint(
        preferenceScreenMetadata: PreferenceScreenMetadata
    ) = this is PreferenceScreenMetadata && preferenceScreenMetadata != this

    companion object {
        private const val TAG = "CatalystSearch"
        /** Prefix to distinguish preference key for Catalyst search. */
        private const val PREFIX = "CS:"

        fun getHighlightKey(key: String?): String? {
            if (key?.startsWith(PREFIX) != true) return key
            val lastSlash = key.lastIndexOf('/')
            return key.substring(lastSlash + 1)
        }
    }
}
+0 −3
Original line number Diff line number Diff line
@@ -135,9 +135,6 @@ interface PreferenceBindingPlaceholder
/** Abstract preference screen to provide preference hierarchy and binding factory. */
interface PreferenceScreenCreator : PreferenceScreenMetadata, PreferenceScreenProvider {

    /** Returns if the flag (e.g. for rollout) is enabled on current screen. */
    fun isFlagEnabled(context: Context): Boolean = true

    val preferenceBindingFactory: PreferenceBindingFactory
        get() = PreferenceBindingFactory.defaultFactory