/*
 * Copyright (C) 2023 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 android.hardware.input;

import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.content.Context;
import android.hardware.BatteryState;
import android.hardware.SensorManager;
import android.hardware.input.InputManager.InputDeviceBatteryListener;
import android.hardware.input.InputManager.InputDeviceListener;
import android.hardware.input.InputManager.KeyEventActivityListener;
import android.hardware.input.InputManager.KeyGestureEventHandler;
import android.hardware.input.InputManager.KeyGestureEventListener;
import android.hardware.input.InputManager.KeyboardBacklightListener;
import android.hardware.input.InputManager.OnTabletModeChangedListener;
import android.hardware.input.InputManager.StickyModifierStateListener;
import android.hardware.lights.Light;
import android.hardware.lights.LightState;
import android.hardware.lights.LightsManager;
import android.hardware.lights.LightsRequest;
import android.os.Binder;
import android.os.CombinedVibration;
import android.os.Handler;
import android.os.IBinder;
import android.os.IVibratorStateListener;
import android.os.InputEventInjectionSync;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.os.VibratorManager;
import android.util.IntArray;
import android.util.Log;
import android.util.SparseArray;
import android.view.Display;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.InputMonitor;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.PointerIcon;
import android.view.View;
import android.view.View.PointerCaptureMode;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;

/**
 * Manages communication with the input manager service on behalf of
 * an application process. You're probably looking for {@link InputManager}.
 *
 * @hide
 */
public final class InputManagerGlobal {
    private static final String TAG = "InputManagerGlobal";
    // To enable these logs, run: 'adb shell setprop log.tag.InputManagerGlobal DEBUG'
    private boolean debug() {
        return Log.isLoggable(TAG, Log.DEBUG);
    }

    @GuardedBy("mInputDeviceListeners")
    @Nullable private SparseArray<InputDevice> mInputDevices;
    @GuardedBy("mInputDeviceListeners")
    @Nullable private InputDevicesChangedListener mInputDevicesChangedListener;
    @GuardedBy("mInputDeviceListeners")
    private final ArrayList<InputDeviceListenerDelegate> mInputDeviceListeners = new ArrayList<>();

    @GuardedBy("mOnTabletModeChangedListeners")
    private final ArrayList<OnTabletModeChangedListenerDelegate> mOnTabletModeChangedListeners =
            new ArrayList<>();

    private final Object mBatteryListenersLock = new Object();
    // Maps a deviceId whose battery is currently being monitored to an entry containing the
    // registered listeners for that device.
    @GuardedBy("mBatteryListenersLock")
    @Nullable private SparseArray<RegisteredBatteryListeners> mBatteryListeners;
    @GuardedBy("mBatteryListenersLock")
    @Nullable private IInputDeviceBatteryListener mInputDeviceBatteryListener;

    private final Object mKeyboardBacklightListenerLock = new Object();
    @GuardedBy("mKeyboardBacklightListenerLock")
    @Nullable private ArrayList<KeyboardBacklightListenerDelegate> mKeyboardBacklightListeners;
    @GuardedBy("mKeyboardBacklightListenerLock")
    @Nullable private IKeyboardBacklightListener mKeyboardBacklightListener;

    private final Object mStickyModifierStateListenerLock = new Object();
    @GuardedBy("mStickyModifierStateListenerLock")
    @Nullable
    private ArrayList<StickyModifierStateListenerDelegate> mStickyModifierStateListeners;
    @GuardedBy("mStickyModifierStateListenerLock")
    @Nullable
    private IStickyModifierStateListener mStickyModifierStateListener;

    private final Object mKeyGestureEventListenerLock = new Object();
    @GuardedBy("mKeyGestureEventListenerLock")
    @Nullable
    private ArrayList<KeyGestureEventListenerDelegate> mKeyGestureEventListeners;
    @GuardedBy("mKeyGestureEventListenerLock")
    @Nullable
    private IKeyGestureEventListener mKeyGestureEventListener;

    private final Object mKeyEventActivityLock = new Object();
    @GuardedBy("mKeyEventActivityLock")
    private ArrayList<KeyEventActivityListener> mKeyEventActivityListeners;
    @GuardedBy("mKeyEventActivityLock")
    @Nullable
    private IKeyEventActivityListener mKeyEventActivityListener;

    @GuardedBy("mKeyGesturesToHandlerMap")
    @Nullable
    private IKeyGestureHandler mKeyGestureHandler;
    @GuardedBy("mKeyGesturesToHandlerMap")
    private final SparseArray<KeyGestureEventHandler> mKeyGesturesToHandlerMap =
            new SparseArray<>();


    // InputDeviceSensorManager gets notified synchronously from the binder thread when input
    // devices change, so it must be synchronized with the input device listeners.
    @GuardedBy("mInputDeviceListeners")
    @Nullable private InputDeviceSensorManager mInputDeviceSensorManager;

    private static InputManagerGlobal sInstance;

    private final String mVelocityTrackerStrategy;

    private final IInputManager mIm;

    public InputManagerGlobal(IInputManager im) {
        mIm = im;
        String strategy = null;
        try {
            strategy = mIm.getVelocityTrackerStrategy();
        } catch (RemoteException ex) {
            Log.w(TAG, "Could not get VelocityTracker strategy: " + ex);
        }
        mVelocityTrackerStrategy = strategy;
    }

    /**
     * Gets an instance of the input manager global singleton.
     *
     * @return The input manager instance, may be null early in system startup
     * before the input manager has been fully initialized.
     */
    public static InputManagerGlobal getInstance() {
        synchronized (InputManagerGlobal.class) {
            if (sInstance == null) {
                IBinder b = ServiceManager.getService(Context.INPUT_SERVICE);
                if (b != null) {
                    sInstance = new InputManagerGlobal(IInputManager.Stub.asInterface(b));
                }
            }
            return sInstance;
        }
    }

    public IInputManager getInputManagerService() {
        return mIm;
    }

    /**
     * A test session tracker for InputManagerGlobal.
     * @see #createTestSession(IInputManager)
     */
    @VisibleForTesting
    public interface TestSession extends AutoCloseable {
        @Override
        void close();
    }

    /**
     * Create and set a test instance of InputManagerGlobal.
     *
     * @return The test session. The session must be {@link TestSession#close()}-ed at the end
     * of the test.
     */
    @VisibleForTesting
    public static TestSession createTestSession(IInputManager inputManagerService) {
        synchronized (InputManagerGlobal.class) {
            final var oldInstance = sInstance;
            sInstance = new InputManagerGlobal(inputManagerService);
            Log.d(TAG, "Starting InputManagerGlobal test session with new service instance "
                    + inputManagerService + ", old service instance = "
                    + (oldInstance != null ? oldInstance.getInputManagerService() : "null"));
            return () -> {
                Log.d(TAG, "Closing test session and restoring service instance to "
                        + (oldInstance != null ? oldInstance.getInputManagerService() : "null"));
                sInstance = oldInstance;
            };
        }
    }

    /**
     * Get the current VelocityTracker strategy.
     * Only works when the system has fully booted up.
     */
    public String getVelocityTrackerStrategy() {
        return mVelocityTrackerStrategy;
    }

    /**
     * @see InputManager#getInputDevice(int)
     */
    @Nullable
    public InputDevice getInputDevice(int id) {
        synchronized (mInputDeviceListeners) {
            populateInputDevicesLocked();

            int index = mInputDevices.indexOfKey(id);
            if (index < 0) {
                return null;
            }

            InputDevice inputDevice = mInputDevices.valueAt(index);
            if (inputDevice == null) {
                try {
                    inputDevice = mIm.getInputDevice(id);
                } catch (RemoteException ex) {
                    throw ex.rethrowFromSystemServer();
                }
                if (inputDevice != null) {
                    mInputDevices.setValueAt(index, inputDevice);
                }
            }
            return inputDevice;
        }
    }

    @GuardedBy("mInputDeviceListeners")
    private void populateInputDevicesLocked() {
        if (mInputDevicesChangedListener == null) {
            final InputDevicesChangedListener
                    listener = new InputDevicesChangedListener();
            try {
                mIm.registerInputDevicesChangedListener(listener);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
            mInputDevicesChangedListener = listener;
        }

        if (mInputDevices == null) {
            final int[] ids;
            try {
                ids = mIm.getInputDeviceIds();
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }

            mInputDevices = new SparseArray<>();
            for (int id : ids) {
                mInputDevices.put(id, null);
            }
        }
    }

    private final class InputDevicesChangedListener extends IInputDevicesChangedListener.Stub {
        @Override
        public void onInputDevicesChanged(int[] deviceIdAndGeneration) throws RemoteException {
            InputManagerGlobal.this.onInputDevicesChanged(deviceIdAndGeneration);
        }
    }

    private void onInputDevicesChanged(int[] deviceIdAndGeneration) {
        final boolean enableDebugLogs = debug();
        if (enableDebugLogs) {
            Log.d(TAG, "Received input devices changed: " + Arrays.toString(deviceIdAndGeneration));
        }

        synchronized (mInputDeviceListeners) {
            for (int i = mInputDevices.size(); --i > 0; ) {
                final int deviceId = mInputDevices.keyAt(i);
                if (!containsDeviceId(deviceIdAndGeneration, deviceId)) {
                    if (enableDebugLogs) {
                        final InputDevice device = mInputDevices.valueAt(i);
                        final String name = device != null ? device.getName() : "<null>";
                        Log.d(TAG, "Device removed: " + deviceId + " (" + name + ")");
                    }
                    mInputDevices.removeAt(i);
                    if (mInputDeviceSensorManager != null) {
                        mInputDeviceSensorManager.onInputDeviceRemoved(deviceId);
                    }
                    sendMessageToInputDeviceListenersLocked(
                            InputDeviceListenerDelegate.MSG_DEVICE_REMOVED, deviceId);
                }
            }

            for (int i = 0; i < deviceIdAndGeneration.length; i += 2) {
                final int deviceId = deviceIdAndGeneration[i];
                int index = mInputDevices.indexOfKey(deviceId);
                if (index >= 0) {
                    final InputDevice device = mInputDevices.valueAt(index);
                    if (device != null) {
                        final int generation = deviceIdAndGeneration[i + 1];
                        if (device.getGeneration() != generation) {
                            if (enableDebugLogs) {
                                Log.d(TAG, "Device changed: " + deviceId + " ("
                                        + device.getName() + ")");
                            }
                            mInputDevices.setValueAt(index, null);
                            if (mInputDeviceSensorManager != null) {
                                mInputDeviceSensorManager.onInputDeviceChanged(deviceId);
                            }
                            sendMessageToInputDeviceListenersLocked(
                                    InputDeviceListenerDelegate.MSG_DEVICE_CHANGED, deviceId);
                        }
                    }
                } else {
                    if (enableDebugLogs) {
                        Log.d(TAG, "Device added: " + deviceId);
                    }
                    mInputDevices.put(deviceId, null);
                    if (mInputDeviceSensorManager != null) {
                        mInputDeviceSensorManager.onInputDeviceAdded(deviceId);
                    }
                    sendMessageToInputDeviceListenersLocked(
                            InputDeviceListenerDelegate.MSG_DEVICE_ADDED, deviceId);
                }
            }
        }
    }

    private static final class InputDeviceListenerDelegate extends Handler {
        public final InputDeviceListener mListener;
        static final int MSG_DEVICE_ADDED = 1;
        static final int MSG_DEVICE_REMOVED = 2;
        static final int MSG_DEVICE_CHANGED = 3;

        InputDeviceListenerDelegate(InputDeviceListener listener, Handler handler) {
            super(handler != null ? handler.getLooper() : Looper.myLooper());
            mListener = listener;
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_DEVICE_ADDED:
                    mListener.onInputDeviceAdded(msg.arg1);
                    break;
                case MSG_DEVICE_REMOVED:
                    mListener.onInputDeviceRemoved(msg.arg1);
                    break;
                case MSG_DEVICE_CHANGED:
                    mListener.onInputDeviceChanged(msg.arg1);
                    break;
            }
        }
    }

    private static boolean containsDeviceId(int[] deviceIdAndGeneration, int deviceId) {
        for (int i = 0; i < deviceIdAndGeneration.length; i += 2) {
            if (deviceIdAndGeneration[i] == deviceId) {
                return true;
            }
        }
        return false;
    }

    @GuardedBy("mInputDeviceListeners")
    private void sendMessageToInputDeviceListenersLocked(int what, int deviceId) {
        final int numListeners = mInputDeviceListeners.size();
        for (int i = 0; i < numListeners; i++) {
            InputDeviceListenerDelegate listener = mInputDeviceListeners.get(i);
            listener.sendMessage(listener.obtainMessage(what, deviceId, 0));
        }
    }

    /**
     * @see InputManager#registerInputDeviceListener
     */
    public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) {
        Objects.requireNonNull(listener, "listener must not be null");

        synchronized (mInputDeviceListeners) {
            populateInputDevicesLocked();
            int index = findInputDeviceListenerLocked(listener);
            if (index < 0) {
                mInputDeviceListeners.add(new InputDeviceListenerDelegate(listener, handler));
            }
        }
    }

    /**
     * @see InputManager#unregisterInputDeviceListener
     */
    public void unregisterInputDeviceListener(InputDeviceListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("listener must not be null");
        }

        synchronized (mInputDeviceListeners) {
            int index = findInputDeviceListenerLocked(listener);
            if (index >= 0) {
                InputDeviceListenerDelegate d = mInputDeviceListeners.get(index);
                d.removeCallbacksAndMessages(null);
                mInputDeviceListeners.remove(index);
            }
        }
    }

    @GuardedBy("mInputDeviceListeners")
    private int findInputDeviceListenerLocked(InputDeviceListener listener) {
        final int numListeners = mInputDeviceListeners.size();
        for (int i = 0; i < numListeners; i++) {
            if (mInputDeviceListeners.get(i).mListener == listener) {
                return i;
            }
        }
        return -1;
    }

    /**
     * @see InputManager#getInputDeviceIds
     */
    public int[] getInputDeviceIds() {
        synchronized (mInputDeviceListeners) {
            populateInputDevicesLocked();

            final int count = mInputDevices.size();
            final int[] ids = new int[count];
            for (int i = 0; i < count; i++) {
                ids[i] = mInputDevices.keyAt(i);
            }
            return ids;
        }
    }

    /**
     * @see InputManager#enableInputDevice(int)
     */
    public void enableInputDevice(int id) {
        try {
            mIm.enableInputDevice(id);
        } catch (RemoteException ex) {
            Log.w(TAG, "Could not enable input device with id = " + id);
            throw ex.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputManager#disableInputDevice(int)
     */
    public void disableInputDevice(int id) {
        try {
            mIm.disableInputDevice(id);
        } catch (RemoteException ex) {
            Log.w(TAG, "Could not disable input device with id = " + id);
            throw ex.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputManager#getInputDeviceByDescriptor
     */
    InputDevice getInputDeviceByDescriptor(String descriptor) {
        Objects.requireNonNull(descriptor, "descriptor must not be null.");

        synchronized (mInputDeviceListeners) {
            populateInputDevicesLocked();

            int numDevices = mInputDevices.size();
            for (int i = 0; i < numDevices; i++) {
                InputDevice inputDevice = mInputDevices.valueAt(i);
                if (inputDevice == null) {
                    int id = mInputDevices.keyAt(i);
                    try {
                        inputDevice = mIm.getInputDevice(id);
                    } catch (RemoteException ex) {
                        throw ex.rethrowFromSystemServer();
                    }
                    if (inputDevice == null) {
                        continue;
                    }
                    mInputDevices.setValueAt(i, inputDevice);
                }
                if (descriptor.equals(inputDevice.getDescriptor())) {
                    return inputDevice;
                }
            }
            return null;
        }
    }

    /**
     * @see InputManager#getHostUsiVersion
     */
    @Nullable
    HostUsiVersion getHostUsiVersion(@NonNull Display display) {
        Objects.requireNonNull(display, "display should not be null");

        // Return the first valid USI version reported by any input device associated with
        // the display.
        synchronized (mInputDeviceListeners) {
            populateInputDevicesLocked();

            for (int i = 0; i < mInputDevices.size(); i++) {
                final InputDevice device = getInputDevice(mInputDevices.keyAt(i));
                if (device != null && device.getAssociatedDisplayId() == display.getDisplayId()) {
                    if (device.getHostUsiVersion() != null) {
                        return device.getHostUsiVersion();
                    }
                }
            }
        }

        // If there are no input devices that report a valid USI version, see if there is a config
        // that specifies the USI version for the display. This is to handle cases where the USI
        // input device is not registered by the kernel/driver all the time.
        try {
            return mIm.getHostUsiVersionFromDisplayConfig(display.getDisplayId());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    private void onTabletModeChanged(long whenNanos, boolean inTabletMode) {
        if (debug()) {
            Log.d(TAG, "Received tablet mode changed: "
                    + "whenNanos=" + whenNanos + ", inTabletMode=" + inTabletMode);
        }
        synchronized (mOnTabletModeChangedListeners) {
            final int numListeners = mOnTabletModeChangedListeners.size();
            for (int i = 0; i < numListeners; i++) {
                OnTabletModeChangedListenerDelegate listener =
                        mOnTabletModeChangedListeners.get(i);
                listener.sendTabletModeChanged(whenNanos, inTabletMode);
            }
        }
    }

    private final class TabletModeChangedListener extends ITabletModeChangedListener.Stub {
        @Override
        public void onTabletModeChanged(long whenNanos, boolean inTabletMode) {
            InputManagerGlobal.this.onTabletModeChanged(whenNanos, inTabletMode);
        }
    }

    private static final class OnTabletModeChangedListenerDelegate extends Handler {
        private static final int MSG_TABLET_MODE_CHANGED = 0;

        public final OnTabletModeChangedListener mListener;

        OnTabletModeChangedListenerDelegate(
                OnTabletModeChangedListener listener, Handler handler) {
            super(handler != null ? handler.getLooper() : Looper.myLooper());
            mListener = listener;
        }

        public void sendTabletModeChanged(long whenNanos, boolean inTabletMode) {
            SomeArgs args = SomeArgs.obtain();
            args.argi1 = (int) whenNanos;
            args.argi2 = (int) (whenNanos >> 32);
            args.arg1 = inTabletMode;
            obtainMessage(MSG_TABLET_MODE_CHANGED, args).sendToTarget();
        }

        @Override
        public void handleMessage(Message msg) {
            if (msg.what == MSG_TABLET_MODE_CHANGED) {
                SomeArgs args = (SomeArgs) msg.obj;
                long whenNanos = (args.argi1 & 0xFFFFFFFFL) | ((long) args.argi2 << 32);
                boolean inTabletMode = (boolean) args.arg1;
                mListener.onTabletModeChanged(whenNanos, inTabletMode);
            }
        }
    }

    /**
     * @see InputManager#registerInputDeviceListener(InputDeviceListener, Handler)
     */
    void registerOnTabletModeChangedListener(
            OnTabletModeChangedListener listener, Handler handler) {
        Objects.requireNonNull(listener, "listener must not be null");

        synchronized (mOnTabletModeChangedListeners) {
            if (mOnTabletModeChangedListeners.isEmpty()) {
                initializeTabletModeListenerLocked();
            }
            int idx = findOnTabletModeChangedListenerLocked(listener);
            if (idx < 0) {
                OnTabletModeChangedListenerDelegate d =
                        new OnTabletModeChangedListenerDelegate(listener, handler);
                mOnTabletModeChangedListeners.add(d);
            }
        }
    }

    /**
     * @see InputManager#unregisterOnTabletModeChangedListener(OnTabletModeChangedListener)
     */
    void unregisterOnTabletModeChangedListener(OnTabletModeChangedListener listener) {
        Objects.requireNonNull(listener, "listener must not be null");

        synchronized (mOnTabletModeChangedListeners) {
            int idx = findOnTabletModeChangedListenerLocked(listener);
            if (idx >= 0) {
                OnTabletModeChangedListenerDelegate d = mOnTabletModeChangedListeners.remove(idx);
                d.removeCallbacksAndMessages(null);
            }
        }
    }

    @GuardedBy("mOnTabletModeChangedListeners")
    private void initializeTabletModeListenerLocked() {
        final TabletModeChangedListener listener = new TabletModeChangedListener();
        try {
            mIm.registerTabletModeChangedListener(listener);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    @GuardedBy("mOnTabletModeChangedListeners")
    private int findOnTabletModeChangedListenerLocked(OnTabletModeChangedListener listener) {
        final int n = mOnTabletModeChangedListeners.size();
        for (int i = 0; i < n; i++) {
            if (mOnTabletModeChangedListeners.get(i).mListener == listener) {
                return i;
            }
        }
        return -1;
    }

    private static final class RegisteredBatteryListeners {
        final List<InputDeviceBatteryListenerDelegate> mDelegates = new ArrayList<>();
        IInputDeviceBatteryState mInputDeviceBatteryState;
    }

    private static final class InputDeviceBatteryListenerDelegate {
        final InputDeviceBatteryListener mListener;
        final Executor mExecutor;

        InputDeviceBatteryListenerDelegate(InputDeviceBatteryListener listener, Executor executor) {
            mListener = listener;
            mExecutor = executor;
        }

        void notifyBatteryStateChanged(IInputDeviceBatteryState state) {
            mExecutor.execute(() ->
                    mListener.onBatteryStateChanged(state.deviceId, state.updateTime,
                            new LocalBatteryState(state.isPresent, state.status, state.capacity)));
        }
    }

    /**
     * @see InputManager#addInputDeviceBatteryListener(int, Executor, InputDeviceBatteryListener)
     */
    public void addInputDeviceBatteryListener(int deviceId, @NonNull Executor executor,
            @NonNull InputDeviceBatteryListener listener) {
        Objects.requireNonNull(executor, "executor should not be null");
        Objects.requireNonNull(listener, "listener should not be null");

        synchronized (mBatteryListenersLock) {
            if (mBatteryListeners == null) {
                mBatteryListeners = new SparseArray<>();
                mInputDeviceBatteryListener = new LocalInputDeviceBatteryListener();
            }
            RegisteredBatteryListeners listenersForDevice = mBatteryListeners.get(deviceId);
            if (listenersForDevice == null) {
                // The deviceId is currently not being monitored for battery changes.
                // Start monitoring the device.
                listenersForDevice = new RegisteredBatteryListeners();
                mBatteryListeners.put(deviceId, listenersForDevice);
                try {
                    mIm.registerBatteryListener(deviceId, mInputDeviceBatteryListener);
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            } else {
                // The deviceId is already being monitored for battery changes.
                // Ensure that the listener is not already registered.
                final int numDelegates = listenersForDevice.mDelegates.size();
                for (int i = 0; i < numDelegates; i++) {
                    InputDeviceBatteryListener registeredListener =
                            listenersForDevice.mDelegates.get(i).mListener;
                    if (Objects.equals(listener, registeredListener)) {
                        throw new IllegalArgumentException(
                                "Attempting to register an InputDeviceBatteryListener that has "
                                        + "already been registered for deviceId: "
                                        + deviceId);
                    }
                }
            }
            final InputDeviceBatteryListenerDelegate delegate =
                    new InputDeviceBatteryListenerDelegate(listener, executor);
            listenersForDevice.mDelegates.add(delegate);

            // Notify the listener immediately if we already have the latest battery state.
            if (listenersForDevice.mInputDeviceBatteryState != null) {
                delegate.notifyBatteryStateChanged(listenersForDevice.mInputDeviceBatteryState);
            }
        }
    }

    /**
     * @see InputManager#removeInputDeviceBatteryListener(int, InputDeviceBatteryListener)
     */
    void removeInputDeviceBatteryListener(int deviceId,
            @NonNull InputDeviceBatteryListener listener) {
        Objects.requireNonNull(listener, "listener should not be null");

        synchronized (mBatteryListenersLock) {
            if (mBatteryListeners == null) {
                return;
            }
            RegisteredBatteryListeners listenersForDevice = mBatteryListeners.get(deviceId);
            if (listenersForDevice == null) {
                // The deviceId is not currently being monitored.
                return;
            }
            final List<InputDeviceBatteryListenerDelegate> delegates =
                    listenersForDevice.mDelegates;
            for (int i = 0; i < delegates.size();) {
                if (Objects.equals(listener, delegates.get(i).mListener)) {
                    delegates.remove(i);
                    continue;
                }
                i++;
            }
            if (!delegates.isEmpty()) {
                return;
            }

            // There are no more battery listeners for this deviceId. Stop monitoring this device.
            mBatteryListeners.remove(deviceId);
            try {
                mIm.unregisterBatteryListener(deviceId, mInputDeviceBatteryListener);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
            if (mBatteryListeners.size() == 0) {
                // There are no more devices being monitored, so the registered
                // IInputDeviceBatteryListener will be automatically dropped by the server.
                mBatteryListeners = null;
                mInputDeviceBatteryListener = null;
            }
        }
    }

    private class LocalInputDeviceBatteryListener extends IInputDeviceBatteryListener.Stub {
        @Override
        public void onBatteryStateChanged(IInputDeviceBatteryState state) {
            synchronized (mBatteryListenersLock) {
                if (mBatteryListeners == null) return;
                final RegisteredBatteryListeners entry = mBatteryListeners.get(state.deviceId);
                if (entry == null) return;

                entry.mInputDeviceBatteryState = state;
                final int numDelegates = entry.mDelegates.size();
                for (int i = 0; i < numDelegates; i++) {
                    entry.mDelegates.get(i)
                            .notifyBatteryStateChanged(entry.mInputDeviceBatteryState);
                }
            }
        }
    }

    /**
     * @see #getInputDeviceBatteryState(int, boolean)
     */
    @NonNull
    public BatteryState getInputDeviceBatteryState(int deviceId, boolean hasBattery) {
        if (!hasBattery) {
            return new LocalBatteryState();
        }
        try {
            final IInputDeviceBatteryState state = mIm.getBatteryState(deviceId);
            return new LocalBatteryState(state.isPresent, state.status, state.capacity);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    // Implementation of the android.hardware.BatteryState interface used to report the battery
    // state via the InputDevice#getBatteryState() and InputDeviceBatteryListener interfaces.
    private static final class LocalBatteryState extends BatteryState {
        private final boolean mIsPresent;
        private final int mStatus;
        private final float mCapacity;

        LocalBatteryState() {
            this(false /*isPresent*/, BatteryState.STATUS_UNKNOWN, Float.NaN /*capacity*/);
        }

        LocalBatteryState(boolean isPresent, int status, float capacity) {
            mIsPresent = isPresent;
            mStatus = status;
            mCapacity = capacity;
        }

        @Override
        public boolean isPresent() {
            return mIsPresent;
        }

        @Override
        public int getStatus() {
            return mStatus;
        }

        @Override
        public float getCapacity() {
            return mCapacity;
        }
    }

    private static final class KeyboardBacklightListenerDelegate {
        final InputManager.KeyboardBacklightListener mListener;
        final Executor mExecutor;

        KeyboardBacklightListenerDelegate(KeyboardBacklightListener listener, Executor executor) {
            mListener = listener;
            mExecutor = executor;
        }

        void notifyKeyboardBacklightChange(int deviceId, IKeyboardBacklightState state,
                boolean isTriggeredByKeyPress) {
            mExecutor.execute(() ->
                    mListener.onKeyboardBacklightChanged(deviceId,
                            new LocalKeyboardBacklightState(state.brightnessLevel,
                                    state.maxBrightnessLevel), isTriggeredByKeyPress));
        }
    }

    private class LocalKeyboardBacklightListener extends IKeyboardBacklightListener.Stub {

        @Override
        public void onBrightnessChanged(int deviceId, IKeyboardBacklightState state,
                boolean isTriggeredByKeyPress) {
            synchronized (mKeyboardBacklightListenerLock) {
                if (mKeyboardBacklightListeners == null) return;
                final int numListeners = mKeyboardBacklightListeners.size();
                for (int i = 0; i < numListeners; i++) {
                    mKeyboardBacklightListeners.get(i)
                            .notifyKeyboardBacklightChange(deviceId, state, isTriggeredByKeyPress);
                }
            }
        }
    }

    // Implementation of the android.hardware.input.KeyboardBacklightState interface used to report
    // the keyboard backlight state via the KeyboardBacklightListener interfaces.
    private static final class LocalKeyboardBacklightState extends KeyboardBacklightState {

        private final int mBrightnessLevel;
        private final int mMaxBrightnessLevel;

        LocalKeyboardBacklightState(int brightnessLevel, int maxBrightnessLevel) {
            mBrightnessLevel = brightnessLevel;
            mMaxBrightnessLevel = maxBrightnessLevel;
        }

        @Override
        public int getBrightnessLevel() {
            return mBrightnessLevel;
        }

        @Override
        public int getMaxBrightnessLevel() {
            return mMaxBrightnessLevel;
        }
    }

    /**
     * @see InputManager#registerKeyboardBacklightListener(Executor, KeyboardBacklightListener)
     */
    @RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_BACKLIGHT)
    void registerKeyboardBacklightListener(@NonNull Executor executor,
            @NonNull KeyboardBacklightListener listener) throws IllegalArgumentException {
        Objects.requireNonNull(executor, "executor should not be null");
        Objects.requireNonNull(listener, "listener should not be null");

        synchronized (mKeyboardBacklightListenerLock) {
            if (mKeyboardBacklightListener == null) {
                mKeyboardBacklightListeners = new ArrayList<>();
                mKeyboardBacklightListener = new LocalKeyboardBacklightListener();

                try {
                    mIm.registerKeyboardBacklightListener(mKeyboardBacklightListener);
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            final int numListeners = mKeyboardBacklightListeners.size();
            for (int i = 0; i < numListeners; i++) {
                if (mKeyboardBacklightListeners.get(i).mListener == listener) {
                    throw new IllegalArgumentException("Listener has already been registered!");
                }
            }
            KeyboardBacklightListenerDelegate delegate =
                    new KeyboardBacklightListenerDelegate(listener, executor);
            mKeyboardBacklightListeners.add(delegate);
        }
    }

    /**
     * @see InputManager#unregisterKeyboardBacklightListener(KeyboardBacklightListener)
     */
    @RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_BACKLIGHT)
    void unregisterKeyboardBacklightListener(
            @NonNull KeyboardBacklightListener listener) {
        Objects.requireNonNull(listener, "listener should not be null");

        synchronized (mKeyboardBacklightListenerLock) {
            if (mKeyboardBacklightListeners == null) {
                return;
            }
            mKeyboardBacklightListeners.removeIf((delegate) -> delegate.mListener == listener);
            if (mKeyboardBacklightListeners.isEmpty()) {
                try {
                    mIm.unregisterKeyboardBacklightListener(mKeyboardBacklightListener);
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
                mKeyboardBacklightListeners = null;
                mKeyboardBacklightListener = null;
            }
        }
    }

    private static final class StickyModifierStateListenerDelegate {
        final InputManager.StickyModifierStateListener mListener;
        final Executor mExecutor;

        StickyModifierStateListenerDelegate(StickyModifierStateListener listener,
                Executor executor) {
            mListener = listener;
            mExecutor = executor;
        }

        void notifyStickyModifierStateChange(int modifierState, int lockedModifierState) {
            mExecutor.execute(() ->
                    mListener.onStickyModifierStateChanged(
                            new LocalStickyModifierState(modifierState, lockedModifierState)));
        }
    }

    private class LocalStickyModifierStateListener extends IStickyModifierStateListener.Stub {

        @Override
        public void onStickyModifierStateChanged(int modifierState, int lockedModifierState) {
            synchronized (mStickyModifierStateListenerLock) {
                if (mStickyModifierStateListeners == null) return;
                final int numListeners = mStickyModifierStateListeners.size();
                for (int i = 0; i < numListeners; i++) {
                    mStickyModifierStateListeners.get(i)
                            .notifyStickyModifierStateChange(modifierState, lockedModifierState);
                }
            }
        }
    }

    // Implementation of the android.hardware.input.StickyModifierState interface used to report
    // the sticky modifier state via the StickyModifierStateListener interfaces.
    private static final class LocalStickyModifierState extends StickyModifierState {

        private final int mModifierState;
        private final int mLockedModifierState;

        LocalStickyModifierState(int modifierState, int lockedModifierState) {
            mModifierState = modifierState;
            mLockedModifierState = lockedModifierState;
        }

        @Override
        public boolean isShiftModifierOn() {
            return (mModifierState & KeyEvent.META_SHIFT_ON) != 0;
        }

        @Override
        public boolean isShiftModifierLocked() {
            return (mLockedModifierState & KeyEvent.META_SHIFT_ON) != 0;
        }

        @Override
        public boolean isCtrlModifierOn() {
            return (mModifierState & KeyEvent.META_CTRL_ON) != 0;
        }

        @Override
        public boolean isCtrlModifierLocked() {
            return (mLockedModifierState & KeyEvent.META_CTRL_ON) != 0;
        }

        @Override
        public boolean isMetaModifierOn() {
            return (mModifierState & KeyEvent.META_META_ON) != 0;
        }

        @Override
        public boolean isMetaModifierLocked() {
            return (mLockedModifierState & KeyEvent.META_META_ON) != 0;
        }

        @Override
        public boolean isAltModifierOn() {
            return (mModifierState & KeyEvent.META_ALT_LEFT_ON) != 0;
        }

        @Override
        public boolean isAltModifierLocked() {
            return (mLockedModifierState & KeyEvent.META_ALT_LEFT_ON) != 0;
        }

        @Override
        public boolean isAltGrModifierOn() {
            return (mModifierState & KeyEvent.META_ALT_RIGHT_ON) != 0;
        }

        @Override
        public boolean isAltGrModifierLocked() {
            return (mLockedModifierState & KeyEvent.META_ALT_RIGHT_ON) != 0;
        }
    }

    /**
     * @see InputManager#registerStickyModifierStateListener(Executor, StickyModifierStateListener)
     */
    @RequiresPermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)
    void registerStickyModifierStateListener(@NonNull Executor executor,
            @NonNull StickyModifierStateListener listener) throws IllegalArgumentException {
        Objects.requireNonNull(executor, "executor should not be null");
        Objects.requireNonNull(listener, "listener should not be null");

        synchronized (mStickyModifierStateListenerLock) {
            if (mStickyModifierStateListener == null) {
                mStickyModifierStateListeners = new ArrayList<>();
                mStickyModifierStateListener = new LocalStickyModifierStateListener();

                try {
                    mIm.registerStickyModifierStateListener(mStickyModifierStateListener);
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            final int numListeners = mStickyModifierStateListeners.size();
            for (int i = 0; i < numListeners; i++) {
                if (mStickyModifierStateListeners.get(i).mListener == listener) {
                    throw new IllegalArgumentException("Listener has already been registered!");
                }
            }
            StickyModifierStateListenerDelegate delegate =
                    new StickyModifierStateListenerDelegate(listener, executor);
            mStickyModifierStateListeners.add(delegate);
        }
    }

    /**
     * @see InputManager#unregisterStickyModifierStateListener(StickyModifierStateListener)
     */
    @RequiresPermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)
    void unregisterStickyModifierStateListener(
            @NonNull StickyModifierStateListener listener) {
        Objects.requireNonNull(listener, "listener should not be null");

        synchronized (mStickyModifierStateListenerLock) {
            if (mStickyModifierStateListeners == null) {
                return;
            }
            mStickyModifierStateListeners.removeIf((delegate) -> delegate.mListener == listener);
            if (mStickyModifierStateListeners.isEmpty()) {
                try {
                    mIm.unregisterStickyModifierStateListener(mStickyModifierStateListener);
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
                mStickyModifierStateListeners = null;
                mStickyModifierStateListener = null;
            }
        }
    }

    private static final class KeyGestureEventListenerDelegate {
        final KeyGestureEventListener mListener;
        final Executor mExecutor;

        KeyGestureEventListenerDelegate(KeyGestureEventListener listener,
                Executor executor) {
            mListener = listener;
            mExecutor = executor;
        }

        void onKeyGestureEvent(KeyGestureEvent event) {
            mExecutor.execute(() -> mListener.onKeyGestureEvent(event));
        }
    }

    private class LocalKeyGestureEventListener extends IKeyGestureEventListener.Stub {
        @Override
        public void onKeyGestureEvent(@NonNull AidlKeyGestureEvent ev) {
            synchronized (mKeyGestureEventListenerLock) {
                if (mKeyGestureEventListeners == null) return;
                final int numListeners = mKeyGestureEventListeners.size();
                final KeyGestureEvent event = new KeyGestureEvent(ev);
                for (int i = 0; i < numListeners; i++) {
                    mKeyGestureEventListeners.get(i).onKeyGestureEvent(event);
                }
            }
        }
    }

    /**
     * @see InputManager#registerKeyGestureEventListener(Executor,
     * KeyGestureEventListener)
     */
    @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES)
    void registerKeyGestureEventListener(@NonNull Executor executor,
            @NonNull KeyGestureEventListener listener) throws IllegalArgumentException {
        Objects.requireNonNull(executor, "executor should not be null");
        Objects.requireNonNull(listener, "listener should not be null");

        synchronized (mKeyGestureEventListenerLock) {
            if (mKeyGestureEventListener == null) {
                mKeyGestureEventListeners = new ArrayList<>();
                mKeyGestureEventListener = new LocalKeyGestureEventListener();

                try {
                    mIm.registerKeyGestureEventListener(mKeyGestureEventListener);
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            final int numListeners = mKeyGestureEventListeners.size();
            for (int i = 0; i < numListeners; i++) {
                if (mKeyGestureEventListeners.get(i).mListener == listener) {
                    throw new IllegalArgumentException("Listener has already been registered!");
                }
            }
            KeyGestureEventListenerDelegate delegate =
                    new KeyGestureEventListenerDelegate(listener, executor);
            mKeyGestureEventListeners.add(delegate);
        }
    }

    /**
     * @see InputManager#unregisterKeyGestureEventListener(KeyGestureEventListener)
     */
    @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES)
    void unregisterKeyGestureEventListener(@NonNull KeyGestureEventListener listener) {
        Objects.requireNonNull(listener, "listener should not be null");

        synchronized (mKeyGestureEventListenerLock) {
            if (mKeyGestureEventListeners == null) {
                return;
            }
            mKeyGestureEventListeners.removeIf((delegate) -> delegate.mListener == listener);
            if (mKeyGestureEventListeners.isEmpty()) {
                try {
                    mIm.unregisterKeyGestureEventListener(mKeyGestureEventListener);
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
                mKeyGestureEventListeners = null;
                mKeyGestureEventListener = null;
            }
        }
    }

    private class LocalKeyGestureHandler extends IKeyGestureHandler.Stub {
        @Override
        public void handleKeyGesture(@NonNull AidlKeyGestureEvent ev, IBinder focusedToken) {
            synchronized (mKeyGesturesToHandlerMap) {
                KeyGestureEventHandler handler = mKeyGesturesToHandlerMap.get(ev.gestureType);
                if (handler == null) {
                    Log.w(TAG, "Key gesture event " + ev.gestureType
                            + " occurred without a registered handler!");
                    return;
                }
                handler.handleKeyGestureEvent(new KeyGestureEvent(ev), focusedToken);
            }
        }
    }

    /**
     * @see InputManager#registerKeyGestureEventHandler(List, KeyGestureEventHandler)
     */
    @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES)
    void registerKeyGestureEventHandler(List<Integer> keyGesturesToHandle,
            @NonNull KeyGestureEventHandler handler) throws IllegalArgumentException {
        Objects.requireNonNull(keyGesturesToHandle, "List of gestures should not be null");
        Objects.requireNonNull(handler, "handler should not be null");

        if (keyGesturesToHandle.isEmpty()) {
            throw new IllegalArgumentException("No key gestures provided!");
        }

        synchronized (mKeyGesturesToHandlerMap) {
            IntArray newKeyGestures = new IntArray(
                    keyGesturesToHandle.size() + mKeyGesturesToHandlerMap.size());

            // Check if the handler already exists
            for (int i = 0; i < mKeyGesturesToHandlerMap.size(); i++) {
                KeyGestureEventHandler h = mKeyGesturesToHandlerMap.valueAt(i);
                if (h == handler) {
                    throw new IllegalArgumentException("Handler has already been registered!");
                }
                newKeyGestures.add(mKeyGesturesToHandlerMap.keyAt(i));
            }

            // Check if any of the key gestures are already handled by existing handlers
            for (int gesture : keyGesturesToHandle) {
                if (mKeyGesturesToHandlerMap.contains(gesture)) {
                    throw new IllegalArgumentException("Key gesture " + gesture
                            + " is already registered by another handler!");
                }
                newKeyGestures.add(gesture);
            }

            try {
                // If handler was already registered for this process, we need to unregister and
                // re-register it for the new set of gestures
                if (mKeyGestureHandler != null) {
                    mIm.unregisterKeyGestureHandler(mKeyGestureHandler);
                } else {
                    mKeyGestureHandler = new LocalKeyGestureHandler();
                }
                mIm.registerKeyGestureHandler(newKeyGestures.toArray(), mKeyGestureHandler);
                for (int gesture : keyGesturesToHandle) {
                    mKeyGesturesToHandlerMap.put(gesture, handler);
                }
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
    }

    /**
     * @see InputManager#unregisterKeyGestureEventHandler(KeyGestureEventHandler)
     */
    @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES)
    void unregisterKeyGestureEventHandler(@NonNull KeyGestureEventHandler handler) {
        Objects.requireNonNull(handler, "handler should not be null");

        synchronized (mKeyGesturesToHandlerMap) {
            if (mKeyGestureHandler == null) {
                return;
            }
            for (int i = mKeyGesturesToHandlerMap.size() - 1; i >= 0; i--) {
                if (mKeyGesturesToHandlerMap.valueAt(i) == handler) {
                    mKeyGesturesToHandlerMap.removeAt(i);
                }
            }
            if (mKeyGesturesToHandlerMap.size() == 0) {
                try {
                    mIm.unregisterKeyGestureHandler(mKeyGestureHandler);
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
                mKeyGestureHandler = null;
            }
        }
    }

    private class LocalKeyEventActivityListener extends IKeyEventActivityListener.Stub {
        @Override
        public void onKeyEventActivity() {
            synchronized (mKeyEventActivityLock) {
                final int numListeners = mKeyEventActivityListeners.size();
                for (int i = 0; i < numListeners; i++) {
                    KeyEventActivityListener listener = mKeyEventActivityListeners.get(i);
                    listener.onKeyEventActivity();
                }
            }
        }
    }

    boolean registerKeyEventActivityListener(@NonNull KeyEventActivityListener listener) {
        Objects.requireNonNull(listener, "listener should not be null");
        boolean success = false;
        synchronized (mKeyEventActivityLock) {
            if (mKeyEventActivityListener == null) {
                mKeyEventActivityListeners = new ArrayList<>();
                mKeyEventActivityListener = new LocalKeyEventActivityListener();

                try {
                    success = mIm.registerKeyEventActivityListener(mKeyEventActivityListener);
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            if (mKeyEventActivityListeners.contains(listener)) {
                throw new IllegalArgumentException("Listener has already been registered!");
            }
            mKeyEventActivityListeners.add(listener);
            return success;
        }
    }

    boolean unregisterKeyEventActivityListener(@NonNull KeyEventActivityListener listener) {
        Objects.requireNonNull(listener, "listener should not be null");

        boolean success = true;
        synchronized (mKeyEventActivityLock) {
            if (mKeyEventActivityListeners == null) {
                return success;
            }
            mKeyEventActivityListeners.remove(listener);
            if (mKeyEventActivityListeners.isEmpty()) {
                try {
                    success = mIm.unregisterKeyEventActivityListener(mKeyEventActivityListener);
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
                mKeyEventActivityListeners = null;
                mKeyEventActivityListener = null;
            }
        }
        return success;
    }

    /**
     * Sets the keyboard layout override for the specified input device. This will set the
     * keyboard layout as the default for the input device irrespective of the underlying IME
     * configuration.
     *
     * <p>
     * Prefer using {@link InputManager#setKeyboardLayoutForInputDevice(InputDeviceIdentifier, int,
     * InputMethodInfo, InputMethodSubtype, String)} for normal use cases.
     * </p><p>
     * This method is to be used only for special cases where we knowingly want to set a
     * particular keyboard layout for a keyboard, ignoring the IME configuration. e.g. Setting a
     * default layout for an Android Emulator where we know the preferred H/W keyboard layout.
     * </p><p>
     * NOTE: This may affect the typing experience if the layout isn't compatible with the IME
     * configuration.
     * </p><p>
     * NOTE: User can still change the keyboard layout configuration from the settings page.
     * </p>
     *
     * @param identifier The identifier for the input device.
     * @param keyboardLayoutDescriptor The keyboard layout descriptor to use.
     *
     * @hide
     */
    @RequiresPermission(Manifest.permission.SET_KEYBOARD_LAYOUT)
    public void setKeyboardLayoutOverrideForInputDevice(@NonNull InputDeviceIdentifier identifier,
            @NonNull String keyboardLayoutDescriptor) {
        Objects.requireNonNull(identifier, "identifier should not be null");
        Objects.requireNonNull(keyboardLayoutDescriptor,
                "keyboardLayoutDescriptor should not be null");
        try {
            mIm.setKeyboardLayoutOverrideForInputDevice(identifier, keyboardLayoutDescriptor);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * TODO(b/330517633): Cleanup the unsupported API
     */
    @NonNull
    public KeyboardLayout[] getKeyboardLayoutsForInputDevice(
            @NonNull InputDeviceIdentifier identifier) {
        return new KeyboardLayout[0];
    }

    /**
     * TODO(b/330517633): Cleanup the unsupported API
     */
    public void setCurrentKeyboardLayoutForInputDevice(
            @NonNull InputDeviceIdentifier identifier,
            @NonNull String keyboardLayoutDescriptor) {}


    /**
     * @see InputDevice#getSensorManager()
     */
    @NonNull
    public SensorManager getInputDeviceSensorManager(int deviceId) {
        synchronized (mInputDeviceListeners) {
            if (mInputDeviceSensorManager == null) {
                mInputDeviceSensorManager = new InputDeviceSensorManager(this);
            }
            return mInputDeviceSensorManager.getSensorManager(deviceId);
        }
    }

    /**
     * Get information about all of the sensors supported by an input device
     * @see InputDeviceSensorManager
     */
    InputSensorInfo[] getSensorList(int deviceId) {
        try {
            return mIm.getSensorList(deviceId);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputDeviceSensorManager
     */
    boolean enableSensor(int deviceId, int sensorType, int samplingPeriodUs,
            int maxBatchReportLatencyUs) {
        try {
            return mIm.enableSensor(deviceId, sensorType, samplingPeriodUs,
                    maxBatchReportLatencyUs);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputDeviceSensorManager
     */
    void disableSensor(int deviceId, int sensorType) {
        try {
            mIm.disableSensor(deviceId, sensorType);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputDeviceSensorManager
     */
    boolean flushSensor(int deviceId, int sensorType) {
        try {
            return mIm.flushSensor(deviceId, sensorType);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputDeviceSensorManager
     */
    boolean registerSensorListener(IInputSensorEventListener listener) {
        try {
            return mIm.registerSensorListener(listener);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputDeviceSensorManager
     */
    void unregisterSensorListener(IInputSensorEventListener listener) {
        try {
            mIm.unregisterSensorListener(listener);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputDevice#getLightsManager()
     */
    @NonNull
    public LightsManager getInputDeviceLightsManager(int deviceId) {
        return new InputDeviceLightsManager(deviceId);
    }

    /**
     * Gets a list of light objects associated with an input device.
     * @return The list of lights, never null.
     */
    @NonNull List<Light> getLights(int deviceId) {
        try {
            return mIm.getLights(deviceId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Returns the state of an input device light.
     * @return the light state
     */
    @NonNull LightState getLightState(int deviceId, @NonNull Light light) {
        try {
            return mIm.getLightState(deviceId, light.getId());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Request to modify the states of multiple lights.
     *
     * @param request the settings for lights that should change
     */
    void requestLights(int deviceId, @NonNull LightsRequest request, IBinder token) {
        try {
            List<Integer> lightIdList = request.getLights();
            int[] lightIds = new int[lightIdList.size()];
            for (int i = 0; i < lightIds.length; i++) {
                lightIds[i] = lightIdList.get(i);
            }
            List<LightState> lightStateList = request.getLightStates();
            mIm.setLightStates(deviceId, lightIds,
                    lightStateList.toArray(new LightState[0]),
                    token);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Open light session for input device manager
     *
     * @param token The token for the light session
     */
    void openLightSession(int deviceId, String opPkg, @NonNull IBinder token) {
        try {
            mIm.openLightSession(deviceId, opPkg, token);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Close light session
     *
     */
    void closeLightSession(int deviceId, @NonNull IBinder token) {
        try {
            mIm.closeLightSession(deviceId, token);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputManager#getInputDeviceVibrator(int, int)
     */
    public Vibrator getInputDeviceVibrator(int deviceId, int vibratorId) {
        return new InputDeviceVibrator(deviceId, vibratorId);
    }

    /**
     * @see InputDevice#getVibratorManager()
     */
    @NonNull
    public VibratorManager getInputDeviceVibratorManager(int deviceId) {
        return new InputDeviceVibratorManager(deviceId);
    }

    /*
     * Get the list of device vibrators
     * @return The list of vibrators IDs
     */
    int[] getVibratorIds(int deviceId) {
        try {
            return mIm.getVibratorIds(deviceId);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /*
     * Perform vibration effect
     */
    void vibrate(int deviceId, VibrationEffect effect, IBinder token) {
        try {
            mIm.vibrate(deviceId, effect, token);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /*
     * Perform combined vibration effect
     */
    void vibrate(int deviceId, CombinedVibration effect, IBinder token) {
        try {
            mIm.vibrateCombined(deviceId, effect, token);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /*
     * Cancel an ongoing vibration
     */
    void cancelVibrate(int deviceId, IBinder token) {
        try {
            mIm.cancelVibrate(deviceId, token);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /*
     * Check if input device is vibrating
     */
    boolean isVibrating(int deviceId)  {
        try {
            return mIm.isVibrating(deviceId);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /**
     * Register input device vibrator state listener
     */
    boolean registerVibratorStateListener(int deviceId, IVibratorStateListener listener) {
        try {
            return mIm.registerVibratorStateListener(deviceId, listener);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /**
     * Unregister input device vibrator state listener
     */
    boolean unregisterVibratorStateListener(int deviceId, IVibratorStateListener listener) {
        try {
            return mIm.unregisterVibratorStateListener(deviceId, listener);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputManager#deviceHasKeys(int[])
     */
    public boolean[] deviceHasKeys(int[] keyCodes) {
        return deviceHasKeys(-1, keyCodes);
    }

    /**
     * @see InputManager#deviceHasKeys(int, int[])
     */
    public boolean[] deviceHasKeys(int id, int[] keyCodes) {
        boolean[] ret = new boolean[keyCodes.length];
        try {
            mIm.hasKeys(id, InputDevice.SOURCE_ANY, keyCodes, ret);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
        return ret;
    }

    /**
     * @see InputManager#getKeyCodeForKeyLocation(int, int)
     */
    public int getKeyCodeForKeyLocation(int deviceId, int locationKeyCode) {
        try {
            return mIm.getKeyCodeForKeyLocation(deviceId, locationKeyCode);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Returns KeyCharacterMap for the provided Keyboard layout. If provided layout is null it will
     * return KeyCharacter map for the default layout {@code Generic.kl}.
     */
    public KeyCharacterMap getKeyCharacterMap(@Nullable KeyboardLayout keyboardLayout) {
        if (keyboardLayout == null) {
            return KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
        }
        try {
            return mIm.getKeyCharacterMap(keyboardLayout.getDescriptor());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputManager#injectInputEvent(InputEvent, int, int)
     */

    public boolean injectInputEvent(InputEvent event, int mode, int targetUid) {
        Objects.requireNonNull(event , "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");
        }

        try {
            return mIm.injectInputEventToTarget(event, mode, targetUid);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputManager#injectInputEvent(InputEvent, int)
     */
    public boolean injectInputEvent(InputEvent event, int mode) {
        return injectInputEvent(event, mode, Process.INVALID_UID);
    }

    /**
     * @see InputManager#createVirtualKeyboard(VirtualKeyboardConfig)
     */
    @NonNull
    @RequiresPermission(anyOf = {
            Manifest.permission.INJECT_KEY_EVENTS,
            Manifest.permission.INJECT_EVENTS
    })
    public VirtualKeyboard createVirtualKeyboard(@NonNull VirtualKeyboardConfig config) {
        IVirtualInputDevice virtualInputDevice;
        try {
            // Pass a token to the server so that the server can be notified when the calling
            // process has died and therefore clean up the virtual device.
            final IBinder token = new Binder(
                    "android.hardware.input.VirtualKeyboard:" + config.getInputDeviceName());
            virtualInputDevice = mIm.createVirtualKeyboard(token, config);
            return new VirtualKeyboard(config, virtualInputDevice);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputManager#setPointerIcon(PointerIcon, int, int, int, IBinder)
     */
    public boolean setPointerIcon(PointerIcon icon, int displayId, int deviceId, int pointerId,
            IBinder inputToken) {
        try {
            return mIm.setPointerIcon(icon, displayId, deviceId, pointerId, inputToken);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /**
     * @see android.view.View#requestPointerCapture()
     */
    public void requestPointerCapture(@NonNull IBinder windowToken, @PointerCaptureMode int mode) {
        // We need to check the mode is valid here too, since the binder call is oneway, so the
        // check in InputManagerService#requestPointerCapture would not result in the exception
        // being propagated to the caller.
        if (mode != View.POINTER_CAPTURE_MODE_UNCAPTURED
                && mode != View.POINTER_CAPTURE_MODE_ABSOLUTE
                && mode != View.POINTER_CAPTURE_MODE_RELATIVE) {
            throw new IllegalArgumentException("Invalid pointer capture mode " + mode);
        }

        try {
            mIm.requestPointerCapture(windowToken, mode);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputManager#monitorGestureInput(String, int)
     */
    public InputMonitor monitorGestureInput(String name, int displayId) {
        try {
            return mIm.monitorGestureInput(new Binder(), name, displayId);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputManager#addUniqueIdAssociationByPort(String, String)
     */
    public void addUniqueIdAssociationByPort(@NonNull String inputPort,
            @NonNull String displayUniqueId) {
        try {
            mIm.addUniqueIdAssociationByPort(inputPort, displayUniqueId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputManager#removeUniqueIdAssociationByPort(String)
     */
    public void removeUniqueIdAssociationByPort(@NonNull String inputPort) {
        try {
            mIm.removeUniqueIdAssociationByPort(inputPort);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputManager#addUniqueIdAssociationByDescriptor(String, String)
     */
    public void addUniqueIdAssociationByDescriptor(@NonNull String inputDeviceDescriptor,
                                                   @NonNull String displayUniqueId) {
        try {
            mIm.addUniqueIdAssociationByDescriptor(inputDeviceDescriptor, displayUniqueId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputManager#removeUniqueIdAssociationByDescriptor(String)
     */
    public void removeUniqueIdAssociationByDescriptor(@NonNull String inputDeviceDescriptor) {
        try {
            mIm.removeUniqueIdAssociationByDescriptor(inputDeviceDescriptor);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputManager#getInputDeviceBluetoothAddress(int)
     */
    @RequiresPermission(Manifest.permission.BLUETOOTH)
    @Nullable
    public String getInputDeviceBluetoothAddress(int deviceId) {
        try {
            return mIm.getInputDeviceBluetoothAddress(deviceId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputManager#cancelCurrentTouch()
     */
    public void cancelCurrentTouch() {
        try {
            mIm.cancelCurrentTouch();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * @see InputManager#pilferPointers(IBinder)
     */
    @RequiresPermission(Manifest.permission.MONITOR_INPUT)
    public void pilferPointers(IBinder inputChannelToken) {
        try {
            mIm.pilferPointers(inputChannelToken);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
}
