blob: 0e107747a3f2ef565a40cfb34d6979fe6cf23726 [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 android.content.ComponentName;
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.DrawableRes;
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.SettingsActivity;
import com.android.settings.core.PreferenceController;
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_RESID;
import static com.android.settings.search.DatabaseResultLoader.*;
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;
/**
* 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;
@VisibleForTesting
final AtomicBoolean mIsIndexingComplete = 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 isIndexingComplete() {
return mIsIndexingComplete.get();
}
public void indexDatabase(IndexingCallback callback) {
IndexingTask task = new IndexingTask(callback);
task.execute();
}
/**
* 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);
String localeStr = Locale.getDefault().toString();
String fingerprint = Build.FINGERPRINT;
final boolean isFullIndex = isFullIndex(localeStr, fingerprint);
if (isFullIndex) {
rebuildDatabase();
}
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;
}
/**
* Reconstruct the database in the following cases:
* - Language has changed
* - Build has changed
*/
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.
* @param localeStr the default locale for the device.
*/
@VisibleForTesting
void updateDatabase(boolean needsReindexing, String localeStr) {
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();
}
}
/**
* 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);
if (TextUtils.isEmpty(key) && Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Empty non-indexable key from: "
+ packageContext.getPackageName());
continue;
}
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 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;
@DrawableRes
int iconResId;
ResultPayload payload;
boolean enabled;
final String fragmentName = sir.className;
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)
.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);
iconResId = XmlParserUtils.getDataIcon(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 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(raw.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.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(mContext));
}
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;
}
/**
* Adds intent to inline payloads, or creates an Intent Payload as a fallback if the
* payload is null.
*/
private void setIntent(Context context) {
if (mPayload != null) {
return;
}
final Intent intent = buildIntent(context);
mPayload = new ResultPayload(intent);
mPayloadType = ResultPayload.PayloadType.INTENT;
}
/**
* Adds Intent payload to builder.
*/
private Intent buildIntent(Context context) {
final Intent intent;
if (TextUtils.isEmpty(mIntentAction)) {
// Action is null, we will launch it as a sub-setting
intent = DatabaseIndexingUtils.buildSubsettingIntent(context, mClassName, mKey,
mScreenTitle);
} else {
intent = new Intent(mIntentAction);
final String targetClass = mIntentTargetClass;
if (!TextUtils.isEmpty(mIntentTargetPackage)
&& !TextUtils.isEmpty(targetClass)) {
final ComponentName component = new ComponentName(mIntentTargetPackage,
targetClass);
intent.setComponent(component);
}
intent.putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, mKey);
}
return intent;
}
public DatabaseRow build(Context context) {
setIntent(context);
return new DatabaseRow(this);
}
}
}
public class IndexingTask extends AsyncTask<Void, Void, Void> {
@VisibleForTesting
IndexingCallback mCallback;
public IndexingTask(IndexingCallback callback) {
mCallback = callback;
}
@Override
protected void onPreExecute() {
mIsIndexingComplete.set(false);
}
@Override
protected Void doInBackground(Void... voids) {
performIndexing();
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
mIsIndexingComplete.set(true);
if (mCallback != null) {
mCallback.onIndexingFinished();
}
}
}
}