Add Search to the Settings App (initial release)

- add basic UI for search
- build the search Index thru sqlite FTS4 (faster than FTS3)
- create the search Index on the fly depending on the locale
- re-index if there is a configuration change
- re-index too if the Android build version has changed (usefull
for an Android OTA or when a new Android version is pushed as
we need to recompute the Index)
- search thru "title" and "summary" Preference's data
- group results in the same order of the Settings categories
into the Drawer
- rewrite "title" and/or "summary" if they are containing
an hyphen "\u2011"
- add Preference Keywords (only for the Settings App) in the
Index and allow search on them (Wi-Fi network preference is
used as an example)

Known restrictions:

- we cannot yet search for "dynamic settings"
- ... nor we cannot search for settings coming from an external App
(like the Phone App and its related settings that are surfacing
into the Settings App).
- will need a few other CLs to add more keywords (and have them translated)

Change-Id: I017a4d6c433f28c257c08cacc1bed98c4c517039
This commit is contained in:
Fabrice Di Meglio
2014-02-03 18:12:25 -08:00
parent 882e6cde60
commit 6f0739a3d9
11 changed files with 1021 additions and 10 deletions

View File

@@ -94,6 +94,8 @@ import com.android.settings.dashboard.DashboardSummary;
import com.android.settings.deviceinfo.Memory;
import com.android.settings.deviceinfo.UsbSettings;
import com.android.settings.fuelgauge.PowerUsageSummary;
import com.android.settings.indexer.Index;
import com.android.settings.indexer.IndexableData;
import com.android.settings.inputmethod.InputMethodAndLanguageSettings;
import com.android.settings.inputmethod.KeyboardLayoutPickerFragment;
import com.android.settings.inputmethod.SpellCheckersSettings;
@@ -159,6 +161,8 @@ public class SettingsActivity extends Activity
*/
public static final String EXTRA_NO_HEADERS = ":settings:no_headers";
public static final String BACK_STACK_PREFS = ":settings:prefs";
// extras that allow any preference activity to be launched as part of a wizard
// show Back and Next buttons? takes boolean parameter
@@ -180,8 +184,6 @@ public class SettingsActivity extends Activity
*/
protected static final String EXTRA_SHOW_FRAGMENT_TITLE = ":settings:show_fragment_title";
private static final String BACK_STACK_PREFS = ":settings:prefs";
private static final String META_DATA_KEY_HEADER_ID =
"com.android.settings.TOP_LEVEL_HEADER_ID";
@@ -340,6 +342,71 @@ public class SettingsActivity extends Activity
}
};
/**
* Searchable data description.
*
* Known restriction: we are only searching (for now) the first level of Settings.
*/
private static IndexableData[] INDEXABLE_DATA = new IndexableData[] {
new IndexableData(1, R.xml.wifi_settings,
"com.android.settings.wifi.WifiSettings",
R.drawable.ic_settings_wireless),
new IndexableData(2, R.xml.bluetooth_settings,
"com.android.settings.bluetooth.BluetoothSettings",
R.drawable.ic_settings_bluetooth2),
new IndexableData(3, R.xml.data_usage_metered_prefs,
"com.android.settings.net.DataUsageMeteredSettings",
R.drawable.ic_settings_data_usage),
new IndexableData(4, R.xml.wireless_settings,
"com.android.settings.WirelessSettings",
R.drawable.empty_icon),
new IndexableData(5, R.xml.home_selection,
"com.android.settings.HomeSettings",
R.drawable.ic_settings_home),
new IndexableData(6, R.xml.sound_settings,
"com.android.settings.SoundSettings",
R.drawable.ic_settings_sound),
new IndexableData(7, R.xml.display_settings,
"com.android.settings.DisplaySettings",
R.drawable.ic_settings_display),
new IndexableData(8, R.xml.device_info_memory,
"com.android.settings.deviceinfo.Memory",
R.drawable.ic_settings_storage),
new IndexableData(9, R.xml.power_usage_summary,
"com.android.settings.fuelgauge.PowerUsageSummary",
R.drawable.ic_settings_battery),
new IndexableData(10, R.xml.user_settings,
"com.android.settings.users.UserSettings",
R.drawable.ic_settings_multiuser),
new IndexableData(11, R.xml.location_settings,
"com.android.settings.location.LocationSettings",
R.drawable.ic_settings_location),
new IndexableData(12, R.xml.security_settings,
"com.android.settings.SecuritySettings",
R.drawable.ic_settings_security),
new IndexableData(13, R.xml.language_settings,
"com.android.settings.inputmethod.InputMethodAndLanguageSettings",
R.drawable.ic_settings_language),
new IndexableData(14, R.xml.privacy_settings,
"com.android.settings.PrivacySettings",
R.drawable.ic_settings_backup),
new IndexableData(15, R.xml.date_time_prefs,
"com.android.settings.DateTimeSettings",
R.drawable.ic_settings_date_time),
new IndexableData(16, R.xml.accessibility_settings,
"com.android.settings.accessibility.AccessibilitySettings",
R.drawable.ic_settings_accessibility),
new IndexableData(17, R.xml.print_settings,
"com.android.settings.print.PrintSettingsFragment",
com.android.internal.R.drawable.ic_print),
new IndexableData(18, R.xml.development_prefs,
"com.android.settings.DevelopmentSettings",
R.drawable.ic_settings_development),
new IndexableData(19, R.xml.device_info_settings,
"com.android.settings.DeviceInfoSettings",
R.drawable.ic_settings_about),
};
@Override
public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) {
// Override the fragment title for Wallpaper settings
@@ -463,6 +530,7 @@ public class SettingsActivity extends Activity
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mDrawerToggle.onConfigurationChanged(newConfig);
Index.getInstance(this).update();
}
@Override
@@ -479,6 +547,9 @@ public class SettingsActivity extends Activity
getWindow().setUiOptions(getIntent().getIntExtra(EXTRA_UI_OPTIONS, 0));
}
Index.getInstance(this).addIndexableData(INDEXABLE_DATA);
Index.getInstance(this).update();
mAuthenticatorHelper = new AuthenticatorHelper();
mAuthenticatorHelper.updateAuthDescriptions(this);
mAuthenticatorHelper.onAccountsUpdated(this, null);

View File

@@ -17,17 +17,97 @@
package com.android.settings.dashboard;
import android.app.Fragment;
import android.content.Context;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.indexer.Index;
public class DashboardSummary extends Fragment {
private static final String SAVE_KEY_QUERY = ":settings:query";
private EditText mEditText;
private ListView mListView;
private SearchResultsAdapter mAdapter;
private Index mIndex;
private UpdateSearchResultsTask mUpdateSearchResultsTask;
/**
* A basic AsyncTask for updating the query results cursor
*/
private class UpdateSearchResultsTask extends AsyncTask<String, Void, Cursor> {
@Override
protected Cursor doInBackground(String... params) {
return mIndex.search(params[0]);
}
@Override
protected void onPostExecute(Cursor cursor) {
if (!isCancelled()) {
setCursor(cursor);
} else if (cursor != null) {
cursor.close();
}
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mIndex = Index.getInstance(getActivity());
mAdapter = new SearchResultsAdapter(getActivity());
}
@Override
public void onStop() {
super.onStop();
clearResults();
}
@Override
public void onStart() {
super.onStart();
updateSearchResults();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (outState != null) {
outState.putString(SAVE_KEY_QUERY, mEditText.getText().toString());
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
final String query = savedInstanceState.getString(SAVE_KEY_QUERY);
if (query != null && !TextUtils.isEmpty(query)) {
mEditText.setText(query);
}
}
}
@Override
@@ -36,6 +116,185 @@ public class DashboardSummary extends Fragment {
final View view = inflater.inflate(R.layout.dashboard, container, false);
mEditText = (EditText)view.findViewById(R.id.edittext_query);
mEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
updateSearchResults();
}
@Override
public void afterTextChanged(Editable s) {
}
});
mEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) {
closeSoftKeyboard();
}
}
});
mListView = (ListView) view.findViewById(R.id.list_results);
mListView.setAdapter(mAdapter);
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
closeSoftKeyboard();
final Cursor cursor = mAdapter.mCursor;
cursor.moveToPosition(position);
final String fragmentName = cursor.getString(Index.COLUMN_INDEX_FRAGMENT_NAME);
final String fragmentTitle = cursor.getString(Index.COLUMN_INDEX_FRAGMENT_TITLE);
((SettingsActivity) getActivity()).startPreferencePanel(fragmentName, null, 0,
fragmentTitle, null, 0);
}
});
return view;
}
private void closeSoftKeyboard() {
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null && imm.isActive(mEditText)) {
imm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
}
}
private void clearResults() {
if (mUpdateSearchResultsTask != null) {
mUpdateSearchResultsTask.cancel(false);
mUpdateSearchResultsTask = null;
}
setCursor(null);
}
private void setCursor(Cursor cursor) {
Cursor oldCursor = mAdapter.swapCursor(cursor);
if (oldCursor != null) {
oldCursor.close();
}
}
private void updateSearchResults() {
if (mUpdateSearchResultsTask != null) {
mUpdateSearchResultsTask.cancel(false);
mUpdateSearchResultsTask = null;
}
final String query = mEditText.getText().toString();
if (TextUtils.isEmpty(query)) {
setCursor(null);
} else {
mUpdateSearchResultsTask = new UpdateSearchResultsTask();
mUpdateSearchResultsTask.execute(query);
}
}
private static class SearchResult {
public String title;
public String summary;
public int iconResId;
public SearchResult(String title, String summary, int iconResId) {
this.title = title;
this.summary = summary;
this.iconResId = iconResId;
}
}
private static class SearchResultsAdapter extends BaseAdapter {
private Cursor mCursor;
private LayoutInflater mInflater;
private boolean mDataValid;
public SearchResultsAdapter(Context context) {
mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mDataValid = false;
}
public Cursor swapCursor(Cursor newCursor) {
if (newCursor == mCursor) {
return null;
}
Cursor oldCursor = mCursor;
mCursor = newCursor;
if (newCursor != null) {
mDataValid = true;
notifyDataSetChanged();
} else {
mDataValid = false;
notifyDataSetInvalidated();
}
return oldCursor;
}
@Override
public int getCount() {
if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0;
return mCursor.getCount();
}
@Override
public Object getItem(int position) {
if (mDataValid && mCursor.moveToPosition(position)) {
final String title = mCursor.getString(Index.COLUMN_INDEX_TITLE);
final String summary = mCursor.getString(Index.COLUMN_INDEX_SUMMARY);
final String iconResStr = mCursor.getString(Index.COLUMN_INDEX_ICON);
final int iconResId =
TextUtils.isEmpty(iconResStr) ? 0 : Integer.parseInt(iconResStr);
return new SearchResult(title, summary, iconResId);
}
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (!mDataValid && convertView == null) {
throw new IllegalStateException(
"this should only be called when the cursor is valid");
}
if (!mCursor.moveToPosition(position)) {
throw new IllegalStateException("couldn't move cursor to position " + position);
}
View view;
TextView textTitle;
TextView textSummary;
ImageView imageView;
if (convertView == null) {
view = mInflater.inflate(R.layout.search_result, parent, false);
} else {
view = convertView;
}
textTitle = (TextView) view.findViewById(R.id.title);
textSummary = (TextView) view.findViewById(R.id.summary);
imageView = (ImageView) view.findViewById(R.id.icon);
SearchResult result = (SearchResult) getItem(position);
textTitle.setText(result.title);
textSummary.setText(result.summary);
if (result.iconResId != R.drawable.empty_icon) {
imageView.setImageResource(result.iconResId);
imageView.setBackgroundResource(R.color.background_search_result_icon);
} else {
imageView.setImageDrawable(null);
imageView.setBackgroundResource(R.drawable.empty_icon);
}
return view;
}
}
}

View File

@@ -0,0 +1,379 @@
/*
* 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.indexer;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
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.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.android.settings.indexer.IndexDatabaseHelper.Tables;
import static com.android.settings.indexer.IndexDatabaseHelper.IndexColumns;
public class Index {
private static final String LOG_TAG = "Indexer";
// Those indices should match the indices of SELECT_COLUMNS !
public static final int COLUMN_INDEX_TITLE = 1;
public static final int COLUMN_INDEX_SUMMARY = 2;
public static final int COLUMN_INDEX_FRAGMENT_NAME = 4;
public static final int COLUMN_INDEX_FRAGMENT_TITLE = 5;
public static final int COLUMN_INDEX_ICON = 7;
// 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,
IndexColumns.DATA_TITLE,
IndexColumns.DATA_SUMMARY,
IndexColumns.DATA_KEYWORDS,
IndexColumns.FRAGMENT_NAME,
IndexColumns.FRAGMENT_TITLE,
IndexColumns.INTENT,
IndexColumns.ICON
};
private static final String EMPTY = "";
private static final String NON_BREAKING_HYPHEN = "\u2011";
private static final String HYPHEN = "-";
private static Index sInstance;
private final AtomicBoolean mIsAvailable = new AtomicBoolean(false);
private final List<IndexableData> mDataToIndex = new ArrayList<IndexableData>();
private final Context mContext;
/**
* A basic singleton
*/
public static Index getInstance(Context context) {
if (sInstance == null) {
sInstance = new Index(context);
}
return sInstance;
}
public Index(Context context) {
mContext = context;
}
public boolean isAvailable() {
return mIsAvailable.get();
}
public Cursor search(String query) {
return getReadableDatabase().rawQuery(buildSQL(query), null);
}
private String buildSQL(String query) {
StringBuilder sb = new StringBuilder();
sb.append(buildSQLForColumn(query, IndexColumns.DATA_TITLE));
sb.append(" UNION ");
sb.append(buildSQLForColumn(query, IndexColumns.DATA_SUMMARY));
sb.append(" UNION ");
sb.append(buildSQLForColumn(query, IndexColumns.DATA_KEYWORDS));
sb.append(" ORDER BY ");
sb.append(IndexColumns.DATA_RANK);
return sb.toString();
}
private String buildSQLForColumn(String query, String columnName) {
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(buildWhereStringForColumn(query, columnName));
return sb.toString();
}
private String buildWhereStringForColumn(String query, String columnName) {
final StringBuilder sb = new StringBuilder(columnName);
sb.append(" MATCH ");
DatabaseUtils.appendEscapedSQLString(sb, query + "*");
sb.append(" AND ");
sb.append(IndexColumns.LOCALE);
sb.append(" = ");
DatabaseUtils.appendEscapedSQLString(sb, Locale.getDefault().toString());
return sb.toString();
}
public void addIndexableData(IndexableData data) {
mDataToIndex.add(data);
}
public void addIndexableData(IndexableData[] array) {
final int count = array.length;
for (int n = 0; n < count; n++) {
addIndexableData(array[n]);
}
}
public boolean update() {
final IndexTask task = new IndexTask();
task.execute();
try {
return task.get();
} catch (InterruptedException e) {
Log.e(LOG_TAG, "Cannot update index: " + e.getMessage());
return false;
} catch (ExecutionException e) {
Log.e(LOG_TAG, "Cannot update index: " + e.getMessage());
return false;
}
}
private SQLiteDatabase getReadableDatabase() {
return IndexDatabaseHelper.getInstance(mContext).getReadableDatabase();
}
private SQLiteDatabase getWritableDatabase() {
return IndexDatabaseHelper.getInstance(mContext).getWritableDatabase();
}
/**
* A private class for updating the Index database
*/
private class IndexTask extends AsyncTask<Void, Integer, Boolean> {
@Override
protected void onPreExecute() {
super.onPreExecute();
mIsAvailable.set(false);
}
@Override
protected void onPostExecute(Boolean aBoolean) {
super.onPostExecute(aBoolean);
mIsAvailable.set(true);
}
@Override
protected Boolean doInBackground(Void... params) {
final SQLiteDatabase database = getWritableDatabase();
boolean result = false;
final Locale locale = Locale.getDefault();
final String localeStr = locale.toString();
if (isLocaleAlreadyIndexed(database, locale)) {
Log.d(LOG_TAG, "Locale '" + localeStr + "' is already indexed");
return true;
}
final long current = System.currentTimeMillis();
try {
database.beginTransaction();
final int count = mDataToIndex.size();
for (int n = 0; n < count; n++) {
final IndexableData data = mDataToIndex.get(n);
indexFromResource(database, locale, data.xmlResId, data.fragmentName,
data.iconResId, data.rank);
}
database.setTransactionSuccessful();
result = true;
} finally {
database.endTransaction();
}
final long now = System.currentTimeMillis();
Log.d(LOG_TAG, "Indexing locale '" + localeStr + "' took " +
(now - current) + " millis");
return result;
}
private boolean isLocaleAlreadyIndexed(SQLiteDatabase database, Locale locale) {
Cursor cursor = null;
boolean result = false;
final StringBuilder sb = new StringBuilder(IndexColumns.LOCALE);
sb.append(" = ");
DatabaseUtils.appendEscapedSQLString(sb, locale.toString());
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;
}
private void indexFromResource(SQLiteDatabase database, Locale locale, int xmlResId,
String fragmentName, int iconResId, int rank) {
XmlResourceParser parser = null;
final String localeStr = locale.toString();
try {
parser = mContext.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 (!"PreferenceScreen".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 fragmentTitle = getData(attrs,
com.android.internal.R.styleable.Preference, com.android.internal.R.styleable.Preference_title);
String title = getDataTitle(attrs);
String summary = getDataSummary(attrs);
String keywords = getDataKeywords(attrs);
// Insert rows for the main PreferenceScreen node. Rewrite the data for removing
// hyphens.
inserOneRowWithFilteredData(database, localeStr, title, summary, fragmentName,
fragmentTitle, iconResId, rank, keywords, "\u2011", "");
inserOneRowWithFilteredData(database, localeStr, title, summary, fragmentName,
fragmentTitle, iconResId, rank, keywords, "\u2011", "-");
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
title = getDataTitle(attrs);
summary = getDataSummary(attrs);
keywords = getDataKeywords(attrs);
// Insert rows for the child nodes of PreferenceScreen
inserOneRowWithFilteredData(database, localeStr, title, summary, fragmentName,
fragmentTitle, iconResId, rank, keywords, NON_BREAKING_HYPHEN, EMPTY);
inserOneRowWithFilteredData(database, localeStr, title, summary, fragmentName,
fragmentTitle, iconResId, rank, keywords, NON_BREAKING_HYPHEN, HYPHEN);
}
} 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 inserOneRowWithFilteredData(SQLiteDatabase database, String locale,
String title, String summary, String fragmentName, String fragmentTitle,
int iconResId, int rank, String keywords, String seq, String replacement) {
String updatedTitle;
String updateSummary;
if (title != null && title.contains(seq)) {
updatedTitle = title.replaceAll(seq, replacement);
} else {
updatedTitle = title;
}
if (summary != null && summary.contains(seq)) {
updateSummary = summary.replaceAll(seq, replacement);
} else {
updateSummary = summary;
}
insertOneRow(database, locale,
updatedTitle, updateSummary,
fragmentName, fragmentTitle, iconResId, rank, keywords);
}
private void insertOneRow(SQLiteDatabase database, String locale, String title,
String summary, String fragmentName, String fragmentTitle,
int iconResId, int rank, String keywords) {
if (TextUtils.isEmpty(title)) {
return;
}
ContentValues values = new ContentValues();
values.put(IndexColumns.LOCALE, locale);
values.put(IndexColumns.DATA_RANK, rank);
values.put(IndexColumns.DATA_TITLE, title);
values.put(IndexColumns.DATA_SUMMARY, summary);
values.put(IndexColumns.DATA_KEYWORDS, keywords);
values.put(IndexColumns.FRAGMENT_NAME, fragmentName);
values.put(IndexColumns.FRAGMENT_TITLE, fragmentTitle);
values.put(IndexColumns.INTENT, "");
values.put(IndexColumns.ICON, iconResId);
database.insertOrThrow(Tables.TABLE_PREFS_INDEX, null, values);
}
private String getDataTitle(AttributeSet attrs) {
return getData(attrs,
com.android.internal.R.styleable.Preference,
com.android.internal.R.styleable.Preference_title);
}
private String getDataSummary(AttributeSet attrs) {
return getData(attrs,
com.android.internal.R.styleable.Preference,
com.android.internal.R.styleable.Preference_summary);
}
private String getDataKeywords(AttributeSet attrs) {
return getData(attrs,
R.styleable.Preference,
R.styleable.Preference_keywords);
}
private String getData(AttributeSet set, int[] attrs, int resId) {
final TypedArray sa = mContext.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 = mContext.getText(tv.resourceId);
} else {
data = tv.string;
}
}
return (data != null) ? data.toString() : null;
}
}
}

View File

@@ -0,0 +1,156 @@
/*
* 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.indexer;
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 = 100;
public interface Tables {
public static final String TABLE_PREFS_INDEX = "prefs_index";
public static final String TABLE_META_INDEX = "meta_index";
}
public interface IndexColumns {
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_SUMMARY = "data_summary";
public static final String DATA_KEYWORDS = "data_keywords";
public static final String FRAGMENT_NAME = "fragment_name";
public static final String FRAGMENT_TITLE = "fragment_title";
public static final String INTENT = "intent";
public static final String ICON = "icon";
}
public interface MetaColumns {
public static final String BUILD = "build";
}
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_SUMMARY +
", " +
IndexColumns.DATA_KEYWORDS +
", " +
IndexColumns.FRAGMENT_NAME +
", " +
IndexColumns.FRAGMENT_TITLE +
", " +
IndexColumns.INTENT +
", " +
IndexColumns.ICON +
");";
private static final String CREATE_META_TABLE =
"CREATE TABLE " + Tables.TABLE_META_INDEX +
"(" +
MetaColumns.BUILD + " VARCHAR(32) NOT NULL" +
")";
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(INSERT_BUILD_VERSION);
Log.i(TAG, "Bootstrapped database");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
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);
}
@Override
public void onOpen(SQLiteDatabase db) {
super.onOpen(db);
if (!Build.VERSION.INCREMENTAL.equals(getBuildVersion(db))) {
Log.w(TAG, "Index needs to be rebuilt");
// We need to drop the tables and recreate them
dropTables(db);
bootstrapDB(db);
} else {
Log.i(TAG, "Index is fine");
}
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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.indexer;
public class IndexableData {
public int rank;
public int xmlResId;
public String fragmentName;
public int iconResId;
public IndexableData(int rank, int dataResId, String name, int iconResId) {
this.rank = rank;
this.xmlResId = dataResId;
this.fragmentName = name;
this.iconResId = iconResId;
}
}