summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/hardware/input/IInputManager.aidl16
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java677
-rw-r--r--services/core/java/com/android/server/input/KeyboardLayoutManager.java736
3 files changed, 781 insertions, 648 deletions
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index fd3d1ac09e3f..bdcbcaacfae4 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -80,14 +80,30 @@ interface IInputManager {
// Keyboard layouts configuration.
KeyboardLayout[] getKeyboardLayouts();
+
KeyboardLayout[] getKeyboardLayoutsForInputDevice(in InputDeviceIdentifier identifier);
+
KeyboardLayout getKeyboardLayout(String keyboardLayoutDescriptor);
+
String getCurrentKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier);
+
+ @EnforcePermission("SET_KEYBOARD_LAYOUT")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ + "android.Manifest.permission.SET_KEYBOARD_LAYOUT)")
void setCurrentKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor);
+
String[] getEnabledKeyboardLayoutsForInputDevice(in InputDeviceIdentifier identifier);
+
+ @EnforcePermission("SET_KEYBOARD_LAYOUT")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ + "android.Manifest.permission.SET_KEYBOARD_LAYOUT)")
void addKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor);
+
+ @EnforcePermission("SET_KEYBOARD_LAYOUT")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ + "android.Manifest.permission.SET_KEYBOARD_LAYOUT)")
void removeKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor);
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index c20d8806412d..ab6b58533fb3 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -25,25 +25,13 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManagerInternal;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
-import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
-import android.content.res.TypedArray;
-import android.content.res.XmlResourceParser;
import android.database.ContentObserver;
import android.graphics.PointF;
import android.hardware.SensorPrivacyManager;
@@ -66,7 +54,6 @@ import android.hardware.lights.Light;
import android.hardware.lights.LightState;
import android.media.AudioManager;
import android.os.Binder;
-import android.os.Bundle;
import android.os.CombinedVibration;
import android.os.Environment;
import android.os.Handler;
@@ -75,7 +62,6 @@ import android.os.IInputConstants;
import android.os.IVibratorStateListener;
import android.os.InputEventInjectionResult;
import android.os.InputEventInjectionSync;
-import android.os.LocaleList;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
@@ -113,18 +99,14 @@ import android.view.SurfaceControl;
import android.view.VerifiedInputEvent;
import android.view.ViewConfiguration;
import android.view.inputmethod.InputMethodSubtype;
-import android.widget.Toast;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.inputmethod.InputMethodSubtypeHandle;
-import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
-import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.Preconditions;
-import com.android.internal.util.XmlUtils;
import com.android.server.DisplayThread;
import com.android.server.LocalServices;
import com.android.server.Watchdog;
@@ -132,7 +114,6 @@ import com.android.server.input.InputManagerInternal.LidSwitchCallback;
import com.android.server.policy.WindowManagerPolicy;
import libcore.io.IoUtils;
-import libcore.io.Streams;
import java.io.File;
import java.io.FileDescriptor;
@@ -141,15 +122,11 @@ import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
-import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.OptionalInt;
@@ -171,12 +148,9 @@ public class InputManagerService extends IInputManager.Stub
private static final String VELOCITYTRACKER_STRATEGY_PROPERTY = "velocitytracker_strategy";
private static final int MSG_DELIVER_INPUT_DEVICES_CHANGED = 1;
- private static final int MSG_SWITCH_KEYBOARD_LAYOUT = 2;
- private static final int MSG_RELOAD_KEYBOARD_LAYOUTS = 3;
- private static final int MSG_UPDATE_KEYBOARD_LAYOUTS = 4;
- private static final int MSG_RELOAD_DEVICE_ALIASES = 5;
- private static final int MSG_DELIVER_TABLET_MODE_CHANGED = 6;
- private static final int MSG_POINTER_DISPLAY_ID_CHANGED = 7;
+ private static final int MSG_RELOAD_DEVICE_ALIASES = 2;
+ private static final int MSG_DELIVER_TABLET_MODE_CHANGED = 3;
+ private static final int MSG_POINTER_DISPLAY_ID_CHANGED = 4;
private static final int DEFAULT_VIBRATION_MAGNITUDE = 192;
private static final AdditionalDisplayInputProperties
@@ -195,7 +169,6 @@ public class InputManagerService extends IInputManager.Stub
private WindowManagerCallbacks mWindowManagerCallbacks;
private WiredAccessoryCallbacks mWiredAccessoryCallbacks;
private boolean mSystemReady;
- private NotificationManager mNotificationManager;
private final Object mTabletModeLock = new Object();
// List of currently registered tablet mode changed listeners by process id
@@ -229,10 +202,6 @@ public class InputManagerService extends IInputManager.Stub
new SparseArray<>();
private final ArrayList<InputDevicesChangedListenerRecord>
mTempInputDevicesChangedListenersToNotify = new ArrayList<>(); // handler thread only
- private final ArrayList<InputDevice> mTempFullKeyboards =
- new ArrayList<>(); // handler thread only
- private boolean mKeyboardLayoutNotificationShown;
- private Toast mSwitchedKeyboardLayoutToast;
// State for vibrator tokens.
private final Object mVibratorLock = new Object();
@@ -315,6 +284,9 @@ public class InputManagerService extends IInputManager.Stub
@GuardedBy("mInputMonitors")
final Map<IBinder, GestureMonitorSpyWindow> mInputMonitors = new HashMap<>();
+ // Manages Keyboard layouts for Physical keyboards
+ private final KeyboardLayoutManager mKeyboardLayoutManager;
+
// Manages battery state for input devices.
private final BatteryController mBatteryController;
@@ -430,6 +402,8 @@ public class InputManagerService extends IInputManager.Stub
mContext = injector.getContext();
mHandler = new InputManagerHandler(injector.getLooper());
mNative = injector.getNativeService(this);
+ mKeyboardLayoutManager = new KeyboardLayoutManager(mContext, mNative, mDataStore,
+ injector.getLooper());
mBatteryController = new BatteryController(mContext, mNative, injector.getLooper());
mKeyboardBacklightController = new KeyboardBacklightController(mContext, mNative,
mDataStore, injector.getLooper());
@@ -518,8 +492,6 @@ public class InputManagerService extends IInputManager.Stub
if (DEBUG) {
Slog.d(TAG, "System ready.");
}
- mNotificationManager = (NotificationManager)mContext.getSystemService(
- Context.NOTIFICATION_SERVICE);
synchronized (mLidSwitchLock) {
mSystemReady = true;
@@ -546,19 +518,7 @@ public class InputManagerService extends IInputManager.Stub
setSensorPrivacy(Sensors.CAMERA, true);
}
- IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
- filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
- filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
- filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
- filter.addDataScheme("package");
- mContext.registerReceiver(new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- updateKeyboardLayouts();
- }
- }, filter, null, mHandler);
-
- filter = new IntentFilter(BluetoothDevice.ACTION_ALIAS_CHANGED);
+ IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_ALIAS_CHANGED);
mContext.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -567,23 +527,16 @@ public class InputManagerService extends IInputManager.Stub
}, filter, null, mHandler);
mHandler.sendEmptyMessage(MSG_RELOAD_DEVICE_ALIASES);
- mHandler.sendEmptyMessage(MSG_UPDATE_KEYBOARD_LAYOUTS);
if (mWiredAccessoryCallbacks != null) {
mWiredAccessoryCallbacks.systemReady();
}
+ mKeyboardLayoutManager.systemRunning();
mBatteryController.systemRunning();
mKeyboardBacklightController.systemRunning();
}
- private void reloadKeyboardLayouts() {
- if (DEBUG) {
- Slog.d(TAG, "Reloading keyboard layouts.");
- }
- mNative.reloadKeyboardLayouts();
- }
-
private void reloadDeviceAliases() {
if (DEBUG) {
Slog.d(TAG, "Reloading device names.");
@@ -1044,9 +997,7 @@ public class InputManagerService extends IInputManager.Stub
// Must be called on handler.
private void deliverInputDevicesChanged(InputDevice[] oldInputDevices) {
// Scan for changes.
- int numFullKeyboardsAdded = 0;
mTempInputDevicesChangedListenersToNotify.clear();
- mTempFullKeyboards.clear();
final int numListeners;
final int[] deviceIdAndGeneration;
synchronized (mInputDevicesLock) {
@@ -1071,15 +1022,6 @@ public class InputManagerService extends IInputManager.Stub
Log.d(TAG, "device " + inputDevice.getId() + " generation "
+ inputDevice.getGeneration());
}
-
- if (!inputDevice.isVirtual() && inputDevice.isFullKeyboard()) {
- if (!containsInputDeviceWithDescriptor(oldInputDevices,
- inputDevice.getDescriptor())) {
- mTempFullKeyboards.add(numFullKeyboardsAdded++, inputDevice);
- } else {
- mTempFullKeyboards.add(inputDevice);
- }
- }
}
}
@@ -1089,119 +1031,6 @@ public class InputManagerService extends IInputManager.Stub
deviceIdAndGeneration);
}
mTempInputDevicesChangedListenersToNotify.clear();
-
- // Check for missing keyboard layouts.
- 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 (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();
- }
- }
- 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((resources, keyboardLayoutResId, layout) -> {
- // Only select a default when we know the layout is appropriate. For now, this
- // means it's a custom layout for a specific keyboard.
- if (layout.getVendorId() != d.getVendorId()
- || layout.getProductId() != d.getProductId()) {
- return;
- }
- final LocaleList locales = layout.getLocales();
- final int numLocales = locales.size();
- for (int localeIndex = 0; localeIndex < numLocales; ++localeIndex) {
- if (isCompatibleLocale(systemLocale, locales.get(localeIndex))) {
- 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);
- final LocaleList locales = layout.getLocales();
- final int numLocales = locales.size();
- for (int localeIndex = 0; localeIndex < numLocales; ++localeIndex) {
- final Locale locale = locales.get(localeIndex);
- if (locale.getCountry().equals(systemLocale.getCountry())
- && locale.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);
- final LocaleList locales = layout.getLocales();
- final int numLocales = locales.size();
- for (int localeIndex = 0; localeIndex < numLocales; ++localeIndex) {
- final Locale locale = locales.get(localeIndex);
- if (locale.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
@@ -1302,446 +1131,61 @@ public class InputManagerService extends IInputManager.Stub
}
}
- // Must be called on handler.
- private void showMissingKeyboardLayoutNotification(InputDevice device) {
- if (!mKeyboardLayoutNotificationShown) {
- final Intent intent = new Intent(Settings.ACTION_HARD_KEYBOARD_SETTINGS);
- if (device != null) {
- intent.putExtra(Settings.EXTRA_INPUT_DEVICE_IDENTIFIER, device.getIdentifier());
- }
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
- | Intent.FLAG_ACTIVITY_CLEAR_TOP);
- final PendingIntent keyboardLayoutIntent = PendingIntent.getActivityAsUser(mContext, 0,
- intent, PendingIntent.FLAG_IMMUTABLE, null, UserHandle.CURRENT);
-
- Resources r = mContext.getResources();
- Notification notification =
- new Notification.Builder(mContext, SystemNotificationChannels.PHYSICAL_KEYBOARD)
- .setContentTitle(r.getString(
- R.string.select_keyboard_layout_notification_title))
- .setContentText(r.getString(
- R.string.select_keyboard_layout_notification_message))
- .setContentIntent(keyboardLayoutIntent)
- .setSmallIcon(R.drawable.ic_settings_language)
- .setColor(mContext.getColor(
- com.android.internal.R.color.system_notification_accent_color))
- .build();
- mNotificationManager.notifyAsUser(null,
- SystemMessage.NOTE_SELECT_KEYBOARD_LAYOUT,
- notification, UserHandle.ALL);
- mKeyboardLayoutNotificationShown = true;
- }
- }
-
- // Must be called on handler.
- private void hideMissingKeyboardLayoutNotification() {
- if (mKeyboardLayoutNotificationShown) {
- mKeyboardLayoutNotificationShown = false;
- mNotificationManager.cancelAsUser(null,
- SystemMessage.NOTE_SELECT_KEYBOARD_LAYOUT,
- UserHandle.ALL);
- }
- }
-
- // Must be called on handler.
- private void updateKeyboardLayouts() {
- // Scan all input devices state for keyboard layouts that have been uninstalled.
- final HashSet<String> availableKeyboardLayouts = new HashSet<String>();
- visitAllKeyboardLayouts((resources, keyboardLayoutResId, layout) ->
- availableKeyboardLayouts.add(layout.getDescriptor()));
- synchronized (mDataStore) {
- try {
- mDataStore.removeUninstalledKeyboardLayouts(availableKeyboardLayouts);
- } finally {
- mDataStore.saveIfNeeded();
- }
- }
-
- // Reload keyboard layouts.
- reloadKeyboardLayouts();
- }
-
- private static boolean containsInputDeviceWithDescriptor(InputDevice[] inputDevices,
- String descriptor) {
- final int numDevices = inputDevices.length;
- for (int i = 0; i < numDevices; i++) {
- final InputDevice inputDevice = inputDevices[i];
- if (inputDevice.getDescriptor().equals(descriptor)) {
- return true;
- }
- }
- return false;
- }
-
@Override // Binder call
public KeyboardLayout[] getKeyboardLayouts() {
- final ArrayList<KeyboardLayout> list = new ArrayList<>();
- visitAllKeyboardLayouts((resources, keyboardLayoutResId, layout) -> list.add(layout));
- return list.toArray(new KeyboardLayout[list.size()]);
+ return mKeyboardLayoutManager.getKeyboardLayouts();
}
@Override // Binder call
public KeyboardLayout[] getKeyboardLayoutsForInputDevice(
final InputDeviceIdentifier identifier) {
- final String[] enabledLayoutDescriptors =
- getEnabledKeyboardLayoutsForInputDevice(identifier);
- final ArrayList<KeyboardLayout> enabledLayouts =
- new ArrayList<>(enabledLayoutDescriptors.length);
- final ArrayList<KeyboardLayout> potentialLayouts = new ArrayList<>();
- visitAllKeyboardLayouts(new KeyboardLayoutVisitor() {
- boolean mHasSeenDeviceSpecificLayout;
-
- @Override
- public void visitKeyboardLayout(Resources resources,
- int keyboardLayoutResId, KeyboardLayout layout) {
- // First check if it's enabled. If the keyboard layout is enabled then we always
- // want to return it as a possible layout for the device.
- for (String s : enabledLayoutDescriptors) {
- if (s != null && s.equals(layout.getDescriptor())) {
- enabledLayouts.add(layout);
- return;
- }
- }
- // Next find any potential layouts that aren't yet enabled for the device. For
- // devices that have special layouts we assume there's a reason that the generic
- // layouts don't work for them so we don't want to return them since it's likely
- // to result in a poor user experience.
- if (layout.getVendorId() == identifier.getVendorId()
- && layout.getProductId() == identifier.getProductId()) {
- if (!mHasSeenDeviceSpecificLayout) {
- mHasSeenDeviceSpecificLayout = true;
- potentialLayouts.clear();
- }
- potentialLayouts.add(layout);
- } else if (layout.getVendorId() == -1 && layout.getProductId() == -1
- && !mHasSeenDeviceSpecificLayout) {
- potentialLayouts.add(layout);
- }
- }
- });
- final int enabledLayoutSize = enabledLayouts.size();
- final int potentialLayoutSize = potentialLayouts.size();
- KeyboardLayout[] layouts = new KeyboardLayout[enabledLayoutSize + potentialLayoutSize];
- enabledLayouts.toArray(layouts);
- for (int i = 0; i < potentialLayoutSize; i++) {
- layouts[enabledLayoutSize + i] = potentialLayouts.get(i);
- }
- return layouts;
+ return mKeyboardLayoutManager.getKeyboardLayoutsForInputDevice(identifier);
}
@Override // Binder call
public KeyboardLayout getKeyboardLayout(String keyboardLayoutDescriptor) {
- Objects.requireNonNull(keyboardLayoutDescriptor,
- "keyboardLayoutDescriptor must not be null");
-
- final KeyboardLayout[] result = new KeyboardLayout[1];
- visitKeyboardLayout(keyboardLayoutDescriptor,
- (resources, keyboardLayoutResId, layout) -> result[0] = layout);
- if (result[0] == null) {
- Slog.w(TAG, "Could not get keyboard layout with descriptor '"
- + keyboardLayoutDescriptor + "'.");
- }
- return result[0];
- }
-
- private void visitAllKeyboardLayouts(KeyboardLayoutVisitor visitor) {
- final PackageManager pm = mContext.getPackageManager();
- Intent intent = new Intent(InputManager.ACTION_QUERY_KEYBOARD_LAYOUTS);
- for (ResolveInfo resolveInfo : pm.queryBroadcastReceivers(intent,
- PackageManager.GET_META_DATA | PackageManager.MATCH_DIRECT_BOOT_AWARE
- | PackageManager.MATCH_DIRECT_BOOT_UNAWARE)) {
- final ActivityInfo activityInfo = resolveInfo.activityInfo;
- final int priority = resolveInfo.priority;
- visitKeyboardLayoutsInPackage(pm, activityInfo, null, priority, visitor);
- }
- }
-
- private void visitKeyboardLayout(String keyboardLayoutDescriptor,
- KeyboardLayoutVisitor visitor) {
- KeyboardLayoutDescriptor d = KeyboardLayoutDescriptor.parse(keyboardLayoutDescriptor);
- if (d != null) {
- final PackageManager pm = mContext.getPackageManager();
- try {
- ActivityInfo receiver = pm.getReceiverInfo(
- new ComponentName(d.packageName, d.receiverName),
- PackageManager.GET_META_DATA
- | PackageManager.MATCH_DIRECT_BOOT_AWARE
- | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
- visitKeyboardLayoutsInPackage(pm, receiver, d.keyboardLayoutName, 0, visitor);
- } catch (NameNotFoundException ignored) {
- }
- }
- }
-
- private void visitKeyboardLayoutsInPackage(PackageManager pm, ActivityInfo receiver,
- String keyboardName, int requestedPriority, KeyboardLayoutVisitor visitor) {
- Bundle metaData = receiver.metaData;
- if (metaData == null) {
- return;
- }
-
- int configResId = metaData.getInt(InputManager.META_DATA_KEYBOARD_LAYOUTS);
- if (configResId == 0) {
- Slog.w(TAG, "Missing meta-data '" + InputManager.META_DATA_KEYBOARD_LAYOUTS
- + "' on receiver " + receiver.packageName + "/" + receiver.name);
- return;
- }
-
- CharSequence receiverLabel = receiver.loadLabel(pm);
- String collection = receiverLabel != null ? receiverLabel.toString() : "";
- int priority;
- if ((receiver.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
- priority = requestedPriority;
- } else {
- priority = 0;
- }
-
- try {
- Resources resources = pm.getResourcesForApplication(receiver.applicationInfo);
- try (XmlResourceParser parser = resources.getXml(configResId)) {
- XmlUtils.beginDocument(parser, "keyboard-layouts");
-
- while (true) {
- XmlUtils.nextElement(parser);
- String element = parser.getName();
- if (element == null) {
- break;
- }
- if (element.equals("keyboard-layout")) {
- TypedArray a = resources.obtainAttributes(
- parser, R.styleable.KeyboardLayout);
- try {
- String name = a.getString(
- R.styleable.KeyboardLayout_name);
- String label = a.getString(
- R.styleable.KeyboardLayout_label);
- int keyboardLayoutResId = a.getResourceId(
- R.styleable.KeyboardLayout_keyboardLayout,
- 0);
- String languageTags = a.getString(
- R.styleable.KeyboardLayout_locale);
- LocaleList locales = getLocalesFromLanguageTags(languageTags);
- int vid = a.getInt(
- R.styleable.KeyboardLayout_vendorId, -1);
- int pid = a.getInt(
- R.styleable.KeyboardLayout_productId, -1);
-
- if (name == null || label == null || keyboardLayoutResId == 0) {
- Slog.w(TAG, "Missing required 'name', 'label' or 'keyboardLayout' "
- + "attributes in keyboard layout "
- + "resource from receiver "
- + receiver.packageName + "/" + receiver.name);
- } else {
- String descriptor = KeyboardLayoutDescriptor.format(
- receiver.packageName, receiver.name, name);
- if (keyboardName == null || name.equals(keyboardName)) {
- KeyboardLayout layout = new KeyboardLayout(
- descriptor, label, collection, priority,
- locales, vid, pid);
- visitor.visitKeyboardLayout(
- resources, keyboardLayoutResId, layout);
- }
- }
- } finally {
- a.recycle();
- }
- } else {
- Slog.w(TAG, "Skipping unrecognized element '" + element
- + "' in keyboard layout resource from receiver "
- + receiver.packageName + "/" + receiver.name);
- }
- }
- }
- } catch (Exception ex) {
- Slog.w(TAG, "Could not parse keyboard layout resource from receiver "
- + receiver.packageName + "/" + receiver.name, ex);
- }
- }
-
- @NonNull
- private static LocaleList getLocalesFromLanguageTags(String languageTags) {
- if (TextUtils.isEmpty(languageTags)) {
- return LocaleList.getEmptyLocaleList();
- }
- return LocaleList.forLanguageTags(languageTags.replace('|', ','));
- }
-
- /**
- * Builds a layout descriptor for the vendor/product. This returns the
- * descriptor for ids that aren't useful (such as the default 0, 0).
- */
- private String getLayoutDescriptor(InputDeviceIdentifier identifier) {
- Objects.requireNonNull(identifier, "identifier must not be null");
- Objects.requireNonNull(identifier.getDescriptor(), "descriptor must not be null");
-
- if (identifier.getVendorId() == 0 && identifier.getProductId() == 0) {
- return identifier.getDescriptor();
- }
- return "vendor:" + identifier.getVendorId() + ",product:" + identifier.getProductId();
+ return mKeyboardLayoutManager.getKeyboardLayout(keyboardLayoutDescriptor);
}
@Override // Binder call
public String getCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier) {
-
- String key = getLayoutDescriptor(identifier);
- synchronized (mDataStore) {
- String layout;
- // try loading it using the layout descriptor if we have it
- layout = mDataStore.getCurrentKeyboardLayout(key);
- if (layout == null && !key.equals(identifier.getDescriptor())) {
- // if it doesn't exist fall back to the device descriptor
- layout = mDataStore.getCurrentKeyboardLayout(identifier.getDescriptor());
- }
- if (DEBUG) {
- Slog.d(TAG, "getCurrentKeyboardLayoutForInputDevice() "
- + identifier.toString() + ": " + layout);
- }
- return layout;
- }
+ return mKeyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(identifier);
}
+ @EnforcePermission(Manifest.permission.SET_KEYBOARD_LAYOUT)
@Override // Binder call
public void setCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor) {
- if (!checkCallingPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT,
- "setCurrentKeyboardLayoutForInputDevice()")) {
- throw new SecurityException("Requires SET_KEYBOARD_LAYOUT permission");
- }
-
- Objects.requireNonNull(keyboardLayoutDescriptor,
- "keyboardLayoutDescriptor must not be null");
-
- String key = getLayoutDescriptor(identifier);
- synchronized (mDataStore) {
- try {
- if (mDataStore.setCurrentKeyboardLayout(key, keyboardLayoutDescriptor)) {
- if (DEBUG) {
- Slog.d(TAG, "setCurrentKeyboardLayoutForInputDevice() " + identifier
- + " key: " + key
- + " keyboardLayoutDescriptor: " + keyboardLayoutDescriptor);
- }
- mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
- }
- } finally {
- mDataStore.saveIfNeeded();
- }
- }
+ super.setCurrentKeyboardLayoutForInputDevice_enforcePermission();
+ mKeyboardLayoutManager.setCurrentKeyboardLayoutForInputDevice(identifier,
+ keyboardLayoutDescriptor);
}
@Override // Binder call
public String[] getEnabledKeyboardLayoutsForInputDevice(InputDeviceIdentifier identifier) {
- String key = getLayoutDescriptor(identifier);
- synchronized (mDataStore) {
- String[] layouts = mDataStore.getKeyboardLayouts(key);
- if ((layouts == null || layouts.length == 0)
- && !key.equals(identifier.getDescriptor())) {
- layouts = mDataStore.getKeyboardLayouts(identifier.getDescriptor());
- }
- return layouts;
- }
+ return mKeyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice(identifier);
}
+ @EnforcePermission(Manifest.permission.SET_KEYBOARD_LAYOUT)
@Override // Binder call
public void addKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor) {
- if (!checkCallingPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT,
- "addKeyboardLayoutForInputDevice()")) {
- throw new SecurityException("Requires SET_KEYBOARD_LAYOUT permission");
- }
- Objects.requireNonNull(keyboardLayoutDescriptor,
- "keyboardLayoutDescriptor must not be null");
-
- String key = getLayoutDescriptor(identifier);
- synchronized (mDataStore) {
- try {
- String oldLayout = mDataStore.getCurrentKeyboardLayout(key);
- if (oldLayout == null && !key.equals(identifier.getDescriptor())) {
- oldLayout = mDataStore.getCurrentKeyboardLayout(identifier.getDescriptor());
- }
- if (mDataStore.addKeyboardLayout(key, keyboardLayoutDescriptor)
- && !Objects.equals(oldLayout,
- mDataStore.getCurrentKeyboardLayout(key))) {
- mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
- }
- } finally {
- mDataStore.saveIfNeeded();
- }
- }
+ super.addKeyboardLayoutForInputDevice_enforcePermission();
+ mKeyboardLayoutManager.addKeyboardLayoutForInputDevice(identifier,
+ keyboardLayoutDescriptor);
}
+ @EnforcePermission(Manifest.permission.SET_KEYBOARD_LAYOUT)
@Override // Binder call
public void removeKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor) {
- if (!checkCallingPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT,
- "removeKeyboardLayoutForInputDevice()")) {
- throw new SecurityException("Requires SET_KEYBOARD_LAYOUT permission");
- }
- Objects.requireNonNull(keyboardLayoutDescriptor,
- "keyboardLayoutDescriptor must not be null");
-
- String key = getLayoutDescriptor(identifier);
- synchronized (mDataStore) {
- try {
- String oldLayout = mDataStore.getCurrentKeyboardLayout(key);
- if (oldLayout == null && !key.equals(identifier.getDescriptor())) {
- oldLayout = mDataStore.getCurrentKeyboardLayout(identifier.getDescriptor());
- }
- boolean removed = mDataStore.removeKeyboardLayout(key, keyboardLayoutDescriptor);
- if (!key.equals(identifier.getDescriptor())) {
- // We need to remove from both places to ensure it is gone
- removed |= mDataStore.removeKeyboardLayout(identifier.getDescriptor(),
- keyboardLayoutDescriptor);
- }
- if (removed && !Objects.equals(oldLayout,
- mDataStore.getCurrentKeyboardLayout(key))) {
- mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
- }
- } finally {
- mDataStore.saveIfNeeded();
- }
- }
+ super.removeKeyboardLayoutForInputDevice_enforcePermission();
+ mKeyboardLayoutManager.removeKeyboardLayoutForInputDevice(identifier,
+ keyboardLayoutDescriptor);
}
public void switchKeyboardLayout(int deviceId, int direction) {
- mHandler.obtainMessage(MSG_SWITCH_KEYBOARD_LAYOUT, deviceId, direction).sendToTarget();
- }
-
- // Must be called on handler.
- private void handleSwitchKeyboardLayout(int deviceId, int direction) {
- final InputDevice device = getInputDevice(deviceId);
- if (device != null) {
- final boolean changed;
- final String keyboardLayoutDescriptor;
-
- String key = getLayoutDescriptor(device.getIdentifier());
- synchronized (mDataStore) {
- try {
- changed = mDataStore.switchKeyboardLayout(key, direction);
- keyboardLayoutDescriptor = mDataStore.getCurrentKeyboardLayout(
- key);
- } finally {
- mDataStore.saveIfNeeded();
- }
- }
-
- if (changed) {
- if (mSwitchedKeyboardLayoutToast != null) {
- mSwitchedKeyboardLayoutToast.cancel();
- mSwitchedKeyboardLayoutToast = null;
- }
- if (keyboardLayoutDescriptor != null) {
- KeyboardLayout keyboardLayout = getKeyboardLayout(keyboardLayoutDescriptor);
- if (keyboardLayout != null) {
- mSwitchedKeyboardLayoutToast = Toast.makeText(
- mContext, keyboardLayout.getLabel(), Toast.LENGTH_SHORT);
- mSwitchedKeyboardLayoutToast.show();
- }
- }
-
- reloadKeyboardLayouts();
- }
- }
+ mKeyboardLayoutManager.switchKeyboardLayout(deviceId, direction);
}
public void setFocusedApplication(int displayId, InputApplicationHandle application) {
@@ -3252,28 +2696,7 @@ public class InputManagerService extends IInputManager.Stub
if (!mSystemReady) {
return null;
}
-
- String keyboardLayoutDescriptor = getCurrentKeyboardLayoutForInputDevice(identifier);
- if (keyboardLayoutDescriptor == null) {
- return null;
- }
-
- final String[] result = new String[2];
- visitKeyboardLayout(keyboardLayoutDescriptor,
- (resources, keyboardLayoutResId, layout) -> {
- try (InputStreamReader stream = new InputStreamReader(
- resources.openRawResource(keyboardLayoutResId))) {
- result[0] = layout.getDescriptor();
- result[1] = Streams.readFully(stream);
- } catch (IOException | NotFoundException ignored) {
- }
- });
- if (result[0] == null) {
- Slog.w(TAG, "Could not get keyboard layout with descriptor '"
- + keyboardLayoutDescriptor + "'.");
- return null;
- }
- return result;
+ return mKeyboardLayoutManager.getKeyboardLayoutOverlay(identifier);
}
// Native callback.
@@ -3471,15 +2894,6 @@ public class InputManagerService extends IInputManager.Stub
case MSG_DELIVER_INPUT_DEVICES_CHANGED:
deliverInputDevicesChanged((InputDevice[])msg.obj);
break;
- case MSG_SWITCH_KEYBOARD_LAYOUT:
- handleSwitchKeyboardLayout(msg.arg1, msg.arg2);
- break;
- case MSG_RELOAD_KEYBOARD_LAYOUTS:
- reloadKeyboardLayouts();
- break;
- case MSG_UPDATE_KEYBOARD_LAYOUTS:
- updateKeyboardLayouts();
- break;
case MSG_RELOAD_DEVICE_ALIASES:
reloadDeviceAliases();
break;
@@ -3548,39 +2962,6 @@ public class InputManagerService extends IInputManager.Stub
}
}
- private static final class KeyboardLayoutDescriptor {
- public String packageName;
- public String receiverName;
- public String keyboardLayoutName;
-
- public static String format(String packageName,
- String receiverName, String keyboardName) {
- return packageName + "/" + receiverName + "/" + keyboardName;
- }
-
- public static KeyboardLayoutDescriptor parse(String descriptor) {
- int pos = descriptor.indexOf('/');
- if (pos < 0 || pos + 1 == descriptor.length()) {
- return null;
- }
- int pos2 = descriptor.indexOf('/', pos + 1);
- if (pos2 < pos + 2 || pos2 + 1 == descriptor.length()) {
- return null;
- }
-
- KeyboardLayoutDescriptor result = new KeyboardLayoutDescriptor();
- result.packageName = descriptor.substring(0, pos);
- result.receiverName = descriptor.substring(pos + 1, pos2);
- result.keyboardLayoutName = descriptor.substring(pos2 + 1);
- return result;
- }
- }
-
- private interface KeyboardLayoutVisitor {
- void visitKeyboardLayout(Resources resources,
- int keyboardLayoutResId, KeyboardLayout layout);
- }
-
private final class InputDevicesChangedListenerRecord implements DeathRecipient {
private final int mPid;
private final IInputDevicesChangedListener mListener;
diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
new file mode 100644
index 000000000000..fac001e7828f
--- /dev/null
+++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
@@ -0,0 +1,736 @@
+/*
+ * Copyright (C) 2022 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.server.input;
+
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.hardware.input.InputDeviceIdentifier;
+import android.hardware.input.InputManager;
+import android.hardware.input.KeyboardLayout;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.LocaleList;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Slog;
+import android.view.InputDevice;
+import android.widget.Toast;
+
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.messages.nano.SystemMessageProto;
+import com.android.internal.notification.SystemNotificationChannels;
+import com.android.internal.util.XmlUtils;
+
+import libcore.io.Streams;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+/**
+ * A component of {@link InputManagerService} responsible for managing Physical Keyboard layouts.
+ *
+ * @hide
+ */
+final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
+
+ private static final String TAG = "KeyboardLayoutManager";
+
+ // To enable these logs, run: 'adb shell setprop log.tag.KeyboardLayoutManager DEBUG'
+ // (requires restart)
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private static final int MSG_SWITCH_KEYBOARD_LAYOUT = 1;
+ private static final int MSG_RELOAD_KEYBOARD_LAYOUTS = 2;
+ private static final int MSG_UPDATE_KEYBOARD_LAYOUTS = 3;
+
+ private final Context mContext;
+ private final NativeInputManagerService mNative;
+ // The PersistentDataStore should be locked before use.
+ @GuardedBy("mDataStore")
+ private final PersistentDataStore mDataStore;
+ private final Handler mHandler;
+ private final List<InputDevice> mKeyboardsWithMissingLayouts = new ArrayList<>();
+ private boolean mKeyboardLayoutNotificationShown = false;
+ private Toast mSwitchedKeyboardLayoutToast;
+
+ KeyboardLayoutManager(Context context, NativeInputManagerService nativeService,
+ PersistentDataStore dataStore, Looper looper) {
+ mContext = context;
+ mNative = nativeService;
+ mDataStore = dataStore;
+ mHandler = new Handler(looper, this::handleMessage, true /* async */);
+ }
+
+ public void systemRunning() {
+ // Listen to new Package installations to fetch new Keyboard layouts
+ IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+ filter.addDataScheme("package");
+ mContext.registerReceiver(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ updateKeyboardLayouts();
+ }
+ }, filter, null, mHandler);
+
+ mHandler.sendEmptyMessage(MSG_UPDATE_KEYBOARD_LAYOUTS);
+
+ // Listen to new InputDevice changes
+ InputManager inputManager = Objects.requireNonNull(
+ mContext.getSystemService(InputManager.class));
+ inputManager.registerInputDeviceListener(this, mHandler);
+ // Circle through all the already added input devices
+ for (int deviceId : inputManager.getInputDeviceIds()) {
+ onInputDeviceAdded(deviceId);
+ }
+ }
+
+ @Override
+ public void onInputDeviceAdded(int deviceId) {
+ onInputDeviceChanged(deviceId);
+ }
+
+ @Override
+ public void onInputDeviceRemoved(int deviceId) {
+ mKeyboardsWithMissingLayouts.removeIf(device -> device.getId() == deviceId);
+ maybeUpdateNotification();
+ }
+
+ @Override
+ public void onInputDeviceChanged(int deviceId) {
+ final InputDevice inputDevice = getInputDevice(deviceId);
+ if (inputDevice == null) {
+ return;
+ }
+ synchronized (mDataStore) {
+ String layout = getCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier());
+ if (layout == null) {
+ layout = getDefaultKeyboardLayout(inputDevice);
+ if (layout != null) {
+ setCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier(), layout);
+ } else {
+ mKeyboardsWithMissingLayouts.add(inputDevice);
+ }
+ }
+ maybeUpdateNotification();
+ }
+ }
+
+ private String getDefaultKeyboardLayout(final InputDevice inputDevice) {
+ 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((resources, keyboardLayoutResId, layout) -> {
+ // Only select a default when we know the layout is appropriate. For now, this
+ // means it's a custom layout for a specific keyboard.
+ if (layout.getVendorId() != inputDevice.getVendorId()
+ || layout.getProductId() != inputDevice.getProductId()) {
+ return;
+ }
+ final LocaleList locales = layout.getLocales();
+ for (int localeIndex = 0; localeIndex < locales.size(); ++localeIndex) {
+ final Locale locale = locales.get(localeIndex);
+ if (locale != null && isCompatibleLocale(systemLocale, locale)) {
+ 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.
+ for (KeyboardLayout layout : layouts) {
+ final LocaleList locales = layout.getLocales();
+ for (int localeIndex = 0; localeIndex < locales.size(); ++localeIndex) {
+ final Locale locale = locales.get(localeIndex);
+ if (locale != null && locale.getCountry().equals(systemLocale.getCountry())
+ && locale.getVariant().equals(systemLocale.getVariant())) {
+ return layout.getDescriptor();
+ }
+ }
+ }
+ // Then try an exact match of language and country
+ for (KeyboardLayout layout : layouts) {
+ final LocaleList locales = layout.getLocales();
+ for (int localeIndex = 0; localeIndex < locales.size(); ++localeIndex) {
+ final Locale locale = locales.get(localeIndex);
+ if (locale != null && locale.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.
+ return TextUtils.isEmpty(systemLocale.getCountry())
+ || TextUtils.isEmpty(keyboardLocale.getCountry())
+ || systemLocale.getCountry().equals(keyboardLocale.getCountry());
+ }
+
+ private void updateKeyboardLayouts() {
+ // Scan all input devices state for keyboard layouts that have been uninstalled.
+ final HashSet<String> availableKeyboardLayouts = new HashSet<String>();
+ visitAllKeyboardLayouts((resources, keyboardLayoutResId, layout) ->
+ availableKeyboardLayouts.add(layout.getDescriptor()));
+ synchronized (mDataStore) {
+ try {
+ mDataStore.removeUninstalledKeyboardLayouts(availableKeyboardLayouts);
+ } finally {
+ mDataStore.saveIfNeeded();
+ }
+ }
+
+ // Reload keyboard layouts.
+ reloadKeyboardLayouts();
+ }
+
+ public KeyboardLayout[] getKeyboardLayouts() {
+ final ArrayList<KeyboardLayout> list = new ArrayList<>();
+ visitAllKeyboardLayouts((resources, keyboardLayoutResId, layout) -> list.add(layout));
+ return list.toArray(new KeyboardLayout[0]);
+ }
+
+ public KeyboardLayout[] getKeyboardLayoutsForInputDevice(
+ final InputDeviceIdentifier identifier) {
+ final String[] enabledLayoutDescriptors =
+ getEnabledKeyboardLayoutsForInputDevice(identifier);
+ final ArrayList<KeyboardLayout> enabledLayouts =
+ new ArrayList<>(enabledLayoutDescriptors.length);
+ final ArrayList<KeyboardLayout> potentialLayouts = new ArrayList<>();
+ visitAllKeyboardLayouts(new KeyboardLayoutVisitor() {
+ boolean mHasSeenDeviceSpecificLayout;
+
+ @Override
+ public void visitKeyboardLayout(Resources resources,
+ int keyboardLayoutResId, KeyboardLayout layout) {
+ // First check if it's enabled. If the keyboard layout is enabled then we always
+ // want to return it as a possible layout for the device.
+ for (String s : enabledLayoutDescriptors) {
+ if (s != null && s.equals(layout.getDescriptor())) {
+ enabledLayouts.add(layout);
+ return;
+ }
+ }
+ // Next find any potential layouts that aren't yet enabled for the device. For
+ // devices that have special layouts we assume there's a reason that the generic
+ // layouts don't work for them so we don't want to return them since it's likely
+ // to result in a poor user experience.
+ if (layout.getVendorId() == identifier.getVendorId()
+ && layout.getProductId() == identifier.getProductId()) {
+ if (!mHasSeenDeviceSpecificLayout) {
+ mHasSeenDeviceSpecificLayout = true;
+ potentialLayouts.clear();
+ }
+ potentialLayouts.add(layout);
+ } else if (layout.getVendorId() == -1 && layout.getProductId() == -1
+ && !mHasSeenDeviceSpecificLayout) {
+ potentialLayouts.add(layout);
+ }
+ }
+ });
+ return Stream.concat(enabledLayouts.stream(), potentialLayouts.stream()).toArray(
+ KeyboardLayout[]::new);
+ }
+
+ public KeyboardLayout getKeyboardLayout(String keyboardLayoutDescriptor) {
+ Objects.requireNonNull(keyboardLayoutDescriptor,
+ "keyboardLayoutDescriptor must not be null");
+
+ final KeyboardLayout[] result = new KeyboardLayout[1];
+ visitKeyboardLayout(keyboardLayoutDescriptor,
+ (resources, keyboardLayoutResId, layout) -> result[0] = layout);
+ if (result[0] == null) {
+ Slog.w(TAG, "Could not get keyboard layout with descriptor '"
+ + keyboardLayoutDescriptor + "'.");
+ }
+ return result[0];
+ }
+
+ private void visitAllKeyboardLayouts(KeyboardLayoutVisitor visitor) {
+ final PackageManager pm = mContext.getPackageManager();
+ Intent intent = new Intent(InputManager.ACTION_QUERY_KEYBOARD_LAYOUTS);
+ for (ResolveInfo resolveInfo : pm.queryBroadcastReceivers(intent,
+ PackageManager.GET_META_DATA | PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE)) {
+ final ActivityInfo activityInfo = resolveInfo.activityInfo;
+ final int priority = resolveInfo.priority;
+ visitKeyboardLayoutsInPackage(pm, activityInfo, null, priority, visitor);
+ }
+ }
+
+ private void visitKeyboardLayout(String keyboardLayoutDescriptor,
+ KeyboardLayoutVisitor visitor) {
+ KeyboardLayoutDescriptor d = KeyboardLayoutDescriptor.parse(keyboardLayoutDescriptor);
+ if (d != null) {
+ final PackageManager pm = mContext.getPackageManager();
+ try {
+ ActivityInfo receiver = pm.getReceiverInfo(
+ new ComponentName(d.packageName, d.receiverName),
+ PackageManager.GET_META_DATA
+ | PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
+ visitKeyboardLayoutsInPackage(pm, receiver, d.keyboardLayoutName, 0, visitor);
+ } catch (PackageManager.NameNotFoundException ignored) {
+ }
+ }
+ }
+
+ private void visitKeyboardLayoutsInPackage(PackageManager pm, ActivityInfo receiver,
+ String keyboardName, int requestedPriority, KeyboardLayoutVisitor visitor) {
+ Bundle metaData = receiver.metaData;
+ if (metaData == null) {
+ return;
+ }
+
+ int configResId = metaData.getInt(InputManager.META_DATA_KEYBOARD_LAYOUTS);
+ if (configResId == 0) {
+ Slog.w(TAG, "Missing meta-data '" + InputManager.META_DATA_KEYBOARD_LAYOUTS
+ + "' on receiver " + receiver.packageName + "/" + receiver.name);
+ return;
+ }
+
+ CharSequence receiverLabel = receiver.loadLabel(pm);
+ String collection = receiverLabel != null ? receiverLabel.toString() : "";
+ int priority;
+ if ((receiver.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
+ priority = requestedPriority;
+ } else {
+ priority = 0;
+ }
+
+ try {
+ Resources resources = pm.getResourcesForApplication(receiver.applicationInfo);
+ try (XmlResourceParser parser = resources.getXml(configResId)) {
+ XmlUtils.beginDocument(parser, "keyboard-layouts");
+
+ while (true) {
+ XmlUtils.nextElement(parser);
+ String element = parser.getName();
+ if (element == null) {
+ break;
+ }
+ if (element.equals("keyboard-layout")) {
+ TypedArray a = resources.obtainAttributes(
+ parser, R.styleable.KeyboardLayout);
+ try {
+ String name = a.getString(
+ R.styleable.KeyboardLayout_name);
+ String label = a.getString(
+ R.styleable.KeyboardLayout_label);
+ int keyboardLayoutResId = a.getResourceId(
+ R.styleable.KeyboardLayout_keyboardLayout,
+ 0);
+ String languageTags = a.getString(
+ R.styleable.KeyboardLayout_locale);
+ LocaleList locales = getLocalesFromLanguageTags(languageTags);
+ int vid = a.getInt(
+ R.styleable.KeyboardLayout_vendorId, -1);
+ int pid = a.getInt(
+ R.styleable.KeyboardLayout_productId, -1);
+
+ if (name == null || label == null || keyboardLayoutResId == 0) {
+ Slog.w(TAG, "Missing required 'name', 'label' or 'keyboardLayout' "
+ + "attributes in keyboard layout "
+ + "resource from receiver "
+ + receiver.packageName + "/" + receiver.name);
+ } else {
+ String descriptor = KeyboardLayoutDescriptor.format(
+ receiver.packageName, receiver.name, name);
+ if (keyboardName == null || name.equals(keyboardName)) {
+ KeyboardLayout layout = new KeyboardLayout(
+ descriptor, label, collection, priority,
+ locales, vid, pid);
+ visitor.visitKeyboardLayout(
+ resources, keyboardLayoutResId, layout);
+ }
+ }
+ } finally {
+ a.recycle();
+ }
+ } else {
+ Slog.w(TAG, "Skipping unrecognized element '" + element
+ + "' in keyboard layout resource from receiver "
+ + receiver.packageName + "/" + receiver.name);
+ }
+ }
+ }
+ } catch (Exception ex) {
+ Slog.w(TAG, "Could not parse keyboard layout resource from receiver "
+ + receiver.packageName + "/" + receiver.name, ex);
+ }
+ }
+
+ @NonNull
+ private static LocaleList getLocalesFromLanguageTags(String languageTags) {
+ if (TextUtils.isEmpty(languageTags)) {
+ return LocaleList.getEmptyLocaleList();
+ }
+ return LocaleList.forLanguageTags(languageTags.replace('|', ','));
+ }
+
+ /**
+ * Builds a layout descriptor for the vendor/product. This returns the
+ * descriptor for ids that aren't useful (such as the default 0, 0).
+ */
+ private String getLayoutDescriptor(InputDeviceIdentifier identifier) {
+ Objects.requireNonNull(identifier, "identifier must not be null");
+ Objects.requireNonNull(identifier.getDescriptor(), "descriptor must not be null");
+
+ if (identifier.getVendorId() == 0 && identifier.getProductId() == 0) {
+ return identifier.getDescriptor();
+ }
+ return "vendor:" + identifier.getVendorId() + ",product:" + identifier.getProductId();
+ }
+
+ public String getCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier) {
+ String key = getLayoutDescriptor(identifier);
+ synchronized (mDataStore) {
+ String layout;
+ // try loading it using the layout descriptor if we have it
+ layout = mDataStore.getCurrentKeyboardLayout(key);
+ if (layout == null && !key.equals(identifier.getDescriptor())) {
+ // if it doesn't exist fall back to the device descriptor
+ layout = mDataStore.getCurrentKeyboardLayout(identifier.getDescriptor());
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "getCurrentKeyboardLayoutForInputDevice() "
+ + identifier.toString() + ": " + layout);
+ }
+ return layout;
+ }
+ }
+
+ public void setCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
+ String keyboardLayoutDescriptor) {
+ Objects.requireNonNull(keyboardLayoutDescriptor,
+ "keyboardLayoutDescriptor must not be null");
+
+ String key = getLayoutDescriptor(identifier);
+ synchronized (mDataStore) {
+ try {
+ if (mDataStore.setCurrentKeyboardLayout(key, keyboardLayoutDescriptor)) {
+ if (DEBUG) {
+ Slog.d(TAG, "setCurrentKeyboardLayoutForInputDevice() " + identifier
+ + " key: " + key
+ + " keyboardLayoutDescriptor: " + keyboardLayoutDescriptor);
+ }
+ mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
+ }
+ } finally {
+ mDataStore.saveIfNeeded();
+ }
+ }
+ }
+
+ public String[] getEnabledKeyboardLayoutsForInputDevice(InputDeviceIdentifier identifier) {
+ String key = getLayoutDescriptor(identifier);
+ synchronized (mDataStore) {
+ String[] layouts = mDataStore.getKeyboardLayouts(key);
+ if ((layouts == null || layouts.length == 0)
+ && !key.equals(identifier.getDescriptor())) {
+ layouts = mDataStore.getKeyboardLayouts(identifier.getDescriptor());
+ }
+ return layouts;
+ }
+ }
+
+ public void addKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
+ String keyboardLayoutDescriptor) {
+ Objects.requireNonNull(keyboardLayoutDescriptor,
+ "keyboardLayoutDescriptor must not be null");
+
+ String key = getLayoutDescriptor(identifier);
+ synchronized (mDataStore) {
+ try {
+ String oldLayout = mDataStore.getCurrentKeyboardLayout(key);
+ if (oldLayout == null && !key.equals(identifier.getDescriptor())) {
+ oldLayout = mDataStore.getCurrentKeyboardLayout(identifier.getDescriptor());
+ }
+ if (mDataStore.addKeyboardLayout(key, keyboardLayoutDescriptor)
+ && !Objects.equals(oldLayout,
+ mDataStore.getCurrentKeyboardLayout(key))) {
+ mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
+ }
+ } finally {
+ mDataStore.saveIfNeeded();
+ }
+ }
+ }
+
+ public void removeKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
+ String keyboardLayoutDescriptor) {
+ Objects.requireNonNull(keyboardLayoutDescriptor,
+ "keyboardLayoutDescriptor must not be null");
+
+ String key = getLayoutDescriptor(identifier);
+ synchronized (mDataStore) {
+ try {
+ String oldLayout = mDataStore.getCurrentKeyboardLayout(key);
+ if (oldLayout == null && !key.equals(identifier.getDescriptor())) {
+ oldLayout = mDataStore.getCurrentKeyboardLayout(identifier.getDescriptor());
+ }
+ boolean removed = mDataStore.removeKeyboardLayout(key, keyboardLayoutDescriptor);
+ if (!key.equals(identifier.getDescriptor())) {
+ // We need to remove from both places to ensure it is gone
+ removed |= mDataStore.removeKeyboardLayout(identifier.getDescriptor(),
+ keyboardLayoutDescriptor);
+ }
+ if (removed && !Objects.equals(oldLayout,
+ mDataStore.getCurrentKeyboardLayout(key))) {
+ mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
+ }
+ } finally {
+ mDataStore.saveIfNeeded();
+ }
+ }
+ }
+
+ public void switchKeyboardLayout(int deviceId, int direction) {
+ mHandler.obtainMessage(MSG_SWITCH_KEYBOARD_LAYOUT, deviceId, direction).sendToTarget();
+ }
+
+ // Must be called on handler.
+ private void handleSwitchKeyboardLayout(int deviceId, int direction) {
+ final InputDevice device = getInputDevice(deviceId);
+ if (device != null) {
+ final boolean changed;
+ final String keyboardLayoutDescriptor;
+
+ String key = getLayoutDescriptor(device.getIdentifier());
+ synchronized (mDataStore) {
+ try {
+ changed = mDataStore.switchKeyboardLayout(key, direction);
+ keyboardLayoutDescriptor = mDataStore.getCurrentKeyboardLayout(
+ key);
+ } finally {
+ mDataStore.saveIfNeeded();
+ }
+ }
+
+ if (changed) {
+ if (mSwitchedKeyboardLayoutToast != null) {
+ mSwitchedKeyboardLayoutToast.cancel();
+ mSwitchedKeyboardLayoutToast = null;
+ }
+ if (keyboardLayoutDescriptor != null) {
+ KeyboardLayout keyboardLayout = getKeyboardLayout(keyboardLayoutDescriptor);
+ if (keyboardLayout != null) {
+ mSwitchedKeyboardLayoutToast = Toast.makeText(
+ mContext, keyboardLayout.getLabel(), Toast.LENGTH_SHORT);
+ mSwitchedKeyboardLayoutToast.show();
+ }
+ }
+
+ reloadKeyboardLayouts();
+ }
+ }
+ }
+
+ public String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier) {
+ String keyboardLayoutDescriptor = getCurrentKeyboardLayoutForInputDevice(identifier);
+ if (keyboardLayoutDescriptor == null) {
+ return null;
+ }
+
+ final String[] result = new String[2];
+ visitKeyboardLayout(keyboardLayoutDescriptor,
+ (resources, keyboardLayoutResId, layout) -> {
+ try (InputStreamReader stream = new InputStreamReader(
+ resources.openRawResource(keyboardLayoutResId))) {
+ result[0] = layout.getDescriptor();
+ result[1] = Streams.readFully(stream);
+ } catch (IOException | Resources.NotFoundException ignored) {
+ }
+ });
+ if (result[0] == null) {
+ Slog.w(TAG, "Could not get keyboard layout with descriptor '"
+ + keyboardLayoutDescriptor + "'.");
+ return null;
+ }
+ return result;
+ }
+
+ private void reloadKeyboardLayouts() {
+ if (DEBUG) {
+ Slog.d(TAG, "Reloading keyboard layouts.");
+ }
+ mNative.reloadKeyboardLayouts();
+ }
+
+ private void maybeUpdateNotification() {
+ NotificationManager notificationManager = mContext.getSystemService(
+ NotificationManager.class);
+ if (notificationManager == null) {
+ return;
+ }
+ if (!mKeyboardsWithMissingLayouts.isEmpty()) {
+ if (mKeyboardsWithMissingLayouts.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(notificationManager, null);
+ } else {
+ showMissingKeyboardLayoutNotification(notificationManager,
+ mKeyboardsWithMissingLayouts.get(0));
+ }
+ } else if (mKeyboardLayoutNotificationShown) {
+ hideMissingKeyboardLayoutNotification(notificationManager);
+ }
+ }
+
+ // Must be called on handler.
+ private void showMissingKeyboardLayoutNotification(NotificationManager notificationManager,
+ InputDevice device) {
+ if (!mKeyboardLayoutNotificationShown) {
+ final Intent intent = new Intent(Settings.ACTION_HARD_KEYBOARD_SETTINGS);
+ if (device != null) {
+ intent.putExtra(Settings.EXTRA_INPUT_DEVICE_IDENTIFIER, device.getIdentifier());
+ }
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ final PendingIntent keyboardLayoutIntent = PendingIntent.getActivityAsUser(mContext, 0,
+ intent, PendingIntent.FLAG_IMMUTABLE, null, UserHandle.CURRENT);
+
+ Resources r = mContext.getResources();
+ Notification notification =
+ new Notification.Builder(mContext, SystemNotificationChannels.PHYSICAL_KEYBOARD)
+ .setContentTitle(r.getString(
+ R.string.select_keyboard_layout_notification_title))
+ .setContentText(r.getString(
+ R.string.select_keyboard_layout_notification_message))
+ .setContentIntent(keyboardLayoutIntent)
+ .setSmallIcon(R.drawable.ic_settings_language)
+ .setColor(mContext.getColor(
+ com.android.internal.R.color.system_notification_accent_color))
+ .build();
+ notificationManager.notifyAsUser(null,
+ SystemMessageProto.SystemMessage.NOTE_SELECT_KEYBOARD_LAYOUT,
+ notification, UserHandle.ALL);
+ mKeyboardLayoutNotificationShown = true;
+ }
+ }
+
+ // Must be called on handler.
+ private void hideMissingKeyboardLayoutNotification(NotificationManager notificationManager) {
+ if (mKeyboardLayoutNotificationShown) {
+ mKeyboardLayoutNotificationShown = false;
+ notificationManager.cancelAsUser(null,
+ SystemMessageProto.SystemMessage.NOTE_SELECT_KEYBOARD_LAYOUT,
+ UserHandle.ALL);
+ }
+ }
+
+ private boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_SWITCH_KEYBOARD_LAYOUT:
+ handleSwitchKeyboardLayout(msg.arg1, msg.arg2);
+ return true;
+ case MSG_RELOAD_KEYBOARD_LAYOUTS:
+ reloadKeyboardLayouts();
+ return true;
+ case MSG_UPDATE_KEYBOARD_LAYOUTS:
+ updateKeyboardLayouts();
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private InputDevice getInputDevice(int deviceId) {
+ InputManager inputManager = mContext.getSystemService(InputManager.class);
+ return inputManager != null ? inputManager.getInputDevice(deviceId) : null;
+ }
+
+ private static final class KeyboardLayoutDescriptor {
+ public String packageName;
+ public String receiverName;
+ public String keyboardLayoutName;
+
+ public static String format(String packageName,
+ String receiverName, String keyboardName) {
+ return packageName + "/" + receiverName + "/" + keyboardName;
+ }
+
+ public static KeyboardLayoutDescriptor parse(String descriptor) {
+ int pos = descriptor.indexOf('/');
+ if (pos < 0 || pos + 1 == descriptor.length()) {
+ return null;
+ }
+ int pos2 = descriptor.indexOf('/', pos + 1);
+ if (pos2 < pos + 2 || pos2 + 1 == descriptor.length()) {
+ return null;
+ }
+
+ KeyboardLayoutDescriptor result = new KeyboardLayoutDescriptor();
+ result.packageName = descriptor.substring(0, pos);
+ result.receiverName = descriptor.substring(pos + 1, pos2);
+ result.keyboardLayoutName = descriptor.substring(pos2 + 1);
+ return result;
+ }
+ }
+
+ private interface KeyboardLayoutVisitor {
+ void visitKeyboardLayout(Resources resources,
+ int keyboardLayoutResId, KeyboardLayout layout);
+ }
+}