| /* |
| * Copyright (C) 2010 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 static android.view.Surface.ROTATION_0; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| 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.Point; |
| import android.graphics.Rect; |
| import android.hardware.display.DisplayManager; |
| import android.hardware.display.DisplayViewport; |
| import android.hardware.input.IInputDevicesChangedListener; |
| import android.hardware.input.IInputManager; |
| import android.hardware.input.IInputSensorEventListener; |
| import android.hardware.input.ITabletModeChangedListener; |
| import android.hardware.input.InputDeviceIdentifier; |
| import android.hardware.input.InputManager; |
| import android.hardware.input.InputManagerInternal; |
| import android.hardware.input.InputManagerInternal.LidSwitchCallback; |
| import android.hardware.input.InputSensorInfo; |
| import android.hardware.input.KeyboardLayout; |
| import android.hardware.input.TouchCalibration; |
| 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; |
| import android.os.IBinder; |
| 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.MessageQueue; |
| import android.os.Process; |
| import android.os.RemoteCallbackList; |
| import android.os.RemoteException; |
| import android.os.ResultReceiver; |
| import android.os.ShellCallback; |
| import android.os.SystemProperties; |
| import android.os.UserHandle; |
| import android.os.VibrationEffect; |
| import android.os.vibrator.StepSegment; |
| import android.os.vibrator.VibrationEffectSegment; |
| import android.provider.DeviceConfig; |
| import android.provider.Settings; |
| import android.provider.Settings.SettingNotFoundException; |
| import android.text.TextUtils; |
| import android.util.ArrayMap; |
| import android.util.Log; |
| import android.util.Slog; |
| import android.util.SparseArray; |
| import android.util.SparseBooleanArray; |
| import android.view.Display; |
| import android.view.IInputFilter; |
| import android.view.IInputFilterHost; |
| import android.view.IInputMonitorHost; |
| import android.view.InputApplicationHandle; |
| import android.view.InputChannel; |
| import android.view.InputDevice; |
| import android.view.InputEvent; |
| import android.view.InputMonitor; |
| import android.view.KeyEvent; |
| import android.view.MotionEvent; |
| import android.view.PointerIcon; |
| import android.view.Surface; |
| import android.view.VerifiedInputEvent; |
| import android.view.ViewConfiguration; |
| import android.widget.Toast; |
| |
| import com.android.internal.R; |
| import com.android.internal.annotations.GuardedBy; |
| 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.ArrayUtils; |
| 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; |
| import com.android.server.policy.WindowManagerPolicy; |
| |
| import libcore.io.IoUtils; |
| import libcore.io.Streams; |
| |
| import java.io.File; |
| import java.io.FileDescriptor; |
| import java.io.FileInputStream; |
| 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; |
| |
| /* |
| * Wraps the C++ InputManager and provides its callbacks. |
| */ |
| public class InputManagerService extends IInputManager.Stub |
| implements Watchdog.Monitor { |
| static final String TAG = "InputManager"; |
| static final boolean DEBUG = false; |
| |
| private static final String EXCLUDED_DEVICES_PATH = "etc/excluded-input-devices.xml"; |
| private static final String PORT_ASSOCIATIONS_PATH = "etc/input-port-associations.xml"; |
| |
| // Feature flag name for the deep press feature |
| private static final String DEEP_PRESS_ENABLED = "deep_press_enabled"; |
| |
| 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 DEFAULT_VIBRATION_MAGNITUDE = 192; |
| |
| /** |
| * We know the issue and are working to fix it, so suppressing the toast to not annoy |
| * dogfooders. |
| * |
| * TODO(b/169067926): Remove this |
| */ |
| private static final String[] PACKAGE_BLOCKLIST_FOR_UNTRUSTED_TOUCHES_TOAST = { |
| "com.snapchat.android" // b/173297887 |
| }; |
| |
| /** TODO(b/169067926): Remove this. */ |
| private static final boolean UNTRUSTED_TOUCHES_TOAST = false; |
| |
| public static final boolean ENABLE_PER_WINDOW_INPUT_ROTATION = |
| SystemProperties.getBoolean("persist.debug.per_window_input_rotation", false); |
| |
| // Pointer to native input manager service object. |
| private final long mPtr; |
| |
| private final Context mContext; |
| private final InputManagerHandler mHandler; |
| |
| // Context cache used for loading pointer resources. |
| private Context mPointerIconDisplayContext; |
| |
| private final File mDoubleTouchGestureEnableFile; |
| |
| 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 |
| private final SparseArray<TabletModeChangedListenerRecord> mTabletModeChangedListeners = |
| new SparseArray<>(); // guarded by mTabletModeLock |
| private final List<TabletModeChangedListenerRecord> mTempTabletModeChangedListenersToNotify = |
| new ArrayList<>(); |
| |
| private final Object mSensorEventLock = new Object(); |
| // List of currently registered sensor event listeners by process id |
| @GuardedBy("mSensorEventLock") |
| private final SparseArray<SensorEventListenerRecord> mSensorEventListeners = |
| new SparseArray<>(); |
| private final List<SensorEventListenerRecord> mSensorEventListenersToNotify = |
| new ArrayList<>(); |
| private final List<SensorEventListenerRecord> mSensorAccuracyListenersToNotify = |
| new ArrayList<>(); |
| |
| // Persistent data store. Must be locked each time during use. |
| private final PersistentDataStore mDataStore = new PersistentDataStore(); |
| |
| // List of currently registered input devices changed listeners by process id. |
| private Object mInputDevicesLock = new Object(); |
| @GuardedBy("mInputDevicesLock") |
| private boolean mInputDevicesChangedPending; // guarded by mInputDevicesLock |
| @GuardedBy("mInputDevicesLock") |
| private InputDevice[] mInputDevices = new InputDevice[0]; |
| private final SparseArray<InputDevicesChangedListenerRecord> mInputDevicesChangedListeners = |
| new SparseArray<InputDevicesChangedListenerRecord>(); // guarded by mInputDevicesLock |
| private final ArrayList<InputDevicesChangedListenerRecord> |
| mTempInputDevicesChangedListenersToNotify = |
| new ArrayList<InputDevicesChangedListenerRecord>(); // handler thread only |
| private final ArrayList<InputDevice> |
| mTempFullKeyboards = new ArrayList<InputDevice>(); // handler thread only |
| private boolean mKeyboardLayoutNotificationShown; |
| private Toast mSwitchedKeyboardLayoutToast; |
| |
| // State for vibrator tokens. |
| private Object mVibratorLock = new Object(); |
| private Map<IBinder, VibratorToken> mVibratorTokens = new ArrayMap<IBinder, VibratorToken>(); |
| private int mNextVibratorTokenValue; |
| |
| // List of currently registered vibrator state changed listeners by device id. |
| @GuardedBy("mVibratorLock") |
| private final SparseArray<RemoteCallbackList<IVibratorStateListener>> mVibratorStateListeners = |
| new SparseArray<RemoteCallbackList<IVibratorStateListener>>(); |
| // List of vibrator states by device id. |
| @GuardedBy("mVibratorLock") |
| private final SparseBooleanArray mIsVibrating = new SparseBooleanArray(); |
| private Object mLightLock = new Object(); |
| // State for light tokens. A light token marks a lights manager session, it is generated |
| // by light session open() and deleted in session close(). |
| // When lights session requests light states, the token will be used to find the light session. |
| @GuardedBy("mLightLock") |
| private final ArrayMap<IBinder, LightSession> mLightSessions = |
| new ArrayMap<IBinder, LightSession>(); |
| |
| // State for lid switch |
| // Lock for the lid switch state. Held when triggering callbacks to guarantee lid switch events |
| // are delivered in order. For ex, when a new lid switch callback is registered the lock is held |
| // while the callback is processing the initial lid switch event which guarantees that any |
| // events that occur at the same time are delivered after the callback has returned. |
| private final Object mLidSwitchLock = new Object(); |
| @GuardedBy("mLidSwitchLock") |
| private List<LidSwitchCallback> mLidSwitchCallbacks = new ArrayList<>(); |
| |
| // State for the currently installed input filter. |
| final Object mInputFilterLock = new Object(); |
| IInputFilter mInputFilter; // guarded by mInputFilterLock |
| InputFilterHost mInputFilterHost; // guarded by mInputFilterLock |
| |
| // The associations of input devices to displays by port. Maps from input device port (String) |
| // to display id (int). Currently only accessed by InputReader. |
| private final Map<String, Integer> mStaticAssociations; |
| private final Object mAssociationsLock = new Object(); |
| @GuardedBy("mAssociationLock") |
| private final Map<String, Integer> mRuntimeAssociations = new ArrayMap<String, Integer>(); |
| @GuardedBy("mAssociationLock") |
| private final Map<String, String> mUniqueIdAssociations = new ArrayMap<>(); |
| |
| private static native long nativeInit(InputManagerService service, |
| Context context, MessageQueue messageQueue); |
| private static native void nativeStart(long ptr); |
| private static native void nativeSetDisplayViewports(long ptr, |
| DisplayViewport[] viewports); |
| |
| private static native int nativeGetScanCodeState(long ptr, |
| int deviceId, int sourceMask, int scanCode); |
| private static native int nativeGetKeyCodeState(long ptr, |
| int deviceId, int sourceMask, int keyCode); |
| private static native int nativeGetSwitchState(long ptr, |
| int deviceId, int sourceMask, int sw); |
| private static native boolean nativeHasKeys(long ptr, |
| int deviceId, int sourceMask, int[] keyCodes, boolean[] keyExists); |
| private static native InputChannel nativeCreateInputChannel(long ptr, String name); |
| private static native InputChannel nativeCreateInputMonitor(long ptr, int displayId, |
| boolean isGestureMonitor, String name, int pid); |
| private static native void nativeRemoveInputChannel(long ptr, IBinder connectionToken); |
| private static native void nativePilferPointers(long ptr, IBinder token); |
| private static native void nativeSetInputFilterEnabled(long ptr, boolean enable); |
| private static native void nativeSetInTouchMode(long ptr, boolean inTouchMode); |
| private static native void nativeSetMaximumObscuringOpacityForTouch(long ptr, float opacity); |
| private static native void nativeSetBlockUntrustedTouchesMode(long ptr, int mode); |
| private static native int nativeInjectInputEvent(long ptr, InputEvent event, |
| int injectorPid, int injectorUid, int syncMode, int timeoutMillis, |
| int policyFlags); |
| private static native VerifiedInputEvent nativeVerifyInputEvent(long ptr, InputEvent event); |
| private static native void nativeToggleCapsLock(long ptr, int deviceId); |
| private static native void nativeDisplayRemoved(long ptr, int displayId); |
| private static native void nativeSetInputDispatchMode(long ptr, boolean enabled, boolean frozen); |
| private static native void nativeSetSystemUiLightsOut(long ptr, boolean lightsOut); |
| private static native void nativeSetFocusedApplication(long ptr, |
| int displayId, InputApplicationHandle application); |
| private static native void nativeSetFocusedDisplay(long ptr, int displayId); |
| private static native boolean nativeTransferTouchFocus(long ptr, |
| IBinder fromChannelToken, IBinder toChannelToken, boolean isDragDrop); |
| private static native boolean nativeTransferTouch(long ptr, IBinder destChannelToken); |
| private static native void nativeSetPointerSpeed(long ptr, int speed); |
| private static native void nativeSetShowTouches(long ptr, boolean enabled); |
| private static native void nativeSetInteractive(long ptr, boolean interactive); |
| private static native void nativeReloadCalibration(long ptr); |
| private static native void nativeVibrate(long ptr, int deviceId, long[] pattern, |
| int[] amplitudes, int repeat, int token); |
| private static native void nativeVibrateCombined(long ptr, int deviceId, long[] pattern, |
| SparseArray<int[]> amplitudes, int repeat, int token); |
| private static native void nativeCancelVibrate(long ptr, int deviceId, int token); |
| private static native boolean nativeIsVibrating(long ptr, int deviceId); |
| private static native int[] nativeGetVibratorIds(long ptr, int deviceId); |
| private static native int nativeGetBatteryCapacity(long ptr, int deviceId); |
| private static native int nativeGetBatteryStatus(long ptr, int deviceId); |
| private static native List<Light> nativeGetLights(long ptr, int deviceId); |
| private static native int nativeGetLightPlayerId(long ptr, int deviceId, int lightId); |
| private static native int nativeGetLightColor(long ptr, int deviceId, int lightId); |
| private static native void nativeSetLightPlayerId(long ptr, int deviceId, int lightId, |
| int playerId); |
| private static native void nativeSetLightColor(long ptr, int deviceId, int lightId, int color); |
| private static native void nativeReloadKeyboardLayouts(long ptr); |
| private static native void nativeReloadDeviceAliases(long ptr); |
| private static native String nativeDump(long ptr); |
| private static native void nativeMonitor(long ptr); |
| private static native boolean nativeIsInputDeviceEnabled(long ptr, int deviceId); |
| private static native void nativeEnableInputDevice(long ptr, int deviceId); |
| private static native void nativeDisableInputDevice(long ptr, int deviceId); |
| private static native void nativeSetPointerIconType(long ptr, int iconId); |
| private static native void nativeReloadPointerIcons(long ptr); |
| private static native void nativeSetCustomPointerIcon(long ptr, PointerIcon icon); |
| private static native void nativeRequestPointerCapture(long ptr, IBinder windowToken, |
| boolean enabled); |
| private static native boolean nativeCanDispatchToDisplay(long ptr, int deviceId, int displayId); |
| private static native void nativeNotifyPortAssociationsChanged(long ptr); |
| private static native void nativeChangeUniqueIdAssociation(long ptr); |
| private static native void nativeSetMotionClassifierEnabled(long ptr, boolean enabled); |
| private static native InputSensorInfo[] nativeGetSensorList(long ptr, int deviceId); |
| private static native boolean nativeFlushSensor(long ptr, int deviceId, int sensorType); |
| private static native boolean nativeEnableSensor(long ptr, int deviceId, int sensorType, |
| int samplingPeriodUs, int maxBatchReportLatencyUs); |
| private static native void nativeDisableSensor(long ptr, int deviceId, int sensorType); |
| |
| // Maximum number of milliseconds to wait for input event injection. |
| private static final int INJECTION_TIMEOUT_MILLIS = 30 * 1000; |
| |
| // Key states (may be returned by queries about the current state of a |
| // particular key code, scan code or switch). |
| |
| /** The key state is unknown or the requested key itself is not supported. */ |
| public static final int KEY_STATE_UNKNOWN = -1; |
| |
| /** The key is up. /*/ |
| public static final int KEY_STATE_UP = 0; |
| |
| /** The key is down. */ |
| public static final int KEY_STATE_DOWN = 1; |
| |
| /** The key is down but is a virtual key press that is being emulated by the system. */ |
| public static final int KEY_STATE_VIRTUAL = 2; |
| |
| /** Scan code: Mouse / trackball button. */ |
| public static final int BTN_MOUSE = 0x110; |
| |
| // Switch code values must match bionic/libc/kernel/common/linux/input.h |
| /** Switch code: Lid switch. When set, lid is shut. */ |
| public static final int SW_LID = 0x00; |
| |
| /** Switch code: Tablet mode switch. |
| * When set, the device is in tablet mode (i.e. no keyboard is connected). |
| */ |
| public static final int SW_TABLET_MODE = 0x01; |
| |
| /** Switch code: Keypad slide. When set, keyboard is exposed. */ |
| public static final int SW_KEYPAD_SLIDE = 0x0a; |
| |
| /** Switch code: Headphone. When set, headphone is inserted. */ |
| public static final int SW_HEADPHONE_INSERT = 0x02; |
| |
| /** Switch code: Microphone. When set, microphone is inserted. */ |
| public static final int SW_MICROPHONE_INSERT = 0x04; |
| |
| /** Switch code: Line out. When set, Line out (hi-Z) is inserted. */ |
| public static final int SW_LINEOUT_INSERT = 0x06; |
| |
| /** Switch code: Headphone/Microphone Jack. When set, something is inserted. */ |
| public static final int SW_JACK_PHYSICAL_INSERT = 0x07; |
| |
| /** Switch code: Camera lens cover. When set the lens is covered. */ |
| public static final int SW_CAMERA_LENS_COVER = 0x09; |
| |
| /** Switch code: Microphone. When set it is off. */ |
| public static final int SW_MUTE_DEVICE = 0x0e; |
| |
| public static final int SW_LID_BIT = 1 << SW_LID; |
| public static final int SW_TABLET_MODE_BIT = 1 << SW_TABLET_MODE; |
| public static final int SW_KEYPAD_SLIDE_BIT = 1 << SW_KEYPAD_SLIDE; |
| public static final int SW_HEADPHONE_INSERT_BIT = 1 << SW_HEADPHONE_INSERT; |
| public static final int SW_MICROPHONE_INSERT_BIT = 1 << SW_MICROPHONE_INSERT; |
| public static final int SW_LINEOUT_INSERT_BIT = 1 << SW_LINEOUT_INSERT; |
| public static final int SW_JACK_PHYSICAL_INSERT_BIT = 1 << SW_JACK_PHYSICAL_INSERT; |
| public static final int SW_JACK_BITS = |
| SW_HEADPHONE_INSERT_BIT | SW_MICROPHONE_INSERT_BIT | SW_JACK_PHYSICAL_INSERT_BIT | SW_LINEOUT_INSERT_BIT; |
| public static final int SW_CAMERA_LENS_COVER_BIT = 1 << SW_CAMERA_LENS_COVER; |
| public static final int SW_MUTE_DEVICE_BIT = 1 << SW_MUTE_DEVICE; |
| |
| /** Whether to use the dev/input/event or uevent subsystem for the audio jack. */ |
| final boolean mUseDevInputEventForAudioJack; |
| |
| public InputManagerService(Context context) { |
| this.mContext = context; |
| this.mHandler = new InputManagerHandler(DisplayThread.get().getLooper()); |
| |
| mStaticAssociations = loadStaticInputPortAssociations(); |
| mUseDevInputEventForAudioJack = |
| context.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack); |
| Slog.i(TAG, "Initializing input manager, mUseDevInputEventForAudioJack=" |
| + mUseDevInputEventForAudioJack); |
| mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue()); |
| |
| String doubleTouchGestureEnablePath = context.getResources().getString( |
| R.string.config_doubleTouchGestureEnableFile); |
| mDoubleTouchGestureEnableFile = TextUtils.isEmpty(doubleTouchGestureEnablePath) ? null : |
| new File(doubleTouchGestureEnablePath); |
| |
| LocalServices.addService(InputManagerInternal.class, new LocalService()); |
| } |
| |
| public void setWindowManagerCallbacks(WindowManagerCallbacks callbacks) { |
| if (mWindowManagerCallbacks != null) { |
| unregisterLidSwitchCallbackInternal(mWindowManagerCallbacks); |
| } |
| mWindowManagerCallbacks = callbacks; |
| registerLidSwitchCallbackInternal(mWindowManagerCallbacks); |
| } |
| |
| public void setWiredAccessoryCallbacks(WiredAccessoryCallbacks callbacks) { |
| mWiredAccessoryCallbacks = callbacks; |
| } |
| |
| void registerLidSwitchCallbackInternal(@NonNull LidSwitchCallback callback) { |
| synchronized (mLidSwitchLock) { |
| mLidSwitchCallbacks.add(callback); |
| |
| // Skip triggering the initial callback if the system is not yet ready as the switch |
| // state will be reported as KEY_STATE_UNKNOWN. The callback will be triggered in |
| // systemRunning(). |
| if (mSystemReady) { |
| boolean lidOpen = getSwitchState(-1 /* deviceId */, InputDevice.SOURCE_ANY, SW_LID) |
| == KEY_STATE_UP; |
| callback.notifyLidSwitchChanged(0 /* whenNanos */, lidOpen); |
| } |
| } |
| } |
| |
| void unregisterLidSwitchCallbackInternal(@NonNull LidSwitchCallback callback) { |
| synchronized (mLidSwitchLock) { |
| mLidSwitchCallbacks.remove(callback); |
| } |
| } |
| |
| public void start() { |
| Slog.i(TAG, "Starting input manager"); |
| nativeStart(mPtr); |
| |
| // Add ourself to the Watchdog monitors. |
| Watchdog.getInstance().addMonitor(this); |
| |
| registerPointerSpeedSettingObserver(); |
| registerShowTouchesSettingObserver(); |
| registerAccessibilityLargePointerSettingObserver(); |
| registerLongPressTimeoutObserver(); |
| registerMaximumObscuringOpacityForTouchSettingObserver(); |
| registerBlockUntrustedTouchesModeSettingObserver(); |
| |
| mContext.registerReceiver(new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| updatePointerSpeedFromSettings(); |
| updateShowTouchesFromSettings(); |
| updateAccessibilityLargePointerFromSettings(); |
| updateDeepPressStatusFromSettings("user switched"); |
| } |
| }, new IntentFilter(Intent.ACTION_USER_SWITCHED), null, mHandler); |
| |
| updatePointerSpeedFromSettings(); |
| updateShowTouchesFromSettings(); |
| updateAccessibilityLargePointerFromSettings(); |
| updateDeepPressStatusFromSettings("just booted"); |
| updateMaximumObscuringOpacityForTouchFromSettings(); |
| updateBlockUntrustedTouchesModeFromSettings(); |
| } |
| |
| // TODO(BT) Pass in parameter for bluetooth system |
| public void systemRunning() { |
| if (DEBUG) { |
| Slog.d(TAG, "System ready."); |
| } |
| mNotificationManager = (NotificationManager)mContext.getSystemService( |
| Context.NOTIFICATION_SERVICE); |
| |
| synchronized (mLidSwitchLock) { |
| mSystemReady = true; |
| |
| // Send the initial lid switch state to any callback registered before the system was |
| // ready. |
| int switchState = getSwitchState(-1 /* deviceId */, InputDevice.SOURCE_ANY, SW_LID); |
| for (int i = 0; i < mLidSwitchCallbacks.size(); i++) { |
| LidSwitchCallback callback = mLidSwitchCallbacks.get(i); |
| callback.notifyLidSwitchChanged(0 /* whenNanos */, switchState == KEY_STATE_UP); |
| } |
| } |
| |
| 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); |
| mContext.registerReceiver(new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| reloadDeviceAliases(); |
| } |
| }, filter, null, mHandler); |
| |
| mHandler.sendEmptyMessage(MSG_RELOAD_DEVICE_ALIASES); |
| mHandler.sendEmptyMessage(MSG_UPDATE_KEYBOARD_LAYOUTS); |
| |
| if (mWiredAccessoryCallbacks != null) { |
| mWiredAccessoryCallbacks.systemReady(); |
| } |
| } |
| |
| private void reloadKeyboardLayouts() { |
| if (DEBUG) { |
| Slog.d(TAG, "Reloading keyboard layouts."); |
| } |
| nativeReloadKeyboardLayouts(mPtr); |
| } |
| |
| private void reloadDeviceAliases() { |
| if (DEBUG) { |
| Slog.d(TAG, "Reloading device names."); |
| } |
| nativeReloadDeviceAliases(mPtr); |
| } |
| |
| /** Rotates CCW by `delta` 90-degree increments. */ |
| private static void rotateBounds(Rect inOutBounds, int parentW, int parentH, int delta) { |
| int rdelta = ((delta % 4) + 4) % 4; |
| int origLeft = inOutBounds.left; |
| switch (rdelta) { |
| case 0: |
| return; |
| case 1: |
| inOutBounds.left = inOutBounds.top; |
| inOutBounds.top = parentW - inOutBounds.right; |
| inOutBounds.right = inOutBounds.bottom; |
| inOutBounds.bottom = parentW - origLeft; |
| return; |
| case 2: |
| inOutBounds.left = parentW - inOutBounds.right; |
| inOutBounds.right = parentW - origLeft; |
| return; |
| case 3: |
| inOutBounds.left = parentH - inOutBounds.bottom; |
| inOutBounds.bottom = inOutBounds.right; |
| inOutBounds.right = parentH - inOutBounds.top; |
| inOutBounds.top = origLeft; |
| return; |
| } |
| } |
| |
| private void setDisplayViewportsInternal(List<DisplayViewport> viewports) { |
| final DisplayViewport[] vArray = new DisplayViewport[viewports.size()]; |
| if (ENABLE_PER_WINDOW_INPUT_ROTATION) { |
| // Remove display projection information from DisplayViewport, leaving only the |
| // orientation. The display projection will be built-into the window transforms. |
| for (int i = viewports.size() - 1; i >= 0; --i) { |
| final DisplayViewport v = vArray[i] = viewports.get(i).makeCopy(); |
| // Note: the deviceWidth/Height are in rotated with the orientation. |
| v.logicalFrame.set(0, 0, v.deviceWidth, v.deviceHeight); |
| v.physicalFrame.set(0, 0, v.deviceWidth, v.deviceHeight); |
| } |
| } else { |
| for (int i = viewports.size() - 1; i >= 0; --i) { |
| vArray[i] = viewports.get(i); |
| } |
| } |
| nativeSetDisplayViewports(mPtr, vArray); |
| } |
| |
| /** |
| * Gets the current state of a key or button by key code. |
| * @param deviceId The input device id, or -1 to consult all devices. |
| * @param sourceMask The input sources to consult, or {@link InputDevice#SOURCE_ANY} to |
| * consider all input sources. An input device is consulted if at least one of its |
| * non-class input source bits matches the specified source mask. |
| * @param keyCode The key code to check. |
| * @return The key state. |
| */ |
| public int getKeyCodeState(int deviceId, int sourceMask, int keyCode) { |
| return nativeGetKeyCodeState(mPtr, deviceId, sourceMask, keyCode); |
| } |
| |
| /** |
| * Gets the current state of a key or button by scan code. |
| * @param deviceId The input device id, or -1 to consult all devices. |
| * @param sourceMask The input sources to consult, or {@link InputDevice#SOURCE_ANY} to |
| * consider all input sources. An input device is consulted if at least one of its |
| * non-class input source bits matches the specified source mask. |
| * @param scanCode The scan code to check. |
| * @return The key state. |
| */ |
| public int getScanCodeState(int deviceId, int sourceMask, int scanCode) { |
| return nativeGetScanCodeState(mPtr, deviceId, sourceMask, scanCode); |
| } |
| |
| /** |
| * Gets the current state of a switch by switch code. |
| * @param deviceId The input device id, or -1 to consult all devices. |
| * @param sourceMask The input sources to consult, or {@link InputDevice#SOURCE_ANY} to |
| * consider all input sources. An input device is consulted if at least one of its |
| * non-class input source bits matches the specified source mask. |
| * @param switchCode The switch code to check. |
| * @return The switch state. |
| */ |
| public int getSwitchState(int deviceId, int sourceMask, int switchCode) { |
| return nativeGetSwitchState(mPtr, deviceId, sourceMask, switchCode); |
| } |
| |
| /** |
| * Determines whether the specified key codes are supported by a particular device. |
| * @param deviceId The input device id, or -1 to consult all devices. |
| * @param sourceMask The input sources to consult, or {@link InputDevice#SOURCE_ANY} to |
| * consider all input sources. An input device is consulted if at least one of its |
| * non-class input source bits matches the specified source mask. |
| * @param keyCodes The array of key codes to check. |
| * @param keyExists An array at least as large as keyCodes whose entries will be set |
| * to true or false based on the presence or absence of support for the corresponding |
| * key codes. |
| * @return True if the lookup was successful, false otherwise. |
| */ |
| @Override // Binder call |
| public boolean hasKeys(int deviceId, int sourceMask, int[] keyCodes, boolean[] keyExists) { |
| if (keyCodes == null) { |
| throw new IllegalArgumentException("keyCodes must not be null."); |
| } |
| if (keyExists == null || keyExists.length < keyCodes.length) { |
| throw new IllegalArgumentException("keyExists must not be null and must be at " |
| + "least as large as keyCodes."); |
| } |
| |
| return nativeHasKeys(mPtr, deviceId, sourceMask, keyCodes, keyExists); |
| } |
| |
| /** |
| * Transfer the current touch gesture to the provided window. |
| * |
| * @param destChannelToken The token of the window or input channel that should receive the |
| * gesture |
| * @return True if the transfer succeeded, false if there was no active touch gesture happening |
| */ |
| public boolean transferTouch(IBinder destChannelToken) { |
| // TODO(b/162194035): Replace this with a SPY window |
| Objects.requireNonNull(destChannelToken, "destChannelToken must not be null."); |
| return nativeTransferTouch(mPtr, destChannelToken); |
| } |
| |
| /** |
| * Creates an input channel that will receive all input from the input dispatcher. |
| * @param inputChannelName The input channel name. |
| * @param displayId Target display id. |
| * @return The input channel. |
| */ |
| public InputChannel monitorInput(String inputChannelName, int displayId) { |
| if (inputChannelName == null) { |
| throw new IllegalArgumentException("inputChannelName must not be null."); |
| } |
| |
| if (displayId < Display.DEFAULT_DISPLAY) { |
| throw new IllegalArgumentException("displayId must >= 0."); |
| } |
| |
| return nativeCreateInputMonitor(mPtr, displayId, false /* isGestureMonitor */, |
| inputChannelName, Binder.getCallingPid()); |
| } |
| |
| /** |
| * Creates an input monitor that will receive pointer events for the purposes of system-wide |
| * gesture interpretation. |
| * |
| * @param inputChannelName The input channel name. |
| * @param displayId Target display id. |
| * @return The input channel. |
| */ |
| @Override // Binder call |
| public InputMonitor monitorGestureInput(String inputChannelName, int displayId) { |
| if (!checkCallingPermission(android.Manifest.permission.MONITOR_INPUT, |
| "monitorInputRegion()")) { |
| throw new SecurityException("Requires MONITOR_INPUT permission"); |
| } |
| |
| Objects.requireNonNull(inputChannelName, "inputChannelName must not be null."); |
| |
| if (displayId < Display.DEFAULT_DISPLAY) { |
| throw new IllegalArgumentException("displayId must >= 0."); |
| } |
| final int pid = Binder.getCallingPid(); |
| |
| final long ident = Binder.clearCallingIdentity(); |
| try { |
| InputChannel inputChannel = nativeCreateInputMonitor( |
| mPtr, displayId, true /*isGestureMonitor*/, inputChannelName, pid); |
| InputMonitorHost host = new InputMonitorHost(inputChannel.getToken()); |
| return new InputMonitor(inputChannel, host); |
| } finally { |
| Binder.restoreCallingIdentity(ident); |
| } |
| } |
| |
| /** |
| * Creates an input channel to be used as an input event target. |
| * |
| * @param name The name of this input channel |
| */ |
| public InputChannel createInputChannel(String name) { |
| return nativeCreateInputChannel(mPtr, name); |
| } |
| |
| /** |
| * Removes an input channel. |
| * @param connectionToken The input channel to unregister. |
| */ |
| public void removeInputChannel(IBinder connectionToken) { |
| if (connectionToken == null) { |
| throw new IllegalArgumentException("connectionToken must not be null."); |
| } |
| |
| nativeRemoveInputChannel(mPtr, connectionToken); |
| } |
| |
| /** |
| * Sets an input filter that will receive all input events before they are dispatched. |
| * The input filter may then reinterpret input events or inject new ones. |
| * |
| * To ensure consistency, the input dispatcher automatically drops all events |
| * in progress whenever an input filter is installed or uninstalled. After an input |
| * filter is uninstalled, it can no longer send input events unless it is reinstalled. |
| * Any events it attempts to send after it has been uninstalled will be dropped. |
| * |
| * @param filter The input filter, or null to remove the current filter. |
| */ |
| public void setInputFilter(IInputFilter filter) { |
| synchronized (mInputFilterLock) { |
| final IInputFilter oldFilter = mInputFilter; |
| if (oldFilter == filter) { |
| return; // nothing to do |
| } |
| |
| if (oldFilter != null) { |
| mInputFilter = null; |
| mInputFilterHost.disconnectLocked(); |
| mInputFilterHost = null; |
| try { |
| oldFilter.uninstall(); |
| } catch (RemoteException re) { |
| /* ignore */ |
| } |
| } |
| |
| if (filter != null) { |
| mInputFilter = filter; |
| mInputFilterHost = new InputFilterHost(); |
| try { |
| filter.install(mInputFilterHost); |
| } catch (RemoteException re) { |
| /* ignore */ |
| } |
| } |
| |
| nativeSetInputFilterEnabled(mPtr, filter != null); |
| } |
| } |
| |
| /** |
| * Set the state of the touch mode. |
| * |
| * WindowManager remains the source of truth of the touch mode state. |
| * However, we need to keep a copy of this state in input. |
| * |
| * The apps determine the touch mode state. Therefore, a single app will |
| * affect the global state. That state change needs to be propagated to |
| * other apps, when they become focused. |
| * |
| * When input dispatches focus to the apps, the touch mode state |
| * will be sent together with the focus change. |
| * |
| * @param inTouchMode true if the device is in touch mode. |
| */ |
| public void setInTouchMode(boolean inTouchMode) { |
| nativeSetInTouchMode(mPtr, inTouchMode); |
| } |
| |
| @Override // Binder call |
| public boolean injectInputEvent(InputEvent event, int mode) { |
| return injectInputEventInternal(event, mode); |
| } |
| |
| private boolean injectInputEventInternal(InputEvent event, int mode) { |
| if (event == null) { |
| throw new IllegalArgumentException("event must not be null"); |
| } |
| if (mode != InputEventInjectionSync.NONE |
| && mode != InputEventInjectionSync.WAIT_FOR_FINISHED |
| && mode != InputEventInjectionSync.WAIT_FOR_RESULT) { |
| throw new IllegalArgumentException("mode is invalid"); |
| } |
| if (ENABLE_PER_WINDOW_INPUT_ROTATION) { |
| // Motion events that are pointer events or relative mouse events will need to have the |
| // inverse display rotation applied to them. |
| if (event instanceof MotionEvent |
| && (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER) |
| || event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE))) { |
| Context displayContext = getContextForDisplay(event.getDisplayId()); |
| if (displayContext == null) { |
| displayContext = Objects.requireNonNull( |
| getContextForDisplay(Display.DEFAULT_DISPLAY)); |
| } |
| final Display display = displayContext.getDisplay(); |
| final int rotation = display.getRotation(); |
| if (rotation != ROTATION_0) { |
| final MotionEvent motion = (MotionEvent) event; |
| // Injections are currently expected to be in the space of the injector (ie. |
| // usually assumed to be post-rotated). Thus we need to un-rotate into raw |
| // input coordinates for dispatch. |
| final Point sz = new Point(); |
| if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) { |
| display.getRealSize(sz); |
| if ((rotation % 2) != 0) { |
| final int tmpX = sz.x; |
| sz.x = sz.y; |
| sz.y = tmpX; |
| } |
| } |
| motion.applyTransform(MotionEvent.createRotateMatrix( |
| (4 - rotation), sz.x, sz.y)); |
| } |
| } |
| } |
| |
| final int pid = Binder.getCallingPid(); |
| final int uid = Binder.getCallingUid(); |
| final long ident = Binder.clearCallingIdentity(); |
| final int result; |
| try { |
| result = nativeInjectInputEvent(mPtr, event, pid, uid, mode, |
| INJECTION_TIMEOUT_MILLIS, WindowManagerPolicy.FLAG_DISABLE_KEY_REPEAT); |
| } finally { |
| Binder.restoreCallingIdentity(ident); |
| } |
| switch (result) { |
| case InputEventInjectionResult.PERMISSION_DENIED: |
| Slog.w(TAG, "Input event injection from pid " + pid + " permission denied."); |
| throw new SecurityException( |
| "Injecting to another application requires INJECT_EVENTS permission"); |
| case InputEventInjectionResult.SUCCEEDED: |
| return true; |
| case InputEventInjectionResult.TIMED_OUT: |
| Slog.w(TAG, "Input event injection from pid " + pid + " timed out."); |
| return false; |
| case InputEventInjectionResult.FAILED: |
| default: |
| Slog.w(TAG, "Input event injection from pid " + pid + " failed."); |
| return false; |
| } |
| } |
| |
| @Override // Binder call |
| public VerifiedInputEvent verifyInputEvent(InputEvent event) { |
| Objects.requireNonNull(event, "event must not be null"); |
| return nativeVerifyInputEvent(mPtr, event); |
| } |
| |
| /** |
| * Gets information about the input device with the specified id. |
| * @param deviceId The device id. |
| * @return The input device or null if not found. |
| */ |
| @Override // Binder call |
| public InputDevice getInputDevice(int deviceId) { |
| synchronized (mInputDevicesLock) { |
| final int count = mInputDevices.length; |
| for (int i = 0; i < count; i++) { |
| final InputDevice inputDevice = mInputDevices[i]; |
| if (inputDevice.getId() == deviceId) { |
| return inputDevice; |
| } |
| } |
| } |
| return null; |
| } |
| |
| // Binder call |
| @Override |
| public boolean isInputDeviceEnabled(int deviceId) { |
| return nativeIsInputDeviceEnabled(mPtr, deviceId); |
| } |
| |
| // Binder call |
| @Override |
| public void enableInputDevice(int deviceId) { |
| if (!checkCallingPermission(android.Manifest.permission.DISABLE_INPUT_DEVICE, |
| "enableInputDevice()")) { |
| throw new SecurityException("Requires DISABLE_INPUT_DEVICE permission"); |
| } |
| nativeEnableInputDevice(mPtr, deviceId); |
| } |
| |
| // Binder call |
| @Override |
| public void disableInputDevice(int deviceId) { |
| if (!checkCallingPermission(android.Manifest.permission.DISABLE_INPUT_DEVICE, |
| "disableInputDevice()")) { |
| throw new SecurityException("Requires DISABLE_INPUT_DEVICE permission"); |
| } |
| nativeDisableInputDevice(mPtr, deviceId); |
| } |
| |
| /** |
| * Gets the ids of all input devices in the system. |
| * @return The input device ids. |
| */ |
| @Override // Binder call |
| public int[] getInputDeviceIds() { |
| synchronized (mInputDevicesLock) { |
| final int count = mInputDevices.length; |
| int[] ids = new int[count]; |
| for (int i = 0; i < count; i++) { |
| ids[i] = mInputDevices[i].getId(); |
| } |
| return ids; |
| } |
| } |
| |
| /** |
| * Gets all input devices in the system. |
| * @return The array of input devices. |
| */ |
| public InputDevice[] getInputDevices() { |
| synchronized (mInputDevicesLock) { |
| return mInputDevices; |
| } |
| } |
| |
| @Override // Binder call |
| public void registerInputDevicesChangedListener(IInputDevicesChangedListener listener) { |
| if (listener == null) { |
| throw new IllegalArgumentException("listener must not be null"); |
| } |
| |
| synchronized (mInputDevicesLock) { |
| int callingPid = Binder.getCallingPid(); |
| if (mInputDevicesChangedListeners.get(callingPid) != null) { |
| throw new SecurityException("The calling process has already " |
| + "registered an InputDevicesChangedListener."); |
| } |
| |
| InputDevicesChangedListenerRecord record = |
| new InputDevicesChangedListenerRecord(callingPid, listener); |
| try { |
| IBinder binder = listener.asBinder(); |
| binder.linkToDeath(record, 0); |
| } catch (RemoteException ex) { |
| // give up |
| throw new RuntimeException(ex); |
| } |
| |
| mInputDevicesChangedListeners.put(callingPid, record); |
| } |
| } |
| |
| private void onInputDevicesChangedListenerDied(int pid) { |
| synchronized (mInputDevicesLock) { |
| mInputDevicesChangedListeners.remove(pid); |
| } |
| } |
| |
| // 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) { |
| if (!mInputDevicesChangedPending) { |
| return; |
| } |
| mInputDevicesChangedPending = false; |
| |
| numListeners = mInputDevicesChangedListeners.size(); |
| for (int i = 0; i < numListeners; i++) { |
| mTempInputDevicesChangedListenersToNotify.add( |
| mInputDevicesChangedListeners.valueAt(i)); |
| } |
| |
| final int numDevices = mInputDevices.length; |
| deviceIdAndGeneration = new int[numDevices * 2]; |
| for (int i = 0; i < numDevices; i++) { |
| final InputDevice inputDevice = mInputDevices[i]; |
| deviceIdAndGeneration[i * 2] = inputDevice.getId(); |
| deviceIdAndGeneration[i * 2 + 1] = inputDevice.getGeneration(); |
| |
| if (!inputDevice.isVirtual() && inputDevice.isFullKeyboard()) { |
| if (!containsInputDeviceWithDescriptor(oldInputDevices, |
| inputDevice.getDescriptor())) { |
| mTempFullKeyboards.add(numFullKeyboardsAdded++, inputDevice); |
| } else { |
| mTempFullKeyboards.add(inputDevice); |
| } |
| } |
| } |
| } |
| |
| // Notify listeners. |
| for (int i = 0; i < numListeners; i++) { |
| mTempInputDevicesChangedListenersToNotify.get(i).notifyInputDevicesChanged( |
| 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(new KeyboardLayoutVisitor() { |
| @Override |
| public void visitKeyboardLayout(Resources resources, |
| int keyboardLayoutResId, KeyboardLayout layout) { |
| // Only select a default when we know the layout is appropriate. For now, this |
| // means its a custom layout for a specific keyboard. |
| if (layout.getVendorId() != d.getVendorId() |
| || layout.getProductId() != d.getProductId()) { |
| return; |
| } |
| 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 |
| public TouchCalibration getTouchCalibrationForInputDevice(String inputDeviceDescriptor, |
| int surfaceRotation) { |
| if (inputDeviceDescriptor == null) { |
| throw new IllegalArgumentException("inputDeviceDescriptor must not be null"); |
| } |
| |
| synchronized (mDataStore) { |
| return mDataStore.getTouchCalibration(inputDeviceDescriptor, surfaceRotation); |
| } |
| } |
| |
| @Override // Binder call |
| public void setTouchCalibrationForInputDevice(String inputDeviceDescriptor, int surfaceRotation, |
| TouchCalibration calibration) { |
| if (!checkCallingPermission(android.Manifest.permission.SET_INPUT_CALIBRATION, |
| "setTouchCalibrationForInputDevice()")) { |
| throw new SecurityException("Requires SET_INPUT_CALIBRATION permission"); |
| } |
| if (inputDeviceDescriptor == null) { |
| throw new IllegalArgumentException("inputDeviceDescriptor must not be null"); |
| } |
| if (calibration == null) { |
| throw new IllegalArgumentException("calibration must not be null"); |
| } |
| if (surfaceRotation < Surface.ROTATION_0 || surfaceRotation > Surface.ROTATION_270) { |
| throw new IllegalArgumentException("surfaceRotation value out of bounds"); |
| } |
| |
| synchronized (mDataStore) { |
| try { |
| if (mDataStore.setTouchCalibration(inputDeviceDescriptor, surfaceRotation, |
| calibration)) { |
| nativeReloadCalibration(mPtr); |
| } |
| } finally { |
| mDataStore.saveIfNeeded(); |
| } |
| } |
| } |
| |
| @Override // Binder call |
| public int isInTabletMode() { |
| if (!checkCallingPermission(android.Manifest.permission.TABLET_MODE, |
| "isInTabletMode()")) { |
| throw new SecurityException("Requires TABLET_MODE permission"); |
| } |
| return getSwitchState(-1, InputDevice.SOURCE_ANY, SW_TABLET_MODE); |
| } |
| |
| @Override // Binder call |
| public int isMicMuted() { |
| return getSwitchState(-1, InputDevice.SOURCE_ANY, SW_MUTE_DEVICE); |
| } |
| |
| @Override // Binder call |
| public void registerTabletModeChangedListener(ITabletModeChangedListener listener) { |
| if (!checkCallingPermission(android.Manifest.permission.TABLET_MODE, |
| "registerTabletModeChangedListener()")) { |
| throw new SecurityException("Requires TABLET_MODE_LISTENER permission"); |
| } |
| if (listener == null) { |
| throw new IllegalArgumentException("listener must not be null"); |
| } |
| |
| synchronized (mTabletModeLock) { |
| final int callingPid = Binder.getCallingPid(); |
| if (mTabletModeChangedListeners.get(callingPid) != null) { |
| throw new IllegalStateException("The calling process has already registered " |
| + "a TabletModeChangedListener."); |
| } |
| TabletModeChangedListenerRecord record = |
| new TabletModeChangedListenerRecord(callingPid, listener); |
| try { |
| IBinder binder = listener.asBinder(); |
| binder.linkToDeath(record, 0); |
| } catch (RemoteException ex) { |
| throw new RuntimeException(ex); |
| } |
| mTabletModeChangedListeners.put(callingPid, record); |
| } |
| } |
| |
| private void onTabletModeChangedListenerDied(int pid) { |
| synchronized (mTabletModeLock) { |
| mTabletModeChangedListeners.remove(pid); |
| } |
| } |
| |
| // Must be called on handler |
| private void deliverTabletModeChanged(long whenNanos, boolean inTabletMode) { |
| mTempTabletModeChangedListenersToNotify.clear(); |
| final int numListeners; |
| synchronized (mTabletModeLock) { |
| numListeners = mTabletModeChangedListeners.size(); |
| for (int i = 0; i < numListeners; i++) { |
| mTempTabletModeChangedListenersToNotify.add( |
| mTabletModeChangedListeners.valueAt(i)); |
| } |
| } |
| for (int i = 0; i < numListeners; i++) { |
| mTempTabletModeChangedListenersToNotify.get(i).notifyTabletModeChanged( |
| whenNanos, inTabletMode); |
| } |
| } |
| |
| // 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(new KeyboardLayoutVisitor() { |
| @Override |
| public void visitKeyboardLayout(Resources resources, |
| int keyboardLayoutResId, KeyboardLayout 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<KeyboardLayout>(); |
| visitAllKeyboardLayouts(new KeyboardLayoutVisitor() { |
| @Override |
| public void visitKeyboardLayout(Resources resources, |
| int keyboardLayoutResId, KeyboardLayout layout) { |
| list.add(layout); |
| } |
| }); |
| return list.toArray(new KeyboardLayout[list.size()]); |
| } |
| |
| @Override // Binder call |
| public KeyboardLayout[] getKeyboardLayoutsForInputDevice( |
| final InputDeviceIdentifier identifier) { |
| final String[] enabledLayoutDescriptors = |
| getEnabledKeyboardLayoutsForInputDevice(identifier); |
| final ArrayList<KeyboardLayout> enabledLayouts = |
| new ArrayList<KeyboardLayout>(enabledLayoutDescriptors.length); |
| final ArrayList<KeyboardLayout> potentialLayouts = new ArrayList<KeyboardLayout>(); |
| 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; |
| } |
| |
| @Override // Binder call |
| public KeyboardLayout getKeyboardLayout(String keyboardLayoutDescriptor) { |
| if (keyboardLayoutDescriptor == null) { |
| throw new IllegalArgumentException("keyboardLayoutDescriptor must not be null"); |
| } |
| |
| final KeyboardLayout[] result = new KeyboardLayout[1]; |
| visitKeyboardLayout(keyboardLayoutDescriptor, new KeyboardLayoutVisitor() { |
| @Override |
| public void visitKeyboardLayout(Resources resources, |
| int keyboardLayoutResId, KeyboardLayout 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 ex) { |
| } |
| } |
| } |
| |
| 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); |
| XmlResourceParser parser = resources.getXml(configResId); |
| try { |
| XmlUtils.beginDocument(parser, "keyboard-layouts"); |
| |
| for (;;) { |
| XmlUtils.nextElement(parser); |
| String element = parser.getName(); |
| if (element == null) { |
| break; |
| } |
| if (element.equals("keyboard-layout")) { |
| TypedArray a = resources.obtainAttributes( |
| parser, com.android.internal.R.styleable.KeyboardLayout); |
| try { |
| String name = a.getString( |
| com.android.internal.R.styleable.KeyboardLayout_name); |
| String label = a.getString( |
| com.android.internal.R.styleable.KeyboardLayout_label); |
| int keyboardLayoutResId = a.getResourceId( |
| com.android.internal.R.styleable.KeyboardLayout_keyboardLayout, |
| 0); |
| String languageTags = a.getString( |
| com.android.internal.R.styleable.KeyboardLayout_locale); |
| LocaleList locales = getLocalesFromLanguageTags(languageTags); |
| int vid = a.getInt( |
| com.android.internal.R.styleable.KeyboardLayout_vendorId, -1); |
| int pid = a.getInt( |
| com.android.internal.R.styleable.KeyboardLayout_productId, -1); |
| |
| if (name == null || label == null || keyboardLayoutResId == 0) { |
| 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); |
| } |
| } |
| } finally { |
| parser.close(); |
| } |
| } 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) { |
| if (identifier == null || identifier.getDescriptor() == null) { |
| throw new IllegalArgumentException("identifier and descriptor must not be null"); |
| } |
| |
| if (identifier.getVendorId() == 0 && identifier.getProductId() == 0) { |
| return identifier.getDescriptor(); |
| } |
| StringBuilder bob = new StringBuilder(); |
| bob.append("vendor:").append(identifier.getVendorId()); |
| bob.append(",product:").append(identifier.getProductId()); |
| return bob.toString(); |
| } |
| |
| @Override // Binder call |
| public String getCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier) { |
| |
| String key = getLayoutDescriptor(identifier); |
| synchronized (mDataStore) { |
| String layout = null; |
| // 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, "Loaded keyboard layout id for " + key + " and got " |
| + layout); |
| } |
| return 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"); |
| } |
| if (keyboardLayoutDescriptor == null) { |
| throw new IllegalArgumentException("keyboardLayoutDescriptor must not be null"); |
| } |
| |
| String key = getLayoutDescriptor(identifier); |
| synchronized (mDataStore) { |
| try { |
| if (mDataStore.setCurrentKeyboardLayout(key, keyboardLayoutDescriptor)) { |
| if (DEBUG) { |
| Slog.d(TAG, "Saved keyboard layout using " + key); |
| } |
| mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS); |
| } |
| } finally { |
| mDataStore.saveIfNeeded(); |
| } |
| } |
| } |
| |
| @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; |
| } |
| } |
| |
| @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"); |
| } |
| if (keyboardLayoutDescriptor == null) { |
| throw new IllegalArgumentException("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(); |
| } |
| } |
| } |
| |
| @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"); |
| } |
| if (keyboardLayoutDescriptor == null) { |
| throw new IllegalArgumentException("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 void setFocusedApplication(int displayId, InputApplicationHandle application) { |
| nativeSetFocusedApplication(mPtr, displayId, application); |
| } |
| |
| public void setFocusedDisplay(int displayId) { |
| nativeSetFocusedDisplay(mPtr, displayId); |
| } |
| |
| /** Clean up input window handles of the given display. */ |
| public void onDisplayRemoved(int displayId) { |
| if (mPointerIconDisplayContext != null |
| && mPointerIconDisplayContext.getDisplay().getDisplayId() == displayId) { |
| mPointerIconDisplayContext = null; |
| } |
| |
| nativeDisplayRemoved(mPtr, displayId); |
| } |
| |
| @Override |
| public void requestPointerCapture(IBinder inputChannelToken, boolean enabled) { |
| if (inputChannelToken == null) { |
| return; |
| } |
| |
| nativeRequestPointerCapture(mPtr, inputChannelToken, enabled); |
| } |
| |
| public void setInputDispatchMode(boolean enabled, boolean frozen) { |
| nativeSetInputDispatchMode(mPtr, enabled, frozen); |
| } |
| |
| public void setSystemUiLightsOut(boolean lightsOut) { |
| nativeSetSystemUiLightsOut(mPtr, lightsOut); |
| } |
| |
| /** |
| * Atomically transfers touch focus from one window to another as identified by |
| * their input channels. It is possible for multiple windows to have |
| * touch focus if they support split touch dispatch |
| * {@link android.view.WindowManager.LayoutParams#FLAG_SPLIT_TOUCH} but this |
| * method only transfers touch focus of the specified window without affecting |
| * other windows that may also have touch focus at the same time. |
| * @param fromChannel The channel of a window that currently has touch focus. |
| * @param toChannel The channel of the window that should receive touch focus in |
| * place of the first. |
| * @param isDragDrop True if transfer touch focus for drag and drop. |
| * @return True if the transfer was successful. False if the window with the |
| * specified channel did not actually have touch focus at the time of the request. |
| */ |
| public boolean transferTouchFocus(@NonNull InputChannel fromChannel, |
| @NonNull InputChannel toChannel, boolean isDragDrop) { |
| return nativeTransferTouchFocus(mPtr, fromChannel.getToken(), toChannel.getToken(), |
| isDragDrop); |
| } |
| |
| /** |
| * Atomically transfers touch focus from one window to another as identified by |
| * their input channels. It is possible for multiple windows to have |
| * touch focus if they support split touch dispatch |
| * {@link android.view.WindowManager.LayoutParams#FLAG_SPLIT_TOUCH} but this |
| * method only transfers touch focus of the specified window without affecting |
| * other windows that may also have touch focus at the same time. |
| * @param fromChannelToken The channel token of a window that currently has touch focus. |
| * @param toChannelToken The channel token of the window that should receive touch focus in |
| * place of the first. |
| * @return True if the transfer was successful. False if the window with the |
| * specified channel did not actually have touch focus at the time of the request. |
| */ |
| public boolean transferTouchFocus(@NonNull IBinder fromChannelToken, |
| @NonNull IBinder toChannelToken) { |
| Objects.nonNull(fromChannelToken); |
| Objects.nonNull(toChannelToken); |
| return nativeTransferTouchFocus(mPtr, fromChannelToken, toChannelToken, |
| false /* isDragDrop */); |
| } |
| |
| @Override // Binder call |
| public void tryPointerSpeed(int speed) { |
| if (!checkCallingPermission(android.Manifest.permission.SET_POINTER_SPEED, |
| "tryPointerSpeed()")) { |
| throw new SecurityException("Requires SET_POINTER_SPEED permission"); |
| } |
| |
| if (speed < InputManager.MIN_POINTER_SPEED || speed > InputManager.MAX_POINTER_SPEED) { |
| throw new IllegalArgumentException("speed out of range"); |
| } |
| |
| setPointerSpeedUnchecked(speed); |
| } |
| |
| private void updatePointerSpeedFromSettings() { |
| int speed = getPointerSpeedSetting(); |
| setPointerSpeedUnchecked(speed); |
| } |
| |
| private void setPointerSpeedUnchecked(int speed) { |
| speed = Math.min(Math.max(speed, InputManager.MIN_POINTER_SPEED), |
| InputManager.MAX_POINTER_SPEED); |
| nativeSetPointerSpeed(mPtr, speed); |
| } |
| |
| private void registerPointerSpeedSettingObserver() { |
| mContext.getContentResolver().registerContentObserver( |
| Settings.System.getUriFor(Settings.System.POINTER_SPEED), true, |
| new ContentObserver(mHandler) { |
| @Override |
| public void onChange(boolean selfChange) { |
| updatePointerSpeedFromSettings(); |
| } |
| }, UserHandle.USER_ALL); |
| } |
| |
| private int getPointerSpeedSetting() { |
| int speed = InputManager.DEFAULT_POINTER_SPEED; |
| try { |
| speed = Settings.System.getIntForUser(mContext.getContentResolver(), |
| Settings.System.POINTER_SPEED, UserHandle.USER_CURRENT); |
| } catch (SettingNotFoundException snfe) { |
| } |
| return speed; |
| } |
| |
| private void updateShowTouchesFromSettings() { |
| int setting = getShowTouchesSetting(0); |
| nativeSetShowTouches(mPtr, setting != 0); |
| } |
| |
| private void registerShowTouchesSettingObserver() { |
| mContext.getContentResolver().registerContentObserver( |
| Settings.System.getUriFor(Settings.System.SHOW_TOUCHES), true, |
| new ContentObserver(mHandler) { |
| @Override |
| public void onChange(boolean selfChange) { |
| updateShowTouchesFromSettings(); |
| } |
| }, UserHandle.USER_ALL); |
| } |
| |
| private void updateAccessibilityLargePointerFromSettings() { |
| final int accessibilityConfig = Settings.Secure.getIntForUser( |
| mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON, |
| 0, UserHandle.USER_CURRENT); |
| PointerIcon.setUseLargeIcons(accessibilityConfig == 1); |
| nativeReloadPointerIcons(mPtr); |
| } |
| |
| private void registerAccessibilityLargePointerSettingObserver() { |
| mContext.getContentResolver().registerContentObserver( |
| Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON), true, |
| new ContentObserver(mHandler) { |
| @Override |
| public void onChange(boolean selfChange) { |
| updateAccessibilityLargePointerFromSettings(); |
| } |
| }, UserHandle.USER_ALL); |
| } |
| |
| private void updateDeepPressStatusFromSettings(String reason) { |
| // Not using ViewConfiguration.getLongPressTimeout here because it may return a stale value |
| final int timeout = Settings.Secure.getIntForUser(mContext.getContentResolver(), |
| Settings.Secure.LONG_PRESS_TIMEOUT, ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT, |
| UserHandle.USER_CURRENT); |
| final boolean featureEnabledFlag = |
| DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT, |
| DEEP_PRESS_ENABLED, true /* default */); |
| final boolean enabled = |
| featureEnabledFlag && timeout <= ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT; |
| Log.i(TAG, |
| (enabled ? "Enabling" : "Disabling") + " motion classifier because " + reason |
| + ": feature " + (featureEnabledFlag ? "enabled" : "disabled") |
| + ", long press timeout = " + timeout); |
| nativeSetMotionClassifierEnabled(mPtr, enabled); |
| } |
| |
| private void registerLongPressTimeoutObserver() { |
| mContext.getContentResolver().registerContentObserver( |
| Settings.Secure.getUriFor(Settings.Secure.LONG_PRESS_TIMEOUT), true, |
| new ContentObserver(mHandler) { |
| @Override |
| public void onChange(boolean selfChange) { |
| updateDeepPressStatusFromSettings("timeout changed"); |
| } |
| }, UserHandle.USER_ALL); |
| } |
| |
| private void registerBlockUntrustedTouchesModeSettingObserver() { |
| mContext.getContentResolver().registerContentObserver( |
| Settings.Global.getUriFor(Settings.Global.BLOCK_UNTRUSTED_TOUCHES_MODE), |
| /* notifyForDescendants */ true, |
| new ContentObserver(mHandler) { |
| @Override |
| public void onChange(boolean selfChange) { |
| updateBlockUntrustedTouchesModeFromSettings(); |
| } |
| }, UserHandle.USER_ALL); |
| } |
| |
| private void updateBlockUntrustedTouchesModeFromSettings() { |
| final int mode = InputManager.getInstance().getBlockUntrustedTouchesMode(mContext); |
| nativeSetBlockUntrustedTouchesMode(mPtr, mode); |
| } |
| |
| private void registerMaximumObscuringOpacityForTouchSettingObserver() { |
| mContext.getContentResolver().registerContentObserver( |
| Settings.Global.getUriFor(Settings.Global.MAXIMUM_OBSCURING_OPACITY_FOR_TOUCH), |
| /* notifyForDescendants */ true, |
| new ContentObserver(mHandler) { |
| @Override |
| public void onChange(boolean selfChange) { |
| updateMaximumObscuringOpacityForTouchFromSettings(); |
| } |
| }, UserHandle.USER_ALL); |
| } |
| |
| private void updateMaximumObscuringOpacityForTouchFromSettings() { |
| final float opacity = InputManager.getInstance().getMaximumObscuringOpacityForTouch(); |
| if (opacity < 0 || opacity > 1) { |
| Log.e(TAG, "Invalid maximum obscuring opacity " + opacity |
| + ", it should be >= 0 and <= 1, rejecting update."); |
| return; |
| } |
| nativeSetMaximumObscuringOpacityForTouch(mPtr, opacity); |
| } |
| |
| private int getShowTouchesSetting(int defaultValue) { |
| int result = defaultValue; |
| try { |
| result = Settings.System.getIntForUser(mContext.getContentResolver(), |
| Settings.System.SHOW_TOUCHES, UserHandle.USER_CURRENT); |
| } catch (SettingNotFoundException snfe) { |
| } |
| return result; |
| } |
| |
| private static class VibrationInfo { |
| private final long[] mPattern; |
| private final int[] mAmplitudes; |
| private final int mRepeat; |
| |
| public long[] getPattern() { |
| return mPattern; |
| } |
| |
| public int[] getAmplitudes() { |
| return mAmplitudes; |
| } |
| |
| public int getRepeatIndex() { |
| return mRepeat; |
| } |
| |
| VibrationInfo(VibrationEffect effect) { |
| long[] pattern = null; |
| int[] amplitudes = null; |
| int patternRepeatIndex = -1; |
| int amplitudeCount = -1; |
| |
| if (effect instanceof VibrationEffect.Composed) { |
| VibrationEffect.Composed composed = (VibrationEffect.Composed) effect; |
| int segmentCount = composed.getSegments().size(); |
| pattern = new long[segmentCount]; |
| amplitudes = new int[segmentCount]; |
| patternRepeatIndex = composed.getRepeatIndex(); |
| amplitudeCount = 0; |
| for (int i = 0; i < segmentCount; i++) { |
| VibrationEffectSegment segment = composed.getSegments().get(i); |
| if (composed.getRepeatIndex() == i) { |
| patternRepeatIndex = amplitudeCount; |
| } |
| if (!(segment instanceof StepSegment)) { |
| Slog.w(TAG, "Input devices don't support segment " + segment); |
| amplitudeCount = -1; |
| break; |
| } |
| float amplitude = ((StepSegment) segment).getAmplitude(); |
| if (Float.compare(amplitude, VibrationEffect.DEFAULT_AMPLITUDE) == 0) { |
| amplitudes[amplitudeCount] = DEFAULT_VIBRATION_MAGNITUDE; |
| } else { |
| amplitudes[amplitudeCount] = |
| (int) (amplitude * VibrationEffect.MAX_AMPLITUDE); |
| } |
| pattern[amplitudeCount++] = segment.getDuration(); |
| } |
| } |
| |
| if (amplitudeCount < 0) { |
| Slog.w(TAG, "Only oneshot and step waveforms are supported on input devices"); |
| mPattern = new long[0]; |
| mAmplitudes = new int[0]; |
| mRepeat = -1; |
| } else { |
| mRepeat = patternRepeatIndex; |
| mPattern = new long[amplitudeCount]; |
| mAmplitudes = new int[amplitudeCount]; |
| System.arraycopy(pattern, 0, mPattern, 0, amplitudeCount); |
| System.arraycopy(amplitudes, 0, mAmplitudes, 0, amplitudeCount); |
| if (mRepeat >= mPattern.length) { |
| throw new ArrayIndexOutOfBoundsException("Repeat index " + mRepeat |
| + " must be within the bounds of the pattern.length " |
| + mPattern.length); |
| } |
| } |
| } |
| } |
| |
| private VibratorToken getVibratorToken(int deviceId, IBinder token) { |
| VibratorToken v; |
| synchronized (mVibratorLock) { |
| v = mVibratorTokens.get(token); |
| if (v == null) { |
| v = new VibratorToken(deviceId, token, mNextVibratorTokenValue++); |
| try { |
| token.linkToDeath(v, 0); |
| } catch (RemoteException ex) { |
| // give up |
| throw new RuntimeException(ex); |
| } |
| mVibratorTokens.put(token, v); |
| } |
| } |
| return v; |
| } |
| |
| // Binder call |
| @Override |
| public void vibrate(int deviceId, VibrationEffect effect, IBinder token) { |
| VibrationInfo info = new VibrationInfo(effect); |
| VibratorToken v = getVibratorToken(deviceId, token); |
| synchronized (v) { |
| v.mVibrating = true; |
| nativeVibrate(mPtr, deviceId, info.getPattern(), info.getAmplitudes(), |
| info.getRepeatIndex(), v.mTokenValue); |
| } |
| } |
| |
| // Binder call |
| @Override |
| public int[] getVibratorIds(int deviceId) { |
| return nativeGetVibratorIds(mPtr, deviceId); |
| } |
| |
| // Binder call |
| @Override |
| public boolean isVibrating(int deviceId) { |
| return nativeIsVibrating(mPtr, deviceId); |
| } |
| |
| // Binder call |
| @Override |
| public void vibrateCombined(int deviceId, CombinedVibration effect, IBinder token) { |
| VibratorToken v = getVibratorToken(deviceId, token); |
| synchronized (v) { |
| if (!(effect instanceof CombinedVibration.Mono) |
| && !(effect instanceof CombinedVibration.Stereo)) { |
| Slog.e(TAG, "Only Mono and Stereo effects are supported"); |
| return; |
| } |
| |
| v.mVibrating = true; |
| if (effect instanceof CombinedVibration.Mono) { |
| CombinedVibration.Mono mono = (CombinedVibration.Mono) effect; |
| VibrationInfo info = new VibrationInfo(mono.getEffect()); |
| nativeVibrate(mPtr, deviceId, info.getPattern(), info.getAmplitudes(), |
| info.getRepeatIndex(), v.mTokenValue); |
| } else if (effect instanceof CombinedVibration.Stereo) { |
| CombinedVibration.Stereo stereo = (CombinedVibration.Stereo) effect; |
| SparseArray<VibrationEffect> effects = stereo.getEffects(); |
| long[] pattern = new long[0]; |
| int repeat = Integer.MIN_VALUE; |
| SparseArray<int[]> amplitudes = new SparseArray<int[]>(effects.size()); |
| for (int i = 0; i < effects.size(); i++) { |
| VibrationInfo info = new VibrationInfo(effects.valueAt(i)); |
| // Pattern of all effects should be same |
| if (pattern.length == 0) { |
| pattern = info.getPattern(); |
| } |
| if (repeat == Integer.MIN_VALUE) { |
| repeat = info.getRepeatIndex(); |
| } |
| amplitudes.put(effects.keyAt(i), info.getAmplitudes()); |
| } |
| nativeVibrateCombined(mPtr, deviceId, pattern, amplitudes, repeat, |
| v.mTokenValue); |
| } |
| } |
| } |
| |
| // Binder call |
| @Override |
| public void cancelVibrate(int deviceId, IBinder token) { |
| VibratorToken v; |
| synchronized (mVibratorLock) { |
| v = mVibratorTokens.get(token); |
| if (v == null || v.mDeviceId != deviceId) { |
| return; // nothing to cancel |
| } |
| } |
| |
| cancelVibrateIfNeeded(v); |
| } |
| |
| void onVibratorTokenDied(VibratorToken v) { |
| synchronized (mVibratorLock) { |
| mVibratorTokens.remove(v.mToken); |
| } |
| |
| cancelVibrateIfNeeded(v); |
| } |
| |
| private void cancelVibrateIfNeeded(VibratorToken v) { |
| synchronized (v) { |
| if (v.mVibrating) { |
| nativeCancelVibrate(mPtr, v.mDeviceId, v.mTokenValue); |
| v.mVibrating = false; |
| } |
| } |
| } |
| |
| // Native callback. |
| private void notifyVibratorState(int deviceId, boolean isOn) { |
| if (DEBUG) { |
| Slog.d(TAG, "notifyVibratorState: deviceId=" + deviceId + " isOn=" + isOn); |
| } |
| synchronized (mVibratorLock) { |
| mIsVibrating.put(deviceId, isOn); |
| notifyVibratorStateListenersLocked(deviceId); |
| } |
| } |
| |
| @GuardedBy("mVibratorLock") |
| private void notifyVibratorStateListenersLocked(int deviceId) { |
| if (!mVibratorStateListeners.contains(deviceId)) { |
| if (DEBUG) { |
| Slog.v(TAG, "Device " + deviceId + " doesn't have vibrator state listener."); |
| } |
| return; |
| } |
| RemoteCallbackList<IVibratorStateListener> listeners = |
| mVibratorStateListeners.get(deviceId); |
| final int length = listeners.beginBroadcast(); |
| try { |
| for (int i = 0; i < length; i++) { |
| notifyVibratorStateListenerLocked(deviceId, listeners.getBroadcastItem(i)); |
| } |
| } finally { |
| listeners.finishBroadcast(); |
| } |
| } |
| |
| @GuardedBy("mVibratorLock") |
| private void notifyVibratorStateListenerLocked(int deviceId, IVibratorStateListener listener) { |
| try { |
| listener.onVibrating(mIsVibrating.get(deviceId)); |
| } catch (RemoteException | RuntimeException e) { |
| Slog.e(TAG, "Vibrator state listener failed to call", e); |
| } |
| } |
| |
| @Override // Binder call |
| public boolean registerVibratorStateListener(int deviceId, IVibratorStateListener listener) { |
| Preconditions.checkNotNull(listener, "listener must not be null"); |
| |
| RemoteCallbackList<IVibratorStateListener> listeners; |
| synchronized (mVibratorLock) { |
| if (!mVibratorStateListeners.contains(deviceId)) { |
| listeners = new RemoteCallbackList<>(); |
| mVibratorStateListeners.put(deviceId, listeners); |
| } else { |
| listeners = mVibratorStateListeners.get(deviceId); |
| } |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| if (!listeners.register(listener)) { |
| Slog.e(TAG, "Could not register vibrator state listener " + listener); |
| return false; |
| } |
| // Notify its callback after new client registered. |
| notifyVibratorStateListenerLocked(deviceId, listener); |
| return true; |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| } |
| |
| @Override // Binder call |
| public boolean unregisterVibratorStateListener(int deviceId, IVibratorStateListener listener) { |
| synchronized (mVibratorLock) { |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| if (!mVibratorStateListeners.contains(deviceId)) { |
| Slog.w(TAG, "Vibrator state listener " + deviceId + " doesn't exist"); |
| return false; |
| } |
| RemoteCallbackList<IVibratorStateListener> listeners = |
| mVibratorStateListeners.get(deviceId); |
| return listeners.unregister(listener); |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| } |
| |
| // Binder call |
| @Override |
| public int getBatteryStatus(int deviceId) { |
| return nativeGetBatteryStatus(mPtr, deviceId); |
| } |
| |
| // Binder call |
| @Override |
| public int getBatteryCapacity(int deviceId) { |
| return nativeGetBatteryCapacity(mPtr, deviceId); |
| } |
| |
| // Binder call |
| @Override |
| public void setPointerIconType(int iconId) { |
| nativeSetPointerIconType(mPtr, iconId); |
| } |
| |
| // Binder call |
| @Override |
| public void setCustomPointerIcon(PointerIcon icon) { |
| Objects.requireNonNull(icon); |
| nativeSetCustomPointerIcon(mPtr, icon); |
| } |
| |
| /** |
| * Add a runtime association between the input port and the display port. This overrides any |
| * static associations. |
| * @param inputPort The port of the input device. |
| * @param displayPort The physical port of the associated display. |
| */ |
| @Override // Binder call |
| public void addPortAssociation(@NonNull String inputPort, int displayPort) { |
| if (!checkCallingPermission( |
| android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY, |
| "addPortAssociation()")) { |
| throw new SecurityException( |
| "Requires ASSOCIATE_INPUT_DEVICE_TO_DISPLAY permission"); |
| } |
| |
| Objects.requireNonNull(inputPort); |
| synchronized (mAssociationsLock) { |
| mRuntimeAssociations.put(inputPort, displayPort); |
| } |
| nativeNotifyPortAssociationsChanged(mPtr); |
| } |
| |
| /** |
| * Remove the runtime association between the input port and the display port. Any existing |
| * static association for the cleared input port will be restored. |
| * @param inputPort The port of the input device to be cleared. |
| */ |
| @Override // Binder call |
| public void removePortAssociation(@NonNull String inputPort) { |
| if (!checkCallingPermission( |
| android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY, |
| "clearPortAssociations()")) { |
| throw new SecurityException( |
| "Requires ASSOCIATE_INPUT_DEVICE_TO_DISPLAY permission"); |
| } |
| |
| Objects.requireNonNull(inputPort); |
| synchronized (mAssociationsLock) { |
| mRuntimeAssociations.remove(inputPort); |
| } |
| nativeNotifyPortAssociationsChanged(mPtr); |
| } |
| |
| /** |
| * Add a runtime association between the input device name and the display unique id. |
| * @param inputDeviceName The name of the input device. |
| * @param displayUniqueId The unique id of the associated display. |
| */ |
| @Override // Binder call |
| public void addUniqueIdAssociation(@NonNull String inputDeviceName, |
| @NonNull String displayUniqueId) { |
| if (!checkCallingPermission( |
| android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY, |
| "addNameAssociation()")) { |
| throw new SecurityException( |
| "Requires ASSOCIATE_INPUT_DEVICE_TO_DISPLAY permission"); |
| } |
| |
| Objects.requireNonNull(inputDeviceName); |
| Objects.requireNonNull(displayUniqueId); |
| synchronized (mAssociationsLock) { |
| mUniqueIdAssociations.put(inputDeviceName, displayUniqueId); |
| } |
| nativeChangeUniqueIdAssociation(mPtr); |
| } |
| |
| /** |
| * Remove the runtime association between the input device and the display. |
| * @param inputDeviceName The port of the input device to be cleared. |
| */ |
| @Override // Binder call |
| public void removeUniqueIdAssociation(@NonNull String inputDeviceName) { |
| if (!checkCallingPermission( |
| android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY, |
| "removeUniqueIdAssociation()")) { |
| throw new SecurityException( |
| "Requires ASSOCIATE_INPUT_DEVICE_TO_DISPLAY permission"); |
| } |
| |
| Objects.requireNonNull(inputDeviceName); |
| synchronized (mAssociationsLock) { |
| mUniqueIdAssociations.remove(inputDeviceName); |
| } |
| nativeChangeUniqueIdAssociation(mPtr); |
| } |
| |
| @Override // Binder call |
| public InputSensorInfo[] getSensorList(int deviceId) { |
| return nativeGetSensorList(mPtr, deviceId); |
| } |
| |
| @Override // Binder call |
| public boolean registerSensorListener(IInputSensorEventListener listener) { |
| if (DEBUG) { |
| Slog.d(TAG, "registerSensorListener: listener=" + listener + " callingPid=" |
| + Binder.getCallingPid()); |
| } |
| if (listener == null) { |
| Slog.e(TAG, "listener must not be null"); |
| return false; |
| } |
| |
| synchronized (mInputDevicesLock) { |
| int callingPid = Binder.getCallingPid(); |
| if (mSensorEventListeners.get(callingPid) != null) { |
| Slog.e(TAG, "The calling process " + callingPid + " has already " |
| + "registered an InputSensorEventListener."); |
| return false; |
| } |
| |
| SensorEventListenerRecord record = |
| new SensorEventListenerRecord(callingPid, listener); |
| try { |
| IBinder binder = listener.asBinder(); |
| binder.linkToDeath(record, 0); |
| } catch (RemoteException ex) { |
| // give up |
| throw new RuntimeException(ex); |
| } |
| |
| mSensorEventListeners.put(callingPid, record); |
| } |
| return true; |
| } |
| |
| @Override // Binder call |
| public void unregisterSensorListener(IInputSensorEventListener listener) { |
| if (DEBUG) { |
| Slog.d(TAG, "unregisterSensorListener: listener=" + listener + " callingPid=" |
| + Binder.getCallingPid()); |
| } |
| |
| if (listener == null) { |
| throw new IllegalArgumentException("listener must not be null"); |
| } |
| |
| synchronized (mInputDevicesLock) { |
| int callingPid = Binder.getCallingPid(); |
| if (mSensorEventListeners.get(callingPid) != null) { |
| SensorEventListenerRecord record = mSensorEventListeners.get(callingPid); |
| if (record.getListener().asBinder() != listener.asBinder()) { |
| throw new IllegalArgumentException("listener is not registered"); |
| } |
| mSensorEventListeners.remove(callingPid); |
| } |
| } |
| } |
| |
| @Override // Binder call |
| public boolean flushSensor(int deviceId, int sensorType) { |
| synchronized (mInputDevicesLock) { |
| int callingPid = Binder.getCallingPid(); |
| SensorEventListenerRecord listener = mSensorEventListeners.get(callingPid); |
| if (listener != null) { |
| return nativeFlushSensor(mPtr, deviceId, sensorType); |
| } |
| return false; |
| } |
| } |
| |
| @Override // Binder call |
| public boolean enableSensor(int deviceId, int sensorType, int samplingPeriodUs, |
| int maxBatchReportLatencyUs) { |
| synchronized (mInputDevicesLock) { |
| return nativeEnableSensor(mPtr, deviceId, sensorType, samplingPeriodUs, |
| maxBatchReportLatencyUs); |
| } |
| } |
| |
| @Override // Binder call |
| public void disableSensor(int deviceId, int sensorType) { |
| synchronized (mInputDevicesLock) { |
| nativeDisableSensor(mPtr, deviceId, sensorType); |
| } |
| } |
| |
| /** |
| * LightSession represents a light session for lights manager. |
| */ |
| private final class LightSession implements DeathRecipient { |
| private final int mDeviceId; |
| private final IBinder mToken; |
| private final String mOpPkg; |
| // The light ids and states that are requested by the light seesion |
| private int[] mLightIds; |
| private LightState[] mLightStates; |
| |
| LightSession(int deviceId, String opPkg, IBinder token) { |
| mDeviceId = deviceId; |
| mOpPkg = opPkg; |
| mToken = token; |
| } |
| |
| @Override |
| public void binderDied() { |
| if (DEBUG) { |
| Slog.d(TAG, "Light token died."); |
| } |
| synchronized (mLightLock) { |
| closeLightSession(mDeviceId, mToken); |
| mLightSessions.remove(mToken); |
| } |
| } |
| } |
| |
| /** |
| * Returns the lights available for apps to control on the specified input device. |
| * Only lights that aren't reserved for system use are available to apps. |
| */ |
| @Override // Binder call |
| public List<Light> getLights(int deviceId) { |
| return nativeGetLights(mPtr, deviceId); |
| } |
| |
| /** |
| * Set specified light state with for a specific input device. |
| */ |
| private void setLightStateInternal(int deviceId, Light light, LightState lightState) { |
| Preconditions.checkNotNull(light, "light does not exist"); |
| if (DEBUG) { |
| Slog.d(TAG, "setLightStateInternal device " + deviceId + " light " + light |
| + "lightState " + lightState); |
| } |
| if (light.getType() == Light.LIGHT_TYPE_PLAYER_ID) { |
| nativeSetLightPlayerId(mPtr, deviceId, light.getId(), lightState.getPlayerId()); |
| } else { |
| // Set ARGB format color to input device light |
| // Refer to https://developer.android.com/reference/kotlin/android/graphics/Color |
| nativeSetLightColor(mPtr, deviceId, light.getId(), lightState.getColor()); |
| } |
| } |
| |
| /** |
| * Set multiple light states with multiple light ids for a specific input device. |
| */ |
| private void setLightStatesInternal(int deviceId, int[] lightIds, LightState[] lightStates) { |
| final List<Light> lights = nativeGetLights(mPtr, deviceId); |
| SparseArray<Light> lightArray = new SparseArray<>(); |
| for (int i = 0; i < lights.size(); i++) { |
| lightArray.put(lights.get(i).getId(), lights.get(i)); |
| } |
| for (int i = 0; i < lightIds.length; i++) { |
| if (lightArray.contains(lightIds[i])) { |
| setLightStateInternal(deviceId, lightArray.get(lightIds[i]), lightStates[i]); |
| } |
| } |
| } |
| |
| /** |
| * Set states for multiple lights for an opened light session. |
| */ |
| @Override |
| public void setLightStates(int deviceId, int[] lightIds, LightState[] lightStates, |
| IBinder token) { |
| Preconditions.checkArgument(lightIds.length == lightStates.length, |
| "lights and light states are not same length"); |
| synchronized (mLightLock) { |
| LightSession lightSession = mLightSessions.get(token); |
| Preconditions.checkArgument(lightSession != null, "not registered"); |
| Preconditions.checkState(lightSession.mDeviceId == deviceId, "Incorrect device ID"); |
| lightSession.mLightIds = lightIds.clone(); |
| lightSession.mLightStates = lightStates.clone(); |
| if (DEBUG) { |
| Slog.d(TAG, "setLightStates for " + lightSession.mOpPkg + " device " + deviceId); |
| } |
| } |
| setLightStatesInternal(deviceId, lightIds, lightStates); |
| } |
| |
| @Override |
| public @Nullable LightState getLightState(int deviceId, int lightId) { |
| synchronized (mLightLock) { |
| int color = nativeGetLightColor(mPtr, deviceId, lightId); |
| int playerId = nativeGetLightPlayerId(mPtr, deviceId, lightId); |
| |
| return new LightState(color, playerId); |
| } |
| } |
| |
| @Override |
| public void openLightSession(int deviceId, String opPkg, IBinder token) { |
| Preconditions.checkNotNull(token); |
| synchronized (mLightLock) { |
| Preconditions.checkState(mLightSessions.get(token) == null, "already registered"); |
| LightSession lightSession = new LightSession(deviceId, opPkg, token); |
| try { |
| token.linkToDeath(lightSession, 0); |
| } catch (RemoteException ex) { |
| // give up |
| ex.rethrowAsRuntimeException(); |
| } |
| mLightSessions.put(token, lightSession); |
| if (DEBUG) { |
| Slog.d(TAG, "Open light session for " + opPkg + " device " + deviceId); |
| } |
| } |
| } |
| |
| @Override |
| public void closeLightSession(int deviceId, IBinder token) { |
| Preconditions.checkNotNull(token); |
| synchronized (mLightLock) { |
| LightSession lightSession = mLightSessions.get(token); |
| Preconditions.checkState(lightSession != null, "not registered"); |
| // Turn off the lights that were previously requested by the session to be closed. |
| Arrays.fill(lightSession.mLightStates, new LightState(0)); |
| setLightStatesInternal(deviceId, lightSession.mLightIds, |
| lightSession.mLightStates); |
| mLightSessions.remove(token); |
| // If any other session is still pending with light request, apply the first session's |
| // request. |
| if (!mLightSessions.isEmpty()) { |
| LightSession nextSession = mLightSessions.valueAt(0); |
| setLightStatesInternal(deviceId, nextSession.mLightIds, nextSession.mLightStates); |
| } |
| } |
| } |
| |
| @Override |
| public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { |
| if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; |
| |
| pw.println("INPUT MANAGER (dumpsys input)\n"); |
| String dumpStr = nativeDump(mPtr); |
| if (dumpStr != null) { |
| pw.println(dumpStr); |
| dumpAssociations(pw); |
| } |
| } |
| |
| private void dumpAssociations(PrintWriter pw) { |
| if (!mStaticAssociations.isEmpty()) { |
| pw.println("Static Associations:"); |
| mStaticAssociations.forEach((k, v) -> { |
| pw.print(" port: " + k); |
| pw.println(" display: " + v); |
| }); |
| } |
| |
| synchronized (mAssociationsLock) { |
| if (!mRuntimeAssociations.isEmpty()) { |
| pw.println("Runtime Associations:"); |
| mRuntimeAssociations.forEach((k, v) -> { |
| pw.print(" port: " + k); |
| pw.println(" display: " + v); |
| }); |
| } |
| } |
| } |
| |
| private boolean checkCallingPermission(String permission, String func) { |
| // Quick check: if the calling permission is me, it's all okay. |
| if (Binder.getCallingPid() == Process.myPid()) { |
| return true; |
| } |
| |
| if (mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED) { |
| return true; |
| } |
| String msg = "Permission Denial: " + func + " from pid=" |
| + Binder.getCallingPid() |
| + ", uid=" + Binder.getCallingUid() |
| + " requires " + permission; |
| Slog.w(TAG, msg); |
| return false; |
| } |
| |
| // Called by the heartbeat to ensure locks are not held indefinitely (for deadlock detection). |
| @Override |
| public void monitor() { |
| synchronized (mInputFilterLock) { } |
| synchronized (mAssociationsLock) { /* Test if blocked by associations lock. */} |
| synchronized (mLidSwitchLock) { /* Test if blocked by lid switch lock. */ } |
| nativeMonitor(mPtr); |
| } |
| |
| // Native callback. |
| private void notifyConfigurationChanged(long whenNanos) { |
| mWindowManagerCallbacks.notifyConfigurationChanged(); |
| } |
| |
| // Native callback. |
| private void notifyInputDevicesChanged(InputDevice[] inputDevices) { |
| synchronized (mInputDevicesLock) { |
| if (!mInputDevicesChangedPending) { |
| mInputDevicesChangedPending = true; |
| mHandler.obtainMessage(MSG_DELIVER_INPUT_DEVICES_CHANGED, |
| mInputDevices).sendToTarget(); |
| } |
| |
| mInputDevices = inputDevices; |
| } |
| } |
| |
| // Native callback. |
| private void notifySwitch(long whenNanos, int switchValues, int switchMask) { |
| if (DEBUG) { |
| Slog.d(TAG, "notifySwitch: values=" + Integer.toHexString(switchValues) |
| + ", mask=" + Integer.toHexString(switchMask)); |
| } |
| |
| if ((switchMask & SW_LID_BIT) != 0) { |
| final boolean lidOpen = ((switchValues & SW_LID_BIT) == 0); |
| synchronized (mLidSwitchLock) { |
| if (mSystemReady) { |
| for (int i = 0; i < mLidSwitchCallbacks.size(); i++) { |
| LidSwitchCallback callbacks = mLidSwitchCallbacks.get(i); |
| callbacks.notifyLidSwitchChanged(whenNanos, lidOpen); |
| } |
| } |
| } |
| } |
| |
| if ((switchMask & SW_CAMERA_LENS_COVER_BIT) != 0) { |
| final boolean lensCovered = ((switchValues & SW_CAMERA_LENS_COVER_BIT) != 0); |
| mWindowManagerCallbacks.notifyCameraLensCoverSwitchChanged(whenNanos, lensCovered); |
| } |
| |
| if (mUseDevInputEventForAudioJack && (switchMask & SW_JACK_BITS) != 0) { |
| mWiredAccessoryCallbacks.notifyWiredAccessoryChanged(whenNanos, switchValues, |
| switchMask); |
| } |
| |
| if ((switchMask & SW_TABLET_MODE_BIT) != 0) { |
| SomeArgs args = SomeArgs.obtain(); |
| args.argi1 = (int) (whenNanos & 0xFFFFFFFF); |
| args.argi2 = (int) (whenNanos >> 32); |
| args.arg1 = Boolean.valueOf((switchValues & SW_TABLET_MODE_BIT) != 0); |
| mHandler.obtainMessage(MSG_DELIVER_TABLET_MODE_CHANGED, |
| args).sendToTarget(); |
| } |
| |
| if ((switchMask & SW_MUTE_DEVICE_BIT) != 0) { |
| final boolean micMute = ((switchValues & SW_MUTE_DEVICE_BIT) != 0); |
| AudioManager audioManager = mContext.getSystemService(AudioManager.class); |
| audioManager.setMicrophoneMuteFromSwitch(micMute); |
| } |
| } |
| |
| // Native callback. |
| private void notifyInputChannelBroken(IBinder token) { |
| mWindowManagerCallbacks.notifyInputChannelBroken(token); |
| } |
| |
| // Native callback |
| private void notifyFocusChanged(IBinder oldToken, IBinder newToken) { |
| mWindowManagerCallbacks.notifyFocusChanged(oldToken, newToken); |
| } |
| |
| // Native callback |
| private void notifyDropWindow(IBinder token, float x, float y) { |
| mWindowManagerCallbacks.notifyDropWindow(token, x, y); |
| } |
| |
| // Native callback |
| private void notifyUntrustedTouch(String packageName) { |
| // TODO(b/169067926): Remove toast after gathering feedback on dogfood. |
| if (!UNTRUSTED_TOUCHES_TOAST || ArrayUtils.contains( |
| PACKAGE_BLOCKLIST_FOR_UNTRUSTED_TOUCHES_TOAST, packageName)) { |
| Log.i(TAG, "Suppressing untrusted touch toast for " + packageName); |
| return; |
| } |
| DisplayThread.getHandler().post(() -> |
| Toast.makeText(mContext, |
| "Touch obscured by " + packageName |
| + " will be blocked. Check go/untrusted-touches", |
| Toast.LENGTH_SHORT).show()); |
| } |
| |
| // Native callback. |
| private void notifyNoFocusedWindowAnr(InputApplicationHandle inputApplicationHandle) { |
| mWindowManagerCallbacks.notifyNoFocusedWindowAnr(inputApplicationHandle); |
| } |
| |
| // Native callback |
| private void notifyWindowUnresponsive(IBinder token, String reason) { |
| mWindowManagerCallbacks.notifyWindowUnresponsive(token, reason); |
| } |
| |
| // Native callback |
| private void notifyMonitorUnresponsive(int pid, String reason) { |
| mWindowManagerCallbacks.notifyGestureMonitorUnresponsive(pid, reason); |
| } |
| |
| // Native callback |
| private void notifyWindowResponsive(IBinder token) { |
| mWindowManagerCallbacks.notifyWindowResponsive(token); |
| } |
| |
| // Native callback |
| private void notifyMonitorResponsive(int pid) { |
| mWindowManagerCallbacks.notifyGestureMonitorResponsive(pid); |
| } |
| |
| // Native callback. |
| private void notifySensorEvent(int deviceId, int sensorType, int accuracy, long timestamp, |
| float[] values) { |
| if (DEBUG) { |
| Slog.d(TAG, "notifySensorEvent: deviceId=" + deviceId + " sensorType=" |
| + sensorType + " values=" + Arrays.toString(values)); |
| } |
| mSensorEventListenersToNotify.clear(); |
| final int numListeners; |
| synchronized (mSensorEventLock) { |
| numListeners = mSensorEventListeners.size(); |
| for (int i = 0; i < numListeners; i++) { |
| mSensorEventListenersToNotify.add( |
| mSensorEventListeners.valueAt(i)); |
| } |
| } |
| for (int i = 0; i < numListeners; i++) { |
| mSensorEventListenersToNotify.get(i).notifySensorEvent(deviceId, sensorType, |
| accuracy, timestamp, values); |
| } |
| mSensorEventListenersToNotify.clear(); |
| } |
| |
| // Native callback. |
| private void notifySensorAccuracy(int deviceId, int sensorType, int accuracy) { |
| mSensorAccuracyListenersToNotify.clear(); |
| final int numListeners; |
| synchronized (mSensorEventLock) { |
| numListeners = mSensorEventListeners.size(); |
| for (int i = 0; i < numListeners; i++) { |
| mSensorAccuracyListenersToNotify.add(mSensorEventListeners.valueAt(i)); |
| } |
| } |
| for (int i = 0; i < numListeners; i++) { |
| mSensorAccuracyListenersToNotify.get(i).notifySensorAccuracy( |
| deviceId, sensorType, accuracy); |
| } |
| mSensorAccuracyListenersToNotify.clear(); |
| } |
| |
| // Native callback. |
| final boolean filterInputEvent(InputEvent event, int policyFlags) { |
| synchronized (mInputFilterLock) { |
| if (mInputFilter != null) { |
| try { |
| mInputFilter.filterInputEvent(event, policyFlags); |
| } catch (RemoteException e) { |
| /* ignore */ |
| } |
| return false; |
| } |
| } |
| event.recycle(); |
| return true; |
| } |
| |
| // Native callback. |
| private int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) { |
| return mWindowManagerCallbacks.interceptKeyBeforeQueueing(event, policyFlags); |
| } |
| |
| // Native callback. |
| private int interceptMotionBeforeQueueingNonInteractive(int displayId, |
| long whenNanos, int policyFlags) { |
| return mWindowManagerCallbacks.interceptMotionBeforeQueueingNonInteractive( |
| displayId, whenNanos, policyFlags); |
| } |
| |
| // Native callback. |
| private long interceptKeyBeforeDispatching(IBinder focus, KeyEvent event, int policyFlags) { |
| return mWindowManagerCallbacks.interceptKeyBeforeDispatching(focus, event, policyFlags); |
| } |
| |
| // Native callback. |
| private KeyEvent dispatchUnhandledKey(IBinder focus, KeyEvent event, int policyFlags) { |
| return mWindowManagerCallbacks.dispatchUnhandledKey(focus, event, policyFlags); |
| } |
| |
| // Native callback. |
| private boolean checkInjectEventsPermission(int injectorPid, int injectorUid) { |
| return mContext.checkPermission(android.Manifest.permission.INJECT_EVENTS, |
| injectorPid, injectorUid) == PackageManager.PERMISSION_GRANTED; |
| } |
| |
| // Native callback. |
| private void onPointerDownOutsideFocus(IBinder touchedToken) { |
| mWindowManagerCallbacks.onPointerDownOutsideFocus(touchedToken); |
| } |
| |
| // Native callback. |
| private int getVirtualKeyQuietTimeMillis() { |
| return mContext.getResources().getInteger( |
| com.android.internal.R.integer.config_virtualKeyQuietTimeMillis); |
| } |
| |
| // Native callback. |
| private static String[] getExcludedDeviceNames() { |
| List<String> names = new ArrayList<>(); |
| // Read partner-provided list of excluded input devices |
| // Environment.getRootDirectory() is a fancy way of saying ANDROID_ROOT or "/system". |
| final File[] baseDirs = { |
| Environment.getRootDirectory(), |
| Environment.getVendorDirectory() |
| }; |
| for (File baseDir: baseDirs) { |
| File confFile = new File(baseDir, EXCLUDED_DEVICES_PATH); |
| try { |
| InputStream stream = new FileInputStream(confFile); |
| names.addAll(ConfigurationProcessor.processExcludedDeviceNames(stream)); |
| } catch (FileNotFoundException e) { |
| // It's ok if the file does not exist. |
| } catch (Exception e) { |
| Slog.e(TAG, "Could not parse '" + confFile.getAbsolutePath() + "'", e); |
| } |
| } |
| return names.toArray(new String[0]); |
| } |
| |
| /** |
| * Flatten a map into a string list, with value positioned directly next to the |
| * key. |
| * @return Flattened list |
| */ |
| private static <T> String[] flatten(@NonNull Map<String, T> map) { |
| final List<String> list = new ArrayList<>(map.size() * 2); |
| map.forEach((k, v)-> { |
| list.add(k); |
| list.add(v.toString()); |
| }); |
| return list.toArray(new String[0]); |
| } |
| |
| /** |
| * Ports are highly platform-specific, so only allow these to be specified in the vendor |
| * directory. |
| */ |
| private static Map<String, Integer> loadStaticInputPortAssociations() { |
| final File baseDir = Environment.getVendorDirectory(); |
| final File confFile = new File(baseDir, PORT_ASSOCIATIONS_PATH); |
| |
| try { |
| final InputStream stream = new FileInputStream(confFile); |
| return ConfigurationProcessor.processInputPortAssociations(stream); |
| } catch (FileNotFoundException e) { |
| // Most of the time, file will not exist, which is expected. |
| } catch (Exception e) { |
| Slog.e(TAG, "Could not parse '" + confFile.getAbsolutePath() + "'", e); |
| } |
| |
| return new HashMap<>(); |
| } |
| |
| // Native callback |
| private String[] getInputPortAssociations() { |
| final Map<String, Integer> associations = new HashMap<>(mStaticAssociations); |
| |
| // merge the runtime associations. |
| synchronized (mAssociationsLock) { |
| associations.putAll(mRuntimeAssociations); |
| } |
| |
| return flatten(associations); |
| } |
| |
| // Native callback |
| private String[] getInputUniqueIdAssociations() { |
| final Map<String, String> associations; |
| synchronized (mAssociationsLock) { |
| associations = new HashMap<>(mUniqueIdAssociations); |
| } |
| |
| return flatten(associations); |
| } |
| |
| /** |
| * Gets if an input device could dispatch to the given display". |
| * @param deviceId The input device id. |
| * @param displayId The specific display id. |
| * @return True if the device could dispatch to the given display, false otherwise. |
| */ |
| public boolean canDispatchToDisplay(int deviceId, int displayId) { |
| return nativeCanDispatchToDisplay(mPtr, deviceId, displayId); |
| } |
| |
| // Native callback. |
| private int getKeyRepeatTimeout() { |
| return ViewConfiguration.getKeyRepeatTimeout(); |
| } |
| |
| // Native callback. |
| private int getKeyRepeatDelay() { |
| return ViewConfiguration.getKeyRepeatDelay(); |
| } |
| |
| // Native callback. |
| private int getHoverTapTimeout() { |
| return ViewConfiguration.getHoverTapTimeout(); |
| } |
| |
| // Native callback. |
| private int getHoverTapSlop() { |
| return ViewConfiguration.getHoverTapSlop(); |
| } |
| |
| // Native callback. |
| private int getDoubleTapTimeout() { |
| return ViewConfiguration.getDoubleTapTimeout(); |
| } |
| |
| // Native callback. |
| private int getLongPressTimeout() { |
| return ViewConfiguration.getLongPressTimeout(); |
| } |
| |
| // Native callback. |
| private int getPointerLayer() { |
| return mWindowManagerCallbacks.getPointerLayer(); |
| } |
| |
| // Native callback. |
| private PointerIcon getPointerIcon(int displayId) { |
| return PointerIcon.getDefaultIcon(getContextForPointerIcon(displayId)); |
| } |
| |
| @NonNull |
| private Context getContextForPointerIcon(int displayId) { |
| if (mPointerIconDisplayContext != null |
| && mPointerIconDisplayContext.getDisplay().getDisplayId() == displayId) { |
| return mPointerIconDisplayContext; |
| } |
| |
| // Create and cache context for non-default display. |
| mPointerIconDisplayContext = getContextForDisplay(displayId); |
| |
| // Fall back to default display if the requested displayId does not exist. |
| if (mPointerIconDisplayContext == null) { |
| mPointerIconDisplayContext = getContextForDisplay(Display.DEFAULT_DISPLAY); |
| } |
| return mPointerIconDisplayContext; |
| } |
| |
| @Nullable |
| private Context getContextForDisplay(int displayId) { |
| if (displayId == Display.INVALID_DISPLAY) { |
| return null; |
| } |
| if (mContext.getDisplay().getDisplayId() == displayId) { |
| return mContext; |
| } |
| |
| final DisplayManager displayManager = Objects.requireNonNull( |
| mContext.getSystemService(DisplayManager.class)); |
| final Display display = displayManager.getDisplay(displayId); |
| if (display == null) { |
| return null; |
| } |
| |
| return mContext.createDisplayContext(display); |
| } |
| |
| // Native callback. |
| private int getPointerDisplayId() { |
| return mWindowManagerCallbacks.getPointerDisplayId(); |
| } |
| |
| // Native callback. |
| private String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier) { |
| if (!mSystemReady) { |
| return null; |
| } |
| |
| String keyboardLayoutDescriptor = getCurrentKeyboardLayoutForInputDevice(identifier); |
| if (keyboardLayoutDescriptor == null) { |
| return null; |
| } |
| |
| final String[] result = new String[2]; |
| visitKeyboardLayout(keyboardLayoutDescriptor, new KeyboardLayoutVisitor() { |
| @Override |
| public void visitKeyboardLayout(Resources resources, |
| int keyboardLayoutResId, KeyboardLayout layout) { |
| try { |
| result[0] = layout.getDescriptor(); |
| result[1] = Streams.readFully(new InputStreamReader( |
| resources.openRawResource(keyboardLayoutResId))); |
| } catch (IOException ex) { |
| } catch (NotFoundException ex) { |
| } |
| } |
| }); |
| if (result[0] == null) { |
| Slog.w(TAG, "Could not get keyboard layout with descriptor '" |
| + keyboardLayoutDescriptor + "'."); |
| return null; |
| } |
| return result; |
| } |
| |
| // Native callback. |
| private String getDeviceAlias(String uniqueId) { |
| if (BluetoothAdapter.checkBluetoothAddress(uniqueId)) { |
| // TODO(BT) mBluetoothService.getRemoteAlias(uniqueId) |
| return null; |
| } |
| return null; |
| } |
| |
| /** |
| * Callback interface implemented by the Window Manager. |
| */ |
| public interface WindowManagerCallbacks extends LidSwitchCallback { |
| /** |
| * This callback is invoked when the configuration changes. |
| */ |
| void notifyConfigurationChanged(); |
| |
| /** |
| * This callback is invoked when the camera lens cover switch changes state. |
| * @param whenNanos the time when the change occurred |
| * @param lensCovered true is the lens is covered |
| */ |
| void notifyCameraLensCoverSwitchChanged(long whenNanos, boolean lensCovered); |
| |
| /** |
| * This callback is invoked when an input channel is closed unexpectedly. |
| * @param token the connection token of the broken channel |
| */ |
| void notifyInputChannelBroken(IBinder token); |
| |
| /** |
| * Notify the window manager about the focused application that does not have any focused |
| * window and is unable to respond to focused input events. |
| */ |
| void notifyNoFocusedWindowAnr(InputApplicationHandle applicationHandle); |
| |
| /** |
| * Notify the window manager about a gesture monitor that is unresponsive. |
| * |
| * @param pid the pid of the gesture monitor process |
| * @param reason the reason why this connection is unresponsive |
| */ |
| void notifyGestureMonitorUnresponsive(int pid, @NonNull String reason); |
| |
| /** |
| * Notify the window manager about a window that is unresponsive. |
| * |
| * @param token the token that can be used to look up the window |
| * @param reason the reason why this connection is unresponsive |
| */ |
| void notifyWindowUnresponsive(@NonNull IBinder token, @NonNull String reason); |
| |
| /** |
| * Notify the window manager about a gesture monitor that has become responsive. |
| * |
| * @param pid the pid of the gesture monitor process |
| */ |
| void notifyGestureMonitorResponsive(int pid); |
| |
| /** |
| * Notify the window manager about a window that has become responsive. |
| * |
| * @param token the token that can be used to look up the window |
| */ |
| void notifyWindowResponsive(@NonNull IBinder token); |
| |
| /** |
| * This callback is invoked when an event first arrives to InputDispatcher and before it is |
| * placed onto InputDispatcher's queue. If this event is intercepted, it will never be |
| * processed by InputDispacher. |
| * @param event The key event that's arriving to InputDispatcher |
| * @param policyFlags The policy flags |
| * @return the flags that tell InputDispatcher how to handle the event (for example, whether |
| * to pass it to the user) |
| */ |
| int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags); |
| |
| /** |
| * Provides an opportunity for the window manager policy to intercept early motion event |
| * processing when the device is in a non-interactive state since these events are normally |
| * dropped. |
| */ |
| int interceptMotionBeforeQueueingNonInteractive(int displayId, long whenNanos, |
| int policyFlags); |
| |
| /** |
| * This callback is invoked just before the key is about to be sent to an application. |
| * This allows the policy to make some last minute decisions on whether to intercept this |
| * key. |
| * @param token the window token that's about to receive this event |
| * @param event the key event that's being dispatched |
| * @param policyFlags the policy flags |
| * @return negative value if the key should be skipped (not sent to the app). 0 if the key |
| * should proceed getting dispatched to the app. positive value to indicate the additional |
| * time delay, in nanoseconds, to wait before sending this key to the app. |
| */ |
| long interceptKeyBeforeDispatching(IBinder token, KeyEvent event, int policyFlags); |
| |
| /** |
| * Dispatch unhandled key |
| */ |
| KeyEvent dispatchUnhandledKey(IBinder token, KeyEvent event, int policyFlags); |
| |
| int getPointerLayer(); |
| |
| int getPointerDisplayId(); |
| |
| /** |
| * Notifies window manager that a {@link android.view.MotionEvent#ACTION_DOWN} pointer event |
| * occurred on a window that did not have focus. |
| * |
| * @param touchedToken The token for the window that received the input event. |
| */ |
| void onPointerDownOutsideFocus(IBinder touchedToken); |
| |
| /** |
| * Called when the focused window has changed. |
| */ |
| void notifyFocusChanged(IBinder oldToken, IBinder newToken); |
| |
| /** |
| * Called when the drag over window has changed. |
| */ |
| void notifyDropWindow(IBinder token, float x, float y); |
| } |
| |
| /** |
| * Callback interface implemented by WiredAccessoryObserver. |
| */ |
| public interface WiredAccessoryCallbacks { |
| public void notifyWiredAccessoryChanged(long whenNanos, int switchValues, int switchMask); |
| public void systemReady(); |
| } |
| |
| /** |
| * Private handler for the input manager. |
| */ |
| private final class InputManagerHandler extends Handler { |
| public InputManagerHandler(Looper looper) { |
| super(looper, null, true /*async*/); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| 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; |
| case MSG_DELIVER_TABLET_MODE_CHANGED: |
| SomeArgs args = (SomeArgs) msg.obj; |
| long whenNanos = (args.argi1 & 0xFFFFFFFFl) | ((long) args.argi2 << 32); |
| boolean inTabletMode = (boolean) args.arg1; |
| deliverTabletModeChanged(whenNanos, inTabletMode); |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Hosting interface for input filters to call back into the input manager. |
| */ |
| private final class InputFilterHost extends IInputFilterHost.Stub { |
| private boolean mDisconnected; |
| |
| public void disconnectLocked() { |
| mDisconnected = true; |
| } |
| |
| @Override |
| public void sendInputEvent(InputEvent event, int policyFlags) { |
| if (event == null) { |
| throw new IllegalArgumentException("event must not be null"); |
| } |
| |
| synchronized (mInputFilterLock) { |
| if (!mDisconnected) { |
| nativeInjectInputEvent(mPtr, event, 0, 0, |
| InputManager.INJECT_INPUT_EVENT_MODE_ASYNC, 0, |
| policyFlags | WindowManagerPolicy.FLAG_FILTERED); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Interface for the system to handle request from InputMonitors. |
| */ |
| private final class InputMonitorHost extends IInputMonitorHost.Stub { |
| private IBinder mToken; |
| |
| InputMonitorHost(IBinder token) { |
| mToken = token; |
| } |
| |
| @Override |
| public void pilferPointers() { |
| if (mToken == null) { |
| throw new IllegalStateException( |
| "Illegal call to pilferPointers after InputMonitorHost is disposed."); |
| } |
| nativePilferPointers(mPtr, mToken); |
| } |
| |
| @Override |
| public void dispose() { |
| // We do not remove the input monitor here by calling nativeRemoveInputChannel because |
| // it causes a race in InputDispatcher between the removal of the InputChannel through |
| // that call and the InputChannel#dispose call (which causes an FD hangup) from the |
| // client (b/189135695). |
| // |
| // NOTE: This means the client is responsible for properly closing the InputMonitor by |
| // disposing the InputChannel and all its duplicates. |
| mToken = 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); |
| } |
| |
| private final class InputDevicesChangedListenerRecord implements DeathRecipient { |
| private final int mPid; |
| private final IInputDevicesChangedListener mListener; |
| |
| public InputDevicesChangedListenerRecord(int pid, IInputDevicesChangedListener listener) { |
| mPid = pid; |
| mListener = listener; |
| } |
| |
| @Override |
| public void binderDied() { |
| if (DEBUG) { |
| Slog.d(TAG, "Input devices changed listener for pid " + mPid + " died."); |
| } |
| onInputDevicesChangedListenerDied(mPid); |
| } |
| |
| public void notifyInputDevicesChanged(int[] info) { |
| try { |
| mListener.onInputDevicesChanged(info); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "Failed to notify process " |
| + mPid + " that input devices changed, assuming it died.", ex); |
| binderDied(); |
| } |
| } |
| } |
| |
| private final class TabletModeChangedListenerRecord implements DeathRecipient { |
| private final int mPid; |
| private final ITabletModeChangedListener mListener; |
| |
| public TabletModeChangedListenerRecord(int pid, ITabletModeChangedListener listener) { |
| mPid = pid; |
| mListener = listener; |
| } |
| |
| @Override |
| public void binderDied() { |
| if (DEBUG) { |
| Slog.d(TAG, "Tablet mode changed listener for pid " + mPid + " died."); |
| } |
| onTabletModeChangedListenerDied(mPid); |
| } |
| |
| public void notifyTabletModeChanged(long whenNanos, boolean inTabletMode) { |
| try { |
| mListener.onTabletModeChanged(whenNanos, inTabletMode); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "Failed to notify process " + mPid + |
| " that tablet mode changed, assuming it died.", ex); |
| binderDied(); |
| } |
| } |
| } |
| |
| private void onSensorEventListenerDied(int pid) { |
| synchronized (mSensorEventLock) { |
| mSensorEventListeners.remove(pid); |
| } |
| } |
| |
| private final class SensorEventListenerRecord implements DeathRecipient { |
| private final int mPid; |
| private final IInputSensorEventListener mListener; |
| |
| SensorEventListenerRecord(int pid, IInputSensorEventListener listener) { |
| mPid = pid; |
| mListener = listener; |
| } |
| |
| @Override |
| public void binderDied() { |
| if (DEBUG) { |
| Slog.d(TAG, "Sensor event listener for pid " + mPid + " died."); |
| } |
| onSensorEventListenerDied(mPid); |
| } |
| |
| public IInputSensorEventListener getListener() { |
| return mListener; |
| } |
| |
| public void notifySensorEvent(int deviceId, int sensorType, int accuracy, long timestamp, |
| float[] values) { |
| try { |
| mListener.onInputSensorChanged(deviceId, sensorType, accuracy, timestamp, |
| values); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "Failed to notify process " + mPid |
| + " that sensor event notified, assuming it died.", ex); |
| binderDied(); |
| } |
| } |
| |
| public void notifySensorAccuracy(int deviceId, int sensorType, int accuracy) { |
| try { |
| mListener.onInputSensorAccuracyChanged(deviceId, sensorType, accuracy); |
| } catch (RemoteException ex) { |
| Slog.w(TAG, "Failed to notify process " + mPid |
| + " that sensor accuracy notified, assuming it died.", ex); |
| binderDied(); |
| } |
| } |
| } |
| |
| private final class VibratorToken implements DeathRecipient { |
| public final int mDeviceId; |
| public final IBinder mToken; |
| public final int mTokenValue; |
| |
| public boolean mVibrating; |
| |
| public VibratorToken(int deviceId, IBinder token, int tokenValue) { |
| mDeviceId = deviceId; |
| mToken = token; |
| mTokenValue = tokenValue; |
| } |
| |
| @Override |
| public void binderDied() { |
| if (DEBUG) { |
| Slog.d(TAG, "Vibrator token died."); |
| } |
| onVibratorTokenDied(this); |
| } |
| } |
| |
| private final class LocalService extends InputManagerInternal { |
| @Override |
| public void setDisplayViewports(List<DisplayViewport> viewports) { |
| setDisplayViewportsInternal(viewports); |
| } |
| |
| @Override |
| public boolean injectInputEvent(InputEvent event, int mode) { |
| return injectInputEventInternal(event, mode); |
| } |
| |
| @Override |
| public void setInteractive(boolean interactive) { |
| nativeSetInteractive(mPtr, interactive); |
| } |
| |
| @Override |
| public void toggleCapsLock(int deviceId) { |
| nativeToggleCapsLock(mPtr, deviceId); |
| } |
| |
| @Override |
| public void setPulseGestureEnabled(boolean enabled) { |
| if (mDoubleTouchGestureEnableFile != null) { |
| FileWriter writer = null; |
| try { |
| writer = new FileWriter(mDoubleTouchGestureEnableFile); |
| writer.write(enabled ? "1" : "0"); |
| } catch (IOException e) { |
| Log.wtf(TAG, "Unable to setPulseGestureEnabled", e); |
| } finally { |
| IoUtils.closeQuietly(writer); |
| } |
| } |
| } |
| |
| @Override |
| public boolean transferTouchFocus(@NonNull IBinder fromChannelToken, |
| @NonNull IBinder toChannelToken) { |
| return InputManagerService.this.transferTouchFocus(fromChannelToken, toChannelToken); |
| } |
| |
| @Override |
| public void registerLidSwitchCallback(LidSwitchCallback callbacks) { |
| registerLidSwitchCallbackInternal(callbacks); |
| } |
| |
| @Override |
| public void unregisterLidSwitchCallback(LidSwitchCallback callbacks) { |
| unregisterLidSwitchCallbackInternal(callbacks); |
| } |
| } |
| |
| @Override |
| public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, |
| String[] args, ShellCallback callback, ResultReceiver resultReceiver) { |
| new InputShellCommand().exec(this, in, out, err, args, callback, resultReceiver); |
| } |
| |
| } |