Loading src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java +7 −5 Original line number Diff line number Diff line Loading @@ -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; } Loading @@ -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"); } Loading @@ -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); } } src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceItem.java 0 → 100644 +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]; } }; } src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java +27 −21 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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(); } Loading @@ -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); Loading @@ -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 = Loading @@ -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)); Loading src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java +173 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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() { Loading Loading @@ -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 Loading Loading @@ -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()); } Loading @@ -128,6 +212,9 @@ public class AudioSharingSwitchBarController extends BasePreferenceController if (mBroadcast != null) { mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback); } if (mAssistant != null) { mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); } } @Override Loading @@ -136,6 +223,9 @@ public class AudioSharingSwitchBarController extends BasePreferenceController if (mBroadcast != null) { mBroadcast.unregisterServiceCallBack(mBroadcastCallback); } if (mAssistant != null) { mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); } } @Override Loading Loading @@ -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); } }); Loading @@ -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); } } } Loading
src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java +7 −5 Original line number Diff line number Diff line Loading @@ -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; } Loading @@ -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"); } Loading @@ -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); } }
src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceItem.java 0 → 100644 +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]; } }; }
src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java +27 −21 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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(); } Loading @@ -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); Loading @@ -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 = Loading @@ -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)); Loading
src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java +173 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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() { Loading Loading @@ -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 Loading Loading @@ -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()); } Loading @@ -128,6 +212,9 @@ public class AudioSharingSwitchBarController extends BasePreferenceController if (mBroadcast != null) { mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback); } if (mAssistant != null) { mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); } } @Override Loading @@ -136,6 +223,9 @@ public class AudioSharingSwitchBarController extends BasePreferenceController if (mBroadcast != null) { mBroadcast.unregisterServiceCallBack(mBroadcastCallback); } if (mAssistant != null) { mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); } } @Override Loading Loading @@ -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); } }); Loading @@ -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); } } }