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