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

Commit 14573301 authored by Yiyi Shen's avatar Yiyi Shen Committed by Android (Google) Code Review
Browse files

Merge "[Audiosharing] Impl the switch audio sharing dialog." into main

parents 402385c6 2a2a748d
Loading
Loading
Loading
Loading
+47 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ 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.
  -->

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="24dp"
    android:orientation="vertical">

    <TextView
        android:id="@+id/share_audio_disconnect_description"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textAlignment="center"
        android:layout_gravity="center"/>

    <com.android.internal.widget.RecyclerView
        android:visibility="visible"
        android:id="@+id/device_btn_list"
        android:nestedScrollingEnabled="false"
        android:overScrollMode="never"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"/>

    <Button
        android:id="@+id/cancel_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="@string/cancel"/>
</LinearLayout>
 No newline at end of file
+184 −24
Original line number Diff line number Diff line
@@ -17,7 +17,9 @@
package com.android.settings.connecteddevice.audiosharing;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcast;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
@@ -41,13 +43,18 @@ import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.flags.Flags;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
import com.android.settingslib.bluetooth.LeAudioProfile;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfile;

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.concurrent.Executor;
import java.util.concurrent.Executors;

@@ -68,6 +75,37 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
    private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
    private DashboardFragment mFragment;

    private final BluetoothLeBroadcast.Callback mBroadcastCallback =
            new BluetoothLeBroadcast.Callback() {
                @Override
                public void onBroadcastStarted(int reason, int broadcastId) {}

                @Override
                public void onBroadcastStartFailed(int reason) {}

                @Override
                public void onBroadcastMetadataChanged(
                        int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) {}

                @Override
                public void onBroadcastStopped(int reason, int broadcastId) {}

                @Override
                public void onBroadcastStopFailed(int reason) {}

                @Override
                public void onBroadcastUpdated(int reason, int broadcastId) {}

                @Override
                public void onBroadcastUpdateFailed(int reason, int broadcastId) {}

                @Override
                public void onPlaybackStarted(int reason, int broadcastId) {}

                @Override
                public void onPlaybackStopped(int reason, int broadcastId) {}
            };

    private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
            new BluetoothLeBroadcastAssistant.Callback() {
                @Override
@@ -169,8 +207,8 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
            Log.d(TAG, "onStart() Bluetooth is not supported on this device");
            return;
        }
        if (mAssistant == null) {
            Log.d(TAG, "onStart() Broadcast assistant is not supported on this device");
        if (mBroadcast == null || mAssistant == null) {
            Log.d(TAG, "onStart() Broadcast or assistant is not supported on this device");
            return;
        }
        if (mBluetoothDeviceUpdater == null) {
@@ -178,6 +216,7 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
            return;
        }
        mLocalBtManager.getEventManager().registerCallback(this);
        mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
        mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
        mBluetoothDeviceUpdater.registerCallback();
        mBluetoothDeviceUpdater.refreshPreference();
@@ -189,8 +228,8 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
            Log.d(TAG, "onStop() Bluetooth is not supported on this device");
            return;
        }
        if (mAssistant == null) {
            Log.d(TAG, "onStop() Broadcast assistant is not supported on this device");
        if (mBroadcast == null || mAssistant == null) {
            Log.d(TAG, "onStop() Broadcast or assistant is not supported on this device");
            return;
        }
        if (mBluetoothDeviceUpdater == null) {
@@ -200,9 +239,12 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
        mLocalBtManager.getEventManager().unregisterCallback(this);
        // TODO: verify the reason for failing to unregister
        try {
            mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
            mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
        } catch (IllegalArgumentException e) {
            Log.e(TAG, "Fail to unregister assistant callback due to " + e.getMessage());
            Log.e(
                    TAG,
                    "Fail to unregister broadcast or assistant callback due to " + e.getMessage());
        }
        mBluetoothDeviceUpdater.unregisterCallback();
    }
@@ -263,25 +305,39 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
            Log.d(TAG, "Ignore onProfileConnectionStateChanged, not connected state");
            return;
        }
        List<LocalBluetoothProfile> supportedProfiles = cachedDevice.getProfiles();
        boolean isLeAudioSupported = false;
        for (LocalBluetoothProfile profile : supportedProfiles) {
            if (profile instanceof LeAudioProfile && profile.isEnabled(cachedDevice.getDevice())) {
                isLeAudioSupported = true;
        if (mFragment == null) {
            Log.d(TAG, "Ignore onProfileConnectionStateChanged, no host fragment");
            return;
        }
            if (profile.getProfileId() != bluetoothProfile
                    && profile.getConnectionStatus(cachedDevice.getDevice())
                            == BluetoothProfile.STATE_CONNECTED) {
        if (mAssistant == null && mBroadcast == null) {
            Log.d(
                    TAG,
                    "Ignore onProfileConnectionStateChanged, no broadcast or assistant supported");
            return;
        }
        boolean isLeAudioSupported = isLeAudioSupported(cachedDevice);
        // For eligible (LE audio) remote device, we only check its connected LE audio profile.
        if (isLeAudioSupported && bluetoothProfile != BluetoothProfile.LE_AUDIO) {
            Log.d(
                    TAG,
                        "Ignore onProfileConnectionStateChanged, not the first connected profile");
                    "Ignore onProfileConnectionStateChanged, not the le profile for le audio"
                        + " device");
            return;
        }
        boolean isFirstConnectedProfile = isFirstConnectedProfile(cachedDevice, bluetoothProfile);
        // For ineligible (non LE audio) remote device, we only check its first connected profile.
        if (!isLeAudioSupported && !isFirstConnectedProfile) {
            Log.d(
                    TAG,
                    "Ignore onProfileConnectionStateChanged, not the first connected profile for"
                        + " non le audio device");
            return;
        }
        // Show stop audio sharing dialog when an ineligible (not le audio) remote device connected
        // during a sharing session.
        if (isBroadcasting() && !isLeAudioSupported) {
            if (mFragment != null) {
        if (!isLeAudioSupported) {
            // Handle connected ineligible (non LE audio) remote device
            if (isBroadcasting()) {
                // Show stop audio sharing dialog when an ineligible (non LE audio) remote device
                // connected during a sharing session.
                AudioSharingStopDialogFragment.show(
                        mFragment,
                        cachedDevice.getName(),
@@ -289,6 +345,46 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
                            mBroadcast.stopBroadcast(mBroadcast.getLatestBroadcastId());
                        });
            }
            // Do nothing for ineligible (non LE audio) remote device when no sharing session.
        } else {
            // Handle connected eligible (LE audio) remote device
            if (isBroadcasting()) {
                // Show audio sharing switch or join dialog according to device count in the sharing
                // session.
                Map<Integer, List<CachedBluetoothDevice>> groupedDevices =
                        fetchConnectedDevicesByGroupId();
                ArrayList<AudioSharingDeviceItem> deviceItems =
                        buildDeviceItemsInSharingSession(groupedDevices);
                // Show switch audio sharing dialog when the third eligible (LE audio) remote device
                // connected during a sharing session.
                if (deviceItems.size() >= 2) {
                    AudioSharingDisconnectDialogFragment.show(
                            mFragment,
                            deviceItems,
                            cachedDevice.getName(),
                            (AudioSharingDeviceItem item) -> {
                                // Remove all sources from the device user clicked
                                for (CachedBluetoothDevice device :
                                        groupedDevices.get(item.getGroupId())) {
                                    for (BluetoothLeBroadcastReceiveState source :
                                            mAssistant.getAllSources(device.getDevice())) {
                                        mAssistant.removeSource(
                                                device.getDevice(), source.getSourceId());
                                    }
                                }
                                // Add current broadcast to the latest connected device
                                mAssistant.addSource(
                                        cachedDevice.getDevice(),
                                        mBroadcast.getLatestBluetoothLeBroadcastMetadata(),
                                        /* isGroupOp= */ true);
                            });
                } else {
                    // TODO: show dialog to add device to sharing session.
                }
            } else {
                // Show audio sharing join dialog when no sharing session.
                // TODO: show dialog to add device to sharing session.
            }
        }
    }

@@ -307,7 +403,71 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
                        fragment.getMetricsCategory());
    }

    private boolean isLeAudioSupported(CachedBluetoothDevice cachedDevice) {
        return cachedDevice.getProfiles().stream()
                .anyMatch(
                        profile ->
                                profile instanceof LeAudioProfile
                                        && profile.isEnabled(cachedDevice.getDevice()));
    }

    private boolean isFirstConnectedProfile(
            CachedBluetoothDevice cachedDevice, int bluetoothProfile) {
        return cachedDevice.getProfiles().stream()
                .noneMatch(
                        profile ->
                                profile.getProfileId() != bluetoothProfile
                                        && profile.getConnectionStatus(cachedDevice.getDevice())
                                                == BluetoothProfile.STATE_CONNECTED);
    }

    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 = mLocalBtManager.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: " + device.getAnonymizedAddress());
                continue;
            }
            if (!groupedDevices.containsKey(groupId)) {
                groupedDevices.put(groupId, new ArrayList<>());
            }
            groupedDevices.get(groupId).add(cachedDevice);
        }
        return groupedDevices;
    }

    private ArrayList<AudioSharingDeviceItem> buildDeviceItemsInSharingSession(
            Map<Integer, List<CachedBluetoothDevice>> groupedDevices) {
        ArrayList<AudioSharingDeviceItem> deviceItems = new ArrayList<>();
        for (List<CachedBluetoothDevice> devices : groupedDevices.values()) {
            for (CachedBluetoothDevice device : devices) {
                List<BluetoothLeBroadcastReceiveState> sourceList =
                        mAssistant.getAllSources(device.getDevice());
                if (!sourceList.isEmpty()) {
                    // Use random device in the group within the sharing session to
                    // represent the group.
                    deviceItems.add(
                            new AudioSharingDeviceItem(device.getName(), device.getGroupId()));
                    break;
                }
            }
        }
        return deviceItems;
    }
}
+124 −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.app.Dialog;
import android.app.settings.SettingsEnums;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;

import com.android.internal.widget.LinearLayoutManager;
import com.android.internal.widget.RecyclerView;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settings.flags.Flags;

import java.util.ArrayList;

public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFragment {
    private static final String TAG = "AudioSharingDisconnectDialog";

    private static final String BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS =
            "bundle_key_device_to_disconnect_items";
    private static final String BUNDLE_KEY_NEW_DEVICE_NAME = "bundle_key_new_device_name";

    // 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 device item to disconnect from the audio sharing in the
         * dialog.
         *
         * @param item The device item clicked.
         */
        void onItemClick(AudioSharingDeviceItem item);
    }

    private static DialogEventListener sListener;

    @Override
    public int getMetricsCategory() {
        return SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE;
    }

    /**
     * Display the {@link AudioSharingDisconnectDialogFragment} dialog.
     *
     * @param host The Fragment this dialog will be hosted.
     */
    public static void show(
            Fragment host,
            ArrayList<AudioSharingDeviceItem> deviceItems,
            String newDeviceName,
            DialogEventListener listener) {
        if (!Flags.enableLeAudioSharing()) return;
        final FragmentManager manager = host.getChildFragmentManager();
        sListener = listener;
        if (manager.findFragmentByTag(TAG) == null) {
            final Bundle bundle = new Bundle();
            bundle.putParcelableArrayList(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, deviceItems);
            bundle.putString(BUNDLE_KEY_NEW_DEVICE_NAME, newDeviceName);
            AudioSharingDisconnectDialogFragment dialog =
                    new AudioSharingDisconnectDialogFragment();
            dialog.setArguments(bundle);
            dialog.show(manager, TAG);
        }
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Bundle arguments = requireArguments();
        ArrayList<AudioSharingDeviceItem> deviceItems =
                arguments.getParcelableArrayList(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS);
        String newDeviceName = arguments.getString(BUNDLE_KEY_NEW_DEVICE_NAME);
        final AlertDialog.Builder builder =
                new AlertDialog.Builder(getActivity())
                        .setTitle("Choose headphone to disconnect")
                        .setCancelable(false);
        View rootView =
                LayoutInflater.from(builder.getContext())
                        .inflate(R.layout.dialog_audio_sharing_disconnect, /* parent= */ null);
        TextView subTitle = rootView.findViewById(R.id.share_audio_disconnect_description);
        subTitle.setText(
                "To share audio with " + newDeviceName + ", disconnect another pair of headphone");
        RecyclerView recyclerView = rootView.findViewById(R.id.device_btn_list);
        recyclerView.setAdapter(
                new AudioSharingDeviceAdapter(
                        deviceItems,
                        (AudioSharingDeviceItem item) -> {
                            sListener.onItemClick(item);
                            dismiss();
                        }));
        recyclerView.setLayoutManager(
                new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
        Button cancelBtn = rootView.findViewById(R.id.cancel_btn);
        cancelBtn.setOnClickListener(
                v -> {
                    dismiss();
                });
        AlertDialog dialog = builder.setView(rootView).create();
        dialog.setCanceledOnTouchOutside(false);
        return dialog;
    }
}