• 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 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_PACKAGE;
21 import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.CLASS_NAME;
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_SUMMARY_ON;
26 import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON_NORMALIZED;
27 import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_TITLE;
28 import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED;
29 import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.ENABLED;
30 import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.ICON;
31 import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.INTENT_ACTION;
32 import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_CLASS;
33 import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_PACKAGE;
34 import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.PAYLOAD;
35 import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.PAYLOAD_TYPE;
36 import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns.SCREEN_TITLE;
37 import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX;
38 import static com.android.settings.intelligence.search.SearchFeatureProvider.DEBUG;
39 
40 import android.content.ContentValues;
41 import android.content.Context;
42 import android.content.Intent;
43 import android.content.pm.ResolveInfo;
44 import android.database.Cursor;
45 import android.database.sqlite.SQLiteDatabase;
46 import android.database.sqlite.SQLiteException;
47 import android.os.AsyncTask;
48 import android.provider.SearchIndexablesContract;
49 import android.support.annotation.VisibleForTesting;
50 import android.text.TextUtils;
51 import android.util.Log;
52 import android.util.Pair;
53 
54 import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
55 import com.android.settings.intelligence.overlay.FeatureFactory;
56 import com.android.settings.intelligence.search.sitemap.SiteMapPair;
57 
58 import java.util.List;
59 import java.util.Map;
60 import java.util.Set;
61 import 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  */
69 public 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 
DatabaseIndexingManager(Context context)81     public DatabaseIndexingManager(Context context) {
82         mContext = context;
83     }
84 
isIndexingComplete()85     public boolean isIndexingComplete() {
86         return mIsIndexingComplete.get();
87     }
88 
indexDatabase(IndexingCallback callback)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      */
performIndexing()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
getIndexDataFromProviders(List<ResolveInfo> providers, boolean isFullIndex)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      */
rebuildDatabase()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
updateDatabase(PreIndexData preIndexData, boolean isFullIndex)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 
getIndexData(PreIndexData data)178     private List<IndexData> getIndexData(PreIndexData data) {
179         if (mConverter == null) {
180             mConverter = new IndexDataConverter(mContext);
181         }
182         return mConverter.convertPreIndexDataToIndexData(data);
183     }
184 
getSiteMapPairs(List<IndexData> indexData, List<Pair<String, String>> siteMapClassNames)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 
insertSiteMapData(SQLiteDatabase database, List<SiteMapPair> siteMapPairs)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      */
insertIndexData(SQLiteDatabase database, List<IndexData> indexData)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
updateDataInDatabase(SQLiteDatabase database, Map<String, Set<String>> nonIndexableKeys)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 
getKeyWhereClause(String key)300     private String getKeyWhereClause(String key) {
301         return IndexDatabaseHelper.IndexColumns.DATA_KEY_REF + " = \"" + key + "\"";
302     }
303 
getWritableDatabase()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 
IndexingTask(IndexingCallback callback)319         public IndexingTask(IndexingCallback callback) {
320             mCallback = callback;
321         }
322 
323         @Override
onPreExecute()324         protected void onPreExecute() {
325             mIndexStartTime = System.currentTimeMillis();
326             mIsIndexingComplete.set(false);
327         }
328 
329         @Override
doInBackground(Void... voids)330         protected Void doInBackground(Void... voids) {
331             performIndexing();
332             return null;
333         }
334 
335         @Override
onPostExecute(Void aVoid)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 }