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.provider.SearchIndexableResource;
54 import android.provider.SearchIndexablesContract;
55 import android.provider.SearchIndexablesProvider;
56 import android.provider.SettingsSlicesContract;
57 import android.text.TextUtils;
58 import android.util.ArrayMap;
59 import android.util.ArraySet;
60 import android.util.Log;
61 
62 import androidx.annotation.Nullable;
63 import androidx.annotation.VisibleForTesting;
64 import androidx.slice.SliceViewManager;
65 
66 import com.android.settings.R;
67 import com.android.settings.SettingsActivity;
68 import com.android.settings.dashboard.CategoryManager;
69 import com.android.settings.dashboard.DashboardFeatureProvider;
70 import com.android.settings.dashboard.DashboardFragmentRegistry;
71 import com.android.settings.overlay.FeatureFactory;
72 import com.android.settings.slices.SettingsSliceProvider;
73 import com.android.settingslib.drawer.ActivityTile;
74 import com.android.settingslib.drawer.DashboardCategory;
75 import com.android.settingslib.drawer.Tile;
76 import com.android.settingslib.search.Indexable;
77 import com.android.settingslib.search.SearchIndexableData;
78 import com.android.settingslib.search.SearchIndexableRaw;
79 
80 import java.util.ArrayList;
81 import java.util.Collection;
82 import java.util.List;
83 import java.util.Map;
84 
85 public class SettingsSearchIndexablesProvider extends SearchIndexablesProvider {
86 
87     public static final boolean DEBUG = false;
88 
89     /**
90      * Flag for a system property which checks if we should crash if there are issues in the
91      * indexing pipeline.
92      */
93     public static final String SYSPROP_CRASH_ON_ERROR =
94             "debug.com.android.settings.search.crash_on_error";
95 
96     private static final String TAG = "SettingsSearchProvider";
97 
98     private static final Collection<String> INVALID_KEYS;
99 
100     // Search enabled states for injection (key: category key, value: search enabled)
101     private Map<String, Boolean> mSearchEnabledByCategoryKeyMap;
102 
103     static {
104         INVALID_KEYS = new ArraySet<>();
105         INVALID_KEYS.add(null);
106         INVALID_KEYS.add("");
107     }
108 
109     @Override
onCreate()110     public boolean onCreate() {
111         mSearchEnabledByCategoryKeyMap = new ArrayMap<>();
112         return true;
113     }
114 
115     @Override
queryXmlResources(String[] projection)116     public Cursor queryXmlResources(String[] projection) {
117         final MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);
118         final List<SearchIndexableResource> resources =
119                 getSearchIndexableResourcesFromProvider(getContext());
120         for (SearchIndexableResource val : resources) {
121             final Object[] ref = new Object[INDEXABLES_XML_RES_COLUMNS.length];
122             ref[COLUMN_INDEX_XML_RES_RANK] = val.rank;
123             ref[COLUMN_INDEX_XML_RES_RESID] = val.xmlResId;
124             ref[COLUMN_INDEX_XML_RES_CLASS_NAME] = val.className;
125             ref[COLUMN_INDEX_XML_RES_ICON_RESID] = val.iconResId;
126             ref[COLUMN_INDEX_XML_RES_INTENT_ACTION] = val.intentAction;
127             ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE] = val.intentTargetPackage;
128             ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS] = null; // intent target class
129             cursor.addRow(ref);
130         }
131 
132         return cursor;
133     }
134 
135     /**
136      * Gets a Cursor of RawData. We use those data in search indexing time
137      */
138     @Override
queryRawData(String[] projection)139     public Cursor queryRawData(String[] projection) {
140         final MatrixCursor cursor = new MatrixCursor(INDEXABLES_RAW_COLUMNS);
141         final List<SearchIndexableRaw> raws = getSearchIndexableRawFromProvider(getContext());
142         for (SearchIndexableRaw val : raws) {
143             cursor.addRow(createIndexableRawColumnObjects(val));
144         }
145 
146         return cursor;
147     }
148 
149     /**
150      * Gets a combined list non-indexable keys that come from providers inside of settings.
151      * The non-indexable keys are used in Settings search at both index and update time to verify
152      * the validity of results in the database.
153      */
154     @Override
queryNonIndexableKeys(String[] projection)155     public Cursor queryNonIndexableKeys(String[] projection) {
156         final MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS);
157         final List<String> nonIndexableKeys = getNonIndexableKeysFromProvider(getContext());
158         for (String nik : nonIndexableKeys) {
159             final Object[] ref = new Object[NON_INDEXABLES_KEYS_COLUMNS.length];
160             ref[COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE] = nik;
161             cursor.addRow(ref);
162         }
163 
164         return cursor;
165     }
166 
167     /**
168      * Gets a Cursor of dynamic Raw data similar to queryRawData. We use those data in search query
169      * time
170      */
171     @Nullable
172     @Override
queryDynamicRawData(String[] projection)173     public Cursor queryDynamicRawData(String[] projection) {
174         final Context context = getContext();
175         final List<SearchIndexableRaw> rawList = new ArrayList<>();
176         final Collection<SearchIndexableData> bundles = FeatureFactory.getFeatureFactory()
177                 .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
178 
179         for (SearchIndexableData bundle : bundles) {
180             rawList.addAll(getDynamicSearchIndexableRawData(context, bundle));
181 
182             // Refresh the search enabled state for indexing injection raw data
183             final Indexable.SearchIndexProvider provider = bundle.getSearchIndexProvider();
184             if (provider instanceof BaseSearchIndexProvider) {
185                 refreshSearchEnabledState(context, (BaseSearchIndexProvider) provider);
186             }
187         }
188         rawList.addAll(getInjectionIndexableRawData(context));
189 
190         final MatrixCursor cursor = new MatrixCursor(INDEXABLES_RAW_COLUMNS);
191         for (SearchIndexableRaw raw : rawList) {
192             cursor.addRow(createIndexableRawColumnObjects(raw));
193         }
194 
195         return cursor;
196     }
197 
198     @Override
querySiteMapPairs()199     public Cursor querySiteMapPairs() {
200         final MatrixCursor cursor = new MatrixCursor(SITE_MAP_COLUMNS);
201         final Context context = getContext();
202         // Loop through all IA categories and pages and build additional SiteMapPairs
203         final List<DashboardCategory> categories = FeatureFactory.getFeatureFactory()
204                 .getDashboardFeatureProvider().getAllCategories();
205         for (DashboardCategory category : categories) {
206             // Use the category key to look up parent (which page hosts this key)
207             final String parentClass = CATEGORY_KEY_TO_PARENT_MAP.get(category.key);
208             if (parentClass == null) {
209                 continue;
210             }
211             // Build parent-child class pairs for all children listed under this key.
212             for (Tile tile : category.getTiles()) {
213                 String childClass = null;
214                 CharSequence childTitle = "";
215                 if (tile.getMetaData() != null) {
216                     childClass = tile.getMetaData().getString(
217                             SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS);
218                 }
219                 if (childClass == null) {
220                     childClass = tile.getComponentName();
221                     childTitle = tile.getTitle(getContext());
222                 }
223                 if (childClass == null) {
224                     continue;
225                 }
226                 cursor.newRow()
227                         .add(SearchIndexablesContract.SiteMapColumns.PARENT_CLASS, parentClass)
228                         .add(SearchIndexablesContract.SiteMapColumns.CHILD_CLASS, childClass)
229                         .add(SearchIndexablesContract.SiteMapColumns.CHILD_TITLE, childTitle);
230             }
231         }
232 
233         // Loop through custom site map registry to build additional SiteMapPairs
234         for (String childClass : CustomSiteMapRegistry.CUSTOM_SITE_MAP.keySet()) {
235             final String parentClass = CustomSiteMapRegistry.CUSTOM_SITE_MAP.get(childClass);
236             cursor.newRow()
237                     .add(SearchIndexablesContract.SiteMapColumns.PARENT_CLASS, parentClass)
238                     .add(SearchIndexablesContract.SiteMapColumns.CHILD_CLASS, childClass);
239         }
240         // Done.
241         return cursor;
242     }
243 
244     @Override
querySliceUriPairs()245     public Cursor querySliceUriPairs() {
246         final SliceViewManager manager = SliceViewManager.getInstance(getContext());
247         final MatrixCursor cursor = new MatrixCursor(SLICE_URI_PAIRS_COLUMNS);
248         final String queryUri = getContext().getString(R.string.config_non_public_slice_query_uri);
249         final Uri baseUri = !TextUtils.isEmpty(queryUri) ? Uri.parse(queryUri)
250                 : new Uri.Builder()
251                         .scheme(ContentResolver.SCHEME_CONTENT)
252                         .authority(SettingsSliceProvider.SLICE_AUTHORITY)
253                         .build();
254 
255         final Uri platformBaseUri =
256                 new Uri.Builder()
257                         .scheme(ContentResolver.SCHEME_CONTENT)
258                         .authority(SettingsSlicesContract.AUTHORITY)
259                         .build();
260 
261         final Collection<Uri> sliceUris = manager.getSliceDescendants(baseUri);
262         sliceUris.addAll(manager.getSliceDescendants(platformBaseUri));
263 
264         for (Uri uri : sliceUris) {
265             cursor.newRow()
266                     .add(SearchIndexablesContract.SliceUriPairColumns.KEY, uri.getLastPathSegment())
267                     .add(SearchIndexablesContract.SliceUriPairColumns.SLICE_URI, uri);
268         }
269 
270         return cursor;
271     }
272 
getNonIndexableKeysFromProvider(Context context)273     private List<String> getNonIndexableKeysFromProvider(Context context) {
274         final Collection<SearchIndexableData> bundles = FeatureFactory.getFeatureFactory()
275                 .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
276 
277         final List<String> nonIndexableKeys = new ArrayList<>();
278 
279         for (SearchIndexableData bundle : bundles) {
280             final long startTime = System.currentTimeMillis();
281             Indexable.SearchIndexProvider provider = bundle.getSearchIndexProvider();
282             List<String> providerNonIndexableKeys;
283             try {
284                 providerNonIndexableKeys = provider.getNonIndexableKeys(context);
285             } catch (Exception e) {
286                 // Catch a generic crash. In the absence of the catch, the background thread will
287                 // silently fail anyway, so we aren't losing information by catching the exception.
288                 // We crash when the system property exists so that we can test if crashes need to
289                 // be fixed.
290                 // The gain is that if there is a crash in a specific controller, we don't lose all
291                 // non-indexable keys, but we can still find specific crashes in development.
292                 if (System.getProperty(SYSPROP_CRASH_ON_ERROR) != null) {
293                     throw new RuntimeException(e);
294                 }
295                 Log.e(TAG, "Error trying to get non-indexable keys from: "
296                         + bundle.getTargetClass().getName(), 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