Implement a functionality to add an entry to the user dictionary

Bug: 8600958

Change-Id: Ic472500406b9d54ec4052c490ee7cef62fc4e52a
diff --git a/java/res/drawable-hdpi/ic_menu_add.png b/java/res/drawable-hdpi/ic_menu_add.png
new file mode 100644
index 0000000..4b68f52
--- /dev/null
+++ b/java/res/drawable-hdpi/ic_menu_add.png
Binary files differ
diff --git a/java/res/drawable-mdpi/ic_menu_add.png b/java/res/drawable-mdpi/ic_menu_add.png
new file mode 100644
index 0000000..15ffadd
--- /dev/null
+++ b/java/res/drawable-mdpi/ic_menu_add.png
Binary files differ
diff --git a/java/res/drawable-xhdpi/ic_menu_add.png b/java/res/drawable-xhdpi/ic_menu_add.png
new file mode 100644
index 0000000..420510e
--- /dev/null
+++ b/java/res/drawable-xhdpi/ic_menu_add.png
Binary files differ
diff --git a/java/res/layout/user_dictionary_add_word.xml b/java/res/layout/user_dictionary_add_word.xml
new file mode 100644
index 0000000..bbf9b1b
--- /dev/null
+++ b/java/res/layout/user_dictionary_add_word.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/user_dict_settings_add_dialog_top"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical" >
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical" >
+
+        <com.android.internal.widget.DialogTitle
+            style="?android:attr/windowTitleStyle"
+            android:layout_width="match_parent"
+            android:layout_height="64dip"
+            android:layout_marginEnd="16dip"
+            android:layout_marginStart="16dip"
+            android:ellipsize="end"
+            android:gravity="center_vertical|start"
+            android:singleLine="true"
+            android:text="@string/user_dict_settings_add_dialog_title" />
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="2dip"
+            android:background="@android:color/holo_blue_light" />
+    </LinearLayout>
+
+    <EditText
+        android:id="@+id/user_dictionary_add_word_text"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="fill_horizontal|center_vertical"
+        android:layout_marginBottom="8dip"
+        android:layout_marginStart="8dip"
+        android:layout_marginTop="8dip"
+        android:hint="@string/user_dict_settings_add_word_hint"
+        android:imeOptions="flagNoFullscreen"
+        android:inputType="textNoSuggestions"
+        android:maxLength="@integer/user_dictionary_max_word_length" >
+
+        <requestFocus />
+    </EditText>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:divider="?android:attr/dividerHorizontal"
+        android:dividerPadding="0dip"
+        android:orientation="vertical"
+        android:showDividers="beginning" >
+
+        <LinearLayout
+            style="?android:attr/buttonBarStyle"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:measureWithLargestChild="true"
+            android:orientation="horizontal" >
+
+            <Button
+                style="?android:attr/buttonBarButtonStyle"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_gravity="start"
+                android:layout_weight="1"
+                android:maxLines="2"
+                android:onClick="onClickCancel"
+                android:text="@string/cancel"
+                android:textSize="14sp" />
+
+            <Button
+                style="?android:attr/buttonBarButtonStyle"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_gravity="end"
+                android:layout_weight="1"
+                android:maxLines="2"
+                android:onClick="onClickConfirm"
+                android:text="@string/user_dict_settings_add_dialog_confirm"
+                android:textSize="14sp" />
+        </LinearLayout>
+    </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/java/res/layout/user_dictionary_add_word_fullscreen.xml b/java/res/layout/user_dictionary_add_word_fullscreen.xml
new file mode 100644
index 0000000..75e86c5
--- /dev/null
+++ b/java/res/layout/user_dictionary_add_word_fullscreen.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/user_dict_settings_add_dialog_top"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical" >
+
+    <TextView
+        style="?android:attr/listSeparatorTextViewStyle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/user_dict_settings_add_screen_title" />
+
+    <EditText
+        android:id="@+id/user_dictionary_add_word_text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="fill_horizontal|center_vertical"
+        android:layout_marginBottom="8dip"
+        android:layout_marginStart="8dip"
+        android:layout_marginTop="8dip"
+        android:hint="@string/user_dict_settings_add_word_hint"
+        android:imeOptions="flagNoFullscreen"
+        android:inputType="textNoSuggestions"
+        android:maxLength="@integer/user_dictionary_max_word_length" >
+
+        <requestFocus />
+    </EditText>
+
+    <GridLayout
+        android:id="@+id/user_dictionary_add_word_grid"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="8dip"
+        android:layout_marginStart="8dip"
+        android:columnCount="2" >
+
+        <TextView
+            android:id="@+id/user_dictionary_add_shortcut_label"
+            style="?android:attr/textAppearanceSmall"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="start|center_vertical"
+            android:text="@string/user_dict_settings_add_shortcut_option_name" />
+
+        <EditText
+            android:id="@+id/user_dictionary_add_shortcut"
+            android:layout_width="wrap_content"
+            android:layout_gravity="fill_horizontal|center_vertical"
+            android:layout_marginBottom="8dip"
+            android:layout_marginStart="8dip"
+            android:layout_marginTop="8dip"
+            android:hint="@string/user_dict_settings_add_shortcut_hint"
+            android:imeOptions="flagNoFullscreen"
+            android:inputType="textNoSuggestions"
+            android:maxLength="@integer/user_dictionary_max_word_length" />
+
+        <TextView
+            android:id="@+id/user_dictionary_add_locale_label"
+            style="?android:attr/textAppearanceSmall"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="start|center_vertical"
+            android:text="@string/user_dict_settings_add_locale_option_name"
+            android:visibility="gone" />
+
+        <Spinner
+            android:id="@+id/user_dictionary_add_locale"
+            android:layout_width="wrap_content"
+            android:layout_gravity="fill_horizontal|center_vertical"
+            android:layout_marginBottom="8dip"
+            android:layout_marginStart="8dip"
+            android:layout_marginTop="8dip"
+            android:visibility="gone" />
+    </GridLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/java/res/layout/user_dictionary_item.xml b/java/res/layout/user_dictionary_item.xml
new file mode 100644
index 0000000..3062ed8
--- /dev/null
+++ b/java/res/layout/user_dictionary_item.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<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:paddingEnd="?android:attr/scrollbarSize"
+    android:background="?android:attr/selectableItemBackground" >
+
+  <RelativeLayout android:layout_width="wrap_content"
+                  android:layout_height="wrap_content"
+                  android:layout_marginStart="15dip"
+                  android:layout_marginEnd="6dip"
+                  android:layout_marginTop="6dip"
+                  android:layout_marginBottom="6dip"
+                  android:layout_weight="1">
+
+    <TextView android:id="@+android:id/text1"
+              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="@+android:id/text2"
+              android:layout_width="wrap_content"
+              android:layout_height="wrap_content"
+              android:layout_below="@android:id/text1"
+              android:layout_alignStart="@android:id/text1"
+              android:textAppearance="?android:attr/textAppearanceSmall"
+              android:textColor="?android:attr/textColorSecondary"
+              android:maxLines="1" />
+
+  </RelativeLayout>
+
+</LinearLayout>
diff --git a/java/res/layout/user_dictionary_preference_list_fragment.xml b/java/res/layout/user_dictionary_preference_list_fragment.xml
new file mode 100644
index 0000000..40e562c
--- /dev/null
+++ b/java/res/layout/user_dictionary_preference_list_fragment.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/transparent"
+    android:orientation="vertical" >
+
+    <ListView
+        android:id="@android:id/list"
+        android:layout_width="match_parent"
+        android:layout_height="0px"
+        android:layout_weight="1"
+        android:cacheColorHint="@android:color/transparent"
+        android:clipToPadding="false"
+        android:drawSelectorOnTop="false"
+        android:paddingTop="0dip"
+        android:scrollbarAlwaysDrawVerticalTrack="true" />
+
+    <TextView
+        android:id="@android:id/empty"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:padding="5dip"
+        android:visibility="gone" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml
index e9b34aa..da735cf 100644
--- a/java/res/values/dimens.xml
+++ b/java/res/values/dimens.xml
@@ -117,4 +117,6 @@
 
     <!-- Inset used in Accessibility mode to avoid accidental key presses when a finger slides off the screen. -->
     <dimen name="accessibility_edge_slop">8dp</dimen>
+
+    <integer name="user_dictionary_max_word_length" translatable="false">48</integer>
 </resources>
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index d8a88a8..ff79426 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -562,4 +562,76 @@
 
     <!-- Version text [CHAR LIMIT=30]-->
     <string name="version_text">Version <xliff:g id="version_number" example="1.0.1864.643521">%1$s</xliff:g></string>
+
+    <!-- User dictionary settings -->
+    <!-- User dictionary settings, The titlebar text of the User dictionary settings screen. -->
+    <!-- This resource is corresponding to msgid="765659257455000490" -->
+    <string name="user_dict_settings_titlebar">User dictionary</string>
+    <!-- User dictionary settings, The title of the list item to go into the User dictionary settings screen when there is only one user dictionary. [CHAR LIMIT=35] -->
+    <!-- This resource is corresponding to msgid="524997218433540614" -->
+    <string name="user_dict_single_settings_title">Personal dictionary</string>
+    <!-- User dictionary settings, The title of the list item to go into the User dictionary list when there are several user dictionaries. [CHAR LIMIT=35] -->
+    <!-- This resource is corresponding to msgid="3735224433307996276" -->
+    <string name="user_dict_multiple_settings_title">Personal dictionaries</string>
+    <!-- User dictionary settings.  The summary of the listem item to go into the User dictionary settings screen. -->
+    <string name="user_dict_settings_summary" translatable="false">""</string>
+    <!-- User dictionary settings. The title of the menu item to add a new word to the user dictionary. -->
+    <!-- This resource is corresponding to  msgid="4056762757149923551" -->
+    <string name="user_dict_settings_add_menu_title">Add</string>
+    <!-- User dictionary settings. The title of the dialog to add a new word to the user dictionary. [CHAR LIMIT=25] -->
+    <!-- This resource is corresponding to msgid="4702613990174126482" -->
+    <string name="user_dict_settings_add_dialog_title">Add to dictionary</string>
+    <!-- User dictionary settings. The title of the screen to add/edit a new word to the user dictionary; it describes the phrase that will be added to the user dictionary. [CHAR LIMIT=25] -->
+    <!-- This resource is corresponding to msgid="742580720124344291" -->
+    <string name="user_dict_settings_add_screen_title">Phrase</string>
+    <!-- User dictionary settings. Text on the dialog button to pop more options for adding a word. [CHAR LIMIT=16] -->
+    <!-- This resource is corresponding to msgid="8848798370746019825" -->
+    <string name="user_dict_settings_add_dialog_more_options">More options</string>
+    <!-- User dictionary settings. Text on the dialog button mask advanced options. [CHAR LIMIT=15] -->
+    <!-- This resource is corresponding to msgid="2441785268726036101" -->
+    <string name="user_dict_settings_add_dialog_less_options">Less options</string>
+    <!-- User dictionary settings. Text on the dialog button to confirm adding a word. [CHAR LIMIT=15] -->
+    <!-- This resource is corresponding to msgid="6225823625332416144" -->
+    <string name="user_dict_settings_add_dialog_confirm">OK</string>
+    <!-- User dictionary settings. Label to put before the word field (that's the word that will actually be added to the user dictionary when OK is pressed). [CHAR LIMIT=20] -->
+    <!-- This resource is corresponding to msgid="7868879174905963135" -->
+    <string name="user_dict_settings_add_word_option_name">Word:</string>
+    <!-- User dictionary settings. Label to put before the shortcut field (once a shortcut is registered, the user can type the shortcut and get the word it points to in the suggestions). [CHAR LIMIT=20] -->
+    <!-- This resource is corresponding to msgid="660089258866063925" -->
+    <string name="user_dict_settings_add_shortcut_option_name">Shortcut:</string>
+    <!-- User dictionary settings. Label to put before the language field. [CHAR LIMIT=20] -->
+    <!-- This resource is corresponding to msgid="5696358317061318532" -->
+    <string name="user_dict_settings_add_locale_option_name">Language:</string>
+    <!-- User dictionary settings. Hint for the text field to type the word to add to the user dictionary. [CHAR LIMIT=35] -->
+    <!-- This resource is corresponding to msgid="5725254076556821247" -->
+    <string name="user_dict_settings_add_word_hint">Type a word</string>
+    <!-- User dictionary settings. Hint for the text field to type the optional shortcut to add to the user dictionary. [CHAR LIMIT=35] -->
+    <!-- This resource is corresponding to msgid="7333763456561873445" -->
+    <string name="user_dict_settings_add_shortcut_hint">Optional shortcut</string>
+    <!-- User dictionary settings. The title of the dialog to edit an existing word in the user dictionary. -->
+    <!-- This resource is corresponding to msgid="8967476444840548674" -->
+    <string name="user_dict_settings_edit_dialog_title">Edit word</string>
+    <!-- User dictionary settings. The title of the context menu item to edit the current word -->
+    <!-- This resource is corresponding to msgid="2210564879320004837" -->
+    <string name="user_dict_settings_context_menu_edit_title">Edit</string>
+    <!-- User dictionary settings. The title of the context menu item to delete the current word -->
+    <!-- This resource is corresponding to msgid="9140703913776549054" -->
+    <string name="user_dict_settings_context_menu_delete_title">Delete</string>
+    <!-- User dictionary settings. The text to show when there are no user-defined words in the dictionary  [CHAR LIMIT=200] -->
+    <!-- This resource is corresponding to msgid="8165273379942105271" -->
+    <string name="user_dict_settings_empty_text">You don\'t have any words in the user dictionary. Add a word by touching the Add (+) button.</string>
+    <!-- User dictionary settings. The list item to choose to insert a word into the user dictionary for all languages -->
+    <!-- This resource is corresponding to msgid="6742000040975959247" -->
+    <string name="user_dict_settings_all_languages">For all languages</string>
+    <!-- User dictionary settings. The text to show for the option that shows the entire list of supported locales to choose one [CHAR LIMIT=30] -->
+    <!-- This resource is corresponding to msgid="7316375944684977910" -->
+    <string name="user_dict_settings_more_languages">More languages…</string>
+    <!-- User dictionary settings. Label to delete an entry in the user dictionary [CHAR LIMIT=30]
+         This resource is copied from packages/apps/Settings/res/values/strings.xml -->
+    <!-- This resource is corresponding to msgid="4219243412325163003" -->
+    <string name="user_dict_settings_delete">Delete</string>
+    <!-- User dictionary settings. Index of the user dictionary [CHAR LIMIT=30]
+         This resource is copied from packages/apps/Settings/res/values/strings.xml -->
+    <!-- This resource is corresponding to msgid="5433275485499039199" -->
+    <string name="user_dict_fast_scroll_alphabet">\u0020ABCDEFGHIJKLMNOPQRSTUVWXYZ</string>
 </resources>
diff --git a/java/src/com/android/inputmethod/latin/SettingsFragment.java b/java/src/com/android/inputmethod/latin/SettingsFragment.java
index 88a2714..c78064b 100644
--- a/java/src/com/android/inputmethod/latin/SettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/SettingsFragment.java
@@ -16,6 +16,7 @@
 
 package com.android.inputmethod.latin;
 
+import android.app.Activity;
 import android.app.backup.BackupManager;
 import android.content.Context;
 import android.content.Intent;
@@ -24,6 +25,7 @@
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
 import android.media.AudioManager;
+import android.os.Build;
 import android.os.Bundle;
 import android.preference.CheckBoxPreference;
 import android.preference.ListPreference;
@@ -31,17 +33,20 @@
 import android.preference.Preference.OnPreferenceClickListener;
 import android.preference.PreferenceGroup;
 import android.preference.PreferenceScreen;
-import android.util.Log;
 import android.view.inputmethod.InputMethodSubtype;
 
+import java.util.TreeSet;
+
 import com.android.inputmethod.dictionarypack.DictionarySettingsActivity;
 import com.android.inputmethod.latin.define.ProductionFlag;
 import com.android.inputmethod.latin.setup.LauncherIconVisibilityManager;
+import com.android.inputmethod.latin.userdictionary.UserDictionaryList;
+import com.android.inputmethod.latin.userdictionary.UserDictionarySettings;
 import com.android.inputmethodcommon.InputMethodSettingsFragment;
 
 public final class SettingsFragment extends InputMethodSettingsFragment
         implements SharedPreferences.OnSharedPreferenceChangeListener {
-    private static final String TAG = SettingsFragment.class.getSimpleName();
+    private static final boolean DBG_USE_INTERNAL_USER_SETTINGS = false;
 
     private ListPreference mVoicePreference;
     private ListPreference mShowCorrectionSuggestionsPreference;
@@ -197,9 +202,13 @@
         final Intent editPersonalDictionaryIntent = editPersonalDictionary.getIntent();
         final ResolveInfo ri = context.getPackageManager().resolveActivity(
                 editPersonalDictionaryIntent, PackageManager.MATCH_DEFAULT_ONLY);
-        if (ri == null) {
-            // TODO: Set a intent that invokes our own edit personal dictionary activity.
-            Log.w(TAG, "No activity that responds to " + editPersonalDictionaryIntent.getAction());
+        if (DBG_USE_INTERNAL_USER_SETTINGS || ri == null) {
+            // TODO: Support ICS
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+                updateUserDictionaryPreference(editPersonalDictionary);
+            } else {
+                removePreference(Settings.PREF_EDIT_PERSONAL_DICTIONARY, getPreferenceScreen());
+            }
         }
 
         if (!Settings.readFromBuildConfigIfGestureInputEnabled(res)) {
@@ -408,4 +417,33 @@
             }
         });
     }
+
+    private void updateUserDictionaryPreference(Preference userDictionaryPreference) {
+        final Activity activity = getActivity();
+        final TreeSet<String> localeList = UserDictionaryList.getUserDictionaryLocalesSet(activity);
+        if (null == localeList) {
+            // The locale list is null if and only if the user dictionary service is
+            // not present or disabled. In this case we need to remove the preference.
+            getPreferenceScreen().removePreference(userDictionaryPreference);
+        } else if (localeList.size() <= 1) {
+            final Intent intent =
+                    new Intent(UserDictionaryList.USER_DICTIONARY_SETTINGS_INTENT_ACTION);
+            userDictionaryPreference.setTitle(R.string.user_dict_single_settings_title);
+            userDictionaryPreference.setIntent(intent);
+            userDictionaryPreference.setFragment(UserDictionarySettings.class.getName());
+            // If the size of localeList is 0, we don't set the locale parameter in the
+            // extras. This will be interpreted by the UserDictionarySettings class as
+            // meaning "the current locale".
+            // Note that with the current code for UserDictionaryList#getUserDictionaryLocalesSet()
+            // the locale list always has at least one element, since it always includes the current
+            // locale explicitly. @see UserDictionaryList.getUserDictionaryLocalesSet().
+            if (localeList.size() == 1) {
+                final String locale = (String)localeList.toArray()[0];
+                userDictionaryPreference.getExtras().putString("locale", locale);
+            }
+        } else {
+            userDictionaryPreference.setTitle(R.string.user_dict_multiple_settings_title);
+            userDictionaryPreference.setFragment(UserDictionaryList.class.getName());
+        }
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java
new file mode 100644
index 0000000..f0dc526
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java
@@ -0,0 +1,247 @@
+/*
+ * 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 com.android.inputmethod.latin.userdictionary;
+
+import com.android.inputmethod.latin.LocaleUtils;
+import com.android.inputmethod.latin.R;
+
+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 java.util.ArrayList;
+import java.util.Locale;
+import java.util.TreeSet;
+
+// 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;
+
+    /* 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);
+        final String word = args.getString(EXTRA_WORD);
+        if (null != word) {
+            mWordEditText.setText(word);
+            mWordEditText.setSelection(word.length());
+        }
+        final String shortcut = args.getString(EXTRA_SHORTCUT);
+        if (null != shortcut && null != mShortcutEditText) {
+            mShortcutEditText.setText(shortcut);
+        }
+        mMode = args.getInt(EXTRA_MODE); // default return value for #getInt() is 0 = MODE_EDIT
+        mOldWord = args.getString(EXTRA_WORD);
+        mOldShortcut = args.getString(EXTRA_SHORTCUT);
+        updateLocale(args.getString(EXTRA_LOCALE));
+    }
+
+    // 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 (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;
+        }
+        // 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 (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'.
+        UserDictionary.Words.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;
+        // LocaleString may NOT be null.
+        public LocaleRenderer(final Context context, 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<LocaleRenderer>();
+        // 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;
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java
new file mode 100644
index 0000000..7970a36
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java
@@ -0,0 +1,153 @@
+/*
+ * 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 com.android.inputmethod.latin.userdictionary;
+
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.userdictionary.UserDictionaryAddWordContents.LocaleRenderer;
+import com.android.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_DELETE = Menu.FIRST;
+
+    private UserDictionaryAddWordContents mContents;
+    private View mRootView;
+    private boolean mIsDeleting = false;
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        setHasOptionsMenu(true);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+        mRootView = inflater.inflate(R.layout.user_dictionary_add_word_fullscreen, null);
+        mIsDeleting = false;
+        if (null == mContents) {
+            mContents = new UserDictionaryAddWordContents(mRootView, getArguments());
+        }
+        return mRootView;
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        MenuItem actionItem = menu.add(0, OPTIONS_MENU_DELETE, 0,
+                R.string.user_dict_settings_delete).setIcon(android.R.drawable.ic_menu_delete);
+        actionItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM |
+                MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+    }
+
+    /**
+     * Callback for the framework when a menu option is pressed.
+     *
+     * This class only supports the delete menu item.
+     * @param MenuItem 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_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<LocaleRenderer>(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/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java
new file mode 100644
index 0000000..2d147aa
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java
@@ -0,0 +1,116 @@
+/*
+ * 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 com.android.inputmethod.latin.userdictionary;
+
+import com.android.inputmethod.latin.LocaleUtils;
+import com.android.inputmethod.latin.R;
+
+import android.app.Activity;
+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 java.util.Locale;
+import java.util.TreeSet;
+
+// 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(Bundle icicle) {
+        super.onCreate(icicle);
+        setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getActivity()));
+    }
+
+    public static TreeSet<String> getUserDictionaryLocalesSet(Activity activity) {
+        @SuppressWarnings("deprecation")
+        final Cursor cursor = activity.managedQuery(UserDictionary.Words.CONTENT_URI,
+                new String[] { UserDictionary.Words.LOCALE },
+                null, null, null);
+        final TreeSet<String> localeList = new TreeSet<String>();
+        if (null == cursor) {
+            // The user dictionary service is not present or disabled. Return null.
+            return null;
+        } else if (cursor.moveToFirst()) {
+            final int columnIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE);
+            do {
+                String locale = cursor.getString(columnIndex);
+                localeList.add(null != locale ? locale : "");
+            } while (cursor.moveToNext());
+        }
+        localeList.add(Locale.getDefault().toString());
+        return localeList;
+    }
+
+    /**
+     * 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(PreferenceGroup userDictGroup) {
+        final Activity activity = getActivity();
+        userDictGroup.removeAll();
+        final TreeSet<String> localeList =
+                UserDictionaryList.getUserDictionaryLocalesSet(activity);
+
+        if (localeList.isEmpty()) {
+            userDictGroup.addPreference(createUserDictionaryPreference(null, activity));
+        } else {
+            for (String locale : localeList) {
+                userDictGroup.addPreference(createUserDictionaryPreference(locale, activity));
+            }
+        }
+    }
+
+    /**
+     * Create a single User Dictionary Preference object, with its parameters set.
+     * @param locale The locale for which this user dictionary is for.
+     * @return The corresponding preference.
+     */
+    protected Preference createUserDictionaryPreference(String locale, Activity activity) {
+        final Preference newPref = new Preference(getActivity());
+        final Intent intent = new Intent(USER_DICTIONARY_SETTINGS_INTENT_ACTION);
+        if (null == locale) {
+            newPref.setTitle(Locale.getDefault().getDisplayName());
+        } else {
+            if ("".equals(locale))
+                newPref.setTitle(getString(R.string.user_dict_settings_all_languages));
+            else
+                newPref.setTitle(LocaleUtils.constructLocaleFromString(locale).getDisplayName());
+            intent.putExtra("locale", locale);
+            newPref.getExtras().putString("locale", locale);
+        }
+        newPref.setIntent(intent);
+        newPref.setFragment(UserDictionarySettings.class.getName());
+        return newPref;
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        createUserDictSettings(getPreferenceScreen());
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryLocalePicker.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryLocalePicker.java
new file mode 100644
index 0000000..58d3fb9
--- /dev/null
+++ b/java/src/com/android/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 com.android.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/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java
new file mode 100644
index 0000000..a250c24
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java
@@ -0,0 +1,286 @@
+/**
+ * Copyright (C) 2013 Google Inc.
+ *
+ * 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.inputmethod.latin.userdictionary;
+
+import com.android.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.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 {
+
+    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";
+
+    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 int OPTIONS_MENU_ADD = Menu.FIRST;
+
+    private Cursor mCursor;
+
+    protected String mLocale;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+    }
+
+    @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;
+        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);
+
+    }
+
+    @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 + ")");
+        } 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 + ")");
+        }
+    }
+
+    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);
+    }
+
+    @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) {
+        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 (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 (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(View v, Cursor c, int columnIndex) {
+                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;
+            }
+        };
+
+        @SuppressWarnings("deprecation")
+        public MyAdapter(Context context, int layout, Cursor c, String[] from, int[] to,
+                UserDictionarySettings settings) {
+            super(context, layout, c, from, to);
+
+            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(int section) {
+            return null == mIndexer ? 0 : mIndexer.getPositionForSection(section);
+        }
+
+        @Override
+        public int getSectionForPosition(int position) {
+            return null == mIndexer ? 0 : mIndexer.getSectionForPosition(position);
+        }
+
+        @Override
+        public Object[] getSections() {
+            return null == mIndexer ? null : mIndexer.getSections();
+        }
+    }
+}