/* * 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 static com.android.bluetooth.Utils.enforceBluetoothPermission; import static com.android.bluetooth.Utils.enforceBluetoothPrivilegedPermission; import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothA2dp.OptionalCodecsPreferenceStatus; import android.bluetooth.BluetoothA2dp.OptionalCodecsSupportStatus; 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.util.Log; import com.android.bluetooth.BluetoothMetricsProto; import com.android.bluetooth.BluetoothStatsLog; import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.MetricsLogger; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.btservice.ServiceFactory; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import java.util.ArrayList; import java.util.List; import java.util.Objects; 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 static A2dpService sA2dpService; private AdapterService mAdapterService; private HandlerThread mStateMachinesThread; @VisibleForTesting A2dpNativeInterface mA2dpNativeInterface; @VisibleForTesting ServiceFactory mFactory = new ServiceFactory(); private AudioManager mAudioManager; private A2dpCodecConfig mA2dpCodecConfig; @GuardedBy("mStateMachines") private BluetoothDevice mActiveDevice; private final ConcurrentMap mStateMachines = new ConcurrentHashMap<>(); // Protect setActiveDevice() so all invoked is handled squentially private final Object mActiveSwitchingGuard = new Object(); // 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; // A2DP Offload Enabled in platform boolean mA2dpOffloadEnabled = false; private BroadcastReceiver mBondStateChangedReceiver; private BroadcastReceiver mConnectionStateChangedReceiver; @Override protected IProfileServiceBinder initBinder() { return new BluetoothA2dpBinder(this); } @Override protected void create() { Log.i(TAG, "create()"); } @Override protected boolean start() { Log.i(TAG, "start()"); if (sA2dpService != null) { throw new IllegalStateException("start() called twice"); } // Step 1: Get AdapterService, A2dpNativeInterface, AudioManager. // None of them can be null. mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(), "AdapterService cannot be null when A2dpService starts"); mA2dpNativeInterface = Objects.requireNonNull(A2dpNativeInterface.getInstance(), "A2dpNativeInterface cannot be null when A2dpService starts"); mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); Objects.requireNonNull(mAudioManager, "AudioManager cannot be null when A2dpService starts"); // Step 2: Get maximum number of connected audio devices mMaxConnectedAudioDevices = mAdapterService.getMaxConnectedAudioDevices(); Log.i(TAG, "Max connected audio devices set to " + mMaxConnectedAudioDevices); // Step 3: Start handler thread for state machines mStateMachines.clear(); mStateMachinesThread = new HandlerThread("A2dpService.StateMachines"); mStateMachinesThread.start(); // Step 4: Setup codec config mA2dpCodecConfig = new A2dpCodecConfig(this, mA2dpNativeInterface); // Step 5: Initialize native interface mA2dpNativeInterface.init(mMaxConnectedAudioDevices, mA2dpCodecConfig.codecConfigPriorities(), mA2dpCodecConfig.codecConfigOffloading()); // Step 6: Check if A2DP is in offload mode mA2dpOffloadEnabled = mAdapterService.isA2dpOffloadEnabled(); if (DBG) { Log.d(TAG, "A2DP offload flag set to " + mA2dpOffloadEnabled); } // Step 7: Setup broadcast receivers IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); mBondStateChangedReceiver = new BondStateChangedReceiver(); registerReceiver(mBondStateChangedReceiver, filter); filter = new IntentFilter(); filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); mConnectionStateChangedReceiver = new ConnectionStateChangedReceiver(); registerReceiver(mConnectionStateChangedReceiver, filter); // Step 8: Mark service as started setA2dpService(this); // Step 9: Clear active device setActiveDevice(null); return true; } @Override protected boolean stop() { Log.i(TAG, "stop()"); if (sA2dpService == null) { Log.w(TAG, "stop() called before start()"); return true; } // Step 9: Clear active device and stop playing audio removeActiveDevice(true); // Step 8: Mark service as stopped setA2dpService(null); // Step 7: Unregister broadcast receivers unregisterReceiver(mConnectionStateChangedReceiver); mConnectionStateChangedReceiver = null; unregisterReceiver(mBondStateChangedReceiver); mBondStateChangedReceiver = null; // Step 6: Cleanup native interface mA2dpNativeInterface.cleanup(); mA2dpNativeInterface = null; // Step 5: Clear codec config mA2dpCodecConfig = null; // Step 4: Destroy state machines and stop handler thread synchronized (mStateMachines) { for (A2dpStateMachine sm : mStateMachines.values()) { sm.doQuit(); sm.cleanup(); } mStateMachines.clear(); } mStateMachinesThread.quitSafely(); mStateMachinesThread = null; // Step 2: Reset maximum number of connected audio devices mMaxConnectedAudioDevices = 1; // Step 1: Clear AdapterService, A2dpNativeInterface, AudioManager mAudioManager = null; mA2dpNativeInterface = null; mAdapterService = null; return true; } @Override protected void cleanup() { Log.i(TAG, "cleanup()"); } 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 (getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { Log.e(TAG, "Cannot connect to " + device + " : CONNECTION_POLICY_FORBIDDEN"); return false; } if (!ArrayUtils.contains(mAdapterService.getRemoteUuids(device), BluetoothUuid.A2DP_SINK)) { Log.e(TAG, "Cannot connect to " + device + " : Remote does not have A2DP Sink UUID"); return false; } synchronized (mStateMachines) { if (!connectionAllowedCheckMaxDevices(device)) { // when mMaxConnectedAudioDevices is one, disconnect current device first. if (mMaxConnectedAudioDevices == 1) { List sinks = getDevicesMatchingConnectionStates( new int[] {BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_DISCONNECTING}); for (BluetoothDevice sink : sinks) { if (sink.equals(device)) { Log.w(TAG, "Connecting to device " + device + " : disconnect skipped"); continue; } disconnect(sink); } } else { Log.e(TAG, "Cannot connect to " + device + " : too many connected devices"); return false; } } 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; } } /** * Disconnects A2dp for the remote bluetooth device * * @param device is the device with which we would like to disconnect a2dp * @return true if profile disconnected, false if device not connected over a2dp */ public 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 */ private boolean connectionAllowedCheckMaxDevices(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); } /** * Check whether can connect to a peer device. * The check considers a number of factors during the evaluation. * * @param device the peer device to connect to * @param isOutgoingRequest if true, the check is for outgoing connection * request, otherwise is for incoming connection request * @return true if connection is allowed, otherwise false */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public boolean okToConnect(BluetoothDevice device, boolean isOutgoingRequest) { Log.i(TAG, "okToConnect: device " + device + " isOutgoingRequest: " + isOutgoingRequest); // Check if this is an incoming connection in Quiet mode. if (mAdapterService.isQuietModeEnabled() && !isOutgoingRequest) { Log.e(TAG, "okToConnect: cannot connect to " + device + " : quiet mode enabled"); return false; } // Check if too many devices if (!connectionAllowedCheckMaxDevices(device)) { Log.e(TAG, "okToConnect: cannot connect to " + device + " : too many connected devices"); return false; } // Check connectionPolicy and accept or reject the connection. int connectionPolicy = getConnectionPolicy(device); int bondState = mAdapterService.getBondState(device); // Allow this connection only if the device is bonded. Any attempt to connect while // bonding would potentially lead to an unauthorized connection. if (bondState != BluetoothDevice.BOND_BONDED) { Log.w(TAG, "okToConnect: return false, bondState=" + bondState); return false; } else if (connectionPolicy != BluetoothProfile.CONNECTION_POLICY_UNKNOWN && connectionPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED) { // Otherwise, reject the connection if connectionPolicy is not valid. Log.w(TAG, "okToConnect: return false, connectionPolicy=" + connectionPolicy); return false; } return true; } List getDevicesMatchingConnectionStates(int[] states) { enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); List devices = new ArrayList<>(); if (states == null) { return devices; } final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices(); if (bondedDevices == null) { return devices; } synchronized (mStateMachines) { for (BluetoothDevice device : bondedDevices) { if (!ArrayUtils.contains(mAdapterService.getRemoteUuids(device), BluetoothUuid.A2DP_SINK)) { continue; } int connectionState = BluetoothProfile.STATE_DISCONNECTED; A2dpStateMachine sm = mStateMachines.get(device); if (sm != null) { connectionState = sm.getConnectionState(); } for (int state : states) { if (connectionState == state) { devices.add(device); break; } } } return devices; } } /** * Get the list of devices that have state machines. * * @return the list of devices that have state machines */ @VisibleForTesting List getDevices() { List devices = new ArrayList<>(); synchronized (mStateMachines) { for (A2dpStateMachine sm : mStateMachines.values()) { devices.add(sm.getDevice()); } 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(); } } private void removeActiveDevice(boolean forceStopPlayingAudio) { synchronized (mActiveSwitchingGuard) { BluetoothDevice previousActiveDevice = null; synchronized (mStateMachines) { if (mActiveDevice == null) return; previousActiveDevice = mActiveDevice; } // This needs to happen before we inform the audio manager that the device // disconnected. Please see comment in updateAndBroadcastActiveDevice() for why. updateAndBroadcastActiveDevice(null); // Make sure the Audio Manager knows the previous Active device is disconnected. // However, if A2DP is still connected and not forcing stop audio for that remote // device, the user has explicitly switched the output to the local device and music // should continue playing. Otherwise, the remote device has been indeed disconnected // and audio should be suspended before switching the output to the local device. boolean suppressNoisyIntent = !forceStopPlayingAudio && (getConnectionState(previousActiveDevice) == BluetoothProfile.STATE_CONNECTED); Log.i(TAG, "removeActiveDevice: suppressNoisyIntent=" + suppressNoisyIntent); mAudioManager.setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent( previousActiveDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.A2DP, suppressNoisyIntent, -1); synchronized (mStateMachines) { // Make sure the Active device in native layer is set to null and audio is off if (!mA2dpNativeInterface.setActiveDevice(null)) { Log.w(TAG, "setActiveDevice(null): Cannot remove active device in native " + "layer"); } } } } /** * Process a change in the silence mode for a {@link BluetoothDevice}. * * @param device the device to change silence mode * @param silence true to enable silence mode, false to disable. * @return true on success, false on error */ @VisibleForTesting public boolean setSilenceMode(BluetoothDevice device, boolean silence) { if (DBG) { Log.d(TAG, "setSilenceMode(" + device + "): " + silence); } if (silence && Objects.equals(mActiveDevice, device)) { removeActiveDevice(true); } else if (!silence && mActiveDevice == null) { // Set the device as the active device if currently no active device. setActiveDevice(device); } if (!mA2dpNativeInterface.setSilenceDevice(device, silence)) { Log.e(TAG, "Cannot set " + device + " silence mode " + silence + " in native layer"); return false; } return true; } /** * Set the active device. * * @param device the active device * @return true on success, otherwise false */ public boolean setActiveDevice(BluetoothDevice device) { enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission"); synchronized (mActiveSwitchingGuard) { if (device == null) { // Remove active device and continue playing audio only if necessary. removeActiveDevice(false); return true; } A2dpStateMachine sm = null; BluetoothDevice previousActiveDevice = null; synchronized (mStateMachines) { if (Objects.equals(device, mActiveDevice)) { Log.i(TAG, "setActiveDevice(" + device + "): current is " + mActiveDevice + " no changed"); // returns true since the device is activated even double attempted return true; } if (DBG) { Log.d(TAG, "setActiveDevice(" + device + "): current is " + mActiveDevice); } 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; } previousActiveDevice = mActiveDevice; } // Switch from one A2DP to another A2DP device if (DBG) { Log.d(TAG, "Switch A2DP devices to " + device + " from " + previousActiveDevice); } // This needs to happen before we inform the audio manager that the device // disconnected. Please see comment in updateAndBroadcastActiveDevice() for why. updateAndBroadcastActiveDevice(device); // 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, -1); } BluetoothDevice newActiveDevice = null; synchronized (mStateMachines) { if (!mA2dpNativeInterface.setActiveDevice(device)) { Log.e(TAG, "setActiveDevice(" + device + "): Cannot set as active in native " + "layer"); // Remove active device and stop playing audio. removeActiveDevice(true); return false; } // Send an intent with the active device codec config BluetoothCodecStatus codecStatus = sm.getCodecStatus(); if (codecStatus != null) { broadcastCodecConfig(mActiveDevice, codecStatus); } newActiveDevice = mActiveDevice; } // Tasks of Bluetooth are done, and now restore the AudioManager side. int rememberedVolume = -1; if (mFactory.getAvrcpTargetService() != null) { rememberedVolume = mFactory.getAvrcpTargetService() .getRememberedVolumeForDevice(newActiveDevice); } mAudioManager.setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent( newActiveDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP, true, rememberedVolume); // 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(newActiveDevice); } return true; } /** * Get the active device. * * @return the active device or null if no device is active */ public BluetoothDevice getActiveDevice() { enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); synchronized (mStateMachines) { return mActiveDevice; } } private boolean isActiveDevice(BluetoothDevice device) { synchronized (mStateMachines) { return (device != null) && Objects.equals(device, mActiveDevice); } } /** * Set connection policy of the profile and connects it if connectionPolicy is * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN} * *

The device should already be paired. * Connection policy can be one of: * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED}, * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN} * * @param device Paired bluetooth device * @param connectionPolicy is the connection policy to set to for this profile * @return true if connectionPolicy is set, false on error */ public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) { enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); if (DBG) { Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy); } boolean setSuccessfully; setSuccessfully = mAdapterService.getDatabase() .setProfileConnectionPolicy(device, BluetoothProfile.A2DP, connectionPolicy); if (setSuccessfully && connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED) { connect(device); } else if (setSuccessfully && connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { disconnect(device); } return setSuccessfully; } /** * Get the connection policy of the profile. * *

The connection policy can be any of: * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED}, * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN} * * @param device Bluetooth device * @return connection policy of the device * @hide */ public int getConnectionPolicy(BluetoothDevice device) { return mAdapterService.getDatabase() .getProfileConnectionPolicy(device, BluetoothProfile.A2DP); } public boolean isAvrcpAbsoluteVolumeSupported() { // TODO (apanicke): Add a hook here for the AvrcpTargetService. return false; } public void setAvrcpAbsoluteVolume(int volume) { // TODO (apanicke): Instead of using A2DP as a middleman for volume changes, add a binder // service to the new AVRCP Profile and have the audio manager use that instead. if (mFactory.getAvrcpTargetService() != null) { mFactory.getAvrcpTargetService().sendVolumeChanged(volume); return; } } 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 current * 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 + ")"); } synchronized (mStateMachines) { if (device == null) { device = mActiveDevice; } if (device == null) { return null; } 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, "setCodecConfigPreference: Invalid device"); return; } if (codecConfig == null) { Log.e(TAG, "setCodecConfigPreference: Codec config can't be null"); return; } BluetoothCodecStatus codecStatus = getCodecStatus(device); if (codecStatus == null) { Log.e(TAG, "setCodecConfigPreference: Codec status is null"); return; } mA2dpCodecConfig.setCodecConfigPreference(device, codecStatus, 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, "enableOptionalCodecs: Invalid device"); return; } if (getSupportsOptionalCodecs(device) != BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED) { Log.e(TAG, "enableOptionalCodecs: No optional codecs"); return; } BluetoothCodecStatus codecStatus = getCodecStatus(device); if (codecStatus == null) { Log.e(TAG, "enableOptionalCodecs: Codec status is null"); return; } mA2dpCodecConfig.enableOptionalCodecs(device, codecStatus.getCodecConfig()); } /** * 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, "disableOptionalCodecs: Invalid device"); return; } if (getSupportsOptionalCodecs(device) != BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED) { Log.e(TAG, "disableOptionalCodecs: No optional codecs"); return; } BluetoothCodecStatus codecStatus = getCodecStatus(device); if (codecStatus == null) { Log.e(TAG, "disableOptionalCodecs: Codec status is null"); return; } mA2dpCodecConfig.disableOptionalCodecs(device, codecStatus.getCodecConfig()); } /** * Checks whether optional codecs are supported * * @param device is the remote bluetooth device. * @return whether optional codecs are supported. Possible values are: * {@link OptionalCodecsSupportStatus#OPTIONAL_CODECS_SUPPORTED}, * {@link OptionalCodecsSupportStatus#OPTIONAL_CODECS_NOT_SUPPORTED}, * {@link OptionalCodecsSupportStatus#OPTIONAL_CODECS_SUPPORT_UNKNOWN}. */ public @OptionalCodecsSupportStatus int getSupportsOptionalCodecs(BluetoothDevice device) { enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission"); return mAdapterService.getDatabase().getA2dpSupportsOptionalCodecs(device); } 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; mAdapterService.getDatabase().setA2dpSupportsOptionalCodecs(device, value); } /** * Checks whether optional codecs are enabled * * @param device is the remote bluetooth device * @return whether the optional codecs are enabled. Possible values are: * {@link OptionalCodecsPreferenceStatus#OPTIONAL_CODECS_PREF_ENABLED}, * {@link OptionalCodecsPreferenceStatus#OPTIONAL_CODECS_PREF_DISABLED}, * {@link OptionalCodecsPreferenceStatus#OPTIONAL_CODECS_PREF_UNKNOWN}. */ public @OptionalCodecsPreferenceStatus int getOptionalCodecsEnabled(BluetoothDevice device) { enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); return mAdapterService.getDatabase().getA2dpOptionalCodecsEnabled(device); } /** * Sets the optional codecs to be set to the passed in value * * @param device is the remote bluetooth device * @param value is the new status for the optional codecs. Possible values are: * {@link OptionalCodecsPreferenceStatus#OPTIONAL_CODECS_PREF_ENABLED}, * {@link OptionalCodecsPreferenceStatus#OPTIONAL_CODECS_PREF_DISABLED}, * {@link OptionalCodecsPreferenceStatus#OPTIONAL_CODECS_PREF_UNKNOWN}. */ public void setOptionalCodecsEnabled(BluetoothDevice device, @OptionalCodecsPreferenceStatus 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; } mAdapterService.getDatabase().setA2dpOptionalCodecsEnabled(device, value); } // Handle messages from native (JNI) to Java void messageFromNative(A2dpStackEvent stackEvent) { Objects.requireNonNull(stackEvent.device, "Device should never be null, event: " + stackEvent); synchronized (mStateMachines) { BluetoothDevice device = stackEvent.device; A2dpStateMachine sm = mStateMachines.get(device); if (sm == null) { if (stackEvent.type == A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) { switch (stackEvent.valueInt) { case A2dpStackEvent.CONNECTION_STATE_CONNECTED: case A2dpStackEvent.CONNECTION_STATE_CONNECTING: // Create a new state machine only when connecting to a device if (!connectionAllowedCheckMaxDevices(device)) { Log.e(TAG, "Cannot connect to " + device + " : too many connected devices"); return; } sm = getOrCreateStateMachine(device); break; default: break; } } } 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 */ @VisibleForTesting public void codecConfigUpdated(BluetoothDevice device, BluetoothCodecStatus codecStatus, boolean sameAudioFeedingParameters) { // Log codec config and capability metrics BluetoothCodecConfig codecConfig = codecStatus.getCodecConfig(); int metricId = mAdapterService.getMetricId(device); BluetoothStatsLog.write(BluetoothStatsLog.BLUETOOTH_A2DP_CODEC_CONFIG_CHANGED, mAdapterService.obfuscateAddress(device), codecConfig.getCodecType(), codecConfig.getCodecPriority(), codecConfig.getSampleRate(), codecConfig.getBitsPerSample(), codecConfig.getChannelMode(), codecConfig.getCodecSpecific1(), codecConfig.getCodecSpecific2(), codecConfig.getCodecSpecific3(), codecConfig.getCodecSpecific4(), metricId); BluetoothCodecConfig[] codecCapabilities = codecStatus.getCodecsSelectableCapabilities(); for (BluetoothCodecConfig codecCapability : codecCapabilities) { BluetoothStatsLog.write(BluetoothStatsLog.BLUETOOTH_A2DP_CODEC_CAPABILITY_CHANGED, mAdapterService.obfuscateAddress(device), codecCapability.getCodecType(), codecCapability.getCodecPriority(), codecCapability.getSampleRate(), codecCapability.getBitsPerSample(), codecCapability.getChannelMode(), codecConfig.getCodecSpecific1(), codecConfig.getCodecSpecific2(), codecConfig.getCodecSpecific3(), codecConfig.getCodecSpecific4(), metricId); } 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, mA2dpNativeInterface, mStateMachinesThread.getLooper()); mStateMachines.put(device, sm); return sm; } } // This needs to run before any of the Audio Manager connection functions since // AVRCP needs to be aware that the audio device is changed before the Audio Manager // changes the volume of the output devices. private void updateAndBroadcastActiveDevice(BluetoothDevice device) { if (DBG) { Log.d(TAG, "updateAndBroadcastActiveDevice(" + device + ")"); } // Make sure volume has been store before device been remove from active. if (mFactory.getAvrcpTargetService() != null) { mFactory.getAvrcpTargetService().volumeDeviceSwitched(device); } synchronized (mStateMachines) { mActiveDevice = device; } BluetoothStatsLog.write(BluetoothStatsLog.BLUETOOTH_ACTIVE_DEVICE_CHANGED, BluetoothProfile.A2DP, mAdapterService.obfuscateAddress(device), mAdapterService.getMetricId(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); } 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); Objects.requireNonNull(device, "ACTION_BOND_STATE_CHANGED with no EXTRA_DEVICE"); bondStateChanged(device, state); } } /** * Process a change in the bonding state for a device. * * @param device the device whose bonding state has changed * @param bondState the new bond state for the device. Possible values are: * {@link BluetoothDevice#BOND_NONE}, * {@link BluetoothDevice#BOND_BONDING}, * {@link BluetoothDevice#BOND_BONDED}. */ @VisibleForTesting void bondStateChanged(BluetoothDevice device, int bondState) { if (DBG) { Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState); } // Remove state machine if the bonding for a device is removed if (bondState != BluetoothDevice.BOND_NONE) { return; } synchronized (mStateMachines) { A2dpStateMachine sm = mStateMachines.get(device); if (sm == null) { return; } if (sm.getConnectionState() != BluetoothProfile.STATE_DISCONNECTED) { return; } } if (mFactory.getAvrcpTargetService() != null) { mFactory.getAvrcpTargetService().removeStoredVolumeForDevice(device); } removeStateMachine(device); } private void removeStateMachine(BluetoothDevice device) { synchronized (mStateMachines) { A2dpStateMachine sm = mStateMachines.get(device); if (sm == null) { Log.w(TAG, "removeStateMachine: device " + device + " does not have a state machine"); return; } Log.i(TAG, "removeStateMachine: removing state machine for device: " + device); sm.doQuit(); sm.cleanup(); mStateMachines.remove(device); } } /** * Update and initiate optional codec status change to native. * * @param device the device to change optional codec status */ @VisibleForTesting public void updateOptionalCodecsSupport(BluetoothDevice device) { int previousSupport = getSupportsOptionalCodecs(device); boolean supportsOptional = false; boolean hasMandatoryCodec = false; synchronized (mStateMachines) { A2dpStateMachine sm = mStateMachines.get(device); if (sm == null) { return; } BluetoothCodecStatus codecStatus = sm.getCodecStatus(); if (codecStatus != null) { for (BluetoothCodecConfig config : codecStatus.getCodecsSelectableCapabilities()) { if (config.isMandatoryCodec()) { hasMandatoryCodec = true; } else { supportsOptional = true; } } } } if (!hasMandatoryCodec) { // Mandatory codec(SBC) is not selectable. It could be caused by the remote device // select codec before native finish get codec capabilities. Stop use this codec // status as the reference to support/enable optional codecs. Log.i(TAG, "updateOptionalCodecsSupport: Mandatory codec is not selectable."); return; } if (previousSupport == BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN || supportsOptional != (previousSupport == BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED)) { setSupportsOptionalCodecs(device, supportsOptional); } if (supportsOptional) { int enabled = getOptionalCodecsEnabled(device); switch (enabled) { case BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN: // Enable optional codec by default. setOptionalCodecsEnabled(device, BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED); // Fall through intended case BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED: enableOptionalCodecs(device); break; case BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED: disableOptionalCodecs(device); break; } } } private void connectionStateChanged(BluetoothDevice device, int fromState, int toState) { if ((device == null) || (fromState == toState)) { return; } if (toState == BluetoothProfile.STATE_CONNECTED) { MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.A2DP); } // 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 is not connected anymore if (isActiveDevice(device) && (fromState == BluetoothProfile.STATE_CONNECTED)) { setActiveDevice(null); } // Check if the device is disconnected - if unbond, remove the state machine if (toState == BluetoothProfile.STATE_DISCONNECTED) { if (mAdapterService.getBondState(device) == BluetoothDevice.BOND_NONE) { if (mFactory.getAvrcpTargetService() != null) { mFactory.getAvrcpTargetService().removeStoredVolumeForDevice(device); } removeStateMachine(device); } } } /** * Receiver for processing device connection state changes. * *

    *
  • Update codec support per device when device is (re)connected *
  • Delete the state machine instance if the device is disconnected and unbond *
*/ 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 a 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 setConnectionPolicy(BluetoothDevice device, int connectionPolicy) { A2dpService service = getService(); if (service == null) { return false; } return service.setConnectionPolicy(device, connectionPolicy); } @Override public int getPriority(BluetoothDevice device) { A2dpService service = getService(); if (service == null) { return BluetoothProfile.CONNECTION_POLICY_UNKNOWN; } enforceBluetoothPermission(service); return service.getConnectionPolicy(device); } @Override public int getConnectionPolicy(BluetoothDevice device) { A2dpService service = getService(); if (service == null) { return BluetoothProfile.CONNECTION_POLICY_UNKNOWN; } enforceBluetoothPrivilegedPermission(service); return service.getConnectionPolicy(device); } @Override public boolean isAvrcpAbsoluteVolumeSupported() { A2dpService service = getService(); if (service == null) { return false; } return service.isAvrcpAbsoluteVolumeSupported(); } @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); ProfileService.println(sb, "mMaxConnectedAudioDevices: " + mMaxConnectedAudioDevices); if (mA2dpCodecConfig != null) { ProfileService.println(sb, "codecConfigPriorities:"); for (BluetoothCodecConfig codecConfig : mA2dpCodecConfig.codecConfigPriorities()) { ProfileService.println(sb, " " + codecConfig.getCodecName() + ": " + codecConfig.getCodecPriority()); } ProfileService.println(sb, "mA2dpOffloadEnabled: " + mA2dpOffloadEnabled); if (mA2dpOffloadEnabled) { ProfileService.println(sb, "codecConfigOffloading:"); for (BluetoothCodecConfig codecConfig : mA2dpCodecConfig.codecConfigOffloading()) { ProfileService.println(sb, " " + codecConfig); } } } else { ProfileService.println(sb, "mA2dpCodecConfig: null"); } for (A2dpStateMachine sm : mStateMachines.values()) { sm.dump(sb); } } }