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