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

Commit dd8cf125 authored by Angela Wang's avatar Angela Wang
Browse files

Integrate some VCP APIs into SettingsLib

Add some VCP APIs to make LE audio device users be able to control the
volume offset in Settings.

Bug: 301198830
Test: atest VolumeControlProfileTest
Change-Id: Ia7dcb7a09c5ed9c797d60c1211946a1c44fa68b9
parent 67863693
Loading
Loading
Loading
Loading
+92 −6
Original line number Diff line number Diff line
@@ -19,6 +19,9 @@ package com.android.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;

import android.annotation.CallbackExecutor;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
@@ -28,10 +31,11 @@ import android.content.Context;
import android.os.Build;
import android.util.Log;

import androidx.annotation.RequiresApi;

import java.util.ArrayList;
import java.util.List;

import androidx.annotation.RequiresApi;
import java.util.concurrent.Executor;

/**
 * VolumeControlProfile handles Bluetooth Volume Control Controller role
@@ -102,6 +106,88 @@ public class VolumeControlProfile implements LocalBluetoothProfile {
                BluetoothProfile.VOLUME_CONTROL);
    }


    /**
     * Registers a {@link BluetoothVolumeControl.Callback} that will be invoked during the
     * operation of this profile.
     *
     * Repeated registration of the same <var>callback</var> object will have no effect after
     * the first call to this method, even when the <var>executor</var> is different. API caller
     * would have to call {@link #unregisterCallback(BluetoothVolumeControl.Callback)} with
     * the same callback object before registering it again.
     *
     * @param executor an {@link Executor} to execute given callback
     * @param callback user implementation of the {@link BluetoothVolumeControl.Callback}
     * @throws IllegalArgumentException if a null executor or callback is given
     */
    public void registerCallback(@NonNull @CallbackExecutor Executor executor,
            @NonNull BluetoothVolumeControl.Callback callback) {
        if (mService == null) {
            Log.w(TAG, "Proxy not attached to service. Cannot register callback.");
            return;
        }
        mService.registerCallback(executor, callback);
    }

    /**
     * Unregisters the specified {@link BluetoothVolumeControl.Callback}.
     * <p>The same {@link BluetoothVolumeControl.Callback} object used when calling
     * {@link #registerCallback(Executor, BluetoothVolumeControl.Callback)} must be used.
     *
     * <p>Callbacks are automatically unregistered when application process goes away
     *
     * @param callback user implementation of the {@link BluetoothVolumeControl.Callback}
     * @throws IllegalArgumentException when callback is null or when no callback is registered
     */
    public void unregisterCallback(@NonNull BluetoothVolumeControl.Callback callback) {
        if (mService == null) {
            Log.w(TAG, "Proxy not attached to service. Cannot unregister callback.");
            return;
        }
        mService.unregisterCallback(callback);
    }

    /**
     * Tells the remote device to set a volume offset to the absolute volume.
     *
     * @param device {@link BluetoothDevice} representing the remote device
     * @param volumeOffset volume offset to be set on the remote device
     */
    public void setVolumeOffset(BluetoothDevice device,
            @IntRange(from = -255, to = 255) int volumeOffset) {
        if (mService == null) {
            Log.w(TAG, "Proxy not attached to service. Cannot set volume offset.");
            return;
        }
        if (device == null) {
            Log.w(TAG, "Device is null. Cannot set volume offset.");
            return;
        }
        mService.setVolumeOffset(device, volumeOffset);
    }

    /**
     * Provides information about the possibility to set volume offset on the remote device.
     * If the remote device supports Volume Offset Control Service, it is automatically
     * connected.
     *
     * @param device {@link BluetoothDevice} representing the remote device
     * @return {@code true} if volume offset function is supported and available to use on the
     *         remote device. When Bluetooth is off, the return value should always be
     *         {@code false}.
     */
    public boolean isVolumeOffsetAvailable(BluetoothDevice device) {
        if (mService == null) {
            Log.w(TAG, "Proxy not attached to service. Cannot get is volume offset available.");
            return false;
        }
        if (device == null) {
            Log.w(TAG, "Device is null. Cannot get is volume offset available.");
            return false;
        }
        return mService.isVolumeOffsetAvailable(device);
    }

    @Override
    public boolean accessProfileEnabled() {
        return false;
@@ -113,12 +199,12 @@ public class VolumeControlProfile implements LocalBluetoothProfile {
    }

    /**
     * Get VolumeControlProfile devices matching connection states{
     * Gets VolumeControlProfile devices matching connection states{
     * {@code BluetoothProfile.STATE_CONNECTED},
     * {@code BluetoothProfile.STATE_CONNECTING},
     * {@code BluetoothProfile.STATE_DISCONNECTING}}
     *
     * @return Matching device list
     * @code BluetoothProfile.STATE_CONNECTED,
     * @code BluetoothProfile.STATE_CONNECTING,
     * @code BluetoothProfile.STATE_DISCONNECTING}
     */
    public List<BluetoothDevice> getConnectedDevices() {
        if (mService == null) {
+245 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.settingslib.bluetooth;

import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;

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

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothVolumeControl;
import android.content.Context;

import androidx.test.core.app.ApplicationProvider;

import com.android.settingslib.testutils.shadow.ShadowBluetoothAdapter;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;

@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowBluetoothAdapter.class})
public class VolumeControlProfileTest {

    private static final int TEST_VOLUME_OFFSET = 10;

    @Rule
    public final MockitoRule mockito = MockitoJUnit.rule();

    @Mock
    private CachedBluetoothDeviceManager mDeviceManager;
    @Mock
    private LocalBluetoothProfileManager mProfileManager;
    @Mock
    private BluetoothDevice mBluetoothDevice;
    @Mock
    private BluetoothVolumeControl mService;

    private final Context mContext = ApplicationProvider.getApplicationContext();
    private BluetoothProfile.ServiceListener mServiceListener;
    private VolumeControlProfile mProfile;

    @Before
    public void setUp() {
        mProfile = new VolumeControlProfile(mContext, mDeviceManager, mProfileManager);
        final BluetoothManager bluetoothManager = mContext.getSystemService(BluetoothManager.class);
        final ShadowBluetoothAdapter shadowBluetoothAdapter =
                Shadow.extract(bluetoothManager.getAdapter());
        mServiceListener = shadowBluetoothAdapter.getServiceListener();
    }

    @Test
    public void onServiceConnected_isProfileReady() {
        mServiceListener.onServiceConnected(BluetoothProfile.VOLUME_CONTROL, mService);

        assertThat(mProfile.isProfileReady()).isTrue();
        verify(mProfileManager).callServiceConnectedListeners();
    }

    @Test
    public void onServiceDisconnected_isProfileNotReady() {
        mServiceListener.onServiceDisconnected(BluetoothProfile.VOLUME_CONTROL);

        assertThat(mProfile.isProfileReady()).isFalse();
        verify(mProfileManager).callServiceDisconnectedListeners();
    }

    @Test
    public void getConnectionStatus_returnCorrectConnectionState() {
        mServiceListener.onServiceConnected(BluetoothProfile.VOLUME_CONTROL, mService);
        when(mService.getConnectionState(mBluetoothDevice))
                .thenReturn(BluetoothProfile.STATE_CONNECTED);

        assertThat(mProfile.getConnectionStatus(mBluetoothDevice))
                .isEqualTo(BluetoothProfile.STATE_CONNECTED);
    }

    @Test
    public void isEnabled_connectionPolicyAllowed_returnTrue() {
        mServiceListener.onServiceConnected(BluetoothProfile.VOLUME_CONTROL, mService);
        when(mService.getConnectionPolicy(mBluetoothDevice)).thenReturn(CONNECTION_POLICY_ALLOWED);

        assertThat(mProfile.isEnabled(mBluetoothDevice)).isTrue();
    }

    @Test
    public void isEnabled_connectionPolicyForbidden_returnFalse() {
        mServiceListener.onServiceConnected(BluetoothProfile.VOLUME_CONTROL, mService);
        when(mService.getConnectionPolicy(mBluetoothDevice))
                .thenReturn(CONNECTION_POLICY_FORBIDDEN);

        assertThat(mProfile.isEnabled(mBluetoothDevice)).isFalse();
    }

    @Test
    public void getConnectionPolicy_returnCorrectConnectionPolicy() {
        mServiceListener.onServiceConnected(BluetoothProfile.VOLUME_CONTROL, mService);
        when(mService.getConnectionPolicy(mBluetoothDevice)).thenReturn(CONNECTION_POLICY_ALLOWED);

        assertThat(mProfile.getConnectionPolicy(mBluetoothDevice))
                .isEqualTo(CONNECTION_POLICY_ALLOWED);
    }

    @Test
    public void setEnabled_connectionPolicyAllowed_setConnectionPolicyAllowed_returnFalse() {
        mServiceListener.onServiceConnected(BluetoothProfile.VOLUME_CONTROL, mService);
        when(mService.getConnectionPolicy(mBluetoothDevice)).thenReturn(CONNECTION_POLICY_ALLOWED);
        when(mService.setConnectionPolicy(mBluetoothDevice, CONNECTION_POLICY_ALLOWED))
                .thenReturn(true);

        assertThat(mProfile.setEnabled(mBluetoothDevice, true)).isFalse();
    }

    @Test
    public void setEnabled_connectionPolicyForbidden_setConnectionPolicyAllowed_returnTrue() {
        mServiceListener.onServiceConnected(BluetoothProfile.VOLUME_CONTROL, mService);
        when(mService.getConnectionPolicy(mBluetoothDevice))
                .thenReturn(CONNECTION_POLICY_FORBIDDEN);
        when(mService.setConnectionPolicy(mBluetoothDevice, CONNECTION_POLICY_ALLOWED))
                .thenReturn(true);

        assertThat(mProfile.setEnabled(mBluetoothDevice, true)).isTrue();
    }

    @Test
    public void setEnabled_connectionPolicyAllowed_setConnectionPolicyForbidden_returnTrue() {
        mServiceListener.onServiceConnected(BluetoothProfile.VOLUME_CONTROL, mService);
        when(mService.getConnectionPolicy(mBluetoothDevice)).thenReturn(CONNECTION_POLICY_ALLOWED);
        when(mService.setConnectionPolicy(mBluetoothDevice, CONNECTION_POLICY_FORBIDDEN))
                .thenReturn(true);

        assertThat(mProfile.setEnabled(mBluetoothDevice, false)).isTrue();
    }

    @Test
    public void setEnabled_connectionPolicyForbidden_setConnectionPolicyForbidden_returnTrue() {
        mServiceListener.onServiceConnected(BluetoothProfile.VOLUME_CONTROL, mService);
        when(mService.getConnectionPolicy(mBluetoothDevice))
                .thenReturn(CONNECTION_POLICY_FORBIDDEN);
        when(mService.setConnectionPolicy(mBluetoothDevice, CONNECTION_POLICY_FORBIDDEN))
                .thenReturn(true);

        assertThat(mProfile.setEnabled(mBluetoothDevice, false)).isTrue();
    }

    @Test
    public void getConnectedDevices_returnCorrectList() {
        mServiceListener.onServiceConnected(BluetoothProfile.VOLUME_CONTROL, mService);
        int[] connectedStates = new int[] {
                BluetoothProfile.STATE_CONNECTED,
                BluetoothProfile.STATE_CONNECTING,
                BluetoothProfile.STATE_DISCONNECTING};
        List<BluetoothDevice> connectedList = Arrays.asList(
                mBluetoothDevice,
                mBluetoothDevice,
                mBluetoothDevice);
        when(mService.getDevicesMatchingConnectionStates(connectedStates))
                .thenReturn(connectedList);

        assertThat(mProfile.getConnectedDevices().size()).isEqualTo(connectedList.size());
    }

    @Test
    public void registerCallback_verifyIsCalled() {
        mServiceListener.onServiceConnected(BluetoothProfile.VOLUME_CONTROL, mService);

        final Executor executor = (command -> new Thread(command).start());
        final BluetoothVolumeControl.Callback callback = (device, volumeOffset) -> {};
        mProfile.registerCallback(executor, callback);

        verify(mService).registerCallback(executor, callback);
    }

    @Test
    public void unregisterCallback_verifyIsCalled() {
        final BluetoothVolumeControl.Callback callback = (device, volumeOffset) -> {};
        mServiceListener.onServiceConnected(BluetoothProfile.VOLUME_CONTROL, mService);

        mProfile.unregisterCallback(callback);

        verify(mService).unregisterCallback(callback);
    }

    @Test
    public void setVolumeOffset_verifyIsCalled() {
        mServiceListener.onServiceConnected(BluetoothProfile.VOLUME_CONTROL, mService);

        mProfile.setVolumeOffset(mBluetoothDevice, TEST_VOLUME_OFFSET);

        verify(mService).setVolumeOffset(mBluetoothDevice, TEST_VOLUME_OFFSET);
    }

    @Test
    public void isVolumeOffsetAvailable_verifyIsCalledAndReturnTrue() {
        mServiceListener.onServiceConnected(BluetoothProfile.VOLUME_CONTROL, mService);
        when(mService.isVolumeOffsetAvailable(mBluetoothDevice)).thenReturn(true);

        final boolean available = mProfile.isVolumeOffsetAvailable(mBluetoothDevice);

        verify(mService).isVolumeOffsetAvailable(mBluetoothDevice);
        assertThat(available).isTrue();
    }

    @Test
    public void isVolumeOffsetAvailable_verifyIsCalledAndReturnFalse() {
        mServiceListener.onServiceConnected(BluetoothProfile.VOLUME_CONTROL, mService);
        when(mService.isVolumeOffsetAvailable(mBluetoothDevice)).thenReturn(false);

        final boolean available = mProfile.isVolumeOffsetAvailable(mBluetoothDevice);

        verify(mService).isVolumeOffsetAvailable(mBluetoothDevice);
        assertThat(available).isFalse();
    }
}