• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
23 package android.server;
24 
25 import android.bluetooth.BluetoothA2dp;
26 import android.bluetooth.BluetoothAdapter;
27 import android.bluetooth.BluetoothDevice;
28 import android.bluetooth.BluetoothUuid;
29 import android.bluetooth.IBluetoothA2dp;
30 import android.os.ParcelUuid;
31 import android.content.BroadcastReceiver;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.content.IntentFilter;
35 import android.media.AudioManager;
36 import android.os.Handler;
37 import android.os.Message;
38 import android.provider.Settings;
39 import android.util.Log;
40 
41 import java.io.FileDescriptor;
42 import java.io.PrintWriter;
43 import java.util.HashMap;
44 import java.util.HashSet;
45 import java.util.Set;
46 
47 public 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 int MESSAGE_CONNECT_TO = 1;
59 
60     private static final String PROPERTY_STATE = "State";
61 
62     private static final String SINK_STATE_DISCONNECTED = "disconnected";
63     private static final String SINK_STATE_CONNECTING = "connecting";
64     private static final String SINK_STATE_CONNECTED = "connected";
65     private static final String SINK_STATE_PLAYING = "playing";
66 
67     private static int mSinkCount;
68 
69     private final Context mContext;
70     private final IntentFilter mIntentFilter;
71     private HashMap<BluetoothDevice, Integer> mAudioDevices;
72     private final AudioManager mAudioManager;
73     private final BluetoothService mBluetoothService;
74     private final BluetoothAdapter mAdapter;
75     private int   mTargetA2dpState;
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_BOND_STATE_CHANGED)) {
95                 int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
96                                                    BluetoothDevice.ERROR);
97                 switch(bondState) {
98                 case BluetoothDevice.BOND_BONDED:
99                     setSinkPriority(device, BluetoothA2dp.PRIORITY_AUTO);
100                     break;
101                 case BluetoothDevice.BOND_BONDING:
102                 case BluetoothDevice.BOND_NONE:
103                     setSinkPriority(device, BluetoothA2dp.PRIORITY_OFF);
104                     break;
105                 }
106             } else if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) {
107                 if (getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF &&
108                         isSinkDevice(device)) {
109                     // This device is a preferred sink. Make an A2DP connection
110                     // after a delay. We delay to avoid connection collisions,
111                     // and to give other profiles such as HFP a chance to
112                     // connect first.
113                     Message msg = Message.obtain(mHandler, MESSAGE_CONNECT_TO, device);
114                     mHandler.sendMessageDelayed(msg, 6000);
115                 }
116             } else if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
117                 synchronized (this) {
118                     if (mAudioDevices.containsKey(device)) {
119                         int state = mAudioDevices.get(device);
120                         handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTED);
121                     }
122                 }
123             }
124         }
125     };
126 
BluetoothA2dpService(Context context, BluetoothService bluetoothService)127     public BluetoothA2dpService(Context context, BluetoothService bluetoothService) {
128         mContext = context;
129 
130         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
131 
132         mBluetoothService = bluetoothService;
133         if (mBluetoothService == null) {
134             throw new RuntimeException("Platform does not support Bluetooth");
135         }
136 
137         if (!initNative()) {
138             throw new RuntimeException("Could not init BluetoothA2dpService");
139         }
140 
141         mAdapter = BluetoothAdapter.getDefaultAdapter();
142 
143         mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
144         mIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
145         mIntentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
146         mIntentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
147         mContext.registerReceiver(mReceiver, mIntentFilter);
148 
149         mAudioDevices = new HashMap<BluetoothDevice, Integer>();
150 
151         if (mBluetoothService.isEnabled())
152             onBluetoothEnable();
153         mTargetA2dpState = -1;
154     }
155 
156     @Override
finalize()157     protected void finalize() throws Throwable {
158         try {
159             cleanupNative();
160         } finally {
161             super.finalize();
162         }
163     }
164 
165     private final Handler mHandler = new Handler() {
166         @Override
167         public void handleMessage(Message msg) {
168             switch (msg.what) {
169             case MESSAGE_CONNECT_TO:
170                 BluetoothDevice device = (BluetoothDevice) msg.obj;
171                 // check bluetooth is still on, device is still preferred, and
172                 // nothing is currently connected
173                 if (mBluetoothService.isEnabled() &&
174                         getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF &&
175                         lookupSinksMatchingStates(new int[] {
176                             BluetoothA2dp.STATE_CONNECTING,
177                             BluetoothA2dp.STATE_CONNECTED,
178                             BluetoothA2dp.STATE_PLAYING,
179                             BluetoothA2dp.STATE_DISCONNECTING}).size() == 0) {
180                     log("Auto-connecting A2DP to sink " + device);
181                     connectSink(device);
182                 }
183                 break;
184             }
185         }
186     };
187 
convertBluezSinkStringtoState(String value)188     private int convertBluezSinkStringtoState(String value) {
189         if (value.equalsIgnoreCase("disconnected"))
190             return BluetoothA2dp.STATE_DISCONNECTED;
191         if (value.equalsIgnoreCase("connecting"))
192             return BluetoothA2dp.STATE_CONNECTING;
193         if (value.equalsIgnoreCase("connected"))
194             return BluetoothA2dp.STATE_CONNECTED;
195         if (value.equalsIgnoreCase("playing"))
196             return BluetoothA2dp.STATE_PLAYING;
197         return -1;
198     }
199 
isSinkDevice(BluetoothDevice device)200     private boolean isSinkDevice(BluetoothDevice device) {
201         ParcelUuid[] uuids = mBluetoothService.getRemoteUuids(device.getAddress());
202         if (uuids != null && BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.AudioSink)) {
203             return true;
204         }
205         return false;
206     }
207 
addAudioSink(BluetoothDevice device)208     private synchronized boolean addAudioSink (BluetoothDevice device) {
209         String path = mBluetoothService.getObjectPathFromAddress(device.getAddress());
210         String propValues[] = (String []) getSinkPropertiesNative(path);
211         if (propValues == null) {
212             Log.e(TAG, "Error while getting AudioSink properties for device: " + device);
213             return false;
214         }
215         Integer state = null;
216         // Properties are name-value pairs
217         for (int i = 0; i < propValues.length; i+=2) {
218             if (propValues[i].equals(PROPERTY_STATE)) {
219                 state = new Integer(convertBluezSinkStringtoState(propValues[i+1]));
220                 break;
221             }
222         }
223         mAudioDevices.put(device, state);
224         handleSinkStateChange(device, BluetoothA2dp.STATE_DISCONNECTED, state);
225         return true;
226     }
227 
onBluetoothEnable()228     private synchronized void onBluetoothEnable() {
229         String devices = mBluetoothService.getProperty("Devices");
230         mSinkCount = 0;
231         if (devices != null) {
232             String [] paths = devices.split(",");
233             for (String path: paths) {
234                 String address = mBluetoothService.getAddressFromObjectPath(path);
235                 BluetoothDevice device = mAdapter.getRemoteDevice(address);
236                 ParcelUuid[] remoteUuids = mBluetoothService.getRemoteUuids(address);
237                 if (remoteUuids != null)
238                     if (BluetoothUuid.containsAnyUuid(remoteUuids,
239                             new ParcelUuid[] {BluetoothUuid.AudioSink,
240                                                 BluetoothUuid.AdvAudioDist})) {
241                         addAudioSink(device);
242                     }
243                 }
244         }
245         mAudioManager.setParameters(BLUETOOTH_ENABLED+"=true");
246         mAudioManager.setParameters("A2dpSuspended=false");
247     }
248 
onBluetoothDisable()249     private synchronized void onBluetoothDisable() {
250         if (!mAudioDevices.isEmpty()) {
251             BluetoothDevice[] devices = new BluetoothDevice[mAudioDevices.size()];
252             devices = mAudioDevices.keySet().toArray(devices);
253             for (BluetoothDevice device : devices) {
254                 int state = getSinkState(device);
255                 switch (state) {
256                     case BluetoothA2dp.STATE_CONNECTING:
257                     case BluetoothA2dp.STATE_CONNECTED:
258                     case BluetoothA2dp.STATE_PLAYING:
259                         disconnectSinkNative(mBluetoothService.getObjectPathFromAddress(
260                                 device.getAddress()));
261                         handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTED);
262                         break;
263                     case BluetoothA2dp.STATE_DISCONNECTING:
264                         handleSinkStateChange(device, BluetoothA2dp.STATE_DISCONNECTING,
265                                               BluetoothA2dp.STATE_DISCONNECTED);
266                         break;
267                 }
268             }
269             mAudioDevices.clear();
270         }
271 
272         mAudioManager.setParameters(BLUETOOTH_ENABLED + "=false");
273     }
274 
connectSink(BluetoothDevice device)275     public synchronized boolean connectSink(BluetoothDevice device) {
276         mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
277                                                 "Need BLUETOOTH_ADMIN permission");
278         if (DBG) log("connectSink(" + device + ")");
279 
280         // ignore if there are any active sinks
281         if (lookupSinksMatchingStates(new int[] {
282                 BluetoothA2dp.STATE_CONNECTING,
283                 BluetoothA2dp.STATE_CONNECTED,
284                 BluetoothA2dp.STATE_PLAYING,
285                 BluetoothA2dp.STATE_DISCONNECTING}).size() != 0) {
286             return false;
287         }
288 
289         if (mAudioDevices.get(device) == null && !addAudioSink(device))
290             return false;
291 
292         int state = mAudioDevices.get(device);
293 
294         switch (state) {
295         case BluetoothA2dp.STATE_CONNECTED:
296         case BluetoothA2dp.STATE_PLAYING:
297         case BluetoothA2dp.STATE_DISCONNECTING:
298             return false;
299         case BluetoothA2dp.STATE_CONNECTING:
300             return true;
301         }
302 
303         String path = mBluetoothService.getObjectPathFromAddress(device.getAddress());
304         if (path == null)
305             return false;
306 
307         // State is DISCONNECTED
308         if (!connectSinkNative(path)) {
309             return false;
310         }
311         return true;
312     }
313 
disconnectSink(BluetoothDevice device)314     public synchronized boolean disconnectSink(BluetoothDevice device) {
315         mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
316                                                 "Need BLUETOOTH_ADMIN permission");
317         if (DBG) log("disconnectSink(" + device + ")");
318 
319         String path = mBluetoothService.getObjectPathFromAddress(device.getAddress());
320         if (path == null) {
321             return false;
322         }
323 
324         switch (getSinkState(device)) {
325         case BluetoothA2dp.STATE_DISCONNECTED:
326             return false;
327         case BluetoothA2dp.STATE_DISCONNECTING:
328             return true;
329         }
330 
331         // State is CONNECTING or CONNECTED or PLAYING
332         if (!disconnectSinkNative(path)) {
333             return false;
334         } else {
335             return true;
336         }
337     }
338 
suspendSink(BluetoothDevice device)339     public synchronized boolean suspendSink(BluetoothDevice device) {
340         mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
341                             "Need BLUETOOTH_ADMIN permission");
342         if (DBG) log("suspendSink(" + device + "), mTargetA2dpState: "+mTargetA2dpState);
343         if (device == null || mAudioDevices == null) {
344             return false;
345         }
346         String path = mBluetoothService.getObjectPathFromAddress(device.getAddress());
347         Integer state = mAudioDevices.get(device);
348         if (path == null || state == null) {
349             return false;
350         }
351 
352         mTargetA2dpState = BluetoothA2dp.STATE_CONNECTED;
353         return checkSinkSuspendState(state.intValue());
354     }
355 
resumeSink(BluetoothDevice device)356     public synchronized boolean resumeSink(BluetoothDevice device) {
357         mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
358                             "Need BLUETOOTH_ADMIN permission");
359         if (DBG) log("resumeSink(" + device + "), mTargetA2dpState: "+mTargetA2dpState);
360         if (device == null || mAudioDevices == null) {
361             return false;
362         }
363         String path = mBluetoothService.getObjectPathFromAddress(device.getAddress());
364         Integer state = mAudioDevices.get(device);
365         if (path == null || state == null) {
366             return false;
367         }
368         mTargetA2dpState = BluetoothA2dp.STATE_PLAYING;
369         return checkSinkSuspendState(state.intValue());
370     }
371 
getConnectedSinks()372     public synchronized BluetoothDevice[] getConnectedSinks() {
373         mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
374         Set<BluetoothDevice> sinks = lookupSinksMatchingStates(
375                 new int[] {BluetoothA2dp.STATE_CONNECTED, BluetoothA2dp.STATE_PLAYING});
376         return sinks.toArray(new BluetoothDevice[sinks.size()]);
377     }
378 
getSinkState(BluetoothDevice device)379     public synchronized int getSinkState(BluetoothDevice device) {
380         mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
381         Integer state = mAudioDevices.get(device);
382         if (state == null)
383             return BluetoothA2dp.STATE_DISCONNECTED;
384         return state;
385     }
386 
getSinkPriority(BluetoothDevice device)387     public synchronized int getSinkPriority(BluetoothDevice device) {
388         mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
389         return Settings.Secure.getInt(mContext.getContentResolver(),
390                 Settings.Secure.getBluetoothA2dpSinkPriorityKey(device.getAddress()),
391                 BluetoothA2dp.PRIORITY_OFF);
392     }
393 
setSinkPriority(BluetoothDevice device, int priority)394     public synchronized boolean setSinkPriority(BluetoothDevice device, int priority) {
395         mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
396                                                 "Need BLUETOOTH_ADMIN permission");
397         if (!BluetoothAdapter.checkBluetoothAddress(device.getAddress())) {
398             return false;
399         }
400         return Settings.Secure.putInt(mContext.getContentResolver(),
401                 Settings.Secure.getBluetoothA2dpSinkPriorityKey(device.getAddress()), priority);
402     }
403 
onSinkPropertyChanged(String path, String []propValues)404     private synchronized void onSinkPropertyChanged(String path, String []propValues) {
405         if (!mBluetoothService.isEnabled()) {
406             return;
407         }
408 
409         String name = propValues[0];
410         String address = mBluetoothService.getAddressFromObjectPath(path);
411         if (address == null) {
412             Log.e(TAG, "onSinkPropertyChanged: Address of the remote device in null");
413             return;
414         }
415 
416         BluetoothDevice device = mAdapter.getRemoteDevice(address);
417 
418         if (name.equals(PROPERTY_STATE)) {
419             int state = convertBluezSinkStringtoState(propValues[1]);
420             if (mAudioDevices.get(device) == null) {
421                 // This is for an incoming connection for a device not known to us.
422                 // We have authorized it and bluez state has changed.
423                 addAudioSink(device);
424             } else {
425                 int prevState = mAudioDevices.get(device);
426                 handleSinkStateChange(device, prevState, state);
427             }
428         }
429     }
430 
handleSinkStateChange(BluetoothDevice device, int prevState, int state)431     private void handleSinkStateChange(BluetoothDevice device, int prevState, int state) {
432         if (state != prevState) {
433             if (state == BluetoothA2dp.STATE_DISCONNECTED ||
434                     state == BluetoothA2dp.STATE_DISCONNECTING) {
435                 if (prevState == BluetoothA2dp.STATE_CONNECTED ||
436                         prevState == BluetoothA2dp.STATE_PLAYING) {
437                    // disconnecting or disconnected
438                    Intent intent = new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
439                    mContext.sendBroadcast(intent);
440                 }
441                 mSinkCount--;
442             } else if (state == BluetoothA2dp.STATE_CONNECTED) {
443                 mSinkCount ++;
444             }
445             mAudioDevices.put(device, state);
446 
447             checkSinkSuspendState(state);
448             mTargetA2dpState = -1;
449 
450             if (state == BluetoothA2dp.STATE_CONNECTING) {
451                 mAudioManager.setParameters("A2dpSuspended=false");
452             }
453             Intent intent = new Intent(BluetoothA2dp.ACTION_SINK_STATE_CHANGED);
454             intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
455             intent.putExtra(BluetoothA2dp.EXTRA_PREVIOUS_SINK_STATE, prevState);
456             intent.putExtra(BluetoothA2dp.EXTRA_SINK_STATE, state);
457             mContext.sendBroadcast(intent, BLUETOOTH_PERM);
458 
459             if (DBG) log("A2DP state : device: " + device + " State:" + prevState + "->" + state);
460         }
461     }
462 
lookupSinksMatchingStates(int[] states)463     private synchronized Set<BluetoothDevice> lookupSinksMatchingStates(int[] states) {
464         Set<BluetoothDevice> sinks = new HashSet<BluetoothDevice>();
465         if (mAudioDevices.isEmpty()) {
466             return sinks;
467         }
468         for (BluetoothDevice device: mAudioDevices.keySet()) {
469             int sinkState = getSinkState(device);
470             for (int state : states) {
471                 if (state == sinkState) {
472                     sinks.add(device);
473                     break;
474                 }
475             }
476         }
477         return sinks;
478     }
479 
checkSinkSuspendState(int state)480     private boolean checkSinkSuspendState(int state) {
481         boolean result = true;
482 
483         if (state != mTargetA2dpState) {
484             if (state == BluetoothA2dp.STATE_PLAYING &&
485                 mTargetA2dpState == BluetoothA2dp.STATE_CONNECTED) {
486                 mAudioManager.setParameters("A2dpSuspended=true");
487             } else if (state == BluetoothA2dp.STATE_CONNECTED &&
488                 mTargetA2dpState == BluetoothA2dp.STATE_PLAYING) {
489                 mAudioManager.setParameters("A2dpSuspended=false");
490             } else {
491                 result = false;
492             }
493         }
494         return result;
495     }
496 
497     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)498     protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
499         if (mAudioDevices.isEmpty()) return;
500         pw.println("Cached audio devices:");
501         for (BluetoothDevice device : mAudioDevices.keySet()) {
502             int state = mAudioDevices.get(device);
503             pw.println(device + " " + BluetoothA2dp.stateToString(state));
504         }
505     }
506 
log(String msg)507     private static void log(String msg) {
508         Log.d(TAG, msg);
509     }
510 
initNative()511     private native boolean initNative();
cleanupNative()512     private native void cleanupNative();
connectSinkNative(String path)513     private synchronized native boolean connectSinkNative(String path);
disconnectSinkNative(String path)514     private synchronized native boolean disconnectSinkNative(String path);
suspendSinkNative(String path)515     private synchronized native boolean suspendSinkNative(String path);
resumeSinkNative(String path)516     private synchronized native boolean resumeSinkNative(String path);
getSinkPropertiesNative(String path)517     private synchronized native Object []getSinkPropertiesNative(String path);
518 }
519