347 lines
16 KiB
Java
347 lines
16 KiB
Java
/*
|
|
* Copyright (C) 2017 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
|
|
package com.android.settings.search;
|
|
|
|
|
|
import static com.android.settings.search.CursorToSearchResultConverter.COLUMN_INDEX_ID;
|
|
import static com.android.settings.search.CursorToSearchResultConverter
|
|
.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE;
|
|
import static com.android.settings.search.CursorToSearchResultConverter.COLUMN_INDEX_KEY;
|
|
import static com.android.settings.search.DatabaseResultLoader.SELECT_COLUMNS;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.CLASS_NAME;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_ENTRIES;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_KEY_REF;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns
|
|
.DATA_SUMMARY_ON_NORMALIZED;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DOCID;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ENABLED;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ICON;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_ACTION;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_CLASS;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_PACKAGE;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.LOCALE;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.PAYLOAD;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.PAYLOAD_TYPE;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.SCREEN_TITLE;
|
|
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.USER_ID;
|
|
import static com.android.settings.search.IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX;
|
|
|
|
import android.content.ContentValues;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.pm.ResolveInfo;
|
|
import android.database.Cursor;
|
|
import android.database.sqlite.SQLiteDatabase;
|
|
import android.database.sqlite.SQLiteException;
|
|
import android.os.Build;
|
|
import android.provider.SearchIndexablesContract;
|
|
import android.provider.SearchIndexablesContract.SiteMapColumns;
|
|
import android.support.annotation.VisibleForTesting;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
|
|
import com.android.settings.search.indexing.IndexData;
|
|
import com.android.settings.search.indexing.IndexDataConverter;
|
|
import com.android.settings.search.indexing.PreIndexData;
|
|
import com.android.settings.search.indexing.PreIndexDataCollector;
|
|
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* Consumes the SearchIndexableProvider content providers.
|
|
* Updates the Resource, Raw Data and non-indexable data for Search.
|
|
*
|
|
* TODO(b/33577327) this class needs to be refactored by moving most of its methods into controllers
|
|
*/
|
|
public class DatabaseIndexingManager {
|
|
|
|
private static final String LOG_TAG = "DatabaseIndexingManager";
|
|
|
|
private PreIndexDataCollector mCollector;
|
|
private IndexDataConverter mConverter;
|
|
|
|
private Context mContext;
|
|
|
|
public DatabaseIndexingManager(Context context) {
|
|
mContext = context;
|
|
}
|
|
|
|
/**
|
|
* Accumulate all data and non-indexable keys from each of the content-providers.
|
|
* Only the first indexing for the default language gets static search results - subsequent
|
|
* calls will only gather non-indexable keys.
|
|
*/
|
|
public void performIndexing() {
|
|
final long startTime = System.currentTimeMillis();
|
|
final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
|
|
final List<ResolveInfo> providers =
|
|
mContext.getPackageManager().queryIntentContentProviders(intent, 0);
|
|
|
|
final String localeStr = Locale.getDefault().toString();
|
|
final String fingerprint = Build.FINGERPRINT;
|
|
final String providerVersionedNames =
|
|
IndexDatabaseHelper.buildProviderVersionedNames(providers);
|
|
|
|
final boolean isFullIndex = isFullIndex(mContext, localeStr, fingerprint,
|
|
providerVersionedNames);
|
|
|
|
if (isFullIndex) {
|
|
rebuildDatabase();
|
|
}
|
|
|
|
PreIndexData indexData = getIndexDataFromProviders(providers, isFullIndex);
|
|
|
|
final long updateDatabaseStartTime = System.currentTimeMillis();
|
|
updateDatabase(indexData, isFullIndex);
|
|
if (SettingsSearchIndexablesProvider.DEBUG) {
|
|
final long updateDatabaseTime = System.currentTimeMillis() - updateDatabaseStartTime;
|
|
Log.d(LOG_TAG, "performIndexing updateDatabase took time: " + updateDatabaseTime);
|
|
}
|
|
|
|
//TODO(63922686): Setting indexed should be a single method, not 3 separate setters.
|
|
IndexDatabaseHelper.setLocaleIndexed(mContext, localeStr);
|
|
IndexDatabaseHelper.setBuildIndexed(mContext, fingerprint);
|
|
IndexDatabaseHelper.setProvidersIndexed(mContext, providerVersionedNames);
|
|
|
|
if (SettingsSearchIndexablesProvider.DEBUG) {
|
|
final long indexingTime = System.currentTimeMillis() - startTime;
|
|
Log.d(LOG_TAG, "performIndexing took time: " + indexingTime
|
|
+ "ms. Full index? " + isFullIndex);
|
|
}
|
|
}
|
|
|
|
@VisibleForTesting
|
|
PreIndexData getIndexDataFromProviders(List<ResolveInfo> providers, boolean isFullIndex) {
|
|
if (mCollector == null) {
|
|
mCollector = new PreIndexDataCollector(mContext);
|
|
}
|
|
return mCollector.collectIndexableData(providers, isFullIndex);
|
|
}
|
|
|
|
/**
|
|
* Checks if the indexed data is obsolete, when either:
|
|
* - Device language has changed
|
|
* - Device has taken an OTA.
|
|
* In both cases, the device requires a full index.
|
|
*
|
|
* @param locale is the default for the device
|
|
* @param fingerprint id for the current build.
|
|
* @return true if a full index should be preformed.
|
|
*/
|
|
@VisibleForTesting
|
|
boolean isFullIndex(Context context, String locale, String fingerprint,
|
|
String providerVersionedNames) {
|
|
final boolean isLocaleIndexed = IndexDatabaseHelper.isLocaleAlreadyIndexed(context, locale);
|
|
final boolean isBuildIndexed = IndexDatabaseHelper.isBuildIndexed(context, fingerprint);
|
|
final boolean areProvidersIndexed = IndexDatabaseHelper
|
|
.areProvidersIndexed(context, providerVersionedNames);
|
|
|
|
return !(isLocaleIndexed && isBuildIndexed && areProvidersIndexed);
|
|
}
|
|
|
|
/**
|
|
* Drop the currently stored database, and clear the flags which mark the database as indexed.
|
|
*/
|
|
private void rebuildDatabase() {
|
|
// Drop the database when the locale or build has changed. This eliminates rows which are
|
|
// dynamically inserted in the old language, or deprecated settings.
|
|
final SQLiteDatabase db = getWritableDatabase();
|
|
IndexDatabaseHelper.getInstance(mContext).reconstruct(db);
|
|
}
|
|
|
|
/**
|
|
* Adds new data to the database and verifies the correctness of the ENABLED column.
|
|
* First, the data to be updated and all non-indexable keys are copied locally.
|
|
* Then all new data to be added is inserted.
|
|
* Then search results are verified to have the correct value of enabled.
|
|
* Finally, we record that the locale has been indexed.
|
|
*
|
|
* @param needsReindexing true the database needs to be rebuilt.
|
|
*/
|
|
@VisibleForTesting
|
|
void updateDatabase(PreIndexData preIndexData, boolean needsReindexing) {
|
|
final Map<String, Set<String>> nonIndexableKeys = preIndexData.nonIndexableKeys;
|
|
|
|
final SQLiteDatabase database = getWritableDatabase();
|
|
if (database == null) {
|
|
Log.w(LOG_TAG, "Cannot indexDatabase Index as I cannot get a writable database");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
database.beginTransaction();
|
|
|
|
// Convert all Pre-index data to Index data.
|
|
List<IndexData> indexData = getIndexData(preIndexData);
|
|
insertIndexData(database, indexData);
|
|
|
|
// Only check for non-indexable key updates after initial index.
|
|
// Enabled state with non-indexable keys is checked when items are first inserted.
|
|
if (!needsReindexing) {
|
|
updateDataInDatabase(database, nonIndexableKeys);
|
|
}
|
|
|
|
database.setTransactionSuccessful();
|
|
} finally {
|
|
database.endTransaction();
|
|
}
|
|
}
|
|
|
|
@VisibleForTesting
|
|
List<IndexData> getIndexData(PreIndexData data) {
|
|
if (mConverter == null) {
|
|
mConverter = new IndexDataConverter(mContext);
|
|
}
|
|
return mConverter.convertPreIndexDataToIndexData(data);
|
|
}
|
|
|
|
/**
|
|
* Inserts all of the entries in {@param indexData} into the {@param database}
|
|
* as Search Data and as part of the Information Hierarchy.
|
|
*/
|
|
@VisibleForTesting
|
|
void insertIndexData(SQLiteDatabase database, List<IndexData> indexData) {
|
|
ContentValues values;
|
|
|
|
for (IndexData dataRow : indexData) {
|
|
if (TextUtils.isEmpty(dataRow.normalizedTitle)) {
|
|
continue;
|
|
}
|
|
|
|
values = new ContentValues();
|
|
values.put(IndexDatabaseHelper.IndexColumns.DOCID, dataRow.getDocId());
|
|
values.put(LOCALE, dataRow.locale);
|
|
values.put(DATA_TITLE, dataRow.updatedTitle);
|
|
values.put(DATA_TITLE_NORMALIZED, dataRow.normalizedTitle);
|
|
values.put(DATA_SUMMARY_ON, dataRow.updatedSummaryOn);
|
|
values.put(DATA_SUMMARY_ON_NORMALIZED, dataRow.normalizedSummaryOn);
|
|
values.put(DATA_ENTRIES, dataRow.entries);
|
|
values.put(DATA_KEYWORDS, dataRow.spaceDelimitedKeywords);
|
|
values.put(CLASS_NAME, dataRow.className);
|
|
values.put(SCREEN_TITLE, dataRow.screenTitle);
|
|
values.put(INTENT_ACTION, dataRow.intentAction);
|
|
values.put(INTENT_TARGET_PACKAGE, dataRow.intentTargetPackage);
|
|
values.put(INTENT_TARGET_CLASS, dataRow.intentTargetClass);
|
|
values.put(ICON, dataRow.iconResId);
|
|
values.put(ENABLED, dataRow.enabled);
|
|
values.put(DATA_KEY_REF, dataRow.key);
|
|
values.put(USER_ID, dataRow.userId);
|
|
values.put(PAYLOAD_TYPE, dataRow.payloadType);
|
|
values.put(PAYLOAD, dataRow.payload);
|
|
|
|
database.replaceOrThrow(TABLE_PREFS_INDEX, null, values);
|
|
|
|
if (!TextUtils.isEmpty(dataRow.className)
|
|
&& !TextUtils.isEmpty(dataRow.childClassName)) {
|
|
final ContentValues siteMapPair = new ContentValues();
|
|
siteMapPair.put(SiteMapColumns.PARENT_CLASS, dataRow.className);
|
|
siteMapPair.put(SiteMapColumns.PARENT_TITLE, dataRow.screenTitle);
|
|
siteMapPair.put(SiteMapColumns.CHILD_CLASS, dataRow.childClassName);
|
|
siteMapPair.put(SiteMapColumns.CHILD_TITLE, dataRow.updatedTitle);
|
|
|
|
database.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_SITE_MAP,
|
|
null /* nullColumnHack */, siteMapPair);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Upholds the validity of enabled data for the user.
|
|
* All rows which are enabled but are now flagged with non-indexable keys will become disabled.
|
|
* All rows which are disabled but no longer a non-indexable key will become enabled.
|
|
*
|
|
* @param database The database to validate.
|
|
* @param nonIndexableKeys A map between package name and the set of non-indexable keys for it.
|
|
*/
|
|
@VisibleForTesting
|
|
void updateDataInDatabase(SQLiteDatabase database,
|
|
Map<String, Set<String>> nonIndexableKeys) {
|
|
final String whereEnabled = ENABLED + " = 1";
|
|
final String whereDisabled = ENABLED + " = 0";
|
|
|
|
final Cursor enabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
|
|
whereEnabled, null, null, null, null);
|
|
|
|
final ContentValues enabledToDisabledValue = new ContentValues();
|
|
enabledToDisabledValue.put(ENABLED, 0);
|
|
|
|
String packageName;
|
|
// TODO Refactor: Move these two loops into one method.
|
|
while (enabledResults.moveToNext()) {
|
|
// Package name is the key for remote providers.
|
|
// If package name is null, the provider is Settings.
|
|
packageName = enabledResults.getString(COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE);
|
|
if (packageName == null) {
|
|
packageName = mContext.getPackageName();
|
|
}
|
|
|
|
final String key = enabledResults.getString(COLUMN_INDEX_KEY);
|
|
final Set<String> packageKeys = nonIndexableKeys.get(packageName);
|
|
|
|
// The indexed item is set to Enabled but is now non-indexable
|
|
if (packageKeys != null && packageKeys.contains(key)) {
|
|
final String whereClause = DOCID + " = " + enabledResults.getInt(COLUMN_INDEX_ID);
|
|
database.update(TABLE_PREFS_INDEX, enabledToDisabledValue, whereClause, null);
|
|
}
|
|
}
|
|
enabledResults.close();
|
|
|
|
final Cursor disabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
|
|
whereDisabled, null, null, null, null);
|
|
|
|
final ContentValues disabledToEnabledValue = new ContentValues();
|
|
disabledToEnabledValue.put(ENABLED, 1);
|
|
|
|
while (disabledResults.moveToNext()) {
|
|
// Package name is the key for remote providers.
|
|
// If package name is null, the provider is Settings.
|
|
packageName = disabledResults.getString(COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE);
|
|
if (packageName == null) {
|
|
packageName = mContext.getPackageName();
|
|
}
|
|
|
|
final String key = disabledResults.getString(COLUMN_INDEX_KEY);
|
|
final Set<String> packageKeys = nonIndexableKeys.get(packageName);
|
|
|
|
// The indexed item is set to Disabled but is no longer non-indexable.
|
|
// We do not enable keys when packageKeys is null because it means the keys came
|
|
// from an unrecognized package and therefore should not be surfaced as results.
|
|
if (packageKeys != null && !packageKeys.contains(key)) {
|
|
String whereClause = DOCID + " = " + disabledResults.getInt(COLUMN_INDEX_ID);
|
|
database.update(TABLE_PREFS_INDEX, disabledToEnabledValue, whereClause, null);
|
|
}
|
|
}
|
|
disabledResults.close();
|
|
}
|
|
|
|
private SQLiteDatabase getWritableDatabase() {
|
|
try {
|
|
return IndexDatabaseHelper.getInstance(mContext).getWritableDatabase();
|
|
} catch (SQLiteException e) {
|
|
Log.e(LOG_TAG, "Cannot open writable database", e);
|
|
return null;
|
|
}
|
|
}
|
|
} |