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
This commit is contained in:
Yan Han 2022-01-31 19:10:16 +01:00
parent 19a4593771
commit 7d4198268d
6 changed files with 632 additions and 62 deletions

View File

@ -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<VolumeInfo> volumes) {
@NonNull List<VolumeInfo> 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<ListenerInfo>) 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<ListenerInfo> volumeListeners;
synchronized (mDeviceVolumeListenerLock) {
volumeListeners = (ArrayList<ListenerInfo>) 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<VolumeInfo> 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<VolumeInfo> 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);
}
}

View File

@ -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 */

View File

@ -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);
}

View File

@ -509,7 +509,8 @@ interface IAudioService {
void registerDeviceVolumeDispatcherForAbsoluteVolume(boolean register,
in IAudioDeviceVolumeDispatcher cb,
in String packageName,
in AudioDeviceAttributes device, in List<VolumeInfo> volumes);
in AudioDeviceAttributes device, in List<VolumeInfo> volumes,
boolean handlesvolumeAdjustment);
String getHalVersion();
}

View File

@ -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<Integer, AbsoluteVolumeDeviceInfo> mAbsoluteVolumeDeviceInfoMap = new ArrayMap<>();
/**
* Stores information about a device using absolute volume behavior.
*/
private static final class AbsoluteVolumeDeviceInfo {
private final AudioDeviceAttributes mDevice;
private final List<VolumeInfo> mVolumeInfos;
private final IAudioDeviceVolumeDispatcher mCallback;
private final boolean mHandlesVolumeAdjustment;
private AbsoluteVolumeDeviceInfo(AudioDeviceAttributes device, List<VolumeInfo> 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<VolumeInfo> volumes) {
AudioDeviceAttributes device, List<VolumeInfo> 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
//====================

View File

@ -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<VolumeInfo> 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<VolumeInfo> 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<VolumeInfo> 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<VolumeInfo> 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());
}
}