הוספת חיפוש לאינדקס בהגדרות הרכב

חיפוש בהגדרות מאפשר לך לחפש ולשנות הגדרות ספציפיות במהירות ובקלות בהגדרות של האפליקציה Automotive Settings (הגדרות כלי רכב) בלי לעבור דרך תפריטי האפליקציות כדי למצוא אותו. החיפוש הוא הדרך היעילה ביותר למצוא הגדרה ספציפית. כברירת מחדל, החיפוש מוצא רק הגדרות 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, שהיא wrapper צר סביב רשימת השדות SearchIndexProvider לכל המקטעים. ניתנת תמיכה בציון יעד ספציפי של הערה (כמו אוטומטי, טלוויזיה ו-Wear), עם זאת, רוב ההערות נותרות כברירת המחדל (All). בתרחיש לדוגמה הזה, לא קיים צורך ספציפי כדי לציין את היעד האוטומטי, ולכן נשאר בתור 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 חייב להיות מפתח הוקצה ל- כדי שכותרת המסך תתווסף לאינדקס.

דוגמאות לאינדקס

במקרים מסוימים, ייתכן שלא תהיה שיוך של פעולת Intent ספציפית למקטע איתו. במקרים כאלה, אפשר להעביר את קטגוריית הפעילות אל Intent אחד (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;
        }
    }
}