1 /* 2 * Copyright (C) 2022 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.settingslib.bluetooth; 18 19 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; 20 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; 21 22 import android.annotation.CallbackExecutor; 23 import android.bluetooth.BluetoothAdapter; 24 import android.bluetooth.BluetoothClass; 25 import android.bluetooth.BluetoothDevice; 26 import android.bluetooth.BluetoothLeBroadcastAssistant; 27 import android.bluetooth.BluetoothLeBroadcastMetadata; 28 import android.bluetooth.BluetoothLeBroadcastReceiveState; 29 import android.bluetooth.BluetoothProfile; 30 import android.bluetooth.BluetoothProfile.ServiceListener; 31 import android.content.Context; 32 import android.os.Build; 33 import android.util.Log; 34 35 import androidx.annotation.NonNull; 36 import androidx.annotation.RequiresApi; 37 38 import com.android.settingslib.R; 39 40 import java.util.ArrayList; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.concurrent.ConcurrentHashMap; 44 import java.util.concurrent.Executor; 45 46 /** 47 * LocalBluetoothLeBroadcastAssistant provides an interface between the Settings app and the 48 * functionality of the local {@link BluetoothLeBroadcastAssistant}. Use the {@link 49 * BluetoothLeBroadcastAssistant.Callback} to get the result callback. 50 */ 51 public class LocalBluetoothLeBroadcastAssistant implements LocalBluetoothProfile { 52 private static final String TAG = "LocalBluetoothLeBroadcastAssistant"; 53 private static final int UNKNOWN_VALUE_PLACEHOLDER = -1; 54 private static final boolean DEBUG = BluetoothUtils.D; 55 56 static final String NAME = "LE_AUDIO_BROADCAST_ASSISTANT"; 57 // Order of this profile in device profiles list 58 private static final int ORDINAL = 1; 59 60 private LocalBluetoothProfileManager mProfileManager; 61 private BluetoothLeBroadcastAssistant mService; 62 private final CachedBluetoothDeviceManager mDeviceManager; 63 private BluetoothLeBroadcastMetadata mBluetoothLeBroadcastMetadata; 64 private BluetoothLeBroadcastMetadata.Builder mBuilder; 65 private boolean mIsProfileReady; 66 // Cached assistant callbacks being register before service is connected. 67 private final Map<BluetoothLeBroadcastAssistant.Callback, Executor> mCachedCallbackExecutorMap = 68 new ConcurrentHashMap<>(); 69 70 private final ServiceListener mServiceListener = 71 new ServiceListener() { 72 @Override 73 public void onServiceConnected(int profile, BluetoothProfile proxy) { 74 if (DEBUG) { 75 Log.d(TAG, "Bluetooth service connected"); 76 } 77 mService = (BluetoothLeBroadcastAssistant) proxy; 78 // We just bound to the service, so refresh the UI for any connected LeAudio 79 // devices. 80 List<BluetoothDevice> deviceList = mService.getConnectedDevices(); 81 while (!deviceList.isEmpty()) { 82 BluetoothDevice nextDevice = deviceList.remove(0); 83 CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice); 84 // we may add a new device here, but generally this should not happen 85 if (device == null) { 86 if (DEBUG) { 87 Log.d( 88 TAG, 89 "LocalBluetoothLeBroadcastAssistant found new device: " 90 + nextDevice); 91 } 92 device = mDeviceManager.addDevice(nextDevice); 93 } 94 device.onProfileStateChanged( 95 LocalBluetoothLeBroadcastAssistant.this, 96 BluetoothProfile.STATE_CONNECTED); 97 device.refresh(); 98 } 99 100 mProfileManager.callServiceConnectedListeners(); 101 mIsProfileReady = true; 102 if (DEBUG) { 103 Log.d( 104 TAG, 105 "onServiceConnected, register mCachedCallbackExecutorMap = " 106 + mCachedCallbackExecutorMap); 107 } 108 mCachedCallbackExecutorMap.forEach( 109 (callback, executor) -> registerServiceCallBack(executor, callback)); 110 } 111 112 @Override 113 public void onServiceDisconnected(int profile) { 114 if (profile != BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) { 115 Log.d(TAG, "The profile is not LE_AUDIO_BROADCAST_ASSISTANT"); 116 return; 117 } 118 if (DEBUG) { 119 Log.d(TAG, "Bluetooth service disconnected"); 120 } 121 mProfileManager.callServiceDisconnectedListeners(); 122 mIsProfileReady = false; 123 mCachedCallbackExecutorMap.clear(); 124 } 125 }; 126 LocalBluetoothLeBroadcastAssistant( Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager)127 public LocalBluetoothLeBroadcastAssistant( 128 Context context, 129 CachedBluetoothDeviceManager deviceManager, 130 LocalBluetoothProfileManager profileManager) { 131 mProfileManager = profileManager; 132 mDeviceManager = deviceManager; 133 BluetoothAdapter.getDefaultAdapter() 134 .getProfileProxy( 135 context, mServiceListener, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); 136 mBuilder = new BluetoothLeBroadcastMetadata.Builder(); 137 } 138 139 /** 140 * Add a Broadcast Source to the Broadcast Sink with {@link BluetoothLeBroadcastMetadata}. 141 * 142 * @param sink Broadcast Sink to which the Broadcast Source should be added 143 * @param metadata Broadcast Source metadata to be added to the Broadcast Sink 144 * @param isGroupOp {@code true} if Application wants to perform this operation for all 145 * coordinated set members throughout this session. Otherwise, caller would have to add, 146 * modify, and remove individual set members. 147 */ addSource( BluetoothDevice sink, BluetoothLeBroadcastMetadata metadata, boolean isGroupOp)148 public void addSource( 149 BluetoothDevice sink, BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) { 150 if (mService == null) { 151 Log.d(TAG, "The BluetoothLeBroadcastAssistant is null"); 152 return; 153 } 154 mService.addSource(sink, metadata, isGroupOp); 155 } 156 157 /** 158 * Add a Broadcast Source to the Broadcast Sink with the information which are separated from 159 * the qr code string. 160 * 161 * @param sink Broadcast Sink to which the Broadcast Source should be added 162 * @param sourceAddressType hardware MAC Address of the device. See {@link 163 * BluetoothDevice.AddressType}. 164 * @param presentationDelayMicros presentation delay of this Broadcast Source in microseconds. 165 * @param sourceAdvertisingSid 1-byte long Advertising_SID of the Broadcast Source. 166 * @param broadcastId 3-byte long Broadcast_ID of the Broadcast Source. 167 * @param paSyncInterval Periodic Advertising Sync interval of the broadcast Source, {@link 168 * BluetoothLeBroadcastMetadata#PA_SYNC_INTERVAL_UNKNOWN} if unknown. 169 * @param isEncrypted whether the Broadcast Source is encrypted. 170 * @param broadcastCode Broadcast Code for this Broadcast Source, null if code is not required. 171 * @param sourceDevice source advertiser address. 172 * @param isGroupOp {@code true} if Application wants to perform this operation for all 173 * coordinated set members throughout this session. Otherwise, caller would have to add, 174 * modify, and remove individual set members. 175 */ addSource( @onNull BluetoothDevice sink, int sourceAddressType, int presentationDelayMicros, int sourceAdvertisingSid, int broadcastId, int paSyncInterval, boolean isEncrypted, byte[] broadcastCode, BluetoothDevice sourceDevice, boolean isGroupOp)176 public void addSource( 177 @NonNull BluetoothDevice sink, 178 int sourceAddressType, 179 int presentationDelayMicros, 180 int sourceAdvertisingSid, 181 int broadcastId, 182 int paSyncInterval, 183 boolean isEncrypted, 184 byte[] broadcastCode, 185 BluetoothDevice sourceDevice, 186 boolean isGroupOp) { 187 if (DEBUG) { 188 Log.d(TAG, "addSource()"); 189 } 190 buildMetadata( 191 sourceAddressType, 192 presentationDelayMicros, 193 sourceAdvertisingSid, 194 broadcastId, 195 paSyncInterval, 196 isEncrypted, 197 broadcastCode, 198 sourceDevice); 199 addSource(sink, mBluetoothLeBroadcastMetadata, isGroupOp); 200 } 201 buildMetadata( int sourceAddressType, int presentationDelayMicros, int sourceAdvertisingSid, int broadcastId, int paSyncInterval, boolean isEncrypted, byte[] broadcastCode, BluetoothDevice sourceDevice)202 private void buildMetadata( 203 int sourceAddressType, 204 int presentationDelayMicros, 205 int sourceAdvertisingSid, 206 int broadcastId, 207 int paSyncInterval, 208 boolean isEncrypted, 209 byte[] broadcastCode, 210 BluetoothDevice sourceDevice) { 211 mBluetoothLeBroadcastMetadata = 212 mBuilder.setSourceDevice(sourceDevice, sourceAddressType) 213 .setSourceAdvertisingSid(sourceAdvertisingSid) 214 .setBroadcastId(broadcastId) 215 .setPaSyncInterval(paSyncInterval) 216 .setEncrypted(isEncrypted) 217 .setBroadcastCode(broadcastCode) 218 .setPresentationDelayMicros(presentationDelayMicros) 219 .build(); 220 } 221 removeSource(@onNull BluetoothDevice sink, int sourceId)222 public void removeSource(@NonNull BluetoothDevice sink, int sourceId) { 223 if (DEBUG) { 224 Log.d(TAG, "removeSource()"); 225 } 226 if (mService == null) { 227 Log.d(TAG, "The BluetoothLeBroadcastAssistant is null"); 228 return; 229 } 230 mService.removeSource(sink, sourceId); 231 } 232 startSearchingForSources(@onNull List<android.bluetooth.le.ScanFilter> filters)233 public void startSearchingForSources(@NonNull List<android.bluetooth.le.ScanFilter> filters) { 234 if (DEBUG) { 235 Log.d(TAG, "startSearchingForSources()"); 236 } 237 if (mService == null) { 238 Log.d(TAG, "The BluetoothLeBroadcastAssistant is null"); 239 return; 240 } 241 mService.startSearchingForSources(filters); 242 } 243 244 /** 245 * Return true if a search has been started by this application. 246 * 247 * @return true if a search has been started by this application 248 * @hide 249 */ isSearchInProgress()250 public boolean isSearchInProgress() { 251 if (DEBUG) { 252 Log.d(TAG, "isSearchInProgress()"); 253 } 254 if (mService == null) { 255 Log.d(TAG, "The BluetoothLeBroadcastAssistant is null"); 256 return false; 257 } 258 return mService.isSearchInProgress(); 259 } 260 261 /** 262 * Stops an ongoing search for nearby Broadcast Sources. 263 * 264 * <p>On success, {@link BluetoothLeBroadcastAssistant.Callback#onSearchStopped(int)} will be 265 * called with reason code {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST}. On failure, 266 * {@link BluetoothLeBroadcastAssistant.Callback#onSearchStopFailed(int)} will be called with 267 * reason code 268 * 269 * @throws IllegalStateException if callback was not registered 270 */ stopSearchingForSources()271 public void stopSearchingForSources() { 272 if (DEBUG) { 273 Log.d(TAG, "stopSearchingForSources()"); 274 } 275 if (mService == null) { 276 Log.d(TAG, "The BluetoothLeBroadcastAssistant is null"); 277 return; 278 } 279 mService.stopSearchingForSources(); 280 } 281 282 /** 283 * Get information about all Broadcast Sources that a Broadcast Sink knows about. 284 * 285 * @param sink Broadcast Sink from which to get all Broadcast Sources 286 * @return the list of Broadcast Receive State {@link BluetoothLeBroadcastReceiveState} stored 287 * in the Broadcast Sink 288 * @throws NullPointerException when <var>sink</var> is null 289 */ getAllSources( @onNull BluetoothDevice sink)290 public @NonNull List<BluetoothLeBroadcastReceiveState> getAllSources( 291 @NonNull BluetoothDevice sink) { 292 if (DEBUG) { 293 Log.d(TAG, "getAllSources()"); 294 } 295 if (mService == null) { 296 Log.d(TAG, "The BluetoothLeBroadcastAssistant is null"); 297 return new ArrayList<BluetoothLeBroadcastReceiveState>(); 298 } 299 return mService.getAllSources(sink); 300 } 301 302 /** 303 * Register Broadcast Assistant Callbacks to track its state and receivers 304 * 305 * @param executor Executor object for callback 306 * @param callback Callback object to be registered 307 */ registerServiceCallBack( @onNull @allbackExecutor Executor executor, @NonNull BluetoothLeBroadcastAssistant.Callback callback)308 public void registerServiceCallBack( 309 @NonNull @CallbackExecutor Executor executor, 310 @NonNull BluetoothLeBroadcastAssistant.Callback callback) { 311 if (mService == null) { 312 Log.d( 313 TAG, 314 "registerServiceCallBack failed, the BluetoothLeBroadcastAssistant is null."); 315 mCachedCallbackExecutorMap.putIfAbsent(callback, executor); 316 return; 317 } 318 319 try { 320 mService.registerCallback(executor, callback); 321 } catch (IllegalArgumentException e) { 322 Log.w(TAG, "registerServiceCallBack failed. " + e.getMessage()); 323 } 324 } 325 326 /** 327 * Unregister previously registered Broadcast Assistant Callbacks 328 * 329 * @param callback Callback object to be unregistered 330 */ unregisterServiceCallBack( @onNull BluetoothLeBroadcastAssistant.Callback callback)331 public void unregisterServiceCallBack( 332 @NonNull BluetoothLeBroadcastAssistant.Callback callback) { 333 mCachedCallbackExecutorMap.remove(callback); 334 if (mService == null) { 335 Log.d( 336 TAG, 337 "unregisterServiceCallBack failed, the BluetoothLeBroadcastAssistant is null."); 338 return; 339 } 340 341 try { 342 mService.unregisterCallback(callback); 343 } catch (IllegalArgumentException e) { 344 Log.w(TAG, "unregisterServiceCallBack failed. " + e.getMessage()); 345 } 346 } 347 isProfileReady()348 public boolean isProfileReady() { 349 return mIsProfileReady; 350 } 351 getProfileId()352 public int getProfileId() { 353 return BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT; 354 } 355 accessProfileEnabled()356 public boolean accessProfileEnabled() { 357 return false; 358 } 359 isAutoConnectable()360 public boolean isAutoConnectable() { 361 return true; 362 } 363 getConnectionStatus(BluetoothDevice device)364 public int getConnectionStatus(BluetoothDevice device) { 365 if (mService == null) { 366 return BluetoothProfile.STATE_DISCONNECTED; 367 } 368 // LE Audio Broadcasts are not connection-oriented. 369 return mService.getConnectionState(device); 370 } 371 getConnectedDevices()372 public List<BluetoothDevice> getConnectedDevices() { 373 if (mService == null) { 374 return new ArrayList<BluetoothDevice>(0); 375 } 376 return mService.getDevicesMatchingConnectionStates( 377 new int[] { 378 BluetoothProfile.STATE_CONNECTED, 379 BluetoothProfile.STATE_CONNECTING, 380 BluetoothProfile.STATE_DISCONNECTING 381 }); 382 } 383 384 /** Gets devices with matched connection states. */ getDevicesMatchingConnectionStates(@onNull int[] states)385 public List<BluetoothDevice> getDevicesMatchingConnectionStates(@NonNull int[] states) { 386 if (mService == null) { 387 return new ArrayList<BluetoothDevice>(0); 388 } 389 return mService.getDevicesMatchingConnectionStates(states); 390 } 391 isEnabled(BluetoothDevice device)392 public boolean isEnabled(BluetoothDevice device) { 393 if (mService == null || device == null) { 394 return false; 395 } 396 return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN; 397 } 398 getConnectionPolicy(BluetoothDevice device)399 public int getConnectionPolicy(BluetoothDevice device) { 400 if (mService == null || device == null) { 401 return CONNECTION_POLICY_FORBIDDEN; 402 } 403 return mService.getConnectionPolicy(device); 404 } 405 setEnabled(BluetoothDevice device, boolean enabled)406 public boolean setEnabled(BluetoothDevice device, boolean enabled) { 407 boolean isEnabled = false; 408 if (mService == null || device == null) { 409 return false; 410 } 411 if (enabled) { 412 if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) { 413 isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED); 414 } 415 } else { 416 isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN); 417 } 418 419 return isEnabled; 420 } 421 toString()422 public String toString() { 423 return NAME; 424 } 425 getOrdinal()426 public int getOrdinal() { 427 return ORDINAL; 428 } 429 getNameResource(BluetoothDevice device)430 public int getNameResource(BluetoothDevice device) { 431 return R.string.summary_empty; 432 } 433 getSummaryResourceForDevice(BluetoothDevice device)434 public int getSummaryResourceForDevice(BluetoothDevice device) { 435 int state = getConnectionStatus(device); 436 return BluetoothUtils.getConnectionStateSummary(state); 437 } 438 getDrawableResource(BluetoothClass btClass)439 public int getDrawableResource(BluetoothClass btClass) { 440 return 0; 441 } 442 443 @RequiresApi(Build.VERSION_CODES.S) finalize()444 protected void finalize() { 445 if (DEBUG) { 446 Log.d(TAG, "finalize()"); 447 } 448 if (mService != null) { 449 try { 450 BluetoothAdapter.getDefaultAdapter() 451 .closeProfileProxy(BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, mService); 452 mService = null; 453 } catch (Throwable t) { 454 Log.w(TAG, "Error cleaning up LeAudio proxy", t); 455 } 456 } 457 } 458 } 459