diff options
Diffstat (limited to 'java/src/org/kelar/inputmethod/latin/userdictionary')
6 files changed, 1060 insertions, 0 deletions
diff --git a/java/src/org/kelar/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java b/java/src/org/kelar/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java new file mode 100644 index 000000000..f214eb82a --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2013 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 org.kelar.inputmethod.latin.userdictionary; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.os.Bundle; +import android.provider.UserDictionary; +import android.text.TextUtils; +import android.view.View; +import android.widget.EditText; + +import org.kelar.inputmethod.compat.UserDictionaryCompatUtils; +import org.kelar.inputmethod.latin.R; +import org.kelar.inputmethod.latin.common.LocaleUtils; + +import java.util.ArrayList; +import java.util.Locale; +import java.util.TreeSet; + +import javax.annotation.Nullable; + +// Caveat: This class is basically taken from +// packages/apps/Settings/src/com/android/settings/inputmethod/UserDictionaryAddWordContents.java +// in order to deal with some devices that have issues with the user dictionary handling + +/** + * A container class to factor common code to UserDictionaryAddWordFragment + * and UserDictionaryAddWordActivity. + */ +public class UserDictionaryAddWordContents { + public static final String EXTRA_MODE = "mode"; + public static final String EXTRA_WORD = "word"; + public static final String EXTRA_SHORTCUT = "shortcut"; + public static final String EXTRA_LOCALE = "locale"; + public static final String EXTRA_ORIGINAL_WORD = "originalWord"; + public static final String EXTRA_ORIGINAL_SHORTCUT = "originalShortcut"; + + public static final int MODE_EDIT = 0; + public static final int MODE_INSERT = 1; + + /* package */ static final int CODE_WORD_ADDED = 0; + /* package */ static final int CODE_CANCEL = 1; + /* package */ static final int CODE_ALREADY_PRESENT = 2; + + private static final int FREQUENCY_FOR_USER_DICTIONARY_ADDS = 250; + + private final int mMode; // Either MODE_EDIT or MODE_INSERT + private final EditText mWordEditText; + private final EditText mShortcutEditText; + private String mLocale; + private final String mOldWord; + private final String mOldShortcut; + private String mSavedWord; + private String mSavedShortcut; + + /* package */ UserDictionaryAddWordContents(final View view, final Bundle args) { + mWordEditText = (EditText)view.findViewById(R.id.user_dictionary_add_word_text); + mShortcutEditText = (EditText)view.findViewById(R.id.user_dictionary_add_shortcut); + if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) { + mShortcutEditText.setVisibility(View.GONE); + view.findViewById(R.id.user_dictionary_add_shortcut_label).setVisibility(View.GONE); + } + final String word = args.getString(EXTRA_WORD); + if (null != word) { + mWordEditText.setText(word); + // Use getText in case the edit text modified the text we set. This happens when + // it's too long to be edited. + mWordEditText.setSelection(mWordEditText.getText().length()); + } + final String shortcut; + if (UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) { + shortcut = args.getString(EXTRA_SHORTCUT); + if (null != shortcut && null != mShortcutEditText) { + mShortcutEditText.setText(shortcut); + } + mOldShortcut = args.getString(EXTRA_SHORTCUT); + } else { + shortcut = null; + mOldShortcut = null; + } + mMode = args.getInt(EXTRA_MODE); // default return value for #getInt() is 0 = MODE_EDIT + mOldWord = args.getString(EXTRA_WORD); + updateLocale(args.getString(EXTRA_LOCALE)); + } + + /* package */ UserDictionaryAddWordContents(final View view, + final UserDictionaryAddWordContents oldInstanceToBeEdited) { + mWordEditText = (EditText)view.findViewById(R.id.user_dictionary_add_word_text); + mShortcutEditText = (EditText)view.findViewById(R.id.user_dictionary_add_shortcut); + mMode = MODE_EDIT; + mOldWord = oldInstanceToBeEdited.mSavedWord; + mOldShortcut = oldInstanceToBeEdited.mSavedShortcut; + updateLocale(mLocale); + } + + // locale may be null, this means default locale + // It may also be the empty string, which means "all locales" + /* package */ void updateLocale(final String locale) { + mLocale = null == locale ? Locale.getDefault().toString() : locale; + } + + /* package */ void saveStateIntoBundle(final Bundle outState) { + outState.putString(EXTRA_WORD, mWordEditText.getText().toString()); + outState.putString(EXTRA_ORIGINAL_WORD, mOldWord); + if (null != mShortcutEditText) { + outState.putString(EXTRA_SHORTCUT, mShortcutEditText.getText().toString()); + } + if (null != mOldShortcut) { + outState.putString(EXTRA_ORIGINAL_SHORTCUT, mOldShortcut); + } + outState.putString(EXTRA_LOCALE, mLocale); + } + + /* package */ void delete(final Context context) { + if (MODE_EDIT == mMode && !TextUtils.isEmpty(mOldWord)) { + // Mode edit: remove the old entry. + final ContentResolver resolver = context.getContentResolver(); + UserDictionarySettings.deleteWord(mOldWord, mOldShortcut, resolver); + } + // If we are in add mode, nothing was added, so we don't need to do anything. + } + + /* package */ + int apply(final Context context, final Bundle outParameters) { + if (null != outParameters) saveStateIntoBundle(outParameters); + final ContentResolver resolver = context.getContentResolver(); + if (MODE_EDIT == mMode && !TextUtils.isEmpty(mOldWord)) { + // Mode edit: remove the old entry. + UserDictionarySettings.deleteWord(mOldWord, mOldShortcut, resolver); + } + final String newWord = mWordEditText.getText().toString(); + final String newShortcut; + if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) { + newShortcut = null; + } else if (null == mShortcutEditText) { + newShortcut = null; + } else { + final String tmpShortcut = mShortcutEditText.getText().toString(); + if (TextUtils.isEmpty(tmpShortcut)) { + newShortcut = null; + } else { + newShortcut = tmpShortcut; + } + } + if (TextUtils.isEmpty(newWord)) { + // If the word is somehow empty, don't insert it. + return CODE_CANCEL; + } + mSavedWord = newWord; + mSavedShortcut = newShortcut; + // If there is no shortcut, and the word already exists in the database, then we + // should not insert, because either A. the word exists with no shortcut, in which + // case the exact same thing we want to insert is already there, or B. the word + // exists with at least one shortcut, in which case it has priority on our word. + if (TextUtils.isEmpty(newShortcut) && hasWord(newWord, context)) { + return CODE_ALREADY_PRESENT; + } + + // Disallow duplicates. If the same word with no shortcut is defined, remove it; if + // the same word with the same shortcut is defined, remove it; but we don't mind if + // there is the same word with a different, non-empty shortcut. + UserDictionarySettings.deleteWord(newWord, null, resolver); + if (!TextUtils.isEmpty(newShortcut)) { + // If newShortcut is empty we just deleted this, no need to do it again + UserDictionarySettings.deleteWord(newWord, newShortcut, resolver); + } + + // In this class we use the empty string to represent 'all locales' and mLocale cannot + // be null. However the addWord method takes null to mean 'all locales'. + UserDictionaryCompatUtils.addWord(context, newWord.toString(), + FREQUENCY_FOR_USER_DICTIONARY_ADDS, newShortcut, TextUtils.isEmpty(mLocale) ? + null : LocaleUtils.constructLocaleFromString(mLocale)); + + return CODE_WORD_ADDED; + } + + private static final String[] HAS_WORD_PROJECTION = { UserDictionary.Words.WORD }; + private static final String HAS_WORD_SELECTION_ONE_LOCALE = UserDictionary.Words.WORD + + "=? AND " + UserDictionary.Words.LOCALE + "=?"; + private static final String HAS_WORD_SELECTION_ALL_LOCALES = UserDictionary.Words.WORD + + "=? AND " + UserDictionary.Words.LOCALE + " is null"; + private boolean hasWord(final String word, final Context context) { + final Cursor cursor; + // mLocale == "" indicates this is an entry for all languages. Here, mLocale can't + // be null at all (it's ensured by the updateLocale method). + if ("".equals(mLocale)) { + cursor = context.getContentResolver().query(UserDictionary.Words.CONTENT_URI, + HAS_WORD_PROJECTION, HAS_WORD_SELECTION_ALL_LOCALES, + new String[] { word }, null /* sort order */); + } else { + cursor = context.getContentResolver().query(UserDictionary.Words.CONTENT_URI, + HAS_WORD_PROJECTION, HAS_WORD_SELECTION_ONE_LOCALE, + new String[] { word, mLocale }, null /* sort order */); + } + try { + if (null == cursor) return false; + return cursor.getCount() > 0; + } finally { + if (null != cursor) cursor.close(); + } + } + + public static class LocaleRenderer { + private final String mLocaleString; + private final String mDescription; + + public LocaleRenderer(final Context context, @Nullable final String localeString) { + mLocaleString = localeString; + if (null == localeString) { + mDescription = context.getString(R.string.user_dict_settings_more_languages); + } else if ("".equals(localeString)) { + mDescription = context.getString(R.string.user_dict_settings_all_languages); + } else { + mDescription = LocaleUtils.constructLocaleFromString(localeString).getDisplayName(); + } + } + @Override + public String toString() { + return mDescription; + } + public String getLocaleString() { + return mLocaleString; + } + // "More languages..." is null ; "All languages" is the empty string. + public boolean isMoreLanguages() { + return null == mLocaleString; + } + } + + private static void addLocaleDisplayNameToList(final Context context, + final ArrayList<LocaleRenderer> list, final String locale) { + if (null != locale) { + list.add(new LocaleRenderer(context, locale)); + } + } + + // Helper method to get the list of locales to display for this word + public ArrayList<LocaleRenderer> getLocalesList(final Activity activity) { + final TreeSet<String> locales = UserDictionaryList.getUserDictionaryLocalesSet(activity); + // Remove our locale if it's in, because we're always gonna put it at the top + locales.remove(mLocale); // mLocale may not be null + final String systemLocale = Locale.getDefault().toString(); + // The system locale should be inside. We want it at the 2nd spot. + locales.remove(systemLocale); // system locale may not be null + locales.remove(""); // Remove the empty string if it's there + final ArrayList<LocaleRenderer> localesList = new ArrayList<>(); + // Add the passed locale, then the system locale at the top of the list. Add an + // "all languages" entry at the bottom of the list. + addLocaleDisplayNameToList(activity, localesList, mLocale); + if (!systemLocale.equals(mLocale)) { + addLocaleDisplayNameToList(activity, localesList, systemLocale); + } + for (final String l : locales) { + // TODO: sort in unicode order + addLocaleDisplayNameToList(activity, localesList, l); + } + if (!"".equals(mLocale)) { + // If mLocale is "", then we already inserted the "all languages" item, so don't do it + addLocaleDisplayNameToList(activity, localesList, ""); // meaning: all languages + } + localesList.add(new LocaleRenderer(activity, null)); // meaning: select another locale + return localesList; + } + + public String getCurrentUserDictionaryLocale() { + return mLocale; + } +} + diff --git a/java/src/org/kelar/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java b/java/src/org/kelar/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java new file mode 100644 index 000000000..33fa4b84d --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2013 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 org.kelar.inputmethod.latin.userdictionary; + +import org.kelar.inputmethod.latin.R; +import org.kelar.inputmethod.latin.userdictionary.UserDictionaryAddWordContents.LocaleRenderer; +import org.kelar.inputmethod.latin.userdictionary.UserDictionaryLocalePicker.LocationChangedListener; + +import android.app.Fragment; +import android.os.Bundle; +import android.preference.PreferenceActivity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Spinner; + +import java.util.ArrayList; +import java.util.Locale; + +// Caveat: This class is basically taken from +// packages/apps/Settings/src/com/android/settings/inputmethod/UserDictionaryAddWordFragment.java +// in order to deal with some devices that have issues with the user dictionary handling + +/** + * Fragment to add a word/shortcut to the user dictionary. + * + * As opposed to the UserDictionaryActivity, this is only invoked within Settings + * from the UserDictionarySettings. + */ +public class UserDictionaryAddWordFragment extends Fragment + implements AdapterView.OnItemSelectedListener, LocationChangedListener { + + private static final int OPTIONS_MENU_ADD = Menu.FIRST; + private static final int OPTIONS_MENU_DELETE = Menu.FIRST + 1; + + private UserDictionaryAddWordContents mContents; + private View mRootView; + private boolean mIsDeleting = false; + + @Override + public void onActivityCreated(final Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setHasOptionsMenu(true); + getActivity().getActionBar().setTitle(R.string.edit_personal_dictionary); + // Keep the instance so that we remember mContents when configuration changes (eg rotation) + setRetainInstance(true); + } + + @Override + public View onCreateView(final LayoutInflater inflater, final ViewGroup container, + final Bundle savedState) { + mRootView = inflater.inflate(R.layout.user_dictionary_add_word_fullscreen, null); + mIsDeleting = false; + // If we have a non-null mContents object, it's the old value before a configuration + // change (eg rotation) so we need to use its values. Otherwise, read from the arguments. + if (null == mContents) { + mContents = new UserDictionaryAddWordContents(mRootView, getArguments()); + } else { + // We create a new mContents object to account for the new situation : a word has + // been added to the user dictionary when we started rotating, and we are now editing + // it. That means in particular if the word undergoes any change, the old version should + // be updated, so the mContents object needs to switch to EDIT mode if it was in + // INSERT mode. + mContents = new UserDictionaryAddWordContents(mRootView, + mContents /* oldInstanceToBeEdited */); + } + getActivity().getActionBar().setSubtitle(UserDictionarySettingsUtils.getLocaleDisplayName( + getActivity(), mContents.getCurrentUserDictionaryLocale())); + return mRootView; + } + + @Override + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { + final MenuItem actionItemAdd = menu.add(0, OPTIONS_MENU_ADD, 0, + R.string.user_dict_settings_add_menu_title).setIcon(R.drawable.ic_menu_add); + actionItemAdd.setShowAsAction( + MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + final MenuItem actionItemDelete = menu.add(0, OPTIONS_MENU_DELETE, 0, + R.string.user_dict_settings_delete).setIcon(android.R.drawable.ic_menu_delete); + actionItemDelete.setShowAsAction( + MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + } + + /** + * Callback for the framework when a menu option is pressed. + * + * @param item the item that was pressed + * @return false to allow normal menu processing to proceed, true to consume it here + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == OPTIONS_MENU_ADD) { + // added the entry in "onPause" + getActivity().onBackPressed(); + return true; + } + if (item.getItemId() == OPTIONS_MENU_DELETE) { + mContents.delete(getActivity()); + mIsDeleting = true; + getActivity().onBackPressed(); + return true; + } + return false; + } + + @Override + public void onResume() { + super.onResume(); + // We are being shown: display the word + updateSpinner(); + } + + private void updateSpinner() { + final ArrayList<LocaleRenderer> localesList = mContents.getLocalesList(getActivity()); + + final Spinner localeSpinner = + (Spinner)mRootView.findViewById(R.id.user_dictionary_add_locale); + final ArrayAdapter<LocaleRenderer> adapter = new ArrayAdapter<>( + getActivity(), android.R.layout.simple_spinner_item, localesList); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + localeSpinner.setAdapter(adapter); + localeSpinner.setOnItemSelectedListener(this); + } + + @Override + public void onPause() { + super.onPause(); + // We are being hidden: commit changes to the user dictionary, unless we were deleting it + if (!mIsDeleting) { + mContents.apply(getActivity(), null); + } + } + + @Override + public void onItemSelected(final AdapterView<?> parent, final View view, final int pos, + final long id) { + final LocaleRenderer locale = (LocaleRenderer)parent.getItemAtPosition(pos); + if (locale.isMoreLanguages()) { + PreferenceActivity preferenceActivity = (PreferenceActivity)getActivity(); + preferenceActivity.startPreferenceFragment(new UserDictionaryLocalePicker(), true); + } else { + mContents.updateLocale(locale.getLocaleString()); + } + } + + @Override + public void onNothingSelected(final AdapterView<?> parent) { + // I'm not sure we can come here, but if we do, that's the right thing to do. + final Bundle args = getArguments(); + mContents.updateLocale(args.getString(UserDictionaryAddWordContents.EXTRA_LOCALE)); + } + + // Called by the locale picker + @Override + public void onLocaleSelected(final Locale locale) { + mContents.updateLocale(locale.toString()); + getActivity().onBackPressed(); + } +} + diff --git a/java/src/org/kelar/inputmethod/latin/userdictionary/UserDictionaryList.java b/java/src/org/kelar/inputmethod/latin/userdictionary/UserDictionaryList.java new file mode 100644 index 000000000..7fd5825ed --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/userdictionary/UserDictionaryList.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2013 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 org.kelar.inputmethod.latin.userdictionary; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.PreferenceGroup; +import android.provider.UserDictionary; +import android.text.TextUtils; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodSubtype; + +import org.kelar.inputmethod.latin.R; +import org.kelar.inputmethod.latin.common.LocaleUtils; + +import java.util.List; +import java.util.Locale; +import java.util.TreeSet; + +import javax.annotation.Nullable; + +// Caveat: This class is basically taken from +// packages/apps/Settings/src/com/android/settings/inputmethod/UserDictionaryList.java +// in order to deal with some devices that have issues with the user dictionary handling + +public class UserDictionaryList extends PreferenceFragment { + + public static final String USER_DICTIONARY_SETTINGS_INTENT_ACTION = + "android.settings.USER_DICTIONARY_SETTINGS"; + + @Override + public void onCreate(final Bundle icicle) { + super.onCreate(icicle); + setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getActivity())); + } + + public static TreeSet<String> getUserDictionaryLocalesSet(final Activity activity) { + final Cursor cursor = activity.getContentResolver().query(UserDictionary.Words.CONTENT_URI, + new String[] { UserDictionary.Words.LOCALE }, + null, null, null); + final TreeSet<String> localeSet = new TreeSet<>(); + if (null == cursor) { + // The user dictionary service is not present or disabled. Return null. + return null; + } + try { + if (cursor.moveToFirst()) { + final int columnIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE); + do { + final String locale = cursor.getString(columnIndex); + localeSet.add(null != locale ? locale : ""); + } while (cursor.moveToNext()); + } + } finally { + cursor.close(); + } + if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) { + // For ICS, we need to show "For all languages" in case that the keyboard locale + // is different from the system locale + localeSet.add(""); + } + + final InputMethodManager imm = + (InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE); + final List<InputMethodInfo> imis = imm.getEnabledInputMethodList(); + for (final InputMethodInfo imi : imis) { + final List<InputMethodSubtype> subtypes = + imm.getEnabledInputMethodSubtypeList( + imi, true /* allowsImplicitlySelectedSubtypes */); + for (InputMethodSubtype subtype : subtypes) { + final String locale = subtype.getLocale(); + if (!TextUtils.isEmpty(locale)) { + localeSet.add(locale); + } + } + } + + // We come here after we have collected locales from existing user dictionary entries and + // enabled subtypes. If we already have the locale-without-country version of the system + // locale, we don't add the system locale to avoid confusion even though it's technically + // correct to add it. + if (!localeSet.contains(Locale.getDefault().getLanguage().toString())) { + localeSet.add(Locale.getDefault().toString()); + } + + return localeSet; + } + + /** + * Creates the entries that allow the user to go into the user dictionary for each locale. + * @param userDictGroup The group to put the settings in. + */ + protected void createUserDictSettings(final PreferenceGroup userDictGroup) { + final Activity activity = getActivity(); + userDictGroup.removeAll(); + final TreeSet<String> localeSet = + UserDictionaryList.getUserDictionaryLocalesSet(activity); + + if (localeSet.size() > 1) { + // Have an "All languages" entry in the languages list if there are two or more active + // languages + localeSet.add(""); + } + + if (localeSet.isEmpty()) { + userDictGroup.addPreference(createUserDictionaryPreference(null)); + } else { + for (String locale : localeSet) { + userDictGroup.addPreference(createUserDictionaryPreference(locale)); + } + } + } + + /** + * Create a single User Dictionary Preference object, with its parameters set. + * @param localeString The locale for which this user dictionary is for. + * @return The corresponding preference. + */ + protected Preference createUserDictionaryPreference(@Nullable final String localeString) { + final Preference newPref = new Preference(getActivity()); + final Intent intent = new Intent(USER_DICTIONARY_SETTINGS_INTENT_ACTION); + if (null == localeString) { + newPref.setTitle(Locale.getDefault().getDisplayName()); + } else { + if (localeString.isEmpty()) { + newPref.setTitle(getString(R.string.user_dict_settings_all_languages)); + } else { + newPref.setTitle( + LocaleUtils.constructLocaleFromString(localeString).getDisplayName()); + } + intent.putExtra("locale", localeString); + newPref.getExtras().putString("locale", localeString); + } + newPref.setIntent(intent); + newPref.setFragment(UserDictionarySettings.class.getName()); + return newPref; + } + + @Override + public void onResume() { + super.onResume(); + createUserDictSettings(getPreferenceScreen()); + } +} + diff --git a/java/src/org/kelar/inputmethod/latin/userdictionary/UserDictionaryLocalePicker.java b/java/src/org/kelar/inputmethod/latin/userdictionary/UserDictionaryLocalePicker.java new file mode 100644 index 000000000..12d9140f8 --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/userdictionary/UserDictionaryLocalePicker.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2013 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 org.kelar.inputmethod.latin.userdictionary; + +import android.app.Fragment; + +import java.util.Locale; + +// Caveat: This class is basically taken from +// packages/apps/Settings/src/com/android/settings/inputmethod/UserDictionaryLocalePicker.java +// in order to deal with some devices that have issues with the user dictionary handling + +public class UserDictionaryLocalePicker extends Fragment { + public UserDictionaryLocalePicker() { + super(); + // TODO: implement + } + + public interface LocationChangedListener { + public void onLocaleSelected(Locale locale); + } +} diff --git a/java/src/org/kelar/inputmethod/latin/userdictionary/UserDictionarySettings.java b/java/src/org/kelar/inputmethod/latin/userdictionary/UserDictionarySettings.java new file mode 100644 index 000000000..d02dbd67c --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/userdictionary/UserDictionarySettings.java @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2013 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 org.kelar.inputmethod.latin.userdictionary; + +import org.kelar.inputmethod.latin.R; + +import android.app.ListFragment; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.os.Build; +import android.os.Bundle; +import android.provider.UserDictionary; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AlphabetIndexer; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.SectionIndexer; +import android.widget.SimpleCursorAdapter; +import android.widget.TextView; + +import java.util.Locale; + +// Caveat: This class is basically taken from +// packages/apps/Settings/src/com/android/settings/inputmethod/UserDictionarySettings.java +// in order to deal with some devices that have issues with the user dictionary handling + +public class UserDictionarySettings extends ListFragment { + + public static final boolean IS_SHORTCUT_API_SUPPORTED = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + + private static final String[] QUERY_PROJECTION_SHORTCUT_UNSUPPORTED = + { UserDictionary.Words._ID, UserDictionary.Words.WORD}; + private static final String[] QUERY_PROJECTION_SHORTCUT_SUPPORTED = + { UserDictionary.Words._ID, UserDictionary.Words.WORD, UserDictionary.Words.SHORTCUT}; + private static final String[] QUERY_PROJECTION = + IS_SHORTCUT_API_SUPPORTED ? + QUERY_PROJECTION_SHORTCUT_SUPPORTED : QUERY_PROJECTION_SHORTCUT_UNSUPPORTED; + + // The index of the shortcut in the above array. + private static final int INDEX_SHORTCUT = 2; + + private static final String[] ADAPTER_FROM_SHORTCUT_UNSUPPORTED = { + UserDictionary.Words.WORD, + }; + + private static final String[] ADAPTER_FROM_SHORTCUT_SUPPORTED = { + UserDictionary.Words.WORD, UserDictionary.Words.SHORTCUT + }; + + private static final String[] ADAPTER_FROM = IS_SHORTCUT_API_SUPPORTED ? + ADAPTER_FROM_SHORTCUT_SUPPORTED : ADAPTER_FROM_SHORTCUT_UNSUPPORTED; + + private static final int[] ADAPTER_TO_SHORTCUT_UNSUPPORTED = { + android.R.id.text1, + }; + + private static final int[] ADAPTER_TO_SHORTCUT_SUPPORTED = { + android.R.id.text1, android.R.id.text2 + }; + + private static final int[] ADAPTER_TO = IS_SHORTCUT_API_SUPPORTED ? + ADAPTER_TO_SHORTCUT_SUPPORTED : ADAPTER_TO_SHORTCUT_UNSUPPORTED; + + // 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"; + + private static final String DELETE_SELECTION_WITH_SHORTCUT = UserDictionary.Words.WORD + + "=? AND " + UserDictionary.Words.SHORTCUT + "=?"; + private static final String DELETE_SELECTION_WITHOUT_SHORTCUT = UserDictionary.Words.WORD + + "=? AND " + UserDictionary.Words.SHORTCUT + " is null OR " + + UserDictionary.Words.SHORTCUT + "=''"; + private static final String DELETE_SELECTION_SHORTCUT_UNSUPPORTED = + UserDictionary.Words.WORD + "=?"; + + private static final int OPTIONS_MENU_ADD = Menu.FIRST; + + private Cursor mCursor; + + protected String mLocale; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getActivity().getActionBar().setTitle(R.string.edit_personal_dictionary); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate( + R.layout.user_dictionary_preference_list_fragment, container, false); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + final Intent intent = getActivity().getIntent(); + final String localeFromIntent = + null == intent ? null : intent.getStringExtra("locale"); + + final Bundle arguments = getArguments(); + final String localeFromArguments = + null == arguments ? null : arguments.getString("locale"); + + final String locale; + if (null != localeFromArguments) { + locale = localeFromArguments; + } else if (null != localeFromIntent) { + locale = localeFromIntent; + } else { + locale = null; + } + + mLocale = locale; + // WARNING: The following cursor is never closed! TODO: don't put that in a member, and + // make sure all cursors are correctly closed. Also, this comes from a call to + // Activity#managedQuery, which has been deprecated for a long time (and which FORBIDS + // closing the cursor, so take care when resolving this TODO). We should either use a + // regular query and close the cursor, or switch to a LoaderManager and a CursorLoader. + mCursor = createCursor(locale); + TextView emptyView = (TextView) 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(); + ListAdapter adapter = getListView().getAdapter(); + if (adapter != null && adapter instanceof MyAdapter) { + // The list view is forced refreshed here. This allows the changes done + // in UserDictionaryAddWordFragment (update/delete/insert) to be seen when + // user goes back to this view. + MyAdapter listAdapter = (MyAdapter) adapter; + listAdapter.notifyDataSetChanged(); + } + } + + @SuppressWarnings("deprecation") + 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 + ")"); + } + 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 + ")"); + } + + private ListAdapter createAdapter() { + return new MyAdapter(getActivity(), R.layout.user_dictionary_item, mCursor, + ADAPTER_FROM, ADAPTER_TO); + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + final String word = getWord(position); + final String shortcut = getShortcut(position); + if (word != null) { + showAddOrEditDialog(word, shortcut); + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) { + final Locale systemLocale = getResources().getConfiguration().locale; + if (!TextUtils.isEmpty(mLocale) && !mLocale.equals(systemLocale.toString())) { + // Hide the add button for ICS because it doesn't support specifying a locale + // for an entry. This new "locale"-aware API has been added in conjunction + // with the shortcut API. + return; + } + } + MenuItem actionItem = + menu.add(0, OPTIONS_MENU_ADD, 0, R.string.user_dict_settings_add_menu_title) + .setIcon(R.drawable.ic_menu_add); + actionItem.setShowAsAction( + MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == OPTIONS_MENU_ADD) { + showAddOrEditDialog(null, null); + return true; + } + return false; + } + + /** + * 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 editingShortcut the shortcut for this entry, or null if none. + */ + private void showAddOrEditDialog(final String editingWord, final String editingShortcut) { + final Bundle args = new Bundle(); + args.putInt(UserDictionaryAddWordContents.EXTRA_MODE, null == editingWord + ? UserDictionaryAddWordContents.MODE_INSERT + : UserDictionaryAddWordContents.MODE_EDIT); + args.putString(UserDictionaryAddWordContents.EXTRA_WORD, editingWord); + args.putString(UserDictionaryAddWordContents.EXTRA_SHORTCUT, editingShortcut); + args.putString(UserDictionaryAddWordContents.EXTRA_LOCALE, mLocale); + android.preference.PreferenceActivity pa = + (android.preference.PreferenceActivity)getActivity(); + pa.startPreferencePanel(UserDictionaryAddWordFragment.class.getName(), + args, R.string.user_dict_settings_add_dialog_title, null, null, 0); + } + + private String getWord(final int position) { + if (null == mCursor) return null; + mCursor.moveToPosition(position); + // Handle a possible race-condition + if (mCursor.isAfterLast()) return null; + + return mCursor.getString( + mCursor.getColumnIndexOrThrow(UserDictionary.Words.WORD)); + } + + private String getShortcut(final int position) { + if (!IS_SHORTCUT_API_SUPPORTED) return null; + if (null == mCursor) return null; + mCursor.moveToPosition(position); + // Handle a possible race-condition + if (mCursor.isAfterLast()) return null; + + return mCursor.getString( + mCursor.getColumnIndexOrThrow(UserDictionary.Words.SHORTCUT)); + } + + public static void deleteWord(final String word, final String shortcut, + final ContentResolver resolver) { + if (!IS_SHORTCUT_API_SUPPORTED) { + resolver.delete(UserDictionary.Words.CONTENT_URI, DELETE_SELECTION_SHORTCUT_UNSUPPORTED, + new String[] { word }); + } else if (TextUtils.isEmpty(shortcut)) { + resolver.delete( + UserDictionary.Words.CONTENT_URI, DELETE_SELECTION_WITHOUT_SHORTCUT, + new String[] { word }); + } else { + resolver.delete( + UserDictionary.Words.CONTENT_URI, DELETE_SELECTION_WITH_SHORTCUT, + new String[] { word, shortcut }); + } + } + + private static class MyAdapter extends SimpleCursorAdapter implements SectionIndexer { + private AlphabetIndexer mIndexer; + + private ViewBinder mViewBinder = new ViewBinder() { + + @Override + public boolean setViewValue(final View v, final Cursor c, final int columnIndex) { + if (!IS_SHORTCUT_API_SUPPORTED) { + // just let SimpleCursorAdapter set the view values + return false; + } + if (columnIndex == INDEX_SHORTCUT) { + final String shortcut = c.getString(INDEX_SHORTCUT); + if (TextUtils.isEmpty(shortcut)) { + v.setVisibility(View.GONE); + } else { + ((TextView)v).setText(shortcut); + v.setVisibility(View.VISIBLE); + } + v.invalidate(); + return true; + } + + return false; + } + }; + + public MyAdapter(final Context context, final int layout, final Cursor c, + final String[] from, final int[] to) { + super(context, layout, c, from, to, 0 /* flags */); + + if (null != c) { + final String alphabet = context.getString(R.string.user_dict_fast_scroll_alphabet); + final int wordColIndex = c.getColumnIndexOrThrow(UserDictionary.Words.WORD); + mIndexer = new AlphabetIndexer(c, wordColIndex, alphabet); + } + setViewBinder(mViewBinder); + } + + @Override + public int getPositionForSection(final int section) { + return null == mIndexer ? 0 : mIndexer.getPositionForSection(section); + } + + @Override + public int getSectionForPosition(final int position) { + return null == mIndexer ? 0 : mIndexer.getSectionForPosition(position); + } + + @Override + public Object[] getSections() { + return null == mIndexer ? null : mIndexer.getSections(); + } + } +} + diff --git a/java/src/org/kelar/inputmethod/latin/userdictionary/UserDictionarySettingsUtils.java b/java/src/org/kelar/inputmethod/latin/userdictionary/UserDictionarySettingsUtils.java new file mode 100644 index 000000000..095ab3e09 --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/userdictionary/UserDictionarySettingsUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2013 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 org.kelar.inputmethod.latin.userdictionary; + +import org.kelar.inputmethod.latin.R; +import org.kelar.inputmethod.latin.common.LocaleUtils; + +import android.content.Context; +import android.text.TextUtils; + +import java.util.Locale; + +/** + * Utilities of the user dictionary settings + * TODO: We really want to move these utilities to a static library. + */ +public class UserDictionarySettingsUtils { + public static String getLocaleDisplayName(Context context, String localeStr) { + if (TextUtils.isEmpty(localeStr)) { + // CAVEAT: localeStr should not be null because a null locale stands for the system + // locale in UserDictionary.Words.addWord. + return context.getResources().getString(R.string.user_dict_settings_all_languages); + } + final Locale locale = LocaleUtils.constructLocaleFromString(localeStr); + final Locale systemLocale = context.getResources().getConfiguration().locale; + return locale.getDisplayName(systemLocale); + } +} |