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