summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractor.kt91
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogLogger.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt14
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt37
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorTest.kt226
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt31
11 files changed, 435 insertions, 16 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractor.kt
new file mode 100644
index 000000000000..d69e41626bd6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractor.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+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 java.util.concurrent.Executor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.merge
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class BluetoothDeviceMetadataInteractor
+@Inject
+constructor(
+ deviceItemInteractor: DeviceItemInteractor,
+ private val bluetoothAdapter: BluetoothAdapter?,
+ private val logger: BluetoothTileDialogLogger,
+ @Background private val executor: Executor,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+) {
+ private fun metadataUpdateForDevice(bluetoothDevice: BluetoothDevice): Flow<Unit> =
+ conflatedCallbackFlow {
+ val metadataChangedListener =
+ BluetoothAdapter.OnMetadataChangedListener { device, key, value ->
+ when (key) {
+ BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY,
+ BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY,
+ BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY,
+ BluetoothDevice.METADATA_MAIN_BATTERY -> {
+ trySendWithFailureLogging(Unit, TAG, "onMetadataChanged")
+ logger.logBatteryChanged(device.address, key, value)
+ }
+ }
+ }
+ bluetoothAdapter?.addOnMetadataChangedListener(
+ bluetoothDevice,
+ executor,
+ metadataChangedListener
+ )
+ awaitClose {
+ bluetoothAdapter?.removeOnMetadataChangedListener(
+ bluetoothDevice,
+ metadataChangedListener
+ )
+ }
+ }
+
+ val metadataUpdate: Flow<Unit> =
+ deviceItemInteractor.deviceItemUpdate
+ .distinctUntilChangedBy { it.bluetoothDevices }
+ .flatMapLatest { items ->
+ items.bluetoothDevices.map { device -> metadataUpdateForDevice(device) }.merge()
+ }
+ .flowOn(backgroundDispatcher)
+
+ private companion object {
+ private const val TAG = "BluetoothDeviceMetadataInteractor"
+ private val List<DeviceItem>.bluetoothDevices: Set<BluetoothDevice>
+ get() =
+ flatMapTo(mutableSetOf()) { item ->
+ listOf(item.cachedBluetoothDevice.device) +
+ item.cachedBluetoothDevice.memberDevice.map { it.device }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
index f5b9a050f33e..5d1613608861 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
@@ -440,7 +440,6 @@ internal constructor(
internal companion object {
const val MIN_HEIGHT_CHANGE_INTERVAL_MS = 800L
- const val MAX_DEVICE_ITEM_ENTRY = 3
const val ACTION_BLUETOOTH_DEVICE_DETAILS =
"com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS"
const val ACTION_PREVIOUSLY_CONNECTED_DEVICE =
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogLogger.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogLogger.kt
index 72312b87dc57..06116f0a21c3 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogLogger.kt
@@ -90,6 +90,18 @@ constructor(@BluetoothTileDialogLog private val logBuffer: LogBuffer) {
{ "ProfileConnectionStateChanged. address=$str1 state=$str2 profileId=$int1" }
)
+ fun logBatteryChanged(address: String, key: Int, value: ByteArray?) =
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = address
+ int1 = key
+ str2 = value?.toString() ?: ""
+ },
+ { "BatteryChanged. address=$str1 key=$int1 value=$str2" }
+ )
+
fun logDeviceFetch(status: JobStatus, trigger: DeviceFetchTrigger, duration: Long) =
logBuffer.log(
TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogRepository.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogRepository.kt
index 6e51915797cc..56b79d199e09 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogRepository.kt
@@ -24,7 +24,7 @@ import javax.inject.Inject
/** Repository to get CachedBluetoothDevices for the Bluetooth Dialog. */
@SysUISingleton
-internal class BluetoothTileDialogRepository
+class BluetoothTileDialogRepository
@Inject
constructor(
private val localBluetoothManager: LocalBluetoothManager?,
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
index eaddc42dcd5a..36764f2ab994 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
@@ -37,7 +37,6 @@ import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Compa
import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_BLUETOOTH_DEVICE_DETAILS
import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PAIR_NEW_DEVICE
import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE
-import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.MAX_DEVICE_ITEM_ENTRY
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
@@ -50,8 +49,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.produce
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -66,6 +67,7 @@ constructor(
private val bluetoothStateInteractor: BluetoothStateInteractor,
private val bluetoothAutoOnInteractor: BluetoothAutoOnInteractor,
private val audioSharingInteractor: AudioSharingInteractor,
+ private val bluetoothDeviceMetadataInteractor: BluetoothDeviceMetadataInteractor,
private val dialogTransitionAnimator: DialogTransitionAnimator,
private val activityStarter: ActivityStarter,
private val uiEventLogger: UiEventLogger,
@@ -104,8 +106,7 @@ constructor(
)
controller?.let {
dialogTransitionAnimator.show(dialog, it, animateBackgroundBoundsChange = true)
- }
- ?: dialog.show()
+ } ?: dialog.show()
updateDeviceItemJob = launch {
deviceItemInteractor.updateDeviceItems(context, DeviceFetchTrigger.FIRST_LOAD)
@@ -113,15 +114,17 @@ constructor(
// deviceItemUpdate is emitted when device item list is done fetching, update UI and
// stop the progress bar.
- deviceItemInteractor.deviceItemUpdate
- .onEach {
+ combine(
+ deviceItemInteractor.deviceItemUpdate,
+ deviceItemInteractor.showSeeAllUpdate
+ ) { deviceItem, showSeeAll ->
updateDialogUiJob?.cancel()
updateDialogUiJob = launch {
dialogDelegate.apply {
onDeviceItemUpdated(
dialog,
- it.take(MAX_DEVICE_ITEM_ENTRY),
- showSeeAll = it.size > MAX_DEVICE_ITEM_ENTRY,
+ deviceItem,
+ showSeeAll,
showPairNewDevice =
bluetoothStateInteractor.isBluetoothEnabled()
)
@@ -132,8 +135,11 @@ constructor(
.launchIn(this)
// deviceItemUpdateRequest is emitted when a bluetooth callback is called, re-fetch
- // the device item list and animiate the progress bar.
- deviceItemInteractor.deviceItemUpdateRequest
+ // the device item list and animate the progress bar.
+ merge(
+ deviceItemInteractor.deviceItemUpdateRequest,
+ bluetoothDeviceMetadataInteractor.metadataUpdate
+ )
.onEach {
dialogDelegate.animateProgressBar(dialog, true)
updateDeviceItemJob?.cancel()
@@ -305,6 +311,7 @@ constructor(
companion object {
private const val INTERACTION_JANK_TAG = "bluetooth_tile_dialog"
private const val CONTENT_HEIGHT_PREF_KEY = Prefs.Key.BLUETOOTH_TILE_DIALOG_CONTENT_HEIGHT
+
private fun getSubtitleResId(isBluetoothEnabled: Boolean) =
if (isBluetoothEnabled) R.string.quick_settings_bluetooth_tile_subtitle
else R.string.bt_is_off
@@ -336,7 +343,10 @@ constructor(
interface BluetoothTileDialogCallback {
fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View)
+
fun onSeeAllClicked(view: View)
+
fun onPairNewDeviceClicked(view: View)
+
fun onAudioSharingButtonClicked(view: View)
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
index d7893dbb0f90..e846bf7b523c 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
@@ -38,7 +38,7 @@ private val actionAccessibilityLabelDisconnect =
R.string.accessibility_quick_settings_bluetooth_device_tap_to_disconnect
/** Factories to create different types of Bluetooth device items from CachedBluetoothDevice. */
-internal abstract class DeviceItemFactory {
+abstract class DeviceItemFactory {
abstract fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
@@ -136,7 +136,7 @@ internal class ActiveHearingDeviceItemFactory : ActiveMediaDeviceItemFactory() {
}
}
-internal open class AvailableMediaDeviceItemFactory : DeviceItemFactory() {
+open class AvailableMediaDeviceItemFactory : DeviceItemFactory() {
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
index 1526cd9675c7..95244964dc44 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
@@ -34,16 +34,18 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
/** Holds business logic for the Bluetooth Dialog after clicking on the Bluetooth QS tile. */
@SysUISingleton
-internal class DeviceItemInteractor
+class DeviceItemInteractor
@Inject
constructor(
private val bluetoothTileDialogRepository: BluetoothTileDialogRepository,
@@ -58,9 +60,13 @@ constructor(
private val mutableDeviceItemUpdate: MutableSharedFlow<List<DeviceItem>> =
MutableSharedFlow(extraBufferCapacity = 1)
- internal val deviceItemUpdate
+ val deviceItemUpdate
get() = mutableDeviceItemUpdate.asSharedFlow()
+ private val mutableShowSeeAllUpdate: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ internal val showSeeAllUpdate
+ get() = mutableShowSeeAllUpdate.asStateFlow()
+
internal val deviceItemUpdateRequest: SharedFlow<Unit> =
conflatedCallbackFlow {
val listener =
@@ -139,7 +145,8 @@ constructor(
.sort(displayPriority, bluetoothAdapter?.mostRecentlyConnectedDevices)
// Only emit when the job is not cancelled
if (isActive) {
- mutableDeviceItemUpdate.tryEmit(deviceItems)
+ mutableDeviceItemUpdate.tryEmit(deviceItems.take(MAX_DEVICE_ITEM_ENTRY))
+ mutableShowSeeAllUpdate.tryEmit(deviceItems.size > MAX_DEVICE_ITEM_ENTRY)
logger.logDeviceFetch(
JobStatus.FINISHED,
trigger,
@@ -177,5 +184,6 @@ constructor(
companion object {
private const val TAG = "DeviceItemInteractor"
+ private const val MAX_DEVICE_ITEM_ENTRY = 3
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt
new file mode 100644
index 000000000000..969e26a8d884
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import com.android.systemui.bluetooth.bluetoothAdapter
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import org.mockito.kotlin.mock
+
+val Kosmos.deviceItemInteractor: DeviceItemInteractor by
+ Kosmos.Fixture { mock<DeviceItemInteractor>() }
+
+val Kosmos.bluetoothDeviceMetadataInteractor by
+ Kosmos.Fixture {
+ BluetoothDeviceMetadataInteractor(
+ deviceItemInteractor,
+ bluetoothAdapter,
+ bluetoothTileDialogLogger,
+ fakeExecutor,
+ testDispatcher,
+ )
+ }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorTest.kt
new file mode 100644
index 000000000000..f06b105a9e26
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorTest.kt
@@ -0,0 +1,226 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bluetooth.bluetoothAdapter
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
+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.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class BluetoothDeviceMetadataInteractorTest : SysuiTestCase() {
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() }
+
+ private val deviceItemUpdate: MutableSharedFlow<List<DeviceItem>> = MutableSharedFlow()
+ @Mock private lateinit var cachedDevice1: CachedBluetoothDevice
+ @Mock private lateinit var bluetoothDevice1: BluetoothDevice
+ @Mock private lateinit var cachedDevice2: CachedBluetoothDevice
+ @Mock private lateinit var bluetoothDevice2: BluetoothDevice
+ @Captor
+ private lateinit var argumentCaptor: ArgumentCaptor<BluetoothAdapter.OnMetadataChangedListener>
+ private lateinit var interactor: BluetoothDeviceMetadataInteractor
+
+ @Before
+ fun setUp() {
+ with(kosmos) {
+ whenever(deviceItemInteractor.deviceItemUpdate).thenReturn(deviceItemUpdate)
+
+ whenever(cachedDevice1.device).thenReturn(bluetoothDevice1)
+ whenever(cachedDevice1.name).thenReturn(DEVICE_NAME)
+ whenever(cachedDevice1.address).thenReturn(DEVICE_ADDRESS)
+ whenever(cachedDevice1.connectionSummary).thenReturn(CONNECTION_SUMMARY)
+ whenever(bluetoothDevice1.address).thenReturn(DEVICE_ADDRESS)
+
+ whenever(cachedDevice2.device).thenReturn(bluetoothDevice2)
+ whenever(cachedDevice2.name).thenReturn(DEVICE_NAME)
+ whenever(cachedDevice2.address).thenReturn(DEVICE_ADDRESS)
+ whenever(cachedDevice2.connectionSummary).thenReturn(CONNECTION_SUMMARY)
+ whenever(bluetoothDevice2.address).thenReturn(DEVICE_ADDRESS)
+
+ interactor = bluetoothDeviceMetadataInteractor
+ }
+ }
+
+ @Test
+ fun deviceItemUpdateEmpty_doNothing() {
+ with(kosmos) {
+ testScope.runTest {
+ val update by collectLastValue(interactor.metadataUpdate)
+ deviceItemUpdate.emit(emptyList())
+ runCurrent()
+
+ assertThat(update).isNull()
+ verify(bluetoothAdapter, never()).addOnMetadataChangedListener(any(), any(), any())
+ verify(bluetoothAdapter, never()).removeOnMetadataChangedListener(any(), any())
+ }
+ }
+ }
+
+ @Test
+ fun deviceItemUpdate_registerListener() {
+ with(kosmos) {
+ testScope.runTest {
+ val deviceItem = AvailableMediaDeviceItemFactory().create(context, cachedDevice1)
+ val update by collectLastValue(interactor.metadataUpdate)
+ deviceItemUpdate.emit(listOf(deviceItem))
+ runCurrent()
+
+ assertThat(update).isNull()
+ verify(bluetoothAdapter)
+ .addOnMetadataChangedListener(eq(bluetoothDevice1), any(), any())
+ verify(bluetoothAdapter, never()).removeOnMetadataChangedListener(any(), any())
+ }
+ }
+ }
+
+ @Test
+ fun deviceItemUpdate_sameDeviceItems_registerListenerOnce() {
+ with(kosmos) {
+ testScope.runTest {
+ val deviceItem = AvailableMediaDeviceItemFactory().create(context, cachedDevice1)
+ val update by collectLastValue(interactor.metadataUpdate)
+ deviceItemUpdate.emit(listOf(deviceItem))
+ deviceItemUpdate.emit(listOf(deviceItem))
+ runCurrent()
+
+ assertThat(update).isNull()
+ verify(bluetoothAdapter)
+ .addOnMetadataChangedListener(eq(bluetoothDevice1), any(), any())
+ verify(bluetoothAdapter, never()).removeOnMetadataChangedListener(any(), any())
+ }
+ }
+ }
+
+ @Test
+ fun deviceItemUpdate_differentDeviceItems_unregisterOldAndRegisterNew() {
+ with(kosmos) {
+ testScope.runTest {
+ val deviceItem1 = AvailableMediaDeviceItemFactory().create(context, cachedDevice1)
+ val deviceItem2 = AvailableMediaDeviceItemFactory().create(context, cachedDevice2)
+ val update by collectLastValue(interactor.metadataUpdate)
+ deviceItemUpdate.emit(listOf(deviceItem1))
+ deviceItemUpdate.emit(listOf(deviceItem1, deviceItem2))
+ runCurrent()
+
+ assertThat(update).isNull()
+ verify(bluetoothAdapter, times(2))
+ .addOnMetadataChangedListener(eq(bluetoothDevice1), any(), any())
+ verify(bluetoothAdapter)
+ .addOnMetadataChangedListener(eq(bluetoothDevice2), any(), any())
+ verify(bluetoothAdapter)
+ .removeOnMetadataChangedListener(eq(bluetoothDevice1), any())
+ }
+ }
+ }
+
+ @Test
+ fun metadataUpdate_triggerCallback_emit() {
+ with(kosmos) {
+ testScope.runTest {
+ val deviceItem = AvailableMediaDeviceItemFactory().create(context, cachedDevice1)
+ val update by collectLastValue(interactor.metadataUpdate)
+ deviceItemUpdate.emit(listOf(deviceItem))
+ runCurrent()
+
+ assertThat(update).isNull()
+ verify(bluetoothAdapter)
+ .addOnMetadataChangedListener(
+ eq(bluetoothDevice1),
+ any(),
+ argumentCaptor.capture()
+ )
+
+ val listener = argumentCaptor.value
+ listener.onMetadataChanged(
+ bluetoothDevice1,
+ BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY,
+ ByteArray(0)
+ )
+ assertThat(update).isEqualTo(Unit)
+ }
+ }
+ }
+
+ @Test
+ fun metadataUpdate_triggerCallbackNonBatteryKey_doNothing() {
+ with(kosmos) {
+ testScope.runTest {
+ val deviceItem = AvailableMediaDeviceItemFactory().create(context, cachedDevice1)
+ val update by collectLastValue(interactor.metadataUpdate)
+ deviceItemUpdate.emit(listOf(deviceItem))
+ runCurrent()
+
+ assertThat(update).isNull()
+ verify(bluetoothAdapter)
+ .addOnMetadataChangedListener(
+ eq(bluetoothDevice1),
+ any(),
+ argumentCaptor.capture()
+ )
+
+ val listener = argumentCaptor.value
+ listener.onMetadataChanged(
+ bluetoothDevice1,
+ BluetoothDevice.METADATA_MODEL_NAME,
+ ByteArray(0)
+ )
+
+ assertThat(update).isNull()
+ }
+ }
+ }
+
+ companion object {
+ private const val DEVICE_NAME = "DeviceName"
+ private const val CONNECTION_SUMMARY = "ConnectionSummary"
+ private const val DEVICE_ADDRESS = "04:52:C7:0B:D8:3C"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
index 9abb85d249eb..d7bea6680c2d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
@@ -77,6 +77,8 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
@Mock private lateinit var audioSharingInteractor: AudioSharingInteractor
+ @Mock private lateinit var bluetoothDeviceMetadataInteractor: BluetoothDeviceMetadataInteractor
+
@Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
@Mock private lateinit var deviceItemActionInteractor: DeviceItemActionInteractor
@@ -138,6 +140,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
)
),
audioSharingInteractor,
+ bluetoothDeviceMetadataInteractor,
mDialogTransitionAnimator,
activityStarter,
uiEventLogger,
@@ -150,6 +153,8 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
whenever(deviceItemInteractor.deviceItemUpdate).thenReturn(MutableSharedFlow())
whenever(deviceItemInteractor.deviceItemUpdateRequest)
.thenReturn(MutableStateFlow(Unit).asStateFlow())
+ whenever(deviceItemInteractor.showSeeAllUpdate).thenReturn(getMutableStateFlow(false))
+ whenever(bluetoothDeviceMetadataInteractor.metadataUpdate).thenReturn(MutableSharedFlow())
whenever(mBluetoothTileDialogDelegateDelegateFactory.create(any(), anyInt(), any(), any()))
.thenReturn(bluetoothTileDialogDelegate)
whenever(bluetoothTileDialogDelegate.createDialog()).thenReturn(sysuiDialog)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
index 7f7abaf9b689..194590c1f626 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
@@ -113,9 +113,11 @@ class DeviceItemInteractorTest : SysuiTestCase() {
)
val latest by collectLastValue(interactor.deviceItemUpdate)
+ val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
interactor.updateDeviceItems(mContext, DeviceFetchTrigger.FIRST_LOAD)
assertThat(latest).isEqualTo(emptyList<DeviceItem>())
+ assertThat(latestShowSeeAll).isFalse()
}
}
@@ -128,9 +130,11 @@ class DeviceItemInteractorTest : SysuiTestCase() {
)
val latest by collectLastValue(interactor.deviceItemUpdate)
+ val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
interactor.updateDeviceItems(mContext, DeviceFetchTrigger.FIRST_LOAD)
assertThat(latest).isEqualTo(emptyList<DeviceItem>())
+ assertThat(latestShowSeeAll).isFalse()
}
}
@@ -143,9 +147,11 @@ class DeviceItemInteractorTest : SysuiTestCase() {
)
val latest by collectLastValue(interactor.deviceItemUpdate)
+ val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
interactor.updateDeviceItems(mContext, DeviceFetchTrigger.FIRST_LOAD)
assertThat(latest).isEqualTo(listOf(deviceItem1))
+ assertThat(latestShowSeeAll).isFalse()
}
}
@@ -158,9 +164,11 @@ class DeviceItemInteractorTest : SysuiTestCase() {
)
val latest by collectLastValue(interactor.deviceItemUpdate)
+ val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
interactor.updateDeviceItems(mContext, DeviceFetchTrigger.FIRST_LOAD)
assertThat(latest).isEqualTo(listOf(deviceItem2, deviceItem2))
+ assertThat(latestShowSeeAll).isFalse()
}
}
@@ -184,9 +192,11 @@ class DeviceItemInteractorTest : SysuiTestCase() {
`when`(deviceItem2.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE)
val latest by collectLastValue(interactor.deviceItemUpdate)
+ val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
interactor.updateDeviceItems(mContext, DeviceFetchTrigger.FIRST_LOAD)
assertThat(latest).isEqualTo(listOf(deviceItem2, deviceItem1))
+ assertThat(latestShowSeeAll).isFalse()
}
}
@@ -207,9 +217,30 @@ class DeviceItemInteractorTest : SysuiTestCase() {
`when`(deviceItem2.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
val latest by collectLastValue(interactor.deviceItemUpdate)
+ val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
interactor.updateDeviceItems(mContext, DeviceFetchTrigger.FIRST_LOAD)
assertThat(latest).isEqualTo(listOf(deviceItem2, deviceItem1))
+ assertThat(latestShowSeeAll).isFalse()
+ }
+ }
+
+ @Test
+ fun testUpdateDeviceItems_showMaxDeviceItems_showSeeAll() {
+ testScope.runTest {
+ `when`(bluetoothTileDialogRepository.cachedDevices)
+ .thenReturn(listOf(cachedDevice2, cachedDevice2, cachedDevice2, cachedDevice2))
+ `when`(adapter.mostRecentlyConnectedDevices).thenReturn(null)
+ interactor.setDeviceItemFactoryListForTesting(
+ listOf(createFactory({ true }, deviceItem2))
+ )
+
+ val latest by collectLastValue(interactor.deviceItemUpdate)
+ val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
+ interactor.updateDeviceItems(mContext, DeviceFetchTrigger.FIRST_LOAD)
+
+ assertThat(latest).isEqualTo(listOf(deviceItem2, deviceItem2, deviceItem2))
+ assertThat(latestShowSeeAll).isTrue()
}
}