| /* |
| * Copyright (C) 2008 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /** |
| * TODO: Move this to services.jar |
| * and make the contructor package private again. |
| * @hide |
| */ |
| |
| package android.server; |
| |
| import android.bluetooth.BluetoothA2dp; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothError; |
| import android.bluetooth.BluetoothIntent; |
| import android.bluetooth.IBluetoothA2dp; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.PackageManager; |
| import android.media.AudioManager; |
| import android.os.Binder; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.provider.Settings; |
| import android.util.Log; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| |
| public class BluetoothA2dpService extends IBluetoothA2dp.Stub { |
| private static final String TAG = "BluetoothA2dpService"; |
| private static final boolean DBG = true; |
| |
| public static final String BLUETOOTH_A2DP_SERVICE = "bluetooth_a2dp"; |
| |
| private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; |
| private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; |
| |
| private static final String A2DP_SINK_ADDRESS = "a2dp_sink_address"; |
| private static final String BLUETOOTH_ENABLED = "bluetooth_enabled"; |
| |
| private static final int MESSAGE_CONNECT_TO = 1; |
| private static final int MESSAGE_DISCONNECT = 2; |
| |
| private final Context mContext; |
| private final IntentFilter mIntentFilter; |
| private HashMap<String, SinkState> mAudioDevices; |
| private final AudioManager mAudioManager; |
| private final BluetoothDevice mBluetooth; |
| |
| // list of disconnected sinks to process after a delay |
| private final ArrayList<String> mPendingDisconnects = new ArrayList<String>(); |
| // number of active sinks |
| private int mSinkCount = 0; |
| |
| private class SinkState { |
| public String address; |
| public int state; |
| public SinkState(String a, int s) {address = a; state = s;} |
| } |
| |
| public BluetoothA2dpService(Context context) { |
| mContext = context; |
| |
| mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); |
| |
| mBluetooth = (BluetoothDevice) mContext.getSystemService(Context.BLUETOOTH_SERVICE); |
| if (mBluetooth == null) { |
| throw new RuntimeException("Platform does not support Bluetooth"); |
| } |
| |
| if (!initNative()) { |
| throw new RuntimeException("Could not init BluetoothA2dpService"); |
| } |
| |
| mIntentFilter = new IntentFilter(BluetoothIntent.BLUETOOTH_STATE_CHANGED_ACTION); |
| mIntentFilter.addAction(BluetoothIntent.BOND_STATE_CHANGED_ACTION); |
| mIntentFilter.addAction(BluetoothIntent.REMOTE_DEVICE_CONNECTED_ACTION); |
| mContext.registerReceiver(mReceiver, mIntentFilter); |
| |
| if (mBluetooth.isEnabled()) { |
| onBluetoothEnable(); |
| } |
| } |
| |
| @Override |
| protected void finalize() throws Throwable { |
| try { |
| cleanupNative(); |
| } finally { |
| super.finalize(); |
| } |
| } |
| |
| private final BroadcastReceiver mReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| String action = intent.getAction(); |
| String address = intent.getStringExtra(BluetoothIntent.ADDRESS); |
| if (action.equals(BluetoothIntent.BLUETOOTH_STATE_CHANGED_ACTION)) { |
| int state = intent.getIntExtra(BluetoothIntent.BLUETOOTH_STATE, |
| BluetoothError.ERROR); |
| switch (state) { |
| case BluetoothDevice.BLUETOOTH_STATE_ON: |
| onBluetoothEnable(); |
| break; |
| case BluetoothDevice.BLUETOOTH_STATE_TURNING_OFF: |
| onBluetoothDisable(); |
| break; |
| } |
| } else if (action.equals(BluetoothIntent.BOND_STATE_CHANGED_ACTION)) { |
| int bondState = intent.getIntExtra(BluetoothIntent.BOND_STATE, |
| BluetoothError.ERROR); |
| switch(bondState) { |
| case BluetoothDevice.BOND_BONDED: |
| setSinkPriority(address, BluetoothA2dp.PRIORITY_AUTO); |
| break; |
| case BluetoothDevice.BOND_BONDING: |
| case BluetoothDevice.BOND_NOT_BONDED: |
| setSinkPriority(address, BluetoothA2dp.PRIORITY_OFF); |
| break; |
| } |
| } else if (action.equals(BluetoothIntent.REMOTE_DEVICE_CONNECTED_ACTION)) { |
| if (getSinkPriority(address) > BluetoothA2dp.PRIORITY_OFF) { |
| // This device is a preferred sink. Make an A2DP connection |
| // after a delay. We delay to avoid connection collisions, |
| // and to give other profiles such as HFP a chance to |
| // connect first. |
| Message msg = Message.obtain(mHandler, MESSAGE_CONNECT_TO, address); |
| mHandler.sendMessageDelayed(msg, 6000); |
| } |
| } |
| } |
| }; |
| |
| private final Handler mHandler = new Handler() { |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MESSAGE_CONNECT_TO: |
| String address = (String)msg.obj; |
| // check bluetooth is still on, device is still preferred, and |
| // nothing is currently connected |
| if (mBluetooth.isEnabled() && |
| getSinkPriority(address) > BluetoothA2dp.PRIORITY_OFF && |
| lookupSinksMatchingStates(new int[] { |
| BluetoothA2dp.STATE_CONNECTING, |
| BluetoothA2dp.STATE_CONNECTED, |
| BluetoothA2dp.STATE_PLAYING, |
| BluetoothA2dp.STATE_DISCONNECTING}).size() == 0) { |
| log("Auto-connecting A2DP to sink " + address); |
| connectSink(address); |
| } |
| break; |
| case MESSAGE_DISCONNECT: |
| handleDeferredDisconnect((String)msg.obj); |
| break; |
| } |
| } |
| }; |
| |
| private synchronized void onBluetoothEnable() { |
| mAudioDevices = new HashMap<String, SinkState>(); |
| String[] paths = (String[])listHeadsetsNative(); |
| if (paths != null) { |
| for (String path : paths) { |
| mAudioDevices.put(path, new SinkState(getAddressNative(path), |
| isSinkConnectedNative(path) ? BluetoothA2dp.STATE_CONNECTED : |
| BluetoothA2dp.STATE_DISCONNECTED)); |
| } |
| } |
| mAudioManager.setParameter(BLUETOOTH_ENABLED, "true"); |
| } |
| |
| private synchronized void onBluetoothDisable() { |
| if (mAudioDevices != null) { |
| // copy to allow modification during iteration |
| String[] paths = new String[mAudioDevices.size()]; |
| paths = mAudioDevices.keySet().toArray(paths); |
| for (String path : paths) { |
| switch (mAudioDevices.get(path).state) { |
| case BluetoothA2dp.STATE_CONNECTING: |
| case BluetoothA2dp.STATE_CONNECTED: |
| case BluetoothA2dp.STATE_PLAYING: |
| disconnectSinkNative(path); |
| updateState(path, BluetoothA2dp.STATE_DISCONNECTED); |
| break; |
| case BluetoothA2dp.STATE_DISCONNECTING: |
| updateState(path, BluetoothA2dp.STATE_DISCONNECTED); |
| break; |
| } |
| } |
| mAudioDevices = null; |
| } |
| mAudioManager.setBluetoothA2dpOn(false); |
| mAudioManager.setParameter(BLUETOOTH_ENABLED, "false"); |
| } |
| |
| public synchronized int connectSink(String address) { |
| mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, |
| "Need BLUETOOTH_ADMIN permission"); |
| if (DBG) log("connectSink(" + address + ")"); |
| if (!BluetoothDevice.checkBluetoothAddress(address)) { |
| return BluetoothError.ERROR; |
| } |
| if (mAudioDevices == null) { |
| return BluetoothError.ERROR; |
| } |
| // ignore if there are any active sinks |
| if (lookupSinksMatchingStates(new int[] { |
| BluetoothA2dp.STATE_CONNECTING, |
| BluetoothA2dp.STATE_CONNECTED, |
| BluetoothA2dp.STATE_PLAYING, |
| BluetoothA2dp.STATE_DISCONNECTING}).size() != 0) { |
| return BluetoothError.ERROR; |
| } |
| |
| String path = lookupPath(address); |
| if (path == null) { |
| path = createHeadsetNative(address); |
| if (DBG) log("new bluez sink: " + address + " (" + path + ")"); |
| } |
| if (path == null) { |
| return BluetoothError.ERROR; |
| } |
| |
| SinkState sink = mAudioDevices.get(path); |
| int state = BluetoothA2dp.STATE_DISCONNECTED; |
| if (sink != null) { |
| state = sink.state; |
| } |
| switch (state) { |
| case BluetoothA2dp.STATE_CONNECTED: |
| case BluetoothA2dp.STATE_PLAYING: |
| case BluetoothA2dp.STATE_DISCONNECTING: |
| return BluetoothError.ERROR; |
| case BluetoothA2dp.STATE_CONNECTING: |
| return BluetoothError.SUCCESS; |
| } |
| |
| // State is DISCONNECTED |
| if (!connectSinkNative(path)) { |
| return BluetoothError.ERROR; |
| } |
| updateState(path, BluetoothA2dp.STATE_CONNECTING); |
| return BluetoothError.SUCCESS; |
| } |
| |
| public synchronized int disconnectSink(String address) { |
| mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, |
| "Need BLUETOOTH_ADMIN permission"); |
| if (DBG) log("disconnectSink(" + address + ")"); |
| if (!BluetoothDevice.checkBluetoothAddress(address)) { |
| return BluetoothError.ERROR; |
| } |
| if (mAudioDevices == null) { |
| return BluetoothError.ERROR; |
| } |
| String path = lookupPath(address); |
| if (path == null) { |
| return BluetoothError.ERROR; |
| } |
| switch (mAudioDevices.get(path).state) { |
| case BluetoothA2dp.STATE_DISCONNECTED: |
| return BluetoothError.ERROR; |
| case BluetoothA2dp.STATE_DISCONNECTING: |
| return BluetoothError.SUCCESS; |
| } |
| |
| // State is CONNECTING or CONNECTED or PLAYING |
| if (!disconnectSinkNative(path)) { |
| return BluetoothError.ERROR; |
| } else { |
| updateState(path, BluetoothA2dp.STATE_DISCONNECTING); |
| return BluetoothError.SUCCESS; |
| } |
| } |
| |
| public synchronized List<String> listConnectedSinks() { |
| mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); |
| return lookupSinksMatchingStates(new int[] {BluetoothA2dp.STATE_CONNECTED, |
| BluetoothA2dp.STATE_PLAYING}); |
| } |
| |
| public synchronized int getSinkState(String address) { |
| mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); |
| if (!BluetoothDevice.checkBluetoothAddress(address)) { |
| return BluetoothError.ERROR; |
| } |
| if (mAudioDevices == null) { |
| return BluetoothA2dp.STATE_DISCONNECTED; |
| } |
| for (SinkState sink : mAudioDevices.values()) { |
| if (address.equals(sink.address)) { |
| return sink.state; |
| } |
| } |
| return BluetoothA2dp.STATE_DISCONNECTED; |
| } |
| |
| public synchronized int getSinkPriority(String address) { |
| mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); |
| if (!BluetoothDevice.checkBluetoothAddress(address)) { |
| return BluetoothError.ERROR; |
| } |
| return Settings.Secure.getInt(mContext.getContentResolver(), |
| Settings.Secure.getBluetoothA2dpSinkPriorityKey(address), |
| BluetoothA2dp.PRIORITY_OFF); |
| } |
| |
| public synchronized int setSinkPriority(String address, int priority) { |
| mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, |
| "Need BLUETOOTH_ADMIN permission"); |
| if (!BluetoothDevice.checkBluetoothAddress(address)) { |
| return BluetoothError.ERROR; |
| } |
| return Settings.Secure.putInt(mContext.getContentResolver(), |
| Settings.Secure.getBluetoothA2dpSinkPriorityKey(address), priority) ? |
| BluetoothError.SUCCESS : BluetoothError.ERROR; |
| } |
| |
| private synchronized void onHeadsetCreated(String path) { |
| updateState(path, BluetoothA2dp.STATE_DISCONNECTED); |
| } |
| |
| private synchronized void onHeadsetRemoved(String path) { |
| if (mAudioDevices == null) return; |
| mAudioDevices.remove(path); |
| } |
| |
| private synchronized void onSinkConnected(String path) { |
| // if we are reconnected, do not process previous disconnect event. |
| mPendingDisconnects.remove(path); |
| |
| if (mAudioDevices == null) return; |
| // bluez 3.36 quietly disconnects the previous sink when a new sink |
| // is connected, so we need to mark all previously connected sinks as |
| // disconnected |
| |
| // copy to allow modification during iteration |
| String[] paths = new String[mAudioDevices.size()]; |
| paths = mAudioDevices.keySet().toArray(paths); |
| for (String oldPath : paths) { |
| if (path.equals(oldPath)) { |
| continue; |
| } |
| int state = mAudioDevices.get(oldPath).state; |
| if (state == BluetoothA2dp.STATE_CONNECTED || state == BluetoothA2dp.STATE_PLAYING) { |
| updateState(path, BluetoothA2dp.STATE_DISCONNECTED); |
| } |
| } |
| |
| updateState(path, BluetoothA2dp.STATE_CONNECTING); |
| mAudioManager.setParameter(A2DP_SINK_ADDRESS, lookupAddress(path)); |
| mAudioManager.setBluetoothA2dpOn(true); |
| updateState(path, BluetoothA2dp.STATE_CONNECTED); |
| } |
| |
| private synchronized void onSinkDisconnected(String path) { |
| // This is to work around a problem in bluez that results |
| // sink disconnect events being sent, immediately followed by a reconnect. |
| // To avoid unnecessary audio routing changes, we defer handling |
| // sink disconnects until after a short delay. |
| mPendingDisconnects.add(path); |
| Message msg = Message.obtain(mHandler, MESSAGE_DISCONNECT, path); |
| mHandler.sendMessageDelayed(msg, 2000); |
| } |
| |
| private synchronized void handleDeferredDisconnect(String path) { |
| if (mPendingDisconnects.contains(path)) { |
| mPendingDisconnects.remove(path); |
| if (mSinkCount == 1) { |
| mAudioManager.setBluetoothA2dpOn(false); |
| } |
| updateState(path, BluetoothA2dp.STATE_DISCONNECTED); |
| } |
| } |
| |
| private synchronized void onSinkPlaying(String path) { |
| updateState(path, BluetoothA2dp.STATE_PLAYING); |
| } |
| |
| private synchronized void onSinkStopped(String path) { |
| updateState(path, BluetoothA2dp.STATE_CONNECTED); |
| } |
| |
| private synchronized final String lookupAddress(String path) { |
| if (mAudioDevices == null) return null; |
| SinkState sink = mAudioDevices.get(path); |
| if (sink == null) { |
| Log.w(TAG, "lookupAddress() called for unknown device " + path); |
| updateState(path, BluetoothA2dp.STATE_DISCONNECTED); |
| } |
| String address = mAudioDevices.get(path).address; |
| if (address == null) Log.e(TAG, "Can't find address for " + path); |
| return address; |
| } |
| |
| private synchronized final String lookupPath(String address) { |
| if (mAudioDevices == null) return null; |
| |
| for (String path : mAudioDevices.keySet()) { |
| if (address.equals(mAudioDevices.get(path).address)) { |
| return path; |
| } |
| } |
| return null; |
| } |
| |
| private synchronized List<String> lookupSinksMatchingStates(int[] states) { |
| List<String> sinks = new ArrayList<String>(); |
| if (mAudioDevices == null) { |
| return sinks; |
| } |
| for (SinkState sink : mAudioDevices.values()) { |
| for (int state : states) { |
| if (sink.state == state) { |
| sinks.add(sink.address); |
| break; |
| } |
| } |
| } |
| return sinks; |
| } |
| |
| private synchronized void updateState(String path, int state) { |
| if (mAudioDevices == null) return; |
| |
| SinkState s = mAudioDevices.get(path); |
| int prevState; |
| String address; |
| if (s == null) { |
| address = getAddressNative(path); |
| mAudioDevices.put(path, new SinkState(address, state)); |
| prevState = BluetoothA2dp.STATE_DISCONNECTED; |
| } else { |
| address = lookupAddress(path); |
| prevState = s.state; |
| s.state = state; |
| } |
| |
| if (state != prevState) { |
| if (DBG) log("state " + address + " (" + path + ") " + prevState + "->" + state); |
| |
| // keep track of the number of active sinks |
| if (prevState == BluetoothA2dp.STATE_DISCONNECTED) { |
| mSinkCount++; |
| } else if (state == BluetoothA2dp.STATE_DISCONNECTED) { |
| mSinkCount--; |
| } |
| |
| Intent intent = new Intent(BluetoothA2dp.SINK_STATE_CHANGED_ACTION); |
| intent.putExtra(BluetoothIntent.ADDRESS, address); |
| intent.putExtra(BluetoothA2dp.SINK_PREVIOUS_STATE, prevState); |
| intent.putExtra(BluetoothA2dp.SINK_STATE, state); |
| mContext.sendBroadcast(intent, BLUETOOTH_PERM); |
| |
| if ((prevState == BluetoothA2dp.STATE_CONNECTED || |
| prevState == BluetoothA2dp.STATE_PLAYING) && |
| (state != BluetoothA2dp.STATE_CONNECTING && |
| state != BluetoothA2dp.STATE_CONNECTED && |
| state != BluetoothA2dp.STATE_PLAYING)) { |
| // disconnected |
| intent = new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY); |
| mContext.sendBroadcast(intent); |
| } |
| } |
| } |
| |
| @Override |
| protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { |
| if (mAudioDevices == null) return; |
| pw.println("Cached audio devices:"); |
| for (String path : mAudioDevices.keySet()) { |
| SinkState sink = mAudioDevices.get(path); |
| pw.println(path + " " + sink.address + " " + BluetoothA2dp.stateToString(sink.state)); |
| } |
| } |
| |
| private static void log(String msg) { |
| Log.d(TAG, msg); |
| } |
| |
| private native boolean initNative(); |
| private native void cleanupNative(); |
| private synchronized native String[] listHeadsetsNative(); |
| private synchronized native String createHeadsetNative(String address); |
| private synchronized native boolean removeHeadsetNative(String path); |
| private synchronized native String getAddressNative(String path); |
| private synchronized native boolean connectSinkNative(String path); |
| private synchronized native boolean disconnectSinkNative(String path); |
| private synchronized native boolean isSinkConnectedNative(String path); |
| } |