Loading packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt +22 −4 Original line number Diff line number Diff line Loading @@ -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. Loading packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenMetadata.kt +6 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceSearchIndexablesProvider.kt 0 → 100644 +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) } } } packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt +0 −3 Original line number Diff line number Diff line Loading @@ -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 Loading Loading
packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt +22 −4 Original line number Diff line number Diff line Loading @@ -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. Loading
packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenMetadata.kt +6 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading
packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceSearchIndexablesProvider.kt 0 → 100644 +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) } } }
packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt +0 −3 Original line number Diff line number Diff line Loading @@ -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 Loading