Merge "Fix the title of the fragment of the personal dictionary"
diff --git a/java/res/xml/spellchecker.xml b/java/res/xml/spellchecker.xml
index 813319c..13e6132 100644
--- a/java/res/xml/spellchecker.xml
+++ b/java/res/xml/spellchecker.xml
@@ -21,7 +21,7 @@
      for the spell checker -->
 
 <spell-checker xmlns:android="http://schemas.android.com/apk/res/android"
-        android:label="@string/aosp_spell_checker_service_name"
+        android:label="@string/spell_checker_service_name"
         android:settingsActivity="com.android.inputmethod.latin.spellcheck.SpellCheckerSettingsActivity">
     <subtype
             android:label="@string/subtype_generic"
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java
index 39cfb60..3077491 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java
@@ -16,11 +16,28 @@
 
 package com.android.inputmethod.dictionarypack;
 
+import android.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.content.ContentValues;
 import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Handler;
 import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
 import android.widget.ProgressBar;
 
 public class DictionaryDownloadProgressBar extends ProgressBar {
+    @SuppressWarnings("unused")
+    private static final String TAG = DictionaryDownloadProgressBar.class.getSimpleName();
+    private static final int NOT_A_DOWNLOADMANAGER_PENDING_ID = 0;
+
+    private String mClientId;
+    private String mWordlistId;
+    private boolean mIsCurrentlyAttachedToWindow = false;
+    private Thread mReporterThread = null;
+
     public DictionaryDownloadProgressBar(final Context context) {
         super(context);
     }
@@ -28,4 +45,132 @@
     public DictionaryDownloadProgressBar(final Context context, final AttributeSet attrs) {
         super(context, attrs);
     }
+
+    public void setIds(final String clientId, final String wordlistId) {
+        mClientId = clientId;
+        mWordlistId = wordlistId;
+    }
+
+    static private int getDownloadManagerPendingIdFromWordlistId(final Context context,
+            final String clientId, final String wordlistId) {
+        final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId);
+        final ContentValues wordlistValues =
+                MetadataDbHelper.getContentValuesOfLatestAvailableWordlistById(db, wordlistId);
+        if (null == wordlistValues) {
+            // We don't know anything about a word list with this id. Bug? This should never
+            // happen, but still return to prevent a crash.
+            Log.e(TAG, "Unexpected word list ID: " + wordlistId);
+            return NOT_A_DOWNLOADMANAGER_PENDING_ID;
+        }
+        return wordlistValues.getAsInteger(MetadataDbHelper.PENDINGID_COLUMN);
+    }
+
+    /*
+     * This method will stop any running updater thread for this progress bar and create and run
+     * a new one only if the progress bar is visible.
+     * Hence, as a result of calling this method, the progress bar will have an updater thread
+     * running if and only if the progress bar is visible.
+     */
+    private void updateReporterThreadRunningStatusAccordingToVisibility() {
+        if (null != mReporterThread) mReporterThread.interrupt();
+        if (mIsCurrentlyAttachedToWindow && View.VISIBLE == getVisibility()) {
+            final int downloadManagerPendingId =
+                    getDownloadManagerPendingIdFromWordlistId(getContext(), mClientId, mWordlistId);
+            if (NOT_A_DOWNLOADMANAGER_PENDING_ID == downloadManagerPendingId) {
+                // Can't get the ID. This is never supposed to happen, but still clear the updater
+                // thread and return to avoid a crash.
+                mReporterThread = null;
+                return;
+            }
+            final UpdaterThread updaterThread =
+                    new UpdaterThread(getContext(), downloadManagerPendingId);
+            updaterThread.start();
+            mReporterThread = updaterThread;
+        } else {
+            // We're not going to restart the thread anyway, so we may as well garbage collect it.
+            mReporterThread = null;
+        }
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        mIsCurrentlyAttachedToWindow = true;
+        updateReporterThreadRunningStatusAccordingToVisibility();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        mIsCurrentlyAttachedToWindow = false;
+        updateReporterThreadRunningStatusAccordingToVisibility();
+    }
+
+    private class UpdaterThread extends Thread {
+        private final static int REPORT_PERIOD = 150; // how often to report progress, in ms
+        final DownloadManager mDownloadManager;
+        final int mId;
+        public UpdaterThread(final Context context, final int id) {
+            super();
+            mDownloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
+            mId = id;
+        }
+        @Override
+        public void run() {
+            try {
+                // It's almost impossible that mDownloadManager is null (it would mean it has been
+                // disabled between pressing the 'install' button and displaying the progress
+                // bar), but just in case.
+                if (null == mDownloadManager) return;
+                final UpdateHelper updateHelper = new UpdateHelper();
+                final Query query = new Query().setFilterById(mId);
+                int lastProgress = 0;
+                while (!isInterrupted()) {
+                    final Cursor cursor = mDownloadManager.query(query);
+                    if (null == cursor) {
+                        // Can't contact DownloadManager: this should never happen.
+                        return;
+                    }
+                    try {
+                        if (cursor.moveToNext()) {
+                            final int columnBytesDownloadedSoFar = cursor.getColumnIndex(
+                                    DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
+                            final int bytesDownloadedSoFar =
+                                    cursor.getInt(columnBytesDownloadedSoFar);
+                            updateHelper.setProgressFromAnotherThread(bytesDownloadedSoFar);
+                        } else {
+                            // Download has finished and DownloadManager has already been asked to
+                            // clean up the db entry.
+                            updateHelper.setProgressFromAnotherThread(getMax());
+                            return;
+                        }
+                    } finally {
+                        cursor.close();
+                    }
+                    Thread.sleep(REPORT_PERIOD);
+                }
+            } catch (InterruptedException e) {
+                // Do nothing and terminate normally.
+            }
+        }
+
+        private class UpdateHelper implements Runnable {
+            private int mProgress;
+            @Override
+            public void run() {
+                setProgress(mProgress);
+            }
+            public void setProgressFromAnotherThread(final int progress) {
+                if (mProgress != progress) {
+                    mProgress = progress;
+                    // For some unknown reason, setProgress just does not work from a separate
+                    // thread, although the code in ProgressBar looks like it should. Thus, we
+                    // resort to a runnable posted to the handler of the view.
+                    final Handler handler = getHandler();
+                    // It's possible to come here before this view has been laid out. If so,
+                    // just ignore the call - it will be updated again later.
+                    if (null == handler) return;
+                    handler.post(this);
+                }
+            }
+        }
+    }
 }
diff --git a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
index 1340a41..29015d6 100644
--- a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
+++ b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
@@ -24,7 +24,6 @@
 import android.view.ViewGroup;
 import android.view.ViewParent;
 import android.widget.ListView;
-import android.widget.ProgressBar;
 import android.widget.TextView;
 
 import com.android.inputmethod.latin.R;
@@ -195,9 +194,10 @@
         super.onBindView(view);
         ((ViewGroup)view).setLayoutTransition(null);
 
-        final ProgressBar progressBar =
-                (ProgressBar)view.findViewById(R.id.dictionary_line_progress_bar);
+        final DictionaryDownloadProgressBar progressBar =
+                (DictionaryDownloadProgressBar)view.findViewById(R.id.dictionary_line_progress_bar);
         final TextView status = (TextView)view.findViewById(android.R.id.summary);
+        progressBar.setIds(mClientId, mWordlistId);
         progressBar.setMax(mFilesize);
         final boolean showProgressBar = (MetadataDbHelper.STATUS_DOWNLOADING == mStatus);
         status.setVisibility(showProgressBar ? View.INVISIBLE : View.VISIBLE);
diff --git a/native/jni/src/suggest/core/dicnode/dic_node.h b/native/jni/src/suggest/core/dicnode/dic_node.h
index 92783de..4225bb3 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node.h
@@ -219,7 +219,7 @@
         return (prevWordLen == 1 && currentWordLen == 1);
     }
 
-    bool isCapitalized() const {
+    bool isFirstCharUppercase() const {
         const int c = getOutputWordBuf()[0];
         return isAsciiUpper(c);
     }
diff --git a/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp b/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp
index 299ca83..f879892 100644
--- a/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp
+++ b/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp
@@ -39,7 +39,7 @@
 const float ScoringParams::ADDITIONAL_PROXIMITY_COST = 0.380f;
 const float ScoringParams::SUBSTITUTION_COST = 0.403f;
 const float ScoringParams::COST_NEW_WORD = 0.042f;
-const float ScoringParams::COST_NEW_WORD_CAPITALIZED = 0.174f;
+const float ScoringParams::COST_SECOND_OR_LATER_WORD_FIRST_CHAR_UPPERCASE = 0.25f;
 const float ScoringParams::DISTANCE_WEIGHT_LANGUAGE = 1.123f;
 const float ScoringParams::COST_FIRST_LOOKAHEAD = 0.545f;
 const float ScoringParams::COST_LOOKAHEAD = 0.073f;
@@ -48,5 +48,5 @@
 const float ScoringParams::HAS_MULTI_WORD_TERMINAL_COST = 0.444f;
 const float ScoringParams::TYPING_BASE_OUTPUT_SCORE = 1.0f;
 const float ScoringParams::TYPING_MAX_OUTPUT_SCORE_PER_INPUT = 0.1f;
-const float ScoringParams::MAX_NORM_DISTANCE_FOR_EDIT = 0.1f;
+const float ScoringParams::NORMALIZED_SPATIAL_DISTANCE_THRESHOLD_FOR_EDIT = 0.06f;
 } // namespace latinime
diff --git a/native/jni/src/suggest/policyimpl/typing/scoring_params.h b/native/jni/src/suggest/policyimpl/typing/scoring_params.h
index 8f104b3..53ac999 100644
--- a/native/jni/src/suggest/policyimpl/typing/scoring_params.h
+++ b/native/jni/src/suggest/policyimpl/typing/scoring_params.h
@@ -48,7 +48,7 @@
     static const float ADDITIONAL_PROXIMITY_COST;
     static const float SUBSTITUTION_COST;
     static const float COST_NEW_WORD;
-    static const float COST_NEW_WORD_CAPITALIZED;
+    static const float COST_SECOND_OR_LATER_WORD_FIRST_CHAR_UPPERCASE;
     static const float DISTANCE_WEIGHT_LANGUAGE;
     static const float COST_FIRST_LOOKAHEAD;
     static const float COST_LOOKAHEAD;
@@ -57,7 +57,7 @@
     static const float HAS_MULTI_WORD_TERMINAL_COST;
     static const float TYPING_BASE_OUTPUT_SCORE;
     static const float TYPING_MAX_OUTPUT_SCORE_PER_INPUT;
-    static const float MAX_NORM_DISTANCE_FOR_EDIT;
+    static const float NORMALIZED_SPATIAL_DISTANCE_THRESHOLD_FOR_EDIT;
 
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(ScoringParams);
diff --git a/native/jni/src/suggest/policyimpl/typing/typing_traversal.h b/native/jni/src/suggest/policyimpl/typing/typing_traversal.h
index fb1fb79..12110d5 100644
--- a/native/jni/src/suggest/policyimpl/typing/typing_traversal.h
+++ b/native/jni/src/suggest/policyimpl/typing/typing_traversal.h
@@ -39,7 +39,7 @@
 
     AK_FORCE_INLINE bool allowsErrorCorrections(const DicNode *const dicNode) const {
         return dicNode->getNormalizedSpatialDistance()
-                < ScoringParams::MAX_NORM_DISTANCE_FOR_EDIT;
+                < ScoringParams::NORMALIZED_SPATIAL_DISTANCE_THRESHOLD_FOR_EDIT;
     }
 
     AK_FORCE_INLINE bool isOmission(const DicTraverseSession *const traverseSession,
diff --git a/native/jni/src/suggest/policyimpl/typing/typing_weighting.h b/native/jni/src/suggest/policyimpl/typing/typing_weighting.h
index e6fa1bd..3938c0e 100644
--- a/native/jni/src/suggest/policyimpl/typing/typing_weighting.h
+++ b/native/jni/src/suggest/policyimpl/typing/typing_weighting.h
@@ -80,8 +80,18 @@
 
         const bool isFirstChar = pointIndex == 0;
         const bool isProximity = isProximityDicNode(traverseSession, dicNode);
-        const float cost = isProximity ? (isFirstChar ? ScoringParams::FIRST_PROXIMITY_COST
+        float cost = isProximity ? (isFirstChar ? ScoringParams::FIRST_PROXIMITY_COST
                 : ScoringParams::PROXIMITY_COST) : 0.0f;
+        if (dicNode->getDepth() == 2) {
+            // At the second character of the current word, we check if the first char is uppercase
+            // and the word is a second or later word of a multiple word suggestion. We demote it
+            // if so.
+            const bool isSecondOrLaterWordFirstCharUppercase =
+                    dicNode->hasMultipleWords() && dicNode->isFirstCharUppercase();
+            if (isSecondOrLaterWordFirstCharUppercase) {
+                cost += ScoringParams::COST_SECOND_OR_LATER_WORD_FIRST_CHAR_UPPERCASE;
+            }
+        }
         return weightedDistance + cost;
     }
 
@@ -129,10 +139,7 @@
 
     float getNewWordCost(const DicTraverseSession *const traverseSession,
             const DicNode *const dicNode) const {
-        const bool isCapitalized = dicNode->isCapitalized();
-        const float cost = isCapitalized ?
-                ScoringParams::COST_NEW_WORD_CAPITALIZED : ScoringParams::COST_NEW_WORD;
-        return cost * traverseSession->getMultiWordCostMultiplier();
+        return ScoringParams::COST_NEW_WORD * traverseSession->getMultiWordCostMultiplier();
     }
 
     float getNewWordBigramCost(const DicTraverseSession *const traverseSession,
@@ -174,9 +181,7 @@
 
     AK_FORCE_INLINE float getSpaceSubstitutionCost(const DicTraverseSession *const traverseSession,
             const DicNode *const dicNode) const {
-        const bool isCapitalized = dicNode->isCapitalized();
-        const float cost = ScoringParams::SPACE_SUBSTITUTION_COST + (isCapitalized ?
-                ScoringParams::COST_NEW_WORD_CAPITALIZED : ScoringParams::COST_NEW_WORD);
+        const float cost = ScoringParams::SPACE_SUBSTITUTION_COST + ScoringParams::COST_NEW_WORD;
         return cost * traverseSession->getMultiWordCostMultiplier();
     }