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