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