From 7d4198268d288ea88ab637d0592859194b536a05 Mon Sep 17 00:00:00 2001 From: Yan Han Date: Mon, 31 Jan 2022 19:10:16 +0100 Subject: [PATCH] AudioService: Add general support for Absolute Volume behavior - Adds a map to store information about audio devices using absolute volume behavior. The map is updated when devices change behavior. - setStreamVolume and adjustStreamVolume now notify absolute volume listeners after changing the stream state. - Added option for absolute volume dispatcher to dispatch abstract volume adjustments (e.g. raise, lower, mute) as a result of calls to adjustStreamVolume. When registering the listener, the controller specifies whether it handles adjustments; if it does, future calls to adjustStreamVolume are forwarded to the controller without affecting stream state. - Refactoring to support the changes above: - rescaleIndex is generalized to allow converting between stream and absolute volume ranges. - Volume adjustment mode constants are moved into AudioDeviceVolumeManager because they are needed when dispatching volume adjustments to absolute volume controllers. Bug: 205817863 Test: atest AbsoluteVolumeBehaviorTest Change-Id: Ie5fb2179adea2096bd2d226fe73b4f09b28b0cf1 --- .../media/AudioDeviceVolumeManager.java | 87 ++++- media/java/android/media/AudioManager.java | 9 + .../media/IAudioDeviceVolumeDispatcher.aidl | 2 + media/java/android/media/IAudioService.aidl | 3 +- .../android/server/audio/AudioService.java | 303 +++++++++++++++--- .../audio/AbsoluteVolumeBehaviorTest.java | 290 +++++++++++++++++ 6 files changed, 632 insertions(+), 62 deletions(-) create mode 100644 services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java diff --git a/media/java/android/media/AudioDeviceVolumeManager.java b/media/java/android/media/AudioDeviceVolumeManager.java index 71042412e546..11cacd01f53d 100644 --- a/media/java/android/media/AudioDeviceVolumeManager.java +++ b/media/java/android/media/AudioDeviceVolumeManager.java @@ -17,6 +17,7 @@ package android.media; import android.annotation.CallbackExecutor; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -27,6 +28,8 @@ 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; @@ -40,6 +43,22 @@ public class AudioDeviceVolumeManager { // define when using Log.* //private static final String TAG = "AudioDeviceVolumeManager"; + + /** Indicates no special treatment in the handling of the volume adjustment */ + public static final int ADJUST_MODE_NORMAL = 0; + /** Indicates the start of a volume adjustment */ + public static final int ADJUST_MODE_START = 1; + /** Indicates the end of a volume adjustment */ + public static final int ADJUST_MODE_END = 2; + + @IntDef(flag = false, prefix = "ADJUST_MODE", value = { + ADJUST_MODE_NORMAL, + ADJUST_MODE_START, + ADJUST_MODE_END} + ) + @Retention(RetentionPolicy.SOURCE) + public @interface VolumeAdjustmentMode {} + private static IAudioService sService; private final String mPackageName; @@ -65,18 +84,33 @@ public class AudioDeviceVolumeManager { 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); } 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) { + @NonNull AudioDeviceAttributes device, boolean handlesVolumeAdjustment) { mListener = listener; mExecutor = exe; mDevice = device; + mHandlesVolumeAdjustment = handlesVolumeAdjustment; } } @@ -98,11 +132,12 @@ public class AudioDeviceVolumeManager { * @param device device for which volume is monitored */ public void register(boolean register, @NonNull AudioDeviceAttributes device, - @NonNull List volumes) { + @NonNull List volumes, boolean handlesVolumeAdjustment) { try { getService().registerDeviceVolumeDispatcherForAbsoluteVolume(register, this, mPackageName, - Objects.requireNonNull(device), Objects.requireNonNull(volumes)); + Objects.requireNonNull(device), Objects.requireNonNull(volumes), + handlesVolumeAdjustment); } catch (RemoteException e) { e.rethrowFromSystemServer(); } @@ -116,12 +151,29 @@ public class AudioDeviceVolumeManager { volumeListeners = (ArrayList) mDeviceVolumeListeners.clone(); } for (ListenerInfo listenerInfo : volumeListeners) { - if (listenerInfo.mDevice.equals(device)) { + 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)); + } + } + } } /** @@ -139,10 +191,12 @@ public class AudioDeviceVolumeManager { @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, @NonNull @CallbackExecutor Executor executor, - @NonNull OnAudioDeviceVolumeChangedListener vclistener) { + @NonNull OnAudioDeviceVolumeChangedListener vclistener, + boolean handlesVolumeAdjustment) { final ArrayList volumes = new ArrayList<>(1); volumes.add(volume); - setDeviceAbsoluteMultiVolumeBehavior(device, volumes, executor, vclistener); + setDeviceAbsoluteMultiVolumeBehavior(device, volumes, executor, vclistener, + handlesVolumeAdjustment); } /** @@ -153,6 +207,9 @@ public class AudioDeviceVolumeManager { * @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 }) @@ -160,14 +217,15 @@ public class AudioDeviceVolumeManager { @NonNull AudioDeviceAttributes device, @NonNull List volumes, @NonNull @CallbackExecutor Executor executor, - @NonNull OnAudioDeviceVolumeChangedListener vclistener) { + @NonNull OnAudioDeviceVolumeChangedListener vclistener, + boolean handlesVolumeAdjustment) { Objects.requireNonNull(device); Objects.requireNonNull(volumes); Objects.requireNonNull(executor); Objects.requireNonNull(vclistener); - // TODO verify not already registered - //final ListenerInfo listenerInfo = new ListenerInfo(vclistener, executor, device); + final ListenerInfo listenerInfo = new ListenerInfo( + vclistener, executor, device, handlesVolumeAdjustment); synchronized (mDeviceVolumeListenerLock) { if (mDeviceVolumeListeners == null) { mDeviceVolumeListeners = new ArrayList<>(); @@ -176,8 +234,17 @@ public class AudioDeviceVolumeManager { if (mDeviceVolumeDispatcherStub == null) { mDeviceVolumeDispatcherStub = new DeviceVolumeDispatcherStub(); } + } else { + for (ListenerInfo info : mDeviceVolumeListeners) { + if (info.mListener == vclistener) { + throw new IllegalArgumentException( + "attempt to call setDeviceAbsoluteMultiVolumeBehavior() " + + "on a previously registered listener"); + } + } } - mDeviceVolumeDispatcherStub.register(true, device, volumes); + mDeviceVolumeListeners.add(listenerInfo); + mDeviceVolumeDispatcherStub.register(true, device, volumes, handlesVolumeAdjustment); } } diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 1a56b1542b07..395062e17b94 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -604,6 +604,13 @@ public class AudioManager { @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) public static final int FLAG_FROM_KEY = 1 << 12; + /** + * Indicates that an absolute volume controller is notifying AudioService of a change in the + * volume or mute status of an external audio system. + * @hide + */ + public static final int FLAG_ABSOLUTE_VOLUME = 1 << 13; + /** @hide */ @IntDef(prefix = {"ENCODED_SURROUND_OUTPUT_"}, value = { ENCODED_SURROUND_OUTPUT_UNKNOWN, @@ -661,6 +668,7 @@ public class AudioManager { FLAG_SHOW_UI_WARNINGS, FLAG_SHOW_VIBRATE_HINT, FLAG_FROM_KEY, + FLAG_ABSOLUTE_VOLUME, }) @Retention(RetentionPolicy.SOURCE) public @interface Flags {} @@ -682,6 +690,7 @@ public class AudioManager { FLAG_NAMES.put(FLAG_SHOW_UI_WARNINGS, "FLAG_SHOW_UI_WARNINGS"); FLAG_NAMES.put(FLAG_SHOW_VIBRATE_HINT, "FLAG_SHOW_VIBRATE_HINT"); FLAG_NAMES.put(FLAG_FROM_KEY, "FLAG_FROM_KEY"); + FLAG_NAMES.put(FLAG_ABSOLUTE_VOLUME, "FLAG_ABSOLUTE_VOLUME"); } /** @hide */ diff --git a/media/java/android/media/IAudioDeviceVolumeDispatcher.aidl b/media/java/android/media/IAudioDeviceVolumeDispatcher.aidl index 65633fe6b104..70b4ab676d11 100644 --- a/media/java/android/media/IAudioDeviceVolumeDispatcher.aidl +++ b/media/java/android/media/IAudioDeviceVolumeDispatcher.aidl @@ -27,5 +27,7 @@ import android.media.VolumeInfo; oneway interface IAudioDeviceVolumeDispatcher { void dispatchDeviceVolumeChanged(in AudioDeviceAttributes device, in VolumeInfo vol); + void dispatchDeviceVolumeAdjusted(in AudioDeviceAttributes device, in VolumeInfo vol, + int direction, int mode); } diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index babafaf26036..e28178a8d5d8 100755 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -509,7 +509,8 @@ interface IAudioService { void registerDeviceVolumeDispatcherForAbsoluteVolume(boolean register, in IAudioDeviceVolumeDispatcher cb, in String packageName, - in AudioDeviceAttributes device, in List volumes); + in AudioDeviceAttributes device, in List volumes, + boolean handlesvolumeAdjustment); String getHalVersion(); } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index a68fc05dbbf1..5aae31966266 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -78,6 +78,7 @@ import android.media.AudioAttributes; import android.media.AudioAttributes.AttributeSystemUsage; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; +import android.media.AudioDeviceVolumeManager; import android.media.AudioFocusInfo; import android.media.AudioFocusRequest; import android.media.AudioFormat; @@ -154,6 +155,7 @@ import android.service.notification.ZenModeConfig; import android.telecom.TelecomManager; import android.text.TextUtils; import android.util.AndroidRuntimeException; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.IntArray; import android.util.Log; @@ -627,6 +629,47 @@ public class AudioService extends IAudioService.Stub AudioSystem.DEVICE_OUT_HDMI_ARC, AudioSystem.DEVICE_OUT_HDMI_EARC )); + + // Devices where the framework sends a full scale audio signal, and controls the volume of + // the external audio system separately. + Map mAbsoluteVolumeDeviceInfoMap = new ArrayMap<>(); + + /** + * Stores information about a device using absolute volume behavior. + */ + private static final class AbsoluteVolumeDeviceInfo { + private final AudioDeviceAttributes mDevice; + private final List mVolumeInfos; + private final IAudioDeviceVolumeDispatcher mCallback; + private final boolean mHandlesVolumeAdjustment; + + private AbsoluteVolumeDeviceInfo(AudioDeviceAttributes device, List volumeInfos, + IAudioDeviceVolumeDispatcher callback, boolean handlesVolumeAdjustment) { + this.mDevice = device; + this.mVolumeInfos = volumeInfos; + this.mCallback = callback; + this.mHandlesVolumeAdjustment = handlesVolumeAdjustment; + } + + /** + * Given a stream type, returns a matching VolumeInfo. + */ + @Nullable + private VolumeInfo getMatchingVolumeInfoForStream(int streamType) { + for (VolumeInfo volumeInfo : mVolumeInfos) { + boolean streamTypeMatches = volumeInfo.hasStreamType() + && volumeInfo.getStreamType() == streamType; + boolean volumeGroupMatches = volumeInfo.hasVolumeGroup() + && Arrays.stream(volumeInfo.getVolumeGroup().getLegacyStreamTypes()) + .anyMatch(s -> s == streamType); + if (streamTypeMatches || volumeGroupMatches) { + return volumeInfo; + } + } + return null; + } + } + // Devices for the which use the "absolute volume" concept (framework sends audio signal // full scale, and volume control separately) and can be used for multiple use cases reflected // by the audio mode (e.g. media playback in MODE_NORMAL, and phone calls in MODE_IN_CALL). @@ -2509,17 +2552,44 @@ public class AudioService extends IAudioService.Stub return (mStreamStates[streamType].getMaxIndex() - mStreamStates[streamType].getMinIndex()); } - private int rescaleIndex(int index, int srcStream, int dstStream) { - int srcRange = getIndexRange(srcStream); - int dstRange = getIndexRange(dstStream); - if (srcRange == 0) { - Log.e(TAG, "rescaleIndex : index range should not be zero"); + private int rescaleIndex(VolumeInfo volumeInfo, int dstStream) { + if (volumeInfo.getVolumeIndex() == VolumeInfo.INDEX_NOT_SET + || volumeInfo.getMinVolumeIndex() == VolumeInfo.INDEX_NOT_SET + || volumeInfo.getMaxVolumeIndex() == VolumeInfo.INDEX_NOT_SET) { + Log.e(TAG, "rescaleIndex: volumeInfo has invalid index or range"); return mStreamStates[dstStream].getMinIndex(); } + return rescaleIndex(volumeInfo.getVolumeIndex(), + volumeInfo.getMinVolumeIndex(), volumeInfo.getMaxVolumeIndex(), + mStreamStates[dstStream].getMinIndex(), mStreamStates[dstStream].getMaxIndex()); + } - return mStreamStates[dstStream].getMinIndex() - + ((index - mStreamStates[srcStream].getMinIndex()) * dstRange + srcRange / 2) - / srcRange; + private int rescaleIndex(int index, int srcStream, VolumeInfo dstVolumeInfo) { + int dstMin = dstVolumeInfo.getMinVolumeIndex(); + int dstMax = dstVolumeInfo.getMaxVolumeIndex(); + // Don't rescale index if the VolumeInfo is missing a min or max index + if (dstMin == VolumeInfo.INDEX_NOT_SET || dstMax == VolumeInfo.INDEX_NOT_SET) { + return index; + } + return rescaleIndex(index, + mStreamStates[srcStream].getMinIndex(), mStreamStates[srcStream].getMaxIndex(), + dstMin, dstMax); + } + + private int rescaleIndex(int index, int srcStream, int dstStream) { + return rescaleIndex(index, + mStreamStates[srcStream].getMinIndex(), mStreamStates[srcStream].getMaxIndex(), + mStreamStates[dstStream].getMinIndex(), mStreamStates[dstStream].getMaxIndex()); + } + + private int rescaleIndex(int index, int srcMin, int srcMax, int dstMin, int dstMax) { + int srcRange = srcMax - srcMin; + int dstRange = dstMax - dstMin; + if (srcRange == 0) { + Log.e(TAG, "rescaleIndex : index range should not be zero"); + return dstMin; + } + return dstMin + ((index - srcMin) * dstRange + srcRange / 2) / srcRange; } private int rescaleStep(int step, int srcStream, int dstStream) { @@ -2752,26 +2822,19 @@ public class AudioService extends IAudioService.Stub return mAudioSystem.getDevicesForAttributes(attributes, forVolume); } - /** Indicates no special treatment in the handling of the volume adjustement */ - private static final int VOL_ADJUST_NORMAL = 0; - /** Indicates the start of a volume adjustement */ - private static final int VOL_ADJUST_START = 1; - /** Indicates the end of a volume adjustment */ - private static final int VOL_ADJUST_END = 2; - // pre-condition: event.getKeyCode() is one of KeyEvent.KEYCODE_VOLUME_UP, // KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_MUTE public void handleVolumeKey(@NonNull KeyEvent event, boolean isOnTv, @NonNull String callingPackage, @NonNull String caller) { - int keyEventMode = VOL_ADJUST_NORMAL; + int keyEventMode = AudioDeviceVolumeManager.ADJUST_MODE_NORMAL; if (isOnTv) { if (event.getAction() == KeyEvent.ACTION_DOWN) { - keyEventMode = VOL_ADJUST_START; + keyEventMode = AudioDeviceVolumeManager.ADJUST_MODE_START; } else { // may catch more than ACTION_UP, but will end vol adjustement // the vol key is either released (ACTION_UP), or multiple keys are pressed // (ACTION_MULTIPLE) and we don't know what to do for volume control on CEC, end // the repeated volume adjustement - keyEventMode = VOL_ADJUST_END; + keyEventMode = AudioDeviceVolumeManager.ADJUST_MODE_END; } } else if (event.getAction() != KeyEvent.ACTION_DOWN) { return; @@ -2796,7 +2859,7 @@ public class AudioService extends IAudioService.Stub adjustSuggestedStreamVolume(AudioManager.ADJUST_TOGGLE_MUTE, AudioManager.USE_DEFAULT_STREAM_TYPE, flags, callingPackage, caller, Binder.getCallingUid(), Binder.getCallingPid(), - true, VOL_ADJUST_NORMAL); + true, AudioDeviceVolumeManager.ADJUST_MODE_NORMAL); } break; default: @@ -2941,7 +3004,7 @@ public class AudioService extends IAudioService.Stub direction/*val1*/, flags/*val2*/, callingPackage)); adjustStreamVolume(streamType, direction, flags, callingPackage, callingPackage, Binder.getCallingUid(), Binder.getCallingPid(), attributionTag, - callingHasAudioSettingsPermission(), VOL_ADJUST_NORMAL); + callingHasAudioSettingsPermission(), AudioDeviceVolumeManager.ADJUST_MODE_NORMAL); } protected void adjustStreamVolume(int streamType, int direction, int flags, @@ -3074,8 +3137,19 @@ public class AudioService extends IAudioService.Stub } int oldIndex = mStreamStates[streamType].getIndex(device); - if (adjustVolume - && (direction != AudioManager.ADJUST_SAME) && (keyEventMode != VOL_ADJUST_END)) { + // Check if the volume adjustment should be handled by an absolute volume controller instead + if (isAbsoluteVolumeDevice(device) + && (flags & AudioManager.FLAG_ABSOLUTE_VOLUME) == 0) { + AbsoluteVolumeDeviceInfo info = mAbsoluteVolumeDeviceInfoMap.get(device); + if (info.mHandlesVolumeAdjustment) { + dispatchAbsoluteVolumeAdjusted(streamType, info, oldIndex, direction, + keyEventMode); + return; + } + } + + if (adjustVolume && (direction != AudioManager.ADJUST_SAME) + && (keyEventMode != AudioDeviceVolumeManager.ADJUST_MODE_END)) { mAudioHandler.removeMessages(MSG_UNMUTE_STREAM); if (isMuteAdjust && !mFullVolumeDevices.contains(device)) { @@ -3136,6 +3210,10 @@ public class AudioService extends IAudioService.Stub + newIndex + "stream=" + streamType); } mDeviceBroker.postSetAvrcpAbsoluteVolumeIndex(newIndex / 10); + } else if (isAbsoluteVolumeDevice(device) + && (flags & AudioManager.FLAG_ABSOLUTE_VOLUME) == 0) { + AbsoluteVolumeDeviceInfo info = mAbsoluteVolumeDeviceInfoMap.get(device); + dispatchAbsoluteVolumeChanged(streamType, info, newIndex); } if (device == AudioSystem.DEVICE_OUT_BLE_HEADSET @@ -3201,14 +3279,14 @@ public class AudioService extends IAudioService.Stub final long ident = Binder.clearCallingIdentity(); try { switch (keyEventMode) { - case VOL_ADJUST_NORMAL: + case AudioDeviceVolumeManager.ADJUST_MODE_NORMAL: fullVolumeHdmiClient.sendVolumeKeyEvent(keyCode, true); fullVolumeHdmiClient.sendVolumeKeyEvent(keyCode, false); break; - case VOL_ADJUST_START: + case AudioDeviceVolumeManager.ADJUST_MODE_START: fullVolumeHdmiClient.sendVolumeKeyEvent(keyCode, true); break; - case VOL_ADJUST_END: + case AudioDeviceVolumeManager.ADJUST_MODE_END: fullVolumeHdmiClient.sendVolumeKeyEvent(keyCode, false); break; default: @@ -3794,6 +3872,11 @@ public class AudioService extends IAudioService.Stub + "stream=" + streamType); } mDeviceBroker.postSetAvrcpAbsoluteVolumeIndex(index / 10); + } else if (isAbsoluteVolumeDevice(device) + && ((flags & AudioManager.FLAG_ABSOLUTE_VOLUME) == 0)) { + AbsoluteVolumeDeviceInfo info = mAbsoluteVolumeDeviceInfoMap.get(device); + + dispatchAbsoluteVolumeChanged(streamType, info, index); } if (device == AudioSystem.DEVICE_OUT_BLE_HEADSET @@ -3872,6 +3955,38 @@ public class AudioService extends IAudioService.Stub return AudioVolumeGroup.DEFAULT_VOLUME_GROUP; } + private void dispatchAbsoluteVolumeChanged(int streamType, AbsoluteVolumeDeviceInfo deviceInfo, + int index) { + VolumeInfo volumeInfo = deviceInfo.getMatchingVolumeInfoForStream(streamType); + if (volumeInfo != null) { + try { + deviceInfo.mCallback.dispatchDeviceVolumeChanged(deviceInfo.mDevice, + new VolumeInfo.Builder(volumeInfo) + .setVolumeIndex(rescaleIndex(index, streamType, volumeInfo)) + .build()); + } catch (RemoteException e) { + Log.w(TAG, "Couldn't dispatch absolute volume behavior volume change"); + } + } + } + + private void dispatchAbsoluteVolumeAdjusted(int streamType, + AbsoluteVolumeDeviceInfo deviceInfo, int index, int direction, int mode) { + VolumeInfo volumeInfo = deviceInfo.getMatchingVolumeInfoForStream(streamType); + if (volumeInfo != null) { + try { + deviceInfo.mCallback.dispatchDeviceVolumeAdjusted(deviceInfo.mDevice, + new VolumeInfo.Builder(volumeInfo) + .setVolumeIndex(rescaleIndex(index, streamType, volumeInfo)) + .build(), + direction, + mode); + } catch (RemoteException e) { + Log.w(TAG, "Couldn't dispatch absolute volume behavior volume adjustment"); + } + } + } + // No ringer or zen muted stream volumes can be changed unless it'll exit dnd private boolean volumeAdjustmentAllowedByDnd(int streamTypeAlias, int flags) { @@ -4007,6 +4122,14 @@ public class AudioService extends IAudioService.Stub if (streamType == AudioSystem.STREAM_MUSIC) { flags = updateFlagsForTvPlatform(flags); + synchronized (mHdmiClientLock) { + // Don't display volume UI on a TV Playback device when using absolute volume + if (mHdmiCecVolumeControlEnabled && mHdmiPlaybackClient != null + && (isAbsoluteVolumeDevice(device) + || isA2dpAbsoluteVolumeDevice(device))) { + flags &= ~AudioManager.FLAG_SHOW_UI; + } + } if (isFullVolumeDevice(device)) { flags &= ~AudioManager.FLAG_SHOW_UI; } @@ -5183,7 +5306,8 @@ public class AudioService extends IAudioService.Stub // direction and stream type swap here because the public // adjustSuggested has a different order than the other methods. adjustSuggestedStreamVolume(direction, streamType, flags, packageName, packageName, - uid, pid, hasAudioSettingsPermission(uid, pid), VOL_ADJUST_NORMAL); + uid, pid, hasAudioSettingsPermission(uid, pid), + AudioDeviceVolumeManager.ADJUST_MODE_NORMAL); } /** @see AudioManager#adjustStreamVolumeForUid(int, int, int, String, int, int, int) */ @@ -5203,7 +5327,8 @@ public class AudioService extends IAudioService.Stub } adjustStreamVolume(streamType, direction, flags, packageName, packageName, uid, pid, - null, hasAudioSettingsPermission(uid, pid), VOL_ADJUST_NORMAL); + null, hasAudioSettingsPermission(uid, pid), + AudioDeviceVolumeManager.ADJUST_MODE_NORMAL); } /** @see AudioManager#setStreamVolumeForUid(int, int, int, String, int, int, int) */ @@ -6423,15 +6548,16 @@ public class AudioService extends IAudioService.Stub /** * @see AudioDeviceVolumeManager#setDeviceAbsoluteMultiVolumeBehavior - * @param cb - * @param attr - * @param volumes + * + * @param register Whether the listener is to be registered or unregistered. If false, the + * device adopts variable volume behavior. */ @RequiresPermission(anyOf = { android.Manifest.permission.MODIFY_AUDIO_ROUTING, android.Manifest.permission.BLUETOOTH_PRIVILEGED }) public void registerDeviceVolumeDispatcherForAbsoluteVolume(boolean register, IAudioDeviceVolumeDispatcher cb, String packageName, - AudioDeviceAttributes device, List volumes) { + AudioDeviceAttributes device, List volumes, + boolean handlesVolumeAdjustment) { // verify permissions if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) != PackageManager.PERMISSION_GRANTED @@ -6444,12 +6570,44 @@ public class AudioService extends IAudioService.Stub Objects.requireNonNull(device); Objects.requireNonNull(volumes); - // current implementation maps this call to existing abs volume API of AudioManager - // TODO implement the volume/device listener through IAudioDeviceVolumeDispatcher - final int volumeBehavior = volumes.size() == 1 - ? AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE - : AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_MULTI_MODE; - setDeviceVolumeBehavior(device, volumeBehavior, packageName); + int deviceOut = device.getInternalType(); + if (register) { + AbsoluteVolumeDeviceInfo info = new AbsoluteVolumeDeviceInfo( + device, volumes, cb, handlesVolumeAdjustment); + boolean volumeBehaviorChanged = + removeAudioSystemDeviceOutFromFullVolumeDevices(deviceOut) + | removeAudioSystemDeviceOutFromFixedVolumeDevices(deviceOut) + | (addAudioSystemDeviceOutToAbsVolumeDevices(deviceOut, info) == null); + if (volumeBehaviorChanged) { + dispatchDeviceVolumeBehavior(device, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE); + } + // Update stream volumes to the given device, if specified in a VolumeInfo. + // Mute state is not updated because it is stream-wide - the only way to mute a + // stream's output to a particular device is to set the volume index to zero. + for (VolumeInfo volumeInfo : volumes) { + if (volumeInfo.getVolumeIndex() != VolumeInfo.INDEX_NOT_SET + && volumeInfo.getMinVolumeIndex() != VolumeInfo.INDEX_NOT_SET + && volumeInfo.getMaxVolumeIndex() != VolumeInfo.INDEX_NOT_SET) { + if (volumeInfo.hasStreamType()) { + setStreamVolumeInt(volumeInfo.getStreamType(), + rescaleIndex(volumeInfo, volumeInfo.getStreamType()), + deviceOut, false /*force*/, packageName, + true /*hasModifyAudioSettings*/); + } else { + for (int streamType : volumeInfo.getVolumeGroup().getLegacyStreamTypes()) { + setStreamVolumeInt(streamType, rescaleIndex(volumeInfo, streamType), + deviceOut, false /*force*/, packageName, + true /*hasModifyAudioSettings*/); + } + } + } + } + } else { + boolean wasAbsVol = removeAudioSystemDeviceOutFromAbsVolumeDevices(deviceOut) != null; + if (wasAbsVol) { + dispatchDeviceVolumeBehavior(device, AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE); + } + } } /** @@ -6486,17 +6644,23 @@ public class AudioService extends IAudioService.Stub case AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE: volumeBehaviorChanged |= removeAudioSystemDeviceOutFromFullVolumeDevices(audioSystemDeviceOut) - | removeAudioSystemDeviceOutFromFixedVolumeDevices(audioSystemDeviceOut); + | removeAudioSystemDeviceOutFromFixedVolumeDevices(audioSystemDeviceOut) + | (removeAudioSystemDeviceOutFromAbsVolumeDevices(audioSystemDeviceOut) + != null); break; case AudioManager.DEVICE_VOLUME_BEHAVIOR_FIXED: volumeBehaviorChanged |= removeAudioSystemDeviceOutFromFullVolumeDevices(audioSystemDeviceOut) - | addAudioSystemDeviceOutToFixedVolumeDevices(audioSystemDeviceOut); + | addAudioSystemDeviceOutToFixedVolumeDevices(audioSystemDeviceOut) + | (removeAudioSystemDeviceOutFromAbsVolumeDevices(audioSystemDeviceOut) + != null); break; case AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL: volumeBehaviorChanged |= addAudioSystemDeviceOutToFullVolumeDevices(audioSystemDeviceOut) - | removeAudioSystemDeviceOutFromFixedVolumeDevices(audioSystemDeviceOut); + | removeAudioSystemDeviceOutFromFixedVolumeDevices(audioSystemDeviceOut) + | (removeAudioSystemDeviceOutFromAbsVolumeDevices(audioSystemDeviceOut) + != null); break; case AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE: case AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_MULTI_MODE: @@ -6545,17 +6709,17 @@ public class AudioService extends IAudioService.Stub // setDeviceVolumeBehavior has not been explicitly called for the device type. Deduce the // current volume behavior. - if ((mFullVolumeDevices.contains(audioSystemDeviceOut))) { + if (mFullVolumeDevices.contains(audioSystemDeviceOut)) { return AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL; } - if ((mFixedVolumeDevices.contains(audioSystemDeviceOut))) { + if (mFixedVolumeDevices.contains(audioSystemDeviceOut)) { return AudioManager.DEVICE_VOLUME_BEHAVIOR_FIXED; } - if ((mAbsVolumeMultiModeCaseDevices.contains(audioSystemDeviceOut))) { + if (mAbsVolumeMultiModeCaseDevices.contains(audioSystemDeviceOut)) { return AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_MULTI_MODE; } - if (audioSystemDeviceOut == AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP - && mAvrcpAbsVolSupported) { + if (isAbsoluteVolumeDevice(audioSystemDeviceOut) + || isA2dpAbsoluteVolumeDevice(audioSystemDeviceOut)) { return AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE; } return AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE; @@ -7320,8 +7484,7 @@ public class AudioService extends IAudioService.Stub int index; if (isFullyMuted()) { index = 0; - } else if (AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device) - && mAvrcpAbsVolSupported) { + } else if (isAbsoluteVolumeDevice(device) || isA2dpAbsoluteVolumeDevice(device)) { index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10); } else if (isFullVolumeDevice(device)) { index = (mIndexMax + 5)/10; @@ -7342,8 +7505,8 @@ public class AudioService extends IAudioService.Stub if (device != AudioSystem.DEVICE_OUT_DEFAULT) { if (isFullyMuted()) { index = 0; - } else if (AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device) - && mAvrcpAbsVolSupported) { + } else if (isAbsoluteVolumeDevice(device) + || isA2dpAbsoluteVolumeDevice(device)) { index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10); } else if (isFullVolumeDevice(device)) { index = (mIndexMax + 5)/10; @@ -7762,8 +7925,9 @@ public class AudioService extends IAudioService.Stub // Make sure volume is also maxed out on A2DP device for aliased stream // that may have a different device selected int streamDevice = getDeviceForStream(streamType); - if ((device != streamDevice) && mAvrcpAbsVolSupported - && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)) { + if ((device != streamDevice) + && (isAbsoluteVolumeDevice(device) + || isA2dpAbsoluteVolumeDevice(device))) { mStreamStates[streamType].applyDeviceVolume_syncVSS(device); } mStreamStates[streamType].applyDeviceVolume_syncVSS(streamDevice); @@ -9688,6 +9852,8 @@ public class AudioService extends IAudioService.Stub pw.print(" mUseFixedVolume="); pw.println(mUseFixedVolume); pw.print(" mFixedVolumeDevices="); pw.println(dumpDeviceTypes(mFixedVolumeDevices)); pw.print(" mFullVolumeDevices="); pw.println(dumpDeviceTypes(mFullVolumeDevices)); + pw.print(" mAbsoluteVolumeDevices.keySet()="); pw.println(dumpDeviceTypes( + mAbsoluteVolumeDeviceInfoMap.keySet())); pw.print(" mExtVolumeController="); pw.println(mExtVolumeController); pw.print(" mHdmiAudioSystemClient="); pw.println(mHdmiAudioSystemClient); pw.print(" mHdmiPlaybackClient="); pw.println(mHdmiPlaybackClient); @@ -11489,6 +11655,23 @@ public class AudioService extends IAudioService.Stub return mFullVolumeDevices.contains(deviceType); } + /** + * Returns whether the input device uses absolute volume behavior. This is distinct + * from Bluetooth A2DP absolute volume behavior ({@link #isA2dpAbsoluteVolumeDevice}). + */ + private boolean isAbsoluteVolumeDevice(int deviceType) { + return mAbsoluteVolumeDeviceInfoMap.containsKey(deviceType); + } + + /** + * Returns whether the input device is a Bluetooth A2dp device that uses absolute volume + * behavior. This is distinct from the general implementation of absolute volume behavior + * ({@link #isAbsoluteVolumeDevice}). + */ + private boolean isA2dpAbsoluteVolumeDevice(int deviceType) { + return mAvrcpAbsVolSupported && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(deviceType); + } + //==================== // Helper functions for {set,get}DeviceVolumeBehavior //==================== @@ -11581,6 +11764,24 @@ public class AudioService extends IAudioService.Stub return mFullVolumeDevices.remove(audioSystemDeviceOut); } + private AbsoluteVolumeDeviceInfo addAudioSystemDeviceOutToAbsVolumeDevices( + int audioSystemDeviceOut, AbsoluteVolumeDeviceInfo info) { + if (DEBUG_VOL) { + Log.d(TAG, "Adding DeviceType: 0x" + Integer.toHexString(audioSystemDeviceOut) + + " from mAbsoluteVolumeDeviceInfoMap"); + } + return mAbsoluteVolumeDeviceInfoMap.put(audioSystemDeviceOut, info); + } + + private AbsoluteVolumeDeviceInfo removeAudioSystemDeviceOutFromAbsVolumeDevices( + int audioSystemDeviceOut) { + if (DEBUG_VOL) { + Log.d(TAG, "Removing DeviceType: 0x" + Integer.toHexString(audioSystemDeviceOut) + + " from mAbsoluteVolumeDeviceInfoMap"); + } + return mAbsoluteVolumeDeviceInfoMap.remove(audioSystemDeviceOut); + } + //==================== // Helper functions for app ops //==================== diff --git a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java new file mode 100644 index 000000000000..ad2e7e4586ba --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java @@ -0,0 +1,290 @@ +/* + * 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 com.android.server.audio; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceInfo; +import android.media.AudioDeviceVolumeManager; +import android.media.AudioManager; +import android.media.AudioSystem; +import android.media.IAudioDeviceVolumeDispatcher; +import android.media.VolumeInfo; +import android.os.RemoteException; +import android.os.test.TestLooper; + +import androidx.test.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; + +public class AbsoluteVolumeBehaviorTest { + private static final String TAG = "AbsoluteVolumeBehaviorTest"; + + private static final AudioDeviceAttributes DEVICE_SPEAKER_OUT = new AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, ""); + + private Context mContext; + private String mPackageName; + private AudioSystemAdapter mSpyAudioSystem; + private SystemServerAdapter mSystemServer; + private SettingsAdapter mSettingsAdapter; + private TestLooper mTestLooper; + + private AudioService mAudioService; + + private IAudioDeviceVolumeDispatcher.Stub mMockDispatcher = + mock(IAudioDeviceVolumeDispatcher.Stub.class); + + @Before + public void setUp() throws Exception { + mTestLooper = new TestLooper(); + mContext = InstrumentationRegistry.getTargetContext(); + mPackageName = mContext.getOpPackageName(); + mSpyAudioSystem = spy(new NoOpAudioSystemAdapter()); + + mSystemServer = new NoOpSystemServerAdapter(); + mSettingsAdapter = new NoOpSettingsAdapter(); + mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer, + mSettingsAdapter, mTestLooper.getLooper()) { + @Override + public int getDeviceForStream(int stream) { + return AudioSystem.DEVICE_OUT_SPEAKER; + } + }; + + mTestLooper.dispatchAll(); + } + + @Test + public void registerDispatcher_setsVolumeBehaviorToAbsolute() { + List volumes = Collections.singletonList( + new VolumeInfo.Builder(AudioManager.STREAM_MUSIC).build()); + + mAudioService.registerDeviceVolumeDispatcherForAbsoluteVolume(true, + mMockDispatcher, mPackageName, DEVICE_SPEAKER_OUT, volumes, true); + mTestLooper.dispatchAll(); + + assertThat(mAudioService.getDeviceVolumeBehavior(DEVICE_SPEAKER_OUT)) + .isEqualTo(AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE); + } + + @Test + public void registerDispatcher_setsVolume() { + List volumes = Collections.singletonList( + new VolumeInfo.Builder(AudioManager.STREAM_MUSIC) + .setMinVolumeIndex(0) + .setMaxVolumeIndex(250) // Max index is 10 times that of STREAM_MUSIC + .setVolumeIndex(50) + .build()); + + mAudioService.registerDeviceVolumeDispatcherForAbsoluteVolume(true, + mMockDispatcher, mPackageName, DEVICE_SPEAKER_OUT, volumes, true); + mTestLooper.dispatchAll(); + + assertThat(mAudioService.getStreamVolume(AudioManager.STREAM_MUSIC)) + .isEqualTo(5); + } + + @Test + public void unregisterDispatcher_deviceBecomesVariableVolume_listenerNoLongerTriggered() + throws RemoteException { + + List volumes = Collections.singletonList( + new VolumeInfo.Builder(AudioManager.STREAM_MUSIC).build()); + + mAudioService.registerDeviceVolumeDispatcherForAbsoluteVolume(true, + mMockDispatcher, mPackageName, DEVICE_SPEAKER_OUT, volumes, true); + mTestLooper.dispatchAll(); + + mAudioService.registerDeviceVolumeDispatcherForAbsoluteVolume(false, + mMockDispatcher, mPackageName, DEVICE_SPEAKER_OUT, volumes, true); + mTestLooper.dispatchAll(); + + assertThat(mAudioService.getDeviceVolumeBehavior(DEVICE_SPEAKER_OUT)) + .isEqualTo(AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE); + + mAudioService.setStreamVolume(AudioManager.STREAM_MUSIC, 15, 0, mPackageName); + mTestLooper.dispatchAll(); + + verify(mMockDispatcher, never()).dispatchDeviceVolumeChanged( + eq(DEVICE_SPEAKER_OUT), any()); + } + + @Test + public void setDeviceVolumeBehavior_unregistersDispatcher() throws RemoteException { + List volumes = Collections.singletonList( + new VolumeInfo.Builder(AudioManager.STREAM_MUSIC).build()); + + mAudioService.registerDeviceVolumeDispatcherForAbsoluteVolume(true, + mMockDispatcher, mPackageName, DEVICE_SPEAKER_OUT, volumes, true); + mTestLooper.dispatchAll(); + + mAudioService.setDeviceVolumeBehavior(DEVICE_SPEAKER_OUT, + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL, mPackageName); + mTestLooper.dispatchAll(); + + mAudioService.setStreamVolume(AudioManager.STREAM_MUSIC, 15, 0, mPackageName); + mTestLooper.dispatchAll(); + + verify(mMockDispatcher, never()).dispatchDeviceVolumeChanged( + eq(DEVICE_SPEAKER_OUT), any()); + } + + @Test + public void setStreamVolume_noAbsVolFlag_dispatchesVolumeChanged() throws RemoteException { + VolumeInfo volumeInfo = new VolumeInfo.Builder(AudioManager.STREAM_MUSIC) + .setMinVolumeIndex(0) + .setMaxVolumeIndex(250) // Max index is 10 times that of STREAM_MUSIC + .setVolumeIndex(50) + .build(); + + mAudioService.registerDeviceVolumeDispatcherForAbsoluteVolume(true, + mMockDispatcher, mPackageName, DEVICE_SPEAKER_OUT, + Collections.singletonList(volumeInfo), true); + mTestLooper.dispatchAll(); + + // Set stream volume without FLAG_ABSOLUTE_VOLUME + mAudioService.setStreamVolume(AudioManager.STREAM_MUSIC, 15, 0, mPackageName); + mTestLooper.dispatchAll(); + + // Dispatched volume index is scaled to the range in the initial VolumeInfo + verify(mMockDispatcher).dispatchDeviceVolumeChanged(DEVICE_SPEAKER_OUT, + new VolumeInfo.Builder(volumeInfo).setVolumeIndex(150).build()); + } + + @Test + public void setStreamVolume_absVolFlagSet_doesNotDispatchVolumeChanged() + throws RemoteException { + VolumeInfo volumeInfo = new VolumeInfo.Builder(AudioManager.STREAM_MUSIC) + .setMinVolumeIndex(0) + .setMaxVolumeIndex(250) + .setVolumeIndex(50) + .build(); + + mAudioService.registerDeviceVolumeDispatcherForAbsoluteVolume(true, + mMockDispatcher, mPackageName, DEVICE_SPEAKER_OUT, + Collections.singletonList(volumeInfo), true); + mTestLooper.dispatchAll(); + + // Set stream volume with FLAG_ABSOLUTE_VOLUME + mAudioService.setStreamVolume(AudioManager.STREAM_MUSIC, 15, + AudioManager.FLAG_ABSOLUTE_VOLUME, mPackageName); + mTestLooper.dispatchAll(); + + verify(mMockDispatcher, never()).dispatchDeviceVolumeChanged(eq(DEVICE_SPEAKER_OUT), + any()); + } + + @Test + public void adjustStreamVolume_handlesAdjust_noAbsVolFlag_noVolChange_dispatchesVolumeAdjusted() + throws RemoteException { + VolumeInfo volumeInfo = new VolumeInfo.Builder(AudioManager.STREAM_MUSIC) + .setMinVolumeIndex(0) + .setMaxVolumeIndex(250) + .setVolumeIndex(0) + .build(); + + // Register dispatcher with handlesVolumeAdjustment = true + mAudioService.registerDeviceVolumeDispatcherForAbsoluteVolume(true, + mMockDispatcher, mPackageName, DEVICE_SPEAKER_OUT, + Collections.singletonList(volumeInfo), true); + mTestLooper.dispatchAll(); + + // Adjust stream volume without FLAG_ABSOLUTE_VOLUME + mAudioService.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_RAISE, + 0, mPackageName); + mTestLooper.dispatchAll(); + + // Stream volume does not change + assertThat(mAudioService.getStreamVolume(AudioManager.STREAM_MUSIC)).isEqualTo(0); + // Listener is notified via dispatchDeviceVolumeAdjusted + verify(mMockDispatcher, never()).dispatchDeviceVolumeChanged(eq(DEVICE_SPEAKER_OUT), any()); + verify(mMockDispatcher).dispatchDeviceVolumeAdjusted(eq(DEVICE_SPEAKER_OUT), + argThat((VolumeInfo v) -> v.getStreamType() == AudioManager.STREAM_MUSIC), + eq(AudioManager.ADJUST_RAISE), eq(AudioDeviceVolumeManager.ADJUST_MODE_NORMAL)); + } + + @Test + public void adjustStreamVolume_noHandleAdjust_noAbsVolFlag_volChanges_dispatchesVolumeChanged() + throws RemoteException { + VolumeInfo volumeInfo = new VolumeInfo.Builder(AudioManager.STREAM_MUSIC) + .setMinVolumeIndex(0) + .setMaxVolumeIndex(250) + .setVolumeIndex(0) + .build(); + + // Register dispatcher with handlesVolumeAdjustment = false + mAudioService.registerDeviceVolumeDispatcherForAbsoluteVolume(true, + mMockDispatcher, mPackageName, DEVICE_SPEAKER_OUT, + Collections.singletonList(volumeInfo), false); + mTestLooper.dispatchAll(); + + // Adjust stream volume without FLAG_ABSOLUTE_VOLUME + mAudioService.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_RAISE, + 0, mPackageName); + mTestLooper.dispatchAll(); + + // Stream volume changes + assertThat(mAudioService.getStreamVolume(AudioManager.STREAM_MUSIC)).isNotEqualTo(0); + // Listener is notified via dispatchDeviceVolumeChanged + verify(mMockDispatcher).dispatchDeviceVolumeChanged(eq(DEVICE_SPEAKER_OUT), any()); + verify(mMockDispatcher, never()).dispatchDeviceVolumeAdjusted(eq(DEVICE_SPEAKER_OUT), any(), + anyInt(), anyInt()); + } + + @Test + public void adjustStreamVolume_absVolFlagSet_streamVolumeChanges_nothingDispatched() + throws RemoteException { + VolumeInfo volumeInfo = new VolumeInfo.Builder(AudioManager.STREAM_MUSIC) + .setMinVolumeIndex(0) + .setMaxVolumeIndex(250) + .setVolumeIndex(0) + .build(); + + mAudioService.registerDeviceVolumeDispatcherForAbsoluteVolume(true, + mMockDispatcher, mPackageName, DEVICE_SPEAKER_OUT, + Collections.singletonList(volumeInfo), true); + mTestLooper.dispatchAll(); + + // Adjust stream volume with FLAG_ABSOLUTE_VOLUME set + mAudioService.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_RAISE, + AudioManager.FLAG_ABSOLUTE_VOLUME, mPackageName); + mTestLooper.dispatchAll(); + + // Stream volume changes + assertThat(mAudioService.getStreamVolume(AudioManager.STREAM_MUSIC)).isNotEqualTo(0); + // Nothing is dispatched + verify(mMockDispatcher, never()).dispatchDeviceVolumeChanged(eq(DEVICE_SPEAKER_OUT), any()); + verify(mMockDispatcher, never()).dispatchDeviceVolumeAdjusted(eq(DEVICE_SPEAKER_OUT), any(), + anyInt(), anyInt()); + } +}