車の設定の検索インデックス

設定検索を使用することで、Automotive 設定アプリの特定の設定をすばやく簡単に検索および変更でき、アプリのメニューを開いて見つける必要がなくなります。設定検索は、特定の設定を見つける最も効果的な方法です。デフォルトでは、AOSP の設定のみが検索されます。追加の設定をインデックス登録するには、それが挿入されているかどうかにかかわらず、追加の変更が必要になります。

概要

要件

設定検索で設定がインデックス登録されるようにするには、対象のデータが以下のデータである必要があります。

  • CarSettings 内の SearchIndexable フラグメント。
  • システムレベルのアプリ。

データの定義

共通フィールド:

  • Key。(必須)検索結果を判読するための、人が読める形式の一意の文字列キー。
  • IconResId。(省略可)アプリで検索結果の横にアイコンを表示する場合は、リソース ID を追加します。追加しない場合は、無視します。
  • IntentActionIntentTargetPackage または IntentTargetClass が定義されていない場合、必須。検索結果インテントで実行するアクションを定義します。
  • IntentTargetPackageIntentAction が定義されていない場合は必須。検索結果インテントが解決されるパッケージを定義します。
  • IntentTargetClassIntentAction が定義されていない場合は必須。検索結果インテントが解決されるクラス(アクティビティ)を定義します。

SearchIndexableResource のみ:

  • XmlResId。(必須)インデックス登録される検索結果を含むページの XML リソース ID を定義します。

SearchIndexableRaw のみ:

  • Title。(必須)検索結果のタイトル。
  • SummaryOn。(省略可)検索結果の概要。
  • Keywords。(省略可)検索結果に関連付けられた単語のリスト。クエリと結果を照合します。
  • ScreenTitle。(省略可)検索結果のページのタイトル。

データの非表示

特に指定のない限り、すべての検索結果が検索に表示されます。静的な検索結果はキャッシュに保存されますが、インデックス登録されないキーの最新リストは検索が開かれるたびに取得されます。結果を非表示にする理由としては以下が挙げられます。

  • 重複: たとえば、特定の設定が複数のページで表示される場合。
  • 条件付きでのみの表示: たとえば、SIM カードがあるときのみモバイルデータの設定を表示したい場合。
  • テンプレート化されたページ: たとえば、個々のアプリの詳細ページを非表示にしたい場合。
  • 設定に、タイトルやサブタイトルよりも詳細なコンテキストが必要: たとえば、画面タイトルにのみ関係する [設定] の設定を非表示にしたい場合。

設定を非表示にするには、プロバイダまたは SEARCH_INDEX_DATA_PROVIDERgetNonIndexableKeys から検索結果のキーを返す必要があります。キーは常に返されるか(重複、テンプレート化されたページの場合)、条件付きで追加できます(モバイルデータでない場合)。

静的インデックス

インデックス データが常に同じである場合は、静的インデックスを使用します。たとえば、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 はコンテンツです。新しいフラグメントを追加する処理を簡単に維持できるようにするには、SearchIndexableResourcesSettingsLib コード生成を使用します。車の設定では、インデックス登録が可能な各フラグメントは @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 を提供する 2 行を更新します。たとえば、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);
}

SearchIndexablesProvider の AAOS 実装は 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. android.provider.SearchIndexablesProvider クラスを拡張して、アプリの 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. インデックス登録が可能なデータをプロバイダに追加します。実装はアプリのニーズによって異なります。SearchIndexableResourceSearchIndexableRaw の 2 種類のデータ型をインデックス登録できます。

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