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