/* * 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.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAudioConfig; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.IBluetoothA2dpSink; import android.util.Log; import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; 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.Set; 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); static final int MAXIMUM_CONNECTED_DEVICES = 1; private final BluetoothAdapter mAdapter; protected Map mDeviceStateMap = new ConcurrentHashMap<>(1); private A2dpSinkStreamHandler mA2dpSinkStreamHandler; private static A2dpSinkService sService; static { classInitNative(); } @Override protected boolean start() { initNative(); sService = this; mA2dpSinkStreamHandler = new A2dpSinkStreamHandler(this, this); return true; } @Override protected boolean stop() { for (A2dpSinkStateMachine stateMachine : mDeviceStateMap.values()) { stateMachine.quitNow(); } sService = null; return true; } public static A2dpSinkService getA2dpSinkService() { return sService; } public A2dpSinkService() { mAdapter = BluetoothAdapter.getDefaultAdapter(); } protected A2dpSinkStateMachine newStateMachine(BluetoothDevice device) { return new A2dpSinkStateMachine(device, this); } protected synchronized A2dpSinkStateMachine getStateMachine(BluetoothDevice device) { return mDeviceStateMap.get(device); } /** * Request audio focus such that the designated device can stream audio */ public void requestAudioFocus(BluetoothDevice device, boolean request) { mA2dpSinkStreamHandler.requestAudioFocus(request); } @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; private A2dpSinkService getService() { if (!Utils.checkCaller()) { Log.w(TAG, "A2dp call not allowed for non-active user"); return null; } if (mService != null) { return mService; } return null; } A2dpSinkServiceBinder(A2dpSinkService svc) { mService = svc; } @Override public void cleanup() { mService = null; } @Override public boolean connect(BluetoothDevice device) { A2dpSinkService service = getService(); if (service == null) { return false; } return service.connect(device); } @Override public boolean disconnect(BluetoothDevice device) { A2dpSinkService service = getService(); if (service == null) { return false; } return service.disconnect(device); } @Override public List getConnectedDevices() { A2dpSinkService service = getService(); if (service == null) { return new ArrayList(0); } return service.getConnectedDevices(); } @Override public List getDevicesMatchingConnectionStates(int[] states) { A2dpSinkService service = getService(); if (service == null) { return new ArrayList(0); } return service.getDevicesMatchingConnectionStates(states); } @Override public int getConnectionState(BluetoothDevice device) { A2dpSinkService service = getService(); if (service == null) { return BluetoothProfile.STATE_DISCONNECTED; } return service.getConnectionState(device); } @Override public boolean setPriority(BluetoothDevice device, int priority) { A2dpSinkService service = getService(); if (service == null) { return false; } return service.setPriority(device, priority); } @Override public int getPriority(BluetoothDevice device) { A2dpSinkService service = getService(); if (service == null) { return BluetoothProfile.PRIORITY_UNDEFINED; } return service.getPriority(device); } @Override public boolean isA2dpPlaying(BluetoothDevice device) { A2dpSinkService service = getService(); if (service == null) { return false; } return service.isA2dpPlaying(device); } @Override public BluetoothAudioConfig getAudioConfig(BluetoothDevice device) { A2dpSinkService service = getService(); 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. */ public synchronized boolean connect(BluetoothDevice device) { 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 (getPriority(device) == BluetoothProfile.PRIORITY_OFF) { Log.w(TAG, "Connection not allowed: <" + device.getAddress() + "> is PRIORITY_OFF"); 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 MAP connections. " + "Connect request rejected on " + device); return false; } } /** * Disconnect the given Bluetooth device. * * @return true if disconnect is successful, false otherwise. */ public synchronized 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 stateMachine = mDeviceStateMap.get(device); if (stateMachine == null) { stateMachine = newStateMachine(device); mDeviceStateMap.put(device, stateMachine); stateMachine.start(); } return stateMachine; } List getDevicesMatchingConnectionStates(int[] states) { if (DBG) Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states)); List deviceList = new ArrayList<>(); Set bondedDevices = mAdapter.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; } synchronized int getConnectionState(BluetoothDevice device) { A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device); return (stateMachine == null) ? BluetoothProfile.STATE_DISCONNECTED : stateMachine.getState(); } /** * Set the priority of the profile. * * @param device the remote device * @param priority the priority of the profile * @return true on success, otherwise false */ public boolean setPriority(BluetoothDevice device, int priority) { enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); if (DBG) { Log.d(TAG, "Saved priority " + device + " = " + priority); } AdapterService.getAdapterService().getDatabase() .setProfilePriority(device, BluetoothProfile.A2DP_SINK, priority); return true; } /** * Get the priority of the profile. * * @param device the remote device * @return priority of the specified device */ public int getPriority(BluetoothDevice device) { enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); return AdapterService.getAdapterService().getDatabase() .getProfilePriority(device, BluetoothProfile.A2DP_SINK); } @Override public void dump(StringBuilder sb) { super.dump(sb); ProfileService.println(sb, "Devices Tracked = " + mDeviceStateMap.size()); for (A2dpSinkStateMachine stateMachine : mDeviceStateMap.values()) { ProfileService.println(sb, "==== StateMachine for " + stateMachine.getDevice() + " ===="); stateMachine.dump(sb); } } /** * Get the current Bluetooth Audio focus state * * @return focus */ public static int getFocusState() { return sService.mA2dpSinkStreamHandler.getFocusState(); } boolean isA2dpPlaying(BluetoothDevice device) { enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return mA2dpSinkStreamHandler.isPlaying(); } 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(); private native void cleanupNative(); native boolean connectA2dpNative(byte[] address); native boolean disconnectA2dpNative(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(getDevice(address), state); A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(event.mDevice); stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event); } private void onAudioStateChanged(byte[] address, int state) { if (state == StackEvent.AUDIO_STATE_STARTED) { mA2dpSinkStreamHandler.obtainMessage( A2dpSinkStreamHandler.SRC_STR_START).sendToTarget(); } else if (state == StackEvent.AUDIO_STATE_STOPPED) { mA2dpSinkStreamHandler.obtainMessage( A2dpSinkStreamHandler.SRC_STR_STOP).sendToTarget(); } } private void onAudioConfigChanged(byte[] address, int sampleRate, int channelCount) { StackEvent event = StackEvent.audioConfigChanged(getDevice(address), sampleRate, channelCount); A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(event.mDevice); stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event); } }