Indización de búsqueda de ajustes de coche

La búsqueda de configuración le permite buscar y cambiar rápida y fácilmente configuraciones específicas en la aplicación Configuración automotriz sin navegar a través de los menús de la aplicación para encontrarla. La búsqueda es la forma más efectiva de encontrar una configuración específica. De forma predeterminada, la búsqueda solo encuentra la configuración de AOSP. Las configuraciones adicionales, ya sea inyectadas o no, requieren cambios adicionales para ser indexadas.

Visión general

Requisitos

Para que una configuración sea indexable mediante la búsqueda de configuraciones, los datos deben provenir de:

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

Definición de los datos

Campos comunes:

  • Key ( Obligatorio ) Clave única de cadena legible por humanos para identificar el resultado.
  • IconResId . Opcional Si aparece un ícono en su aplicación junto al resultado, agregue la identificación del recurso; de lo contrario, ignórelo.
  • IntentAction . Obligatorio si IntentTargetPackage o IntentTargetClass no están definidos. Define la acción que debe realizar la intención del resultado de la búsqueda.
  • IntentTargetPackage . Obligatorio si IntentAction no está definido. Define el paquete en el que se resolverá la intención del resultado de la búsqueda.
  • IntentTargetClass . Obligatorio si IntentAction no está definido. Define la clase (actividad) a la que se debe resolver la intención del resultado de la búsqueda.

SearchIndexableResource solamente:

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

SearchIndexableRaw solamente:

  • Title ( Requerido ) Título del resultado de la búsqueda.
  • SummaryOn . ( Opcional ) Resumen del resultado de la búsqueda.
  • Keywords ( Opcional ) Lista de palabras asociadas al resultado de la búsqueda. Hace coincidir la consulta con su resultado.
  • ScreenTitle . ( Opcional ) Título de la página con el resultado de su búsqueda.

ocultar datos

Cada resultado de búsqueda aparecerá en Buscar a menos que se indique lo contrario. Si bien los resultados de la búsqueda estática se almacenan en caché, se recupera una nueva lista de claves no indexables cada vez que se abre la búsqueda. Las razones para ocultar los resultados pueden incluir:

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

Para ocultar una configuración, su proveedor o SEARCH_INDEX_DATA_PROVIDER debe devolver la clave del resultado de búsqueda de getNonIndexableKeys . La clave siempre se puede devolver (casos de página con plantilla duplicada) o agregarse condicionalmente (sin caso de datos móviles).

índice estático

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

Para indexables dentro de la configuración, implemente getXmlResourcesToIndex y/o getRawDataToIndex . Para configuraciones inyectadas, implemente los queryXmlResources y/o queryRawData .

índice dinámico

Si los datos indexables se pueden actualizar en consecuencia, use el método dinámico para indexar sus datos. La búsqueda de configuraciones actualizará esta lista dinámica cuando se inicie.

Para indexables dentro de la configuración, implemente getDynamicRawDataToIndex . Para la configuración inyectada, implemente los queryDynamicRawData methods .

Indexación en la configuración del automóvil

Visión general

Para indexar diferentes configuraciones que se incluirán en la función de búsqueda, consulte Proveedores de contenido . Los paquetes SettingsLib y android.provider ya tienen interfaces definidas y clases abstractas para ampliar y proporcionar nuevas entradas para indexar. En la configuración de AOSP, las implementaciones de estas clases se utilizan para indexar los resultados. La interfaz principal que debe cumplirse 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 podría agregarse a los resultados en SearchIndexablesProvider , en cuyo caso SettingsIntelligence sería el contenido. Para facilitar el mantenimiento del proceso para agregar nuevos fragmentos, use la generación de código SettingsLib de SearchIndexableResources . Específico 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 dan como resultado 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 contenedor delgado alrededor de la lista de campos SearchIndexProvider para todos los fragmentos. Se proporciona soporte para especificar un objetivo específico para una anotación (como Auto, TV y Wear), sin embargo, 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 destino automático, por lo que permanece como All . La implementación base de SearchIndexProvider pretende 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 will be suppressed during search query.
     */
    protected boolean isPageSearchEnabled(Context context) {
        return true;
    }
}

Indexación de un nuevo fragmento

Con este diseño, es relativamente fácil agregar un nuevo SettingsFragment para indexarlo, generalmente una actualización de dos líneas que proporciona el XML para el fragmento y la intención a 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 hace la traducción de SearchIndexProviders al esquema de la base de datos para SettingsIntelligence , pero es independiente de los fragmentos que se indexan. SettingsIntelligence admite cualquier número de proveedores, por lo que se pueden crear nuevos proveedores para admitir casos de uso especializados, lo que permite que cada uno se especialice y se centre en resultados con estructuras similares. Cada una de las preferencias definidas en PreferenceScreen para el fragmento debe tener una clave única asignada para poder indexarse. Además, PreferenceScreen debe tener una clave asignada para que se indexe el título de la pantalla.

Ejemplos de indexación

En algunos casos, es posible que un fragmento no tenga asociada una acción de intención específica. En tales casos, es posible pasar la clase de actividad a la intención CarBaseSearchIndexProvider utilizando 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 sea necesario anular algunos métodos de CarBaseSearchIndexProvider para obtener los resultados deseados que se indexarán. Por ejemplo, en NetworkAndInternetFragment , las preferencias relacionadas con la red móvil no debían indexarse ​​en dispositivos sin red móvil. En este caso, anule el método getNonIndexableKeys y marque las claves apropiadas 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;
                }
            };
}

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

Indexación de configuraciones inyectadas

Visión general

Para inyectar una configuración para ser indexada:

  1. Defina un SearchIndexablesProvider para su aplicación extendiendo la clase android.provider.SearchIndexablesProvider .
  2. Actualice el AndroidManifest.xml de la aplicación con el proveedor en el Paso 1. El formato es:
    <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. Agregue datos indexables a su proveedor. La implementación depende de las necesidades de la aplicación. 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;
        }
    }
}