• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.search;
18 
19 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE;
20 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_CLASS_NAME;
21 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ENTRIES;
22 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID;
23 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_ACTION;
24 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS;
25 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE;
26 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEY;
27 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS;
28 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SCREEN_TITLE;
29 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_OFF;
30 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON;
31 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE;
32 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_USER_ID;
33 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_CLASS_NAME;
34 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID;
35 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_ACTION;
36 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS;
37 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE;
38 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RANK;
39 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID;
40 import static android.provider.SearchIndexablesContract.INDEXABLES_RAW_COLUMNS;
41 import static android.provider.SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS;
42 import static android.provider.SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS;
43 import static android.provider.SearchIndexablesContract.SITE_MAP_COLUMNS;
44 import static android.provider.SearchIndexablesContract.SLICE_URI_PAIRS_COLUMNS;
45 
46 import static com.android.settings.dashboard.DashboardFragmentRegistry.CATEGORY_KEY_TO_PARENT_MAP;
47 
48 import android.content.ContentResolver;
49 import android.content.Context;
50 import android.database.Cursor;
51 import android.database.MatrixCursor;
52 import android.net.Uri;
53 import android.os.Build;
54 import android.provider.SearchIndexableResource;
55 import android.provider.SearchIndexablesContract;
56 import android.provider.SearchIndexablesProvider;
57 import android.provider.SettingsSlicesContract;
58 import android.text.TextUtils;
59 import android.util.ArrayMap;
60 import android.util.ArraySet;
61 import android.util.Log;
62 
63 import androidx.annotation.Nullable;
64 import androidx.annotation.VisibleForTesting;
65 import androidx.slice.SliceViewManager;
66 
67 import com.android.settings.R;
68 import com.android.settings.SettingsActivity;
69 import com.android.settings.dashboard.CategoryManager;
70 import com.android.settings.dashboard.DashboardFeatureProvider;
71 import com.android.settings.dashboard.DashboardFragmentRegistry;
72 import com.android.settings.overlay.FeatureFactory;
73 import com.android.settings.slices.SettingsSliceProvider;
74 import com.android.settingslib.drawer.ActivityTile;
75 import com.android.settingslib.drawer.DashboardCategory;
76 import com.android.settingslib.drawer.Tile;
77 import com.android.settingslib.search.Indexable;
78 import com.android.settingslib.search.SearchIndexableData;
79 import com.android.settingslib.search.SearchIndexableRaw;
80 
81 import java.util.ArrayList;
82 import java.util.Collection;
83 import java.util.List;
84 import java.util.Map;
85 
86 public class SettingsSearchIndexablesProvider extends SearchIndexablesProvider {
87 
88     public static final boolean DEBUG = false;
89 
90     /**
91      * Flag for a system property which checks if we should crash if there are issues in the
92      * indexing pipeline.
93      */
94     public static final String SYSPROP_CRASH_ON_ERROR =
95             "debug.com.android.settings.search.crash_on_error";
96 
97     private static final String TAG = "SettingsSearchProvider";
98 
99     private static final Collection<String> INVALID_KEYS;
100 
101     // Search enabled states for injection (key: category key, value: search enabled)
102     private Map<String, Boolean> mSearchEnabledByCategoryKeyMap;
103 
104     static {
105         INVALID_KEYS = new ArraySet<>();
106         INVALID_KEYS.add(null);
107         INVALID_KEYS.add("");
108     }
109 
110     @Override
onCreate()111     public boolean onCreate() {
112         mSearchEnabledByCategoryKeyMap = new ArrayMap<>();
113         return true;
114     }
115 
116     @Override
queryXmlResources(String[] projection)117     public Cursor queryXmlResources(String[] projection) {
118         final MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);
119         final List<SearchIndexableResource> resources =
120                 getSearchIndexableResourcesFromProvider(getContext());
121         for (SearchIndexableResource val : resources) {
122             final Object[] ref = new Object[INDEXABLES_XML_RES_COLUMNS.length];
123             ref[COLUMN_INDEX_XML_RES_RANK] = val.rank;
124             ref[COLUMN_INDEX_XML_RES_RESID] = val.xmlResId;
125             ref[COLUMN_INDEX_XML_RES_CLASS_NAME] = val.className;
126             ref[COLUMN_INDEX_XML_RES_ICON_RESID] = val.iconResId;
127             ref[COLUMN_INDEX_XML_RES_INTENT_ACTION] = val.intentAction;
128             ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE] = val.intentTargetPackage;
129             ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS] = null; // intent target class
130             cursor.addRow(ref);
131         }
132 
133         return cursor;
134     }
135 
136     /**
137      * Gets a Cursor of RawData. We use those data in search indexing time
138      */
139     @Override
queryRawData(String[] projection)140     public Cursor queryRawData(String[] projection) {
141         final MatrixCursor cursor = new MatrixCursor(INDEXABLES_RAW_COLUMNS);
142         final List<SearchIndexableRaw> raws = getSearchIndexableRawFromProvider(getContext());
143         for (SearchIndexableRaw val : raws) {
144             cursor.addRow(createIndexableRawColumnObjects(val));
145         }
146 
147         return cursor;
148     }
149 
150     /**
151      * Gets a combined list non-indexable keys that come from providers inside of settings.
152      * The non-indexable keys are used in Settings search at both index and update time to verify
153      * the validity of results in the database.
154      */
155     @Override
queryNonIndexableKeys(String[] projection)156     public Cursor queryNonIndexableKeys(String[] projection) {
157         final MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS);
158         final List<String> nonIndexableKeys = getNonIndexableKeysFromProvider(getContext());
159         for (String nik : nonIndexableKeys) {
160             final Object[] ref = new Object[NON_INDEXABLES_KEYS_COLUMNS.length];
161             ref[COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE] = nik;
162             cursor.addRow(ref);
163         }
164 
165         return cursor;
166     }
167 
168     /**
169      * Gets a Cursor of dynamic Raw data similar to queryRawData. We use those data in search query
170      * time
171      */
172     @Nullable
173     @Override
queryDynamicRawData(String[] projection)174     public Cursor queryDynamicRawData(String[] projection) {
175         final Context context = getContext();
176         final List<SearchIndexableRaw> rawList = new ArrayList<>();
177         final Collection<SearchIndexableData> bundles = FeatureFactory.getFeatureFactory()
178                 .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
179 
180         for (SearchIndexableData bundle : bundles) {
181             rawList.addAll(getDynamicSearchIndexableRawData(context, bundle));
182 
183             // Refresh the search enabled state for indexing injection raw data
184             final Indexable.SearchIndexProvider provider = bundle.getSearchIndexProvider();
185             if (provider instanceof BaseSearchIndexProvider) {
186                 refreshSearchEnabledState(context, (BaseSearchIndexProvider) provider);
187             }
188         }
189         rawList.addAll(getInjectionIndexableRawData(context));
190 
191         final MatrixCursor cursor = new MatrixCursor(INDEXABLES_RAW_COLUMNS);
192         for (SearchIndexableRaw raw : rawList) {
193             cursor.addRow(createIndexableRawColumnObjects(raw));
194         }
195 
196         return cursor;
197     }
198 
199     @Override
querySiteMapPairs()200     public Cursor querySiteMapPairs() {
201         final MatrixCursor cursor = new MatrixCursor(SITE_MAP_COLUMNS);
202         final Context context = getContext();
203         // Loop through all IA categories and pages and build additional SiteMapPairs
204         final List<DashboardCategory> categories = FeatureFactory.getFeatureFactory()
205                 .getDashboardFeatureProvider().getAllCategories();
206         for (DashboardCategory category : categories) {
207             // Use the category key to look up parent (which page hosts this key)
208             final String parentClass = CATEGORY_KEY_TO_PARENT_MAP.get(category.key);
209             if (parentClass == null) {
210                 continue;
211             }
212             // Build parent-child class pairs for all children listed under this key.
213             for (Tile tile : category.getTiles()) {
214                 String childClass = null;
215                 CharSequence childTitle = "";
216                 if (tile.getMetaData() != null) {
217                     childClass = tile.getMetaData().getString(
218                             SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS);
219                 }
220                 if (childClass == null) {
221                     childClass = tile.getComponentName();
222                     childTitle = tile.getTitle(getContext());
223                 }
224                 if (childClass == null) {
225                     continue;
226                 }
227                 cursor.newRow()
228                         .add(SearchIndexablesContract.SiteMapColumns.PARENT_CLASS, parentClass)
229                         .add(SearchIndexablesContract.SiteMapColumns.CHILD_CLASS, childClass)
230                         .add(SearchIndexablesContract.SiteMapColumns.CHILD_TITLE, childTitle);
231             }
232         }
233 
234         // Loop through custom site map registry to build additional SiteMapPairs
235         for (String childClass : CustomSiteMapRegistry.CUSTOM_SITE_MAP.keySet()) {
236             final String parentClass = CustomSiteMapRegistry.CUSTOM_SITE_MAP.get(childClass);
237             cursor.newRow()
238                     .add(SearchIndexablesContract.SiteMapColumns.PARENT_CLASS, parentClass)
239                     .add(SearchIndexablesContract.SiteMapColumns.CHILD_CLASS, childClass);
240         }
241         // Done.
242         return cursor;
243     }
244 
245     @Override
querySliceUriPairs()246     public Cursor querySliceUriPairs() {
247         final SliceViewManager manager = SliceViewManager.getInstance(getContext());
248         final MatrixCursor cursor = new MatrixCursor(SLICE_URI_PAIRS_COLUMNS);
249         final String queryUri = getContext().getString(R.string.config_non_public_slice_query_uri);
250         final Uri baseUri = !TextUtils.isEmpty(queryUri) ? Uri.parse(queryUri)
251                 : new Uri.Builder()
252                         .scheme(ContentResolver.SCHEME_CONTENT)
253                         .authority(SettingsSliceProvider.SLICE_AUTHORITY)
254                         .build();
255 
256         final Uri platformBaseUri =
257                 new Uri.Builder()
258                         .scheme(ContentResolver.SCHEME_CONTENT)
259                         .authority(SettingsSlicesContract.AUTHORITY)
260                         .build();
261 
262         final Collection<Uri> sliceUris = manager.getSliceDescendants(baseUri);
263         sliceUris.addAll(manager.getSliceDescendants(platformBaseUri));
264 
265         for (Uri uri : sliceUris) {
266             cursor.newRow()
267                     .add(SearchIndexablesContract.SliceUriPairColumns.KEY, uri.getLastPathSegment())
268                     .add(SearchIndexablesContract.SliceUriPairColumns.SLICE_URI, uri);
269         }
270 
271         return cursor;
272     }
273 
getNonIndexableKeysFromProvider(Context context)274     private List<String> getNonIndexableKeysFromProvider(Context context) {
275         final Collection<SearchIndexableData> bundles = FeatureFactory.getFeatureFactory()
276                 .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
277 
278         final List<String> nonIndexableKeys = new ArrayList<>();
279 
280         for (SearchIndexableData bundle : bundles) {
281             final long startTime = System.currentTimeMillis();
282             Indexable.SearchIndexProvider provider = bundle.getSearchIndexProvider();
283             List<String> providerNonIndexableKeys;
284             try {
285                 providerNonIndexableKeys = provider.getNonIndexableKeys(context);
286             } catch (Exception e) {
287                 String msg = "Error trying to get non-indexable keys from: "
288                         + bundle.getTargetClass().getName();
289                 // Catch a generic crash. In the absence of the catch, the background thread will
290                 // silently fail anyway, so we aren't losing information by catching the exception.
291                 // We crash on debuggable build or when the system property exists, so that we can
292                 // test if crashes need to be fixed.
293                 if (Build.IS_DEBUGGABLE || System.getProperty(SYSPROP_CRASH_ON_ERROR) != null) {
294                     throw new RuntimeException(msg, e);
295                 }
296                 Log.e(TAG, msg, e);
297                 continue;
298             }
299 
300             if (providerNonIndexableKeys == null || providerNonIndexableKeys.isEmpty()) {
301                 if (DEBUG) {
302                     final long totalTime = System.currentTimeMillis() - startTime;
303                     Log.d(TAG, "No indexable, total time " + totalTime);
304                 }
305                 continue;
306             }
307 
308             if (providerNonIndexableKeys.removeAll(INVALID_KEYS)) {
309                 Log.v(TAG, provider + " tried to add an empty non-indexable key");
310             }
311 
312             if (DEBUG) {
313                 final long totalTime = System.currentTimeMillis() - startTime;
314                 Log.d(TAG, "Non-indexables " + providerNonIndexableKeys.size() + ", total time "
315                         + totalTime);
316             }
317 
318             nonIndexableKeys.addAll(providerNonIndexableKeys);
319         }
320 
321         return nonIndexableKeys;
322     }
323 
getSearchIndexableResourcesFromProvider(Context context)324     private List<SearchIndexableResource> getSearchIndexableResourcesFromProvider(Context context) {
325         final Collection<SearchIndexableData> bundles = FeatureFactory.getFeatureFactory()
326                 .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
327         List<SearchIndexableResource> resourceList = new ArrayList<>();
328 
329         for (SearchIndexableData bundle : bundles) {
330             Indexable.SearchIndexProvider provider = bundle.getSearchIndexProvider();
331             final List<SearchIndexableResource> resList =
332                     provider.getXmlResourcesToIndex(context, true);
333 
334             if (resList == null) {
335                 continue;
336             }
337 
338             for (SearchIndexableResource item : resList) {
339                 item.className = TextUtils.isEmpty(item.className)
340                         ? bundle.getTargetClass().getName()
341                         : item.className;
342             }
343 
344             resourceList.addAll(resList);
345         }
346 
347         return resourceList;
348     }
349 
getSearchIndexableRawFromProvider(Context context)350     private List<SearchIndexableRaw> getSearchIndexableRawFromProvider(Context context) {
351         final Collection<SearchIndexableData> bundles = FeatureFactory.getFeatureFactory()
352                 .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
353         final List<SearchIndexableRaw> rawList = new ArrayList<>();
354 
355         for (SearchIndexableData bundle : bundles) {
356             Indexable.SearchIndexProvider provider = bundle.getSearchIndexProvider();
357             final List<SearchIndexableRaw> providerRaws = provider.getRawDataToIndex(context,
358                     true /* enabled */);
359 
360             if (providerRaws == null) {
361                 continue;
362             }
363 
364             for (SearchIndexableRaw raw : providerRaws) {
365                 // The classname and intent information comes from the PreIndexData
366                 // This will be more clear when provider conversion is done at PreIndex time.
367                 raw.className = bundle.getTargetClass().getName();
368             }
369             rawList.addAll(providerRaws);
370         }
371 
372         return rawList;
373     }
374 
getDynamicSearchIndexableRawData(Context context, SearchIndexableData bundle)375     private List<SearchIndexableRaw> getDynamicSearchIndexableRawData(Context context,
376             SearchIndexableData bundle) {
377         final Indexable.SearchIndexProvider provider = bundle.getSearchIndexProvider();
378         final List<SearchIndexableRaw> providerRaws =
379                 provider.getDynamicRawDataToIndex(context, true /* enabled */);
380         if (providerRaws == null) {
381             return new ArrayList<>();
382         }
383 
384         for (SearchIndexableRaw raw : providerRaws) {
385             // The classname and intent information comes from the PreIndexData
386             // This will be more clear when provider conversion is done at PreIndex time.
387             raw.className = bundle.getTargetClass().getName();
388         }
389         return providerRaws;
390     }
391 
392     @VisibleForTesting
getInjectionIndexableRawData(Context context)393     List<SearchIndexableRaw> getInjectionIndexableRawData(Context context) {
394         final DashboardFeatureProvider dashboardFeatureProvider =
395                 FeatureFactory.getFeatureFactory().getDashboardFeatureProvider();
396         final List<SearchIndexableRaw> rawList = new ArrayList<>();
397         final String currentPackageName = context.getPackageName();
398         for (DashboardCategory category : dashboardFeatureProvider.getAllCategories()) {
399             if (mSearchEnabledByCategoryKeyMap.containsKey(category.key)
400                     && !mSearchEnabledByCategoryKeyMap.get(category.key)) {
401                 Log.i(TAG, "Skip indexing category: " + category.key);
402                 continue;
403             }
404             for (Tile tile : category.getTiles()) {
405                 if (!isEligibleForIndexing(currentPackageName, tile)) {
406                     continue;
407                 }
408                 final SearchIndexableRaw raw = new SearchIndexableRaw(context);
409                 final CharSequence title = tile.getTitle(context);
410                 raw.title = TextUtils.isEmpty(title) ? null : title.toString();
411                 if (TextUtils.isEmpty(raw.title)) {
412                     continue;
413                 }
414                 raw.key = dashboardFeatureProvider.getDashboardKeyForTile(tile);
415                 final CharSequence summary = tile.getSummary(context);
416                 raw.summaryOn = TextUtils.isEmpty(summary) ? null : summary.toString();
417                 raw.summaryOff = raw.summaryOn;
418                 raw.className = CATEGORY_KEY_TO_PARENT_MAP.get(tile.getCategory());
419                 rawList.add(raw);
420             }
421         }
422 
423         return rawList;
424     }
425 
426     @VisibleForTesting
refreshSearchEnabledState(Context context, BaseSearchIndexProvider provider)427     void refreshSearchEnabledState(Context context, BaseSearchIndexProvider provider) {
428         // Provider's class name is like "com.android.settings.Settings$1"
429         String className = provider.getClass().getName();
430         final int delimiter = className.lastIndexOf("$");
431         if (delimiter > 0) {
432             // Parse the outer class name of this provider
433             className = className.substring(0, delimiter);
434         }
435 
436         // Lookup the category key by the class name
437         final String categoryKey = DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP
438                 .get(className);
439         if (categoryKey == null) {
440             return;
441         }
442 
443         final DashboardCategory category = CategoryManager.get(context)
444                 .getTilesByCategory(context, categoryKey);
445         if (category != null) {
446             mSearchEnabledByCategoryKeyMap.put(category.key, provider.isPageSearchEnabled(context));
447         }
448     }
449 
450     @VisibleForTesting
isEligibleForIndexing(String packageName, Tile tile)451     boolean isEligibleForIndexing(String packageName, Tile tile) {
452         if (TextUtils.equals(packageName, tile.getPackageName())
453                 && tile instanceof ActivityTile) {
454             // Skip Settings injected items because they should be indexed in the sub-pages.
455             return false;
456         }
457         return tile.isSearchable();
458     }
459 
createIndexableRawColumnObjects(SearchIndexableRaw raw)460     private static Object[] createIndexableRawColumnObjects(SearchIndexableRaw raw) {
461         final Object[] ref = new Object[INDEXABLES_RAW_COLUMNS.length];
462         ref[COLUMN_INDEX_RAW_TITLE] = raw.title;
463         ref[COLUMN_INDEX_RAW_SUMMARY_ON] = raw.summaryOn;
464         ref[COLUMN_INDEX_RAW_SUMMARY_OFF] = raw.summaryOff;
465         ref[COLUMN_INDEX_RAW_ENTRIES] = raw.entries;
466         ref[COLUMN_INDEX_RAW_KEYWORDS] = raw.keywords;
467         ref[COLUMN_INDEX_RAW_SCREEN_TITLE] = raw.screenTitle;
468         ref[COLUMN_INDEX_RAW_CLASS_NAME] = raw.className;
469         ref[COLUMN_INDEX_RAW_ICON_RESID] = raw.iconResId;
470         ref[COLUMN_INDEX_RAW_INTENT_ACTION] = raw.intentAction;
471         ref[COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE] = raw.intentTargetPackage;
472         ref[COLUMN_INDEX_RAW_INTENT_TARGET_CLASS] = raw.intentTargetClass;
473         ref[COLUMN_INDEX_RAW_KEY] = raw.key;
474         ref[COLUMN_INDEX_RAW_USER_ID] = raw.userId;
475         return ref;
476     }
477 }
478