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