Merge "Fix exact match checking for words with digraph."
diff --git a/java/res/layout/dictionary_line.xml b/java/res/layout/dictionary_line.xml
index 9eeea69..9ddbcf7 100644
--- a/java/res/layout/dictionary_line.xml
+++ b/java/res/layout/dictionary_line.xml
@@ -84,5 +84,21 @@
         android:singleLine="true"
         android:textAppearance="?android:attr/textAppearanceMedium"
         android:text="@string/install_dict" />
+    <Button
+        android:id="@+android:id/dict_cancel_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="right|center_vertical"
+        android:singleLine="true"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:text="@string/cancel_download_dict" />
+    <Button
+        android:id="@+android:id/dict_delete_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="right|center_vertical"
+        android:singleLine="true"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:text="@string/delete_dict" />
   </com.android.inputmethod.dictionarypack.ButtonSwitcher>
 </LinearLayout>
diff --git a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
index faf5d3c..a9d7992 100644
--- a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
+++ b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
@@ -138,7 +138,12 @@
             if (null == manager) return;
 
             // This is an upgraded word list: we should download it.
-            final Uri uri = Uri.parse(mWordList.mRemoteFilename);
+            // Adding a disambiguator to circumvent a bug in older versions of DownloadManager.
+            // DownloadManager also stupidly cuts the extension to replace with its own that it
+            // gets from the content-type. We need to circumvent this.
+            final String disambiguator = "#" + System.currentTimeMillis()
+                    + com.android.inputmethod.latin.Utils.getVersionName(context) + ".dict";
+            final Uri uri = Uri.parse(mWordList.mRemoteFilename + disambiguator);
             final Request request = new Request(uri);
 
             final Resources res = context.getResources();
diff --git a/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java b/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
index a062298..196c211 100644
--- a/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
+++ b/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
@@ -28,10 +28,24 @@
  * A view that handles buttons inside it according to a status.
  */
 public class ButtonSwitcher extends FrameLayout {
+    public static final int NOT_INITIALIZED = -1;
+    public static final int STATUS_NO_BUTTON = 0;
+    public static final int STATUS_INSTALL = 1;
+    public static final int STATUS_CANCEL = 2;
+    public static final int STATUS_DELETE = 3;
+    // One of the above
+    private int mStatus = NOT_INITIALIZED;
+    private int mAnimateToStatus = NOT_INITIALIZED;
+
     // Animation directions
     public static final int ANIMATION_IN = 1;
     public static final int ANIMATION_OUT = 2;
 
+    private Button mInstallButton;
+    private Button mCancelButton;
+    private Button mDeleteButton;
+    private OnClickListener mOnClickListener;
+
     public ButtonSwitcher(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
@@ -40,26 +54,79 @@
         super(context, attrs, defStyle);
     }
 
-    public void setText(final CharSequence text) {
-        ((Button)findViewById(R.id.dict_install_button)).setText(text);
+    @Override
+    protected void onLayout(final boolean changed, final int left, final int top, final int right,
+            final int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        mInstallButton = (Button)findViewById(R.id.dict_install_button);
+        mCancelButton = (Button)findViewById(R.id.dict_cancel_button);
+        mDeleteButton = (Button)findViewById(R.id.dict_delete_button);
+        mInstallButton.setOnClickListener(mOnClickListener);
+        mCancelButton.setOnClickListener(mOnClickListener);
+        mDeleteButton.setOnClickListener(mOnClickListener);
+        setButtonPositionWithoutAnimation(mStatus);
+        if (mAnimateToStatus != NOT_INITIALIZED) {
+            // We have been asked to animate before we were ready, so we took a note of it.
+            // We are now ready: launch the animation.
+            animateButtonPosition(mStatus, mAnimateToStatus);
+            mStatus = mAnimateToStatus;
+            mAnimateToStatus = NOT_INITIALIZED;
+        }
     }
 
-    public void setInternalButtonVisiblility(final int visibility) {
-        findViewById(R.id.dict_install_button).setVisibility(visibility);
+    private Button getButton(final int status) {
+        switch(status) {
+        case STATUS_INSTALL:
+            return mInstallButton;
+        case STATUS_CANCEL:
+            return mCancelButton;
+        case STATUS_DELETE:
+            return mDeleteButton;
+        default:
+            return null;
+        }
+    }
+
+    public void setStatusAndUpdateVisuals(final int status) {
+        if (mStatus == NOT_INITIALIZED) {
+            setButtonPositionWithoutAnimation(status);
+            mStatus = status;
+        } else {
+            if (null == mInstallButton) {
+                // We may come here before we have been layout. In this case we don't know our
+                // size yet so we can't start animations so we need to remember what animation to
+                // start once layout has gone through.
+                mAnimateToStatus = status;
+            } else {
+                animateButtonPosition(mStatus, status);
+                mStatus = status;
+            }
+        }
+    }
+
+    private void setButtonPositionWithoutAnimation(final int status) {
+        // This may be called by setStatus() before the layout has come yet.
+        if (null == mInstallButton) return;
+        final int width = getWidth();
+        // Set to out of the screen if that's not the currently displayed status
+        mInstallButton.setTranslationX(STATUS_INSTALL == status ? 0 : width);
+        mCancelButton.setTranslationX(STATUS_CANCEL == status ? 0 : width);
+        mDeleteButton.setTranslationX(STATUS_DELETE == status ? 0 : width);
+    }
+
+    private void animateButtonPosition(final int oldStatus, final int newStatus) {
+        animateButton(getButton(oldStatus), ANIMATION_OUT);
+        animateButton(getButton(newStatus), ANIMATION_IN);
     }
 
     public void setInternalOnClickListener(final OnClickListener listener) {
-        findViewById(R.id.dict_install_button).setOnClickListener(listener);
+        mOnClickListener = listener;
     }
 
-    public void animateButton(final int direction) {
-        final View button = findViewById(R.id.dict_install_button);
+    private void animateButton(final View button, final int direction) {
+        if (null == button) return;
         final float outerX = getWidth();
         final float innerX = button.getX() - button.getTranslationX();
-        if (View.INVISIBLE == button.getVisibility()) {
-            button.setTranslationX(outerX - innerX);
-            button.setVisibility(View.VISIBLE);
-        }
         if (ANIMATION_IN == direction) {
             button.animate().translationX(0);
         } else {
diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
index a596609..3f917f1 100644
--- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
+++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
@@ -212,7 +212,12 @@
     private static void updateClientsWithMetadataUri(final Context context,
             final boolean updateNow, final String metadataUri) {
         PrivateLog.log("Update for metadata URI " + Utils.s(metadataUri));
-        final Request metadataRequest = new Request(Uri.parse(metadataUri));
+        // Adding a disambiguator to circumvent a bug in older versions of DownloadManager.
+        // DownloadManager also stupidly cuts the extension to replace with its own that it
+        // gets from the content-type. We need to circumvent this.
+        final String disambiguator = "#" + System.currentTimeMillis()
+                + com.android.inputmethod.latin.Utils.getVersionName(context) + ".json";
+        final Request metadataRequest = new Request(Uri.parse(metadataUri + disambiguator));
         Utils.l("Request =", metadataRequest);
 
         final Resources res = context.getResources();
@@ -351,7 +356,13 @@
                 final int columnUri = cursor.getColumnIndex(DownloadManager.COLUMN_URI);
                 final int error = cursor.getInt(columnError);
                 status = cursor.getInt(columnStatus);
-                uri = cursor.getString(columnUri);
+                final String uriWithAnchor = cursor.getString(columnUri);
+                int anchorIndex = uriWithAnchor.indexOf('#');
+                if (anchorIndex != -1) {
+                    uri = uriWithAnchor.substring(0, anchorIndex);
+                } else {
+                    uri = uriWithAnchor;
+                }
                 if (DownloadManager.STATUS_SUCCESSFUL != status) {
                     Log.e(TAG, "Permanent failure of download " + downloadId
                             + " with error code: " + error);
diff --git a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
index f1c0228..2d15bed 100644
--- a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
+++ b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
@@ -110,29 +110,31 @@
         }
     }
 
+    // The table below needs to be kept in sync with MetadataDbHelper.STATUS_* since it uses
+    // the values as indices.
     private static final int sStatusActionList[][] = {
         // MetadataDbHelper.STATUS_UNKNOWN
         {},
         // MetadataDbHelper.STATUS_AVAILABLE
-        { R.string.install_dict, ACTION_ENABLE_DICT },
+        { ButtonSwitcher.STATUS_INSTALL, ACTION_ENABLE_DICT },
         // MetadataDbHelper.STATUS_DOWNLOADING
-        { R.string.cancel_download_dict, ACTION_DISABLE_DICT },
+        { ButtonSwitcher.STATUS_CANCEL, ACTION_DISABLE_DICT },
         // MetadataDbHelper.STATUS_INSTALLED
-        { R.string.delete_dict, ACTION_DELETE_DICT },
+        { ButtonSwitcher.STATUS_DELETE, ACTION_DELETE_DICT },
         // MetadataDbHelper.STATUS_DISABLED
-        { R.string.delete_dict, ACTION_DELETE_DICT },
+        { ButtonSwitcher.STATUS_DELETE, ACTION_DELETE_DICT },
         // MetadataDbHelper.STATUS_DELETING
         // We show 'install' because the file is supposed to be deleted.
         // The user may reinstall it.
-        { R.string.install_dict, ACTION_ENABLE_DICT }
+        { ButtonSwitcher.STATUS_INSTALL, ACTION_ENABLE_DICT }
     };
 
-    private CharSequence getButtonLabel(final int status) {
+    private int getButtonSwitcherStatus(final int status) {
         if (status >= sStatusActionList.length) {
             Log.e(TAG, "Unknown status " + status);
-            return "";
+            return ButtonSwitcher.STATUS_NO_BUTTON;
         }
-        return mContext.getString(sStatusActionList[status][0]);
+        return sStatusActionList[status][0];
     }
 
     private static int getActionIdFromStatusAndMenuEntry(final int status) {
@@ -189,9 +191,8 @@
         ((ViewGroup)view).setLayoutTransition(null);
         final ButtonSwitcher buttonSwitcher =
                 (ButtonSwitcher)view.findViewById(R.id.wordlist_button_switcher);
-        buttonSwitcher.setText(getButtonLabel(mStatus));
-        buttonSwitcher.setInternalButtonVisiblility(mInterfaceState.isOpen(mWordlistId) ?
-                View.VISIBLE : View.INVISIBLE);
+        buttonSwitcher.setStatusAndUpdateVisuals(mInterfaceState.isOpen(mWordlistId) ?
+                getButtonSwitcherStatus(mStatus) : ButtonSwitcher.STATUS_NO_BUTTON);
         buttonSwitcher.setInternalOnClickListener(mActionButtonClickHandler);
         view.setOnClickListener(mPreferenceClickHandler);
     }
@@ -224,9 +225,9 @@
                 final ButtonSwitcher buttonSwitcher = (ButtonSwitcher)listView.getChildAt(i)
                         .findViewById(R.id.wordlist_button_switcher);
                 if (i == indexToOpen) {
-                    buttonSwitcher.animateButton(ButtonSwitcher.ANIMATION_IN);
+                    buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(mStatus));
                 } else {
-                    buttonSwitcher.animateButton(ButtonSwitcher.ANIMATION_OUT);
+                    buttonSwitcher.setStatusAndUpdateVisuals(ButtonSwitcher.STATUS_NO_BUTTON);
                 }
             }
         }
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
index 2943128..ddd72f1 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -72,10 +72,16 @@
     public static String getTempFileName(final String id, final Context context)
             throws IOException {
         final String safeId = DictionaryInfoUtils.replaceFileNameDangerousCharacters(id);
+        final File directory = new File(DictionaryInfoUtils.getWordListTempDirectory(context));
+        if (!directory.exists()) {
+            if (!directory.mkdirs()) {
+                Log.e(TAG, "Could not create the temporary directory");
+            }
+        }
         // If the first argument is less than three chars, createTempFile throws a
         // RuntimeException. We don't really care about what name we get, so just
         // put a three-chars prefix makes us safe.
-        return File.createTempFile("xxx" + safeId, null).getAbsolutePath();
+        return File.createTempFile("xxx" + safeId, null, directory).getAbsolutePath();
     }
 
     /**
diff --git a/java/src/com/android/inputmethod/latin/DebugSettings.java b/java/src/com/android/inputmethod/latin/DebugSettings.java
index 9d47941..5969a63 100644
--- a/java/src/com/android/inputmethod/latin/DebugSettings.java
+++ b/java/src/com/android/inputmethod/latin/DebugSettings.java
@@ -122,7 +122,7 @@
         }
         boolean isDebugMode = mDebugMode.isChecked();
         final String version = getResources().getString(
-                R.string.version_text, Utils.getSdkVersion(getActivity()));
+                R.string.version_text, Utils.getVersionName(getActivity()));
         if (!isDebugMode) {
             mDebugMode.setTitle(version);
             mDebugMode.setSummary("");
diff --git a/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
index dcfa483..df7bad8 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
@@ -129,6 +129,13 @@
     }
 
     /**
+     * Helper method to get the top level temp directory.
+     */
+    public static String getWordListTempDirectory(final Context context) {
+        return context.getFilesDir() + File.separator + "tmp";
+    }
+
+    /**
      * Reverse escaping done by replaceFileNameDangerousCharacters.
      */
     public static String getWordListIdFromFileName(final String fname) {
diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java
index fc32bd4..0f96c54 100644
--- a/java/src/com/android/inputmethod/latin/Utils.java
+++ b/java/src/com/android/inputmethod/latin/Utils.java
@@ -475,7 +475,7 @@
         return 0;
     }
 
-    public static String getSdkVersion(Context context) {
+    public static String getVersionName(Context context) {
         try {
             if (context == null) {
                 return "";