• 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 android.app.settings.SettingsEnums;
20 import android.bluetooth.BluetoothAdapter;
21 import android.bluetooth.BluetoothDevice;
22 import android.bluetooth.BluetoothLeBroadcast;
23 import android.bluetooth.BluetoothLeBroadcastAssistant;
24 import android.bluetooth.BluetoothLeBroadcastMetadata;
25 import android.bluetooth.BluetoothLeBroadcastReceiveState;
26 import android.bluetooth.BluetoothProfile;
27 import android.content.BroadcastReceiver;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.util.FeatureFlagUtils;
32 import android.util.Log;
33 import android.util.Pair;
34 import android.widget.CompoundButton;
35 import android.widget.CompoundButton.OnCheckedChangeListener;
36 
37 import androidx.annotation.NonNull;
38 import androidx.annotation.Nullable;
39 import androidx.annotation.VisibleForTesting;
40 import androidx.fragment.app.Fragment;
41 import androidx.lifecycle.DefaultLifecycleObserver;
42 import androidx.lifecycle.LifecycleOwner;
43 
44 import com.android.settings.bluetooth.Utils;
45 import com.android.settings.core.BasePreferenceController;
46 import com.android.settings.overlay.FeatureFactory;
47 import com.android.settings.widget.SettingsMainSwitchBar;
48 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
49 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
50 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
51 import com.android.settingslib.bluetooth.LocalBluetoothManager;
52 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
53 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
54 import com.android.settingslib.utils.ThreadUtils;
55 
56 import com.google.common.collect.ImmutableList;
57 
58 import java.util.ArrayList;
59 import java.util.HashMap;
60 import java.util.List;
61 import java.util.Locale;
62 import java.util.Map;
63 import java.util.Objects;
64 import java.util.concurrent.Executor;
65 import java.util.concurrent.Executors;
66 import java.util.concurrent.atomic.AtomicBoolean;
67 import java.util.stream.Collectors;
68 
69 public class AudioSharingSwitchBarController extends BasePreferenceController
70         implements DefaultLifecycleObserver,
71                 OnCheckedChangeListener,
72                 LocalBluetoothProfileManager.ServiceListener {
73     private static final String TAG = "AudioSharingSwitchBarCtl";
74     private static final String PREF_KEY = "audio_sharing_main_switch";
75 
76     interface OnAudioSharingStateChangedListener {
77         /**
78          * The callback which will be triggered when:
79          *
80          * <p>1. Bluetooth on/off state changes. 2. Broadcast and assistant profile
81          * connect/disconnect state changes. 3. Audio sharing start/stop state changes.
82          */
onAudioSharingStateChanged()83         void onAudioSharingStateChanged();
84 
85         /**
86          * The callback which will be triggered when:
87          *
88          * <p>Broadcast and assistant profile connected.
89          */
onAudioSharingProfilesConnected()90         void onAudioSharingProfilesConnected();
91     }
92 
93     private final SettingsMainSwitchBar mSwitchBar;
94     private final BluetoothAdapter mBluetoothAdapter;
95     @Nullable private final LocalBluetoothManager mBtManager;
96     @Nullable private final LocalBluetoothProfileManager mProfileManager;
97     @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
98     @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
99     @Nullable private Fragment mFragment;
100     private final Executor mExecutor;
101     private final MetricsFeatureProvider mMetricsFeatureProvider;
102     private final OnAudioSharingStateChangedListener mListener;
103     private Map<Integer, List<CachedBluetoothDevice>> mGroupedConnectedDevices = new HashMap<>();
104     private List<BluetoothDevice> mTargetActiveSinks = new ArrayList<>();
105     private List<AudioSharingDeviceItem> mDeviceItemsForSharing = new ArrayList<>();
106     @VisibleForTesting IntentFilter mIntentFilter;
107     private final AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
108 
109     @VisibleForTesting
110     BroadcastReceiver mReceiver =
111             new BroadcastReceiver() {
112                 @Override
113                 public void onReceive(Context context, Intent intent) {
114                     updateSwitch();
115                     mListener.onAudioSharingStateChanged();
116                 }
117             };
118 
119     @VisibleForTesting
120     final BluetoothLeBroadcast.Callback mBroadcastCallback =
121             new BluetoothLeBroadcast.Callback() {
122                 @Override
123                 public void onBroadcastStarted(int reason, int broadcastId) {
124                     Log.d(
125                             TAG,
126                             "onBroadcastStarted(), reason = "
127                                     + reason
128                                     + ", broadcastId = "
129                                     + broadcastId);
130                     updateSwitch();
131                     mListener.onAudioSharingStateChanged();
132                 }
133 
134                 @Override
135                 public void onBroadcastStartFailed(int reason) {
136                     Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason);
137                     // TODO: handle broadcast start fail
138                     updateSwitch();
139                 }
140 
141                 @Override
142                 public void onBroadcastMetadataChanged(
143                         int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) {
144                     Log.d(
145                             TAG,
146                             "onBroadcastMetadataChanged(), broadcastId = "
147                                     + broadcastId
148                                     + ", metadata = "
149                                     + metadata.getBroadcastName());
150                 }
151 
152                 @Override
153                 public void onBroadcastStopped(int reason, int broadcastId) {
154                     Log.d(
155                             TAG,
156                             "onBroadcastStopped(), reason = "
157                                     + reason
158                                     + ", broadcastId = "
159                                     + broadcastId);
160                     updateSwitch();
161                     mListener.onAudioSharingStateChanged();
162                 }
163 
164                 @Override
165                 public void onBroadcastStopFailed(int reason) {
166                     Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason);
167                     // TODO: handle broadcast stop fail
168                     updateSwitch();
169                 }
170 
171                 @Override
172                 public void onBroadcastUpdated(int reason, int broadcastId) {}
173 
174                 @Override
175                 public void onBroadcastUpdateFailed(int reason, int broadcastId) {}
176 
177                 @Override
178                 public void onPlaybackStarted(int reason, int broadcastId) {
179                     Log.d(
180                             TAG,
181                             "onPlaybackStarted(), reason = "
182                                     + reason
183                                     + ", broadcastId = "
184                                     + broadcastId);
185                     handleOnBroadcastReady();
186                 }
187 
188                 @Override
189                 public void onPlaybackStopped(int reason, int broadcastId) {}
190             };
191 
192     private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
193             new BluetoothLeBroadcastAssistant.Callback() {
194                 @Override
195                 public void onSearchStarted(int reason) {}
196 
197                 @Override
198                 public void onSearchStartFailed(int reason) {}
199 
200                 @Override
201                 public void onSearchStopped(int reason) {}
202 
203                 @Override
204                 public void onSearchStopFailed(int reason) {}
205 
206                 @Override
207                 public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
208 
209                 @Override
210                 public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {
211                     Log.d(
212                             TAG,
213                             "onSourceAdded(), sink = "
214                                     + sink
215                                     + ", sourceId = "
216                                     + sourceId
217                                     + ", reason = "
218                                     + reason);
219                 }
220 
221                 @Override
222                 public void onSourceAddFailed(
223                         @NonNull BluetoothDevice sink,
224                         @NonNull BluetoothLeBroadcastMetadata source,
225                         int reason) {
226                     Log.d(
227                             TAG,
228                             "onSourceAddFailed(), sink = "
229                                     + sink
230                                     + ", source = "
231                                     + source
232                                     + ", reason = "
233                                     + reason);
234                     AudioSharingUtils.toastMessage(
235                             mContext,
236                             String.format(
237                                     Locale.US,
238                                     "Fail to add source to %s reason %d",
239                                     sink.getAddress(),
240                                     reason));
241                 }
242 
243                 @Override
244                 public void onSourceModified(
245                         @NonNull BluetoothDevice sink, int sourceId, int reason) {}
246 
247                 @Override
248                 public void onSourceModifyFailed(
249                         @NonNull BluetoothDevice sink, int sourceId, int reason) {}
250 
251                 @Override
252                 public void onSourceRemoved(
253                         @NonNull BluetoothDevice sink, int sourceId, int reason) {}
254 
255                 @Override
256                 public void onSourceRemoveFailed(
257                         @NonNull BluetoothDevice sink, int sourceId, int reason) {}
258 
259                 @Override
260                 public void onReceiveStateChanged(
261                         @NonNull BluetoothDevice sink,
262                         int sourceId,
263                         @NonNull BluetoothLeBroadcastReceiveState state) {}
264             };
265 
AudioSharingSwitchBarController( Context context, SettingsMainSwitchBar switchBar, OnAudioSharingStateChangedListener listener)266     AudioSharingSwitchBarController(
267             Context context,
268             SettingsMainSwitchBar switchBar,
269             OnAudioSharingStateChangedListener listener) {
270         super(context, PREF_KEY);
271         mSwitchBar = switchBar;
272         mListener = listener;
273         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
274         mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
275         mBtManager = Utils.getLocalBtManager(context);
276         mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
277         mBroadcast = mProfileManager == null ? null : mProfileManager.getLeAudioBroadcastProfile();
278         mAssistant =
279                 mProfileManager == null
280                         ? null
281                         : mProfileManager.getLeAudioBroadcastAssistantProfile();
282         mExecutor = Executors.newSingleThreadExecutor();
283         mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
284     }
285 
286     @Override
onStart(@onNull LifecycleOwner owner)287     public void onStart(@NonNull LifecycleOwner owner) {
288         if (!isAvailable()) {
289             Log.d(TAG, "Skip register callbacks. Feature is not available.");
290             return;
291         }
292         mContext.registerReceiver(mReceiver, mIntentFilter, Context.RECEIVER_EXPORTED_UNAUDITED);
293         updateSwitch();
294         if (!AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
295             if (mProfileManager != null) {
296                 mProfileManager.addServiceListener(this);
297             }
298             Log.d(TAG, "Skip register callbacks. Profile is not ready.");
299             return;
300         }
301         registerCallbacks();
302     }
303 
304     @Override
onStop(@onNull LifecycleOwner owner)305     public void onStop(@NonNull LifecycleOwner owner) {
306         if (!isAvailable()) {
307             Log.d(TAG, "Skip unregister callbacks. Feature is not available.");
308             return;
309         }
310         mContext.unregisterReceiver(mReceiver);
311         if (mProfileManager != null) {
312             mProfileManager.removeServiceListener(this);
313         }
314         unregisterCallbacks();
315     }
316 
317     @Override
onCheckedChanged(CompoundButton buttonView, boolean isChecked)318     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
319         // Filter out unnecessary callbacks when switch is disabled.
320         if (!buttonView.isEnabled()) return;
321         if (isChecked) {
322             mSwitchBar.setEnabled(false);
323             boolean isBroadcasting = AudioSharingUtils.isBroadcasting(mBtManager);
324             if (mAssistant == null || mBroadcast == null || isBroadcasting) {
325                 Log.d(TAG, "Skip startAudioSharing, already broadcasting or not support.");
326                 mSwitchBar.setEnabled(true);
327                 if (!isBroadcasting) {
328                     mSwitchBar.setChecked(false);
329                 }
330                 return;
331             }
332             // FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST is always true in
333             // prod. We can turn off the flag for debug purpose.
334             if (FeatureFlagUtils.isEnabled(
335                             mContext,
336                             FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST)
337                     && mAssistant
338                             .getDevicesMatchingConnectionStates(
339                                     new int[] {BluetoothProfile.STATE_CONNECTED})
340                             .isEmpty()) {
341                 // Pop up dialog to ask users to connect at least one lea buds before audio sharing.
342                 AudioSharingUtils.postOnMainThread(
343                         mContext,
344                         () -> {
345                             mSwitchBar.setEnabled(true);
346                             mSwitchBar.setChecked(false);
347                             if (mFragment != null) {
348                                 AudioSharingConfirmDialogFragment.show(mFragment);
349                             }
350                         });
351                 return;
352             }
353             startAudioSharing();
354         } else {
355             stopAudioSharing();
356         }
357     }
358 
359     @Override
getAvailabilityStatus()360     public int getAvailabilityStatus() {
361         return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
362     }
363 
364     @Override
onServiceConnected()365     public void onServiceConnected() {
366         Log.d(TAG, "onServiceConnected()");
367         if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
368             registerCallbacks();
369             updateSwitch();
370             mListener.onAudioSharingProfilesConnected();
371             mListener.onAudioSharingStateChanged();
372             if (mProfileManager != null) {
373                 mProfileManager.removeServiceListener(this);
374             }
375         }
376     }
377 
378     @Override
onServiceDisconnected()379     public void onServiceDisconnected() {
380         Log.d(TAG, "onServiceDisconnected()");
381         // Do nothing.
382     }
383 
384     /**
385      * Initialize the controller.
386      *
387      * @param fragment The fragment to host the {@link AudioSharingSwitchBarController} dialog.
388      */
init(@onNull Fragment fragment)389     public void init(@NonNull Fragment fragment) {
390         this.mFragment = fragment;
391     }
392 
393     /** Test only: set callback registration status in tests. */
394     @VisibleForTesting
setCallbacksRegistered(boolean registered)395     void setCallbacksRegistered(boolean registered) {
396         mCallbacksRegistered.set(registered);
397     }
398 
registerCallbacks()399     private void registerCallbacks() {
400         if (!isAvailable()) {
401             Log.d(TAG, "Skip registerCallbacks(). Feature is not available.");
402             return;
403         }
404         if (mBroadcast == null || mAssistant == null) {
405             Log.d(TAG, "Skip registerCallbacks(). Profile not support on this device.");
406             return;
407         }
408         if (!mCallbacksRegistered.get()) {
409             Log.d(TAG, "registerCallbacks()");
410             mSwitchBar.addOnSwitchChangeListener(this);
411             mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
412             mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
413             mCallbacksRegistered.set(true);
414         }
415     }
416 
unregisterCallbacks()417     private void unregisterCallbacks() {
418         if (!isAvailable() || !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
419             Log.d(TAG, "Skip unregisterCallbacks(). Feature is not available.");
420             return;
421         }
422         if (mBroadcast == null || mAssistant == null) {
423             Log.d(TAG, "Skip unregisterCallbacks(). Profile not support on this device.");
424             return;
425         }
426         if (mCallbacksRegistered.get()) {
427             Log.d(TAG, "unregisterCallbacks()");
428             mSwitchBar.removeOnSwitchChangeListener(this);
429             mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
430             mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
431             mCallbacksRegistered.set(false);
432         }
433     }
434 
startAudioSharing()435     private void startAudioSharing() {
436         // Compute the device connection state before start audio sharing since the devices will
437         // be set to inactive after the broadcast started.
438         mGroupedConnectedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(mBtManager);
439         List<AudioSharingDeviceItem> deviceItems =
440                 AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
441                         mBtManager, mGroupedConnectedDevices, /* filterByInSharing= */ false);
442         // deviceItems is ordered. The active device is the first place if exits.
443         mDeviceItemsForSharing = new ArrayList<>(deviceItems);
444         mTargetActiveSinks = new ArrayList<>();
445         if (!deviceItems.isEmpty() && deviceItems.get(0).isActive()) {
446             for (CachedBluetoothDevice device :
447                     mGroupedConnectedDevices.getOrDefault(
448                             deviceItems.get(0).getGroupId(), ImmutableList.of())) {
449                 // If active device exists for audio sharing, share to it
450                 // automatically once the broadcast is started.
451                 mTargetActiveSinks.add(device.getDevice());
452             }
453             mDeviceItemsForSharing.remove(0);
454         }
455         if (mBroadcast != null) {
456             mBroadcast.startPrivateBroadcast();
457         }
458     }
459 
stopAudioSharing()460     private void stopAudioSharing() {
461         mSwitchBar.setEnabled(false);
462         if (!AudioSharingUtils.isBroadcasting(mBtManager)) {
463             Log.d(TAG, "Skip stopAudioSharing, already not broadcasting or broadcast not support.");
464             mSwitchBar.setEnabled(true);
465             return;
466         }
467         if (mBroadcast != null) {
468             mBroadcast.stopBroadcast(mBroadcast.getLatestBroadcastId());
469         }
470     }
471 
updateSwitch()472     private void updateSwitch() {
473         var unused =
474                 ThreadUtils.postOnBackgroundThread(
475                         () -> {
476                             boolean isBroadcasting = AudioSharingUtils.isBroadcasting(mBtManager);
477                             boolean isStateReady =
478                                     isBluetoothOn()
479                                             && AudioSharingUtils.isAudioSharingProfileReady(
480                                                     mProfileManager);
481                             AudioSharingUtils.postOnMainThread(
482                                     mContext,
483                                     () -> {
484                                         if (mSwitchBar.isChecked() != isBroadcasting) {
485                                             mSwitchBar.setChecked(isBroadcasting);
486                                         }
487                                         if (mSwitchBar.isEnabled() != isStateReady) {
488                                             mSwitchBar.setEnabled(isStateReady);
489                                         }
490                                         Log.d(
491                                                 TAG,
492                                                 "updateSwitch, checked = "
493                                                         + isBroadcasting
494                                                         + ", enabled = "
495                                                         + isStateReady);
496                                     });
497                         });
498     }
499 
isBluetoothOn()500     private boolean isBluetoothOn() {
501         return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled();
502     }
503 
handleOnBroadcastReady()504     private void handleOnBroadcastReady() {
505         Pair<Integer, Object>[] eventData =
506                 AudioSharingUtils.buildAudioSharingDialogEventData(
507                         SettingsEnums.AUDIO_SHARING_SETTINGS,
508                         SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE,
509                         /* userTriggered= */ false,
510                         /* deviceCountInSharing= */ mTargetActiveSinks.isEmpty() ? 0 : 1,
511                         /* candidateDeviceCount= */ mDeviceItemsForSharing.size());
512         if (!mTargetActiveSinks.isEmpty()) {
513             Log.d(TAG, "handleOnBroadcastReady: automatically add source to active sinks.");
514             AudioSharingUtils.addSourceToTargetSinks(mTargetActiveSinks, mBtManager);
515             mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_AUTO_JOIN_AUDIO_SHARING);
516             mTargetActiveSinks.clear();
517         }
518         if (mFragment == null) {
519             Log.d(TAG, "handleOnBroadcastReady: dialog fail to show due to null fragment.");
520             mGroupedConnectedDevices.clear();
521             mDeviceItemsForSharing.clear();
522             return;
523         }
524         showDialog(eventData);
525     }
526 
showDialog(Pair<Integer, Object>[] eventData)527     private void showDialog(Pair<Integer, Object>[] eventData) {
528         AudioSharingDialogFragment.DialogEventListener listener =
529                 new AudioSharingDialogFragment.DialogEventListener() {
530                     @Override
531                     public void onItemClick(@NonNull AudioSharingDeviceItem item) {
532                         AudioSharingUtils.addSourceToTargetSinks(
533                                 mGroupedConnectedDevices
534                                         .getOrDefault(item.getGroupId(), ImmutableList.of())
535                                         .stream()
536                                         .map(CachedBluetoothDevice::getDevice)
537                                         .filter(Objects::nonNull)
538                                         .collect(Collectors.toList()),
539                                 mBtManager);
540                         mGroupedConnectedDevices.clear();
541                         mDeviceItemsForSharing.clear();
542                     }
543 
544                     @Override
545                     public void onCancelClick() {
546                         mGroupedConnectedDevices.clear();
547                         mDeviceItemsForSharing.clear();
548                     }
549                 };
550         AudioSharingUtils.postOnMainThread(
551                 mContext,
552                 () -> {
553                     // Check nullability to pass NullAway check
554                     if (mFragment != null) {
555                         AudioSharingDialogFragment.show(
556                                 mFragment, mDeviceItemsForSharing, listener, eventData);
557                     }
558                 });
559     }
560 }
561