Merge "Add KeyboardRepository listening to keyboard connection" into tm-qpr-dev am: 56f93f42be

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

Change-Id: Id01c588277f4e5c0fbee0d7f7adc1f42e0aa38ca
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Michał Brzeziński
2023-02-22 13:03:30 +00:00
committed by Automerger Merge Worker
4 changed files with 324 additions and 1 deletions

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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"
}
}

View File

@@ -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)
}
}
}