diff options
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() } } |