Lập chỉ mục tìm kiếm trong phần Cài đặt ô tô

Tính năng tìm kiếm trong phần Cài đặt giúp bạn nhanh chóng và dễ dàng tìm kiếm cũng như thay đổi các chế độ cài đặt cụ thể trong ứng dụng Cài đặt ô tô mà không cần phải chuyển qua các trình đơn ứng dụng để tìm. Tìm kiếm là cách hiệu quả nhất để tìm một chế độ cài đặt cụ thể. Theo mặc định, tính năng tìm kiếm chỉ tìm thấy các chế độ cài đặt AOSP. Các chế độ cài đặt bổ sung, dù có được chèn hay không, đều cần có thêm thay đổi để được lập chỉ mục.

Yêu cầu

Để một chế độ cài đặt có thể được lập chỉ mục bằng tính năng Tìm kiếm trong phần Cài đặt, dữ liệu phải đến từ:

  • Mảnh SearchIndexable bên trong CarSettings.
  • Ứng dụng cấp hệ thống.

Xác định dữ liệu

Các trường phổ biến:

  • Key. (Bắt buộc) Khoá Chuỗi duy nhất mà con người có thể đọc để xác định kết quả.
  • IconResId. Không bắt buộc Nếu một biểu tượng xuất hiện trong ứng dụng của bạn bên cạnh kết quả, hãy thêm mã nhận dạng tài nguyên, nếu không, hãy bỏ qua.
  • IntentAction. Bắt buộc nếu bạn không xác định IntentTargetPackage hoặc IntentTargetClass. Xác định hành động mà ý định kết quả tìm kiếm sẽ thực hiện.
  • IntentTargetPackage. Bắt buộc nếu bạn chưa xác định IntentAction. Xác định gói mà ý định kết quả tìm kiếm sẽ phân giải đến.
  • IntentTargetClass. Bắt buộc nếu bạn chưa xác định IntentAction. Xác định lớp (hoạt động) mà ý định kết quả tìm kiếm sẽ phân giải đến.

Chỉ SearchIndexableResource:

  • XmlResId. (Bắt buộc) Xác định mã nhận dạng tài nguyên XML của trang chứa kết quả cần lập chỉ mục.

Chỉ SearchIndexableRaw:

  • Title. (Bắt buộc) Tiêu đề của kết quả tìm kiếm.
  • SummaryOn. (Không bắt buộc) Tóm tắt kết quả tìm kiếm.
  • Keywords. (Không bắt buộc) Danh sách từ liên kết với kết quả tìm kiếm. So khớp cụm từ tìm kiếm với kết quả.
  • ScreenTitle. (Không bắt buộc) Tiêu đề của trang có kết quả tìm kiếm.

Ẩn dữ liệu

Mỗi kết quả tìm kiếm sẽ xuất hiện trên Tìm kiếm trừ phi được đánh dấu là không xuất hiện. Mặc dù kết quả tìm kiếm tĩnh được lưu vào bộ nhớ đệm, nhưng danh sách mới của các khoá không thể lập chỉ mục sẽ được truy xuất mỗi khi mở tìm kiếm. Sau đây là một số lý do khiến kết quả bị ẩn:

  • Sao chép. Ví dụ: xuất hiện trên nhiều trang.
  • Chỉ hiển thị có điều kiện. Ví dụ: chỉ hiển thị chế độ cài đặt dữ liệu di động khi có thẻ SIM).
  • Trang được tạo bằng mẫu. Ví dụ: trang chi tiết cho một ứng dụng riêng lẻ.
  • Chế độ cài đặt cần nhiều ngữ cảnh hơn so với Tiêu đề và Phụ đề. Ví dụ: chế độ cài đặt "Cài đặt" chỉ liên quan đến tiêu đề màn hình.

Để ẩn một chế độ cài đặt, nhà cung cấp hoặc SEARCH_INDEX_DATA_PROVIDER phải trả về khoá của kết quả tìm kiếm từ getNonIndexableKeys. Khoá này luôn có thể được trả về (trường hợp trang trùng lặp, trang mẫu) hoặc được thêm có điều kiện (trường hợp không có dữ liệu di động).

Chỉ mục tĩnh

Sử dụng chỉ mục tĩnh nếu dữ liệu chỉ mục của bạn luôn giống nhau. Ví dụ: tiêu đề và bản tóm tắt của dữ liệu XML hoặc dữ liệu thô mã cứng. Dữ liệu tĩnh chỉ được lập chỉ mục một lần khi lần đầu tiên chạy tính năng tìm kiếm trong phần Cài đặt.

Đối với các mục có thể lập chỉ mục bên trong phần cài đặt, hãy triển khai getXmlResourcesToIndex và/hoặc getRawDataToIndex. Đối với các chế độ cài đặt được chèn, hãy triển khai các phương thức queryXmlResources và/hoặc queryRawData.

Chỉ mục động

Nếu bạn có thể cập nhật dữ liệu có thể lập chỉ mục cho phù hợp, hãy sử dụng phương thức động để lập chỉ mục dữ liệu. Tìm kiếm trong phần Cài đặt sẽ cập nhật danh sách động này khi được khởi chạy.

Đối với các mục có thể lập chỉ mục bên trong phần cài đặt, hãy triển khai getDynamicRawDataToIndex. Đối với các chế độ cài đặt được chèn, hãy triển khai queryDynamicRawData methods.

Chỉ mục trong phần Cài đặt ô tô

Để lập chỉ mục các chế độ cài đặt khác nhau để đưa vào tính năng tìm kiếm, hãy xem phần Nhà cung cấp nội dung. Các gói SettingsLibandroid.provider đã xác định các giao diện và lớp trừu tượng để mở rộng nhằm cung cấp các mục nhập mới cần được lập chỉ mục. Trong phần cài đặt AOSP, việc triển khai các lớp này được dùng để lập chỉ mục kết quả. Giao diện chính cần thực hiện là SearchIndexablesProvider, được SettingsIntelligence sử dụng để lập chỉ mục dữ liệu.

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

Về lý thuyết, bạn có thể thêm từng mảnh vào kết quả trong SearchIndexablesProvider, trong trường hợp đó SettingsIntelligence sẽ là nội dung. Để dễ dàng duy trì quá trình thêm các mảnh mới, hãy sử dụng tính năng tạo mã SettingsLib của SearchIndexableResources. Riêng đối với phần Cài đặt ô tô, mỗi mảnh có thể lập chỉ mục được chú thích bằng @SearchIndexable, sau đó có một trường SearchIndexProvider tĩnh cung cấp dữ liệu liên quan cho mảnh đó. Các mảnh có chú thích nhưng thiếu SearchIndexProvider sẽ dẫn đến lỗi biên dịch.

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

Sau đó, tất cả các mảnh này sẽ được thêm vào lớp SearchIndexableResourcesAuto được tạo tự động. Đây là một trình bao bọc mỏng xung quanh danh sách các trường SearchIndexProvider cho tất cả các mảnh. Hỗ trợ được cung cấp để chỉ định một mục tiêu cụ thể cho chú thích (chẳng hạn như Auto, TV và Wear), tuy nhiên, hầu hết chú thích đều được đặt ở chế độ mặc định (All). Trong trường hợp sử dụng này, không cần thiết phải chỉ định mục tiêu Auto, vì vậy, mục tiêu này vẫn là All. Cách triển khai cơ sở của SearchIndexProvider là đủ cho hầu hết các mảnh.

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

Lập chỉ mục mảnh mới

Với thiết kế này, bạn có thể dễ dàng thêm một SettingsFragment mới để lập chỉ mục, thường là một bản cập nhật hai dòng cung cấp XML cho mảnh và ý định cần tuân theo. Ví dụ về 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);
}

Việc triển khai AAOS của SearchIndexablesProvider sử dụng SearchIndexableResources và thực hiện việc dịch từ SearchIndexProviders sang giản đồ cơ sở dữ liệu cho SettingsIntelligence, nhưng không phụ thuộc vào những mảnh đang được lập chỉ mục. SettingsIntelligence hỗ trợ số lượng nhà cung cấp bất kỳ, vì vậy, bạn có thể tạo nhà cung cấp mới để hỗ trợ các trường hợp sử dụng chuyên biệt, cho phép mỗi nhà cung cấp chuyên biệt và tập trung vào kết quả có cấu trúc tương tự. Các tuỳ chọn được xác định trong PreferenceScreen cho mảnh phải có một khoá duy nhất được chỉ định để được lập chỉ mục. Ngoài ra, PreferenceScreen phải có khoá được chỉ định để lập chỉ mục tiêu đề màn hình.

Ví dụ về chỉ mục

Trong một số trường hợp, một mảnh có thể không có hành động theo ý định cụ thể liên kết với mảnh đó. Trong những trường hợp như vậy, bạn có thể truyền lớp hoạt động vào ý định CarBaseSearchIndexProvider bằng cách sử dụng một thành phần thay vì một hành động. Điều này vẫn yêu cầu hoạt động phải có trong tệp kê khai và được xuất.

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

Trong một số trường hợp đặc biệt, bạn có thể cần ghi đè một số phương thức của CarBaseSearchIndexProvider để lập chỉ mục kết quả mong muốn. Ví dụ: trong NetworkAndInternetFragment, các lựa chọn ưu tiên liên quan đến mạng di động sẽ không được lập chỉ mục trên các thiết bị không có mạng di động. Trong trường hợp này, hãy ghi đè phương thức getNonIndexableKeys và đánh dấu các khoá thích hợp là không thể lập chỉ mục khi thiết bị không có mạng di động.

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

Tuỳ thuộc vào nhu cầu của mảnh cụ thể, các phương thức khác của CarBaseSearchIndexProvider có thể bị ghi đè để bao gồm các dữ liệu có thể lập chỉ mục khác, chẳng hạn như dữ liệu thô tĩnh và động.

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

Chế độ cài đặt về chỉ mục được chèn

Cách chèn chế độ cài đặt để được lập chỉ mục:

  1. Xác định SearchIndexablesProvider cho ứng dụng bằng cách mở rộng lớp android.provider.SearchIndexablesProvider.
  2. Cập nhật AndroidManifest.xml của ứng dụng bằng nhà cung cấp trong Bước 1. Định dạng là:
    <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. Thêm dữ liệu có thể lập chỉ mục vào nhà cung cấp. Việc triển khai phụ thuộc vào nhu cầu của ứng dụng. Bạn có thể lập chỉ mục hai loại dữ liệu khác nhau: SearchIndexableResourceSearchIndexableRaw.

Ví dụ về 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;
        }
    }
}