blob: e793cac5475ecfe846bd2c5d11500a861da86054 [file] [log] [blame]
/*
* 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.DatabaseResultLoader.COLUMN_INDEX_ID;
import static com.android.settings.search.DatabaseResultLoader
.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE;
import static com.android.settings.search.DatabaseResultLoader.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 androidx.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
import com.android.settings.overlay.FeatureFactory;
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;
}
}
}