diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig index 6a1ee3a3c623ddb8ea35375586a3d0baa0f0a795..54c5a14702f6c590ee398bbad15433550814680c 100644 --- a/packages/SettingsLib/aconfig/settingslib.aconfig +++ b/packages/SettingsLib/aconfig/settingslib.aconfig @@ -43,4 +43,11 @@ flag { namespace: "pixel_cross_device_control" description: "Gates whether to enable LE audio private broadcast sharing via QR code" bug: "308368124" -} \ No newline at end of file +} + +flag { + name: "enable_hide_exclusively_managed_bluetooth_device" + namespace: "dck_framework" + description: "Hide exclusively managed Bluetooth devices in BT settings menu." + bug: "324475542" +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt index 1c9be0f105b25fa7f1749191d17d0557cc59e080..f13ecf3e91b91c426942a902da13e5e39adee9e1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt @@ -21,6 +21,7 @@ import android.content.Context import android.media.AudioManager import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.flags.Flags import com.android.systemui.res.R private val backgroundOn = R.drawable.settingslib_switch_bar_bg_on @@ -36,6 +37,7 @@ private val actionAccessibilityLabelDisconnect = /** Factories to create different types of Bluetooth device items from CachedBluetoothDevice. */ internal abstract class DeviceItemFactory { abstract fun isFilterMatched( + context: Context, cachedDevice: CachedBluetoothDevice, audioManager: AudioManager? ): Boolean @@ -45,6 +47,7 @@ internal abstract class DeviceItemFactory { internal class ActiveMediaDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( + context: Context, cachedDevice: CachedBluetoothDevice, audioManager: AudioManager? ): Boolean { @@ -71,6 +74,7 @@ internal class ActiveMediaDeviceItemFactory : DeviceItemFactory() { internal class AvailableMediaDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( + context: Context, cachedDevice: CachedBluetoothDevice, audioManager: AudioManager? ): Boolean { @@ -99,10 +103,18 @@ internal class AvailableMediaDeviceItemFactory : DeviceItemFactory() { internal class ConnectedDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( + context: Context, cachedDevice: CachedBluetoothDevice, audioManager: AudioManager? ): Boolean { - return BluetoothUtils.isConnectedBluetoothDevice(cachedDevice, audioManager) + return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) { + !BluetoothUtils.isExclusivelyManagedBluetoothDevice( + context, + cachedDevice.getDevice() + ) && BluetoothUtils.isConnectedBluetoothDevice(cachedDevice, audioManager) + } else { + BluetoothUtils.isConnectedBluetoothDevice(cachedDevice, audioManager) + } } override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem { @@ -125,10 +137,18 @@ internal class ConnectedDeviceItemFactory : DeviceItemFactory() { internal class SavedDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( + context: Context, cachedDevice: CachedBluetoothDevice, audioManager: AudioManager? ): Boolean { - return cachedDevice.bondState == BluetoothDevice.BOND_BONDED && !cachedDevice.isConnected + return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) { + !BluetoothUtils.isExclusivelyManagedBluetoothDevice( + context, + cachedDevice.getDevice() + ) && cachedDevice.bondState == BluetoothDevice.BOND_BONDED && !cachedDevice.isConnected + } else { + cachedDevice.bondState == BluetoothDevice.BOND_BONDED && !cachedDevice.isConnected + } } override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt index fcd45a6431bba2e246048d51a7931276ec0a0ec0..1df496b17aa5c4a3e8cddceb7c7b10983825051a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt @@ -133,7 +133,7 @@ constructor( bluetoothTileDialogRepository.cachedDevices .mapNotNull { cachedDevice -> deviceItemFactoryList - .firstOrNull { it.isFilterMatched(cachedDevice, audioManager) } + .firstOrNull { it.isFilterMatched(context, cachedDevice, audioManager) } ?.create(context, cachedDevice) } .sort(displayPriority, bluetoothAdapter?.mostRecentlyConnectedDevices) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactoryTest.kt index 92c73261a95e0afb22252d282a7dc59a49a9f2f9..a8cd8c801a95e5ec72bd2d4d19e274e82fefdf51 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactoryTest.kt @@ -16,10 +16,18 @@ package com.android.systemui.qs.tiles.dialog.bluetooth +import android.bluetooth.BluetoothDevice +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.media.AudioManager +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.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.flags.Flags import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -35,19 +43,26 @@ import org.mockito.junit.MockitoRule @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) class DeviceItemFactoryTest : SysuiTestCase() { - @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() @Mock private lateinit var cachedDevice: CachedBluetoothDevice + @Mock private lateinit var bluetoothDevice: BluetoothDevice + @Mock private lateinit var packageManager: PackageManager private val availableMediaDeviceItemFactory = AvailableMediaDeviceItemFactory() private val connectedDeviceItemFactory = ConnectedDeviceItemFactory() private val savedDeviceItemFactory = SavedDeviceItemFactory() + private val audioManager = context.getSystemService(AudioManager::class.java)!! + @Before fun setup() { `when`(cachedDevice.name).thenReturn(DEVICE_NAME) + `when`(cachedDevice.address).thenReturn(DEVICE_ADDRESS) + `when`(cachedDevice.device).thenReturn(bluetoothDevice) `when`(cachedDevice.connectionSummary).thenReturn(CONNECTION_SUMMARY) + + context.setMockPackageManager(packageManager) } @Test @@ -72,6 +87,225 @@ class DeviceItemFactoryTest : SysuiTestCase() { assertThat(deviceItem.background).isNotNull() } + @Test + @DisableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testSavedFactory_isFilterMatched_bondedAndNotConnected_returnsTrue() { + `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(cachedDevice.isConnected).thenReturn(false) + + assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isTrue() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testSavedFactory_isFilterMatched_connected_returnsFalse() { + `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(cachedDevice.isConnected).thenReturn(true) + + assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testSavedFactory_isFilterMatched_notBonded_returnsFalse() { + `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_NONE) + `when`(cachedDevice.isConnected).thenReturn(false) + + assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testSavedFactory_isFilterMatched_exclusivelyManaged_returnsFalse() { + val exclusiveManagerName = + BluetoothUtils.getExclusiveManagers().firstOrNull() ?: FAKE_EXCLUSIVE_MANAGER_NAME + `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)) + .thenReturn(exclusiveManagerName.toByteArray()) + `when`(packageManager.getPackageInfo(exclusiveManagerName, 0)).thenReturn(PackageInfo()) + `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(cachedDevice.isConnected).thenReturn(false) + + assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testSavedFactory_isFilterMatched_noExclusiveManager_returnsTrue() { + `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(cachedDevice.isConnected).thenReturn(false) + + assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testSavedFactory_isFilterMatched_notAllowedExclusiveManager_returnsTrue() { + `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)) + .thenReturn(FAKE_EXCLUSIVE_MANAGER_NAME.toByteArray()) + `when`(packageManager.getPackageInfo(FAKE_EXCLUSIVE_MANAGER_NAME, 0)) + .thenReturn(PackageInfo()) + `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(cachedDevice.isConnected).thenReturn(false) + + assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testSavedFactory_isFilterMatched_uninstalledExclusiveManager_returnsTrue() { + val exclusiveManagerName = + BluetoothUtils.getExclusiveManagers().firstOrNull() ?: FAKE_EXCLUSIVE_MANAGER_NAME + `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)) + .thenReturn(exclusiveManagerName.toByteArray()) + `when`(packageManager.getPackageInfo(exclusiveManagerName, 0)) + .thenThrow(PackageManager.NameNotFoundException("Test!")) + `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(cachedDevice.isConnected).thenReturn(false) + + assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testSavedFactory_isFilterMatched_notExclusivelyManaged_notBonded_returnsFalse() { + `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_NONE) + `when`(cachedDevice.isConnected).thenReturn(false) + + assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testSavedFactory_isFilterMatched_notExclusivelyManaged_connected_returnsFalse() { + `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(cachedDevice.isConnected).thenReturn(true) + + assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testConnectedFactory_isFilterMatched_bondedAndConnected_returnsTrue() { + `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(bluetoothDevice.isConnected).thenReturn(true) + audioManager.setMode(AudioManager.MODE_NORMAL) + + assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isTrue() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testConnectedFactory_isFilterMatched_notConnected_returnsFalse() { + `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(bluetoothDevice.isConnected).thenReturn(false) + audioManager.setMode(AudioManager.MODE_NORMAL) + + assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testConnectedFactory_isFilterMatched_notBonded_returnsFalse() { + `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_NONE) + `when`(bluetoothDevice.isConnected).thenReturn(true) + audioManager.setMode(AudioManager.MODE_NORMAL) + + assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testConnectedFactory_isFilterMatched_exclusivelyManaged_returnsFalse() { + val exclusiveManagerName = + BluetoothUtils.getExclusiveManagers().firstOrNull() ?: FAKE_EXCLUSIVE_MANAGER_NAME + `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)) + .thenReturn(exclusiveManagerName.toByteArray()) + `when`(packageManager.getPackageInfo(exclusiveManagerName, 0)).thenReturn(PackageInfo()) + `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(bluetoothDevice.isConnected).thenReturn(true) + audioManager.setMode(AudioManager.MODE_NORMAL) + + assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testConnectedFactory_isFilterMatched_noExclusiveManager_returnsTrue() { + `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(bluetoothDevice.isConnected).thenReturn(true) + audioManager.setMode(AudioManager.MODE_NORMAL) + + assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testConnectedFactory_isFilterMatched_notAllowedExclusiveManager_returnsTrue() { + `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)) + .thenReturn(FAKE_EXCLUSIVE_MANAGER_NAME.toByteArray()) + `when`(packageManager.getPackageInfo(FAKE_EXCLUSIVE_MANAGER_NAME, 0)) + .thenReturn(PackageInfo()) + `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(bluetoothDevice.isConnected).thenReturn(true) + audioManager.setMode(AudioManager.MODE_NORMAL) + + assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testConnectedFactory_isFilterMatched_uninstalledExclusiveManager_returnsTrue() { + val exclusiveManagerName = + BluetoothUtils.getExclusiveManagers().firstOrNull() ?: FAKE_EXCLUSIVE_MANAGER_NAME + `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)) + .thenReturn(exclusiveManagerName.toByteArray()) + `when`(packageManager.getPackageInfo(exclusiveManagerName, 0)) + .thenThrow(PackageManager.NameNotFoundException("Test!")) + `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(bluetoothDevice.isConnected).thenReturn(true) + audioManager.setMode(AudioManager.MODE_NORMAL) + + assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testConnectedFactory_isFilterMatched_notExclusivelyManaged_notBonded_returnsFalse() { + `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_NONE) + `when`(bluetoothDevice.isConnected).thenReturn(true) + audioManager.setMode(AudioManager.MODE_NORMAL) + + assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testConnectedFactory_isFilterMatched_notExclusivelyManaged_notConnected_returnsFalse() { + `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(bluetoothDevice.isConnected).thenReturn(false) + audioManager.setMode(AudioManager.MODE_NORMAL) + + assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + private fun assertDeviceItem(deviceItem: DeviceItem?, deviceItemType: DeviceItemType) { assertThat(deviceItem).isNotNull() assertThat(deviceItem!!.type).isEqualTo(deviceItemType) @@ -83,5 +317,7 @@ class DeviceItemFactoryTest : SysuiTestCase() { companion object { const val DEVICE_NAME = "DeviceName" const val CONNECTION_SUMMARY = "ConnectionSummary" + private const val FAKE_EXCLUSIVE_MANAGER_NAME = "com.fake.name" + private const val DEVICE_ADDRESS = "04:52:C7:0B:D8:3C" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt index e236f4a7730fe9aeb35e6f09425378091a70c30c..ddf0b9a781659e875cb04779eab3b19072fb88ae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt @@ -279,6 +279,7 @@ class DeviceItemInteractorTest : SysuiTestCase() { ): DeviceItemFactory { return object : DeviceItemFactory() { override fun isFilterMatched( + context: Context, cachedDevice: CachedBluetoothDevice, audioManager: AudioManager? ) = isFilterMatchFunc(cachedDevice)