Merge "Use text input for various internationalized listeners" into oc-dev
diff --git a/core/java/android/text/method/DateKeyListener.java b/core/java/android/text/method/DateKeyListener.java
index e14cd2c..0accbf6 100644
--- a/core/java/android/text/method/DateKeyListener.java
+++ b/core/java/android/text/method/DateKeyListener.java
@@ -22,6 +22,7 @@
 import android.view.KeyEvent;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ArrayUtils;
 
 import java.util.HashMap;
 import java.util.LinkedHashSet;
@@ -37,8 +38,11 @@
 public class DateKeyListener extends NumberKeyListener
 {
     public int getInputType() {
-        return InputType.TYPE_CLASS_DATETIME
-                | InputType.TYPE_DATETIME_VARIATION_DATE;
+        if (mNeedsAdvancedInput) {
+            return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL;
+        } else {
+            return InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_DATE;
+        }
     }
 
     @Override
@@ -65,7 +69,13 @@
         final boolean success = NumberKeyListener.addDigits(chars, locale)
                                 && NumberKeyListener.addFormatCharsFromSkeletons(
                                         chars, locale, SKELETONS, SYMBOLS_TO_IGNORE);
-        mCharacters = success ? NumberKeyListener.collectionToArray(chars) : CHARACTERS;
+        if (success) {
+            mCharacters = NumberKeyListener.collectionToArray(chars);
+            mNeedsAdvancedInput = !ArrayUtils.containsAll(CHARACTERS, mCharacters);
+        } else {
+            mCharacters = CHARACTERS;
+            mNeedsAdvancedInput = false;
+        }
     }
 
     /**
@@ -110,6 +120,7 @@
         };
 
     private final char[] mCharacters;
+    private final boolean mNeedsAdvancedInput;
 
     private static final Object sLock = new Object();
     @GuardedBy("sLock")
diff --git a/core/java/android/text/method/DateTimeKeyListener.java b/core/java/android/text/method/DateTimeKeyListener.java
index 62e3ade..551db55 100644
--- a/core/java/android/text/method/DateTimeKeyListener.java
+++ b/core/java/android/text/method/DateTimeKeyListener.java
@@ -22,6 +22,7 @@
 import android.view.KeyEvent;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ArrayUtils;
 
 import java.util.HashMap;
 import java.util.LinkedHashSet;
@@ -37,10 +38,13 @@
 public class DateTimeKeyListener extends NumberKeyListener
 {
     public int getInputType() {
-        return InputType.TYPE_CLASS_DATETIME
-                | InputType.TYPE_DATETIME_VARIATION_NORMAL;
+        if (mNeedsAdvancedInput) {
+            return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL;
+        } else {
+            return InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_NORMAL;
+        }
     }
-    
+
     @Override
     @NonNull
     protected char[] getAcceptedChars()
@@ -70,7 +74,13 @@
                               chars, locale, SKELETON_12HOUR, SYMBOLS_TO_IGNORE)
                           && NumberKeyListener.addFormatCharsFromSkeleton(
                               chars, locale, SKELETON_24HOUR, SYMBOLS_TO_IGNORE);
-        mCharacters = success ? NumberKeyListener.collectionToArray(chars) : CHARACTERS;
+        if (success) {
+            mCharacters = NumberKeyListener.collectionToArray(chars);
+            mNeedsAdvancedInput = !ArrayUtils.containsAll(CHARACTERS, mCharacters);
+        } else {
+            mCharacters = CHARACTERS;
+            mNeedsAdvancedInput = false;
+        }
     }
 
     /**
@@ -114,6 +124,7 @@
         };
 
     private final char[] mCharacters;
+    private final boolean mNeedsAdvancedInput;
 
     private static final Object sLock = new Object();
     @GuardedBy("sLock")
diff --git a/core/java/android/text/method/DigitsKeyListener.java b/core/java/android/text/method/DigitsKeyListener.java
index 26c69ab..d9f2dcf 100644
--- a/core/java/android/text/method/DigitsKeyListener.java
+++ b/core/java/android/text/method/DigitsKeyListener.java
@@ -27,6 +27,7 @@
 import android.view.KeyEvent;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ArrayUtils;
 
 import java.util.HashMap;
 import java.util.LinkedHashSet;
@@ -42,8 +43,12 @@
 public class DigitsKeyListener extends NumberKeyListener
 {
     private char[] mAccepted;
+    private boolean mNeedsAdvancedInput;
     private final boolean mSign;
     private final boolean mDecimal;
+    private final boolean mStringMode;
+    @Nullable
+    private final Locale mLocale;
 
     private static final String DEFAULT_DECIMAL_POINT_CHARS = ".";
     private static final String DEFAULT_SIGN_CHARS = "-+";
@@ -112,11 +117,17 @@
         this(locale, false, false);
     }
 
-    private void setToCompat(boolean sign, boolean decimal) {
+    private void setToCompat() {
         mDecimalPointChars = DEFAULT_DECIMAL_POINT_CHARS;
         mSignChars = DEFAULT_SIGN_CHARS;
-        final int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
+        final int kind = (mSign ? SIGN : 0) | (mDecimal ? DECIMAL : 0);
         mAccepted = COMPATIBILITY_CHARACTERS[kind];
+        mNeedsAdvancedInput = false;
+    }
+
+    private void calculateNeedForAdvancedInput() {
+        final int kind = (mSign ? SIGN : 0) | (mDecimal ? DECIMAL : 0);
+        mNeedsAdvancedInput = !ArrayUtils.containsAll(COMPATIBILITY_CHARACTERS[kind], mAccepted);
     }
 
     // Takes a sign string and strips off its bidi controls, if any.
@@ -144,14 +155,16 @@
     public DigitsKeyListener(@Nullable Locale locale, boolean sign, boolean decimal) {
         mSign = sign;
         mDecimal = decimal;
+        mStringMode = false;
+        mLocale = locale;
         if (locale == null) {
-            setToCompat(sign, decimal);
+            setToCompat();
             return;
         }
         LinkedHashSet<Character> chars = new LinkedHashSet<>();
         final boolean success = NumberKeyListener.addDigits(chars, locale);
         if (!success) {
-            setToCompat(sign, decimal);
+            setToCompat();
             return;
         }
         if (sign || decimal) {
@@ -161,7 +174,7 @@
                 final String plusString = stripBidiControls(symbols.getPlusSignString());
                 if (minusString.length() > 1 || plusString.length() > 1) {
                     // non-BMP and multi-character signs are not supported.
-                    setToCompat(sign, decimal);
+                    setToCompat();
                     return;
                 }
                 final char minus = minusString.charAt(0);
@@ -181,7 +194,7 @@
                 final String separatorString = symbols.getDecimalSeparatorString();
                 if (separatorString.length() > 1) {
                     // non-BMP and multi-character decimal separators are not supported.
-                    setToCompat(sign, decimal);
+                    setToCompat();
                     return;
                 }
                 final Character separatorChar = Character.valueOf(separatorString.charAt(0));
@@ -190,13 +203,19 @@
             }
         }
         mAccepted = NumberKeyListener.collectionToArray(chars);
+        calculateNeedForAdvancedInput();
     }
 
     private DigitsKeyListener(@NonNull final String accepted) {
         mSign = false;
         mDecimal = false;
+        mStringMode = true;
+        mLocale = null;
         mAccepted = new char[accepted.length()];
         accepted.getChars(0, accepted.length(), mAccepted, 0);
+        // Theoretically we may need advanced input, but for backward compatibility, we don't change
+        // the input type.
+        mNeedsAdvancedInput = false;
     }
 
     /**
@@ -280,13 +299,38 @@
         return result;
     }
 
-    public int getInputType() {
-        int contentType = InputType.TYPE_CLASS_NUMBER;
-        if (mSign) {
-            contentType |= InputType.TYPE_NUMBER_FLAG_SIGNED;
+    /**
+     * Returns a DigitsKeyListener based on an the settings of a existing DigitsKeyListener, with
+     * the locale modified.
+     *
+     * @hide
+     */
+    @NonNull
+    public static DigitsKeyListener getInstance(
+            @Nullable Locale locale,
+            @NonNull DigitsKeyListener listener) {
+        if (listener.mStringMode) {
+            return listener; // string-mode DigitsKeyListeners have no locale.
+        } else {
+            return getInstance(locale, listener.mSign, listener.mDecimal);
         }
-        if (mDecimal) {
-            contentType |= InputType.TYPE_NUMBER_FLAG_DECIMAL;
+    }
+
+    /**
+     * Returns the input type for the listener.
+     */
+    public int getInputType() {
+        int contentType;
+        if (mNeedsAdvancedInput) {
+            contentType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL;
+        } else {
+            contentType = InputType.TYPE_CLASS_NUMBER;
+            if (mSign) {
+                contentType |= InputType.TYPE_NUMBER_FLAG_SIGNED;
+            }
+            if (mDecimal) {
+                contentType |= InputType.TYPE_NUMBER_FLAG_DECIMAL;
+            }
         }
         return contentType;
     }
diff --git a/core/java/android/text/method/TimeKeyListener.java b/core/java/android/text/method/TimeKeyListener.java
index c9f9f9f..5b1db11 100644
--- a/core/java/android/text/method/TimeKeyListener.java
+++ b/core/java/android/text/method/TimeKeyListener.java
@@ -22,6 +22,7 @@
 import android.view.KeyEvent;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ArrayUtils;
 
 import java.util.HashMap;
 import java.util.LinkedHashSet;
@@ -37,8 +38,11 @@
 public class TimeKeyListener extends NumberKeyListener
 {
     public int getInputType() {
-        return InputType.TYPE_CLASS_DATETIME
-        | InputType.TYPE_DATETIME_VARIATION_TIME;
+        if (mNeedsAdvancedInput) {
+            return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL;
+        } else {
+            return InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_TIME;
+        }
     }
 
     @Override
@@ -70,7 +74,13 @@
                               chars, locale, SKELETON_12HOUR, SYMBOLS_TO_IGNORE)
                           && NumberKeyListener.addFormatCharsFromSkeleton(
                               chars, locale, SKELETON_24HOUR, SYMBOLS_TO_IGNORE);
-        mCharacters = success ? NumberKeyListener.collectionToArray(chars) : CHARACTERS;
+        if (success) {
+            mCharacters = NumberKeyListener.collectionToArray(chars);
+            mNeedsAdvancedInput = !ArrayUtils.containsAll(CHARACTERS, mCharacters);
+        } else {
+            mCharacters = CHARACTERS;
+            mNeedsAdvancedInput = false;
+        }
     }
 
     /**
@@ -114,6 +124,7 @@
         };
 
     private final char[] mCharacters;
+    private final boolean mNeedsAdvancedInput;
 
     private static final Object sLock = new Object();
     @GuardedBy("sLock")
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 1b60ebc..7d9253b 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -598,6 +598,11 @@
     private Layout mLayout;
     private boolean mLocalesChanged = false;
 
+    // True if setKeyListener() has been explicitly called
+    private boolean mListenerChanged = false;
+    // True if internationalized input should be used for numbers and date and time.
+    private final boolean mUseInternationalizedInput;
+
     @ViewDebug.ExportedProperty(category = "text")
     private int mGravity = Gravity.TOP | Gravity.START;
     private boolean mHorizontallyScrolling;
@@ -1356,6 +1361,9 @@
         final boolean numberPasswordInputType = variation
                 == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
 
+        mUseInternationalizedInput =
+                context.getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O;
+
         if (inputMethod != null) {
             Class<?> c;
 
@@ -1398,15 +1406,11 @@
             mEditor.mInputType = inputType = EditorInfo.TYPE_CLASS_PHONE;
         } else if (numeric != 0) {
             createEditorIfNeeded();
-            mEditor.mKeyListener = DigitsKeyListener.getInstance((numeric & SIGNED) != 0,
-                                                   (numeric & DECIMAL) != 0);
-            inputType = EditorInfo.TYPE_CLASS_NUMBER;
-            if ((numeric & SIGNED) != 0) {
-                inputType |= EditorInfo.TYPE_NUMBER_FLAG_SIGNED;
-            }
-            if ((numeric & DECIMAL) != 0) {
-                inputType |= EditorInfo.TYPE_NUMBER_FLAG_DECIMAL;
-            }
+            mEditor.mKeyListener = DigitsKeyListener.getInstance(
+                    mUseInternationalizedInput ? getTextLocale() : null,
+                    (numeric & SIGNED) != 0,
+                    (numeric & DECIMAL) != 0);
+            inputType = mEditor.mKeyListener.getInputType();
             mEditor.mInputType = inputType;
         } else if (autotext || autocap != -1) {
             TextKeyListener.Capitalize cap;
@@ -2308,19 +2312,13 @@
      * @attr ref android.R.styleable#TextView_autoText
      */
     public void setKeyListener(KeyListener input) {
+        mListenerChanged = true;
         setKeyListenerOnly(input);
         fixFocusableAndClickableSettings();
 
         if (input != null) {
             createEditorIfNeeded();
-            try {
-                mEditor.mInputType = mEditor.mKeyListener.getInputType();
-            } catch (IncompatibleClassChangeError e) {
-                mEditor.mInputType = EditorInfo.TYPE_CLASS_TEXT;
-            }
-            // Change inputType, without affecting transformation.
-            // No need to applySingleLine since mSingleLine is unchanged.
-            setInputTypeSingleLine(mSingleLine);
+            setInputTypeFromEditor();
         } else {
             if (mEditor != null) mEditor.mInputType = EditorInfo.TYPE_NULL;
         }
@@ -2329,6 +2327,17 @@
         if (imm != null) imm.restartInput(this);
     }
 
+    private void setInputTypeFromEditor() {
+        try {
+            mEditor.mInputType = mEditor.mKeyListener.getInputType();
+        } catch (IncompatibleClassChangeError e) {
+            mEditor.mInputType = EditorInfo.TYPE_CLASS_TEXT;
+        }
+        // Change inputType, without affecting transformation.
+        // No need to applySingleLine since mSingleLine is unchanged.
+        setInputTypeSingleLine(mSingleLine);
+    }
+
     private void setKeyListenerOnly(KeyListener input) {
         if (mEditor == null && input == null) return; // null is the default value
 
@@ -3390,6 +3399,29 @@
         return mTextPaint.getTextLocales();
     }
 
+    private void changeListenerLocaleTo(@NonNull Locale locale) {
+        if (mListenerChanged) {
+            // If a listener has been explicitly set, don't change it. We may break something.
+            return;
+        }
+        if (mEditor != null) {
+            KeyListener listener = mEditor.mKeyListener;
+            if (listener instanceof DigitsKeyListener) {
+                listener = DigitsKeyListener.getInstance(locale, (DigitsKeyListener) listener);
+            } else if (listener instanceof DateKeyListener) {
+                listener = DateKeyListener.getInstance(locale);
+            } else if (listener instanceof TimeKeyListener) {
+                listener = TimeKeyListener.getInstance(locale);
+            } else if (listener instanceof DateTimeKeyListener) {
+                listener = DateTimeKeyListener.getInstance(locale);
+            } else {
+                return;
+            }
+            setKeyListenerOnly(listener);
+            setInputTypeFromEditor();
+        }
+    }
+
     /**
      * Set the default {@link LocaleList} of the text in this TextView to a one-member list
      * containing just the given value.
@@ -3401,6 +3433,7 @@
     public void setTextLocale(@NonNull Locale locale) {
         mLocalesChanged = true;
         mTextPaint.setTextLocale(locale);
+        changeListenerLocaleTo(locale);
         if (mLayout != null) {
             nullLayouts();
             requestLayout();
@@ -3422,6 +3455,7 @@
     public void setTextLocales(@NonNull @Size(min = 1) LocaleList locales) {
         mLocalesChanged = true;
         mTextPaint.setTextLocales(locales);
+        changeListenerLocaleTo(locales.get(0));
         if (mLayout != null) {
             nullLayouts();
             requestLayout();
@@ -3433,7 +3467,9 @@
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
         if (!mLocalesChanged) {
-            mTextPaint.setTextLocales(LocaleList.getDefault());
+            final LocaleList locales = LocaleList.getDefault();
+            mTextPaint.setTextLocales(locales);
+            changeListenerLocaleTo(locales.get(0));
             if (mLayout != null) {
                 nullLayouts();
                 requestLayout();
@@ -5567,26 +5603,35 @@
             input = TextKeyListener.getInstance(autotext, cap);
         } else if (cls == EditorInfo.TYPE_CLASS_NUMBER) {
             input = DigitsKeyListener.getInstance(
+                    mUseInternationalizedInput ? getTextLocale() : null,
                     (type & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0,
                     (type & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0);
+            if (mUseInternationalizedInput) {
+                type = input.getInputType(); // Override type, if necessary for i18n.
+            }
         } else if (cls == EditorInfo.TYPE_CLASS_DATETIME) {
+            final Locale locale = mUseInternationalizedInput ? getTextLocale() : null;
             switch (type & EditorInfo.TYPE_MASK_VARIATION) {
                 case EditorInfo.TYPE_DATETIME_VARIATION_DATE:
-                    input = DateKeyListener.getInstance();
+                    input = DateKeyListener.getInstance(locale);
                     break;
                 case EditorInfo.TYPE_DATETIME_VARIATION_TIME:
-                    input = TimeKeyListener.getInstance();
+                    input = TimeKeyListener.getInstance(locale);
                     break;
                 default:
-                    input = DateTimeKeyListener.getInstance();
+                    input = DateTimeKeyListener.getInstance(locale);
                     break;
             }
+            if (mUseInternationalizedInput) {
+                type = input.getInputType(); // Override type, if necessary for i18n.
+            }
         } else if (cls == EditorInfo.TYPE_CLASS_PHONE) {
             input = DialerKeyListener.getInstance();
         } else {
             input = TextKeyListener.getInstance();
         }
         setRawInputType(type);
+        mListenerChanged = false;
         if (direct) {
             createEditorIfNeeded();
             mEditor.mKeyListener = input;
diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java
index f4dd5a6..2c8e4e0 100644
--- a/core/java/com/android/internal/util/ArrayUtils.java
+++ b/core/java/com/android/internal/util/ArrayUtils.java
@@ -236,6 +236,29 @@
         return false;
     }
 
+    public static boolean contains(@Nullable char[] array, char value) {
+        if (array == null) return false;
+        for (char element : array) {
+            if (element == value) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Test if all {@code check} items are contained in {@code array}.
+     */
+    public static <T> boolean containsAll(@Nullable char[] array, char[] check) {
+        if (check == null) return true;
+        for (char checkItem : check) {
+            if (!contains(array, checkItem)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     public static long total(@Nullable long[] array) {
         long total = 0;
         if (array != null) {