A pesquisa de configurações permite pesquisar e mudar rapidamente configurações específicas no app Configurações automotivas sem navegar pelos menus do app para encontrá-las. A pesquisa é a maneira mais eficaz de encontrar uma configuração específica. Por padrão, a pesquisa encontra apenas as configurações do AOSP. Outras configurações, injetadas ou não, exigem mudanças adicionais para serem indexadas.
Requisitos
Para que uma configuração seja indexada pela pesquisa de configurações, os dados precisam vir de:
- Fragmento
SearchIndexable
dentro deCarSettings
. - App do sistema.
Definir os dados
Campos comuns:
Key
: obrigatório. Chave de string legível por humanos exclusiva para identificar o resultado.IconResId
. Opcional: se um ícone aparecer no app ao lado do resultado, adicione o ID do recurso. Caso contrário, ignore.IntentAction
. Obrigatório seIntentTargetPackage
ouIntentTargetClass
não estiver definido. Define a ação que a intent do resultado da pesquisa vai realizar.IntentTargetPackage
. Obrigatório seIntentAction
não estiver definido. Define o pacote para o qual a intent do resultado da pesquisa será resolvida.IntentTargetClass
. Obrigatório seIntentAction
não estiver definido. Define a classe (atividade) para a qual a intent do resultado da pesquisa será resolvida.
Somente SearchIndexableResource
:
XmlResId
: Obrigatório. Define o ID do recurso XML da página que contém os resultados a serem indexados.
Somente SearchIndexableRaw
:
Title
. (Obrigatório) Título do resultado da pesquisa.SummaryOn
: (opcional) resumo do resultado da pesquisa.Keywords
: (opcional) lista de palavras associadas ao resultado da pesquisa. Faz a correspondência da consulta com o resultado.ScreenTitle
. (Opcional) Título da página com o resultado da pesquisa.
Ocultar dados
Cada resultado da pesquisa aparece na Pesquisa, a menos que seja marcado de outra forma. Enquanto os resultados estáticos da pesquisa são armazenados em cache, uma nova lista de chaves não indexáveis é recuperada toda vez que a pesquisa é aberta. Os motivos para ocultar resultados podem incluir:
- Duplicado. Por exemplo, aparece em várias páginas.
- Só aparece condicionalmente. Por exemplo, só mostra as configurações de dados móveis quando um chip está presente).
- Página com modelo. Por exemplo, uma página de detalhes de um app específico.
- A configuração precisa de mais contexto do que um título e um subtítulo. Por exemplo, uma configuração "Configurações", que é relevante apenas para o título da tela.
Para ocultar uma configuração, o provedor ou o SEARCH_INDEX_DATA_PROVIDER
precisa
retornar a chave do resultado da pesquisa de getNonIndexableKeys
. A chave pode
sempre ser retornada (casos de páginas duplicadas ou com modelos) ou adicionada condicionalmente (caso de dados
móveis).
Índice estático
Use o índice estático se os dados do índice forem sempre os mesmos. Por exemplo, o título e o resumo dos dados XML ou os dados brutos do código fixo. Os dados estáticos são indexados apenas uma vez quando a pesquisa de configurações é iniciada pela primeira vez.
Para indexáveis nas configurações, implemente getXmlResourcesToIndex
e/ou getRawDataToIndex
. Para configurações injetadas, implemente os métodos
queryXmlResources
e/ou queryRawData
.
Índice dinâmico
Se os dados indexáveis puderem ser atualizados de acordo, use o método dinâmico para indexar seus dados. A pesquisa de configurações atualiza essa lista dinâmica quando ela é iniciada.
Para indexáveis dentro de configurações, implemente getDynamicRawDataToIndex
.
Para configurações injetadas, implemente o queryDynamicRawData methods
.
Índice nas configurações do carro
Para indexar diferentes configurações a serem incluídas no recurso de pesquisa, consulte
Provedores
de conteúdo. Os pacotes SettingsLib
e android.provider
já têm interfaces e classes abstratas definidas para estender e
fornecer novas entradas a serem indexadas. Nas configurações do AOSP, as implementações dessas
classes são usadas para indexar resultados. A interface principal a ser atendida é
SearchIndexablesProvider
, que é usada por
SettingsIntelligence
para indexar dados.
public abstract class SearchIndexablesProvider extends ContentProvider { public abstract Cursor queryXmlResources(String[] projection); public abstract Cursor queryRawData(String[] projection); public abstract Cursor queryNonIndexableKeys(String[] projection); }
Em teoria, cada fragmento pode ser adicionado aos resultados em
SearchIndexablesProvider
, caso em que SettingsIntelligence
seria o conteúdo. Para facilitar a manutenção do processo e adicionar novos fragmentos,
use a geração de código SettingsLib
de SearchIndexableResources
.
Específico para as configurações do carro, cada fragmento indexável é anotado com
@SearchIndexable
e tem um campo SearchIndexProvider
estático que fornece os dados relevantes para esse fragmento. Fragmentos com a
anotação, mas sem um SearchIndexProvider
, resultam em um erro de
compilação.
interface SearchIndexProvider { List<SearchIndexableResource> getXmlResourcesToIndex(Context context, boolean enabled); List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled); List<SearchIndexableRaw> getDynamicRawDataToIndex(Context context, boolean enabled); List<String> getNonIndexableKeys(Context context); }
Todos esses fragmentos são adicionados à classe
SearchIndexableResourcesAuto
gerada automaticamente, que é um wrapper simples
da lista de campos SearchIndexProvider
de todos os fragmentos.
Há suporte para especificar um destino específico para uma anotação (como
Auto, TV e Wear), mas a maioria das anotações é deixada no padrão (All
).
Nesse caso de uso, não há necessidade específica de especificar o destino automático, então ele permanece
como All
. A implementação básica de SearchIndexProvider
é suficiente para a maioria dos fragmentos.
public class CarBaseSearchIndexProvider implements Indexable.SearchIndexProvider { private static final Logger LOG = new Logger(CarBaseSearchIndexProvider.class); private final int mXmlRes; private final String mIntentAction; private final String mIntentClass; public CarBaseSearchIndexProvider(@XmlRes int xmlRes, String intentAction) { mXmlRes = xmlRes; mIntentAction = intentAction; mIntentClass = null; } public CarBaseSearchIndexProvider(@XmlRes int xmlRes, @NonNull Class intentClass) { mXmlRes = xmlRes; mIntentAction = null; mIntentClass = intentClass.getName(); } @Override public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, boolean enabled) { SearchIndexableResource sir = new SearchIndexableResource(context); sir.xmlResId = mXmlRes; sir.intentAction = mIntentAction; sir.intentTargetPackage = context.getPackageName(); sir.intentTargetClass = mIntentClass; return Collections.singletonList(sir); } @Override public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { return null; } @Override public List<SearchIndexableRaw> getDynamicRawDataToIndex(Context context, boolean enabled) { return null; } @Override public List<String> getNonIndexableKeys(Context context) { if (!isPageSearchEnabled(context)) { try { return PreferenceXmlParser.extractMetadata(context, mXmlRes, FLAG_NEED_KEY) .stream() .map(bundle -> bundle.getString(METADATA_KEY)) .collect(Collectors.toList()); } catch (IOException | XmlPullParserException e) { LOG.w("Error parsing non-indexable XML - " + mXmlRes); } } return null; } /** * Returns true if the page should be considered in search query. If return false, entire page is suppressed during search query. */ protected boolean isPageSearchEnabled(Context context) { return true; } }
Indexar um novo fragmento
Com esse design, é relativamente fácil adicionar um novo SettingsFragment
para ser indexado, geralmente uma atualização de duas linhas que fornece o XML para o fragmento e a
intent a ser seguida. Usando WifiSettingsFragment
como exemplo:
@SearchIndexable public class WifiSettingsFragment extends SettingsFragment { [...] public static final CarBaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new CarBaseSearchIndexProvider(R.xml.wifi_list_fragment, Settings.ACTION_WIFI_SETTINGS); }
A implementação do AAOS do SearchIndexablesProvider
, que
usa SearchIndexableResources
e faz a tradução de
SearchIndexProviders
para o esquema do banco de dados para
SettingsIntelligence
, mas não sabe quais fragmentos estão
sendo indexados. O SettingsIntelligence
oferece suporte a qualquer número de
provedores, para que novos provedores possam ser criados para oferecer suporte a casos de uso
especializados, permitindo que cada um seja especializado e focado em resultados com estruturas
semelhantes. As preferências definidas no PreferenceScreen
do fragmento precisam ter uma chave exclusiva atribuída para serem
indexadas. Além disso, o PreferenceScreen
precisa ter uma chave
atribuída para que o título da tela seja indexado.
Exemplos de índices
Em alguns casos, um fragmento pode não ter uma ação de intent específica associada
a ele. Nesses casos, é possível transmitir a classe de atividade para a
intent CarBaseSearchIndexProvider
usando um componente em vez de
uma ação. Isso ainda exige que a atividade esteja presente no arquivo de manifesto
e seja exportada.
@SearchIndexable public class LanguagesAndInputFragment extends SettingsFragment { [...] public static final CarBaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new CarBaseSearchIndexProvider(R.xml.languages_and_input_fragment, LanguagesAndInputActivity.class); }
Em alguns casos especiais, pode ser necessário substituir alguns métodos de CarBaseSearchIndexProvider
para que os resultados desejados sejam indexados. Por exemplo, em
NetworkAndInternetFragment
, as preferências relacionadas à rede móvel não
eram indexadas em dispositivos sem uma rede móvel. Nesse caso, substitua o método
getNonIndexableKeys
e marque as chaves adequadas como
não indexáveis quando um dispositivo não tiver uma rede móvel.
@SearchIndexable public class NetworkAndInternetFragment extends SettingsFragment { [...] public static final CarBaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new CarBaseSearchIndexProvider(R.xml.network_and_internet_fragment, Settings.Panel.ACTION_INTERNET_CONNECTIVITY) { @Override public List<String> getNonIndexableKeys(Context context) { if (!NetworkUtils.hasMobileNetwork( context.getSystemService(ConnectivityManager.class))) { List<String> nonIndexableKeys = new ArrayList<>(); nonIndexableKeys.add(context.getString( R.string.pk_mobile_network_settings_entry)); nonIndexableKeys.add(context.getString( R.string.pk_data_usage_settings_entry)); return nonIndexableKeys; } return null; } }; }
Dependendo das necessidades do fragmento específico, outros métodos do
CarBaseSearchIndexProvider
podem ser substituídos para incluir outros
dados indexáveis, como dados brutos estáticos e dinâmicos.
@SearchIndexable public class RawIndexDemoFragment extends SettingsFragment { public static final String KEY_CUSTOM_RESULT = "custom_result_key"; [...] public static final CarBaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new CarBaseSearchIndexProvider(R.xml.raw_index_demo_fragment, RawIndexDemoActivity.class) { @Override public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { List<SearchIndexableRaw> rawData = new ArrayList<>(); SearchIndexableRaw customResult = new SearchIndexableRaw(context); customResult.key = KEY_CUSTOM_RESULT; customResult.title = context.getString(R.string.my_title); customResult.screenTitle = context.getString(R.string.my_screen_title); rawData.add(customResult); return rawData; } @Override public List<SearchIndexableRaw> getDynamicRawDataToIndex(Context context, boolean enabled) { List<SearchIndexableRaw> rawData = new ArrayList<>(); SearchIndexableRaw customResult = new SearchIndexableRaw(context); if (hasIndexData()) { customResult.key = KEY_CUSTOM_RESULT; customResult.title = context.getString(R.string.my_title); customResult.screenTitle = context.getString(R.string.my_screen_title); } rawData.add(customResult); return rawData; } }; }
Configurações injetadas no índice
Para injetar uma configuração a ser indexada:
- Estenda a classe
android.provider.SearchIndexablesProvider
para definir umSearchIndexablesProvider
para o app. - Atualize o
AndroidManifest.xml
do app com o provedor na etapa 1. O formato é:<provider android:name="PROVIDER_CLASS_NAME" android:authorities="PROVIDER_AUTHORITY" android:multiprocess="false" android:grantUriPermissions="true" android:permission="android.permission.READ_SEARCH_INDEXABLES" android:exported="true"> <intent-filter> <action android:name="android.content.action.SEARCH_INDEXABLES_PROVIDER" /> </intent-filter> </provider>
-
Adicione dados indexáveis ao seu provedor. A implementação depende das necessidades
do app. Dois tipos de dados diferentes podem ser indexados:
SearchIndexableResource
eSearchIndexableRaw
.
Exemplo de SearchIndexablesProvider
public class SearchDemoProvider extends SearchIndexablesProvider { /** * Key for Auto brightness setting. */ public static final String KEY_AUTO_BRIGHTNESS = "auto_brightness"; /** * Key for my magic preference. */ public static final String KEY_MY_PREFERENCE = "my_preference_key"; /** * Key for my custom search result. */ public static final String KEY_CUSTOM_RESULT = "custom_result_key"; private String mPackageName; @Override public boolean onCreate() { mPackageName = getContext().getPackageName(); return true; } @Override public Cursor queryXmlResources(String[] projection) { MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS); cursor.addRow(getResourceRow(R.xml.demo_xml)); return cursor; } @Override public Cursor queryRawData(String[] projection) { MatrixCursor cursor = new MatrixCursor(INDEXABLES_RAW_COLUMNS); Context context = getContext(); Object[] raw = new Object[INDEXABLES_RAW_COLUMNS.length]; raw[COLUMN_INDEX_RAW_TITLE] = context.getString(R.string.my_title); raw[COLUMN_INDEX_RAW_SUMMARY_ON] = context.getString(R.string.my_summary); raw[COLUMN_INDEX_RAW_KEYWORDS] = context.getString(R.string.my_keywords); raw[COLUMN_INDEX_RAW_SCREEN_TITLE] = context.getString(R.string.my_screen_title); raw[COLUMN_INDEX_RAW_KEY] = KEY_CUSTOM_RESULT; raw[COLUMN_INDEX_RAW_INTENT_ACTION] = Intent.ACTION_MAIN; raw[COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE] = mPackageName; raw[COLUMN_INDEX_RAW_INTENT_TARGET_CLASS] = MyDemoFragment.class.getName(); cursor.addRow(raw); return cursor; } @Override public Cursor queryDynamicRawData(String[] projection) { MatrixCursor cursor = new MatrixCursor(INDEXABLES_RAW_COLUMNS); DemoObject object = getDynamicIndexData(); Object[] raw = new Object[INDEXABLES_RAW_COLUMNS.length]; raw[COLUMN_INDEX_RAW_KEY] = object.key; raw[COLUMN_INDEX_RAW_TITLE] = object.title; raw[COLUMN_INDEX_RAW_KEYWORDS] = object.keywords; raw[COLUMN_INDEX_RAW_INTENT_ACTION] = object.intentAction; raw[COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE] = object.mPackageName; raw[COLUMN_INDEX_RAW_INTENT_TARGET_CLASS] = object.className; cursor.addRow(raw); return cursor; } @Override public Cursor queryNonIndexableKeys(String[] projection) { MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS); cursor.addRow(getNonIndexableRow(KEY_AUTO_BRIGHTNESS)); if (!Utils.isMyPreferenceAvailable) { cursor.addRow(getNonIndexableRow(KEY_MY_PREFERENCE)); } return cursor; } private Object[] getResourceRow(int xmlResId) { Object[] row = new Object[INDEXABLES_XML_RES_COLUMNS.length]; row[COLUMN_INDEX_XML_RES_RESID] = xmlResId; row[COLUMN_INDEX_XML_RES_ICON_RESID] = 0; row[COLUMN_INDEX_XML_RES_INTENT_ACTION] = Intent.ACTION_MAIN; row[COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE] = mPackageName; row[COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS] = SearchResult.class.getName(); return row; } private Object[] getNonIndexableRow(String key) { final Object[] ref = new Object[NON_INDEXABLES_KEYS_COLUMNS.length]; ref[COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE] = key; return ref; } private DemoObject getDynamicIndexData() { if (hasIndexData) { DemoObject object = new DemoObject(); object.key = "demo key"; object.title = "demo title"; object.keywords = "demo, keywords"; object.intentAction = "com.demo.DYNAMIC_INDEX"; object.packageName = "com.demo"; object.className = "DemoClass"; return object; } } }