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