Indexación de búsqueda de configuración del automóvil

La búsqueda de configuración le permite buscar y cambiar configuraciones específicas de manera rápida y sencilla en la aplicación Configuración automotriz sin tener que navegar por los menús de la aplicación para encontrarla. La búsqueda es la forma más eficaz de encontrar una configuración específica. De forma predeterminada, la búsqueda solo encuentra configuraciones de AOSP. Las configuraciones adicionales, ya sean inyectadas o no, requieren cambios adicionales para poder indexarse.

Requisitos

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

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

Definir los datos

Campos comunes:

  • Key . ( Obligatorio ) Clave de cadena única legible por humanos para identificar el resultado.
  • IconResId . Opcional Si aparece un icono 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 el resultado de la búsqueda.
  • IntentTargetPackage . Obligatorio si IntentAction no está definido. Define el paquete en el que se resolverá el resultado de la búsqueda.
  • IntentTargetClass . Obligatorio si IntentAction no está definido. Define la clase (actividad) en la que se resolverá el resultado de la búsqueda.

SearchIndexableResource únicamente:

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

SearchIndexableRaw solamente:

  • Title . ( Obligatorio ) 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 aparece en Buscar a menos que esté marcado de otra manera. Si bien los resultados de la búsqueda estática se almacenan en caché, cada vez que se abre la búsqueda se recupera una lista nueva de claves no indexables. Las razones para ocultar los resultados pueden incluir:

  • Duplicar. Por ejemplo, aparece en varias páginas.
  • Sólo se muestra de forma condicional. 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 sólo 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 la búsqueda de getNonIndexableKeys . La clave siempre se puede devolver (casos de páginas duplicadas y con plantilla) o agregarse condicionalmente (sin casos 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 físico. Los datos estáticos se indexan solo una vez cuando se inicia por primera vez la búsqueda de Configuración.

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

índice dinámico

Si los datos indexables se pueden actualizar en consecuencia, utilice el método dinámico para indexar sus datos. La búsqueda de configuración actualiza esta lista dinámica cuando se inicia.

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

Índice en la configuración del automóvil

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 resultados. La interfaz principal que se debe cumplir es SearchIndexablesProvider , que utiliza SettingsIntelligence 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 estaría contento. Para que el proceso sea fácil de mantener y agregar nuevos fragmentos, utilice la generación de código SettingsLib de SearchIndexableResources . Específicamente para la configuración del automóvil, cada fragmento indexable está anotado 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 provocan 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 una envoltura delgada 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 básica 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 is suppressed during search query.
     */
    protected boolean isPageSearchEnabled(Context context) {
        return true;
    }
}

Indexar un nuevo fragmento

Con este diseño, es relativamente fácil agregar un nuevo SettingsFragment para indexar, generalmente una actualización de dos líneas que proporciona el XML del 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 AAOS de SearchIndexablesProvider , que utiliza SearchIndexableResources y realiza la traducción de SearchIndexProviders al esquema de la base de datos para SettingsIntelligence , pero es independiente de qué fragmentos se indexan. SettingsIntelligence admite cualquier cantidad 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. Las preferencias definidas en PreferenceScreen para el fragmento deben tener cada una una clave única asignada para poder ser indexadas. Además, PreferenceScreen debe tener una clave asignada para que se indexe el título de la pantalla.

Ejemplos de índice

En algunos casos, es posible que un fragmento no tenga una acción de intención específica asociada. En tales casos, es posible pasar la clase de actividad al intent CarBaseSearchIndexProvider usando 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 pueda exportar.

@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 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 inyectada de índice

Para inyectar una configuración para indexar:

  1. Defina un SearchIndexablesProvider para su aplicación extendiendo la clase android.provider.SearchIndexablesProvider .
  2. Actualice 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 proveedor de SearchIndexables

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