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