[Audiosharing] Refine share then pair flow

Currently when there is one active LEA headset and users toggle on the audio sharing, a dialog will pop up to ask users to pair new headset and share audio with it. After user click pair new device button on the dialog:

1. Route users to pair new device page.
2. If users pair an LEA headset, finish the pair new device page and
   auto add source to the headset with loading indicators on audio
   sharing page.
3. If users pair a classic headset, wait for timeout, pop up dialog
   saying the paired headset is not compatible for audio sharing and
   finish the pair new device page.

Test: atest
Flag: com.android.settingslib.flags.enable_le_audio_sharing
Bug: 331892035
Change-Id: Ifb9579db0ef57d3a379cb5d17c66a604d1396bb4
This commit is contained in:
Yiyi Shen
2024-08-15 16:50:52 +08:00
parent 958639b79c
commit 800f81c832
11 changed files with 672 additions and 75 deletions

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;
}