Bandaid over broken keyboard layout selection process.

Automatically select a keyboard layout if we have one that is device
specific and is made for our current locale. Also, provide a way of
requesting layouts for a specific input device rather than just
getting all of them. Custom layouts may not be appropriate for
typical keyboard devices (and custom keyboards may not work with
typical layouts).

Bug: 25062009

Change-Id: I3a0ae5ad68f956b936485791ceb78c347fad7d4f
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index 3601b39..d308d43 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -48,11 +48,12 @@
 
     // Keyboard layouts configuration.
     KeyboardLayout[] getKeyboardLayouts();
+    KeyboardLayout[] getKeyboardLayoutsForInputDevice(in InputDeviceIdentifier identifier);
     KeyboardLayout getKeyboardLayout(String keyboardLayoutDescriptor);
     String getCurrentKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier);
     void setCurrentKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier,
             String keyboardLayoutDescriptor);
-    String[] getKeyboardLayoutsForInputDevice(in InputDeviceIdentifier identifier);
+    String[] getEnabledKeyboardLayoutsForInputDevice(in InputDeviceIdentifier identifier);
     void addKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier,
             String keyboardLayoutDescriptor);
     void removeKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier,
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 618864f..4934752 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -471,6 +471,29 @@
     }
 
     /**
+     * Gets information about all supported keyboard layouts appropriate
+     * for a specific input device.
+     * <p>
+     * The input manager consults the built-in keyboard layouts as well
+     * as all keyboard layouts advertised by applications using a
+     * {@link #ACTION_QUERY_KEYBOARD_LAYOUTS} broadcast receiver.
+     * </p>
+     *
+     * @return A list of all supported keyboard layouts for a specific
+     * input device.
+     *
+     * @hide
+     */
+    public KeyboardLayout[] getKeyboardLayoutsForInputDevice(InputDeviceIdentifier identifier) {
+        try {
+            return mIm.getKeyboardLayoutsForInputDevice(identifier);
+        } catch (RemoteException ex) {
+            Log.w(TAG, "Could not get list of keyboard layouts for input device.", ex);
+            return new KeyboardLayout[0];
+        }
+    }
+
+    /**
      * Gets the keyboard layout with the specified descriptor.
      *
      * @param keyboardLayoutDescriptor The keyboard layout descriptor, as returned by
@@ -548,13 +571,13 @@
      * @return The keyboard layout descriptors.
      * @hide
      */
-    public String[] getKeyboardLayoutsForInputDevice(InputDeviceIdentifier identifier) {
+    public String[] getEnabledKeyboardLayoutsForInputDevice(InputDeviceIdentifier identifier) {
         if (identifier == null) {
             throw new IllegalArgumentException("inputDeviceDescriptor must not be null");
         }
 
         try {
-            return mIm.getKeyboardLayoutsForInputDevice(identifier);
+            return mIm.getEnabledKeyboardLayoutsForInputDevice(identifier);
         } catch (RemoteException ex) {
             Log.w(TAG, "Could not get keyboard layouts for input device.", ex);
             return ArrayUtils.emptyArray(String.class);
diff --git a/core/java/android/hardware/input/KeyboardLayout.java b/core/java/android/hardware/input/KeyboardLayout.java
index ed51402..584008c 100644
--- a/core/java/android/hardware/input/KeyboardLayout.java
+++ b/core/java/android/hardware/input/KeyboardLayout.java
@@ -19,6 +19,8 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import java.util.Locale;
+
 /**
  * Describes a keyboard layout.
  *
@@ -30,6 +32,9 @@
     private final String mLabel;
     private final String mCollection;
     private final int mPriority;
+    private final Locale[] mLocales;
+    private final int mVendorId;
+    private final int mProductId;
 
     public static final Parcelable.Creator<KeyboardLayout> CREATOR =
             new Parcelable.Creator<KeyboardLayout>() {
@@ -41,11 +46,19 @@
         }
     };
 
-    public KeyboardLayout(String descriptor, String label, String collection, int priority) {
+    public KeyboardLayout(String descriptor, String label, String collection, int priority,
+            Locale[] locales, int vid, int pid) {
         mDescriptor = descriptor;
         mLabel = label;
         mCollection = collection;
         mPriority = priority;
+        if (locales != null) {
+            mLocales = locales;
+        } else {
+            mLocales = new Locale[0];
+        }
+        mVendorId = vid;
+        mProductId = pid;
     }
 
     private KeyboardLayout(Parcel source) {
@@ -53,6 +66,13 @@
         mLabel = source.readString();
         mCollection = source.readString();
         mPriority = source.readInt();
+        int N = source.readInt();
+        mLocales = new Locale[N];
+        for (int i = 0; i < N; i++) {
+            mLocales[i] = Locale.forLanguageTag(source.readString());
+        }
+        mVendorId = source.readInt();
+        mProductId = source.readInt();
     }
 
     /**
@@ -83,6 +103,33 @@
         return mCollection;
     }
 
+    /**
+     * Gets the locales that this keyboard layout is intended for.
+     * This may be empty if a locale has not been assigned to this keyboard layout.
+     * @return The keyboard layout's intended locale.
+     */
+    public Locale[] getLocales() {
+        return mLocales;
+    }
+
+    /**
+     * Gets the vendor ID of the hardware device this keyboard layout is intended for.
+     * Returns -1 if this is not specific to any piece of hardware.
+     * @return The hardware vendor ID of the keyboard layout's intended device.
+     */
+    public int getVendorId() {
+        return mVendorId;
+    }
+
+    /**
+     * Gets the product ID of the hardware device this keyboard layout is intended for.
+     * Returns -1 if this is not specific to any piece of hardware.
+     * @return The hardware product ID of the keyboard layout's intended device.
+     */
+    public int getProductId() {
+        return mProductId;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -94,6 +141,16 @@
         dest.writeString(mLabel);
         dest.writeString(mCollection);
         dest.writeInt(mPriority);
+        if (mLocales != null) {
+            dest.writeInt(mLocales.length);
+            for (Locale l : mLocales) {
+                dest.writeString(l.toLanguageTag());
+            }
+        } else {
+            dest.writeInt(0);
+        }
+        dest.writeInt(mVendorId);
+        dest.writeInt(mProductId);
     }
 
     @Override
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 67abe8d..2e574aa 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -7675,6 +7675,12 @@
         <attr name="label" />
         <!-- The key character map file resource. -->
         <attr name="keyboardLayout" format="reference" />
+        <!-- The locales the given keyboard layout corresponds to. -->
+        <attr name="locale" format="string" />
+        <!-- The vendor ID of the hardware the given layout corresponds to. @hide -->
+        <attr name="vendorId" format="integer" />
+        <!-- The product ID of the hardware the given layout corresponds to. @hide -->
+        <attr name="productId" format="integer" />
     </declare-styleable>
 
     <declare-styleable name="MediaRouteButton">
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 0205a20..38e6a32 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -69,7 +69,7 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.provider.Settings.SettingNotFoundException;
-import android.util.Log;
+import android.text.TextUtils;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.Xml;
@@ -93,9 +93,11 @@
 import java.io.InputStreamReader;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 
 import libcore.io.Streams;
 import libcore.util.Objects;
@@ -708,40 +710,35 @@
         mTempInputDevicesChangedListenersToNotify.clear();
 
         // Check for missing keyboard layouts.
-        if (mNotificationManager != null) {
-            final int numFullKeyboards = mTempFullKeyboards.size();
-            boolean missingLayoutForExternalKeyboard = false;
-            boolean missingLayoutForExternalKeyboardAdded = false;
-            boolean multipleMissingLayoutsForExternalKeyboardsAdded = false;
-            InputDevice keyboardMissingLayout = null;
-            synchronized (mDataStore) {
-                for (int i = 0; i < numFullKeyboards; i++) {
-                    final InputDevice inputDevice = mTempFullKeyboards.get(i);
-                    final String layout =
-                            getCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier());
-                    if (layout == null) {
-                        missingLayoutForExternalKeyboard = true;
-                        if (i < numFullKeyboardsAdded) {
-                            missingLayoutForExternalKeyboardAdded = true;
-                            if (keyboardMissingLayout == null) {
-                                keyboardMissingLayout = inputDevice;
-                            } else {
-                                multipleMissingLayoutsForExternalKeyboardsAdded = true;
-                            }
-                        }
+        List<InputDevice> keyboardsMissingLayout = new ArrayList<>();
+        final int numFullKeyboards = mTempFullKeyboards.size();
+        synchronized (mDataStore) {
+            for (int i = 0; i < numFullKeyboards; i++) {
+                final InputDevice inputDevice = mTempFullKeyboards.get(i);
+                String layout =
+                    getCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier());
+                if (layout == null) {
+                    layout = getDefaultKeyboardLayout(inputDevice);
+                    if (layout != null) {
+                        setCurrentKeyboardLayoutForInputDevice(
+                                inputDevice.getIdentifier(), layout);
                     }
                 }
+                if (layout == null) {
+                    keyboardsMissingLayout.add(inputDevice);
+                }
             }
-            if (missingLayoutForExternalKeyboard) {
-                if (missingLayoutForExternalKeyboardAdded) {
-                    if (multipleMissingLayoutsForExternalKeyboardsAdded) {
-                        // We have more than one keyboard missing a layout, so drop the
-                        // user at the generic input methods page so they can pick which
-                        // one to set.
-                        showMissingKeyboardLayoutNotification(null);
-                    } else {
-                        showMissingKeyboardLayoutNotification(keyboardMissingLayout);
-                    }
+        }
+
+        if (mNotificationManager != null) {
+            if (!keyboardsMissingLayout.isEmpty()) {
+                if (keyboardsMissingLayout.size() > 1) {
+                    // We have more than one keyboard missing a layout, so drop the
+                    // user at the generic input methods page so they can pick which
+                    // one to set.
+                    showMissingKeyboardLayoutNotification(null);
+                } else {
+                    showMissingKeyboardLayoutNotification(keyboardsMissingLayout.get(0));
                 }
             } else if (mKeyboardLayoutNotificationShown) {
                 hideMissingKeyboardLayoutNotification();
@@ -750,6 +747,78 @@
         mTempFullKeyboards.clear();
     }
 
+    private String getDefaultKeyboardLayout(final InputDevice d) {
+        final Locale systemLocale = mContext.getResources().getConfiguration().locale;
+        // If our locale doesn't have a language for some reason, then we don't really have a
+        // reasonable default.
+        if (TextUtils.isEmpty(systemLocale.getLanguage())) {
+            return null;
+        }
+        final List<KeyboardLayout> layouts = new ArrayList<>();
+        visitAllKeyboardLayouts(new KeyboardLayoutVisitor() {
+            @Override
+            public void visitKeyboardLayout(Resources resources,
+                    int keyboardLayoutResId, KeyboardLayout layout) {
+                // Only select a default when we know the layout is appropriate. For now, this
+                // means its a custom layout for a specific keyboard.
+                if (layout.getVendorId() != d.getVendorId()
+                        || layout.getProductId() != d.getProductId()) {
+                    return;
+                }
+                for (Locale l : layout.getLocales()) {
+                    if (isCompatibleLocale(systemLocale, l)) {
+                        layouts.add(layout);
+                        break;
+                    }
+                }
+            }
+        });
+
+        if (layouts.isEmpty()) {
+            return null;
+        }
+
+        // First sort so that ones with higher priority are listed at the top
+        Collections.sort(layouts);
+        // Next we want to try to find an exact match of language, country and variant.
+        final int N = layouts.size();
+        for (int i = 0; i < N; i++) {
+            KeyboardLayout layout = layouts.get(i);
+            for (Locale l : layout.getLocales()) {
+                if (l.getCountry().equals(systemLocale.getCountry())
+                        && l.getVariant().equals(systemLocale.getVariant())) {
+                    return layout.getDescriptor();
+                }
+            }
+        }
+        // Then try an exact match of language and country
+        for (int i = 0; i < N; i++) {
+            KeyboardLayout layout = layouts.get(i);
+            for (Locale l : layout.getLocales()) {
+                if (l.getCountry().equals(systemLocale.getCountry())) {
+                    return layout.getDescriptor();
+                }
+            }
+        }
+
+        // Give up and just use the highest priority layout with matching language
+        return layouts.get(0).getDescriptor();
+    }
+
+    private static boolean isCompatibleLocale(Locale systemLocale, Locale keyboardLocale) {
+        // Different languages are never compatible
+        if (!systemLocale.getLanguage().equals(keyboardLocale.getLanguage())) {
+            return false;
+        }
+        // If both the system and the keyboard layout have a country specifier, they must be equal.
+        if (!TextUtils.isEmpty(systemLocale.getCountry())
+                && !TextUtils.isEmpty(keyboardLocale.getCountry())
+                && !systemLocale.getCountry().equals(keyboardLocale.getCountry())) {
+            return false;
+        }
+        return true;
+    }
+
     @Override // Binder call & native callback
     public TouchCalibration getTouchCalibrationForInputDevice(String inputDeviceDescriptor,
             int surfaceRotation) {
@@ -899,9 +968,9 @@
         final HashSet<String> availableKeyboardLayouts = new HashSet<String>();
         visitAllKeyboardLayouts(new KeyboardLayoutVisitor() {
             @Override
-            public void visitKeyboardLayout(Resources resources, String descriptor, String label,
-                    String collection, int keyboardLayoutResId, int priority) {
-                availableKeyboardLayouts.add(descriptor);
+            public void visitKeyboardLayout(Resources resources,
+                    int keyboardLayoutResId, KeyboardLayout layout) {
+                availableKeyboardLayouts.add(layout.getDescriptor());
             }
         });
         synchronized (mDataStore) {
@@ -933,9 +1002,35 @@
         final ArrayList<KeyboardLayout> list = new ArrayList<KeyboardLayout>();
         visitAllKeyboardLayouts(new KeyboardLayoutVisitor() {
             @Override
-            public void visitKeyboardLayout(Resources resources, String descriptor, String label,
-                    String collection, int keyboardLayoutResId, int priority) {
-                list.add(new KeyboardLayout(descriptor, label, collection, priority));
+            public void visitKeyboardLayout(Resources resources,
+                    int keyboardLayoutResId, KeyboardLayout layout) {
+                list.add(layout);
+            }
+        });
+        return list.toArray(new KeyboardLayout[list.size()]);
+    }
+
+    @Override
+    public KeyboardLayout[] getKeyboardLayoutsForInputDevice(
+            final InputDeviceIdentifier identifier) {
+        final ArrayList<KeyboardLayout> list = new ArrayList<KeyboardLayout>();
+        visitAllKeyboardLayouts(new KeyboardLayoutVisitor() {
+            boolean mHasSeenDeviceSpecificLayout;
+
+            @Override
+            public void visitKeyboardLayout(Resources resources,
+                    int keyboardLayoutResId, KeyboardLayout layout) {
+                if (layout.getVendorId() == identifier.getVendorId()
+                        && layout.getProductId() == identifier.getProductId()) {
+                    if (!mHasSeenDeviceSpecificLayout) {
+                        mHasSeenDeviceSpecificLayout = true;
+                        list.clear();
+                    }
+                    list.add(layout);
+                } else if (layout.getVendorId() == -1 && layout.getProductId() == -1
+                        && !mHasSeenDeviceSpecificLayout) {
+                    list.add(layout);
+                }
             }
         });
         return list.toArray(new KeyboardLayout[list.size()]);
@@ -950,13 +1045,13 @@
         final KeyboardLayout[] result = new KeyboardLayout[1];
         visitKeyboardLayout(keyboardLayoutDescriptor, new KeyboardLayoutVisitor() {
             @Override
-            public void visitKeyboardLayout(Resources resources, String descriptor,
-                    String label, String collection, int keyboardLayoutResId, int priority) {
-                result[0] = new KeyboardLayout(descriptor, label, collection, priority);
+            public void visitKeyboardLayout(Resources resources,
+                    int keyboardLayoutResId, KeyboardLayout layout) {
+                result[0] = layout;
             }
         });
         if (result[0] == null) {
-            Log.w(TAG, "Could not get keyboard layout with descriptor '"
+            Slog.w(TAG, "Could not get keyboard layout with descriptor '"
                     + keyboardLayoutDescriptor + "'.");
         }
         return result[0];
@@ -997,7 +1092,7 @@
 
         int configResId = metaData.getInt(InputManager.META_DATA_KEYBOARD_LAYOUTS);
         if (configResId == 0) {
-            Log.w(TAG, "Missing meta-data '" + InputManager.META_DATA_KEYBOARD_LAYOUTS
+            Slog.w(TAG, "Missing meta-data '" + InputManager.META_DATA_KEYBOARD_LAYOUTS
                     + "' on receiver " + receiver.packageName + "/" + receiver.name);
             return;
         }
@@ -1034,8 +1129,16 @@
                             int keyboardLayoutResId = a.getResourceId(
                                     com.android.internal.R.styleable.KeyboardLayout_keyboardLayout,
                                     0);
+                            String languageTags = a.getString(
+                                    com.android.internal.R.styleable.KeyboardLayout_locale);
+                            Locale[] locales = getLocalesFromLanguageTags(languageTags);
+                            int vid = a.getInt(
+                                    com.android.internal.R.styleable.KeyboardLayout_vendorId, -1);
+                            int pid = a.getInt(
+                                    com.android.internal.R.styleable.KeyboardLayout_productId, -1);
+
                             if (name == null || label == null || keyboardLayoutResId == 0) {
-                                Log.w(TAG, "Missing required 'name', 'label' or 'keyboardLayout' "
+                                Slog.w(TAG, "Missing required 'name', 'label' or 'keyboardLayout' "
                                         + "attributes in keyboard layout "
                                         + "resource from receiver "
                                         + receiver.packageName + "/" + receiver.name);
@@ -1043,15 +1146,18 @@
                                 String descriptor = KeyboardLayoutDescriptor.format(
                                         receiver.packageName, receiver.name, name);
                                 if (keyboardName == null || name.equals(keyboardName)) {
-                                    visitor.visitKeyboardLayout(resources, descriptor,
-                                            label, collection, keyboardLayoutResId, priority);
+                                    KeyboardLayout layout = new KeyboardLayout(
+                                            descriptor, label, collection, priority,
+                                            locales, vid, pid);
+                                    visitor.visitKeyboardLayout(
+                                            resources, keyboardLayoutResId, layout);
                                 }
                             }
                         } finally {
                             a.recycle();
                         }
                     } else {
-                        Log.w(TAG, "Skipping unrecognized element '" + element
+                        Slog.w(TAG, "Skipping unrecognized element '" + element
                                 + "' in keyboard layout resource from receiver "
                                 + receiver.packageName + "/" + receiver.name);
                     }
@@ -1060,11 +1166,23 @@
                 parser.close();
             }
         } catch (Exception ex) {
-            Log.w(TAG, "Could not parse keyboard layout resource from receiver "
+            Slog.w(TAG, "Could not parse keyboard layout resource from receiver "
                     + receiver.packageName + "/" + receiver.name, ex);
         }
     }
 
+    private static Locale[] getLocalesFromLanguageTags(String languageTags) {
+        if (TextUtils.isEmpty(languageTags)) {
+            return new Locale[0];
+        }
+        String[] tags = languageTags.split("\\|");
+        Locale[] locales = new Locale[tags.length];
+        for (int i = 0; i < tags.length; i++) {
+            locales[i] = Locale.forLanguageTag(tags[i]);
+        }
+        return locales;
+    }
+
     /**
      * Builds a layout descriptor for the vendor/product. This returns the
      * descriptor for ids that aren't useful (such as the default 0, 0).
@@ -1130,7 +1248,7 @@
     }
 
     @Override // Binder call
-    public String[] getKeyboardLayoutsForInputDevice(InputDeviceIdentifier identifier) {
+    public String[] getEnabledKeyboardLayoutsForInputDevice(InputDeviceIdentifier identifier) {
         String key = getLayoutDescriptor(identifier);
         synchronized (mDataStore) {
             String[] layouts = mDataStore.getKeyboardLayouts(key);
@@ -1660,10 +1778,10 @@
         final String[] result = new String[2];
         visitKeyboardLayout(keyboardLayoutDescriptor, new KeyboardLayoutVisitor() {
             @Override
-            public void visitKeyboardLayout(Resources resources, String descriptor, String label,
-                    String collection, int keyboardLayoutResId, int priority) {
+            public void visitKeyboardLayout(Resources resources,
+                    int keyboardLayoutResId, KeyboardLayout layout) {
                 try {
-                    result[0] = descriptor;
+                    result[0] = layout.getDescriptor();
                     result[1] = Streams.readFully(new InputStreamReader(
                             resources.openRawResource(keyboardLayoutResId)));
                 } catch (IOException ex) {
@@ -1672,7 +1790,7 @@
             }
         });
         if (result[0] == null) {
-            Log.w(TAG, "Could not get keyboard layout with descriptor '"
+            Slog.w(TAG, "Could not get keyboard layout with descriptor '"
                     + keyboardLayoutDescriptor + "'.");
             return null;
         }
@@ -1815,8 +1933,8 @@
     }
 
     private interface KeyboardLayoutVisitor {
-        void visitKeyboardLayout(Resources resources, String descriptor, String label,
-                String collection, int keyboardLayoutResId, int priority);
+        void visitKeyboardLayout(Resources resources,
+                int keyboardLayoutResId, KeyboardLayout layout);
     }
 
     private final class InputDevicesChangedListenerRecord implements DeathRecipient {