diff options
4 files changed, 324 insertions, 1 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt index e9b8908214fc..496c64e1120e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt @@ -17,6 +17,14 @@ package com.android.systemui.keyboard +import com.android.systemui.keyboard.data.repository.KeyboardRepository +import com.android.systemui.keyboard.data.repository.KeyboardRepositoryImpl +import dagger.Binds import dagger.Module -@Module abstract class KeyboardModule +@Module +abstract class KeyboardModule { + + @Binds + abstract fun bindKeyboardRepository(repository: KeyboardRepositoryImpl): KeyboardRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/model/BacklightModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/model/BacklightModel.kt new file mode 100644 index 000000000000..ea15a9f18584 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/model/BacklightModel.kt @@ -0,0 +1,24 @@ +/* + * 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 com.android.systemui.keyboard.data.model + +/** + * Model for current state of keyboard backlight brightness. [level] indicates current level of + * backlight brightness and [maxLevel] its max possible value. + */ +data class BacklightModel(val level: Int, val maxLevel: Int) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt new file mode 100644 index 000000000000..70faf406d621 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt @@ -0,0 +1,100 @@ +/* + * 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 com.android.systemui.keyboard.data.repository + +import android.hardware.input.InputManager +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.keyboard.data.model.BacklightModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn + +interface KeyboardRepository { + val keyboardConnected: Flow<Boolean> + val backlight: Flow<BacklightModel> +} + +@SysUISingleton +class KeyboardRepositoryImpl +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val inputManager: InputManager, +) : KeyboardRepository { + + private val connectedDeviceIds: Flow<Set<Int>> = + conflatedCallbackFlow { + fun send(element: Set<Int>) = trySendWithFailureLogging(element, TAG) + + var connectedKeyboards = inputManager.inputDeviceIds.toSet() + val listener = + object : InputManager.InputDeviceListener { + override fun onInputDeviceAdded(deviceId: Int) { + connectedKeyboards = connectedKeyboards + deviceId + send(connectedKeyboards) + } + + override fun onInputDeviceChanged(deviceId: Int) = Unit + + override fun onInputDeviceRemoved(deviceId: Int) { + connectedKeyboards = connectedKeyboards - deviceId + send(connectedKeyboards) + } + } + send(connectedKeyboards) + inputManager.registerInputDeviceListener(listener, /* handler= */ null) + awaitClose { inputManager.unregisterInputDeviceListener(listener) } + } + .shareIn( + scope = applicationScope, + started = SharingStarted.Lazily, + replay = 1, + ) + + override val keyboardConnected: Flow<Boolean> = + connectedDeviceIds + .map { it.any { deviceId -> isPhysicalFullKeyboard(deviceId) } } + .distinctUntilChanged() + .flowOn(backgroundDispatcher) + + override val backlight: Flow<BacklightModel> = + conflatedCallbackFlow { + // TODO(b/268645734) register BacklightListener + } + + private fun isPhysicalFullKeyboard(deviceId: Int): Boolean { + val device = inputManager.getInputDevice(deviceId) + return !device.isVirtual && device.isFullKeyboard + } + + companion object { + const val TAG = "KeyboardRepositoryImpl" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt new file mode 100644 index 000000000000..f6ff4b214035 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt @@ -0,0 +1,191 @@ +/* + * 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 com.android.systemui.keyboard.data.repository + +import android.hardware.input.InputManager +import android.view.InputDevice +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.nullable +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class KeyboardRepositoryTest : SysuiTestCase() { + + @Captor + private lateinit var deviceListenerCaptor: ArgumentCaptor<InputManager.InputDeviceListener> + @Mock private lateinit var inputManager: InputManager + + private lateinit var underTest: KeyboardRepository + private lateinit var dispatcher: CoroutineDispatcher + private lateinit var testScope: TestScope + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf()) + whenever(inputManager.getInputDevice(any())).then { invocation -> + val id = invocation.arguments.first() + INPUT_DEVICES_MAP[id] + } + dispatcher = StandardTestDispatcher() + testScope = TestScope(dispatcher) + underTest = KeyboardRepositoryImpl(testScope.backgroundScope, dispatcher, inputManager) + } + + @Test + fun emitsDisconnected_ifNothingIsConnected() = + testScope.runTest { + val initialState = underTest.keyboardConnected.first() + assertThat(initialState).isFalse() + } + + @Test + fun emitsConnected_ifKeyboardAlreadyConnectedAtTheStart() = + testScope.runTest { + whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(PHYSICAL_FULL_KEYBOARD_ID)) + val initialValue = underTest.keyboardConnected.first() + assertThat(initialValue).isTrue() + } + + @Test + fun emitsConnected_whenNewPhysicalKeyboardConnects() = + testScope.runTest { + val deviceListener = captureDeviceListener() + val isKeyboardConnected by collectLastValue(underTest.keyboardConnected) + + deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID) + + assertThat(isKeyboardConnected).isTrue() + } + + @Test + fun emitsDisconnected_whenKeyboardDisconnects() = + testScope.runTest { + val deviceListener = captureDeviceListener() + val isKeyboardConnected by collectLastValue(underTest.keyboardConnected) + + deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID) + assertThat(isKeyboardConnected).isTrue() + + deviceListener.onInputDeviceRemoved(PHYSICAL_FULL_KEYBOARD_ID) + assertThat(isKeyboardConnected).isFalse() + } + + private suspend fun captureDeviceListener(): InputManager.InputDeviceListener { + underTest.keyboardConnected.first() + verify(inputManager).registerInputDeviceListener(deviceListenerCaptor.capture(), nullable()) + return deviceListenerCaptor.value + } + + @Test + fun emitsDisconnected_whenVirtualOrNotFullKeyboardConnects() = + testScope.runTest { + val deviceListener = captureDeviceListener() + val isKeyboardConnected by collectLastValue(underTest.keyboardConnected) + + deviceListener.onInputDeviceAdded(PHYSICAL_NOT_FULL_KEYBOARD_ID) + assertThat(isKeyboardConnected).isFalse() + + deviceListener.onInputDeviceAdded(VIRTUAL_FULL_KEYBOARD_ID) + assertThat(isKeyboardConnected).isFalse() + } + + @Test + fun emitsDisconnected_whenKeyboardDisconnectsAndWasAlreadyConnectedAtTheStart() = + testScope.runTest { + val deviceListener = captureDeviceListener() + val isKeyboardConnected by collectLastValue(underTest.keyboardConnected) + + deviceListener.onInputDeviceRemoved(PHYSICAL_FULL_KEYBOARD_ID) + assertThat(isKeyboardConnected).isFalse() + } + + @Test + fun emitsConnected_whenAnotherDeviceDisconnects() = + testScope.runTest { + val deviceListener = captureDeviceListener() + val isKeyboardConnected by collectLastValue(underTest.keyboardConnected) + + deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID) + deviceListener.onInputDeviceRemoved(VIRTUAL_FULL_KEYBOARD_ID) + + assertThat(isKeyboardConnected).isTrue() + } + + @Test + fun emitsConnected_whenOnePhysicalKeyboardDisconnectsButAnotherRemainsConnected() = + testScope.runTest { + val deviceListener = captureDeviceListener() + val isKeyboardConnected by collectLastValue(underTest.keyboardConnected) + + deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID) + deviceListener.onInputDeviceAdded(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID) + deviceListener.onInputDeviceRemoved(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID) + + assertThat(isKeyboardConnected).isTrue() + } + + @Test + fun passesKeyboardBacklightValues_fromBacklightListener() { + // TODO(b/268645734): implement when implementing backlight listener + } + + private companion object { + private const val PHYSICAL_FULL_KEYBOARD_ID = 1 + private const val VIRTUAL_FULL_KEYBOARD_ID = 2 + private const val PHYSICAL_NOT_FULL_KEYBOARD_ID = 3 + private const val ANOTHER_PHYSICAL_FULL_KEYBOARD_ID = 4 + + private val INPUT_DEVICES_MAP: Map<Int, InputDevice> = + mapOf( + PHYSICAL_FULL_KEYBOARD_ID to inputDevice(virtual = false, fullKeyboard = true), + VIRTUAL_FULL_KEYBOARD_ID to inputDevice(virtual = true, fullKeyboard = true), + PHYSICAL_NOT_FULL_KEYBOARD_ID to inputDevice(virtual = false, fullKeyboard = false), + ANOTHER_PHYSICAL_FULL_KEYBOARD_ID to + inputDevice(virtual = false, fullKeyboard = true) + ) + + private fun inputDevice(virtual: Boolean, fullKeyboard: Boolean): InputDevice = + mock<InputDevice>().also { + whenever(it.isVirtual).thenReturn(virtual) + whenever(it.isFullKeyboard).thenReturn(fullKeyboard) + } + } +} |