Detect keyboard/touchpad connection with CoreStartable
Launch OOBE right away for now
Bug: 344862874
Flag: com.android.systemui.shared.new_touchpad_gestures_tutorial
Test: KeyboardRepositoryTest.kt
Test: TouchpadRepositoryTest.kt
Change-Id: Iaf91e64ced9cba5cce3339e1677314cd4c000864
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 88601da..4286646 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -30,6 +30,7 @@
import com.android.systemui.dreams.DreamMonitor
import com.android.systemui.dreams.homecontrols.HomeControlsDreamStartable
import com.android.systemui.globalactions.GlobalActionsComponent
+import com.android.systemui.inputdevice.oobe.KeyboardTouchpadOobeTutorialCoreStartable
import com.android.systemui.keyboard.KeyboardUI
import com.android.systemui.keyboard.PhysicalKeyboardCoreStartable
import com.android.systemui.keyguard.KeyguardViewConfigurator
@@ -257,6 +258,13 @@
@Binds
@IntoMap
+ @ClassKey(KeyboardTouchpadOobeTutorialCoreStartable::class)
+ abstract fun bindOobeSchedulerCoreStartable(
+ listener: KeyboardTouchpadOobeTutorialCoreStartable
+ ): CoreStartable
+
+ @Binds
+ @IntoMap
@ClassKey(PhysicalKeyboardCoreStartable::class)
abstract fun bindKeyboardCoreStartable(listener: PhysicalKeyboardCoreStartable): CoreStartable
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 1771f4d..bf1222b 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -143,6 +143,7 @@
import com.android.systemui.statusbar.window.StatusBarWindowModule;
import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule;
import com.android.systemui.temporarydisplay.dagger.TemporaryDisplayModule;
+import com.android.systemui.touchpad.TouchpadModule;
import com.android.systemui.tuner.dagger.TunerModule;
import com.android.systemui.user.UserModule;
import com.android.systemui.user.domain.UserDomainLayerModule;
@@ -257,6 +258,7 @@
CommonSystemUIUnfoldModule.class,
TelephonyRepositoryModule.class,
TemporaryDisplayModule.class,
+ TouchpadModule.class,
TunerModule.class,
UserDomainLayerModule.class,
UserModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
index 3b161b6..5a008bd 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
@@ -45,7 +45,7 @@
data class DeviceAdded(val deviceId: Int) : DeviceChange
- data object DeviceRemoved : DeviceChange
+ data class DeviceRemoved(val deviceId: Int) : DeviceChange
data object FreshStart : DeviceChange
@@ -72,7 +72,7 @@
override fun onInputDeviceRemoved(deviceId: Int) {
connectedDevices = connectedDevices - deviceId
- sendWithLogging(connectedDevices to DeviceRemoved)
+ sendWithLogging(connectedDevices to DeviceRemoved(deviceId))
}
}
sendWithLogging(connectedDevices to FreshStart)
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadOobeTutorialCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadOobeTutorialCoreStartable.kt
new file mode 100644
index 0000000..dbfea76
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadOobeTutorialCoreStartable.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 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.inputdevice.oobe
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.inputdevice.oobe.domain.interactor.OobeTutorialSchedulerInteractor
+import com.android.systemui.shared.Flags.newTouchpadGesturesTutorial
+import dagger.Lazy
+import javax.inject.Inject
+
+/** A [CoreStartable] to launch a scheduler for keyboard and touchpad OOBE education */
+@SysUISingleton
+class KeyboardTouchpadOobeTutorialCoreStartable
+@Inject
+constructor(private val oobeTutorialSchedulerInteractor: Lazy<OobeTutorialSchedulerInteractor>) :
+ CoreStartable {
+ override fun start() {
+ if (newTouchpadGesturesTutorial()) {
+ oobeTutorialSchedulerInteractor.get().start()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/domain/interactor/OobeTutorialSchedulerInteractor.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/domain/interactor/OobeTutorialSchedulerInteractor.kt
new file mode 100644
index 0000000..0d69081
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/domain/interactor/OobeTutorialSchedulerInteractor.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 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.inputdevice.oobe.domain.interactor
+
+import android.content.Context
+import android.content.Intent
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyboard.data.repository.KeyboardRepository
+import com.android.systemui.touchpad.data.repository.TouchpadRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/** When keyboards or touchpads are connected, schedule a tutorial after given time has elapsed */
+@SysUISingleton
+class OobeTutorialSchedulerInteractor
+@Inject
+constructor(
+ @Application private val context: Context,
+ @Application private val applicationScope: CoroutineScope,
+ keyboardRepository: KeyboardRepository,
+ touchpadRepository: TouchpadRepository
+) {
+ private val isAnyKeyboardConnected = keyboardRepository.isAnyKeyboardConnected
+ private val isAnyTouchpadConnected = touchpadRepository.isAnyTouchpadConnected
+
+ fun start() {
+ applicationScope.launch { isAnyKeyboardConnected.collect { startOobe() } }
+ applicationScope.launch { isAnyTouchpadConnected.collect { startOobe() } }
+ }
+
+ private fun startOobe() {
+ val intent = Intent(TUTORIAL_ACTION)
+ intent.addCategory(Intent.CATEGORY_DEFAULT)
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(intent)
+ }
+
+ companion object {
+ const val TAG = "OobeSchedulerInteractor"
+ const val TUTORIAL_ACTION = "com.android.systemui.action.TOUCHPAD_TUTORIAL"
+ }
+}
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
index 817849c..b654307 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
@@ -41,6 +41,7 @@
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
@@ -78,9 +79,15 @@
) : KeyboardRepository {
private val keyboardsChange: Flow<Pair<Collection<Int>, DeviceChange>> =
- inputDeviceRepository.deviceChange.map { (ids, change) ->
- ids.filter { id -> isPhysicalFullKeyboard(id) } to change
- }
+ inputDeviceRepository.deviceChange
+ .map { (ids, change) -> ids.filter { id -> isPhysicalFullKeyboard(id) } to change }
+ .filter { (_, change) ->
+ when (change) {
+ FreshStart -> true
+ is DeviceAdded -> isPhysicalFullKeyboard(change.deviceId)
+ is DeviceRemoved -> isPhysicalFullKeyboard(change.deviceId)
+ }
+ }
@FlowPreview
override val newlyConnectedKeyboard: Flow<Keyboard> =
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/TouchpadModule.kt b/packages/SystemUI/src/com/android/systemui/touchpad/TouchpadModule.kt
new file mode 100644
index 0000000..c86ac2f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/TouchpadModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 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.touchpad
+
+import com.android.systemui.touchpad.data.repository.TouchpadRepository
+import com.android.systemui.touchpad.data.repository.TouchpadRepositoryImpl
+import dagger.Binds
+import dagger.Module
+
+@Module
+abstract class TouchpadModule {
+
+ @Binds
+ abstract fun bindTouchpadRepository(repository: TouchpadRepositoryImpl): TouchpadRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/data/repository/TouchpadRepository.kt b/packages/SystemUI/src/com/android/systemui/touchpad/data/repository/TouchpadRepository.kt
new file mode 100644
index 0000000..7131546
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/data/repository/TouchpadRepository.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 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.touchpad.data.repository
+
+import android.hardware.input.InputManager
+import android.view.InputDevice.SOURCE_TOUCHPAD
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.inputdevice.data.repository.InputDeviceRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+interface TouchpadRepository {
+ /** Emits true if any touchpad is connected to the device, false otherwise. */
+ val isAnyTouchpadConnected: Flow<Boolean>
+}
+
+@SysUISingleton
+class TouchpadRepositoryImpl
+@Inject
+constructor(
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val inputManager: InputManager,
+ inputDeviceRepository: InputDeviceRepository
+) : TouchpadRepository {
+
+ override val isAnyTouchpadConnected: Flow<Boolean> =
+ inputDeviceRepository.deviceChange
+ .map { (ids, _) -> ids.any { id -> isTouchpad(id) } }
+ .distinctUntilChanged()
+ .flowOn(backgroundDispatcher)
+
+ private fun isTouchpad(deviceId: Int): Boolean {
+ val device = inputManager.getInputDevice(deviceId) ?: return false
+ return device.supportsSource(SOURCE_TOUCHPAD)
+ }
+
+ companion object {
+ const val TAG = "TouchpadRepositoryImpl"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/data/repository/TouchpadRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/data/repository/TouchpadRepositoryTest.kt
new file mode 100644
index 0000000..3783af5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/data/repository/TouchpadRepositoryTest.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2024 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.touchpad.data.repository
+
+import android.hardware.input.FakeInputManager
+import android.hardware.input.InputManager.InputDeviceListener
+import android.hardware.input.fakeInputManager
+import android.testing.TestableLooper
+import android.view.InputDevice
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.inputdevice.data.repository.InputDeviceRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.utils.os.FakeHandler
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
+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.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Captor
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidJUnit4::class)
+class TouchpadRepositoryTest : SysuiTestCase() {
+
+ @Captor private lateinit var deviceListenerCaptor: ArgumentCaptor<InputDeviceListener>
+ private lateinit var fakeInputManager: FakeInputManager
+
+ private lateinit var underTest: TouchpadRepository
+ private lateinit var dispatcher: CoroutineDispatcher
+ private lateinit var inputDeviceRepo: InputDeviceRepository
+ private lateinit var testScope: TestScope
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ fakeInputManager = testKosmos().fakeInputManager
+ dispatcher = StandardTestDispatcher()
+ testScope = TestScope(dispatcher)
+ val handler = FakeHandler(TestableLooper.get(this).looper)
+ inputDeviceRepo =
+ InputDeviceRepository(handler, testScope.backgroundScope, fakeInputManager.inputManager)
+ underTest =
+ TouchpadRepositoryImpl(dispatcher, fakeInputManager.inputManager, inputDeviceRepo)
+ }
+
+ @Test
+ fun emitsDisconnected_ifNothingIsConnected() =
+ testScope.runTest {
+ val initialState = underTest.isAnyTouchpadConnected.first()
+ assertThat(initialState).isFalse()
+ }
+
+ @Test
+ fun emitsConnected_ifTouchpadAlreadyConnectedAtTheStart() =
+ testScope.runTest {
+ fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+ val initialValue = underTest.isAnyTouchpadConnected.first()
+ assertThat(initialValue).isTrue()
+ }
+
+ @Test
+ fun emitsConnected_whenNewTouchpadConnects() =
+ testScope.runTest {
+ captureDeviceListener()
+ val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+ fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+
+ assertThat(isTouchpadConnected).isTrue()
+ }
+
+ @Test
+ fun emitsDisconnected_whenDeviceWithIdDoesNotExist() =
+ testScope.runTest {
+ captureDeviceListener()
+ val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+ whenever(fakeInputManager.inputManager.getInputDevice(eq(NULL_DEVICE_ID)))
+ .thenReturn(null)
+ fakeInputManager.addDevice(NULL_DEVICE_ID, InputDevice.SOURCE_UNKNOWN)
+ assertThat(isTouchpadConnected).isFalse()
+ }
+
+ @Test
+ fun emitsDisconnected_whenTouchpadDisconnects() =
+ testScope.runTest {
+ captureDeviceListener()
+ val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+ fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+ assertThat(isTouchpadConnected).isTrue()
+
+ fakeInputManager.removeDevice(TOUCHPAD_ID)
+ assertThat(isTouchpadConnected).isFalse()
+ }
+
+ private suspend fun captureDeviceListener() {
+ underTest.isAnyTouchpadConnected.first()
+ Mockito.verify(fakeInputManager.inputManager)
+ .registerInputDeviceListener(deviceListenerCaptor.capture(), anyOrNull())
+ fakeInputManager.registerInputDeviceListener(deviceListenerCaptor.value)
+ }
+
+ @Test
+ fun emitsDisconnected_whenNonTouchpadConnects() =
+ testScope.runTest {
+ captureDeviceListener()
+ val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+ fakeInputManager.addDevice(NON_TOUCHPAD_ID, InputDevice.SOURCE_KEYBOARD)
+ assertThat(isTouchpadConnected).isFalse()
+ }
+
+ @Test
+ fun emitsDisconnected_whenTouchpadDisconnectsAndWasAlreadyConnectedAtTheStart() =
+ testScope.runTest {
+ captureDeviceListener()
+ val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+ fakeInputManager.removeDevice(TOUCHPAD_ID)
+ assertThat(isTouchpadConnected).isFalse()
+ }
+
+ @Test
+ fun emitsConnected_whenAnotherDeviceDisconnects() =
+ testScope.runTest {
+ captureDeviceListener()
+ val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+ fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+ fakeInputManager.removeDevice(NON_TOUCHPAD_ID)
+
+ assertThat(isTouchpadConnected).isTrue()
+ }
+
+ @Test
+ fun emitsConnected_whenOneTouchpadDisconnectsButAnotherRemainsConnected() =
+ testScope.runTest {
+ captureDeviceListener()
+ val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+ fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+ fakeInputManager.addDevice(ANOTHER_TOUCHPAD_ID, TOUCHPAD)
+ fakeInputManager.removeDevice(TOUCHPAD_ID)
+
+ assertThat(isTouchpadConnected).isTrue()
+ }
+
+ private companion object {
+ private const val TOUCHPAD_ID = 1
+ private const val NON_TOUCHPAD_ID = 2
+ private const val ANOTHER_TOUCHPAD_ID = 3
+ private const val NULL_DEVICE_ID = 4
+
+ private const val TOUCHPAD = InputDevice.SOURCE_TOUCHPAD
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
index 6e7c05c..ee36cad 100644
--- a/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
+++ b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
@@ -16,6 +16,7 @@
package android.hardware.input
+import android.hardware.input.InputManager.InputDeviceListener
import android.view.InputDevice
import android.view.KeyCharacterMap
import android.view.KeyCharacterMap.VIRTUAL_KEYBOARD
@@ -47,6 +48,8 @@
VIRTUAL_KEYBOARD to allKeyCodes.toMutableSet()
)
+ private var inputDeviceListener: InputDeviceListener? = null
+
val inputManager =
mock<InputManager> {
whenever(getInputDevice(anyInt())).thenAnswer { invocation ->
@@ -84,6 +87,11 @@
addPhysicalKeyboard(deviceId, enabled)
}
+ fun registerInputDeviceListener(listener: InputDeviceListener) {
+ // TODO (b/355422259): handle this by listening to inputManager.registerInputDeviceListener
+ inputDeviceListener = listener
+ }
+
fun addPhysicalKeyboard(id: Int, enabled: Boolean = true) {
check(id > 0) { "Physical keyboard ids have to be > 0" }
addKeyboard(id, enabled)
@@ -106,6 +114,16 @@
supportedKeyCodesByDeviceId[id] = allKeyCodes.toMutableSet()
}
+ fun addDevice(id: Int, sources: Int) {
+ devices[id] = InputDevice.Builder().setId(id).setSources(sources).build()
+ inputDeviceListener?.onInputDeviceAdded(id)
+ }
+
+ fun removeDevice(id: Int) {
+ devices.remove(id)
+ inputDeviceListener?.onInputDeviceRemoved(id)
+ }
+
private fun InputDevice.copy(
id: Int = getId(),
type: Int = keyboardType,