From ac00aef0426c20e0cfd30c222829708313b24c36 Mon Sep 17 00:00:00 2001 From: chelseahao Date: Tue, 21 May 2024 16:19:10 +0800 Subject: Move overlay click handling back to SystemUI. Test: atest -c com.android.systemui.bluetooth.qsdialog Bug: 340379827 Flag: NA Change-Id: Ieddca3ec1f05d30aff94b50e9849b6a80e153666 --- packages/SystemUI/Android.bp | 1 + .../qsdialog/BluetoothTileDialogLogger.kt | 26 ++ .../qsdialog/BluetoothTileDialogModule.kt | 29 -- .../qsdialog/BluetoothTileDialogUiEvent.kt | 12 +- .../qsdialog/DeviceItemActionInteractor.kt | 251 ++++++++++- .../com/android/systemui/qs/dagger/QSModule.java | 2 - .../qsdialog/DeviceItemActionInteractorImplTest.kt | 121 ------ .../qsdialog/DeviceItemActionInteractorKosmos.kt | 13 +- .../qsdialog/DeviceItemActionInteractorTest.kt | 459 +++++++++++++++++++++ 9 files changed, 751 insertions(+), 163 deletions(-) delete mode 100644 packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogModule.kt delete mode 100644 packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorImplTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 1e79bb7b8cc8..fde7c2caca06 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -368,6 +368,7 @@ filegroup { "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/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt", "tests/src/**/systemui/notetask/LaunchNotesRoleSettingsTrampolineActivityTest.kt", "tests/src/**/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt", diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogLogger.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogLogger.kt index c30aea07e959..72312b87dc57 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogLogger.kt @@ -16,6 +16,7 @@ package com.android.systemui.bluetooth.qsdialog +import android.bluetooth.BluetoothDevice import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel.DEBUG import com.android.systemui.log.dagger.BluetoothTileDialogLog @@ -103,4 +104,29 @@ constructor(@BluetoothTileDialogLog private val logBuffer: LogBuffer) { fun logDeviceUiUpdate(duration: Long) = logBuffer.log(TAG, DEBUG, { long1 = duration }, { "DeviceUiUpdate. duration=$long1" }) + + fun logDeviceClickInAudioSharingWhenEnabled(inAudioSharing: Boolean) { + logBuffer.log( + TAG, + DEBUG, + { str1 = inAudioSharing.toString() }, + { "DeviceClick. in audio sharing=$str1" } + ) + } + + fun logConnectedLeByGroupId(map: Map>) { + logBuffer.log(TAG, DEBUG, { str1 = map.toString() }, { "ConnectedLeByGroupId. map=$str1" }) + } + + fun logLaunchSettingsCriteriaMatched(criteria: String, deviceItem: DeviceItem) { + logBuffer.log( + TAG, + DEBUG, + { + str1 = criteria + str2 = deviceItem.toString() + }, + { "$str1. deviceItem=$str2" } + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogModule.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogModule.kt deleted file mode 100644 index 2e9169e03d80..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogModule.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.dagger.SysUISingleton -import dagger.Binds -import dagger.Module - -@Module -interface BluetoothTileDialogModule { - @Binds - @SysUISingleton - fun bindDeviceItemActionInteractor( - impl: DeviceItemActionInteractorImpl - ): DeviceItemActionInteractor -} 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 b592b8ed4332..4a358c0b1292 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt @@ -35,7 +35,17 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent CONNECTED_OTHER_DEVICE_DISCONNECT(1508), @UiEvent(doc = "The auto on toggle is clicked") BLUETOOTH_AUTO_ON_TOGGLE_CLICKED(1617), @UiEvent(doc = "The audio sharing button is clicked") - BLUETOOTH_AUDIO_SHARING_BUTTON_CLICKED(1700); + BLUETOOTH_AUDIO_SHARING_BUTTON_CLICKED(1700), + @UiEvent(doc = "Currently broadcasting and a LE audio supported device is clicked") + 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), + @UiEvent( + doc = "Not broadcasting, having one connected, another saved LE audio device is clicked" + ) + LAUNCH_SETTINGS_NOT_SHARING_SAVED_LE_DEVICE_CLICKED(1719), + @UiEvent(doc = "Not broadcasting, one of the two connected LE audio devices is clicked") + LAUNCH_SETTINGS_NOT_SHARING_CONNECTED_LE_DEVICE_CLICKED(1720); override fun getId() = metricId } 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 931176003b1b..4dafa93ab5c2 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt @@ -16,32 +16,87 @@ 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 -/** Defines interface for click handling of a DeviceItem. */ -interface DeviceItemActionInteractor { - suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) -} - @SysUISingleton -open class DeviceItemActionInteractorImpl +class DeviceItemActionInteractor @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, -) : DeviceItemActionInteractor { +) { + private val leAudioProfile: LeAudioProfile? + get() = localBluetoothManager?.profileManager?.leAudioProfile + + private val assistantProfile: LocalBluetoothLeBroadcastAssistant? + get() = localBluetoothManager?.profileManager?.leAudioBroadcastAssistantProfile + + private val launchSettingsCriteriaList: List + get() = + listOf( + InSharingClickedNoSource(localBluetoothManager, backgroundDispatcher, logger), + NotSharingClickedNonConnect( + leAudioProfile, + assistantProfile, + backgroundDispatcher, + logger + ), + NotSharingClickedConnected( + leAudioProfile, + assistantProfile, + backgroundDispatcher, + logger + ) + ) - override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) { + 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 -> { @@ -69,4 +124,184 @@ constructor( } } } + + 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 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> { + 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 NotSharingClickedConnected( + 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 any connected LE audio + // devices + 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.isActiveOrConnectedLeAudioSupported + } + } ?: false + + if (matched) { + logger.logLaunchSettingsCriteriaMatched( + "NotSharingClickedConnected", + deviceItem + ) + } + + matched + } + } + + override suspend fun getClickUiEvent(deviceItem: DeviceItem) = + BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_NOT_SHARING_CONNECTED_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.isActiveOrConnectedLeAudioSupported: Boolean + get() = + (type == DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE || + type == DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE) && isLeAudioSupported + + val DeviceItem.isMediaDevice: Boolean + get() = + cachedBluetoothDevice.connectableProfiles.any { + it is A2dpProfile || + it is HearingAidProfile || + it is LeAudioProfile || + it is HeadsetProfile + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java index ea89be61d773..b705a0389300 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java +++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java @@ -21,7 +21,6 @@ import static com.android.systemui.qs.dagger.QSFlagsModule.RBC_AVAILABLE; import android.content.Context; import android.os.Handler; -import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogModule; import com.android.systemui.dagger.NightDisplayListenerModule; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; @@ -61,7 +60,6 @@ import javax.inject.Named; */ @Module(subcomponents = {QSFragmentComponent.class, QSSceneComponent.class}, includes = { - BluetoothTileDialogModule.class, MediaModule.class, PanelsModule.class, QSExternalModule.class, diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorImplTest.kt deleted file mode 100644 index 64bd742d3af1..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorImplTest.kt +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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.TestableLooper -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.settingslib.bluetooth.CachedBluetoothDevice -import com.android.systemui.SysuiTestCase -import com.android.systemui.kosmos.testDispatcher -import com.android.systemui.kosmos.testScope -import com.android.systemui.statusbar.phone.SystemUIDialog -import com.android.systemui.testKosmos -import com.android.systemui.util.mockito.whenever -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule - -@SmallTest -@RunWith(AndroidJUnit4::class) -@TestableLooper.RunWithLooper(setAsMainLooper = true) -@OptIn(ExperimentalCoroutinesApi::class) -class DeviceItemActionInteractorImplTest : SysuiTestCase() { - @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() - private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() } - private lateinit var actionInteractorImpl: DeviceItemActionInteractor - - @Mock private lateinit var dialog: SystemUIDialog - @Mock private lateinit var cachedDevice: CachedBluetoothDevice - @Mock private lateinit var device: BluetoothDevice - @Mock private lateinit var deviceItem: DeviceItem - - @Before - fun setUp() { - actionInteractorImpl = kosmos.deviceItemActionInteractor - whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedDevice) - whenever(cachedDevice.address).thenReturn("ADDRESS") - whenever(cachedDevice.device).thenReturn(device) - } - - @Test - fun testOnClick_connectedMedia_setActive() { - with(kosmos) { - testScope.runTest { - whenever(deviceItem.type) - .thenReturn(DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE) - actionInteractorImpl.onClick(deviceItem, dialog) - verify(cachedDevice).setActive() - verify(bluetoothTileDialogLogger) - .logDeviceClick( - cachedDevice.address, - DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE - ) - } - } - } - - @Test - fun testOnClick_activeMedia_disconnect() { - with(kosmos) { - testScope.runTest { - whenever(deviceItem.type).thenReturn(DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE) - actionInteractorImpl.onClick(deviceItem, dialog) - verify(cachedDevice).disconnect() - verify(bluetoothTileDialogLogger) - .logDeviceClick( - cachedDevice.address, - DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE - ) - } - } - } - - @Test - fun testOnClick_connectedOtherDevice_disconnect() { - with(kosmos) { - testScope.runTest { - whenever(deviceItem.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE) - actionInteractorImpl.onClick(deviceItem, dialog) - verify(cachedDevice).disconnect() - verify(bluetoothTileDialogLogger) - .logDeviceClick(cachedDevice.address, DeviceItemType.CONNECTED_BLUETOOTH_DEVICE) - } - } - } - - @Test - fun testOnClick_saved_connect() { - with(kosmos) { - testScope.runTest { - whenever(deviceItem.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE) - actionInteractorImpl.onClick(deviceItem, dialog) - verify(cachedDevice).connect() - verify(bluetoothTileDialogLogger) - .logDeviceClick(cachedDevice.address, DeviceItemType.SAVED_BLUETOOTH_DEVICE) - } - } - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt index e8e37bc81866..5ff46346b386 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt @@ -13,19 +13,28 @@ * 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.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 val Kosmos.bluetoothTileDialogLogger: BluetoothTileDialogLogger by Kosmos.Fixture { mock {} } +val Kosmos.localBluetoothManager: LocalBluetoothManager by Kosmos.Fixture { mock {} } + +val Kosmos.dialogTransitionAnimator: DialogTransitionAnimator by Kosmos.Fixture { mock {} } + val Kosmos.deviceItemActionInteractor: DeviceItemActionInteractor by Kosmos.Fixture { - DeviceItemActionInteractorImpl( + DeviceItemActionInteractor( + activityStarter, + dialogTransitionAnimator, + localBluetoothManager, testDispatcher, bluetoothTileDialogLogger, uiEventLogger, 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 new file mode 100644 index 000000000000..82465065c1e1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt @@ -0,0 +1,459 @@ +/* + * 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.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 +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@OptIn(ExperimentalCoroutinesApi::class) +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 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, + deviceName = DEVICE_NAME, + connectionSummary = DEVICE_CONNECTION_SUMMARY, + iconWithDescription = null, + background = null + ) + notConnectedDeviceItem = + DeviceItem( + type = DeviceItemType.SAVED_BLUETOOTH_DEVICE, + cachedBluetoothDevice = cachedBluetoothDevice, + deviceName = DEVICE_NAME, + connectionSummary = DEVICE_CONNECTION_SUMMARY, + iconWithDescription = null, + background = null + ) + connectedMediaDeviceItem = + DeviceItem( + type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, + cachedBluetoothDevice = cachedBluetoothDevice, + deviceName = DEVICE_NAME, + connectionSummary = DEVICE_CONNECTION_SUMMARY, + iconWithDescription = null, + background = null + ) + connectedOtherDeviceItem = + DeviceItem( + type = DeviceItemType.CONNECTED_BLUETOOTH_DEVICE, + cachedBluetoothDevice = cachedBluetoothDevice, + deviceName = DEVICE_NAME, + connectionSummary = DEVICE_CONNECTION_SUMMARY, + iconWithDescription = null, + background = null + ) + actionInteractorImpl = kosmos.deviceItemActionInteractor + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + } + + @Test + fun testOnClick_connectedMedia_setActive() { + 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 + ) + } + } + } + + @Test + fun testOnClick_activeMedia_disconnect() { + 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 + ) + } + } + } + + @Test + fun testOnClick_connectedOtherDevice_disconnect() { + 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 + ) + } + } + } + + @Test + fun testOnClick_saved_connect() { + 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_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.connectableProfiles) + .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.connectableProfiles) + .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_clickedConnectedLe_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(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" + const val DEVICE_ADDRESS = "address" + const val GROUP_ID_1 = 1 + const val GROUP_ID_2 = 2 + } +} -- cgit v1.2.3-59-g8ed1b