/* * Copyright (C) 2012 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. */ package com.android.bluetooth.a2dp; import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothCodecConfig; import android.bluetooth.BluetoothCodecStatus; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; 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.HandlerThread; import android.os.ParcelUuid; import android.provider.Settings; import android.support.annotation.VisibleForTesting; import android.util.Log; import com.android.bluetooth.Utils; import com.android.bluetooth.avrcp.Avrcp; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * Provides Bluetooth A2DP profile, as a service in the Bluetooth application. * @hide */ public class A2dpService extends ProfileService { private static final boolean DBG = true; private static final String TAG = "A2dpService"; private BluetoothAdapter mAdapter = null; private HandlerThread mStateMachinesThread = null; private Avrcp mAvrcp; @VisibleForTesting A2dpNativeInterface mA2dpNativeInterface = null; private AudioManager mAudioManager; private A2dpCodecConfig mA2dpCodecConfig = null; private BluetoothDevice mActiveDevice = null; private final ConcurrentMap mStateMachines = new ConcurrentHashMap<>(); // Upper limit of all A2DP devices: Bonded or Connected private static final int MAX_A2DP_STATE_MACHINES = 50; // Upper limit of all A2DP devices that are Connected or Connecting private int mMaxConnectedAudioDevices = 1; private BroadcastReceiver mBondStateChangedReceiver = null; private BroadcastReceiver mConnectionStateChangedReceiver = null; public A2dpService() { mA2dpNativeInterface = A2dpNativeInterface.getInstance(); } private static A2dpService sA2dpService; static final ParcelUuid[] A2DP_SOURCE_UUID = { BluetoothUuid.AudioSource }; static final ParcelUuid[] A2DP_SOURCE_SINK_UUIDS = { BluetoothUuid.AudioSource, BluetoothUuid.AudioSink }; @Override protected IProfileServiceBinder initBinder() { return new BluetoothA2dpBinder(this); } @Override protected boolean start() { if (DBG) { Log.d(TAG, "start()"); } AdapterService adapterService = Objects.requireNonNull(AdapterService.getAdapterService(), "AdapterService cannot be null when A2dpService starts"); mMaxConnectedAudioDevices = adapterService.getMaxConnectedAudioDevices(); if (DBG) { Log.d(TAG, "Max connected audio devices set to " + mMaxConnectedAudioDevices); } mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); mAdapter = BluetoothAdapter.getDefaultAdapter(); mStateMachines.clear(); mStateMachinesThread = new HandlerThread("A2dpService.StateMachines"); mStateMachinesThread.start(); mAvrcp = Avrcp.make(this); setA2dpService(this); mA2dpCodecConfig = new A2dpCodecConfig(this, mA2dpNativeInterface); mA2dpNativeInterface.init(mA2dpCodecConfig.codecConfigPriorities()); mActiveDevice = null; if (mBondStateChangedReceiver == null) { IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); mBondStateChangedReceiver = new BondStateChangedReceiver(); registerReceiver(mBondStateChangedReceiver, filter); } if (mConnectionStateChangedReceiver == null) { IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); mConnectionStateChangedReceiver = new ConnectionStateChangedReceiver(); registerReceiver(mConnectionStateChangedReceiver, filter); } return true; } @Override protected boolean stop() { if (DBG) { Log.d(TAG, "stop()"); } for (A2dpStateMachine sm : mStateMachines.values()) { sm.doQuit(); } if (mAvrcp != null) { mAvrcp.doQuit(); } if (mStateMachinesThread != null) { mStateMachinesThread.quit(); mStateMachinesThread = null; } return true; } @Override protected void cleanup() { if (DBG) { Log.d(TAG, "cleanup()"); } if (mConnectionStateChangedReceiver != null) { unregisterReceiver(mConnectionStateChangedReceiver); mConnectionStateChangedReceiver = null; } if (mBondStateChangedReceiver != null) { unregisterReceiver(mBondStateChangedReceiver); mBondStateChangedReceiver = null; } for (Iterator> it = mStateMachines.entrySet().iterator(); it.hasNext(); ) { A2dpStateMachine sm = it.next().getValue(); sm.cleanup(); it.remove(); } mA2dpNativeInterface.cleanup(); if (mAvrcp != null) { mAvrcp.cleanup(); mAvrcp = null; } // TODO(b/72948646): should be moved to stop() setA2dpService(null); } public static synchronized A2dpService getA2dpService() { if (sA2dpService == null) { Log.w(TAG, "getA2dpService(): service is null"); return null; } if (!sA2dpService.isAvailable()) { Log.w(TAG, "getA2dpService(): service is not available"); return null; } return sA2dpService; } private static synchronized void setA2dpService(A2dpService instance) { if (DBG) { Log.d(TAG, "setA2dpService(): set to: " + instance); } sA2dpService = instance; } public boolean connect(BluetoothDevice device) { enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission"); if (DBG) { Log.d(TAG, "connect(): " + device); } if (getPriority(device) == BluetoothProfile.PRIORITY_OFF) { return false; } ParcelUuid[] featureUuids = device.getUuids(); if ((BluetoothUuid.containsAnyUuid(featureUuids, A2DP_SOURCE_UUID)) && !(BluetoothUuid.containsAllUuids(featureUuids, A2DP_SOURCE_SINK_UUIDS))) { Log.e(TAG, "Cannot connect to " + device + " : Remote does not have A2DP Sink UUID"); return false; } synchronized (mStateMachines) { A2dpStateMachine smConnect = getOrCreateStateMachine(device); if (smConnect == null) { Log.e(TAG, "Cannot connect to " + device + " : no state machine"); return false; } smConnect.sendMessage(A2dpStateMachine.CONNECT); return true; } } boolean disconnect(BluetoothDevice device) { enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission"); if (DBG) { Log.d(TAG, "disconnect(): " + device); } synchronized (mStateMachines) { A2dpStateMachine sm = mStateMachines.get(device); if (sm == null) { Log.e(TAG, "Ignored disconnect request for " + device + " : no state machine"); return false; } sm.sendMessage(A2dpStateMachine.DISCONNECT); return true; } } public List getConnectedDevices() { enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); synchronized (mStateMachines) { List devices = new ArrayList(); for (A2dpStateMachine sm : mStateMachines.values()) { if (sm.isConnected()) { devices.add(sm.getDevice()); } } return devices; } } /** * Check whether can connect to a peer device. * The check considers the maximum number of connected peers. * * @param device the peer device to connect to * @return true if connection is allowed, otherwise false */ @VisibleForTesting public boolean canConnectToDevice(BluetoothDevice device) { int connected = 0; // Count devices that are in the process of connecting or already connected synchronized (mStateMachines) { for (A2dpStateMachine sm : mStateMachines.values()) { switch (sm.getConnectionState()) { case BluetoothProfile.STATE_CONNECTING: case BluetoothProfile.STATE_CONNECTED: if (Objects.equals(device, sm.getDevice())) { return true; // Already connected or accounted for } connected++; break; default: break; } } } return (connected < mMaxConnectedAudioDevices); } List getDevicesMatchingConnectionStates(int[] states) { enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); synchronized (mStateMachines) { List devices = new ArrayList(); Set bondedDevices = mAdapter.getBondedDevices(); for (BluetoothDevice device : bondedDevices) { ParcelUuid[] featureUuids = device.getUuids(); if (!BluetoothUuid.isUuidPresent(featureUuids, BluetoothUuid.AudioSink)) { continue; } int connectionState = BluetoothProfile.STATE_DISCONNECTED; A2dpStateMachine sm = mStateMachines.get(device); if (sm != null) { connectionState = sm.getConnectionState(); } for (int i = 0; i < states.length; i++) { if (connectionState == states[i]) { devices.add(device); } } } return devices; } } public int getConnectionState(BluetoothDevice device) { enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); synchronized (mStateMachines) { A2dpStateMachine sm = mStateMachines.get(device); if (sm == null) { return BluetoothProfile.STATE_DISCONNECTED; } return sm.getConnectionState(); } } /** * Set the active device. * * @param device the active device * @return true on success, otherwise false */ public synchronized boolean setActiveDevice(BluetoothDevice device) { enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission"); BluetoothDevice previousActiveDevice = mActiveDevice; if (DBG) { Log.d(TAG, "setActiveDevice(" + device + "): previous is " + previousActiveDevice); } if (device == null) { // Clear the active device mActiveDevice = null; broadcastActiveDevice(null); if (previousActiveDevice != null) { // Make sure the Audio Manager knows the previous Active device is disconnected mAudioManager.setBluetoothA2dpDeviceConnectionState( previousActiveDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.A2DP); } return true; } BluetoothCodecStatus codecStatus = null; synchronized (mStateMachines) { A2dpStateMachine sm = mStateMachines.get(device); if (sm == null) { Log.e(TAG, "setActiveDevice(" + device + "): Cannot set as active: " + "no state machine"); return false; } if (sm.getConnectionState() != BluetoothProfile.STATE_CONNECTED) { Log.e(TAG, "setActiveDevice(" + device + "): Cannot set as active: " + "device is not connected"); return false; } if (!mA2dpNativeInterface.setActiveDevice(device)) { Log.e(TAG, "setActiveDevice(" + device + "): Cannot set as active in native layer"); return false; } codecStatus = sm.getCodecStatus(); } boolean deviceChanged = !Objects.equals(device, mActiveDevice); mActiveDevice = device; broadcastActiveDevice(mActiveDevice); if (deviceChanged) { // Send an intent with the active device codec config if (codecStatus != null) { broadcastCodecConfig(mActiveDevice, codecStatus); } // Make sure the Audio Manager knows the previous Active device is disconnected, // and the new Active device is connected. if (previousActiveDevice != null) { mAudioManager.setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent( previousActiveDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.A2DP, true); } mAudioManager.setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent( mActiveDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP, true); // Inform the Audio Service about the codec configuration // change, so the Audio Service can reset accordingly the audio // feeding parameters in the Audio HAL to the Bluetooth stack. mAudioManager.handleBluetoothA2dpDeviceConfigChange(mActiveDevice); } return true; } /** * Get the active device. * * @return the active device or null if no device is active */ public synchronized BluetoothDevice getActiveDevice() { enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return mActiveDevice; } private synchronized boolean isActiveDevice(BluetoothDevice device) { return (device != null) && Objects.equals(device, mActiveDevice); } public boolean setPriority(BluetoothDevice device, int priority) { enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); Settings.Global.putInt(getContentResolver(), Settings.Global.getBluetoothA2dpSinkPriorityKey(device.getAddress()), priority); if (DBG) { Log.d(TAG, "Saved priority " + device + " = " + priority); } return true; } public int getPriority(BluetoothDevice device) { enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); int priority = Settings.Global.getInt(getContentResolver(), Settings.Global.getBluetoothA2dpSinkPriorityKey(device.getAddress()), BluetoothProfile.PRIORITY_UNDEFINED); return priority; } /* Absolute volume implementation */ public boolean isAvrcpAbsoluteVolumeSupported() { return mAvrcp.isAbsoluteVolumeSupported(); } public void adjustAvrcpAbsoluteVolume(int direction) { mAvrcp.adjustVolume(direction); } public void setAvrcpAbsoluteVolume(int volume) { mAvrcp.setAbsoluteVolume(volume); } public void setAvrcpAudioState(int state) { mAvrcp.setA2dpAudioState(state); } public void resetAvrcpBlacklist(BluetoothDevice device) { if (mAvrcp != null) { mAvrcp.resetBlackList(device.getAddress()); } } synchronized boolean isA2dpPlaying(BluetoothDevice device) { enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (DBG) { Log.d(TAG, "isA2dpPlaying(" + device + ")"); } synchronized (mStateMachines) { A2dpStateMachine sm = mStateMachines.get(device); if (sm == null) { return false; } return sm.isPlaying(); } } /** * Gets the current codec status (configuration and capability). * * @param device the remote Bluetooth device. If null, use the currect * active A2DP Bluetooth device. * @return the current codec status * @hide */ public BluetoothCodecStatus getCodecStatus(BluetoothDevice device) { enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (DBG) { Log.d(TAG, "getCodecStatus(" + device + ")"); } if (device == null) { device = mActiveDevice; } if (device == null) { return null; } synchronized (mStateMachines) { A2dpStateMachine sm = mStateMachines.get(device); if (sm != null) { return sm.getCodecStatus(); } return null; } } /** * Sets the codec configuration preference. * * @param device the remote Bluetooth device. If null, use the currect * active A2DP Bluetooth device. * @param codecConfig the codec configuration preference * @hide */ public void setCodecConfigPreference(BluetoothDevice device, BluetoothCodecConfig codecConfig) { enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (DBG) { Log.d(TAG, "setCodecConfigPreference(" + device + "): " + Objects.toString(codecConfig)); } if (device == null) { device = mActiveDevice; } if (device == null) { Log.e(TAG, "Cannot set codec config preference: no active A2DP device"); return; } mA2dpCodecConfig.setCodecConfigPreference(device, codecConfig); } /** * Enables the optional codecs. * * @param device the remote Bluetooth device. If null, use the currect * active A2DP Bluetooth device. * @hide */ public void enableOptionalCodecs(BluetoothDevice device) { enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (DBG) { Log.d(TAG, "enableOptionalCodecs(" + device + ")"); } if (device == null) { device = mActiveDevice; } if (device == null) { Log.e(TAG, "Cannot enable optional codecs: no active A2DP device"); return; } mA2dpCodecConfig.enableOptionalCodecs(device); } /** * Disables the optional codecs. * * @param device the remote Bluetooth device. If null, use the currect * active A2DP Bluetooth device. * @hide */ public void disableOptionalCodecs(BluetoothDevice device) { enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (DBG) { Log.d(TAG, "disableOptionalCodecs(" + device + ")"); } if (device == null) { device = mActiveDevice; } if (device == null) { Log.e(TAG, "Cannot disable optional codecs: no active A2DP device"); return; } mA2dpCodecConfig.disableOptionalCodecs(device); } public int getSupportsOptionalCodecs(BluetoothDevice device) { enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); int support = Settings.Global.getInt(getContentResolver(), Settings.Global.getBluetoothA2dpSupportsOptionalCodecsKey(device.getAddress()), BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN); return support; } public void setSupportsOptionalCodecs(BluetoothDevice device, boolean doesSupport) { enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); int value = doesSupport ? BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED : BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED; Settings.Global.putInt(getContentResolver(), Settings.Global.getBluetoothA2dpSupportsOptionalCodecsKey(device.getAddress()), value); } public int getOptionalCodecsEnabled(BluetoothDevice device) { enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); return Settings.Global.getInt(getContentResolver(), Settings.Global.getBluetoothA2dpOptionalCodecsEnabledKey(device.getAddress()), BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN); } public void setOptionalCodecsEnabled(BluetoothDevice device, int value) { enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); if (value != BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN && value != BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED && value != BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED) { Log.w(TAG, "Unexpected value passed to setOptionalCodecsEnabled:" + value); return; } Settings.Global.putInt(getContentResolver(), Settings.Global.getBluetoothA2dpOptionalCodecsEnabledKey(device.getAddress()), value); } // Handle messages from native (JNI) to Java void messageFromNative(A2dpStackEvent stackEvent) { synchronized (mStateMachines) { BluetoothDevice device = stackEvent.device; A2dpStateMachine sm = getOrCreateStateMachine(device); if (sm == null) { Log.e(TAG, "Cannot process stack event: no state machine: " + stackEvent); return; } sm.sendMessage(A2dpStateMachine.STACK_EVENT, stackEvent); } } /** * The codec configuration for a device has been updated. * * @param device the remote device * @param codecStatus the new codec status * @param sameAudioFeedingParameters if true the audio feeding parameters * haven't been changed */ void codecConfigUpdated(BluetoothDevice device, BluetoothCodecStatus codecStatus, boolean sameAudioFeedingParameters) { broadcastCodecConfig(device, codecStatus); // Inform the Audio Service about the codec configuration change, // so the Audio Service can reset accordingly the audio feeding // parameters in the Audio HAL to the Bluetooth stack. if (isActiveDevice(device) && !sameAudioFeedingParameters) { mAudioManager.handleBluetoothA2dpDeviceConfigChange(device); } } private A2dpStateMachine getOrCreateStateMachine(BluetoothDevice device) { if (device == null) { Log.e(TAG, "getOrCreateStateMachine failed: device cannot be null"); return null; } synchronized (mStateMachines) { A2dpStateMachine sm = mStateMachines.get(device); if (sm != null) { return sm; } // Limit the maximum number of state machines to avoid DoS attack if (mStateMachines.size() > MAX_A2DP_STATE_MACHINES) { Log.e(TAG, "Maximum number of A2DP state machines reached: " + MAX_A2DP_STATE_MACHINES); return null; } if (DBG) { Log.d(TAG, "Creating a new state machine for " + device); } sm = A2dpStateMachine.make(device, this, this, mA2dpNativeInterface, mStateMachinesThread.getLooper()); mStateMachines.put(device, sm); return sm; } } private void broadcastActiveDevice(BluetoothDevice device) { if (DBG) { Log.d(TAG, "broadcastActiveDevice(" + device + ")"); } Intent intent = new Intent(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); sendBroadcast(intent, ProfileService.BLUETOOTH_PERM); } private void broadcastCodecConfig(BluetoothDevice device, BluetoothCodecStatus codecStatus) { if (DBG) { Log.d(TAG, "broadcastCodecConfig(" + device + "): " + codecStatus); } Intent intent = new Intent(BluetoothA2dp.ACTION_CODEC_CONFIG_CHANGED); intent.putExtra(BluetoothCodecStatus.EXTRA_CODEC_STATUS, codecStatus); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); sendBroadcast(intent, A2dpService.BLUETOOTH_PERM); } // Remove state machine if the bonding for a device is removed private class BondStateChangedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (!BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) { return; } int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR); BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (DBG) { Log.d(TAG, "Bond state changed for device: " + device + " state: " + state); } if (state != BluetoothDevice.BOND_NONE) { return; } synchronized (mStateMachines) { A2dpStateMachine sm = mStateMachines.get(device); if (sm == null) { return; } if (DBG) { Log.d(TAG, "Removing state machine for device: " + device); } sm.doQuit(); sm.cleanup(); mStateMachines.remove(device); } } } private void updateOptionalCodecsSupport(BluetoothDevice device) { int previousSupport = getSupportsOptionalCodecs(device); boolean supportsOptional = false; synchronized (mStateMachines) { A2dpStateMachine sm = getOrCreateStateMachine(device); if (sm == null) { return; } BluetoothCodecStatus codecStatus = sm.getCodecStatus(); if (codecStatus != null) { for (BluetoothCodecConfig config : codecStatus.getCodecsSelectableCapabilities()) { if (!config.isMandatoryCodec()) { supportsOptional = true; break; } } } } if (previousSupport == BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN || supportsOptional != (previousSupport == BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED)) { setSupportsOptionalCodecs(device, supportsOptional); } if (supportsOptional) { int enabled = getOptionalCodecsEnabled(device); if (enabled == BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED) { enableOptionalCodecs(device); } else if (enabled == BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED) { disableOptionalCodecs(device); } } } private synchronized void connectionStateChanged(BluetoothDevice device, int fromState, int toState) { if ((device == null) || (fromState == toState)) { return; } if (toState == BluetoothProfile.STATE_CONNECTED) { // Each time a device connects, we want to re-check if it supports optional // codecs (perhaps it's had a firmware update, etc.) and save that state if // it differs from what we had saved before. updateOptionalCodecsSupport(device); } // Set the active device if only one connected device is supported and it was connected if (toState == BluetoothProfile.STATE_CONNECTED && (mMaxConnectedAudioDevices == 1)) { setActiveDevice(device); } // Check if the active device has been disconnected if (isActiveDevice(device) && (fromState == BluetoothProfile.STATE_CONNECTED)) { setActiveDevice(null); } } // Update codec support per device when device is (re)connected. private class ConnectionStateChangedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (!BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { return; } BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); int toState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); int fromState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1); connectionStateChanged(device, fromState, toState); } } // Binder object: Must be static class or memory leak may occur @VisibleForTesting static class BluetoothA2dpBinder extends IBluetoothA2dp.Stub implements IProfileServiceBinder { private A2dpService mService; private A2dpService getService() { if (!Utils.checkCaller()) { Log.w(TAG, "A2DP call not allowed for non-active user"); return null; } if (mService != null && mService.isAvailable()) { return mService; } return null; } BluetoothA2dpBinder(A2dpService svc) { mService = svc; } @Override public void cleanup() { mService = null; } @Override public boolean connect(BluetoothDevice device) { A2dpService service = getService(); if (service == null) { return false; } return service.connect(device); } @Override public boolean disconnect(BluetoothDevice device) { A2dpService service = getService(); if (service == null) { return false; } return service.disconnect(device); } @Override public List getConnectedDevices() { A2dpService service = getService(); if (service == null) { return new ArrayList(0); } return service.getConnectedDevices(); } @Override public List getDevicesMatchingConnectionStates(int[] states) { A2dpService service = getService(); if (service == null) { return new ArrayList(0); } return service.getDevicesMatchingConnectionStates(states); } @Override public int getConnectionState(BluetoothDevice device) { A2dpService service = getService(); if (service == null) { return BluetoothProfile.STATE_DISCONNECTED; } return service.getConnectionState(device); } @Override public boolean setActiveDevice(BluetoothDevice device) { A2dpService service = getService(); if (service == null) { return false; } return service.setActiveDevice(device); } @Override public BluetoothDevice getActiveDevice() { A2dpService service = getService(); if (service == null) { return null; } return service.getActiveDevice(); } @Override public boolean setPriority(BluetoothDevice device, int priority) { A2dpService service = getService(); if (service == null) { return false; } return service.setPriority(device, priority); } @Override public int getPriority(BluetoothDevice device) { A2dpService service = getService(); if (service == null) { return BluetoothProfile.PRIORITY_UNDEFINED; } return service.getPriority(device); } @Override public boolean isAvrcpAbsoluteVolumeSupported() { A2dpService service = getService(); if (service == null) { return false; } return service.isAvrcpAbsoluteVolumeSupported(); } @Override public void adjustAvrcpAbsoluteVolume(int direction) { A2dpService service = getService(); if (service == null) { return; } service.adjustAvrcpAbsoluteVolume(direction); } @Override public void setAvrcpAbsoluteVolume(int volume) { A2dpService service = getService(); if (service == null) { return; } service.setAvrcpAbsoluteVolume(volume); } @Override public boolean isA2dpPlaying(BluetoothDevice device) { A2dpService service = getService(); if (service == null) { return false; } return service.isA2dpPlaying(device); } @Override public BluetoothCodecStatus getCodecStatus(BluetoothDevice device) { A2dpService service = getService(); if (service == null) { return null; } return service.getCodecStatus(device); } @Override public void setCodecConfigPreference(BluetoothDevice device, BluetoothCodecConfig codecConfig) { A2dpService service = getService(); if (service == null) { return; } service.setCodecConfigPreference(device, codecConfig); } @Override public void enableOptionalCodecs(BluetoothDevice device) { A2dpService service = getService(); if (service == null) { return; } service.enableOptionalCodecs(device); } @Override public void disableOptionalCodecs(BluetoothDevice device) { A2dpService service = getService(); if (service == null) { return; } service.disableOptionalCodecs(device); } public int supportsOptionalCodecs(BluetoothDevice device) { A2dpService service = getService(); if (service == null) { return BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN; } return service.getSupportsOptionalCodecs(device); } public int getOptionalCodecsEnabled(BluetoothDevice device) { A2dpService service = getService(); if (service == null) { return BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN; } return service.getOptionalCodecsEnabled(device); } public void setOptionalCodecsEnabled(BluetoothDevice device, int value) { A2dpService service = getService(); if (service == null) { return; } service.setOptionalCodecsEnabled(device, value); } } @Override public void dump(StringBuilder sb) { super.dump(sb); ProfileService.println(sb, "mActiveDevice: " + mActiveDevice); for (A2dpStateMachine sm : mStateMachines.values()) { sm.dump(sb); } if (mAvrcp != null) { mAvrcp.dump(sb); } } }