Indexação de pesquisa de configurações do carro

A pesquisa de configurações permite pesquisar e alterar de forma rápida e fácil nas configurações do Automotive Settings sem navegar pelos menus do app para encontrá-lo. A pesquisa é a maneira mais eficaz de encontrar uma configuração específica. Por padrão, Pesquisa encontra apenas as configurações do AOSP. Configurações adicionais, sejam injetadas ou não, exigem alterações adicionais para serem indexados.

Requisitos

Para que uma configuração seja indexável pela pesquisa das Configurações, os dados precisam ser provenientes de:

  • Fragmento SearchIndexable dentro de CarSettings.
  • App no nível do sistema.

Definir os dados

Campos comuns:

  • Key: (Obrigatório) Chave de string exclusiva e legível por humanos para identificar o resultado.
  • IconResId: Opcional Se um ícone aparecer no seu app ao lado do resultado, adicione o ID do recurso. Caso contrário, ignore.
  • IntentAction: Obrigatório se IntentTargetPackage ou IntentTargetClass não está definido. Define a ação que o resultado da pesquisa que queremos tomar.
  • IntentTargetPackage: Obrigatório se IntentAction não for definido. Define o pacote para o qual a intent do resultado da pesquisa deve resolver.
  • IntentTargetClass: Obrigatório se IntentAction não for definido. Define a classe (atividade) para a qual a intent do resultado da pesquisa deve resolver.

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. Corresponde a consulta ao seu resultado.
  • ScreenTitle: (Opcional) Título da página com o resultado da pesquisa.

Ocultar dados

Todos os resultados aparecem na Pesquisa, a menos que estejam marcados de outra forma. Enquanto está estático os resultados da pesquisa são armazenados em cache, uma nova lista de chaves não indexáveis é recuperada sempre que a pesquisa é aberta. Os motivos para ocultar resultados podem incluir:

  • Duplicar. Por exemplo, aparece em várias páginas.
  • Exibido apenas condicionalmente. Por exemplo, mostra apenas configurações de dados móveis quando há um chip).
  • Página com modelo. Por exemplo, a página de detalhes de um app individual.
  • A configuração precisa de mais contexto do que um título e subtítulo. Para por exemplo, "Configurações" que só é relevante para o título da tela.

Para ocultar uma configuração, seu provedor ou SEARCH_INDEX_DATA_PROVIDER precisa retorna a chave do resultado da pesquisa de getNonIndexableKeys. A chave pode ser sempre retornados (duplicadas, com modelos de página) ou adicionadas condicionalmente (sem suporte para caso de dados).

Í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. Os dados estáticos são indexados apenas uma vez quando a pesquisa de Configurações for iniciada pela primeira vez.

Para indexáveis nas configurações, implemente getXmlResourcesToIndex. e/ou getRawDataToIndex. Para configurações injetadas, implemente a métodos queryXmlResources e/ou queryRawData.

Índice dinâmico

Se os dados indexáveis puderem ser atualizados adequadamente, use o método dinâmico para indexar seus dados. A pesquisa de configurações atualiza esta lista dinâmica quando ela é iniciada.

Para indexáveis nas 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 Conteúdo provedores. SettingsLib e android.provider pacotes já têm interfaces definidas e classes abstratas para estender para fornecendo novas entradas a serem indexadas. Nas configurações do AOSP, as implementações desses são usadas para indexar resultados. A interface principal a ser preenchida é SearchIndexablesProvider, que é usado por SettingsIntelligence para indexar os 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);
}

Teoricamente, cada fragmento poderia ser adicionado aos resultados em SearchIndexablesProvider. Nesse caso, SettingsIntelligence. seria contente. Para facilitar a manutenção do processo e a adição de novos fragmentos, use a geração de código SettingsLib de SearchIndexableResources. Especificamente para "Car Settings", cada fragmento indexável é anotado com @SearchIndexable e, em seguida, tem um SearchIndexProvider estático. que fornece os dados relevantes para esse fragmento. Fragmentos com o anotação, mas sem um resultado SearchIndexProvider em uma compilação erro.

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 à versão Classe SearchIndexableResourcesAuto, que é um wrapper fino na lista de campos SearchIndexProvider para todos os fragmentos. Há suporte para a especificação de 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 a segmentação automática. Portanto, ela permanece como All. A implementação básica de SearchIndexProvider deve ser 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 indexação, normalmente uma atualização de duas linhas fornecendo o XML para o fragmento e o que devem ser seguidas. Tomando 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 AAOS do SearchIndexablesProvider, que usa SearchIndexableResources e faz a tradução do SearchIndexProviders no esquema do banco de dados para SettingsIntelligence, mas não depende do que são fragmentos que está sendo indexado. SettingsIntelligence suporta qualquer número de para que seja possível criar novos provedores para dar suporte ao uso especializado em vários casos, permitindo que cada um se especialize e se concentre em resultados com estruturas de trabalho. As preferências definidas no PreferenceScreen do fragmento devem ter uma chave exclusiva atribuída para serem indexado. Além disso, o PreferenceScreen precisa ter uma chave atribuído para o título da tela que será indexado.

Exemplos de índice

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 o CarBaseSearchIndexProvider usando um componente em vez de uma ação. Isso ainda exige que a atividade esteja presente no arquivo de manifesto e para que sejam exportados.

@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, alguns métodos de CarBaseSearchIndexProvider pode precisar ser substituído para que os resultados desejados sejam indexados. Por exemplo, em NetworkAndInternetFragment, as preferências relacionadas à rede móvel foram não sejam indexados em dispositivos sem uma rede móvel. Nesse caso, substitua o valor-chave getNonIndexableKeys e marque as chaves apropriadas como não indexável quando o dispositivo não tem 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 pode ser substituído 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 de injeção de índice

Para injetar uma configuração a ser indexada:

  1. Defina um SearchIndexablesProvider para o app estendendo o android.provider.SearchIndexablesProvider.
  2. 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>
    
  3. Adicione dados indexáveis ao seu provedor. A implementação depende das necessidades o app. Dois tipos de dados diferentes podem ser indexados: SearchIndexableResource e SearchIndexableRaw.

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;
        }
    }
}