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