/* * 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 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.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.RequiresNoPermission; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.bluetooth.annotations.RequiresBluetoothConnectPermission; import android.content.AttributionSource; import android.os.RemoteException; import com.android.bluetooth.flags.Flags; 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) * *

Each {@link AudioInputControl} object represents an instance of the Audio Input Control * Service (AICS) on the remote device. A device may have multiple instances of the AICS, as * described in the Audio Input * Control Service Specification (AICS 1.0). * * @see BluetoothVolumeControl#getAudioInputControlServices * @hide */ @FlaggedApi(Flags.FLAG_AICS_API) @SystemApi public class AudioInputControl { private static final String TAG = AudioInputControl.class.getSimpleName(); /** Unspecified Input */ public static final int AUDIO_INPUT_TYPE_UNSPECIFIED = bluetooth.constants.AudioInputType.UNSPECIFIED; /** Bluetooth Audio Stream */ public static final int AUDIO_INPUT_TYPE_BLUETOOTH = bluetooth.constants.AudioInputType.BLUETOOTH; /** Microphone */ public static final int AUDIO_INPUT_TYPE_MICROPHONE = bluetooth.constants.AudioInputType.MICROPHONE; /** Analog Interface */ public static final int AUDIO_INPUT_TYPE_ANALOG = bluetooth.constants.AudioInputType.ANALOG; /** Digital Interface */ public static final int AUDIO_INPUT_TYPE_DIGITAL = bluetooth.constants.AudioInputType.DIGITAL; /** AM/FM/XM/etc. */ public static final int AUDIO_INPUT_TYPE_RADIO = bluetooth.constants.AudioInputType.RADIO; /** Streaming Audio Source */ public static final int AUDIO_INPUT_TYPE_STREAMING = bluetooth.constants.AudioInputType.STREAMING; /** Transparency/Pass-through */ public static final int AUDIO_INPUT_TYPE_AMBIENT = bluetooth.constants.AudioInputType.AMBIENT; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef( prefix = {"AUDIO_INPUT_TYPE_"}, value = { AUDIO_INPUT_TYPE_UNSPECIFIED, AUDIO_INPUT_TYPE_BLUETOOTH, AUDIO_INPUT_TYPE_MICROPHONE, AUDIO_INPUT_TYPE_ANALOG, AUDIO_INPUT_TYPE_DIGITAL, AUDIO_INPUT_TYPE_RADIO, AUDIO_INPUT_TYPE_STREAMING, AUDIO_INPUT_TYPE_AMBIENT, }) public @interface AudioInputType {} /** Inactive */ public static final int AUDIO_INPUT_STATUS_INACTIVE = bluetooth.constants.aics.AudioInputStatus.INACTIVE; /** Active */ public static final int AUDIO_INPUT_STATUS_ACTIVE = bluetooth.constants.aics.AudioInputStatus.ACTIVE; /** * Status is none of {@link #AUDIO_INPUT_STATUS_ACTIVE}, {@link #AUDIO_INPUT_STATUS_INACTIVE}. * This fallback value will be used for forward compatibility, if the 3.4. Audio Input Status * field extend its definition. */ public static final int AUDIO_INPUT_STATUS_UNKNOWN = -1; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef( prefix = {"AUDIO_INPUT_STATUS_"}, value = { AUDIO_INPUT_STATUS_INACTIVE, AUDIO_INPUT_STATUS_ACTIVE, AUDIO_INPUT_STATUS_UNKNOWN, }) public @interface AudioInputStatus {} /** Not Muted */ public static final int MUTE_NOT_MUTED = bluetooth.constants.aics.Mute.NOT_MUTED; /** Muted */ public static final int MUTE_MUTED = bluetooth.constants.aics.Mute.MUTED; /** * Disabled * *

Mute command are disabled by the server. For example with a local privacy switch. */ public static final int MUTE_DISABLED = bluetooth.constants.aics.Mute.DISABLED; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef( prefix = {"MUTE_"}, value = { MUTE_NOT_MUTED, MUTE_MUTED, MUTE_DISABLED, }) public @interface Mute {} /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef( prefix = {"MUTE_"}, value = { MUTE_NOT_MUTED, MUTE_MUTED, }) public @interface MuteSettable {} /** * Manual Only * *

Gain adjustments are made manually through {@link #setGainSetting}. * *

The server cannot be switched to automatic gain. */ public static final int GAIN_MODE_MANUAL_ONLY = bluetooth.constants.aics.GainMode.MANUAL_ONLY; /** * Automatic Only * *

Gain adjustments are automatic and calls to {@link #setGainSetting} are ignored. * *

The server cannot be switched to manual gain. */ public static final int GAIN_MODE_AUTOMATIC_ONLY = bluetooth.constants.aics.GainMode.AUTOMATIC_ONLY; /** * Manual * *

Gain adjustments are made manually through {@link #setGainSetting}. * *

The server supports switching between manual and automatic gain mode. */ public static final int GAIN_MODE_MANUAL = bluetooth.constants.aics.GainMode.MANUAL; /** * Automatic * *

Gain adjustments are automatic and calls to {@link #setGainSetting} are ignored. * *

The server supports switching between manual and automatic gain mode. */ public static final int GAIN_MODE_AUTOMATIC = bluetooth.constants.aics.GainMode.AUTOMATIC; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef( prefix = {"GAIN_MODE_"}, value = { GAIN_MODE_MANUAL_ONLY, GAIN_MODE_AUTOMATIC_ONLY, GAIN_MODE_MANUAL, GAIN_MODE_AUTOMATIC, }) public @interface GainMode {} /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef( prefix = {"GAIN_MODE_"}, value = { GAIN_MODE_MANUAL, GAIN_MODE_AUTOMATIC, }) public @interface GainModeSettable {} /** Local identifier of the AICS */ private final int mInstanceId; private final IBluetoothVolumeControl mService; private final BluetoothDevice mDevice; private final AttributionSource mAttributionSource; private final CallbackWrapper mCallbackWrapper; 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( 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.onAudioInputStatusChanged(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 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 callback is invoked when the remote AICS notifies a local client of a value changed. It * can also be invoked when a locally initiated operation failed. */ public interface AudioInputCallback { /** see {@link #setDescription(String)} */ default void onDescriptionChanged(@NonNull String description) {} /** see {@link #getStatus()} */ default void onAudioInputStatusChanged(@AudioInputStatus int status) {} /** see {@link #setGainSetting(int)} */ default void onGainSettingChanged(int gainSetting) {} /** see {@link #setGainSetting(int)} */ default void onSetGainSettingFailed() {} /** see {@link #setMute(int)} */ default void onMuteChanged(@Mute int mute) {} /** see {@link #setMute(int)} */ default void onSetMuteFailed() {} /** see {@link #setGainMode(int)} */ default void onGainModeChanged(@GainMode int gainMode) {} /** see {@link #setGainMode(int)} */ default void onSetGainModeFailed() {} } /** * Register an {@link AudioInputCallback} to receive callbacks when the state of the AICS on the * remote device changes. * *

Repeated registration of the same callback object will have no effect after the first call * to this method, even when the executor is different. API caller must call {@link * #unregisterCallback(AudioInputCallback)} with the same callback object before registering it * again. * *

Callbacks are automatically unregistered when the application process goes away. * * @param executor an {@link Executor} to execute given callback * @param callback user implementation of the {@link AudioInputCallback} * @throws IllegalArgumentException if callback is already registered */ @RequiresBluetoothConnectPermission @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) public void registerCallback( @NonNull @CallbackExecutor Executor executor, @NonNull AudioInputCallback callback) { mCallbackWrapper.registerCallback(mService, callback, executor); } /** * Unregister the {@link AudioInputCallback}. * *

The same {@link AudioInputCallback} object used when calling {@link * #registerCallback(Executor, AudioInputCallback)} must be used. * *

Callbacks are automatically unregistered when the application process goes away. * * @param callback user implementation of the {@link AudioInputCallback} * @throws IllegalArgumentException when no callback is registered */ @RequiresBluetoothConnectPermission @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) public void unregisterCallback(@NonNull AudioInputCallback callback) { mCallbackWrapper.unregisterCallback(mService, callback); } /** * Gets the Audio Input Type. * *

This reflects the source of audio for the audio input described by this AICS. The * description may optionally further describe the Audio Input Type with values such as * Microphone, HDMI, etc… * * @return The Audio Input Type as defined in AICS 1.0 - 3.3. */ @RequiresBluetoothConnectPermission @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) public @AudioInputType int getAudioInputType() { return callService( mService, s -> s.getAudioInputType(mAttributionSource, mDevice, mInstanceId), bluetooth.constants.AudioInputType.UNSPECIFIED); } /** * Gets the unit of the gain setting. * *

It reflects the size of a single increment or decrement of {@link #setGainSetting} in 0.1 * decibel units. * * @return The Gain Setting Units as defined in AICS 1.0 - 3.2.1. */ @RequiresBluetoothConnectPermission @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) public @IntRange(from = 0, to = 0xFF) int getGainSettingUnit() { return callService( mService, s -> s.getAudioInputGainSettingUnit(mAttributionSource, mDevice, mInstanceId), 0); } /** * Gets the minimum value for the gain setting. * * @return The minimum Gain Setting as defined in AICS 1.0 - 3.2.2. */ @RequiresBluetoothConnectPermission @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) public @IntRange(from = -128, to = 127) int getGainSettingMin() { return callService( mService, s -> s.getAudioInputGainSettingMin(mAttributionSource, mDevice, mInstanceId), 0); } /** * Gets the maximum value for the gain setting. * * @return The maximum Gain Setting as defined in AICS 1.0 - 3.2.3. */ @RequiresBluetoothConnectPermission @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) public @IntRange(from = -128, to = 127) int getGainSettingMax() { return callService( mService, s -> s.getAudioInputGainSettingMax(mAttributionSource, mDevice, mInstanceId), 0); } /** * Gets the description. * *

Register an {@link AudioInputCallback} to be notified via {@link * AudioInputCallback#onDescriptionChanged} when the description changes. * *

This describes the AICS. For example, if a device instantiated a service for both * “Bluetooth” and “Line In” audio inputs, then the description value would be set to * “Bluetooth” on one service and “Line In” on the other service. If multiple Bluetooth audio * inputs are represented, the server may set the Audio Input Description to the remote source’s * name, a string representing the content type, the content control server name, etc. The value * is a UTF-8 string of zero or more characters. * * @return The description as defined in AICS 1.0 - 3.6. */ @RequiresBluetoothConnectPermission @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) public @NonNull String getDescription() { return callService( mService, s -> s.getAudioInputDescription(mAttributionSource, mDevice, mInstanceId), ""); } /** * Checks whether the description is writable as defined in AICS 1.0 - 3.6. * * @return true if the description can be written to, false otherwise. */ @RequiresBluetoothConnectPermission @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) public boolean isDescriptionWritable() { return callService( mService, s -> s.isAudioInputDescriptionWritable(mAttributionSource, mDevice, mInstanceId), false); } /** * Sets the description as defined in AICS 1.0 - 3.6. * *

The operation will fail if the description is not writable. This can be verified with * {@link #isDescriptionWritable} * *

Register an {@link AudioInputCallback} to be notified via {@link * AudioInputCallback#onDescriptionChanged} when the description change is applied on remote * device. * * @param description The description of the AICS. * @return true if the operation is successfully initiated, false otherwise. * @throws IllegalStateException if the description is not writable */ @RequiresBluetoothConnectPermission @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) public boolean setDescription(@NonNull String description) { requireNonNull(description); return callService( mService, s -> s.setAudioInputDescription( mAttributionSource, mDevice, mInstanceId, description), false); } /** * Gets the Audio Input Status. * *

Register an {@link AudioInputCallback} to be notified via {@link * AudioInputCallback#onAudioInputStatusChanged} when the status changes. * * @return The Audio Input Status as defined in AICS 1.0 - 3.4. */ @RequiresBluetoothConnectPermission @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) public @AudioInputStatus int getAudioInputStatus() { return callService( mService, s -> s.getAudioInputStatus(mAttributionSource, mDevice, mInstanceId), (int) bluetooth.constants.aics.AudioInputStatus.INACTIVE); } /** * Gets the gain setting. * *

Register an {@link AudioInputCallback} to be notified via {@link * AudioInputCallback#onGainSettingChanged} when the gain setting changes. * * @return The current gain setting as defined in AICS 1.0 - 2.2.1.1. */ @RequiresBluetoothConnectPermission @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) public @IntRange(from = -128, to = 127) int getGainSetting() { return callService( mService, s -> s.getAudioInputGainSetting(mAttributionSource, mDevice, mInstanceId), 0); } /** * Sets the gain setting as defined in AICS 1.0 - 3.5.2.1. * *

The operation will fail if the current gain mode is {@link #AUTOMATIC} or {@link * #AUTOMATIC_ONLY}. * *

Register an {@link AudioInputControl.AudioInputCallback} to be notified via * *

* * The gain setting 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 * unit (see {@link #getGainSettingUnit()}). A gain setting value of 0 should result in no * change to the input’s original amplitude. * * @param gainSetting The desired gain setting value. Refer to {@link #getGainSettingMin()} and * {@link #getGainSettingMax()} for the allowed range. Refer to {@link * #getGainSettingUnit()} to knows how much decibel this represents. * @return true if the operation is successfully initiated, false otherwise. The callback {@link * AudioInputCallback#onSetGainSettingFailed()} will not be call if false is returned * @throws IllegalStateException if the gain mode is {@link #AUTOMATIC} or {@link * #AUTOMATIC_ONLY} * @throws IllegalArgumentException if the gain setting is not in range */ @RequiresBluetoothConnectPermission @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) public boolean setGainSetting(@IntRange(from = -128, to = 127) int gainSetting) { return callService( mService, s -> s.setAudioInputGainSetting( mAttributionSource, mDevice, mInstanceId, gainSetting), false); } /** * Gets the gain mode. * *

Register an {@link AudioInputCallback} to be notified via {@link * AudioInputCallback#onGainModeChanged} when the gain mode changes. * * @return The current gain mode as defined in AICS 1.0 - 2.2.1.3. */ @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); } /** * Sets the gain mode as defined in AICS 1.0 - 3.5.2.4/5. * *

The operation will fail if the current gain mode is {@link #MANUAL_ONLY} or {@link * #AUTOMATIC_ONLY}. * *

Register an {@link AudioInputControl.AudioInputCallback} to be notified via * *

* * @param gainMode The desired gain mode * @return true if the operation is successfully initiated, false otherwise. The callback {@link * AudioInputCallback#onSetGainModeFailed()} will not be call if false is returned * @throws IllegalStateException if the gain mode is {@link #MANUAL_ONLY} or {@link * #AUTOMATIC_ONLY} * @throws IllegalArgumentException if the gain mode value is invalid. */ @RequiresBluetoothConnectPermission @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) public boolean setGainMode(@GainModeSettable int gainMode) { if (gainMode != GAIN_MODE_MANUAL && gainMode != GAIN_MODE_AUTOMATIC) { throw new IllegalArgumentException("Illegal GainMode value: " + gainMode); } return callService( mService, s -> s.setAudioInputGainMode(mAttributionSource, mDevice, mInstanceId, gainMode), false); } /** * Gets the mute state. * *

Register an {@link AudioInputCallback} to be notified via {@link * AudioInputCallback#onMuteChanged} when the mute state changes. * * @return The current mute state as defined in AICS 1.0 - 2.2.1.2. */ @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); } /** * Sets the mute state as defined in AICS 1.0 - 3.5.2.2/3. * *

The operation will fail if the current mute state is {@link #MUTE_DISABLED}. * *

Register an {@link AudioInputControl.AudioInputCallback} to be notified via * *

* * @param mute the new mute state. * @return true on success, false otherwise. * @throws IllegalStateException if the mute state is {@link #MUTE_DISABLED} * @throws IllegalArgumentException if the provided {@code mute} is not valid */ @RequiresBluetoothConnectPermission @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) public boolean setMute(@MuteSettable 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); } }