diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java b/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java index d71328eed3e..86f090e8be5 100644 --- a/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java +++ b/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java @@ -18,32 +18,94 @@ package com.android.settings.bluetooth; import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_PAIR_AND_JOIN_SHARING; + +import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.content.Intent; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; import android.util.Log; +import android.view.LayoutInflater; import android.view.View; +import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.accessibility.AccessibilityStatsLogUtils; +import com.android.settings.connecteddevice.audiosharing.AudioSharingIncompatibleDialogFragment; import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.HearingAidStatsLogUtils; +import com.android.settingslib.utils.ThreadUtils; + +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; /** * Abstract class for providing basic interaction for a list of Bluetooth devices in bluetooth * device pairing detail page. */ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPreferenceFragment { + private static final long AUTO_DISMISS_TIME_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(10); + private static final int AUTO_DISMISS_MESSAGE_ID = 1001; protected boolean mInitialScanStarted; @VisibleForTesting protected BluetoothProgressCategory mAvailableDevicesCategory; + @Nullable + private volatile BluetoothDevice mJustBonded = null; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + @Nullable + private AlertDialog mLoadingDialog = null; + @VisibleForTesting + boolean mShouldTriggerAudioSharingShareThenPairFlow = false; + private CopyOnWriteArrayList mDevicesWithMetadataChangedListener = + new CopyOnWriteArrayList<>(); + + // BluetoothDevicePreference updates the summary based on several callbacks, including + // BluetoothAdapter.OnMetadataChangedListener and BluetoothCallback. In most cases, + // metadata changes callback will be triggered before onDeviceBondStateChanged(BOND_BONDED). + // And before we hear onDeviceBondStateChanged(BOND_BONDED), the BluetoothDevice.getState() has + // already been BOND_BONDED. These event sequence will lead to: before we hear + // onDeviceBondStateChanged(BOND_BONDED), BluetoothDevicePreference's summary has already + // change from "Pairing..." to empty since it listens to metadata changes happens earlier. + // + // In share then pair flow, we have to wait on this page till the device is connected. + // The BluetoothDevicePreference summary will be blank for seconds between "Pairing..." and + // "Connecting..." To help users better understand the process, we listen to metadata change + // as well and show a loading dialog with "Connecting to ...." once BluetoothDevice.getState() + // gets to BOND_BONDED. + final BluetoothAdapter.OnMetadataChangedListener mMetadataListener = + new BluetoothAdapter.OnMetadataChangedListener() { + @Override + public void onMetadataChanged(@NonNull BluetoothDevice device, int key, + @Nullable byte[] value) { + Log.d(getLogTag(), "onMetadataChanged device = " + device + ", key = " + key); + if (mShouldTriggerAudioSharingShareThenPairFlow && mLoadingDialog == null + && device.getBondState() == BluetoothDevice.BOND_BONDED + && mSelectedList.contains(device)) { + triggerAudioSharingShareThenPairFlow(device); + // Once device is bonded, remove the listener + removeOnMetadataChangedListener(device); + } + } + }; public BluetoothDevicePairingDetailBase() { super(DISALLOW_CONFIG_BLUETOOTH); @@ -68,6 +130,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere return; } updateBluetooth(); + mShouldTriggerAudioSharingShareThenPairFlow = shouldTriggerAudioSharingShareThenPairFlow(); } @Override @@ -80,6 +143,26 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere disableScanning(); } + @Override + public void onDestroy() { + super.onDestroy(); + var unused = ThreadUtils.postOnBackgroundThread(() -> { + mDevicesWithMetadataChangedListener.forEach( + device -> { + try { + if (mBluetoothAdapter != null) { + mBluetoothAdapter.removeOnMetadataChangedListener(device, + mMetadataListener); + mDevicesWithMetadataChangedListener.remove(device); + } + } catch (IllegalArgumentException e) { + Log.d(getLogTag(), "Fail to remove listener: " + e); + } + }); + mDevicesWithMetadataChangedListener.clear(); + }); + } + @Override public void onBluetoothStateChanged(int bluetoothState) { super.onBluetoothStateChanged(bluetoothState); @@ -92,16 +175,37 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere @Override public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { if (bondState == BluetoothDevice.BOND_BONDED) { + if (cachedDevice != null && mShouldTriggerAudioSharingShareThenPairFlow) { + BluetoothDevice device = cachedDevice.getDevice(); + if (device != null && mSelectedList.contains(device)) { + triggerAudioSharingShareThenPairFlow(device); + removeOnMetadataChangedListener(device); + return; + } + } // If one device is connected(bonded), then close this fragment. finish(); return; } else if (bondState == BluetoothDevice.BOND_BONDING) { + if (mShouldTriggerAudioSharingShareThenPairFlow && cachedDevice != null) { + BluetoothDevice device = cachedDevice.getDevice(); + if (device != null && mSelectedList.contains(device)) { + addOnMetadataChangedListener(device); + } + } // Set the bond entry where binding process starts for logging hearing aid device info final int pageId = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider() .getAttribution(getActivity()); final int bondEntry = AccessibilityStatsLogUtils.convertToHearingAidInfoBondEntry( pageId); HearingAidStatsLogUtils.setBondEntryForDevice(bondEntry, cachedDevice); + } else if (bondState == BluetoothDevice.BOND_NONE) { + if (mShouldTriggerAudioSharingShareThenPairFlow && cachedDevice != null) { + BluetoothDevice device = cachedDevice.getDevice(); + if (device != null && mSelectedList.contains(device)) { + removeOnMetadataChangedListener(device); + } + } } if (mSelectedDevice != null && cachedDevice != null) { BluetoothDevice device = cachedDevice.getDevice(); @@ -114,7 +218,8 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere } @Override - public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, + public void onProfileConnectionStateChanged( + @NonNull CachedBluetoothDevice cachedDevice, @ConnectionState int state, int bluetoothProfile) { // This callback is used to handle the case that bonded device is connected in pairing list. // 1. If user selected multiple bonded devices in pairing list, after connected @@ -123,8 +228,22 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere // removed from paring list. if (cachedDevice != null && cachedDevice.isConnected()) { final BluetoothDevice device = cachedDevice.getDevice(); - if (device != null && mSelectedList.contains(device)) { - finish(); + if (device != null + && mSelectedList.contains(device)) { + if (!BluetoothUtils.isAudioSharingEnabled()) { + finish(); + return; + } + if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT + && state == BluetoothAdapter.STATE_CONNECTED + && device.equals(mJustBonded) + && mShouldTriggerAudioSharingShareThenPairFlow) { + Log.d(getLogTag(), + "onProfileConnectionStateChanged, assistant profile connected"); + dismissConnectingDialog(); + mHandler.removeMessages(AUTO_DISMISS_MESSAGE_ID); + finishFragmentWithResultForAudioSharing(device); + } } else { onDeviceDeleted(cachedDevice); } @@ -148,6 +267,8 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere public void onDevicePreferenceClick(BluetoothDevicePreference btPreference) { disableScanning(); super.onDevicePreferenceClick(btPreference); + // Clean up the previous bond value + mJustBonded = null; } @VisibleForTesting @@ -165,8 +286,8 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere * {@code bluetoothState} is off. * * @param bluetoothState the current Bluetooth state, the possible values that will handle here: - * {@link android.bluetooth.BluetoothAdapter#STATE_OFF}, - * {@link android.bluetooth.BluetoothAdapter#STATE_ON}, + * {@link android.bluetooth.BluetoothAdapter#STATE_OFF}, + * {@link android.bluetooth.BluetoothAdapter#STATE_ON}, */ @VisibleForTesting public void updateContent(int bluetoothState) { @@ -187,4 +308,122 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere Toast.makeText(getContext(), R.string.connected_device_bluetooth_turned_on_toast, Toast.LENGTH_SHORT).show(); } + + @VisibleForTesting + boolean shouldTriggerAudioSharingShareThenPairFlow() { + if (!BluetoothUtils.isAudioSharingEnabled()) return false; + Activity activity = getActivity(); + Intent intent = activity == null ? null : activity.getIntent(); + Bundle args = + intent == null ? null : + intent.getBundleExtra( + SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS); + return args != null + && args.getBoolean(EXTRA_PAIR_AND_JOIN_SHARING, false); + } + + private void addOnMetadataChangedListener(@Nullable BluetoothDevice device) { + var unused = ThreadUtils.postOnBackgroundThread(() -> { + if (mBluetoothAdapter != null && device != null + && !mDevicesWithMetadataChangedListener.contains(device)) { + mBluetoothAdapter.addOnMetadataChangedListener(device, mExecutor, + mMetadataListener); + mDevicesWithMetadataChangedListener.add(device); + } + }); + } + + private void removeOnMetadataChangedListener(@Nullable BluetoothDevice device) { + var unused = ThreadUtils.postOnBackgroundThread(() -> { + if (mBluetoothAdapter != null && device != null + && mDevicesWithMetadataChangedListener.contains(device)) { + try { + mBluetoothAdapter.removeOnMetadataChangedListener(device, mMetadataListener); + mDevicesWithMetadataChangedListener.remove(device); + } catch (IllegalArgumentException e) { + Log.d(getLogTag(), "Fail to remove listener: " + e); + } + } + }); + } + + private void triggerAudioSharingShareThenPairFlow( + @NonNull BluetoothDevice device) { + var unused = ThreadUtils.postOnBackgroundThread(() -> { + if (mJustBonded != null) { + Log.d(getLogTag(), "Skip triggerAudioSharingShareThenPairFlow, already done"); + return; + } + mJustBonded = device; + // Show connecting device loading state + String aliasName = device.getAlias(); + String deviceName = TextUtils.isEmpty(aliasName) ? device.getAddress() + : aliasName; + showConnectingDialog("Connecting to " + deviceName + "..."); + // Wait for AUTO_DISMISS_TIME_THRESHOLD_MS and check if the paired device supports audio + // sharing. + if (!mHandler.hasMessages(AUTO_DISMISS_MESSAGE_ID)) { + mHandler.postDelayed(() -> + postOnMainThread( + () -> { + Log.d(getLogTag(), "Show incompatible dialog when timeout"); + dismissConnectingDialog(); + AudioSharingIncompatibleDialogFragment.show(this, deviceName, + () -> finish()); + }), AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); + } + }); + } + + private void finishFragmentWithResultForAudioSharing(@Nullable BluetoothDevice device) { + Intent resultIntent = new Intent(); + resultIntent.putExtra(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, device); + if (getActivity() != null) { + getActivity().setResult(Activity.RESULT_OK, resultIntent); + } + finish(); + } + + // TODO: use DialogFragment + private void showConnectingDialog(@NonNull String message) { + postOnMainThread(() -> { + if (mLoadingDialog != null) { + Log.d(getLogTag(), "showConnectingDialog, is already showing"); + TextView textView = mLoadingDialog.findViewById(R.id.message); + if (textView != null && !message.equals(textView.getText().toString())) { + Log.d(getLogTag(), "showConnectingDialog, update message"); + // TODO: use string res once finalized + textView.setText(message); + } + return; + } + Log.d(getLogTag(), "showConnectingDialog, show dialog"); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + LayoutInflater inflater = LayoutInflater.from(builder.getContext()); + View customView = inflater.inflate( + R.layout.dialog_audio_sharing_loading_state, /* root= */ + null); + TextView textView = customView.findViewById(R.id.message); + if (textView != null) { + // TODO: use string res once finalized + textView.setText(message); + } + AlertDialog dialog = builder.setView(customView).setCancelable(false).create(); + dialog.setCanceledOnTouchOutside(false); + mLoadingDialog = dialog; + dialog.show(); + }); + } + + private void dismissConnectingDialog() { + postOnMainThread(() -> { + if (mLoadingDialog != null) { + mLoadingDialog.dismiss(); + } + }); + } + + private void postOnMainThread(@NonNull Runnable runnable) { + getContext().getMainExecutor().execute(runnable); + } } diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java index ad41e8a2997..786e1dccc05 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java @@ -16,10 +16,18 @@ package com.android.settings.connecteddevice.audiosharing; -import android.app.settings.SettingsEnums; -import android.content.Context; -import android.os.Bundle; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE; + +import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.settings.R; @@ -27,16 +35,21 @@ import com.android.settings.SettingsActivity; import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsCategoryController; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.widget.SettingsMainSwitchBar; +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.utils.ThreadUtils; public class AudioSharingDashboardFragment extends DashboardFragment implements AudioSharingSwitchBarController.OnAudioSharingStateChangedListener { private static final String TAG = "AudioSharingDashboardFrag"; + public static final int SHARE_THEN_PAIR_REQUEST_CODE = 1002; + SettingsMainSwitchBar mMainSwitchBar; private AudioSharingDeviceVolumeGroupController mAudioSharingDeviceVolumeGroupController; private AudioSharingCallAudioPreferenceController mAudioSharingCallAudioPreferenceController; private AudioSharingPlaySoundPreferenceController mAudioSharingPlaySoundPreferenceController; private AudioStreamsCategoryController mAudioStreamsCategoryController; + private AudioSharingSwitchBarController mAudioSharingSwitchBarController; public AudioSharingDashboardFragment() { super(); @@ -84,13 +97,38 @@ public class AudioSharingDashboardFragment extends DashboardFragment final SettingsActivity activity = (SettingsActivity) getActivity(); mMainSwitchBar = activity.getSwitchBar(); mMainSwitchBar.setTitle(getText(R.string.audio_sharing_switch_title)); - AudioSharingSwitchBarController switchBarController = + mAudioSharingSwitchBarController = new AudioSharingSwitchBarController(activity, mMainSwitchBar, this); - switchBarController.init(this); - getSettingsLifecycle().addObserver(switchBarController); + mAudioSharingSwitchBarController.init(this); + getSettingsLifecycle().addObserver(mAudioSharingSwitchBarController); mMainSwitchBar.show(); } + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (!BluetoothUtils.isAudioSharingEnabled()) return; + // In share then pair flow, after users be routed to pair new device page and successfully + // pair and connect an LEA headset, the pair fragment will be finished with RESULT_OK + // and EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, pass the BT device to switch bar controller, + // which is responsible for adding source to the device with loading indicator. + if (requestCode == SHARE_THEN_PAIR_REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK) { + BluetoothDevice btDevice = + data != null + ? data.getParcelableExtra(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, + BluetoothDevice.class) + : null; + Log.d(TAG, "onActivityResult: RESULT_OK with device = " + btDevice); + if (btDevice != null) { + var unused = ThreadUtils.postOnBackgroundThread( + () -> mAudioSharingSwitchBarController.handleAutoAddSourceAfterPair( + btDevice)); + } + } + } + } + @Override public void onAudioSharingStateChanged() { updateVisibilityForAttachedPreferences(); @@ -107,11 +145,13 @@ public class AudioSharingDashboardFragment extends DashboardFragment AudioSharingDeviceVolumeGroupController volumeGroupController, AudioSharingCallAudioPreferenceController callAudioController, AudioSharingPlaySoundPreferenceController playSoundController, - AudioStreamsCategoryController streamsCategoryController) { + AudioStreamsCategoryController streamsCategoryController, + AudioSharingSwitchBarController switchBarController) { mAudioSharingDeviceVolumeGroupController = volumeGroupController; mAudioSharingCallAudioPreferenceController = callAudioController; mAudioSharingPlaySoundPreferenceController = playSoundController; mAudioStreamsCategoryController = streamsCategoryController; + mAudioSharingSwitchBarController = switchBarController; } private void updateVisibilityForAttachedPreferences() { diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java index 1ae541ca01d..1b68eaccbfe 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java @@ -16,6 +16,9 @@ package com.android.settings.connecteddevice.audiosharing; +import static com.android.settings.connecteddevice.audiosharing.AudioSharingDashboardFragment.SHARE_THEN_PAIR_REQUEST_CODE; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_PAIR_AND_JOIN_SHARING; + import android.app.Dialog; import android.app.settings.SettingsEnums; import android.os.Bundle; @@ -48,19 +51,23 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { // The host creates an instance of this dialog fragment must implement this interface to receive // event callbacks. public interface DialogEventListener { + /** Called when users click the positive button in the dialog. */ + default void onPositiveClick() {} + /** * Called when users click the device item for sharing in the dialog. * * @param item The device item clicked. */ - void onItemClick(AudioSharingDeviceItem item); + default void onItemClick(@NonNull AudioSharingDeviceItem item) {} /** Called when users click the cancel button in the dialog. */ - void onCancelClick(); + default void onCancelClick() {} } @Nullable private static DialogEventListener sListener; private static Pair[] sEventData = new Pair[0]; + @Nullable private static Fragment sHost; @Override public int getMetricsCategory() { @@ -70,10 +77,10 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { /** * Display the {@link AudioSharingDialogFragment} dialog. * - * @param host The Fragment this dialog will be hosted. + * @param host The Fragment this dialog will be hosted. * @param deviceItems The connected device items eligible for audio sharing. - * @param listener The callback to handle the user action on this dialog. - * @param eventData The eventData to log with for dialog onClick events. + * @param listener The callback to handle the user action on this dialog. + * @param eventData The eventData to log with for dialog onClick events. */ public static void show( @NonNull Fragment host, @@ -88,6 +95,7 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { Log.d(TAG, "Fail to show dialog: " + e.getMessage()); return; } + sHost = host; sListener = listener; sEventData = eventData; AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG); @@ -136,23 +144,33 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { .setCustomPositiveButton( R.string.audio_sharing_pair_button_label, v -> { - dismiss(); - new SubSettingLauncher(getContext()) - .setDestination(BluetoothPairingDetail.class.getName()) - .setSourceMetricsCategory(getMetricsCategory()) - .launch(); + if (sListener != null) { + sListener.onPositiveClick(); + } logDialogPositiveBtnClick(); + dismiss(); + Bundle args = new Bundle(); + args.putBoolean(EXTRA_PAIR_AND_JOIN_SHARING, true); + SubSettingLauncher launcher = + new SubSettingLauncher(getContext()) + .setDestination( + BluetoothPairingDetail.class.getName()) + .setSourceMetricsCategory(getMetricsCategory()) + .setArguments(args); + if (sHost != null) { + launcher.setResultListener(sHost, SHARE_THEN_PAIR_REQUEST_CODE); + } + launcher.launch(); }) .setCustomNegativeButton( R.string.audio_sharing_qrcode_button_label, v -> { - dismiss(); + onCancelClick(); new SubSettingLauncher(getContext()) .setTitleRes(R.string.audio_streams_qr_code_page_title) .setDestination(AudioStreamsQrCodeFragment.class.getName()) .setSourceMetricsCategory(getMetricsCategory()) .launch(); - logDialogNegativeBtnClick(); }); } else if (deviceItems.size() == 1) { AudioSharingDeviceItem deviceItem = Iterables.getOnlyElement(deviceItems); @@ -166,8 +184,8 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { v -> { if (sListener != null) { sListener.onItemClick(deviceItem); - logDialogPositiveBtnClick(); } + logDialogPositiveBtnClick(); dismiss(); }) .setCustomNegativeButton( @@ -182,8 +200,8 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { (AudioSharingDeviceItem item) -> { if (sListener != null) { sListener.onItemClick(item); - logDialogPositiveBtnClick(); } + logDialogPositiveBtnClick(); dismiss(); }, AudioSharingDeviceAdapter.ActionType.SHARE)) @@ -196,8 +214,8 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { private void onCancelClick() { if (sListener != null) { sListener.onCancelClick(); - logDialogNegativeBtnClick(); } + logDialogNegativeBtnClick(); dismiss(); } diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java index 5de615e968f..aceeb94420e 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java @@ -29,7 +29,6 @@ import androidx.fragment.app.FragmentManager; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settingslib.bluetooth.BluetoothUtils; -import com.android.settingslib.bluetooth.CachedBluetoothDevice; public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFragment { private static final String TAG = "AudioSharingIncompatDlg"; @@ -59,7 +58,7 @@ public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFr * * @param host The Fragment this dialog will be hosted. */ - public static void show(@Nullable Fragment host, @NonNull CachedBluetoothDevice cachedDevice, + public static void show(@Nullable Fragment host, @NonNull String deviceName, @NonNull DialogEventListener listener) { if (host == null || !BluetoothUtils.isAudioSharingEnabled()) return; final FragmentManager manager; @@ -77,7 +76,7 @@ public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFr } Log.d(TAG, "Show up the incompatible device dialog."); final Bundle bundle = new Bundle(); - bundle.putString(BUNDLE_KEY_DEVICE_NAME, cachedDevice.getName()); + bundle.putString(BUNDLE_KEY_DEVICE_NAME, deviceName); AudioSharingIncompatibleDialogFragment dialogFrag = new AudioSharingIncompatibleDialogFragment(); dialogFrag.setArguments(bundle); diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingLoadingStateDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingLoadingStateDialogFragment.java index 79cc56ea6dd..8706590c362 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingLoadingStateDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingLoadingStateDialogFragment.java @@ -115,10 +115,6 @@ public class AudioSharingLoadingStateDialogFragment extends InstrumentedDialogFr @NonNull public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mHandler = new Handler(Looper.getMainLooper()); - mHandler.postDelayed(() -> { - Log.d(TAG, "Auto dismiss dialog after timeout"); - dismiss(); - }, AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); Bundle args = requireArguments(); String message = args.getString(BUNDLE_KEY_MESSAGE, ""); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); @@ -132,6 +128,26 @@ public class AudioSharingLoadingStateDialogFragment extends InstrumentedDialogFr return dialog; } + @Override + public void onStart() { + super.onStart(); + if (mHandler != null) { + Log.d(TAG, "onStart, postTimeOut for auto dismiss"); + mHandler.postDelayed(() -> { + Log.d(TAG, "Try to auto dismiss dialog after timeout"); + try { + Dialog dialog = getDialog(); + if (dialog != null) { + Log.d(TAG, "Dialog is not null, dismiss"); + dismissAllowingStateLoss(); + } + } catch (IllegalStateException e) { + Log.d(TAG, "Fail to dismiss: " + e.getMessage()); + } + }, AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); + } + } + @Override public void onDismiss(@NonNull DialogInterface dialog) { super.onDismiss(dialog); diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java index c0f463d863f..395647ca84a 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java @@ -56,6 +56,7 @@ import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.BluetoothEventManager; import com.android.settingslib.bluetooth.BluetoothUtils; 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; @@ -78,9 +79,9 @@ import java.util.concurrent.atomic.AtomicInteger; public class AudioSharingSwitchBarController extends BasePreferenceController implements DefaultLifecycleObserver, - OnCheckedChangeListener, - LocalBluetoothProfileManager.ServiceListener, - BluetoothCallback { + OnCheckedChangeListener, + LocalBluetoothProfileManager.ServiceListener, + BluetoothCallback { private static final String TAG = "AudioSharingSwitchCtlr"; private static final String PREF_KEY = "audio_sharing_main_switch"; @@ -464,6 +465,18 @@ public class AudioSharingSwitchBarController extends BasePreferenceController this.mFragment = fragment; } + /** Handle auto add source to the just paired device in share then pair flow. */ + public void handleAutoAddSourceAfterPair(@NonNull BluetoothDevice device) { + CachedBluetoothDeviceManager deviceManager = + mBtManager == null ? null : mBtManager.getCachedDeviceManager(); + CachedBluetoothDevice cachedDevice = + deviceManager == null ? null : deviceManager.findDevice(device); + if (cachedDevice != null) { + Log.d(TAG, "handleAutoAddSourceAfterPair, device = " + device.getAnonymizedAddress()); + addSourceToTargetSinks(ImmutableList.of(device), cachedDevice.getName()); + } + } + /** Test only: set callback registration status in tests. */ @VisibleForTesting void setCallbacksRegistered(boolean registered) { @@ -610,8 +623,8 @@ public class AudioSharingSwitchBarController extends BasePreferenceController mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_AUTO_JOIN_AUDIO_SHARING); mTargetActiveItem = null; if (mIntentHandleStage.compareAndSet( - StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(), - StartIntentHandleStage.HANDLED.ordinal()) + StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(), + StartIntentHandleStage.HANDLED.ordinal()) && mDeviceItemsForSharing.size() == 1) { Log.d(TAG, "handleOnBroadcastReady: auto add source to the second device"); AudioSharingDeviceItem target = mDeviceItemsForSharing.get(0); @@ -638,6 +651,13 @@ public class AudioSharingSwitchBarController extends BasePreferenceController private void showDialog(Pair[] eventData) { AudioSharingDialogFragment.DialogEventListener listener = new AudioSharingDialogFragment.DialogEventListener() { + @Override + public void onPositiveClick() { + // Could go to other pages, dismiss the loading dialog. + dismissLoadingStateDialogIfNeeded(); + cleanUp(); + } + @Override public void onItemClick(@NonNull AudioSharingDeviceItem item) { List targetSinks = mGroupedConnectedDevices.getOrDefault( @@ -648,6 +668,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController @Override public void onCancelClick() { + // Could go to other pages, dismiss the loading dialog. dismissLoadingStateDialogIfNeeded(); cleanUp(); } @@ -669,8 +690,8 @@ public class AudioSharingSwitchBarController extends BasePreferenceController @NonNull ViewGroup host, @NonNull View view, @NonNull AccessibilityEvent event) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED && (event.getContentChangeTypes() - & AccessibilityEvent.CONTENT_CHANGE_TYPE_ENABLED) - != 0) { + & AccessibilityEvent.CONTENT_CHANGE_TYPE_ENABLED) + != 0) { Log.d(TAG, "Skip accessibility event for CONTENT_CHANGE_TYPE_ENABLED"); return false; } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java index 40f7895d8dc..e326c1e2630 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java @@ -16,52 +16,80 @@ package com.android.settings.bluetooth; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_PAIR_AND_JOIN_SHARING; + import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; +import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothStatusCodes; import android.content.Context; +import android.content.Intent; import android.content.res.Resources; import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Looper; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.Pair; +import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; import androidx.test.core.app.ApplicationProvider; +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.flags.Flags; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; +import java.util.concurrent.Executor; + /** Tests for {@link BluetoothDevicePairingDetailBase}. */ @RunWith(RobolectricTestRunner.class) @Config(shadows = { ShadowBluetoothAdapter.class, + ShadowAlertDialogCompat.class, com.android.settings.testutils.shadow.ShadowFragment.class, }) public class BluetoothDevicePairingDetailBaseTest { @Rule - public final MockitoRule mockito = MockitoJUnit.rule(); + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); public static final String KEY_DEVICE_LIST_GROUP = "test_key"; @@ -86,8 +114,12 @@ public class BluetoothDevicePairingDetailBaseTest { @Before public void setUp() { mAvailableDevicesCategory = spy(new BluetoothProgressCategory(mContext)); - mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mBluetoothAdapter = spy(BluetoothAdapter.getDefaultAdapter()); mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); when(mCachedBluetoothDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS); final Pair pairs = new Pair<>(mDrawable, "fake_device"); when(mCachedBluetoothDevice.getDrawableWithDescription()).thenReturn(pairs); @@ -155,8 +187,88 @@ public class BluetoothDevicePairingDetailBaseTest { verify(mFragment).showBluetoothTurnedOnToast(); } + @Test + public void onDeviceBondStateChanged_bonded_pairAndJoinSharingDisabled_finish() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithPairAndJoinSharingIntent(false); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); + + verify(mFragment).finish(); + } + + @Test + public void onDeviceBondStateChanged_bonded_pairAndJoinSharingEnabled_handle() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithPairAndJoinSharingIntent(true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); + shadowOf(Looper.getMainLooper()).idle(); + + AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + assertThat(dialog).isNotNull(); + TextView message = dialog.findViewById(R.id.message); + assertThat(message).isNotNull(); + // TODO: use stringr res once finalized + assertThat(message.getText().toString()).isEqualTo( + "Connecting to " + TEST_DEVICE_ADDRESS + "..."); + verify(mFragment, never()).finish(); + } + + @Test + public void onDeviceBondStateChanged_bonding_pairAndJoinSharingDisabled_doNothing() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithPairAndJoinSharingIntent(false); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); + + verify(mBluetoothAdapter, never()).addOnMetadataChangedListener(any(BluetoothDevice.class), + any(Executor.class), any(BluetoothAdapter.OnMetadataChangedListener.class)); + } + + @Test + public void onDeviceBondStateChanged_bonding_pairAndJoinSharingEnabled_addListener() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithPairAndJoinSharingIntent(true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); + + verify(mBluetoothAdapter).addOnMetadataChangedListener(eq(mBluetoothDevice), + any(Executor.class), + any(BluetoothAdapter.OnMetadataChangedListener.class)); + } + + @Test + public void onDeviceBondStateChanged_unbonded_pairAndJoinSharingDisabled_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_NONE); + + verify(mBluetoothAdapter, never()).removeOnMetadataChangedListener( + any(BluetoothDevice.class), any(BluetoothAdapter.OnMetadataChangedListener.class)); + } + + @Test + public void onDeviceBondStateChanged_unbonded_pairAndJoinSharingEnabled_removeListener() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithPairAndJoinSharingIntent(true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_NONE); + + verify(mBluetoothAdapter).removeOnMetadataChangedListener(eq(mBluetoothDevice), + any(BluetoothAdapter.OnMetadataChangedListener.class)); + } + @Test public void onProfileConnectionStateChanged_deviceInSelectedListAndConnected_finish() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_B); mFragment.mSelectedList.add(mBluetoothDevice); mFragment.mSelectedList.add(device); @@ -165,13 +277,43 @@ public class BluetoothDevicePairingDetailBaseTest { when(mCachedBluetoothDevice.getDevice()).thenReturn(device); mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, - BluetoothProfile.A2DP, BluetoothAdapter.STATE_CONNECTED); + BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.A2DP); verify(mFragment).finish(); } + @Test + public void + onProfileConnectionStateChanged_deviceInSelectedListAndConnected_pairAndJoinSharing() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithPairAndJoinSharingIntent(true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); + shadowOf(Looper.getMainLooper()).idle(); + + when(mCachedBluetoothDevice.isConnected()).thenReturn(true); + + mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, + BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + shadowOf(Looper.getMainLooper()).idle(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + verify(mFragment.getActivity()).setResult(eq(Activity.RESULT_OK), captor.capture()); + Intent intent = captor.getValue(); + BluetoothDevice btDevice = + intent != null + ? intent.getParcelableExtra(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, + BluetoothDevice.class) + : null; + assertThat(btDevice).isNotNull(); + assertThat(btDevice).isEqualTo(mBluetoothDevice); + verify(mFragment).finish(); + } + @Test public void onProfileConnectionStateChanged_deviceNotInSelectedList_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_B); mFragment.mSelectedList.add(device); @@ -179,13 +321,14 @@ public class BluetoothDevicePairingDetailBaseTest { when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, - BluetoothProfile.A2DP, BluetoothAdapter.STATE_CONNECTED); + BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.A2DP); // not crash } @Test public void onProfileConnectionStateChanged_deviceDisconnected_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_B); mFragment.mSelectedList.add(mBluetoothDevice); mFragment.mSelectedList.add(device); @@ -194,13 +337,14 @@ public class BluetoothDevicePairingDetailBaseTest { when(mCachedBluetoothDevice.getDevice()).thenReturn(device); mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, - BluetoothProfile.A2DP, BluetoothAdapter.STATE_DISCONNECTED); + BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.A2DP); // not crash } @Test public void onProfileConnectionStateChanged_deviceInPreferenceMapAndConnected_removed() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final BluetoothDevicePreference preference = new BluetoothDevicePreference(mContext, mCachedBluetoothDevice, true, BluetoothDevicePreference.SortType.TYPE_FIFO); @@ -211,13 +355,14 @@ public class BluetoothDevicePairingDetailBaseTest { when(mCachedBluetoothDevice.getDevice()).thenReturn(device); mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, - BluetoothProfile.A2DP, BluetoothAdapter.STATE_CONNECTED); + BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.A2DP); assertThat(mFragment.getDevicePreferenceMap().size()).isEqualTo(0); } @Test public void onProfileConnectionStateChanged_deviceNotInPreferenceMap_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class); final BluetoothDevicePreference preference = new BluetoothDevicePreference(mContext, mCachedBluetoothDevice, @@ -233,12 +378,26 @@ public class BluetoothDevicePairingDetailBaseTest { when(cachedDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS_B); when(cachedDevice.getIdentityAddress()).thenReturn(TEST_DEVICE_ADDRESS_B); - mFragment.onProfileConnectionStateChanged(cachedDevice, BluetoothProfile.A2DP, - BluetoothAdapter.STATE_CONNECTED); + mFragment.onProfileConnectionStateChanged(cachedDevice, BluetoothAdapter.STATE_CONNECTED, + BluetoothProfile.A2DP); // not crash } + private void setUpFragmentWithPairAndJoinSharingIntent(boolean enablePairAndJoinSharing) { + Bundle args = new Bundle(); + args.putBoolean(EXTRA_PAIR_AND_JOIN_SHARING, enablePairAndJoinSharing); + Intent intent = new Intent(); + intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); + FragmentActivity activity = spy(Robolectric.setupActivity(FragmentActivity.class)); + doReturn(intent).when(activity).getIntent(); + doReturn(activity).when(mFragment).getActivity(); + FragmentManager fragmentManager = mock(FragmentManager.class); + doReturn(fragmentManager).when(mFragment).getFragmentManager(); + mFragment.mShouldTriggerAudioSharingShareThenPairFlow = + mFragment.shouldTriggerAudioSharingShareThenPairFlow(); + } + private static class TestBluetoothDevicePairingDetailBase extends BluetoothDevicePairingDetailBase { diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java index 7d8846d0506..1ce3316811b 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java @@ -16,16 +16,29 @@ package com.android.settings.connecteddevice.audiosharing; +import static com.android.settings.connecteddevice.audiosharing.AudioSharingDashboardFragment.SHARE_THEN_PAIR_REQUEST_CODE; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE; + import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; +import android.app.Activity; import android.app.settings.SettingsEnums; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothStatusCodes; import android.content.Context; +import android.content.Intent; import android.os.Bundle; +import android.os.Looper; +import android.platform.test.flag.junit.SetFlagsRule; import android.view.View; import androidx.test.core.app.ApplicationProvider; @@ -33,24 +46,29 @@ import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsCategoryController; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; import com.android.settings.testutils.shadow.ShadowFragment; import com.android.settings.widget.SettingsMainSwitchBar; +import com.android.settingslib.flags.Flags; 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; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; @RunWith(RobolectricTestRunner.class) -@Config(shadows = {ShadowFragment.class}) +@Config(shadows = {ShadowFragment.class, ShadowBluetoothAdapter.class}) public class AudioSharingDashboardFragmentTest { @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Mock private SettingsActivity mActivity; @Mock private SettingsMainSwitchBar mSwitchBar; @@ -59,11 +77,19 @@ public class AudioSharingDashboardFragmentTest { @Mock private AudioSharingCallAudioPreferenceController mCallAudioController; @Mock private AudioSharingPlaySoundPreferenceController mPlaySoundController; @Mock private AudioStreamsCategoryController mStreamsCategoryController; + @Mock private AudioSharingSwitchBarController mSwitchBarController; private final Context mContext = ApplicationProvider.getApplicationContext(); private AudioSharingDashboardFragment mFragment; + private ShadowBluetoothAdapter mShadowBluetoothAdapter; @Before public void setUp() { + mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + mShadowBluetoothAdapter.setEnabled(true); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); when(mSwitchBar.getRootView()).thenReturn(mView); mFragment = new AudioSharingDashboardFragment(); } @@ -100,13 +126,73 @@ public class AudioSharingDashboardFragmentTest { verify(mSwitchBar).show(); } + @Test + public void onActivityResult_shareThenPairWithBadCode_doNothing() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mFragment.setControllers( + mVolumeGroupController, + mCallAudioController, + mPlaySoundController, + mStreamsCategoryController, + mSwitchBarController); + Intent data = new Intent(); + Bundle extras = new Bundle(); + BluetoothDevice device = Mockito.mock(BluetoothDevice.class); + extras.putParcelable(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, device); + data.putExtras(extras); + mFragment.onActivityResult(SHARE_THEN_PAIR_REQUEST_CODE, Activity.RESULT_CANCELED, data); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mSwitchBarController, never()).handleAutoAddSourceAfterPair(device); + } + + @Test + public void onActivityResult_shareThenPairWithNoDevice_doNothing() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mFragment.setControllers( + mVolumeGroupController, + mCallAudioController, + mPlaySoundController, + mStreamsCategoryController, + mSwitchBarController); + Intent data = new Intent(); + Bundle extras = new Bundle(); + extras.putParcelable(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, null); + data.putExtras(extras); + mFragment.onActivityResult(SHARE_THEN_PAIR_REQUEST_CODE, Activity.RESULT_CANCELED, data); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mSwitchBarController, never()).handleAutoAddSourceAfterPair(any()); + } + + @Test + public void onActivityResult_shareThenPairWithDevice_handleAutoAddSource() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mFragment.setControllers( + mVolumeGroupController, + mCallAudioController, + mPlaySoundController, + mStreamsCategoryController, + mSwitchBarController); + Intent data = new Intent(); + Bundle extras = new Bundle(); + BluetoothDevice device = Mockito.mock(BluetoothDevice.class); + extras.putParcelable(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, device); + data.putExtras(extras); + mFragment.onActivityResult(SHARE_THEN_PAIR_REQUEST_CODE, Activity.RESULT_OK, data); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mSwitchBarController).handleAutoAddSourceAfterPair(device); + } + @Test public void onAudioSharingStateChanged_updateVisibilityForControllers() { mFragment.setControllers( mVolumeGroupController, mCallAudioController, mPlaySoundController, - mStreamsCategoryController); + mStreamsCategoryController, + mSwitchBarController); mFragment.onAudioSharingStateChanged(); verify(mVolumeGroupController).updateVisibility(); verify(mCallAudioController).updateVisibility(); @@ -120,7 +206,8 @@ public class AudioSharingDashboardFragmentTest { mVolumeGroupController, mCallAudioController, mPlaySoundController, - mStreamsCategoryController); + mStreamsCategoryController, + mSwitchBarController); mFragment.onAudioSharingProfilesConnected(); verify(mVolumeGroupController).onAudioSharingProfilesConnected(); } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java index 7227f37998b..dec85e47b80 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java @@ -34,6 +34,7 @@ import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; @@ -82,11 +83,6 @@ public class AudioSharingDialogFragmentTest { new AudioSharingDeviceItem(TEST_DEVICE_NAME3, /* groupId= */ 3, /* isActive= */ false); private static final AudioSharingDialogFragment.DialogEventListener EMPTY_EVENT_LISTENER = new AudioSharingDialogFragment.DialogEventListener() { - @Override - public void onItemClick(AudioSharingDeviceItem item) {} - - @Override - public void onCancelClick() {} }; private static final Pair TEST_EVENT_DATA = Pair.create(1, 1); private static final Pair[] TEST_EVENT_DATA_LIST = @@ -176,8 +172,17 @@ public class AudioSharingDialogFragmentTest { @Test public void onCreateDialog_noExtraConnectedDevice_pairNewDevice() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + AtomicBoolean isPairBtnClicked = new AtomicBoolean(false); AudioSharingDialogFragment.show( - mParent, new ArrayList<>(), EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST); + mParent, + new ArrayList<>(), + new AudioSharingDialogFragment.DialogEventListener() { + @Override + public void onPositiveClick() { + isPairBtnClicked.set(true); + } + }, + TEST_EVENT_DATA_LIST); shadowMainLooper().idle(); AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); assertThat(dialog).isNotNull(); @@ -191,14 +196,24 @@ public class AudioSharingDialogFragmentTest { any(Context.class), eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED), eq(TEST_EVENT_DATA)); + assertThat(isPairBtnClicked.get()).isTrue(); assertThat(dialog.isShowing()).isFalse(); } @Test public void onCreateDialog_noExtraConnectedDevice_showQRCode() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + AtomicBoolean isQrCodeBtnClicked = new AtomicBoolean(false); AudioSharingDialogFragment.show( - mParent, new ArrayList<>(), EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST); + mParent, + new ArrayList<>(), + new AudioSharingDialogFragment.DialogEventListener() { + @Override + public void onCancelClick() { + isQrCodeBtnClicked.set(true); + } + }, + TEST_EVENT_DATA_LIST); shadowMainLooper().idle(); AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); assertThat(dialog).isNotNull(); @@ -212,6 +227,7 @@ public class AudioSharingDialogFragmentTest { any(Context.class), eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED), eq(TEST_EVENT_DATA)); + assertThat(isQrCodeBtnClicked.get()).isTrue(); assertThat(dialog.isShowing()).isFalse(); } @@ -286,12 +302,9 @@ public class AudioSharingDialogFragmentTest { list, new AudioSharingDialogFragment.DialogEventListener() { @Override - public void onItemClick(AudioSharingDeviceItem item) { + public void onItemClick(@NonNull AudioSharingDeviceItem item) { isShareBtnClicked.set(true); } - - @Override - public void onCancelClick() {} }, TEST_EVENT_DATA_LIST); shadowMainLooper().idle(); @@ -359,9 +372,6 @@ public class AudioSharingDialogFragmentTest { mParent, list, new AudioSharingDialogFragment.DialogEventListener() { - @Override - public void onItemClick(AudioSharingDeviceItem item) {} - @Override public void onCancelClick() { isCancelBtnClicked.set(true); diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragmentTest.java index 7f172918d36..67cb2aa915b 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragmentTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragmentTest.java @@ -18,7 +18,6 @@ package com.android.settings.connecteddevice.audiosharing; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.when; import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; import android.bluetooth.BluetoothAdapter; @@ -34,7 +33,6 @@ import androidx.fragment.app.FragmentActivity; import com.android.settings.R; import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; -import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.flags.Flags; import org.junit.After; @@ -42,7 +40,6 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; @@ -62,7 +59,6 @@ public class AudioSharingIncompatibleDialogFragmentTest { @Rule public final MockitoRule mocks = MockitoJUnit.rule(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - @Mock private CachedBluetoothDevice mCachedBluetoothDevice; private Fragment mParent; private AudioSharingIncompatibleDialogFragment mFragment; @@ -76,7 +72,6 @@ public class AudioSharingIncompatibleDialogFragmentTest { BluetoothStatusCodes.FEATURE_SUPPORTED); shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( BluetoothStatusCodes.FEATURE_SUPPORTED); - when(mCachedBluetoothDevice.getName()).thenReturn(TEST_DEVICE_NAME); mFragment = new AudioSharingIncompatibleDialogFragment(); mParent = new Fragment(); FragmentController.setupFragment(mParent, FragmentActivity.class, /* containerViewId= */ @@ -97,7 +92,7 @@ public class AudioSharingIncompatibleDialogFragmentTest { @Test public void onCreateDialog_flagOff_dialogNotExist() { mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - AudioSharingIncompatibleDialogFragment.show(mParent, mCachedBluetoothDevice, + AudioSharingIncompatibleDialogFragment.show(mParent, TEST_DEVICE_NAME, EMPTY_EVENT_LISTENER); shadowMainLooper().idle(); AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); @@ -107,7 +102,7 @@ public class AudioSharingIncompatibleDialogFragmentTest { @Test public void onCreateDialog_unattachedFragment_dialogNotExist() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - AudioSharingIncompatibleDialogFragment.show(new Fragment(), mCachedBluetoothDevice, + AudioSharingIncompatibleDialogFragment.show(new Fragment(), TEST_DEVICE_NAME, EMPTY_EVENT_LISTENER); shadowMainLooper().idle(); AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); @@ -117,7 +112,7 @@ public class AudioSharingIncompatibleDialogFragmentTest { @Test public void onCreateDialog_flagOn_showDialog() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - AudioSharingIncompatibleDialogFragment.show(mParent, mCachedBluetoothDevice, + AudioSharingIncompatibleDialogFragment.show(mParent, TEST_DEVICE_NAME, EMPTY_EVENT_LISTENER); shadowMainLooper().idle(); AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); @@ -134,7 +129,7 @@ public class AudioSharingIncompatibleDialogFragmentTest { public void onCreateDialog_clickBtn_callbackTriggered() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); AtomicBoolean isBtnClicked = new AtomicBoolean(false); - AudioSharingIncompatibleDialogFragment.show(mParent, mCachedBluetoothDevice, + AudioSharingIncompatibleDialogFragment.show(mParent, TEST_DEVICE_NAME, () -> isBtnClicked.set(true)); shadowMainLooper().idle(); AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java index 0d21f18b821..eb2083ebe7b 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java @@ -934,6 +934,19 @@ public class AudioSharingSwitchBarControllerTest { childFragments.forEach(fragment -> ((DialogFragment) fragment).dismiss()); } + @Test + public void handleAutoAddSourceAfterPair() { + when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(mDevice1)); + when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(mMetadata); + mController.handleAutoAddSourceAfterPair(mDevice1); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mAssistant).addSource(mDevice1, mMetadata, /* isGroupOp= */ false); + List childFragments = mParentFragment.getChildFragmentManager().getFragments(); + assertThat(childFragments).comparingElementsUsing(CLAZZNAME_EQUALS).containsExactly( + AudioSharingLoadingStateDialogFragment.class.getName()); + } + private Fragment setUpFragmentWithStartSharingIntent() { Bundle args = new Bundle(); args.putBoolean(EXTRA_START_LE_AUDIO_SHARING, true);