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