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

Поиск настроек позволяет быстро и легко искать и изменять определенные настройки в приложении «Настройки автомобиля» без необходимости перемещаться по меню приложения, чтобы найти их. Поиск — наиболее эффективный способ найти конкретную настройку. По умолчанию поиск находит только настройки 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 для всех фрагментов. Предоставляется поддержка для указания конкретной цели для аннотации (например, «Авто», «ТВ» и «Износ»), однако для большинства аннотаций оставляются значения по умолчанию ( 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 is 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;
        }
    }
}