Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a345bc5a authored by Vania Januar's avatar Vania Januar Committed by Automerger Merge Worker
Browse files

Merge "Move stylus first usage detection into StylusManager." into tm-qpr-dev...

Merge "Move stylus first usage detection into StylusManager." into tm-qpr-dev am: 93cbbde6 am: d86046df

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20794367



Change-Id: If7b30ef6bdd8050441c11898270d19b5ca3cbd89
Signed-off-by: default avatarAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
parents c2a7f5aa d86046df
Loading
Loading
Loading
Loading
+0 −136
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.systemui.stylus

import android.content.Context
import android.hardware.BatteryState
import android.hardware.input.InputManager
import android.os.Handler
import android.util.Log
import android.view.InputDevice
import androidx.annotation.VisibleForTesting
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import java.util.concurrent.Executor
import javax.inject.Inject

/**
 * A listener that detects when a stylus has first been used, by detecting 1) the presence of an
 * internal SOURCE_STYLUS with a battery, or 2) any added SOURCE_STYLUS device with a bluetooth
 * address.
 */
@SysUISingleton
class StylusFirstUsageListener
@Inject
constructor(
    private val context: Context,
    private val inputManager: InputManager,
    private val stylusManager: StylusManager,
    private val featureFlags: FeatureFlags,
    @Background private val executor: Executor,
    @Background private val handler: Handler,
) : CoreStartable, StylusManager.StylusCallback, InputManager.InputDeviceBatteryListener {

    // Set must be only accessed from the background handler, which is the same handler that
    // runs the StylusManager callbacks.
    private val internalStylusDeviceIds: MutableSet<Int> = mutableSetOf()
    @VisibleForTesting var hasStarted = false

    override fun start() {
        if (true) return // TODO(b/261826950): remove on main
        if (hasStarted) return
        if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return
        if (inputManager.isStylusEverUsed(context)) return
        if (!hostDeviceSupportsStylusInput()) return

        hasStarted = true
        inputManager.inputDeviceIds.forEach(this::onStylusAdded)
        stylusManager.registerCallback(this)
        stylusManager.startListener()
    }

    override fun onStylusAdded(deviceId: Int) {
        if (!hasStarted) return

        val device = inputManager.getInputDevice(deviceId) ?: return
        if (device.isExternal || !device.supportsSource(InputDevice.SOURCE_STYLUS)) return

        try {
            inputManager.addInputDeviceBatteryListener(deviceId, executor, this)
            internalStylusDeviceIds += deviceId
        } catch (e: SecurityException) {
            Log.e(TAG, "$e: Failed to register battery listener for $deviceId ${device.name}.")
        }
    }

    override fun onStylusRemoved(deviceId: Int) {
        if (!hasStarted) return

        if (!internalStylusDeviceIds.contains(deviceId)) return
        try {
            inputManager.removeInputDeviceBatteryListener(deviceId, this)
            internalStylusDeviceIds.remove(deviceId)
        } catch (e: SecurityException) {
            Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.")
        }
    }

    override fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {
        if (!hasStarted) return

        onRemoteDeviceFound()
    }

    override fun onBatteryStateChanged(
        deviceId: Int,
        eventTimeMillis: Long,
        batteryState: BatteryState
    ) {
        if (!hasStarted) return

        if (batteryState.isPresent) {
            onRemoteDeviceFound()
        }
    }

    private fun onRemoteDeviceFound() {
        inputManager.setStylusEverUsed(context, true)
        cleanupListeners()
    }

    private fun cleanupListeners() {
        stylusManager.unregisterCallback(this)
        handler.post {
            internalStylusDeviceIds.forEach {
                inputManager.removeInputDeviceBatteryListener(it, this)
            }
        }
    }

    private fun hostDeviceSupportsStylusInput(): Boolean {
        return inputManager.inputDeviceIds
            .asSequence()
            .mapNotNull { inputManager.getInputDevice(it) }
            .any { it.supportsSource(InputDevice.SOURCE_STYLUS) && !it.isExternal }
    }

    companion object {
        private val TAG = StylusFirstUsageListener::class.simpleName.orEmpty()
    }
}
+109 −9
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package com.android.systemui.stylus

import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.content.Context
import android.hardware.BatteryState
import android.hardware.input.InputManager
import android.os.Handler
import android.util.ArrayMap
@@ -25,6 +27,8 @@ import android.util.Log
import android.view.InputDevice
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executor
import javax.inject.Inject
@@ -37,26 +41,38 @@ import javax.inject.Inject
class StylusManager
@Inject
constructor(
    private val context: Context,
    private val inputManager: InputManager,
    private val bluetoothAdapter: BluetoothAdapter?,
    @Background private val handler: Handler,
    @Background private val executor: Executor,
) : InputManager.InputDeviceListener, BluetoothAdapter.OnMetadataChangedListener {
    private val featureFlags: FeatureFlags,
) :
    InputManager.InputDeviceListener,
    InputManager.InputDeviceBatteryListener,
    BluetoothAdapter.OnMetadataChangedListener {

    private val stylusCallbacks: CopyOnWriteArrayList<StylusCallback> = CopyOnWriteArrayList()
    private val stylusBatteryCallbacks: CopyOnWriteArrayList<StylusBatteryCallback> =
        CopyOnWriteArrayList()
    // This map should only be accessed on the handler
    private val inputDeviceAddressMap: MutableMap<Int, String?> = ArrayMap()
    // This variable should only be accessed on the handler
    private var hasStarted: Boolean = false

    /**
     * Starts listening to InputManager InputDevice events. Will also load the InputManager snapshot
     * at time of starting.
     */
    fun startListener() {
        handler.post {
            if (hasStarted) return@post
            hasStarted = true
            addExistingStylusToMap()

            inputManager.registerInputDeviceListener(this, handler)
        }
    }

    /** Registers a StylusCallback to listen to stylus events. */
    fun registerCallback(callback: StylusCallback) {
@@ -77,21 +93,30 @@ constructor(
    }

    override fun onInputDeviceAdded(deviceId: Int) {
        if (!hasStarted) return

        val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return
        if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return

        if (!device.isExternal) {
            registerBatteryListener(deviceId)
        }

        // TODO(b/257936830): get address once input api available
        val btAddress: String? = null
        inputDeviceAddressMap[deviceId] = btAddress
        executeStylusCallbacks { cb -> cb.onStylusAdded(deviceId) }

        if (btAddress != null) {
            onStylusUsed()
            onStylusBluetoothConnected(btAddress)
            executeStylusCallbacks { cb -> cb.onStylusBluetoothConnected(deviceId, btAddress) }
        }
    }

    override fun onInputDeviceChanged(deviceId: Int) {
        if (!hasStarted) return

        val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return
        if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return

@@ -112,7 +137,10 @@ constructor(
    }

    override fun onInputDeviceRemoved(deviceId: Int) {
        if (!hasStarted) return

        if (!inputDeviceAddressMap.contains(deviceId)) return
        unregisterBatteryListener(deviceId)

        val btAddress: String? = inputDeviceAddressMap[deviceId]
        inputDeviceAddressMap.remove(deviceId)
@@ -124,13 +152,14 @@ constructor(
    }

    override fun onMetadataChanged(device: BluetoothDevice, key: Int, value: ByteArray?) {
        handler.post executeMetadataChanged@{
            if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null)
                return@executeMetadataChanged
        handler.post {
            if (!hasStarted) return@post

            if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null) return@post

            val inputDeviceId: Int =
                inputDeviceAddressMap.filterValues { it == device.address }.keys.firstOrNull()
                    ?: return@executeMetadataChanged
                    ?: return@post

            val isCharging = String(value) == "true"

@@ -140,6 +169,24 @@ constructor(
        }
    }

    override fun onBatteryStateChanged(
        deviceId: Int,
        eventTimeMillis: Long,
        batteryState: BatteryState
    ) {
        handler.post {
            if (!hasStarted) return@post

            if (batteryState.isPresent) {
                onStylusUsed()
            }

            executeStylusBatteryCallbacks { cb ->
                cb.onStylusUsiBatteryStateChanged(deviceId, eventTimeMillis, batteryState)
            }
        }
    }

    private fun onStylusBluetoothConnected(btAddress: String) {
        val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return
        try {
@@ -158,6 +205,21 @@ constructor(
        }
    }

    /**
     * An InputDevice that supports [InputDevice.SOURCE_STYLUS] may still be present even when a
     * physical stylus device has never been used. This method is run when 1) a USI stylus battery
     * event happens, or 2) a bluetooth stylus is connected, as they are both indicators that a
     * physical stylus device has actually been used.
     */
    private fun onStylusUsed() {
        if (true) return // TODO(b/261826950): remove on main
        if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return
        if (inputManager.isStylusEverUsed(context)) return

        inputManager.setStylusEverUsed(context, true)
        executeStylusCallbacks { cb -> cb.onStylusFirstUsed() }
    }

    private fun executeStylusCallbacks(run: (cb: StylusCallback) -> Unit) {
        stylusCallbacks.forEach(run)
    }
@@ -166,31 +228,69 @@ constructor(
        stylusBatteryCallbacks.forEach(run)
    }

    private fun registerBatteryListener(deviceId: Int) {
        try {
            inputManager.addInputDeviceBatteryListener(deviceId, executor, this)
        } catch (e: SecurityException) {
            Log.e(TAG, "$e: Failed to register battery listener for $deviceId.")
        }
    }

    private fun unregisterBatteryListener(deviceId: Int) {
        // If deviceId wasn't registered, the result is a no-op, so an "is registered"
        // check is not needed.
        try {
            inputManager.removeInputDeviceBatteryListener(deviceId, this)
        } catch (e: SecurityException) {
            Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.")
        }
    }

    private fun addExistingStylusToMap() {
        for (deviceId: Int in inputManager.inputDeviceIds) {
            val device: InputDevice = inputManager.getInputDevice(deviceId) ?: continue
            if (device.supportsSource(InputDevice.SOURCE_STYLUS)) {
                // TODO(b/257936830): get address once input api available
                inputDeviceAddressMap[deviceId] = null

                if (!device.isExternal) { // TODO(b/263556967): add supportsUsi check once available
                    // For most devices, an active (non-bluetooth) stylus is represented by an
                    // internal InputDevice. This InputDevice will be present in InputManager
                    // before CoreStartables run, and will not be removed.
                    // In many cases, it reports the battery level of the stylus.
                    registerBatteryListener(deviceId)
                }
            }
        }
    }

    /** Callback interface to receive events from the StylusManager. */
    /**
     * Callback interface to receive events from the StylusManager. All callbacks are run on the
     * same background handler.
     */
    interface StylusCallback {
        fun onStylusAdded(deviceId: Int) {}
        fun onStylusRemoved(deviceId: Int) {}
        fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {}
        fun onStylusBluetoothDisconnected(deviceId: Int, btAddress: String) {}
        fun onStylusFirstUsed() {}
    }

    /** Callback interface to receive stylus battery events from the StylusManager. */
    /**
     * Callback interface to receive stylus battery events from the StylusManager. All callbacks are
     * runs on the same background handler.
     */
    interface StylusBatteryCallback {
        fun onStylusBluetoothChargingStateChanged(
            inputDeviceId: Int,
            btDevice: BluetoothDevice,
            isCharging: Boolean
        ) {}
        fun onStylusUsiBatteryStateChanged(
            deviceId: Int,
            eventTimeMillis: Long,
            batteryState: BatteryState,
        ) {}
    }

    companion object {
+6 −46
Original line number Diff line number Diff line
@@ -18,14 +18,11 @@ package com.android.systemui.stylus

import android.hardware.BatteryState
import android.hardware.input.InputManager
import android.util.Log
import android.view.InputDevice
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import java.util.concurrent.Executor
import javax.inject.Inject

/**
@@ -40,16 +37,7 @@ constructor(
    private val inputManager: InputManager,
    private val stylusUsiPowerUi: StylusUsiPowerUI,
    private val featureFlags: FeatureFlags,
    @Background private val executor: Executor,
) : CoreStartable, StylusManager.StylusCallback, InputManager.InputDeviceBatteryListener {

    override fun onStylusAdded(deviceId: Int) {
        val device = inputManager.getInputDevice(deviceId) ?: return

        if (!device.isExternal) {
            registerBatteryListener(deviceId)
        }
    }
) : CoreStartable, StylusManager.StylusCallback, StylusManager.StylusBatteryCallback {

    override fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {
        stylusUsiPowerUi.refresh()
@@ -59,15 +47,7 @@ constructor(
        stylusUsiPowerUi.refresh()
    }

    override fun onStylusRemoved(deviceId: Int) {
        val device = inputManager.getInputDevice(deviceId) ?: return

        if (!device.isExternal) {
            unregisterBatteryListener(deviceId)
        }
    }

    override fun onBatteryStateChanged(
    override fun onStylusUsiBatteryStateChanged(
        deviceId: Int,
        eventTimeMillis: Long,
        batteryState: BatteryState
@@ -77,39 +57,19 @@ constructor(
        }
    }

    private fun registerBatteryListener(deviceId: Int) {
        try {
            inputManager.addInputDeviceBatteryListener(deviceId, executor, this)
        } catch (e: SecurityException) {
            Log.e(TAG, "$e: Failed to register battery listener for $deviceId.")
        }
    }

    private fun unregisterBatteryListener(deviceId: Int) {
        try {
            inputManager.removeInputDeviceBatteryListener(deviceId, this)
        } catch (e: SecurityException) {
            Log.e(TAG, "$e: Failed to unregister battery listener for $deviceId.")
        }
    }

    override fun start() {
        if (!featureFlags.isEnabled(Flags.ENABLE_USI_BATTERY_NOTIFICATIONS)) return
        addBatteryListenerForInternalStyluses()
        if (!hostDeviceSupportsStylusInput()) return

        stylusManager.registerCallback(this)
        stylusManager.startListener()
    }

    private fun addBatteryListenerForInternalStyluses() {
        // For most devices, an active stylus is represented by an internal InputDevice.
        // This InputDevice will be present in InputManager before CoreStartables run,
        // and will not be removed. In many cases, it reports the battery level of the stylus.
        inputManager.inputDeviceIds
    private fun hostDeviceSupportsStylusInput(): Boolean {
        return inputManager.inputDeviceIds
            .asSequence()
            .mapNotNull { inputManager.getInputDevice(it) }
            .filter { it.supportsSource(InputDevice.SOURCE_STYLUS) }
            .forEach { onStylusAdded(it.id) }
            .any { it.supportsSource(InputDevice.SOURCE_STYLUS) && !it.isExternal }
    }

    companion object {
+0 −289

File deleted.

Preview size limit exceeded, changes collapsed.

+177 −13

File changed.

Preview size limit exceeded, changes collapsed.

Loading