• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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