Add subtypeId for keeping enabled "InputMethodSubtype"s even if subtype parameters are changed

Bug: 6752230
Change-Id: I3a2d512e395fe8645edf6ab82108948b927c629a
diff --git a/api/current.txt b/api/current.txt
index fbda3c6..5307833 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -947,6 +947,7 @@
     field public static final int subtitle = 16843473; // 0x10102d1
     field public static final int subtitleTextStyle = 16843513; // 0x10102f9
     field public static final int subtypeExtraValue = 16843674; // 0x101039a
+    field public static final int subtypeId = 16843713; // 0x10103c1
     field public static final int subtypeLocale = 16843673; // 0x1010399
     field public static final int suggestActionMsg = 16843228; // 0x10101dc
     field public static final int suggestActionMsgColumn = 16843229; // 0x10101dd
@@ -26642,6 +26643,7 @@
 
   public final class InputMethodSubtype implements android.os.Parcelable {
     ctor public InputMethodSubtype(int, int, java.lang.String, java.lang.String, java.lang.String, boolean, boolean);
+    ctor public InputMethodSubtype(int, int, java.lang.String, java.lang.String, java.lang.String, boolean, boolean, int);
     method public boolean containsExtraValueKey(java.lang.String);
     method public int describeContents();
     method public java.lang.CharSequence getDisplayName(android.content.Context, java.lang.String, android.content.pm.ApplicationInfo);
diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java
index 131f0ae..08e30aa 100644
--- a/core/java/android/view/inputmethod/InputMethodInfo.java
+++ b/core/java/android/view/inputmethod/InputMethodInfo.java
@@ -34,6 +34,7 @@
 import android.os.Parcelable;
 import android.util.AttributeSet;
 import android.util.Printer;
+import android.util.Slog;
 import android.util.Xml;
 
 import java.io.IOException;
@@ -169,7 +170,10 @@
                             a.getBoolean(com.android.internal.R.styleable
                                     .InputMethod_Subtype_isAuxiliary, false),
                             a.getBoolean(com.android.internal.R.styleable
-                                    .InputMethod_Subtype_overridesImplicitlyEnabledSubtype, false));
+                                    .InputMethod_Subtype_overridesImplicitlyEnabledSubtype, false),
+                            a.getInt(com.android.internal.R.styleable
+                                    .InputMethod_Subtype_subtypeId, 0 /* use Arrays.hashCode */)
+                            );
                     if (!subtype.isAuxiliary()) {
                         mIsAuxIme = false;
                     }
@@ -194,6 +198,9 @@
                 final InputMethodSubtype subtype = additionalSubtypes.get(i);
                 if (!mSubtypes.contains(subtype)) {
                     mSubtypes.add(subtype);
+                } else {
+                    Slog.w(TAG, "Duplicated subtype definition found: "
+                            + subtype.getLocale() + ", " + subtype.getMode());
                 }
             }
         }
diff --git a/core/java/android/view/inputmethod/InputMethodSubtype.java b/core/java/android/view/inputmethod/InputMethodSubtype.java
index b7c94a3..7895e6f 100644
--- a/core/java/android/view/inputmethod/InputMethodSubtype.java
+++ b/core/java/android/view/inputmethod/InputMethodSubtype.java
@@ -55,13 +55,14 @@
     private final int mSubtypeHashCode;
     private final int mSubtypeIconResId;
     private final int mSubtypeNameResId;
+    private final int mSubtypeId;
     private final String mSubtypeLocale;
     private final String mSubtypeMode;
     private final String mSubtypeExtraValue;
     private volatile HashMap<String, String> mExtraValueHashMapCache;
 
     /**
-     * Constructor.
+     * Constructor with no subtype ID specified, overridesImplicitlyEnabledSubtype not specified.
      * @param nameId Resource ID of the subtype name string. The string resource may have exactly
      * one %s in it. If there is, the %s part will be replaced with the locale's display name by
      * the formatter. Please refer to {@link #getDisplayName} for details.
@@ -87,7 +88,7 @@
     }
 
     /**
-     * Constructor.
+     * Constructor with no subtype ID specified.
      * @param nameId Resource ID of the subtype name string. The string resource may have exactly
      * one %s in it. If there is, the %s part will be replaced with the locale's display name by
      * the formatter. Please refer to {@link #getDisplayName} for details.
@@ -112,6 +113,41 @@
      */
     public InputMethodSubtype(int nameId, int iconId, String locale, String mode, String extraValue,
             boolean isAuxiliary, boolean overridesImplicitlyEnabledSubtype) {
+        this(nameId, iconId, locale, mode, extraValue, isAuxiliary,
+                overridesImplicitlyEnabledSubtype, 0);
+    }
+
+    /**
+     * Constructor.
+     * @param nameId Resource ID of the subtype name string. The string resource may have exactly
+     * one %s in it. If there is, the %s part will be replaced with the locale's display name by
+     * the formatter. Please refer to {@link #getDisplayName} for details.
+     * @param iconId Resource ID of the subtype icon drawable.
+     * @param locale The locale supported by the subtype
+     * @param mode The mode supported by the subtype
+     * @param extraValue The extra value of the subtype. This string is free-form, but the API
+     * supplies tools to deal with a key-value comma-separated list; see
+     * {@link #containsExtraValueKey} and {@link #getExtraValueOf}.
+     * @param isAuxiliary true when this subtype is auxiliary, false otherwise. An auxiliary
+     * subtype will not be shown in the list of enabled IMEs for choosing the current IME in
+     * the Settings even when this subtype is enabled. Please note that this subtype will still
+     * be shown in the list of IMEs in the IME switcher to allow the user to tentatively switch
+     * to this subtype while an IME is shown. The framework will never switch the current IME to
+     * this subtype by {@link android.view.inputmethod.InputMethodManager#switchToLastInputMethod}.
+     * The intent of having this flag is to allow for IMEs that are invoked in a one-shot way as
+     * auxiliary input mode, and return to the previous IME once it is finished (e.g. voice input).
+     * @param overridesImplicitlyEnabledSubtype true when this subtype should be enabled by default
+     * if no other subtypes in the IME are enabled explicitly. Note that a subtype with this
+     * parameter being true will not be shown in the list of subtypes in each IME's subtype enabler.
+     * Having an "automatic" subtype is an example use of this flag.
+     * @param id The unique ID for the subtype. The input method framework keeps track of enabled
+     * subtypes by ID. When the IME package gets upgraded, enabled IDs will stay enabled even if
+     * other attributes are different. If the ID is unspecified or 0,
+     * Arrays.hashCode(new Object[] {locale, mode, extraValue,
+     * isAuxiliary, overridesImplicitlyEnabledSubtype}) will be used instead.
+     */
+    public InputMethodSubtype(int nameId, int iconId, String locale, String mode, String extraValue,
+            boolean isAuxiliary, boolean overridesImplicitlyEnabledSubtype, int id) {
         mSubtypeNameResId = nameId;
         mSubtypeIconResId = iconId;
         mSubtypeLocale = locale != null ? locale : "";
@@ -119,8 +155,11 @@
         mSubtypeExtraValue = extraValue != null ? extraValue : "";
         mIsAuxiliary = isAuxiliary;
         mOverridesImplicitlyEnabledSubtype = overridesImplicitlyEnabledSubtype;
-        mSubtypeHashCode = hashCodeInternal(mSubtypeLocale, mSubtypeMode, mSubtypeExtraValue,
-                mIsAuxiliary, mOverridesImplicitlyEnabledSubtype);
+        // If hashCode() of this subtype is 0 and you want to specify it as an id of this subtype,
+        // just specify 0 as this subtype's id. Then, this subtype's id is treated as 0.
+        mSubtypeHashCode = id != 0 ? id : hashCodeInternal(mSubtypeLocale, mSubtypeMode,
+                mSubtypeExtraValue, mIsAuxiliary, mOverridesImplicitlyEnabledSubtype);
+        mSubtypeId = id;
     }
 
     InputMethodSubtype(Parcel source) {
@@ -135,8 +174,8 @@
         mSubtypeExtraValue = s != null ? s : "";
         mIsAuxiliary = (source.readInt() == 1);
         mOverridesImplicitlyEnabledSubtype = (source.readInt() == 1);
-        mSubtypeHashCode = hashCodeInternal(mSubtypeLocale, mSubtypeMode, mSubtypeExtraValue,
-                mIsAuxiliary, mOverridesImplicitlyEnabledSubtype);
+        mSubtypeHashCode = source.readInt();
+        mSubtypeId = source.readInt();
     }
 
     /**
@@ -288,6 +327,9 @@
     public boolean equals(Object o) {
         if (o instanceof InputMethodSubtype) {
             InputMethodSubtype subtype = (InputMethodSubtype) o;
+            if (subtype.mSubtypeId != 0 || mSubtypeId != 0) {
+                return (subtype.hashCode() == hashCode());
+            }
             return (subtype.hashCode() == hashCode())
                 && (subtype.getNameResId() == getNameResId())
                 && (subtype.getMode().equals(getMode()))
@@ -313,6 +355,8 @@
         dest.writeString(mSubtypeExtraValue);
         dest.writeInt(mIsAuxiliary ? 1 : 0);
         dest.writeInt(mOverridesImplicitlyEnabledSubtype ? 1 : 0);
+        dest.writeInt(mSubtypeHashCode);
+        dest.writeInt(mSubtypeId);
     }
 
     public static final Parcelable.Creator<InputMethodSubtype> CREATOR
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 3757afc..9601ad4 100755
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -2374,6 +2374,12 @@
         <!-- The extra value of the subtype. This string can be any string and will be passed to
              the IME when the framework calls the IME with the subtype.  -->
         <attr name="imeSubtypeExtraValue" format="string" />
+        <!-- The unique id for the subtype. The input method framework keeps track of enabled
+             subtypes by ID. When the IME package gets upgraded, enabled IDs will stay enabled even
+             if other attributes are different. If the ID is unspecified (by calling the other
+             constructor or 0. Arrays.hashCode(new Object[] {locale, mode, extraValue,
+             isAuxiliary, overridesImplicitlyEnabledSubtype}) will be used instead. -->
+        <attr name="subtypeId" format="integer"/>
     </declare-styleable>
 
     <!-- Use <code>spell-checker</code> as the root tag of the XML resource that
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 8e0eb15..5bb4a30 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -3761,5 +3761,6 @@
   <public type="attr" name="listPreferredItemPaddingEnd" />
   <public type="attr" name="singleUser" />
   <public type="attr" name="presentationTheme" />
+  <public type="attr" name="subtypeId"/>
 
 </resources>
diff --git a/services/java/com/android/server/InputMethodManagerService.java b/services/java/com/android/server/InputMethodManagerService.java
index 747cf0b..c685473 100644
--- a/services/java/com/android/server/InputMethodManagerService.java
+++ b/services/java/com/android/server/InputMethodManagerService.java
@@ -3579,7 +3579,7 @@
         private static final String ATTR_IS_AUXILIARY = "isAuxiliary";
         private final AtomicFile mAdditionalInputMethodSubtypeFile;
         private final HashMap<String, InputMethodInfo> mMethodMap;
-        private final HashMap<String, List<InputMethodSubtype>> mSubtypesMap =
+        private final HashMap<String, List<InputMethodSubtype>> mAdditionalSubtypesMap =
                 new HashMap<String, List<InputMethodSubtype>>();
         public InputMethodFileManager(HashMap<String, InputMethodInfo> methodMap) {
             if (methodMap == null) {
@@ -3595,18 +3595,19 @@
             mAdditionalInputMethodSubtypeFile = new AtomicFile(subtypeFile);
             if (!subtypeFile.exists()) {
                 // If "subtypes.xml" doesn't exist, create a blank file.
-                writeAdditionalInputMethodSubtypes(mSubtypesMap, mAdditionalInputMethodSubtypeFile,
-                        methodMap);
+                writeAdditionalInputMethodSubtypes(
+                        mAdditionalSubtypesMap, mAdditionalInputMethodSubtypeFile, methodMap);
             } else {
-                readAdditionalInputMethodSubtypes(mSubtypesMap, mAdditionalInputMethodSubtypeFile);
+                readAdditionalInputMethodSubtypes(
+                        mAdditionalSubtypesMap, mAdditionalInputMethodSubtypeFile);
             }
         }
 
         private void deleteAllInputMethodSubtypes(String imiId) {
             synchronized (mMethodMap) {
-                mSubtypesMap.remove(imiId);
-                writeAdditionalInputMethodSubtypes(mSubtypesMap, mAdditionalInputMethodSubtypeFile,
-                        mMethodMap);
+                mAdditionalSubtypesMap.remove(imiId);
+                writeAdditionalInputMethodSubtypes(
+                        mAdditionalSubtypesMap, mAdditionalInputMethodSubtypeFile, mMethodMap);
             }
         }
 
@@ -3619,17 +3620,20 @@
                     final InputMethodSubtype subtype = additionalSubtypes[i];
                     if (!subtypes.contains(subtype)) {
                         subtypes.add(subtype);
+                    } else {
+                        Slog.w(TAG, "Duplicated subtype definition found: "
+                                + subtype.getLocale() + ", " + subtype.getMode());
                     }
                 }
-                mSubtypesMap.put(imi.getId(), subtypes);
-                writeAdditionalInputMethodSubtypes(mSubtypesMap, mAdditionalInputMethodSubtypeFile,
-                        mMethodMap);
+                mAdditionalSubtypesMap.put(imi.getId(), subtypes);
+                writeAdditionalInputMethodSubtypes(
+                        mAdditionalSubtypesMap, mAdditionalInputMethodSubtypeFile, mMethodMap);
             }
         }
 
         public HashMap<String, List<InputMethodSubtype>> getAllAdditionalInputMethodSubtypes() {
             synchronized (mMethodMap) {
-                return mSubtypesMap;
+                return mAdditionalSubtypesMap;
             }
         }