Merge "Fix possible NPE"
diff --git a/java/res/values/keypress-vibration-durations.xml b/java/res/values/keypress-vibration-durations.xml
index 0474b1c..e8d9225 100644
--- a/java/res/values/keypress-vibration-durations.xml
+++ b/java/res/values/keypress-vibration-durations.xml
@@ -51,7 +51,7 @@
         <!-- HTC One X -->
         <item>MODEL=HTC One X:MANUFACTURER=HTC,20</item>
         <!-- HTC One -->
-        <item>MODEL=HTC One:MANUFACTURER=HTC,15</item>
+        <item>MODEL=HTC One( special edition)?:MANUFACTURER=HTC,15</item>
         <!-- Motorola Razor M -->
         <item>MODEL=XT907:MANUFACTURER=motorola,30</item>
         <!-- Sony Xperia Z -->
diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
index 1511dbc..dac1213 100644
--- a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
+++ b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
@@ -36,8 +36,6 @@
  * Various helper functions for the state database
  */
 public class MetadataDbHelper extends SQLiteOpenHelper {
-
-    @SuppressWarnings("unused")
     private static final String TAG = MetadataDbHelper.class.getSimpleName();
 
     // This was the initial release version of the database. It should never be
@@ -437,37 +435,37 @@
      */
     public static ContentValues completeWithDefaultValues(final ContentValues result)
             throws BadFormatException {
-        if (!result.containsKey(WORDLISTID_COLUMN) || !result.containsKey(LOCALE_COLUMN)) {
+        if (null == result.get(WORDLISTID_COLUMN) || null == result.get(LOCALE_COLUMN)) {
             throw new BadFormatException();
         }
         // 0 for the pending id, because there is none
-        if (!result.containsKey(PENDINGID_COLUMN)) result.put(PENDINGID_COLUMN, 0);
+        if (null == result.get(PENDINGID_COLUMN)) result.put(PENDINGID_COLUMN, 0);
         // This is a binary blob of a dictionary
-        if (!result.containsKey(TYPE_COLUMN)) result.put(TYPE_COLUMN, TYPE_BULK);
+        if (null == result.get(TYPE_COLUMN)) result.put(TYPE_COLUMN, TYPE_BULK);
         // This word list is unknown, but it's present, else we wouldn't be here, so INSTALLED
-        if (!result.containsKey(STATUS_COLUMN)) result.put(STATUS_COLUMN, STATUS_INSTALLED);
+        if (null == result.get(STATUS_COLUMN)) result.put(STATUS_COLUMN, STATUS_INSTALLED);
         // No description unless specified, because we can't guess it
-        if (!result.containsKey(DESCRIPTION_COLUMN)) result.put(DESCRIPTION_COLUMN, "");
+        if (null == result.get(DESCRIPTION_COLUMN)) result.put(DESCRIPTION_COLUMN, "");
         // File name - this is an asset, so it works as an already deleted file.
         //     hence, we need to supply a non-existent file name. Anything will
         //     do as long as it returns false when tested with File#exist(), and
         //     the empty string does not, so it's set to "_".
-        if (!result.containsKey(LOCAL_FILENAME_COLUMN)) result.put(LOCAL_FILENAME_COLUMN, "_");
+        if (null == result.get(LOCAL_FILENAME_COLUMN)) result.put(LOCAL_FILENAME_COLUMN, "_");
         // No remote file name : this can't be downloaded. Unless specified.
-        if (!result.containsKey(REMOTE_FILENAME_COLUMN)) result.put(REMOTE_FILENAME_COLUMN, "");
+        if (null == result.get(REMOTE_FILENAME_COLUMN)) result.put(REMOTE_FILENAME_COLUMN, "");
         // 0 for the update date : 1970/1/1. Unless specified.
-        if (!result.containsKey(DATE_COLUMN)) result.put(DATE_COLUMN, 0);
+        if (null == result.get(DATE_COLUMN)) result.put(DATE_COLUMN, 0);
         // Checksum unknown unless specified
-        if (!result.containsKey(CHECKSUM_COLUMN)) result.put(CHECKSUM_COLUMN, "");
+        if (null == result.get(CHECKSUM_COLUMN)) result.put(CHECKSUM_COLUMN, "");
         // No filesize unless specified
-        if (!result.containsKey(FILESIZE_COLUMN)) result.put(FILESIZE_COLUMN, 0);
+        if (null == result.get(FILESIZE_COLUMN)) result.put(FILESIZE_COLUMN, 0);
         // Smallest possible version unless specified
-        if (!result.containsKey(VERSION_COLUMN)) result.put(VERSION_COLUMN, 1);
+        if (null == result.get(VERSION_COLUMN)) result.put(VERSION_COLUMN, 1);
         // Assume current format unless specified
-        if (!result.containsKey(FORMATVERSION_COLUMN))
+        if (null == result.get(FORMATVERSION_COLUMN))
             result.put(FORMATVERSION_COLUMN, UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION);
         // No flags unless specified
-        if (!result.containsKey(FLAGS_COLUMN)) result.put(FLAGS_COLUMN, 0);
+        if (null == result.get(FLAGS_COLUMN)) result.put(FLAGS_COLUMN, 0);
         return result;
     }
 
diff --git a/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
index df7bad8..9d47849 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
@@ -30,6 +30,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.Locale;
 
 /**
@@ -301,12 +302,14 @@
 
     private static void addOrUpdateDictInfo(final ArrayList<DictionaryInfo> dictList,
             final DictionaryInfo newElement) {
-        for (final DictionaryInfo info : dictList) {
-            if (info.mLocale.equals(newElement.mLocale)) {
-                if (newElement.mVersion <= info.mVersion) {
+        final Iterator<DictionaryInfo> iter = dictList.iterator();
+        while (iter.hasNext()) {
+            final DictionaryInfo thisDictInfo = iter.next();
+            if (thisDictInfo.mLocale.equals(newElement.mLocale)) {
+                if (newElement.mVersion <= thisDictInfo.mVersion) {
                     return;
                 }
-                dictList.remove(info);
+                iter.remove();
             }
         }
         dictList.add(newElement);
diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
index 0dd302a..94513e6 100644
--- a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
+++ b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
@@ -54,13 +54,6 @@
         return sInstance;
     }
 
-    // Caveat: This may cause IPC
-    public static boolean isInputMethodManagerValidForUserOfThisProcess(final Context context) {
-        // Basically called to check whether this IME has been triggered by the current user or not
-        return !((InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE)).
-                getInputMethodList().isEmpty();
-    }
-
     public static void init(final Context context) {
         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
         sInstance.initInternal(context, prefs);
diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
index 282b579..1eca68a 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
@@ -43,20 +43,23 @@
     private static final String TAG = SubtypeSwitcher.class.getSimpleName();
 
     private static final SubtypeSwitcher sInstance = new SubtypeSwitcher();
+
     private /* final */ RichInputMethodManager mRichImm;
     private /* final */ Resources mResources;
     private /* final */ ConnectivityManager mConnectivityManager;
 
-    /*-----------------------------------------------------------*/
-    // Variants which should be changed only by reload functions.
-    private NeedsToDisplayLanguage mNeedsToDisplayLanguage = new NeedsToDisplayLanguage();
+    private final NeedsToDisplayLanguage mNeedsToDisplayLanguage = new NeedsToDisplayLanguage();
     private InputMethodInfo mShortcutInputMethodInfo;
     private InputMethodSubtype mShortcutSubtype;
     private InputMethodSubtype mNoLanguageSubtype;
-    /*-----------------------------------------------------------*/
-
     private boolean mIsNetworkConnected;
 
+    // Dummy no language QWERTY subtype. See {@link R.xml.method}.
+    private static final InputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE = new InputMethodSubtype(
+            R.string.subtype_no_language_qwerty, R.drawable.ic_subtype_keyboard, "zz", "keyboard",
+            "KeyboardLayoutSet=qwerty,AsciiCapable,EnabledWhenDefaultIsNotAsciiCapable",
+            false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */);
+
     static final class NeedsToDisplayLanguage {
         private int mEnabledSubtypeCount;
         private boolean mIsSystemLanguageSameAsInputLanguage;
@@ -96,11 +99,6 @@
         mRichImm = RichInputMethodManager.getInstance();
         mConnectivityManager = (ConnectivityManager) context.getSystemService(
                 Context.CONNECTIVITY_SERVICE);
-        mNoLanguageSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                SubtypeLocale.NO_LANGUAGE, SubtypeLocale.QWERTY);
-        if (mNoLanguageSubtype == null) {
-            throw new RuntimeException("Can't find no lanugage with QWERTY subtype");
-        }
 
         final NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
         mIsNetworkConnected = (info != null && info.isConnected());
@@ -255,10 +253,20 @@
     }
 
     public InputMethodSubtype getCurrentSubtype() {
-        return mRichImm.getCurrentInputMethodSubtype(mNoLanguageSubtype);
+        return mRichImm.getCurrentInputMethodSubtype(getNoLanguageSubtype());
     }
 
     public InputMethodSubtype getNoLanguageSubtype() {
-        return mNoLanguageSubtype;
+        if (mNoLanguageSubtype == null) {
+            mNoLanguageSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+                    SubtypeLocale.NO_LANGUAGE, SubtypeLocale.QWERTY);
+        }
+        if (mNoLanguageSubtype != null) {
+            return mNoLanguageSubtype;
+        }
+        Log.w(TAG, "Can't find no lanugage with QWERTY subtype");
+        Log.w(TAG, "No input method subtype found; return dummy subtype: "
+                + DUMMY_NO_LANGUAGE_SUBTYPE);
+        return DUMMY_NO_LANGUAGE_SUBTYPE;
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java b/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java
index 604ebee..63d2fec 100644
--- a/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java
+++ b/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java
@@ -25,9 +25,9 @@
 import android.os.Process;
 import android.preference.PreferenceManager;
 import android.util.Log;
+import android.view.inputmethod.InputMethodManager;
 
 import com.android.inputmethod.compat.IntentCompatUtils;
-import com.android.inputmethod.latin.RichInputMethodManager;
 import com.android.inputmethod.latin.Settings;
 
 /**
@@ -65,17 +65,16 @@
         }
 
         // The process that hosts this broadcast receiver is invoked and remains alive even after
-        // 1) the package has been re-installed, 2) the device has been booted,
-        // 3) a multiuser has been created.
+        // 1) the package has been re-installed, 2) the device has just booted,
+        // 3) a new user has been created.
         // There is no good reason to keep the process alive if this IME isn't a current IME.
-        final boolean isCurrentImeOfCurrentUser;
-        if (RichInputMethodManager.isInputMethodManagerValidForUserOfThisProcess(context)) {
-            RichInputMethodManager.init(context);
-            isCurrentImeOfCurrentUser = SetupActivity.isThisImeCurrent(context);
-        } else {
-            isCurrentImeOfCurrentUser = false;
-        }
-
+        final InputMethodManager imm =
+                (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE);
+        // Called to check whether this IME has been triggered by the current user or not
+        final boolean isInputMethodManagerValidForUserOfThisProcess =
+                !imm.getInputMethodList().isEmpty();
+        final boolean isCurrentImeOfCurrentUser = isInputMethodManagerValidForUserOfThisProcess
+                && SetupActivity.isThisImeCurrent(context, imm);
         if (!isCurrentImeOfCurrentUser) {
             final int myPid = Process.myPid();
             Log.i(TAG, "Killing my process: pid=" + myPid);
diff --git a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java
index 8a2de88..a68f98f 100644
--- a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java
+++ b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java
@@ -24,8 +24,6 @@
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodManager;
 
-import com.android.inputmethod.latin.RichInputMethodManager;
-
 public final class SetupActivity extends Activity {
     @Override
     protected void onCreate(final Bundle savedInstanceState) {
@@ -40,17 +38,24 @@
         }
     }
 
+    /*
+     * We may not be able to get our own {@link InputMethodInfo} just after this IME is installed
+     * because {@link InputMethodManagerService} may not be aware of this IME yet.
+     * Note: {@link RichInputMethodManager} has similar methods. Here in setup wizard, we can't
+     * use it for the reason above.
+     */
+
     /**
      * Check if the IME specified by the context is enabled.
-     * Note that {@link RichInputMethodManager} must have been initialized before calling this
-     * method.
+     * CAVEAT: This may cause a round trip IPC.
      *
      * @param context package context of the IME to be checked.
+     * @param imm the {@link InputMethodManager}.
      * @return true if this IME is enabled.
      */
-    public static boolean isThisImeEnabled(final Context context) {
+    /* package */ static boolean isThisImeEnabled(final Context context,
+            final InputMethodManager imm) {
         final String packageName = context.getPackageName();
-        final InputMethodManager imm = RichInputMethodManager.getInstance().getInputMethodManager();
         for (final InputMethodInfo imi : imm.getEnabledInputMethodList()) {
             if (packageName.equals(imi.getPackageName())) {
                 return true;
@@ -61,17 +66,36 @@
 
     /**
      * Check if the IME specified by the context is the current IME.
-     * Note that {@link RichInputMethodManager} must have been initialized before calling this
-     * method.
+     * CAVEAT: This may cause a round trip IPC.
      *
      * @param context package context of the IME to be checked.
+     * @param imm the {@link InputMethodManager}.
      * @return true if this IME is the current IME.
      */
-    public static boolean isThisImeCurrent(final Context context) {
-        final InputMethodInfo myImi =
-                RichInputMethodManager.getInstance().getInputMethodInfoOfThisIme();
+    /* package */ static boolean isThisImeCurrent(final Context context,
+            final InputMethodManager imm) {
+        final InputMethodInfo imi = getInputMethodInfoOf(context.getPackageName(), imm);
         final String currentImeId = Settings.Secure.getString(
                 context.getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD);
-        return myImi.getId().equals(currentImeId);
+        return imi != null && imi.getId().equals(currentImeId);
+    }
+
+    /**
+     * Get {@link InputMethodInfo} of the IME specified by the package name.
+     * CAVEAT: This may cause a round trip IPC.
+     *
+     * @param packageName package name of the IME.
+     * @param imm the {@link InputMethodManager}.
+     * @return the {@link InputMethodInfo} of the IME specified by the <code>packageName</code>,
+     * or null if not found.
+     */
+    /* package */ static InputMethodInfo getInputMethodInfoOf(final String packageName,
+            final InputMethodManager imm) {
+        for (final InputMethodInfo imi : imm.getInputMethodList()) {
+            if (packageName.equals(imi.getPackageName())) {
+                return imi;
+            }
+        }
+        return null;
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java
index 78a6478..13fa9d9 100644
--- a/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java
+++ b/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java
@@ -28,6 +28,7 @@
 import android.util.Log;
 import android.view.View;
 import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
 import android.widget.ImageView;
 import android.widget.TextView;
 import android.widget.VideoView;
@@ -36,7 +37,6 @@
 import com.android.inputmethod.compat.ViewCompatUtils;
 import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.RichInputMethodManager;
 import com.android.inputmethod.latin.SettingsActivity;
 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
 
@@ -48,6 +48,8 @@
 
     private static final boolean ENABLE_WELCOME_VIDEO = true;
 
+    private InputMethodManager mImm;
+
     private View mSetupWizard;
     private View mWelcomeScreen;
     private View mSetupScreen;
@@ -69,15 +71,19 @@
     private static final int STEP_LAUNCHING_IME_SETTINGS = 4;
     private static final int STEP_BACK_FROM_IME_SETTINGS = 5;
 
-    final SettingsPoolingHandler mHandler = new SettingsPoolingHandler(this);
+    private SettingsPoolingHandler mHandler;
 
-    static final class SettingsPoolingHandler
+    private static final class SettingsPoolingHandler
             extends StaticInnerHandlerWrapper<SetupWizardActivity> {
         private static final int MSG_POLLING_IME_SETTINGS = 0;
         private static final long IME_SETTINGS_POLLING_INTERVAL = 200;
 
-        public SettingsPoolingHandler(final SetupWizardActivity outerInstance) {
+        private final InputMethodManager mImmInHandler;
+
+        public SettingsPoolingHandler(final SetupWizardActivity outerInstance,
+                final InputMethodManager imm) {
             super(outerInstance);
+            mImmInHandler = imm;
         }
 
         @Override
@@ -88,7 +94,7 @@
             }
             switch (msg.what) {
             case MSG_POLLING_IME_SETTINGS:
-                if (SetupActivity.isThisImeEnabled(setupWizardActivity)) {
+                if (SetupActivity.isThisImeEnabled(setupWizardActivity, mImmInHandler)) {
                     setupWizardActivity.invokeSetupWizardOfThisIme();
                     return;
                 }
@@ -112,11 +118,12 @@
         setTheme(android.R.style.Theme_Translucent_NoTitleBar);
         super.onCreate(savedInstanceState);
 
+        mImm = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);
+        mHandler = new SettingsPoolingHandler(this, mImm);
+
         setContentView(R.layout.setup_wizard);
         mSetupWizard = findViewById(R.id.setup_wizard);
 
-        RichInputMethodManager.init(this);
-
         if (savedInstanceState == null) {
             mStepNumber = determineSetupStepNumberFromLauncher();
         } else {
@@ -143,11 +150,12 @@
                 R.string.setup_step1_title, R.string.setup_step1_instruction,
                 R.string.setup_step1_finished_instruction, R.drawable.ic_setup_step1,
                 R.string.setup_step1_action);
+        final SettingsPoolingHandler handler = mHandler;
         step1.setAction(new Runnable() {
             @Override
             public void run() {
                 invokeLanguageAndInputSettings();
-                mHandler.startPollingImeSettings();
+                handler.startPollingImeSettings();
             }
         });
         mSetupStepGroup.addStep(step1);
@@ -265,14 +273,15 @@
 
     void invokeInputMethodPicker() {
         // Invoke input method picker.
-        RichInputMethodManager.getInstance().getInputMethodManager()
-                .showInputMethodPicker();
+        mImm.showInputMethodPicker();
         mNeedsToAdjustStepNumberToSystemState = true;
     }
 
     void invokeSubtypeEnablerOfThisIme() {
-        final InputMethodInfo imi =
-                RichInputMethodManager.getInstance().getInputMethodInfoOfThisIme();
+        final InputMethodInfo imi = SetupActivity.getInputMethodInfoOf(getPackageName(), mImm);
+        if (imi == null) {
+            return;
+        }
         final Intent intent = new Intent();
         intent.setAction(Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS);
         intent.addCategory(Intent.CATEGORY_DEFAULT);
@@ -293,10 +302,10 @@
 
     private int determineSetupStepNumber() {
         mHandler.cancelPollingImeSettings();
-        if (!SetupActivity.isThisImeEnabled(this)) {
+        if (!SetupActivity.isThisImeEnabled(this, mImm)) {
             return STEP_1;
         }
-        if (!SetupActivity.isThisImeCurrent(this)) {
+        if (!SetupActivity.isThisImeCurrent(this, mImm)) {
             return STEP_2;
         }
         return STEP_3;
diff --git a/java/src/com/android/inputmethod/latin/utils/Base64Reader.java b/java/src/com/android/inputmethod/latin/utils/Base64Reader.java
new file mode 100644
index 0000000..3eca6e7
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/Base64Reader.java
@@ -0,0 +1,117 @@
+/*
+ * 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.utils;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.LineNumberReader;
+
+@UsedForTesting
+public class Base64Reader {
+    private final LineNumberReader mReader;
+
+    private String mLine;
+    private int mCharPos;
+    private int mByteCount;
+
+    @UsedForTesting
+    public Base64Reader(final LineNumberReader reader) {
+        mReader = reader;
+        reset();
+    }
+
+    @UsedForTesting
+    public void reset() {
+        mLine = null;
+        mCharPos = 0;
+        mByteCount = 0;
+    }
+
+    @UsedForTesting
+    public int getLineNumber() {
+        return mReader.getLineNumber();
+    }
+
+    @UsedForTesting
+    public int getByteCount() {
+        return mByteCount;
+    }
+
+    private void fillBuffer() throws IOException {
+        if (mLine == null || mCharPos >= mLine.length()) {
+            mLine = mReader.readLine();
+            mCharPos = 0;
+        }
+        if (mLine == null) {
+            throw new EOFException();
+        }
+    }
+
+    private int peekUint8() throws IOException {
+        fillBuffer();
+        final char c = mLine.charAt(mCharPos);
+        if (c >= 'A' && c <= 'Z')
+            return c - 'A' + 0;
+        if (c >= 'a' && c <= 'z')
+            return c - 'a' + 26;
+        if (c >= '0' && c <= '9')
+            return c - '0' + 52;
+        if (c == '+')
+            return 62;
+        if (c == '/')
+            return 63;
+        if (c == '=')
+            return 0;
+        throw new RuntimeException("Unknown character '" + c + "' in base64 at line "
+                + mReader.getLineNumber());
+    }
+
+    private int getUint8() throws IOException {
+        final int value = peekUint8();
+        mCharPos++;
+        return value;
+    }
+
+    @UsedForTesting
+    public int readUint8() throws IOException {
+        final int value1, value2;
+        switch (mByteCount % 3) {
+        case 0:
+            value1 = getUint8() << 2;
+            value2 = value1 | (peekUint8() >> 4);
+            break;
+        case 1:
+            value1 = (getUint8() & 0x0f) << 4;
+            value2 = value1 | (peekUint8() >> 2);
+            break;
+        default:
+            value1 = (getUint8() & 0x03) << 6;
+            value2 = value1 | getUint8();
+            break;
+        }
+        mByteCount++;
+        return value2;
+    }
+
+    @UsedForTesting
+    public short readInt16() throws IOException {
+        final int data = readUint8() << 8;
+        return (short)(data | readUint8());
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/utils/Base64ReaderTests.java b/tests/src/com/android/inputmethod/latin/utils/Base64ReaderTests.java
new file mode 100644
index 0000000..b311f5d
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/utils/Base64ReaderTests.java
@@ -0,0 +1,225 @@
+/*
+ * 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.utils;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.io.StringReader;
+
+@SmallTest
+public class Base64ReaderTests extends AndroidTestCase {
+    private static final String EMPTY_STRING = "";
+    private static final String INCOMPLETE_CHAR1 = "Q";
+    // Encode 'A'.
+    private static final String INCOMPLETE_CHAR2 = "QQ";
+    // Encode 'A', 'B'
+    private static final String INCOMPLETE_CHAR3 = "QUI";
+    // Encode 'A', 'B', 'C'
+    private static final String COMPLETE_CHAR4 = "QUJD";
+    private static final String ALL_BYTE_PATTERN =
+            "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIj\n"
+            + "JCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZH\n"
+            + "SElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWpr\n"
+            + "bG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6P\n"
+            + "kJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKz\n"
+            + "tLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX\n"
+            + "2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7\n"
+            + "/P3+/w==";
+
+    public void test0CharInt8() {
+        final Base64Reader reader = new Base64Reader(
+                new LineNumberReader(new StringReader(EMPTY_STRING)));
+        try {
+            reader.readUint8();
+            fail("0 char");
+        } catch (final EOFException e) {
+            assertEquals("0 char", 0, reader.getByteCount());
+        } catch (final IOException e) {
+            fail("IOException: " + e);
+        }
+    }
+
+    public void test1CharInt8() {
+        final Base64Reader reader = new Base64Reader(
+                new LineNumberReader(new StringReader(INCOMPLETE_CHAR1)));
+        try {
+            reader.readUint8();
+            fail("1 char");
+        } catch (final EOFException e) {
+            assertEquals("1 char", 0, reader.getByteCount());
+        } catch (final IOException e) {
+            fail("IOException: " + e);
+        }
+    }
+
+    public void test2CharsInt8() {
+        final Base64Reader reader = new Base64Reader(
+                new LineNumberReader(new StringReader(INCOMPLETE_CHAR2)));
+        try {
+            final int v1 = reader.readUint8();
+            assertEquals("2 chars pos 0", 'A', v1);
+            reader.readUint8();
+            fail("2 chars");
+        } catch (final EOFException e) {
+            assertEquals("2 chars", 1, reader.getByteCount());
+        } catch (final IOException e) {
+            fail("IOException: " + e);
+        }
+    }
+
+    public void test3CharsInt8() {
+        final Base64Reader reader = new Base64Reader(
+                new LineNumberReader(new StringReader(INCOMPLETE_CHAR3)));
+        try {
+            final int v1 = reader.readUint8();
+            assertEquals("3 chars pos 0", 'A', v1);
+            final int v2 = reader.readUint8();
+            assertEquals("3 chars pos 1", 'B', v2);
+            reader.readUint8();
+            fail("3 chars");
+        } catch (final EOFException e) {
+            assertEquals("3 chars", 2, reader.getByteCount());
+        } catch (final IOException e) {
+            fail("IOException: " + e);
+        }
+    }
+
+    public void test4CharsInt8() {
+        final Base64Reader reader = new Base64Reader(
+                new LineNumberReader(new StringReader(COMPLETE_CHAR4)));
+        try {
+            final int v1 = reader.readUint8();
+            assertEquals("4 chars pos 0", 'A', v1);
+            final int v2 = reader.readUint8();
+            assertEquals("4 chars pos 1", 'B', v2);
+            final int v3 = reader.readUint8();
+            assertEquals("4 chars pos 2", 'C', v3);
+            reader.readUint8();
+            fail("4 chars");
+        } catch (final EOFException e) {
+            assertEquals("4 chars", 3, reader.getByteCount());
+        } catch (final IOException e) {
+            fail("IOException: " + e);
+        }
+    }
+
+    public void testAllBytePatternInt8() {
+        final Base64Reader reader = new Base64Reader(
+                new LineNumberReader(new StringReader(ALL_BYTE_PATTERN)));
+        try {
+            for (int i = 0; i <= 0xff; i++) {
+                final int v = reader.readUint8();
+                assertEquals("value: all byte pattern: pos " + i, i, v);
+                assertEquals("count: all byte pattern: pos " + i, i + 1, reader.getByteCount());
+            }
+        } catch (final EOFException e) {
+            assertEquals("all byte pattern", 256, reader.getByteCount());
+        } catch (final IOException e) {
+            fail("IOException: " + e);
+        }
+    }
+
+    public void test0CharInt16() {
+        final Base64Reader reader = new Base64Reader(
+                new LineNumberReader(new StringReader(EMPTY_STRING)));
+        try {
+            reader.readInt16();
+            fail("0 char");
+        } catch (final EOFException e) {
+            assertEquals("0 char", 0, reader.getByteCount());
+        } catch (final IOException e) {
+            fail("IOException: " + e);
+        }
+    }
+
+    public void test1CharInt16() {
+        final Base64Reader reader = new Base64Reader(
+                new LineNumberReader(new StringReader(INCOMPLETE_CHAR1)));
+        try {
+            reader.readInt16();
+            fail("1 char");
+        } catch (final EOFException e) {
+            assertEquals("1 char", 0, reader.getByteCount());
+        } catch (final IOException e) {
+            fail("IOException: " + e);
+        }
+    }
+
+    public void test2CharsInt16() {
+        final Base64Reader reader = new Base64Reader(
+                new LineNumberReader(new StringReader(INCOMPLETE_CHAR2)));
+        try {
+            reader.readInt16();
+            fail("2 chars");
+        } catch (final EOFException e) {
+            assertEquals("2 chars", 1, reader.getByteCount());
+        } catch (final IOException e) {
+            fail("IOException: " + e);
+        }
+    }
+
+    public void test3CharsInt16() {
+        final Base64Reader reader = new Base64Reader(
+                new LineNumberReader(new StringReader(INCOMPLETE_CHAR3)));
+        try {
+            final short v1 = reader.readInt16();
+            assertEquals("3 chars pos 0", 'A' << 8 | 'B', v1);
+            reader.readInt16();
+            fail("3 chars");
+        } catch (final EOFException e) {
+            assertEquals("3 chars", 2, reader.getByteCount());
+        } catch (final IOException e) {
+            fail("IOException: " + e);
+        }
+    }
+
+    public void test4CharsInt16() {
+        final Base64Reader reader = new Base64Reader(
+                new LineNumberReader(new StringReader(COMPLETE_CHAR4)));
+        try {
+            final short v1 = reader.readInt16();
+            assertEquals("4 chars pos 0", 'A' << 8 | 'B', v1);
+            reader.readInt16();
+            fail("4 chars");
+        } catch (final EOFException e) {
+            assertEquals("4 chars", 3, reader.getByteCount());
+        } catch (final IOException e) {
+            fail("IOException: " + e);
+        }
+    }
+
+    public void testAllBytePatternInt16() {
+        final Base64Reader reader = new Base64Reader(
+                new LineNumberReader(new StringReader(ALL_BYTE_PATTERN)));
+        try {
+            for (int i = 0; i <= 0xff; i += 2) {
+                final short v = reader.readInt16();
+                final short expected = (short)(i << 8 | (i + 1));
+                assertEquals("value: all byte pattern: pos " + i, expected, v);
+                assertEquals("count: all byte pattern: pos " + i, i + 2, reader.getByteCount());
+            }
+        } catch (final EOFException e) {
+            assertEquals("all byte pattern", 256, reader.getByteCount());
+        } catch (final IOException e) {
+            fail("IOException: " + e);
+        }
+    }
+}