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