자동차 설정 검색 색인 생성

설정 검색을 사용하면 앱 메뉴를 탐색하지 않고도 Automotive 설정 앱에서 특정 설정을 쉽고 빠르게 검색하고 변경할 수 있습니다. 검색은 특정 설정을 찾는 가장 효과적인 방법입니다. 기본적으로 검색은 AOSP 설정만 찾습니다. 삽입 여부와 관계없이 추가 설정의 색인을 생성하기 위해서는 추가 변경이 필요합니다.

개요

요구사항

설정 검색으로 설정의 색인을 생성할 수 있으려면 데이터의 출처가 다음과 같아야 합니다.

  • CarSettings 내의 SearchIndexable 프래그먼트
  • 시스템 수준 애플리케이션

데이터 정의

공통 필드

  • Key. 필수. 사람이 읽을 수 있는 고유한 문자열 키로, 결과를 식별합니다.
  • IconResId. 선택사항. 앱에서 결과 옆에 아이콘이 표시되면 리소스 ID를 추가하고 표시되지 않으면 무시합니다.
  • IntentAction. IntentTargetPackageIntentTargetClass가 정의되지 않은 경우 필수입니다. 검색결과 인텐트가 실행할 작업을 정의합니다.
  • IntentTargetPackage. IntentAction이 정의되지 않은 경우 필수입니다. 검색결과 인텐트가 확인할 패키지를 정의합니다.
  • IntentTargetClass. IntentAction이 정의되지 않은 경우 필수입니다. 검색결과 인텐트가 확인할 클래스(활동)를 정의합니다.

SearchIndexableResource 전용

  • XmlResId. 필수. 색인을 생성할 결과가 포함된 페이지의 XML 리소스 ID를 정의합니다.

SearchIndexableRaw 전용

  • Title. 필수. 검색결과의 제목입니다.
  • SummaryOn. 선택사항. 검색결과의 요약입니다.
  • Keywords. 선택사항. 검색결과와 관련된 단어 목록입니다. 검색어를 결과와 일치시킵니다.
  • ScreenTitle. 선택사항. 검색결과가 있는 페이지의 제목입니다.

데이터 숨기기

각 검색결과는 다르게 표시되지 않는 한 Google 검색에 표시됩니다. 정적 검색결과가 캐시되는 동안 검색이 열릴 때마다 색인을 생성할 수 없는 키의 새로운 목록이 검색됩니다. 검색결과를 숨기는 이유는 다음과 같습니다.

  • 중복. 예를 들어 여러 페이지에 표시됩니다.
  • 조건부로만 표시. 예를 들어 SIM 카드가 있을 때만 모바일 데이터 설정을 표시합니다.
  • 템플릿 페이지. 예를 들어 개별 앱의 세부정보 페이지입니다.
  • 제목과 부제보다 더 많은 컨텍스트가 필요한 설정. 예를 들어 화면 제목과만 관련이 있는 'Settings' 설정입니다.

설정을 숨기려면 제공자나 SEARCH_INDEX_DATA_PROVIDERgetNonIndexableKeys에서 검색결과 키를 반환해야 합니다. 키는 항상 반환되거나(중복, 템플릿 페이지 사례) 조건부로 추가될 수 있습니다(모바일 데이터 사례 없음).

정적 색인

색인 데이터가 항상 동일하다면 정적 색인을 사용하세요. 예를 들어 XML 데이터의 제목과 요약 또는 하드 코드 원시 데이터가 이에 해당합니다. 정적 데이터는 설정 검색이 처음 실행될 때 한 번만 색인이 생성됩니다.

설정 내에서 색인을 생성할 수 있는 항목의 경우 getXmlResourcesToIndexgetRawDataToIndex를 구현합니다. 삽입된 설정의 경우 queryXmlResources 메서드나 queryRawData 메서드를 구현합니다.

동적 색인

색인을 생성할 수 있는 데이터를 적절히 업데이트할 수 있다면 동적 메서드를 사용하여 데이터의 색인을 생성합니다. 설정 검색을 실행하면 이 동적 목록이 업데이트됩니다.

설정 내에서 색인을 생성할 수 있는 항목의 경우 getDynamicRawDataToIndex를 구현합니다. 삽입된 설정의 경우 queryDynamicRawData methods를 구현합니다.

자동차 설정에서 색인 생성

개요

검색 기능에 포함할 다양한 설정의 색인을 생성하려면 콘텐츠 제공자를 참고하세요. SettingsLibandroid.provider 패키지에는 색인을 생성할 새 항목을 제공하기 위해 확장할 인터페이스와 추상 클래스가 이미 정의되어 있습니다. AOSP 설정에서는 이러한 클래스를 구현하여 결과의 색인을 생성합니다. 처리할 기본 인터페이스는 SettingsIntelligence에서 데이터의 색인을 생성하는 데 사용하는 SearchIndexablesProvider입니다.

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과 따라야 할 인텐트를 제공하는 두 줄 업데이트입니다. 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. 1단계의 제공자로 애플리케이션의 AndroidManifest.xml을 업데이트합니다. 형식은 다음과 같습니다.
    <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의 색인을 생성할 수 있습니다.

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