From 87372de071fc53c9b2334f35a2179b565dbbc63a Mon Sep 17 00:00:00 2001 From: Yiyi Shen Date: Mon, 6 Nov 2023 15:57:47 +0800 Subject: [PATCH] [Audiosharing] Impl audio sharing main switch. Start/stop broadcast when >=1 eligible buds connected. Flagged with enable_le_audio_sharing Bug: 305620450 Test: Manual Change-Id: Ic982571f49ab79c39d0503929df4bb8be64b720e --- .../AudioSharingDeviceAdapter.java | 12 +- .../audiosharing/AudioSharingDeviceItem.java | 67 +++++++ .../AudioSharingDialogFragment.java | 48 ++--- .../AudioSharingSwitchBarController.java | 178 +++++++++++++++++- 4 files changed, 274 insertions(+), 31 deletions(-) create mode 100644 src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceItem.java diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java index 6d5b693a7a0..bc8ff2121e1 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java @@ -30,10 +30,11 @@ import java.util.ArrayList; public class AudioSharingDeviceAdapter extends RecyclerView.Adapter { private static final String TAG = "AudioSharingDeviceAdapter"; - private final ArrayList mDevices; + private final ArrayList mDevices; private final OnClickListener mOnClickListener; - public AudioSharingDeviceAdapter(ArrayList devices, OnClickListener listener) { + public AudioSharingDeviceAdapter( + ArrayList devices, OnClickListener listener) { mDevices = devices; mOnClickListener = listener; } @@ -48,8 +49,9 @@ public class AudioSharingDeviceAdapter extends RecyclerView.Adapter mOnClickListener.onClick(position)); + mButtonView.setText(mDevices.get(position).getName()); + mButtonView.setOnClickListener( + v -> mOnClickListener.onClick(mDevices.get(position))); } else { Log.w(TAG, "bind view skipped due to button view is null"); } @@ -76,6 +78,6 @@ public class AudioSharingDeviceAdapter extends RecyclerView.Adapter CREATOR = + new Creator() { + @Override + public AudioSharingDeviceItem createFromParcel(Parcel in) { + return new AudioSharingDeviceItem(in); + } + + @Override + public AudioSharingDeviceItem[] newArray(int size) { + return new AudioSharingDeviceItem[size]; + } + }; +} diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java index 1fd0b878602..bcf0c120e72 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java @@ -34,11 +34,12 @@ import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.flags.Flags; import java.util.ArrayList; +import java.util.Locale; public class AudioSharingDialogFragment extends InstrumentedDialogFragment { private static final String TAG = "AudioSharingDialog"; - private static final String BUNDLE_KEY_DEVICE_NAMES = "bundle_key_device_names"; + private static final String BUNDLE_KEY_DEVICE_ITEMS = "bundle_key_device_names"; // The host creates an instance of this dialog fragment must implement this interface to receive // event callbacks. @@ -46,13 +47,11 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { /** * Called when users click the device item for sharing in the dialog. * - * @param position The position of the item clicked. + * @param item The device item clicked. */ - void onItemClick(int position); + void onItemClick(AudioSharingDeviceItem item); - /** - * Called when users click the cancel button in the dialog. - */ + /** Called when users click the cancel button in the dialog. */ void onCancelClick(); } @@ -71,13 +70,15 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { * @param host The Fragment this dialog will be hosted. */ public static void show( - Fragment host, ArrayList deviceNames, DialogEventListener listener) { + Fragment host, + ArrayList deviceItems, + DialogEventListener listener) { if (!Flags.enableLeAudioSharing()) return; final FragmentManager manager = host.getChildFragmentManager(); sListener = listener; if (manager.findFragmentByTag(TAG) == null) { final Bundle bundle = new Bundle(); - bundle.putStringArrayList(BUNDLE_KEY_DEVICE_NAMES, deviceNames); + bundle.putParcelableArrayList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems); AudioSharingDialogFragment dialog = new AudioSharingDialogFragment(); dialog.setArguments(bundle); dialog.show(manager, TAG); @@ -87,7 +88,8 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Bundle arguments = requireArguments(); - ArrayList deviceNames = arguments.getStringArrayList(BUNDLE_KEY_DEVICE_NAMES); + ArrayList deviceItems = + arguments.getParcelableArrayList(BUNDLE_KEY_DEVICE_ITEMS); final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()).setTitle("Share audio").setCancelable(false); mRootView = @@ -95,29 +97,33 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { .inflate(R.layout.dialog_audio_sharing, /* parent= */ null); TextView subTitle1 = mRootView.findViewById(R.id.share_audio_subtitle1); TextView subTitle2 = mRootView.findViewById(R.id.share_audio_subtitle2); - if (deviceNames.isEmpty()) { + if (deviceItems.isEmpty()) { subTitle1.setVisibility(View.INVISIBLE); - subTitle2.setText("To start sharing audio, connect headphones that support LE audio"); + subTitle2.setText( + "To start sharing audio, connect additional headphones that support LE audio"); builder.setNegativeButton( "Close", (dialog, which) -> { sListener.onCancelClick(); }); - } else if (deviceNames.size() == 1) { - // TODO: add real impl - subTitle1.setText("1 devices connected"); - subTitle2.setText("placeholder"); } else { - // TODO: add real impl - subTitle1.setText("2 devices connected"); - subTitle2.setText("placeholder"); + subTitle1.setText( + String.format( + Locale.US, + "%d additional device%s connected", + deviceItems.size(), + deviceItems.size() > 1 ? "" : "s")); + subTitle2.setText( + "The headphones you share audio with will hear videos and music playing on this" + + " phone"); } RecyclerView recyclerView = mRootView.findViewById(R.id.btn_list); recyclerView.setAdapter( new AudioSharingDeviceAdapter( - deviceNames, - (int position) -> { - sListener.onItemClick(position); + deviceItems, + (AudioSharingDeviceItem item) -> { + sListener.onItemClick(item); + dismiss(); })); recyclerView.setLayoutManager( new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false)); diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java index bd8027c7bd8..ff383a7269d 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java @@ -16,8 +16,13 @@ package com.android.settings.connecteddevice.audiosharing; +import android.bluetooth.BluetoothCsipSetCoordinator; +import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeBroadcast; +import android.bluetooth.BluetoothLeBroadcastAssistant; import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.bluetooth.BluetoothProfile; import android.content.Context; import android.util.Log; import android.widget.Switch; @@ -31,12 +36,21 @@ import com.android.settings.core.BasePreferenceController; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.flags.Flags; import com.android.settings.widget.SettingsMainSwitchBar; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.widget.OnMainSwitchChangeListener; +import com.google.common.collect.ImmutableList; + import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -47,8 +61,10 @@ public class AudioSharingSwitchBarController extends BasePreferenceController private final SettingsMainSwitchBar mSwitchBar; private final LocalBluetoothManager mBtManager; private final LocalBluetoothLeBroadcast mBroadcast; + private final LocalBluetoothLeBroadcastAssistant mAssistant; private final Executor mExecutor; private DashboardFragment mFragment; + private List mTargetSinks = new ArrayList<>(); private final BluetoothLeBroadcast.Callback mBroadcastCallback = new BluetoothLeBroadcast.Callback() { @@ -79,7 +95,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController + broadcastId + ", metadata = " + metadata); - // TODO: handle add sink if there are connected lea devices. + addSourceToTargetDevices(mTargetSinks); } @Override @@ -113,11 +129,79 @@ public class AudioSharingSwitchBarController extends BasePreferenceController public void onPlaybackStopped(int reason, int broadcastId) {} }; + private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = + new BluetoothLeBroadcastAssistant.Callback() { + @Override + public void onSearchStarted(int reason) {} + + @Override + public void onSearchStartFailed(int reason) {} + + @Override + public void onSearchStopped(int reason) {} + + @Override + public void onSearchStopFailed(int reason) {} + + @Override + public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {} + + @Override + public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) { + Log.d( + TAG, + "onSourceAdded(), sink = " + + sink + + ", sourceId = " + + sourceId + + ", reason = " + + reason); + } + + @Override + public void onSourceAddFailed( + @NonNull BluetoothDevice sink, + @NonNull BluetoothLeBroadcastMetadata source, + int reason) { + Log.d( + TAG, + "onSourceAddFailed(), sink = " + + sink + + ", source = " + + source + + ", reason = " + + reason); + } + + @Override + public void onSourceModified( + @NonNull BluetoothDevice sink, int sourceId, int reason) {} + + @Override + public void onSourceModifyFailed( + @NonNull BluetoothDevice sink, int sourceId, int reason) {} + + @Override + public void onSourceRemoved( + @NonNull BluetoothDevice sink, int sourceId, int reason) {} + + @Override + public void onSourceRemoveFailed( + @NonNull BluetoothDevice sink, int sourceId, int reason) {} + + @Override + public void onReceiveStateChanged( + BluetoothDevice sink, + int sourceId, + BluetoothLeBroadcastReceiveState state) {} + }; + AudioSharingSwitchBarController(Context context, SettingsMainSwitchBar switchBar) { super(context, PREF_KEY); mSwitchBar = switchBar; mBtManager = Utils.getLocalBtManager(context); mBroadcast = mBtManager.getProfileManager().getLeAudioBroadcastProfile(); + mAssistant = mBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); mExecutor = Executors.newSingleThreadExecutor(); mSwitchBar.setChecked(isBroadcasting()); } @@ -128,6 +212,9 @@ public class AudioSharingSwitchBarController extends BasePreferenceController if (mBroadcast != null) { mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback); } + if (mAssistant != null) { + mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); + } } @Override @@ -136,6 +223,9 @@ public class AudioSharingSwitchBarController extends BasePreferenceController if (mBroadcast != null) { mBroadcast.unregisterServiceCallBack(mBroadcastCallback); } + if (mAssistant != null) { + mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); + } } @Override @@ -175,18 +265,49 @@ public class AudioSharingSwitchBarController extends BasePreferenceController mSwitchBar.setEnabled(true); return; } - ArrayList deviceNames = new ArrayList<>(); + Map> groupedDevices = fetchConnectedDevicesByGroupId(); + ArrayList deviceItems = new ArrayList<>(); + Optional activeGroupId = Optional.empty(); + for (List devices : groupedDevices.values()) { + // Use random device in the group to represent the group. + CachedBluetoothDevice device = devices.get(0); + // TODO: add BluetoothUtils.isActiveLeAudioDevice to avoid directly using isActiveDevice + if (device.isActiveDevice(BluetoothProfile.LE_AUDIO)) { + activeGroupId = Optional.of(device.getGroupId()); + } else { + AudioSharingDeviceItem item = + new AudioSharingDeviceItem(device.getName(), device.getGroupId()); + deviceItems.add(item); + } + } + mTargetSinks = new ArrayList<>(); + activeGroupId.ifPresent( + gId -> { + if (groupedDevices.containsKey(gId)) { + for (CachedBluetoothDevice device : groupedDevices.get(gId)) { + mTargetSinks.add(device.getDevice()); + } + } + }); AudioSharingDialogFragment.show( mFragment, - deviceNames, + deviceItems, new AudioSharingDialogFragment.DialogEventListener() { @Override - public void onItemClick(int position) { - // TODO: handle broadcast based on the dialog device item clicked + public void onItemClick(AudioSharingDeviceItem item) { + if (groupedDevices.containsKey(item.getGroupId())) { + for (CachedBluetoothDevice device : + groupedDevices.get(item.getGroupId())) { + mTargetSinks.add(device.getDevice()); + } + } + // TODO: handle app source name for broadcasting. + mBroadcast.startBroadcast("test", /* language= */ null); } @Override public void onCancelClick() { + // TODO: handle app source name for broadcasting. mBroadcast.startBroadcast("test", /* language= */ null); } }); @@ -213,4 +334,51 @@ public class AudioSharingSwitchBarController extends BasePreferenceController private boolean isBroadcasting() { return mBroadcast != null && mBroadcast.isEnabled(null); } + + private Map> fetchConnectedDevicesByGroupId() { + // TODO: filter out devices with le audio disabled. + List connectedDevices = + mAssistant == null ? ImmutableList.of() : mAssistant.getConnectedDevices(); + Map> groupedDevices = new HashMap<>(); + CachedBluetoothDeviceManager cacheManager = mBtManager.getCachedDeviceManager(); + for (BluetoothDevice device : connectedDevices) { + CachedBluetoothDevice cachedDevice = cacheManager.findDevice(device); + if (cachedDevice == null) { + Log.d(TAG, "Skip device due to not being cached: " + device.getAnonymizedAddress()); + continue; + } + int groupId = cachedDevice.getGroupId(); + if (groupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { + Log.d(TAG, "Skip device due to no valid group id"); + continue; + } + if (!groupedDevices.containsKey(groupId)) { + groupedDevices.put(groupId, new ArrayList<>()); + } + groupedDevices.get(groupId).add(cachedDevice); + } + return groupedDevices; + } + + private void addSourceToTargetDevices(List sinks) { + if (sinks.isEmpty() || mBroadcast == null || mAssistant == null) { + Log.d(TAG, "Skip adding source to target."); + return; + } + BluetoothLeBroadcastMetadata broadcastMetadata = + mBroadcast.getLatestBluetoothLeBroadcastMetadata(); + if (broadcastMetadata == null) { + Log.e(TAG, "Error: There is no broadcastMetadata."); + return; + } + for (BluetoothDevice sink : sinks) { + Log.d( + TAG, + "Add broadcast with broadcastId: " + + broadcastMetadata.getBroadcastId() + + "to the device: " + + sink.getAnonymizedAddress()); + mAssistant.addSource(sink, broadcastMetadata, /* isGroupOp= */ false); + } + } }