Files
app_Settings/src/com/android/settings/search2/DatabaseIndexingManager.java
Matthew Fritze 75f55d6e05 Prevent search crashes from uninstalled apps
All search results are now refreshed when resuming the
search fragment, to prevent crashes from results that
no longer exist.

Change-Id: I905465d14f415598ec7a2ebe7b29ed620cde0962
Fixes: 34817357
Test: make RunSettingsRoboTests
Merged-In: I96a0cbfee711ab9dee49d56bfdc4e885202d9ecd
2017-04-21 15:56:44 -07:00

1220 lines
51 KiB
Java

/*
* Copyright (C) 2016 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.search2;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.XmlResourceParser;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.provider.SearchIndexableData;
import android.provider.SearchIndexableResource;
import android.provider.SearchIndexablesContract;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Xml;
import com.android.settings.core.PreferenceController;
import com.android.settings.search.IndexDatabaseHelper;
import com.android.settings.search.Indexable;
import com.android.settings.search.SearchIndexableRaw;
import com.android.settings.search.SearchIndexableResources;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_CLASS_NAME;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ENTRIES;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_ACTION;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEY;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_RANK;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SCREEN_TITLE;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_OFF;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_USER_ID;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_CLASS_NAME;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_ACTION;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RANK;
import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID;
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_RANK;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_OFF;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_OFF_NORMALIZED;
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 static com.android.settings.search2.DatabaseResultLoader.COLUMN_INDEX_ID;
import static com.android.settings.search2.DatabaseResultLoader.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE;
import static com.android.settings.search2.DatabaseResultLoader.COLUMN_INDEX_KEY;
import static com.android.settings.search2.DatabaseResultLoader.SELECT_COLUMNS;
/**
* Consumes the SearchIndexableProvider content providers.
* Updates the Resource, Raw Data and non-indexable data for Search.
*
* TODO this class needs to be refactored by moving most of its methods into controllers
*/
public class DatabaseIndexingManager {
private static final String LOG_TAG = "DatabaseIndexingManager";
public static final String FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER =
"SEARCH_INDEX_DATA_PROVIDER";
private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen";
private static final String NODE_NAME_CHECK_BOX_PREFERENCE = "CheckBoxPreference";
private static final String NODE_NAME_LIST_PREFERENCE = "ListPreference";
private static final List<String> EMPTY_LIST = Collections.emptyList();
private final String mBaseAuthority;
private final AtomicBoolean mIsAvailable = new AtomicBoolean(false);
@VisibleForTesting
final UpdateData mDataToProcess = new UpdateData();
private Context mContext;
public DatabaseIndexingManager(Context context, String baseAuthority) {
mContext = context;
mBaseAuthority = baseAuthority;
}
public void setContext(Context context) {
mContext = context;
}
public boolean isAvailable() {
return mIsAvailable.get();
}
public void indexDatabase() {
AsyncTask.execute(new Runnable() {
@Override
public void run() {
performIndexing();
}
});
}
/**
* 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.
*/
@VisibleForTesting
void performIndexing() {
final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
final List<ResolveInfo> list =
mContext.getPackageManager().queryIntentContentProviders(intent, 0);
final String localeStr = Locale.getDefault().toString();
final String fingerprint = Build.FINGERPRINT;
final boolean isFullIndex = isFullIndex(localeStr, fingerprint);
// Drop the database when the locale or build has changed. This eliminates rows which are
// dynamically inserted in the old language, or deprecated settings.
if (isFullIndex) {
final SQLiteDatabase db = getWritableDatabase();
IndexDatabaseHelper.getInstance(mContext).reconstruct(db);
}
for (final ResolveInfo info : list) {
if (!DatabaseIndexingUtils.isWellKnownProvider(info, mContext)) {
continue;
}
final String authority = info.providerInfo.authority;
final String packageName = info.providerInfo.packageName;
if (isFullIndex) {
addIndexablesFromRemoteProvider(packageName, authority);
}
addNonIndexablesKeysFromRemoteProvider(packageName, authority);
}
updateDatabase(isFullIndex, localeStr);
IndexDatabaseHelper.setLocaleIndexed(mContext, localeStr);
IndexDatabaseHelper.setBuildIndexed(mContext, fingerprint);
}
/**
* Perform a full index on an OTA or when the locale has changed
*
* @param locale is the default for the device
* @param fingerprint id for the current build.
* @return true when the locale or build has changed since last index.
*/
@VisibleForTesting
boolean isFullIndex(String locale, String fingerprint) {
final boolean isLocaleIndexed = IndexDatabaseHelper.getInstance(mContext)
.isLocaleAlreadyIndexed(mContext, locale);
final boolean isBuildIndexed = IndexDatabaseHelper.getInstance(mContext)
.isBuildIndexed(mContext, fingerprint);
return !isLocaleIndexed || !isBuildIndexed;
}
/**
* 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.
* @param localeStr the default locale for the device.
*/
@VisibleForTesting
void updateDatabase(boolean needsReindexing, String localeStr) {
mIsAvailable.set(false);
final UpdateData copy;
synchronized (mDataToProcess) {
copy = mDataToProcess.copy();
mDataToProcess.clear();
}
final List<SearchIndexableData> dataToUpdate = copy.dataToUpdate;
final Map<String, Set<String>> nonIndexableKeys = copy.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();
// Add new data from Providers at initial index time, or inserted later.
if (dataToUpdate.size() > 0) {
addDataToDatabase(database, localeStr, dataToUpdate, nonIndexableKeys);
}
// 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();
}
mIsAvailable.set(true);
}
/**
* Inserts {@link SearchIndexableData} into the database.
*
* @param database where the data will be inserted.
* @param localeStr is the locale of the data to be inserted.
* @param dataToUpdate is a {@link List} of the data to be inserted.
* @param nonIndexableKeys is a {@link Map} from Package Name to a {@link Set} of keys which
* identify search results which should not be surfaced.
*/
@VisibleForTesting
void addDataToDatabase(SQLiteDatabase database, String localeStr,
List<SearchIndexableData> dataToUpdate, Map<String, Set<String>> nonIndexableKeys) {
final long current = System.currentTimeMillis();
for (SearchIndexableData data : dataToUpdate) {
try {
indexOneSearchIndexableData(database, localeStr, data, nonIndexableKeys);
} catch (Exception e) {
Log.e(LOG_TAG, "Cannot index: " + (data != null ? data.className : data)
+ " for locale: " + localeStr, e);
}
}
final long now = System.currentTimeMillis();
Log.d(LOG_TAG, "Indexing locale '" + localeStr + "' took " +
(now - current) + " millis");
}
/**
* 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();
}
@VisibleForTesting
boolean addIndexablesFromRemoteProvider(String packageName, String authority) {
try {
final Context context = mBaseAuthority.equals(authority) ?
mContext : mContext.createPackageContext(packageName, 0);
final Uri uriForResources = buildUriForXmlResources(authority);
addIndexablesForXmlResourceUri(context, packageName, uriForResources,
SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS);
final Uri uriForRawData = buildUriForRawData(authority);
addIndexablesForRawDataUri(context, packageName, uriForRawData,
SearchIndexablesContract.INDEXABLES_RAW_COLUMNS);
return true;
} catch (PackageManager.NameNotFoundException e) {
Log.w(LOG_TAG, "Could not create context for " + packageName + ": "
+ Log.getStackTraceString(e));
return false;
}
}
@VisibleForTesting
void addNonIndexablesKeysFromRemoteProvider(String packageName,
String authority) {
final List<String> keys =
getNonIndexablesKeysFromRemoteProvider(packageName, authority);
addNonIndexableKeys(packageName, new HashSet<>(keys));
}
private List<String> getNonIndexablesKeysFromRemoteProvider(String packageName,
String authority) {
try {
final Context packageContext = mContext.createPackageContext(packageName, 0);
final Uri uriForNonIndexableKeys = buildUriForNonIndexableKeys(authority);
return getNonIndexablesKeys(packageContext, uriForNonIndexableKeys,
SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS);
} catch (PackageManager.NameNotFoundException e) {
Log.w(LOG_TAG, "Could not create context for " + packageName + ": "
+ Log.getStackTraceString(e));
return EMPTY_LIST;
}
}
private List<String> getNonIndexablesKeys(Context packageContext, Uri uri,
String[] projection) {
final ContentResolver resolver = packageContext.getContentResolver();
final Cursor cursor = resolver.query(uri, projection, null, null, null);
if (cursor == null) {
Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString());
return EMPTY_LIST;
}
final List<String> result = new ArrayList<>();
try {
final int count = cursor.getCount();
if (count > 0) {
while (cursor.moveToNext()) {
final String key = cursor.getString(COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE);
result.add(key);
}
}
return result;
} finally {
cursor.close();
}
}
public void addIndexableData(SearchIndexableData data) {
synchronized (mDataToProcess) {
mDataToProcess.dataToUpdate.add(data);
}
}
public void addNonIndexableKeys(String authority, Set<String> keys) {
synchronized (mDataToProcess) {
mDataToProcess.nonIndexableKeys.put(authority, keys);
}
}
/**
* Update the Index for a specific class name resources
*
* @param className the class name (typically a fragment name).
* @param includeInSearchResults true means that you want the bit "enabled" set so that the
* data will be seen included into the search results
*/
public void updateFromClassNameResource(String className, boolean includeInSearchResults) {
if (className == null) {
throw new IllegalArgumentException("class name cannot be null!");
}
final SearchIndexableResource res = SearchIndexableResources.getResourceByName(className);
if (res == null) {
Log.e(LOG_TAG, "Cannot find SearchIndexableResources for class name: " + className);
return;
}
res.context = mContext;
res.enabled = includeInSearchResults;
AsyncTask.execute(new Runnable() {
@Override
public void run() {
addIndexableData(res);
updateDatabase(false, Locale.getDefault().toString());
res.enabled = false;
}
});
}
private SQLiteDatabase getWritableDatabase() {
try {
return IndexDatabaseHelper.getInstance(mContext).getWritableDatabase();
} catch (SQLiteException e) {
Log.e(LOG_TAG, "Cannot open writable database", e);
return null;
}
}
private static Uri buildUriForXmlResources(String authority) {
return Uri.parse("content://" + authority + "/" +
SearchIndexablesContract.INDEXABLES_XML_RES_PATH);
}
private static Uri buildUriForRawData(String authority) {
return Uri.parse("content://" + authority + "/" +
SearchIndexablesContract.INDEXABLES_RAW_PATH);
}
private static Uri buildUriForNonIndexableKeys(String authority) {
return Uri.parse("content://" + authority + "/" +
SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH);
}
private void addIndexablesForXmlResourceUri(Context packageContext, String packageName,
Uri uri, String[] projection) {
final ContentResolver resolver = packageContext.getContentResolver();
final Cursor cursor = resolver.query(uri, projection, null, null, null);
if (cursor == null) {
Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString());
return;
}
try {
final int count = cursor.getCount();
if (count > 0) {
while (cursor.moveToNext()) {
final int providerRank = cursor.getInt(COLUMN_INDEX_XML_RES_RANK);
// TODO remove provider rank
final int xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID);
final String className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME);
final int iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID);
final String action = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION);
final String targetPackage = cursor.getString(
COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE);
final String targetClass = cursor.getString(
COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS);
SearchIndexableResource sir = new SearchIndexableResource(packageContext);
sir.xmlResId = xmlResId;
sir.className = className;
sir.packageName = packageName;
sir.iconResId = iconResId;
sir.intentAction = action;
sir.intentTargetPackage = targetPackage;
sir.intentTargetClass = targetClass;
addIndexableData(sir);
}
}
} finally {
cursor.close();
}
}
private void addIndexablesForRawDataUri(Context packageContext, String packageName,
Uri uri, String[] projection) {
final ContentResolver resolver = packageContext.getContentResolver();
final Cursor cursor = resolver.query(uri, projection, null, null, null);
if (cursor == null) {
Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString());
return;
}
try {
final int count = cursor.getCount();
if (count > 0) {
while (cursor.moveToNext()) {
final int providerRank = cursor.getInt(COLUMN_INDEX_RAW_RANK);
// TODO Remove rank
final String title = cursor.getString(COLUMN_INDEX_RAW_TITLE);
final String summaryOn = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_ON);
final String summaryOff = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_OFF);
final String entries = cursor.getString(COLUMN_INDEX_RAW_ENTRIES);
final String keywords = cursor.getString(COLUMN_INDEX_RAW_KEYWORDS);
final String screenTitle = cursor.getString(COLUMN_INDEX_RAW_SCREEN_TITLE);
final String className = cursor.getString(COLUMN_INDEX_RAW_CLASS_NAME);
final int iconResId = cursor.getInt(COLUMN_INDEX_RAW_ICON_RESID);
final String action = cursor.getString(COLUMN_INDEX_RAW_INTENT_ACTION);
final String targetPackage = cursor.getString(
COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE);
final String targetClass = cursor.getString(
COLUMN_INDEX_RAW_INTENT_TARGET_CLASS);
final String key = cursor.getString(COLUMN_INDEX_RAW_KEY);
final int userId = cursor.getInt(COLUMN_INDEX_RAW_USER_ID);
SearchIndexableRaw data = new SearchIndexableRaw(packageContext);
data.title = title;
data.summaryOn = summaryOn;
data.summaryOff = summaryOff;
data.entries = entries;
data.keywords = keywords;
data.screenTitle = screenTitle;
data.className = className;
data.packageName = packageName;
data.iconResId = iconResId;
data.intentAction = action;
data.intentTargetPackage = targetPackage;
data.intentTargetClass = targetClass;
data.key = key;
data.userId = userId;
addIndexableData(data);
}
}
} finally {
cursor.close();
}
}
public void indexOneSearchIndexableData(SQLiteDatabase database, String localeStr,
SearchIndexableData data, Map<String, Set<String>> nonIndexableKeys) {
if (data instanceof SearchIndexableResource) {
indexOneResource(database, localeStr, (SearchIndexableResource) data, nonIndexableKeys);
} else if (data instanceof SearchIndexableRaw) {
indexOneRaw(database, localeStr, (SearchIndexableRaw) data);
}
}
private void indexOneRaw(SQLiteDatabase database, String localeStr,
SearchIndexableRaw raw) {
// Should be the same locale as the one we are processing
if (!raw.locale.toString().equalsIgnoreCase(localeStr)) {
return;
}
DatabaseRow.Builder builder = new DatabaseRow.Builder();
builder.setLocale(localeStr)
.setEntries(raw.entries)
.setClassName(raw.className)
.setScreenTitle(raw.screenTitle)
.setIconResId(raw.iconResId)
.setRank(raw.rank)
.setIntentAction(raw.intentAction)
.setIntentTargetPackage(raw.intentTargetPackage)
.setIntentTargetClass(raw.intentTargetClass)
.setEnabled(raw.enabled)
.setKey(raw.key)
.setUserId(raw.userId);
updateOneRowWithFilteredData(database, builder, raw.title, raw.summaryOn, raw.summaryOff,
raw.keywords);
}
private void indexOneResource(SQLiteDatabase database, String localeStr,
SearchIndexableResource sir, Map<String, Set<String>> nonIndexableKeysFromResource) {
if (sir == null) {
Log.e(LOG_TAG, "Cannot index a null resource!");
return;
}
final List<String> nonIndexableKeys = new ArrayList<String>();
if (sir.xmlResId > SearchIndexableResources.NO_DATA_RES_ID) {
Set<String> resNonIndexableKeys = nonIndexableKeysFromResource.get(sir.packageName);
if (resNonIndexableKeys != null && resNonIndexableKeys.size() > 0) {
nonIndexableKeys.addAll(resNonIndexableKeys);
}
indexFromResource(database, localeStr, sir, nonIndexableKeys);
} else {
if (TextUtils.isEmpty(sir.className)) {
Log.w(LOG_TAG, "Cannot index an empty Search Provider name!");
return;
}
final Class<?> clazz = DatabaseIndexingUtils.getIndexableClass(sir.className);
if (clazz == null) {
Log.d(LOG_TAG, "SearchIndexableResource '" + sir.className +
"' should implement the " + Indexable.class.getName() + " interface!");
return;
}
// Will be non null only for a Local provider implementing a
// SEARCH_INDEX_DATA_PROVIDER field
final Indexable.SearchIndexProvider provider =
DatabaseIndexingUtils.getSearchIndexProvider(clazz);
if (provider != null) {
List<String> providerNonIndexableKeys = provider.getNonIndexableKeys(sir.context);
if (providerNonIndexableKeys != null && providerNonIndexableKeys.size() > 0) {
nonIndexableKeys.addAll(providerNonIndexableKeys);
}
indexFromProvider(database, localeStr, provider, sir, nonIndexableKeys);
}
}
}
@VisibleForTesting
void indexFromResource(SQLiteDatabase database, String localeStr,
SearchIndexableResource sir, List<String> nonIndexableKeys) {
final Context context = sir.context;
XmlResourceParser parser = null;
try {
parser = context.getResources().getXml(sir.xmlResId);
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& type != XmlPullParser.START_TAG) {
// Parse next until start tag is found
}
String nodeName = parser.getName();
if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) {
throw new RuntimeException(
"XML document must start with <PreferenceScreen> tag; found"
+ nodeName + " at " + parser.getPositionDescription());
}
final int outerDepth = parser.getDepth();
final AttributeSet attrs = Xml.asAttributeSet(parser);
final String screenTitle = XmlParserUtils.getDataTitle(context, attrs);
String key = XmlParserUtils.getDataKey(context, attrs);
String title;
String headerTitle;
String summary;
String headerSummary;
String keywords;
String headerKeywords;
String childFragment;
ResultPayload payload;
boolean enabled;
final String fragmentName = sir.className;
final int iconResId = sir.iconResId;
final int rank = sir.rank;
final String intentAction = sir.intentAction;
final String intentTargetPackage = sir.intentTargetPackage;
final String intentTargetClass = sir.intentTargetClass;
Map<String, PreferenceController> controllerUriMap = null;
if (fragmentName != null) {
controllerUriMap = DatabaseIndexingUtils
.getPreferenceControllerUriMap(fragmentName, context);
}
// Insert rows for the main PreferenceScreen node. Rewrite the data for removing
// hyphens.
headerTitle = XmlParserUtils.getDataTitle(context, attrs);
headerSummary = XmlParserUtils.getDataSummary(context, attrs);
headerKeywords = XmlParserUtils.getDataKeywords(context, attrs);
enabled = !nonIndexableKeys.contains(key);
// TODO: Set payload type for header results
DatabaseRow.Builder headerBuilder = new DatabaseRow.Builder();
headerBuilder.setLocale(localeStr)
.setEntries(null)
.setClassName(fragmentName)
.setScreenTitle(screenTitle)
.setIconResId(iconResId)
.setRank(rank)
.setIntentAction(intentAction)
.setIntentTargetPackage(intentTargetPackage)
.setIntentTargetClass(intentTargetClass)
.setEnabled(enabled)
.setKey(key)
.setUserId(-1 /* default user id */);
// Flag for XML headers which a child element's title.
boolean isHeaderUnique = true;
DatabaseRow.Builder builder;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
nodeName = parser.getName();
title = XmlParserUtils.getDataTitle(context, attrs);
key = XmlParserUtils.getDataKey(context, attrs);
enabled = ! nonIndexableKeys.contains(key);
keywords = XmlParserUtils.getDataKeywords(context, attrs);
if (isHeaderUnique && TextUtils.equals(headerTitle, title)) {
isHeaderUnique = false;
}
builder = new DatabaseRow.Builder();
builder.setLocale(localeStr)
.setClassName(fragmentName)
.setScreenTitle(screenTitle)
.setIconResId(iconResId)
.setRank(rank)
.setIntentAction(intentAction)
.setIntentTargetPackage(intentTargetPackage)
.setIntentTargetClass(intentTargetClass)
.setEnabled(enabled)
.setKey(key)
.setUserId(-1 /* default user id */);
if (!nodeName.equals(NODE_NAME_CHECK_BOX_PREFERENCE)) {
summary = XmlParserUtils.getDataSummary(context, attrs);
String entries = null;
if (nodeName.endsWith(NODE_NAME_LIST_PREFERENCE)) {
entries = XmlParserUtils.getDataEntries(context, attrs);
}
payload = DatabaseIndexingUtils.getPayloadFromUriMap(controllerUriMap, key);
childFragment = XmlParserUtils.getDataChildFragment(context, attrs);
builder.setEntries(entries)
.setChildClassName(childFragment)
.setPayload(payload);
// Insert rows for the child nodes of PreferenceScreen
updateOneRowWithFilteredData(database, builder, title, summary,
null /* summary off */, keywords);
} else {
String summaryOn = XmlParserUtils.getDataSummaryOn(context, attrs);
String summaryOff = XmlParserUtils.getDataSummaryOff(context, attrs);
if (TextUtils.isEmpty(summaryOn) && TextUtils.isEmpty(summaryOff)) {
summaryOn = XmlParserUtils.getDataSummary(context, attrs);
}
updateOneRowWithFilteredData(database, builder, title, summaryOn, summaryOff,
keywords);
}
}
// The xml header's title does not match the title of one of the child settings.
if (isHeaderUnique) {
updateOneRowWithFilteredData(database, headerBuilder, headerTitle, headerSummary,
null /* summary off */, headerKeywords);
}
} catch (XmlPullParserException e) {
throw new RuntimeException("Error parsing PreferenceScreen", e);
} catch (IOException e) {
throw new RuntimeException("Error parsing PreferenceScreen", e);
} finally {
if (parser != null) parser.close();
}
}
private void indexFromProvider(SQLiteDatabase database, String localeStr,
Indexable.SearchIndexProvider provider, SearchIndexableResource sir,
List<String> nonIndexableKeys) {
final String className = sir.className;
final int iconResId = sir.iconResId;
final int rank = sir.rank;
if (provider == null) {
Log.w(LOG_TAG, "Cannot find provider: " + className);
return;
}
final List<SearchIndexableRaw> rawList = provider.getRawDataToIndex(mContext,
true /* enabled */);
if (rawList != null) {
final int rawSize = rawList.size();
for (int i = 0; i < rawSize; i++) {
SearchIndexableRaw raw = rawList.get(i);
// Should be the same locale as the one we are processing
if (!raw.locale.toString().equalsIgnoreCase(localeStr)) {
continue;
}
boolean enabled = !nonIndexableKeys.contains(raw.key);
DatabaseRow.Builder builder = new DatabaseRow.Builder();
builder.setLocale(localeStr)
.setEntries(raw.entries)
.setClassName(className)
.setScreenTitle(raw.screenTitle)
.setIconResId(iconResId)
.setRank(rank)
.setIntentAction(raw.intentAction)
.setIntentTargetPackage(raw.intentTargetPackage)
.setIntentTargetClass(raw.intentTargetClass)
.setEnabled(enabled)
.setKey(raw.key)
.setUserId(raw.userId);
updateOneRowWithFilteredData(database, builder, raw.title, raw.summaryOn,
raw.summaryOff, raw.keywords);
}
}
final List<SearchIndexableResource> resList =
provider.getXmlResourcesToIndex(mContext, true);
if (resList != null) {
final int resSize = resList.size();
for (int i = 0; i < resSize; i++) {
SearchIndexableResource item = resList.get(i);
// Should be the same locale as the one we are processing
if (!item.locale.toString().equalsIgnoreCase(localeStr)) {
continue;
}
item.iconResId = (item.iconResId == 0) ? iconResId : item.iconResId;
item.className = (TextUtils.isEmpty(item.className)) ? className : item.className;
indexFromResource(database, localeStr, item, nonIndexableKeys);
}
}
}
private void updateOneRowWithFilteredData(SQLiteDatabase database, DatabaseRow.Builder builder,
String title, String summaryOn, String summaryOff, String keywords) {
final String updatedTitle = DatabaseIndexingUtils.normalizeHyphen(title);
final String updatedSummaryOn = DatabaseIndexingUtils.normalizeHyphen(summaryOn);
final String updatedSummaryOff = DatabaseIndexingUtils.normalizeHyphen(summaryOff);
final String normalizedTitle = DatabaseIndexingUtils.normalizeString(updatedTitle);
final String normalizedSummaryOn = DatabaseIndexingUtils.normalizeString(updatedSummaryOn);
final String normalizedSummaryOff = DatabaseIndexingUtils
.normalizeString(updatedSummaryOff);
final String spaceDelimitedKeywords = DatabaseIndexingUtils.normalizeKeywords(keywords);
builder.setUpdatedTitle(updatedTitle)
.setUpdatedSummaryOn(updatedSummaryOn)
.setUpdatedSummaryOff(updatedSummaryOff)
.setNormalizedTitle(normalizedTitle)
.setNormalizedSummaryOn(normalizedSummaryOn)
.setNormalizedSummaryOff(normalizedSummaryOff)
.setSpaceDelimitedKeywords(spaceDelimitedKeywords);
updateOneRow(database, builder.build());
}
private void updateOneRow(SQLiteDatabase database, DatabaseRow row) {
if (TextUtils.isEmpty(row.updatedTitle)) {
return;
}
ContentValues values = new ContentValues();
values.put(IndexDatabaseHelper.IndexColumns.DOCID, row.getDocId());
values.put(LOCALE, row.locale);
values.put(DATA_RANK, row.rank);
values.put(DATA_TITLE, row.updatedTitle);
values.put(DATA_TITLE_NORMALIZED, row.normalizedTitle);
values.put(DATA_SUMMARY_ON, row.updatedSummaryOn);
values.put(DATA_SUMMARY_ON_NORMALIZED, row.normalizedSummaryOn);
values.put(DATA_SUMMARY_OFF, row.updatedSummaryOff);
values.put(DATA_SUMMARY_OFF_NORMALIZED, row.normalizedSummaryOff);
values.put(DATA_ENTRIES, row.entries);
values.put(DATA_KEYWORDS, row.spaceDelimitedKeywords);
values.put(CLASS_NAME, row.className);
values.put(SCREEN_TITLE, row.screenTitle);
values.put(INTENT_ACTION, row.intentAction);
values.put(INTENT_TARGET_PACKAGE, row.intentTargetPackage);
values.put(INTENT_TARGET_CLASS, row.intentTargetClass);
values.put(ICON, row.iconResId);
values.put(ENABLED, row.enabled);
values.put(DATA_KEY_REF, row.key);
values.put(USER_ID, row.userId);
values.put(PAYLOAD_TYPE, row.payloadType);
values.put(PAYLOAD, row.payload);
database.replaceOrThrow(TABLE_PREFS_INDEX, null, values);
if (!TextUtils.isEmpty(row.className) && !TextUtils.isEmpty(row.childClassName)) {
ContentValues siteMapPair = new ContentValues();
final int pairDocId = Objects.hash(row.className, row.childClassName);
siteMapPair.put(IndexDatabaseHelper.SiteMapColumns.DOCID, pairDocId);
siteMapPair.put(IndexDatabaseHelper.SiteMapColumns.PARENT_CLASS, row.className);
siteMapPair.put(IndexDatabaseHelper.SiteMapColumns.PARENT_TITLE, row.screenTitle);
siteMapPair.put(IndexDatabaseHelper.SiteMapColumns.CHILD_CLASS, row.childClassName);
siteMapPair.put(IndexDatabaseHelper.SiteMapColumns.CHILD_TITLE, row.updatedTitle);
database.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_SITE_MAP, null, siteMapPair);
}
}
/**
* A private class to describe the indexDatabase data for the Index database
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
static class UpdateData {
public List<SearchIndexableData> dataToUpdate;
public List<SearchIndexableData> dataToDisable;
public Map<String, Set<String>> nonIndexableKeys;
public UpdateData() {
dataToUpdate = new ArrayList<>();
dataToDisable = new ArrayList<>();
nonIndexableKeys = new HashMap<>();
}
public UpdateData(UpdateData other) {
dataToUpdate = new ArrayList<>(other.dataToUpdate);
dataToDisable = new ArrayList<>(other.dataToDisable);
nonIndexableKeys = new HashMap<>(other.nonIndexableKeys);
}
public UpdateData copy() {
return new UpdateData(this);
}
public void clear() {
dataToUpdate.clear();
dataToDisable.clear();
nonIndexableKeys.clear();
}
}
public static class DatabaseRow {
public final String locale;
public final String updatedTitle;
public final String normalizedTitle;
public final String updatedSummaryOn;
public final String normalizedSummaryOn;
public final String updatedSummaryOff;
public final String normalizedSummaryOff;
public final String entries;
public final String className;
public final String childClassName;
public final String screenTitle;
public final int iconResId;
public final int rank;
public final String spaceDelimitedKeywords;
public final String intentAction;
public final String intentTargetPackage;
public final String intentTargetClass;
public final boolean enabled;
public final String key;
public final int userId;
public final int payloadType;
public final byte[] payload;
private DatabaseRow(Builder builder) {
locale = builder.mLocale;
updatedTitle = builder.mUpdatedTitle;
normalizedTitle = builder.mNormalizedTitle;
updatedSummaryOn = builder.mUpdatedSummaryOn;
normalizedSummaryOn = builder.mNormalizedSummaryOn;
updatedSummaryOff = builder.mUpdatedSummaryOff;
normalizedSummaryOff = builder.mNormalizedSummaryOff;
entries = builder.mEntries;
className = builder.mClassName;
childClassName = builder.mChildClassName;
screenTitle = builder.mScreenTitle;
iconResId = builder.mIconResId;
rank = builder.mRank;
spaceDelimitedKeywords = builder.mSpaceDelimitedKeywords;
intentAction = builder.mIntentAction;
intentTargetPackage = builder.mIntentTargetPackage;
intentTargetClass = builder.mIntentTargetClass;
enabled = builder.mEnabled;
key = builder.mKey;
userId = builder.mUserId;
payloadType = builder.mPayloadType;
payload = builder.mPayload != null ? ResultPayloadUtils.marshall(builder.mPayload)
: null;
}
/**
* Returns the doc id for this row.
*/
public int getDocId() {
// The DocID should contains more than the title string itself (you may have two
// settings with the same title). So we need to use a combination of multiple
// attributes from this row.
return Objects.hash(updatedTitle, screenTitle, key, payloadType);
}
public static class Builder {
private String mLocale;
private String mUpdatedTitle;
private String mNormalizedTitle;
private String mUpdatedSummaryOn;
private String mNormalizedSummaryOn;
private String mUpdatedSummaryOff;
private String mNormalizedSummaryOff;
private String mEntries;
private String mClassName;
private String mChildClassName;
private String mScreenTitle;
private int mIconResId;
private int mRank;
private String mSpaceDelimitedKeywords;
private String mIntentAction;
private String mIntentTargetPackage;
private String mIntentTargetClass;
private boolean mEnabled;
private String mKey;
private int mUserId;
@ResultPayload.PayloadType
private int mPayloadType;
private ResultPayload mPayload;
public Builder setLocale(String locale) {
mLocale = locale;
return this;
}
public Builder setUpdatedTitle(String updatedTitle) {
mUpdatedTitle = updatedTitle;
return this;
}
public Builder setNormalizedTitle(String normalizedTitle) {
mNormalizedTitle = normalizedTitle;
return this;
}
public Builder setUpdatedSummaryOn(String updatedSummaryOn) {
mUpdatedSummaryOn = updatedSummaryOn;
return this;
}
public Builder setNormalizedSummaryOn(String normalizedSummaryOn) {
mNormalizedSummaryOn = normalizedSummaryOn;
return this;
}
public Builder setUpdatedSummaryOff(String updatedSummaryOff) {
mUpdatedSummaryOff = updatedSummaryOff;
return this;
}
public Builder setNormalizedSummaryOff(String normalizedSummaryOff) {
this.mNormalizedSummaryOff = normalizedSummaryOff;
return this;
}
public Builder setEntries(String entries) {
mEntries = entries;
return this;
}
public Builder setClassName(String className) {
mClassName = className;
return this;
}
public Builder setChildClassName(String childClassName) {
mChildClassName = childClassName;
return this;
}
public Builder setScreenTitle(String screenTitle) {
mScreenTitle = screenTitle;
return this;
}
public Builder setIconResId(int iconResId) {
mIconResId = iconResId;
return this;
}
public Builder setRank(int rank) {
mRank = rank;
return this;
}
public Builder setSpaceDelimitedKeywords(String spaceDelimitedKeywords) {
mSpaceDelimitedKeywords = spaceDelimitedKeywords;
return this;
}
public Builder setIntentAction(String intentAction) {
mIntentAction = intentAction;
return this;
}
public Builder setIntentTargetPackage(String intentTargetPackage) {
mIntentTargetPackage = intentTargetPackage;
return this;
}
public Builder setIntentTargetClass(String intentTargetClass) {
mIntentTargetClass = intentTargetClass;
return this;
}
public Builder setEnabled(boolean enabled) {
mEnabled = enabled;
return this;
}
public Builder setKey(String key) {
mKey = key;
return this;
}
public Builder setUserId(int userId) {
mUserId = userId;
return this;
}
public Builder setPayload(ResultPayload payload) {
mPayload = payload;
if (mPayload != null) {
setPayloadType(mPayload.getType());
}
return this;
}
/**
* Payload type is added when a Payload is added to the Builder in {setPayload}
*
* @param payloadType PayloadType
* @return The Builder
*/
private Builder setPayloadType(@ResultPayload.PayloadType int payloadType) {
mPayloadType = payloadType;
return this;
}
public DatabaseRow build() {
return new DatabaseRow(this);
}
}
}
}