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(