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

@@ -19,11 +19,39 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:orientation="vertical">
<TextView android:id="@+id/dash_text"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/dashboard_wip"
android:padding="16dp"
android:layout_weight="0"
android:background="#ffcccccc"
android:textSize="16sp"
/>
<EditText android:id="@+id/edittext_query"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/query_hint_text"
android:layout_weight="0"/>
<FrameLayout android:id="@+id/dashboard"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1">
<ListView android:id="@+id/list_results"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
</LinearLayout>
</FrameLayout>

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:gravity="center_vertical"
android:paddingStart="@*android:dimen/preference_item_padding_side"
android:paddingEnd="?android:attr/scrollbarSize">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingEnd="@*android:dimen/preference_item_padding_inner"
android:paddingTop="6dip"
android:paddingBottom="6dip">
<TextView android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceMedium"
android:ellipsize="marquee"
android:fadingEdge="horizontal" />
<TextView android:id="@+id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/title"
android:layout_alignStart="@id/title"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
android:maxLines="10" />
</RelativeLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:minWidth="@*android:dimen/preference_icon_minWidth"
android:orientation="horizontal">
<ImageView
android:id="@+id/icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:minWidth="48dp"
android:scaleType="centerInside"
android:layout_marginEnd="@*android:dimen/preference_item_padding_inner"
/>
</LinearLayout>
</LinearLayout>

View File

@@ -83,4 +83,9 @@
</declare-styleable>
<attr name="apnPreferenceStyle" format="reference" />
<!-- For Search -->
<declare-styleable name="Preference">
<attr name="keywords" format="string" />
</declare-styleable>
</resources>

View File

@@ -17,6 +17,7 @@
<resources>
<color name="background_drawer">@android:color/white</color>
<color name="background_drawer_icon">#ffcccccc</color>
<color name="background_search_result_icon">#ffcccccc</color>
<color name="black">#000</color>
<color name="red">#F00</color>

View File

@@ -4998,7 +4998,14 @@
<!--Dashboard strings-->
<!-- Text to describe the dashboard entry into the Drawer [CHAR LIMIT=16] -->
<string name="dashboard_title">Overview</string>
<string name="dashboard_wip" translatable="false">Overview - work in progress\n\nUse the Drawer on the left to see the settings list</string>
<string name="dashboard_wip" translatable="false">Overview and Search are work in progress and Confidential\n\nDrag the Drawer on the left to see the settings list</string>
<!-- Search strings -->
<!-- Text used as a search hint into the search box -->
<string name="query_hint_text">What are you looking for?</string>
<!--Search Keywords-->
<string name="keywords_wifi">wifi wi-fi network connection</string>
<!-- Notifications on lockscreen -->
<!-- Label for checkbox controlling the contents of notifications shown on

View File

@@ -15,6 +15,8 @@
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:title="@string/wifi_settings">
xmlns:settings="http://schemas.android.com/apk/res/com.android.settings"
android:title="@string/wifi_settings"
settings:keywords="@string/keywords_wifi">
</PreferenceScreen>

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;
}
}