Make the string builder pool in Suggest a singleton.

This is internal refactoring, done as preliminary work to fix
Bug: 5175740

Change-Id: I21bd4c001c27e7b925ddb87a152105b4dcab320a
diff --git a/java/src/com/android/inputmethod/latin/StringBuilderPool.java b/java/src/com/android/inputmethod/latin/StringBuilderPool.java
new file mode 100644
index 0000000..66f1237
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/StringBuilderPool.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011 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;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A pool of string builders to be used from anywhere.
+ */
+public class StringBuilderPool {
+    // Singleton
+    private static final StringBuilderPool sInstance = new StringBuilderPool();
+    private StringBuilderPool() {}
+    // TODO: Make this a normal array with a size of 20
+    private final List<StringBuilder> mPool =
+            Collections.synchronizedList(new ArrayList<StringBuilder>());
+
+    public static StringBuilder getStringBuilder(final int initialSize) {
+        final int poolSize = sInstance.mPool.size();
+        final StringBuilder sb = poolSize > 0 ? (StringBuilder) sInstance.mPool.remove(poolSize - 1)
+                : new StringBuilder(initialSize);
+        sb.setLength(0);
+        return sb;
+    }
+
+    public static void recycle(final StringBuilder garbage) {
+        sInstance.mPool.add(garbage);
+    }
+
+    public static void ensureCapacity(final int capacity, final int initialSize) {
+        for (int i = sInstance.mPool.size(); i < capacity; ++i) {
+            final StringBuilder sb = new StringBuilder(initialSize);
+            sInstance.mPool.add(sb);
+        }
+    }
+
+    public static int getSize() {
+        return sInstance.mPool.size();
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 937457e..c3caae4 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -105,9 +105,6 @@
 
     private ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>();
     ArrayList<CharSequence> mBigramSuggestions  = new ArrayList<CharSequence>();
-    // TODO: maybe this should be synchronized, it's quite scary as it is.
-    // TODO: if it becomes synchronized, also move initPool in the thread in initAsynchronously
-    private ArrayList<CharSequence> mStringPool = new ArrayList<CharSequence>();
     private CharSequence mTypedWord;
 
     // TODO: Remove these member variables by passing more context to addWord() callback method
@@ -130,7 +127,7 @@
         mWhiteListDictionary = WhitelistDictionary.init(context);
         addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_WHITELIST, mWhiteListDictionary);
         mAutoCorrection = new AutoCorrection();
-        initPool();
+        StringBuilderPool.ensureCapacity(mPrefMaxSuggestions, getApproxMaxWordLength());
     }
 
     private void initAsynchronously(final Context context, final int dictionaryResId,
@@ -138,7 +135,7 @@
         resetMainDict(context, dictionaryResId, locale);
 
         // TODO: read the whitelist and init the pool asynchronously too.
-        // initPool should be done asynchronously but the pool is not thread-safe at the moment.
+        // initPool should be done asynchronously now that the pool is thread-safe.
         initWhitelistAndAutocorrectAndPool(context);
     }
 
@@ -173,12 +170,6 @@
         }.start();
     }
 
-    private void initPool() {
-        for (int i = 0; i < mPrefMaxSuggestions; i++) {
-            StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
-            mStringPool.add(sb);
-        }
-    }
 
     public void setQuickFixesEnabled(boolean enabled) {
         mQuickFixesEnabled = enabled;
@@ -259,10 +250,7 @@
         mScores = new int[mPrefMaxSuggestions];
         mBigramScores = new int[PREF_MAX_BIGRAMS];
         collectGarbage(mSuggestions, mPrefMaxSuggestions);
-        while (mStringPool.size() < mPrefMaxSuggestions) {
-            StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
-            mStringPool.add(sb);
-        }
+        StringBuilderPool.ensureCapacity(mPrefMaxSuggestions, getApproxMaxWordLength());
     }
 
     /**
@@ -282,11 +270,7 @@
     private CharSequence capitalizeWord(boolean all, boolean first, CharSequence word) {
         if (TextUtils.isEmpty(word) || !(all || first)) return word;
         final int wordLength = word.length();
-        final int poolSize = mStringPool.size();
-        final StringBuilder sb =
-                poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1)
-                        : new StringBuilder(getApproxMaxWordLength());
-        sb.setLength(0);
+        final StringBuilder sb = StringBuilderPool.getStringBuilder(getApproxMaxWordLength());
         // TODO: Must pay attention to locale when changing case.
         if (all) {
             sb.append(word.toString().toUpperCase());
@@ -300,13 +284,7 @@
     }
 
     protected void addBigramToSuggestions(CharSequence bigram) {
-        final int poolSize = mStringPool.size();
-        final StringBuilder sb = poolSize > 0 ?
-                (StringBuilder) mStringPool.remove(poolSize - 1)
-                        : new StringBuilder(getApproxMaxWordLength());
-        sb.setLength(0);
-        sb.append(bigram);
-        mSuggestions.add(sb);
+        mSuggestions.add(bigram);
     }
 
     // TODO: cleanup dictionaries looking up and suggestions building with SuggestedWords.Builder
@@ -475,9 +453,8 @@
     private static void removeFromSuggestions(final ArrayList<CharSequence> suggestions,
             final int index) {
         final CharSequence garbage = suggestions.remove(index);
-        if (garbage != null && garbage instanceof StringBuilder) {
-            // TODO: rebase this over the static string builder pool
-            //            mStringPool.add(garbage);
+        if (garbage instanceof StringBuilder) {
+            StringBuilderPool.recycle((StringBuilder)garbage);
         }
     }
 
@@ -555,10 +532,7 @@
 
         System.arraycopy(sortedScores, pos, sortedScores, pos + 1, prefMaxSuggestions - pos - 1);
         sortedScores[pos] = score;
-        int poolSize = mStringPool.size();
-        StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1)
-                : new StringBuilder(getApproxMaxWordLength());
-        sb.setLength(0);
+        final StringBuilder sb = StringBuilderPool.getStringBuilder(getApproxMaxWordLength());
         // TODO: Must pay attention to locale when changing case.
         if (mIsAllUpperCase) {
             sb.append(new String(word, offset, length).toUpperCase());
@@ -572,9 +546,9 @@
         }
         suggestions.add(pos, sb);
         if (suggestions.size() > prefMaxSuggestions) {
-            CharSequence garbage = suggestions.remove(prefMaxSuggestions);
+            final CharSequence garbage = suggestions.remove(prefMaxSuggestions);
             if (garbage instanceof StringBuilder) {
-                mStringPool.add(garbage);
+                StringBuilderPool.recycle((StringBuilder)garbage);
             }
         } else {
             LatinImeLogger.onAddSuggestedWord(sb.toString(), dicTypeId, dataTypeForLog);
@@ -603,12 +577,12 @@
     }
 
     private void collectGarbage(ArrayList<CharSequence> suggestions, int prefMaxSuggestions) {
-        int poolSize = mStringPool.size();
+        int poolSize = StringBuilderPool.getSize();
         int garbageSize = suggestions.size();
         while (poolSize < prefMaxSuggestions && garbageSize > 0) {
-            CharSequence garbage = suggestions.get(garbageSize - 1);
-            if (garbage != null && garbage instanceof StringBuilder) {
-                mStringPool.add(garbage);
+            final CharSequence garbage = suggestions.get(garbageSize - 1);
+            if (garbage instanceof StringBuilder) {
+                StringBuilderPool.recycle((StringBuilder)garbage);
                 poolSize++;
             }
             garbageSize--;