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    /**
94     * A2DP sink device is 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_PLAYING   =  10;
99
100    /**
101     * A2DP sink device is NOT streaming music. This state can be one of
102     * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of
103     * {@link #ACTION_PLAYING_STATE_CHANGED} intent.
104     */
105    public static final int STATE_NOT_PLAYING   =  11;
106
107    private Context mContext;
108    private ServiceListener mServiceListener;
109    private IBluetoothA2dp mService;
110    private BluetoothAdapter mAdapter;
111
112    final private IBluetoothStateChangeCallback mBluetoothStateChangeCallback =
113            new IBluetoothStateChangeCallback.Stub() {
114                public void onBluetoothStateChange(boolean up) {
115                    if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up);
116                    if (!up) {
117                        if (VDBG) Log.d(TAG,"Unbinding service...");
118                        synchronized (mConnection) {
119                            try {
120                                mService = null;
121                                mContext.unbindService(mConnection);
122                            } catch (Exception re) {
123                                Log.e(TAG,"",re);
124                            }
125                        }
126                    } else {
127                        synchronized (mConnection) {
128                            try {
129                                if (mService == null) {
130                                    if (VDBG) Log.d(TAG,"Binding service...");
131                                    if (!mContext.bindService(new Intent(IBluetoothA2dp.class.getName()), mConnection, 0)) {
132                                        Log.e(TAG, "Could not bind to Bluetooth A2DP Service");
133                                    }
134                                }
135                            } catch (Exception re) {
136                                Log.e(TAG,"",re);
137                            }
138                        }
139                    }
140                }
141        };
142    /**
143     * Create a BluetoothA2dp proxy object for interacting with the local
144     * Bluetooth A2DP service.
145     *
146     */
147    /*package*/ BluetoothA2dp(Context context, ServiceListener l) {
148        mContext = context;
149        mServiceListener = l;
150        mAdapter = BluetoothAdapter.getDefaultAdapter();
151        IBluetoothManager mgr = mAdapter.getBluetoothManager();
152        if (mgr != null) {
153            try {
154                mgr.registerStateChangeCallback(mBluetoothStateChangeCallback);
155            } catch (RemoteException e) {
156                Log.e(TAG,"",e);
157            }
158        }
159
160        if (!context.bindService(new Intent(IBluetoothA2dp.class.getName()), mConnection, 0)) {
161            Log.e(TAG, "Could not bind to Bluetooth A2DP Service");
162        }
163    }
164
165    /*package*/ void close() {
166        mServiceListener = null;
167        IBluetoothManager mgr = mAdapter.getBluetoothManager();
168        if (mgr != null) {
169            try {
170                mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback);
171            } catch (Exception e) {
172                Log.e(TAG,"",e);
173            }
174        }
175
176        synchronized (mConnection) {
177            if (mService != null) {
178                try {
179                    mService = null;
180                    mContext.unbindService(mConnection);
181                } catch (Exception re) {
182                    Log.e(TAG,"",re);
183                }
184            }
185        }
186    }
187
188    public void finalize() {
189        close();
190    }
191    /**
192     * Initiate connection to a profile of the remote bluetooth device.
193     *
194     * <p> Currently, the system supports only 1 connection to the
195     * A2DP profile. The API will automatically disconnect connected
196     * devices before connecting.
197     *
198     * <p> This API returns false in scenarios like the profile on the
199     * device is already connected or Bluetooth is not turned on.
200     * When this API returns true, it is guaranteed that
201     * connection state intent for the profile will be broadcasted with
202     * the state. Users can get the connection state of the profile
203     * from this intent.
204     *
205     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
206     * permission.
207     *
208     * @param device Remote Bluetooth Device
209     * @return false on immediate error,
210     *               true otherwise
211     * @hide
212     */
213    public boolean connect(BluetoothDevice device) {
214        if (DBG) log("connect(" + device + ")");
215        if (mService != null && isEnabled() &&
216            isValidDevice(device)) {
217            try {
218                return mService.connect(device);
219            } catch (RemoteException e) {
220                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
221                return false;
222            }
223        }
224        if (mService == null) Log.w(TAG, "Proxy not attached to service");
225        return false;
226    }
227
228    /**
229     * Initiate disconnection from a profile
230     *
231     * <p> This API will return false in scenarios like the profile on the
232     * Bluetooth device is not in connected state etc. When this API returns,
233     * true, it is guaranteed that the connection state change
234     * intent will be broadcasted with the state. Users can get the
235     * disconnection state of the profile from this intent.
236     *
237     * <p> If the disconnection is initiated by a remote device, the state
238     * will transition from {@link #STATE_CONNECTED} to
239     * {@link #STATE_DISCONNECTED}. If the disconnect is initiated by the
240     * host (local) device the state will transition from
241     * {@link #STATE_CONNECTED} to state {@link #STATE_DISCONNECTING} to
242     * state {@link #STATE_DISCONNECTED}. The transition to
243     * {@link #STATE_DISCONNECTING} can be used to distinguish between the
244     * two scenarios.
245     *
246     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
247     * permission.
248     *
249     * @param device Remote Bluetooth Device
250     * @return false on immediate error,
251     *               true otherwise
252     * @hide
253     */
254    public boolean disconnect(BluetoothDevice device) {
255        if (DBG) log("disconnect(" + device + ")");
256        if (mService != null && isEnabled() &&
257            isValidDevice(device)) {
258            try {
259                return mService.disconnect(device);
260            } catch (RemoteException e) {
261                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
262                return false;
263            }
264        }
265        if (mService == null) Log.w(TAG, "Proxy not attached to service");
266        return false;
267    }
268
269    /**
270     * {@inheritDoc}
271     */
272    public List<BluetoothDevice> getConnectedDevices() {
273        if (VDBG) log("getConnectedDevices()");
274        if (mService != null && isEnabled()) {
275            try {
276                return mService.getConnectedDevices();
277            } catch (RemoteException e) {
278                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
279                return new ArrayList<BluetoothDevice>();
280            }
281        }
282        if (mService == null) Log.w(TAG, "Proxy not attached to service");
283        return new ArrayList<BluetoothDevice>();
284    }
285
286    /**
287     * {@inheritDoc}
288     */
289    public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
290        if (VDBG) log("getDevicesMatchingStates()");
291        if (mService != null && isEnabled()) {
292            try {
293                return mService.getDevicesMatchingConnectionStates(states);
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 int getConnectionState(BluetoothDevice device) {
307        if (VDBG) log("getState(" + device + ")");
308        if (mService != null && isEnabled()
309            && isValidDevice(device)) {
310            try {
311                return mService.getConnectionState(device);
312            } catch (RemoteException e) {
313                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
314                return BluetoothProfile.STATE_DISCONNECTED;
315            }
316        }
317        if (mService == null) Log.w(TAG, "Proxy not attached to service");
318        return BluetoothProfile.STATE_DISCONNECTED;
319    }
320
321    /**
322     * Set priority of the profile
323     *
324     * <p> The device should already be paired.
325     *  Priority can be one of {@link #PRIORITY_ON} orgetBluetoothManager
326     * {@link #PRIORITY_OFF},
327     *
328     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
329     * permission.
330     *
331     * @param device Paired bluetooth device
332     * @param priority
333     * @return true if priority is set, false on error
334     * @hide
335     */
336    public boolean setPriority(BluetoothDevice device, int priority) {
337        if (DBG) log("setPriority(" + device + ", " + priority + ")");
338        if (mService != null && isEnabled()
339            && isValidDevice(device)) {
340            if (priority != BluetoothProfile.PRIORITY_OFF &&
341                priority != BluetoothProfile.PRIORITY_ON){
342              return false;
343            }
344            try {
345                return mService.setPriority(device, priority);
346            } catch (RemoteException e) {
347                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
348                return false;
349            }
350        }
351        if (mService == null) Log.w(TAG, "Proxy not attached to service");
352        return false;
353    }
354
355    /**
356     * Get the priority of the profile.
357     *
358     * <p> The priority can be any of:
359     * {@link #PRIORITY_AUTO_CONNECT}, {@link #PRIORITY_OFF},
360     * {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
361     *
362     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
363     *
364     * @param device Bluetooth device
365     * @return priority of the device
366     * @hide
367     */
368    public int getPriority(BluetoothDevice device) {
369        if (VDBG) log("getPriority(" + device + ")");
370        if (mService != null && isEnabled()
371            && isValidDevice(device)) {
372            try {
373                return mService.getPriority(device);
374            } catch (RemoteException e) {
375                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
376                return BluetoothProfile.PRIORITY_OFF;
377            }
378        }
379        if (mService == null) Log.w(TAG, "Proxy not attached to service");
380        return BluetoothProfile.PRIORITY_OFF;
381    }
382
383    /**
384     * Check if A2DP profile is streaming music.
385     *
386     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
387     *
388     * @param device BluetoothDevice device
389     */
390    public boolean isA2dpPlaying(BluetoothDevice device) {
391        if (mService != null && isEnabled()
392            && isValidDevice(device)) {
393            try {
394                return mService.isA2dpPlaying(device);
395            } catch (RemoteException e) {
396                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
397                return false;
398            }
399        }
400        if (mService == null) Log.w(TAG, "Proxy not attached to service");
401        return false;
402    }
403
404    /**
405     * This function checks if the remote device is an AVCRP
406     * target and thus whether we should send volume keys
407     * changes or not.
408     * @hide
409     */
410    public boolean shouldSendVolumeKeys(BluetoothDevice device) {
411        if (isEnabled() && isValidDevice(device)) {
412            ParcelUuid[] uuids = device.getUuids();
413            if (uuids == null) return false;
414
415            for (ParcelUuid uuid: uuids) {
416                if (BluetoothUuid.isAvrcpTarget(uuid)) {
417                    return true;
418                }
419            }
420        }
421        return false;
422    }
423
424    /**
425     * Helper for converting a state to a string.
426     *
427     * For debug use only - strings are not internationalized.
428     * @hide
429     */
430    public static String stateToString(int state) {
431        switch (state) {
432        case STATE_DISCONNECTED:
433            return "disconnected";
434        case STATE_CONNECTING:
435            return "connecting";
436        case STATE_CONNECTED:
437            return "connected";
438        case STATE_DISCONNECTING:
439            return "disconnecting";
440        case STATE_PLAYING:
441            return "playing";
442        case STATE_NOT_PLAYING:
443          return "not playing";
444        default:
445            return "<unknown state " + state + ">";
446        }
447    }
448
449    private ServiceConnection mConnection = new ServiceConnection() {
450        public void onServiceConnected(ComponentName className, IBinder service) {
451            if (DBG) Log.d(TAG, "Proxy object connected");
452            mService = IBluetoothA2dp.Stub.asInterface(service);
453
454            if (mServiceListener != null) {
455                mServiceListener.onServiceConnected(BluetoothProfile.A2DP, BluetoothA2dp.this);
456            }
457        }
458        public void onServiceDisconnected(ComponentName className) {
459            if (DBG) Log.d(TAG, "Proxy object disconnected");
460            mService = null;
461            if (mServiceListener != null) {
462                mServiceListener.onServiceDisconnected(BluetoothProfile.A2DP);
463            }
464        }
465    };
466
467    private boolean isEnabled() {
468       if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true;
469       return false;
470    }
471
472    private boolean isValidDevice(BluetoothDevice device) {
473       if (device == null) return false;
474
475       if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true;
476       return false;
477    }
478
479    private static void log(String msg) {
480      Log.d(TAG, msg);
481    }
482}
483