leaudio: Add support for Mute/Unmute
Bug: 231580214
Test: atest --host bluetooth_vc_test
Sponsor: @jpawlowski
Change-Id: Ib894ab01916d7eb989db6a0c28b79b321dd82ad6
(cherry picked from commit 636dc13c93a5d18589b010afe7e105c550eab17e)
diff --git a/android/app/jni/com_android_bluetooth_vc.cpp b/android/app/jni/com_android_bluetooth_vc.cpp
index cc1e296..ad31918 100644
--- a/android/app/jni/com_android_bluetooth_vc.cpp
+++ b/android/app/jni/com_android_bluetooth_vc.cpp
@@ -357,6 +357,60 @@
sVolumeControlInterface->SetVolume(group_id, volume);
}
+static void muteNative(JNIEnv* env, jobject object, jbyteArray address) {
+ if (!sVolumeControlInterface) {
+ LOG(ERROR) << __func__
+ << ": Failed to get the Bluetooth Volume Control Interface";
+ return;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, nullptr);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return;
+ }
+
+ RawAddress* tmpraw = (RawAddress*)addr;
+ sVolumeControlInterface->Mute(*tmpraw);
+ env->ReleaseByteArrayElements(address, addr, 0);
+}
+
+static void muteGroupNative(JNIEnv* env, jobject object, jint group_id) {
+ if (!sVolumeControlInterface) {
+ LOG(ERROR) << __func__
+ << ": Failed to get the Bluetooth Volume Control Interface";
+ return;
+ }
+ sVolumeControlInterface->Mute(group_id);
+}
+
+static void unmuteNative(JNIEnv* env, jobject object, jbyteArray address) {
+ if (!sVolumeControlInterface) {
+ LOG(ERROR) << __func__
+ << ": Failed to get the Bluetooth Volume Control Interface";
+ return;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, nullptr);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return;
+ }
+
+ RawAddress* tmpraw = (RawAddress*)addr;
+ sVolumeControlInterface->Unmute(*tmpraw);
+ env->ReleaseByteArrayElements(address, addr, 0);
+}
+
+static void unmuteGroupNative(JNIEnv* env, jobject object, jint group_id) {
+ if (!sVolumeControlInterface) {
+ LOG(ERROR) << __func__
+ << ": Failed to get the Bluetooth Volume Control Interface";
+ return;
+ }
+ sVolumeControlInterface->Unmute(group_id);
+}
+
/* Native methods for exterbak audio outputs */
static jboolean getExtAudioOutVolumeOffsetNative(JNIEnv* env, jobject object,
jbyteArray address,
@@ -494,6 +548,10 @@
(void*)disconnectVolumeControlNative},
{"setVolumeNative", "([BI)V", (void*)setVolumeNative},
{"setVolumeGroupNative", "(II)V", (void*)setVolumeGroupNative},
+ {"muteNative", "([B)V", (void*)muteNative},
+ {"muteGroupNative", "(I)V", (void*)muteGroupNative},
+ {"unmuteNative", "([B)V", (void*)unmuteNative},
+ {"unmuteGroupNative", "(I)V", (void*)unmuteGroupNative},
{"getExtAudioOutVolumeOffsetNative", "([BI)Z",
(void*)getExtAudioOutVolumeOffsetNative},
{"setExtAudioOutVolumeOffsetNative", "([BII)Z",
diff --git a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java
index 2b99a80..edb3f17 100644
--- a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java
+++ b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java
@@ -176,6 +176,8 @@
private BroadcastReceiver mBondStateChangedReceiver;
private BroadcastReceiver mConnectionStateChangedReceiver;
+ private BroadcastReceiver mMuteStateChangedReceiver;
+ private int mStoredRingerMode = -1;
private Handler mHandler = new Handler(Looper.getMainLooper());
private final Map<Integer, Integer> mBroadcastStateMap = new HashMap<>();
@@ -239,6 +241,11 @@
filter.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
mConnectionStateChangedReceiver = new ConnectionStateChangedReceiver();
registerReceiver(mConnectionStateChangedReceiver, filter);
+ filter = new IntentFilter();
+ filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
+ mMuteStateChangedReceiver = new MuteStateChangedReceiver();
+ registerReceiver(mMuteStateChangedReceiver, filter);
+
mLeAudioCallbacks = new RemoteCallbackList<IBluetoothLeAudioCallback>();
int tmapRoleMask =
@@ -330,6 +337,8 @@
mBondStateChangedReceiver = null;
unregisterReceiver(mConnectionStateChangedReceiver);
mConnectionStateChangedReceiver = null;
+ unregisterReceiver(mMuteStateChangedReceiver);
+ mMuteStateChangedReceiver = null;
// Destroy state machines and stop handler thread
synchronized (mStateMachines) {
@@ -1542,6 +1551,39 @@
}
}
+ private synchronized boolean isSilentModeEnabled() {
+ return mStoredRingerMode != AudioManager.RINGER_MODE_NORMAL;
+ }
+
+ private class MuteStateChangedReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!AudioManager.RINGER_MODE_CHANGED_ACTION.equals(intent.getAction())) {
+ return;
+ }
+
+ final String action = intent.getAction();
+ if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
+ int ringerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, -1);
+
+ if (ringerMode < 0 || ringerMode == mStoredRingerMode) return;
+
+ mStoredRingerMode = ringerMode;
+ int activeGroupId = getActiveGroupId();
+ if (activeGroupId == LE_AUDIO_GROUP_ID_INVALID) return;
+
+ VolumeControlService service = mServiceFactory.getVolumeControlService();
+ if (service == null) return;
+
+ if (isSilentModeEnabled()) {
+ service.muteGroup(activeGroupId);
+ } else {
+ service.unmuteGroup(activeGroupId);
+ }
+ }
+ }
+ }
+
/**
* Check whether can connect to a peer device.
* The check considers a number of factors during the evaluation.
diff --git a/android/app/src/com/android/bluetooth/vc/VolumeControlNativeInterface.java b/android/app/src/com/android/bluetooth/vc/VolumeControlNativeInterface.java
index 421405c..0a50c6d 100644
--- a/android/app/src/com/android/bluetooth/vc/VolumeControlNativeInterface.java
+++ b/android/app/src/com/android/bluetooth/vc/VolumeControlNativeInterface.java
@@ -117,6 +117,44 @@
setVolumeGroupNative(groupId, volume);
}
+ /**
+ * Mute the VolumeControl volume
+ * @param device
+ * @param unmute
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void mute(BluetoothDevice device) {
+ muteNative(getByteAddress(device));
+ }
+
+ /**
+ * Mute the VolumeControl volume in the group
+ * @param groupId
+ * @param unmute
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void muteGroup(int groupId) {
+ muteGroupNative(groupId);
+ }
+
+ /**
+ * Unmute the VolumeControl volume
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void unmute(BluetoothDevice device) {
+ unmuteNative(getByteAddress(device));
+ }
+
+ /**
+ * Unmute the VolumeControl volume group
+ * @param groupId
+ * @param unmute
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void unmuteGroup(int groupId) {
+ unmuteGroupNative(groupId);
+ }
+
/**
* Gets external audio output volume offset from a remote device.
*
@@ -333,6 +371,10 @@
private native boolean disconnectVolumeControlNative(byte[] address);
private native void setVolumeNative(byte[] address, int volume);
private native void setVolumeGroupNative(int groupId, int volume);
+ private native void muteNative(byte[] address);
+ private native void muteGroupNative(int groupId);
+ private native void unmuteNative(byte[] address);
+ private native void unmuteGroupNative(int groupId);
private native boolean getExtAudioOutVolumeOffsetNative(byte[] address, int externalOutputId);
private native boolean setExtAudioOutVolumeOffsetNative(byte[] address, int externalOutputId,
int offset);
diff --git a/android/app/src/com/android/bluetooth/vc/VolumeControlService.java b/android/app/src/com/android/bluetooth/vc/VolumeControlService.java
index 5b03839..e04c2e6 100644
--- a/android/app/src/com/android/bluetooth/vc/VolumeControlService.java
+++ b/android/app/src/com/android/bluetooth/vc/VolumeControlService.java
@@ -582,6 +582,34 @@
mVolumeControlNativeInterface.setVolumeGroup(groupId, volume);
}
+ /**
+ * {@hide}
+ */
+ public void mute(BluetoothDevice device) {
+ mVolumeControlNativeInterface.mute(device);
+ }
+
+ /**
+ * {@hide}
+ */
+ public void muteGroup(int groupId) {
+ mVolumeControlNativeInterface.muteGroup(groupId);
+ }
+
+ /**
+ * {@hide}
+ */
+ public void unmute(BluetoothDevice device) {
+ mVolumeControlNativeInterface.unmute(device);
+ }
+
+ /**
+ * {@hide}
+ */
+ public void unmuteGroup(int groupId) {
+ mVolumeControlNativeInterface.unmuteGroup(groupId);
+ }
+
void handleVolumeControlChanged(BluetoothDevice device, int groupId,
int volume, boolean mute, boolean isAutonomous) {
if (!isAutonomous) {
@@ -1114,6 +1142,76 @@
}
@Override
+ public void mute(BluetoothDevice device, AttributionSource source,
+ SynchronousResultReceiver receiver) {
+ try {
+ Objects.requireNonNull(device, "device cannot be null");
+ Objects.requireNonNull(source, "source cannot be null");
+ Objects.requireNonNull(receiver, "receiver cannot be null");
+
+ VolumeControlService service = getService(source);
+ if (service != null) {
+ service.mute(device);
+ }
+ receiver.send(null);
+ } catch (RuntimeException e) {
+ receiver.propagateException(e);
+ }
+ }
+
+ @Override
+ public void muteGroup(int groupId, AttributionSource source,
+ SynchronousResultReceiver receiver) {
+ try {
+ Objects.requireNonNull(source, "source cannot be null");
+ Objects.requireNonNull(receiver, "receiver cannot be null");
+
+ VolumeControlService service = getService(source);
+ if (service != null) {
+ service.muteGroup(groupId);
+ }
+ receiver.send(null);
+ } catch (RuntimeException e) {
+ receiver.propagateException(e);
+ }
+ }
+
+ @Override
+ public void unmute(BluetoothDevice device, AttributionSource source,
+ SynchronousResultReceiver receiver) {
+ try {
+ Objects.requireNonNull(device, "device cannot be null");
+ Objects.requireNonNull(source, "source cannot be null");
+ Objects.requireNonNull(receiver, "receiver cannot be null");
+
+ VolumeControlService service = getService(source);
+ if (service != null) {
+ service.unmute(device);
+ }
+ receiver.send(null);
+ } catch (RuntimeException e) {
+ receiver.propagateException(e);
+ }
+ }
+
+ @Override
+ public void unmuteGroup(int groupId, AttributionSource source,
+ SynchronousResultReceiver receiver) {
+ try {
+ Objects.requireNonNull(source, "source cannot be null");
+ Objects.requireNonNull(receiver, "receiver cannot be null");
+
+ VolumeControlService service = getService(source);
+ if (service != null) {
+ service.unmuteGroup(groupId);
+ }
+ receiver.send(null);
+ } catch (RuntimeException e) {
+ receiver.propagateException(e);
+ }
+ }
+
+ @Override
public void registerCallback(IBluetoothVolumeControlCallback callback,
AttributionSource source, SynchronousResultReceiver receiver) {
try {
diff --git a/system/binder/android/bluetooth/IBluetoothVolumeControl.aidl b/system/binder/android/bluetooth/IBluetoothVolumeControl.aidl
index 9049260..eb6ddd1 100644
--- a/system/binder/android/bluetooth/IBluetoothVolumeControl.aidl
+++ b/system/binder/android/bluetooth/IBluetoothVolumeControl.aidl
@@ -52,6 +52,16 @@
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
void setVolumeGroup(int group_id, int volume, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+ void mute(in BluetoothDevice device, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+ void muteGroup(int group_id, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+ void unmute(in BluetoothDevice device, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+ void unmuteGroup(int group_id, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})")
void registerCallback(in IBluetoothVolumeControlCallback callback, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})")
diff --git a/system/bta/Android.bp b/system/bta/Android.bp
index fb40322..146b821 100644
--- a/system/bta/Android.bp
+++ b/system/bta/Android.bp
@@ -400,6 +400,7 @@
"packages/modules/Bluetooth/system",
"packages/modules/Bluetooth/system/bta/include",
"packages/modules/Bluetooth/system/bta/test/common",
+ "packages/modules/Bluetooth/system/gd",
"packages/modules/Bluetooth/system/stack/include",
],
srcs : [
diff --git a/system/bta/include/bta_vc_api.h b/system/bta/include/bta_vc_api.h
index b8913eb..3b2db1b 100644
--- a/system/bta/include/bta_vc_api.h
+++ b/system/bta/include/bta_vc_api.h
@@ -39,6 +39,9 @@
virtual void Disconnect(const RawAddress& address) = 0;
virtual void SetVolume(std::variant<RawAddress, int> addr_or_group_id,
uint8_t volume) = 0;
+ virtual void Mute(std::variant<RawAddress, int> addr_or_group_id) = 0;
+ virtual void UnMute(std::variant<RawAddress, int> addr_or_group_id) = 0;
+
/* Volume Offset Control Service (VOCS) */
virtual void SetExtAudioOutVolumeOffset(const RawAddress& address,
uint8_t ext_output_id,
diff --git a/system/bta/vc/vc.cc b/system/bta/vc/vc.cc
index 4ecbb8e..d81f00c 100644
--- a/system/bta/vc/vc.cc
+++ b/system/bta/vc/vc.cc
@@ -32,6 +32,8 @@
#include "bta_vc_api.h"
#include "btif_storage.h"
#include "devices.h"
+#include "gd/common/strings.h"
+#include "osi/include/log.h"
#include "osi/include/osi.h"
#include "types/bluetooth/uuid.h"
#include "types/raw_address.h"
@@ -711,6 +713,61 @@
is_autonomous, opcode, arguments, devices);
}
+ void MuteUnmute(std::variant<RawAddress, int> addr_or_group_id, bool mute) {
+ std::vector<uint8_t> arg;
+
+ uint8_t opcode = mute ? kControlPointOpcodeMute : kControlPointOpcodeUnmute;
+
+ if (std::holds_alternative<RawAddress>(addr_or_group_id)) {
+ LOG_DEBUG("Address: %s: ",
+ (std::get<RawAddress>(addr_or_group_id)).ToString().c_str());
+ std::vector<RawAddress> devices = {
+ std::get<RawAddress>(addr_or_group_id)};
+
+ PrepareVolumeControlOperation(devices, bluetooth::groups::kGroupUnknown,
+ false, opcode, arg);
+ } else {
+ /* Handle group change */
+ auto group_id = std::get<int>(addr_or_group_id);
+ LOG_DEBUG("group: %d", group_id);
+ auto csis_api = CsisClient::Get();
+ if (!csis_api) {
+ LOG(ERROR) << __func__ << " Csis is not there";
+ return;
+ }
+
+ auto devices = csis_api->GetDeviceList(group_id);
+ for (auto it = devices.begin(); it != devices.end();) {
+ auto dev = volume_control_devices_.FindByAddress(*it);
+ if (!dev || !dev->IsConnected()) {
+ it = devices.erase(it);
+ } else {
+ it++;
+ }
+ }
+
+ if (devices.empty()) {
+ LOG(ERROR) << __func__ << " group id : " << group_id
+ << " is not connected? ";
+ return;
+ }
+
+ PrepareVolumeControlOperation(devices, group_id, false, opcode, arg);
+ }
+
+ StartQueueOperation();
+ }
+
+ void Mute(std::variant<RawAddress, int> addr_or_group_id) override {
+ LOG_DEBUG();
+ MuteUnmute(addr_or_group_id, true /* mute */);
+ }
+
+ void UnMute(std::variant<RawAddress, int> addr_or_group_id) override {
+ LOG_DEBUG();
+ MuteUnmute(addr_or_group_id, false /* mute */);
+ }
+
void SetVolume(std::variant<RawAddress, int> addr_or_group_id,
uint8_t volume) override {
DLOG(INFO) << __func__ << " vol: " << +volume;
diff --git a/system/bta/vc/vc_test.cc b/system/bta/vc/vc_test.cc
index e90ceee..7225f59 100644
--- a/system/bta/vc/vc_test.cc
+++ b/system/bta/vc/vc_test.cc
@@ -919,6 +919,20 @@
VolumeControl::Get()->SetVolume(test_address, 0x10);
}
+TEST_F(VolumeControlValueSetTest, test_mute) {
+ std::vector<uint8_t> mute({0x06, 0x00});
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(conn_id, 0x0024, mute, GATT_WRITE, _, _));
+ VolumeControl::Get()->Mute(test_address);
+}
+
+TEST_F(VolumeControlValueSetTest, test_unmute) {
+ std::vector<uint8_t> unmute({0x05, 0x00});
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(conn_id, 0x0024, unmute, GATT_WRITE, _, _));
+ VolumeControl::Get()->UnMute(test_address);
+}
+
TEST_F(VolumeControlValueSetTest, test_set_ext_audio_out_volume_offset) {
std::vector<uint8_t> expected_data({0x01, 0x00, 0x34, 0x12});
EXPECT_CALL(gatt_queue, WriteCharacteristic(conn_id, 0x0088, expected_data,
diff --git a/system/btif/src/btif_vc.cc b/system/btif/src/btif_vc.cc
index 28aaa34..00b6b8e 100644
--- a/system/btif/src/btif_vc.cc
+++ b/system/btif/src/btif_vc.cc
@@ -143,6 +143,20 @@
std::move(addr_or_group_id), volume));
}
+ void Mute(std::variant<RawAddress, int> addr_or_group_id) override {
+ DVLOG(2) << __func__;
+ do_in_main_thread(
+ FROM_HERE, Bind(&VolumeControl::Mute, Unretained(VolumeControl::Get()),
+ std::move(addr_or_group_id)));
+ }
+
+ void Unmute(std::variant<RawAddress, int> addr_or_group_id) override {
+ DVLOG(2) << __func__;
+ do_in_main_thread(FROM_HERE, Bind(&VolumeControl::UnMute,
+ Unretained(VolumeControl::Get()),
+ std::move(addr_or_group_id)));
+ }
+
void RemoveDevice(const RawAddress& address) override {
DVLOG(2) << __func__ << " address: " << address;
diff --git a/system/include/hardware/bt_vc.h b/system/include/hardware/bt_vc.h
index 18fd764..090ff9c 100644
--- a/system/include/hardware/bt_vc.h
+++ b/system/include/hardware/bt_vc.h
@@ -86,6 +86,12 @@
/** Set the volume */
virtual void SetVolume(std::variant<RawAddress, int> addr_or_group_id,
uint8_t volume) = 0;
+ /** Mute the volume */
+ virtual void Mute(std::variant<RawAddress, int> addr_or_group_id) = 0;
+
+ /** Unmute the volume */
+ virtual void Unmute(std::variant<RawAddress, int> addr_or_group_id) = 0;
+
virtual void GetExtAudioOutVolumeOffset(const RawAddress& address,
uint8_t ext_output_id) = 0;
virtual void SetExtAudioOutVolumeOffset(const RawAddress& address,