diff --git a/res/values/strings.xml b/res/values/strings.xml index 3ac0a600d94..0f2b201ff1f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -13942,6 +13942,12 @@ Can\'t share audio with %1$s Audio sharing only works with headphones that support LE Audio + + Turn off Audio Sharing + + To pair a new device, turn off Audio Sharing first. + + Turn off Connect to a LE audio stream diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java index e29096126f9..d5f5cb06854 100644 --- a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java +++ b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java @@ -48,6 +48,7 @@ import com.android.settings.widget.GearPreference; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.flags.Flags; import com.android.settingslib.utils.ThreadUtils; import java.lang.annotation.Retention; @@ -93,6 +94,7 @@ public final class BluetoothDevicePreference extends GearPreference { private final int mType; private AlertDialog mDisconnectDialog; + @Nullable private AlertDialog mBlockPairingDialog; private String contentDescription = null; private boolean mHideSecondTarget = false; private boolean mIsCallbackRemoved = true; @@ -409,13 +411,24 @@ public final class BluetoothDevicePreference extends GearPreference { SettingsEnums.ACTION_SETTINGS_BLUETOOTH_CONNECT); mCachedDevice.connect(); } else if (bondState == BluetoothDevice.BOND_NONE) { - metricsFeatureProvider.action(context, - SettingsEnums.ACTION_SETTINGS_BLUETOOTH_PAIR); - if (!mCachedDevice.hasHumanReadableName()) { + var unused = ThreadUtils.postOnBackgroundThread(() -> { + if (Flags.enableTemporaryBondDevicesUi() && Utils.shouldBlockPairingInAudioSharing( + mLocalBtManager)) { + // TODO: collect metric + context.getMainExecutor().execute(() -> + mBlockPairingDialog = + Utils.showBlockPairingDialog(context, mBlockPairingDialog, + mLocalBtManager)); + return; + } metricsFeatureProvider.action(context, - SettingsEnums.ACTION_SETTINGS_BLUETOOTH_PAIR_DEVICES_WITHOUT_NAMES); - } - pair(); + SettingsEnums.ACTION_SETTINGS_BLUETOOTH_PAIR); + if (!mCachedDevice.hasHumanReadableName()) { + metricsFeatureProvider.action(context, + SettingsEnums.ACTION_SETTINGS_BLUETOOTH_PAIR_DEVICES_WITHOUT_NAMES); + } + context.getMainExecutor().execute(() -> pair()); + }); } } diff --git a/src/com/android/settings/bluetooth/Utils.java b/src/com/android/settings/bluetooth/Utils.java index ea76fafc3a5..7c27386ac20 100644 --- a/src/com/android/settings/bluetooth/Utils.java +++ b/src/com/android/settings/bluetooth/Utils.java @@ -34,6 +34,7 @@ import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; @@ -44,6 +45,7 @@ import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.BluetoothUtils.ErrorListener; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothManager.BluetoothManagerCallback; @@ -324,4 +326,33 @@ public final class Utils { .map(d -> BluetoothUtils.getGroupId(deviceManager.findDevice(d))).collect( Collectors.toSet()).size() >= 2); } + + /** + * Show block pairing dialog during audio sharing + * @param context The dialog context + * @param dialog The dialog if already exists + * @param localBtManager {@link LocalBluetoothManager} + * @return The block pairing dialog + */ + @Nullable + static AlertDialog showBlockPairingDialog(@NonNull Context context, + @Nullable AlertDialog dialog, @Nullable LocalBluetoothManager localBtManager) { + if (!com.android.settingslib.flags.Flags.enableTemporaryBondDevicesUi()) return null; + if (dialog != null && dialog.isShowing()) return dialog; + if (dialog == null) { + AlertDialog.Builder builder = new AlertDialog.Builder(context) + .setNegativeButton(android.R.string.cancel, null) + .setTitle(R.string.audio_sharing_block_pairing_dialog_title) + .setMessage(R.string.audio_sharing_block_pairing_dialog_content); + LocalBluetoothLeBroadcast broadcast = localBtManager == null ? null : + localBtManager.getProfileManager().getLeAudioBroadcastProfile(); + if (broadcast != null) { + builder.setPositiveButton(R.string.audio_sharing_turn_off_button_label, + (dlg, which) -> broadcast.stopLatestBroadcast()); + } + dialog = builder.create(); + } + dialog.show(); + return dialog; + } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePreferenceTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePreferenceTest.java index ba90ccf63d0..6a72c7d2e0c 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePreferenceTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePreferenceTest.java @@ -28,11 +28,17 @@ import static org.mockito.Mockito.when; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.bluetooth.BluetoothStatusCodes; import android.content.Context; import android.graphics.drawable.Drawable; +import android.os.Looper; import android.os.UserManager; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.Pair; +import androidx.appcompat.app.AlertDialog; import androidx.test.core.app.ApplicationProvider; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -42,8 +48,13 @@ import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; import com.android.settings.testutils.shadow.ShadowBluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.flags.Flags; +import com.android.settingslib.testutils.shadow.ShadowBluetoothAdapter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -57,7 +68,9 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; +import org.robolectric.Shadows; import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; import org.robolectric.util.ReflectionHelpers; import java.util.ArrayList; @@ -81,6 +94,8 @@ public class BluetoothDevicePreferenceTest { @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Mock private CachedBluetoothDevice mCachedBluetoothDevice; @Mock @@ -107,6 +122,7 @@ public class BluetoothDevicePreferenceTest { private CachedBluetoothDeviceManager mDeviceManager; private Context mContext = ApplicationProvider.getApplicationContext(); + private ShadowBluetoothAdapter mShadowBluetoothAdapter; private FakeFeatureFactory mFakeFeatureFactory; private MetricsFeatureProvider mMetricsFeatureProvider; @@ -166,6 +182,7 @@ public class BluetoothDevicePreferenceTest { when(mCachedBluetoothDevice.hasHumanReadableName()).thenReturn(true); mPreference.onClicked(); + Shadows.shadowOf(Looper.getMainLooper()).idle(); verify(mMetricsFeatureProvider) .action(mContext, MetricsEvent.ACTION_SETTINGS_BLUETOOTH_PAIR); @@ -182,6 +199,7 @@ public class BluetoothDevicePreferenceTest { when(mCachedBluetoothDevice.hasHumanReadableName()).thenReturn(false); mPreference.onClicked(); + Shadows.shadowOf(Looper.getMainLooper()).idle(); verify(mMetricsFeatureProvider) .action(mContext, MetricsEvent.ACTION_SETTINGS_BLUETOOTH_PAIR); @@ -190,6 +208,58 @@ public class BluetoothDevicePreferenceTest { MetricsEvent.ACTION_SETTINGS_BLUETOOTH_PAIR_DEVICES_WITHOUT_NAMES); } + @Test + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void onClicked_deviceNotBonded_blockPairing() { + mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + mShadowBluetoothAdapter.setEnabled(true); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class); + LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class); + LocalBluetoothLeBroadcastAssistant assistant = mock( + LocalBluetoothLeBroadcastAssistant.class); + when(mLocalBluetoothManager.getProfileManager()).thenReturn(profileManager); + when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast); + when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); + when(broadcast.isEnabled(null)).thenReturn(true); + when(broadcast.getLatestBroadcastId()).thenReturn(1); + BluetoothDevice device1 = mock(BluetoothDevice.class); + BluetoothDevice device2 = mock(BluetoothDevice.class); + CachedBluetoothDevice cachedDevice1 = mock(CachedBluetoothDevice.class); + CachedBluetoothDevice cachedDevice2 = mock(CachedBluetoothDevice.class); + when(cachedDevice1.getDevice()).thenReturn(device1); + when(cachedDevice2.getDevice()).thenReturn(device2); + when(cachedDevice1.getGroupId()).thenReturn(1); + when(cachedDevice2.getGroupId()).thenReturn(2); + when(mDeviceManager.findDevice(device1)).thenReturn(cachedDevice1); + when(mDeviceManager.findDevice(device2)).thenReturn(cachedDevice2); + when(assistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(device1, device2)); + BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); + when(state.getBroadcastId()).thenReturn(1); + when(assistant.getAllSources(any())).thenReturn(ImmutableList.of(state)); + when(mCachedBluetoothDevice.isConnected()).thenReturn(false); + when(mCachedBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE); + when(mCachedBluetoothDevice.startPairing()).thenReturn(true); + when(mCachedBluetoothDevice.hasHumanReadableName()).thenReturn(true); + + mPreference.onClicked(); + Shadows.shadowOf(Looper.getMainLooper()).idle(); + + AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + assertThat(dialog).isNotNull(); + + ShadowAlertDialogCompat shadowAlertDialog = ShadowAlertDialogCompat.shadowOf(dialog); + assertThat(shadowAlertDialog.getTitle().toString()).isEqualTo( + mContext.getString(R.string.audio_sharing_block_pairing_dialog_title)); + + verify(mMetricsFeatureProvider, never()) + .action(mContext, MetricsEvent.ACTION_SETTINGS_BLUETOOTH_PAIR); + verify(mCachedBluetoothDevice, never()).startPairing(); + } + @Test public void getSecondTargetResource_shouldBeGearIconLayout() { assertThat(mPreference.getSecondTargetResId()).isEqualTo(R.layout.preference_widget_gear);