Merge "[Audiosharing] Refine share then pair flow" into main

This commit is contained in:
Yiyi Shen
2024-09-19 13:29:37 +00:00
committed by Android (Google) Code Review
11 changed files with 672 additions and 75 deletions

View File

@@ -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<BluetoothDevice> 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);
}
}

View File

@@ -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() {

View File

@@ -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<Integer, Object>[] 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();
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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<Integer, Object>[] 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<BluetoothDevice> 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;
}

View File

@@ -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<Drawable, String> 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<Intent> 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 {

View File

@@ -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();
}

View File

@@ -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<Integer, Object> TEST_EVENT_DATA = Pair.create(1, 1);
private static final Pair<Integer, Object>[] 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);

View File

@@ -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();

View File

@@ -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<Fragment> 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);