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