Handle bluetooth callback and toggle switch, also moved `getDeviceItems` to background thread.

The dialog opens first with empty device list, then the list updates after `getDeviceItems` finishes.

Flag: BLUETOOTH_QS_TILE_DIALOG
Test: atest -c BluetoothTileDialogTest BluetoothTileDialogViewModelTest DeviceItemFactoryTest DeviceItemInteractorTest BluetoothTileDialogRepositoryTest BluetoothStateInteractorTest
Bug: b/298124674 b/299400510
Change-Id: Ib7d4ab9773b7741caf474a273c45d9568eb52566
diff --git a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
index 95ad1e3..9d14d0f 100644
--- a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
+++ b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
@@ -114,7 +114,8 @@
             android:id="@+id/see_all_layout"
             style="@style/BluetoothTileDialog.Device"
             android:layout_height="64dp"
-            android:paddingStart="20dp">
+            android:paddingStart="20dp"
+            android:visibility="gone">
 
             <FrameLayout
                 android:layout_width="24dp"
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractor.kt
new file mode 100644
index 0000000..efad9ec
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractor.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.qs.tiles.dialog.bluetooth
+
+import android.bluetooth.BluetoothAdapter.STATE_OFF
+import android.bluetooth.BluetoothAdapter.STATE_ON
+import com.android.settingslib.bluetooth.BluetoothCallback
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+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 javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+
+/** Holds business logic for the Bluetooth Dialog's bluetooth and device connection state */
+@SysUISingleton
+internal class BluetoothStateInteractor
+@Inject
+constructor(
+    private val localBluetoothManager: LocalBluetoothManager?,
+    @Application private val coroutineScope: CoroutineScope,
+) {
+
+    internal val updateBluetoothStateFlow: StateFlow<Boolean?> =
+        conflatedCallbackFlow {
+                val listener =
+                    object : BluetoothCallback {
+                        override fun onBluetoothStateChanged(bluetoothState: Int) {
+                            if (bluetoothState == STATE_ON || bluetoothState == STATE_OFF) {
+                                super.onBluetoothStateChanged(bluetoothState)
+                                trySendWithFailureLogging(
+                                    bluetoothState == STATE_ON,
+                                    TAG,
+                                    "onBluetoothStateChanged"
+                                )
+                            }
+                        }
+                    }
+                localBluetoothManager?.eventManager?.registerCallback(listener)
+                awaitClose { localBluetoothManager?.eventManager?.unregisterCallback(listener) }
+            }
+            .stateIn(
+                coroutineScope,
+                SharingStarted.WhileSubscribed(replayExpirationMillis = 0),
+                initialValue = null
+            )
+
+    internal var isBluetoothEnabled: Boolean
+        get() = localBluetoothManager?.bluetoothAdapter?.isEnabled == true
+        set(value) {
+            if (isBluetoothEnabled != value) {
+                localBluetoothManager?.bluetoothAdapter?.apply {
+                    if (value) enable() else disable()
+                }
+            }
+        }
+
+    companion object {
+        private const val TAG = "BtStateInteractor"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
index feeebe7d..7a436a7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
@@ -20,55 +20,98 @@
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
 import android.view.ViewGroup
 import android.widget.ImageView
+import android.widget.Switch
 import android.widget.TextView
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.phone.SystemUIDialog
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
 
 /** Dialog for showing active, connected and saved bluetooth devices. */
 @SysUISingleton
 internal class BluetoothTileDialog
 constructor(
-    deviceItem: List<DeviceItem>,
-    deviceItemOnClickCallback: DeviceItemOnClickCallback,
+    private val bluetoothToggleInitialValue: Boolean,
+    private val bluetoothTileDialogCallback: BluetoothTileDialogCallback,
     context: Context,
 ) : SystemUIDialog(context, DEFAULT_THEME, DEFAULT_DISMISS_ON_DEVICE_LOCK) {
 
-    private val deviceItemAdapter: Adapter =
-        Adapter(deviceItem.toMutableList(), deviceItemOnClickCallback)
+    private val mutableBluetoothStateSwitchedFlow: MutableStateFlow<Boolean?> =
+        MutableStateFlow(null)
+    internal val bluetoothStateSwitchedFlow
+        get() = mutableBluetoothStateSwitchedFlow.asStateFlow()
+
+    private val mutableClickedFlow: MutableSharedFlow<Pair<DeviceItem, Int>> =
+        MutableSharedFlow(extraBufferCapacity = 1)
+    internal val deviceItemClickedFlow
+        get() = mutableClickedFlow.asSharedFlow()
+
+    private val deviceItemAdapter: Adapter = Adapter()
+
+    private lateinit var toggleView: Switch
+    private lateinit var doneButton: View
+    private lateinit var seeAllView: View
+    private lateinit var deviceListView: RecyclerView
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
         setContentView(LayoutInflater.from(context).inflate(R.layout.bluetooth_tile_dialog, null))
 
-        setupDoneButton()
+        toggleView = requireViewById(R.id.bluetooth_toggle)
+        doneButton = requireViewById(R.id.done_button)
+        seeAllView = requireViewById(R.id.see_all_layout)
+        deviceListView = requireViewById<RecyclerView>(R.id.device_list)
+
+        setupToggle()
         setupRecyclerView()
+
+        doneButton.setOnClickListener { dismiss() }
     }
 
-    internal fun onDeviceItemUpdated(deviceItem: DeviceItem, position: Int) {
+    internal fun onDeviceItemUpdated(deviceItem: List<DeviceItem>, showSeeAll: Boolean) {
+        seeAllView.visibility = if (showSeeAll) VISIBLE else GONE
+        deviceItemAdapter.refreshDeviceItemList(deviceItem)
+    }
+
+    internal fun onDeviceItemUpdatedAtPosition(deviceItem: DeviceItem, position: Int) {
         deviceItemAdapter.refreshDeviceItem(deviceItem, position)
     }
 
-    private fun setupDoneButton() {
-        requireViewById<View>(R.id.done_button).setOnClickListener { dismiss() }
+    internal fun onBluetoothStateUpdated(isEnabled: Boolean) {
+        toggleView.isChecked = isEnabled
+    }
+
+    private fun setupToggle() {
+        toggleView.isChecked = bluetoothToggleInitialValue
+        toggleView.setOnCheckedChangeListener { _, isChecked ->
+            mutableBluetoothStateSwitchedFlow.value = isChecked
+        }
     }
 
     private fun setupRecyclerView() {
-        requireViewById<RecyclerView>(R.id.device_list).apply {
+        deviceListView.apply {
             layoutManager = LinearLayoutManager(context)
             adapter = deviceItemAdapter
         }
     }
 
-    internal class Adapter(
-        private var deviceItem: MutableList<DeviceItem>,
-        private val onClickCallback: DeviceItemOnClickCallback
-    ) : RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() {
+    internal inner class Adapter : RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() {
+
+        init {
+            setHasStableIds(true)
+        }
+
+        private val deviceItem: MutableList<DeviceItem> = mutableListOf()
 
         override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceItemViewHolder {
             val view =
@@ -79,36 +122,38 @@
 
         override fun getItemCount() = deviceItem.size
 
+        override fun getItemId(position: Int) = position.toLong()
+
         override fun onBindViewHolder(holder: DeviceItemViewHolder, position: Int) {
             val item = getItem(position)
-            holder.bind(item, position, onClickCallback)
+            holder.bind(item, position)
         }
 
         internal fun getItem(position: Int) = deviceItem[position]
 
+        internal fun refreshDeviceItemList(updated: List<DeviceItem>) {
+            deviceItem.clear()
+            deviceItem.addAll(updated)
+            notifyDataSetChanged()
+        }
+
         internal fun refreshDeviceItem(updated: DeviceItem, position: Int) {
             deviceItem[position] = updated
             notifyItemChanged(position)
         }
 
-        internal class DeviceItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+        internal inner class DeviceItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
             private val container = view.requireViewById<View>(R.id.bluetooth_device)
             private val nameView = view.requireViewById<TextView>(R.id.bluetooth_device_name)
             private val summaryView = view.requireViewById<TextView>(R.id.bluetooth_device_summary)
             private val iconView = view.requireViewById<ImageView>(R.id.bluetooth_device_icon)
 
-            internal fun bind(
-                item: DeviceItem,
-                position: Int,
-                deviceItemOnClickCallback: DeviceItemOnClickCallback
-            ) {
+            internal fun bind(item: DeviceItem, position: Int) {
                 container.apply {
                     isEnabled = item.isEnabled
                     alpha = item.alpha
                     background = item.background
-                    setOnClickListener {
-                        deviceItemOnClickCallback.onDeviceItemClicked(item, position)
-                    }
+                    setOnClickListener { mutableClickedFlow.tryEmit(Pair(item, position)) }
                 }
                 nameView.text = item.deviceName
                 summaryView.text = item.connectionSummary
@@ -125,5 +170,6 @@
     internal companion object {
         const val ENABLED_ALPHA = 1.0f
         const val DISABLED_ALPHA = 0.3f
+        const val MAX_DEVICE_ITEM_ENTRY = 3
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
index f1196a6..63f0531 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
@@ -17,13 +17,22 @@
 package com.android.systemui.qs.tiles.dialog.bluetooth
 
 import android.content.Context
-import android.os.Handler
 import android.view.View
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.animation.DialogLaunchAnimator
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.MAX_DEVICE_ITEM_ENTRY
+import com.android.systemui.statusbar.phone.SystemUIDialog
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
 
 /** ViewModel for Bluetooth Dialog after clicking on the Bluetooth QS tile. */
 @SysUISingleton
@@ -31,13 +40,15 @@
 @Inject
 constructor(
     private val deviceItemInteractor: DeviceItemInteractor,
+    private val bluetoothStateInteractor: BluetoothStateInteractor,
     private val dialogLaunchAnimator: DialogLaunchAnimator,
-    @Main private val uiHandler: Handler
-) : DeviceItemOnClickCallback {
-    private var deviceItems: List<DeviceItem> = emptyList()
+    @Application private val coroutineScope: CoroutineScope,
+    @Main private val mainDispatcher: CoroutineDispatcher,
+) : BluetoothTileDialogCallback {
 
-    @VisibleForTesting
-    var dialog: BluetoothTileDialog? = null
+    private var job: Job? = null
+
+    @VisibleForTesting internal var dialog: BluetoothTileDialog? = null
 
     /**
      * Shows the dialog.
@@ -48,27 +59,79 @@
     fun showDialog(context: Context, view: View?) {
         dismissDialog()
 
-        deviceItems = deviceItemInteractor.getDeviceItems(context)
+        var updateDeviceItemJob: Job? = null
 
-        uiHandler.post {
-            dialog = BluetoothTileDialog(deviceItems, this, context)
+        job =
+            coroutineScope.launch(mainDispatcher) {
+                dialog = createBluetoothTileDialog(context)
+                view?.let { dialogLaunchAnimator.showFromView(dialog!!, it) } ?: dialog!!.show()
+                updateDeviceItemJob?.cancel()
+                updateDeviceItemJob = launch { deviceItemInteractor.updateDeviceItems(context) }
 
-            view?.let { dialogLaunchAnimator.showFromView(dialog!!, it) } ?: dialog!!.show()
-        }
+                bluetoothStateInteractor.updateBluetoothStateFlow
+                    .filterNotNull()
+                    .onEach {
+                        dialog!!.onBluetoothStateUpdated(it)
+                        updateDeviceItemJob?.cancel()
+                        updateDeviceItemJob = launch {
+                            deviceItemInteractor.updateDeviceItems(context)
+                        }
+                    }
+                    .launchIn(this)
+
+                deviceItemInteractor.updateDeviceItemsFlow
+                    .onEach {
+                        updateDeviceItemJob?.cancel()
+                        updateDeviceItemJob = launch {
+                            deviceItemInteractor.updateDeviceItems(context)
+                        }
+                    }
+                    .launchIn(this)
+
+                deviceItemInteractor.deviceItemFlow
+                    .filterNotNull()
+                    .onEach {
+                        dialog!!.onDeviceItemUpdated(
+                            it.take(MAX_DEVICE_ITEM_ENTRY),
+                            showSeeAll = it.size > MAX_DEVICE_ITEM_ENTRY
+                        )
+                    }
+                    .launchIn(this)
+
+                dialog!!
+                    .bluetoothStateSwitchedFlow
+                    .filterNotNull()
+                    .onEach { bluetoothStateInteractor.isBluetoothEnabled = it }
+                    .launchIn(this)
+
+                dialog!!
+                    .deviceItemClickedFlow
+                    .onEach {
+                        if (deviceItemInteractor.updateDeviceItemOnClick(it.first)) {
+                            dialog!!.onDeviceItemUpdatedAtPosition(it.first, it.second)
+                        }
+                    }
+                    .launchIn(this)
+            }
     }
 
-    override fun onDeviceItemClicked(deviceItem: DeviceItem, position: Int) {
-        if (deviceItemInteractor.updateDeviceItemOnClick(deviceItem)) {
-            dialog?.onDeviceItemUpdated(deviceItem, position)
-        }
+    private fun createBluetoothTileDialog(context: Context): BluetoothTileDialog {
+        return BluetoothTileDialog(
+                bluetoothStateInteractor.isBluetoothEnabled,
+                this@BluetoothTileDialogViewModel,
+                context
+            )
+            .apply { SystemUIDialog.registerDismissListener(this) { dismissDialog() } }
     }
 
     private fun dismissDialog() {
+        job?.cancel()
+        job = null
         dialog?.dismiss()
         dialog = null
     }
 }
 
-internal interface DeviceItemOnClickCallback {
-    fun onDeviceItemClicked(deviceItem: DeviceItem, position: Int)
+internal interface BluetoothTileDialogCallback {
+    // TODO(b/298124674): Add click events for gear, see all and pair new device.
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt
index bbe5127..6ffb614 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt
@@ -20,9 +20,25 @@
 import android.bluetooth.BluetoothDevice
 import android.content.Context
 import android.media.AudioManager
+import com.android.settingslib.bluetooth.BluetoothCallback
 import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+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 javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.withContext
 
 /** Holds business logic for the Bluetooth Dialog after clicking on the Bluetooth QS tile. */
 @SysUISingleton
@@ -31,8 +47,59 @@
 constructor(
     private val bluetoothTileDialogRepository: BluetoothTileDialogRepository,
     private val audioManager: AudioManager,
-    private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
+    private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter(),
+    private val localBluetoothManager: LocalBluetoothManager?,
+    @Application private val coroutineScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
 ) {
+
+    private val mutableDeviceItemFlow: MutableStateFlow<List<DeviceItem>?> = MutableStateFlow(null)
+    internal val deviceItemFlow
+        get() = mutableDeviceItemFlow.asStateFlow()
+
+    internal val updateDeviceItemsFlow: SharedFlow<Unit> =
+        conflatedCallbackFlow {
+                val listener =
+                    object : BluetoothCallback {
+                        override fun onActiveDeviceChanged(
+                            activeDevice: CachedBluetoothDevice?,
+                            bluetoothProfile: Int
+                        ) {
+                            super.onActiveDeviceChanged(activeDevice, bluetoothProfile)
+                            trySendWithFailureLogging(Unit, TAG, "onActiveDeviceChanged")
+                        }
+
+                        override fun onConnectionStateChanged(
+                            cachedDevice: CachedBluetoothDevice?,
+                            state: Int
+                        ) {
+                            super.onConnectionStateChanged(cachedDevice, state)
+                            trySendWithFailureLogging(Unit, TAG, "onConnectionStateChanged")
+                        }
+
+                        override fun onDeviceAdded(cachedDevice: CachedBluetoothDevice) {
+                            super.onDeviceAdded(cachedDevice)
+                            trySendWithFailureLogging(Unit, TAG, "onDeviceAdded")
+                        }
+
+                        override fun onProfileConnectionStateChanged(
+                            cachedDevice: CachedBluetoothDevice,
+                            state: Int,
+                            bluetoothProfile: Int
+                        ) {
+                            super.onProfileConnectionStateChanged(
+                                cachedDevice,
+                                state,
+                                bluetoothProfile
+                            )
+                            trySendWithFailureLogging(Unit, TAG, "onProfileConnectionStateChanged")
+                        }
+                    }
+                localBluetoothManager?.eventManager?.registerCallback(listener)
+                awaitClose { localBluetoothManager?.eventManager?.unregisterCallback(listener) }
+            }
+            .shareIn(coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0))
+
     private var deviceItemFactoryList: List<DeviceItemFactory> =
         listOf(
             AvailableMediaDeviceItemFactory(),
@@ -47,16 +114,19 @@
             DeviceItemType.SAVED_BLUETOOTH_DEVICE,
         )
 
-    internal fun getDeviceItems(context: Context): List<DeviceItem> {
-        val mostRecentlyConnectedDevices = bluetoothAdapter?.mostRecentlyConnectedDevices
+    internal suspend fun updateDeviceItems(context: Context) {
+        withContext(backgroundDispatcher) {
+            val mostRecentlyConnectedDevices = bluetoothAdapter?.mostRecentlyConnectedDevices
 
-        return bluetoothTileDialogRepository.cachedDevices
-            .mapNotNull { cachedDevice ->
-                deviceItemFactoryList
-                    .firstOrNull { it.isFilterMatched(cachedDevice, audioManager) }
-                    ?.create(context, cachedDevice)
-            }
-            .sort(displayPriority, mostRecentlyConnectedDevices)
+            mutableDeviceItemFlow.value =
+                bluetoothTileDialogRepository.cachedDevices
+                    .mapNotNull { cachedDevice ->
+                        deviceItemFactoryList
+                            .firstOrNull { it.isFilterMatched(cachedDevice, audioManager) }
+                            ?.create(context, cachedDevice)
+                    }
+                    .sort(displayPriority, mostRecentlyConnectedDevices)
+        }
     }
 
     private fun List<DeviceItem>.sort(
@@ -100,4 +170,8 @@
     internal fun setDisplayPriorityForTesting(list: List<DeviceItemType>) {
         displayPriority = list
     }
+
+    companion object {
+        private const val TAG = "DeviceItemInteractor"
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractorTest.kt
new file mode 100644
index 0000000..fc2b7a64
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractorTest.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.qs.tiles.dialog.bluetooth
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.LocalBluetoothAdapter
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class BluetoothStateInteractorTest : SysuiTestCase() {
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+    private val testScope = TestScope()
+
+    private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
+
+    @Mock private lateinit var bluetoothAdapter: LocalBluetoothAdapter
+    @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
+
+    @Before
+    fun setUp() {
+        bluetoothStateInteractor =
+            BluetoothStateInteractor(localBluetoothManager, testScope.backgroundScope)
+        `when`(localBluetoothManager.bluetoothAdapter).thenReturn(bluetoothAdapter)
+    }
+
+    @Test
+    fun testGet_isBluetoothEnabled() {
+        testScope.runTest {
+            `when`(bluetoothAdapter.isEnabled).thenReturn(true)
+
+            assertThat(bluetoothStateInteractor.isBluetoothEnabled).isTrue()
+        }
+    }
+
+    @Test
+    fun testGet_isBluetoothDisabled() {
+        testScope.runTest {
+            `when`(bluetoothAdapter.isEnabled).thenReturn(false)
+
+            assertThat(bluetoothStateInteractor.isBluetoothEnabled).isFalse()
+        }
+    }
+
+    @Test
+    fun testSet_bluetoothEnabled() {
+        testScope.runTest {
+            `when`(bluetoothAdapter.isEnabled).thenReturn(false)
+
+            bluetoothStateInteractor.isBluetoothEnabled = true
+            verify(bluetoothAdapter).enable()
+        }
+    }
+
+    @Test
+    fun testSet_bluetoothNoChange() {
+        testScope.runTest {
+            `when`(bluetoothAdapter.isEnabled).thenReturn(false)
+
+            bluetoothStateInteractor.isBluetoothEnabled = false
+            verify(bluetoothAdapter, never()).enable()
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
index c00f6d8..e1d177d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
@@ -21,6 +21,7 @@
 import android.testing.TestableLooper
 import android.view.LayoutInflater
 import android.view.View
+import android.view.View.GONE
 import android.view.View.VISIBLE
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
@@ -47,13 +48,14 @@
     companion object {
         const val DEVICE_NAME = "device"
         const val DEVICE_CONNECTION_SUMMARY = "active"
+        const val ENABLED = true
     }
 
     @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
 
     @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice
 
-    @Mock private lateinit var deviceItemOnClickCallback: DeviceItemOnClickCallback
+    @Mock private lateinit var bluetoothTileDialogCallback: BluetoothTileDialogCallback
 
     @Mock private lateinit var drawable: Drawable
 
@@ -63,7 +65,7 @@
 
     @Before
     fun setUp() {
-        bluetoothTileDialog = BluetoothTileDialog(emptyList(), deviceItemOnClickCallback, mContext)
+        bluetoothTileDialog = BluetoothTileDialog(ENABLED, bluetoothTileDialogCallback, mContext)
         icon = Pair(drawable, DEVICE_NAME)
         deviceItem =
             DeviceItem(
@@ -92,10 +94,9 @@
 
     @Test
     fun testShowDialog_displayBluetoothDevice() {
-        bluetoothTileDialog =
-            BluetoothTileDialog(listOf(deviceItem), deviceItemOnClickCallback, mContext)
-
+        bluetoothTileDialog = BluetoothTileDialog(ENABLED, bluetoothTileDialogCallback, mContext)
         bluetoothTileDialog.show()
+        bluetoothTileDialog.onDeviceItemUpdated(listOf(deviceItem), false)
 
         val recyclerView = bluetoothTileDialog.findViewById<RecyclerView>(R.id.device_list)
         val adapter = recyclerView?.adapter as BluetoothTileDialog.Adapter
@@ -112,8 +113,11 @@
 
         val view =
             LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
-        val viewHolder = BluetoothTileDialog.Adapter.DeviceItemViewHolder(view)
-        viewHolder.bind(deviceItem, 0, deviceItemOnClickCallback)
+        val viewHolder =
+            BluetoothTileDialog(ENABLED, bluetoothTileDialogCallback, mContext)
+                .Adapter()
+                .DeviceItemViewHolder(view)
+        viewHolder.bind(deviceItem, 0)
         val container = view.findViewById<View>(R.id.bluetooth_device)
 
         assertThat(container).isNotNull()
@@ -129,8 +133,11 @@
 
         val view =
             LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
-        val viewHolder = BluetoothTileDialog.Adapter.DeviceItemViewHolder(view)
-        viewHolder.bind(deviceItem, 0, deviceItemOnClickCallback)
+        val viewHolder =
+            BluetoothTileDialog(ENABLED, bluetoothTileDialogCallback, mContext)
+                .Adapter()
+                .DeviceItemViewHolder(view)
+        viewHolder.bind(deviceItem, 0)
         val container = view.findViewById<View>(R.id.bluetooth_device)
 
         assertThat(container).isNotNull()
@@ -138,4 +145,19 @@
         assertThat(container.alpha).isEqualTo(DISABLED_ALPHA)
         assertThat(container.hasOnClickListeners()).isTrue()
     }
+
+    @Test
+    fun testOnDeviceUpdated_hideSeeAll() {
+        bluetoothTileDialog = BluetoothTileDialog(ENABLED, bluetoothTileDialogCallback, mContext)
+        bluetoothTileDialog.show()
+        bluetoothTileDialog.onDeviceItemUpdated(listOf(deviceItem), false)
+
+        val seeAllLayout = bluetoothTileDialog.findViewById<View>(R.id.see_all_layout)
+        val recyclerView = bluetoothTileDialog.findViewById<RecyclerView>(R.id.device_list)
+        val adapter = recyclerView?.adapter as BluetoothTileDialog.Adapter
+
+        assertThat(seeAllLayout).isNotNull()
+        assertThat(seeAllLayout!!.visibility).isEqualTo(GONE)
+        assertThat(adapter.itemCount).isEqualTo(1)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
index dd5aacf..975f1e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.qs.tiles.dialog.bluetooth
 
-import android.os.Handler
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.widget.LinearLayout
@@ -28,7 +27,13 @@
 import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
-import org.junit.After
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -52,56 +57,87 @@
 
     private lateinit var bluetoothTileDialogViewModel: BluetoothTileDialogViewModel
 
+    @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
+
     @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
 
     @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
 
-    private lateinit var testableLooper: TestableLooper
+    private lateinit var scheduler: TestCoroutineScheduler
+    private lateinit var dispatcher: CoroutineDispatcher
+    private lateinit var testScope: TestScope
 
     @Before
     fun setUp() {
-        testableLooper = TestableLooper.get(this)
-        `when`(deviceItemInteractor.getDeviceItems(any())).thenReturn(emptyList())
+        scheduler = TestCoroutineScheduler()
+        dispatcher = UnconfinedTestDispatcher(scheduler)
+        testScope = TestScope(dispatcher)
         bluetoothTileDialogViewModel =
             BluetoothTileDialogViewModel(
                 deviceItemInteractor,
+                bluetoothStateInteractor,
                 dialogLaunchAnimator,
-                Handler(testableLooper.looper)
+                testScope.backgroundScope,
+                dispatcher,
             )
-    }
-
-    @After
-    fun tearDown() {
-        testableLooper.processAllMessages()
+        `when`(deviceItemInteractor.deviceItemFlow).thenReturn(MutableStateFlow(null).asStateFlow())
+        `when`(bluetoothStateInteractor.updateBluetoothStateFlow)
+            .thenReturn(MutableStateFlow(null).asStateFlow())
+        `when`(deviceItemInteractor.updateDeviceItemsFlow)
+            .thenReturn(MutableStateFlow(Unit).asStateFlow())
+        `when`(bluetoothStateInteractor.isBluetoothEnabled).thenReturn(true)
     }
 
     @Test
     fun testShowDialog_noAnimation() {
-        bluetoothTileDialogViewModel.showDialog(context, null)
-        testableLooper.processAllMessages()
+        testScope.runTest {
+            bluetoothTileDialogViewModel.showDialog(context, null)
 
-        assertThat(bluetoothTileDialogViewModel.dialog).isNotNull()
-        verify(dialogLaunchAnimator, never()).showFromView(any(), any(), any(), any())
-        assertThat(bluetoothTileDialogViewModel.dialog?.isShowing).isTrue()
+            assertThat(bluetoothTileDialogViewModel.dialog).isNotNull()
+            verify(dialogLaunchAnimator, never()).showFromView(any(), any(), any(), any())
+            assertThat(bluetoothTileDialogViewModel.dialog?.isShowing).isTrue()
+        }
     }
 
     @Test
     fun testShowDialog_animated() {
-        bluetoothTileDialogViewModel.showDialog(mContext, LinearLayout(mContext))
-        testableLooper.processAllMessages()
+        testScope.runTest {
+            bluetoothTileDialogViewModel.showDialog(mContext, LinearLayout(mContext))
 
-        assertThat(bluetoothTileDialogViewModel.dialog).isNotNull()
-        verify(dialogLaunchAnimator).showFromView(any(), any(), nullable(), anyBoolean())
+            assertThat(bluetoothTileDialogViewModel.dialog).isNotNull()
+            verify(dialogLaunchAnimator).showFromView(any(), any(), nullable(), anyBoolean())
+        }
     }
 
     @Test
     fun testShowDialog_animated_callInBackgroundThread() {
-        backgroundExecutor.execute {
-            bluetoothTileDialogViewModel.showDialog(mContext, LinearLayout(mContext))
-            testableLooper.processAllMessages()
+        testScope.runTest {
+            backgroundExecutor.execute {
+                bluetoothTileDialogViewModel.showDialog(mContext, LinearLayout(mContext))
+
+                assertThat(bluetoothTileDialogViewModel.dialog).isNotNull()
+                verify(dialogLaunchAnimator).showFromView(any(), any(), nullable(), anyBoolean())
+            }
+        }
+    }
+
+    @Test
+    fun testShowDialog_fetchDeviceItem() {
+        testScope.runTest {
+            bluetoothTileDialogViewModel.showDialog(context, null)
 
             assertThat(bluetoothTileDialogViewModel.dialog).isNotNull()
-            verify(dialogLaunchAnimator).showFromView(any(), any(), nullable(), anyBoolean())
+            verify(deviceItemInteractor).deviceItemFlow
+        }
+    }
+
+    @Test
+    fun testShowDialog_withBluetoothStateValue() {
+        testScope.runTest {
+            bluetoothTileDialogViewModel.showDialog(context, null)
+
+            assertThat(bluetoothTileDialogViewModel.dialog).isNotNull()
+            verify(bluetoothStateInteractor).updateBluetoothStateFlow
         }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt
index d04caaf..df9914a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt
@@ -24,8 +24,13 @@
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothManager
 import com.android.systemui.SysuiTestCase
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -60,11 +65,27 @@
 
     @Mock private lateinit var adapter: BluetoothAdapter
 
+    @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
+
     private lateinit var interactor: DeviceItemInteractor
 
+    private lateinit var dispatcher: CoroutineDispatcher
+
+    private lateinit var testScope: TestScope
+
     @Before
     fun setUp() {
-        interactor = DeviceItemInteractor(bluetoothTileDialogRepository, audioManager, adapter)
+        dispatcher = StandardTestDispatcher()
+        testScope = TestScope(dispatcher)
+        interactor =
+            DeviceItemInteractor(
+                bluetoothTileDialogRepository,
+                audioManager,
+                adapter,
+                localBluetoothManager,
+                testScope.backgroundScope,
+                dispatcher
+            )
 
         `when`(deviceItem1.cachedBluetoothDevice).thenReturn(cachedDevice1)
         `when`(deviceItem2.cachedBluetoothDevice).thenReturn(cachedDevice2)
@@ -75,88 +96,109 @@
     }
 
     @Test
-    fun testGetDeviceItems_noCachedDevice_returnEmpty() {
-        `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(emptyList())
-        interactor.setDeviceItemFactoryListForTesting(listOf(createFactory({ true }, deviceItem1)))
-
-        val deviceItems = interactor.getDeviceItems(mContext)
-
-        assertThat(deviceItems).isEmpty()
-    }
-
-    @Test
-    fun testGetDeviceItems_hasCachedDevice_filterNotMatch_returnEmpty() {
-        `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(listOf(cachedDevice1))
-        interactor.setDeviceItemFactoryListForTesting(listOf(createFactory({ false }, deviceItem1)))
-
-        val deviceItems = interactor.getDeviceItems(mContext)
-
-        assertThat(deviceItems).isEmpty()
-    }
-
-    @Test
-    fun testGetDeviceItems_hasCachedDevice_filterMatch_returnDeviceItem() {
-        `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(listOf(cachedDevice1))
-        interactor.setDeviceItemFactoryListForTesting(listOf(createFactory({ true }, deviceItem1)))
-
-        val deviceItems = interactor.getDeviceItems(mContext)
-
-        assertThat(deviceItems).hasSize(1)
-        assertThat(deviceItems[0]).isEqualTo(deviceItem1)
-    }
-
-    @Test
-    fun testGetDeviceItems_hasCachedDevice_filterMatch_returnMultipleDeviceItem() {
-        `when`(adapter.mostRecentlyConnectedDevices).thenReturn(null)
-        interactor.setDeviceItemFactoryListForTesting(
-            listOf(createFactory({ false }, deviceItem1), createFactory({ true }, deviceItem2))
-        )
-
-        val deviceItems = interactor.getDeviceItems(mContext)
-
-        assertThat(deviceItems).hasSize(2)
-        assertThat(deviceItems[0]).isEqualTo(deviceItem2)
-        assertThat(deviceItems[1]).isEqualTo(deviceItem2)
-    }
-
-    @Test
-    fun testGetDeviceItems_sortByDisplayPriority() {
-        `when`(adapter.mostRecentlyConnectedDevices).thenReturn(null)
-        interactor.setDeviceItemFactoryListForTesting(
-            listOf(
-                createFactory({ cachedDevice -> cachedDevice.device == device1 }, deviceItem1),
-                createFactory({ cachedDevice -> cachedDevice.device == device2 }, deviceItem2)
+    fun testUpdateDeviceItems_noCachedDevice_returnEmpty() {
+        testScope.runTest {
+            `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(emptyList())
+            interactor.setDeviceItemFactoryListForTesting(
+                listOf(createFactory({ true }, deviceItem1))
             )
-        )
-        interactor.setDisplayPriorityForTesting(
-            listOf(DeviceItemType.SAVED_BLUETOOTH_DEVICE, DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
-        )
-        `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
-        `when`(deviceItem2.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE)
 
-        val deviceItems = interactor.getDeviceItems(mContext)
+            interactor.updateDeviceItems(mContext)
 
-        assertThat(deviceItems).isEqualTo(listOf(deviceItem2, deviceItem1))
+            assertThat(interactor.deviceItemFlow.value).isEmpty()
+        }
     }
 
     @Test
-    fun testGetDeviceItems_sameType_sortByRecentlyConnected() {
-        `when`(adapter.mostRecentlyConnectedDevices).thenReturn(listOf(device2, device1))
-        interactor.setDeviceItemFactoryListForTesting(
-            listOf(
-                createFactory({ cachedDevice -> cachedDevice.device == device1 }, deviceItem1),
-                createFactory({ cachedDevice -> cachedDevice.device == device2 }, deviceItem2)
+    fun testUpdateDeviceItems_hasCachedDevice_filterNotMatch_returnEmpty() {
+        testScope.runTest {
+            `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(listOf(cachedDevice1))
+            interactor.setDeviceItemFactoryListForTesting(
+                listOf(createFactory({ false }, deviceItem1))
             )
-        )
-        interactor.setDisplayPriorityForTesting(
-            listOf(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
-        )
-        `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
-        `when`(deviceItem2.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
 
-        val deviceItems = interactor.getDeviceItems(mContext)
+            interactor.updateDeviceItems(mContext)
 
-        assertThat(deviceItems).isEqualTo(listOf(deviceItem2, deviceItem1))
+            assertThat(interactor.deviceItemFlow.value).isEmpty()
+        }
+    }
+
+    @Test
+    fun testUpdateDeviceItems_hasCachedDevice_filterMatch_returnDeviceItem() {
+        testScope.runTest {
+            `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(listOf(cachedDevice1))
+            interactor.setDeviceItemFactoryListForTesting(
+                listOf(createFactory({ true }, deviceItem1))
+            )
+
+            interactor.updateDeviceItems(mContext)
+
+            assertThat(interactor.deviceItemFlow.value).hasSize(1)
+            assertThat(interactor.deviceItemFlow.value!![0]).isEqualTo(deviceItem1)
+        }
+    }
+
+    @Test
+    fun testUpdateDeviceItems_hasCachedDevice_filterMatch_returnMultipleDeviceItem() {
+        testScope.runTest {
+            `when`(adapter.mostRecentlyConnectedDevices).thenReturn(null)
+            interactor.setDeviceItemFactoryListForTesting(
+                listOf(createFactory({ false }, deviceItem1), createFactory({ true }, deviceItem2))
+            )
+
+            interactor.updateDeviceItems(mContext)
+
+            assertThat(interactor.deviceItemFlow.value).hasSize(2)
+            assertThat(interactor.deviceItemFlow.value!![0]).isEqualTo(deviceItem2)
+            assertThat(interactor.deviceItemFlow.value!![1]).isEqualTo(deviceItem2)
+        }
+    }
+
+    @Test
+    fun testUpdateDeviceItems_sortByDisplayPriority() {
+        testScope.runTest {
+            `when`(adapter.mostRecentlyConnectedDevices).thenReturn(null)
+            interactor.setDeviceItemFactoryListForTesting(
+                listOf(
+                    createFactory({ cachedDevice -> cachedDevice.device == device1 }, deviceItem1),
+                    createFactory({ cachedDevice -> cachedDevice.device == device2 }, deviceItem2)
+                )
+            )
+            interactor.setDisplayPriorityForTesting(
+                listOf(
+                    DeviceItemType.SAVED_BLUETOOTH_DEVICE,
+                    DeviceItemType.CONNECTED_BLUETOOTH_DEVICE
+                )
+            )
+            `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+            `when`(deviceItem2.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE)
+
+            interactor.updateDeviceItems(mContext)
+
+            assertThat(interactor.deviceItemFlow.value).isEqualTo(listOf(deviceItem2, deviceItem1))
+        }
+    }
+
+    @Test
+    fun testUpdateDeviceItems_sameType_sortByRecentlyConnected() {
+        testScope.runTest {
+            `when`(adapter.mostRecentlyConnectedDevices).thenReturn(listOf(device2, device1))
+            interactor.setDeviceItemFactoryListForTesting(
+                listOf(
+                    createFactory({ cachedDevice -> cachedDevice.device == device1 }, deviceItem1),
+                    createFactory({ cachedDevice -> cachedDevice.device == device2 }, deviceItem2)
+                )
+            )
+            interactor.setDisplayPriorityForTesting(
+                listOf(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+            )
+            `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+            `when`(deviceItem2.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+
+            interactor.updateDeviceItems(mContext)
+
+            assertThat(interactor.deviceItemFlow.value).isEqualTo(listOf(deviceItem2, deviceItem1))
+        }
     }
 
     private fun createFactory(