DatabaseIndexingManager.java revision 518e648acf5e6c53c3f941949f8f5f02b1d75e9f
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
18package com.android.settings.search;
19
20
21import static com.android.settings.search.CursorToSearchResultConverter.COLUMN_INDEX_ID;
22import static com.android.settings.search.CursorToSearchResultConverter.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE;
23import static com.android.settings.search.CursorToSearchResultConverter.COLUMN_INDEX_KEY;
24import static com.android.settings.search.DatabaseResultLoader.SELECT_COLUMNS;
25import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.CLASS_NAME;
26import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_ENTRIES;
27import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS;
28import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_KEY_REF;
29import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON;
30import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON_NORMALIZED;
31import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE;
32import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED;
33import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DOCID;
34import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ENABLED;
35import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ICON;
36import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_ACTION;
37import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_CLASS;
38import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_PACKAGE;
39import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.LOCALE;
40import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.PAYLOAD;
41import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.PAYLOAD_TYPE;
42import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.SCREEN_TITLE;
43import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.USER_ID;
44import static com.android.settings.search.IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX;
45
46import android.content.ContentValues;
47import android.content.Context;
48import android.content.Intent;
49import android.content.pm.ResolveInfo;
50import android.database.Cursor;
51import android.database.sqlite.SQLiteDatabase;
52import android.database.sqlite.SQLiteException;
53import android.os.AsyncTask;
54import android.os.Build;
55import android.provider.SearchIndexablesContract;
56import android.provider.SearchIndexablesContract.SiteMapColumns;
57import android.support.annotation.VisibleForTesting;
58import android.text.TextUtils;
59import android.util.Log;
60
61import com.android.settings.overlay.FeatureFactory;
62import com.android.settings.search.indexing.IndexData;
63import com.android.settings.search.indexing.IndexDataConverter;
64import com.android.settings.search.indexing.PreIndexData;
65import com.android.settings.search.indexing.PreIndexDataCollector;
66
67import java.util.List;
68import java.util.Locale;
69import java.util.Map;
70import java.util.Set;
71import java.util.concurrent.atomic.AtomicBoolean;
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 */
79public class DatabaseIndexingManager {
80
81    private static final String LOG_TAG = "DatabaseIndexingManager";
82
83    private static final String METRICS_ACTION_SETTINGS_ASYNC_INDEX =
84            "search_asynchronous_indexing";
85
86    public static final String FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER =
87            "SEARCH_INDEX_DATA_PROVIDER";
88
89    @VisibleForTesting
90    final AtomicBoolean mIsIndexingComplete = new AtomicBoolean(false);
91
92    private PreIndexDataCollector mCollector;
93    private IndexDataConverter mConverter;
94
95    private Context mContext;
96
97    public DatabaseIndexingManager(Context context) {
98        mContext = context;
99    }
100
101    public boolean isIndexingComplete() {
102        return mIsIndexingComplete.get();
103    }
104
105    public void indexDatabase(IndexingCallback callback) {
106        IndexingTask task = new IndexingTask(callback);
107        task.execute();
108    }
109
110    /**
111     * Accumulate all data and non-indexable keys from each of the content-providers.
112     * Only the first indexing for the default language gets static search results - subsequent
113     * calls will only gather non-indexable keys.
114     */
115    public void performIndexing() {
116        final long startTime = System.currentTimeMillis();
117        final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
118        final List<ResolveInfo> providers =
119                mContext.getPackageManager().queryIntentContentProviders(intent, 0);
120
121        final String localeStr = Locale.getDefault().toString();
122        final String fingerprint = Build.FINGERPRINT;
123        final String providerVersionedNames =
124                IndexDatabaseHelper.buildProviderVersionedNames(providers);
125
126        final boolean isFullIndex = isFullIndex(mContext, localeStr, fingerprint,
127                providerVersionedNames);
128
129        if (isFullIndex) {
130            rebuildDatabase();
131        }
132
133        PreIndexData indexData = getIndexDataFromProviders(providers, isFullIndex);
134
135        final long updateDatabaseStartTime = System.currentTimeMillis();
136        updateDatabase(indexData, isFullIndex);
137        if (SettingsSearchIndexablesProvider.DEBUG) {
138            final long updateDatabaseTime = System.currentTimeMillis() - updateDatabaseStartTime;
139            Log.d(LOG_TAG, "performIndexing updateDatabase took time: " + updateDatabaseTime);
140        }
141
142        //TODO(63922686): Setting indexed should be a single method, not 3 separate setters.
143        IndexDatabaseHelper.setLocaleIndexed(mContext, localeStr);
144        IndexDatabaseHelper.setBuildIndexed(mContext, fingerprint);
145        IndexDatabaseHelper.setProvidersIndexed(mContext, providerVersionedNames);
146
147        if (SettingsSearchIndexablesProvider.DEBUG) {
148            final long indexingTime = System.currentTimeMillis() - startTime;
149            Log.d(LOG_TAG, "performIndexing took time: " + indexingTime
150                    + "ms. Full index? " + isFullIndex);
151        }
152    }
153
154    @VisibleForTesting
155    PreIndexData getIndexDataFromProviders(List<ResolveInfo> providers, boolean isFullIndex) {
156        if (mCollector == null) {
157            mCollector = new PreIndexDataCollector(mContext);
158        }
159        return mCollector.collectIndexableData(providers, isFullIndex);
160    }
161
162    /**
163     * Checks if the indexed data is obsolete, when either:
164     * - Device language has changed
165     * - Device has taken an OTA.
166     * In both cases, the device requires a full index.
167     *
168     * @param locale      is the default for the device
169     * @param fingerprint id for the current build.
170     * @return true if a full index should be preformed.
171     */
172    @VisibleForTesting
173    boolean isFullIndex(Context context, String locale, String fingerprint,
174            String providerVersionedNames) {
175        final boolean isLocaleIndexed = IndexDatabaseHelper.isLocaleAlreadyIndexed(context, locale);
176        final boolean isBuildIndexed = IndexDatabaseHelper.isBuildIndexed(context, fingerprint);
177        final boolean areProvidersIndexed = IndexDatabaseHelper
178                .areProvidersIndexed(context, providerVersionedNames);
179
180        return !(isLocaleIndexed && isBuildIndexed && areProvidersIndexed);
181    }
182
183    /**
184     * Drop the currently stored database, and clear the flags which mark the database as indexed.
185     */
186    private void rebuildDatabase() {
187        // Drop the database when the locale or build has changed. This eliminates rows which are
188        // dynamically inserted in the old language, or deprecated settings.
189        final SQLiteDatabase db = getWritableDatabase();
190        IndexDatabaseHelper.getInstance(mContext).reconstruct(db);
191    }
192
193    /**
194     * Adds new data to the database and verifies the correctness of the ENABLED column.
195     * First, the data to be updated and all non-indexable keys are copied locally.
196     * Then all new data to be added is inserted.
197     * Then search results are verified to have the correct value of enabled.
198     * Finally, we record that the locale has been indexed.
199     *
200     * @param needsReindexing true the database needs to be rebuilt.
201     */
202    @VisibleForTesting
203    void updateDatabase(PreIndexData preIndexData, boolean needsReindexing) {
204        final Map<String, Set<String>> nonIndexableKeys = preIndexData.nonIndexableKeys;
205
206        final SQLiteDatabase database = getWritableDatabase();
207        if (database == null) {
208            Log.w(LOG_TAG, "Cannot indexDatabase Index as I cannot get a writable database");
209            return;
210        }
211
212        try {
213            database.beginTransaction();
214
215            // Convert all Pre-index data to Index data.
216            List<IndexData> indexData = getIndexData(preIndexData);
217            insertIndexData(database, indexData);
218
219            // Only check for non-indexable key updates after initial index.
220            // Enabled state with non-indexable keys is checked when items are first inserted.
221            if (!needsReindexing) {
222                updateDataInDatabase(database, nonIndexableKeys);
223            }
224
225            database.setTransactionSuccessful();
226        } finally {
227            database.endTransaction();
228        }
229    }
230
231    @VisibleForTesting
232    List<IndexData> getIndexData(PreIndexData data) {
233        if (mConverter == null) {
234            mConverter = new IndexDataConverter(mContext);
235        }
236        return mConverter.convertPreIndexDataToIndexData(data);
237    }
238
239    /**
240     * Inserts all of the entries in {@param indexData} into the {@param database}
241     * as Search Data and as part of the Information Hierarchy.
242     */
243    @VisibleForTesting
244    void insertIndexData(SQLiteDatabase database, List<IndexData> indexData) {
245        ContentValues values;
246
247        for (IndexData dataRow : indexData) {
248            if (TextUtils.isEmpty(dataRow.normalizedTitle)) {
249                continue;
250            }
251
252            values = new ContentValues();
253            values.put(IndexDatabaseHelper.IndexColumns.DOCID, dataRow.getDocId());
254            values.put(LOCALE, dataRow.locale);
255            values.put(DATA_TITLE, dataRow.updatedTitle);
256            values.put(DATA_TITLE_NORMALIZED, dataRow.normalizedTitle);
257            values.put(DATA_SUMMARY_ON, dataRow.updatedSummaryOn);
258            values.put(DATA_SUMMARY_ON_NORMALIZED, dataRow.normalizedSummaryOn);
259            values.put(DATA_ENTRIES, dataRow.entries);
260            values.put(DATA_KEYWORDS, dataRow.spaceDelimitedKeywords);
261            values.put(CLASS_NAME, dataRow.className);
262            values.put(SCREEN_TITLE, dataRow.screenTitle);
263            values.put(INTENT_ACTION, dataRow.intentAction);
264            values.put(INTENT_TARGET_PACKAGE, dataRow.intentTargetPackage);
265            values.put(INTENT_TARGET_CLASS, dataRow.intentTargetClass);
266            values.put(ICON, dataRow.iconResId);
267            values.put(ENABLED, dataRow.enabled);
268            values.put(DATA_KEY_REF, dataRow.key);
269            values.put(USER_ID, dataRow.userId);
270            values.put(PAYLOAD_TYPE, dataRow.payloadType);
271            values.put(PAYLOAD, dataRow.payload);
272
273            database.replaceOrThrow(TABLE_PREFS_INDEX, null, values);
274
275            if (!TextUtils.isEmpty(dataRow.className)
276                    && !TextUtils.isEmpty(dataRow.childClassName)) {
277                final ContentValues siteMapPair = new ContentValues();
278                siteMapPair.put(SiteMapColumns.PARENT_CLASS, dataRow.className);
279                siteMapPair.put(SiteMapColumns.PARENT_TITLE, dataRow.screenTitle);
280                siteMapPair.put(SiteMapColumns.CHILD_CLASS, dataRow.childClassName);
281                siteMapPair.put(SiteMapColumns.CHILD_TITLE, dataRow.updatedTitle);
282
283                database.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_SITE_MAP,
284                        null /* nullColumnHack */, siteMapPair);
285            }
286        }
287    }
288
289    /**
290     * Upholds the validity of enabled data for the user.
291     * All rows which are enabled but are now flagged with non-indexable keys will become disabled.
292     * All rows which are disabled but no longer a non-indexable key will become enabled.
293     *
294     * @param database         The database to validate.
295     * @param nonIndexableKeys A map between package name and the set of non-indexable keys for it.
296     */
297    @VisibleForTesting
298    void updateDataInDatabase(SQLiteDatabase database,
299            Map<String, Set<String>> nonIndexableKeys) {
300        final String whereEnabled = ENABLED + " = 1";
301        final String whereDisabled = ENABLED + " = 0";
302
303        final Cursor enabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
304                whereEnabled, null, null, null, null);
305
306        final ContentValues enabledToDisabledValue = new ContentValues();
307        enabledToDisabledValue.put(ENABLED, 0);
308
309        String packageName;
310        // TODO Refactor: Move these two loops into one method.
311        while (enabledResults.moveToNext()) {
312            // Package name is the key for remote providers.
313            // If package name is null, the provider is Settings.
314            packageName = enabledResults.getString(COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE);
315            if (packageName == null) {
316                packageName = mContext.getPackageName();
317            }
318
319            final String key = enabledResults.getString(COLUMN_INDEX_KEY);
320            final Set<String> packageKeys = nonIndexableKeys.get(packageName);
321
322            // The indexed item is set to Enabled but is now non-indexable
323            if (packageKeys != null && packageKeys.contains(key)) {
324                final String whereClause = DOCID + " = " + enabledResults.getInt(COLUMN_INDEX_ID);
325                database.update(TABLE_PREFS_INDEX, enabledToDisabledValue, whereClause, null);
326            }
327        }
328        enabledResults.close();
329
330        final Cursor disabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
331                whereDisabled, null, null, null, null);
332
333        final ContentValues disabledToEnabledValue = new ContentValues();
334        disabledToEnabledValue.put(ENABLED, 1);
335
336        while (disabledResults.moveToNext()) {
337            // Package name is the key for remote providers.
338            // If package name is null, the provider is Settings.
339            packageName = disabledResults.getString(COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE);
340            if (packageName == null) {
341                packageName = mContext.getPackageName();
342            }
343
344            final String key = disabledResults.getString(COLUMN_INDEX_KEY);
345            final Set<String> packageKeys = nonIndexableKeys.get(packageName);
346
347            // The indexed item is set to Disabled but is no longer non-indexable.
348            // We do not enable keys when packageKeys is null because it means the keys came
349            // from an unrecognized package and therefore should not be surfaced as results.
350            if (packageKeys != null && !packageKeys.contains(key)) {
351                String whereClause = DOCID + " = " + disabledResults.getInt(COLUMN_INDEX_ID);
352                database.update(TABLE_PREFS_INDEX, disabledToEnabledValue, whereClause, null);
353            }
354        }
355        disabledResults.close();
356    }
357
358    private SQLiteDatabase getWritableDatabase() {
359        try {
360            return IndexDatabaseHelper.getInstance(mContext).getWritableDatabase();
361        } catch (SQLiteException e) {
362            Log.e(LOG_TAG, "Cannot open writable database", e);
363            return null;
364        }
365    }
366
367    public class IndexingTask extends AsyncTask<Void, Void, Void> {
368
369        @VisibleForTesting
370        IndexingCallback mCallback;
371        private long mIndexStartTime;
372
373        public IndexingTask(IndexingCallback callback) {
374            mCallback = callback;
375        }
376
377        @Override
378        protected void onPreExecute() {
379            mIndexStartTime = System.currentTimeMillis();
380            mIsIndexingComplete.set(false);
381        }
382
383        @Override
384        protected Void doInBackground(Void... voids) {
385            performIndexing();
386            return null;
387        }
388
389        @Override
390        protected void onPostExecute(Void aVoid) {
391            int indexingTime = (int) (System.currentTimeMillis() - mIndexStartTime);
392            FeatureFactory.getFactory(mContext).getMetricsFeatureProvider()
393                    .histogram(mContext, METRICS_ACTION_SETTINGS_ASYNC_INDEX, indexingTime);
394
395            mIsIndexingComplete.set(true);
396            if (mCallback != null) {
397                mCallback.onIndexingFinished();
398            }
399        }
400    }
401}