From: Fan Zhang Date: Thu, 13 Jul 2017 21:00:44 +0000 (-0700) Subject: Load only unique dictionary words X-Git-Tag: android-x86-9.0-r1~447^2 X-Git-Url: http://git.osdn.net/view?a=commitdiff_plain;h=6539c9a1d0b2437660f47ead816d9209c05473c4;p=android-x86%2Fpackages-apps-Settings.git Load only unique dictionary words - Move UserDictionarySettings to sub package - Convert cursor to loader Fix: 22058788 Test: robotests Change-Id: I1e8828abee58362b815abc210c044b678bf9d578 --- diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java index 273c313f92..1c2a1cbd8a 100644 --- a/src/com/android/settings/core/gateway/SettingsGateway.java +++ b/src/com/android/settings/core/gateway/SettingsGateway.java @@ -31,7 +31,6 @@ import com.android.settings.Settings; import com.android.settings.TestingSettings; import com.android.settings.TetherSettings; import com.android.settings.TrustedCredentialsSettings; -import com.android.settings.UserDictionarySettings; import com.android.settings.WifiCallingSettings; import com.android.settings.accessibility.AccessibilitySettings; import com.android.settings.accessibility.AccessibilitySettingsForSetupWizard; @@ -91,6 +90,7 @@ import com.android.settings.inputmethod.KeyboardLayoutPickerFragment; import com.android.settings.inputmethod.PhysicalKeyboardFragment; import com.android.settings.inputmethod.SpellCheckersSettings; import com.android.settings.inputmethod.UserDictionaryList; +import com.android.settings.inputmethod.UserDictionarySettings; import com.android.settings.language.LanguageAndInputSettings; import com.android.settings.localepicker.LocaleListEditor; import com.android.settings.location.LocationSettings; diff --git a/src/com/android/settings/inputmethod/UserDictionaryAddWordContents.java b/src/com/android/settings/inputmethod/UserDictionaryAddWordContents.java index 98c4db2ea4..620bc65281 100644 --- a/src/com/android/settings/inputmethod/UserDictionaryAddWordContents.java +++ b/src/com/android/settings/inputmethod/UserDictionaryAddWordContents.java @@ -27,7 +27,6 @@ import android.view.View; import android.widget.EditText; import com.android.settings.R; -import com.android.settings.UserDictionarySettings; import com.android.settings.Utils; import java.util.ArrayList; diff --git a/src/com/android/settings/inputmethod/UserDictionaryCursorLoader.java b/src/com/android/settings/inputmethod/UserDictionaryCursorLoader.java new file mode 100644 index 0000000000..d5b742d4ee --- /dev/null +++ b/src/com/android/settings/inputmethod/UserDictionaryCursorLoader.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.inputmethod; + +import android.content.Context; +import android.content.CursorLoader; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.provider.UserDictionary; +import android.support.annotation.VisibleForTesting; +import android.util.ArraySet; + +import java.util.Locale; +import java.util.Objects; +import java.util.Set; + +public class UserDictionaryCursorLoader extends CursorLoader { + + @VisibleForTesting + static final String[] QUERY_PROJECTION = { + UserDictionary.Words._ID, + UserDictionary.Words.WORD, + UserDictionary.Words.SHORTCUT + }; + + // The index of the shortcut in the above array. + static final int INDEX_SHORTCUT = 2; + + // Either the locale is empty (means the word is applicable to all locales) + // or the word equals our current locale + private static final String QUERY_SELECTION = + UserDictionary.Words.LOCALE + "=?"; + private static final String QUERY_SELECTION_ALL_LOCALES = + UserDictionary.Words.LOCALE + " is null"; + + + // Locale can be any of: + // - The string representation of a locale, as returned by Locale#toString() + // - The empty string. This means we want a cursor returning words valid for all locales. + // - null. This means we want a cursor for the current locale, whatever this is. + // Note that this contrasts with the data inside the database, where NULL means "all + // locales" and there should never be an empty string. The confusion is called by the + // historical use of null for "all locales". + // TODO: it should be easy to make this more readable by making the special values + // human-readable, like "all_locales" and "current_locales" strings, provided they + // can be guaranteed not to match locales that may exist. + private final String mLocale; + + public UserDictionaryCursorLoader(Context context, String locale) { + super(context); + mLocale = locale; + } + + @Override + public Cursor loadInBackground() { + final MatrixCursor result = new MatrixCursor(QUERY_PROJECTION); + final Cursor candidate; + if ("".equals(mLocale)) { + // Case-insensitive sort + candidate = getContext().getContentResolver().query( + UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION, + QUERY_SELECTION_ALL_LOCALES, null, + "UPPER(" + UserDictionary.Words.WORD + ")"); + } else { + final String queryLocale = null != mLocale ? mLocale : Locale.getDefault().toString(); + candidate = getContext().getContentResolver().query(UserDictionary.Words.CONTENT_URI, + QUERY_PROJECTION, QUERY_SELECTION, + new String[]{queryLocale}, "UPPER(" + UserDictionary.Words.WORD + ")"); + } + final Set hashSet = new ArraySet<>(); + for (candidate.moveToFirst(); !candidate.isAfterLast(); candidate.moveToNext()) { + final int id = candidate.getInt(0); + final String word = candidate.getString(1); + final String shortcut = candidate.getString(2); + final int hash = Objects.hash(word, shortcut); + if (hashSet.contains(hash)) { + continue; + } + hashSet.add(hash); + result.addRow(new Object[]{id, word, shortcut}); + } + return result; + } +} diff --git a/src/com/android/settings/inputmethod/UserDictionaryList.java b/src/com/android/settings/inputmethod/UserDictionaryList.java index b8e87efbcb..cf4eccdf9f 100644 --- a/src/com/android/settings/inputmethod/UserDictionaryList.java +++ b/src/com/android/settings/inputmethod/UserDictionaryList.java @@ -180,7 +180,7 @@ public class UserDictionaryList extends SettingsPreferenceFragment { newPref.getExtras().putString("locale", locale); } newPref.setIntent(intent); - newPref.setFragment(com.android.settings.UserDictionarySettings.class.getName()); + newPref.setFragment(UserDictionarySettings.class.getName()); return newPref; } diff --git a/src/com/android/settings/UserDictionarySettings.java b/src/com/android/settings/inputmethod/UserDictionarySettings.java similarity index 68% rename from src/com/android/settings/UserDictionarySettings.java rename to src/com/android/settings/inputmethod/UserDictionarySettings.java index 5571c2d718..9680af10c6 100644 --- a/src/com/android/settings/UserDictionarySettings.java +++ b/src/com/android/settings/inputmethod/UserDictionarySettings.java @@ -1,25 +1,29 @@ -/** - * Copyright (C) 2009 Google Inc. +/* + * Copyright (C) 2009 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 + * 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 + * 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. + * 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; +package com.android.settings.inputmethod; +import android.annotation.Nullable; +import android.app.ActionBar; import android.app.ListFragment; +import android.app.LoaderManager; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.Loader; import android.database.Cursor; import android.os.Bundle; import android.provider.UserDictionary; @@ -38,28 +42,13 @@ import android.widget.SimpleCursorAdapter; import android.widget.TextView; import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.core.instrumentation.VisibilityLoggerMixin; -import com.android.settings.inputmethod.UserDictionaryAddWordContents; -import com.android.settings.inputmethod.UserDictionarySettingsUtils; +import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.core.instrumentation.Instrumentable; +import com.android.settings.core.instrumentation.VisibilityLoggerMixin; -import java.util.Locale; - -public class UserDictionarySettings extends ListFragment implements Instrumentable { - - private static final String[] QUERY_PROJECTION = { - UserDictionary.Words._ID, UserDictionary.Words.WORD, UserDictionary.Words.SHORTCUT - }; - - // The index of the shortcut in the above array. - private static final int INDEX_SHORTCUT = 2; - - // Either the locale is empty (means the word is applicable to all locales) - // or the word equals our current locale - private static final String QUERY_SELECTION = - UserDictionary.Words.LOCALE + "=?"; - private static final String QUERY_SELECTION_ALL_LOCALES = - UserDictionary.Words.LOCALE + " is null"; +public class UserDictionarySettings extends ListFragment implements Instrumentable, + LoaderManager.LoaderCallbacks { private static final String DELETE_SELECTION_WITH_SHORTCUT = UserDictionary.Words.WORD + "=? AND " + UserDictionary.Words.SHORTCUT + "=?"; @@ -68,12 +57,13 @@ public class UserDictionarySettings extends ListFragment implements Instrumentab + UserDictionary.Words.SHORTCUT + "=''"; private static final int OPTIONS_MENU_ADD = Menu.FIRST; + private static final int LOADER_ID = 1; private final VisibilityLoggerMixin mVisibilityLoggerMixin = new VisibilityLoggerMixin(getMetricsCategory()); private Cursor mCursor; - protected String mLocale; + private String mLocale; @Override public int getMetricsCategory() { @@ -87,16 +77,8 @@ public class UserDictionarySettings extends ListFragment implements Instrumentab } @Override - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate( - com.android.internal.R.layout.preference_list_fragment, container, false); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - getActivity().getActionBar().setTitle(R.string.user_dict_settings_title); + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); final Intent intent = getActivity().getIntent(); final String localeFromIntent = @@ -116,56 +98,49 @@ public class UserDictionarySettings extends ListFragment implements Instrumentab } mLocale = locale; - mCursor = createCursor(locale); - TextView emptyView = (TextView) getView().findViewById(android.R.id.empty); + + setHasOptionsMenu(true); + getLoaderManager().initLoader(LOADER_ID, null, this /* callback */); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // Show the language as a subtitle of the action bar + final ActionBar actionBar = getActivity().getActionBar(); + if (actionBar != null) { + actionBar.setTitle(R.string.user_dict_settings_title); + actionBar.setSubtitle( + UserDictionarySettingsUtils.getLocaleDisplayName(getActivity(), mLocale)); + } + + return inflater.inflate( + com.android.internal.R.layout.preference_list_fragment, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + TextView emptyView = getView().findViewById(android.R.id.empty); emptyView.setText(R.string.user_dict_settings_empty_text); final ListView listView = getListView(); - listView.setAdapter(createAdapter()); listView.setFastScrollEnabled(true); listView.setEmptyView(emptyView); - - setHasOptionsMenu(true); - // Show the language as a subtitle of the action bar - getActivity().getActionBar().setSubtitle( - UserDictionarySettingsUtils.getLocaleDisplayName(getActivity(), mLocale)); } @Override public void onResume() { super.onResume(); mVisibilityLoggerMixin.onResume(); - } - - private Cursor createCursor(final String locale) { - // Locale can be any of: - // - The string representation of a locale, as returned by Locale#toString() - // - The empty string. This means we want a cursor returning words valid for all locales. - // - null. This means we want a cursor for the current locale, whatever this is. - // Note that this contrasts with the data inside the database, where NULL means "all - // locales" and there should never be an empty string. The confusion is called by the - // historical use of null for "all locales". - // TODO: it should be easy to make this more readable by making the special values - // human-readable, like "all_locales" and "current_locales" strings, provided they - // can be guaranteed not to match locales that may exist. - if ("".equals(locale)) { - // Case-insensitive sort - return getActivity().managedQuery(UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION, - QUERY_SELECTION_ALL_LOCALES, null, - "UPPER(" + UserDictionary.Words.WORD + ")"); - } else { - final String queryLocale = null != locale ? locale : Locale.getDefault().toString(); - return getActivity().managedQuery(UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION, - QUERY_SELECTION, new String[] { queryLocale }, - "UPPER(" + UserDictionary.Words.WORD + ")"); - } + getLoaderManager().restartLoader(LOADER_ID, null, this /* callback */); } private ListAdapter createAdapter() { return new MyAdapter(getActivity(), R.layout.user_dictionary_item, mCursor, - new String[] { UserDictionary.Words.WORD, UserDictionary.Words.SHORTCUT }, - new int[] { android.R.id.text1, android.R.id.text2 }, this); + new String[]{UserDictionary.Words.WORD, UserDictionary.Words.SHORTCUT}, + new int[]{android.R.id.text1, android.R.id.text2}); } @Override @@ -181,7 +156,7 @@ public class UserDictionarySettings extends ListFragment implements Instrumentab public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { MenuItem actionItem = menu.add(0, OPTIONS_MENU_ADD, 0, R.string.user_dict_settings_add_menu_title) - .setIcon(R.drawable.ic_menu_add_white); + .setIcon(R.drawable.ic_menu_add_white); actionItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); } @@ -203,7 +178,8 @@ public class UserDictionarySettings extends ListFragment implements Instrumentab /** * Add or edit a word. If editingWord is null, it's an add; otherwise, it's an edit. - * @param editingWord the word to edit, or null if it's an add. + * + * @param editingWord the word to edit, or null if it's an add. * @param editingShortcut the shortcut for this entry, or null if none. */ private void showAddOrEditDialog(final String editingWord, final String editingShortcut) { @@ -245,14 +221,30 @@ public class UserDictionarySettings extends ListFragment implements Instrumentab if (TextUtils.isEmpty(shortcut)) { resolver.delete( UserDictionary.Words.CONTENT_URI, DELETE_SELECTION_WITHOUT_SHORTCUT, - new String[] { word }); + new String[]{word}); } else { resolver.delete( UserDictionary.Words.CONTENT_URI, DELETE_SELECTION_WITH_SHORTCUT, - new String[] { word, shortcut }); + new String[]{word, shortcut}); } } + @Override + public Loader onCreateLoader(int id, Bundle args) { + return new UserDictionaryCursorLoader(getContext(), mLocale); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + mCursor = data; + getListView().setAdapter(createAdapter()); + } + + @Override + public void onLoaderReset(Loader loader) { + + } + private static class MyAdapter extends SimpleCursorAdapter implements SectionIndexer { private AlphabetIndexer mIndexer; @@ -261,12 +253,12 @@ public class UserDictionarySettings extends ListFragment implements Instrumentab @Override public boolean setViewValue(View v, Cursor c, int columnIndex) { - if (columnIndex == INDEX_SHORTCUT) { - final String shortcut = c.getString(INDEX_SHORTCUT); + if (columnIndex == UserDictionaryCursorLoader.INDEX_SHORTCUT) { + final String shortcut = c.getString(UserDictionaryCursorLoader.INDEX_SHORTCUT); if (TextUtils.isEmpty(shortcut)) { v.setVisibility(View.GONE); } else { - ((TextView)v).setText(shortcut); + ((TextView) v).setText(shortcut); v.setVisibility(View.VISIBLE); } v.invalidate(); @@ -277,8 +269,7 @@ public class UserDictionarySettings extends ListFragment implements Instrumentab } }; - public MyAdapter(Context context, int layout, Cursor c, String[] from, int[] to, - UserDictionarySettings settings) { + public MyAdapter(Context context, int layout, Cursor c, String[] from, int[] to) { super(context, layout, c, from, to); if (null != c) { diff --git a/src/com/android/settings/language/UserDictionaryPreferenceController.java b/src/com/android/settings/language/UserDictionaryPreferenceController.java index 7d9a6ef4e3..137f44abd0 100644 --- a/src/com/android/settings/language/UserDictionaryPreferenceController.java +++ b/src/com/android/settings/language/UserDictionaryPreferenceController.java @@ -21,9 +21,9 @@ import android.content.Context; import android.os.Bundle; import android.support.v7.preference.Preference; -import com.android.settings.UserDictionarySettings; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.inputmethod.UserDictionaryList; +import com.android.settings.inputmethod.UserDictionarySettings; import com.android.settingslib.core.AbstractPreferenceController; import java.util.TreeSet; diff --git a/tests/robotests/src/com/android/settings/inputmethod/UserDictionaryCursorLoaderTest.java b/tests/robotests/src/com/android/settings/inputmethod/UserDictionaryCursorLoaderTest.java new file mode 100644 index 0000000000..a76aebc627 --- /dev/null +++ b/tests/robotests/src/com/android/settings/inputmethod/UserDictionaryCursorLoaderTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.inputmethod; + + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.provider.UserDictionary; + +import com.android.settings.TestConfig; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowContentResolver; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class UserDictionaryCursorLoaderTest { + + private ContentProvider mContentProvider; + private UserDictionaryCursorLoader mLoader; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContentProvider = new FakeProvider(); + mLoader = new UserDictionaryCursorLoader(RuntimeEnvironment.application, "" /* locale */); + ShadowContentResolver.registerProvider(UserDictionary.AUTHORITY, mContentProvider); + } + + @Test + public void testLoad_shouldRemoveDuplicate() { + final Cursor cursor = mLoader.loadInBackground(); + + assertThat(cursor.getCount()).isEqualTo(4); + } + + public static class FakeProvider extends ContentProvider { + + + @Override + public boolean onCreate() { + return false; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + final MatrixCursor cursor = new MatrixCursor( + UserDictionaryCursorLoader.QUERY_PROJECTION); + cursor.addRow(new Object[]{1, "word1", "shortcut1"}); + cursor.addRow(new Object[]{2, "word2", "shortcut2"}); + cursor.addRow(new Object[]{3, "word3", "shortcut3"}); + cursor.addRow(new Object[]{4, "word3", "shortcut3"}); // dupe of 3 + cursor.addRow(new Object[]{5, "word5", null}); // no shortcut + return cursor; + } + + @Override + public String getType(Uri uri) { + return null; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + return null; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return 0; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + return 0; + } + } + +} diff --git a/tests/robotests/src/com/android/settings/language/UserDictionaryPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/language/UserDictionaryPreferenceControllerTest.java index 3fc99d2d83..fd1f722caf 100644 --- a/tests/robotests/src/com/android/settings/language/UserDictionaryPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/language/UserDictionaryPreferenceControllerTest.java @@ -16,15 +16,16 @@ package com.android.settings.language; +import static com.google.common.truth.Truth.assertThat; + import android.content.Context; -import android.speech.tts.TtsEngines; import android.support.v7.preference.Preference; -import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.TestConfig; -import com.android.settings.UserDictionarySettings; import com.android.settings.inputmethod.UserDictionaryList; +import com.android.settings.inputmethod.UserDictionarySettings; import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.SettingsRobolectricTestRunner; import org.junit.Before; import org.junit.Test; @@ -37,16 +38,12 @@ import org.robolectric.shadows.ShadowApplication; import java.util.TreeSet; -import static com.google.common.truth.Truth.assertThat; - @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) public class UserDictionaryPreferenceControllerTest { @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext; - @Mock - private TtsEngines mTtsEngines; private Preference mPreference; private TestController mController;