Merge "Small debug helper"
diff --git a/java/res/drawable-xxhdpi/btn_keyboard_key_dark_active_holo.9.png b/java/res/drawable-xxhdpi/btn_keyboard_key_dark_active_holo.9.png
new file mode 100644
index 0000000..680421e
--- /dev/null
+++ b/java/res/drawable-xxhdpi/btn_keyboard_key_dark_active_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/btn_keyboard_key_dark_normal_holo.9.png b/java/res/drawable-xxhdpi/btn_keyboard_key_dark_normal_holo.9.png
new file mode 100644
index 0000000..ae26750
--- /dev/null
+++ b/java/res/drawable-xxhdpi/btn_keyboard_key_dark_normal_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/btn_keyboard_key_dark_normal_off_holo.9.png b/java/res/drawable-xxhdpi/btn_keyboard_key_dark_normal_off_holo.9.png
new file mode 100644
index 0000000..c92a669
--- /dev/null
+++ b/java/res/drawable-xxhdpi/btn_keyboard_key_dark_normal_off_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/btn_keyboard_key_dark_normal_on_holo.9.png b/java/res/drawable-xxhdpi/btn_keyboard_key_dark_normal_on_holo.9.png
new file mode 100644
index 0000000..40f5011
--- /dev/null
+++ b/java/res/drawable-xxhdpi/btn_keyboard_key_dark_normal_on_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/btn_keyboard_key_dark_pressed_holo.9.png b/java/res/drawable-xxhdpi/btn_keyboard_key_dark_pressed_holo.9.png
new file mode 100644
index 0000000..6ff6319
--- /dev/null
+++ b/java/res/drawable-xxhdpi/btn_keyboard_key_dark_pressed_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/btn_keyboard_key_dark_pressed_off_holo.9.png b/java/res/drawable-xxhdpi/btn_keyboard_key_dark_pressed_off_holo.9.png
new file mode 100644
index 0000000..818ea70
--- /dev/null
+++ b/java/res/drawable-xxhdpi/btn_keyboard_key_dark_pressed_off_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/btn_keyboard_key_dark_pressed_on_holo.9.png b/java/res/drawable-xxhdpi/btn_keyboard_key_dark_pressed_on_holo.9.png
new file mode 100644
index 0000000..a476d2a
--- /dev/null
+++ b/java/res/drawable-xxhdpi/btn_keyboard_key_dark_pressed_on_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/btn_keyboard_key_light_normal_holo.9.png b/java/res/drawable-xxhdpi/btn_keyboard_key_light_normal_holo.9.png
new file mode 100644
index 0000000..9c280a6
--- /dev/null
+++ b/java/res/drawable-xxhdpi/btn_keyboard_key_light_normal_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/btn_keyboard_key_light_pressed_holo.9.png b/java/res/drawable-xxhdpi/btn_keyboard_key_light_pressed_holo.9.png
new file mode 100644
index 0000000..3c17c5e
--- /dev/null
+++ b/java/res/drawable-xxhdpi/btn_keyboard_key_light_pressed_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/btn_keyboard_key_popup_selected_holo.9.png b/java/res/drawable-xxhdpi/btn_keyboard_key_popup_selected_holo.9.png
new file mode 100644
index 0000000..6d2af59
--- /dev/null
+++ b/java/res/drawable-xxhdpi/btn_keyboard_key_popup_selected_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/ic_notify_dictionary.png b/java/res/drawable-xxhdpi/ic_notify_dictionary.png
new file mode 100644
index 0000000..b61d504
--- /dev/null
+++ b/java/res/drawable-xxhdpi/ic_notify_dictionary.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/ic_subtype_keyboard.png b/java/res/drawable-xxhdpi/ic_subtype_keyboard.png
new file mode 100644
index 0000000..0bb4283
--- /dev/null
+++ b/java/res/drawable-xxhdpi/ic_subtype_keyboard.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/keyboard_background_holo.9.png b/java/res/drawable-xxhdpi/keyboard_background_holo.9.png
new file mode 100644
index 0000000..bcef0f8
--- /dev/null
+++ b/java/res/drawable-xxhdpi/keyboard_background_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/keyboard_key_feedback_background_holo.9.png b/java/res/drawable-xxhdpi/keyboard_key_feedback_background_holo.9.png
new file mode 100644
index 0000000..bd1ef3c
--- /dev/null
+++ b/java/res/drawable-xxhdpi/keyboard_key_feedback_background_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/keyboard_key_feedback_left_background_holo.9.png b/java/res/drawable-xxhdpi/keyboard_key_feedback_left_background_holo.9.png
new file mode 100644
index 0000000..65af4b5
--- /dev/null
+++ b/java/res/drawable-xxhdpi/keyboard_key_feedback_left_background_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/keyboard_key_feedback_left_more_background_holo.9.png b/java/res/drawable-xxhdpi/keyboard_key_feedback_left_more_background_holo.9.png
new file mode 100644
index 0000000..ac6750d
--- /dev/null
+++ b/java/res/drawable-xxhdpi/keyboard_key_feedback_left_more_background_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/keyboard_key_feedback_more_background_holo.9.png b/java/res/drawable-xxhdpi/keyboard_key_feedback_more_background_holo.9.png
new file mode 100644
index 0000000..cea7c05
--- /dev/null
+++ b/java/res/drawable-xxhdpi/keyboard_key_feedback_more_background_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/keyboard_key_feedback_right_background_holo.9.png b/java/res/drawable-xxhdpi/keyboard_key_feedback_right_background_holo.9.png
new file mode 100644
index 0000000..520fa7c
--- /dev/null
+++ b/java/res/drawable-xxhdpi/keyboard_key_feedback_right_background_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/keyboard_key_feedback_right_more_background_holo.9.png b/java/res/drawable-xxhdpi/keyboard_key_feedback_right_more_background_holo.9.png
new file mode 100644
index 0000000..eee2217
--- /dev/null
+++ b/java/res/drawable-xxhdpi/keyboard_key_feedback_right_more_background_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/keyboard_popup_panel_background_holo.9.png b/java/res/drawable-xxhdpi/keyboard_popup_panel_background_holo.9.png
new file mode 100644
index 0000000..721c244
--- /dev/null
+++ b/java/res/drawable-xxhdpi/keyboard_popup_panel_background_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/keyboard_suggest_strip_holo.9.png b/java/res/drawable-xxhdpi/keyboard_suggest_strip_holo.9.png
new file mode 100644
index 0000000..08176fe
--- /dev/null
+++ b/java/res/drawable-xxhdpi/keyboard_suggest_strip_holo.9.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/suggestions_strip_divider.png b/java/res/drawable-xxhdpi/suggestions_strip_divider.png
new file mode 100644
index 0000000..d13ca42
--- /dev/null
+++ b/java/res/drawable-xxhdpi/suggestions_strip_divider.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/sym_keyboard_delete_holo.png b/java/res/drawable-xxhdpi/sym_keyboard_delete_holo.png
new file mode 100644
index 0000000..be3cb7c
--- /dev/null
+++ b/java/res/drawable-xxhdpi/sym_keyboard_delete_holo.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/sym_keyboard_language_switch.png b/java/res/drawable-xxhdpi/sym_keyboard_language_switch.png
new file mode 100644
index 0000000..7cd0684
--- /dev/null
+++ b/java/res/drawable-xxhdpi/sym_keyboard_language_switch.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/sym_keyboard_return_holo.png b/java/res/drawable-xxhdpi/sym_keyboard_return_holo.png
new file mode 100644
index 0000000..7d95807
--- /dev/null
+++ b/java/res/drawable-xxhdpi/sym_keyboard_return_holo.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/sym_keyboard_search_holo.png b/java/res/drawable-xxhdpi/sym_keyboard_search_holo.png
new file mode 100644
index 0000000..6b09d8e
--- /dev/null
+++ b/java/res/drawable-xxhdpi/sym_keyboard_search_holo.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/sym_keyboard_settings_holo.png b/java/res/drawable-xxhdpi/sym_keyboard_settings_holo.png
new file mode 100644
index 0000000..7041bb6
--- /dev/null
+++ b/java/res/drawable-xxhdpi/sym_keyboard_settings_holo.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/sym_keyboard_shift_holo.png b/java/res/drawable-xxhdpi/sym_keyboard_shift_holo.png
new file mode 100644
index 0000000..2b4fbbb
--- /dev/null
+++ b/java/res/drawable-xxhdpi/sym_keyboard_shift_holo.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/sym_keyboard_shift_locked_holo.png b/java/res/drawable-xxhdpi/sym_keyboard_shift_locked_holo.png
new file mode 100644
index 0000000..91c8603
--- /dev/null
+++ b/java/res/drawable-xxhdpi/sym_keyboard_shift_locked_holo.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/sym_keyboard_space_holo.png b/java/res/drawable-xxhdpi/sym_keyboard_space_holo.png
new file mode 100644
index 0000000..65aa5ea
--- /dev/null
+++ b/java/res/drawable-xxhdpi/sym_keyboard_space_holo.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/sym_keyboard_tab_holo.png b/java/res/drawable-xxhdpi/sym_keyboard_tab_holo.png
new file mode 100644
index 0000000..1f4ae3d
--- /dev/null
+++ b/java/res/drawable-xxhdpi/sym_keyboard_tab_holo.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/sym_keyboard_voice_holo.png b/java/res/drawable-xxhdpi/sym_keyboard_voice_holo.png
new file mode 100644
index 0000000..f04cadf
--- /dev/null
+++ b/java/res/drawable-xxhdpi/sym_keyboard_voice_holo.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/sym_keyboard_voice_off_holo.png b/java/res/drawable-xxhdpi/sym_keyboard_voice_off_holo.png
new file mode 100644
index 0000000..e74d523
--- /dev/null
+++ b/java/res/drawable-xxhdpi/sym_keyboard_voice_off_holo.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/sym_keyboard_zwj_holo.png b/java/res/drawable-xxhdpi/sym_keyboard_zwj_holo.png
new file mode 100644
index 0000000..85289b2
--- /dev/null
+++ b/java/res/drawable-xxhdpi/sym_keyboard_zwj_holo.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/sym_keyboard_zwnj_holo.png b/java/res/drawable-xxhdpi/sym_keyboard_zwnj_holo.png
new file mode 100644
index 0000000..e610678
--- /dev/null
+++ b/java/res/drawable-xxhdpi/sym_keyboard_zwnj_holo.png
Binary files differ
diff --git a/java/res/values-fr/donottranslate.xml b/java/res/values-fr/donottranslate.xml
index 2e916a7..f064411 100644
--- a/java/res/values-fr/donottranslate.xml
+++ b/java/res/values-fr/donottranslate.xml
@@ -19,9 +19,9 @@
 -->
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <!-- Symbols that are normally preceded by a space (used to add an auto-space before these) -->
-    <string name="symbols_preceded_by_space">([{*&amp;;:!?</string>
+    <string name="symbols_preceded_by_space">([{&amp;;:!?</string>
     <!-- Symbols that are normally followed by a space (used to add an auto-space after these) -->
-    <string name="symbols_followed_by_space">.,;:!?)]}*&amp;</string>
+    <string name="symbols_followed_by_space">.,;:!?)]}&amp;</string>
     <!-- Symbols that separate words -->
     <!-- Don't remove the enclosing double quotes, they protect whitespace (not just U+0020) -->
     <string name="symbols_word_separators">"&#x0009;&#x0020;\n"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"</string>
diff --git a/java/res/values/donottranslate.xml b/java/res/values/donottranslate.xml
index d2554ee..5e990ed 100644
--- a/java/res/values/donottranslate.xml
+++ b/java/res/values/donottranslate.xml
@@ -21,9 +21,9 @@
     <!-- Symbols that are suggested between words -->
     <string name="suggested_punctuations">!,?,\\,,:,;,\",(,),\',-,/,@,_</string>
     <!-- Symbols that are normally preceded by a space (used to add an auto-space before these) -->
-    <string name="symbols_preceded_by_space">([{*&amp;</string>
+    <string name="symbols_preceded_by_space">([{&amp;</string>
     <!-- Symbols that are normally followed by a space (used to add an auto-space after these) -->
-    <string name="symbols_followed_by_space">.,;:!?)]}*&amp;</string>
+    <string name="symbols_followed_by_space">.,;:!?)]}&amp;</string>
     <!-- Symbols that separate words -->
     <!-- Don't remove the enclosing double quotes, they protect whitespace (not just U+0020) -->
     <string name="symbols_word_separators">"&#x0009;&#x0020;\n"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"</string>
diff --git a/java/res/xml-sw600dp/keys_dvorak_123.xml b/java/res/xml-sw600dp/keys_dvorak_123.xml
index 851c14b..58416ab 100644
--- a/java/res/xml-sw600dp/keys_dvorak_123.xml
+++ b/java/res/xml-sw600dp/keys_dvorak_123.xml
@@ -53,8 +53,7 @@
                 latin:keyLabel="."
                 latin:keyHintLabel="3"
                 latin:additionalMoreKeys="3"
-                latin:keyLabelFlags="hasPopupHint|preserveCase"
-                latin:moreKeys="!text/more_keys_for_punctuation,%,&gt;" />
+                latin:moreKeys="&gt;" />
         </default>
     </switch>
 </merge>
diff --git a/java/res/xml/keys_dvorak_123.xml b/java/res/xml/keys_dvorak_123.xml
index 7124c89..fa94f1f 100644
--- a/java/res/xml/keys_dvorak_123.xml
+++ b/java/res/xml/keys_dvorak_123.xml
@@ -77,8 +77,7 @@
                 latin:keyLabel="."
                 latin:keyHintLabel="3"
                 latin:additionalMoreKeys="3"
-                latin:keyLabelFlags="hasPopupHint|preserveCase"
-                latin:moreKeys="!text/more_keys_for_punctuation,%,&gt;" />
+                latin:moreKeys="&gt;" />
         </default>
     </switch>
 </merge>
diff --git a/java/res/xml/row_dvorak4.xml b/java/res/xml/row_dvorak4.xml
index 69bac13..e6d487e 100644
--- a/java/res/xml/row_dvorak4.xml
+++ b/java/res/xml/row_dvorak4.xml
@@ -68,7 +68,8 @@
             latin:keyboardLayout="@xml/key_space" />
         <Key
             latin:keyLabel="z"
-            latin:moreKeys="!text/more_keys_for_z" />
+            latin:keyLabelFlags="hasPopupHint"
+            latin:moreKeys="!text/more_keys_for_punctuation,!text/more_keys_for_z" />
         <Key
             latin:keyStyle="enterKeyStyle"
             latin:keyWidth="fillRight" />
diff --git a/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java b/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
index c5aca17..6d6c8f5 100644
--- a/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
+++ b/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
@@ -47,6 +47,7 @@
     private Button mInstallButton;
     private Button mCancelButton;
     private Button mDeleteButton;
+    private DictionaryListInterfaceState mInterfaceState;
     private OnClickListener mOnClickListener;
 
     public ButtonSwitcher(Context context, AttributeSet attrs) {
@@ -57,9 +58,10 @@
         super(context, attrs, defStyle);
     }
 
-    public void reset() {
+    public void reset(final DictionaryListInterfaceState interfaceState) {
         mStatus = NOT_INITIALIZED;
         mAnimateToStatus = NOT_INITIALIZED;
+        mInterfaceState = interfaceState;
     }
 
     @Override
@@ -153,6 +155,7 @@
     private ViewPropertyAnimator animateButton(final View button, final int direction) {
         final float outerX = getWidth();
         final float innerX = button.getX() - button.getTranslationX();
+        mInterfaceState.removeFromCache((View)getParent());
         if (ANIMATION_IN == direction) {
             button.setClickable(true);
             return button.animate().translationX(0);
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java
index f1a2a83..13c07de 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java
@@ -80,4 +80,8 @@
         mViewCache.add(view);
         return view;
     }
+
+    public void removeFromCache(final View view) {
+        mViewCache.remove(view);
+    }
 }
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java
index 767f895..939c25f 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java
@@ -22,13 +22,14 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.IBinder;
-import android.util.Log;
 import android.widget.Toast;
 
 import com.android.inputmethod.latin.R;
 
 import java.util.Locale;
 import java.util.Random;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -92,21 +93,27 @@
     private static final long VERY_LONG_TIME = TimeUnit.DAYS.toMillis(14);
 
     /**
-     * The last seen start Id. This must be stored because we must only call stopSelfResult() with
-     * the last seen Id, or the service won't stop.
+     * An executor that serializes tasks given to it.
      */
-    private int mLastSeenStartId;
-
-    /**
-     * The command count. We need this because we need to not call stopSelfResult() while we still
-     * have commands running.
-     */
-    private int mCommandCount;
+    private ThreadPoolExecutor mExecutor;
+    private static final int WORKER_THREAD_TIMEOUT_SECONDS = 15;
 
     @Override
     public void onCreate() {
-        mLastSeenStartId = 0;
-        mCommandCount = 0;
+        // By default, a thread pool executor does not timeout its core threads, so it will
+        // never kill them when there isn't any work to do any more. That would mean the service
+        // can never die! By creating it this way and calling allowCoreThreadTimeOut, we allow
+        // the single thread to time out after WORKER_THREAD_TIMEOUT_SECONDS = 15 seconds, allowing
+        // the process to be reclaimed by the system any time after that if it's not doing
+        // anything else.
+        // Executors#newSingleThreadExecutor creates a ThreadPoolExecutor but it returns the
+        // superclass ExecutorService which does not have the #allowCoreThreadTimeOut method,
+        // so we can't use that.
+        mExecutor = new ThreadPoolExecutor(1 /* corePoolSize */, 1 /* maximumPoolSize */,
+                WORKER_THREAD_TIMEOUT_SECONDS /* keepAliveTime */,
+                TimeUnit.SECONDS /* unit for keepAliveTime */,
+                new LinkedBlockingQueue<Runnable>() /* workQueue */);
+        mExecutor.allowCoreThreadTimeOut(true);
     }
 
     @Override
@@ -131,33 +138,35 @@
      * - Handle a finished download.
      *     This executes the actions that must be taken after a file (metadata or dictionary data
      *     has been downloaded (or failed to download).
+     * The commands that can be spun an another thread will be executed serially, in order, on
+     * a worker thread that is created on demand and terminates after a short while if there isn't
+     * any work left to do.
      */
     @Override
     public synchronized int onStartCommand(final Intent intent, final int flags,
             final int startId) {
         final DictionaryService self = this;
-        mLastSeenStartId = startId;
-        mCommandCount += 1;
         if (SHOW_DOWNLOAD_TOAST_INTENT_ACTION.equals(intent.getAction())) {
             // This is a UI action, it can't be run in another thread
             showStartDownloadingToast(this, LocaleUtils.constructLocaleFromString(
                     intent.getStringExtra(LOCALE_INTENT_ARGUMENT)));
         } else {
-            // If it's a command that does not require UI, create a thread to do the work
-            // and return right away. DATE_CHANGED or UPDATE_NOW are examples of such commands.
-            new Thread("updateOrFinishDownload") {
+            // If it's a command that does not require UI, arrange for the work to be done on a
+            // separate thread, so that we can return right away. The executor will spawn a thread
+            // if necessary, or reuse a thread that has become idle as appropriate.
+            // DATE_CHANGED or UPDATE_NOW are examples of commands that can be done on another
+            // thread.
+            mExecutor.submit(new Runnable() {
                 @Override
                 public void run() {
                     dispatchBroadcast(self, intent);
-                    synchronized(self) {
-                        if (--mCommandCount <= 0) {
-                            if (!stopSelfResult(mLastSeenStartId)) {
-                                Log.e(TAG, "Can't stop ourselves");
-                            }
-                        }
-                    }
+                    // Since calls to onStartCommand are serialized, the submissions to the executor
+                    // are serialized. That means we are guaranteed to call the stopSelfResult()
+                    // in the same order that we got them, so we don't need to take care of the
+                    // order.
+                    stopSelfResult(startId);
                 }
-            }.start();
+            });
         }
         return Service.START_REDELIVER_INTENT;
     }
diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
index 99cc5b9..ff5aba6 100644
--- a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
+++ b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
@@ -199,6 +199,7 @@
             final ContentValues defaultMetadataValues = new ContentValues();
             defaultMetadataValues.put(CLIENT_CLIENT_ID_COLUMN, "");
             defaultMetadataValues.put(CLIENT_METADATA_URI_COLUMN, defaultMetadataUri);
+            defaultMetadataValues.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID);
             db.insert(CLIENT_TABLE_NAME, null, defaultMetadataValues);
         }
     }
@@ -358,21 +359,21 @@
     }
 
     /**
-     * Get the metadata download ID for a client ID.
+     * Get the metadata download ID for a metadata URI.
      *
-     * This will retrieve the download ID for the metadata file associated with a client ID.
-     * If there is no metadata download in progress for this client, it will return NOT_AN_ID.
+     * This will retrieve the download ID for the metadata file that has the passed URI.
+     * If this URI is not being downloaded right now, it will return NOT_AN_ID.
      *
      * @param context a context instance to open the database on
-     * @param clientId the client ID to retrieve the metadata download ID of
+     * @param uri the URI to retrieve the metadata download ID of
      * @return the metadata download ID, or NOT_AN_ID if no download is in progress
      */
-    public static long getMetadataDownloadIdForClient(final Context context,
-            final String clientId) {
+    public static long getMetadataDownloadIdForURI(final Context context,
+            final String uri) {
         SQLiteDatabase defaultDb = getDb(context, null);
         final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME,
                 new String[] { CLIENT_PENDINGID_COLUMN },
-                CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId },
+                CLIENT_METADATA_URI_COLUMN + " = ?", new String[] { uri },
                 null, null, null, null);
         try {
             if (!cursor.moveToFirst()) return UpdateHandler.NOT_AN_ID;
@@ -782,6 +783,8 @@
                     " but the values " + "contain a different ID : ", valuesClientId);
             return;
         }
+        // Default value for a pending ID is NOT_AN_ID
+        values.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID);
         final SQLiteDatabase defaultDb = getDb(context, "");
         if (-1 == defaultDb.insert(CLIENT_TABLE_NAME, null, values)) {
             defaultDb.update(CLIENT_TABLE_NAME, values,
diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
index f66ef87..0e7c3bb 100644
--- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
+++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
@@ -272,23 +272,22 @@
     }
 
     /**
-     * Cancels a pending update, if there is one.
+     * Cancels downloading a file, if there is one for this URI.
      *
-     * If none, this is a no-op.
+     * If we are not currently downloading the file at this URI, this is a no-op.
      *
      * @param context the context to open the database on
-     * @param clientId the id of the client
+     * @param metadataUri the URI to cancel
      * @param manager an instance of DownloadManager
      */
     private static void cancelUpdateWithDownloadManager(final Context context,
-            final String clientId, final DownloadManager manager) {
+            final String metadataUri, final DownloadManager manager) {
         synchronized (sSharedIdProtector) {
             final long metadataDownloadId =
-                    MetadataDbHelper.getMetadataDownloadIdForClient(context, clientId);
+                    MetadataDbHelper.getMetadataDownloadIdForURI(context, metadataUri);
             if (NOT_AN_ID == metadataDownloadId) return;
             manager.remove(metadataDownloadId);
-            writeMetadataDownloadId(context,
-                    MetadataDbHelper.getMetadataUriAsString(context, clientId), NOT_AN_ID);
+            writeMetadataDownloadId(context, metadataUri, NOT_AN_ID);
         }
         // Consider a cancellation as a failure. As such, inform listeners that the download
         // has failed.
@@ -298,10 +297,10 @@
     }
 
     /**
-     * Cancels a pending update, if there is one.
+     * Cancels a pending update for this client, if there is one.
      *
-     * If there is none, this is a no-op. This is a helper method that gets the
-     * download manager service.
+     * If we are not currently updating metadata for this client, this is a no-op. This is a helper
+     * method that gets the download manager service and the metadata URI for this client.
      *
      * @param context the context, to get an instance of DownloadManager
      * @param clientId the ID of the client we want to cancel the update of
@@ -309,7 +308,8 @@
     public static void cancelUpdate(final Context context, final String clientId) {
         final DownloadManager manager =
                     (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
-        if (null != manager) cancelUpdateWithDownloadManager(context, clientId, manager);
+        final String metadataUri = MetadataDbHelper.getMetadataUriAsString(context, clientId);
+        if (null != manager) cancelUpdateWithDownloadManager(context, metadataUri, manager);
     }
 
     /**
@@ -773,7 +773,7 @@
                     // We may come here if there is a new word list that we can't handle.
                     Log.i(TAG, "Can't handle word list with id '" + id + "' because it has format"
                             + " version " + metadataInfo.mFormatVersion + " and the maximum version"
-                            + "we can handle is " + MAXIMUM_SUPPORTED_FORMAT_VERSION);
+                            + " we can handle is " + MAXIMUM_SUPPORTED_FORMAT_VERSION);
                 }
                 continue;
             } else if (null == currentInfo) {
diff --git a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
index 7ec7e9c..ba1fce1 100644
--- a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
+++ b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
@@ -224,7 +224,7 @@
                 (ButtonSwitcher)view.findViewById(R.id.wordlist_button_switcher);
         // We need to clear the state of the button switcher, because we reuse views; if we didn't
         // reset it would animate from whatever its old state was.
-        buttonSwitcher.reset();
+        buttonSwitcher.reset(mInterfaceState);
         if (mInterfaceState.isOpen(mWordlistId)) {
             // The button is open.
             final int previousStatus = mInterfaceState.getStatus(mWordlistId);
diff --git a/native/jni/src/defines.h b/native/jni/src/defines.h
index 607a744..974bb48 100644
--- a/native/jni/src/defines.h
+++ b/native/jni/src/defines.h
@@ -292,7 +292,7 @@
 // of the binary dictionary where a {key,value} string pair scheme is used.
 #define LARGEST_INT_DIGIT_COUNT 11
 
-#define NOT_VALID_WORD (-99)
+#define NOT_A_VALID_WORD_POS (-99)
 #define NOT_A_CODE_POINT (-1)
 #define NOT_A_DISTANCE (-1)
 #define NOT_A_COORDINATE (-1)
diff --git a/native/jni/src/suggest/core/dicnode/dic_node.h b/native/jni/src/suggest/core/dicnode/dic_node.h
index 973da67..696be0a 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node.h
@@ -112,7 +112,7 @@
         mIsUsed = true;
         mIsCachedForNextSuggestion = false;
         mDicNodeProperties.init(
-                NOT_A_DICT_POS, rootGroupPos, NOT_A_DICT_POS /* attributesPos */,
+                NOT_A_VALID_WORD_POS /* pos */, rootGroupPos, NOT_A_DICT_POS /* attributesPos */,
                 NOT_A_CODE_POINT /* nodeCodePoint */, NOT_A_PROBABILITY /* probability */,
                 false /* isTerminal */, true /* hasChildren */,
                 false /* isBlacklistedOrNotAWord */, 0 /* depth */, 0 /* terminalDepth */);
@@ -125,7 +125,7 @@
         mIsUsed = true;
         mIsCachedForNextSuggestion = dicNode->mIsCachedForNextSuggestion;
         mDicNodeProperties.init(
-                NOT_A_DICT_POS, rootGroupPos, NOT_A_DICT_POS /* attributesPos */,
+                NOT_A_VALID_WORD_POS /* pos */, rootGroupPos, NOT_A_DICT_POS /* attributesPos */,
                 NOT_A_CODE_POINT /* nodeCodePoint */, NOT_A_PROBABILITY /* probability */,
                 false /* isTerminal */, true /* hasChildren */,
                 false /* isBlacklistedOrNotAWord */,  0 /* depth */, 0 /* terminalDepth */);
@@ -231,7 +231,7 @@
     }
 
     bool isFirstWord() const {
-        return mDicNodeState.mDicNodeStatePrevWord.getPrevWordNodePos() == NOT_VALID_WORD;
+        return mDicNodeState.mDicNodeStatePrevWord.getPrevWordNodePos() == NOT_A_VALID_WORD_POS;
     }
 
     bool isCompletion(const int inputSize) const {
diff --git a/native/jni/src/suggest/core/dicnode/dic_node_state_prevword.h b/native/jni/src/suggest/core/dicnode/dic_node_state_prevword.h
index c3968c0..5854f4f 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node_state_prevword.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node_state_prevword.h
@@ -29,7 +29,7 @@
  public:
     AK_FORCE_INLINE DicNodeStatePrevWord()
             : mPrevWordCount(0), mPrevWordLength(0), mPrevWordStart(0), mPrevWordProbability(0),
-              mPrevWordNodePos(0) {
+              mPrevWordNodePos(NOT_A_VALID_WORD_POS) {
         memset(mPrevWord, 0, sizeof(mPrevWord));
         memset(mPrevSpacePositions, 0, sizeof(mPrevSpacePositions));
     }
@@ -41,7 +41,7 @@
         mPrevWordCount = 0;
         mPrevWordStart = 0;
         mPrevWordProbability = -1;
-        mPrevWordNodePos = NOT_VALID_WORD;
+        mPrevWordNodePos = NOT_A_VALID_WORD_POS;
         memset(mPrevSpacePositions, 0, sizeof(mPrevSpacePositions));
     }
 
diff --git a/native/jni/src/suggest/core/dicnode/dic_node_utils.cpp b/native/jni/src/suggest/core/dicnode/dic_node_utils.cpp
index 6c7f666..67fbc1a 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node_utils.cpp
+++ b/native/jni/src/suggest/core/dicnode/dic_node_utils.cpp
@@ -179,8 +179,9 @@
     const int unigramProbability = node->getProbability();
     const int wordPos = node->getPos();
     const int prevWordPos = node->getPrevWordPos();
-    if (NOT_VALID_WORD == wordPos || NOT_VALID_WORD == prevWordPos) {
-        // Note: Normally wordPos comes from the dictionary and should never equal NOT_VALID_WORD.
+    if (NOT_A_VALID_WORD_POS == wordPos || NOT_A_VALID_WORD_POS == prevWordPos) {
+        // Note: Normally wordPos comes from the dictionary and should never equal
+        // NOT_A_VALID_WORD_POS.
         return ProbabilityUtils::backoff(unigramProbability);
     }
     if (multiBigramMap) {
diff --git a/native/jni/src/suggest/core/dictionary/bigram_dictionary.cpp b/native/jni/src/suggest/core/dictionary/bigram_dictionary.cpp
index 7088009..7484302 100644
--- a/native/jni/src/suggest/core/dictionary/bigram_dictionary.cpp
+++ b/native/jni/src/suggest/core/dictionary/bigram_dictionary.cpp
@@ -153,7 +153,7 @@
     if (0 >= prevWordLength) return 0;
     int pos = mBinaryDictionaryInfo->getStructurePolicy()->getTerminalNodePositionOfWord(
             mBinaryDictionaryInfo, prevWord, prevWordLength, forceLowerCaseSearch);
-    if (NOT_VALID_WORD == pos) return 0;
+    if (NOT_A_VALID_WORD_POS == pos) return 0;
     return BinaryFormat::getBigramListPositionForWordPosition(
             mBinaryDictionaryInfo->getDictRoot(), pos);
 }
@@ -181,7 +181,7 @@
     if (0 == pos) return false;
     int nextWordPos = mBinaryDictionaryInfo->getStructurePolicy()->getTerminalNodePositionOfWord(
             mBinaryDictionaryInfo, word1, length1, false /* forceLowerCaseSearch */);
-    if (NOT_VALID_WORD == nextWordPos) return false;
+    if (NOT_A_VALID_WORD_POS == nextWordPos) return false;
 
     for (BinaryDictionaryBigramsIterator bigramsIt(mBinaryDictionaryInfo, pos);
             bigramsIt.hasNext(); /* no-op */) {
diff --git a/native/jni/src/suggest/core/dictionary/binary_format.h b/native/jni/src/suggest/core/dictionary/binary_format.h
index d3d597b..6a5afd1 100644
--- a/native/jni/src/suggest/core/dictionary/binary_format.h
+++ b/native/jni/src/suggest/core/dictionary/binary_format.h
@@ -255,7 +255,7 @@
 }
 
 // This function gets the byte position of the last chargroup of the exact matching word in the
-// dictionary. If no match is found, it returns NOT_VALID_WORD.
+// dictionary. If no match is found, it returns NOT_A_VALID_WORD_POS.
 AK_FORCE_INLINE int BinaryFormat::getTerminalPosition(const uint8_t *const root,
         const int *const inWord, const int length, const bool forceLowerCaseSearch) {
     int pos = 0;
@@ -264,22 +264,22 @@
     while (true) {
         // If we already traversed the tree further than the word is long, there means
         // there was no match (or we would have found it).
-        if (wordPos >= length) return NOT_VALID_WORD;
+        if (wordPos >= length) return NOT_A_VALID_WORD_POS;
         int charGroupCount = BinaryFormat::getGroupCountAndForwardPointer(root, &pos);
         const int wChar = forceLowerCaseSearch
                 ? CharUtils::toLowerCase(inWord[wordPos]) : inWord[wordPos];
         while (true) {
             // If there are no more character groups in this node, it means we could not
             // find a matching character for this depth, therefore there is no match.
-            if (0 >= charGroupCount) return NOT_VALID_WORD;
+            if (0 >= charGroupCount) return NOT_A_VALID_WORD_POS;
             const int charGroupPos = pos;
             const uint8_t flags = BinaryFormat::getFlagsAndForwardPointer(root, &pos);
             int character = BinaryFormat::getCodePointAndForwardPointer(root, &pos);
             if (character == wChar) {
                 // This is the correct node. Only one character group may start with the same
                 // char within a node, so either we found our match in this node, or there is
-                // no match and we can return NOT_VALID_WORD. So we will check all the characters
-                // in this character group indeed does match.
+                // no match and we can return NOT_A_VALID_WORD_POS. So we will check all the
+                // characters in this character group indeed does match.
                 if (FLAG_HAS_MULTIPLE_CHARS & flags) {
                     character = BinaryFormat::getCodePointAndForwardPointer(root, &pos);
                     while (NOT_A_CODE_POINT != character) {
@@ -288,8 +288,8 @@
                         // character that does not match, as explained above, it means the word is
                         // not in the dictionary (by virtue of this chargroup being the only one to
                         // match the word on the first character, but not matching the whole word).
-                        if (wordPos >= length) return NOT_VALID_WORD;
-                        if (inWord[wordPos] != character) return NOT_VALID_WORD;
+                        if (wordPos >= length) return NOT_A_VALID_WORD_POS;
+                        if (inWord[wordPos] != character) return NOT_A_VALID_WORD_POS;
                         character = BinaryFormat::getCodePointAndForwardPointer(root, &pos);
                     }
                 }
@@ -305,7 +305,7 @@
                     pos = BinaryFormat::skipProbability(FLAG_IS_TERMINAL, pos);
                 }
                 if (FLAG_GROUP_ADDRESS_TYPE_NOADDRESS == (MASK_GROUP_ADDRESS_TYPE & flags)) {
-                    return NOT_VALID_WORD;
+                    return NOT_A_VALID_WORD_POS;
                 }
                 // We have children and we are still shorter than the word we are searching for, so
                 // we need to traverse children. Put the pointer on the children position, and
@@ -474,7 +474,7 @@
 
 AK_FORCE_INLINE int BinaryFormat::getBigramListPositionForWordPosition(
         const uint8_t *const root, int position) {
-    if (NOT_VALID_WORD == position) return 0;
+    if (NOT_A_VALID_WORD_POS == position) return 0;
     const uint8_t flags = getFlagsAndForwardPointer(root, &position);
     if (!(flags & FLAG_HAS_BIGRAMS)) return 0;
     if (flags & FLAG_HAS_MULTIPLE_CHARS) {
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.cpp b/native/jni/src/suggest/core/dictionary/dictionary.cpp
index f520a75..52e6359 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.cpp
+++ b/native/jni/src/suggest/core/dictionary/dictionary.cpp
@@ -89,7 +89,7 @@
             mBinaryDictionaryInfo.getStructurePolicy();
     int pos = structurePolicy->getTerminalNodePositionOfWord(&mBinaryDictionaryInfo, word, length,
             false /* forceLowerCaseSearch */);
-    if (NOT_VALID_WORD == pos) {
+    if (NOT_A_VALID_WORD_POS == pos) {
         return NOT_A_PROBABILITY;
     }
     return structurePolicy->getUnigramProbability(&mBinaryDictionaryInfo, pos);
diff --git a/native/jni/src/suggest/core/session/dic_traverse_session.cpp b/native/jni/src/suggest/core/session/dic_traverse_session.cpp
index 71d3698..7651b19 100644
--- a/native/jni/src/suggest/core/session/dic_traverse_session.cpp
+++ b/native/jni/src/suggest/core/session/dic_traverse_session.cpp
@@ -32,13 +32,13 @@
     mMultiWordCostMultiplier = binaryDictionaryInfo->getHeader()->getMultiWordCostMultiplier();
     mSuggestOptions = suggestOptions;
     if (!prevWord) {
-        mPrevWordPos = NOT_VALID_WORD;
+        mPrevWordPos = NOT_A_VALID_WORD_POS;
         return;
     }
     // TODO: merge following similar calls to getTerminalPosition into one case-insensitive call.
     mPrevWordPos = binaryDictionaryInfo->getStructurePolicy()->getTerminalNodePositionOfWord(
             binaryDictionaryInfo, prevWord, prevWordLength, false /* forceLowerCaseSearch */);
-    if (mPrevWordPos == NOT_VALID_WORD) {
+    if (mPrevWordPos == NOT_A_VALID_WORD_POS) {
         // Check bigrams for lower-cased previous word if original was not found. Useful for
         // auto-capitalized words like "The [current_word]".
         mPrevWordPos = binaryDictionaryInfo->getStructurePolicy()->getTerminalNodePositionOfWord(
diff --git a/native/jni/src/suggest/core/session/dic_traverse_session.h b/native/jni/src/suggest/core/session/dic_traverse_session.h
index f95a0b2..de57e04 100644
--- a/native/jni/src/suggest/core/session/dic_traverse_session.h
+++ b/native/jni/src/suggest/core/session/dic_traverse_session.h
@@ -55,7 +55,7 @@
     }
 
     AK_FORCE_INLINE DicTraverseSession(JNIEnv *env, jstring localeStr)
-            : mPrevWordPos(NOT_VALID_WORD), mProximityInfo(0),
+            : mPrevWordPos(NOT_A_VALID_WORD_POS), mProximityInfo(0),
               mDictionary(0), mSuggestOptions(0), mDicNodesCache(), mMultiBigramMap(),
               mInputSize(0), mPartiallyCommited(false), mMaxPointerCount(1),
               mMultiWordCostMultiplier(1.0f) {
diff --git a/tests/src/com/android/inputmethod/latin/UserHistoryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/UserHistoryDictionaryTests.java
index 93befd7..e0a0863 100644
--- a/tests/src/com/android/inputmethod/latin/UserHistoryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/UserHistoryDictionaryTests.java
@@ -82,7 +82,7 @@
         File dictFile = null;
         try {
             Log.d(TAG, "This test can be used for profiling.");
-            Log.d(TAG, "Usage: please set UserHisotoryDictionary.PROFILE_SAVE_RESTORE to true.");
+            Log.d(TAG, "Usage: please set UserHistoryDictionary.PROFILE_SAVE_RESTORE to true.");
             final int numberOfWords = 1000;
             final Random random = new Random(123456);
             List<String> words = generateWords(numberOfWords, random);
@@ -118,4 +118,54 @@
             }
         }
     }
+
+    public void testStressTestForSwitchingLanguagesAndAddingWords() {
+        final int numberOfLanguages = 2;
+        final int numberOfLanguageSwitching = 100;
+        final int numberOfWordsIntertedForEachLanguageSwitch = 100;
+
+        final File dictFiles[] = new File[numberOfLanguages];
+        try {
+            final Random random = new Random(123456);
+
+            // Create locales for this test.
+            String locales[] = new String[numberOfLanguages];
+            for (int i = 0; i < numberOfLanguages; i++) {
+                locales[i] = "testSwitchingLanguages" + i;
+                final String fileName = "UserHistoryDictionary." + locales[i] + ".dict";
+                dictFiles[i] = new File(getContext().getFilesDir(), fileName);
+            }
+
+            final long now = System.currentTimeMillis();
+
+            for (int i = 0; i < numberOfLanguageSwitching; i++) {
+                final int index = i % numberOfLanguages;
+                // Switch languages to locales[index].
+                final UserHistoryDictionary dict = UserHistoryDictionary.getInstance(getContext(),
+                        locales[index], mPrefs);
+                final List<String> words = generateWords(
+                        numberOfWordsIntertedForEachLanguageSwitch, random);
+                // Add random words to the user history dictionary.
+                addToDict(dict, words);
+                // write to file
+                dict.close();
+            }
+
+            final long end = System.currentTimeMillis();
+            Log.d(TAG, "testStressTestForSwitchingLanguageAndAddingWords took "
+                    + (end - now) + " ms");
+            try {
+                Log.d(TAG, "waiting for writing ...");
+                Thread.sleep(5000);
+            } catch (InterruptedException e) {
+                Log.d(TAG, "InterruptedException: " + e);
+            }
+        } finally {
+            for (final File file : dictFiles) {
+                if (file != null) {
+                    file.delete();
+                }
+            }
+        }
+    }
 }