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