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