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 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE; 21 22 import android.app.settings.SettingsEnums; 23 import android.bluetooth.BluetoothAdapter; 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.content.Intent; 32 import android.database.ContentObserver; 33 import android.os.Bundle; 34 import android.os.Handler; 35 import android.os.Looper; 36 import android.provider.Settings; 37 import android.util.Log; 38 39 import androidx.annotation.NonNull; 40 import androidx.annotation.Nullable; 41 import androidx.annotation.VisibleForTesting; 42 import androidx.lifecycle.DefaultLifecycleObserver; 43 import androidx.lifecycle.LifecycleOwner; 44 import androidx.preference.Preference; 45 import androidx.preference.PreferenceGroup; 46 import androidx.preference.PreferenceScreen; 47 48 import com.android.settings.R; 49 import com.android.settings.SettingsActivity; 50 import com.android.settings.bluetooth.BluetoothDevicePreference; 51 import com.android.settings.bluetooth.BluetoothDeviceUpdater; 52 import com.android.settings.bluetooth.Utils; 53 import com.android.settings.connecteddevice.DevicePreferenceCallback; 54 import com.android.settings.core.BasePreferenceController; 55 import com.android.settings.dashboard.DashboardFragment; 56 import com.android.settings.overlay.FeatureFactory; 57 import com.android.settingslib.bluetooth.A2dpProfile; 58 import com.android.settingslib.bluetooth.BluetoothCallback; 59 import com.android.settingslib.bluetooth.BluetoothEventManager; 60 import com.android.settingslib.bluetooth.BluetoothUtils; 61 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 62 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; 63 import com.android.settingslib.bluetooth.HeadsetProfile; 64 import com.android.settingslib.bluetooth.HearingAidProfile; 65 import com.android.settingslib.bluetooth.LeAudioProfile; 66 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; 67 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; 68 import com.android.settingslib.bluetooth.LocalBluetoothManager; 69 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 70 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 71 import com.android.settingslib.flags.Flags; 72 import com.android.settingslib.utils.ThreadUtils; 73 74 import java.util.Locale; 75 import java.util.concurrent.Executor; 76 import java.util.concurrent.Executors; 77 import java.util.concurrent.atomic.AtomicBoolean; 78 79 public class AudioSharingDevicePreferenceController extends BasePreferenceController 80 implements DefaultLifecycleObserver, 81 DevicePreferenceCallback, 82 BluetoothCallback, 83 LocalBluetoothProfileManager.ServiceListener { 84 private static final boolean DEBUG = BluetoothUtils.D; 85 86 private static final String TAG = "AudioSharingDevicePrefController"; 87 private static final String KEY = "audio_sharing_device_list"; 88 89 @Nullable private final LocalBluetoothManager mBtManager; 90 @Nullable private final CachedBluetoothDeviceManager mDeviceManager; 91 @Nullable private final BluetoothEventManager mEventManager; 92 @Nullable private final LocalBluetoothProfileManager mProfileManager; 93 @Nullable private final LocalBluetoothLeBroadcast mBroadcast; 94 @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant; 95 @Nullable private final ContentResolver mContentResolver; 96 private final Executor mExecutor; 97 private final MetricsFeatureProvider mMetricsFeatureProvider; 98 @Nullable private PreferenceGroup mPreferenceGroup; 99 @Nullable private BluetoothDeviceUpdater mBluetoothDeviceUpdater; 100 @Nullable private DashboardFragment mFragment; 101 @Nullable private AudioSharingDialogHandler mDialogHandler; 102 private AtomicBoolean mIntentHandled = new AtomicBoolean(false); 103 private AtomicBoolean mIsAudioModeOngoingCall = new AtomicBoolean(false); 104 105 @VisibleForTesting 106 BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = 107 new BluetoothLeBroadcastAssistant.Callback() { 108 @Override 109 public void onSearchStarted(int reason) {} 110 111 @Override 112 public void onSearchStartFailed(int reason) {} 113 114 @Override 115 public void onSearchStopped(int reason) {} 116 117 @Override 118 public void onSearchStopFailed(int reason) {} 119 120 @Override 121 public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {} 122 123 @Override 124 public void onSourceAdded( 125 @NonNull BluetoothDevice sink, int sourceId, int reason) { 126 Log.d(TAG, "onSourceAdded: update sharing device list."); 127 if (mBluetoothDeviceUpdater != null) { 128 mBluetoothDeviceUpdater.forceUpdate(); 129 } 130 if (mDeviceManager != null && mDialogHandler != null) { 131 CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(sink); 132 if (cachedDevice != null) { 133 mDialogHandler.closeOpeningDialogsForLeaDevice(cachedDevice); 134 } 135 } 136 } 137 138 @Override 139 public void onSourceAddFailed( 140 @NonNull BluetoothDevice sink, 141 @NonNull BluetoothLeBroadcastMetadata source, 142 int reason) { 143 mMetricsFeatureProvider.action( 144 mContext, 145 SettingsEnums.ACTION_AUDIO_SHARING_JOIN_FAILED, 146 SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY); 147 AudioSharingUtils.toastMessage( 148 mContext, 149 String.format( 150 Locale.US, 151 "Fail to add source to %s reason %d", 152 sink.getAddress(), 153 reason)); 154 } 155 156 @Override 157 public void onSourceModified( 158 @NonNull BluetoothDevice sink, int sourceId, int reason) {} 159 160 @Override 161 public void onSourceModifyFailed( 162 @NonNull BluetoothDevice sink, int sourceId, int reason) {} 163 164 @Override 165 public void onSourceRemoved( 166 @NonNull BluetoothDevice sink, int sourceId, int reason) { 167 Log.d(TAG, "onSourceRemoved: update sharing device list."); 168 if (mBluetoothDeviceUpdater != null) { 169 mBluetoothDeviceUpdater.forceUpdate(); 170 } 171 } 172 173 @Override 174 public void onSourceRemoveFailed( 175 @NonNull BluetoothDevice sink, int sourceId, int reason) { 176 mMetricsFeatureProvider.action( 177 mContext, 178 SettingsEnums.ACTION_AUDIO_SHARING_LEAVE_FAILED, 179 SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY); 180 AudioSharingUtils.toastMessage( 181 mContext, 182 String.format( 183 Locale.US, 184 "Fail to remove source from %s reason %d", 185 sink.getAddress(), 186 reason)); 187 } 188 189 @Override 190 public void onReceiveStateChanged( 191 @NonNull BluetoothDevice sink, 192 int sourceId, 193 @NonNull BluetoothLeBroadcastReceiveState state) {} 194 }; 195 196 @VisibleForTesting 197 ContentObserver mSettingsObserver = new ContentObserver(new Handler(Looper.getMainLooper())) { 198 @Override 199 public void onChange(boolean selfChange) { 200 Log.d(TAG, "onChange, primary group id has been changed, refresh list"); 201 if (mBluetoothDeviceUpdater != null) { 202 mBluetoothDeviceUpdater.refreshPreference(); 203 } 204 } 205 }; 206 AudioSharingDevicePreferenceController(Context context)207 public AudioSharingDevicePreferenceController(Context context) { 208 super(context, KEY); 209 mBtManager = Utils.getLocalBtManager(mContext); 210 mEventManager = mBtManager == null ? null : mBtManager.getEventManager(); 211 mDeviceManager = mBtManager == null ? null : mBtManager.getCachedDeviceManager(); 212 mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager(); 213 mBroadcast = mProfileManager == null ? null : mProfileManager.getLeAudioBroadcastProfile(); 214 mAssistant = 215 mProfileManager == null 216 ? null 217 : mProfileManager.getLeAudioBroadcastAssistantProfile(); 218 mContentResolver = context.getContentResolver(); 219 mExecutor = Executors.newSingleThreadExecutor(); 220 mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); 221 } 222 223 @Override onStart(@onNull LifecycleOwner owner)224 public void onStart(@NonNull LifecycleOwner owner) { 225 var unused = ThreadUtils.postOnBackgroundThread(() -> { 226 if (!isAvailable()) { 227 Log.d(TAG, "Skip onStart(), feature is not supported."); 228 return; 229 } 230 if (!AudioSharingUtils.isAudioSharingProfileReady(mProfileManager) 231 && mProfileManager != null) { 232 Log.d(TAG, "Register profile service listener"); 233 mProfileManager.addServiceListener(this); 234 } 235 if (mEventManager == null 236 || mAssistant == null 237 || mDialogHandler == null 238 || mContentResolver == null 239 || mBluetoothDeviceUpdater == null) { 240 Log.d(TAG, "Skip onStart(), profile is not ready."); 241 return; 242 } 243 Log.d(TAG, "onStart() Register callbacks."); 244 mEventManager.registerCallback(this); 245 mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); 246 mDialogHandler.registerCallbacks(mExecutor); 247 mContentResolver.registerContentObserver( 248 Settings.Secure.getUriFor(BluetoothUtils.getPrimaryGroupIdUriForBroadcast()), 249 false, 250 mSettingsObserver); 251 mBluetoothDeviceUpdater.registerCallback(); 252 mBluetoothDeviceUpdater.refreshPreference(); 253 mIsAudioModeOngoingCall.set(isAudioModeOngoingCall(mContext)); 254 updateTitle(); 255 }); 256 } 257 258 @Override onStop(@onNull LifecycleOwner owner)259 public void onStop(@NonNull LifecycleOwner owner) { 260 var unused = ThreadUtils.postOnBackgroundThread(() -> { 261 if (!isAvailable()) { 262 Log.d(TAG, "Skip onStop(), feature is not supported."); 263 return; 264 } 265 if (mProfileManager != null) { 266 mProfileManager.removeServiceListener(this); 267 } 268 if (mEventManager == null 269 || mAssistant == null 270 || mDialogHandler == null 271 || mContentResolver == null 272 || mBluetoothDeviceUpdater == null) { 273 Log.d(TAG, "Skip onStop(), profile is not ready."); 274 return; 275 } 276 Log.d(TAG, "onStop() Unregister callbacks."); 277 mEventManager.unregisterCallback(this); 278 mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); 279 mDialogHandler.unregisterCallbacks(); 280 mContentResolver.unregisterContentObserver(mSettingsObserver); 281 mBluetoothDeviceUpdater.unregisterCallback(); 282 }); 283 } 284 285 @Override onServiceConnected()286 public void onServiceConnected() { 287 if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) { 288 if (mProfileManager != null) { 289 mProfileManager.removeServiceListener(this); 290 } 291 if (!mIntentHandled.get()) { 292 Log.d(TAG, "onServiceConnected: handleDeviceClickFromIntent"); 293 handleDeviceClickFromIntent(); 294 mIntentHandled.set(true); 295 } 296 } 297 } 298 299 @Override onServiceDisconnected()300 public void onServiceDisconnected() { 301 // Do nothing 302 } 303 304 @Override displayPreference(PreferenceScreen screen)305 public void displayPreference(PreferenceScreen screen) { 306 super.displayPreference(screen); 307 mPreferenceGroup = screen.findPreference(KEY); 308 if (mPreferenceGroup != null) { 309 mPreferenceGroup.setVisible(false); 310 } 311 312 if (isAvailable()) { 313 if (mBluetoothDeviceUpdater != null) { 314 mBluetoothDeviceUpdater.setPrefContext(screen.getContext()); 315 mBluetoothDeviceUpdater.forceUpdate(); 316 } 317 if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) { 318 if (!mIntentHandled.get()) { 319 Log.d(TAG, "displayPreference: profile ready, handleDeviceClickFromIntent"); 320 var unused = 321 ThreadUtils.postOnBackgroundThread(() -> handleDeviceClickFromIntent()); 322 mIntentHandled.set(true); 323 } 324 } 325 } 326 } 327 328 @Override getAvailabilityStatus()329 public int getAvailabilityStatus() { 330 return (BluetoothUtils.isAudioSharingUIAvailable(mContext) 331 && mBluetoothDeviceUpdater != null) 332 ? AVAILABLE_UNSEARCHABLE 333 : UNSUPPORTED_ON_DEVICE; 334 } 335 336 @Override getPreferenceKey()337 public String getPreferenceKey() { 338 return KEY; 339 } 340 341 @Override onDeviceAdded(Preference preference)342 public void onDeviceAdded(Preference preference) { 343 if (mPreferenceGroup != null) { 344 if (mPreferenceGroup.getPreferenceCount() == 0) { 345 mPreferenceGroup.setVisible(true); 346 } 347 mPreferenceGroup.addPreference(preference); 348 } 349 } 350 351 @Override onDeviceRemoved(Preference preference)352 public void onDeviceRemoved(Preference preference) { 353 if (mPreferenceGroup != null) { 354 mPreferenceGroup.removePreference(preference); 355 if (mPreferenceGroup.getPreferenceCount() == 0) { 356 mPreferenceGroup.setVisible(false); 357 } 358 } 359 } 360 361 @Override onProfileConnectionStateChanged( @onNull CachedBluetoothDevice cachedDevice, @ConnectionState int state, int bluetoothProfile)362 public void onProfileConnectionStateChanged( 363 @NonNull CachedBluetoothDevice cachedDevice, 364 @ConnectionState int state, 365 int bluetoothProfile) { 366 if (mDialogHandler == null || mBroadcast == null || mAssistant == null 367 || mFragment == null) { 368 Log.d(TAG, "Ignore onProfileConnectionStateChanged, not init correctly"); 369 return; 370 } 371 if (Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice() && mBroadcast.isEnabled( 372 null)) { 373 Log.d(TAG, "Ignore onProfileConnectionStateChanged, in broadcast"); 374 // Device connected in broadcast will be handled in sysui via settingslib 375 return; 376 } 377 if (!isMediaDevice(cachedDevice)) { 378 Log.d(TAG, "Ignore onProfileConnectionStateChanged, not a media device"); 379 return; 380 } 381 // Close related dialogs if the BT remote device is disconnected. 382 if (state == BluetoothAdapter.STATE_DISCONNECTED) { 383 boolean isLeAudioSupported = BluetoothUtils.isLeAudioSupported(cachedDevice); 384 if (isLeAudioSupported 385 && bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) { 386 Log.d(TAG, "closeOpeningDialogsForLeaDevice"); 387 mDialogHandler.closeOpeningDialogsForLeaDevice(cachedDevice); 388 return; 389 } 390 if (!isLeAudioSupported && !cachedDevice.isConnected()) { 391 Log.d(TAG, "closeOpeningDialogsForNonLeaDevice"); 392 mDialogHandler.closeOpeningDialogsForNonLeaDevice(cachedDevice); 393 return; 394 } 395 } 396 if (state != BluetoothAdapter.STATE_CONNECTED || !cachedDevice.getDevice().isConnected()) { 397 Log.d(TAG, "Ignore onProfileConnectionStateChanged, not connected state"); 398 return; 399 } 400 handleOnProfileStateChanged(cachedDevice, bluetoothProfile); 401 } 402 403 @Override onBluetoothStateChanged(@dapterState int bluetoothState)404 public void onBluetoothStateChanged(@AdapterState int bluetoothState) { 405 if (bluetoothState == BluetoothAdapter.STATE_OFF && mDialogHandler != null) { 406 mDialogHandler.closeOpeningDialogsOtherThan(""); 407 } 408 } 409 410 @Override onAudioModeChanged()411 public void onAudioModeChanged() { 412 mIsAudioModeOngoingCall.set(isAudioModeOngoingCall(mContext)); 413 updateTitle(); 414 } 415 416 @Override onDeviceClick(@onNull Preference preference)417 public void onDeviceClick(@NonNull Preference preference) { 418 boolean isCallMode = mIsAudioModeOngoingCall.get(); 419 if (isCallMode) { 420 Log.d(TAG, "onDeviceClick, set active in call mode"); 421 CachedBluetoothDevice cachedDevice = 422 ((BluetoothDevicePreference) preference).getBluetoothDevice(); 423 cachedDevice.setActive(); 424 AudioSharingUtils.setUserPreferredPrimary(mContext, cachedDevice); 425 } 426 mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_AUDIO_SHARING_DEVICE_CLICK, 427 isCallMode); 428 } 429 430 /** 431 * Initialize the controller. 432 * 433 * @param fragment The fragment to provide the context and metrics category for {@link 434 * AudioSharingBluetoothDeviceUpdater} and provide the host for dialogs. 435 */ init(DashboardFragment fragment)436 public void init(DashboardFragment fragment) { 437 mFragment = fragment; 438 mBluetoothDeviceUpdater = 439 new AudioSharingBluetoothDeviceUpdater( 440 fragment.getContext(), 441 AudioSharingDevicePreferenceController.this, 442 fragment.getMetricsCategory()); 443 mDialogHandler = new AudioSharingDialogHandler(mContext, fragment); 444 } 445 446 @VisibleForTesting setBluetoothDeviceUpdater(@ullable BluetoothDeviceUpdater bluetoothDeviceUpdater)447 void setBluetoothDeviceUpdater(@Nullable BluetoothDeviceUpdater bluetoothDeviceUpdater) { 448 mBluetoothDeviceUpdater = bluetoothDeviceUpdater; 449 } 450 451 @VisibleForTesting setDialogHandler(@ullable AudioSharingDialogHandler dialogHandler)452 void setDialogHandler(@Nullable AudioSharingDialogHandler dialogHandler) { 453 mDialogHandler = dialogHandler; 454 } 455 456 @VisibleForTesting setHostFragment(@ullable DashboardFragment fragment)457 void setHostFragment(@Nullable DashboardFragment fragment) { 458 mFragment = fragment; 459 } 460 461 /** Test only: set intent handle state for test. */ 462 @VisibleForTesting setIntentHandled(boolean handled)463 void setIntentHandled(boolean handled) { 464 mIntentHandled.set(handled); 465 } 466 handleOnProfileStateChanged( @onNull CachedBluetoothDevice cachedDevice, int bluetoothProfile)467 private void handleOnProfileStateChanged( 468 @NonNull CachedBluetoothDevice cachedDevice, int bluetoothProfile) { 469 boolean isLeAudioSupported = BluetoothUtils.isLeAudioSupported(cachedDevice); 470 // For eligible (LE audio) remote device, we only check its connected LE audio assistant 471 // profile. 472 if (isLeAudioSupported 473 && bluetoothProfile != BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) { 474 Log.d( 475 TAG, 476 "Ignore onProfileConnectionStateChanged, not the le assistant profile for" 477 + " le audio device"); 478 return; 479 } 480 boolean isFirstConnectedProfile = isFirstConnectedProfile(cachedDevice, bluetoothProfile); 481 // For ineligible (non LE audio) remote device, we only check its first connected profile. 482 if (!isLeAudioSupported && !isFirstConnectedProfile) { 483 Log.d( 484 TAG, 485 "Ignore onProfileConnectionStateChanged, not the first connected profile for" 486 + " non le audio device"); 487 return; 488 } 489 Log.d(TAG, "Start handling onProfileConnectionStateChanged for " 490 + cachedDevice.getDevice().getAnonymizedAddress()); 491 // Check nullability to pass NullAway check 492 if (mDialogHandler != null) { 493 mDialogHandler.handleDeviceConnected(cachedDevice, /* userTriggered= */ false); 494 } 495 } 496 isMediaDevice(CachedBluetoothDevice cachedDevice)497 private boolean isMediaDevice(CachedBluetoothDevice cachedDevice) { 498 return cachedDevice.getUiAccessibleProfiles().stream() 499 .anyMatch( 500 profile -> 501 profile instanceof A2dpProfile 502 || profile instanceof HearingAidProfile 503 || profile instanceof LeAudioProfile 504 || profile instanceof HeadsetProfile); 505 } 506 isFirstConnectedProfile( CachedBluetoothDevice cachedDevice, int bluetoothProfile)507 private boolean isFirstConnectedProfile( 508 CachedBluetoothDevice cachedDevice, int bluetoothProfile) { 509 return cachedDevice.getProfiles().stream() 510 .noneMatch( 511 profile -> 512 profile.getProfileId() != bluetoothProfile 513 && profile.getConnectionStatus(cachedDevice.getDevice()) 514 == BluetoothProfile.STATE_CONNECTED); 515 } 516 517 /** 518 * Handle device click triggered by intent. 519 * 520 * <p>When user click device from BT QS dialog, BT QS will send intent to open {@link 521 * com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment} and handle device 522 * click event under some conditions. 523 * 524 * <p>This method will be called when displayPreference if the audio sharing profiles are ready. 525 * If the profiles are not ready when the preference display, this method will be called when 526 * onServiceConnected. 527 */ handleDeviceClickFromIntent()528 private void handleDeviceClickFromIntent() { 529 if (mFragment == null 530 || mFragment.getActivity() == null 531 || mFragment.getActivity().getIntent() == null) { 532 Log.d(TAG, "Skip handleDeviceClickFromIntent, fragment intent is null"); 533 return; 534 } 535 Intent intent = mFragment.getActivity().getIntent(); 536 Bundle args = intent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS); 537 BluetoothDevice device = 538 args == null 539 ? null 540 : args.getParcelable(EXTRA_BLUETOOTH_DEVICE, BluetoothDevice.class); 541 CachedBluetoothDevice cachedDevice = 542 (device == null || mDeviceManager == null) 543 ? null 544 : mDeviceManager.findDevice(device); 545 if (cachedDevice == null) { 546 Log.d(TAG, "Skip handleDeviceClickFromIntent, device is null"); 547 return; 548 } 549 // Check nullability to pass NullAway check 550 if (device != null && !device.isConnected()) { 551 Log.d(TAG, "handleDeviceClickFromIntent: connect"); 552 cachedDevice.connect(); 553 } else if (mDialogHandler != null) { 554 Log.d(TAG, "handleDeviceClickFromIntent: trigger dialog handler"); 555 mDialogHandler.handleDeviceConnected(cachedDevice, /* userTriggered= */ true); 556 } 557 } 558 updateTitle()559 private void updateTitle() { 560 if (mPreferenceGroup == null) return; 561 int titleResId; 562 if (mIsAudioModeOngoingCall.get()) { 563 // in phone call 564 titleResId = R.string.connected_device_call_device_title; 565 } else { 566 // without phone call 567 titleResId = R.string.audio_sharing_device_group_title; 568 } 569 AudioSharingUtils.postOnMainThread(mContext, 570 () -> { 571 if (mPreferenceGroup != null) { 572 mPreferenceGroup.setTitle(titleResId); 573 } 574 }); 575 } 576 } 577