Loading src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java +52 −13 Original line number Diff line number Diff line Loading @@ -38,6 +38,26 @@ import java.util.ArrayList; public class AudioSharingDialogFragment extends InstrumentedDialogFragment { private static final String TAG = "AudioSharingDialog"; private static final String BUNDLE_KEY_DEVICE_NAMES = "bundle_key_device_names"; // 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 for sharing in the dialog. * * @param position The position of the item clicked. */ void onItemClick(int position); /** * Called when users click the cancel button in the dialog. */ void onCancelClick(); } private static DialogEventListener sListener; private View mRootView; @Override Loading @@ -50,40 +70,59 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { * * @param host The Fragment this dialog will be hosted. */ public static void show(Fragment host) { public static void show( Fragment host, ArrayList<String> deviceNames, DialogEventListener listener) { if (!Flags.enableLeAudioSharing()) return; final FragmentManager manager = host.getChildFragmentManager(); sListener = listener; if (manager.findFragmentByTag(TAG) == null) { final AudioSharingDialogFragment dialog = new AudioSharingDialogFragment(); final Bundle bundle = new Bundle(); bundle.putStringArrayList(BUNDLE_KEY_DEVICE_NAMES, deviceNames); AudioSharingDialogFragment dialog = new AudioSharingDialogFragment(); dialog.setArguments(bundle); dialog.show(manager, TAG); } } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Bundle arguments = requireArguments(); ArrayList<String> deviceNames = arguments.getStringArrayList(BUNDLE_KEY_DEVICE_NAMES); final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()).setTitle("Share audio"); new AlertDialog.Builder(getActivity()).setTitle("Share audio").setCancelable(false); mRootView = LayoutInflater.from(builder.getContext()) .inflate(R.layout.dialog_audio_sharing, /* parent= */ null); // TODO: use real subtitle according to device count. TextView subTitle1 = mRootView.findViewById(R.id.share_audio_subtitle1); TextView subTitle2 = mRootView.findViewById(R.id.share_audio_subtitle2); if (deviceNames.isEmpty()) { subTitle1.setVisibility(View.INVISIBLE); subTitle2.setText("To start sharing audio, connect 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"); } RecyclerView recyclerView = mRootView.findViewById(R.id.btn_list); // TODO: use real audio sharing device list. ArrayList<String> devices = new ArrayList<>(); devices.add("Buds 1"); devices.add("Buds 2"); recyclerView.setAdapter( new AudioSharingDeviceAdapter( devices, deviceNames, (int position) -> { // TODO: add on click callback. sListener.onItemClick(position); })); recyclerView.setLayoutManager( new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false)); return builder.setView(mRootView).create(); AlertDialog dialog = builder.setView(mRootView).create(); dialog.setCanceledOnTouchOutside(false); return dialog; } } src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java +133 −8 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.settings.connecteddevice.audiosharing; import android.bluetooth.BluetoothLeBroadcast; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.content.Context; import android.util.Log; import android.widget.Switch; Loading @@ -24,36 +26,116 @@ import androidx.annotation.NonNull; import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.LifecycleOwner; import com.android.settings.bluetooth.Utils; 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.LocalBluetoothLeBroadcast; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.widget.OnMainSwitchChangeListener; import java.util.ArrayList; import java.util.concurrent.Executor; import java.util.concurrent.Executors; public class AudioSharingSwitchBarController extends BasePreferenceController implements DefaultLifecycleObserver, OnMainSwitchChangeListener { private static final String TAG = "AudioSharingSwitchBarCtl"; private static final String PREF_KEY = "audio_sharing_main_switch"; private final Context mContext; private final SettingsMainSwitchBar mSwitchBar; private final LocalBluetoothManager mBtManager; private final LocalBluetoothLeBroadcast mBroadcast; private final Executor mExecutor; private DashboardFragment mFragment; private final BluetoothLeBroadcast.Callback mBroadcastCallback = new BluetoothLeBroadcast.Callback() { @Override public void onBroadcastStarted(int reason, int broadcastId) { Log.d( TAG, "onBroadcastStarted(), reason = " + reason + ", broadcastId = " + broadcastId); updateSwitch(); } @Override public void onBroadcastStartFailed(int reason) { Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason); // TODO: handle broadcast start fail updateSwitch(); } @Override public void onBroadcastMetadataChanged( int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) { Log.d( TAG, "onBroadcastMetadataChanged(), broadcastId = " + broadcastId + ", metadata = " + metadata); // TODO: handle add sink if there are connected lea devices. } @Override public void onBroadcastStopped(int reason, int broadcastId) { Log.d( TAG, "onBroadcastStopped(), reason = " + reason + ", broadcastId = " + broadcastId); updateSwitch(); } @Override public void onBroadcastStopFailed(int reason) { Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason); // TODO: handle broadcast stop fail updateSwitch(); } @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) {} }; AudioSharingSwitchBarController(Context context, SettingsMainSwitchBar switchBar) { super(context, PREF_KEY); mContext = context; mSwitchBar = switchBar; mSwitchBar.setChecked(false); mBtManager = Utils.getLocalBtManager(context); mBroadcast = mBtManager.getProfileManager().getLeAudioBroadcastProfile(); mExecutor = Executors.newSingleThreadExecutor(); mSwitchBar.setChecked(isBroadcasting()); } @Override public void onStart(@NonNull LifecycleOwner owner) { mSwitchBar.addOnSwitchChangeListener(this); if (mBroadcast != null) { mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback); } } @Override public void onStop(@NonNull LifecycleOwner owner) { mSwitchBar.removeOnSwitchChangeListener(this); if (mBroadcast != null) { mBroadcast.unregisterServiceCallBack(mBroadcastCallback); } } @Override Loading @@ -63,7 +145,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController if (isChecked) { startAudioSharing(); } else { // TODO: stop sharing stopAudioSharing(); } } Loading @@ -82,10 +164,53 @@ public class AudioSharingSwitchBarController extends BasePreferenceController } private void startAudioSharing() { if (mFragment != null) { AudioSharingDialogFragment.show(mFragment); } else { mSwitchBar.setEnabled(false); if (mBroadcast == null || isBroadcasting()) { Log.d(TAG, "Already in broadcasting or broadcast not support, ignore!"); mSwitchBar.setEnabled(true); return; } if (mFragment == null) { Log.w(TAG, "Dialog fail to show due to null fragment."); mSwitchBar.setEnabled(true); return; } ArrayList<String> deviceNames = new ArrayList<>(); AudioSharingDialogFragment.show( mFragment, deviceNames, new AudioSharingDialogFragment.DialogEventListener() { @Override public void onItemClick(int position) { // TODO: handle broadcast based on the dialog device item clicked } @Override public void onCancelClick() { mBroadcast.startBroadcast("test", /* language= */ null); } }); } private void stopAudioSharing() { mSwitchBar.setEnabled(false); if (mBroadcast == null || !isBroadcasting()) { Log.d(TAG, "Already not broadcasting or broadcast not support, ignore!"); mSwitchBar.setEnabled(true); return; } mBroadcast.stopBroadcast(mBroadcast.getLatestBroadcastId()); } private void updateSwitch() { ThreadUtils.postOnMainThread( () -> { mSwitchBar.setChecked(isBroadcasting()); mSwitchBar.setEnabled(true); }); } private boolean isBroadcasting() { return mBroadcast != null && mBroadcast.isEnabled(null); } } Loading
src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java +52 −13 Original line number Diff line number Diff line Loading @@ -38,6 +38,26 @@ import java.util.ArrayList; public class AudioSharingDialogFragment extends InstrumentedDialogFragment { private static final String TAG = "AudioSharingDialog"; private static final String BUNDLE_KEY_DEVICE_NAMES = "bundle_key_device_names"; // 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 for sharing in the dialog. * * @param position The position of the item clicked. */ void onItemClick(int position); /** * Called when users click the cancel button in the dialog. */ void onCancelClick(); } private static DialogEventListener sListener; private View mRootView; @Override Loading @@ -50,40 +70,59 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { * * @param host The Fragment this dialog will be hosted. */ public static void show(Fragment host) { public static void show( Fragment host, ArrayList<String> deviceNames, DialogEventListener listener) { if (!Flags.enableLeAudioSharing()) return; final FragmentManager manager = host.getChildFragmentManager(); sListener = listener; if (manager.findFragmentByTag(TAG) == null) { final AudioSharingDialogFragment dialog = new AudioSharingDialogFragment(); final Bundle bundle = new Bundle(); bundle.putStringArrayList(BUNDLE_KEY_DEVICE_NAMES, deviceNames); AudioSharingDialogFragment dialog = new AudioSharingDialogFragment(); dialog.setArguments(bundle); dialog.show(manager, TAG); } } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Bundle arguments = requireArguments(); ArrayList<String> deviceNames = arguments.getStringArrayList(BUNDLE_KEY_DEVICE_NAMES); final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()).setTitle("Share audio"); new AlertDialog.Builder(getActivity()).setTitle("Share audio").setCancelable(false); mRootView = LayoutInflater.from(builder.getContext()) .inflate(R.layout.dialog_audio_sharing, /* parent= */ null); // TODO: use real subtitle according to device count. TextView subTitle1 = mRootView.findViewById(R.id.share_audio_subtitle1); TextView subTitle2 = mRootView.findViewById(R.id.share_audio_subtitle2); if (deviceNames.isEmpty()) { subTitle1.setVisibility(View.INVISIBLE); subTitle2.setText("To start sharing audio, connect 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"); } RecyclerView recyclerView = mRootView.findViewById(R.id.btn_list); // TODO: use real audio sharing device list. ArrayList<String> devices = new ArrayList<>(); devices.add("Buds 1"); devices.add("Buds 2"); recyclerView.setAdapter( new AudioSharingDeviceAdapter( devices, deviceNames, (int position) -> { // TODO: add on click callback. sListener.onItemClick(position); })); recyclerView.setLayoutManager( new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false)); return builder.setView(mRootView).create(); AlertDialog dialog = builder.setView(mRootView).create(); dialog.setCanceledOnTouchOutside(false); return dialog; } }
src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java +133 −8 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.settings.connecteddevice.audiosharing; import android.bluetooth.BluetoothLeBroadcast; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.content.Context; import android.util.Log; import android.widget.Switch; Loading @@ -24,36 +26,116 @@ import androidx.annotation.NonNull; import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.LifecycleOwner; import com.android.settings.bluetooth.Utils; 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.LocalBluetoothLeBroadcast; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.widget.OnMainSwitchChangeListener; import java.util.ArrayList; import java.util.concurrent.Executor; import java.util.concurrent.Executors; public class AudioSharingSwitchBarController extends BasePreferenceController implements DefaultLifecycleObserver, OnMainSwitchChangeListener { private static final String TAG = "AudioSharingSwitchBarCtl"; private static final String PREF_KEY = "audio_sharing_main_switch"; private final Context mContext; private final SettingsMainSwitchBar mSwitchBar; private final LocalBluetoothManager mBtManager; private final LocalBluetoothLeBroadcast mBroadcast; private final Executor mExecutor; private DashboardFragment mFragment; private final BluetoothLeBroadcast.Callback mBroadcastCallback = new BluetoothLeBroadcast.Callback() { @Override public void onBroadcastStarted(int reason, int broadcastId) { Log.d( TAG, "onBroadcastStarted(), reason = " + reason + ", broadcastId = " + broadcastId); updateSwitch(); } @Override public void onBroadcastStartFailed(int reason) { Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason); // TODO: handle broadcast start fail updateSwitch(); } @Override public void onBroadcastMetadataChanged( int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) { Log.d( TAG, "onBroadcastMetadataChanged(), broadcastId = " + broadcastId + ", metadata = " + metadata); // TODO: handle add sink if there are connected lea devices. } @Override public void onBroadcastStopped(int reason, int broadcastId) { Log.d( TAG, "onBroadcastStopped(), reason = " + reason + ", broadcastId = " + broadcastId); updateSwitch(); } @Override public void onBroadcastStopFailed(int reason) { Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason); // TODO: handle broadcast stop fail updateSwitch(); } @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) {} }; AudioSharingSwitchBarController(Context context, SettingsMainSwitchBar switchBar) { super(context, PREF_KEY); mContext = context; mSwitchBar = switchBar; mSwitchBar.setChecked(false); mBtManager = Utils.getLocalBtManager(context); mBroadcast = mBtManager.getProfileManager().getLeAudioBroadcastProfile(); mExecutor = Executors.newSingleThreadExecutor(); mSwitchBar.setChecked(isBroadcasting()); } @Override public void onStart(@NonNull LifecycleOwner owner) { mSwitchBar.addOnSwitchChangeListener(this); if (mBroadcast != null) { mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback); } } @Override public void onStop(@NonNull LifecycleOwner owner) { mSwitchBar.removeOnSwitchChangeListener(this); if (mBroadcast != null) { mBroadcast.unregisterServiceCallBack(mBroadcastCallback); } } @Override Loading @@ -63,7 +145,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController if (isChecked) { startAudioSharing(); } else { // TODO: stop sharing stopAudioSharing(); } } Loading @@ -82,10 +164,53 @@ public class AudioSharingSwitchBarController extends BasePreferenceController } private void startAudioSharing() { if (mFragment != null) { AudioSharingDialogFragment.show(mFragment); } else { mSwitchBar.setEnabled(false); if (mBroadcast == null || isBroadcasting()) { Log.d(TAG, "Already in broadcasting or broadcast not support, ignore!"); mSwitchBar.setEnabled(true); return; } if (mFragment == null) { Log.w(TAG, "Dialog fail to show due to null fragment."); mSwitchBar.setEnabled(true); return; } ArrayList<String> deviceNames = new ArrayList<>(); AudioSharingDialogFragment.show( mFragment, deviceNames, new AudioSharingDialogFragment.DialogEventListener() { @Override public void onItemClick(int position) { // TODO: handle broadcast based on the dialog device item clicked } @Override public void onCancelClick() { mBroadcast.startBroadcast("test", /* language= */ null); } }); } private void stopAudioSharing() { mSwitchBar.setEnabled(false); if (mBroadcast == null || !isBroadcasting()) { Log.d(TAG, "Already not broadcasting or broadcast not support, ignore!"); mSwitchBar.setEnabled(true); return; } mBroadcast.stopBroadcast(mBroadcast.getLatestBroadcastId()); } private void updateSwitch() { ThreadUtils.postOnMainThread( () -> { mSwitchBar.setChecked(isBroadcasting()); mSwitchBar.setEnabled(true); }); } private boolean isBroadcasting() { return mBroadcast != null && mBroadcast.isEnabled(null); } }