Indexación de la búsqueda de la configuración del vehículo

La búsqueda de configuración te permite buscar y cambiar parámetros de configuración específicos de la app de configuración de Automotive de forma rápida y sencilla sin navegar por los menús de la app para encontrarlos. La búsqueda es la forma más eficaz de encontrar un parámetro de configuración específico. De forma predeterminada, la búsqueda solo encuentra la configuración de AOSP. La configuración adicional, ya sea insertada o no, requiere cambios adicionales para indexarse.

Requisitos

Para que un parámetro de configuración sea indexable por la búsqueda de Configuración, los datos deben provenir de una de las siguientes fuentes:

  • Fragmento SearchIndexable dentro de CarSettings
  • Aplicación a nivel del sistema.

Define los datos

Campos comunes:

  • Key. (Obligatorio) Clave de cadena única legible por humanos para identificar el resultado.
  • IconResId. Opcional Si aparece un ícono en tu app junto al resultado, agrega el ID de recurso; de lo contrario, ignóralo.
  • IntentAction. Obligatorio si no se definen IntentTargetPackage o IntentTargetClass. Define la acción que debe realizar el intent del resultado de la búsqueda.
  • IntentTargetPackage. Obligatorio si no se define IntentAction. Define el paquete al que se resolverá el intent de resultado de la búsqueda.
  • IntentTargetClass. Obligatorio si no se define IntentAction. Define la clase (actividad) a la que se resolverá el intent del resultado de la búsqueda.

Solo SearchIndexableResource:

  • XmlResId. (Obligatorio) Define el ID de recurso XML de la página que contiene los resultados que se indexarán.

Solo SearchIndexableRaw:

  • Title. (Obligatorio) Es el título del resultado de la búsqueda.
  • SummaryOn: (Opcional) Es el resumen del resultado de la búsqueda.
  • Keywords: Es una lista de palabras asociadas con el resultado de la búsqueda (opcional). Hace coincidir la consulta con tu resultado.
  • ScreenTitle. (Opcional) Es el título de la página con el resultado de la búsqueda.

Ocultar datos

Cada resultado de la búsqueda aparece en la Búsqueda, a menos que se marque lo contrario. Mientras los resultados de la búsqueda estáticos se almacenan en caché, se recupera una lista actualizada de claves no indexables cada vez que se abre la búsqueda. Entre los motivos para ocultar resultados, se incluyen los siguientes:

  • Duplicado. Por ejemplo, aparece en varias páginas.
  • Solo se muestra de forma condicional. Por ejemplo, solo muestra la configuración de datos móviles cuando hay una tarjeta SIM.
  • Página con plantilla. Por ejemplo, una página de detalles de una app individual.
  • La configuración necesita más contexto que un título y un subtítulo. Por ejemplo, un parámetro de configuración "Configuración", que solo es relevante para el título de la pantalla.

Para ocultar un parámetro de configuración, tu proveedor o SEARCH_INDEX_DATA_PROVIDER debe mostrar la clave del resultado de la búsqueda desde getNonIndexableKeys. La clave siempre se puede mostrar (casos de páginas duplicadas o con plantillas) o agregar de forma condicional (sin caso de datos para dispositivos móviles).

Índice estático

Usa el índice estático si los datos de tu índice siempre son los mismos. Por ejemplo, el título y el resumen de los datos XML o los datos sin procesar de código duro. Los datos estáticos se indexan solo una vez cuando se inicia la búsqueda de Configuración por primera vez.

Para los elementos indexables dentro de la configuración, implementa getXmlResourcesToIndex o getRawDataToIndex. Para la configuración insertada, implementa los métodos queryXmlResources o queryRawData.

Índice dinámico

Si los datos indexables se pueden actualizar según corresponda, usa el método dinámico para indexarlos. La búsqueda de configuración actualiza esta lista dinámica cuando se inicia.

Para los elementos indexables dentro de la configuración, implementa getDynamicRawDataToIndex. Para la configuración insertada, implementa queryDynamicRawData methods.

Índice en la configuración del vehículo

Para indexar diferentes parámetros de configuración que se incluirán en la función de búsqueda, consulta Proveedores de contenido. Los paquetes SettingsLib y android.provider ya tienen interfaces y clases abstractas definidas para extenderse y proporcionar entradas nuevas que se indexarán. En la configuración de AOSP, se usan implementaciones de estas clases para indexar resultados. La interfaz principal que se debe cumplir es SearchIndexablesProvider, que SettingsIntelligence usa para indexar datos.

public abstract class SearchIndexablesProvider extends ContentProvider {
    public abstract Cursor queryXmlResources(String[] projection);
    public abstract Cursor queryRawData(String[] projection);
    public abstract Cursor queryNonIndexableKeys(String[] projection);
}

En teoría, cada fragmento se podría agregar a los resultados en SearchIndexablesProvider, en cuyo caso SettingsIntelligence sería el contenido. Para facilitar el proceso de mantenimiento y agregar fragmentos nuevos, usa la generación de código SettingsLib de SearchIndexableResources. Específicamente para la configuración del automóvil, cada fragmento indexable se anota con @SearchIndexable y, luego, tiene un campo SearchIndexProvider estático que proporciona los datos relevantes para ese fragmento. Los fragmentos con la anotación, pero sin un SearchIndexProvider, generan un error de compilación.

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

Luego, todos estos fragmentos se agregan a la clase SearchIndexableResourcesAuto generada automáticamente, que es un wrapper delgado alrededor de la lista de campos SearchIndexProvider para todos los fragmentos. Se proporciona compatibilidad para especificar un objetivo específico para una anotación (como Auto, TV y Wear), pero la mayoría de las anotaciones se dejan en el valor predeterminado (All). En este caso de uso, no existe una necesidad específica de especificar el objetivo Auto, por lo que permanece como All. La implementación básica de SearchIndexProvider está diseñada para ser suficiente para la mayoría de los 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;
    }
}

Cómo indexar un fragmento nuevo

Con este diseño, es relativamente fácil agregar un nuevo SettingsFragment para indexar, por lo general, una actualización de dos líneas que proporciona el XML del fragmento y el intent que se debe seguir. Con WifiSettingsFragment como ejemplo:

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

La implementación de AAOS de SearchIndexablesProvider, que usa SearchIndexableResources y realiza la traducción de SearchIndexProviders al esquema de la base de datos para SettingsIntelligence, pero no es compatible con los fragmentos que se indexan. SettingsIntelligence admite cualquier cantidad de proveedores, por lo que se pueden crear proveedores nuevos para admitir casos de uso especializados, lo que permite que cada uno se especialice y se enfoque en los resultados con estructuras similares. Las preferencias definidas en el PreferenceScreen para el fragmento deben tener una clave única asignada para indexarse. Además, PreferenceScreen debe tener asignada una clave para que se indexe el título de la pantalla.

Ejemplos de índices

En algunos casos, es posible que un fragmento no tenga una acción de intent específica asociada. En esos casos, es posible pasar la clase de actividad al intent CarBaseSearchIndexProvider con un componente en lugar de una acción. Esto aún requiere que la actividad esté presente en el archivo de manifiesto y que se exporte.

@SearchIndexable
public class LanguagesAndInputFragment extends SettingsFragment {
[...]
    public static final CarBaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
        new CarBaseSearchIndexProvider(R.xml.languages_and_input_fragment,
                LanguagesAndInputActivity.class);
}

En algunos casos especiales, es posible que debas anular algunos métodos de CarBaseSearchIndexProvider para que se indexen los resultados deseados. Por ejemplo, en NetworkAndInternetFragment, las preferencias relacionadas con la red móvil no se indexaban en dispositivos sin una red móvil. En este caso, anula el método getNonIndexableKeys y marca las claves adecuadas como no indexables cuando un dispositivo no tenga una red móvil.

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

Según las necesidades del fragmento en particular, se pueden anular otros métodos de CarBaseSearchIndexProvider para incluir otros datos indexables, como datos sin procesar estáticos y 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;
                }
            };
}

Configuración de inserción de índices

Para insertar un parámetro de configuración que se indexará, haz lo siguiente:

  1. Para definir un SearchIndexablesProvider para tu app, extiende la clase android.provider.SearchIndexablesProvider.
  2. Actualiza el AndroidManifest.xml de la app con el proveedor en el paso 1. El formato es el siguiente:
    <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. Agrega datos indexables a tu proveedor. La implementación depende de las necesidades de la app. Se pueden indexar dos tipos de datos diferentes: SearchIndexableResource y SearchIndexableRaw.

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