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