Индексация поиска настроек автомобиля

Поиск настроек позволяет быстро и легко находить и изменять определенные настройки в приложении Automotive Settings, не перемещаясь по меню приложения, чтобы найти их. Поиск — самый эффективный способ найти конкретную настройку. По умолчанию поиск находит только настройки AOSP. Дополнительные настройки, внедренные или нет, требуют дополнительных изменений для индексации.

Обзор

Требования

Чтобы параметр можно было индексировать при поиске настроек, данные должны поступать из:

  • Фрагмент SearchIndexable внутри CarSettings .
  • Приложение системного уровня.

Определение данных

Общие поля:

  • Key . ( Обязательно ) Уникальный удобочитаемый строковый ключ для идентификации результата.
  • IconResId . Необязательно . Если в вашем приложении рядом с результатом появляется значок, добавьте идентификатор ресурса, в противном случае проигнорируйте.
  • IntentAction . Требуется , если IntentTargetPackage или IntentTargetClass не определены. Определяет действие, которое должен выполнить результат поиска.
  • IntentTargetPackage . Требуется , если IntentAction не определен. Определяет пакет, в который должен быть разрешен результат поиска.
  • IntentTargetClass . Требуется , если IntentAction не определен. Определяет класс (активность), в который должен быть разрешен результат поиска.

Только SearchIndexableResource :

  • XmlResId . ( Обязательно ) Определяет идентификатор XML-ресурса страницы, содержащей результаты для индексации.

Только SearchIndexableRaw :

  • Title . ( Обязательно ) Название результата поиска.
  • SummaryOn . ( Необязательно ) Сводка результатов поиска.
  • Keywords . ( Необязательно ) Список слов, связанных с результатом поиска. Сопоставляет запрос с вашим результатом.
  • ScreenTitle . ( Необязательно ) Название страницы с результатами поиска.

Скрытие данных

Каждый результат поиска будет отображаться в поиске, если не указано иное. Хотя статические результаты поиска кэшируются, при каждом открытии поиска извлекается свежий список неиндексируемых ключей. Причины скрытия результатов могут включать:

  • Дублировать. Например, появляется на нескольких страницах.
  • Только показано условно. Например, настройки мобильных данных отображаются только при наличии SIM-карты).
  • Шаблонная страница. Например, страница сведений об отдельном приложении.
  • Настройка требует большего контекста, чем заголовок и подзаголовок. Например, параметр «Настройки», относящийся только к заголовку экрана.

Чтобы скрыть параметр, ваш провайдер или SEARCH_INDEX_DATA_PROVIDER должны вернуть ключ результата поиска из getNonIndexableKeys . Ключ всегда можно вернуть (дубликаты, шаблоны страниц) или добавить условно (без мобильных данных).

Статический индекс

Используйте статический индекс, если ваши данные индекса всегда одинаковы. Например, заголовок и сводка данных XML или необработанные данные жесткого кода. Статические данные будут проиндексированы только один раз при первом запуске поиска настроек.

Для индексируемых элементов внутри настроек реализуйте getXmlResourcesToIndex и/или getRawDataToIndex . Для введенных параметров реализуйте методы queryXmlResources и/или queryRawData .

Динамический индекс

Если индексируемые данные можно соответствующим образом обновить, используйте динамический метод для индексации данных. Поиск настроек будет обновлять этот динамический список при его запуске.

Для индексируемых элементов внутри настроек реализуйте getDynamicRawDataToIndex . Для внедренных настроек queryDynamicRawData methods .

Индексация в настройках автомобиля

Обзор

Чтобы проиндексировать различные настройки, которые будут включены в функцию поиска, см. раздел Поставщики контента . Пакеты SettingsLib и android.provider уже имеют определенные интерфейсы и абстрактные классы для расширения для предоставления новых записей для индексации. В настройках AOSP реализации этих классов используются для индексации результатов. Основной интерфейс, который необходимо выполнить, — это SearchIndexablesProvider , который используется SettingsIntelligence для индексации данных.

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

Теоретически каждый фрагмент может быть добавлен к результатам в SearchIndexablesProvider , и в этом случае SettingsIntelligence будет содержимым. Чтобы упростить процесс обслуживания и легко добавлять новые фрагменты, используйте генерацию кода SettingsLib для SearchIndexableResources . В настройках автомобиля каждый индексируемый фрагмент аннотируется @SearchIndexable а затем имеет статическое поле SearchIndexProvider , предоставляющее соответствующие данные для этого фрагмента. Фрагменты с аннотацией, но без SearchIndexProvider приводят к ошибке компиляции.

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

Затем все эти фрагменты добавляются в автоматически сгенерированный класс SearchIndexableResourcesAuto , который представляет собой тонкую оболочку списка полей SearchIndexProvider для всех фрагментов. Предусмотрена поддержка указания конкретной цели для аннотации (например, Auto, TV и Wear), однако для большинства аннотаций используется значение по умолчанию ( All ). В этом случае использования нет особой необходимости указывать цель Auto, поэтому она остается All . Базовая реализация SearchIndexProvider должна быть достаточной для большинства фрагментов.

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

Индексация нового фрагмента

При таком дизайне относительно легко добавить новый SettingsFragment для индексации, обычно это двухстрочное обновление, предоставляющее XML для фрагмента и намерение, которому нужно следовать. С WifiSettingsFragment в качестве примера:

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

Реализация AAOS SearchIndexablesProvider , которая использует SearchIndexableResources и выполняет перевод из SearchIndexProviders в схему базы данных для SettingsIntelligence , но не зависит от того, какие фрагменты индексируются. SettingsIntelligence поддерживает любое количество поставщиков, поэтому можно создавать новые поставщики для поддержки специализированных вариантов использования, позволяя каждому быть специализированным и ориентированным на результаты с аналогичной структурой. Предпочтения, определенные в PreferenceScreen для фрагмента, должны иметь уникальный ключ, назначенный им, чтобы их можно было проиндексировать. Кроме того, PreferenceScreen должен иметь ключ, назначенный для индексации заголовка экрана.

Примеры индексации

В некоторых случаях фрагмент может не иметь связанного с ним конкретного действия намерения. В таких случаях можно передать класс активности в намерение CarBaseSearchIndexProvider , используя компонент, а не действие. Для этого по-прежнему требуется, чтобы активность присутствовала в файле манифеста и чтобы ее можно было экспортировать.

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

В некоторых особых случаях может потребоваться переопределение некоторых методов CarBaseSearchIndexProvider , чтобы получить желаемые результаты для индексации. Например, в NetworkAndInternetFragment предпочтения, связанные с мобильной сетью, не должны были индексироваться на устройствах без мобильной сети. В этом случае переопределите метод getNonIndexableKeys и пометьте соответствующие ключи как неиндексируемые , если на устройстве нет мобильной сети.

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

В зависимости от потребностей конкретного фрагмента другие методы CarBaseSearchIndexProvider могут быть переопределены для включения других индексируемых данных, таких как статические и динамические необработанные данные.

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

Индексация внедренных настроек

Обзор

Чтобы внедрить параметр для индексации:

  1. Определите SearchIndexablesProvider для своего приложения, расширив класс android.provider.SearchIndexablesProvider .
  2. Обновите AndroidManifest.xml приложения, указав поставщика на шаге 1. Формат:
    <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. Добавьте индексируемые данные к вашему провайдеру. Реализация зависит от потребностей приложения. Можно индексировать два разных типа данных: SearchIndexableResource и SearchIndexableRaw .

Пример 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;
        }
    }
}