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

Commit 21caf040 authored by helen cheuk's avatar helen cheuk
Browse files

[Action Corner] Only subscribe to cursor position when there is pointer

device connected

Subscribing to cursor position would trigger adding input monitor for
the display. It should only be done when there is any pointer device
(e.g. mouse/touchpad) is connected to avoid unnecessary overhead.

Bug: 411091884
Test: PointerDeviceRepositoryTest
Test: ActionCornerRepositoryTest
Flag: com.android.systemui.shared.cursor_hot_corner
Change-Id: Ia98612e919555338e7f36fff676a78eb17bcc8b4
parent f045ec4d
Loading
Loading
Loading
Loading
+24 −8
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ import com.android.systemui.cursorposition.data.model.CursorPosition
import com.android.systemui.cursorposition.domain.data.repository.multiDisplayCursorPositionRepository
import com.android.systemui.display.data.repository.fakeDisplayWindowPropertiesRepository
import com.android.systemui.display.shared.model.DisplayWindowProperties
import com.android.systemui.inputdevice.data.repository.FakePointerDeviceRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.backgroundScope
@@ -59,10 +60,12 @@ import org.mockito.kotlin.whenever
class ActionCornerRepositoryTest : SysuiTestCase() {
    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val Kosmos.fakePointerRepository by Fixture { FakePointerDeviceRepository() }
    private val Kosmos.underTest by Fixture {
        ActionCornerRepositoryImpl(
            cursorPositionRepository,
            kosmos.fakeDisplayWindowPropertiesRepository,
            kosmos.fakePointerRepository,
            kosmos.backgroundScope,
        )
    }
@@ -75,12 +78,13 @@ class ActionCornerRepositoryTest : SysuiTestCase() {
    fun setup() {
        whenever(windowManager.currentWindowMetrics).thenReturn(metrics)
        displayRepository.insert(createDisplayWindowProperties())
        kosmos.fakePointerRepository.setIsAnyPointerConnected(true)
    }

    @Test
    fun topLeftCursor_topLeftActionCornerEmitted() =
        kosmos.runTest {
            val model by kosmos.collectLastValue(underTest.actionCornerState)
            val model by collectLastValue(underTest.actionCornerState)
            cursorPositionRepository.addCursorPosition(display.topLeftCursorPos)
            assertThat(model)
                .isEqualTo(
@@ -94,7 +98,7 @@ class ActionCornerRepositoryTest : SysuiTestCase() {
    @Test
    fun outOfBoundTopLeftCursor_noActionCornerEmitted() =
        kosmos.runTest {
            val model by kosmos.collectLastValue(underTest.actionCornerState)
            val model by collectLastValue(underTest.actionCornerState)
            val actionCornerPos = display.topLeftCursorPos
            // Update x and y to make it just out of bound of action corner
            cursorPositionRepository.addCursorPosition(
@@ -110,7 +114,7 @@ class ActionCornerRepositoryTest : SysuiTestCase() {
    @Test
    fun topRightCursor_topRightActionCornerEmitted() =
        kosmos.runTest {
            val model by kosmos.collectLastValue(underTest.actionCornerState)
            val model by collectLastValue(underTest.actionCornerState)
            val actionCornerPos = display.topRightCursorPos
            cursorPositionRepository.addCursorPosition(actionCornerPos)
            assertThat(model)
@@ -122,7 +126,7 @@ class ActionCornerRepositoryTest : SysuiTestCase() {
    @Test
    fun outOfBoundTopRightCursor_noActionCornerEmitted() =
        kosmos.runTest {
            val model by kosmos.collectLastValue(underTest.actionCornerState)
            val model by collectLastValue(underTest.actionCornerState)
            val actionCornerPos = display.topRightCursorPos
            cursorPositionRepository.addCursorPosition(
                CursorPosition(
@@ -137,7 +141,7 @@ class ActionCornerRepositoryTest : SysuiTestCase() {
    @Test
    fun bottomLeftCursor_bottomLeftActionCornerEmitted() =
        kosmos.runTest {
            val model by kosmos.collectLastValue(underTest.actionCornerState)
            val model by collectLastValue(underTest.actionCornerState)
            val actionCornerPos = display.bottomLeftCursorPos
            cursorPositionRepository.addCursorPosition(actionCornerPos)
            assertThat(model)
@@ -149,7 +153,7 @@ class ActionCornerRepositoryTest : SysuiTestCase() {
    @Test
    fun outOfBoundBottomLeftCursor_noActionCornerEmitted() =
        kosmos.runTest {
            val model by kosmos.collectLastValue(underTest.actionCornerState)
            val model by collectLastValue(underTest.actionCornerState)
            val actionCornerPos = display.bottomLeftCursorPos
            cursorPositionRepository.addCursorPosition(
                CursorPosition(
@@ -164,7 +168,7 @@ class ActionCornerRepositoryTest : SysuiTestCase() {
    @Test
    fun bottomRightCursor_bottomRightActionCornerEmitted() =
        kosmos.runTest {
            val model by kosmos.collectLastValue(underTest.actionCornerState)
            val model by collectLastValue(underTest.actionCornerState)
            val actionCornerPos = display.bottomRightCursorPos
            cursorPositionRepository.addCursorPosition(actionCornerPos)
            assertThat(model)
@@ -176,7 +180,7 @@ class ActionCornerRepositoryTest : SysuiTestCase() {
    @Test
    fun outOfBoundBottomRightCursor_noActionCornerEmitted() =
        kosmos.runTest {
            val model by kosmos.collectLastValue(underTest.actionCornerState)
            val model by collectLastValue(underTest.actionCornerState)
            val actionCornerPos = display.bottomRightCursorPos
            cursorPositionRepository.addCursorPosition(
                CursorPosition(
@@ -233,6 +237,18 @@ class ActionCornerRepositoryTest : SysuiTestCase() {
            assertThat(models.size).isEqualTo(1)
        }

    @Test
    fun activeActionCorner_pointerDeviceDisconnected_inactiveActionCorner() =
        kosmos.runTest {
            val actionCornerPos = display.bottomRightCursorPos
            cursorPositionRepository.addCursorPosition(actionCornerPos)

            fakePointerRepository.setIsAnyPointerConnected(false)

            val model by collectLastValue(underTest.actionCornerState)
            assertThat(model).isEqualTo(InactiveActionCorner)
        }

    private fun createDisplayWindowProperties() =
        DisplayWindowProperties(
            DEFAULT_DISPLAY,
+139 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.inputdevice.data.repository

import android.hardware.input.InputManager.InputDeviceListener
import android.hardware.input.fakeInputManager
import android.os.fakeHandler
import android.testing.TestableLooper
import android.view.InputDevice
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.eq
import org.mockito.Captor
import org.mockito.Mockito
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.whenever
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters

@SmallTest
@TestableLooper.RunWithLooper
@RunWith(ParameterizedAndroidJunit4::class)
class PointerDeviceRepositoryTest(private val pointer: Int) : SysuiTestCase() {

    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()

    @Captor private lateinit var deviceListenerCaptor: ArgumentCaptor<InputDeviceListener>
    private val kosmos = testKosmos()
    private val fakeInputManager = kosmos.fakeInputManager

    private val dispatcher: CoroutineDispatcher = kosmos.testDispatcher
    private val testScope = kosmos.testScope
    private val inputDeviceRepo =
        InputDeviceRepository(
            kosmos.fakeHandler,
            testScope.backgroundScope,
            fakeInputManager.inputManager,
        )
    private val underTest =
        PointerDeviceRepositoryImpl(dispatcher, fakeInputManager.inputManager, inputDeviceRepo)

    @Test
    fun emitsDisconnected_ifNothingIsConnected() =
        kosmos.runTest {
            val initialState = underTest.isAnyPointerDeviceConnected.first()
            assertThat(initialState).isFalse()
        }

    @Test
    fun emitsConnected_ifPointerAlreadyConnectedAtTheStart() =
        kosmos.runTest {
            fakeInputManager.addDevice(POINTER_ID, pointer)
            val initialValue = underTest.isAnyPointerDeviceConnected.first()
            assertThat(initialValue).isTrue()
        }

    @Test
    fun emitsConnected_whenNewPointerConnects() =
        kosmos.runTest {
            captureDeviceListener()
            val isPointerConnected by collectLastValue(underTest.isAnyPointerDeviceConnected)

            fakeInputManager.addDevice(POINTER_ID, pointer)

            assertThat(isPointerConnected).isTrue()
        }

    @Test
    fun emitsDisconnected_whenDeviceWithIdDoesNotExist() =
        testScope.runTest {
            captureDeviceListener()
            val isPointerConnected by collectLastValue(underTest.isAnyPointerDeviceConnected)
            whenever(fakeInputManager.inputManager.getInputDevice(eq(NULL_DEVICE_ID)))
                .thenReturn(null)
            fakeInputManager.addDevice(NULL_DEVICE_ID, InputDevice.SOURCE_UNKNOWN)
            assertThat(isPointerConnected).isFalse()
        }

    @Test
    fun emitsDisconnected_whenPointerDisconnects() =
        testScope.runTest {
            captureDeviceListener()
            val isPointerConnected by collectLastValue(underTest.isAnyPointerDeviceConnected)

            fakeInputManager.addDevice(POINTER_ID, pointer)
            assertThat(isPointerConnected).isTrue()

            fakeInputManager.removeDevice(POINTER_ID)
            assertThat(isPointerConnected).isFalse()
        }

    private suspend fun captureDeviceListener() {
        underTest.isAnyPointerDeviceConnected.first()
        Mockito.verify(fakeInputManager.inputManager)
            .registerInputDeviceListener(deviceListenerCaptor.capture(), anyOrNull())
        fakeInputManager.registerInputDeviceListener(deviceListenerCaptor.value)
    }

    private companion object {
        private const val POINTER_ID = 1
        private const val NULL_DEVICE_ID = 4

        @JvmStatic
        @Parameters(name = "{0}")
        fun getParams(): List<Int> {
            return listOf(InputDevice.SOURCE_TOUCHPAD, InputDevice.SOURCE_MOUSE)
        }
    }
}
+14 −5
Original line number Diff line number Diff line
@@ -29,11 +29,13 @@ import com.android.systemui.cursorposition.data.model.CursorPosition
import com.android.systemui.cursorposition.data.repository.MultiDisplayCursorPositionRepository
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository
import com.android.systemui.inputdevice.data.repository.PointerDeviceRepository
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

@@ -53,14 +55,21 @@ class ActionCornerRepositoryImpl
constructor(
    cursorRepository: MultiDisplayCursorPositionRepository,
    private val displayWindowPropertiesRepository: DisplayWindowPropertiesRepository,
    pointerDeviceRepository: PointerDeviceRepository,
    @Background private val backgroundScope: CoroutineScope,
) : ActionCornerRepository {

    override val actionCornerState: StateFlow<ActionCornerState> =
        cursorRepository.cursorPositions
            .map(::mapToActionCornerState)
            // Avoid emitting duplicate values when cursor moves within the same corner
            .distinctUntilChanged()
        pointerDeviceRepository.isAnyPointerDeviceConnected
            .flatMapLatest { isConnected ->
                if (isConnected) {
                    cursorRepository.cursorPositions.map(::mapToActionCornerState)
                } else {
                    // When not connected, emit an InactiveActionCorner state and then complete this
                    // inner flow.
                    flowOf(InactiveActionCorner)
                }
            }
            .stateIn(backgroundScope, SharingStarted.WhileSubscribed(), InactiveActionCorner)

    private fun mapToActionCornerState(cursorPos: CursorPosition?): ActionCornerState {
+3 −1
Original line number Diff line number Diff line
@@ -75,6 +75,7 @@ import com.android.systemui.flags.FlagDependenciesModule;
import com.android.systemui.flags.FlagsModule;
import com.android.systemui.growth.dagger.GrowthModule;
import com.android.systemui.haptics.msdl.dagger.MSDLModule;
import com.android.systemui.inputdevice.InputDeviceModule;
import com.android.systemui.inputmethod.InputMethodModule;
import com.android.systemui.keyboard.KeyboardModule;
import com.android.systemui.keyevent.data.repository.KeyEventRepositoryModule;
@@ -302,7 +303,8 @@ import javax.inject.Named;
        WalletModule.class,
        LowLightModule.class,
        LowLightClockModule.class,
        PerDisplayRepositoriesModule.class
        PerDisplayRepositoriesModule.class,
        InputDeviceModule.class,
},
        subcomponents = {
                ComplicationComponent.class,
+33 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.inputdevice

import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.inputdevice.data.repository.PointerDeviceRepository
import com.android.systemui.inputdevice.data.repository.PointerDeviceRepositoryImpl
import dagger.Binds
import dagger.Module

@Module
abstract class InputDeviceModule {

    @Binds
    @SysUISingleton
    abstract fun bindPointerDeviceRepository(
        repository: PointerDeviceRepositoryImpl
    ): PointerDeviceRepository
}
Loading