/* * Copyright (C) 2014 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.a2dpsink; import android.annotation.RequiresPermission; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAudioConfig; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.IBluetoothA2dpSink; import android.content.Attributable; import android.content.AttributionSource; import android.media.AudioManager; import android.util.Log; import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.btservice.storage.DatabaseManager; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; /** * Provides Bluetooth A2DP Sink profile, as a service in the Bluetooth application. * @hide */ public class A2dpSinkService extends ProfileService { private static final String TAG = "A2dpSinkService"; private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); private int mMaxConnectedAudioDevices; private AdapterService mAdapterService; private DatabaseManager mDatabaseManager; protected Map mDeviceStateMap = new ConcurrentHashMap<>(1); private final Object mStreamHandlerLock = new Object(); private final Object mActiveDeviceLock = new Object(); private BluetoothDevice mActiveDevice = null; private A2dpSinkStreamHandler mA2dpSinkStreamHandler; private static A2dpSinkService sService; static { classInitNative(); } @Override protected boolean start() { mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(), "AdapterService cannot be null when A2dpSinkService starts"); mDatabaseManager = Objects.requireNonNull(AdapterService.getAdapterService().getDatabase(), "DatabaseManager cannot be null when A2dpSinkService starts"); synchronized (mStreamHandlerLock) { mA2dpSinkStreamHandler = new A2dpSinkStreamHandler(this, this); } mMaxConnectedAudioDevices = mAdapterService.getMaxConnectedAudioDevices(); initNative(mMaxConnectedAudioDevices); setA2dpSinkService(this); return true; } @Override protected boolean stop() { setA2dpSinkService(null); cleanupNative(); for (A2dpSinkStateMachine stateMachine : mDeviceStateMap.values()) { stateMachine.quitNow(); } mDeviceStateMap.clear(); synchronized (mStreamHandlerLock) { if (mA2dpSinkStreamHandler != null) { mA2dpSinkStreamHandler.cleanup(); mA2dpSinkStreamHandler = null; } } return true; } public static synchronized A2dpSinkService getA2dpSinkService() { return sService; } /** * Testing API to inject a mockA2dpSinkService. * @hide */ @VisibleForTesting public static synchronized void setA2dpSinkService(A2dpSinkService service) { sService = service; } public A2dpSinkService() {} /** * Set the device that should be allowed to actively stream */ public boolean setActiveDevice(BluetoothDevice device) { // Translate to byte address for JNI. Use an all 0 MAC for no active device byte[] address = null; if (device != null) { address = Utils.getByteAddress(device); } else { address = Utils.getBytesFromAddress("00:00:00:00:00:00"); } synchronized (mActiveDeviceLock) { if (setActiveDeviceNative(address)) { mActiveDevice = device; return true; } return false; } } /** * Get the device that is allowed to be actively streaming */ public BluetoothDevice getActiveDevice() { synchronized (mActiveDeviceLock) { return mActiveDevice; } } /** * Request audio focus such that the designated device can stream audio */ public void requestAudioFocus(BluetoothDevice device, boolean request) { synchronized (mStreamHandlerLock) { if (mA2dpSinkStreamHandler == null) return; mA2dpSinkStreamHandler.requestAudioFocus(request); } } /** * Get the current Bluetooth Audio focus state * * @return AudioManger.AUDIOFOCUS_* states on success, or AudioManager.ERROR on error */ public int getFocusState() { synchronized (mStreamHandlerLock) { if (mA2dpSinkStreamHandler == null) return AudioManager.ERROR; return mA2dpSinkStreamHandler.getFocusState(); } } @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) boolean isA2dpPlaying(BluetoothDevice device) { enforceCallingOrSelfPermission( BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); synchronized (mStreamHandlerLock) { if (mA2dpSinkStreamHandler == null) return false; return mA2dpSinkStreamHandler.isPlaying(); } } @Override protected IProfileServiceBinder initBinder() { return new A2dpSinkServiceBinder(this); } //Binder object: Must be static class or memory leak may occur private static class A2dpSinkServiceBinder extends IBluetoothA2dpSink.Stub implements IProfileServiceBinder { private A2dpSinkService mService; @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) private A2dpSinkService getService(AttributionSource source) { if (!Utils.checkCallerIsSystemOrActiveUser(TAG) || !Utils.checkServiceAvailable(mService, TAG) || !Utils.checkConnectPermissionForDataDelivery(mService, source, TAG)) { return null; } return mService; } A2dpSinkServiceBinder(A2dpSinkService svc) { mService = svc; } @Override public void cleanup() { mService = null; } @Override public boolean connect(BluetoothDevice device, AttributionSource source) { Attributable.setAttributionSource(device, source); A2dpSinkService service = getService(source); if (service == null) { return false; } return service.connect(device); } @Override public boolean disconnect(BluetoothDevice device, AttributionSource source) { Attributable.setAttributionSource(device, source); A2dpSinkService service = getService(source); if (service == null) { return false; } return service.disconnect(device); } @Override public List getConnectedDevices(AttributionSource source) { A2dpSinkService service = getService(source); if (service == null) { return new ArrayList(0); } return service.getConnectedDevices(); } @Override public List getDevicesMatchingConnectionStates(int[] states, AttributionSource source) { A2dpSinkService service = getService(source); if (service == null) { return new ArrayList(0); } return service.getDevicesMatchingConnectionStates(states); } @Override public int getConnectionState(BluetoothDevice device, AttributionSource source) { Attributable.setAttributionSource(device, source); A2dpSinkService service = getService(source); if (service == null) { return BluetoothProfile.STATE_DISCONNECTED; } return service.getConnectionState(device); } @Override public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy, AttributionSource source) { Attributable.setAttributionSource(device, source); A2dpSinkService service = getService(source); if (service == null) { return false; } return service.setConnectionPolicy(device, connectionPolicy); } @Override public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { Attributable.setAttributionSource(device, source); A2dpSinkService service = getService(source); if (service == null) { return BluetoothProfile.CONNECTION_POLICY_UNKNOWN; } return service.getConnectionPolicy(device); } @Override public boolean isA2dpPlaying(BluetoothDevice device, AttributionSource source) { Attributable.setAttributionSource(device, source); A2dpSinkService service = getService(source); if (service == null) { return false; } return service.isA2dpPlaying(device); } @Override public BluetoothAudioConfig getAudioConfig(BluetoothDevice device, AttributionSource source) { Attributable.setAttributionSource(device, source); A2dpSinkService service = getService(source); if (service == null) { return null; } return service.getAudioConfig(device); } } /* Generic Profile Code */ /** * Connect the given Bluetooth device. * * @return true if connection is successful, false otherwise. */ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public boolean connect(BluetoothDevice device) { enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); if (device == null) { throw new IllegalArgumentException("Null device"); } if (DBG) { StringBuilder sb = new StringBuilder(); dump(sb); Log.d(TAG, " connect device: " + device + ", InstanceMap start state: " + sb.toString()); } if (getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { Log.w(TAG, "Connection not allowed: <" + device.getAddress() + "> is CONNECTION_POLICY_FORBIDDEN"); return false; } A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(device); if (stateMachine != null) { stateMachine.connect(); return true; } else { // a state machine instance doesn't exist yet, and the max has been reached. Log.e(TAG, "Maxed out on the number of allowed A2DP Sink connections. " + "Connect request rejected on " + device); return false; } } /** * Disconnect the given Bluetooth device. * * @return true if disconnect is successful, false otherwise. */ public boolean disconnect(BluetoothDevice device) { if (DBG) { StringBuilder sb = new StringBuilder(); dump(sb); Log.d(TAG, "A2DP disconnect device: " + device + ", InstanceMap start state: " + sb.toString()); } A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device); // a state machine instance doesn't exist. maybe it is already gone? if (stateMachine == null) { return false; } int connectionState = stateMachine.getState(); if (connectionState == BluetoothProfile.STATE_DISCONNECTED || connectionState == BluetoothProfile.STATE_DISCONNECTING) { return false; } // upon completion of disconnect, the state machine will remove itself from the available // devices map stateMachine.disconnect(); return true; } void removeStateMachine(A2dpSinkStateMachine stateMachine) { mDeviceStateMap.remove(stateMachine.getDevice()); } public List getConnectedDevices() { return getDevicesMatchingConnectionStates(new int[]{BluetoothAdapter.STATE_CONNECTED}); } protected A2dpSinkStateMachine getOrCreateStateMachine(BluetoothDevice device) { A2dpSinkStateMachine newStateMachine = new A2dpSinkStateMachine(device, this); A2dpSinkStateMachine existingStateMachine = mDeviceStateMap.putIfAbsent(device, newStateMachine); // Given null is not a valid value in our map, ConcurrentHashMap will return null if the // key was absent and our new value was added. We should then start and return it. if (existingStateMachine == null) { newStateMachine.start(); return newStateMachine; } return existingStateMachine; } List getDevicesMatchingConnectionStates(int[] states) { if (DBG) Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states)); List deviceList = new ArrayList<>(); BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices(); int connectionState; for (BluetoothDevice device : bondedDevices) { connectionState = getConnectionState(device); if (DBG) Log.d(TAG, "Device: " + device + "State: " + connectionState); for (int i = 0; i < states.length; i++) { if (connectionState == states[i]) { deviceList.add(device); } } } if (DBG) Log.d(TAG, deviceList.toString()); Log.d(TAG, "GetDevicesDone"); return deviceList; } /** * Get the current connection state of the profile * * @param device is the remote bluetooth device * @return {@link BluetoothProfile#STATE_DISCONNECTED} if this profile is disconnected, * {@link BluetoothProfile#STATE_CONNECTING} if this profile is being connected, * {@link BluetoothProfile#STATE_CONNECTED} if this profile is connected, or * {@link BluetoothProfile#STATE_DISCONNECTING} if this profile is being disconnected */ public int getConnectionState(BluetoothDevice device) { A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device); return (stateMachine == null) ? BluetoothProfile.STATE_DISCONNECTED : stateMachine.getState(); } /** * 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 */ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) { enforceCallingOrSelfPermission( BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); if (DBG) { Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy); } if (!mDatabaseManager.setProfileConnectionPolicy(device, BluetoothProfile.A2DP_SINK, connectionPolicy)) { return false; } if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED) { connect(device); } else if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { disconnect(device); } return true; } /** * Get the connection policy of the profile. * * @param device the remote device * @return connection policy of the specified device */ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public int getConnectionPolicy(BluetoothDevice device) { enforceCallingOrSelfPermission( BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); return mDatabaseManager .getProfileConnectionPolicy(device, BluetoothProfile.A2DP_SINK); } @Override public void dump(StringBuilder sb) { super.dump(sb); ProfileService.println(sb, "Active Device = " + getActiveDevice()); ProfileService.println(sb, "Max Connected Devices = " + mMaxConnectedAudioDevices); ProfileService.println(sb, "Devices Tracked = " + mDeviceStateMap.size()); for (A2dpSinkStateMachine stateMachine : mDeviceStateMap.values()) { ProfileService.println(sb, "==== StateMachine for " + stateMachine.getDevice() + " ===="); stateMachine.dump(sb); } } BluetoothAudioConfig getAudioConfig(BluetoothDevice device) { A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device); // a state machine instance doesn't exist. maybe it is already gone? if (stateMachine == null) { return null; } return stateMachine.getAudioConfig(); } /* JNI interfaces*/ private static native void classInitNative(); private native void initNative(int maxConnectedAudioDevices); private native void cleanupNative(); native boolean connectA2dpNative(byte[] address); native boolean disconnectA2dpNative(byte[] address); /** * set A2DP state machine as the active device * the active device is the only one that will receive passthrough commands and the only one * that will have its audio decoded * * @hide * @param address * @return active device request has been scheduled */ public native boolean setActiveDeviceNative(byte[] address); /** * inform A2DP decoder of the current audio focus * * @param focusGranted */ @VisibleForTesting public native void informAudioFocusStateNative(int focusGranted); /** * inform A2DP decoder the desired audio gain * * @param gain */ @VisibleForTesting public native void informAudioTrackGainNative(float gain); private void onConnectionStateChanged(byte[] address, int state) { StackEvent event = StackEvent.connectionStateChanged(getAnonymousDevice(address), state); A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(event.mDevice); stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event); } private void onAudioStateChanged(byte[] address, int state) { synchronized (mStreamHandlerLock) { if (mA2dpSinkStreamHandler == null) { Log.e(TAG, "Received audio state change before we've been started"); return; } else if (state == StackEvent.AUDIO_STATE_STARTED) { mA2dpSinkStreamHandler.obtainMessage( A2dpSinkStreamHandler.SRC_STR_START).sendToTarget(); } else if (state == StackEvent.AUDIO_STATE_STOPPED || state == StackEvent.AUDIO_STATE_REMOTE_SUSPEND) { mA2dpSinkStreamHandler.obtainMessage( A2dpSinkStreamHandler.SRC_STR_STOP).sendToTarget(); } } } private void onAudioConfigChanged(byte[] address, int sampleRate, int channelCount) { StackEvent event = StackEvent.audioConfigChanged(getAnonymousDevice(address), sampleRate, channelCount); A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(event.mDevice); stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event); } }