Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7f5b57c0 authored by Łukasz Rymanowski's avatar Łukasz Rymanowski
Browse files

VolumeControlService: Add AICS support

Patch adds internal VolumeControlInputDescriptor which keeps
informations about external inputs on the remote side.

Bug: 291285174
Bug: 361263965
Test: atest VolumeControlInputDescriptorTest
Flag: com.android.bluetooth.flags.leaudio_add_aics_support
Change-Id: I28f9c9b8ec471d3b29975e1b343517b99e40c6dc
parent f373eebf
Loading
Loading
Loading
Loading
+228 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.vc;

import android.bluetooth.BluetoothVolumeControl;
import android.util.Log;

import com.android.bluetooth.btservice.ProfileService;

import java.util.HashMap;
import java.util.Map;

class VolumeControlInputDescriptor {
    private static final String TAG = "VolumeControlInputDescriptor";
    final Map<Integer, Descriptor> mVolumeInputs = new HashMap<>();

    public static final int AUDIO_INPUT_TYPE_UNSPECIFIED = 0x00;
    public static final int AUDIO_INPUT_TYPE_BLUETOOTH = 0x01;
    public static final int AUDIO_INPUT_TYPE_MICROPHONE = 0x02;
    public static final int AUDIO_INPUT_TYPE_ANALOG = 0x03;
    public static final int AUDIO_INPUT_TYPE_DIGITAL = 0x04;
    public static final int AUDIO_INPUT_TYPE_RADIO = 0x05;
    public static final int AUDIO_INPUT_TYPE_STREAMING = 0x06;
    public static final int AUDIO_INPUT_TYPE_AMBIENT = 0x07;

    private static class Descriptor {
        /* True when input is active, false otherwise */
        boolean mIsActive = false;

        /* Defined as in Assigned Numbers in the BluetoothVolumeControl.AUDIO_INPUT_TYPE_ */
        int mType = AUDIO_INPUT_TYPE_UNSPECIFIED;

        int mGainValue = 0;

        /* As per AICS 1.0
         * 3.1.3. Gain_Mode field
         * The Gain_Mode field shall be set to a value that reflects whether gain modes are
         *  manual or automatic.
         *
         * If the Gain_Mode field value is Manual Only, the server allows only manual gain.
         * If the Gain_Mode field is Automatic Only, the server allows only automatic gain.
         *
         * For all other Gain_Mode field values, the server allows switchable
         * automatic/manual gain.
         */
        int mGainMode = 0;

        boolean mIsMute = false;

        /* As per AICS 1.0
         * The Gain_Setting (mGainValue) field is a signed value for which a single increment
         * or decrement should result in a corresponding increase or decrease of the input
         * amplitude by the value of the Gain_Setting_Units (mGainSettingsUnits)
         * field of the Gain Setting Properties characteristic value.
         */
        int mGainSettingsUnits = 0;

        int mGainSettingsMaxSetting = 0;
        int mGainSettingsMinSetting = 0;

        String mDescription = "";
    }

    int size() {
        return mVolumeInputs.size();
    }

    void add(int id) {
        if (!mVolumeInputs.containsKey(id)) {
            mVolumeInputs.put(id, new Descriptor());
        }
    }

    boolean setActive(int id, boolean active) {
        Descriptor desc = mVolumeInputs.get(id);
        if (desc == null) {
            Log.e(TAG, "setActive, Id " + id + " is unknown");
            return false;
        }
        desc.mIsActive = active;
        return true;
    }

    boolean isActive(int id) {
        Descriptor desc = mVolumeInputs.get(id);
        if (desc == null) {
            Log.e(TAG, "isActive, Id " + id + " is unknown");
            return false;
        }
        return desc.mIsActive;
    }

    boolean setDescription(int id, String description) {
        Descriptor desc = mVolumeInputs.get(id);
        if (desc == null) {
            Log.e(TAG, "setDescription, Id " + id + " is unknown");
            return false;
        }
        desc.mDescription = description;
        return true;
    }

    String getDescription(int id) {
        Descriptor desc = mVolumeInputs.get(id);
        if (desc == null) {
            Log.e(TAG, "getDescription, Id " + id + " is unknown");
            return null;
        }
        return desc.mDescription;
    }

    boolean setType(int id, int type) {
        Descriptor desc = mVolumeInputs.get(id);
        if (desc == null) {
            Log.e(TAG, "setType, Id " + id + " is unknown");
            return false;
        }

        if (type > AUDIO_INPUT_TYPE_AMBIENT) {
            Log.e(TAG, "setType, Type " + type + "for id " + id + " is invalid");
            return false;
        }

        desc.mType = type;
        return true;
    }

    int getType(int id) {
        Descriptor desc = mVolumeInputs.get(id);
        if (desc == null) {
            Log.e(TAG, "getType, Id " + id + " is unknown");
            return AUDIO_INPUT_TYPE_UNSPECIFIED;
        }
        return desc.mType;
    }

    int getGain(int id) {
        Descriptor desc = mVolumeInputs.get(id);
        if (desc == null) {
            Log.e(TAG, "getGain, Id " + id + " is unknown");
            return 0;
        }
        return desc.mGainValue;
    }

    boolean isMuted(int id) {
        Descriptor desc = mVolumeInputs.get(id);
        if (desc == null) {
            Log.e(TAG, "isMuted, Id " + id + " is unknown");
            return false;
        }
        return desc.mIsMute;
    }

    boolean setPropSettings(int id, int gainUnit, int gainMin, int gainMax) {
        Descriptor desc = mVolumeInputs.get(id);
        if (desc == null) {
            Log.e(TAG, "setPropSettings, Id " + id + " is unknown");
            return false;
        }

        desc.mGainSettingsUnits = gainUnit;
        desc.mGainSettingsMinSetting = gainMin;
        desc.mGainSettingsMaxSetting = gainMax;

        return true;
    }

    boolean setState(int id, int gainValue, int gainMode, boolean mute) {
        Descriptor desc = mVolumeInputs.get(id);
        if (desc == null) {
            Log.e(TAG, "Id " + id + " is unknown");
            return false;
        }

        if (gainValue > desc.mGainSettingsMaxSetting || gainValue < desc.mGainSettingsMinSetting) {
            Log.e(TAG, "Invalid gainValue " + gainValue);
            return false;
        }

        desc.mGainValue = gainValue;
        desc.mGainMode = gainMode;
        desc.mIsMute = mute;

        return true;
    }

    void remove(int id) {
        Log.d(TAG, "remove, id: " + id);
        mVolumeInputs.remove(id);
    }

    void clear() {
        Log.d(TAG, "clear all inputs");
        mVolumeInputs.clear();
    }

    void dump(StringBuilder sb) {
        for (Map.Entry<Integer, Descriptor> entry : mVolumeInputs.entrySet()) {
            Descriptor desc = entry.getValue();
            Integer id = entry.getKey();
            ProfileService.println(sb, "        id: " + id);
            ProfileService.println(sb, "        description: " + desc.mDescription);
            ProfileService.println(sb, "        type: " + desc.mType);
            ProfileService.println(sb, "        isActive: " + desc.mIsActive);
            ProfileService.println(sb, "        gainValue: " + desc.mGainValue);
            ProfileService.println(sb, "        gainMode: " + desc.mGainMode);
            ProfileService.println(sb, "        mute: " + desc.mIsMute);
            ProfileService.println(sb, "        units:" + desc.mGainSettingsUnits);
            ProfileService.println(sb, "        minGain:" + desc.mGainSettingsMinSetting);
            ProfileService.println(sb, "        maxGain:" + desc.mGainSettingsMaxSetting);
        }
    }
}
+209 −2
Original line number Diff line number Diff line
@@ -93,6 +93,7 @@ public class VolumeControlService extends ProfileService {
    private final Map<BluetoothDevice, VolumeControlStateMachine> mStateMachines = new HashMap<>();
    private final Map<BluetoothDevice, VolumeControlOffsetDescriptor> mAudioOffsets =
            new HashMap<>();
    private final Map<BluetoothDevice, VolumeControlInputDescriptor> mAudioInputs = new HashMap<>();
    private final Map<Integer, Integer> mGroupVolumeCache = new HashMap<>();
    private final Map<Integer, Boolean> mGroupMuteCache = new HashMap<>();
    private final Map<BluetoothDevice, Integer> mDeviceVolumeCache = new HashMap<>();
@@ -937,7 +938,7 @@ public class VolumeControlService extends ProfileService {
        return AudioManager.STREAM_MUSIC;
    }

    void handleDeviceAvailable(BluetoothDevice device, int numberOfExternalOutputs) {
    void handleExternalOutputs(BluetoothDevice device, int numberOfExternalOutputs) {
        if (numberOfExternalOutputs == 0) {
            Log.i(TAG, "Volume offset not available");
            return;
@@ -956,9 +957,38 @@ public class VolumeControlService extends ProfileService {
         * Offset ids a countinous from 1 to number_of_ext_outputs*/
        for (int i = 1; i <= numberOfExternalOutputs; i++) {
            offsets.add(i);
            /* Native stack is doing required reads under the hood */
        }
    }

    void handleExternalInputs(BluetoothDevice device, int numberOfExternalInputs) {
        if (numberOfExternalInputs == 0) {
            Log.i(TAG, "Volume offset not available");
            return;
        }

        VolumeControlInputDescriptor inputs = mAudioInputs.get(device);
        if (inputs == null) {
            inputs = new VolumeControlInputDescriptor();
            mAudioInputs.put(device, inputs);
        } else if (inputs.size() != numberOfExternalInputs) {
            Log.i(TAG, "Number of inputs changed: ");
            inputs.clear();
        }

        /* Stack delivers us number of audio inputs.
         * Offset ids a countinous from 1 to numberOfExternalInputs*/
        for (int i = 1; i <= numberOfExternalInputs; i++) {
            inputs.add(i);
        }
    }

    void handleDeviceAvailable(
            BluetoothDevice device, int numberOfExternalOutputs, int numberOfExternaInputs) {
        handleExternalOutputs(device, numberOfExternalOutputs);
        handleExternalInputs(device, numberOfExternaInputs);
    }

    void handleDeviceExtAudioOffsetChanged(BluetoothDevice device, int id, int value) {
        Log.d(TAG, " device: " + device + " offset_id: " + id + " value: " + value);
        VolumeControlOffsetDescriptor offsets = mAudioOffsets.get(device);
@@ -1036,6 +1066,137 @@ public class VolumeControlService extends ProfileService {
        }
    }

    void handleDeviceExtInputStateChanged(
            BluetoothDevice device, int id, int gainValue, int gainMode, boolean mute) {
        Log.d(
                TAG,
                ("handleDeviceExtInputStateChanged, device: " + device)
                        + (" inputId: " + id)
                        + (" gainValue: " + gainValue)
                        + (" gainMode: " + gainMode)
                        + (" mute: " + mute));

        VolumeControlInputDescriptor input = mAudioInputs.get(device);
        if (input == null) {
            Log.e(
                    TAG,
                    ("handleDeviceExtInputStateChanged, inputId: " + id)
                            + (" not found for device: " + device));
            return;
        }
        if (!input.setState(id, gainValue, gainMode, mute)) {
            Log.e(
                    TAG,
                    ("handleDeviceExtInputStateChanged, error while setting inputId: " + id)
                            + ("for: " + device));
        }
    }

    void handleDeviceExtInputStatusChanged(BluetoothDevice device, int id, int status) {
        Log.d(TAG, " device: " + device + " inputId: " + id + " status: " + status);

        VolumeControlInputDescriptor input = mAudioInputs.get(device);
        if (input == null) {
            Log.e(TAG, " inputId: " + id + " not found for device: " + device);
            return;
        }

        /*
         * As per ACIS 1.0. Status
         * Inactive: 0x00
         * Active: 0x01
         */
        if (status > 1 || status < 0) {
            Log.e(
                    TAG,
                    ("handleDeviceExtInputStatusChanged, invalid status: " + status)
                            + (" for: " + device));
            return;
        }

        boolean active = (status == 0x01);
        if (!input.setActive(id, active)) {
            Log.e(
                    TAG,
                    ("handleDeviceExtInputStatusChanged, error while setting inputId: " + id)
                            + ("for: " + device));
        }
    }

    void handleDeviceExtInputTypeChanged(BluetoothDevice device, int id, int type) {
        Log.d(
                TAG,
                ("handleDeviceExtInputTypeChanged, device: " + device)
                        + (" inputId: " + id)
                        + (" type: " + type));

        VolumeControlInputDescriptor input = mAudioInputs.get(device);
        if (input == null) {
            Log.e(
                    TAG,
                    ("handleDeviceExtInputTypeChanged, inputId: " + id)
                            + (" not found for device: " + device));
            return;
        }

        if (!input.setType(id, type)) {
            Log.e(
                    TAG,
                    ("handleDeviceExtInputTypeChanged, error while setting inputId: " + id)
                            + ("for: " + device));
        }
    }

    void handleDeviceExtInputDescriptionChanged(
            BluetoothDevice device, int id, String description) {
        Log.d(
                TAG,
                ("handleDeviceExtInputDescriptionChanged, device: " + device)
                        + (" inputId: " + id)
                        + (" description: " + description));

        VolumeControlInputDescriptor input = mAudioInputs.get(device);
        if (input == null) {
            Log.e(
                    TAG,
                    ("handleDeviceExtInputDescriptionChanged, inputId: " + id)
                            + (" not found for device: " + device));
            return;
        }

        if (!input.setDescription(id, description)) {
            Log.e(
                    TAG,
                    ("handleDeviceExtInputDescriptionChanged, error while setting inputId: " + id)
                            + ("for: " + device));
        }
    }

    void handleDeviceExtInputGainPropsChanged(
            BluetoothDevice device, int id, int unit, int min, int max) {
        Log.d(
                TAG,
                ("handleDeviceExtInputGainPropsChanged, device: " + device)
                        + (" inputId: " + id)
                        + (" unit: " + unit + " min" + min + " max:" + max));

        VolumeControlInputDescriptor input = mAudioInputs.get(device);
        if (input == null) {
            Log.e(
                    TAG,
                    ("handleDeviceExtInputGainPropsChanged, inputId: " + id)
                            + (" not found for device: " + device));
            return;
        }

        if (!input.setPropSettings(id, unit, min, max)) {
            Log.e(
                    TAG,
                    ("handleDeviceExtInputGainPropsChanged, error while setting inputId: " + id)
                            + ("for: " + device));
        }
    }

    void messageFromNative(VolumeControlStackEvent stackEvent) {
        Log.d(TAG, "messageFromNative: " + stackEvent);

@@ -1054,7 +1215,7 @@ public class VolumeControlService extends ProfileService {

        BluetoothDevice device = stackEvent.device;
        if (stackEvent.type == VolumeControlStackEvent.EVENT_TYPE_DEVICE_AVAILABLE) {
            handleDeviceAvailable(device, stackEvent.valueInt1);
            handleDeviceAvailable(device, stackEvent.valueInt1, stackEvent.valueInt2);
            return;
        }

@@ -1076,6 +1237,42 @@ public class VolumeControlService extends ProfileService {
            return;
        }

        if (stackEvent.type == VolumeControlStackEvent.EVENT_TYPE_EXT_AUDIO_IN_STATE_CHANGED) {
            handleDeviceExtInputStateChanged(
                    device,
                    stackEvent.valueInt1,
                    stackEvent.valueInt2,
                    stackEvent.valueInt3,
                    stackEvent.valueBool1);
            return;
        }

        if (stackEvent.type == VolumeControlStackEvent.EVENT_TYPE_EXT_AUDIO_IN_STATUS_CHANGED) {
            handleDeviceExtInputStatusChanged(device, stackEvent.valueInt1, stackEvent.valueInt2);
            return;
        }

        if (stackEvent.type == VolumeControlStackEvent.EVENT_TYPE_EXT_AUDIO_IN_TYPE_CHANGED) {
            handleDeviceExtInputTypeChanged(device, stackEvent.valueInt1, stackEvent.valueInt2);
            return;
        }

        if (stackEvent.type == VolumeControlStackEvent.EVENT_TYPE_EXT_AUDIO_IN_DESCR_CHANGED) {
            handleDeviceExtInputDescriptionChanged(
                    device, stackEvent.valueInt1, stackEvent.valueString1);
            return;
        }

        if (stackEvent.type == VolumeControlStackEvent.EVENT_TYPE_EXT_AUDIO_IN_GAIN_PROPS_CHANGED) {
            handleDeviceExtInputGainPropsChanged(
                    device,
                    stackEvent.valueInt1,
                    stackEvent.valueInt2,
                    stackEvent.valueInt3,
                    stackEvent.valueInt4);
            return;
        }

        synchronized (mStateMachines) {
            VolumeControlStateMachine sm = mStateMachines.get(device);
            if (sm == null) {
@@ -1619,6 +1816,16 @@ public class VolumeControlService extends ProfileService {
            ProfileService.println(sb, "    Volume offset cnt: " + descriptor.size());
            descriptor.dump(sb);
        }

        for (Map.Entry<BluetoothDevice, VolumeControlInputDescriptor> entry :
                mAudioInputs.entrySet()) {
            VolumeControlInputDescriptor descriptor = entry.getValue();
            BluetoothDevice device = entry.getKey();
            ProfileService.println(sb, "    Device: " + device);
            ProfileService.println(sb, "    Volume input cnt: " + descriptor.size());
            descriptor.dump(sb);
        }

        for (Map.Entry<Integer, Integer> entry : mGroupVolumeCache.entrySet()) {
            Boolean isMute = mGroupMuteCache.getOrDefault(entry.getKey(), false);
            ProfileService.println(
+185 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.vc;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.*;

import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;

import androidx.test.filters.MediumTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.bluetooth.flags.Flags;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

@MediumTest
@RunWith(AndroidJUnit4.class)
public class VolumeControlInputDescriptorTest {

    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

    @Before
    public void setUp() throws Exception {
        // placeholder
    }

    @After
    public void tearDown() throws Exception {
        // placeholder
    }

    @Test
    @EnableFlags(Flags.FLAG_LEAUDIO_ADD_AICS_SUPPORT)
    public void testVolumeControlInputDescriptorInvalidIdOperations() throws Exception {
        VolumeControlInputDescriptor descriptor = new VolumeControlInputDescriptor();

        int invalidId = 1;
        int testGainValue = 100;
        int testGainMode = 1;
        boolean testGainMute = true;
        String testDesc = "testDescription";
        int testType = VolumeControlInputDescriptor.AUDIO_INPUT_TYPE_AMBIENT;
        int testGainSettingsMax = 100;
        int testGainSettingsMin = 0;
        int testGainSettingsUnit = 1;

        // Test adding all props using invalid ID
        assertThat(descriptor.isActive(invalidId)).isFalse();
        assertThat(descriptor.setActive(invalidId, true)).isFalse();
        assertThat(descriptor.setDescription(invalidId, testDesc)).isFalse();
        assertThat(descriptor.getDescription(invalidId)).isNull();
        assertThat(descriptor.setType(invalidId, testType)).isFalse();
        assertThat(descriptor.getType(invalidId))
                .isEqualTo(VolumeControlInputDescriptor.AUDIO_INPUT_TYPE_UNSPECIFIED);

        assertThat(descriptor.getGain(invalidId)).isEqualTo(0);
        assertThat(descriptor.isMuted(invalidId)).isFalse();
        assertThat(
                        descriptor.setPropSettings(
                                invalidId,
                                testGainSettingsUnit,
                                testGainSettingsMin,
                                testGainSettingsMax))
                .isFalse();
        assertThat(descriptor.setState(invalidId, testGainValue, testGainMode, testGainMute))
                .isFalse();
    }

    @Test
    @EnableFlags(Flags.FLAG_LEAUDIO_ADD_AICS_SUPPORT)
    public void testVolumeControlInputDescriptorMultipleInstanceAdded() throws Exception {

        VolumeControlInputDescriptor descriptor = new VolumeControlInputDescriptor();

        int validId = 10;

        // Verify that adding descriptor works increase descriptor size
        assertThat(descriptor.size()).isEqualTo(0);
        descriptor.add(validId);
        assertThat(descriptor.size()).isEqualTo(1);

        // Check if adding same id will not increase descriptor count.
        descriptor.add(validId);
        assertThat(descriptor.size()).isEqualTo(1);
    }

    @Test
    @EnableFlags(Flags.FLAG_LEAUDIO_ADD_AICS_SUPPORT)
    public void testVolumeControlInputDescriptorInstanceRemoveAndClear() throws Exception {

        VolumeControlInputDescriptor descriptor = new VolumeControlInputDescriptor();

        int id_1 = 10;
        int id_2 = 20;
        int invalidId = 1;

        // Verify that adding descriptor works increase descriptor size
        assertThat(descriptor.size()).isEqualTo(0);
        descriptor.add(id_1);
        assertThat(descriptor.size()).isEqualTo(1);
        descriptor.add(id_2);

        // Remove valid id
        descriptor.remove(id_1);
        assertThat(descriptor.size()).isEqualTo(1);

        // Remove invalid id not change number of descriptors
        descriptor.remove(invalidId);
        assertThat(descriptor.size()).isEqualTo(1);

        // Check clear API
        descriptor.clear();
        assertThat(descriptor.size()).isEqualTo(0);
    }

    @Test
    @EnableFlags(Flags.FLAG_LEAUDIO_ADD_AICS_SUPPORT)
    public void testVolumeControlInputDescriptorAllValidApiCalls() throws Exception {

        VolumeControlInputDescriptor descriptor = new VolumeControlInputDescriptor();

        int validId = 10;
        int testGainValue = 100;
        int testGainMode = 1;
        boolean testGainMute = true;
        String defaultDesc = "";
        String testDesc = "testDescription";
        int testType = VolumeControlInputDescriptor.AUDIO_INPUT_TYPE_AMBIENT;
        int testGainSettingsMax = 100;
        int testGainSettingsMin = 0;
        int testGainSettingsUnit = 1;

        descriptor.add(validId);

        // Active state
        assertThat(descriptor.isActive(validId)).isFalse();
        assertThat(descriptor.setActive(validId, true)).isTrue();
        assertThat(descriptor.isActive(validId)).isTrue();

        // Descriptor
        assertThat(descriptor.getDescription(validId)).isEqualTo(defaultDesc);
        assertThat(descriptor.setDescription(validId, testDesc)).isTrue();
        assertThat(descriptor.getDescription(validId)).isEqualTo(testDesc);

        // Type
        assertThat(descriptor.getType(validId))
                .isEqualTo(VolumeControlInputDescriptor.AUDIO_INPUT_TYPE_UNSPECIFIED);
        assertThat(descriptor.setType(validId, testType)).isTrue();
        assertThat(descriptor.getType(validId)).isEqualTo(testType);

        // Properties
        assertThat(
                        descriptor.setPropSettings(
                                validId,
                                testGainSettingsUnit,
                                testGainSettingsMin,
                                testGainSettingsMax))
                .isTrue();

        // State
        assertThat(descriptor.setState(validId, testGainValue, testGainMode, testGainMute))
                .isTrue();
        assertThat(descriptor.getGain(validId)).isEqualTo(testGainValue);
    }
}