Loading res/layout/dialog_audio_sharing_join.xml 0 → 100644 +53 −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_subtitle1" android:layout_width="match_parent" android:layout_height="wrap_content" android:textAlignment="center" android:layout_gravity="center"/> <TextView android:id="@+id/share_audio_subtitle2" android:layout_width="match_parent" android:layout_height="wrap_content" android:textAlignment="center" android:layout_gravity="center"/> <Button android:id="@+id/share_btn" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:text=""/> <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 src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceController.java +107 −16 Original line number Diff line number Diff line Loading @@ -74,24 +74,54 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro private Preference mAudioSharingSettingsPreference; private BluetoothDeviceUpdater mBluetoothDeviceUpdater; private DashboardFragment mFragment; private List<BluetoothDevice> mTargetSinks = new ArrayList<>(); private final BluetoothLeBroadcast.Callback mBroadcastCallback = new BluetoothLeBroadcast.Callback() { @Override public void onBroadcastStarted(int reason, int broadcastId) {} public void onBroadcastStarted(int reason, int broadcastId) { Log.d( TAG, "onBroadcastStarted(), reason = " + reason + ", broadcastId = " + broadcastId); } @Override public void onBroadcastStartFailed(int reason) {} public void onBroadcastStartFailed(int reason) { Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason); // TODO: handle broadcast start fail } @Override public void onBroadcastMetadataChanged( int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) {} int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) { Log.d( TAG, "onBroadcastMetadataChanged(), broadcastId = " + broadcastId + ", metadata = " + metadata); addSourceToTargetDevices(mTargetSinks); mTargetSinks = new ArrayList<>(); } @Override public void onBroadcastStopped(int reason, int broadcastId) {} public void onBroadcastStopped(int reason, int broadcastId) { Log.d( TAG, "onBroadcastStopped(), reason = " + reason + ", broadcastId = " + broadcastId); } @Override public void onBroadcastStopFailed(int reason) {} public void onBroadcastStopFailed(int reason) { Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason); // TODO: handle broadcast stop fail } @Override public void onBroadcastUpdated(int reason, int broadcastId) {} Loading Loading @@ -347,20 +377,20 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro } // Do nothing for ineligible (non LE audio) remote device when no sharing session. } else { Map<Integer, List<CachedBluetoothDevice>> groupedDevices = fetchConnectedDevicesByGroupId(); // 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 = ArrayList<AudioSharingDeviceItem> deviceItemsInSharingSession = buildDeviceItemsInSharingSession(groupedDevices); // Show switch audio sharing dialog when the third eligible (LE audio) remote device // Show audio sharing switch dialog when the third eligible (LE audio) remote device // connected during a sharing session. if (deviceItems.size() >= 2) { if (deviceItemsInSharingSession.size() >= 2) { AudioSharingDisconnectDialogFragment.show( mFragment, deviceItems, deviceItemsInSharingSession, cachedDevice.getName(), (AudioSharingDeviceItem item) -> { // Remove all sources from the device user clicked Loading @@ -379,11 +409,50 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro /* isGroupOp= */ true); }); } else { // TODO: show dialog to add device to sharing session. // Show audio sharing join dialog when the first or second eligible (LE audio) // remote device connected during a sharing session. AudioSharingJoinDialogFragment.show( mFragment, deviceItemsInSharingSession, cachedDevice.getName(), () -> { // Add current broadcast to the latest connected device mAssistant.addSource( cachedDevice.getDevice(), mBroadcast.getLatestBluetoothLeBroadcastMetadata(), /* isGroupOp= */ true); }); } } else { // Show audio sharing join dialog when no sharing session. // TODO: show dialog to add device to sharing session. ArrayList<AudioSharingDeviceItem> deviceItems = new ArrayList<>(); for (List<CachedBluetoothDevice> devices : groupedDevices.values()) { // Use random device in the group within the sharing session to // represent the group. CachedBluetoothDevice device = devices.get(0); if (device.getGroupId() == cachedDevice.getGroupId()) { continue; } deviceItems.add( new AudioSharingDeviceItem(device.getName(), device.getGroupId())); } // Show audio sharing join dialog when the second eligible (LE audio) remote device // connect and no sharing session. if (deviceItems.size() == 1) { AudioSharingJoinDialogFragment.show( mFragment, deviceItems, cachedDevice.getName(), () -> { mTargetSinks = new ArrayList<>(); for (List<CachedBluetoothDevice> devices : groupedDevices.values()) { for (CachedBluetoothDevice device : devices) { mTargetSinks.add(device.getDevice()); } } mBroadcast.startBroadcast("test", null); }); } } } } Loading Loading @@ -470,4 +539,26 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro } return deviceItems; } 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); } } } src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java 0 → 100644 +127 −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.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.flags.Flags; import java.util.ArrayList; import java.util.Locale; import java.util.stream.Collectors; public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment { private static final String TAG = "AudioSharingJoinDialog"; private static final String BUNDLE_KEY_DEVICE_ITEMS = "bundle_key_device_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 share audio button in the dialog. */ void onShareClick(); } private static DialogEventListener sListener; private View mRootView; @Override public int getMetricsCategory() { return SettingsEnums.DIALOG_START_AUDIO_SHARING; } /** * Display the {@link AudioSharingJoinDialogFragment} dialog. * * @param host The Fragment this dialog will be hosted. * @param deviceItems The existing connected device items eligible for audio sharing. * @param newDeviceName The name of the latest connected device triggered this dialog. * @param listener The callback to handle the user action on this dialog. */ 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_ITEMS, deviceItems); bundle.putString(BUNDLE_KEY_NEW_DEVICE_NAME, newDeviceName); final AudioSharingJoinDialogFragment dialog = new AudioSharingJoinDialogFragment(); dialog.setArguments(bundle); dialog.show(manager, TAG); } } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Bundle arguments = requireArguments(); ArrayList<AudioSharingDeviceItem> deviceItems = arguments.getParcelableArrayList(BUNDLE_KEY_DEVICE_ITEMS); String newDeviceName = arguments.getString(BUNDLE_KEY_NEW_DEVICE_NAME); final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) .setTitle("Share audio?") .setCancelable(false); mRootView = LayoutInflater.from(builder.getContext()) .inflate(R.layout.dialog_audio_sharing_join, null /* parent */); TextView subtitle1 = mRootView.findViewById(R.id.share_audio_subtitle1); TextView subtitle2 = mRootView.findViewById(R.id.share_audio_subtitle2); if (deviceItems.isEmpty()) { subtitle1.setText(newDeviceName); } else { subtitle1.setText( String.format( Locale.US, "%s and %s", deviceItems.stream() .map(AudioSharingDeviceItem::getName) .collect(Collectors.joining(", ")), newDeviceName)); } subtitle2.setText( "Connected eligible headphones will hear videos ad music playing on this phone"); Button shareBtn = mRootView.findViewById(R.id.share_btn); Button cancelBtn = mRootView.findViewById(R.id.cancel_btn); shareBtn.setOnClickListener( v -> { sListener.onShareClick(); dismiss(); }); shareBtn.setText("Share audio"); cancelBtn.setOnClickListener(v -> dismiss()); Dialog dialog = builder.setView(mRootView).create(); dialog.setCanceledOnTouchOutside(false); return dialog; } } Loading
res/layout/dialog_audio_sharing_join.xml 0 → 100644 +53 −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_subtitle1" android:layout_width="match_parent" android:layout_height="wrap_content" android:textAlignment="center" android:layout_gravity="center"/> <TextView android:id="@+id/share_audio_subtitle2" android:layout_width="match_parent" android:layout_height="wrap_content" android:textAlignment="center" android:layout_gravity="center"/> <Button android:id="@+id/share_btn" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:text=""/> <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
src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceController.java +107 −16 Original line number Diff line number Diff line Loading @@ -74,24 +74,54 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro private Preference mAudioSharingSettingsPreference; private BluetoothDeviceUpdater mBluetoothDeviceUpdater; private DashboardFragment mFragment; private List<BluetoothDevice> mTargetSinks = new ArrayList<>(); private final BluetoothLeBroadcast.Callback mBroadcastCallback = new BluetoothLeBroadcast.Callback() { @Override public void onBroadcastStarted(int reason, int broadcastId) {} public void onBroadcastStarted(int reason, int broadcastId) { Log.d( TAG, "onBroadcastStarted(), reason = " + reason + ", broadcastId = " + broadcastId); } @Override public void onBroadcastStartFailed(int reason) {} public void onBroadcastStartFailed(int reason) { Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason); // TODO: handle broadcast start fail } @Override public void onBroadcastMetadataChanged( int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) {} int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) { Log.d( TAG, "onBroadcastMetadataChanged(), broadcastId = " + broadcastId + ", metadata = " + metadata); addSourceToTargetDevices(mTargetSinks); mTargetSinks = new ArrayList<>(); } @Override public void onBroadcastStopped(int reason, int broadcastId) {} public void onBroadcastStopped(int reason, int broadcastId) { Log.d( TAG, "onBroadcastStopped(), reason = " + reason + ", broadcastId = " + broadcastId); } @Override public void onBroadcastStopFailed(int reason) {} public void onBroadcastStopFailed(int reason) { Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason); // TODO: handle broadcast stop fail } @Override public void onBroadcastUpdated(int reason, int broadcastId) {} Loading Loading @@ -347,20 +377,20 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro } // Do nothing for ineligible (non LE audio) remote device when no sharing session. } else { Map<Integer, List<CachedBluetoothDevice>> groupedDevices = fetchConnectedDevicesByGroupId(); // 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 = ArrayList<AudioSharingDeviceItem> deviceItemsInSharingSession = buildDeviceItemsInSharingSession(groupedDevices); // Show switch audio sharing dialog when the third eligible (LE audio) remote device // Show audio sharing switch dialog when the third eligible (LE audio) remote device // connected during a sharing session. if (deviceItems.size() >= 2) { if (deviceItemsInSharingSession.size() >= 2) { AudioSharingDisconnectDialogFragment.show( mFragment, deviceItems, deviceItemsInSharingSession, cachedDevice.getName(), (AudioSharingDeviceItem item) -> { // Remove all sources from the device user clicked Loading @@ -379,11 +409,50 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro /* isGroupOp= */ true); }); } else { // TODO: show dialog to add device to sharing session. // Show audio sharing join dialog when the first or second eligible (LE audio) // remote device connected during a sharing session. AudioSharingJoinDialogFragment.show( mFragment, deviceItemsInSharingSession, cachedDevice.getName(), () -> { // Add current broadcast to the latest connected device mAssistant.addSource( cachedDevice.getDevice(), mBroadcast.getLatestBluetoothLeBroadcastMetadata(), /* isGroupOp= */ true); }); } } else { // Show audio sharing join dialog when no sharing session. // TODO: show dialog to add device to sharing session. ArrayList<AudioSharingDeviceItem> deviceItems = new ArrayList<>(); for (List<CachedBluetoothDevice> devices : groupedDevices.values()) { // Use random device in the group within the sharing session to // represent the group. CachedBluetoothDevice device = devices.get(0); if (device.getGroupId() == cachedDevice.getGroupId()) { continue; } deviceItems.add( new AudioSharingDeviceItem(device.getName(), device.getGroupId())); } // Show audio sharing join dialog when the second eligible (LE audio) remote device // connect and no sharing session. if (deviceItems.size() == 1) { AudioSharingJoinDialogFragment.show( mFragment, deviceItems, cachedDevice.getName(), () -> { mTargetSinks = new ArrayList<>(); for (List<CachedBluetoothDevice> devices : groupedDevices.values()) { for (CachedBluetoothDevice device : devices) { mTargetSinks.add(device.getDevice()); } } mBroadcast.startBroadcast("test", null); }); } } } } Loading Loading @@ -470,4 +539,26 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro } return deviceItems; } 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); } } }
src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java 0 → 100644 +127 −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.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.flags.Flags; import java.util.ArrayList; import java.util.Locale; import java.util.stream.Collectors; public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment { private static final String TAG = "AudioSharingJoinDialog"; private static final String BUNDLE_KEY_DEVICE_ITEMS = "bundle_key_device_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 share audio button in the dialog. */ void onShareClick(); } private static DialogEventListener sListener; private View mRootView; @Override public int getMetricsCategory() { return SettingsEnums.DIALOG_START_AUDIO_SHARING; } /** * Display the {@link AudioSharingJoinDialogFragment} dialog. * * @param host The Fragment this dialog will be hosted. * @param deviceItems The existing connected device items eligible for audio sharing. * @param newDeviceName The name of the latest connected device triggered this dialog. * @param listener The callback to handle the user action on this dialog. */ 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_ITEMS, deviceItems); bundle.putString(BUNDLE_KEY_NEW_DEVICE_NAME, newDeviceName); final AudioSharingJoinDialogFragment dialog = new AudioSharingJoinDialogFragment(); dialog.setArguments(bundle); dialog.show(manager, TAG); } } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Bundle arguments = requireArguments(); ArrayList<AudioSharingDeviceItem> deviceItems = arguments.getParcelableArrayList(BUNDLE_KEY_DEVICE_ITEMS); String newDeviceName = arguments.getString(BUNDLE_KEY_NEW_DEVICE_NAME); final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) .setTitle("Share audio?") .setCancelable(false); mRootView = LayoutInflater.from(builder.getContext()) .inflate(R.layout.dialog_audio_sharing_join, null /* parent */); TextView subtitle1 = mRootView.findViewById(R.id.share_audio_subtitle1); TextView subtitle2 = mRootView.findViewById(R.id.share_audio_subtitle2); if (deviceItems.isEmpty()) { subtitle1.setText(newDeviceName); } else { subtitle1.setText( String.format( Locale.US, "%s and %s", deviceItems.stream() .map(AudioSharingDeviceItem::getName) .collect(Collectors.joining(", ")), newDeviceName)); } subtitle2.setText( "Connected eligible headphones will hear videos ad music playing on this phone"); Button shareBtn = mRootView.findViewById(R.id.share_btn); Button cancelBtn = mRootView.findViewById(R.id.cancel_btn); shareBtn.setOnClickListener( v -> { sListener.onShareClick(); dismiss(); }); shareBtn.setText("Share audio"); cancelBtn.setOnClickListener(v -> dismiss()); Dialog dialog = builder.setView(mRootView).create(); dialog.setCanceledOnTouchOutside(false); return dialog; } }