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.ArraySet;
59 import android.util.Log;
60 
61 import androidx.annotation.Nullable;
62 import androidx.slice.SliceViewManager;
63 
64 import com.android.internal.annotations.VisibleForTesting;
65 import com.android.settings.R;
66 import com.android.settings.SettingsActivity;
67 import com.android.settings.dashboard.DashboardFeatureProvider;
68 import com.android.settings.overlay.FeatureFactory;
69 import com.android.settings.slices.SettingsSliceProvider;
70 import com.android.settingslib.drawer.ActivityTile;
71 import com.android.settingslib.drawer.CategoryKey;
72 import com.android.settingslib.drawer.DashboardCategory;
73 import com.android.settingslib.drawer.Tile;
74 import com.android.settingslib.search.Indexable;
75 import com.android.settingslib.search.SearchIndexableData;
76 import com.android.settingslib.search.SearchIndexableRaw;
77 
78 import java.util.ArrayList;
79 import java.util.Collection;
80 import java.util.List;
81 
82 public class SettingsSearchIndexablesProvider extends SearchIndexablesProvider {
83 
84     public static final boolean DEBUG = false;
85 
86     /**
87      * Flag for a system property which checks if we should crash if there are issues in the
88      * indexing pipeline.
89      */
90     public static final String SYSPROP_CRASH_ON_ERROR =
91             "debug.com.android.settings.search.crash_on_error";
92 
93     private static final String TAG = "SettingsSearchProvider";
94 
95     private static final Collection<String> INVALID_KEYS;
96 
97     static {
98         INVALID_KEYS = new ArraySet<>();
99         INVALID_KEYS.add(null);
100         INVALID_KEYS.add("");
101     }
102 
103     @Override
onCreate()104     public boolean onCreate() {
105         return true;
106     }
107 
108     @Override
queryXmlResources(String[] projection)109     public Cursor queryXmlResources(String[] projection) {
110         final MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);
111         final List<SearchIndexableResource> resources =
112                 getSearchIndexableResourcesFromProvider(getContext());
113         for (SearchIndexableResource val : resources) {
114             final Object[] ref = new Object[INDEXABLES_XML_RES_COLUMNS.length];
115             ref[COLUMN_INDEX_XML_RES_RANK] = val.rank;
116             ref[COLUMN_INDEX_XML_RES_RESID] = val.xmlResId;
117             ref[COLUMN_INDEX_XML_RES_CLASS_NAME] = val.className;
118             ref[COLUMN_INDEX_XML_RES_ICON_RESID] = val.iconResId;
119             ref[COLUMN_INDEX_XML_RES_INTENT_ACTION] = val.intentAction;
120             ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE] = val.intentTargetPackage;
121             ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS] = null; // intent target class
122             cursor.addRow(ref);
123         }
124 
125         return cursor;
126     }
127 
128     /**
129      * Gets a Cursor of RawData. We use those data in search indexing time
130      */
131     @Override
queryRawData(String[] projection)132     public Cursor queryRawData(String[] projection) {
133         final MatrixCursor cursor = new MatrixCursor(INDEXABLES_RAW_COLUMNS);
134         final List<SearchIndexableRaw> raws = getSearchIndexableRawFromProvider(getContext());
135         for (SearchIndexableRaw val : raws) {
136             cursor.addRow(createIndexableRawColumnObjects(val));
137         }
138 
139         return cursor;
140     }
141 
142     /**
143      * Gets a combined list non-indexable keys that come from providers inside of settings.
144      * The non-indexable keys are used in Settings search at both index and update time to verify
145      * the validity of results in the database.
146      */
147     @Override
queryNonIndexableKeys(String[] projection)148     public Cursor queryNonIndexableKeys(String[] projection) {
149         final MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS);
150         final List<String> nonIndexableKeys = getNonIndexableKeysFromProvider(getContext());
151         for (String nik : nonIndexableKeys) {
152             final Object[] ref = new Object[NON_INDEXABLES_KEYS_COLUMNS.length];
153             ref[COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE] = nik;
154             cursor.addRow(ref);
155         }
156 
157         return cursor;
158     }
159 
160     /**
161      * Gets a Cursor of dynamic Raw data similar to queryRawData. We use those data in search query
162      * time
163      */
164     @Nullable
165     @Override
queryDynamicRawData(String[] projection)166     public Cursor queryDynamicRawData(String[] projection) {
167         final Context context = getContext();
168         final List<SearchIndexableRaw> rawList = new ArrayList<>();
169         rawList.addAll(getDynamicSearchIndexableRawFromProvider(context));
170         rawList.addAll(getInjectionIndexableRawData(context));
171 
172         final MatrixCursor cursor = new MatrixCursor(INDEXABLES_RAW_COLUMNS);
173         for (SearchIndexableRaw raw : rawList) {
174             cursor.addRow(createIndexableRawColumnObjects(raw));
175         }
176 
177         return cursor;
178     }
179 
180     @Override
querySiteMapPairs()181     public Cursor querySiteMapPairs() {
182         final MatrixCursor cursor = new MatrixCursor(SITE_MAP_COLUMNS);
183         final Context context = getContext();
184         // Loop through all IA categories and pages and build additional SiteMapPairs
185         final List<DashboardCategory> categories = FeatureFactory.getFactory(context)
186                 .getDashboardFeatureProvider(context).getAllCategories();
187         for (DashboardCategory category : categories) {
188             // Use the category key to look up parent (which page hosts this key)
189             final String parentClass = CATEGORY_KEY_TO_PARENT_MAP.get(category.key);
190             if (parentClass == null) {
191                 continue;
192             }
193             // Build parent-child class pairs for all children listed under this key.
194             for (Tile tile : category.getTiles()) {
195                 String childClass = null;
196                 CharSequence childTitle = "";
197                 if (tile.getMetaData() != null) {
198                     childClass = tile.getMetaData().getString(
199                             SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS);
200                 }
201                 if (childClass == null) {
202                     childClass = tile.getComponentName();
203                     childTitle = tile.getTitle(getContext());
204                 }
205                 if (childClass == null) {
206                     continue;
207                 }
208                 cursor.newRow()
209                         .add(SearchIndexablesContract.SiteMapColumns.PARENT_CLASS, parentClass)
210                         .add(SearchIndexablesContract.SiteMapColumns.CHILD_CLASS, childClass)
211                         .add(SearchIndexablesContract.SiteMapColumns.CHILD_TITLE, childTitle);
212             }
213         }
214 
215         // Loop through custom site map registry to build additional SiteMapPairs
216         for (String childClass : CustomSiteMapRegistry.CUSTOM_SITE_MAP.keySet()) {
217             final String parentClass = CustomSiteMapRegistry.CUSTOM_SITE_MAP.get(childClass);
218             cursor.newRow()
219                     .add(SearchIndexablesContract.SiteMapColumns.PARENT_CLASS, parentClass)
220                     .add(SearchIndexablesContract.SiteMapColumns.CHILD_CLASS, childClass);
221         }
222         // Done.
223         return cursor;
224     }
225 
226     @Override
querySliceUriPairs()227     public Cursor querySliceUriPairs() {
228         final SliceViewManager manager = SliceViewManager.getInstance(getContext());
229         final MatrixCursor cursor = new MatrixCursor(SLICE_URI_PAIRS_COLUMNS);
230         final String queryUri = getContext().getString(R.string.config_non_public_slice_query_uri);
231         final Uri baseUri = !TextUtils.isEmpty(queryUri) ? Uri.parse(queryUri)
232                 : new Uri.Builder()
233                         .scheme(ContentResolver.SCHEME_CONTENT)
234                         .authority(SettingsSliceProvider.SLICE_AUTHORITY)
235                         .build();
236 
237         final Uri platformBaseUri =
238                 new Uri.Builder()
239                         .scheme(ContentResolver.SCHEME_CONTENT)
240                         .authority(SettingsSlicesContract.AUTHORITY)
241                         .build();
242 
243         final Collection<Uri> sliceUris = manager.getSliceDescendants(baseUri);
244         sliceUris.addAll(manager.getSliceDescendants(platformBaseUri));
245 
246         for (Uri uri : sliceUris) {
247             cursor.newRow()
248                     .add(SearchIndexablesContract.SliceUriPairColumns.KEY, uri.getLastPathSegment())
249                     .add(SearchIndexablesContract.SliceUriPairColumns.SLICE_URI, uri);
250         }
251 
252         return cursor;
253     }
254 
getNonIndexableKeysFromProvider(Context context)255     private List<String> getNonIndexableKeysFromProvider(Context context) {
256         final Collection<SearchIndexableData> bundles = FeatureFactory.getFactory(context)
257                 .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
258 
259         final List<String> nonIndexableKeys = new ArrayList<>();
260 
261         for (SearchIndexableData bundle : bundles) {
262             final long startTime = System.currentTimeMillis();
263             Indexable.SearchIndexProvider provider = bundle.getSearchIndexProvider();
264             List<String> providerNonIndexableKeys;
265             try {
266                 providerNonIndexableKeys = provider.getNonIndexableKeys(context);
267             } catch (Exception e) {
268                 // Catch a generic crash. In the absence of the catch, the background thread will
269                 // silently fail anyway, so we aren't losing information by catching the exception.
270                 // We crash when the system property exists so that we can test if crashes need to
271                 // be fixed.
272                 // The gain is that if there is a crash in a specific controller, we don't lose all
273                 // non-indexable keys, but we can still find specific crashes in development.
274                 if (System.getProperty(SYSPROP_CRASH_ON_ERROR) != null) {
275                     throw new RuntimeException(e);
276                 }
277                 Log.e(TAG, "Error trying to get non-indexable keys from: "
278                         + bundle.getTargetClass().getName(), e);
279                 continue;
280             }
281 
282             if (providerNonIndexableKeys == null || providerNonIndexableKeys.isEmpty()) {
283                 if (DEBUG) {
284                     final long totalTime = System.currentTimeMillis() - startTime;
285                     Log.d(TAG, "No indexable, total time " + totalTime);
286                 }
287                 continue;
288             }
289 
290             if (providerNonIndexableKeys.removeAll(INVALID_KEYS)) {
291                 Log.v(TAG, provider + " tried to add an empty non-indexable key");
292             }
293 
294             if (DEBUG) {
295                 final long totalTime = System.currentTimeMillis() - startTime;
296                 Log.d(TAG, "Non-indexables " + providerNonIndexableKeys.size() + ", total time "
297                         + totalTime);
298             }
299 
300             nonIndexableKeys.addAll(providerNonIndexableKeys);
301         }
302 
303         return nonIndexableKeys;
304     }
305 
getSearchIndexableResourcesFromProvider(Context context)306     private List<SearchIndexableResource> getSearchIndexableResourcesFromProvider(Context context) {
307         final Collection<SearchIndexableData> bundles = FeatureFactory.getFactory(context)
308                 .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
309         List<SearchIndexableResource> resourceList = new ArrayList<>();
310 
311         for (SearchIndexableData bundle : bundles) {
312             Indexable.SearchIndexProvider provider = bundle.getSearchIndexProvider();
313             final List<SearchIndexableResource> resList =
314                     provider.getXmlResourcesToIndex(context, true);
315 
316             if (resList == null) {
317                 continue;
318             }
319 
320             for (SearchIndexableResource item : resList) {
321                 item.className = TextUtils.isEmpty(item.className)
322                         ? bundle.getTargetClass().getName()
323                         : item.className;
324             }
325 
326             resourceList.addAll(resList);
327         }
328 
329         return resourceList;
330     }
331 
getSearchIndexableRawFromProvider(Context context)332     private List<SearchIndexableRaw> getSearchIndexableRawFromProvider(Context context) {
333         final Collection<SearchIndexableData> bundles = FeatureFactory.getFactory(context)
334                 .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
335         final List<SearchIndexableRaw> rawList = new ArrayList<>();
336 
337         for (SearchIndexableData bundle : bundles) {
338             Indexable.SearchIndexProvider provider = bundle.getSearchIndexProvider();
339             final List<SearchIndexableRaw> providerRaws = provider.getRawDataToIndex(context,
340                     true /* enabled */);
341 
342             if (providerRaws == null) {
343                 continue;
344             }
345 
346             for (SearchIndexableRaw raw : providerRaws) {
347                 // The classname and intent information comes from the PreIndexData
348                 // This will be more clear when provider conversion is done at PreIndex time.
349                 raw.className = bundle.getTargetClass().getName();
350 
351             }
352             rawList.addAll(providerRaws);
353         }
354 
355         return rawList;
356     }
357 
getDynamicSearchIndexableRawFromProvider(Context context)358     private List<SearchIndexableRaw> getDynamicSearchIndexableRawFromProvider(Context context) {
359         final Collection<SearchIndexableData> bundles = FeatureFactory.getFactory(context)
360                 .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
361         final List<SearchIndexableRaw> rawList = new ArrayList<>();
362 
363         for (SearchIndexableData bundle : bundles) {
364             final Indexable.SearchIndexProvider provider = bundle.getSearchIndexProvider();
365             final List<SearchIndexableRaw> providerRaws =
366                     provider.getDynamicRawDataToIndex(context, true /* enabled */);
367 
368             if (providerRaws == null) {
369                 continue;
370             }
371 
372             for (SearchIndexableRaw raw : providerRaws) {
373                 // The classname and intent information comes from the PreIndexData
374                 // This will be more clear when provider conversion is done at PreIndex time.
375                 raw.className = bundle.getTargetClass().getName();
376 
377             }
378             rawList.addAll(providerRaws);
379         }
380 
381         return rawList;
382     }
383 
getInjectionIndexableRawData(Context context)384     private List<SearchIndexableRaw> getInjectionIndexableRawData(Context context) {
385         final DashboardFeatureProvider dashboardFeatureProvider =
386                 FeatureFactory.getFactory(context).getDashboardFeatureProvider(context);
387 
388         final List<SearchIndexableRaw> rawList = new ArrayList<>();
389         final String currentPackageName = context.getPackageName();
390         for (DashboardCategory category : dashboardFeatureProvider.getAllCategories()) {
391             for (Tile tile : category.getTiles()) {
392                 if (!isEligibleForIndexing(currentPackageName, tile)) {
393                     continue;
394                 }
395                 final SearchIndexableRaw raw = new SearchIndexableRaw(context);
396                 final CharSequence title = tile.getTitle(context);
397                 raw.title = TextUtils.isEmpty(title) ? null : title.toString();
398                 if (TextUtils.isEmpty(raw.title)) {
399                     continue;
400                 }
401                 raw.key = dashboardFeatureProvider.getDashboardKeyForTile(tile);
402                 final CharSequence summary = tile.getSummary(context);
403                 raw.summaryOn = TextUtils.isEmpty(summary) ? null : summary.toString();
404                 raw.summaryOff = raw.summaryOn;
405                 raw.className = CATEGORY_KEY_TO_PARENT_MAP.get(tile.getCategory());
406                 rawList.add(raw);
407             }
408         }
409 
410         return rawList;
411     }
412 
413     @VisibleForTesting
isEligibleForIndexing(String packageName, Tile tile)414     boolean isEligibleForIndexing(String packageName, Tile tile) {
415         if (TextUtils.equals(packageName, tile.getPackageName())
416                 && tile instanceof ActivityTile) {
417             // Skip Settings injected items because they should be indexed in the sub-pages.
418             return false;
419         }
420         if (TextUtils.equals(tile.getCategory(), CategoryKey.CATEGORY_HOMEPAGE)) {
421             // Skip homepage injected items since we would like to index their target activity.
422             return false;
423         }
424         return true;
425     }
426 
createIndexableRawColumnObjects(SearchIndexableRaw raw)427     private static Object[] createIndexableRawColumnObjects(SearchIndexableRaw raw) {
428         final Object[] ref = new Object[INDEXABLES_RAW_COLUMNS.length];
429         ref[COLUMN_INDEX_RAW_TITLE] = raw.title;
430         ref[COLUMN_INDEX_RAW_SUMMARY_ON] = raw.summaryOn;
431         ref[COLUMN_INDEX_RAW_SUMMARY_OFF] = raw.summaryOff;
432         ref[COLUMN_INDEX_RAW_ENTRIES] = raw.entries;
433         ref[COLUMN_INDEX_RAW_KEYWORDS] = raw.keywords;
434         ref[COLUMN_INDEX_RAW_SCREEN_TITLE] = raw.screenTitle;
435         ref[COLUMN_INDEX_RAW_CLASS_NAME] = raw.className;
436         ref[COLUMN_INDEX_RAW_ICON_RESID] = raw.iconResId;
437         ref[COLUMN_INDEX_RAW_INTENT_ACTION] = raw.intentAction;
438         ref[COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE] = raw.intentTargetPackage;
439         ref[COLUMN_INDEX_RAW_INTENT_TARGET_CLASS] = raw.intentTargetClass;
440         ref[COLUMN_INDEX_RAW_KEY] = raw.key;
441         ref[COLUMN_INDEX_RAW_USER_ID] = raw.userId;
442         return ref;
443     }
444 }
445