• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
18 package com.android.settings.search;
19 
20 
21 import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_ID;
22 import static com.android.settings.search.DatabaseResultLoader
23         .COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE;
24 import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_KEY;
25 import static com.android.settings.search.DatabaseResultLoader.SELECT_COLUMNS;
26 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.CLASS_NAME;
27 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_ENTRIES;
28 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS;
29 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_KEY_REF;
30 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON;
31 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns
32         .DATA_SUMMARY_ON_NORMALIZED;
33 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE;
34 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED;
35 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DOCID;
36 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ENABLED;
37 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ICON;
38 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_ACTION;
39 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_CLASS;
40 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_PACKAGE;
41 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.LOCALE;
42 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.PAYLOAD;
43 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.PAYLOAD_TYPE;
44 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.SCREEN_TITLE;
45 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.USER_ID;
46 import static com.android.settings.search.IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX;
47 
48 import android.content.ContentValues;
49 import android.content.Context;
50 import android.content.Intent;
51 import android.content.pm.ResolveInfo;
52 import android.database.Cursor;
53 import android.database.sqlite.SQLiteDatabase;
54 import android.database.sqlite.SQLiteException;
55 import android.os.Build;
56 import android.provider.SearchIndexablesContract;
57 import android.provider.SearchIndexablesContract.SiteMapColumns;
58 import android.support.annotation.VisibleForTesting;
59 import android.text.TextUtils;
60 import android.util.Log;
61 
62 import com.android.settings.overlay.FeatureFactory;
63 import com.android.settings.search.indexing.IndexData;
64 import com.android.settings.search.indexing.IndexDataConverter;
65 import com.android.settings.search.indexing.PreIndexData;
66 import com.android.settings.search.indexing.PreIndexDataCollector;
67 
68 import java.util.List;
69 import java.util.Locale;
70 import java.util.Map;
71 import 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  */
79 public 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 
DatabaseIndexingManager(Context context)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      */
performIndexing()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
getIndexDataFromProviders(List<ResolveInfo> providers, boolean isFullIndex)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
isFullIndex(Context context, String locale, String fingerprint, String providerVersionedNames)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      */
rebuildDatabase()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
updateDatabase(PreIndexData preIndexData, boolean needsReindexing)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
getIndexData(PreIndexData data)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
insertIndexData(SQLiteDatabase database, List<IndexData> indexData)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
updateDataInDatabase(SQLiteDatabase database, Map<String, Set<String>> nonIndexableKeys)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 
getWritableDatabase()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 }