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 }