[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
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java
index 6d5b693..bc8ff21 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java
@@ -30,10 +30,11 @@
 public class AudioSharingDeviceAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
 
     private static final String TAG = "AudioSharingDeviceAdapter";
-    private final ArrayList<String> mDevices;
+    private final ArrayList<AudioSharingDeviceItem> mDevices;
     private final OnClickListener mOnClickListener;
 
-    public AudioSharingDeviceAdapter(ArrayList<String> devices, OnClickListener listener) {
+    public AudioSharingDeviceAdapter(
+            ArrayList<AudioSharingDeviceItem> devices, OnClickListener listener) {
         mDevices = devices;
         mOnClickListener = listener;
     }
@@ -48,8 +49,9 @@
 
         public void bindView(int position) {
             if (mButtonView != null) {
-                mButtonView.setText(mDevices.get(position));
-                mButtonView.setOnClickListener(v -> 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 interface OnClickListener {
         /** Called when an item has been clicked. */
-        void onClick(int position);
+        void onClick(AudioSharingDeviceItem item);
     }
 }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceItem.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceItem.java
new file mode 100644
index 0000000..a68117a
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceItem.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.audiosharing;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public final class AudioSharingDeviceItem implements Parcelable {
+    private final String mName;
+    private final int mGroupId;
+
+    public AudioSharingDeviceItem(String name, int groupId) {
+        mName = name;
+        mGroupId = groupId;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public int getGroupId() {
+        return mGroupId;
+    }
+
+    public AudioSharingDeviceItem(Parcel in) {
+        mName = in.readString();
+        mGroupId = in.readInt();
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mName);
+        dest.writeInt(mGroupId);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Creator<AudioSharingDeviceItem> CREATOR =
+            new Creator<AudioSharingDeviceItem>() {
+                @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 1fd0b87..bcf0c12 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.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 @@
         /**
          * 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 @@
      * @param host The Fragment this dialog will be hosted.
      */
     public static void show(
-            Fragment host, ArrayList<String> deviceNames, DialogEventListener listener) {
+            Fragment host,
+            ArrayList<AudioSharingDeviceItem> 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 @@
     @Override
     public Dialog onCreateDialog(Bundle savedInstanceState) {
         Bundle arguments = requireArguments();
-        ArrayList<String> deviceNames = arguments.getStringArrayList(BUNDLE_KEY_DEVICE_NAMES);
+        ArrayList<AudioSharingDeviceItem> 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 @@
                         .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 bd8027c..ff383a7 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.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 @@
     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<BluetoothDevice> mTargetSinks = new ArrayList<>();
 
     private final BluetoothLeBroadcast.Callback mBroadcastCallback =
             new BluetoothLeBroadcast.Callback() {
@@ -79,7 +95,7 @@
                                     + broadcastId
                                     + ", metadata = "
                                     + metadata);
-                    // TODO: handle add sink if there are connected lea devices.
+                    addSourceToTargetDevices(mTargetSinks);
                 }
 
                 @Override
@@ -113,11 +129,79 @@
                 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 @@
         if (mBroadcast != null) {
             mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
         }
+        if (mAssistant != null) {
+            mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+        }
     }
 
     @Override
@@ -136,6 +223,9 @@
         if (mBroadcast != null) {
             mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
         }
+        if (mAssistant != null) {
+            mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
+        }
     }
 
     @Override
@@ -175,18 +265,49 @@
             mSwitchBar.setEnabled(true);
             return;
         }
-        ArrayList<String> deviceNames = new ArrayList<>();
+        Map<Integer, List<CachedBluetoothDevice>> groupedDevices = fetchConnectedDevicesByGroupId();
+        ArrayList<AudioSharingDeviceItem> deviceItems = new ArrayList<>();
+        Optional<Integer> activeGroupId = Optional.empty();
+        for (List<CachedBluetoothDevice> 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 @@
     private boolean isBroadcasting() {
         return mBroadcast != null && mBroadcast.isEnabled(null);
     }
+
+    private Map<Integer, List<CachedBluetoothDevice>> fetchConnectedDevicesByGroupId() {
+        // TODO: filter out devices with le audio disabled.
+        List<BluetoothDevice> connectedDevices =
+                mAssistant == null ? ImmutableList.of() : mAssistant.getConnectedDevices();
+        Map<Integer, List<CachedBluetoothDevice>> 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<BluetoothDevice> 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);
+        }
+    }
 }