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 17package com.android.settings.intelligence.search.indexing; 18 19import static com.android.settings.intelligence.search.query.DatabaseResultTask.SELECT_COLUMNS; 20import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_PACKAGE; 21import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.CLASS_NAME; 22import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_ENTRIES; 23import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS; 24import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_KEY_REF; 25import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON; 26import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON_NORMALIZED; 27import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_TITLE; 28import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED; 29import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.ENABLED; 30import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.ICON; 31import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.INTENT_ACTION; 32import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_CLASS; 33import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_PACKAGE; 34import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.PAYLOAD; 35import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.PAYLOAD_TYPE; 36import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.SCREEN_TITLE; 37import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX; 38import static com.android.settings.intelligence.search.SearchFeatureProvider.DEBUG; 39 40import android.content.ContentValues; 41import android.content.Context; 42import android.content.Intent; 43import android.content.pm.ResolveInfo; 44import android.database.Cursor; 45import android.database.sqlite.SQLiteDatabase; 46import android.database.sqlite.SQLiteException; 47import android.os.AsyncTask; 48import android.provider.SearchIndexablesContract; 49import android.support.annotation.VisibleForTesting; 50import android.text.TextUtils; 51import android.util.Log; 52import android.util.Pair; 53 54import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto; 55import com.android.settings.intelligence.overlay.FeatureFactory; 56import com.android.settings.intelligence.search.sitemap.SiteMapPair; 57 58import java.util.List; 59import java.util.Map; 60import java.util.Set; 61import java.util.concurrent.atomic.AtomicBoolean; 62 63/** 64 * Consumes the SearchIndexableProvider content providers. 65 * Updates the Resource, Raw Data and non-indexable data for Search. 66 * 67 * TODO(b/33577327) this class needs to be refactored by moving most of its methods into controllers 68 */ 69public class DatabaseIndexingManager { 70 71 private static final String TAG = "DatabaseIndexingManager"; 72 73 @VisibleForTesting 74 final AtomicBoolean mIsIndexingComplete = new AtomicBoolean(false); 75 76 private PreIndexDataCollector mCollector; 77 private IndexDataConverter mConverter; 78 79 private Context mContext; 80 81 public DatabaseIndexingManager(Context context) { 82 mContext = context; 83 } 84 85 public boolean isIndexingComplete() { 86 return mIsIndexingComplete.get(); 87 } 88 89 public void indexDatabase(IndexingCallback callback) { 90 IndexingTask task = new IndexingTask(callback); 91 task.execute(); 92 } 93 94 /** 95 * Accumulate all data and non-indexable keys from each of the content-providers. 96 * Only the first indexing for the default language gets static search results - subsequent 97 * calls will only gather non-indexable keys. 98 */ 99 public void performIndexing() { 100 final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE); 101 final List<ResolveInfo> providers = 102 mContext.getPackageManager().queryIntentContentProviders(intent, 0); 103 104 final boolean isFullIndex = IndexDatabaseHelper.isFullIndex(mContext, providers); 105 106 if (isFullIndex) { 107 rebuildDatabase(); 108 } 109 110 PreIndexData indexData = getIndexDataFromProviders(providers, isFullIndex); 111 112 final long updateDatabaseStartTime = System.currentTimeMillis(); 113 updateDatabase(indexData, isFullIndex); 114 IndexDatabaseHelper.setIndexed(mContext, providers); 115 if (DEBUG) { 116 final long updateDatabaseTime = System.currentTimeMillis() - updateDatabaseStartTime; 117 Log.d(TAG, "performIndexing updateDatabase took time: " + updateDatabaseTime); 118 } 119 } 120 121 @VisibleForTesting 122 PreIndexData getIndexDataFromProviders(List<ResolveInfo> providers, boolean isFullIndex) { 123 if (mCollector == null) { 124 mCollector = new PreIndexDataCollector(mContext); 125 } 126 return mCollector.collectIndexableData(providers, isFullIndex); 127 } 128 129 /** 130 * Drop the currently stored database, and clear the flags which mark the database as indexed. 131 */ 132 private void rebuildDatabase() { 133 // Drop the database when the locale or build has changed. This eliminates rows which are 134 // dynamically inserted in the old language, or deprecated settings. 135 final SQLiteDatabase db = getWritableDatabase(); 136 IndexDatabaseHelper.getInstance(mContext).reconstruct(db); 137 } 138 139 /** 140 * Adds new data to the database and verifies the correctness of the ENABLED column. 141 * First, the data to be updated and all non-indexable keys are copied locally. 142 * Then all new data to be added is inserted. 143 * Then search results are verified to have the correct value of enabled. 144 * Finally, we record that the locale has been indexed. 145 * 146 * @param isFullIndex true the database needs to be rebuilt. 147 */ 148 @VisibleForTesting 149 void updateDatabase(PreIndexData preIndexData, boolean isFullIndex) { 150 final Map<String, Set<String>> nonIndexableKeys = preIndexData.getNonIndexableKeys(); 151 152 final SQLiteDatabase database = getWritableDatabase(); 153 if (database == null) { 154 Log.w(TAG, "Cannot indexDatabase Index as I cannot get a writable database"); 155 return; 156 } 157 158 try { 159 database.beginTransaction(); 160 161 // Convert all Pre-index data to Index data and and insert to db. 162 final List<IndexData> indexData = getIndexData(preIndexData); 163 insertIndexData(database, indexData); 164 insertSiteMapData(database, getSiteMapPairs(indexData, preIndexData.getSiteMapPairs())); 165 166 // Only check for non-indexable key updates after initial index. 167 // Enabled state with non-indexable keys is checked when items are first inserted. 168 if (!isFullIndex) { 169 updateDataInDatabase(database, nonIndexableKeys); 170 } 171 172 database.setTransactionSuccessful(); 173 } finally { 174 database.endTransaction(); 175 } 176 } 177 178 private List<IndexData> getIndexData(PreIndexData data) { 179 if (mConverter == null) { 180 mConverter = new IndexDataConverter(mContext); 181 } 182 return mConverter.convertPreIndexDataToIndexData(data); 183 } 184 185 private List<SiteMapPair> getSiteMapPairs(List<IndexData> indexData, 186 List<Pair<String, String>> siteMapClassNames) { 187 if (mConverter == null) { 188 mConverter = new IndexDataConverter(mContext); 189 } 190 return mConverter.convertSiteMapPairs(indexData, siteMapClassNames); 191 } 192 193 private void insertSiteMapData(SQLiteDatabase database, List<SiteMapPair> siteMapPairs) { 194 if (siteMapPairs == null) { 195 return; 196 } 197 for (SiteMapPair pair : siteMapPairs) { 198 database.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_SITE_MAP, 199 null /* nullColumnHack */, pair.toContentValue()); 200 } 201 } 202 203 /** 204 * Inserts all of the entries in {@param indexData} into the {@param database} 205 * as Search Data and as part of the Information Hierarchy. 206 */ 207 private void insertIndexData(SQLiteDatabase database, List<IndexData> indexData) { 208 ContentValues values; 209 210 for (IndexData dataRow : indexData) { 211 if (TextUtils.isEmpty(dataRow.normalizedTitle)) { 212 continue; 213 } 214 215 values = new ContentValues(); 216 values.put(DATA_TITLE, dataRow.updatedTitle); 217 values.put(DATA_TITLE_NORMALIZED, dataRow.normalizedTitle); 218 values.put(DATA_SUMMARY_ON, dataRow.updatedSummaryOn); 219 values.put(DATA_SUMMARY_ON_NORMALIZED, dataRow.normalizedSummaryOn); 220 values.put(DATA_ENTRIES, dataRow.entries); 221 values.put(DATA_KEYWORDS, dataRow.spaceDelimitedKeywords); 222 values.put(DATA_PACKAGE, dataRow.packageName); 223 values.put(CLASS_NAME, dataRow.className); 224 values.put(SCREEN_TITLE, dataRow.screenTitle); 225 values.put(INTENT_ACTION, dataRow.intentAction); 226 values.put(INTENT_TARGET_PACKAGE, dataRow.intentTargetPackage); 227 values.put(INTENT_TARGET_CLASS, dataRow.intentTargetClass); 228 values.put(ICON, dataRow.iconResId); 229 values.put(ENABLED, dataRow.enabled); 230 values.put(DATA_KEY_REF, dataRow.key); 231 values.put(PAYLOAD_TYPE, dataRow.payloadType); 232 values.put(PAYLOAD, dataRow.payload); 233 234 database.replaceOrThrow(TABLE_PREFS_INDEX, null, values); 235 } 236 } 237 238 /** 239 * Upholds the validity of enabled data for the user. 240 * All rows which are enabled but are now flagged with non-indexable keys will become disabled. 241 * All rows which are disabled but no longer a non-indexable key will become enabled. 242 * 243 * @param database The database to validate. 244 * @param nonIndexableKeys A map between package name and the set of non-indexable keys for it. 245 */ 246 @VisibleForTesting 247 void updateDataInDatabase(SQLiteDatabase database, 248 Map<String, Set<String>> nonIndexableKeys) { 249 final String whereEnabled = ENABLED + " = 1"; 250 final String whereDisabled = ENABLED + " = 0"; 251 252 final Cursor enabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS, 253 whereEnabled, null, null, null, null); 254 255 final ContentValues enabledToDisabledValue = new ContentValues(); 256 enabledToDisabledValue.put(ENABLED, 0); 257 258 String packageName; 259 // TODO Refactor: Move these two loops into one method. 260 while (enabledResults.moveToNext()) { 261 packageName = enabledResults.getString(enabledResults.getColumnIndexOrThrow( 262 IndexDatabaseHelper.IndexColumns.DATA_PACKAGE)); 263 final String key = enabledResults.getString(enabledResults.getColumnIndexOrThrow( 264 IndexDatabaseHelper.IndexColumns.DATA_KEY_REF)); 265 final Set<String> packageKeys = nonIndexableKeys.get(packageName); 266 267 // The indexed item is set to Enabled but is now non-indexable 268 if (packageKeys != null && packageKeys.contains(key)) { 269 final String whereClause = getKeyWhereClause(key); 270 database.update(TABLE_PREFS_INDEX, enabledToDisabledValue, whereClause, null); 271 } 272 } 273 enabledResults.close(); 274 275 final Cursor disabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS, 276 whereDisabled, null, null, null, null); 277 278 final ContentValues disabledToEnabledValue = new ContentValues(); 279 disabledToEnabledValue.put(ENABLED, 1); 280 281 while (disabledResults.moveToNext()) { 282 packageName = disabledResults.getString(disabledResults.getColumnIndexOrThrow( 283 IndexDatabaseHelper.IndexColumns.DATA_PACKAGE)); 284 285 final String key = disabledResults.getString(disabledResults.getColumnIndexOrThrow( 286 IndexDatabaseHelper.IndexColumns.DATA_KEY_REF)); 287 final Set<String> packageKeys = nonIndexableKeys.get(packageName); 288 289 // The indexed item is set to Disabled but is no longer non-indexable. 290 // We do not enable keys when packageKeys is null because it means the keys came 291 // from an unrecognized package and therefore should not be surfaced as results. 292 if (packageKeys != null && !packageKeys.contains(key)) { 293 final String whereClause = getKeyWhereClause(key); 294 database.update(TABLE_PREFS_INDEX, disabledToEnabledValue, whereClause, null); 295 } 296 } 297 disabledResults.close(); 298 } 299 300 private String getKeyWhereClause(String key) { 301 return IndexDatabaseHelper.IndexColumns.DATA_KEY_REF + " = \"" + key + "\""; 302 } 303 304 private SQLiteDatabase getWritableDatabase() { 305 try { 306 return IndexDatabaseHelper.getInstance(mContext).getWritableDatabase(); 307 } catch (SQLiteException e) { 308 Log.e(TAG, "Cannot open writable database", e); 309 return null; 310 } 311 } 312 313 public class IndexingTask extends AsyncTask<Void, Void, Void> { 314 315 @VisibleForTesting 316 IndexingCallback mCallback; 317 private long mIndexStartTime; 318 319 public IndexingTask(IndexingCallback callback) { 320 mCallback = callback; 321 } 322 323 @Override 324 protected void onPreExecute() { 325 mIndexStartTime = System.currentTimeMillis(); 326 mIsIndexingComplete.set(false); 327 } 328 329 @Override 330 protected Void doInBackground(Void... voids) { 331 performIndexing(); 332 return null; 333 } 334 335 @Override 336 protected void onPostExecute(Void aVoid) { 337 int indexingTime = (int) (System.currentTimeMillis() - mIndexStartTime); 338 FeatureFactory.get(mContext).metricsFeatureProvider(mContext).logEvent( 339 SettingsIntelligenceLogProto.SettingsIntelligenceEvent.INDEX_SEARCH, 340 indexingTime); 341 342 mIsIndexingComplete.set(true); 343 if (mCallback != null) { 344 mCallback.onIndexingFinished(); 345 } 346 } 347 } 348}