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