Merge "Add KeyboardRepository listening to keyboard connection" into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
56f93f42be
@@ -17,6 +17,14 @@
|
|||||||
|
|
||||||
package com.android.systemui.keyboard
|
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
|
import dagger.Module
|
||||||
|
|
||||||
@Module abstract class KeyboardModule
|
@Module
|
||||||
|
abstract class KeyboardModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindKeyboardRepository(repository: KeyboardRepositoryImpl): KeyboardRepository
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user