FileDocCategorySizeDatePackage
BluetoothA2dpService.javaAPI DocAndroid 1.5 API20308Wed May 06 22:41:56 BST 2009android.server

BluetoothA2dpService

public class BluetoothA2dpService extends IBluetoothA2dp.Stub
TODO: Move this to services.jar and make the contructor package private again.
hide

Fields Summary
private static final String
TAG
private static final boolean
DBG
public static final String
BLUETOOTH_A2DP_SERVICE
private static final String
BLUETOOTH_ADMIN_PERM
private static final String
BLUETOOTH_PERM
private static final String
A2DP_SINK_ADDRESS
private static final String
BLUETOOTH_ENABLED
private static final int
MESSAGE_CONNECT_TO
private static final int
MESSAGE_DISCONNECT
private final android.content.Context
mContext
private final android.content.IntentFilter
mIntentFilter
private HashMap
mAudioDevices
private final android.media.AudioManager
mAudioManager
private final android.bluetooth.BluetoothDevice
mBluetooth
private final ArrayList
mPendingDisconnects
private int
mSinkCount
private final android.content.BroadcastReceiver
mReceiver
private final android.os.Handler
mHandler
Constructors Summary
public BluetoothA2dpService(android.content.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();
        }
    
Methods Summary
private native voidcleanupNative()

public synchronized intconnectSink(java.lang.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;
    
private native synchronized booleanconnectSinkNative(java.lang.String path)

private native synchronized java.lang.StringcreateHeadsetNative(java.lang.String address)

public synchronized intdisconnectSink(java.lang.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;
        }
    
private native synchronized booleandisconnectSinkNative(java.lang.String path)

protected synchronized voiddump(java.io.FileDescriptor fd, java.io.PrintWriter pw, java.lang.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));
        }
    
protected voidfinalize()

        try {
            cleanupNative();
        } finally {
            super.finalize();
        }
    
private native synchronized java.lang.StringgetAddressNative(java.lang.String path)

public synchronized intgetSinkPriority(java.lang.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 intgetSinkState(java.lang.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;
    
private synchronized voidhandleDeferredDisconnect(java.lang.String path)

        if (mPendingDisconnects.contains(path)) {
            mPendingDisconnects.remove(path);
            if (mSinkCount == 1) {
                mAudioManager.setBluetoothA2dpOn(false);
            }
            updateState(path, BluetoothA2dp.STATE_DISCONNECTED);
        }
    
private native booleaninitNative()

private native synchronized booleanisSinkConnectedNative(java.lang.String path)

public synchronized java.util.ListlistConnectedSinks()

        mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
        return lookupSinksMatchingStates(new int[] {BluetoothA2dp.STATE_CONNECTED,
                                                    BluetoothA2dp.STATE_PLAYING});
    
private native synchronized java.lang.String[]listHeadsetsNative()

private static voidlog(java.lang.String msg)

        Log.d(TAG, msg);
    
private final synchronized java.lang.StringlookupAddress(java.lang.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 final synchronized java.lang.StringlookupPath(java.lang.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 java.util.ListlookupSinksMatchingStates(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 voidonBluetoothDisable()

        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");
    
private synchronized voidonBluetoothEnable()


        
        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 voidonHeadsetCreated(java.lang.String path)

        updateState(path, BluetoothA2dp.STATE_DISCONNECTED);
    
private synchronized voidonHeadsetRemoved(java.lang.String path)

        if (mAudioDevices == null) return;
        mAudioDevices.remove(path);
    
private synchronized voidonSinkConnected(java.lang.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 voidonSinkDisconnected(java.lang.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 voidonSinkPlaying(java.lang.String path)

        updateState(path, BluetoothA2dp.STATE_PLAYING);
    
private synchronized voidonSinkStopped(java.lang.String path)

        updateState(path, BluetoothA2dp.STATE_CONNECTED);
    
private native synchronized booleanremoveHeadsetNative(java.lang.String path)

public synchronized intsetSinkPriority(java.lang.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 voidupdateState(java.lang.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);
            }
        }