diff options
9 files changed, 229 insertions, 22 deletions
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index f213224b981e..49c0f9261c53 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -161,4 +161,11 @@ interface IInputManager { void registerBatteryListener(int deviceId, IInputDeviceBatteryListener listener); void unregisterBatteryListener(int deviceId, IInputDeviceBatteryListener listener); + + // Get the bluetooth address of an input device if known, returning null if it either is not + // connected via bluetooth or if the address cannot be determined. + @EnforcePermission("BLUETOOTH") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.BLUETOOTH)") + String getInputDeviceBluetoothAddress(int deviceId); } diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 8d4aac4bba88..0cf15f76103e 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -1481,6 +1481,24 @@ public final class InputManager { } /** + * Returns the Bluetooth address of this input device, if known. + * + * The returned string is always null if this input device is not connected + * via Bluetooth, or if the Bluetooth address of the device cannot be + * determined. The returned address will look like: "11:22:33:44:55:66". + * @hide + */ + @RequiresPermission(Manifest.permission.BLUETOOTH) + @Nullable + public String getInputDeviceBluetoothAddress(int deviceId) { + try { + return mIm.getInputDeviceBluetoothAddress(deviceId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Gets a vibrator service associated with an input device, always creates a new instance. * @return The vibrator, never null. * @hide diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index 9b1d8673390b..799955b1107a 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -16,6 +16,7 @@ package android.view; +import android.Manifest; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -1010,6 +1011,22 @@ public final class InputDevice implements Parcelable { } /** + * Returns the Bluetooth address of this input device, if known. + * + * The returned string is always null if this input device is not connected + * via Bluetooth, or if the Bluetooth address of the device cannot be + * determined. The returned address will look like: "11:22:33:44:55:66". + * @hide + */ + @RequiresPermission(Manifest.permission.BLUETOOTH) + @Nullable + public String getBluetoothAddress() { + // We query the address via a separate InputManager API instead of pre-populating it in + // this class to avoid leaking it to apps that do not have sufficient permissions. + return InputManager.getInstance().getInputDeviceBluetoothAddress(mId); + } + + /** * Gets the vibrator service associated with the device, if there is one. * Even if the device does not have a vibrator, the result is never null. * Use {@link Vibrator#hasVibrator} to determine whether a vibrator is diff --git a/core/jni/android_view_InputDevice.cpp b/core/jni/android_view_InputDevice.cpp index 39ec0374dc5e..b2994f41af4b 100644 --- a/core/jni/android_view_InputDevice.cpp +++ b/core/jni/android_view_InputDevice.cpp @@ -70,6 +70,8 @@ jobject android_view_InputDevice_create(JNIEnv* env, const InputDeviceInfo& devi deviceInfo.hasMic(), deviceInfo.hasButtonUnderPad(), deviceInfo.hasSensor(), deviceInfo.hasBattery(), deviceInfo.supportsUsi())); + // Note: We do not populate the Bluetooth address into the InputDevice object to avoid leaking + // it to apps that do not have the Bluetooth permission. const std::vector<InputDeviceInfo::MotionRange>& ranges = deviceInfo.getMotionRanges(); for (const InputDeviceInfo::MotionRange& range: ranges) { diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java index 36199debaa6e..9d4f18113555 100644 --- a/services/core/java/com/android/server/input/BatteryController.java +++ b/services/core/java/com/android/server/input/BatteryController.java @@ -371,6 +371,17 @@ final class BatteryController { } } + public void notifyStylusGestureStarted(int deviceId, long eventTime) { + synchronized (mLock) { + final DeviceMonitor monitor = mDeviceMonitors.get(deviceId); + if (monitor == null) { + return; + } + + monitor.onStylusGestureStarted(eventTime); + } + } + public void dump(PrintWriter pw, String prefix) { synchronized (mLock) { final String indent = prefix + " "; @@ -557,6 +568,8 @@ final class BatteryController { public void onTimeout(long eventTime) {} + public void onStylusGestureStarted(long eventTime) {} + // Returns the current battery state that can be used to notify listeners BatteryController. public State getBatteryStateForReporting() { return new State(mState); @@ -600,6 +613,22 @@ final class BatteryController { } @Override + public void onStylusGestureStarted(long eventTime) { + processChangesAndNotify(eventTime, (time) -> { + final boolean wasValid = mValidityTimeoutCallback != null; + if (!wasValid && mState.capacity == 0.f) { + // Handle a special case where the USI device reports a battery capacity of 0 + // at boot until the first battery update. To avoid wrongly sending out a + // battery capacity of 0 if we detect stylus presence before the capacity + // is first updated, do not validate the battery state when the state is not + // valid and the capacity is 0. + return; + } + markUsiBatteryValid(); + }); + } + + @Override public void onTimeout(long eventTime) { processChangesAndNotify(eventTime, (time) -> markUsiBatteryInvalid()); } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 69b0e65e38da..31f63d864f6c 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -19,6 +19,8 @@ package com.android.server.input; import static android.provider.DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT; import static android.view.KeyEvent.KEYCODE_UNKNOWN; +import android.Manifest; +import android.annotation.EnforcePermission; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManagerInternal; @@ -2671,6 +2673,12 @@ public class InputManagerService extends IInputManager.Stub mBatteryController.unregisterBatteryListener(deviceId, listener, Binder.getCallingPid()); } + @EnforcePermission(Manifest.permission.BLUETOOTH) + @Override + public String getInputDeviceBluetoothAddress(int deviceId) { + return mNative.getBluetoothAddress(deviceId); + } + @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; @@ -3052,6 +3060,12 @@ public class InputManagerService extends IInputManager.Stub com.android.internal.R.bool.config_perDisplayFocusEnabled); } + // Native callback. + @SuppressWarnings("unused") + private void notifyStylusGestureStarted(int deviceId, long eventTime) { + mBatteryController.notifyStylusGestureStarted(deviceId, eventTime); + } + /** * Flatten a map into a string list, with value positioned directly next to the * key. diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index 63c0a88bf467..cfa7fb141be1 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -204,6 +204,9 @@ interface NativeInputManagerService { /** Set the displayId on which the mouse cursor should be shown. */ void setPointerDisplayId(int displayId); + /** Get the bluetooth address of an input device if known, otherwise return null. */ + String getBluetoothAddress(int deviceId); + /** The native implementation of InputManagerService methods. */ class NativeImpl implements NativeInputManagerService { /** Pointer to native input manager service object, used by native code. */ @@ -418,5 +421,8 @@ interface NativeInputManagerService { @Override public native void setPointerDisplayId(int displayId); + + @Override + public native String getBluetoothAddress(int deviceId); } } diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 3f380e7914d0..0d872370dcdc 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -107,6 +107,7 @@ static struct { jmethodID notifyFocusChanged; jmethodID notifySensorEvent; jmethodID notifySensorAccuracy; + jmethodID notifyStylusGestureStarted; jmethodID notifyVibratorState; jmethodID filterInputEvent; jmethodID interceptKeyBeforeQueueing; @@ -299,6 +300,7 @@ public: void requestPointerCapture(const sp<IBinder>& windowToken, bool enabled); void setCustomPointerIcon(const SpriteIcon& icon); void setMotionClassifierEnabled(bool enabled); + std::optional<std::string> getBluetoothAddress(int32_t deviceId); /* --- InputReaderPolicyInterface implementation --- */ @@ -312,6 +314,7 @@ public: int32_t surfaceRotation) override; TouchAffineTransformation getTouchAffineTransformation(JNIEnv* env, jfloatArray matrixArr); + void notifyStylusGestureStarted(int32_t deviceId, nsecs_t eventTime) override; /* --- InputDispatcherPolicyInterface implementation --- */ @@ -370,37 +373,37 @@ private: Mutex mLock; struct Locked { // Display size information. - std::vector<DisplayViewport> viewports; + std::vector<DisplayViewport> viewports{}; // True if System UI is less noticeable. - bool systemUiLightsOut; + bool systemUiLightsOut{false}; // Pointer speed. - int32_t pointerSpeed; + int32_t pointerSpeed{0}; // Pointer acceleration. - float pointerAcceleration; + float pointerAcceleration{android::os::IInputConstants::DEFAULT_POINTER_ACCELERATION}; // True if pointer gestures are enabled. - bool pointerGesturesEnabled; + bool pointerGesturesEnabled{true}; // Show touches feature enable/disable. - bool showTouches; + bool showTouches{false}; // The latest request to enable or disable Pointer Capture. - PointerCaptureRequest pointerCaptureRequest; + PointerCaptureRequest pointerCaptureRequest{}; // Sprite controller singleton, created on first use. - sp<SpriteController> spriteController; + sp<SpriteController> spriteController{}; // Pointer controller singleton, created and destroyed as needed. - std::weak_ptr<PointerController> pointerController; + std::weak_ptr<PointerController> pointerController{}; // Input devices to be disabled - std::set<int32_t> disabledInputDevices; + std::set<int32_t> disabledInputDevices{}; // Associated Pointer controller display. - int32_t pointerDisplayId; + int32_t pointerDisplayId{ADISPLAY_ID_DEFAULT}; } mLocked GUARDED_BY(mLock); std::atomic<bool> mInteractive; @@ -419,16 +422,6 @@ NativeInputManager::NativeInputManager(jobject serviceObj, const sp<Looper>& loo mServiceObj = env->NewGlobalRef(serviceObj); - { - AutoMutex _l(mLock); - mLocked.systemUiLightsOut = false; - mLocked.pointerSpeed = 0; - mLocked.pointerAcceleration = android::os::IInputConstants::DEFAULT_POINTER_ACCELERATION; - mLocked.pointerGesturesEnabled = true; - mLocked.showTouches = false; - mLocked.pointerDisplayId = ADISPLAY_ID_DEFAULT; - } - mInteractive = true; InputManager* im = new InputManager(this, this); mInputManager = im; defaultServiceManager()->addService(String16("inputflinger"), im); @@ -1177,6 +1170,13 @@ TouchAffineTransformation NativeInputManager::getTouchAffineTransformation( return transform; } +void NativeInputManager::notifyStylusGestureStarted(int32_t deviceId, nsecs_t eventTime) { + JNIEnv* env = jniEnv(); + env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifyStylusGestureStarted, deviceId, + eventTime); + checkAndClearExceptionFromCallback(env, "notifyStylusGestureStarted"); +} + bool NativeInputManager::filterInputEvent(const InputEvent* inputEvent, uint32_t policyFlags) { ATRACE_CALL(); jobject inputEventObj; @@ -1487,6 +1487,10 @@ void NativeInputManager::setMotionClassifierEnabled(bool enabled) { mInputManager->getProcessor().setMotionClassifierEnabled(enabled); } +std::optional<std::string> NativeInputManager::getBluetoothAddress(int32_t deviceId) { + return mInputManager->getReader().getBluetoothAddress(deviceId); +} + bool NativeInputManager::isPerDisplayTouchModeEnabled() { JNIEnv* env = jniEnv(); jboolean enabled = @@ -2326,6 +2330,12 @@ static void nativeSetPointerDisplayId(JNIEnv* env, jobject nativeImplObj, jint d im->setPointerDisplayId(displayId); } +static jstring nativeGetBluetoothAddress(JNIEnv* env, jobject nativeImplObj, jint deviceId) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + const auto address = im->getBluetoothAddress(deviceId); + return address ? env->NewStringUTF(address->c_str()) : nullptr; +} + // ---------------------------------------------------------------------------- static const JNINativeMethod gInputManagerMethods[] = { @@ -2408,6 +2418,7 @@ static const JNINativeMethod gInputManagerMethods[] = { {"flushSensor", "(II)Z", (void*)nativeFlushSensor}, {"cancelCurrentTouch", "()V", (void*)nativeCancelCurrentTouch}, {"setPointerDisplayId", "(I)V", (void*)nativeSetPointerDisplayId}, + {"getBluetoothAddress", "(I)Ljava/lang/String;", (void*)nativeGetBluetoothAddress}, }; #define FIND_CLASS(var, className) \ @@ -2469,6 +2480,9 @@ int register_android_server_InputManager(JNIEnv* env) { GET_METHOD_ID(gServiceClassInfo.notifySensorAccuracy, clazz, "notifySensorAccuracy", "(III)V"); + GET_METHOD_ID(gServiceClassInfo.notifyStylusGestureStarted, clazz, "notifyStylusGestureStarted", + "(IJ)V"); + GET_METHOD_ID(gServiceClassInfo.notifyVibratorState, clazz, "notifyVibratorState", "(IZ)V"); GET_METHOD_ID(gServiceClassInfo.notifyNoFocusedWindowAnr, clazz, "notifyNoFocusedWindowAnr", diff --git a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt index c68db3460dac..6590a2b500e4 100644 --- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt @@ -36,6 +36,7 @@ import androidx.test.InstrumentationRegistry import com.android.server.input.BatteryController.POLLING_PERIOD_MILLIS import com.android.server.input.BatteryController.UEventManager import com.android.server.input.BatteryController.UEventManager.UEventBatteryListener +import com.android.server.input.BatteryController.USI_BATTERY_VALIDITY_DURATION_MILLIS import org.hamcrest.Description import org.hamcrest.Matcher import org.hamcrest.MatcherAssert.assertThat @@ -528,10 +529,109 @@ class BatteryControllerTests { matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f)) // The battery is no longer present after the timeout expires. - testLooper.moveTimeForward(BatteryController.USI_BATTERY_VALIDITY_DURATION_MILLIS - 1) + testLooper.moveTimeForward(USI_BATTERY_VALIDITY_DURATION_MILLIS - 1) testLooper.dispatchNext() listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID), times(2)) assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), isInvalidBatteryState(USI_DEVICE_ID)) } + + @Test + fun testStylusPresenceExtendsValidUsiBatteryState() { + `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device") + `when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_DISCHARGING) + `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(78) + + addInputDevice(USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) + verify(uEventManager) + .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + + // There is a UEvent signaling a battery change. The battery state is now valid. + uEventListener.value!!.onBatteryUEvent(TIMESTAMP) + val listener = createMockListener() + batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) + listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)) + + // Stylus presence is detected before the validity timeout expires. + testLooper.moveTimeForward(100) + testLooper.dispatchAll() + batteryController.notifyStylusGestureStarted(USI_DEVICE_ID, TIMESTAMP) + + // Ensure that timeout was extended, and the battery state is now valid for longer. + testLooper.moveTimeForward(USI_BATTERY_VALIDITY_DURATION_MILLIS - 100) + testLooper.dispatchAll() + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)) + + // Ensure the validity period expires after the expected amount of time. + testLooper.moveTimeForward(100) + testLooper.dispatchNext() + listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID)) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID)) + } + + @Test + fun testStylusPresenceMakesUsiBatteryStateValid() { + `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device") + `when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_DISCHARGING) + `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(78) + + addInputDevice(USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) + verify(uEventManager) + .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + + // The USI battery state is initially invalid. + val listener = createMockListener() + batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) + listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID)) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID)) + + // A stylus presence is detected. This validates the battery state. + batteryController.notifyStylusGestureStarted(USI_DEVICE_ID, TIMESTAMP) + + listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)) + } + + @Test + fun testStylusPresenceDoesNotMakeUsiBatteryStateValidAtBoot() { + `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device") + // At boot, the USI device always reports a capacity value of 0. + `when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_UNKNOWN) + `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(0) + + addInputDevice(USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) + verify(uEventManager) + .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + + // The USI battery state is initially invalid. + val listener = createMockListener() + batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) + listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID)) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID)) + + // Since the capacity reported is 0, stylus presence does not validate the battery state. + batteryController.notifyStylusGestureStarted(USI_DEVICE_ID, TIMESTAMP) + + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID)) + + // However, if a UEvent reports a battery capacity of 0, the battery state is now valid. + uEventListener.value!!.onBatteryUEvent(TIMESTAMP) + listener.verifyNotified(USI_DEVICE_ID, status = STATUS_UNKNOWN, capacity = 0f) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_UNKNOWN, capacity = 0f)) + } } |