blob: 5271cc79139f164ac8382fa81195db90d09d652b [file] [log] [blame]
/*
* 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<BluetoothDevice, A2dpSinkStateMachine> 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<BluetoothDevice> getConnectedDevices() {
A2dpSinkService service = getService();
if (service == null) {
return new ArrayList<BluetoothDevice>(0);
}
return service.getConnectedDevices();
}
@Override
public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
A2dpSinkService service = getService();
if (service == null) {
return new ArrayList<BluetoothDevice>(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<BluetoothDevice> 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<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
if (DBG) Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states));
List<BluetoothDevice> deviceList = new ArrayList<>();
Set<BluetoothDevice> 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
|| 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(getDevice(address), sampleRate,
channelCount);
A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(event.mDevice);
stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event);
}
}