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