blob: ffb17a42718043647274e0a989d7c6ba7a7f74d3 [file] [log] [blame]
/*
* Copyright 2021 HIMSA II K/S - www.himsa.com.
* Represented by EHIMA - www.ehima.com
*
* 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.vc;
import static android.Manifest.permission.BLUETOOTH_CONNECT;
import static com.android.bluetooth.Utils.enforceBluetoothPrivilegedPermission;
import android.annotation.RequiresPermission;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.BluetoothVolumeControl;
import android.bluetooth.IBluetoothVolumeControl;
import android.content.AttributionSource;
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.util.Log;
import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.btservice.ServiceFactory;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class VolumeControlService extends ProfileService {
private static final boolean DBG = false;
private static final String TAG = "VolumeControlService";
// Upper limit of all VolumeControl devices: Bonded or Connected
private static final int MAX_VC_STATE_MACHINES = 10;
private static VolumeControlService sVolumeControlService;
private AdapterService mAdapterService;
private HandlerThread mStateMachinesThread;
private BluetoothDevice mPreviousAudioDevice;
@VisibleForTesting
VolumeControlNativeInterface mVolumeControlNativeInterface;
@VisibleForTesting
AudioManager mAudioManager;
private final Map<BluetoothDevice, VolumeControlStateMachine> mStateMachines = new HashMap<>();
private BroadcastReceiver mBondStateChangedReceiver;
private BroadcastReceiver mConnectionStateChangedReceiver;
private final ServiceFactory mFactory = new ServiceFactory();
@Override
protected IProfileServiceBinder initBinder() {
return new BluetoothVolumeControlBinder(this);
}
@Override
protected void create() {
if (DBG) {
Log.d(TAG, "create()");
}
}
@Override
protected boolean start() {
if (DBG) {
Log.d(TAG, "start()");
}
if (sVolumeControlService != null) {
throw new IllegalStateException("start() called twice");
}
// Get AdapterService, VolumeControlNativeInterface, AudioManager.
// None of them can be null.
mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(),
"AdapterService cannot be null when VolumeControlService starts");
mVolumeControlNativeInterface = Objects.requireNonNull(
VolumeControlNativeInterface.getInstance(),
"VolumeControlNativeInterface cannot be null when VolumeControlService starts");
mAudioManager = getSystemService(AudioManager.class);
Objects.requireNonNull(mAudioManager,
"AudioManager cannot be null when VolumeControlService starts");
// Start handler thread for state machines
mStateMachines.clear();
mStateMachinesThread = new HandlerThread("VolumeControlService.StateMachines");
mStateMachinesThread.start();
// 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(BluetoothVolumeControl.ACTION_CONNECTION_STATE_CHANGED);
mConnectionStateChangedReceiver = new ConnectionStateChangedReceiver();
registerReceiver(mConnectionStateChangedReceiver, filter);
// Mark service as started
setVolumeControlService(this);
// Initialize native interface
mVolumeControlNativeInterface.init();
return true;
}
@Override
protected boolean stop() {
if (DBG) {
Log.d(TAG, "stop()");
}
if (sVolumeControlService == null) {
Log.w(TAG, "stop() called before start()");
return true;
}
// Cleanup native interface
mVolumeControlNativeInterface.cleanup();
mVolumeControlNativeInterface = null;
// Mark service as stopped
setVolumeControlService(null);
// Unregister broadcast receivers
unregisterReceiver(mBondStateChangedReceiver);
mBondStateChangedReceiver = null;
unregisterReceiver(mConnectionStateChangedReceiver);
mConnectionStateChangedReceiver = null;
// Destroy state machines and stop handler thread
synchronized (mStateMachines) {
for (VolumeControlStateMachine sm : mStateMachines.values()) {
sm.doQuit();
sm.cleanup();
}
mStateMachines.clear();
}
if (mStateMachinesThread != null) {
mStateMachinesThread.quitSafely();
mStateMachinesThread = null;
}
// Clear AdapterService, VolumeControlNativeInterface
mAudioManager = null;
mVolumeControlNativeInterface = null;
mAdapterService = null;
return true;
}
@Override
protected void cleanup() {
if (DBG) {
Log.d(TAG, "cleanup()");
}
}
/**
* Get the VolumeControlService instance
* @return VolumeControlService instance
*/
public static synchronized VolumeControlService getVolumeControlService() {
if (sVolumeControlService == null) {
Log.w(TAG, "getVolumeControlService(): service is NULL");
return null;
}
if (!sVolumeControlService.isAvailable()) {
Log.w(TAG, "getVolumeControlService(): service is not available");
return null;
}
return sVolumeControlService;
}
private static synchronized void setVolumeControlService(VolumeControlService instance) {
if (DBG) {
Log.d(TAG, "setVolumeControlService(): set to: " + instance);
}
sVolumeControlService = instance;
}
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
public boolean connect(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
"Need BLUETOOTH_PRIVILEGED permission");
if (DBG) {
Log.d(TAG, "connect(): " + device);
}
if (device == null) {
return false;
}
if (getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
return false;
}
ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device);
if (!ArrayUtils.contains(featureUuids, BluetoothUuid.VOLUME_CONTROL)) {
Log.e(TAG, "Cannot connect to " + device
+ " : Remote does not have Volume Control UUID");
return false;
}
synchronized (mStateMachines) {
VolumeControlStateMachine smConnect = getOrCreateStateMachine(device);
if (smConnect == null) {
Log.e(TAG, "Cannot connect to " + device + " : no state machine");
}
smConnect.sendMessage(VolumeControlStateMachine.CONNECT);
}
return true;
}
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
public boolean disconnect(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
"Need BLUETOOTH_PRIVILEGED permission");
if (DBG) {
Log.d(TAG, "disconnect(): " + device);
}
if (device == null) {
return false;
}
synchronized (mStateMachines) {
VolumeControlStateMachine sm = getOrCreateStateMachine(device);
if (sm != null) {
sm.sendMessage(VolumeControlStateMachine.DISCONNECT);
}
}
return true;
}
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
List<BluetoothDevice> getConnectedDevices() {
enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
"Need BLUETOOTH_PRIVILEGED permission");
synchronized (mStateMachines) {
List<BluetoothDevice> devices = new ArrayList<>();
for (VolumeControlStateMachine sm : mStateMachines.values()) {
if (sm.isConnected()) {
devices.add(sm.getDevice());
}
}
return devices;
}
}
/**
* 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
* @return true if connection is allowed, otherwise false
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public boolean okToConnect(BluetoothDevice device) {
// Check if this is an incoming connection in Quiet mode.
if (mAdapterService.isQuietModeEnabled()) {
Log.e(TAG, "okToConnect: cannot connect to " + device + " : quiet mode enabled");
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;
}
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
"Need BLUETOOTH_PRIVILEGED permission");
ArrayList<BluetoothDevice> devices = new ArrayList<>();
if (states == null) {
return devices;
}
final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
if (bondedDevices == null) {
return devices;
}
synchronized (mStateMachines) {
for (BluetoothDevice device : bondedDevices) {
final ParcelUuid[] featureUuids = device.getUuids();
if (!ArrayUtils.contains(featureUuids, BluetoothUuid.VOLUME_CONTROL)) {
continue;
}
int connectionState = BluetoothProfile.STATE_DISCONNECTED;
VolumeControlStateMachine 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<BluetoothDevice> getDevices() {
List<BluetoothDevice> devices = new ArrayList<>();
synchronized (mStateMachines) {
for (VolumeControlStateMachine sm : mStateMachines.values()) {
devices.add(sm.getDevice());
}
return devices;
}
}
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
public int getConnectionState(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_CONNECT,
"Need BLUETOOTH_CONNECT permission");
synchronized (mStateMachines) {
VolumeControlStateMachine sm = mStateMachines.get(device);
if (sm == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return sm.getConnectionState();
}
}
/**
* 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}
*
* <p> 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 the remote device
* @param connectionPolicy is the connection policy to set to for this profile
* @return true on success, otherwise false
*/
@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);
}
mAdapterService.getDatabase()
.setProfileConnectionPolicy(device, BluetoothProfile.VOLUME_CONTROL,
connectionPolicy);
if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
connect(device);
} else if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
disconnect(device);
}
return true;
}
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
public int getConnectionPolicy(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
"Need BLUETOOTH_PRIVILEGED permission");
return mAdapterService.getDatabase()
.getProfileConnectionPolicy(device, BluetoothProfile.VOLUME_CONTROL);
}
void setVolume(BluetoothDevice device, int volume) {
mVolumeControlNativeInterface.setVolume(device, volume);
}
void setVolumeGroup(int groupId, int volume) {
mVolumeControlNativeInterface.setVolumeGroup(groupId, volume);
}
void handleVolumeControlChanged(BluetoothDevice device, int groupId,
int volume, boolean mute) {
/* TODO handle volume change for group in case of unicast le audio
* or per device in case of broadcast or simple remote controller.
* Note: minimum volume is 0 and maximum 255.
*/
}
void messageFromNative(VolumeControlStackEvent stackEvent) {
Objects.requireNonNull(stackEvent.device,
"Device should never be null, event: " + stackEvent);
Intent intent = null;
if (stackEvent.type == VolumeControlStackEvent.EVENT_TYPE_VOLUME_STATE_CHANGED) {
handleVolumeControlChanged(stackEvent.device, stackEvent.valueInt1,
stackEvent.valueInt2, stackEvent.valueBool1);
}
if (intent != null) {
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
| Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
sendBroadcast(intent, BLUETOOTH_CONNECT);
return;
}
synchronized (mStateMachines) {
BluetoothDevice device = stackEvent.device;
VolumeControlStateMachine sm = mStateMachines.get(device);
if (sm == null) {
if (stackEvent.type
== VolumeControlStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) {
switch (stackEvent.valueInt1) {
case VolumeControlStackEvent.CONNECTION_STATE_CONNECTED:
case VolumeControlStackEvent.CONNECTION_STATE_CONNECTING:
sm = getOrCreateStateMachine(device);
break;
default:
break;
}
}
}
if (sm == null) {
Log.e(TAG, "Cannot process stack event: no state machine: " + stackEvent);
return;
}
sm.sendMessage(VolumeControlStateMachine.STACK_EVENT, stackEvent);
}
}
private VolumeControlStateMachine getOrCreateStateMachine(BluetoothDevice device) {
if (device == null) {
Log.e(TAG, "getOrCreateStateMachine failed: device cannot be null");
return null;
}
synchronized (mStateMachines) {
VolumeControlStateMachine sm = mStateMachines.get(device);
if (sm != null) {
return sm;
}
// Limit the maximum number of state machines to avoid DoS attack
if (mStateMachines.size() >= MAX_VC_STATE_MACHINES) {
Log.e(TAG, "Maximum number of VolumeControl state machines reached: "
+ MAX_VC_STATE_MACHINES);
return null;
}
if (DBG) {
Log.d(TAG, "Creating a new state machine for " + device);
}
sm = VolumeControlStateMachine.make(device, this,
mVolumeControlNativeInterface, mStateMachinesThread.getLooper());
mStateMachines.put(device, sm);
return sm;
}
}
// 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);
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) {
VolumeControlStateMachine sm = mStateMachines.get(device);
if (sm == null) {
return;
}
if (sm.getConnectionState() != BluetoothProfile.STATE_DISCONNECTED) {
return;
}
removeStateMachine(device);
}
}
private void removeStateMachine(BluetoothDevice device) {
synchronized (mStateMachines) {
VolumeControlStateMachine 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);
}
}
@VisibleForTesting
synchronized void connectionStateChanged(BluetoothDevice device, int fromState,
int toState) {
if ((device == null) || (fromState == toState)) {
Log.e(TAG, "connectionStateChanged: unexpected invocation. device=" + device
+ " fromState=" + fromState + " toState=" + toState);
return;
}
// Check if the device is disconnected - if unbond, remove the state machine
if (toState == BluetoothProfile.STATE_DISCONNECTED) {
int bondState = mAdapterService.getBondState(device);
if (bondState == BluetoothDevice.BOND_NONE) {
if (DBG) {
Log.d(TAG, device + " is unbond. Remove state machine");
}
removeStateMachine(device);
}
}
}
private class ConnectionStateChangedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!BluetoothVolumeControl.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 BluetoothVolumeControlBinder extends IBluetoothVolumeControl.Stub
implements IProfileServiceBinder {
private VolumeControlService mService;
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
private VolumeControlService getService(AttributionSource source) {
if (!Utils.checkCallerIsSystemOrActiveUser(TAG)
|| !Utils.checkServiceAvailable(mService, TAG)
|| !Utils.checkConnectPermissionForDataDelivery(mService, source, TAG)) {
return null;
}
return mService;
}
BluetoothVolumeControlBinder(VolumeControlService svc) {
mService = svc;
}
@Override
public void cleanup() {
mService = null;
}
@Override
public boolean connect(BluetoothDevice device, AttributionSource source) {
VolumeControlService service = getService(source);
if (service == null) {
return false;
}
return service.connect(device);
}
@Override
public boolean disconnect(BluetoothDevice device, AttributionSource source) {
VolumeControlService service = getService(source);
if (service == null) {
return false;
}
return service.disconnect(device);
}
@Override
public List<BluetoothDevice> getConnectedDevices(AttributionSource source) {
VolumeControlService service = getService(source);
if (service == null) {
return new ArrayList<>();
}
enforceBluetoothPrivilegedPermission(service);
return service.getConnectedDevices();
}
@Override
public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states,
AttributionSource source) {
VolumeControlService service = getService(source);
if (service == null) {
return new ArrayList<>();
}
return service.getDevicesMatchingConnectionStates(states);
}
@Override
public int getConnectionState(BluetoothDevice device, AttributionSource source) {
VolumeControlService service = getService(source);
if (service == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return service.getConnectionState(device);
}
@Override
public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy,
AttributionSource source) {
VolumeControlService service = getService(source);
if (service == null) {
return false;
}
return service.setConnectionPolicy(device, connectionPolicy);
}
@Override
public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) {
VolumeControlService service = getService(source);
if (service == null) {
return BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
}
return service.getConnectionPolicy(device);
}
@Override
public void setVolume(BluetoothDevice device, int volume, AttributionSource source) {
VolumeControlService service = getService(source);
if (service == null) {
return;
}
service.setVolume(device, volume);
}
@Override
public void setVolumeGroup(int groupId, int volume, AttributionSource source) {
VolumeControlService service = getService(source);
if (service == null) {
return;
}
service.setVolumeGroup(groupId, volume);
}
}
@Override
public void dump(StringBuilder sb) {
super.dump(sb);
for (VolumeControlStateMachine sm : mStateMachines.values()) {
sm.dump(sb);
}
}
}