• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.connecteddevice.audiosharing;
18 
19 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_START_LE_AUDIO_SHARING;
20 
21 import android.app.settings.SettingsEnums;
22 import android.bluetooth.BluetoothAdapter;
23 import android.bluetooth.BluetoothDevice;
24 import android.bluetooth.BluetoothLeBroadcast;
25 import android.bluetooth.BluetoothLeBroadcastAssistant;
26 import android.bluetooth.BluetoothLeBroadcastMetadata;
27 import android.bluetooth.BluetoothLeBroadcastReceiveState;
28 import android.content.BroadcastReceiver;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.IntentFilter;
32 import android.os.Bundle;
33 import android.util.FeatureFlagUtils;
34 import android.util.Log;
35 import android.util.Pair;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.view.accessibility.AccessibilityEvent;
39 import android.widget.CompoundButton;
40 import android.widget.CompoundButton.OnCheckedChangeListener;
41 
42 import androidx.annotation.NonNull;
43 import androidx.annotation.Nullable;
44 import androidx.annotation.UiThread;
45 import androidx.annotation.VisibleForTesting;
46 import androidx.fragment.app.DialogFragment;
47 import androidx.fragment.app.Fragment;
48 import androidx.lifecycle.DefaultLifecycleObserver;
49 import androidx.lifecycle.LifecycleOwner;
50 
51 import com.android.settings.R;
52 import com.android.settings.SettingsActivity;
53 import com.android.settings.bluetooth.Utils;
54 import com.android.settings.core.BasePreferenceController;
55 import com.android.settings.overlay.FeatureFactory;
56 import com.android.settings.widget.SettingsMainSwitchBar;
57 import com.android.settingslib.bluetooth.BluetoothCallback;
58 import com.android.settingslib.bluetooth.BluetoothEventManager;
59 import com.android.settingslib.bluetooth.BluetoothUtils;
60 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
61 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
62 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
63 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
64 import com.android.settingslib.bluetooth.LocalBluetoothManager;
65 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
66 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
67 import com.android.settingslib.utils.ThreadUtils;
68 
69 import com.google.common.collect.ImmutableList;
70 
71 import java.util.ArrayList;
72 import java.util.HashMap;
73 import java.util.List;
74 import java.util.Map;
75 import java.util.concurrent.CopyOnWriteArrayList;
76 import java.util.concurrent.Executor;
77 import java.util.concurrent.Executors;
78 import java.util.concurrent.atomic.AtomicBoolean;
79 import java.util.concurrent.atomic.AtomicInteger;
80 
81 public class AudioSharingSwitchBarController extends BasePreferenceController
82         implements DefaultLifecycleObserver,
83         OnCheckedChangeListener,
84         LocalBluetoothProfileManager.ServiceListener,
85         BluetoothCallback {
86     private static final String TAG = "AudioSharingSwitchCtlr";
87     private static final String PREF_KEY = "audio_sharing_main_switch";
88 
89     interface OnAudioSharingStateChangedListener {
90         /**
91          * The callback which will be triggered when:
92          *
93          * <p>1. Bluetooth on/off state changes. 2. Broadcast and assistant profile
94          * connect/disconnect state changes. 3. Audio sharing start/stop state changes.
95          */
onAudioSharingStateChanged()96         void onAudioSharingStateChanged();
97 
98         /**
99          * The callback which will be triggered when:
100          *
101          * <p>Broadcast and assistant profile connected.
102          */
onAudioSharingProfilesConnected()103         void onAudioSharingProfilesConnected();
104     }
105 
106     private final SettingsMainSwitchBar mSwitchBar;
107     private final BluetoothAdapter mBluetoothAdapter;
108     @Nullable private final LocalBluetoothManager mBtManager;
109     @Nullable private final BluetoothEventManager mEventManager;
110     @Nullable private final LocalBluetoothProfileManager mProfileManager;
111     @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
112     @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
113     @Nullable private Fragment mFragment;
114     private final Executor mExecutor;
115     private final MetricsFeatureProvider mMetricsFeatureProvider;
116     private final OnAudioSharingStateChangedListener mListener;
117     @VisibleForTesting IntentFilter mIntentFilter;
118     private Map<Integer, List<BluetoothDevice>> mGroupedConnectedDevices = new HashMap<>();
119     @Nullable private AudioSharingDeviceItem mTargetActiveItem;
120     private List<AudioSharingDeviceItem> mDeviceItemsForSharing = new ArrayList<>();
121     private final AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
122     private AtomicInteger mIntentHandleStage =
123             new AtomicInteger(StartIntentHandleStage.TO_HANDLE.ordinal());
124     // The sinks in adding source process. We show the progress dialog based on this list.
125     private CopyOnWriteArrayList<BluetoothDevice> mSinksInAdding = new CopyOnWriteArrayList<>();
126     // The primary/active sinks in adding source process.
127     // To avoid users advance to share then pair flow before the primary/active sinks successfully
128     // join the audio sharing, we will wait for the process complete for this list of sinks and then
129     // popup audio sharing dialog with options to pair new device.
130     private CopyOnWriteArrayList<BluetoothDevice> mSinksToWaitFor = new CopyOnWriteArrayList<>();
131     private AtomicBoolean mStartingSharing = new AtomicBoolean(false);
132     private AtomicBoolean mStoppingSharing = new AtomicBoolean(false);
133 
134     @VisibleForTesting
135     BroadcastReceiver mReceiver =
136             new BroadcastReceiver() {
137                 @Override
138                 public void onReceive(Context context, Intent intent) {
139                     if (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
140                             == BluetoothAdapter.STATE_ON
141                             && !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
142                         if (mProfileManager != null) {
143                             mProfileManager.addServiceListener(
144                                     AudioSharingSwitchBarController.this);
145                         }
146                     } else {
147                         updateSwitch();
148                         mListener.onAudioSharingStateChanged();
149                     }
150                 }
151             };
152 
153     @VisibleForTesting
154     final BluetoothLeBroadcast.Callback mBroadcastCallback =
155             new BluetoothLeBroadcast.Callback() {
156                 @Override
157                 public void onBroadcastStarted(int reason, int broadcastId) {
158                     Log.d(
159                             TAG,
160                             "onBroadcastStarted(), reason = "
161                                     + reason
162                                     + ", broadcastId = "
163                                     + broadcastId);
164                     updateSwitch();
165                     AudioSharingUtils.toastMessage(
166                             mContext, mContext.getString(R.string.audio_sharing_sharing_label));
167                     mListener.onAudioSharingStateChanged();
168                 }
169 
170                 @Override
171                 public void onBroadcastStartFailed(int reason) {
172                     Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason);
173                     mStartingSharing.compareAndSet(true, false);
174                     updateSwitch();
175                     showErrorDialog();
176                     mMetricsFeatureProvider.action(
177                             mContext,
178                             SettingsEnums.ACTION_AUDIO_SHARING_START_FAILED,
179                             SettingsEnums.AUDIO_SHARING_SETTINGS);
180                 }
181 
182                 @Override
183                 public void onBroadcastMetadataChanged(
184                         int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) {
185                     Log.d(
186                             TAG,
187                             "onBroadcastMetadataChanged(), broadcastId = "
188                                     + broadcastId
189                                     + ", metadata = "
190                                     + metadata.getBroadcastName());
191                     if (!mStartingSharing.compareAndSet(true, false)) {
192                         Log.d(TAG, "Skip handleOnBroadcastReady, not in starting process");
193                         return;
194                     }
195                     handleOnBroadcastReady(metadata);
196                 }
197 
198                 @Override
199                 public void onBroadcastStopped(int reason, int broadcastId) {
200                     Log.d(
201                             TAG,
202                             "onBroadcastStopped(), reason = "
203                                     + reason
204                                     + ", broadcastId = "
205                                     + broadcastId);
206                     mStoppingSharing.compareAndSet(true, false);
207                     updateSwitch();
208                     AudioSharingUtils.postOnMainThread(mContext,
209                             () -> dismissStaleDialogsOtherThanErrorDialog());
210                     AudioSharingUtils.toastMessage(
211                             mContext,
212                             mContext.getString(R.string.audio_sharing_sharing_stopped_label));
213                     mListener.onAudioSharingStateChanged();
214                 }
215 
216                 @Override
217                 public void onBroadcastStopFailed(int reason) {
218                     Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason);
219                     mStoppingSharing.compareAndSet(true, false);
220                     updateSwitch();
221                     mMetricsFeatureProvider.action(
222                             mContext,
223                             SettingsEnums.ACTION_AUDIO_SHARING_STOP_FAILED,
224                             SettingsEnums.AUDIO_SHARING_SETTINGS);
225                 }
226 
227                 @Override
228                 public void onBroadcastUpdated(int reason, int broadcastId) {}
229 
230                 @Override
231                 public void onBroadcastUpdateFailed(int reason, int broadcastId) {}
232 
233                 @Override
234                 public void onPlaybackStarted(int reason, int broadcastId) {
235                     Log.d(
236                             TAG,
237                             "onPlaybackStarted(), reason = "
238                                     + reason
239                                     + ", broadcastId = "
240                                     + broadcastId);
241                 }
242 
243                 @Override
244                 public void onPlaybackStopped(int reason, int broadcastId) {}
245             };
246 
247     @VisibleForTesting
248     final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
249             new BluetoothLeBroadcastAssistant.Callback() {
250                 @Override
251                 public void onSearchStarted(int reason) {}
252 
253                 @Override
254                 public void onSearchStartFailed(int reason) {}
255 
256                 @Override
257                 public void onSearchStopped(int reason) {}
258 
259                 @Override
260                 public void onSearchStopFailed(int reason) {}
261 
262                 @Override
263                 public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
264 
265                 @Override
266                 public void onSourceAdded(
267                         @NonNull BluetoothDevice sink, int sourceId, int reason) {
268                     if (mSinksInAdding.contains(sink)) {
269                         mSinksInAdding.remove(sink);
270                     }
271                     dismissProgressDialogIfNeeded();
272                     Log.d(TAG, "onSourceAdded(), sink = " + sink + ", remaining sinks = "
273                             + mSinksInAdding);
274                     if (mSinksToWaitFor.contains(sink)) {
275                         mSinksToWaitFor.remove(sink);
276                         if (mSinksToWaitFor.isEmpty() && mBroadcast != null) {
277                             // To avoid users advance to share then pair flow before the
278                             // primary/active sinks successfully join the audio sharing,
279                             // popup dialog till adding source complete for mSinksToWaitFor.
280                             Pair<Integer, Object>[] eventData =
281                                     AudioSharingUtils.buildAudioSharingDialogEventData(
282                                             SettingsEnums.AUDIO_SHARING_SETTINGS,
283                                             SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE,
284                                             /* userTriggered= */ false,
285                                             /* deviceCountInSharing= */ 1,
286                                             /* candidateDeviceCount= */ 0);
287                             showJoinAudioSharingDialog(eventData,
288                                     mBroadcast.getLatestBluetoothLeBroadcastMetadata());
289                         }
290                     }
291                 }
292 
293                 @Override
294                 public void onSourceAddFailed(
295                         @NonNull BluetoothDevice sink,
296                         @NonNull BluetoothLeBroadcastMetadata source,
297                         int reason) {
298                     Log.d(
299                             TAG,
300                             "onSourceAddFailed(), sink = "
301                                     + sink
302                                     + ", source = "
303                                     + source
304                                     + ", reason = "
305                                     + reason);
306                     if (mSinksInAdding.contains(sink)) {
307                         stopAudioSharing();
308                         showErrorDialog();
309                         mMetricsFeatureProvider.action(
310                                 mContext,
311                                 SettingsEnums.ACTION_AUDIO_SHARING_JOIN_FAILED,
312                                 SettingsEnums.AUDIO_SHARING_SETTINGS);
313                     }
314                 }
315 
316                 @Override
317                 public void onSourceModified(
318                         @NonNull BluetoothDevice sink, int sourceId, int reason) {}
319 
320                 @Override
321                 public void onSourceModifyFailed(
322                         @NonNull BluetoothDevice sink, int sourceId, int reason) {}
323 
324                 @Override
325                 public void onSourceRemoved(
326                         @NonNull BluetoothDevice sink, int sourceId, int reason) {}
327 
328                 @Override
329                 public void onSourceRemoveFailed(
330                         @NonNull BluetoothDevice sink, int sourceId, int reason) {}
331 
332                 @Override
333                 public void onReceiveStateChanged(
334                         @NonNull BluetoothDevice sink,
335                         int sourceId,
336                         @NonNull BluetoothLeBroadcastReceiveState state) {
337                     Log.d(TAG,
338                             "onReceiveStateChanged(), sink = " + sink + ", sourceId = " + sourceId
339                                     + ", state = " + state);
340                 }
341             };
342 
AudioSharingSwitchBarController( Context context, SettingsMainSwitchBar switchBar, OnAudioSharingStateChangedListener listener)343     AudioSharingSwitchBarController(
344             Context context,
345             SettingsMainSwitchBar switchBar,
346             OnAudioSharingStateChangedListener listener) {
347         super(context, PREF_KEY);
348         mSwitchBar = switchBar;
349         mListener = listener;
350         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
351         mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
352         mBtManager = Utils.getLocalBtManager(context);
353         mEventManager = mBtManager == null ? null : mBtManager.getEventManager();
354         mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
355         mBroadcast = mProfileManager == null ? null : mProfileManager.getLeAudioBroadcastProfile();
356         mAssistant =
357                 mProfileManager == null
358                         ? null
359                         : mProfileManager.getLeAudioBroadcastAssistantProfile();
360         mExecutor = Executors.newSingleThreadExecutor();
361         mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
362         mSwitchBar.getRootView().setAccessibilityDelegate(new MainSwitchAccessibilityDelegate());
363     }
364 
365     @Override
onStart(@onNull LifecycleOwner owner)366     public void onStart(@NonNull LifecycleOwner owner) {
367         if (!isAvailable()) {
368             Log.d(TAG, "Skip register callbacks. Feature is not available.");
369             return;
370         }
371         mContext.registerReceiver(mReceiver, mIntentFilter, Context.RECEIVER_EXPORTED_UNAUDITED);
372         updateSwitch();
373         registerCallbacks();
374         if (!AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
375             if (mProfileManager != null) {
376                 mProfileManager.addServiceListener(this);
377             }
378             Log.d(TAG, "Skip handleStartAudioSharingFromIntent. Profile is not ready.");
379             return;
380         }
381         if (mIntentHandleStage.compareAndSet(
382                 StartIntentHandleStage.TO_HANDLE.ordinal(),
383                 StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal())) {
384             Log.d(TAG, "onStart: handleStartAudioSharingFromIntent");
385             handleStartAudioSharingFromIntent();
386         }
387     }
388 
389     @Override
onStop(@onNull LifecycleOwner owner)390     public void onStop(@NonNull LifecycleOwner owner) {
391         if (!isAvailable()) {
392             Log.d(TAG, "Skip unregister callbacks. Feature is not available.");
393             return;
394         }
395         mContext.unregisterReceiver(mReceiver);
396         if (mProfileManager != null) {
397             mProfileManager.removeServiceListener(this);
398         }
399         unregisterCallbacks();
400     }
401 
402     @Override
onCheckedChanged(CompoundButton buttonView, boolean isChecked)403     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
404         // Filter out unnecessary callbacks when switch is disabled.
405         if (!buttonView.isEnabled()) return;
406         if (mBroadcast == null || mAssistant == null) {
407             mSwitchBar.setChecked(false);
408             Log.d(TAG, "Skip onCheckedChanged, profile not support.");
409             return;
410         }
411         mSwitchBar.setEnabled(false);
412         boolean isBroadcasting = BluetoothUtils.isBroadcasting(mBtManager);
413         if (isChecked) {
414             if (isBroadcasting) {
415                 Log.d(TAG, "Skip startAudioSharing, already broadcasting.");
416                 mSwitchBar.setEnabled(true);
417                 return;
418             }
419             // FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST is always true in
420             // prod. We can turn off the flag for debug purpose.
421             if (FeatureFlagUtils.isEnabled(
422                     mContext,
423                     FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST)
424                     && hasEmptyConnectedSink()) {
425                 // Pop up dialog to ask users to connect at least one lea buds before audio sharing.
426                 AudioSharingUtils.postOnMainThread(
427                         mContext,
428                         () -> {
429                             mSwitchBar.setEnabled(true);
430                             mSwitchBar.setChecked(false);
431                             AudioSharingConfirmDialogFragment.show(mFragment);
432                         });
433                 return;
434             }
435             startAudioSharing();
436         } else {
437             if (!isBroadcasting) {
438                 Log.d(TAG, "Skip stopAudioSharing, already not broadcasting.");
439                 mSwitchBar.setEnabled(true);
440                 return;
441             }
442             stopAudioSharing();
443             mMetricsFeatureProvider.action(
444                     mContext, SettingsEnums.ACTION_AUDIO_SHARING_MAIN_SWITCH_OFF);
445         }
446     }
447 
448     @Override
getAvailabilityStatus()449     public int getAvailabilityStatus() {
450         return BluetoothUtils.isAudioSharingUIAvailable(mContext) ? AVAILABLE
451                 : UNSUPPORTED_ON_DEVICE;
452     }
453 
454     @Override
onServiceConnected()455     public void onServiceConnected() {
456         Log.d(TAG, "onServiceConnected()");
457         if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
458             updateSwitch();
459             mListener.onAudioSharingProfilesConnected();
460             mListener.onAudioSharingStateChanged();
461             if (mProfileManager != null) {
462                 mProfileManager.removeServiceListener(this);
463             }
464             if (mIntentHandleStage.compareAndSet(
465                     StartIntentHandleStage.TO_HANDLE.ordinal(),
466                     StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal())) {
467                 Log.d(TAG, "onServiceConnected: handleStartAudioSharingFromIntent");
468                 handleStartAudioSharingFromIntent();
469             }
470         }
471     }
472 
473     @Override
onServiceDisconnected()474     public void onServiceDisconnected() {
475         Log.d(TAG, "onServiceDisconnected()");
476         // Do nothing.
477     }
478 
479     @Override
onActiveDeviceChanged(@ullable CachedBluetoothDevice activeDevice, int bluetoothProfile)480     public void onActiveDeviceChanged(@Nullable CachedBluetoothDevice activeDevice,
481             int bluetoothProfile) {
482         if (activeDevice != null) {
483             Log.d(TAG, "onActiveDeviceChanged: device = "
484                     + activeDevice.getDevice().getAnonymizedAddress()
485                     + ", profile = " + bluetoothProfile);
486             updateSwitch();
487         }
488     }
489 
490     /**
491      * Initialize the controller.
492      *
493      * @param fragment The fragment to host the {@link AudioSharingSwitchBarController} dialog.
494      */
init(@onNull Fragment fragment)495     public void init(@NonNull Fragment fragment) {
496         this.mFragment = fragment;
497     }
498 
499     /** Handle auto add source to the just paired device in share then pair flow. */
handleAutoAddSourceAfterPair(@onNull BluetoothDevice device)500     public void handleAutoAddSourceAfterPair(@NonNull BluetoothDevice device) {
501         CachedBluetoothDeviceManager deviceManager =
502                 mBtManager == null ? null : mBtManager.getCachedDeviceManager();
503         CachedBluetoothDevice cachedDevice =
504                 deviceManager == null ? null : deviceManager.findDevice(device);
505         if (cachedDevice != null && mBroadcast != null) {
506             Log.d(TAG, "handleAutoAddSourceAfterPair, device = " + device.getAnonymizedAddress());
507             addSourceToTargetSinks(ImmutableList.of(device), cachedDevice.getName(),
508                     mBroadcast.getLatestBluetoothLeBroadcastMetadata());
509         }
510     }
511 
512     /** Test only: set callback registration status in tests. */
513     @VisibleForTesting
setCallbacksRegistered(boolean registered)514     void setCallbacksRegistered(boolean registered) {
515         mCallbacksRegistered.set(registered);
516     }
517 
registerCallbacks()518     private void registerCallbacks() {
519         if (!isAvailable()) {
520             Log.d(TAG, "Skip registerCallbacks(). Feature is not available.");
521             return;
522         }
523         if (mBroadcast == null || mAssistant == null || mEventManager == null) {
524             Log.d(TAG, "Skip registerCallbacks(). Profile not support on this device.");
525             return;
526         }
527         if (!mCallbacksRegistered.get()) {
528             Log.d(TAG, "registerCallbacks()");
529             mSwitchBar.addOnSwitchChangeListener(this);
530             mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
531             mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
532             mEventManager.registerCallback(this);
533             mCallbacksRegistered.set(true);
534         }
535     }
536 
unregisterCallbacks()537     private void unregisterCallbacks() {
538         if (!isAvailable()) {
539             Log.d(TAG, "Skip unregisterCallbacks(). Feature is not available.");
540             return;
541         }
542         if (mBroadcast == null || mAssistant == null || mEventManager == null) {
543             Log.d(TAG, "Skip unregisterCallbacks(). Profile not support on this device.");
544             return;
545         }
546         if (mCallbacksRegistered.get()) {
547             Log.d(TAG, "unregisterCallbacks()");
548             mSwitchBar.removeOnSwitchChangeListener(this);
549             mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
550             mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
551             mEventManager.unregisterCallback(this);
552             mCallbacksRegistered.set(false);
553         }
554     }
555 
startAudioSharing()556     private void startAudioSharing() {
557         // Compute the device connection state before start audio sharing since the devices will
558         // be set to inactive after the broadcast started.
559         mGroupedConnectedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(mBtManager);
560         List<AudioSharingDeviceItem> deviceItems =
561                 AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
562                         mBtManager, mGroupedConnectedDevices, /* filterByInSharing= */ false);
563         // deviceItems is ordered. The active device is the first place if exits.
564         mDeviceItemsForSharing = new ArrayList<>(deviceItems);
565         mTargetActiveItem = null;
566         if (!deviceItems.isEmpty() && deviceItems.get(0).isActive()) {
567             // If active device exists for audio sharing, share to it
568             // automatically once the broadcast is started.
569             mTargetActiveItem = deviceItems.get(0);
570             mDeviceItemsForSharing.remove(0);
571         }
572         if (mBroadcast != null) {
573             mStartingSharing.set(true);
574             mBroadcast.startPrivateBroadcast();
575             mSinksInAdding.clear();
576             AudioSharingUtils.postOnMainThread(mContext,
577                     () -> AudioSharingProgressDialogFragment.show(mFragment,
578                             mContext.getString(
579                                     R.string.audio_sharing_progress_dialog_start_stream_content)));
580             mMetricsFeatureProvider.action(
581                     mContext,
582                     SettingsEnums.ACTION_AUDIO_SHARING_MAIN_SWITCH_ON,
583                     deviceItems.size());
584         }
585     }
586 
stopAudioSharing()587     private void stopAudioSharing() {
588         if (mBroadcast != null) {
589             int broadcastId = mBroadcast.getLatestBroadcastId();
590             if (broadcastId != -1) {
591                 mBroadcast.stopBroadcast(broadcastId);
592                 mStoppingSharing.set(true);
593                 mSinksInAdding.clear();
594                 mSinksToWaitFor.clear();
595             }
596             cleanUpStatesForStartSharing();
597         }
598     }
599 
updateSwitch()600     private void updateSwitch() {
601         var unused =
602                 ThreadUtils.postOnBackgroundThread(
603                         () -> {
604                             boolean isBroadcasting = BluetoothUtils.isBroadcasting(mBtManager);
605                             boolean hasActiveDevice =
606                                     AudioSharingUtils.hasActiveConnectedLeadDevice(mBtManager);
607                             boolean hasEmptyConnectedDevice = hasEmptyConnectedSink();
608                             boolean isStateReady =
609                                     isBluetoothOn()
610                                             && AudioSharingUtils.isAudioSharingProfileReady(
611                                             mProfileManager)
612                                             && (isBroadcasting
613                                             // Always enable toggle when no connected sink. We have
614                                             // dialog to guide users to connect compatible devices
615                                             // for audio sharing.
616                                             || hasEmptyConnectedDevice
617                                             // Disable toggle till device gets active after
618                                             // broadcast ends.
619                                             || hasActiveDevice);
620                             AudioSharingUtils.postOnMainThread(
621                                     mContext,
622                                     () -> {
623                                         if (mSwitchBar.isChecked() != isBroadcasting) {
624                                             mSwitchBar.setChecked(isBroadcasting);
625                                         }
626                                         if (mSwitchBar.isEnabled() != isStateReady) {
627                                             mSwitchBar.setEnabled(isStateReady);
628                                         }
629                                         Log.d(
630                                                 TAG,
631                                                 "updateSwitch, checked = "
632                                                         + isBroadcasting
633                                                         + ", enabled = "
634                                                         + isStateReady);
635                                     });
636                         });
637     }
638 
isBluetoothOn()639     private boolean isBluetoothOn() {
640         return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled();
641     }
642 
hasEmptyConnectedSink()643     private boolean hasEmptyConnectedSink() {
644         return mAssistant != null && mAssistant.getAllConnectedDevices().isEmpty();
645     }
646 
handleOnBroadcastReady(@onNull BluetoothLeBroadcastMetadata metadata)647     private void handleOnBroadcastReady(@NonNull BluetoothLeBroadcastMetadata metadata) {
648         List<BluetoothDevice> targetActiveSinks = mTargetActiveItem == null ? ImmutableList.of()
649                 : mGroupedConnectedDevices.getOrDefault(
650                         mTargetActiveItem.getGroupId(), ImmutableList.of());
651         Pair<Integer, Object>[] eventData =
652                 AudioSharingUtils.buildAudioSharingDialogEventData(
653                         SettingsEnums.AUDIO_SHARING_SETTINGS,
654                         SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE,
655                         /* userTriggered= */ false,
656                         /* deviceCountInSharing= */ targetActiveSinks.isEmpty() ? 0 : 1,
657                         /* candidateDeviceCount= */ mDeviceItemsForSharing.size());
658         // Auto add primary/active sinks w/o user interactions.
659         if (!targetActiveSinks.isEmpty() && mTargetActiveItem != null) {
660             Log.d(TAG, "handleOnBroadcastReady: automatically add source to active sinks.");
661             addSourceToTargetSinks(targetActiveSinks, mTargetActiveItem.getName(), metadata);
662             // To avoid users advance to share then pair flow before the primary/active sinks
663             // successfully join the audio sharing, save the primary/active sinks in mSinksToWaitFor
664             // and popup dialog till adding source complete for these sinks.
665             if (mDeviceItemsForSharing.isEmpty()) {
666                 mSinksToWaitFor.clear();
667                 mSinksToWaitFor.addAll(targetActiveSinks);
668             }
669             mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_AUTO_JOIN_AUDIO_SHARING);
670             mTargetActiveItem = null;
671             // When audio sharing page is brought up by intent with EXTRA_START_LE_AUDIO_SHARING
672             // == true, plus there is one active lea headset and one connected lea headset, we
673             // should auto add these sinks without user interactions.
674             if (mIntentHandleStage.compareAndSet(
675                     StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(),
676                     StartIntentHandleStage.HANDLED.ordinal())
677                     && mDeviceItemsForSharing.size() == 1) {
678                 Log.d(TAG, "handleOnBroadcastReady: auto add source to the second device");
679                 AudioSharingDeviceItem target = mDeviceItemsForSharing.get(0);
680                 List<BluetoothDevice> targetSinks = mGroupedConnectedDevices.getOrDefault(
681                         target.getGroupId(), ImmutableList.of());
682                 addSourceToTargetSinks(targetSinks, target.getName(), metadata);
683                 cleanUpStatesForStartSharing();
684                 // TODO: Add metric for auto add by intent
685                 return;
686             }
687         }
688         // Still mark intent as handled if early returned due to preconditions not met
689         mIntentHandleStage.compareAndSet(
690                 StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(),
691                 StartIntentHandleStage.HANDLED.ordinal());
692         if (mFragment == null) {
693             Log.d(TAG, "handleOnBroadcastReady: dialog fail to show due to null fragment.");
694             // Clean up states before early return.
695             dismissProgressDialogIfNeeded();
696             cleanUpStatesForStartSharing();
697             return;
698         }
699         // To avoid users advance to share then pair flow before the primary/active sinks
700         // successfully join the audio sharing, popup dialog till adding source complete for
701         // mSinksToWaitFor.
702         if (mSinksToWaitFor.isEmpty() && !mStoppingSharing.get()) {
703             showJoinAudioSharingDialog(eventData, metadata);
704         }
705     }
706 
showJoinAudioSharingDialog(Pair<Integer, Object>[] eventData, @Nullable BluetoothLeBroadcastMetadata metadata)707     private void showJoinAudioSharingDialog(Pair<Integer, Object>[] eventData,
708             @Nullable BluetoothLeBroadcastMetadata metadata) {
709         if (!BluetoothUtils.isBroadcasting(mBtManager)) {
710             Log.d(TAG, "Skip showJoinAudioSharingDialog, broadcast is stopped");
711             return;
712         }
713         AudioSharingDialogFragment.DialogEventListener listener =
714                 new AudioSharingDialogFragment.DialogEventListener() {
715                     @Override
716                     public void onPositiveClick() {
717                         // Could go to other pages (pair new device), dismiss the progress dialog.
718                         dismissProgressDialogIfNeeded();
719                         cleanUpStatesForStartSharing();
720                     }
721 
722                     @Override
723                     public void onItemClick(@NonNull AudioSharingDeviceItem item) {
724                         List<BluetoothDevice> targetSinks = mGroupedConnectedDevices.getOrDefault(
725                                 item.getGroupId(), ImmutableList.of());
726                         addSourceToTargetSinks(targetSinks, item.getName(), metadata);
727                         cleanUpStatesForStartSharing();
728                     }
729 
730                     @Override
731                     public void onCancelClick() {
732                         // Could go to other pages (show qr code), dismiss the progress dialog.
733                         dismissProgressDialogIfNeeded();
734                         cleanUpStatesForStartSharing();
735                     }
736                 };
737         AudioSharingUtils.postOnMainThread(
738                 mContext,
739                 () -> AudioSharingDialogFragment.show(
740                         mFragment,
741                         mDeviceItemsForSharing,
742                         metadata,
743                         listener,
744                         eventData));
745     }
746 
showErrorDialog()747     private void showErrorDialog() {
748         AudioSharingUtils.postOnMainThread(mContext,
749                 () -> {
750                     // Remove all stale dialogs before showing error dialog
751                     dismissStaleDialogsOtherThanErrorDialog();
752                     AudioSharingErrorDialogFragment.show(mFragment);
753                 });
754     }
755 
756     @UiThread
dismissStaleDialogsOtherThanErrorDialog()757     private void dismissStaleDialogsOtherThanErrorDialog() {
758         List<Fragment> fragments = new ArrayList<Fragment>();
759         try {
760             if (mFragment != null) {
761                 fragments =
762                         mFragment.getChildFragmentManager().getFragments();
763             }
764         } catch (Exception e) {
765             Log.e(TAG, "Fail to dismiss stale dialogs: " + e.getMessage());
766         }
767         for (Fragment fragment : fragments) {
768             if (fragment != null && fragment instanceof DialogFragment
769                     && !(fragment instanceof AudioSharingErrorDialogFragment)
770                     && ((DialogFragment) fragment).getDialog() != null) {
771                 Log.d(TAG, "Remove stale dialog = " + fragment.getTag());
772                 ((DialogFragment) fragment).dismissAllowingStateLoss();
773             }
774         }
775     }
776 
777     private static final class MainSwitchAccessibilityDelegate extends View.AccessibilityDelegate {
778         @Override
onRequestSendAccessibilityEvent( @onNull ViewGroup host, @NonNull View view, @NonNull AccessibilityEvent event)779         public boolean onRequestSendAccessibilityEvent(
780                 @NonNull ViewGroup host, @NonNull View view, @NonNull AccessibilityEvent event) {
781             if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
782                     && (event.getContentChangeTypes()
783                     & AccessibilityEvent.CONTENT_CHANGE_TYPE_ENABLED)
784                     != 0) {
785                 Log.d(TAG, "Skip accessibility event for CONTENT_CHANGE_TYPE_ENABLED");
786                 return false;
787             }
788             return super.onRequestSendAccessibilityEvent(host, view, event);
789         }
790     }
791 
handleStartAudioSharingFromIntent()792     private void handleStartAudioSharingFromIntent() {
793         var unused =
794                 ThreadUtils.postOnBackgroundThread(
795                         () -> {
796                             if (mFragment == null
797                                     || mFragment.getActivity() == null
798                                     || mFragment.getActivity().getIntent() == null) {
799                                 Log.d(
800                                         TAG,
801                                         "Skip handleStartAudioSharingFromIntent, "
802                                                 + "fragment intent is null");
803                                 return;
804                             }
805                             Intent intent = mFragment.getActivity().getIntent();
806                             Bundle args =
807                                     intent.getBundleExtra(
808                                             SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
809                             Boolean shouldStart =
810                                     args != null
811                                             && args.getBoolean(EXTRA_START_LE_AUDIO_SHARING, false);
812                             if (!shouldStart) {
813                                 Log.d(TAG, "Skip handleStartAudioSharingFromIntent, arg false");
814                                 mIntentHandleStage.compareAndSet(
815                                         StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(),
816                                         StartIntentHandleStage.HANDLED.ordinal());
817                                 return;
818                             }
819                             if (BluetoothUtils.isBroadcasting(mBtManager)) {
820                                 Log.d(TAG, "Skip handleStartAudioSharingFromIntent, in broadcast");
821                                 mIntentHandleStage.compareAndSet(
822                                         StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(),
823                                         StartIntentHandleStage.HANDLED.ordinal());
824                                 return;
825                             }
826                             Log.d(TAG, "HandleStartAudioSharingFromIntent, start broadcast");
827                             AudioSharingUtils.postOnMainThread(
828                                     mContext, () -> mSwitchBar.setChecked(true));
829                         });
830     }
831 
addSourceToTargetSinks(List<BluetoothDevice> targetGroupedSinks, @NonNull String targetSinkName, @Nullable BluetoothLeBroadcastMetadata metadata)832     private void addSourceToTargetSinks(List<BluetoothDevice> targetGroupedSinks,
833             @NonNull String targetSinkName, @Nullable BluetoothLeBroadcastMetadata metadata) {
834         if (targetGroupedSinks.isEmpty()) {
835             Log.d(TAG, "Skip addSourceToTargetSinks, no sinks.");
836             return;
837         }
838         if (metadata == null) {
839             Log.d(TAG, "Skip addSourceToTargetSinks, metadata is null");
840             return;
841         }
842         if (mAssistant == null) {
843             Log.d(TAG, "skip addSourceToTargetDevices, assistant profile is null.");
844             return;
845         }
846         mSinksInAdding.addAll(targetGroupedSinks);
847         String progressMessage = mContext.getString(
848                 R.string.audio_sharing_progress_dialog_add_source_content, targetSinkName);
849         showProgressDialog(progressMessage);
850         for (BluetoothDevice sink : targetGroupedSinks) {
851             mAssistant.addSource(sink, metadata, /* isGroupOp= */ false);
852         }
853     }
854 
showProgressDialog(@onNull String progressMessage)855     private void showProgressDialog(@NonNull String progressMessage) {
856         AudioSharingUtils.postOnMainThread(mContext,
857                 () -> AudioSharingProgressDialogFragment.show(mFragment, progressMessage));
858     }
859 
dismissProgressDialogIfNeeded()860     private void dismissProgressDialogIfNeeded() {
861         if (mSinksInAdding.isEmpty()) {
862             AudioSharingUtils.postOnMainThread(mContext,
863                     () -> AudioSharingProgressDialogFragment.dismiss(mFragment));
864         }
865     }
866 
cleanUpStatesForStartSharing()867     private void cleanUpStatesForStartSharing() {
868         mGroupedConnectedDevices.clear();
869         mDeviceItemsForSharing.clear();
870     }
871 
872     private enum StartIntentHandleStage {
873         TO_HANDLE,
874         HANDLE_AUTO_ADD,
875         HANDLED,
876     }
877 }
878