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