diff options
Diffstat (limited to 'src/com/android/settings/search')
9 files changed, 2568 insertions, 0 deletions
diff --git a/src/com/android/settings/search/BaseSearchIndexProvider.java b/src/com/android/settings/search/BaseSearchIndexProvider.java new file mode 100644 index 0000000..0fe1944 --- /dev/null +++ b/src/com/android/settings/search/BaseSearchIndexProvider.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2014 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.Context; +import android.provider.SearchIndexableResource; + +import java.util.Collections; +import java.util.List; + +/** + * A basic SearchIndexProvider that returns no data to index. + */ +public class BaseSearchIndexProvider implements Indexable.SearchIndexProvider { + + private static final List<String> EMPTY_LIST = Collections.<String>emptyList(); + + public BaseSearchIndexProvider() { + } + + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, boolean enabled) { + return null; + } + + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { + return null; + } + + @Override + public List<String> getNonIndexableKeys(Context context) { + return EMPTY_LIST; + } +} diff --git a/src/com/android/settings/search/DynamicIndexableContentMonitor.java b/src/com/android/settings/search/DynamicIndexableContentMonitor.java new file mode 100644 index 0000000..12bb6ef --- /dev/null +++ b/src/com/android/settings/search/DynamicIndexableContentMonitor.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2014 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.accessibilityservice.AccessibilityService; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.database.ContentObserver; +import android.hardware.input.InputManager; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.UserHandle; +import android.print.PrintManager; +import android.printservice.PrintService; +import android.printservice.PrintServiceInfo; +import android.provider.UserDictionary; +import android.view.accessibility.AccessibilityManager; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; +import com.android.internal.content.PackageMonitor; +import com.android.settings.accessibility.AccessibilitySettings; +import com.android.settings.inputmethod.InputMethodAndLanguageSettings; +import com.android.settings.print.PrintSettingsFragment; + +import java.util.ArrayList; +import java.util.List; + +public final class DynamicIndexableContentMonitor extends PackageMonitor implements + InputManager.InputDeviceListener { + + private static final long DELAY_PROCESS_PACKAGE_CHANGE = 2000; + + private static final int MSG_PACKAGE_AVAILABLE = 1; + private static final int MSG_PACKAGE_UNAVAILABLE = 2; + + private final List<String> mAccessibilityServices = new ArrayList<String>(); + private final List<String> mPrintServices = new ArrayList<String>(); + private final List<String> mImeServices = new ArrayList<String>(); + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_PACKAGE_AVAILABLE: { + String packageName = (String) msg.obj; + handlePackageAvailable(packageName); + } break; + + case MSG_PACKAGE_UNAVAILABLE: { + String packageName = (String) msg.obj; + handlePackageUnavailable(packageName); + } break; + } + } + }; + + private final ContentObserver mUserDictionaryContentObserver = + new UserDictionaryContentObserver(mHandler); + + private Context mContext; + private boolean mHasFeaturePrinting; + private boolean mHasFeatureIme; + + private static Intent getAccessibilityServiceIntent(String packageName) { + final Intent intent = new Intent(AccessibilityService.SERVICE_INTERFACE); + intent.setPackage(packageName); + return intent; + } + + private static Intent getPrintServiceIntent(String packageName) { + final Intent intent = new Intent(PrintService.SERVICE_INTERFACE); + intent.setPackage(packageName); + return intent; + } + + private static Intent getIMEServiceIntent(String packageName) { + final Intent intent = new Intent("android.view.InputMethod"); + intent.setPackage(packageName); + return intent; + } + + public void register(Context context) { + mContext = context; + + mHasFeaturePrinting = mContext.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_PRINTING); + mHasFeatureIme = mContext.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_INPUT_METHODS); + + // Cache accessibility service packages to know when they go away. + AccessibilityManager accessibilityManager = (AccessibilityManager) + mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); + List<AccessibilityServiceInfo> accessibilityServices = accessibilityManager + .getInstalledAccessibilityServiceList(); + final int accessibilityServiceCount = accessibilityServices.size(); + for (int i = 0; i < accessibilityServiceCount; i++) { + AccessibilityServiceInfo accessibilityService = accessibilityServices.get(i); + ResolveInfo resolveInfo = accessibilityService.getResolveInfo(); + if (resolveInfo == null || resolveInfo.serviceInfo == null) { + continue; + } + mAccessibilityServices.add(resolveInfo.serviceInfo.packageName); + } + + if (mHasFeaturePrinting) { + // Cache print service packages to know when they go away. + PrintManager printManager = (PrintManager) + mContext.getSystemService(Context.PRINT_SERVICE); + List<PrintServiceInfo> printServices = printManager.getInstalledPrintServices(); + final int serviceCount = printServices.size(); + for (int i = 0; i < serviceCount; i++) { + PrintServiceInfo printService = printServices.get(i); + ResolveInfo resolveInfo = printService.getResolveInfo(); + if (resolveInfo == null || resolveInfo.serviceInfo == null) { + continue; + } + mPrintServices.add(resolveInfo.serviceInfo.packageName); + } + } + + // Cache IME service packages to know when they go away. + if (mHasFeatureIme) { + InputMethodManager imeManager = (InputMethodManager) + mContext.getSystemService(Context.INPUT_METHOD_SERVICE); + List<InputMethodInfo> inputMethods = imeManager.getInputMethodList(); + final int inputMethodCount = inputMethods.size(); + for (int i = 0; i < inputMethodCount; i++) { + InputMethodInfo inputMethod = inputMethods.get(i); + ServiceInfo serviceInfo = inputMethod.getServiceInfo(); + if (serviceInfo == null) continue; + mImeServices.add(serviceInfo.packageName); + } + + // Watch for related content URIs. + mContext.getContentResolver().registerContentObserver( + UserDictionary.Words.CONTENT_URI, true, mUserDictionaryContentObserver); + } + + // Watch for input device changes. + InputManager inputManager = (InputManager) context.getSystemService( + Context.INPUT_SERVICE); + inputManager.registerInputDeviceListener(this, mHandler); + + // Start tracking packages. + register(context, Looper.getMainLooper(), UserHandle.CURRENT, false); + } + + public void unregister() { + super.unregister(); + + InputManager inputManager = (InputManager) mContext.getSystemService( + Context.INPUT_SERVICE); + inputManager.unregisterInputDeviceListener(this); + + if (mHasFeatureIme) { + mContext.getContentResolver().unregisterContentObserver( + mUserDictionaryContentObserver); + } + + mAccessibilityServices.clear(); + mPrintServices.clear(); + mImeServices.clear(); + } + + // Covers installed, appeared external storage with the package, upgraded. + @Override + public void onPackageAppeared(String packageName, int uid) { + postMessage(MSG_PACKAGE_AVAILABLE, packageName); + } + + // Covers uninstalled, removed external storage with the package. + @Override + public void onPackageDisappeared(String packageName, int uid) { + postMessage(MSG_PACKAGE_UNAVAILABLE, packageName); + } + + // Covers enabled, disabled. + @Override + public void onPackageModified(String packageName) { + super.onPackageModified(packageName); + final int state = mContext.getPackageManager().getApplicationEnabledSetting( + packageName); + if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT + || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { + postMessage(MSG_PACKAGE_AVAILABLE, packageName); + } else { + postMessage(MSG_PACKAGE_UNAVAILABLE, packageName); + } + } + + @Override + public void onInputDeviceAdded(int deviceId) { + Index.getInstance(mContext).updateFromClassNameResource( + InputMethodAndLanguageSettings.class.getName(), false, true); + } + + @Override + public void onInputDeviceRemoved(int deviceId) { + onInputDeviceChanged(deviceId); + } + + @Override + public void onInputDeviceChanged(int deviceId) { + Index.getInstance(mContext).updateFromClassNameResource( + InputMethodAndLanguageSettings.class.getName(), true, true); + } + + private void postMessage(int what, String packageName) { + Message message = mHandler.obtainMessage(what, packageName); + mHandler.sendMessageDelayed(message, DELAY_PROCESS_PACKAGE_CHANGE); + } + + private void handlePackageAvailable(String packageName) { + if (!mAccessibilityServices.contains(packageName)) { + final Intent intent = getAccessibilityServiceIntent(packageName); + if (!mContext.getPackageManager().queryIntentServices(intent, 0).isEmpty()) { + mAccessibilityServices.add(packageName); + Index.getInstance(mContext).updateFromClassNameResource( + AccessibilitySettings.class.getName(), false, true); + } + } + + if (mHasFeaturePrinting) { + if (!mPrintServices.contains(packageName)) { + final Intent intent = getPrintServiceIntent(packageName); + if (!mContext.getPackageManager().queryIntentServices(intent, 0).isEmpty()) { + mPrintServices.add(packageName); + Index.getInstance(mContext).updateFromClassNameResource( + PrintSettingsFragment.class.getName(), false, true); + } + } + } + + if (mHasFeatureIme) { + if (!mImeServices.contains(packageName)) { + Intent intent = getIMEServiceIntent(packageName); + if (!mContext.getPackageManager().queryIntentServices(intent, 0).isEmpty()) { + mImeServices.add(packageName); + Index.getInstance(mContext).updateFromClassNameResource( + InputMethodAndLanguageSettings.class.getName(), false, true); + } + } + } + } + + private void handlePackageUnavailable(String packageName) { + final int accessibilityIndex = mAccessibilityServices.indexOf(packageName); + if (accessibilityIndex >= 0) { + mAccessibilityServices.remove(accessibilityIndex); + Index.getInstance(mContext).updateFromClassNameResource( + AccessibilitySettings.class.getName(), true, true); + } + + if (mHasFeaturePrinting) { + final int printIndex = mPrintServices.indexOf(packageName); + if (printIndex >= 0) { + mPrintServices.remove(printIndex); + Index.getInstance(mContext).updateFromClassNameResource( + PrintSettingsFragment.class.getName(), true, true); + } + } + + if (mHasFeatureIme) { + final int imeIndex = mImeServices.indexOf(packageName); + if (imeIndex >= 0) { + mImeServices.remove(imeIndex); + Index.getInstance(mContext).updateFromClassNameResource( + InputMethodAndLanguageSettings.class.getName(), true, true); + } + } + } + + private final class UserDictionaryContentObserver extends ContentObserver { + + public UserDictionaryContentObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (UserDictionary.Words.CONTENT_URI.equals(uri)) { + Index.getInstance(mContext).updateFromClassNameResource( + InputMethodAndLanguageSettings.class.getName(), true, true); + } + }; + } +} diff --git a/src/com/android/settings/search/Index.java b/src/com/android/settings/search/Index.java new file mode 100644 index 0000000..3957cf6 --- /dev/null +++ b/src/com/android/settings/search/Index.java @@ -0,0 +1,1320 @@ +/* + * Copyright (C) 2014 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.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.MergeCursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.AsyncTask; +import android.provider.SearchIndexableData; +import android.provider.SearchIndexableResource; +import android.provider.SearchIndexablesContract; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.util.Xml; +import com.android.settings.R; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.text.Normalizer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; + +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_RANK; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_OFF; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ENTRIES; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SCREEN_TITLE; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_CLASS_NAME; +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_PACKAGE; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEY; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_USER_ID; + +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RANK; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID; +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_PACKAGE; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS; + +import static com.android.settings.search.IndexDatabaseHelper.Tables; +import static com.android.settings.search.IndexDatabaseHelper.IndexColumns; + +public class Index { + + private static final String LOG_TAG = "Index"; + + // Those indices should match the indices of SELECT_COLUMNS ! + public static final int COLUMN_INDEX_RANK = 0; + public static final int COLUMN_INDEX_TITLE = 1; + public static final int COLUMN_INDEX_SUMMARY_ON = 2; + public static final int COLUMN_INDEX_SUMMARY_OFF = 3; + public static final int COLUMN_INDEX_ENTRIES = 4; + public static final int COLUMN_INDEX_KEYWORDS = 5; + public static final int COLUMN_INDEX_CLASS_NAME = 6; + public static final int COLUMN_INDEX_SCREEN_TITLE = 7; + public static final int COLUMN_INDEX_ICON = 8; + public static final int COLUMN_INDEX_INTENT_ACTION = 9; + public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE = 10; + public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS = 11; + public static final int COLUMN_INDEX_ENABLED = 12; + public static final int COLUMN_INDEX_KEY = 13; + public static final int COLUMN_INDEX_USER_ID = 14; + + public static final String ENTRIES_SEPARATOR = "|"; + + // If you change the order of columns here, you SHOULD change the COLUMN_INDEX_XXX values + private static final String[] SELECT_COLUMNS = new String[] { + IndexColumns.DATA_RANK, // 0 + IndexColumns.DATA_TITLE, // 1 + IndexColumns.DATA_SUMMARY_ON, // 2 + IndexColumns.DATA_SUMMARY_OFF, // 3 + IndexColumns.DATA_ENTRIES, // 4 + IndexColumns.DATA_KEYWORDS, // 5 + IndexColumns.CLASS_NAME, // 6 + IndexColumns.SCREEN_TITLE, // 7 + IndexColumns.ICON, // 8 + IndexColumns.INTENT_ACTION, // 9 + IndexColumns.INTENT_TARGET_PACKAGE, // 10 + IndexColumns.INTENT_TARGET_CLASS, // 11 + IndexColumns.ENABLED, // 12 + IndexColumns.DATA_KEY_REF // 13 + }; + + private static final String[] MATCH_COLUMNS_PRIMARY = { + IndexColumns.DATA_TITLE, + IndexColumns.DATA_TITLE_NORMALIZED, + IndexColumns.DATA_KEYWORDS + }; + + private static final String[] MATCH_COLUMNS_SECONDARY = { + IndexColumns.DATA_SUMMARY_ON, + IndexColumns.DATA_SUMMARY_ON_NORMALIZED, + IndexColumns.DATA_SUMMARY_OFF, + IndexColumns.DATA_SUMMARY_OFF_NORMALIZED, + IndexColumns.DATA_ENTRIES + }; + + // Max number of saved search queries (who will be used for proposing suggestions) + private static long MAX_SAVED_SEARCH_QUERY = 64; + // Max number of proposed suggestions + private static final int MAX_PROPOSED_SUGGESTIONS = 5; + + private static final String BASE_AUTHORITY = "com.android.settings"; + + private static final String EMPTY = ""; + private static final String NON_BREAKING_HYPHEN = "\u2011"; + private static final String HYPHEN = "-"; + + private 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.<String>emptyList(); + + private static Index sInstance; + + private static final Pattern REMOVE_DIACRITICALS_PATTERN + = Pattern.compile("\\p{InCombiningDiacriticalMarks}+"); + + /** + * A private class to describe the update data for the Index database + */ + private static class UpdateData { + public List<SearchIndexableData> dataToUpdate; + public List<SearchIndexableData> dataToDelete; + public Map<String, List<String>> nonIndexableKeys; + + public boolean forceUpdate = false; + + public UpdateData() { + dataToUpdate = new ArrayList<SearchIndexableData>(); + dataToDelete = new ArrayList<SearchIndexableData>(); + nonIndexableKeys = new HashMap<String, List<String>>(); + } + + public UpdateData(UpdateData other) { + dataToUpdate = new ArrayList<SearchIndexableData>(other.dataToUpdate); + dataToDelete = new ArrayList<SearchIndexableData>(other.dataToDelete); + nonIndexableKeys = new HashMap<String, List<String>>(other.nonIndexableKeys); + forceUpdate = other.forceUpdate; + } + + public UpdateData copy() { + return new UpdateData(this); + } + + public void clear() { + dataToUpdate.clear(); + dataToDelete.clear(); + nonIndexableKeys.clear(); + forceUpdate = false; + } + } + + private final AtomicBoolean mIsAvailable = new AtomicBoolean(false); + private final UpdateData mDataToProcess = new UpdateData(); + private Context mContext; + private final String mBaseAuthority; + + /** + * A basic singleton + */ + public static Index getInstance(Context context) { + if (sInstance == null) { + sInstance = new Index(context, BASE_AUTHORITY); + } else { + sInstance.setContext(context); + } + return sInstance; + } + + public Index(Context context, String baseAuthority) { + mContext = context; + mBaseAuthority = baseAuthority; + } + + public void setContext(Context context) { + mContext = context; + } + + public boolean isAvailable() { + return mIsAvailable.get(); + } + + public Cursor search(String query) { + final SQLiteDatabase database = getReadableDatabase(); + final Cursor[] cursors = new Cursor[2]; + + final String primarySql = buildSearchSQL(query, MATCH_COLUMNS_PRIMARY, true); + Log.d(LOG_TAG, "Search primary query: " + primarySql); + cursors[0] = database.rawQuery(primarySql, null); + + // We need to use an EXCEPT operator as negate MATCH queries do not work. + StringBuilder sql = new StringBuilder( + buildSearchSQL(query, MATCH_COLUMNS_SECONDARY, false)); + sql.append(" EXCEPT "); + sql.append(primarySql); + + final String secondarySql = sql.toString(); + Log.d(LOG_TAG, "Search secondary query: " + secondarySql); + cursors[1] = database.rawQuery(secondarySql, null); + + return new MergeCursor(cursors); + } + + public Cursor getSuggestions(String query) { + final String sql = buildSuggestionsSQL(query); + Log.d(LOG_TAG, "Suggestions query: " + sql); + return getReadableDatabase().rawQuery(sql, null); + } + + private String buildSuggestionsSQL(String query) { + StringBuilder sb = new StringBuilder(); + + sb.append("SELECT "); + sb.append(IndexDatabaseHelper.SavedQueriesColums.QUERY); + sb.append(" FROM "); + sb.append(Tables.TABLE_SAVED_QUERIES); + + if (TextUtils.isEmpty(query)) { + sb.append(" ORDER BY rowId DESC"); + } else { + sb.append(" WHERE "); + sb.append(IndexDatabaseHelper.SavedQueriesColums.QUERY); + sb.append(" LIKE "); + sb.append("'"); + sb.append(query); + sb.append("%"); + sb.append("'"); + } + + sb.append(" LIMIT "); + sb.append(MAX_PROPOSED_SUGGESTIONS); + + return sb.toString(); + } + + public long addSavedQuery(String query){ + final SaveSearchQueryTask task = new SaveSearchQueryTask(); + task.execute(query); + try { + return task.get(); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "Cannot insert saved query: " + query, e); + return -1 ; + } catch (ExecutionException e) { + Log.e(LOG_TAG, "Cannot insert saved query: " + query, e); + return -1; + } + } + + public void update() { + final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE); + List<ResolveInfo> list = + mContext.getPackageManager().queryIntentContentProviders(intent, 0); + + final int size = list.size(); + for (int n = 0; n < size; n++) { + final ResolveInfo info = list.get(n); + if (!isWellKnownProvider(info)) { + continue; + } + final String authority = info.providerInfo.authority; + final String packageName = info.providerInfo.packageName; + + addIndexablesFromRemoteProvider(packageName, authority); + addNonIndexablesKeysFromRemoteProvider(packageName, authority); + } + + updateInternal(); + } + + private boolean addIndexablesFromRemoteProvider(String packageName, String authority) { + try { + final int baseRank = Ranking.getBaseRankForAuthority(authority); + + 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, baseRank); + + final Uri uriForRawData = buildUriForRawData(authority); + addIndexablesForRawDataUri(context, packageName, uriForRawData, + SearchIndexablesContract.INDEXABLES_RAW_COLUMNS, baseRank); + return true; + } catch (PackageManager.NameNotFoundException e) { + Log.w(LOG_TAG, "Could not create context for " + packageName + ": " + + Log.getStackTraceString(e)); + return false; + } + } + + private void addNonIndexablesKeysFromRemoteProvider(String packageName, + String authority) { + final List<String> keys = + getNonIndexablesKeysFromRemoteProvider(packageName, authority); + addNonIndexableKeys(packageName, 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; + } + + List<String> result = new ArrayList<String>(); + 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 addIndexableData(SearchIndexableResource[] array) { + synchronized (mDataToProcess) { + final int count = array.length; + for (int n = 0; n < count; n++) { + mDataToProcess.dataToUpdate.add(array[n]); + } + } + } + + public void deleteIndexableData(SearchIndexableData data) { + synchronized (mDataToProcess) { + mDataToProcess.dataToDelete.add(data); + } + } + + public void addNonIndexableKeys(String authority, List<String> keys) { + synchronized (mDataToProcess) { + mDataToProcess.nonIndexableKeys.put(authority, keys); + } + } + + /** + * Only allow a "well known" SearchIndexablesProvider. The provider should: + * + * - have read/write {@link android.Manifest.permission#READ_SEARCH_INDEXABLES} + * - be from a privileged package + */ + private boolean isWellKnownProvider(ResolveInfo info) { + final String authority = info.providerInfo.authority; + final String packageName = info.providerInfo.applicationInfo.packageName; + + if (TextUtils.isEmpty(authority) || TextUtils.isEmpty(packageName)) { + return false; + } + + final String readPermission = info.providerInfo.readPermission; + final String writePermission = info.providerInfo.writePermission; + + if (TextUtils.isEmpty(readPermission) || TextUtils.isEmpty(writePermission)) { + return false; + } + + if (!android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(readPermission) || + !android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(writePermission)) { + return false; + } + + return isPrivilegedPackage(packageName); + } + + private boolean isPrivilegedPackage(String packageName) { + final PackageManager pm = mContext.getPackageManager(); + try { + PackageInfo packInfo = pm.getPackageInfo(packageName, 0); + return ((packInfo.applicationInfo.flags & ApplicationInfo.FLAG_PRIVILEGED) != 0); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + private void updateFromRemoteProvider(String packageName, String authority) { + if (addIndexablesFromRemoteProvider(packageName, authority)) { + updateInternal(); + } + } + + /** + * Update the Index for a specific class name resources + * + * @param className the class name (typically a fragment name). + * @param rebuild true means that you want to delete the data from the Index first. + * @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 rebuild, + 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; + if (rebuild) { + deleteIndexableData(res); + } + addIndexableData(res); + mDataToProcess.forceUpdate = true; + updateInternal(); + res.enabled = false; + } + + public void updateFromSearchIndexableData(SearchIndexableData data) { + addIndexableData(data); + mDataToProcess.forceUpdate = true; + updateInternal(); + } + + private SQLiteDatabase getReadableDatabase() { + return IndexDatabaseHelper.getInstance(mContext).getReadableDatabase(); + } + + private SQLiteDatabase getWritableDatabase() { + return IndexDatabaseHelper.getInstance(mContext).getWritableDatabase(); + } + + 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 updateInternal() { + synchronized (mDataToProcess) { + final UpdateIndexTask task = new UpdateIndexTask(); + UpdateData copy = mDataToProcess.copy(); + task.execute(copy); + mDataToProcess.clear(); + } + } + + private void addIndexablesForXmlResourceUri(Context packageContext, String packageName, + Uri uri, String[] projection, int baseRank) { + + 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); + final int rank = (providerRank > 0) ? baseRank + providerRank : baseRank; + + 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.rank = rank; + 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, int baseRank) { + + 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); + final int rank = (providerRank > 0) ? baseRank + providerRank : baseRank; + + 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.rank = rank; + 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(); + } + } + + private String buildSearchSQL(String query, String[] colums, boolean withOrderBy) { + StringBuilder sb = new StringBuilder(); + sb.append(buildSearchSQLForColumn(query, colums)); + if (withOrderBy) { + sb.append(" ORDER BY "); + sb.append(IndexColumns.DATA_RANK); + } + return sb.toString(); + } + + private String buildSearchSQLForColumn(String query, String[] columnNames) { + StringBuilder sb = new StringBuilder(); + sb.append("SELECT "); + for (int n = 0; n < SELECT_COLUMNS.length; n++) { + sb.append(SELECT_COLUMNS[n]); + if (n < SELECT_COLUMNS.length - 1) { + sb.append(", "); + } + } + sb.append(" FROM "); + sb.append(Tables.TABLE_PREFS_INDEX); + sb.append(" WHERE "); + sb.append(buildSearchWhereStringForColumns(query, columnNames)); + + return sb.toString(); + } + + private String buildSearchWhereStringForColumns(String query, String[] columnNames) { + final StringBuilder sb = new StringBuilder(Tables.TABLE_PREFS_INDEX); + sb.append(" MATCH "); + DatabaseUtils.appendEscapedSQLString(sb, + buildSearchMatchStringForColumns(query, columnNames)); + sb.append(" AND "); + sb.append(IndexColumns.LOCALE); + sb.append(" = "); + DatabaseUtils.appendEscapedSQLString(sb, Locale.getDefault().toString()); + sb.append(" AND "); + sb.append(IndexColumns.ENABLED); + sb.append(" = 1"); + return sb.toString(); + } + + private String buildSearchMatchStringForColumns(String query, String[] columnNames) { + final String value = query + "*"; + StringBuilder sb = new StringBuilder(); + final int count = columnNames.length; + for (int n = 0; n < count; n++) { + sb.append(columnNames[n]); + sb.append(":"); + sb.append(value); + if (n < count - 1) { + sb.append(" OR "); + } + } + return sb.toString(); + } + + private void indexOneSearchIndexableData(SQLiteDatabase database, String localeStr, + SearchIndexableData data, Map<String, List<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; + } + + updateOneRowWithFilteredData(database, localeStr, + raw.title, + raw.summaryOn, + raw.summaryOff, + raw.entries, + raw.className, + raw.screenTitle, + raw.iconResId, + raw.rank, + raw.keywords, + raw.intentAction, + raw.intentTargetPackage, + raw.intentTargetClass, + raw.enabled, + raw.key, + raw.userId); + } + + private static boolean isIndexableClass(final Class<?> clazz) { + return (clazz != null) && Indexable.class.isAssignableFrom(clazz); + } + + private static Class<?> getIndexableClass(String className) { + final Class<?> clazz; + try { + clazz = Class.forName(className); + } catch (ClassNotFoundException e) { + Log.d(LOG_TAG, "Cannot find class: " + className); + return null; + } + return isIndexableClass(clazz) ? clazz : null; + } + + private void indexOneResource(SQLiteDatabase database, String localeStr, + SearchIndexableResource sir, Map<String, List<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) { + List<String> resNonIndxableKeys = nonIndexableKeysFromResource.get(sir.packageName); + if (resNonIndxableKeys != null && resNonIndxableKeys.size() > 0) { + nonIndexableKeys.addAll(resNonIndxableKeys); + } + + indexFromResource(sir.context, database, localeStr, + sir.xmlResId, sir.className, sir.iconResId, sir.rank, + sir.intentAction, sir.intentTargetPackage, sir.intentTargetClass, + nonIndexableKeys); + } else { + if (TextUtils.isEmpty(sir.className)) { + Log.w(LOG_TAG, "Cannot index an empty Search Provider name!"); + return; + } + + final Class<?> clazz = 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 = getSearchIndexProvider(clazz); + if (provider != null) { + List<String> providerNonIndexableKeys = provider.getNonIndexableKeys(sir.context); + if (providerNonIndexableKeys != null && providerNonIndexableKeys.size() > 0) { + nonIndexableKeys.addAll(providerNonIndexableKeys); + } + + indexFromProvider(mContext, database, localeStr, provider, sir.className, + sir.iconResId, sir.rank, sir.enabled, nonIndexableKeys); + } + } + } + + private Indexable.SearchIndexProvider getSearchIndexProvider(final Class<?> clazz) { + try { + final Field f = clazz.getField(FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER); + return (Indexable.SearchIndexProvider) f.get(null); + } catch (NoSuchFieldException e) { + Log.d(LOG_TAG, "Cannot find field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); + } catch (SecurityException se) { + Log.d(LOG_TAG, + "Security exception for field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); + } catch (IllegalAccessException e) { + Log.d(LOG_TAG, + "Illegal access to field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); + } catch (IllegalArgumentException e) { + Log.d(LOG_TAG, + "Illegal argument when accessing field '" + + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); + } + return null; + } + + private void indexFromResource(Context context, SQLiteDatabase database, String localeStr, + int xmlResId, String fragmentName, int iconResId, int rank, + String intentAction, String intentTargetPackage, String intentTargetClass, + List<String> nonIndexableKeys) { + + XmlResourceParser parser = null; + try { + parser = context.getResources().getXml(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 = getDataTitle(context, attrs); + + String key = getDataKey(context, attrs); + + String title; + String summary; + String keywords; + + // Insert rows for the main PreferenceScreen node. Rewrite the data for removing + // hyphens. + if (!nonIndexableKeys.contains(key)) { + title = getDataTitle(context, attrs); + summary = getDataSummary(context, attrs); + keywords = getDataKeywords(context, attrs); + + updateOneRowWithFilteredData(database, localeStr, title, summary, null, null, + fragmentName, screenTitle, iconResId, rank, + keywords, intentAction, intentTargetPackage, intentTargetClass, true, + key, -1 /* default user id */); + } + + 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(); + + key = getDataKey(context, attrs); + if (nonIndexableKeys.contains(key)) { + continue; + } + + title = getDataTitle(context, attrs); + keywords = getDataKeywords(context, attrs); + + if (!nodeName.equals(NODE_NAME_CHECK_BOX_PREFERENCE)) { + summary = getDataSummary(context, attrs); + + String entries = null; + + if (nodeName.endsWith(NODE_NAME_LIST_PREFERENCE)) { + entries = getDataEntries(context, attrs); + } + + // Insert rows for the child nodes of PreferenceScreen + updateOneRowWithFilteredData(database, localeStr, title, summary, null, entries, + fragmentName, screenTitle, iconResId, rank, + keywords, intentAction, intentTargetPackage, intentTargetClass, + true, key, -1 /* default user id */); + } else { + String summaryOn = getDataSummaryOn(context, attrs); + String summaryOff = getDataSummaryOff(context, attrs); + + if (TextUtils.isEmpty(summaryOn) && TextUtils.isEmpty(summaryOff)) { + summaryOn = getDataSummary(context, attrs); + } + + updateOneRowWithFilteredData(database, localeStr, title, summaryOn, summaryOff, + null, fragmentName, screenTitle, iconResId, rank, + keywords, intentAction, intentTargetPackage, intentTargetClass, + true, key, -1 /* default user id */); + } + } + + } 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(Context context, SQLiteDatabase database, String localeStr, + Indexable.SearchIndexProvider provider, String className, int iconResId, int rank, + boolean enabled, List<String> nonIndexableKeys) { + + if (provider == null) { + Log.w(LOG_TAG, "Cannot find provider: " + className); + return; + } + + final List<SearchIndexableRaw> rawList = provider.getRawDataToIndex(context, 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; + } + + if (nonIndexableKeys.contains(raw.key)) { + continue; + } + + updateOneRowWithFilteredData(database, localeStr, + raw.title, + raw.summaryOn, + raw.summaryOff, + raw.entries, + className, + raw.screenTitle, + iconResId, + rank, + raw.keywords, + raw.intentAction, + raw.intentTargetPackage, + raw.intentTargetClass, + raw.enabled, + raw.key, + raw.userId); + } + } + + final List<SearchIndexableResource> resList = + provider.getXmlResourcesToIndex(context, enabled); + 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; + } + + final int itemIconResId = (item.iconResId == 0) ? iconResId : item.iconResId; + final int itemRank = (item.rank == 0) ? rank : item.rank; + String itemClassName = (TextUtils.isEmpty(item.className)) + ? className : item.className; + + indexFromResource(context, database, localeStr, + item.xmlResId, itemClassName, itemIconResId, itemRank, + item.intentAction, item.intentTargetPackage, + item.intentTargetClass, nonIndexableKeys); + } + } + } + + private void updateOneRowWithFilteredData(SQLiteDatabase database, String locale, + String title, String summaryOn, String summaryOff, String entries, + String className, + String screenTitle, int iconResId, int rank, String keywords, + String intentAction, String intentTargetPackage, String intentTargetClass, + boolean enabled, String key, int userId) { + + final String updatedTitle = normalizeHyphen(title); + final String updatedSummaryOn = normalizeHyphen(summaryOn); + final String updatedSummaryOff = normalizeHyphen(summaryOff); + + final String normalizedTitle = normalizeString(updatedTitle); + final String normalizedSummaryOn = normalizeString(updatedSummaryOn); + final String normalizedSummaryOff = normalizeString(updatedSummaryOff); + + updateOneRow(database, locale, + updatedTitle, normalizedTitle, updatedSummaryOn, normalizedSummaryOn, + updatedSummaryOff, normalizedSummaryOff, entries, + className, screenTitle, iconResId, + rank, keywords, intentAction, intentTargetPackage, intentTargetClass, enabled, + key, userId); + } + + private static String normalizeHyphen(String input) { + return (input != null) ? input.replaceAll(NON_BREAKING_HYPHEN, HYPHEN) : EMPTY; + } + + private static String normalizeString(String input) { + final String nohyphen = (input != null) ? input.replaceAll(HYPHEN, EMPTY) : EMPTY; + final String normalized = Normalizer.normalize(nohyphen, Normalizer.Form.NFD); + + return REMOVE_DIACRITICALS_PATTERN.matcher(normalized).replaceAll("").toLowerCase(); + } + + private void updateOneRow(SQLiteDatabase database, String locale, + String updatedTitle, String normalizedTitle, + String updatedSummaryOn, String normalizedSummaryOn, + String updatedSummaryOff, String normalizedSummaryOff, String entries, + String className, String screenTitle, int iconResId, int rank, String keywords, + String intentAction, String intentTargetPackage, String intentTargetClass, + boolean enabled, String key, int userId) { + + if (TextUtils.isEmpty(updatedTitle)) { + return; + } + + // 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 the title and the screenTitle. + StringBuilder sb = new StringBuilder(updatedTitle); + sb.append(screenTitle); + int docId = sb.toString().hashCode(); + + ContentValues values = new ContentValues(); + values.put(IndexColumns.DOCID, docId); + values.put(IndexColumns.LOCALE, locale); + values.put(IndexColumns.DATA_RANK, rank); + values.put(IndexColumns.DATA_TITLE, updatedTitle); + values.put(IndexColumns.DATA_TITLE_NORMALIZED, normalizedTitle); + values.put(IndexColumns.DATA_SUMMARY_ON, updatedSummaryOn); + values.put(IndexColumns.DATA_SUMMARY_ON_NORMALIZED, normalizedSummaryOn); + values.put(IndexColumns.DATA_SUMMARY_OFF, updatedSummaryOff); + values.put(IndexColumns.DATA_SUMMARY_OFF_NORMALIZED, normalizedSummaryOff); + values.put(IndexColumns.DATA_ENTRIES, entries); + values.put(IndexColumns.DATA_KEYWORDS, keywords); + values.put(IndexColumns.CLASS_NAME, className); + values.put(IndexColumns.SCREEN_TITLE, screenTitle); + values.put(IndexColumns.INTENT_ACTION, intentAction); + values.put(IndexColumns.INTENT_TARGET_PACKAGE, intentTargetPackage); + values.put(IndexColumns.INTENT_TARGET_CLASS, intentTargetClass); + values.put(IndexColumns.ICON, iconResId); + values.put(IndexColumns.ENABLED, enabled); + values.put(IndexColumns.DATA_KEY_REF, key); + values.put(IndexColumns.USER_ID, userId); + + database.replaceOrThrow(Tables.TABLE_PREFS_INDEX, null, values); + } + + private String getDataKey(Context context, AttributeSet attrs) { + return getData(context, attrs, + com.android.internal.R.styleable.Preference, + com.android.internal.R.styleable.Preference_key); + } + + private String getDataTitle(Context context, AttributeSet attrs) { + return getData(context, attrs, + com.android.internal.R.styleable.Preference, + com.android.internal.R.styleable.Preference_title); + } + + private String getDataSummary(Context context, AttributeSet attrs) { + return getData(context, attrs, + com.android.internal.R.styleable.Preference, + com.android.internal.R.styleable.Preference_summary); + } + + private String getDataSummaryOn(Context context, AttributeSet attrs) { + return getData(context, attrs, + com.android.internal.R.styleable.CheckBoxPreference, + com.android.internal.R.styleable.CheckBoxPreference_summaryOn); + } + + private String getDataSummaryOff(Context context, AttributeSet attrs) { + return getData(context, attrs, + com.android.internal.R.styleable.CheckBoxPreference, + com.android.internal.R.styleable.CheckBoxPreference_summaryOff); + } + + private String getDataEntries(Context context, AttributeSet attrs) { + return getDataEntries(context, attrs, + com.android.internal.R.styleable.ListPreference, + com.android.internal.R.styleable.ListPreference_entries); + } + + private String getDataKeywords(Context context, AttributeSet attrs) { + return getData(context, attrs, R.styleable.Preference, R.styleable.Preference_keywords); + } + + private String getData(Context context, AttributeSet set, int[] attrs, int resId) { + final TypedArray sa = context.obtainStyledAttributes(set, attrs); + final TypedValue tv = sa.peekValue(resId); + + CharSequence data = null; + if (tv != null && tv.type == TypedValue.TYPE_STRING) { + if (tv.resourceId != 0) { + data = context.getText(tv.resourceId); + } else { + data = tv.string; + } + } + return (data != null) ? data.toString() : null; + } + + private String getDataEntries(Context context, AttributeSet set, int[] attrs, int resId) { + final TypedArray sa = context.obtainStyledAttributes(set, attrs); + final TypedValue tv = sa.peekValue(resId); + + String[] data = null; + if (tv != null && tv.type == TypedValue.TYPE_REFERENCE) { + if (tv.resourceId != 0) { + data = context.getResources().getStringArray(tv.resourceId); + } + } + final int count = (data == null ) ? 0 : data.length; + if (count == 0) { + return null; + } + final StringBuilder result = new StringBuilder(); + for (int n = 0; n < count; n++) { + result.append(data[n]); + result.append(ENTRIES_SEPARATOR); + } + return result.toString(); + } + + private int getResId(Context context, AttributeSet set, int[] attrs, int resId) { + final TypedArray sa = context.obtainStyledAttributes(set, attrs); + final TypedValue tv = sa.peekValue(resId); + + if (tv != null && tv.type == TypedValue.TYPE_STRING) { + return tv.resourceId; + } else { + return 0; + } + } + + /** + * A private class for updating the Index database + */ + private class UpdateIndexTask extends AsyncTask<UpdateData, Integer, Void> { + + @Override + protected void onPreExecute() { + super.onPreExecute(); + mIsAvailable.set(false); + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + mIsAvailable.set(true); + } + + @Override + protected Void doInBackground(UpdateData... params) { + final List<SearchIndexableData> dataToUpdate = params[0].dataToUpdate; + final List<SearchIndexableData> dataToDelete = params[0].dataToDelete; + final Map<String, List<String>> nonIndexableKeys = params[0].nonIndexableKeys; + + final boolean forceUpdate = params[0].forceUpdate; + + final SQLiteDatabase database = getWritableDatabase(); + final String localeStr = Locale.getDefault().toString(); + + try { + database.beginTransaction(); + if (dataToDelete.size() > 0) { + processDataToDelete(database, localeStr, dataToDelete); + } + if (dataToUpdate.size() > 0) { + processDataToUpdate(database, localeStr, dataToUpdate, nonIndexableKeys, + forceUpdate); + } + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } + + return null; + } + + private boolean processDataToUpdate(SQLiteDatabase database, String localeStr, + List<SearchIndexableData> dataToUpdate, Map<String, List<String>> nonIndexableKeys, + boolean forceUpdate) { + + if (!forceUpdate && isLocaleAlreadyIndexed(database, localeStr)) { + Log.d(LOG_TAG, "Locale '" + localeStr + "' is already indexed"); + return true; + } + + boolean result = false; + final long current = System.currentTimeMillis(); + + final int count = dataToUpdate.size(); + for (int n = 0; n < count; n++) { + final SearchIndexableData data = dataToUpdate.get(n); + try { + indexOneSearchIndexableData(database, localeStr, data, nonIndexableKeys); + } catch (Exception e) { + Log.e(LOG_TAG, + "Cannot index: " + data.className + " for locale: " + localeStr, e); + } + } + + final long now = System.currentTimeMillis(); + Log.d(LOG_TAG, "Indexing locale '" + localeStr + "' took " + + (now - current) + " millis"); + return result; + } + + private boolean processDataToDelete(SQLiteDatabase database, String localeStr, + List<SearchIndexableData> dataToDelete) { + + boolean result = false; + final long current = System.currentTimeMillis(); + + final int count = dataToDelete.size(); + for (int n = 0; n < count; n++) { + final SearchIndexableData data = dataToDelete.get(n); + if (data == null) { + continue; + } + if (!TextUtils.isEmpty(data.className)) { + delete(database, IndexColumns.CLASS_NAME, data.className); + } else { + if (data instanceof SearchIndexableRaw) { + final SearchIndexableRaw raw = (SearchIndexableRaw) data; + if (!TextUtils.isEmpty(raw.title)) { + delete(database, IndexColumns.DATA_TITLE, raw.title); + } + } + } + } + + final long now = System.currentTimeMillis(); + Log.d(LOG_TAG, "Deleting data for locale '" + localeStr + "' took " + + (now - current) + " millis"); + return result; + } + + private int delete(SQLiteDatabase database, String columName, String value) { + final String whereClause = columName + "=?"; + final String[] whereArgs = new String[] { value }; + + return database.delete(Tables.TABLE_PREFS_INDEX, whereClause, whereArgs); + } + + private boolean isLocaleAlreadyIndexed(SQLiteDatabase database, String locale) { + Cursor cursor = null; + boolean result = false; + final StringBuilder sb = new StringBuilder(IndexColumns.LOCALE); + sb.append(" = "); + DatabaseUtils.appendEscapedSQLString(sb, locale); + try { + // We care only for 1 row + cursor = database.query(Tables.TABLE_PREFS_INDEX, null, + sb.toString(), null, null, null, null, "1"); + final int count = cursor.getCount(); + result = (count >= 1); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return result; + } + } + + /** + * A basic AsyncTask for saving a Search query into the database + */ + private class SaveSearchQueryTask extends AsyncTask<String, Void, Long> { + + @Override + protected Long doInBackground(String... params) { + final long now = new Date().getTime(); + + final ContentValues values = new ContentValues(); + values.put(IndexDatabaseHelper.SavedQueriesColums.QUERY, params[0]); + values.put(IndexDatabaseHelper.SavedQueriesColums.TIME_STAMP, now); + + final SQLiteDatabase database = getWritableDatabase(); + + long lastInsertedRowId = -1; + try { + // First, delete all saved queries that are the same + database.delete(Tables.TABLE_SAVED_QUERIES, + IndexDatabaseHelper.SavedQueriesColums.QUERY + " = ?", + new String[] { params[0] }); + + // Second, insert the saved query + lastInsertedRowId = + database.insertOrThrow(Tables.TABLE_SAVED_QUERIES, null, values); + + // Last, remove "old" saved queries + final long delta = lastInsertedRowId - MAX_SAVED_SEARCH_QUERY; + if (delta > 0) { + int count = database.delete(Tables.TABLE_SAVED_QUERIES, "rowId <= ?", + new String[] { Long.toString(delta) }); + Log.d(LOG_TAG, "Deleted '" + count + "' saved Search query(ies)"); + } + } catch (Exception e) { + Log.d(LOG_TAG, "Cannot update saved Search queries", e); + } + + return lastInsertedRowId; + } + } +} diff --git a/src/com/android/settings/search/IndexDatabaseHelper.java b/src/com/android/settings/search/IndexDatabaseHelper.java new file mode 100644 index 0000000..152cbf3 --- /dev/null +++ b/src/com/android/settings/search/IndexDatabaseHelper.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2014 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.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.os.Build; +import android.util.Log; + +public class IndexDatabaseHelper extends SQLiteOpenHelper { + + private static final String TAG = "IndexDatabaseHelper"; + + private static final String DATABASE_NAME = "search_index.db"; + private static final int DATABASE_VERSION = 115; + + public interface Tables { + public static final String TABLE_PREFS_INDEX = "prefs_index"; + public static final String TABLE_META_INDEX = "meta_index"; + public static final String TABLE_SAVED_QUERIES = "saved_queries"; + } + + public interface IndexColumns { + public static final String DOCID = "docid"; + public static final String LOCALE = "locale"; + public static final String DATA_RANK = "data_rank"; + public static final String DATA_TITLE = "data_title"; + public static final String DATA_TITLE_NORMALIZED = "data_title_normalized"; + public static final String DATA_SUMMARY_ON = "data_summary_on"; + public static final String DATA_SUMMARY_ON_NORMALIZED = "data_summary_on_normalized"; + public static final String DATA_SUMMARY_OFF = "data_summary_off"; + public static final String DATA_SUMMARY_OFF_NORMALIZED = "data_summary_off_normalized"; + public static final String DATA_ENTRIES = "data_entries"; + public static final String DATA_KEYWORDS = "data_keywords"; + public static final String CLASS_NAME = "class_name"; + public static final String SCREEN_TITLE = "screen_title"; + public static final String INTENT_ACTION = "intent_action"; + public static final String INTENT_TARGET_PACKAGE = "intent_target_package"; + public static final String INTENT_TARGET_CLASS = "intent_target_class"; + public static final String ICON = "icon"; + public static final String ENABLED = "enabled"; + public static final String DATA_KEY_REF = "data_key_reference"; + public static final String USER_ID = "user_id"; + } + + public interface MetaColumns { + public static final String BUILD = "build"; + } + + public interface SavedQueriesColums { + public static final String QUERY = "query"; + public static final String TIME_STAMP = "timestamp"; + } + + private static final String CREATE_INDEX_TABLE = + "CREATE VIRTUAL TABLE " + Tables.TABLE_PREFS_INDEX + " USING fts4" + + "(" + + IndexColumns.LOCALE + + ", " + + IndexColumns.DATA_RANK + + ", " + + IndexColumns.DATA_TITLE + + ", " + + IndexColumns.DATA_TITLE_NORMALIZED + + ", " + + IndexColumns.DATA_SUMMARY_ON + + ", " + + IndexColumns.DATA_SUMMARY_ON_NORMALIZED + + ", " + + IndexColumns.DATA_SUMMARY_OFF + + ", " + + IndexColumns.DATA_SUMMARY_OFF_NORMALIZED + + ", " + + IndexColumns.DATA_ENTRIES + + ", " + + IndexColumns.DATA_KEYWORDS + + ", " + + IndexColumns.SCREEN_TITLE + + ", " + + IndexColumns.CLASS_NAME + + ", " + + IndexColumns.ICON + + ", " + + IndexColumns.INTENT_ACTION + + ", " + + IndexColumns.INTENT_TARGET_PACKAGE + + ", " + + IndexColumns.INTENT_TARGET_CLASS + + ", " + + IndexColumns.ENABLED + + ", " + + IndexColumns.DATA_KEY_REF + + ", " + + IndexColumns.USER_ID + + ");"; + + private static final String CREATE_META_TABLE = + "CREATE TABLE " + Tables.TABLE_META_INDEX + + "(" + + MetaColumns.BUILD + " VARCHAR(32) NOT NULL" + + ")"; + + private static final String CREATE_SAVED_QUERIES_TABLE = + "CREATE TABLE " + Tables.TABLE_SAVED_QUERIES + + "(" + + SavedQueriesColums.QUERY + " VARCHAR(64) NOT NULL" + + ", " + + SavedQueriesColums.TIME_STAMP + " INTEGER" + + ")"; + + private static final String INSERT_BUILD_VERSION = + "INSERT INTO " + Tables.TABLE_META_INDEX + + " VALUES ('" + Build.VERSION.INCREMENTAL + "');"; + + private static final String SELECT_BUILD_VERSION = + "SELECT " + MetaColumns.BUILD + " FROM " + Tables.TABLE_META_INDEX + " LIMIT 1;"; + + private static IndexDatabaseHelper sSingleton; + + public static synchronized IndexDatabaseHelper getInstance(Context context) { + if (sSingleton == null) { + sSingleton = new IndexDatabaseHelper(context); + } + return sSingleton; + } + + public IndexDatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + bootstrapDB(db); + } + + private void bootstrapDB(SQLiteDatabase db) { + db.execSQL(CREATE_INDEX_TABLE); + db.execSQL(CREATE_META_TABLE); + db.execSQL(CREATE_SAVED_QUERIES_TABLE); + db.execSQL(INSERT_BUILD_VERSION); + Log.i(TAG, "Bootstrapped database"); + } + + @Override + public void onOpen(SQLiteDatabase db) { + super.onOpen(db); + + Log.i(TAG, "Using schema version: " + db.getVersion()); + + if (!Build.VERSION.INCREMENTAL.equals(getBuildVersion(db))) { + Log.w(TAG, "Index needs to be rebuilt as build-version is not the same"); + // We need to drop the tables and recreate them + reconstruct(db); + } else { + Log.i(TAG, "Index is fine"); + } + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion < DATABASE_VERSION) { + Log.w(TAG, "Detected schema version '" + oldVersion + "'. " + + "Index needs to be rebuilt for schema version '" + newVersion + "'."); + // We need to drop the tables and recreate them + reconstruct(db); + } + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.w(TAG, "Detected schema version '" + oldVersion + "'. " + + "Index needs to be rebuilt for schema version '" + newVersion + "'."); + // We need to drop the tables and recreate them + reconstruct(db); + } + + private void reconstruct(SQLiteDatabase db) { + dropTables(db); + bootstrapDB(db); + } + + private String getBuildVersion(SQLiteDatabase db) { + String version = null; + Cursor cursor = null; + try { + cursor = db.rawQuery(SELECT_BUILD_VERSION, null); + if (cursor.moveToFirst()) { + version = cursor.getString(0); + } + } + catch (Exception e) { + Log.e(TAG, "Cannot get build version from Index metadata"); + } + finally { + if (cursor != null) { + cursor.close(); + } + } + return version; + } + + private void dropTables(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_META_INDEX); + db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_PREFS_INDEX); + db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SAVED_QUERIES); + } +} diff --git a/src/com/android/settings/search/Indexable.java b/src/com/android/settings/search/Indexable.java new file mode 100644 index 0000000..19f88ae --- /dev/null +++ b/src/com/android/settings/search/Indexable.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2014 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.Context; +import android.provider.SearchIndexableResource; + +import java.util.List; + +/** + * Interface for classes whose instances can provide data for indexing. + * + * Classes implementing the Indexable interface must have a static field called + * <code>SEARCH_INDEX_DATA_PROVIDER</code>, which is an object implementing the + * {@link Indexable.SearchIndexProvider} interface. + * + * See {@link android.provider.SearchIndexableResource} and {@link SearchIndexableRaw}. + * + */ +public interface Indexable { + + public interface SearchIndexProvider { + /** + * Return a list of references for indexing. + * + * See {@link android.provider.SearchIndexableResource} + * + * + * @param context the context. + * @param enabled hint telling if the data needs to be considered into the search results + * or not. + * @return a list of {@link android.provider.SearchIndexableResource} references. + * Can be null. + */ + List<SearchIndexableResource> getXmlResourcesToIndex(Context context, boolean enabled); + + /** + * Return a list of raw data for indexing. See {@link SearchIndexableRaw} + * + * @param context the context. + * @param enabled hint telling if the data needs to be considered into the search results + * or not. + * @return a list of {@link SearchIndexableRaw} references. Can be null. + */ + List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled); + + /** + * Return a list of data keys that cannot be indexed. See {@link SearchIndexableRaw} + * + * @param context the context. + * @return a list of {@link SearchIndexableRaw} references. Can be null. + */ + List<String> getNonIndexableKeys(Context context); + } +} diff --git a/src/com/android/settings/search/Ranking.java b/src/com/android/settings/search/Ranking.java new file mode 100644 index 0000000..2c76002 --- /dev/null +++ b/src/com/android/settings/search/Ranking.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2014 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 com.android.settings.ChooseLockGeneric; +import com.android.settings.DataUsageSummary; +import com.android.settings.DateTimeSettings; +import com.android.settings.DevelopmentSettings; +import com.android.settings.DeviceInfoSettings; +import com.android.settings.DisplaySettings; +import com.android.settings.HomeSettings; +import com.android.settings.ScreenPinningSettings; +import com.android.settings.PrivacySettings; +import com.android.settings.SecuritySettings; +import com.android.settings.WallpaperTypeSettings; +import com.android.settings.WirelessSettings; +import com.android.settings.accessibility.AccessibilitySettings; +import com.android.settings.bluetooth.BluetoothSettings; +import com.android.settings.deviceinfo.Memory; +import com.android.settings.fuelgauge.BatterySaverSettings; +import com.android.settings.fuelgauge.PowerUsageSummary; +import com.android.settings.inputmethod.InputMethodAndLanguageSettings; +import com.android.settings.location.LocationSettings; +import com.android.settings.net.DataUsageMeteredSettings; +import com.android.settings.notification.NotificationSettings; +import com.android.settings.notification.OtherSoundSettings; +import com.android.settings.notification.ZenModeSettings; +import com.android.settings.print.PrintSettingsFragment; +import com.android.settings.sim.SimSettings; +import com.android.settings.users.UserSettings; +import com.android.settings.voice.VoiceInputSettings; +import com.android.settings.wifi.AdvancedWifiSettings; +import com.android.settings.wifi.SavedAccessPointsWifiSettings; +import com.android.settings.wifi.WifiSettings; + +import java.util.HashMap; + +/** + * Utility class for dealing with Search Ranking. + */ +public final class Ranking { + + public static final int RANK_WIFI = 1; + public static final int RANK_BT = 2; + public static final int RANK_SIM = 3; + public static final int RANK_DATA_USAGE = 4; + public static final int RANK_WIRELESS = 5; + public static final int RANK_HOME = 6; + public static final int RANK_DISPLAY = 7; + public static final int RANK_WALLPAPER = 8; + public static final int RANK_NOTIFICATIONS = 9; + public static final int RANK_MEMORY = 10; + public static final int RANK_POWER_USAGE = 11; + public static final int RANK_USERS = 12; + public static final int RANK_LOCATION = 13; + public static final int RANK_SECURITY = 14; + public static final int RANK_IME = 15; + public static final int RANK_PRIVACY = 16; + public static final int RANK_DATE_TIME = 17; + public static final int RANK_ACCESSIBILITY = 18; + public static final int RANK_PRINTING = 19; + public static final int RANK_DEVELOPEMENT = 20; + public static final int RANK_DEVICE_INFO = 21; + + public static final int RANK_UNDEFINED = -1; + public static final int RANK_OTHERS = 1024; + public static final int BASE_RANK_DEFAULT = 2048; + + public static int sCurrentBaseRank = BASE_RANK_DEFAULT; + + private static HashMap<String, Integer> sRankMap = new HashMap<String, Integer>(); + private static HashMap<String, Integer> sBaseRankMap = new HashMap<String, Integer>(); + + static { + // Wi-Fi + sRankMap.put(WifiSettings.class.getName(), RANK_WIFI); + sRankMap.put(AdvancedWifiSettings.class.getName(), RANK_WIFI); + sRankMap.put(SavedAccessPointsWifiSettings.class.getName(), RANK_WIFI); + + // BT + sRankMap.put(BluetoothSettings.class.getName(), RANK_BT); + + // SIM Cards + sRankMap.put(SimSettings.class.getName(), RANK_SIM); + + // DataUsage + sRankMap.put(DataUsageSummary.class.getName(), RANK_DATA_USAGE); + sRankMap.put(DataUsageMeteredSettings.class.getName(), RANK_DATA_USAGE); + + // Other wireless settinfs + sRankMap.put(WirelessSettings.class.getName(), RANK_WIRELESS); + + // Home + sRankMap.put(HomeSettings.class.getName(), RANK_HOME); + + // Display + sRankMap.put(DisplaySettings.class.getName(), RANK_DISPLAY); + + // Wallpapers + sRankMap.put(WallpaperTypeSettings.class.getName(), RANK_WALLPAPER); + + // Notifications + sRankMap.put(NotificationSettings.class.getName(), RANK_NOTIFICATIONS); + sRankMap.put(OtherSoundSettings.class.getName(), RANK_NOTIFICATIONS); + sRankMap.put(ZenModeSettings.class.getName(), RANK_NOTIFICATIONS); + + // Memory + sRankMap.put(Memory.class.getName(), RANK_MEMORY); + + // Battery + sRankMap.put(PowerUsageSummary.class.getName(), RANK_POWER_USAGE); + sRankMap.put(BatterySaverSettings.class.getName(), RANK_POWER_USAGE); + + // Users + sRankMap.put(UserSettings.class.getName(), RANK_USERS); + + // Location + sRankMap.put(LocationSettings.class.getName(), RANK_LOCATION); + + // Security + sRankMap.put(SecuritySettings.class.getName(), RANK_SECURITY); + sRankMap.put(ChooseLockGeneric.ChooseLockGenericFragment.class.getName(), RANK_SECURITY); + sRankMap.put(ScreenPinningSettings.class.getName(), RANK_SECURITY); + + // IMEs + sRankMap.put(InputMethodAndLanguageSettings.class.getName(), RANK_IME); + sRankMap.put(VoiceInputSettings.class.getName(), RANK_IME); + + // Privacy + sRankMap.put(PrivacySettings.class.getName(), RANK_PRIVACY); + + // Date / Time + sRankMap.put(DateTimeSettings.class.getName(), RANK_DATE_TIME); + + // Accessibility + sRankMap.put(AccessibilitySettings.class.getName(), RANK_ACCESSIBILITY); + + // Print + sRankMap.put(PrintSettingsFragment.class.getName(), RANK_PRINTING); + + // Development + sRankMap.put(DevelopmentSettings.class.getName(), RANK_DEVELOPEMENT); + + // Device infos + sRankMap.put(DeviceInfoSettings.class.getName(), RANK_DEVICE_INFO); + + sBaseRankMap.put("com.android.settings", 0); + } + + public static int getRankForClassName(String className) { + Integer rank = sRankMap.get(className); + return (rank != null) ? (int) rank: RANK_OTHERS; + } + + public static int getBaseRankForAuthority(String authority) { + synchronized (sBaseRankMap) { + Integer base = sBaseRankMap.get(authority); + if (base != null) { + return base; + } + sCurrentBaseRank++; + sBaseRankMap.put(authority, sCurrentBaseRank); + return sCurrentBaseRank; + } + } +} diff --git a/src/com/android/settings/search/SearchIndexableRaw.java b/src/com/android/settings/search/SearchIndexableRaw.java new file mode 100644 index 0000000..b8a1699 --- /dev/null +++ b/src/com/android/settings/search/SearchIndexableRaw.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2014 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.Context; +import android.provider.SearchIndexableData; + +/** + * Indexable raw data for Search. + * + * This is the raw data used by the Indexer and should match its data model. + * + * See {@link Indexable} and {@link android.provider.SearchIndexableResource}. + */ +public class SearchIndexableRaw extends SearchIndexableData { + + /** + * Title's raw data. + */ + public String title; + + /** + * Summary's raw data when the data is "ON". + */ + public String summaryOn; + + /** + * Summary's raw data when the data is "OFF". + */ + public String summaryOff; + + /** + * Entries associated with the raw data (when the data can have several values). + */ + public String entries; + + /** + * Keywords' raw data. + */ + public String keywords; + + /** + * Fragment's or Activity's title associated with the raw data. + */ + public String screenTitle; + + public SearchIndexableRaw(Context context) { + super(context); + } +} diff --git a/src/com/android/settings/search/SearchIndexableResources.java b/src/com/android/settings/search/SearchIndexableResources.java new file mode 100644 index 0000000..7b3fa77 --- /dev/null +++ b/src/com/android/settings/search/SearchIndexableResources.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2014 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.provider.SearchIndexableResource; + +import com.android.settings.DataUsageSummary; +import com.android.settings.DateTimeSettings; +import com.android.settings.DevelopmentSettings; +import com.android.settings.DeviceInfoSettings; +import com.android.settings.DisplaySettings; +import com.android.settings.HomeSettings; +import com.android.settings.ScreenPinningSettings; +import com.android.settings.PrivacySettings; +import com.android.settings.R; +import com.android.settings.SecuritySettings; +import com.android.settings.WallpaperTypeSettings; +import com.android.settings.WirelessSettings; +import com.android.settings.accessibility.AccessibilitySettings; +import com.android.settings.bluetooth.BluetoothSettings; +import com.android.settings.deviceinfo.Memory; +import com.android.settings.fuelgauge.BatterySaverSettings; +import com.android.settings.fuelgauge.PowerUsageSummary; +import com.android.settings.inputmethod.InputMethodAndLanguageSettings; +import com.android.settings.location.LocationSettings; +import com.android.settings.net.DataUsageMeteredSettings; +import com.android.settings.notification.NotificationSettings; +import com.android.settings.notification.OtherSoundSettings; +import com.android.settings.notification.ZenModeSettings; +import com.android.settings.print.PrintSettingsFragment; +import com.android.settings.sim.SimSettings; +import com.android.settings.users.UserSettings; +import com.android.settings.voice.VoiceInputSettings; +import com.android.settings.wifi.AdvancedWifiSettings; +import com.android.settings.wifi.SavedAccessPointsWifiSettings; +import com.android.settings.wifi.WifiSettings; + +import java.util.Collection; +import java.util.HashMap; + +public final class SearchIndexableResources { + + public static int NO_DATA_RES_ID = 0; + + private static HashMap<String, SearchIndexableResource> sResMap = + new HashMap<String, SearchIndexableResource>(); + + static { + sResMap.put(WifiSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(WifiSettings.class.getName()), + NO_DATA_RES_ID, + WifiSettings.class.getName(), + R.drawable.ic_settings_wireless)); + + sResMap.put(AdvancedWifiSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(AdvancedWifiSettings.class.getName()), + R.xml.wifi_advanced_settings, + AdvancedWifiSettings.class.getName(), + R.drawable.ic_settings_wireless)); + + sResMap.put(SavedAccessPointsWifiSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(SavedAccessPointsWifiSettings.class.getName()), + R.xml.wifi_display_saved_access_points, + SavedAccessPointsWifiSettings.class.getName(), + R.drawable.ic_settings_wireless)); + + sResMap.put(BluetoothSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(BluetoothSettings.class.getName()), + NO_DATA_RES_ID, + BluetoothSettings.class.getName(), + R.drawable.ic_settings_bluetooth2)); + + sResMap.put(SimSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(SimSettings.class.getName()), + NO_DATA_RES_ID, + SimSettings.class.getName(), + R.drawable.ic_sim_sd)); + + sResMap.put(DataUsageSummary.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(DataUsageSummary.class.getName()), + NO_DATA_RES_ID, + DataUsageSummary.class.getName(), + R.drawable.ic_settings_data_usage)); + + sResMap.put(DataUsageMeteredSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(DataUsageMeteredSettings.class.getName()), + NO_DATA_RES_ID, + DataUsageMeteredSettings.class.getName(), + R.drawable.ic_settings_data_usage)); + + sResMap.put(WirelessSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(WirelessSettings.class.getName()), + NO_DATA_RES_ID, + WirelessSettings.class.getName(), + R.drawable.ic_settings_more)); + + sResMap.put(HomeSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(HomeSettings.class.getName()), + NO_DATA_RES_ID, + HomeSettings.class.getName(), + R.drawable.ic_settings_home)); + + sResMap.put(DisplaySettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(DisplaySettings.class.getName()), + NO_DATA_RES_ID, + DisplaySettings.class.getName(), + R.drawable.ic_settings_display)); + + sResMap.put(WallpaperTypeSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(WallpaperTypeSettings.class.getName()), + NO_DATA_RES_ID, + WallpaperTypeSettings.class.getName(), + R.drawable.ic_settings_display)); + + sResMap.put(NotificationSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(NotificationSettings.class.getName()), + NO_DATA_RES_ID, + NotificationSettings.class.getName(), + R.drawable.ic_settings_notifications)); + + sResMap.put(OtherSoundSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(OtherSoundSettings.class.getName()), + NO_DATA_RES_ID, + OtherSoundSettings.class.getName(), + R.drawable.ic_settings_notifications)); + + sResMap.put(ZenModeSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(ZenModeSettings.class.getName()), + NO_DATA_RES_ID, + ZenModeSettings.class.getName(), + R.drawable.ic_settings_notifications)); + + sResMap.put(Memory.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(Memory.class.getName()), + NO_DATA_RES_ID, + Memory.class.getName(), + R.drawable.ic_settings_storage)); + + sResMap.put(PowerUsageSummary.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(PowerUsageSummary.class.getName()), + R.xml.power_usage_summary, + PowerUsageSummary.class.getName(), + R.drawable.ic_settings_battery)); + + sResMap.put(BatterySaverSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(BatterySaverSettings.class.getName()), + R.xml.battery_saver_settings, + BatterySaverSettings.class.getName(), + R.drawable.ic_settings_battery)); + + sResMap.put(UserSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(UserSettings.class.getName()), + R.xml.user_settings, + UserSettings.class.getName(), + R.drawable.ic_settings_multiuser)); + + sResMap.put(LocationSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(LocationSettings.class.getName()), + R.xml.location_settings, + LocationSettings.class.getName(), + R.drawable.ic_settings_location)); + + sResMap.put(SecuritySettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(SecuritySettings.class.getName()), + NO_DATA_RES_ID, + SecuritySettings.class.getName(), + R.drawable.ic_settings_security)); + + sResMap.put(ScreenPinningSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(ScreenPinningSettings.class.getName()), + NO_DATA_RES_ID, + ScreenPinningSettings.class.getName(), + R.drawable.ic_settings_security)); + + sResMap.put(InputMethodAndLanguageSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(InputMethodAndLanguageSettings.class.getName()), + NO_DATA_RES_ID, + InputMethodAndLanguageSettings.class.getName(), + R.drawable.ic_settings_language)); + + sResMap.put(VoiceInputSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(VoiceInputSettings.class.getName()), + NO_DATA_RES_ID, + VoiceInputSettings.class.getName(), + R.drawable.ic_settings_language)); + + sResMap.put(PrivacySettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(PrivacySettings.class.getName()), + NO_DATA_RES_ID, + PrivacySettings.class.getName(), + R.drawable.ic_settings_backup)); + + sResMap.put(DateTimeSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(DateTimeSettings.class.getName()), + R.xml.date_time_prefs, + DateTimeSettings.class.getName(), + R.drawable.ic_settings_date_time)); + + sResMap.put(AccessibilitySettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(AccessibilitySettings.class.getName()), + NO_DATA_RES_ID, + AccessibilitySettings.class.getName(), + R.drawable.ic_settings_accessibility)); + + sResMap.put(PrintSettingsFragment.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(PrintSettingsFragment.class.getName()), + NO_DATA_RES_ID, + PrintSettingsFragment.class.getName(), + R.drawable.ic_settings_print)); + + sResMap.put(DevelopmentSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(DevelopmentSettings.class.getName()), + NO_DATA_RES_ID, + DevelopmentSettings.class.getName(), + R.drawable.ic_settings_development)); + + sResMap.put(DeviceInfoSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(DeviceInfoSettings.class.getName()), + NO_DATA_RES_ID, + DeviceInfoSettings.class.getName(), + R.drawable.ic_settings_about)); + } + + private SearchIndexableResources() { + } + + public static int size() { + return sResMap.size(); + } + + public static SearchIndexableResource getResourceByName(String className) { + return sResMap.get(className); + } + + public static Collection<SearchIndexableResource> values() { + return sResMap.values(); + } +} diff --git a/src/com/android/settings/search/SettingsSearchIndexablesProvider.java b/src/com/android/settings/search/SettingsSearchIndexablesProvider.java new file mode 100644 index 0000000..c0afcaf --- /dev/null +++ b/src/com/android/settings/search/SettingsSearchIndexablesProvider.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2014 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.database.Cursor; +import android.database.MatrixCursor; +import android.provider.SearchIndexableResource; +import android.provider.SearchIndexablesProvider; + +import java.util.Collection; + +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RANK; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID; +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_PACKAGE; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS; + +import static android.provider.SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS; +import static android.provider.SearchIndexablesContract.INDEXABLES_RAW_COLUMNS; +import static android.provider.SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS; + +public class SettingsSearchIndexablesProvider extends SearchIndexablesProvider { + private static final String TAG = "SettingsSearchIndexablesProvider"; + + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor queryXmlResources(String[] projection) { + MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS); + Collection<SearchIndexableResource> values = SearchIndexableResources.values(); + for (SearchIndexableResource val : values) { + Object[] ref = new Object[7]; + ref[COLUMN_INDEX_XML_RES_RANK] = val.rank; + ref[COLUMN_INDEX_XML_RES_RESID] = val.xmlResId; + ref[COLUMN_INDEX_XML_RES_CLASS_NAME] = val.className; + ref[COLUMN_INDEX_XML_RES_ICON_RESID] = val.iconResId; + ref[COLUMN_INDEX_XML_RES_INTENT_ACTION] = null; // intent action + ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE] = null; // intent target package + ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS] = null; // intent target class + cursor.addRow(ref); + } + return cursor; + } + + @Override + public Cursor queryRawData(String[] projection) { + MatrixCursor result = new MatrixCursor(INDEXABLES_RAW_COLUMNS); + return result; + } + + @Override + public Cursor queryNonIndexableKeys(String[] projection) { + MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS); + return cursor; + } +} |