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