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.Utils.isAudioModeOngoingCall; 20 21 import android.app.settings.SettingsEnums; 22 import android.bluetooth.BluetoothAdapter; 23 import android.bluetooth.BluetoothCsipSetCoordinator; 24 import android.bluetooth.BluetoothDevice; 25 import android.bluetooth.BluetoothLeBroadcastAssistant; 26 import android.bluetooth.BluetoothLeBroadcastMetadata; 27 import android.bluetooth.BluetoothLeBroadcastReceiveState; 28 import android.bluetooth.BluetoothProfile; 29 import android.content.ContentResolver; 30 import android.content.Context; 31 import android.database.ContentObserver; 32 import android.os.Handler; 33 import android.os.Looper; 34 import android.provider.Settings; 35 import android.util.Log; 36 import android.util.Pair; 37 38 import androidx.annotation.NonNull; 39 import androidx.annotation.Nullable; 40 import androidx.annotation.VisibleForTesting; 41 import androidx.fragment.app.Fragment; 42 import androidx.lifecycle.LifecycleOwner; 43 import androidx.preference.PreferenceScreen; 44 45 import com.android.settings.R; 46 import com.android.settings.bluetooth.Utils; 47 import com.android.settings.overlay.FeatureFactory; 48 import com.android.settingslib.bluetooth.BluetoothCallback; 49 import com.android.settingslib.bluetooth.BluetoothEventManager; 50 import com.android.settingslib.bluetooth.BluetoothUtils; 51 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 52 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; 53 import com.android.settingslib.bluetooth.LeAudioProfile; 54 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; 55 import com.android.settingslib.bluetooth.LocalBluetoothManager; 56 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 57 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 58 import com.android.settingslib.flags.Flags; 59 import com.android.settingslib.utils.ThreadUtils; 60 61 import com.google.common.collect.ImmutableList; 62 63 import java.util.ArrayList; 64 import java.util.HashMap; 65 import java.util.List; 66 import java.util.Map; 67 import java.util.concurrent.Executor; 68 import java.util.concurrent.Executors; 69 import java.util.concurrent.atomic.AtomicBoolean; 70 71 /** PreferenceController to control the dialog to choose the active device for calls and alarms */ 72 public class AudioSharingCallAudioPreferenceController extends AudioSharingBasePreferenceController 73 implements BluetoothCallback { 74 private static final String TAG = "CallAudioPrefController"; 75 private static final String PREF_KEY = "calls_and_alarms"; 76 77 @VisibleForTesting 78 enum ChangeCallAudioType { 79 UNKNOWN, 80 CONNECTED_EARLIER, 81 CONNECTED_LATER 82 } 83 84 @Nullable private final LocalBluetoothManager mBtManager; 85 @Nullable private final BluetoothEventManager mEventManager; 86 @Nullable private final ContentResolver mContentResolver; 87 @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant; 88 @Nullable private final CachedBluetoothDeviceManager mCacheManager; 89 private final Executor mExecutor; 90 private final ContentObserver mSettingsObserver; 91 private final MetricsFeatureProvider mMetricsFeatureProvider; 92 @Nullable private Fragment mFragment; 93 Map<Integer, List<BluetoothDevice>> mGroupedConnectedDevices = new HashMap<>(); 94 private List<AudioSharingDeviceItem> mDeviceItemsInSharingSession = new ArrayList<>(); 95 private final AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false); 96 private AtomicBoolean mIsAudioModeOngoingCall = new AtomicBoolean(false); 97 98 @VisibleForTesting 99 final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = 100 new BluetoothLeBroadcastAssistant.Callback() { 101 @Override 102 public void onSearchStarted(int reason) {} 103 104 @Override 105 public void onSearchStartFailed(int reason) {} 106 107 @Override 108 public void onSearchStopped(int reason) {} 109 110 @Override 111 public void onSearchStopFailed(int reason) {} 112 113 @Override 114 public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {} 115 116 @Override 117 public void onSourceAdded( 118 @NonNull BluetoothDevice sink, int sourceId, int reason) { 119 Log.d(TAG, "onSourceAdded: updateSummary"); 120 updateSummary(); 121 } 122 123 @Override 124 public void onSourceAddFailed( 125 @NonNull BluetoothDevice sink, 126 @NonNull BluetoothLeBroadcastMetadata source, 127 int reason) {} 128 129 @Override 130 public void onSourceModified( 131 @NonNull BluetoothDevice sink, int sourceId, int reason) {} 132 133 @Override 134 public void onSourceModifyFailed( 135 @NonNull BluetoothDevice sink, int sourceId, int reason) {} 136 137 @Override 138 public void onSourceRemoved( 139 @NonNull BluetoothDevice sink, int sourceId, int reason) {} 140 141 @Override 142 public void onSourceRemoveFailed( 143 @NonNull BluetoothDevice sink, int sourceId, int reason) {} 144 145 @Override 146 public void onReceiveStateChanged( 147 @NonNull BluetoothDevice sink, 148 int sourceId, 149 @NonNull BluetoothLeBroadcastReceiveState state) {} 150 }; 151 AudioSharingCallAudioPreferenceController(Context context)152 public AudioSharingCallAudioPreferenceController(Context context) { 153 super(context, PREF_KEY); 154 mBtManager = Utils.getLocalBtManager(mContext); 155 LocalBluetoothProfileManager profileManager = 156 mBtManager == null ? null : mBtManager.getProfileManager(); 157 mEventManager = mBtManager == null ? null : mBtManager.getEventManager(); 158 mAssistant = 159 profileManager == null 160 ? null 161 : profileManager.getLeAudioBroadcastAssistantProfile(); 162 mCacheManager = mBtManager == null ? null : mBtManager.getCachedDeviceManager(); 163 mExecutor = Executors.newSingleThreadExecutor(); 164 mContentResolver = context.getContentResolver(); 165 mSettingsObserver = new FallbackDeviceGroupIdSettingsObserver(); 166 mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); 167 } 168 169 private class FallbackDeviceGroupIdSettingsObserver extends ContentObserver { FallbackDeviceGroupIdSettingsObserver()170 FallbackDeviceGroupIdSettingsObserver() { 171 super(new Handler(Looper.getMainLooper())); 172 } 173 174 @Override onChange(boolean selfChange)175 public void onChange(boolean selfChange) { 176 Log.d(TAG, "onChange, fallback device group id has been changed"); 177 var unused = 178 ThreadUtils.postOnBackgroundThread( 179 AudioSharingCallAudioPreferenceController.this::updateSummary); 180 } 181 } 182 183 @Override getPreferenceKey()184 public String getPreferenceKey() { 185 return PREF_KEY; 186 } 187 188 @Override displayPreference(@onNull PreferenceScreen screen)189 public void displayPreference(@NonNull PreferenceScreen screen) { 190 super.displayPreference(screen); 191 if (mPreference != null) { 192 mPreference.setVisible(false); 193 updateSummary(); 194 mPreference.setOnPreferenceClickListener( 195 preference -> { 196 if (mFragment == null) { 197 Log.w(TAG, "Dialog fail to show due to null host."); 198 return true; 199 } 200 updateDeviceItemsInSharingSession(); 201 if (!mDeviceItemsInSharingSession.isEmpty()) { 202 Pair<Integer, AudioSharingDeviceItem> pair = getActiveItemWithIndex(); 203 AudioSharingCallAudioDialogFragment.show( 204 mFragment, 205 mDeviceItemsInSharingSession, 206 pair == null ? -1 : pair.first, 207 (AudioSharingDeviceItem item) -> { 208 int currentCallAudioGroupId = 209 BluetoothUtils.getPrimaryGroupIdForBroadcast( 210 mContext.getContentResolver(), mBtManager); 211 int clickedGroupId = item.getGroupId(); 212 if (clickedGroupId == currentCallAudioGroupId) { 213 Log.d(TAG, "Skip set call audio device: unchanged"); 214 return; 215 } 216 setCallAudioGroup(clickedGroupId); 217 }); 218 } 219 return true; 220 }); 221 } 222 } 223 224 @Override onStart(@onNull LifecycleOwner owner)225 public void onStart(@NonNull LifecycleOwner owner) { 226 super.onStart(owner); 227 registerCallbacks(); 228 } 229 230 @Override onStop(@onNull LifecycleOwner owner)231 public void onStop(@NonNull LifecycleOwner owner) { 232 super.onStop(owner); 233 unregisterCallbacks(); 234 } 235 236 @Override onProfileConnectionStateChanged( @onNull CachedBluetoothDevice cachedDevice, @ConnectionState int state, int bluetoothProfile)237 public void onProfileConnectionStateChanged( 238 @NonNull CachedBluetoothDevice cachedDevice, 239 @ConnectionState int state, 240 int bluetoothProfile) { 241 if (state == BluetoothAdapter.STATE_DISCONNECTED 242 && bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) { 243 Log.d(TAG, "updatePreference, LE_AUDIO_BROADCAST_ASSISTANT is disconnected."); 244 // The fallback active device could be updated if the previous fallback device is 245 // disconnected. 246 updateSummary(); 247 } 248 } 249 250 @Override onActiveDeviceChanged(@ullable CachedBluetoothDevice activeDevice, int bluetoothProfile)251 public void onActiveDeviceChanged(@Nullable CachedBluetoothDevice activeDevice, 252 int bluetoothProfile) { 253 if (activeDevice != null && bluetoothProfile == BluetoothProfile.LE_AUDIO 254 && BluetoothUtils.isBroadcasting(mBtManager)) { 255 Log.d(TAG, "onActiveDeviceChanged: update summary, device = " 256 + activeDevice.getDevice().getAnonymizedAddress() 257 + ", profile = " + bluetoothProfile); 258 updateSummary(); 259 } 260 } 261 262 @Override onAudioModeChanged()263 public void onAudioModeChanged() { 264 mIsAudioModeOngoingCall.set(isAudioModeOngoingCall(mContext)); 265 } 266 267 /** 268 * Initialize the controller. 269 * 270 * @param fragment The fragment to host the {@link AudioSharingCallAudioDialogFragment} dialog. 271 */ init(Fragment fragment)272 public void init(Fragment fragment) { 273 this.mFragment = fragment; 274 } 275 276 @VisibleForTesting getSettingsObserver()277 ContentObserver getSettingsObserver() { 278 return mSettingsObserver; 279 } 280 281 /** Test only: set callback registration status in tests. */ 282 @VisibleForTesting setCallbacksRegistered(boolean registered)283 void setCallbacksRegistered(boolean registered) { 284 mCallbacksRegistered.set(registered); 285 } 286 registerCallbacks()287 private void registerCallbacks() { 288 if (!isAvailable()) { 289 Log.d(TAG, "Skip registerCallbacks(). Feature is not available."); 290 return; 291 } 292 if (mEventManager == null || mContentResolver == null || mAssistant == null) { 293 Log.d( 294 TAG, 295 "Skip registerCallbacks(). Init is not ready: eventManager = " 296 + (mEventManager == null) 297 + ", contentResolver" 298 + (mContentResolver == null)); 299 return; 300 } 301 if (!mCallbacksRegistered.get()) { 302 Log.d(TAG, "registerCallbacks()"); 303 mEventManager.registerCallback(this); 304 mContentResolver.registerContentObserver( 305 Settings.Secure.getUriFor(BluetoothUtils.getPrimaryGroupIdUriForBroadcast()), 306 false, 307 mSettingsObserver); 308 mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); 309 mIsAudioModeOngoingCall.set(isAudioModeOngoingCall(mContext)); 310 mCallbacksRegistered.set(true); 311 } 312 } 313 unregisterCallbacks()314 private void unregisterCallbacks() { 315 if (!isAvailable()) { 316 Log.d(TAG, "Skip unregisterCallbacks(). Feature is not available."); 317 return; 318 } 319 if (mEventManager == null || mContentResolver == null || mAssistant == null) { 320 Log.d(TAG, "Skip unregisterCallbacks(). Init is not ready."); 321 return; 322 } 323 if (mCallbacksRegistered.get()) { 324 Log.d(TAG, "unregisterCallbacks()"); 325 mEventManager.unregisterCallback(this); 326 mContentResolver.unregisterContentObserver(mSettingsObserver); 327 mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); 328 mCallbacksRegistered.set(false); 329 } 330 } 331 setCallAudioGroup(int groupId)332 private void setCallAudioGroup(int groupId) { 333 List<BluetoothDevice> devices = 334 mGroupedConnectedDevices.getOrDefault( 335 groupId, ImmutableList.of()); 336 CachedBluetoothDevice lead = 337 AudioSharingUtils.getLeadDevice( 338 mCacheManager, devices); 339 if (lead != null) { 340 String addr = lead.getDevice().getAnonymizedAddress(); 341 Log.d(TAG, "Set call audio device: " + addr); 342 if ((Flags.adoptPrimaryGroupManagementApi() || (Flags.audioSharingDeveloperOption() 343 && BluetoothUtils.getAudioSharingPreviewValue(mContentResolver))) 344 && !mIsAudioModeOngoingCall.get()) { 345 LeAudioProfile leaProfile = mBtManager == null ? null 346 : mBtManager.getProfileManager().getLeAudioProfile(); 347 if (leaProfile != null) { 348 leaProfile.setBroadcastToUnicastFallbackGroup(groupId); 349 } 350 } else { 351 lead.setActive(); 352 } 353 AudioSharingUtils.setUserPreferredPrimary(mContext, lead); 354 logCallAudioDeviceChange(groupId, lead); 355 } else { 356 Log.d(TAG, "Skip set call audio device: no lead"); 357 } 358 } 359 360 /** 361 * Update the preference summary: current headset for call audio. 362 * 363 * <p>The summary should be updated when: 364 * 365 * <p>1. displayPreference. 366 * 367 * <p>2. ContentObserver#onChange: the fallback device value in SettingsProvider is changed. 368 * 369 * <p>3. onProfileConnectionStateChanged: the assistant profile of fallback device disconnected. 370 * When the last headset in audio sharing disconnected, both Settings and bluetooth framework 371 * won't set the SettingsProvider, so no ContentObserver#onChange. 372 * 373 * <p>4. onReceiveStateChanged: new headset join the audio sharing. If the headset has already 374 * been set as fallback device in SettingsProvider by bluetooth framework when the broadcast is 375 * started, Settings won't set the SettingsProvider again when the headset join the audio 376 * sharing, so there won't be ContentObserver#onChange. We need listen to onReceiveStateChanged 377 * to handle this scenario. 378 */ updateSummary()379 private void updateSummary() { 380 updateDeviceItemsInSharingSession(); 381 Pair<Integer, AudioSharingDeviceItem> pair = getActiveItemWithIndex(); 382 if (pair != null) { 383 Log.d(TAG, "updateSummary, group = " + pair.second.getGroupId()); 384 AudioSharingUtils.postOnMainThread( 385 mContext, 386 () -> { 387 if (mPreference != null) { 388 mPreference.setSummary( 389 mContext.getString( 390 R.string.audio_sharing_call_audio_description, 391 pair.second.getName())); 392 } 393 }); 394 return; 395 } 396 Log.d(TAG, "updateSummary: set empty"); 397 AudioSharingUtils.postOnMainThread( 398 mContext, 399 () -> { 400 if (mPreference != null) { 401 mPreference.setSummary(""); 402 } 403 }); 404 } 405 updateDeviceItemsInSharingSession()406 private void updateDeviceItemsInSharingSession() { 407 mGroupedConnectedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(mBtManager); 408 mDeviceItemsInSharingSession = 409 AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem( 410 mBtManager, mGroupedConnectedDevices, /* filterByInSharing= */ true); 411 } 412 413 @Nullable getActiveItemWithIndex()414 private Pair<Integer, AudioSharingDeviceItem> getActiveItemWithIndex() { 415 List<AudioSharingDeviceItem> deviceItems = new ArrayList<>(mDeviceItemsInSharingSession); 416 int fallbackActiveGroupId = 417 BluetoothUtils.getPrimaryGroupIdForBroadcast(mContext.getContentResolver(), 418 mBtManager); 419 if (fallbackActiveGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { 420 for (AudioSharingDeviceItem item : deviceItems) { 421 if (item.getGroupId() == fallbackActiveGroupId) { 422 Log.d(TAG, "getActiveItemWithIndex, fallback group = " + item.getGroupId()); 423 return new Pair<>(deviceItems.indexOf(item), item); 424 } 425 } 426 } 427 for (AudioSharingDeviceItem item : deviceItems) { 428 if (item.isActive()) { 429 Log.d(TAG, "getActiveItemWithIndex, active LEA group = " + item.getGroupId()); 430 return new Pair<>(deviceItems.indexOf(item), item); 431 } 432 } 433 return null; 434 } 435 436 @VisibleForTesting logCallAudioDeviceChange(int currentGroupId, CachedBluetoothDevice target)437 void logCallAudioDeviceChange(int currentGroupId, CachedBluetoothDevice target) { 438 var unused = 439 ThreadUtils.postOnBackgroundThread( 440 () -> { 441 ChangeCallAudioType type = ChangeCallAudioType.UNKNOWN; 442 if (mCacheManager != null) { 443 int targetDeviceGroupId = BluetoothUtils.getGroupId(target); 444 List<BluetoothDevice> mostRecentDevices = 445 BluetoothAdapter.getDefaultAdapter() 446 .getMostRecentlyConnectedDevices(); 447 int targetDeviceIdx = -1; 448 int currentDeviceIdx = -1; 449 for (int idx = 0; idx < mostRecentDevices.size(); idx++) { 450 BluetoothDevice device = mostRecentDevices.get(idx); 451 CachedBluetoothDevice cachedDevice = 452 mCacheManager.findDevice(device); 453 int groupId = 454 cachedDevice != null 455 ? BluetoothUtils.getGroupId(cachedDevice) 456 : BluetoothCsipSetCoordinator.GROUP_ID_INVALID; 457 if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { 458 if (groupId == targetDeviceGroupId) { 459 targetDeviceIdx = idx; 460 } else if (groupId == currentGroupId) { 461 currentDeviceIdx = idx; 462 } 463 } 464 if (targetDeviceIdx != -1 && currentDeviceIdx != -1) break; 465 } 466 if (targetDeviceIdx != -1 && currentDeviceIdx != -1) { 467 type = 468 targetDeviceIdx < currentDeviceIdx 469 ? ChangeCallAudioType.CONNECTED_LATER 470 : ChangeCallAudioType.CONNECTED_EARLIER; 471 } 472 } 473 mMetricsFeatureProvider.action( 474 mContext, 475 SettingsEnums.ACTION_AUDIO_SHARING_CHANGE_CALL_AUDIO, 476 type.ordinal()); 477 }); 478 } 479 } 480