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
17/**
18 * TODO: Move this to services.jar
19 * and make the constructor package private again.
20 * @hide
21 */
22
23package android.server;
24
25import android.bluetooth.BluetoothA2dp;
26import android.bluetooth.BluetoothAdapter;
27import android.bluetooth.BluetoothDevice;
28import android.bluetooth.BluetoothProfile;
29import android.bluetooth.BluetoothUuid;
30import android.bluetooth.IBluetoothA2dp;
31import android.content.BroadcastReceiver;
32import android.content.Context;
33import android.content.Intent;
34import android.content.IntentFilter;
35import android.media.AudioManager;
36import android.os.Handler;
37import android.os.Message;
38import android.os.ParcelUuid;
39import android.os.PowerManager;
40import android.os.PowerManager.WakeLock;
41import android.provider.Settings;
42import android.util.Log;
43
44import java.io.FileDescriptor;
45import java.io.PrintWriter;
46import java.util.ArrayList;
47import java.util.HashMap;
48import java.util.List;
49
50
51public class BluetoothA2dpService extends IBluetoothA2dp.Stub {
52    private static final String TAG = "BluetoothA2dpService";
53    private static final boolean DBG = true;
54
55    public static final String BLUETOOTH_A2DP_SERVICE = "bluetooth_a2dp";
56
57    private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN;
58    private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH;
59
60    private static final String BLUETOOTH_ENABLED = "bluetooth_enabled";
61
62    private static final String PROPERTY_STATE = "State";
63
64    private final Context mContext;
65    private final IntentFilter mIntentFilter;
66    private HashMap<BluetoothDevice, Integer> mAudioDevices;
67    private final AudioManager mAudioManager;
68    private final BluetoothService mBluetoothService;
69    private final BluetoothAdapter mAdapter;
70    private int   mTargetA2dpState;
71    private BluetoothDevice mPlayingA2dpDevice;
72    private IntentBroadcastHandler mIntentBroadcastHandler;
73    private final WakeLock mWakeLock;
74
75    private static final int MSG_CONNECTION_STATE_CHANGED = 0;
76
77    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
78        @Override
79        public void onReceive(Context context, Intent intent) {
80            String action = intent.getAction();
81            BluetoothDevice device =
82                    intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
83            if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
84                int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
85                                               BluetoothAdapter.ERROR);
86                switch (state) {
87                case BluetoothAdapter.STATE_ON:
88                    onBluetoothEnable();
89                    break;
90                case BluetoothAdapter.STATE_TURNING_OFF:
91                    onBluetoothDisable();
92                    break;
93                }
94            } else if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
95                synchronized (this) {
96                    if (mAudioDevices.containsKey(device)) {
97                        int state = mAudioDevices.get(device);
98                        handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTED);
99                    }
100                }
101            } else if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) {
102                int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
103                if (streamType == AudioManager.STREAM_MUSIC) {
104                    List<BluetoothDevice> sinks = getConnectedDevices();
105
106                    if (sinks.size() != 0 && isPhoneDocked(sinks.get(0))) {
107                        String address = sinks.get(0).getAddress();
108                        int newVolLevel =
109                          intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0);
110                        int oldVolLevel =
111                          intent.getIntExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0);
112                        String path = mBluetoothService.getObjectPathFromAddress(address);
113                        if (newVolLevel > oldVolLevel) {
114                            avrcpVolumeUpNative(path);
115                        } else if (newVolLevel < oldVolLevel) {
116                            avrcpVolumeDownNative(path);
117                        }
118                    }
119                }
120            }
121        }
122    };
123
124    private boolean isPhoneDocked(BluetoothDevice device) {
125        // This works only because these broadcast intents are "sticky"
126        Intent i = mContext.registerReceiver(null, new IntentFilter(Intent.ACTION_DOCK_EVENT));
127        if (i != null) {
128            int state = i.getIntExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED);
129            if (state != Intent.EXTRA_DOCK_STATE_UNDOCKED) {
130                BluetoothDevice dockDevice = i.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
131                if (dockDevice != null && device.equals(dockDevice)) {
132                    return true;
133                }
134            }
135        }
136        return false;
137    }
138
139    public BluetoothA2dpService(Context context, BluetoothService bluetoothService) {
140        mContext = context;
141
142        PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
143        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "BluetoothA2dpService");
144
145        mIntentBroadcastHandler = new IntentBroadcastHandler();
146
147        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
148
149        mBluetoothService = bluetoothService;
150        if (mBluetoothService == null) {
151            throw new RuntimeException("Platform does not support Bluetooth");
152        }
153
154        if (!initNative()) {
155            throw new RuntimeException("Could not init BluetoothA2dpService");
156        }
157
158        mAdapter = BluetoothAdapter.getDefaultAdapter();
159
160        mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
161        mIntentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
162        mIntentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
163        mIntentFilter.addAction(AudioManager.VOLUME_CHANGED_ACTION);
164        mContext.registerReceiver(mReceiver, mIntentFilter);
165
166        mAudioDevices = new HashMap<BluetoothDevice, Integer>();
167
168        if (mBluetoothService.isEnabled())
169            onBluetoothEnable();
170        mTargetA2dpState = -1;
171        mBluetoothService.setA2dpService(this);
172    }
173
174    @Override
175    protected void finalize() throws Throwable {
176        try {
177            cleanupNative();
178        } finally {
179            super.finalize();
180        }
181    }
182
183    private int convertBluezSinkStringToState(String value) {
184        if (value.equalsIgnoreCase("disconnected"))
185            return BluetoothA2dp.STATE_DISCONNECTED;
186        if (value.equalsIgnoreCase("connecting"))
187            return BluetoothA2dp.STATE_CONNECTING;
188        if (value.equalsIgnoreCase("connected"))
189            return BluetoothA2dp.STATE_CONNECTED;
190        if (value.equalsIgnoreCase("playing"))
191            return BluetoothA2dp.STATE_PLAYING;
192        return -1;
193    }
194
195    private boolean isSinkDevice(BluetoothDevice device) {
196        ParcelUuid[] uuids = mBluetoothService.getRemoteUuids(device.getAddress());
197        if (uuids != null && BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.AudioSink)) {
198            return true;
199        }
200        return false;
201    }
202
203    private synchronized void addAudioSink(BluetoothDevice device) {
204        if (mAudioDevices.get(device) == null) {
205            mAudioDevices.put(device, BluetoothA2dp.STATE_DISCONNECTED);
206        }
207    }
208
209    private synchronized void onBluetoothEnable() {
210        String devices = mBluetoothService.getProperty("Devices", true);
211        if (devices != null) {
212            String [] paths = devices.split(",");
213            for (String path: paths) {
214                String address = mBluetoothService.getAddressFromObjectPath(path);
215                BluetoothDevice device = mAdapter.getRemoteDevice(address);
216                ParcelUuid[] remoteUuids = mBluetoothService.getRemoteUuids(address);
217                if (remoteUuids != null)
218                    if (BluetoothUuid.containsAnyUuid(remoteUuids,
219                            new ParcelUuid[] {BluetoothUuid.AudioSink,
220                                                BluetoothUuid.AdvAudioDist})) {
221                        addAudioSink(device);
222                    }
223                }
224        }
225        mAudioManager.setParameters(BLUETOOTH_ENABLED+"=true");
226        mAudioManager.setParameters("A2dpSuspended=false");
227    }
228
229    private synchronized void onBluetoothDisable() {
230        if (!mAudioDevices.isEmpty()) {
231            BluetoothDevice[] devices = new BluetoothDevice[mAudioDevices.size()];
232            devices = mAudioDevices.keySet().toArray(devices);
233            for (BluetoothDevice device : devices) {
234                int state = getConnectionState(device);
235                switch (state) {
236                    case BluetoothA2dp.STATE_CONNECTING:
237                    case BluetoothA2dp.STATE_CONNECTED:
238                    case BluetoothA2dp.STATE_PLAYING:
239                        disconnectSinkNative(mBluetoothService.getObjectPathFromAddress(
240                                device.getAddress()));
241                        handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTED);
242                        break;
243                    case BluetoothA2dp.STATE_DISCONNECTING:
244                        handleSinkStateChange(device, BluetoothA2dp.STATE_DISCONNECTING,
245                                              BluetoothA2dp.STATE_DISCONNECTED);
246                        break;
247                }
248            }
249            mAudioDevices.clear();
250        }
251
252        mAudioManager.setParameters(BLUETOOTH_ENABLED + "=false");
253    }
254
255    private synchronized boolean isConnectSinkFeasible(BluetoothDevice device) {
256        if (!mBluetoothService.isEnabled() || !isSinkDevice(device) ||
257                getPriority(device) == BluetoothA2dp.PRIORITY_OFF) {
258            return false;
259        }
260
261        addAudioSink(device);
262
263        String path = mBluetoothService.getObjectPathFromAddress(device.getAddress());
264        if (path == null) {
265            return false;
266        }
267        return true;
268    }
269
270    public synchronized boolean isA2dpPlaying(BluetoothDevice device) {
271        mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
272            "Need BLUETOOTH_ADMIN permission");
273        if (DBG) log("isA2dpPlaying(" + device + ")");
274        if (device.equals(mPlayingA2dpDevice)) return true;
275        return false;
276    }
277
278    public synchronized boolean connect(BluetoothDevice device) {
279        mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
280                                                "Need BLUETOOTH_ADMIN permission");
281        if (DBG) log("connectSink(" + device + ")");
282        if (!isConnectSinkFeasible(device)) return false;
283
284        for (BluetoothDevice sinkDevice : mAudioDevices.keySet()) {
285            if (getConnectionState(sinkDevice) != BluetoothProfile.STATE_DISCONNECTED) {
286                disconnect(sinkDevice);
287            }
288        }
289
290        return mBluetoothService.connectSink(device.getAddress());
291    }
292
293    public synchronized boolean connectSinkInternal(BluetoothDevice device) {
294        if (!mBluetoothService.isEnabled()) return false;
295
296        int state = mAudioDevices.get(device);
297
298        // ignore if there are any active sinks
299        if (getDevicesMatchingConnectionStates(new int[] {
300                BluetoothA2dp.STATE_CONNECTING,
301                BluetoothA2dp.STATE_CONNECTED,
302                BluetoothA2dp.STATE_DISCONNECTING}).size() != 0) {
303            return false;
304        }
305
306        switch (state) {
307        case BluetoothA2dp.STATE_CONNECTED:
308        case BluetoothA2dp.STATE_DISCONNECTING:
309            return false;
310        case BluetoothA2dp.STATE_CONNECTING:
311            return true;
312        }
313
314        String path = mBluetoothService.getObjectPathFromAddress(device.getAddress());
315
316        // State is DISCONNECTED and we are connecting.
317        if (getPriority(device) < BluetoothA2dp.PRIORITY_AUTO_CONNECT) {
318            setPriority(device, BluetoothA2dp.PRIORITY_AUTO_CONNECT);
319        }
320        handleSinkStateChange(device, state, BluetoothA2dp.STATE_CONNECTING);
321
322        if (!connectSinkNative(path)) {
323            // Restore previous state
324            handleSinkStateChange(device, mAudioDevices.get(device), state);
325            return false;
326        }
327        return true;
328    }
329
330    private synchronized boolean isDisconnectSinkFeasible(BluetoothDevice device) {
331        String path = mBluetoothService.getObjectPathFromAddress(device.getAddress());
332        if (path == null) {
333            return false;
334        }
335
336        int state = getConnectionState(device);
337        switch (state) {
338        case BluetoothA2dp.STATE_DISCONNECTED:
339        case BluetoothA2dp.STATE_DISCONNECTING:
340            return false;
341        }
342        return true;
343    }
344
345    public synchronized boolean disconnect(BluetoothDevice device) {
346        mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
347                                                "Need BLUETOOTH_ADMIN permission");
348        if (DBG) log("disconnectSink(" + device + ")");
349        if (!isDisconnectSinkFeasible(device)) return false;
350        return mBluetoothService.disconnectSink(device.getAddress());
351    }
352
353    public synchronized boolean disconnectSinkInternal(BluetoothDevice device) {
354        int state = getConnectionState(device);
355        String path = mBluetoothService.getObjectPathFromAddress(device.getAddress());
356
357        switch (state) {
358            case BluetoothA2dp.STATE_DISCONNECTED:
359            case BluetoothA2dp.STATE_DISCONNECTING:
360                return false;
361        }
362        // State is CONNECTING or CONNECTED or PLAYING
363        handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTING);
364        if (!disconnectSinkNative(path)) {
365            // Restore previous state
366            handleSinkStateChange(device, mAudioDevices.get(device), state);
367            return false;
368        }
369        return true;
370    }
371
372    public synchronized boolean suspendSink(BluetoothDevice device) {
373        mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
374                            "Need BLUETOOTH_ADMIN permission");
375        if (DBG) log("suspendSink(" + device + "), mTargetA2dpState: "+mTargetA2dpState);
376        if (device == null || mAudioDevices == null) {
377            return false;
378        }
379        String path = mBluetoothService.getObjectPathFromAddress(device.getAddress());
380        Integer state = mAudioDevices.get(device);
381        if (path == null || state == null) {
382            return false;
383        }
384
385        mTargetA2dpState = BluetoothA2dp.STATE_CONNECTED;
386        return checkSinkSuspendState(state.intValue());
387    }
388
389    public synchronized boolean resumeSink(BluetoothDevice device) {
390        mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
391                            "Need BLUETOOTH_ADMIN permission");
392        if (DBG) log("resumeSink(" + device + "), mTargetA2dpState: "+mTargetA2dpState);
393        if (device == null || mAudioDevices == null) {
394            return false;
395        }
396        String path = mBluetoothService.getObjectPathFromAddress(device.getAddress());
397        Integer state = mAudioDevices.get(device);
398        if (path == null || state == null) {
399            return false;
400        }
401        mTargetA2dpState = BluetoothA2dp.STATE_PLAYING;
402        return checkSinkSuspendState(state.intValue());
403    }
404
405    public synchronized int getConnectionState(BluetoothDevice device) {
406        mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
407        Integer state = mAudioDevices.get(device);
408        if (state == null)
409            return BluetoothA2dp.STATE_DISCONNECTED;
410        return state;
411    }
412
413    public synchronized List<BluetoothDevice> getConnectedDevices() {
414        mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
415        List<BluetoothDevice> sinks = getDevicesMatchingConnectionStates(
416                new int[] {BluetoothA2dp.STATE_CONNECTED});
417        return sinks;
418    }
419
420    public synchronized List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
421        mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
422        ArrayList<BluetoothDevice> sinks = new ArrayList<BluetoothDevice>();
423        for (BluetoothDevice device: mAudioDevices.keySet()) {
424            int sinkState = getConnectionState(device);
425            for (int state : states) {
426                if (state == sinkState) {
427                    sinks.add(device);
428                    break;
429                }
430            }
431        }
432        return sinks;
433    }
434
435    public synchronized int getPriority(BluetoothDevice device) {
436        mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
437        return Settings.Secure.getInt(mContext.getContentResolver(),
438                Settings.Secure.getBluetoothA2dpSinkPriorityKey(device.getAddress()),
439                BluetoothA2dp.PRIORITY_UNDEFINED);
440    }
441
442    public synchronized boolean setPriority(BluetoothDevice device, int priority) {
443        mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
444                                                "Need BLUETOOTH_ADMIN permission");
445        return Settings.Secure.putInt(mContext.getContentResolver(),
446                Settings.Secure.getBluetoothA2dpSinkPriorityKey(device.getAddress()), priority);
447    }
448
449    public synchronized boolean allowIncomingConnect(BluetoothDevice device, boolean value) {
450        mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
451                                                "Need BLUETOOTH_ADMIN permission");
452        String address = device.getAddress();
453        if (!BluetoothAdapter.checkBluetoothAddress(address)) {
454            return false;
455        }
456        Integer data = mBluetoothService.getAuthorizationAgentRequestData(address);
457        if (data == null) {
458            Log.w(TAG, "allowIncomingConnect(" + device + ") called but no native data available");
459            return false;
460        }
461        log("allowIncomingConnect: A2DP: " + device + ":" + value);
462        return mBluetoothService.setAuthorizationNative(address, value, data.intValue());
463    }
464
465    /**
466     * Called by native code on a PropertyChanged signal from
467     * org.bluez.AudioSink.
468     *
469     * @param path the object path for the changed device
470     * @param propValues a string array containing the key and one or more
471     *  values.
472     */
473    private synchronized void onSinkPropertyChanged(String path, String[] propValues) {
474        if (!mBluetoothService.isEnabled()) {
475            return;
476        }
477
478        String name = propValues[0];
479        String address = mBluetoothService.getAddressFromObjectPath(path);
480        if (address == null) {
481            Log.e(TAG, "onSinkPropertyChanged: Address of the remote device in null");
482            return;
483        }
484
485        BluetoothDevice device = mAdapter.getRemoteDevice(address);
486
487        if (name.equals(PROPERTY_STATE)) {
488            int state = convertBluezSinkStringToState(propValues[1]);
489            log("A2DP: onSinkPropertyChanged newState is: " + state + "mPlayingA2dpDevice: " + mPlayingA2dpDevice);
490
491            if (mAudioDevices.get(device) == null) {
492                // This is for an incoming connection for a device not known to us.
493                // We have authorized it and bluez state has changed.
494                addAudioSink(device);
495                handleSinkStateChange(device, BluetoothA2dp.STATE_DISCONNECTED, state);
496            } else {
497                if (state == BluetoothA2dp.STATE_PLAYING && mPlayingA2dpDevice == null) {
498                   mPlayingA2dpDevice = device;
499                   handleSinkPlayingStateChange(device, state, BluetoothA2dp.STATE_NOT_PLAYING);
500                } else if (state == BluetoothA2dp.STATE_CONNECTED && mPlayingA2dpDevice != null) {
501                    mPlayingA2dpDevice = null;
502                    handleSinkPlayingStateChange(device, BluetoothA2dp.STATE_NOT_PLAYING,
503                        BluetoothA2dp.STATE_PLAYING);
504                } else {
505                   mPlayingA2dpDevice = null;
506                   int prevState = mAudioDevices.get(device);
507                   handleSinkStateChange(device, prevState, state);
508                }
509            }
510        }
511    }
512
513    private void handleSinkStateChange(BluetoothDevice device, int prevState, int state) {
514        if (state != prevState) {
515            mAudioDevices.put(device, state);
516
517            checkSinkSuspendState(state);
518            mTargetA2dpState = -1;
519
520            if (getPriority(device) > BluetoothA2dp.PRIORITY_OFF &&
521                    state == BluetoothA2dp.STATE_CONNECTED) {
522                // We have connected or attempting to connect.
523                // Bump priority
524                setPriority(device, BluetoothA2dp.PRIORITY_AUTO_CONNECT);
525                // We will only have 1 device with AUTO_CONNECT priority
526                // To be backward compatible set everyone else to have PRIORITY_ON
527                adjustOtherSinkPriorities(device);
528            }
529
530            int delay = mAudioManager.setBluetoothA2dpDeviceConnectionState(device, state);
531
532            mWakeLock.acquire();
533            mIntentBroadcastHandler.sendMessageDelayed(mIntentBroadcastHandler.obtainMessage(
534                                                            MSG_CONNECTION_STATE_CHANGED,
535                                                            prevState,
536                                                            state,
537                                                            device),
538                                                       delay);
539        }
540    }
541
542    private void handleSinkPlayingStateChange(BluetoothDevice device, int state, int prevState) {
543        Intent intent = new Intent(BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED);
544        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
545        intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
546        intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
547        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
548        mContext.sendBroadcast(intent, BLUETOOTH_PERM);
549
550        if (DBG) log("A2DP Playing state : device: " + device + " State:" + prevState + "->" + state);
551    }
552
553    private void adjustOtherSinkPriorities(BluetoothDevice connectedDevice) {
554        for (BluetoothDevice device : mAdapter.getBondedDevices()) {
555            if (getPriority(device) >= BluetoothA2dp.PRIORITY_AUTO_CONNECT &&
556                !device.equals(connectedDevice)) {
557                setPriority(device, BluetoothA2dp.PRIORITY_ON);
558            }
559        }
560    }
561
562    private boolean checkSinkSuspendState(int state) {
563        boolean result = true;
564
565        if (state != mTargetA2dpState) {
566            if (state == BluetoothA2dp.STATE_PLAYING &&
567                mTargetA2dpState == BluetoothA2dp.STATE_CONNECTED) {
568                mAudioManager.setParameters("A2dpSuspended=true");
569            } else if (state == BluetoothA2dp.STATE_CONNECTED &&
570                mTargetA2dpState == BluetoothA2dp.STATE_PLAYING) {
571                mAudioManager.setParameters("A2dpSuspended=false");
572            } else {
573                result = false;
574            }
575        }
576        return result;
577    }
578
579    /**
580     * Called by native code for the async response to a Connect
581     * method call to org.bluez.AudioSink.
582     *
583     * @param deviceObjectPath the object path for the connecting device
584     * @param result true on success; false on error
585     */
586    private void onConnectSinkResult(String deviceObjectPath, boolean result) {
587        // If the call was a success, ignore we will update the state
588        // when we a Sink Property Change
589        if (!result) {
590            if (deviceObjectPath != null) {
591                String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath);
592                if (address == null) return;
593                BluetoothDevice device = mAdapter.getRemoteDevice(address);
594                int state = getConnectionState(device);
595                handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTED);
596            }
597        }
598    }
599
600    /** Handles A2DP connection state change intent broadcasts. */
601    private class IntentBroadcastHandler extends Handler {
602
603        private void onConnectionStateChanged(BluetoothDevice device, int prevState, int state) {
604            Intent intent = new Intent(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
605            intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
606            intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
607            intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
608            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
609            mContext.sendBroadcast(intent, BLUETOOTH_PERM);
610
611            if (DBG) log("A2DP state : device: " + device + " State:" + prevState + "->" + state);
612
613            mBluetoothService.sendConnectionStateChange(device, BluetoothProfile.A2DP, state,
614                                                        prevState);
615        }
616
617        @Override
618        public void handleMessage(Message msg) {
619            switch (msg.what) {
620                case MSG_CONNECTION_STATE_CHANGED:
621                    onConnectionStateChanged((BluetoothDevice) msg.obj, msg.arg1, msg.arg2);
622                    mWakeLock.release();
623                    break;
624            }
625        }
626    }
627
628    @Override
629    protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
630        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
631
632        if (mAudioDevices.isEmpty()) return;
633        pw.println("Cached audio devices:");
634        for (BluetoothDevice device : mAudioDevices.keySet()) {
635            int state = mAudioDevices.get(device);
636            pw.println(device + " " + BluetoothA2dp.stateToString(state));
637        }
638    }
639
640    private static void log(String msg) {
641        Log.d(TAG, msg);
642    }
643
644    private native boolean initNative();
645    private native void cleanupNative();
646    private synchronized native boolean connectSinkNative(String path);
647    private synchronized native boolean disconnectSinkNative(String path);
648    private synchronized native boolean suspendSinkNative(String path);
649    private synchronized native boolean resumeSinkNative(String path);
650    private synchronized native Object []getSinkPropertiesNative(String path);
651    private synchronized native boolean avrcpVolumeUpNative(String path);
652    private synchronized native boolean avrcpVolumeDownNative(String path);
653}
654