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