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

Commit 60855574 authored by William Escande's avatar William Escande
Browse files

AICS: expose implementation in framework

Bug: 372328699
Bug: 359916608
Flag: com.android.bluetooth.flags.aics_api
Test: atest CtsBluetoothTestCases
Change-Id: Ie0ff7fb44db852c41794e91388fedb3555d2df4d
parent ed59a230
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -77,6 +77,9 @@ interface IBluetoothVolumeControl {
    // ---------------------
    // AICS related methods:

    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})")
    int getNumberOfAudioInputControlServices(in AttributionSource attributionSource, in BluetoothDevice device);

    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})")
    void registerAudioInputControlCallback(in AttributionSource attributionSource, in BluetoothDevice device, int instanceId, in IAudioInputCallback callback);
    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})")
+8 −0
Original line number Diff line number Diff line
@@ -1780,6 +1780,14 @@ public class VolumeControlService extends ProfileService {
            return fn.apply(inputs);
        }

        @Override
        public int getNumberOfAudioInputControlServices(
                AttributionSource source, BluetoothDevice device) {
            validateBluetoothDevice(device);
            Log.d(TAG, "getNumberOfAudioInputControlServices(" + device + ")");
            return aicsWrapper(source, device, i -> i.size(), 0);
        }

        @Override
        public void registerAudioInputControlCallback(
                AttributionSource source,
+382 −0
Original line number Diff line number Diff line
@@ -16,10 +16,28 @@

package android.bluetooth;

import static android.Manifest.permission.BLUETOOTH_CONNECT;
import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
import static android.bluetooth.BluetoothUtils.callService;
import static android.bluetooth.BluetoothUtils.logRemoteException;

import static java.util.Objects.requireNonNull;

import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresNoPermission;
import android.annotation.RequiresPermission;
import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
import android.content.AttributionSource;
import android.os.RemoteException;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * This class provides APIs to control a remote AICS(Audio Input Control Service)
@@ -28,6 +46,8 @@ import java.lang.annotation.RetentionPolicy;
 * @hide
 */
public final class AudioInputControl {
    private static final String TAG = AudioInputControl.class.getSimpleName();

    /** Unspecified Input */
    public static final int AUDIO_INPUT_TYPE_UNSPECIFIED =
            bluetooth.constants.AudioInputType.UNSPECIFIED;
@@ -134,4 +154,366 @@ public final class AudioInputControl {
                GAIN_MODE_AUTOMATIC,
            })
    public @interface GainMode {}

    private final IBluetoothVolumeControl mService;
    private final @NonNull BluetoothDevice mDevice;
    private final int mInstanceId;
    private final AttributionSource mAttributionSource;
    private final CallbackWrapper<AudioInputCallback, IBluetoothVolumeControl> mCallbackWrapper;

    /** @hide */
    public AudioInputControl(
            @NonNull BluetoothDevice device,
            int id,
            @NonNull IBluetoothVolumeControl service,
            @NonNull AttributionSource source) {
        mDevice = requireNonNull(device);
        mInstanceId = id;
        mService = requireNonNull(service);
        mAttributionSource = requireNonNull(source);
        mCallbackWrapper =
                new CallbackWrapper<AudioInputCallback, IBluetoothVolumeControl>(
                        this::registerCallbackFn, this::unregisterCallbackFn);
    }

    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    private void registerCallbackFn(IBluetoothVolumeControl vcs) {
        try {
            vcs.registerAudioInputControlCallback(
                    mAttributionSource, mDevice, mInstanceId, mCallback);
        } catch (RemoteException e) {
            logRemoteException(TAG, e);
        }
    }

    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    private void unregisterCallbackFn(IBluetoothVolumeControl vcs) {
        try {
            vcs.unregisterAudioInputControlCallback(
                    mAttributionSource, mDevice, mInstanceId, mCallback);
        } catch (RemoteException e) {
            logRemoteException(TAG, e);
        }
    }

    private final IAudioInputCallback mCallback =
            new IAudioInputCallback.Stub() {
                @Override
                @RequiresNoPermission
                public void onDescriptionChanged(String description) {
                    mCallbackWrapper.forEach(cb -> cb.onDescriptionChanged(description));
                }

                @Override
                @RequiresNoPermission
                public void onStatusChanged(int status) {
                    mCallbackWrapper.forEach(cb -> cb.onStatusChanged(status));
                }

                @Override
                @RequiresNoPermission
                public void onStateChanged(int gainSetting, int mute, int gainMode) {
                    mCallbackWrapper.forEach(cb -> cb.onGainSettingChanged(gainSetting));
                    mCallbackWrapper.forEach(cb -> cb.onMuteChanged(mute));
                    mCallbackWrapper.forEach(cb -> cb.onGainModeChanged(gainMode));
                }

                @Override
                @RequiresNoPermission
                public void onSetGainSettingFailed() {
                    mCallbackWrapper.forEach(cb -> cb.onSetGainSettingFailed());
                }

                @Override
                @RequiresNoPermission
                public void onSetGainModeFailed() {
                    mCallbackWrapper.forEach(cb -> cb.onSetGainModeFailed());
                }

                @Override
                @RequiresNoPermission
                public void onSetMuteFailed() {
                    mCallbackWrapper.forEach(cb -> cb.onSetMuteFailed());
                }
            };

    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    static List<AudioInputControl> getAudioInputControlServices(
            @NonNull IBluetoothVolumeControl service,
            @NonNull AttributionSource source,
            @NonNull BluetoothDevice device) {
        requireNonNull(service);
        requireNonNull(source);
        requireNonNull(device);
        int numberOfAics = 0;
        try {
            numberOfAics = service.getNumberOfAudioInputControlServices(source, device);
        } catch (RemoteException e) {
            logRemoteException(TAG, e);
        }
        return IntStream.range(0, numberOfAics)
                .mapToObj(i -> new AudioInputControl(device, i, service, source))
                .collect(Collectors.toList());
    }

    /**
     * This class provides a callback that is invoked when value changes on the remote device.
     *
     * @hide
     */
    public interface AudioInputCallback {
        /** @hide */
        default void onDescriptionChanged(@NonNull String description) {}

        /** @hide */
        default void onStatusChanged(@AudioInputStatus int status) {}

        /** @hide */
        default void onGainModeChanged(@GainMode int gainMode) {}

        /** @hide */
        default void onMuteChanged(@Mute int mute) {}

        /** @hide */
        default void onGainSettingChanged(int gainSetting) {}

        /** @hide */
        default void onSetGainSettingFailed() {}

        /** @hide */
        default void onSetGainModeFailed() {}

        /** @hide */
        default void onSetMuteFailed() {}
    }

    /**
     * Register a {@link AudioInputCallback}
     *
     * <p>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(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 AudioInputCallback}
     * @throws IllegalArgumentException if callback is already registered
     * @hide
     */
    @RequiresBluetoothConnectPermission
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public void registerCallback(
            @NonNull @CallbackExecutor Executor executor, @NonNull AudioInputCallback callback) {
        mCallbackWrapper.registerCallback(mService, callback, executor);
    }

    /**
     * Unregister the specified {@link AudioInputCallback}.
     *
     * <p>The same {@link AudioInputCallback} object used when calling {@link
     * #registerCallback(Executor, AudioInputCallback)} must be used.
     *
     * <p>Callbacks are automatically unregistered when application process goes away
     *
     * @param callback user implementation of the {@link AudioInputCallback}
     * @throws IllegalArgumentException when no callback is registered
     * @hide
     */
    @RequiresBluetoothConnectPermission
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public void unregisterCallback(@NonNull AudioInputCallback callback) {
        mCallbackWrapper.unregisterCallback(mService, callback);
    }

    /**
     * @return The Audio Input Type as defined in Audio Input Control Service 1.0 - 3.3.
     * @hide
     */
    @RequiresBluetoothConnectPermission
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public @AudioInputType int getType() {
        return callService(
                mService,
                s -> s.getAudioInputType(mAttributionSource, mDevice, mInstanceId),
                bluetooth.constants.AudioInputType.UNSPECIFIED);
    }

    /**
     * @return The Gain Setting Units as defined in Audio Input Control Service 1.0 - 3.2.1
     * @hide
     */
    @RequiresBluetoothConnectPermission
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public int getGainSettingUnit() {
        return callService(
                mService,
                s -> s.getAudioInputGainSettingUnit(mAttributionSource, mDevice, mInstanceId),
                0);
    }

    /**
     * @return The Gain Setting Units as defined in Audio Input Control Service 1.0 - 3.2.1
     * @hide
     */
    @RequiresBluetoothConnectPermission
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public int getGainSettingMin() {
        return callService(
                mService,
                s -> s.getAudioInputGainSettingMin(mAttributionSource, mDevice, mInstanceId),
                0);
    }

    /**
     * @return The Gain Setting Units as defined in Audio Input Control Service 1.0 - 3.2.1
     * @hide
     */
    @RequiresBluetoothConnectPermission
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public int getGainSettingMax() {
        return callService(
                mService,
                s -> s.getAudioInputGainSettingMax(mAttributionSource, mDevice, mInstanceId),
                0);
    }

    /**
     * @return The Gain Setting Units as defined in Audio Input Control Service 1.0 - 3.2.1
     * @hide
     */
    @RequiresBluetoothConnectPermission
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public @NonNull String getDescription() {
        return callService(
                mService,
                s -> s.getAudioInputDescription(mAttributionSource, mDevice, mInstanceId),
                "");
    }

    /**
     * @return The Gain Setting Units as defined in Audio Input Control Service 1.0 - 3.2.1
     * @hide
     */
    @RequiresBluetoothConnectPermission
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public boolean isDescriptionWritable() {
        return callService(
                mService,
                s -> s.isAudioInputDescriptionWritable(mAttributionSource, mDevice, mInstanceId),
                false);
    }

    /**
     * @return The Gain Setting Units as defined in Audio Input Control Service 1.0 - 3.2.1
     * @hide
     */
    @RequiresBluetoothConnectPermission
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public boolean setDescription(@NonNull String description) {
        return callService(
                mService,
                s ->
                        s.setAudioInputDescription(
                                mAttributionSource, mDevice, mInstanceId, description),
                false);
    }

    /**
     * @return The Audio Input Status as defined in Audio Input Control Service 1.0 - 3.4.
     * @hide
     */
    @RequiresBluetoothConnectPermission
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public @AudioInputStatus int getStatus() {
        return callService(
                mService,
                s -> s.getAudioInputStatus(mAttributionSource, mDevice, mInstanceId),
                (int) bluetooth.constants.aics.AudioInputStatus.INACTIVE);
    }

    /**
     * @return The Audio Input Status as defined in Audio Input Control Service 1.0 - 3.4.
     * @hide
     */
    @RequiresBluetoothConnectPermission
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public int getGainSetting() {
        return callService(
                mService,
                s -> s.getAudioInputGainSetting(mAttributionSource, mDevice, mInstanceId),
                0);
    }

    /**
     * @return The Audio Input Status as defined in Audio Input Control Service 1.0 - 3.4.
     * @hide
     */
    @RequiresBluetoothConnectPermission
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public boolean setGainSetting(int gainSetting) {
        return callService(
                mService,
                s ->
                        s.setAudioInputGainSetting(
                                mAttributionSource, mDevice, mInstanceId, gainSetting),
                false);
    }

    /**
     * @return The Audio Input Status as defined in Audio Input Control Service 1.0 - 3.4.
     * @hide
     */
    @RequiresBluetoothConnectPermission
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public @GainMode int getGainMode() {
        return callService(
                mService,
                s -> s.getAudioInputGainMode(mAttributionSource, mDevice, mInstanceId),
                (int) bluetooth.constants.aics.GainMode.AUTOMATIC_ONLY);
    }

    /**
     * @return The Audio Input Status as defined in Audio Input Control Service 1.0 - 3.4.
     * @hide
     */
    @RequiresBluetoothConnectPermission
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public boolean setGainMode(@GainMode int gainMode) {
        if (gainMode < GAIN_MODE_MANUAL_ONLY || gainMode > GAIN_MODE_AUTOMATIC) {
            throw new IllegalArgumentException("Illegal GainMode value: " + gainMode);
        }
        return callService(
                mService,
                s -> s.setAudioInputGainMode(mAttributionSource, mDevice, mInstanceId, gainMode),
                false);
    }

    /**
     * @return The Audio Input Status as defined in Audio Input Control Service 1.0 - 3.4.
     * @hide
     */
    @RequiresBluetoothConnectPermission
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public @Mute int getMute() {
        return callService(
                mService,
                s -> s.getAudioInputMute(mAttributionSource, mDevice, mInstanceId),
                (int) bluetooth.constants.aics.Mute.DISABLED);
    }

    /**
     * @return The Audio Input Status as defined in Audio Input Control Service 1.0 - 3.4.
     * @hide
     */
    @RequiresBluetoothConnectPermission
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public boolean setMute(@Mute int mute) {
        if (mute < MUTE_NOT_MUTED || mute > MUTE_MUTED) {
            throw new IllegalArgumentException("Illegal mute value: " + mute);
        }
        return callService(
                mService,
                s -> s.setAudioInputMute(mAttributionSource, mDevice, mInstanceId, mute),
                false);
    }
}
+6 −1
Original line number Diff line number Diff line
@@ -381,6 +381,11 @@ public final class BluetoothUtils {
        }
    }

    public static <S, R> R callService(
            S service, RemoteExceptionIgnoringFunction<S, R> function, R defaultValue) {
        return function.apply(service, defaultValue);
    }

    public static <S, R> R callServiceIfEnabled(
            BluetoothAdapter adapter,
            Supplier<S> provider,
@@ -395,7 +400,7 @@ public final class BluetoothUtils {
            Log.d(TAG, "Proxy not attached to service");
            return defaultValue;
        }
        return function.apply(service, defaultValue);
        return callService(service, function, defaultValue);
    }

    public static <S> void callServiceIfEnabled(
+20 −0
Original line number Diff line number Diff line
@@ -704,6 +704,26 @@ public final class BluetoothVolumeControl implements BluetoothProfile, AutoClose
                s -> s.setDeviceVolume(device, volume, isGroupOperation, mAttributionSource));
    }

    /**
     * @return The list of {@code AudioInputControl} associated with a device
     * @hide
     */
    @RequiresBluetoothConnectPermission
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public @NonNull List<AudioInputControl> getAudioInputControlPoints(
            @NonNull BluetoothDevice device) {
        requireNonNull(device);
        Log.d(TAG, "getAudioInputControlPoints(" + device + ")");
        if (!isValidDevice(device)) {
            throw new IllegalArgumentException("Invalid device " + device);
        }
        return callServiceIfEnabled(
                mAdapter,
                this::getService,
                s -> AudioInputControl.getAudioInputControlServices(s, mAttributionSource, device),
                Collections.emptyList());
    }

    private static boolean isValidDevice(@Nullable BluetoothDevice device) {
        return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
    }