summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/data/model/BacklightModel.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt100
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt191
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)
+ }
+ }
+}