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