فهرسة البحث في "إعدادات السيارة"

يتيح لك البحث في الإعدادات البحث عن إعدادات معيّنة وتغييرها بسرعة وسهولة في تطبيق "إعدادات المركبات" بدون الانتقال إلى قوائم التطبيق للعثور عليها. البحث هو الطريقة الأكثر فعالية للعثور على إعداد معيّن. بشكلٍ تلقائي، يبحث عن إعدادات AOSP فقط. إنّ الإعدادات الإضافية، سواء تمّ إدراجها أم لا، تتطلّب تغييرات إضافية لتتم فهرستها.

المتطلبات

لكي يكون الإعداد قابلاً للفهرسة من خلال البحث في "الإعدادات"، يجب أن تأتي البيانات من:

  • جزء SearchIndexable داخل CarSettings
  • تطبيق على مستوى النظام

تحديد البيانات

الحقول الشائعة:

  • Key. (مطلوبة) مفتاح سلسلة فريد يمكن لشخص عادي قراءته لتحديد النتيجة.
  • IconResId. اختياري: إذا ظهر رمز في تطبيقك بجانب نتيجة ، أضِف معرّف المورد، وإلا تجاهل هذا الحقل.
  • IntentAction: سمة مطلوبة في حال عدم تحديد IntentTargetPackage أو IntentTargetClass. تحدّد الإجراء الذي يريده نية نتيجة البحث.
  • IntentTargetPackage: سمة مطلوبة في حال عدم تحديد IntentAction تُحدِّد الحزمة التي سيتمّ حلّ نية نتيجة البحث من أجلها.
  • IntentTargetClass: سمة مطلوبة في حال عدم تحديد IntentAction يحدِّد الفئة (النشاط) التي سيتمّ حلّ نية نتيجة البحث من أجلها.

SearchIndexableResource فقط:

  • XmlResId. (مطلوبة) لتحديد معرّف مرجع XML للصفحة التي تحتوي على النتائج المطلوب فهرستها.

SearchIndexableRaw فقط:

  • Title. (مطلوبة) عنوان نتيجة البحث.
  • SummaryOn. (اختياري) ملخّص لنتيجة البحث
  • Keywords. (اختياري) قائمة بالكلمات المرتبطة بنتيجة البحث مطابقة طلب البحث مع النتيجة
  • ScreenTitle. (اختياري) عنوان الصفحة التي تتضمّن نتيجة البحث.

إخفاء البيانات

تظهر كل نتيجة بحث في "بحث Google" ما لم يتم وضع علامة أخرى عليها. في حين أنّه يتم تخزين نتائج البحث الثابتة مؤقتًا، يتم استرداد قائمة جديدة بالمفاتيح غير القابلة للفهرسة في كل مرة يتم فيها فتح البحث. يمكن أن تشمل أسباب إخفاء النتائج ما يلي:

  • تكرار على سبيل المثال، يظهر على صفحات متعدّدة.
  • لا يتم عرضها إلا بشكل مشروط. على سبيل المثال، لا يتم عرض إعدادات بيانات الجوّال إلا عند توفُّر شريحة 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 محتوى. لتسهيل عملية إضافة أقسام جديدة، استخدِم ميزة إنشاء الرموز البرمجية في SearchIndexableResources.SettingsLib بالنسبة إلى "إعدادات السيارة"، يتم وضع تعليق توضيحي على كلّ جزء قابل للفهرسة باستخدام @SearchIndexable، ثمّ يتمّ إضافة حقل SearchIndexProvider static الذي يقدّم البيانات ذات الصلة بهذا الجزء. يؤدي استخدام مقتطفات تتضمّن التعليق التوضيحي ولكن لا تتضمّن SearchIndexProvider إلى حدوث خطأ compiling.

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 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 لتضمين data أخرى قابلة للفهرسة، مثل البيانات الأولية الثابتة والديناميكية.

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