DatabaseIndexingManager.java revision ab741bb62756e27457632c841460f2d5b05957c3
1/* 2 * Copyright (C) 2017 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 18package com.android.settings.search; 19 20import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_ID; 21import static com.android.settings.search.DatabaseResultLoader 22 .COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE; 23import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_KEY; 24import static com.android.settings.search.DatabaseResultLoader.SELECT_COLUMNS; 25import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DOCID; 26import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.CLASS_NAME; 27import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_ENTRIES; 28import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS; 29import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_KEY_REF; 30import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON; 31import static com.android.settings.search.IndexDatabaseHelper.IndexColumns 32 .DATA_SUMMARY_ON_NORMALIZED; 33import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE; 34import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED; 35import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ENABLED; 36import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ICON; 37import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_ACTION; 38import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_CLASS; 39import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_PACKAGE; 40import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.LOCALE; 41import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.PAYLOAD; 42import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.PAYLOAD_TYPE; 43import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.SCREEN_TITLE; 44import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.USER_ID; 45import static com.android.settings.search.IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX; 46 47import android.content.ContentValues; 48import android.content.Context; 49import android.content.Intent; 50import android.content.pm.ResolveInfo; 51import android.database.Cursor; 52import android.database.sqlite.SQLiteDatabase; 53import android.database.sqlite.SQLiteException; 54import android.os.AsyncTask; 55import android.os.Build; 56import android.provider.SearchIndexableResource; 57import android.provider.SearchIndexablesContract; 58import android.support.annotation.VisibleForTesting; 59import android.text.TextUtils; 60import android.util.Log; 61 62import com.android.settings.overlay.FeatureFactory; 63 64import com.android.settings.search.indexing.IndexData; 65import com.android.settings.search.indexing.IndexDataConverter; 66import com.android.settings.search.indexing.PreIndexData; 67import com.android.settings.search.indexing.PreIndexDataCollector; 68 69import java.util.List; 70import java.util.Locale; 71import java.util.Map; 72import java.util.Objects; 73import java.util.Set; 74import java.util.concurrent.atomic.AtomicBoolean; 75 76/** 77 * Consumes the SearchIndexableProvider content providers. 78 * Updates the Resource, Raw Data and non-indexable data for Search. 79 * 80 * TODO(b/33577327) this class needs to be refactored by moving most of its methods into controllers 81 */ 82public class DatabaseIndexingManager { 83 84 private static final String LOG_TAG = "DatabaseIndexingManager"; 85 86 private static final String METRICS_ACTION_SETTINGS_ASYNC_INDEX = 87 "search_asynchronous_indexing"; 88 89 public static final String FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER = 90 "SEARCH_INDEX_DATA_PROVIDER"; 91 92 @VisibleForTesting 93 final AtomicBoolean mIsIndexingComplete = new AtomicBoolean(false); 94 95 private PreIndexDataCollector mCollector; 96 private IndexDataConverter mConverter; 97 98 private Context mContext; 99 100 public DatabaseIndexingManager(Context context) { 101 mContext = context; 102 } 103 104 public boolean isIndexingComplete() { 105 return mIsIndexingComplete.get(); 106 } 107 108 public void indexDatabase(IndexingCallback callback) { 109 IndexingTask task = new IndexingTask(callback); 110 task.execute(); 111 } 112 113 /** 114 * Accumulate all data and non-indexable keys from each of the content-providers. 115 * Only the first indexing for the default language gets static search results - subsequent 116 * calls will only gather non-indexable keys. 117 */ 118 public void performIndexing() { 119 final long startTime = System.currentTimeMillis(); 120 final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE); 121 final List<ResolveInfo> providers = 122 mContext.getPackageManager().queryIntentContentProviders(intent, 0); 123 124 final String localeStr = Locale.getDefault().toString(); 125 final String fingerprint = Build.FINGERPRINT; 126 final String providerVersionedNames = 127 IndexDatabaseHelper.buildProviderVersionedNames(providers); 128 129 final boolean isFullIndex = isFullIndex(mContext, localeStr, fingerprint, 130 providerVersionedNames); 131 132 if (isFullIndex) { 133 rebuildDatabase(); 134 } 135 136 PreIndexData indexData = getIndexDataFromProviders(providers, isFullIndex); 137 138 final long updateDatabaseStartTime = System.currentTimeMillis(); 139 updateDatabase(indexData, isFullIndex, localeStr); 140 if (SettingsSearchIndexablesProvider.DEBUG) { 141 final long updateDatabaseTime = System.currentTimeMillis() - updateDatabaseStartTime; 142 Log.d(LOG_TAG, "performIndexing updateDatabase took time: " + updateDatabaseTime); 143 } 144 145 //TODO(63922686): Setting indexed should be a single method, not 3 separate setters. 146 IndexDatabaseHelper.setLocaleIndexed(mContext, localeStr); 147 IndexDatabaseHelper.setBuildIndexed(mContext, fingerprint); 148 IndexDatabaseHelper.setProvidersIndexed(mContext, providerVersionedNames); 149 150 if (SettingsSearchIndexablesProvider.DEBUG) { 151 final long indexingTime = System.currentTimeMillis() - startTime; 152 Log.d(LOG_TAG, "performIndexing took time: " + indexingTime 153 + "ms. Full index? " + isFullIndex); 154 } 155 } 156 157 @VisibleForTesting 158 PreIndexData getIndexDataFromProviders(List<ResolveInfo> providers, boolean isFullIndex) { 159 if (mCollector == null) { 160 mCollector = new PreIndexDataCollector(mContext); 161 } 162 return mCollector.collectIndexableData(providers, isFullIndex); 163 } 164 165 /** 166 * Checks if the indexed data is obsolete, when either: 167 * - Device language has changed 168 * - Device has taken an OTA. 169 * In both cases, the device requires a full index. 170 * 171 * @param locale is the default for the device 172 * @param fingerprint id for the current build. 173 * @return true if a full index should be preformed. 174 */ 175 @VisibleForTesting 176 boolean isFullIndex(Context context, String locale, String fingerprint, 177 String providerVersionedNames) { 178 final boolean isLocaleIndexed = IndexDatabaseHelper.isLocaleAlreadyIndexed(context, locale); 179 final boolean isBuildIndexed = IndexDatabaseHelper.isBuildIndexed(context, fingerprint); 180 final boolean areProvidersIndexed = IndexDatabaseHelper 181 .areProvidersIndexed(context, providerVersionedNames); 182 183 return !(isLocaleIndexed && isBuildIndexed && areProvidersIndexed); 184 } 185 186 /** 187 * Drop the currently stored database, and clear the flags which mark the database as indexed. 188 */ 189 private void rebuildDatabase() { 190 // Drop the database when the locale or build has changed. This eliminates rows which are 191 // dynamically inserted in the old language, or deprecated settings. 192 final SQLiteDatabase db = getWritableDatabase(); 193 IndexDatabaseHelper.getInstance(mContext).reconstruct(db); 194 } 195 196 /** 197 * Adds new data to the database and verifies the correctness of the ENABLED column. 198 * First, the data to be updated and all non-indexable keys are copied locally. 199 * Then all new data to be added is inserted. 200 * Then search results are verified to have the correct value of enabled. 201 * Finally, we record that the locale has been indexed. 202 * 203 * @param needsReindexing true the database needs to be rebuilt. 204 * @param localeStr the default locale for the device. 205 */ 206 @VisibleForTesting 207 void updateDatabase(PreIndexData preIndexData, boolean needsReindexing, String localeStr) { 208 final Map<String, Set<String>> nonIndexableKeys = preIndexData.nonIndexableKeys; 209 210 final SQLiteDatabase database = getWritableDatabase(); 211 if (database == null) { 212 Log.w(LOG_TAG, "Cannot indexDatabase Index as I cannot get a writable database"); 213 return; 214 } 215 216 try { 217 database.beginTransaction(); 218 219 // Convert all Pre-index data to Index data. 220 List<IndexData> indexData = getIndexData(localeStr, preIndexData); 221 insertIndexData(database, indexData); 222 223 // Only check for non-indexable key updates after initial index. 224 // Enabled state with non-indexable keys is checked when items are first inserted. 225 if (!needsReindexing) { 226 updateDataInDatabase(database, nonIndexableKeys); 227 } 228 229 database.setTransactionSuccessful(); 230 } finally { 231 database.endTransaction(); 232 } 233 } 234 235 @VisibleForTesting 236 List<IndexData> getIndexData(String locale, PreIndexData data) { 237 if (mConverter == null) { 238 mConverter = new IndexDataConverter(mContext, locale); 239 } 240 return mConverter.convertPreIndexDataToIndexData(data); 241 } 242 243 /** 244 * Inserts all of the entries in {@param indexData} into the {@param database} 245 * as Search Data and as part of the Information Hierarchy. 246 */ 247 @VisibleForTesting 248 void insertIndexData(SQLiteDatabase database, List<IndexData> indexData) { 249 ContentValues values; 250 251 for (IndexData dataRow : indexData) { 252 if (TextUtils.isEmpty(dataRow.normalizedTitle)) { 253 continue; 254 } 255 256 values = new ContentValues(); 257 values.put(IndexDatabaseHelper.IndexColumns.DOCID, dataRow.getDocId()); 258 values.put(LOCALE, dataRow.locale); 259 values.put(DATA_TITLE, dataRow.updatedTitle); 260 values.put(DATA_TITLE_NORMALIZED, dataRow.normalizedTitle); 261 values.put(DATA_SUMMARY_ON, dataRow.updatedSummaryOn); 262 values.put(DATA_SUMMARY_ON_NORMALIZED, dataRow.normalizedSummaryOn); 263 values.put(DATA_ENTRIES, dataRow.entries); 264 values.put(DATA_KEYWORDS, dataRow.spaceDelimitedKeywords); 265 values.put(CLASS_NAME, dataRow.className); 266 values.put(SCREEN_TITLE, dataRow.screenTitle); 267 values.put(INTENT_ACTION, dataRow.intentAction); 268 values.put(INTENT_TARGET_PACKAGE, dataRow.intentTargetPackage); 269 values.put(INTENT_TARGET_CLASS, dataRow.intentTargetClass); 270 values.put(ICON, dataRow.iconResId); 271 values.put(ENABLED, dataRow.enabled); 272 values.put(DATA_KEY_REF, dataRow.key); 273 values.put(USER_ID, dataRow.userId); 274 values.put(PAYLOAD_TYPE, dataRow.payloadType); 275 values.put(PAYLOAD, dataRow.payload); 276 277 database.replaceOrThrow(TABLE_PREFS_INDEX, null, values); 278 279 if (!TextUtils.isEmpty(dataRow.className) 280 && !TextUtils.isEmpty(dataRow.childClassName)) { 281 ContentValues siteMapPair = new ContentValues(); 282 final int pairDocId = Objects.hash(dataRow.className, dataRow.childClassName); 283 siteMapPair.put(IndexDatabaseHelper.SiteMapColumns.DOCID, pairDocId); 284 siteMapPair.put(IndexDatabaseHelper.SiteMapColumns.PARENT_CLASS, 285 dataRow.className); 286 siteMapPair.put(IndexDatabaseHelper.SiteMapColumns.PARENT_TITLE, 287 dataRow.screenTitle); 288 siteMapPair.put(IndexDatabaseHelper.SiteMapColumns.CHILD_CLASS, 289 dataRow.childClassName); 290 siteMapPair.put(IndexDatabaseHelper.SiteMapColumns.CHILD_TITLE, 291 dataRow.updatedTitle); 292 293 database.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_SITE_MAP, 294 null /* nullColumnHack */, siteMapPair); 295 } 296 } 297 } 298 299 /** 300 * Upholds the validity of enabled data for the user. 301 * All rows which are enabled but are now flagged with non-indexable keys will become disabled. 302 * All rows which are disabled but no longer a non-indexable key will become enabled. 303 * 304 * @param database The database to validate. 305 * @param nonIndexableKeys A map between package name and the set of non-indexable keys for it. 306 */ 307 @VisibleForTesting 308 void updateDataInDatabase(SQLiteDatabase database, 309 Map<String, Set<String>> nonIndexableKeys) { 310 final String whereEnabled = ENABLED + " = 1"; 311 final String whereDisabled = ENABLED + " = 0"; 312 313 final Cursor enabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS, 314 whereEnabled, null, null, null, null); 315 316 final ContentValues enabledToDisabledValue = new ContentValues(); 317 enabledToDisabledValue.put(ENABLED, 0); 318 319 String packageName; 320 // TODO Refactor: Move these two loops into one method. 321 while (enabledResults.moveToNext()) { 322 // Package name is the key for remote providers. 323 // If package name is null, the provider is Settings. 324 packageName = enabledResults.getString(COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE); 325 if (packageName == null) { 326 packageName = mContext.getPackageName(); 327 } 328 329 final String key = enabledResults.getString(COLUMN_INDEX_KEY); 330 final Set<String> packageKeys = nonIndexableKeys.get(packageName); 331 332 // The indexed item is set to Enabled but is now non-indexable 333 if (packageKeys != null && packageKeys.contains(key)) { 334 final String whereClause = DOCID + " = " + enabledResults.getInt(COLUMN_INDEX_ID); 335 database.update(TABLE_PREFS_INDEX, enabledToDisabledValue, whereClause, null); 336 } 337 } 338 enabledResults.close(); 339 340 final Cursor disabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS, 341 whereDisabled, null, null, null, null); 342 343 final ContentValues disabledToEnabledValue = new ContentValues(); 344 disabledToEnabledValue.put(ENABLED, 1); 345 346 while (disabledResults.moveToNext()) { 347 // Package name is the key for remote providers. 348 // If package name is null, the provider is Settings. 349 packageName = disabledResults.getString(COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE); 350 if (packageName == null) { 351 packageName = mContext.getPackageName(); 352 } 353 354 final String key = disabledResults.getString(COLUMN_INDEX_KEY); 355 final Set<String> packageKeys = nonIndexableKeys.get(packageName); 356 357 // The indexed item is set to Disabled but is no longer non-indexable. 358 // We do not enable keys when packageKeys is null because it means the keys came 359 // from an unrecognized package and therefore should not be surfaced as results. 360 if (packageKeys != null && !packageKeys.contains(key)) { 361 String whereClause = DOCID + " = " + disabledResults.getInt(COLUMN_INDEX_ID); 362 database.update(TABLE_PREFS_INDEX, disabledToEnabledValue, whereClause, null); 363 } 364 } 365 disabledResults.close(); 366 } 367 368 /** 369 * TODO (b/64951285): Deprecate this method 370 * 371 * Update the Index for a specific class name resources 372 * 373 * @param className the class name (typically a fragment name). 374 * @param includeInSearchResults true means that you want the bit "enabled" set so that the 375 * data will be seen included into the search results 376 */ 377 public void updateFromClassNameResource(String className, boolean includeInSearchResults) { 378 if (className == null) { 379 throw new IllegalArgumentException("class name cannot be null!"); 380 } 381 final SearchIndexableResource res = SearchIndexableResources.getResourceByName(className); 382 if (res == null) { 383 Log.e(LOG_TAG, "Cannot find SearchIndexableResources for class name: " + className); 384 return; 385 } 386 res.context = mContext; 387 res.enabled = includeInSearchResults; 388 AsyncTask.execute(new Runnable() { 389 @Override 390 public void run() { 391// addIndexableData(res); 392// updateDatabase(false, Locale.getDefault().toString()); 393// res.enabled = false; 394 } 395 }); 396 } 397 398 private SQLiteDatabase getWritableDatabase() { 399 try { 400 return IndexDatabaseHelper.getInstance(mContext).getWritableDatabase(); 401 } catch (SQLiteException e) { 402 Log.e(LOG_TAG, "Cannot open writable database", e); 403 return null; 404 } 405 } 406 407 public class IndexingTask extends AsyncTask<Void, Void, Void> { 408 409 @VisibleForTesting 410 IndexingCallback mCallback; 411 private long mIndexStartTime; 412 413 public IndexingTask(IndexingCallback callback) { 414 mCallback = callback; 415 } 416 417 @Override 418 protected void onPreExecute() { 419 mIndexStartTime = System.currentTimeMillis(); 420 mIsIndexingComplete.set(false); 421 } 422 423 @Override 424 protected Void doInBackground(Void... voids) { 425 performIndexing(); 426 return null; 427 } 428 429 @Override 430 protected void onPostExecute(Void aVoid) { 431 int indexingTime = (int) (System.currentTimeMillis() - mIndexStartTime); 432 FeatureFactory.getFactory(mContext).getMetricsFeatureProvider() 433 .histogram(mContext, METRICS_ACTION_SETTINGS_ASYNC_INDEX, indexingTime); 434 435 mIsIndexingComplete.set(true); 436 if (mCallback != null) { 437 mCallback.onIndexingFinished(); 438 } 439 } 440 } 441}