Merge "Move device connection logic to a separated class" into main
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
new file mode 100644
index 0000000..3b161b6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.data.repository
+
+import android.annotation.SuppressLint
+import android.hardware.input.InputManager
+import android.os.Handler
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.SendChannel
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.shareIn
+
+@SysUISingleton
+class InputDeviceRepository
+@Inject
+constructor(
+    @Background private val backgroundHandler: Handler,
+    @Background private val backgroundScope: CoroutineScope,
+    private val inputManager: InputManager
+) {
+
+    sealed interface DeviceChange
+
+    data class DeviceAdded(val deviceId: Int) : DeviceChange
+
+    data object DeviceRemoved : DeviceChange
+
+    data object FreshStart : DeviceChange
+
+    /**
+     * Emits collection of all currently connected keyboards and what was the last [DeviceChange].
+     * It emits collection so that every new subscriber to this SharedFlow can get latest state of
+     * all keyboards. Otherwise we might get into situation where subscriber timing on
+     * initialization matter and later subscriber will only get latest device and will miss all
+     * previous devices.
+     */
+    // TODO(b/351984587): Replace with StateFlow
+    @SuppressLint("SharedFlowCreation")
+    val deviceChange: Flow<Pair<Collection<Int>, DeviceChange>> =
+        conflatedCallbackFlow {
+                var connectedDevices = inputManager.inputDeviceIds.toSet()
+                val listener =
+                    object : InputManager.InputDeviceListener {
+                        override fun onInputDeviceAdded(deviceId: Int) {
+                            connectedDevices = connectedDevices + deviceId
+                            sendWithLogging(connectedDevices to DeviceAdded(deviceId))
+                        }
+
+                        override fun onInputDeviceChanged(deviceId: Int) = Unit
+
+                        override fun onInputDeviceRemoved(deviceId: Int) {
+                            connectedDevices = connectedDevices - deviceId
+                            sendWithLogging(connectedDevices to DeviceRemoved)
+                        }
+                    }
+                sendWithLogging(connectedDevices to FreshStart)
+                inputManager.registerInputDeviceListener(listener, backgroundHandler)
+                awaitClose { inputManager.unregisterInputDeviceListener(listener) }
+            }
+            .shareIn(
+                scope = backgroundScope,
+                started = SharingStarted.Lazily,
+                replay = 1,
+            )
+
+    private fun <T> SendChannel<T>.sendWithLogging(element: T) {
+        trySendWithFailureLogging(element, TAG)
+    }
+
+    companion object {
+        const val TAG = "InputDeviceRepository"
+    }
+}
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 91d5280..817849c 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
@@ -21,21 +21,23 @@
 import android.hardware.input.InputManager.KeyboardBacklightListener
 import android.hardware.input.KeyboardBacklightState
 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.inputdevice.data.repository.InputDeviceRepository
+import com.android.systemui.inputdevice.data.repository.InputDeviceRepository.DeviceAdded
+import com.android.systemui.inputdevice.data.repository.InputDeviceRepository.DeviceChange
+import com.android.systemui.inputdevice.data.repository.InputDeviceRepository.DeviceRemoved
+import com.android.systemui.inputdevice.data.repository.InputDeviceRepository.FreshStart
 import com.android.systemui.keyboard.data.model.Keyboard
 import com.android.systemui.keyboard.shared.model.BacklightModel
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
 import java.util.concurrent.Executor
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.FlowPreview
 import kotlinx.coroutines.channels.SendChannel
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.asFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.emptyFlow
@@ -44,7 +46,6 @@
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapNotNull
-import kotlinx.coroutines.flow.shareIn
 
 /**
  * Provides information about physical keyboard states. [CommandLineKeyboardRepository] can be
@@ -71,50 +72,15 @@
 class KeyboardRepositoryImpl
 @Inject
 constructor(
-    @Application private val applicationScope: CoroutineScope,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val inputManager: InputManager,
+    inputDeviceRepository: InputDeviceRepository
 ) : KeyboardRepository {
 
-    private sealed interface DeviceChange
-    private data class DeviceAdded(val deviceId: Int) : DeviceChange
-    private object DeviceRemoved : DeviceChange
-    private object FreshStart : DeviceChange
-
-    /**
-     * Emits collection of all currently connected keyboards and what was the last [DeviceChange].
-     * It emits collection so that every new subscriber to this SharedFlow can get latest state of
-     * all keyboards. Otherwise we might get into situation where subscriber timing on
-     * initialization matter and later subscriber will only get latest device and will miss all
-     * previous devices.
-     */
     private val keyboardsChange: Flow<Pair<Collection<Int>, DeviceChange>> =
-        conflatedCallbackFlow {
-                var connectedDevices = inputManager.inputDeviceIds.toSet()
-                val listener =
-                    object : InputManager.InputDeviceListener {
-                        override fun onInputDeviceAdded(deviceId: Int) {
-                            connectedDevices = connectedDevices + deviceId
-                            sendWithLogging(connectedDevices to DeviceAdded(deviceId))
-                        }
-
-                        override fun onInputDeviceChanged(deviceId: Int) = Unit
-
-                        override fun onInputDeviceRemoved(deviceId: Int) {
-                            connectedDevices = connectedDevices - deviceId
-                            sendWithLogging(connectedDevices to DeviceRemoved)
-                        }
-                    }
-                sendWithLogging(connectedDevices to FreshStart)
-                inputManager.registerInputDeviceListener(listener, /* handler= */ null)
-                awaitClose { inputManager.unregisterInputDeviceListener(listener) }
-            }
-            .map { (ids, change) -> ids.filter { id -> isPhysicalFullKeyboard(id) } to change }
-            .shareIn(
-                scope = applicationScope,
-                started = SharingStarted.Lazily,
-                replay = 1,
-            )
+        inputDeviceRepository.deviceChange.map { (ids, change) ->
+            ids.filter { id -> isPhysicalFullKeyboard(id) } to change
+        }
 
     @FlowPreview
     override val newlyConnectedKeyboard: Flow<Keyboard> =
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
index 53bcf86..361e768 100644
--- 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
@@ -20,6 +20,7 @@
 import android.hardware.input.InputManager
 import android.hardware.input.InputManager.KeyboardBacklightListener
 import android.hardware.input.KeyboardBacklightState
+import android.testing.TestableLooper
 import android.view.InputDevice
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -27,11 +28,13 @@
 import com.android.systemui.coroutines.FlowValue
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
+import com.android.systemui.inputdevice.data.repository.InputDeviceRepository
 import com.android.systemui.keyboard.data.model.Keyboard
 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.android.systemui.utils.os.FakeHandler
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -53,6 +56,7 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
+@TestableLooper.RunWithLooper
 @RunWith(AndroidJUnit4::class)
 class KeyboardRepositoryTest : SysuiTestCase() {
 
@@ -63,6 +67,7 @@
 
     private lateinit var underTest: KeyboardRepository
     private lateinit var dispatcher: CoroutineDispatcher
+    private lateinit var inputDeviceRepo: InputDeviceRepository
     private lateinit var testScope: TestScope
 
     @Before
@@ -75,7 +80,9 @@
         }
         dispatcher = StandardTestDispatcher()
         testScope = TestScope(dispatcher)
-        underTest = KeyboardRepositoryImpl(testScope.backgroundScope, dispatcher, inputManager)
+        val handler = FakeHandler(TestableLooper.get(this).looper)
+        inputDeviceRepo = InputDeviceRepository(handler, testScope.backgroundScope, inputManager)
+        underTest = KeyboardRepositoryImpl(dispatcher, inputManager, inputDeviceRepo)
     }
 
     @Test
@@ -363,6 +370,7 @@
         private val maxBrightnessLevel: Int
     ) : KeyboardBacklightState() {
         override fun getBrightnessLevel() = brightnessLevel
+
         override fun getMaxBrightnessLevel() = maxBrightnessLevel
     }
 }