Refactor: Move Keyboard layout related logic out of IMS
This is precursor to adding new APIs for PK layout selection implementation which will be handled in KeyboardLayoutManager. Bug: 259530132 Test: atest KeyboardLayoutChangeTest Change-Id: I99e396816838c05f28ce57ac50680a3369221921 Change-Id: I4f8ba5350e6affa9f6b2df31262c8aadafc0da05
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user