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