/* * Copyright (C) 2022 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.media; import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.content.Context; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; import com.android.internal.annotations.GuardedBy; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; /** * @hide * AudioDeviceVolumeManager provides access to audio device volume control. */ @SystemApi public class AudioDeviceVolumeManager { private static final String TAG = "AudioDeviceVolumeManager"; /** @hide * Indicates no special treatment in the handling of the volume adjustment */ public static final int ADJUST_MODE_NORMAL = 0; /** @hide * Indicates the start of a volume adjustment */ public static final int ADJUST_MODE_START = 1; /** @hide * Indicates the end of a volume adjustment */ public static final int ADJUST_MODE_END = 2; /** @hide */ @IntDef(flag = false, prefix = "ADJUST_MODE", value = { ADJUST_MODE_NORMAL, ADJUST_MODE_START, ADJUST_MODE_END} ) /** @hide */ @Retention(RetentionPolicy.SOURCE) public @interface VolumeAdjustmentMode {} private static IAudioService sService; private final @NonNull String mPackageName; /** * @hide * Constructor * @param context the Context for the device volume operations */ public AudioDeviceVolumeManager(@NonNull Context context) { Objects.requireNonNull(context); mPackageName = context.getApplicationContext().getOpPackageName(); } /** * @hide * Interface to receive volume changes on a device that behaves in absolute volume mode. * @see #setDeviceAbsoluteMultiVolumeBehavior(AudioDeviceAttributes, List, Executor, * OnAudioDeviceVolumeChangeListener) * @see #setDeviceAbsoluteVolumeBehavior(AudioDeviceAttributes, VolumeInfo, Executor, * OnAudioDeviceVolumeChangeListener) */ public interface OnAudioDeviceVolumeChangedListener { /** * Called the device for the given audio device has changed. * @param device the audio device whose volume has changed * @param vol the new volume for the device */ void onAudioDeviceVolumeChanged( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo vol); /** * Called when the volume for the given audio device has been adjusted. * @param device the audio device whose volume has been adjusted * @param vol the volume info for the device * @param direction the direction of the adjustment * @param mode the volume adjustment mode */ void onAudioDeviceVolumeAdjusted( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo vol, @AudioManager.VolumeAdjustment int direction, @VolumeAdjustmentMode int mode); } /** @hide */ static class ListenerInfo { final @NonNull OnAudioDeviceVolumeChangedListener mListener; final @NonNull Executor mExecutor; final @NonNull AudioDeviceAttributes mDevice; final @NonNull boolean mHandlesVolumeAdjustment; ListenerInfo(@NonNull OnAudioDeviceVolumeChangedListener listener, @NonNull Executor exe, @NonNull AudioDeviceAttributes device, boolean handlesVolumeAdjustment) { mListener = listener; mExecutor = exe; mDevice = device; mHandlesVolumeAdjustment = handlesVolumeAdjustment; } } private final Object mDeviceVolumeListenerLock = new Object(); /** * List of listeners for volume changes, the associated device, and their associated Executor. * List is lazy-initialized on first registration */ @GuardedBy("mDeviceVolumeListenerLock") private @Nullable ArrayList mDeviceVolumeListeners; @GuardedBy("mDeviceVolumeListenerLock") private DeviceVolumeDispatcherStub mDeviceVolumeDispatcherStub; /** @hide */ final class DeviceVolumeDispatcherStub extends IAudioDeviceVolumeDispatcher.Stub { /** * Register / unregister the stub * @param register true for registering, false for unregistering * @param device device for which volume is monitored */ @RequiresPermission(anyOf = { android.Manifest.permission.MODIFY_AUDIO_ROUTING, android.Manifest.permission.BLUETOOTH_PRIVILEGED }) public void register(boolean register, @NonNull AudioDeviceAttributes device, @NonNull List volumes, boolean handlesVolumeAdjustment, @AudioManager.AbsoluteDeviceVolumeBehavior int behavior) { try { getService().registerDeviceVolumeDispatcherForAbsoluteVolume(register, this, mPackageName, Objects.requireNonNull(device), Objects.requireNonNull(volumes), handlesVolumeAdjustment, behavior); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } @Override public void dispatchDeviceVolumeChanged( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo vol) { final ArrayList volumeListeners; synchronized (mDeviceVolumeListenerLock) { volumeListeners = (ArrayList) mDeviceVolumeListeners.clone(); } for (ListenerInfo listenerInfo : volumeListeners) { if (listenerInfo.mDevice.equalTypeAddress(device)) { listenerInfo.mExecutor.execute( () -> listenerInfo.mListener.onAudioDeviceVolumeChanged(device, vol)); } } } @Override public void dispatchDeviceVolumeAdjusted( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo vol, int direction, int mode) { final ArrayList volumeListeners; synchronized (mDeviceVolumeListenerLock) { volumeListeners = (ArrayList) mDeviceVolumeListeners.clone(); } for (ListenerInfo listenerInfo : volumeListeners) { if (listenerInfo.mDevice.equalTypeAddress(device)) { listenerInfo.mExecutor.execute( () -> listenerInfo.mListener.onAudioDeviceVolumeAdjusted(device, vol, direction, mode)); } } } } /** * @hide * Configures a device to use absolute volume model, and registers a listener for receiving * volume updates to apply on that device * @param device the audio device set to absolute volume mode * @param volume the type of volume this device responds to * @param executor the Executor used for receiving volume updates through the listener * @param vclistener the callback for volume updates */ @RequiresPermission(anyOf = { android.Manifest.permission.MODIFY_AUDIO_ROUTING, android.Manifest.permission.BLUETOOTH_PRIVILEGED }) public void setDeviceAbsoluteVolumeBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, @NonNull @CallbackExecutor Executor executor, @NonNull OnAudioDeviceVolumeChangedListener vclistener, boolean handlesVolumeAdjustment) { final ArrayList volumes = new ArrayList<>(1); volumes.add(volume); setDeviceAbsoluteMultiVolumeBehavior(device, volumes, executor, vclistener, handlesVolumeAdjustment); } /** * @hide * Configures a device to use absolute volume model applied to different volume types, and * registers a listener for receiving volume updates to apply on that device * @param device the audio device set to absolute multi-volume mode * @param volumes the list of volumes the given device responds to * @param executor the Executor used for receiving volume updates through the listener * @param vclistener the callback for volume updates * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume} * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}. */ @RequiresPermission(anyOf = { android.Manifest.permission.MODIFY_AUDIO_ROUTING, android.Manifest.permission.BLUETOOTH_PRIVILEGED }) public void setDeviceAbsoluteMultiVolumeBehavior( @NonNull AudioDeviceAttributes device, @NonNull List volumes, @NonNull @CallbackExecutor Executor executor, @NonNull OnAudioDeviceVolumeChangedListener vclistener, boolean handlesVolumeAdjustment) { baseSetDeviceAbsoluteMultiVolumeBehavior(device, volumes, executor, vclistener, handlesVolumeAdjustment, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE); } /** * @hide * Configures a device to use absolute volume model, and registers a listener for receiving * volume updates to apply on that device. * * Should be used instead of {@link #setDeviceAbsoluteVolumeBehavior} when there is no reliable * way to set the device's volume to a percentage. * * @param device the audio device set to absolute volume mode * @param volume the type of volume this device responds to * @param executor the Executor used for receiving volume updates through the listener * @param vclistener the callback for volume updates */ @RequiresPermission(anyOf = { android.Manifest.permission.MODIFY_AUDIO_ROUTING, android.Manifest.permission.BLUETOOTH_PRIVILEGED }) public void setDeviceAbsoluteVolumeAdjustOnlyBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, @NonNull @CallbackExecutor Executor executor, @NonNull OnAudioDeviceVolumeChangedListener vclistener, boolean handlesVolumeAdjustment) { final ArrayList volumes = new ArrayList<>(1); volumes.add(volume); setDeviceAbsoluteMultiVolumeAdjustOnlyBehavior(device, volumes, executor, vclistener, handlesVolumeAdjustment); } /** * @hide * Configures a device to use absolute volume model applied to different volume types, and * registers a listener for receiving volume updates to apply on that device. * * Should be used instead of {@link #setDeviceAbsoluteMultiVolumeBehavior} when there is * no reliable way to set the device's volume to a percentage. * * @param device the audio device set to absolute multi-volume mode * @param volumes the list of volumes the given device responds to * @param executor the Executor used for receiving volume updates through the listener * @param vclistener the callback for volume updates */ @RequiresPermission(anyOf = { android.Manifest.permission.MODIFY_AUDIO_ROUTING, android.Manifest.permission.BLUETOOTH_PRIVILEGED }) public void setDeviceAbsoluteMultiVolumeAdjustOnlyBehavior( @NonNull AudioDeviceAttributes device, @NonNull List volumes, @NonNull @CallbackExecutor Executor executor, @NonNull OnAudioDeviceVolumeChangedListener vclistener, boolean handlesVolumeAdjustment) { baseSetDeviceAbsoluteMultiVolumeBehavior(device, volumes, executor, vclistener, handlesVolumeAdjustment, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY); } /** * Base method for configuring a device to use absolute volume behavior, or one of its variants. * See {@link AudioManager#AbsoluteDeviceVolumeBehavior} for a list of allowed behaviors. * * @param behavior the variant of absolute device volume behavior to adopt */ @RequiresPermission(anyOf = { android.Manifest.permission.MODIFY_AUDIO_ROUTING, android.Manifest.permission.BLUETOOTH_PRIVILEGED }) private void baseSetDeviceAbsoluteMultiVolumeBehavior( @NonNull AudioDeviceAttributes device, @NonNull List volumes, @NonNull @CallbackExecutor Executor executor, @NonNull OnAudioDeviceVolumeChangedListener vclistener, boolean handlesVolumeAdjustment, @AudioManager.AbsoluteDeviceVolumeBehavior int behavior) { Objects.requireNonNull(device); Objects.requireNonNull(volumes); Objects.requireNonNull(executor); Objects.requireNonNull(vclistener); final ListenerInfo listenerInfo = new ListenerInfo( vclistener, executor, device, handlesVolumeAdjustment); synchronized (mDeviceVolumeListenerLock) { if (mDeviceVolumeListeners == null) { mDeviceVolumeListeners = new ArrayList<>(); } if (mDeviceVolumeListeners.size() == 0) { if (mDeviceVolumeDispatcherStub == null) { mDeviceVolumeDispatcherStub = new DeviceVolumeDispatcherStub(); } } else { mDeviceVolumeListeners.removeIf(info -> info.mDevice.equalTypeAddress(device)); } mDeviceVolumeListeners.add(listenerInfo); mDeviceVolumeDispatcherStub.register(true, device, volumes, handlesVolumeAdjustment, behavior); } } /** * Manages the OnDeviceVolumeBehaviorChangedListener listeners and * DeviceVolumeBehaviorDispatcherStub */ private final CallbackUtil.LazyListenerManager mDeviceVolumeBehaviorChangedListenerMgr = new CallbackUtil.LazyListenerManager(); /** * @hide * Interface definition of a callback to be invoked when the volume behavior of an audio device * is updated. */ public interface OnDeviceVolumeBehaviorChangedListener { /** * Called on the listener to indicate that the volume behavior of a device has changed. * @param device the audio device whose volume behavior changed * @param volumeBehavior the new volume behavior of the audio device */ void onDeviceVolumeBehaviorChanged( @NonNull AudioDeviceAttributes device, @AudioManager.DeviceVolumeBehavior int volumeBehavior); } /** * @hide * Adds a listener for being notified of changes to any device's volume behavior. * @throws SecurityException if the caller doesn't hold the required permission */ @RequiresPermission(anyOf = { android.Manifest.permission.MODIFY_AUDIO_ROUTING, android.Manifest.permission.QUERY_AUDIO_STATE }) public void addOnDeviceVolumeBehaviorChangedListener( @NonNull @CallbackExecutor Executor executor, @NonNull OnDeviceVolumeBehaviorChangedListener listener) throws SecurityException { mDeviceVolumeBehaviorChangedListenerMgr.addListener(executor, listener, "addOnDeviceVolumeBehaviorChangedListener", () -> new DeviceVolumeBehaviorDispatcherStub()); } /** * @hide * Removes a previously added listener of changes to device volume behavior. */ @RequiresPermission(anyOf = { android.Manifest.permission.MODIFY_AUDIO_ROUTING, android.Manifest.permission.QUERY_AUDIO_STATE }) public void removeOnDeviceVolumeBehaviorChangedListener( @NonNull OnDeviceVolumeBehaviorChangedListener listener) { mDeviceVolumeBehaviorChangedListenerMgr.removeListener(listener, "removeOnDeviceVolumeBehaviorChangedListener"); } /** * @hide * Sets the volume on the given audio device * @param vi the volume information, only stream-based volumes are supported * @param ada the device for which volume is to be modified */ @SystemApi @RequiresPermission(anyOf = { Manifest.permission.MODIFY_AUDIO_ROUTING, Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED }) public void setDeviceVolume(@NonNull VolumeInfo vi, @NonNull AudioDeviceAttributes ada) { try { getService().setDeviceVolume(vi, ada, mPackageName); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * @hide * Returns the volume on the given audio device for the given volume information. * For instance if using a {@link VolumeInfo} configured for {@link AudioManager#STREAM_ALARM}, * it will return the alarm volume. When no volume index has ever been set for the given * device, the default volume will be returned (the volume setting that would have been * applied if playback for that use case had started). * @param vi the volume information, only stream-based volumes are supported. Information * other than the stream type is ignored. * @param ada the device for which volume is to be retrieved */ @SystemApi @RequiresPermission(anyOf = { Manifest.permission.MODIFY_AUDIO_ROUTING, Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED }) public @NonNull VolumeInfo getDeviceVolume(@NonNull VolumeInfo vi, @NonNull AudioDeviceAttributes ada) { try { return getService().getDeviceVolume(vi, ada, mPackageName); } catch (RemoteException e) { e.rethrowFromSystemServer(); } return VolumeInfo.getDefaultVolumeInfo(); } /** * @hide * Return human-readable name for volume behavior * @param behavior one of the volume behaviors defined in AudioManager * @return a string for the given behavior */ public static String volumeBehaviorName(@AudioManager.DeviceVolumeBehavior int behavior) { switch (behavior) { case AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE: return "DEVICE_VOLUME_BEHAVIOR_VARIABLE"; case AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL: return "DEVICE_VOLUME_BEHAVIOR_FULL"; case AudioManager.DEVICE_VOLUME_BEHAVIOR_FIXED: return "DEVICE_VOLUME_BEHAVIOR_FIXED"; case AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE: return "DEVICE_VOLUME_BEHAVIOR_ABSOLUTE"; case AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_MULTI_MODE: return "DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_MULTI_MODE"; case AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY: return "DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY"; default: return "invalid volume behavior " + behavior; } } private final class DeviceVolumeBehaviorDispatcherStub extends IDeviceVolumeBehaviorDispatcher.Stub implements CallbackUtil.DispatcherStub { public void register(boolean register) { try { getService().registerDeviceVolumeBehaviorDispatcher(register, this); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } @Override public void dispatchDeviceVolumeBehaviorChanged(@NonNull AudioDeviceAttributes device, @AudioManager.DeviceVolumeBehavior int volumeBehavior) { mDeviceVolumeBehaviorChangedListenerMgr.callListeners((listener) -> listener.onDeviceVolumeBehaviorChanged(device, volumeBehavior)); } } private static IAudioService getService() { if (sService != null) { return sService; } IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); sService = IAudioService.Stub.asInterface(b); return sService; } }