Create the path for specifying combining rules in the subtype

Also add the skeleton of the combining rules for Myanmar
reordering as a pilot.

Bug: 13945569
Change-Id: Iabeb6838f45f1efff482143595ce5b48543e0c43
diff --git a/java/res/xml/method.xml b/java/res/xml/method.xml
index 0156996..2adc957 100644
--- a/java/res/xml/method.xml
+++ b/java/res/xml/method.xml
@@ -503,7 +503,7 @@
             android:subtypeId="0xea266ea4"
             android:imeSubtypeLocale="my_MM"
             android:imeSubtypeMode="keyboard"
-            android:imeSubtypeExtraValue="KeyboardLayoutSet=myanmar,EmojiCapable"
+            android:imeSubtypeExtraValue="KeyboardLayoutSet=myanmar,EmojiCapable,CombiningRules=MyanmarReordering"
             android:isAsciiCapable="false"
     />
     <subtype android:icon="@drawable/ic_ime_switcher_dark"
diff --git a/java/src/com/android/inputmethod/event/CombinerChain.java b/java/src/com/android/inputmethod/event/CombinerChain.java
index 8b59dc5..990f7de 100644
--- a/java/src/com/android/inputmethod/event/CombinerChain.java
+++ b/java/src/com/android/inputmethod/event/CombinerChain.java
@@ -23,6 +23,7 @@
 import com.android.inputmethod.latin.utils.CollectionUtils;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 
 /**
  * This class implements the logic chain between receiving events and generating code points.
@@ -43,6 +44,13 @@
     private SpannableStringBuilder mStateFeedback;
     private final ArrayList<Combiner> mCombiners;
 
+    private static final HashMap<String, Class> IMPLEMENTED_COMBINERS
+            = new HashMap<String, Class>();
+    static {
+        IMPLEMENTED_COMBINERS.put("MyanmarReordering", MyanmarReordering.class);
+    }
+    private static final String COMBINER_SPEC_SEPARATOR = ";";
+
     /**
      * Create an combiner chain.
      *
@@ -56,6 +64,9 @@
         mCombiners = CollectionUtils.newArrayList();
         // The dead key combiner is always active, and always first
         mCombiners.add(new DeadKeyCombiner());
+        for (final Combiner combiner : combinerList) {
+            mCombiners.add(combiner);
+        }
         mCombinedText = new StringBuilder();
         mStateFeedback = new SpannableStringBuilder();
     }
@@ -114,4 +125,29 @@
         final SpannableStringBuilder s = new SpannableStringBuilder(mCombinedText);
         return s.append(mStateFeedback);
     }
+
+    public static Combiner[] createCombiners(final String spec) {
+        if (TextUtils.isEmpty(spec)) {
+            return new Combiner[0];
+        }
+        final String[] combinerDescriptors = spec.split(COMBINER_SPEC_SEPARATOR);
+        final Combiner[] combiners = new Combiner[combinerDescriptors.length];
+        int i = 0;
+        for (final String combinerDescriptor : combinerDescriptors) {
+            final Class combinerClass = IMPLEMENTED_COMBINERS.get(combinerDescriptor);
+            if (null == combinerClass) {
+                throw new RuntimeException("Unknown combiner descriptor: " + combinerDescriptor);
+            }
+            try {
+                combiners[i++] = (Combiner)combinerClass.newInstance();
+            } catch (InstantiationException e) {
+                throw new RuntimeException("Unable to instantiate combiner: " + combinerDescriptor,
+                        e);
+            } catch (IllegalAccessException e) {
+                throw new RuntimeException("Unable to instantiate combiner: " + combinerDescriptor,
+                        e);
+            }
+        }
+        return combiners;
+    }
 }
diff --git a/java/src/com/android/inputmethod/event/MyanmarReordering.java b/java/src/com/android/inputmethod/event/MyanmarReordering.java
new file mode 100644
index 0000000..0831b63
--- /dev/null
+++ b/java/src/com/android/inputmethod/event/MyanmarReordering.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2014 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.event;
+
+import java.util.ArrayList;
+
+/**
+ * A combiner that reorders input for Myanmar.
+ */
+public class MyanmarReordering implements Combiner {
+    @Override
+    public Event processEvent(ArrayList<Event> previousEvents, Event event) {
+        return event;
+    }
+
+    @Override
+    public CharSequence getCombiningStateFeedback() {
+        return "";
+    }
+
+    @Override
+    public void reset() {
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
index e71723a..5bc9b62 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -115,6 +115,11 @@
              */
             public static final String IS_ADDITIONAL_SUBTYPE = "isAdditionalSubtype";
 
+            /**
+             * The subtype extra value used to specify the combining rules.
+             */
+            public static final String COMBINING_RULES = "CombiningRules";
+
             private ExtraValue() {
                 // This utility class is not publicly instantiable.
             }
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index d64a1a6..7dc566a 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -733,6 +733,7 @@
         // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
         // is not guaranteed. It may even be called at the same time on a different thread.
         mSubtypeSwitcher.onSubtypeChanged(subtype);
+        mInputLogic.onSubtypeChanged(SubtypeLocaleUtils.getCombiningRulesExtraValue(subtype));
         loadKeyboard();
     }
 
@@ -808,7 +809,10 @@
 
         // The app calling setText() has the effect of clearing the composing
         // span, so we should reset our state unconditionally, even if restarting is true.
-        mInputLogic.startInput(restarting, editorInfo);
+        // We also tell the input logic about the combining rules for the current subtype, so
+        // it can adjust its combiners if needed.
+        mInputLogic.startInput(restarting, editorInfo,
+                mSubtypeSwitcher.getCombiningRulesExtraValueOfCurrentSubtype());
 
         // Note: the following does a round-trip IPC on the main thread: be careful
         final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
index 998a8b4..c8a2fb2 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
@@ -326,4 +326,8 @@
                 + DUMMY_EMOJI_SUBTYPE);
         return DUMMY_EMOJI_SUBTYPE;
     }
+
+    public String getCombiningRulesExtraValueOfCurrentSubtype() {
+        return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype());
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index d755195..cdee496 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -41,6 +41,7 @@
     public static final int CAPS_MODE_AUTO_SHIFT_LOCKED = 0x7;
 
     private CombinerChain mCombinerChain;
+    private String mCombiningSpec; // Memory so that we don't uselessly recreate the combiner chain
 
     // The list of events that served to compose this string.
     private final ArrayList<Event> mEvents;
@@ -91,6 +92,21 @@
     }
 
     /**
+     * Restart input with a new combining spec.
+     * @param combiningSpec The spec string for combining. This is found in the extra value.
+     */
+    public void restart(final String combiningSpec) {
+        final String nonNullCombiningSpec = null == combiningSpec ? "" : combiningSpec;
+        if (nonNullCombiningSpec.equals(mCombiningSpec)) {
+            mCombinerChain.reset();
+        } else {
+            mCombinerChain = new CombinerChain(CombinerChain.createCombiners(nonNullCombiningSpec));
+            mCombiningSpec = nonNullCombiningSpec;
+        }
+        reset();
+    }
+
+    /**
      * Clear out the keys registered so far.
      */
     public void reset() {
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index 75432fb..e80ee35 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -97,6 +97,11 @@
     private boolean mIsAutoCorrectionIndicatorOn;
     private long mDoubleSpacePeriodCountdownStart;
 
+    /**
+     * Create a new instance of the input logic.
+     * @param latinIME the instance of the parent LatinIME. We should remove this when we can.
+     * @param suggestionStripViewAccessor an object to access the suggestion strip view.
+     */
     public InputLogic(final LatinIME latinIME,
             final SuggestionStripViewAccessor suggestionStripViewAccessor) {
         mLatinIME = latinIME;
@@ -117,9 +122,12 @@
      *
      * @param restarting whether input is starting in the same field as before. Unused for now.
      * @param editorInfo the editorInfo associated with the editor.
+     * @param combiningSpec the combining spec string for this subtype
      */
-    public void startInput(final boolean restarting, final EditorInfo editorInfo) {
+    public void startInput(final boolean restarting, final EditorInfo editorInfo,
+            final String combiningSpec) {
         mEnteredText = null;
+        mWordComposer.restart(combiningSpec);
         resetComposingState(true /* alsoResetLastComposedWord */);
         mDeleteCount = 0;
         mSpaceState = SpaceState.NONE;
@@ -138,6 +146,14 @@
     }
 
     /**
+     * Call this when the subtype changes.
+     * @param combiningSpec the spec string for the combining rules
+     */
+    public void onSubtypeChanged(final String combiningSpec) {
+        mWordComposer.restart(combiningSpec);
+    }
+
+    /**
      * Clean up the input logic after input is finished.
      */
     public void finishInput() {
diff --git a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
index b37779b..938d271 100644
--- a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
@@ -324,4 +324,8 @@
     public static boolean isRtlLanguage(final InputMethodSubtype subtype) {
         return isRtlLanguage(getSubtypeLocale(subtype));
     }
+
+    public static String getCombiningRulesExtraValue(final InputMethodSubtype subtype) {
+        return subtype.getExtraValueOf(Constants.Subtype.ExtraValue.COMBINING_RULES);
+    }
 }
diff --git a/tools/dicttool/compat/com/android/inputmethod/event/CombinerChain.java b/tools/dicttool/compat/com/android/inputmethod/event/CombinerChain.java
index b68df1c..47f781e 100644
--- a/tools/dicttool/compat/com/android/inputmethod/event/CombinerChain.java
+++ b/tools/dicttool/compat/com/android/inputmethod/event/CombinerChain.java
@@ -20,6 +20,13 @@
 
 import java.util.ArrayList;
 
+/**
+ * Compatibility class that stands in for the combiner chain in LatinIME.
+ *
+ * This is not used by dicttool, it's just needed by the dependency chain.
+ */
+// TODO: there should not be a dependency to this in dicttool, so there
+// should be a sensible way to separate them cleanly.
 public class CombinerChain {
     private StringBuilder mComposingWord = new StringBuilder();
     public CombinerChain(final Combiner... combinerList) {}
@@ -35,4 +42,9 @@
     public void reset() {
         mComposingWord.setLength(0);
     }
+
+    public static Combiner[] createCombiners(final String spec) {
+        // Dicttool never uses a combiner at all, so we just return a zero-sized array.
+        return new Combiner[0];
+    }
 }