diff options
30 files changed, 1500 insertions, 803 deletions
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index bd7067bc1293..e3570010fbd3 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -425,8 +425,8 @@ filegroup { "tests/src/**/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt", "tests/src/**/systemui/shared/system/RemoteTransitionTest.java", "tests/src/**/systemui/navigationbar/NavigationBarControllerImplTest.java", - "tests/src/**/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt", - "tests/src/**/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt", + "tests/src/**/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt", + "tests/src/**/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt", "tests/src/**/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt", "tests/src/**/systemui/notetask/LaunchNotesRoleSettingsTrampolineActivityTest.kt", "tests/src/**/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt", diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt index 5ff46346b386..b5b2f5e3e802 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt @@ -20,8 +20,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher -import com.android.systemui.plugins.activityStarter -import com.android.systemui.util.mockito.mock +import org.mockito.kotlin.mock val Kosmos.bluetoothTileDialogLogger: BluetoothTileDialogLogger by Kosmos.Fixture { mock {} } @@ -29,14 +28,10 @@ val Kosmos.localBluetoothManager: LocalBluetoothManager by Kosmos.Fixture { mock val Kosmos.dialogTransitionAnimator: DialogTransitionAnimator by Kosmos.Fixture { mock {} } -val Kosmos.deviceItemActionInteractor: DeviceItemActionInteractor by +val Kosmos.deviceItemActionInteractorImpl: DeviceItemActionInteractorImpl by Kosmos.Fixture { - DeviceItemActionInteractor( - activityStarter, - dialogTransitionAnimator, - localBluetoothManager, + DeviceItemActionInteractorImpl( testDispatcher, - bluetoothTileDialogLogger, uiEventLogger, ) } diff --git a/packages/SystemUI/res/layout/audio_sharing_dialog.xml b/packages/SystemUI/res/layout/audio_sharing_dialog.xml new file mode 100644 index 000000000000..7534e159beb0 --- /dev/null +++ b/packages/SystemUI/res/layout/audio_sharing_dialog.xml @@ -0,0 +1,115 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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. + --> + +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/root" + style="@style/Widget.SliceView.Panel" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <ImageView android:id="@+id/icon" + android:layout_width="28dp" + android:layout_height="28dp" + android:src="@drawable/ic_bt_le_audio_sharing" + android:layout_marginTop="5dp" + android:layout_marginBottom="20dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toTopOf="@id/title" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="20dp" + android:gravity="center_vertical|center_horizontal" + android:maxLines="1" + android:ellipsize="end" + android:text="@string/quick_settings_bluetooth_audio_sharing_dialog_title" + android:textAppearance="@style/TextAppearance.Dialog.Title" + android:textSize="24sp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toTopOf="@id/subtitle" + app:layout_constraintTop_toBottomOf="@id/icon" /> + + <TextView + android:id="@+id/subtitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="20dp" + android:gravity="center_vertical|center_horizontal" + android:ellipsize="end" + android:maxLines="2" + android:textAppearance="@style/TextAppearance.Dialog.Body.Message" + android:textFontWeight="500" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toTopOf="@id/message" + app:layout_constraintTop_toBottomOf="@id/title" /> + + <TextView + android:id="@+id/message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="20dp" + android:gravity="center_vertical|center_horizontal" + android:ellipsize="end" + android:maxLines="2" + android:text="@string/quick_settings_bluetooth_audio_sharing_dialog_message" + android:textAppearance="@style/TextAppearance.Dialog.Body.Message" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toTopOf="@id/share_audio_button" + app:layout_constraintTop_toBottomOf="@id/subtitle" /> + + <Button + android:id="@+id/share_audio_button" + style="@style/SettingsLibActionButton" + android:textColor="?androidprv:attr/textColorOnAccent" + android:background="@drawable/audio_sharing_rounded_bg_ripple" + android:layout_marginBottom="4dp" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:minHeight="64dp" + android:contentDescription="@string/accessibility_bluetooth_device_settings_see_all" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@+id/message" + app:layout_constraintBottom_toTopOf="@+id/switch_active_button" + android:text="@string/quick_settings_bluetooth_audio_sharing_button" + android:maxLines="2" /> + + <Button + android:id="@+id/switch_active_button" + style="@style/SettingsLibActionButton" + android:textColor="?androidprv:attr/textColorOnAccent" + android:background="@drawable/audio_sharing_rounded_bg_ripple" + android:layout_marginBottom="20dp" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:minHeight="64dp" + android:contentDescription="@string/accessibility_bluetooth_device_settings_pair_new_device" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@+id/share_audio_button" + app:layout_constraintBottom_toBottomOf="parent" + android:maxLines="2" /> +</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index c76b35f0cf9b..4e9fd4b852b2 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -765,6 +765,14 @@ <string name="quick_settings_bluetooth_audio_sharing_button_sharing">Sharing audio</string> <!-- QuickSettings: Bluetooth dialog audio sharing button text accessibility label. Used as part of the string "Double tap to enter audio sharing settings". [CHAR LIMIT=50]--> <string name="quick_settings_bluetooth_audio_sharing_button_accessibility">enter audio sharing settings</string> + <!-- QuickSettings: Bluetooth audio sharing dialog message. [CHAR LIMIT=NONE]--> + <string name="quick_settings_bluetooth_audio_sharing_dialog_message">This device\'s music and videos will play on both pairs of headphones</string> + <!-- QuickSettings: Bluetooth audio sharing dialog title. [CHAR LIMIT=NONE]--> + <string name="quick_settings_bluetooth_audio_sharing_dialog_title">Share your audio</string> + <!-- QuickSettings: Bluetooth audio sharing dialog subtitle. [CHAR LIMIT=NONE]--> + <string name="quick_settings_bluetooth_audio_sharing_dialog_subtitle"><xliff:g id="available_device_name" example="device 1">%1$s</xliff:g> and <xliff:g id="active_device_name" example="device 2">%2$s</xliff:g></string> + <!-- QuickSettings: Bluetooth audio sharing dialog button text. [CHAR LIMIT=NONE]--> + <string name="quick_settings_bluetooth_audio_sharing_dialog_switch_to_button">Switch to <xliff:g id="available_device_name" example="device 1">%1$s</xliff:g></string> <!-- QuickSettings: Bluetooth secondary label for the battery level of a connected device [CHAR LIMIT=20]--> <string name="quick_settings_bluetooth_secondary_label_battery_level"><xliff:g id="battery_level_as_percentage">%s</xliff:g> battery</string> diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModel.kt new file mode 100644 index 000000000000..5213a0e702d2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModel.kt @@ -0,0 +1,98 @@ +/* + * 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 androidx.annotation.StringRes +import com.android.settingslib.bluetooth.BluetoothUtils +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.res.R +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine + +sealed class AudioSharingButtonState { + object Gone : AudioSharingButtonState() + + data class Visible(@StringRes val resId: Int, val isActive: Boolean) : + AudioSharingButtonState() +} + +class AudioSharingButtonViewModel +@AssistedInject +constructor( + private val localBluetoothManager: LocalBluetoothManager?, + private val bluetoothStateInteractor: BluetoothStateInteractor, + private val deviceItemInteractor: DeviceItemInteractor, +) : ExclusiveActivatable() { + + private val mutableButtonState = + MutableStateFlow<AudioSharingButtonState>(AudioSharingButtonState.Gone) + /** Flow representing the update of AudioSharingButtonState. */ + val audioSharingButtonStateUpdate: StateFlow<AudioSharingButtonState> = + mutableButtonState.asStateFlow() + + override suspend fun onActivated(): Nothing { + combine( + bluetoothStateInteractor.bluetoothStateUpdate, + deviceItemInteractor.deviceItemUpdate + ) { bluetoothState, deviceItem -> + getButtonState(bluetoothState, deviceItem) + } + .collect { mutableButtonState.value = it } + awaitCancellation() + } + + private fun getButtonState( + bluetoothState: Boolean, + deviceItem: List<DeviceItem> + ): AudioSharingButtonState { + return when { + // Don't show button when bluetooth is off + !bluetoothState -> AudioSharingButtonState.Gone + // Show sharing audio when broadcasting + BluetoothUtils.isBroadcasting(localBluetoothManager) -> + AudioSharingButtonState.Visible( + R.string.quick_settings_bluetooth_audio_sharing_button_sharing, + isActive = true + ) + // When not broadcasting, don't show button if there's connected source in any device + deviceItem.any { + BluetoothUtils.hasConnectedBroadcastSource( + it.cachedBluetoothDevice, + localBluetoothManager + ) + } -> AudioSharingButtonState.Gone + // Show audio sharing when there's a connected LE audio device + deviceItem.any { BluetoothUtils.isActiveLeAudioDevice(it.cachedBluetoothDevice) } -> + AudioSharingButtonState.Visible( + R.string.quick_settings_bluetooth_audio_sharing_button, + isActive = false + ) + else -> AudioSharingButtonState.Gone + } + } + + @AssistedFactory + interface Factory { + fun create(): AudioSharingButtonViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt new file mode 100644 index 000000000000..eb5706f9e8b6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt @@ -0,0 +1,148 @@ +/* + * 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.BluetoothDevice +import android.content.Intent +import android.os.Bundle +import android.provider.Settings +import com.android.internal.logging.UiEventLogger +import com.android.settingslib.bluetooth.A2dpProfile +import com.android.settingslib.bluetooth.BluetoothUtils +import com.android.settingslib.bluetooth.HeadsetProfile +import com.android.settingslib.bluetooth.HearingAidProfile +import com.android.settingslib.bluetooth.LeAudioProfile +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.settingslib.flags.Flags.audioSharingQsDialogImprovement +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.phone.SystemUIDialog +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +@SysUISingleton +class AudioSharingDeviceItemActionInteractorImpl +@Inject +constructor( + private val activityStarter: ActivityStarter, + private val dialogTransitionAnimator: DialogTransitionAnimator, + private val localBluetoothManager: LocalBluetoothManager?, + @Main private val mainDispatcher: CoroutineDispatcher, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val logger: BluetoothTileDialogLogger, + private val uiEventLogger: UiEventLogger, + private val delegateFactory: AudioSharingDialogDelegate.Factory, + private val deviceItemActionInteractorImpl: DeviceItemActionInteractorImpl, +) : DeviceItemActionInteractor { + + override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) { + withContext(backgroundDispatcher) { + val inAudioSharing = BluetoothUtils.isBroadcasting(localBluetoothManager) + logger.logDeviceClickInAudioSharingWhenEnabled(inAudioSharing) + + when { + deviceItem.type == + DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { + if (audioSharingQsDialogImprovement()) { + withContext(mainDispatcher) { + delegateFactory + .create(deviceItem.cachedBluetoothDevice) + .createDialog() + .let { dialogTransitionAnimator.showFromDialog(it, dialog) } + } + } else { + launchSettings(deviceItem.cachedBluetoothDevice.device, dialog) + logger.logLaunchSettingsCriteriaMatched( + "AvailableAudioSharingDeviceClicked", + deviceItem + ) + } + uiEventLogger.log( + BluetoothTileDialogUiEvent.AVAILABLE_AUDIO_SHARING_DEVICE_CLICKED + ) + } + inSharingAndDeviceNoSource(inAudioSharing, deviceItem) -> { + launchSettings(deviceItem.cachedBluetoothDevice.device, dialog) + logger.logLaunchSettingsCriteriaMatched("InSharingClickedNoSource", deviceItem) + uiEventLogger.log( + if (deviceItem.isLeAudioSupported) + BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_IN_SHARING_LE_DEVICE_CLICKED + else + BluetoothTileDialogUiEvent + .LAUNCH_SETTINGS_IN_SHARING_NON_LE_DEVICE_CLICKED + ) + } + else -> { + deviceItemActionInteractorImpl.onClick(deviceItem, dialog) + } + } + } + } + + private fun inSharingAndDeviceNoSource( + inAudioSharing: Boolean, + deviceItem: DeviceItem + ): Boolean { + return inAudioSharing && + deviceItem.isMediaDevice && + !BluetoothUtils.hasConnectedBroadcastSource( + deviceItem.cachedBluetoothDevice, + localBluetoothManager + ) + } + + private fun launchSettings(device: BluetoothDevice, dialog: SystemUIDialog) { + val intent = + Intent(Settings.ACTION_BLUETOOTH_SETTINGS).apply { + putExtra( + EXTRA_SHOW_FRAGMENT_ARGUMENTS, + Bundle().apply { + putParcelable(LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE, device) + } + ) + } + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK + activityStarter.postStartActivityDismissingKeyguard( + intent, + 0, + dialogTransitionAnimator.createActivityTransitionController(dialog) + ) + } + + private companion object { + const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args" + + val DeviceItem.isLeAudioSupported: Boolean + get() = + cachedBluetoothDevice.profiles.any { profile -> + profile is LeAudioProfile && profile.isEnabled(cachedBluetoothDevice.device) + } + + val DeviceItem.isMediaDevice: Boolean + get() = + cachedBluetoothDevice.uiAccessibleProfiles.any { + it is A2dpProfile || + it is HearingAidProfile || + it is LeAudioProfile || + it is HeadsetProfile + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegate.kt new file mode 100644 index 000000000000..6b39d8f43b84 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegate.kt @@ -0,0 +1,68 @@ +/* + * 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.os.Bundle +import android.widget.Button +import android.widget.TextView +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.SystemUIDialog +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class AudioSharingDialogDelegate +@AssistedInject +constructor( + @Assisted private val cachedBluetoothDevice: CachedBluetoothDevice, + @Application private val coroutineScope: CoroutineScope, + private val viewModelFactory: AudioSharingDialogViewModel.Factory, + private val sysuiDialogFactory: SystemUIDialog.Factory, +) : SystemUIDialog.Delegate { + + override fun createDialog(): SystemUIDialog = sysuiDialogFactory.create(this) + + override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { + with(dialog.layoutInflater.inflate(R.layout.audio_sharing_dialog, null)) { + dialog.setView(this) + val subtitleTextView = requireViewById<TextView>(R.id.subtitle) + val switchActiveButton = requireViewById<Button>(R.id.switch_active_button) + val job = + coroutineScope.launch { + viewModelFactory.create(cachedBluetoothDevice).dialogState.collect { + when (it) { + is AudioSharingDialogState.Hide -> dialog.dismiss() + is AudioSharingDialogState.Show -> { + subtitleTextView.text = it.subtitle + switchActiveButton.text = it.switchButtonText + } + } + } + } + SystemUIDialog.registerDismissListener(dialog) { job.cancel() } + } + } + + @AssistedFactory + interface Factory { + fun create(cachedBluetoothDevice: CachedBluetoothDevice): AudioSharingDialogDelegate + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModel.kt new file mode 100644 index 000000000000..55686aaf87d2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModel.kt @@ -0,0 +1,97 @@ +/* + * 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.content.Context +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.res.R +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart + +sealed class AudioSharingDialogState { + data object Hide : AudioSharingDialogState() + + data class Show(val subtitle: String, val switchButtonText: String) : AudioSharingDialogState() +} + +class AudioSharingDialogViewModel +@AssistedInject +constructor( + deviceItemInteractor: DeviceItemInteractor, + audioSharingInteractor: AudioSharingInteractor, + private val context: Context, + private val localBluetoothManager: LocalBluetoothManager?, + @Assisted private val cachedBluetoothDevice: CachedBluetoothDevice, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) { + val dialogState: Flow<AudioSharingDialogState> = + deviceItemInteractor.deviceItemUpdateRequest + .map { + if ( + audioSharingInteractor.isAvailableAudioSharingMediaBluetoothDevice( + cachedBluetoothDevice + ) + ) { + createShowState(cachedBluetoothDevice) + } else { + AudioSharingDialogState.Hide + } + } + .onStart { emit(createShowState(cachedBluetoothDevice)) } + .flowOn(backgroundDispatcher) + .distinctUntilChanged() + + private fun createShowState( + cachedBluetoothDevice: CachedBluetoothDevice + ): AudioSharingDialogState { + val activeDeviceName = + localBluetoothManager + ?.profileManager + ?.leAudioProfile + ?.activeDevices + ?.firstOrNull() + ?.let { localBluetoothManager.cachedDeviceManager?.findDevice(it)?.name } ?: "" + val availableDeviceName = cachedBluetoothDevice.name + return AudioSharingDialogState.Show( + context.getString( + R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle, + availableDeviceName, + activeDeviceName + ), + context.getString( + R.string.quick_settings_bluetooth_audio_sharing_dialog_switch_to_button, + availableDeviceName + ) + ) + } + + @AssistedFactory + interface Factory { + fun create( + cachedBluetoothDevice: CachedBluetoothDevice, + ): AudioSharingDialogViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt index 817f2d7f6f70..227ccfea4394 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt @@ -16,82 +16,45 @@ package com.android.systemui.bluetooth.qsdialog -import androidx.annotation.StringRes import com.android.settingslib.bluetooth.BluetoothUtils +import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.res.R import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext -internal sealed class AudioSharingButtonState { - object Gone : AudioSharingButtonState() - - data class Visible(@StringRes val resId: Int, val isActive: Boolean) : - AudioSharingButtonState() +/** Holds business logic for the audio sharing state. */ +interface AudioSharingInteractor { + suspend fun isAvailableAudioSharingMediaBluetoothDevice( + cachedBluetoothDevice: CachedBluetoothDevice + ): Boolean } -/** Holds business logic for the audio sharing state. */ @SysUISingleton -internal class AudioSharingInteractor +class AudioSharingInteractorImpl @Inject constructor( private val localBluetoothManager: LocalBluetoothManager?, - bluetoothStateInteractor: BluetoothStateInteractor, - deviceItemInteractor: DeviceItemInteractor, - @Application private val coroutineScope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, -) { - /** Flow representing the update of AudioSharingButtonState. */ - internal val audioSharingButtonStateUpdate: Flow<AudioSharingButtonState> = - combine( - bluetoothStateInteractor.bluetoothStateUpdate, - deviceItemInteractor.deviceItemUpdate - ) { bluetoothState, deviceItem -> - getButtonState(bluetoothState, deviceItem) - } - .flowOn(backgroundDispatcher) - .stateIn( - coroutineScope, - SharingStarted.WhileSubscribed(replayExpirationMillis = 0), - initialValue = AudioSharingButtonState.Gone - ) +) : AudioSharingInteractor { - private fun getButtonState( - bluetoothState: Boolean, - deviceItem: List<DeviceItem> - ): AudioSharingButtonState { - return when { - // Don't show button when bluetooth is off - !bluetoothState -> AudioSharingButtonState.Gone - // Show sharing audio when broadcasting - BluetoothUtils.isBroadcasting(localBluetoothManager) -> - AudioSharingButtonState.Visible( - R.string.quick_settings_bluetooth_audio_sharing_button_sharing, - isActive = true - ) - // When not broadcasting, don't show button if there's connected source in any device - deviceItem.any { - BluetoothUtils.hasConnectedBroadcastSource( - it.cachedBluetoothDevice, - localBluetoothManager - ) - } -> AudioSharingButtonState.Gone - // Show audio sharing when there's a connected LE audio device - deviceItem.any { BluetoothUtils.isActiveLeAudioDevice(it.cachedBluetoothDevice) } -> - AudioSharingButtonState.Visible( - R.string.quick_settings_bluetooth_audio_sharing_button, - isActive = false - ) - else -> AudioSharingButtonState.Gone + override suspend fun isAvailableAudioSharingMediaBluetoothDevice( + cachedBluetoothDevice: CachedBluetoothDevice + ): Boolean { + return withContext(backgroundDispatcher) { + BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice( + cachedBluetoothDevice, + localBluetoothManager + ) } } } + +@SysUISingleton +class AudioSharingInteractorEmptyImpl @Inject constructor() : AudioSharingInteractor { + override suspend fun isAvailableAudioSharingMediaBluetoothDevice( + cachedBluetoothDevice: CachedBluetoothDevice + ) = false +} diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt index 17f9e634ec62..55d4d3efbe27 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt @@ -39,7 +39,7 @@ import kotlinx.coroutines.withContext /** Holds business logic for the Bluetooth Dialog's bluetooth and device connection state */ @SysUISingleton -internal class BluetoothStateInteractor +class BluetoothStateInteractor @Inject constructor( private val localBluetoothManager: LocalBluetoothManager?, diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt index bdd4c161ad59..12a6626314cd 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt @@ -42,6 +42,7 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent LAUNCH_SETTINGS_IN_SHARING_LE_DEVICE_CLICKED(1717), @UiEvent(doc = "Currently broadcasting and a non-LE audio supported device is clicked") LAUNCH_SETTINGS_IN_SHARING_NON_LE_DEVICE_CLICKED(1718), + @Deprecated("Use case no longer needed") @UiEvent( doc = "Not broadcasting, having one connected, another saved LE audio device is clicked" ) @@ -52,6 +53,7 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent ) @UiEvent(doc = "Not broadcasting, one of the two connected LE audio devices is clicked") LAUNCH_SETTINGS_NOT_SHARING_CONNECTED_LE_DEVICE_CLICKED(1720), + @Deprecated("Use case no longer needed") @UiEvent(doc = "Not broadcasting, having two connected, the active LE audio devices is clicked") LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED(1881); 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 a8f7fc345001..a1b6e0b64c94 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt @@ -67,11 +67,12 @@ constructor( private val deviceItemActionInteractor: DeviceItemActionInteractor, private val bluetoothStateInteractor: BluetoothStateInteractor, private val bluetoothAutoOnInteractor: BluetoothAutoOnInteractor, - private val audioSharingInteractor: AudioSharingInteractor, + private val audioSharingButtonViewModelFactory: AudioSharingButtonViewModel.Factory, private val bluetoothDeviceMetadataInteractor: BluetoothDeviceMetadataInteractor, private val dialogTransitionAnimator: DialogTransitionAnimator, private val activityStarter: ActivityStarter, private val uiEventLogger: UiEventLogger, + private val logger: BluetoothTileDialogLogger, @Application private val coroutineScope: CoroutineScope, @Main private val mainDispatcher: CoroutineDispatcher, @Background private val backgroundDispatcher: CoroutineDispatcher, @@ -154,28 +155,31 @@ constructor( .launchIn(this) if (BluetoothUtils.isAudioSharingEnabled()) { - audioSharingInteractor.audioSharingButtonStateUpdate - .onEach { - when (it) { - is AudioSharingButtonState.Visible -> { - dialogDelegate.onAudioSharingButtonUpdated( - dialog, - VISIBLE, - context.getString(it.resId), - it.isActive - ) - } - is AudioSharingButtonState.Gone -> { - dialogDelegate.onAudioSharingButtonUpdated( - dialog, - GONE, - label = null, - isActive = false - ) + audioSharingButtonViewModelFactory.create().run { + audioSharingButtonStateUpdate + .onEach { + when (it) { + is AudioSharingButtonState.Visible -> { + dialogDelegate.onAudioSharingButtonUpdated( + dialog, + VISIBLE, + context.getString(it.resId), + it.isActive + ) + } + is AudioSharingButtonState.Gone -> { + dialogDelegate.onAudioSharingButtonUpdated( + dialog, + GONE, + label = null, + isActive = false + ) + } } } - } - .launchIn(this) + .launchIn(this@launch) + launch { activate() } + } } // bluetoothStateUpdate is emitted when bluetooth on/off state is changed, re-fetch @@ -209,7 +213,10 @@ constructor( // deviceItemClick is emitted when user clicked on a device item. dialogDelegate.deviceItemClick - .onEach { deviceItemActionInteractor.onClick(it, dialog) } + .onEach { + deviceItemActionInteractor.onClick(it, dialog) + logger.logDeviceClick(it.cachedBluetoothDevice.address, it.type) + } .launchIn(this) // contentHeight is emitted when the dialog is dismissed. diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt index f1894d3bb111..cf0f19f1d361 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt @@ -16,87 +16,28 @@ package com.android.systemui.bluetooth.qsdialog -import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothProfile -import android.content.Intent -import android.os.Bundle -import android.provider.Settings import com.android.internal.logging.UiEventLogger -import com.android.settingslib.bluetooth.A2dpProfile -import com.android.settingslib.bluetooth.BluetoothUtils -import com.android.settingslib.bluetooth.HeadsetProfile -import com.android.settingslib.bluetooth.HearingAidProfile -import com.android.settingslib.bluetooth.LeAudioProfile -import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast -import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant -import com.android.settingslib.bluetooth.LocalBluetoothManager -import com.android.systemui.animation.DialogTransitionAnimator -import com.android.systemui.bluetooth.qsdialog.DeviceItemActionInteractor.LaunchSettingsCriteria.Companion.getCurrentConnectedLeByGroupId import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.phone.SystemUIDialog import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext +interface DeviceItemActionInteractor { + suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {} +} + @SysUISingleton -class DeviceItemActionInteractor +class DeviceItemActionInteractorImpl @Inject constructor( - private val activityStarter: ActivityStarter, - private val dialogTransitionAnimator: DialogTransitionAnimator, - private val localBluetoothManager: LocalBluetoothManager?, @Background private val backgroundDispatcher: CoroutineDispatcher, - private val logger: BluetoothTileDialogLogger, private val uiEventLogger: UiEventLogger, -) { - private val leAudioProfile: LeAudioProfile? - get() = localBluetoothManager?.profileManager?.leAudioProfile - - private val assistantProfile: LocalBluetoothLeBroadcastAssistant? - get() = localBluetoothManager?.profileManager?.leAudioBroadcastAssistantProfile - - private val launchSettingsCriteriaList: List<LaunchSettingsCriteria> - get() = - listOf( - InSharingClickedNoSource(localBluetoothManager, backgroundDispatcher, logger), - NotSharingClickedNonConnect( - leAudioProfile, - assistantProfile, - backgroundDispatcher, - logger - ), - NotSharingClickedActive( - leAudioProfile, - assistantProfile, - backgroundDispatcher, - logger - ) - ) +) : DeviceItemActionInteractor { - suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) { + override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) { withContext(backgroundDispatcher) { - logger.logDeviceClick(deviceItem.cachedBluetoothDevice.address, deviceItem.type) - if ( - BluetoothUtils.isAudioSharingEnabled() && - localBluetoothManager != null && - leAudioProfile != null && - assistantProfile != null - ) { - val inAudioSharing = BluetoothUtils.isBroadcasting(localBluetoothManager) - logger.logDeviceClickInAudioSharingWhenEnabled(inAudioSharing) - - val criteriaMatched = - launchSettingsCriteriaList.firstOrNull { - it.matched(inAudioSharing, deviceItem) - } - if (criteriaMatched != null) { - uiEventLogger.log(criteriaMatched.getClickUiEvent(deviceItem)) - launchSettings(deviceItem.cachedBluetoothDevice.device, dialog) - return@withContext - } - } deviceItem.cachedBluetoothDevice.apply { when (deviceItem.type) { DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE -> { @@ -106,12 +47,6 @@ constructor( DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { uiEventLogger.log(BluetoothTileDialogUiEvent.AUDIO_SHARING_DEVICE_CLICKED) } - DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { - // TODO(b/360759048): pop up dialog - uiEventLogger.log( - BluetoothTileDialogUiEvent.AVAILABLE_AUDIO_SHARING_DEVICE_CLICKED - ) - } DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> { setActive() uiEventLogger.log(BluetoothTileDialogUiEvent.CONNECTED_DEVICE_SET_ACTIVE) @@ -126,186 +61,12 @@ constructor( connect() uiEventLogger.log(BluetoothTileDialogUiEvent.SAVED_DEVICE_CONNECT) } - } - } - } - } - - private fun launchSettings(device: BluetoothDevice, dialog: SystemUIDialog) { - val intent = - Intent(Settings.ACTION_BLUETOOTH_SETTINGS).apply { - putExtra( - EXTRA_SHOW_FRAGMENT_ARGUMENTS, - Bundle().apply { - putParcelable(LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE, device) + DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { + // Do nothing. Should already be handled in + // AudioSharingDeviceItemActionInteractor. } - ) - } - intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK - activityStarter.postStartActivityDismissingKeyguard( - intent, - 0, - dialogTransitionAnimator.createActivityTransitionController(dialog) - ) - } - - private interface LaunchSettingsCriteria { - suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean - - suspend fun getClickUiEvent(deviceItem: DeviceItem): BluetoothTileDialogUiEvent - - companion object { - suspend fun getCurrentConnectedLeByGroupId( - leAudioProfile: LeAudioProfile, - assistantProfile: LocalBluetoothLeBroadcastAssistant, - @Background backgroundDispatcher: CoroutineDispatcher, - logger: BluetoothTileDialogLogger, - ): Map<Int, List<BluetoothDevice>> { - return withContext(backgroundDispatcher) { - assistantProfile - .getDevicesMatchingConnectionStates( - intArrayOf(BluetoothProfile.STATE_CONNECTED) - ) - ?.filterNotNull() - ?.groupBy { leAudioProfile.getGroupId(it) } - ?.also { logger.logConnectedLeByGroupId(it) } ?: emptyMap() } } } } - - private class InSharingClickedNoSource( - private val localBluetoothManager: LocalBluetoothManager?, - @Background private val backgroundDispatcher: CoroutineDispatcher, - private val logger: BluetoothTileDialogLogger, - ) : LaunchSettingsCriteria { - // If currently broadcasting and the clicked device is not connected to the source - override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean { - return withContext(backgroundDispatcher) { - val matched = - inAudioSharing && - deviceItem.isMediaDevice && - !BluetoothUtils.hasConnectedBroadcastSource( - deviceItem.cachedBluetoothDevice, - localBluetoothManager - ) - - if (matched) { - logger.logLaunchSettingsCriteriaMatched("InSharingClickedNoSource", deviceItem) - } - - matched - } - } - - override suspend fun getClickUiEvent(deviceItem: DeviceItem) = - if (deviceItem.isLeAudioSupported) - BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_IN_SHARING_LE_DEVICE_CLICKED - else BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_IN_SHARING_NON_LE_DEVICE_CLICKED - } - - private class NotSharingClickedNonConnect( - private val leAudioProfile: LeAudioProfile?, - private val assistantProfile: LocalBluetoothLeBroadcastAssistant?, - @Background private val backgroundDispatcher: CoroutineDispatcher, - private val logger: BluetoothTileDialogLogger, - ) : LaunchSettingsCriteria { - // If not broadcasting, having one device connected, and clicked on a not yet connected LE - // audio device - override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean { - return withContext(backgroundDispatcher) { - val matched = - leAudioProfile?.let { leAudio -> - assistantProfile?.let { assistant -> - !inAudioSharing && - getCurrentConnectedLeByGroupId( - leAudio, - assistant, - backgroundDispatcher, - logger - ) - .size == 1 && - deviceItem.isNotConnectedLeAudioSupported - } - } ?: false - - if (matched) { - logger.logLaunchSettingsCriteriaMatched( - "NotSharingClickedNonConnect", - deviceItem - ) - } - - matched - } - } - - override suspend fun getClickUiEvent(deviceItem: DeviceItem) = - BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_NOT_SHARING_SAVED_LE_DEVICE_CLICKED - } - - private class NotSharingClickedActive( - private val leAudioProfile: LeAudioProfile?, - private val assistantProfile: LocalBluetoothLeBroadcastAssistant?, - @Background private val backgroundDispatcher: CoroutineDispatcher, - private val logger: BluetoothTileDialogLogger, - ) : LaunchSettingsCriteria { - // If not broadcasting, having two device connected, clicked on the active LE audio - // device - override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean { - return withContext(backgroundDispatcher) { - val matched = - leAudioProfile?.let { leAudio -> - assistantProfile?.let { assistant -> - !inAudioSharing && - getCurrentConnectedLeByGroupId( - leAudio, - assistant, - backgroundDispatcher, - logger - ) - .size == 2 && - deviceItem.isActiveLeAudioSupported - } - } ?: false - - if (matched) { - logger.logLaunchSettingsCriteriaMatched( - "NotSharingClickedConnected", - deviceItem - ) - } - - matched - } - } - - override suspend fun getClickUiEvent(deviceItem: DeviceItem) = - BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED - } - - private companion object { - const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args" - - val DeviceItem.isLeAudioSupported: Boolean - get() = - cachedBluetoothDevice.profiles.any { profile -> - profile is LeAudioProfile && profile.isEnabled(cachedBluetoothDevice.device) - } - - val DeviceItem.isNotConnectedLeAudioSupported: Boolean - get() = type == DeviceItemType.SAVED_BLUETOOTH_DEVICE && isLeAudioSupported - - val DeviceItem.isActiveLeAudioSupported: Boolean - get() = type == DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE && isLeAudioSupported - - val DeviceItem.isMediaDevice: Boolean - get() = - cachedBluetoothDevice.uiAccessibleProfiles.any { - it is A2dpProfile || - it is HearingAidProfile || - it is LeAudioProfile || - it is HeadsetProfile - } - } } 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 7280489e0835..d55d1559efaf 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt @@ -23,7 +23,6 @@ import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.flags.Flags -import com.android.settingslib.flags.Flags.enableLeAudioSharing import com.android.systemui.res.R private val backgroundOn = R.drawable.settingslib_switch_bar_bg_on @@ -107,8 +106,7 @@ internal class AudioSharingMediaDeviceItemFactory( cachedDevice: CachedBluetoothDevice, audioManager: AudioManager ): Boolean { - return enableLeAudioSharing() && - BluetoothUtils.hasConnectedBroadcastSource(cachedDevice, localBluetoothManager) + return BluetoothUtils.hasConnectedBroadcastSource(cachedDevice, localBluetoothManager) } override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem { @@ -133,8 +131,7 @@ internal class AvailableAudioSharingMediaDeviceItemFactory( cachedDevice: CachedBluetoothDevice, audioManager: AudioManager ): Boolean { - return BluetoothUtils.isAudioSharingEnabled() && - super.isFilterMatched(context, cachedDevice, audioManager) && + return super.isFilterMatched(context, cachedDevice, audioManager) && BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice( cachedDevice, localBluetoothManager 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 9114ecac7ac7..0118e56a773c 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt @@ -54,6 +54,8 @@ constructor( private val localBluetoothManager: LocalBluetoothManager?, private val systemClock: SystemClock, private val logger: BluetoothTileDialogLogger, + private val deviceItemFactoryList: List<@JvmSuppressWildcards DeviceItemFactory>, + private val deviceItemDisplayPriority: List<@JvmSuppressWildcards DeviceItemType>, @Application private val coroutineScope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, ) { @@ -67,7 +69,7 @@ constructor( internal val showSeeAllUpdate get() = mutableShowSeeAllUpdate.asStateFlow() - internal val deviceItemUpdateRequest: SharedFlow<Unit> = + val deviceItemUpdateRequest: SharedFlow<Unit> = conflatedCallbackFlow { val listener = object : BluetoothCallback { @@ -114,26 +116,6 @@ constructor( } .shareIn(coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0)) - private var deviceItemFactoryList: List<DeviceItemFactory> = - listOf( - ActiveMediaDeviceItemFactory(), - AudioSharingMediaDeviceItemFactory(localBluetoothManager), - AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager), - AvailableMediaDeviceItemFactory(), - ConnectedDeviceItemFactory(), - SavedDeviceItemFactory() - ) - - private var displayPriority: List<DeviceItemType> = - listOf( - DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE, - DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, - DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, - DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, - DeviceItemType.CONNECTED_BLUETOOTH_DEVICE, - DeviceItemType.SAVED_BLUETOOTH_DEVICE, - ) - internal suspend fun updateDeviceItems(context: Context, trigger: DeviceFetchTrigger) { withContext(backgroundDispatcher) { val start = systemClock.elapsedRealtime() @@ -144,7 +126,7 @@ constructor( .firstOrNull { it.isFilterMatched(context, cachedDevice, audioManager) } ?.create(context, cachedDevice) } - .sort(displayPriority, bluetoothAdapter?.mostRecentlyConnectedDevices) + .sort(deviceItemDisplayPriority, bluetoothAdapter?.mostRecentlyConnectedDevices) // Only emit when the job is not cancelled if (isActive) { mutableDeviceItemUpdate.tryEmit(deviceItems.take(MAX_DEVICE_ITEM_ENTRY)) @@ -176,14 +158,6 @@ constructor( ) } - internal fun setDeviceItemFactoryListForTesting(list: List<DeviceItemFactory>) { - deviceItemFactoryList = list - } - - internal fun setDisplayPriorityForTesting(list: List<DeviceItemType>) { - displayPriority = list - } - companion object { private const val TAG = "DeviceItemInteractor" private const val MAX_DEVICE_ITEM_ENTRY = 3 diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt new file mode 100644 index 000000000000..3b1cf07adf19 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt @@ -0,0 +1,97 @@ +/* + * 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.dagger + +import com.android.settingslib.bluetooth.BluetoothUtils +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.bluetooth.qsdialog.ActiveMediaDeviceItemFactory +import com.android.systemui.bluetooth.qsdialog.AudioSharingDeviceItemActionInteractorImpl +import com.android.systemui.bluetooth.qsdialog.AudioSharingInteractor +import com.android.systemui.bluetooth.qsdialog.AudioSharingInteractorEmptyImpl +import com.android.systemui.bluetooth.qsdialog.AudioSharingInteractorImpl +import com.android.systemui.bluetooth.qsdialog.AudioSharingMediaDeviceItemFactory +import com.android.systemui.bluetooth.qsdialog.AvailableAudioSharingMediaDeviceItemFactory +import com.android.systemui.bluetooth.qsdialog.AvailableMediaDeviceItemFactory +import com.android.systemui.bluetooth.qsdialog.ConnectedDeviceItemFactory +import com.android.systemui.bluetooth.qsdialog.DeviceItemActionInteractor +import com.android.systemui.bluetooth.qsdialog.DeviceItemActionInteractorImpl +import com.android.systemui.bluetooth.qsdialog.DeviceItemFactory +import com.android.systemui.bluetooth.qsdialog.DeviceItemType +import com.android.systemui.bluetooth.qsdialog.SavedDeviceItemFactory +import com.android.systemui.dagger.SysUISingleton +import dagger.Lazy +import dagger.Module +import dagger.Provides + +/** Dagger module for audio sharing code for BT QS dialog */ +@Module +interface AudioSharingModule { + + companion object { + @Provides + @SysUISingleton + fun provideAudioSharingInteractor( + impl: Lazy<AudioSharingInteractorImpl>, + emptyImpl: Lazy<AudioSharingInteractorEmptyImpl>, + ): AudioSharingInteractor = + if (BluetoothUtils.isAudioSharingEnabled()) { + impl.get() + } else { + emptyImpl.get() + } + + @Provides + @SysUISingleton + fun provideDeviceItemActionInteractor( + audioSharingImpl: Lazy<AudioSharingDeviceItemActionInteractorImpl>, + impl: Lazy<DeviceItemActionInteractorImpl>, + ): DeviceItemActionInteractor = + if (BluetoothUtils.isAudioSharingEnabled()) { + audioSharingImpl.get() + } else { + impl.get() + } + + @Provides + @SysUISingleton + fun provideDeviceItemFactoryList( + localBluetoothManager: LocalBluetoothManager? + ): List<DeviceItemFactory> = buildList { + add(ActiveMediaDeviceItemFactory()) + if (BluetoothUtils.isAudioSharingEnabled()) { + add(AudioSharingMediaDeviceItemFactory(localBluetoothManager)) + add(AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager)) + } + add(AvailableMediaDeviceItemFactory()) + add(ConnectedDeviceItemFactory()) + add(SavedDeviceItemFactory()) + } + + @Provides + @SysUISingleton + fun provideDeviceItemDisplayPriority(): List<DeviceItemType> = buildList { + add(DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE) + if (BluetoothUtils.isAudioSharingEnabled()) { + add(DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE) + add(DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE) + } + add(DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE) + add(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE) + add(DeviceItemType.SAVED_BLUETOOTH_DEVICE) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt index dac01028ef64..1009028345de 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.connectivity import android.os.UserManager +import com.android.systemui.bluetooth.qsdialog.dagger.AudioSharingModule import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags.SIGNAL_CALLBACK_DEPRECATION import com.android.systemui.qs.QsEventLogger @@ -56,7 +57,7 @@ import dagger.Provides import dagger.multibindings.IntoMap import dagger.multibindings.StringKey -@Module +@Module(includes = [AudioSharingModule::class]) interface ConnectivityModule { /** Inject BluetoothTile into tileMap in QSModule */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelKosmos.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelKosmos.kt new file mode 100644 index 000000000000..df10f03120de --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelKosmos.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.kosmos.Kosmos + +val Kosmos.audioSharingButtonViewModel: AudioSharingButtonViewModel by + Kosmos.Fixture { + AudioSharingButtonViewModel( + localBluetoothManager, + bluetoothStateInteractor, + deviceItemInteractor, + ) + } + +val Kosmos.audioSharingButtonViewModelFactory: AudioSharingButtonViewModel.Factory by + Kosmos.Fixture { + object : AudioSharingButtonViewModel.Factory { + override fun create(): AudioSharingButtonViewModel { + return audioSharingButtonViewModel + } + } + } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt index 2c53fd67c89d..fa9c27e9a438 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt @@ -26,6 +26,7 @@ import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.lifecycle.activateIn import com.android.systemui.res.R import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -46,7 +47,7 @@ import org.mockito.Mock @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) -class AudioSharingInteractorTest : SysuiTestCase() { +class AudioSharingButtonViewModelTest : SysuiTestCase() { private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) private val bluetoothState = MutableStateFlow(false) @@ -57,7 +58,7 @@ class AudioSharingInteractorTest : SysuiTestCase() { @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor @Mock private lateinit var deviceItem: DeviceItem private lateinit var mockitoSession: StaticMockitoSession - private lateinit var audioSharingInteractor: AudioSharingInteractor + private lateinit var audioSharingButtonViewModel: AudioSharingButtonViewModel @Before fun setUp() { @@ -65,14 +66,13 @@ class AudioSharingInteractorTest : SysuiTestCase() { mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking() whenever(bluetoothStateInteractor.bluetoothStateUpdate).thenReturn(bluetoothState) whenever(deviceItemInteractor.deviceItemUpdate).thenReturn(deviceItemUpdate) - audioSharingInteractor = - AudioSharingInteractor( + audioSharingButtonViewModel = + AudioSharingButtonViewModel( localBluetoothManager, bluetoothStateInteractor, deviceItemInteractor, - testScope.backgroundScope, - testDispatcher, ) + audioSharingButtonViewModel.activateIn(testScope) } @After @@ -83,7 +83,8 @@ class AudioSharingInteractorTest : SysuiTestCase() { @Test fun testButtonStateUpdate_bluetoothOff_returnGone() { testScope.runTest { - val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate) + val actual by + collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate) assertThat(actual).isEqualTo(AudioSharingButtonState.Gone) } @@ -92,7 +93,8 @@ class AudioSharingInteractorTest : SysuiTestCase() { @Test fun testButtonStateUpdate_noDevice_returnGone() { testScope.runTest { - val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate) + val actual by + collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate) bluetoothState.value = true runCurrent() @@ -105,7 +107,8 @@ class AudioSharingInteractorTest : SysuiTestCase() { testScope.runTest { whenever(BluetoothUtils.isBroadcasting(localBluetoothManager)).thenReturn(true) - val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate) + val actual by + collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate) bluetoothState.value = true deviceItemUpdate.emit(listOf()) runCurrent() @@ -133,7 +136,8 @@ class AudioSharingInteractorTest : SysuiTestCase() { ) .thenReturn(true) - val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate) + val actual by + collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate) bluetoothState.value = true deviceItemUpdate.emit(listOf(deviceItem)) runCurrent() @@ -156,7 +160,8 @@ class AudioSharingInteractorTest : SysuiTestCase() { .thenReturn(false) whenever(BluetoothUtils.isActiveLeAudioDevice(cachedBluetoothDevice)).thenReturn(true) - val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate) + val actual by + collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate) bluetoothState.value = true deviceItemUpdate.emit(listOf(deviceItem)) runCurrent() diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorKosmos.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorKosmos.kt new file mode 100644 index 000000000000..ea167df839f6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorKosmos.kt @@ -0,0 +1,49 @@ +/* + * 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.internal.logging.uiEventLogger +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.plugins.activityStarter + +val Kosmos.audioSharingDeviceItemActionInteractorImpl: AudioSharingDeviceItemActionInteractorImpl by + Kosmos.Fixture { + AudioSharingDeviceItemActionInteractorImpl( + activityStarter, + dialogTransitionAnimator, + localBluetoothManager, + testDispatcher, + testDispatcher, + bluetoothTileDialogLogger, + uiEventLogger, + audioSharingDialogDelegateFactory, + deviceItemActionInteractorImpl, + ) + } + +val Kosmos.audioSharingDialogDelegateFactory: AudioSharingDialogDelegate.Factory by + Kosmos.Fixture { + object : AudioSharingDialogDelegate.Factory { + override fun create( + cachedBluetoothDevice: CachedBluetoothDevice + ): AudioSharingDialogDelegate { + return audioSharingDialogDelegate + } + } + } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt new file mode 100644 index 000000000000..130f774d6994 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt @@ -0,0 +1,189 @@ +/* + * 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.BluetoothDevice +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.settingslib.bluetooth.BluetoothUtils +import com.android.settingslib.bluetooth.LeAudioProfile +import com.android.settingslib.flags.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.activityStarter +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.testKosmos +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.verify +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.whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@OptIn(ExperimentalCoroutinesApi::class) +class AudioSharingDeviceItemActionInteractorTest : SysuiTestCase() { + @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() } + private lateinit var actionInteractorImpl: DeviceItemActionInteractor + private lateinit var mockitoSession: StaticMockitoSession + private lateinit var connectedAudioSharingMediaDeviceItem: DeviceItem + private lateinit var connectedMediaDeviceItem: DeviceItem + @Mock private lateinit var dialog: SystemUIDialog + @Mock private lateinit var leAudioProfile: LeAudioProfile + @Mock private lateinit var bluetoothDevice: BluetoothDevice + + @Before + fun setUp() { + mockitoSession = + mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking() + connectedMediaDeviceItem = + DeviceItem( + type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, + cachedBluetoothDevice = kosmos.cachedBluetoothDevice, + deviceName = DEVICE_NAME, + connectionSummary = DEVICE_CONNECTION_SUMMARY, + iconWithDescription = null, + background = null + ) + connectedAudioSharingMediaDeviceItem = + DeviceItem( + type = DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, + cachedBluetoothDevice = kosmos.cachedBluetoothDevice, + deviceName = DEVICE_NAME, + connectionSummary = DEVICE_CONNECTION_SUMMARY, + iconWithDescription = null, + background = null + ) + actionInteractorImpl = kosmos.audioSharingDeviceItemActionInteractorImpl + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + } + + @Test + @EnableFlags(Flags.FLAG_AUDIO_SHARING_QS_DIALOG_IMPROVEMENT) + fun testOnClick_connectedAudioSharingMediaDevice_flagOn_createDialog() { + with(kosmos) { + testScope.runTest { + actionInteractorImpl.onClick(connectedAudioSharingMediaDeviceItem, dialog) + verify(dialogTransitionAnimator) + .showFromDialog(any(), any(), eq(null), anyBoolean()) + } + } + } + + @Test + @DisableFlags(Flags.FLAG_AUDIO_SHARING_QS_DIALOG_IMPROVEMENT) + fun testOnClick_connectedAudioSharingMediaDevice_flagOff_shouldLaunchSettings() { + with(kosmos) { + testScope.runTest { + whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice) + actionInteractorImpl.onClick(connectedAudioSharingMediaDeviceItem, dialog) + verify(activityStarter) + .postStartActivityDismissingKeyguard( + ArgumentMatchers.any(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any() + ) + verify(dialogTransitionAnimator, never()) + .showFromDialog(any(), any(), eq(null), anyBoolean()) + } + } + } + + @Test + fun testOnClick_inAudioSharing_clickedDeviceHasSource_shouldNotLaunchSettings() { + with(kosmos) { + testScope.runTest { + whenever(cachedBluetoothDevice.uiAccessibleProfiles) + .thenReturn(listOf(leAudioProfile)) + whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true) + whenever( + BluetoothUtils.hasConnectedBroadcastSource( + ArgumentMatchers.any(), + ArgumentMatchers.any() + ) + ) + .thenReturn(true) + + actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) + verify(activityStarter, Mockito.never()) + .postStartActivityDismissingKeyguard( + ArgumentMatchers.any(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any() + ) + } + } + } + + @Test + fun testOnClick_inAudioSharing_clickedDeviceNoSource_shouldLaunchSettings() { + with(kosmos) { + testScope.runTest { + whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice) + whenever(cachedBluetoothDevice.uiAccessibleProfiles) + .thenReturn(listOf(leAudioProfile)) + + whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true) + whenever( + BluetoothUtils.hasConnectedBroadcastSource( + ArgumentMatchers.any(), + ArgumentMatchers.any() + ) + ) + .thenReturn(false) + + actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) + verify(activityStarter) + .postStartActivityDismissingKeyguard( + ArgumentMatchers.any(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any() + ) + } + } + } + + private companion object { + const val DEVICE_NAME = "device" + const val DEVICE_CONNECTION_SUMMARY = "active" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt new file mode 100644 index 000000000000..1e0a7db6b688 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt @@ -0,0 +1,113 @@ +/* + * 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.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.widget.Button +import android.widget.TextView +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +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.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@OptIn(ExperimentalCoroutinesApi::class) +class AudioSharingDialogDelegateTest : SysuiTestCase() { + @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + private val kosmos = testKosmos() + private val updateFlow = MutableSharedFlow<Unit>() + private lateinit var underTest: AudioSharingDialogDelegate + + @Before + fun setUp() { + with(kosmos) { + // TODO(b/364515243): use real object instead of mock + whenever(deviceItemInteractor.deviceItemUpdateRequest).thenReturn(updateFlow) + whenever(deviceItemInteractor.deviceItemUpdate) + .thenReturn(MutableStateFlow(emptyList())) + underTest = audioSharingDialogDelegate + } + } + + @Test + fun testCreateDialog() = + kosmos.testScope.runTest { + val dialog = underTest.createDialog() + assertThat(dialog).isInstanceOf(SystemUIDialog::class.java) + } + + @Test + fun testCreateDialog_showState() = + with(kosmos) { + testScope.runTest { + val availableDeviceName = "name" + whenever(cachedBluetoothDevice.name).thenReturn(availableDeviceName) + val dialog = underTest.createDialog() + dialog.show() + runCurrent() + val subtitleTextView = dialog.findViewById<TextView>(R.id.subtitle) + val switchActiveButton = dialog.findViewById<Button>(R.id.switch_active_button) + val subtitle = + context.getString( + R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle, + availableDeviceName, + "" + ) + val switchButtonText = + context.getString( + R.string.quick_settings_bluetooth_audio_sharing_dialog_switch_to_button, + availableDeviceName + ) + assertThat(subtitleTextView.text).isEqualTo(subtitle) + assertThat(switchActiveButton.text).isEqualTo(switchButtonText) + dialog.dismiss() + } + } + + @Test + fun testCreateDialog_hideState() = + with(kosmos) { + testScope.runTest { + val dialog = spy(underTest.createDialog()) + dialog.show() + runCurrent() + updateFlow.emit(Unit) + runCurrent() + verify(dialog).dismiss() + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelKosmos.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelKosmos.kt new file mode 100644 index 000000000000..3ffd771014ba --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelKosmos.kt @@ -0,0 +1,60 @@ +/* + * 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.content.applicationContext +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.phone.systemUIDialogDotFactory +import org.mockito.kotlin.mock + +val Kosmos.cachedBluetoothDevice: CachedBluetoothDevice by Kosmos.Fixture { mock {} } + +val Kosmos.audioSharingDialogViewModel: AudioSharingDialogViewModel by + Kosmos.Fixture { + AudioSharingDialogViewModel( + deviceItemInteractor, + audioSharingInteractor, + applicationContext, + localBluetoothManager, + cachedBluetoothDevice, + testDispatcher + ) + } + +val Kosmos.audioSharingDialogViewModelFactory: AudioSharingDialogViewModel.Factory by + Kosmos.Fixture { + object : AudioSharingDialogViewModel.Factory { + override fun create( + cachedBluetoothDevice: CachedBluetoothDevice + ): AudioSharingDialogViewModel { + return audioSharingDialogViewModel + } + } + } + +val Kosmos.audioSharingDialogDelegate: AudioSharingDialogDelegate by + Kosmos.Fixture { + AudioSharingDialogDelegate( + cachedBluetoothDevice, + testScope.backgroundScope, + audioSharingDialogViewModelFactory, + systemUIDialogDotFactory, + ) + } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelTest.kt new file mode 100644 index 000000000000..beb816cae095 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelTest.kt @@ -0,0 +1,142 @@ +/* + * 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.BluetoothDevice +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.LeAudioProfile +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager +import com.android.systemui.SysuiTestCase +import com.android.systemui.bluetooth.cachedBluetoothDeviceManager +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +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.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@OptIn(ExperimentalCoroutinesApi::class) +class AudioSharingDialogViewModelTest : SysuiTestCase() { + @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() } + @Mock private lateinit var profileManager: LocalBluetoothProfileManager + @Mock private lateinit var leAudioProfile: LeAudioProfile + private val updateFlow = MutableSharedFlow<Unit>() + private lateinit var underTest: AudioSharingDialogViewModel + + @Before + fun setUp() { + with(kosmos) { + // TODO(b/364515243): use real object instead of mock + whenever(deviceItemInteractor.deviceItemUpdateRequest).thenReturn(updateFlow) + whenever(deviceItemInteractor.deviceItemUpdate) + .thenReturn(MutableStateFlow(emptyList())) + underTest = audioSharingDialogViewModel + } + } + + @Test + fun testDialogState_show() = + with(kosmos) { + testScope.runTest { + val deviceName = "name" + whenever(cachedBluetoothDevice.name).thenReturn(deviceName) + val actual by collectLastValue(underTest.dialogState) + runCurrent() + assertThat(actual) + .isEqualTo( + AudioSharingDialogState.Show( + context.getString( + R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle, + deviceName, + "" + ), + context.getString( + R.string + .quick_settings_bluetooth_audio_sharing_dialog_switch_to_button, + deviceName + ) + ) + ) + } + } + + @Test + fun testDialogState_showWithActiveDeviceName() = + with(kosmos) { + testScope.runTest { + val deviceName = "name" + whenever(cachedBluetoothDevice.name).thenReturn(deviceName) + whenever(localBluetoothManager.profileManager).thenReturn(profileManager) + whenever(localBluetoothManager.cachedDeviceManager) + .thenReturn(cachedBluetoothDeviceManager) + whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) + whenever(leAudioProfile.activeDevices).thenReturn(listOf(mock<BluetoothDevice>())) + whenever(cachedBluetoothDeviceManager.findDevice(any())) + .thenReturn(cachedBluetoothDevice) + val actual by collectLastValue(underTest.dialogState) + runCurrent() + assertThat(actual) + .isEqualTo( + AudioSharingDialogState.Show( + context.getString( + R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle, + deviceName, + deviceName + ), + context.getString( + R.string + .quick_settings_bluetooth_audio_sharing_dialog_switch_to_button, + deviceName + ) + ) + ) + } + } + + @Test + fun testDialogState_hide() = + with(kosmos) { + testScope.runTest { + val actual by collectLastValue(underTest.dialogState) + runCurrent() + updateFlow.emit(Unit) + assertThat(actual).isEqualTo(AudioSharingDialogState.Hide) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorKosmos.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorKosmos.kt new file mode 100644 index 000000000000..8e473bee1450 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorKosmos.kt @@ -0,0 +1,28 @@ +/* + * 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.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher + +val Kosmos.audioSharingInteractor: AudioSharingInteractor by + Kosmos.Fixture { + AudioSharingInteractorImpl( + localBluetoothManager, + testDispatcher, + ) + } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorKosmos.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorKosmos.kt new file mode 100644 index 000000000000..aaa918c9ff35 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorKosmos.kt @@ -0,0 +1,31 @@ +/* + * 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.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope + +val Kosmos.bluetoothStateInteractor: BluetoothStateInteractor by + Kosmos.Fixture { + BluetoothStateInteractor( + localBluetoothManager, + bluetoothTileDialogLogger, + testScope.backgroundScope, + testDispatcher + ) + } 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 d7bea6680c2d..bd7a60c45e36 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 @@ -33,6 +33,7 @@ import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.testKosmos import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.kotlin.getMutableStateFlow @@ -68,6 +69,7 @@ import org.mockito.junit.MockitoRule class BluetoothTileDialogViewModelTest : SysuiTestCase() { @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + private val kosmos = testKosmos() private val fakeSystemClock = FakeSystemClock() private val backgroundExecutor = FakeExecutor(fakeSystemClock) @@ -75,8 +77,6 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor - @Mock private lateinit var audioSharingInteractor: AudioSharingInteractor - @Mock private lateinit var bluetoothDeviceMetadataInteractor: BluetoothDeviceMetadataInteractor @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor @@ -120,6 +120,8 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { scheduler = TestCoroutineScheduler() dispatcher = UnconfinedTestDispatcher(scheduler) testScope = TestScope(dispatcher) + // TODO(b/364515243): use real object instead of mock + whenever(kosmos.deviceItemInteractor.deviceItemUpdate).thenReturn(MutableSharedFlow()) bluetoothTileDialogViewModel = BluetoothTileDialogViewModel( deviceItemInteractor, @@ -139,11 +141,12 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { dispatcher ) ), - audioSharingInteractor, + kosmos.audioSharingButtonViewModelFactory, bluetoothDeviceMetadataInteractor, mDialogTransitionAnimator, activityStarter, uiEventLogger, + bluetoothTileDialogLogger, testScope.backgroundScope, dispatcher, dispatcher, @@ -161,13 +164,10 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { whenever(sysuiDialog.context).thenReturn(mContext) whenever(bluetoothTileDialogDelegate.bluetoothStateToggle) .thenReturn(getMutableStateFlow(false)) - whenever(bluetoothTileDialogDelegate.deviceItemClick) - .thenReturn(getMutableStateFlow(deviceItem)) + whenever(bluetoothTileDialogDelegate.deviceItemClick).thenReturn(MutableSharedFlow()) whenever(bluetoothTileDialogDelegate.contentHeight).thenReturn(getMutableStateFlow(0)) whenever(bluetoothTileDialogDelegate.bluetoothAutoOnToggle) .thenReturn(getMutableStateFlow(false)) - whenever(audioSharingInteractor.audioSharingButtonStateUpdate) - .thenReturn(getMutableStateFlow(AudioSharingButtonState.Gone)) whenever(expandable.dialogTransitionController(any())).thenReturn(controller) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt index 681ea754e630..9c427c6b085e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt @@ -15,34 +15,22 @@ */ package com.android.systemui.bluetooth.qsdialog -import android.bluetooth.BluetoothDevice import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest -import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession -import com.android.dx.mockito.inline.extended.StaticMockitoSession -import com.android.settingslib.bluetooth.BluetoothUtils -import com.android.settingslib.bluetooth.CachedBluetoothDevice -import com.android.settingslib.bluetooth.LeAudioProfile -import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant -import com.android.settingslib.bluetooth.LocalBluetoothProfileManager import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope -import com.android.systemui.plugins.activityStarter import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.testKosmos import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest -import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers import org.mockito.Mock -import org.mockito.Mockito import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule @@ -56,28 +44,18 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() } private lateinit var actionInteractorImpl: DeviceItemActionInteractor - private lateinit var mockitoSession: StaticMockitoSession private lateinit var activeMediaDeviceItem: DeviceItem private lateinit var notConnectedDeviceItem: DeviceItem - private lateinit var connectedAudioSharingMediaDeviceItem: DeviceItem private lateinit var connectedMediaDeviceItem: DeviceItem private lateinit var connectedOtherDeviceItem: DeviceItem @Mock private lateinit var dialog: SystemUIDialog - @Mock private lateinit var profileManager: LocalBluetoothProfileManager - @Mock private lateinit var leAudioProfile: LeAudioProfile - @Mock private lateinit var assistantProfile: LocalBluetoothLeBroadcastAssistant - @Mock private lateinit var bluetoothDevice: BluetoothDevice - @Mock private lateinit var bluetoothDeviceGroupId2: BluetoothDevice - @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice @Before fun setUp() { - mockitoSession = - mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking() activeMediaDeviceItem = DeviceItem( type = DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE, - cachedBluetoothDevice = cachedBluetoothDevice, + cachedBluetoothDevice = kosmos.cachedBluetoothDevice, deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, @@ -86,7 +64,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { notConnectedDeviceItem = DeviceItem( type = DeviceItemType.SAVED_BLUETOOTH_DEVICE, - cachedBluetoothDevice = cachedBluetoothDevice, + cachedBluetoothDevice = kosmos.cachedBluetoothDevice, deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, @@ -95,16 +73,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { connectedMediaDeviceItem = DeviceItem( type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, - cachedBluetoothDevice = cachedBluetoothDevice, - deviceName = DEVICE_NAME, - connectionSummary = DEVICE_CONNECTION_SUMMARY, - iconWithDescription = null, - background = null - ) - connectedAudioSharingMediaDeviceItem = - DeviceItem( - type = DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, - cachedBluetoothDevice = cachedBluetoothDevice, + cachedBluetoothDevice = kosmos.cachedBluetoothDevice, deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, @@ -113,18 +82,13 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { connectedOtherDeviceItem = DeviceItem( type = DeviceItemType.CONNECTED_BLUETOOTH_DEVICE, - cachedBluetoothDevice = cachedBluetoothDevice, + cachedBluetoothDevice = kosmos.cachedBluetoothDevice, deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, background = null ) - actionInteractorImpl = kosmos.deviceItemActionInteractor - } - - @After - fun tearDown() { - mockitoSession.finishMocking() + actionInteractorImpl = kosmos.deviceItemActionInteractorImpl } @Test @@ -132,14 +96,8 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { with(kosmos) { testScope.runTest { whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false) actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) verify(cachedBluetoothDevice).setActive() - verify(bluetoothTileDialogLogger) - .logDeviceClick( - cachedBluetoothDevice.address, - DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE - ) } } } @@ -149,14 +107,8 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { with(kosmos) { testScope.runTest { whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false) actionInteractorImpl.onClick(activeMediaDeviceItem, dialog) verify(cachedBluetoothDevice).disconnect() - verify(bluetoothTileDialogLogger) - .logDeviceClick( - cachedBluetoothDevice.address, - DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE - ) } } } @@ -166,14 +118,8 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { with(kosmos) { testScope.runTest { whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false) actionInteractorImpl.onClick(connectedOtherDeviceItem, dialog) verify(cachedBluetoothDevice).disconnect() - verify(bluetoothTileDialogLogger) - .logDeviceClick( - cachedBluetoothDevice.address, - DeviceItemType.CONNECTED_BLUETOOTH_DEVICE - ) } } } @@ -183,293 +129,8 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { with(kosmos) { testScope.runTest { whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false) actionInteractorImpl.onClick(notConnectedDeviceItem, dialog) verify(cachedBluetoothDevice).connect() - verify(bluetoothTileDialogLogger) - .logDeviceClick( - cachedBluetoothDevice.address, - DeviceItemType.SAVED_BLUETOOTH_DEVICE - ) - } - } - } - - @Test - fun testOnClick_connectedAudioSharingMediaDevice_logClick() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - actionInteractorImpl.onClick(connectedAudioSharingMediaDeviceItem, dialog) - verify(bluetoothTileDialogLogger) - .logDeviceClick( - cachedBluetoothDevice.address, - DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE - ) - } - } - } - - @Test - fun testOnClick_audioSharingDisabled_shouldNotLaunchSettings() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false) - - actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) - verify(activityStarter, Mockito.never()) - .postStartActivityDismissingKeyguard( - ArgumentMatchers.any(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - } - } - } - - @Test - fun testOnClick_inAudioSharing_clickedDeviceHasSource_shouldNotLaunchSettings() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(cachedBluetoothDevice.uiAccessibleProfiles) - .thenReturn(listOf(leAudioProfile)) - - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) - whenever(localBluetoothManager.profileManager).thenReturn(profileManager) - whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) - whenever(profileManager.leAudioBroadcastAssistantProfile) - .thenReturn(assistantProfile) - - whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true) - whenever( - BluetoothUtils.hasConnectedBroadcastSource( - ArgumentMatchers.any(), - ArgumentMatchers.any() - ) - ) - .thenReturn(true) - - actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) - verify(activityStarter, Mockito.never()) - .postStartActivityDismissingKeyguard( - ArgumentMatchers.any(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - } - } - } - - @Test - fun testOnClick_inAudioSharing_clickedDeviceNoSource_shouldLaunchSettings() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice) - whenever(cachedBluetoothDevice.uiAccessibleProfiles) - .thenReturn(listOf(leAudioProfile)) - - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) - whenever(localBluetoothManager.profileManager).thenReturn(profileManager) - whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) - whenever(profileManager.leAudioBroadcastAssistantProfile) - .thenReturn(assistantProfile) - - whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true) - whenever( - BluetoothUtils.hasConnectedBroadcastSource( - ArgumentMatchers.any(), - ArgumentMatchers.any() - ) - ) - .thenReturn(false) - - actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) - verify(activityStarter) - .postStartActivityDismissingKeyguard( - ArgumentMatchers.any(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - } - } - } - - @Test - fun testOnClick_noConnectedLeDevice_shouldNotLaunchSettings() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) - whenever(localBluetoothManager.profileManager).thenReturn(profileManager) - whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) - whenever(profileManager.leAudioBroadcastAssistantProfile) - .thenReturn(assistantProfile) - - actionInteractorImpl.onClick(notConnectedDeviceItem, dialog) - verify(activityStarter, Mockito.never()) - .postStartActivityDismissingKeyguard( - ArgumentMatchers.any(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - } - } - } - - @Test - fun testOnClick_hasOneConnectedLeDevice_clickedNonLe_shouldNotLaunchSettings() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) - whenever(localBluetoothManager.profileManager).thenReturn(profileManager) - whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) - whenever(profileManager.leAudioBroadcastAssistantProfile) - .thenReturn(assistantProfile) - - whenever( - assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any()) - ) - .thenReturn(listOf(bluetoothDevice)) - - actionInteractorImpl.onClick(notConnectedDeviceItem, dialog) - verify(activityStarter, Mockito.never()) - .postStartActivityDismissingKeyguard( - ArgumentMatchers.any(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - } - } - } - - @Test - fun testOnClick_hasOneConnectedLeDevice_clickedLe_shouldLaunchSettings() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice) - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(cachedBluetoothDevice.profiles).thenReturn(listOf(leAudioProfile)) - whenever(leAudioProfile.isEnabled(ArgumentMatchers.any())).thenReturn(true) - - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) - whenever(localBluetoothManager.profileManager).thenReturn(profileManager) - whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) - whenever(profileManager.leAudioBroadcastAssistantProfile) - .thenReturn(assistantProfile) - - whenever( - assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any()) - ) - .thenReturn(listOf(bluetoothDevice)) - - actionInteractorImpl.onClick(notConnectedDeviceItem, dialog) - verify(activityStarter) - .postStartActivityDismissingKeyguard( - ArgumentMatchers.any(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - } - } - } - - @Test - fun testOnClick_hasOneConnectedLeDevice_clickedConnectedLe_shouldNotLaunchSettings() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) - whenever(localBluetoothManager.profileManager).thenReturn(profileManager) - whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) - whenever(profileManager.leAudioBroadcastAssistantProfile) - .thenReturn(assistantProfile) - - whenever( - assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any()) - ) - .thenReturn(listOf(bluetoothDevice)) - - actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) - verify(activityStarter, Mockito.never()) - .postStartActivityDismissingKeyguard( - ArgumentMatchers.any(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - } - } - } - - @Test - fun testOnClick_hasTwoConnectedLeDevice_clickedNotConnectedLe_shouldNotLaunchSettings() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) - whenever(localBluetoothManager.profileManager).thenReturn(profileManager) - whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) - whenever(profileManager.leAudioBroadcastAssistantProfile) - .thenReturn(assistantProfile) - - whenever( - assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any()) - ) - .thenReturn(listOf(bluetoothDevice, bluetoothDeviceGroupId2)) - whenever(leAudioProfile.getGroupId(ArgumentMatchers.any())).thenAnswer { - val device = it.arguments.first() as BluetoothDevice - if (device == bluetoothDevice) GROUP_ID_1 else GROUP_ID_2 - } - - actionInteractorImpl.onClick(notConnectedDeviceItem, dialog) - verify(activityStarter, Mockito.never()) - .postStartActivityDismissingKeyguard( - ArgumentMatchers.any(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - } - } - } - - @Test - fun testOnClick_hasTwoConnectedLeDevice_clickedActiveLe_shouldLaunchSettings() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice) - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(cachedBluetoothDevice.profiles).thenReturn(listOf(leAudioProfile)) - whenever(leAudioProfile.isEnabled(ArgumentMatchers.any())).thenReturn(true) - - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) - whenever(localBluetoothManager.profileManager).thenReturn(profileManager) - whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) - whenever(profileManager.leAudioBroadcastAssistantProfile) - .thenReturn(assistantProfile) - - whenever( - assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any()) - ) - .thenReturn(listOf(bluetoothDevice, bluetoothDeviceGroupId2)) - whenever(leAudioProfile.getGroupId(ArgumentMatchers.any())).thenAnswer { - val device = it.arguments.first() as BluetoothDevice - if (device == bluetoothDevice) GROUP_ID_1 else GROUP_ID_2 - } - - actionInteractorImpl.onClick(activeMediaDeviceItem, dialog) - verify(activityStarter) - .postStartActivityDismissingKeyguard( - ArgumentMatchers.any(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) } } } @@ -478,7 +139,5 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { const val DEVICE_NAME = "device" const val DEVICE_CONNECTION_SUMMARY = "active" const val DEVICE_ADDRESS = "address" - const val GROUP_ID_1 = 1 - const val GROUP_ID_2 = 2 } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt index ef441c1dc12c..274f916710ac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt @@ -133,10 +133,6 @@ class DeviceItemFactoryTest : SysuiTestCase() { @Test fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_flagOff_returnsFalse() { - // Flags.FLAG_ENABLE_LE_AUDIO_SHARING off or the device doesn't support broadcast - // source or assistant. - `when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false) - assertThat( AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager) .isFilterMatched(context, cachedDevice, audioManager) @@ -146,9 +142,6 @@ class DeviceItemFactoryTest : SysuiTestCase() { @Test fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isActiveDevice_returnsFalse() { - // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and - // assistant. - `when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) `when`(BluetoothUtils.isActiveMediaDevice(any())).thenReturn(true) assertThat( @@ -160,9 +153,6 @@ class DeviceItemFactoryTest : SysuiTestCase() { @Test fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isNotAvailable_returnsFalse() { - // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and - // assistant. - `when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) `when`(BluetoothUtils.isActiveMediaDevice(any())).thenReturn(false) `when`(BluetoothUtils.isAvailableMediaBluetoothDevice(any(), any())).thenReturn(true) `when`(BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice(any(), any())) @@ -177,9 +167,6 @@ class DeviceItemFactoryTest : SysuiTestCase() { @Test fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_returnsTrue() { - // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and - // assistant. - `when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) `when`(BluetoothUtils.isActiveMediaDevice(any())).thenReturn(false) `when`(BluetoothUtils.isAvailableMediaBluetoothDevice(any(), any())).thenReturn(true) `when`(BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice(any(), any())) 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 194590c1f626..c39b9a606cfe 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 @@ -83,18 +83,6 @@ class DeviceItemInteractorTest : SysuiTestCase() { fun setUp() { dispatcher = UnconfinedTestDispatcher() testScope = TestScope(dispatcher) - interactor = - DeviceItemInteractor( - bluetoothTileDialogRepository, - audioManager, - adapter, - localBluetoothManager, - fakeSystemClock, - logger, - testScope.backgroundScope, - dispatcher - ) - `when`(deviceItem1.cachedBluetoothDevice).thenReturn(cachedDevice1) `when`(deviceItem2.cachedBluetoothDevice).thenReturn(cachedDevice2) `when`(cachedDevice1.address).thenReturn("ADDRESS") @@ -108,9 +96,19 @@ class DeviceItemInteractorTest : SysuiTestCase() { fun testUpdateDeviceItems_noCachedDevice_returnEmpty() { testScope.runTest { `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(emptyList()) - interactor.setDeviceItemFactoryListForTesting( - listOf(createFactory({ true }, deviceItem1)) - ) + interactor = + DeviceItemInteractor( + bluetoothTileDialogRepository, + audioManager, + adapter, + localBluetoothManager, + fakeSystemClock, + logger, + listOf(createFactory({ true }, deviceItem1)), + emptyList(), + testScope.backgroundScope, + dispatcher + ) val latest by collectLastValue(interactor.deviceItemUpdate) val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate) @@ -125,9 +123,19 @@ class DeviceItemInteractorTest : SysuiTestCase() { fun testUpdateDeviceItems_hasCachedDevice_filterNotMatch_returnEmpty() { testScope.runTest { `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(listOf(cachedDevice1)) - interactor.setDeviceItemFactoryListForTesting( - listOf(createFactory({ false }, deviceItem1)) - ) + interactor = + DeviceItemInteractor( + bluetoothTileDialogRepository, + audioManager, + adapter, + localBluetoothManager, + fakeSystemClock, + logger, + listOf(createFactory({ false }, deviceItem1)), + emptyList(), + testScope.backgroundScope, + dispatcher + ) val latest by collectLastValue(interactor.deviceItemUpdate) val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate) @@ -142,9 +150,19 @@ class DeviceItemInteractorTest : SysuiTestCase() { fun testUpdateDeviceItems_hasCachedDevice_filterMatch_returnDeviceItem() { testScope.runTest { `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(listOf(cachedDevice1)) - interactor.setDeviceItemFactoryListForTesting( - listOf(createFactory({ true }, deviceItem1)) - ) + interactor = + DeviceItemInteractor( + bluetoothTileDialogRepository, + audioManager, + adapter, + localBluetoothManager, + fakeSystemClock, + logger, + listOf(createFactory({ true }, deviceItem1)), + emptyList(), + testScope.backgroundScope, + dispatcher + ) val latest by collectLastValue(interactor.deviceItemUpdate) val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate) @@ -159,9 +177,22 @@ class DeviceItemInteractorTest : SysuiTestCase() { fun testUpdateDeviceItems_hasCachedDevice_filterMatch_returnMultipleDeviceItem() { testScope.runTest { `when`(adapter.mostRecentlyConnectedDevices).thenReturn(null) - interactor.setDeviceItemFactoryListForTesting( - listOf(createFactory({ false }, deviceItem1), createFactory({ true }, deviceItem2)) - ) + interactor = + DeviceItemInteractor( + bluetoothTileDialogRepository, + audioManager, + adapter, + localBluetoothManager, + fakeSystemClock, + logger, + listOf( + createFactory({ false }, deviceItem1), + createFactory({ true }, deviceItem2) + ), + emptyList(), + testScope.backgroundScope, + dispatcher + ) val latest by collectLastValue(interactor.deviceItemUpdate) val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate) @@ -176,18 +207,31 @@ class DeviceItemInteractorTest : SysuiTestCase() { 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 + interactor = + DeviceItemInteractor( + bluetoothTileDialogRepository, + audioManager, + adapter, + localBluetoothManager, + fakeSystemClock, + logger, + listOf( + createFactory( + { cachedDevice -> cachedDevice.device == device1 }, + deviceItem1 + ), + createFactory( + { cachedDevice -> cachedDevice.device == device2 }, + deviceItem2 + ) + ), + listOf( + DeviceItemType.SAVED_BLUETOOTH_DEVICE, + DeviceItemType.CONNECTED_BLUETOOTH_DEVICE + ), + testScope.backgroundScope, + dispatcher ) - ) `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE) `when`(deviceItem2.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE) @@ -204,15 +248,28 @@ class DeviceItemInteractorTest : SysuiTestCase() { 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 = + DeviceItemInteractor( + bluetoothTileDialogRepository, + audioManager, + adapter, + localBluetoothManager, + fakeSystemClock, + logger, + listOf( + createFactory( + { cachedDevice -> cachedDevice.device == device1 }, + deviceItem1 + ), + createFactory( + { cachedDevice -> cachedDevice.device == device2 }, + deviceItem2 + ) + ), + listOf(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE), + testScope.backgroundScope, + dispatcher ) - ) - interactor.setDisplayPriorityForTesting( - listOf(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE) - ) `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE) `when`(deviceItem2.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE) @@ -231,10 +288,19 @@ class DeviceItemInteractorTest : SysuiTestCase() { `when`(bluetoothTileDialogRepository.cachedDevices) .thenReturn(listOf(cachedDevice2, cachedDevice2, cachedDevice2, cachedDevice2)) `when`(adapter.mostRecentlyConnectedDevices).thenReturn(null) - interactor.setDeviceItemFactoryListForTesting( - listOf(createFactory({ true }, deviceItem2)) - ) - + interactor = + DeviceItemInteractor( + bluetoothTileDialogRepository, + audioManager, + adapter, + localBluetoothManager, + fakeSystemClock, + logger, + listOf(createFactory({ true }, deviceItem2)), + emptyList(), + testScope.backgroundScope, + dispatcher + ) val latest by collectLastValue(interactor.deviceItemUpdate) val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate) interactor.updateDeviceItems(mContext, DeviceFetchTrigger.FIRST_LOAD) |