blob: 58f9491b98ff8cab527311c41eeff63e85f221e4 [file] [log] [blame]
/*
* 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
* java/services/com/android/server/BluetoothA2dpService.java
* 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.provider.Settings;
import android.util.Log;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Iterator;
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 final Context mContext;
private final IntentFilter mIntentFilter;
private HashMap<String, SinkState> mAudioDevices;
private final AudioManager mAudioManager;
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);
BluetoothDevice device =
(BluetoothDevice)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
if (device == null) {
throw new RuntimeException("Platform does not support Bluetooth");
}
if (!initNative()) {
throw new RuntimeException("Could not init BluetoothA2dpService");
}
mIntentFilter = new IntentFilter(BluetoothIntent.ENABLED_ACTION);
mIntentFilter.addAction(BluetoothIntent.DISABLED_ACTION);
mIntentFilter.addAction(BluetoothIntent.BOND_STATE_CHANGED_ACTION);
mContext.registerReceiver(mReceiver, mIntentFilter);
if (device.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.ENABLED_ACTION)) {
onBluetoothEnable();
} else if (action.equals(BluetoothIntent.DISABLED_ACTION)) {
onBluetoothDisable();
} 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;
}
}
}
};
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) {
for (String path : mAudioDevices.keySet()) {
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;
}
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");
List<String> connectedSinks = new ArrayList<String>();
if (mAudioDevices == null) {
return connectedSinks;
}
for (SinkState sink : mAudioDevices.values()) {
if (sink.state == BluetoothA2dp.STATE_CONNECTED ||
sink.state == BluetoothA2dp.STATE_PLAYING) {
connectedSinks.add(sink.address);
}
}
return connectedSinks;
}
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 (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
for (String oldPath : mAudioDevices.keySet()) {
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) {
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 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);
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_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);
}