Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 87372de0 authored by Yiyi Shen's avatar Yiyi Shen
Browse files

[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
parent cded970a
Loading
Loading
Loading
Loading
+7 −5
Original line number Diff line number Diff line
@@ -30,10 +30,11 @@ import java.util.ArrayList;
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 class AudioSharingDeviceAdapter extends RecyclerView.Adapter<RecyclerView

        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 class AudioSharingDeviceAdapter extends RecyclerView.Adapter<RecyclerView

    public interface OnClickListener {
        /** Called when an item has been clicked. */
        void onClick(int position);
        void onClick(AudioSharingDeviceItem item);
    }
}
+67 −0
Original line number Diff line number Diff line
/*
 * 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];
                }
            };
}
+27 −21
Original line number Diff line number Diff line
@@ -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<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 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
    @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 @@ 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));
+173 −5
Original line number Diff line number Diff line
@@ -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<BluetoothDevice> 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<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 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
    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);
        }
    }
}