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.slice.SliceViewManager;
62 
63 import com.android.settings.SettingsActivity;
64 import com.android.settings.overlay.FeatureFactory;
65 import com.android.settings.slices.SettingsSliceProvider;
66 import com.android.settingslib.drawer.DashboardCategory;
67 import com.android.settingslib.drawer.Tile;
68 
69 import java.util.ArrayList;
70 import java.util.Collection;
71 import java.util.List;
72 
73 public class SettingsSearchIndexablesProvider extends SearchIndexablesProvider {
74 
75     public static final boolean DEBUG = false;
76 
77     /**
78      * Flag for a system property which checks if we should crash if there are issues in the
79      * indexing pipeline.
80      */
81     public static final String SYSPROP_CRASH_ON_ERROR =
82             "debug.com.android.settings.search.crash_on_error";
83 
84     private static final String TAG = "SettingsSearchProvider";
85 
86     private static final Collection<String> INVALID_KEYS;
87 
88     static {
89         INVALID_KEYS = new ArraySet<>();
90         INVALID_KEYS.add(null);
91         INVALID_KEYS.add("");
92     }
93 
94     @Override
onCreate()95     public boolean onCreate() {
96         return true;
97     }
98 
99     @Override
queryXmlResources(String[] projection)100     public Cursor queryXmlResources(String[] projection) {
101         MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);
102         final List<SearchIndexableResource> resources =
103                 getSearchIndexableResourcesFromProvider(getContext());
104         for (SearchIndexableResource val : resources) {
105             Object[] ref = new Object[INDEXABLES_XML_RES_COLUMNS.length];
106             ref[COLUMN_INDEX_XML_RES_RANK] = val.rank;
107             ref[COLUMN_INDEX_XML_RES_RESID] = val.xmlResId;
108             ref[COLUMN_INDEX_XML_RES_CLASS_NAME] = val.className;
109             ref[COLUMN_INDEX_XML_RES_ICON_RESID] = val.iconResId;
110             ref[COLUMN_INDEX_XML_RES_INTENT_ACTION] = val.intentAction;
111             ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE] = val.intentTargetPackage;
112             ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS] = null; // intent target class
113             cursor.addRow(ref);
114         }
115 
116         return cursor;
117     }
118 
119     @Override
queryRawData(String[] projection)120     public Cursor queryRawData(String[] projection) {
121         MatrixCursor cursor = new MatrixCursor(INDEXABLES_RAW_COLUMNS);
122         final List<SearchIndexableRaw> raws = getSearchIndexableRawFromProvider(getContext());
123         for (SearchIndexableRaw val : raws) {
124             Object[] ref = new Object[INDEXABLES_RAW_COLUMNS.length];
125             ref[COLUMN_INDEX_RAW_TITLE] = val.title;
126             ref[COLUMN_INDEX_RAW_SUMMARY_ON] = val.summaryOn;
127             ref[COLUMN_INDEX_RAW_SUMMARY_OFF] = val.summaryOff;
128             ref[COLUMN_INDEX_RAW_ENTRIES] = val.entries;
129             ref[COLUMN_INDEX_RAW_KEYWORDS] = val.keywords;
130             ref[COLUMN_INDEX_RAW_SCREEN_TITLE] = val.screenTitle;
131             ref[COLUMN_INDEX_RAW_CLASS_NAME] = val.className;
132             ref[COLUMN_INDEX_RAW_ICON_RESID] = val.iconResId;
133             ref[COLUMN_INDEX_RAW_INTENT_ACTION] = val.intentAction;
134             ref[COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE] = val.intentTargetPackage;
135             ref[COLUMN_INDEX_RAW_INTENT_TARGET_CLASS] = val.intentTargetClass;
136             ref[COLUMN_INDEX_RAW_KEY] = val.key;
137             ref[COLUMN_INDEX_RAW_USER_ID] = val.userId;
138             cursor.addRow(ref);
139         }
140 
141         return cursor;
142     }
143 
144     /**
145      * Gets a combined list non-indexable keys that come from providers inside of settings.
146      * The non-indexable keys are used in Settings search at both index and update time to verify
147      * the validity of results in the database.
148      */
149     @Override
queryNonIndexableKeys(String[] projection)150     public Cursor queryNonIndexableKeys(String[] projection) {
151         MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS);
152         final List<String> nonIndexableKeys = getNonIndexableKeysFromProvider(getContext());
153         for (String nik : nonIndexableKeys) {
154             final Object[] ref = new Object[NON_INDEXABLES_KEYS_COLUMNS.length];
155             ref[COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE] = nik;
156             cursor.addRow(ref);
157         }
158 
159         return cursor;
160     }
161 
162     @Override
querySiteMapPairs()163     public Cursor querySiteMapPairs() {
164         final MatrixCursor cursor = new MatrixCursor(SITE_MAP_COLUMNS);
165         final Context context = getContext();
166         // Loop through all IA categories and pages and build additional SiteMapPairs
167         final List<DashboardCategory> categories = FeatureFactory.getFactory(context)
168                 .getDashboardFeatureProvider(context).getAllCategories();
169         for (DashboardCategory category : categories) {
170             // Use the category key to look up parent (which page hosts this key)
171             final String parentClass = CATEGORY_KEY_TO_PARENT_MAP.get(category.key);
172             if (parentClass == null) {
173                 continue;
174             }
175             // Build parent-child class pairs for all children listed under this key.
176             for (Tile tile : category.getTiles()) {
177                 String childClass = null;
178                 if (tile.getMetaData() != null) {
179                     childClass = tile.getMetaData().getString(
180                             SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS);
181                 }
182                 if (childClass == null) {
183                     continue;
184                 }
185                 cursor.newRow()
186                         .add(SearchIndexablesContract.SiteMapColumns.PARENT_CLASS, parentClass)
187                         .add(SearchIndexablesContract.SiteMapColumns.CHILD_CLASS, childClass);
188             }
189         }
190         // Done.
191         return cursor;
192     }
193 
194     @Override
querySliceUriPairs()195     public Cursor querySliceUriPairs() {
196         final SliceViewManager manager = SliceViewManager.getInstance(getContext());
197         final MatrixCursor cursor = new MatrixCursor(SLICE_URI_PAIRS_COLUMNS);
198         final Uri baseUri =
199                 new Uri.Builder()
200                         .scheme(ContentResolver.SCHEME_CONTENT)
201                         .authority(SettingsSliceProvider.SLICE_AUTHORITY)
202                         .build();
203         final Uri platformBaseUri =
204                 new Uri.Builder()
205                         .scheme(ContentResolver.SCHEME_CONTENT)
206                         .authority(SettingsSlicesContract.AUTHORITY)
207                         .build();
208 
209         final Collection<Uri> sliceUris = manager.getSliceDescendants(baseUri);
210         sliceUris.addAll(manager.getSliceDescendants(platformBaseUri));
211 
212         for (Uri uri : sliceUris) {
213             cursor.newRow()
214                     .add(SearchIndexablesContract.SliceUriPairColumns.KEY, uri.getLastPathSegment())
215                     .add(SearchIndexablesContract.SliceUriPairColumns.SLICE_URI, uri);
216         }
217 
218         return cursor;
219     }
220 
getNonIndexableKeysFromProvider(Context context)221     private List<String> getNonIndexableKeysFromProvider(Context context) {
222         final Collection<Class> values = FeatureFactory.getFactory(context)
223                 .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
224         final List<String> nonIndexableKeys = new ArrayList<>();
225 
226         for (Class<?> clazz : values) {
227             final long startTime = System.currentTimeMillis();
228             Indexable.SearchIndexProvider provider = DatabaseIndexingUtils.getSearchIndexProvider(
229                     clazz);
230 
231             List<String> providerNonIndexableKeys;
232             try {
233                 providerNonIndexableKeys = provider.getNonIndexableKeys(context);
234             } catch (Exception e) {
235                 // Catch a generic crash. In the absence of the catch, the background thread will
236                 // silently fail anyway, so we aren't losing information by catching the exception.
237                 // We crash when the system property exists so that we can test if crashes need to
238                 // be fixed.
239                 // The gain is that if there is a crash in a specific controller, we don't lose all
240                 // non-indexable keys, but we can still find specific crashes in development.
241                 if (System.getProperty(SYSPROP_CRASH_ON_ERROR) != null) {
242                     throw new RuntimeException(e);
243                 }
244                 Log.e(TAG, "Error trying to get non-indexable keys from: " + clazz.getName(), e);
245                 continue;
246             }
247 
248             if (providerNonIndexableKeys == null || providerNonIndexableKeys.isEmpty()) {
249                 if (DEBUG) {
250                     final long totalTime = System.currentTimeMillis() - startTime;
251                     Log.d(TAG, "No indexable, total time " + totalTime);
252                 }
253                 continue;
254             }
255 
256             if (providerNonIndexableKeys.removeAll(INVALID_KEYS)) {
257                 Log.v(TAG, provider + " tried to add an empty non-indexable key");
258             }
259 
260             if (DEBUG) {
261                 final long totalTime = System.currentTimeMillis() - startTime;
262                 Log.d(TAG, "Non-indexables " + providerNonIndexableKeys.size() + ", total time "
263                         + totalTime);
264             }
265 
266             nonIndexableKeys.addAll(providerNonIndexableKeys);
267         }
268 
269         return nonIndexableKeys;
270     }
271 
getSearchIndexableResourcesFromProvider(Context context)272     private List<SearchIndexableResource> getSearchIndexableResourcesFromProvider(Context context) {
273         Collection<Class> values = FeatureFactory.getFactory(context)
274                 .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
275         List<SearchIndexableResource> resourceList = new ArrayList<>();
276 
277         for (Class<?> clazz : values) {
278             Indexable.SearchIndexProvider provider = DatabaseIndexingUtils.getSearchIndexProvider(
279                     clazz);
280 
281             final List<SearchIndexableResource> resList =
282                     provider.getXmlResourcesToIndex(context, true);
283 
284             if (resList == null) {
285                 continue;
286             }
287 
288             for (SearchIndexableResource item : resList) {
289                 item.className = TextUtils.isEmpty(item.className)
290                         ? clazz.getName()
291                         : item.className;
292             }
293 
294             resourceList.addAll(resList);
295         }
296 
297         return resourceList;
298     }
299 
getSearchIndexableRawFromProvider(Context context)300     private List<SearchIndexableRaw> getSearchIndexableRawFromProvider(Context context) {
301         final Collection<Class> values = FeatureFactory.getFactory(context)
302                 .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
303         final List<SearchIndexableRaw> rawList = new ArrayList<>();
304 
305         for (Class<?> clazz : values) {
306             Indexable.SearchIndexProvider provider = DatabaseIndexingUtils.getSearchIndexProvider(
307                     clazz);
308             final List<SearchIndexableRaw> providerRaws = provider.getRawDataToIndex(context,
309                     true /* enabled */);
310 
311             if (providerRaws == null) {
312                 continue;
313             }
314 
315             for (SearchIndexableRaw raw : providerRaws) {
316                 // The classname and intent information comes from the PreIndexData
317                 // This will be more clear when provider conversion is done at PreIndex time.
318                 raw.className = clazz.getName();
319 
320             }
321             rawList.addAll(providerRaws);
322         }
323 
324         return rawList;
325     }
326 }
327