/* * 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.BluetoothUuid; import android.bluetooth.IBluetoothA2dp; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; 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; import java.util.UUID; 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 String PROPERTY_STATE = "State"; private static final String SINK_STATE_DISCONNECTED = "disconnected"; private static final String SINK_STATE_CONNECTING = "connecting"; private static final String SINK_STATE_CONNECTED = "connected"; private static final String SINK_STATE_PLAYING = "playing"; private static int mSinkCount; private final Context mContext; private final IntentFilter mIntentFilter; private HashMap mAudioDevices; private final AudioManager mAudioManager; private final BluetoothDeviceService mBluetoothService; 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); } } } }; public BluetoothA2dpService(Context context, BluetoothDeviceService bluetoothService) { mContext = context; mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mBluetoothService = bluetoothService; if (mBluetoothService == 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); mAudioDevices = new HashMap(); if (mBluetoothService.isEnabled()) onBluetoothEnable(); } @Override protected void finalize() throws Throwable { try { cleanupNative(); } finally { super.finalize(); } } 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 (mBluetoothService.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; } } }; private int convertBluezSinkStringtoState(String value) { if (value.equalsIgnoreCase("disconnected")) return BluetoothA2dp.STATE_DISCONNECTED; if (value.equalsIgnoreCase("connecting")) return BluetoothA2dp.STATE_CONNECTING; if (value.equalsIgnoreCase("connected")) return BluetoothA2dp.STATE_CONNECTED; if (value.equalsIgnoreCase("playing")) return BluetoothA2dp.STATE_PLAYING; return -1; } private synchronized boolean addAudioSink (String address) { String path = mBluetoothService.getObjectPathFromAddress(address); String propValues[] = (String []) getSinkPropertiesNative(path); if (propValues == null) { Log.e(TAG, "Error while getting AudioSink properties for device: " + address); return false; } Integer state = null; // Properties are name-value pairs for (int i = 0; i < propValues.length; i+=2) { if (propValues[i].equals(PROPERTY_STATE)) { state = new Integer(convertBluezSinkStringtoState(propValues[i+1])); break; } } mAudioDevices.put(address, state); handleSinkStateChange(address, BluetoothA2dp.STATE_DISCONNECTED, state); return true; } private synchronized void onBluetoothEnable() { String devices = mBluetoothService.getProperty("Devices"); mSinkCount = 0; if (devices != null) { String [] paths = devices.split(","); for (String path: paths) { String address = mBluetoothService.getAddressFromObjectPath(path); String []uuids = mBluetoothService.getRemoteUuids(address); if (uuids != null) for (String uuid: uuids) { UUID remoteUuid = UUID.fromString(uuid); if (BluetoothUuid.isAudioSink(remoteUuid) || BluetoothUuid.isAudioSource(remoteUuid) || BluetoothUuid.isAdvAudioDist(remoteUuid)) { addAudioSink(address); break; } } } } mAudioManager.setParameter(BLUETOOTH_ENABLED, "true"); } private synchronized void onBluetoothDisable() { if (!mAudioDevices.isEmpty()) { String [] addresses = new String[mAudioDevices.size()]; addresses = mAudioDevices.keySet().toArray(addresses); for (String address : addresses) { int state = getSinkState(address); switch (state) { case BluetoothA2dp.STATE_CONNECTING: case BluetoothA2dp.STATE_CONNECTED: case BluetoothA2dp.STATE_PLAYING: disconnectSinkNative(mBluetoothService.getObjectPathFromAddress(address)); handleSinkStateChange(address,state, BluetoothA2dp.STATE_DISCONNECTED); break; case BluetoothA2dp.STATE_DISCONNECTING: handleSinkStateChange(address, BluetoothA2dp.STATE_DISCONNECTING, BluetoothA2dp.STATE_DISCONNECTED); break; } } mAudioDevices.clear(); } 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; } // 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; } if (mAudioDevices.get(address) == null && !addAudioSink(address)) return BluetoothError.ERROR; int state = mAudioDevices.get(address); switch (state) { case BluetoothA2dp.STATE_CONNECTED: case BluetoothA2dp.STATE_PLAYING: case BluetoothA2dp.STATE_DISCONNECTING: return BluetoothError.ERROR; case BluetoothA2dp.STATE_CONNECTING: return BluetoothError.SUCCESS; } String path = mBluetoothService.getObjectPathFromAddress(address); if (path == null) return BluetoothError.ERROR; // State is DISCONNECTED if (!connectSinkNative(path)) { return BluetoothError.ERROR; } 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; } String path = mBluetoothService.getObjectPathFromAddress(address); if (path == null) { return BluetoothError.ERROR; } switch (getSinkState(address)) { 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 { return BluetoothError.SUCCESS; } } public synchronized List 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; } Integer state = mAudioDevices.get(address); if (state == null) return BluetoothA2dp.STATE_DISCONNECTED; return state; } 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 onSinkPropertyChanged(String path, String []propValues) { String name = propValues[0]; String address = mBluetoothService.getAddressFromObjectPath(path); if (address == null) { Log.e(TAG, "onSinkPropertyChanged: Address of the remote device in null"); return; } if (mAudioDevices.get(address) == null) { // Ignore this state change, since it means we have got it after // bluetooth has been disabled. return; } if (name.equals(PROPERTY_STATE)) { int state = convertBluezSinkStringtoState(propValues[1]); int prevState = mAudioDevices.get(address); handleSinkStateChange(address, prevState, state); } } private void handleSinkStateChange(String address, int prevState, int state) { if (state != prevState) { if (state == BluetoothA2dp.STATE_DISCONNECTED || state == BluetoothA2dp.STATE_DISCONNECTING) { if (prevState == BluetoothA2dp.STATE_CONNECTED || prevState == BluetoothA2dp.STATE_PLAYING) { // disconnecting or disconnected Intent intent = new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY); mContext.sendBroadcast(intent); } if (--mSinkCount == 0) mAudioManager.setBluetoothA2dpOn(false); } else if (state == BluetoothA2dp.STATE_CONNECTED) { mSinkCount ++; } mAudioDevices.put(address, state); 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 (DBG) log("A2DP state : address: " + address + " State:" + prevState + "->" + state); if (state == BluetoothA2dp.STATE_CONNECTED) { mAudioManager.setParameter(A2DP_SINK_ADDRESS, address); mAudioManager.setBluetoothA2dpOn(true); } } } private synchronized List lookupSinksMatchingStates(int[] states) { List sinks = new ArrayList(); if (mAudioDevices.isEmpty()) { return sinks; } for (String path: mAudioDevices.keySet()) { int sinkState = getSinkState(path); for (int state : states) { if (state == sinkState) { sinks.add(path); break; } } } return sinks; } @Override protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (mAudioDevices.isEmpty()) return; pw.println("Cached audio devices:"); for (String address : mAudioDevices.keySet()) { int state = mAudioDevices.get(address); pw.println(address + " " + BluetoothA2dp.stateToString(state)); } } private static void log(String msg) { Log.d(TAG, msg); } private native boolean initNative(); private native void cleanupNative(); private synchronized native boolean connectSinkNative(String path); private synchronized native boolean disconnectSinkNative(String path); private synchronized native Object []getSinkPropertiesNative(String path); }