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
This commit is contained in:
Michael Wright
2015-10-30 16:20:13 +00:00
parent f5baedb000
commit 0748342d2e
5 changed files with 264 additions and 59 deletions

View File

@@ -49,11 +49,12 @@ interface IInputManager {
// 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,

View File

@@ -473,6 +473,29 @@ public final class InputManager {
}
}
/**
* 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.
*
@@ -551,13 +574,13 @@ public final class InputManager {
* @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);

View File

@@ -19,6 +19,8 @@ package android.hardware.input;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Locale;
/**
* Describes a keyboard layout.
*
@@ -30,6 +32,9 @@ public final class KeyboardLayout implements Parcelable,
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 final class KeyboardLayout implements Parcelable,
}
};
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 @@ public final class KeyboardLayout implements Parcelable,
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 @@ public final class KeyboardLayout implements Parcelable,
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 @@ public final class KeyboardLayout implements Parcelable,
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

View File

@@ -7836,6 +7836,12 @@ i
<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">

View File

@@ -70,7 +70,7 @@ import android.os.RemoteException;
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;
@@ -97,9 +97,11 @@ import java.io.IOException;
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;
@@ -720,40 +722,35 @@ public class InputManagerService extends IInputManager.Stub
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();
@@ -762,6 +759,78 @@ public class InputManagerService extends IInputManager.Stub
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) {
@@ -911,9 +980,9 @@ public class InputManagerService extends IInputManager.Stub
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) {
@@ -945,9 +1014,35 @@ public class InputManagerService extends IInputManager.Stub
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()]);
@@ -962,13 +1057,13 @@ public class InputManagerService extends IInputManager.Stub
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];
@@ -1010,7 +1105,7 @@ public class InputManagerService extends IInputManager.Stub
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;
}
@@ -1047,8 +1142,16 @@ public class InputManagerService extends IInputManager.Stub
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);
@@ -1056,15 +1159,18 @@ public class InputManagerService extends IInputManager.Stub
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);
}
@@ -1073,11 +1179,23 @@ public class InputManagerService extends IInputManager.Stub
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).
@@ -1143,7 +1261,7 @@ public class InputManagerService extends IInputManager.Stub
}
@Override // Binder call
public String[] getKeyboardLayoutsForInputDevice(InputDeviceIdentifier identifier) {
public String[] getEnabledKeyboardLayoutsForInputDevice(InputDeviceIdentifier identifier) {
String key = getLayoutDescriptor(identifier);
synchronized (mDataStore) {
String[] layouts = mDataStore.getKeyboardLayouts(key);
@@ -1718,10 +1836,10 @@ public class InputManagerService extends IInputManager.Stub
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) {
@@ -1730,7 +1848,7 @@ public class InputManagerService extends IInputManager.Stub
}
});
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;
}
@@ -1883,8 +2001,8 @@ public class InputManagerService extends IInputManager.Stub
}
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 {