Move overlay click handling back to SystemUI.
Test: atest -c com.android.systemui.bluetooth.qsdialog
Bug: 340379827
Flag: NA
Change-Id: Ieddca3ec1f05d30aff94b50e9849b6a80e153666
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 1e79bb7..fde7c2c 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -368,6 +368,7 @@
"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 c30aea0..72312b8 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 @@
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<Int, List<BluetoothDevice>>) {
+ 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 2e9169e..0000000
--- 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 b592b8e..4a358c0 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 @@
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 9311760..4dafa93 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
- override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {
+ private val assistantProfile: LocalBluetoothLeBroadcastAssistant?
+ get() = localBluetoothManager?.profileManager?.leAudioBroadcastAssistantProfile
+
+ private val launchSettingsCriteriaList: List<LaunchSettingsCriteria>
+ get() =
+ listOf(
+ InSharingClickedNoSource(localBluetoothManager, backgroundDispatcher, logger),
+ NotSharingClickedNonConnect(
+ leAudioProfile,
+ assistantProfile,
+ backgroundDispatcher,
+ logger
+ ),
+ NotSharingClickedConnected(
+ leAudioProfile,
+ assistantProfile,
+ backgroundDispatcher,
+ logger
+ )
+ )
+
+ 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 @@
}
}
}
+
+ 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<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 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 ea89be6..b705a03 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 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 @@
*/
@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 64bd742..0000000
--- 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 e8e37bc..5ff4634 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 0000000..8246506
--- /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
+ }
+}