1 /* 2 * Copyright (C) 2014 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 android.bluetooth; 18 19 import static android.bluetooth.BluetoothUtils.getSyncTimeout; 20 21 import android.Manifest; 22 import android.annotation.NonNull; 23 import android.annotation.RequiresPermission; 24 import android.annotation.SdkConstant; 25 import android.annotation.SdkConstant.SdkConstantType; 26 import android.annotation.SuppressLint; 27 import android.annotation.SystemApi; 28 import android.bluetooth.annotations.RequiresBluetoothConnectPermission; 29 import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission; 30 import android.bluetooth.annotations.RequiresLegacyBluetoothPermission; 31 import android.compat.annotation.UnsupportedAppUsage; 32 import android.content.AttributionSource; 33 import android.content.Context; 34 import android.os.Build; 35 import android.os.IBinder; 36 import android.os.RemoteException; 37 import android.util.Log; 38 39 import com.android.modules.utils.SynchronousResultReceiver; 40 41 import java.util.ArrayList; 42 import java.util.List; 43 import java.util.concurrent.TimeoutException; 44 45 /** 46 * This class provides the public APIs to control the Bluetooth A2DP Sink 47 * profile. 48 * 49 * <p>BluetoothA2dpSink is a proxy object for controlling the Bluetooth A2DP Sink 50 * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get 51 * the BluetoothA2dpSink proxy object. 52 * 53 * @hide 54 */ 55 @SystemApi 56 public final class BluetoothA2dpSink implements BluetoothProfile { 57 private static final String TAG = "BluetoothA2dpSink"; 58 private static final boolean DBG = true; 59 private static final boolean VDBG = false; 60 61 /** 62 * Intent used to broadcast the change in connection state of the A2DP Sink 63 * profile. 64 * 65 * <p>This intent will have 3 extras: 66 * <ul> 67 * <li> {@link #EXTRA_STATE} - The current state of the profile. </li> 68 * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.</li> 69 * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li> 70 * </ul> 71 * 72 * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of 73 * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, 74 * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}. 75 * 76 * @hide 77 */ 78 @SystemApi 79 @SuppressLint("ActionValue") 80 @RequiresBluetoothConnectPermission 81 @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) 82 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 83 public static final String ACTION_CONNECTION_STATE_CHANGED = 84 "android.bluetooth.a2dp-sink.profile.action.CONNECTION_STATE_CHANGED"; 85 86 private final BluetoothAdapter mAdapter; 87 private final AttributionSource mAttributionSource; 88 private final BluetoothProfileConnector<IBluetoothA2dpSink> mProfileConnector = 89 new BluetoothProfileConnector(this, BluetoothProfile.A2DP_SINK, 90 "BluetoothA2dpSink", IBluetoothA2dpSink.class.getName()) { 91 @Override 92 public IBluetoothA2dpSink getServiceInterface(IBinder service) { 93 return IBluetoothA2dpSink.Stub.asInterface(service); 94 } 95 }; 96 97 /** 98 * Create a BluetoothA2dp proxy object for interacting with the local 99 * Bluetooth A2DP service. 100 */ BluetoothA2dpSink(Context context, ServiceListener listener, BluetoothAdapter adapter)101 /* package */ BluetoothA2dpSink(Context context, ServiceListener listener, 102 BluetoothAdapter adapter) { 103 mAdapter = adapter; 104 mAttributionSource = adapter.getAttributionSource(); 105 mProfileConnector.connect(context, listener); 106 } 107 close()108 /*package*/ void close() { 109 mProfileConnector.disconnect(); 110 } 111 getService()112 private IBluetoothA2dpSink getService() { 113 return mProfileConnector.getService(); 114 } 115 116 @Override finalize()117 public void finalize() { 118 close(); 119 } 120 121 /** 122 * Initiate connection to a profile of the remote bluetooth device. 123 * 124 * <p> Currently, the system supports only 1 connection to the 125 * A2DP profile. The API will automatically disconnect connected 126 * devices before connecting. 127 * 128 * <p> This API returns false in scenarios like the profile on the 129 * device is already connected or Bluetooth is not turned on. 130 * When this API returns true, it is guaranteed that 131 * connection state intent for the profile will be broadcasted with 132 * the state. Users can get the connection state of the profile 133 * from this intent. 134 * 135 * @param device Remote Bluetooth Device 136 * @return false on immediate error, true otherwise 137 * @hide 138 */ 139 @RequiresBluetoothConnectPermission 140 @RequiresPermission(allOf = { 141 android.Manifest.permission.BLUETOOTH_CONNECT, 142 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 143 }) connect(BluetoothDevice device)144 public boolean connect(BluetoothDevice device) { 145 if (DBG) log("connect(" + device + ")"); 146 final IBluetoothA2dpSink service = getService(); 147 final boolean defaultValue = false; 148 if (service == null) { 149 Log.w(TAG, "Proxy not attached to service"); 150 if (DBG) log(Log.getStackTraceString(new Throwable())); 151 } else if (isEnabled() && isValidDevice(device)) { 152 try { 153 final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get(); 154 service.connect(device, mAttributionSource, recv); 155 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); 156 } catch (RemoteException | TimeoutException e) { 157 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 158 } 159 } 160 return defaultValue; 161 } 162 163 /** 164 * Initiate disconnection from a profile 165 * 166 * <p> This API will return false in scenarios like the profile on the 167 * Bluetooth device is not in connected state etc. When this API returns, 168 * true, it is guaranteed that the connection state change 169 * intent will be broadcasted with the state. Users can get the 170 * disconnection state of the profile from this intent. 171 * 172 * <p> If the disconnection is initiated by a remote device, the state 173 * will transition from {@link #STATE_CONNECTED} to 174 * {@link #STATE_DISCONNECTED}. If the disconnect is initiated by the 175 * host (local) device the state will transition from 176 * {@link #STATE_CONNECTED} to state {@link #STATE_DISCONNECTING} to 177 * state {@link #STATE_DISCONNECTED}. The transition to 178 * {@link #STATE_DISCONNECTING} can be used to distinguish between the 179 * two scenarios. 180 * 181 * @param device Remote Bluetooth Device 182 * @return false on immediate error, true otherwise 183 * @hide 184 */ 185 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 186 @RequiresLegacyBluetoothAdminPermission 187 @RequiresBluetoothConnectPermission 188 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) disconnect(BluetoothDevice device)189 public boolean disconnect(BluetoothDevice device) { 190 if (DBG) log("disconnect(" + device + ")"); 191 final IBluetoothA2dpSink service = getService(); 192 final boolean defaultValue = false; 193 if (service == null) { 194 Log.w(TAG, "Proxy not attached to service"); 195 if (DBG) log(Log.getStackTraceString(new Throwable())); 196 } else if (isEnabled() && isValidDevice(device)) { 197 try { 198 final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get(); 199 service.disconnect(device, mAttributionSource, recv); 200 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); 201 } catch (RemoteException | TimeoutException e) { 202 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 203 } 204 } 205 return defaultValue; 206 } 207 208 /** 209 * {@inheritDoc} 210 * 211 * @hide 212 */ 213 @Override 214 @RequiresBluetoothConnectPermission 215 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getConnectedDevices()216 public List<BluetoothDevice> getConnectedDevices() { 217 if (VDBG) log("getConnectedDevices()"); 218 final IBluetoothA2dpSink service = getService(); 219 final List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>(); 220 if (service == null) { 221 Log.w(TAG, "Proxy not attached to service"); 222 if (DBG) log(Log.getStackTraceString(new Throwable())); 223 } else if (isEnabled()) { 224 try { 225 final SynchronousResultReceiver<List<BluetoothDevice>> recv = 226 SynchronousResultReceiver.get(); 227 service.getConnectedDevices(mAttributionSource, recv); 228 return Attributable.setAttributionSource( 229 recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue), 230 mAttributionSource); 231 } catch (RemoteException | TimeoutException e) { 232 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 233 } 234 } 235 return defaultValue; 236 } 237 238 /** 239 * {@inheritDoc} 240 * 241 * @hide 242 */ 243 @Override 244 @RequiresBluetoothConnectPermission 245 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getDevicesMatchingConnectionStates(int[] states)246 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 247 if (VDBG) log("getDevicesMatchingStates()"); 248 final IBluetoothA2dpSink service = getService(); 249 final List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>(); 250 if (service == null) { 251 Log.w(TAG, "Proxy not attached to service"); 252 if (DBG) log(Log.getStackTraceString(new Throwable())); 253 } else if (isEnabled()) { 254 try { 255 final SynchronousResultReceiver<List<BluetoothDevice>> recv = 256 SynchronousResultReceiver.get(); 257 service.getDevicesMatchingConnectionStates(states, mAttributionSource, recv); 258 return Attributable.setAttributionSource( 259 recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue), 260 mAttributionSource); 261 } catch (RemoteException | TimeoutException e) { 262 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 263 } 264 } 265 return defaultValue; 266 } 267 268 /** 269 * {@inheritDoc} 270 * 271 * @hide 272 */ 273 @Override 274 @RequiresBluetoothConnectPermission 275 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getConnectionState(BluetoothDevice device)276 public int getConnectionState(BluetoothDevice device) { 277 if (VDBG) log("getConnectionState(" + device + ")"); 278 final IBluetoothA2dpSink service = getService(); 279 final int defaultValue = BluetoothProfile.STATE_DISCONNECTED; 280 if (service == null) { 281 Log.w(TAG, "Proxy not attached to service"); 282 if (DBG) log(Log.getStackTraceString(new Throwable())); 283 } else if (isEnabled() && isValidDevice(device)) { 284 try { 285 final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get(); 286 service.getConnectionState(device, mAttributionSource, recv); 287 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); 288 } catch (RemoteException | TimeoutException e) { 289 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 290 } 291 } 292 return defaultValue; 293 } 294 295 /** 296 * Get the current audio configuration for the A2DP source device, 297 * or null if the device has no audio configuration 298 * 299 * @param device Remote bluetooth device. 300 * @return audio configuration for the device, or null 301 * 302 * {@see BluetoothAudioConfig} 303 * 304 * @hide 305 */ 306 @RequiresLegacyBluetoothPermission 307 @RequiresBluetoothConnectPermission 308 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getAudioConfig(BluetoothDevice device)309 public BluetoothAudioConfig getAudioConfig(BluetoothDevice device) { 310 if (VDBG) log("getAudioConfig(" + device + ")"); 311 final IBluetoothA2dpSink service = getService(); 312 final BluetoothAudioConfig defaultValue = null; 313 if (service == null) { 314 Log.w(TAG, "Proxy not attached to service"); 315 if (DBG) log(Log.getStackTraceString(new Throwable())); 316 } else if (isEnabled() && isValidDevice(device)) { 317 try { 318 final SynchronousResultReceiver<BluetoothAudioConfig> recv = 319 SynchronousResultReceiver.get(); 320 service.getAudioConfig(device, mAttributionSource, recv); 321 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); 322 } catch (RemoteException | TimeoutException e) { 323 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 324 } 325 } 326 return defaultValue; 327 } 328 329 /** 330 * Set priority of the profile 331 * 332 * <p> The device should already be paired. 333 * Priority can be one of {@link #PRIORITY_ON} or {@link #PRIORITY_OFF} 334 * 335 * @param device Paired bluetooth device 336 * @param priority 337 * @return true if priority is set, false on error 338 * @hide 339 */ 340 @RequiresBluetoothConnectPermission 341 @RequiresPermission(allOf = { 342 android.Manifest.permission.BLUETOOTH_CONNECT, 343 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 344 }) setPriority(BluetoothDevice device, int priority)345 public boolean setPriority(BluetoothDevice device, int priority) { 346 if (DBG) log("setPriority(" + device + ", " + priority + ")"); 347 return setConnectionPolicy(device, BluetoothAdapter.priorityToConnectionPolicy(priority)); 348 } 349 350 /** 351 * Set connection policy of the profile 352 * 353 * <p> The device should already be paired. 354 * Connection policy can be one of {@link #CONNECTION_POLICY_ALLOWED}, 355 * {@link #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN} 356 * 357 * @param device Paired bluetooth device 358 * @param connectionPolicy is the connection policy to set to for this profile 359 * @return true if connectionPolicy is set, false on error 360 * @hide 361 */ 362 @SystemApi 363 @RequiresBluetoothConnectPermission 364 @RequiresPermission(allOf = { 365 android.Manifest.permission.BLUETOOTH_CONNECT, 366 android.Manifest.permission.BLUETOOTH_PRIVILEGED 367 }) setConnectionPolicy(@onNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy)368 public boolean setConnectionPolicy(@NonNull BluetoothDevice device, 369 @ConnectionPolicy int connectionPolicy) { 370 if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")"); 371 final IBluetoothA2dpSink service = getService(); 372 final boolean defaultValue = false; 373 if (service == null) { 374 Log.w(TAG, "Proxy not attached to service"); 375 if (DBG) log(Log.getStackTraceString(new Throwable())); 376 } else if (isEnabled() && isValidDevice(device) 377 && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN 378 || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) { 379 try { 380 final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get(); 381 service.setConnectionPolicy(device, connectionPolicy, mAttributionSource, recv); 382 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); 383 } catch (RemoteException | TimeoutException e) { 384 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 385 } 386 } 387 return defaultValue; 388 } 389 390 /** 391 * Get the priority of the profile. 392 * 393 * <p> The priority can be any of: 394 * {@link #PRIORITY_OFF}, {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED} 395 * 396 * @param device Bluetooth device 397 * @return priority of the device 398 * @hide 399 */ 400 @RequiresBluetoothConnectPermission 401 @RequiresPermission(allOf = { 402 android.Manifest.permission.BLUETOOTH_CONNECT, 403 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 404 }) getPriority(BluetoothDevice device)405 public int getPriority(BluetoothDevice device) { 406 if (VDBG) log("getPriority(" + device + ")"); 407 return BluetoothAdapter.connectionPolicyToPriority(getConnectionPolicy(device)); 408 } 409 410 /** 411 * Get the connection policy of the profile. 412 * 413 * <p> The connection policy can be any of: 414 * {@link #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN}, 415 * {@link #CONNECTION_POLICY_UNKNOWN} 416 * 417 * @param device Bluetooth device 418 * @return connection policy of the device 419 * @hide 420 */ 421 @SystemApi 422 @RequiresBluetoothConnectPermission 423 @RequiresPermission(allOf = { 424 android.Manifest.permission.BLUETOOTH_CONNECT, 425 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 426 }) getConnectionPolicy(@onNull BluetoothDevice device)427 public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) { 428 if (VDBG) log("getConnectionPolicy(" + device + ")"); 429 final IBluetoothA2dpSink service = getService(); 430 final int defaultValue = BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; 431 if (service == null) { 432 Log.w(TAG, "Proxy not attached to service"); 433 if (DBG) log(Log.getStackTraceString(new Throwable())); 434 } else if (isEnabled() && isValidDevice(device)) { 435 try { 436 final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get(); 437 service.getConnectionPolicy(device, mAttributionSource, recv); 438 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); 439 } catch (RemoteException | TimeoutException e) { 440 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 441 } 442 } 443 return defaultValue; 444 } 445 446 /** 447 * Check if audio is playing on the bluetooth device (A2DP profile is streaming music). 448 * 449 * @param device BluetoothDevice device 450 * @return true if audio is playing (A2dp is streaming music), false otherwise 451 * 452 * @hide 453 */ 454 @SystemApi 455 @RequiresBluetoothConnectPermission 456 @RequiresPermission(allOf = { 457 android.Manifest.permission.BLUETOOTH_CONNECT, 458 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 459 }) isAudioPlaying(@onNull BluetoothDevice device)460 public boolean isAudioPlaying(@NonNull BluetoothDevice device) { 461 if (VDBG) log("isAudioPlaying(" + device + ")"); 462 final IBluetoothA2dpSink service = getService(); 463 final boolean defaultValue = false; 464 if (service == null) { 465 Log.w(TAG, "Proxy not attached to service"); 466 if (DBG) log(Log.getStackTraceString(new Throwable())); 467 } else if (isEnabled() && isValidDevice(device)) { 468 try { 469 final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get(); 470 service.isA2dpPlaying(device, mAttributionSource, recv); 471 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); 472 } catch (RemoteException | TimeoutException e) { 473 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 474 } 475 } 476 return defaultValue; 477 } 478 479 /** 480 * Helper for converting a state to a string. 481 * 482 * For debug use only - strings are not internationalized. 483 * 484 * @hide 485 */ stateToString(int state)486 public static String stateToString(int state) { 487 switch (state) { 488 case STATE_DISCONNECTED: 489 return "disconnected"; 490 case STATE_CONNECTING: 491 return "connecting"; 492 case STATE_CONNECTED: 493 return "connected"; 494 case STATE_DISCONNECTING: 495 return "disconnecting"; 496 case BluetoothA2dp.STATE_PLAYING: 497 return "playing"; 498 case BluetoothA2dp.STATE_NOT_PLAYING: 499 return "not playing"; 500 default: 501 return "<unknown state " + state + ">"; 502 } 503 } 504 isEnabled()505 private boolean isEnabled() { 506 return mAdapter.getState() == BluetoothAdapter.STATE_ON; 507 } 508 isValidDevice(BluetoothDevice device)509 private static boolean isValidDevice(BluetoothDevice device) { 510 return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress()); 511 } 512 log(String msg)513 private static void log(String msg) { 514 Log.d(TAG, msg); 515 } 516 } 517