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