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