Loading packages/SystemUI/multivalentTests/src/com/android/systemui/cursorposition/data/repository/MultiDisplayCursorPositionRepositoryTest.kt 0 → 100644 +195 −0 Original line number Original line 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.cursorposition.data.repository import android.os.Handler import android.testing.TestableLooper import android.testing.TestableLooper.RunWithLooper import android.view.Display.DEFAULT_DISPLAY import android.view.Display.TYPE_EXTERNAL import android.view.Display.TYPE_INTERNAL import android.view.InputDevice.SOURCE_MOUSE import android.view.InputDevice.SOURCE_TOUCHPAD import android.view.MotionEvent import androidx.test.filters.SmallTest import com.android.app.displaylib.PerDisplayInstanceRepositoryImpl import com.android.systemui.SysuiTestCase import com.android.systemui.cursorposition.data.model.CursorPosition import com.android.systemui.cursorposition.data.repository.SingleDisplayCursorPositionRepositoryImpl.Companion.defaultInputEventListenerBuilder import com.android.systemui.cursorposition.domain.data.repository.TestCursorPositionRepositoryInstanceProvider import com.android.systemui.display.data.repository.display import com.android.systemui.display.data.repository.displayRepository import com.android.systemui.display.data.repository.fakeDisplayInstanceLifecycleManager import com.android.systemui.display.data.repository.perDisplayDumpHelper import com.android.systemui.kosmos.backgroundScope import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.shared.system.InputChannelCompat import com.android.systemui.shared.system.InputMonitorCompat import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.launch import org.junit.Before import org.junit.Rule import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.mockito.kotlin.any import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters @SmallTest @RunWith(ParameterizedAndroidJunit4::class) @RunWithLooper @kotlinx.coroutines.ExperimentalCoroutinesApi class MultiDisplayCursorPositionRepositoryTest(private val cursorEventSource: Int) : SysuiTestCase() { @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() private lateinit var underTest: MultiDisplayCursorPositionRepository private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val displayRepository = kosmos.displayRepository private val displayLifecycleManager = kosmos.fakeDisplayInstanceLifecycleManager private lateinit var listener: InputChannelCompat.InputEventListener private var emittedCursorPosition: CursorPosition? = null @Mock private lateinit var inputMonitor: InputMonitorCompat @Mock private lateinit var inputReceiver: InputChannelCompat.InputEventReceiver private val x = 100f private val y = 200f private lateinit var testableLooper: TestableLooper @Before fun setup() { testableLooper = TestableLooper.get(this) val testHandler = Handler(testableLooper.looper) whenever(inputMonitor.getInputReceiver(any(), any(), any())).thenReturn(inputReceiver) displayLifecycleManager.displayIds.value = setOf(DEFAULT_DISPLAY, DISPLAY_2) val cursorPerDisplayRepository = PerDisplayInstanceRepositoryImpl( debugName = "testCursorPositionPerDisplayInstanceRepository", instanceProvider = TestCursorPositionRepositoryInstanceProvider( testHandler, { channel -> listener = defaultInputEventListenerBuilder.build(channel) listener }, ) { _: String, _: Int -> inputMonitor }, displayLifecycleManager, kosmos.backgroundScope, displayRepository, kosmos.perDisplayDumpHelper, ) underTest = MultiDisplayCursorPositionRepositoryImpl( displayRepository, backgroundScope = kosmos.backgroundScope, cursorPerDisplayRepository, ) } @Test fun getCursorPositionFromDefaultDisplay() = setUpAndRunTest { val event = getMotionEvent(x, y, 0) listener.onInputEvent(event) assertThat(emittedCursorPosition).isEqualTo(CursorPosition(x, y, 0)) } @Test fun getCursorPositionFromAdditionDisplay() = setUpAndRunTest { addDisplay(id = DISPLAY_2, type = TYPE_EXTERNAL) val event = getMotionEvent(x, y, DISPLAY_2) listener.onInputEvent(event) assertThat(emittedCursorPosition).isEqualTo(CursorPosition(x, y, DISPLAY_2)) } @Test fun noCursorPositionFromRemovedDisplay() = setUpAndRunTest { addDisplay(id = DISPLAY_2, type = TYPE_EXTERNAL) removeDisplay(DISPLAY_2) val event = getMotionEvent(x, y, DISPLAY_2) listener.onInputEvent(event) assertThat(emittedCursorPosition).isEqualTo(null) } @Test fun disposeInputMonitorAndInputReceiver() = setUpAndRunTest { addDisplay(DISPLAY_2, TYPE_EXTERNAL) removeDisplay(DISPLAY_2) verify(inputMonitor).dispose() verify(inputReceiver).dispose() } private fun setUpAndRunTest(block: suspend () -> Unit) = kosmos.runTest { // Add default display before creating cursor repository displayRepository.addDisplays(display(id = DEFAULT_DISPLAY, type = TYPE_INTERNAL)) backgroundScope.launch { underTest.cursorPositions.collect { emittedCursorPosition = it } } // Run all tasks received by TestHandler to create input monitors testableLooper.processAllMessages() block() } private suspend fun addDisplay(id: Int, type: Int) { displayRepository.addDisplays(display(id = id, type = type)) testableLooper.processAllMessages() } private suspend fun removeDisplay(id: Int) { displayRepository.removeDisplay(id) testableLooper.processAllMessages() } private fun getMotionEvent(x: Float, y: Float, displayId: Int): MotionEvent { val event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, x, y, 0) event.source = cursorEventSource event.displayId = displayId return event } private companion object { const val DISPLAY_2 = DEFAULT_DISPLAY + 1 @JvmStatic @Parameters(name = "source = {0}") fun data(): List<Int> { return listOf(SOURCE_MOUSE, SOURCE_TOUCHPAD) } } } packages/SystemUI/src/com/android/systemui/cursorposition/data/model/CursorPosition.kt 0 → 100644 +30 −0 Original line number Original line 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.cursorposition.data.model /** * Represents the position of cursor hotspot on the screen. Hotspot is the specific pixel that * signifies the location of the pointer's interaction with the user interface. By default, hotspot * of a cursor is the tip of arrow. * * @property x The x-coordinate of the cursor hotspot, relative to the top-left corner of the * screen. * @property y The y-coordinate of the cursor hotspot, relative to the top-left corner of the * screen. * @property displayId The display on which the cursor is located. */ data class CursorPosition(val x: Float, val y: Float, val displayId: Int) packages/SystemUI/src/com/android/systemui/cursorposition/data/repository/MultiDisplayCursorPositionRepository.kt 0 → 100644 +71 −0 Original line number Original line 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.cursorposition.data.repository import com.android.app.displaylib.PerDisplayRepository import com.android.systemui.cursorposition.data.model.CursorPosition import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DisplayRepository import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn /** Repository for cursor position of multi displays. */ interface MultiDisplayCursorPositionRepository { val cursorPositions: Flow<CursorPosition?> } /** * Implementation of [MultiDisplayCursorPositionRepository] that aggregates cursor position updates * from multiple displays. * * This class uses a [DisplayRepository] to track added displays and a [PerDisplayRepository] to * manage [SingleDisplayCursorPositionRepository] instances for each display. [PerDisplayRepository] * would destroy the instance if the display is removed. This class combines the cursor position * from all displays into a single cursorPositions StateFlow. */ @SysUISingleton class MultiDisplayCursorPositionRepositoryImpl @Inject constructor( private val displayRepository: DisplayRepository, @Background private val backgroundScope: CoroutineScope, private val cursorRepositories: PerDisplayRepository<SingleDisplayCursorPositionRepository>, ) : MultiDisplayCursorPositionRepository { private val allDisplaysCursorPositions: Flow<CursorPosition> = displayRepository.displayAdditionEvent .mapNotNull { c -> c?.displayId } .onStart { emitAll(displayRepository.displayIds.value.asFlow()) } .flatMapMerge { val repo = cursorRepositories[it] repo?.cursorPositions ?: emptyFlow() } override val cursorPositions: StateFlow<CursorPosition?> = allDisplaysCursorPositions.stateIn(backgroundScope, SharingStarted.WhileSubscribed(), null) } packages/SystemUI/src/com/android/systemui/cursorposition/data/repository/SingleDisplayCursorPositionRepository.kt 0 → 100644 +154 −0 Original line number Original line 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.cursorposition.data.repository import android.os.Handler import android.os.Looper import android.view.Choreographer import android.view.InputDevice.SOURCE_MOUSE import android.view.InputDevice.SOURCE_TOUCHPAD import android.view.MotionEvent import com.android.app.displaylib.PerDisplayInstanceProviderWithTeardown import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.cursorposition.data.model.CursorPosition import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.shared.system.InputChannelCompat import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver import com.android.systemui.shared.system.InputMonitorCompat import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import javax.inject.Inject import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn /** Repository for cursor position in single display. */ interface SingleDisplayCursorPositionRepository { /** Flow of [CursorPosition] for the display. */ val cursorPositions: Flow<CursorPosition> /** Destroys the repository. */ fun destroy() } /** * Implementation of [SingleDisplayCursorPositionRepository]. * * @param displayId the display id * @param backgroundHandler the background handler * @param listenerBuilder the builder for [InputChannelCompat.InputEventListener] * @param inputMonitorBuilder the builder for [InputMonitorCompat] */ class SingleDisplayCursorPositionRepositoryImpl @AssistedInject constructor( @Assisted displayId: Int, @Background private val backgroundHandler: Handler, @Assisted private val listenerBuilder: InputEventListenerBuilder = defaultInputEventListenerBuilder, @Assisted private val inputMonitorBuilder: InputMonitorBuilder = defaultInputMonitorBuilder, ) : SingleDisplayCursorPositionRepository { private var scope: ProducerScope<CursorPosition>? = null private fun createInputMonitorCallbackFlow(displayId: Int): Flow<CursorPosition> = conflatedCallbackFlow { val inputMonitor: InputMonitorCompat = inputMonitorBuilder.build(TAG, displayId) val inputReceiver: InputEventReceiver = inputMonitor.getInputReceiver( Looper.myLooper(), Choreographer.getInstance(), listenerBuilder.build(this), ) scope = this awaitClose { inputMonitor.dispose() inputReceiver.dispose() } } // Use backgroundHandler as dispatcher because it has a looper (unlike // "backgroundDispatcher" which does not have a looper) and input receiver could use // its background looper and choreographer .flowOn(backgroundHandler.asCoroutineDispatcher()) override val cursorPositions: Flow<CursorPosition> = createInputMonitorCallbackFlow(displayId) override fun destroy() { scope?.close() } @AssistedFactory interface Factory { /** * Creates a new instance of [SingleDisplayCursorPositionRepositoryImpl] for a given * [displayId]. */ fun create( displayId: Int, listenerBuilder: InputEventListenerBuilder = defaultInputEventListenerBuilder, inputMonitorBuilder: InputMonitorBuilder = defaultInputMonitorBuilder, ): SingleDisplayCursorPositionRepositoryImpl } companion object { private const val TAG = "CursorPositionPerDisplayRepositoryImpl" private val defaultInputMonitorBuilder = InputMonitorBuilder { name, displayId -> InputMonitorCompat(name, displayId) } val defaultInputEventListenerBuilder = InputEventListenerBuilder { channel -> InputChannelCompat.InputEventListener { event -> if ( event is MotionEvent && (event.source == SOURCE_MOUSE || event.source == SOURCE_TOUCHPAD) ) { val cursorEvent = CursorPosition(event.x, event.y, event.displayId) channel.trySendWithFailureLogging(cursorEvent, TAG) } } } } } fun interface InputEventListenerBuilder { fun build(channel: SendChannel<CursorPosition>): InputChannelCompat.InputEventListener } fun interface InputMonitorBuilder { fun build(name: String, displayId: Int): InputMonitorCompat } @SysUISingleton class SingleDisplayCursorPositionRepositoryFactory @Inject constructor(private val factory: SingleDisplayCursorPositionRepositoryImpl.Factory) : PerDisplayInstanceProviderWithTeardown<SingleDisplayCursorPositionRepository> { override fun createInstance(displayId: Int): SingleDisplayCursorPositionRepository { return factory.create(displayId) } override fun destroyInstance(instance: SingleDisplayCursorPositionRepository) { instance.destroy() } } packages/SystemUI/tests/utils/src/com/android/systemui/cursorposition/domain/data/repository/TestCursorPositionRepositoryInstanceProvider.kt 0 → 100644 +44 −0 Original line number Original line 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.cursorposition.domain.data.repository import android.os.Handler import com.android.app.displaylib.PerDisplayInstanceProviderWithTeardown import com.android.systemui.cursorposition.data.repository.InputEventListenerBuilder import com.android.systemui.cursorposition.data.repository.InputMonitorBuilder import com.android.systemui.cursorposition.data.repository.SingleDisplayCursorPositionRepository import com.android.systemui.cursorposition.data.repository.SingleDisplayCursorPositionRepositoryImpl class TestCursorPositionRepositoryInstanceProvider( private val handler: Handler, private val listenerBuilder: InputEventListenerBuilder, private val inputMonitorBuilder: InputMonitorBuilder, ) : PerDisplayInstanceProviderWithTeardown<SingleDisplayCursorPositionRepository> { override fun destroyInstance(instance: SingleDisplayCursorPositionRepository) { instance.destroy() } override fun createInstance(displayId: Int): SingleDisplayCursorPositionRepository { return SingleDisplayCursorPositionRepositoryImpl( displayId, handler, listenerBuilder, inputMonitorBuilder, ) } } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/cursorposition/data/repository/MultiDisplayCursorPositionRepositoryTest.kt 0 → 100644 +195 −0 Original line number Original line 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.cursorposition.data.repository import android.os.Handler import android.testing.TestableLooper import android.testing.TestableLooper.RunWithLooper import android.view.Display.DEFAULT_DISPLAY import android.view.Display.TYPE_EXTERNAL import android.view.Display.TYPE_INTERNAL import android.view.InputDevice.SOURCE_MOUSE import android.view.InputDevice.SOURCE_TOUCHPAD import android.view.MotionEvent import androidx.test.filters.SmallTest import com.android.app.displaylib.PerDisplayInstanceRepositoryImpl import com.android.systemui.SysuiTestCase import com.android.systemui.cursorposition.data.model.CursorPosition import com.android.systemui.cursorposition.data.repository.SingleDisplayCursorPositionRepositoryImpl.Companion.defaultInputEventListenerBuilder import com.android.systemui.cursorposition.domain.data.repository.TestCursorPositionRepositoryInstanceProvider import com.android.systemui.display.data.repository.display import com.android.systemui.display.data.repository.displayRepository import com.android.systemui.display.data.repository.fakeDisplayInstanceLifecycleManager import com.android.systemui.display.data.repository.perDisplayDumpHelper import com.android.systemui.kosmos.backgroundScope import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.shared.system.InputChannelCompat import com.android.systemui.shared.system.InputMonitorCompat import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.launch import org.junit.Before import org.junit.Rule import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.mockito.kotlin.any import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters @SmallTest @RunWith(ParameterizedAndroidJunit4::class) @RunWithLooper @kotlinx.coroutines.ExperimentalCoroutinesApi class MultiDisplayCursorPositionRepositoryTest(private val cursorEventSource: Int) : SysuiTestCase() { @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() private lateinit var underTest: MultiDisplayCursorPositionRepository private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val displayRepository = kosmos.displayRepository private val displayLifecycleManager = kosmos.fakeDisplayInstanceLifecycleManager private lateinit var listener: InputChannelCompat.InputEventListener private var emittedCursorPosition: CursorPosition? = null @Mock private lateinit var inputMonitor: InputMonitorCompat @Mock private lateinit var inputReceiver: InputChannelCompat.InputEventReceiver private val x = 100f private val y = 200f private lateinit var testableLooper: TestableLooper @Before fun setup() { testableLooper = TestableLooper.get(this) val testHandler = Handler(testableLooper.looper) whenever(inputMonitor.getInputReceiver(any(), any(), any())).thenReturn(inputReceiver) displayLifecycleManager.displayIds.value = setOf(DEFAULT_DISPLAY, DISPLAY_2) val cursorPerDisplayRepository = PerDisplayInstanceRepositoryImpl( debugName = "testCursorPositionPerDisplayInstanceRepository", instanceProvider = TestCursorPositionRepositoryInstanceProvider( testHandler, { channel -> listener = defaultInputEventListenerBuilder.build(channel) listener }, ) { _: String, _: Int -> inputMonitor }, displayLifecycleManager, kosmos.backgroundScope, displayRepository, kosmos.perDisplayDumpHelper, ) underTest = MultiDisplayCursorPositionRepositoryImpl( displayRepository, backgroundScope = kosmos.backgroundScope, cursorPerDisplayRepository, ) } @Test fun getCursorPositionFromDefaultDisplay() = setUpAndRunTest { val event = getMotionEvent(x, y, 0) listener.onInputEvent(event) assertThat(emittedCursorPosition).isEqualTo(CursorPosition(x, y, 0)) } @Test fun getCursorPositionFromAdditionDisplay() = setUpAndRunTest { addDisplay(id = DISPLAY_2, type = TYPE_EXTERNAL) val event = getMotionEvent(x, y, DISPLAY_2) listener.onInputEvent(event) assertThat(emittedCursorPosition).isEqualTo(CursorPosition(x, y, DISPLAY_2)) } @Test fun noCursorPositionFromRemovedDisplay() = setUpAndRunTest { addDisplay(id = DISPLAY_2, type = TYPE_EXTERNAL) removeDisplay(DISPLAY_2) val event = getMotionEvent(x, y, DISPLAY_2) listener.onInputEvent(event) assertThat(emittedCursorPosition).isEqualTo(null) } @Test fun disposeInputMonitorAndInputReceiver() = setUpAndRunTest { addDisplay(DISPLAY_2, TYPE_EXTERNAL) removeDisplay(DISPLAY_2) verify(inputMonitor).dispose() verify(inputReceiver).dispose() } private fun setUpAndRunTest(block: suspend () -> Unit) = kosmos.runTest { // Add default display before creating cursor repository displayRepository.addDisplays(display(id = DEFAULT_DISPLAY, type = TYPE_INTERNAL)) backgroundScope.launch { underTest.cursorPositions.collect { emittedCursorPosition = it } } // Run all tasks received by TestHandler to create input monitors testableLooper.processAllMessages() block() } private suspend fun addDisplay(id: Int, type: Int) { displayRepository.addDisplays(display(id = id, type = type)) testableLooper.processAllMessages() } private suspend fun removeDisplay(id: Int) { displayRepository.removeDisplay(id) testableLooper.processAllMessages() } private fun getMotionEvent(x: Float, y: Float, displayId: Int): MotionEvent { val event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, x, y, 0) event.source = cursorEventSource event.displayId = displayId return event } private companion object { const val DISPLAY_2 = DEFAULT_DISPLAY + 1 @JvmStatic @Parameters(name = "source = {0}") fun data(): List<Int> { return listOf(SOURCE_MOUSE, SOURCE_TOUCHPAD) } } }
packages/SystemUI/src/com/android/systemui/cursorposition/data/model/CursorPosition.kt 0 → 100644 +30 −0 Original line number Original line 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.cursorposition.data.model /** * Represents the position of cursor hotspot on the screen. Hotspot is the specific pixel that * signifies the location of the pointer's interaction with the user interface. By default, hotspot * of a cursor is the tip of arrow. * * @property x The x-coordinate of the cursor hotspot, relative to the top-left corner of the * screen. * @property y The y-coordinate of the cursor hotspot, relative to the top-left corner of the * screen. * @property displayId The display on which the cursor is located. */ data class CursorPosition(val x: Float, val y: Float, val displayId: Int)
packages/SystemUI/src/com/android/systemui/cursorposition/data/repository/MultiDisplayCursorPositionRepository.kt 0 → 100644 +71 −0 Original line number Original line 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.cursorposition.data.repository import com.android.app.displaylib.PerDisplayRepository import com.android.systemui.cursorposition.data.model.CursorPosition import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DisplayRepository import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn /** Repository for cursor position of multi displays. */ interface MultiDisplayCursorPositionRepository { val cursorPositions: Flow<CursorPosition?> } /** * Implementation of [MultiDisplayCursorPositionRepository] that aggregates cursor position updates * from multiple displays. * * This class uses a [DisplayRepository] to track added displays and a [PerDisplayRepository] to * manage [SingleDisplayCursorPositionRepository] instances for each display. [PerDisplayRepository] * would destroy the instance if the display is removed. This class combines the cursor position * from all displays into a single cursorPositions StateFlow. */ @SysUISingleton class MultiDisplayCursorPositionRepositoryImpl @Inject constructor( private val displayRepository: DisplayRepository, @Background private val backgroundScope: CoroutineScope, private val cursorRepositories: PerDisplayRepository<SingleDisplayCursorPositionRepository>, ) : MultiDisplayCursorPositionRepository { private val allDisplaysCursorPositions: Flow<CursorPosition> = displayRepository.displayAdditionEvent .mapNotNull { c -> c?.displayId } .onStart { emitAll(displayRepository.displayIds.value.asFlow()) } .flatMapMerge { val repo = cursorRepositories[it] repo?.cursorPositions ?: emptyFlow() } override val cursorPositions: StateFlow<CursorPosition?> = allDisplaysCursorPositions.stateIn(backgroundScope, SharingStarted.WhileSubscribed(), null) }
packages/SystemUI/src/com/android/systemui/cursorposition/data/repository/SingleDisplayCursorPositionRepository.kt 0 → 100644 +154 −0 Original line number Original line 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.cursorposition.data.repository import android.os.Handler import android.os.Looper import android.view.Choreographer import android.view.InputDevice.SOURCE_MOUSE import android.view.InputDevice.SOURCE_TOUCHPAD import android.view.MotionEvent import com.android.app.displaylib.PerDisplayInstanceProviderWithTeardown import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.cursorposition.data.model.CursorPosition import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.shared.system.InputChannelCompat import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver import com.android.systemui.shared.system.InputMonitorCompat import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import javax.inject.Inject import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn /** Repository for cursor position in single display. */ interface SingleDisplayCursorPositionRepository { /** Flow of [CursorPosition] for the display. */ val cursorPositions: Flow<CursorPosition> /** Destroys the repository. */ fun destroy() } /** * Implementation of [SingleDisplayCursorPositionRepository]. * * @param displayId the display id * @param backgroundHandler the background handler * @param listenerBuilder the builder for [InputChannelCompat.InputEventListener] * @param inputMonitorBuilder the builder for [InputMonitorCompat] */ class SingleDisplayCursorPositionRepositoryImpl @AssistedInject constructor( @Assisted displayId: Int, @Background private val backgroundHandler: Handler, @Assisted private val listenerBuilder: InputEventListenerBuilder = defaultInputEventListenerBuilder, @Assisted private val inputMonitorBuilder: InputMonitorBuilder = defaultInputMonitorBuilder, ) : SingleDisplayCursorPositionRepository { private var scope: ProducerScope<CursorPosition>? = null private fun createInputMonitorCallbackFlow(displayId: Int): Flow<CursorPosition> = conflatedCallbackFlow { val inputMonitor: InputMonitorCompat = inputMonitorBuilder.build(TAG, displayId) val inputReceiver: InputEventReceiver = inputMonitor.getInputReceiver( Looper.myLooper(), Choreographer.getInstance(), listenerBuilder.build(this), ) scope = this awaitClose { inputMonitor.dispose() inputReceiver.dispose() } } // Use backgroundHandler as dispatcher because it has a looper (unlike // "backgroundDispatcher" which does not have a looper) and input receiver could use // its background looper and choreographer .flowOn(backgroundHandler.asCoroutineDispatcher()) override val cursorPositions: Flow<CursorPosition> = createInputMonitorCallbackFlow(displayId) override fun destroy() { scope?.close() } @AssistedFactory interface Factory { /** * Creates a new instance of [SingleDisplayCursorPositionRepositoryImpl] for a given * [displayId]. */ fun create( displayId: Int, listenerBuilder: InputEventListenerBuilder = defaultInputEventListenerBuilder, inputMonitorBuilder: InputMonitorBuilder = defaultInputMonitorBuilder, ): SingleDisplayCursorPositionRepositoryImpl } companion object { private const val TAG = "CursorPositionPerDisplayRepositoryImpl" private val defaultInputMonitorBuilder = InputMonitorBuilder { name, displayId -> InputMonitorCompat(name, displayId) } val defaultInputEventListenerBuilder = InputEventListenerBuilder { channel -> InputChannelCompat.InputEventListener { event -> if ( event is MotionEvent && (event.source == SOURCE_MOUSE || event.source == SOURCE_TOUCHPAD) ) { val cursorEvent = CursorPosition(event.x, event.y, event.displayId) channel.trySendWithFailureLogging(cursorEvent, TAG) } } } } } fun interface InputEventListenerBuilder { fun build(channel: SendChannel<CursorPosition>): InputChannelCompat.InputEventListener } fun interface InputMonitorBuilder { fun build(name: String, displayId: Int): InputMonitorCompat } @SysUISingleton class SingleDisplayCursorPositionRepositoryFactory @Inject constructor(private val factory: SingleDisplayCursorPositionRepositoryImpl.Factory) : PerDisplayInstanceProviderWithTeardown<SingleDisplayCursorPositionRepository> { override fun createInstance(displayId: Int): SingleDisplayCursorPositionRepository { return factory.create(displayId) } override fun destroyInstance(instance: SingleDisplayCursorPositionRepository) { instance.destroy() } }
packages/SystemUI/tests/utils/src/com/android/systemui/cursorposition/domain/data/repository/TestCursorPositionRepositoryInstanceProvider.kt 0 → 100644 +44 −0 Original line number Original line 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.cursorposition.domain.data.repository import android.os.Handler import com.android.app.displaylib.PerDisplayInstanceProviderWithTeardown import com.android.systemui.cursorposition.data.repository.InputEventListenerBuilder import com.android.systemui.cursorposition.data.repository.InputMonitorBuilder import com.android.systemui.cursorposition.data.repository.SingleDisplayCursorPositionRepository import com.android.systemui.cursorposition.data.repository.SingleDisplayCursorPositionRepositoryImpl class TestCursorPositionRepositoryInstanceProvider( private val handler: Handler, private val listenerBuilder: InputEventListenerBuilder, private val inputMonitorBuilder: InputMonitorBuilder, ) : PerDisplayInstanceProviderWithTeardown<SingleDisplayCursorPositionRepository> { override fun destroyInstance(instance: SingleDisplayCursorPositionRepository) { instance.destroy() } override fun createInstance(displayId: Int): SingleDisplayCursorPositionRepository { return SingleDisplayCursorPositionRepositoryImpl( displayId, handler, listenerBuilder, inputMonitorBuilder, ) } }