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