AudioDeviceVolumeManager and VolumeInfo API changes

Context service name for accessing AudioDeviceVolume manager.
Add support in VolumeInfo to know whether it carries an explicit
  mute/unmute information (vs just default value of isMuted()).

Bug: 244326361
Test: atest android.media.audio.cts.AudioDeviceVolumeManagerTest
Test: atest android.media.audio.cts.VolumeInfoTest
Change-Id: Id718fbd7e0d5795ac480d9188faad4a70fba9808
Merged-In: Id718fbd7e0d5795ac480d9188faad4a70fba9808
This commit is contained in:
Jean-Michel Trivi
2022-09-28 21:46:51 +00:00
parent 86e827d327
commit 7ec3e24816
8 changed files with 202 additions and 61 deletions

View File

@@ -115,6 +115,7 @@ import android.location.CountryDetector;
import android.location.ICountryDetector;
import android.location.ILocationManager;
import android.location.LocationManager;
import android.media.AudioDeviceVolumeManager;
import android.media.AudioManager;
import android.media.MediaFrameworkInitializer;
import android.media.MediaFrameworkPlatformInitializer;
@@ -339,6 +340,13 @@ public final class SystemServiceRegistry {
return new AudioManager(ctx);
}});
registerService(Context.AUDIO_DEVICE_VOLUME_SERVICE, AudioDeviceVolumeManager.class,
new CachedServiceFetcher<AudioDeviceVolumeManager>() {
@Override
public AudioDeviceVolumeManager createService(ContextImpl ctx) {
return new AudioDeviceVolumeManager(ctx);
}});
registerService(Context.MEDIA_ROUTER_SERVICE, MediaRouter.class,
new CachedServiceFetcher<MediaRouter>() {
@Override

View File

@@ -3846,6 +3846,7 @@ public abstract class Context {
WIFI_RTT_RANGING_SERVICE,
NSD_SERVICE,
AUDIO_SERVICE,
AUDIO_DEVICE_VOLUME_SERVICE,
AUTH_SERVICE,
FINGERPRINT_SERVICE,
//@hide: FACE_SERVICE,
@@ -4686,6 +4687,17 @@ public abstract class Context {
*/
public static final String AUDIO_SERVICE = "audio";
/**
* @hide
* Use with {@link #getSystemService(String)} to retrieve a
* {@link android.media.AudioDeviceVolumeManager} for handling management of audio device
* (e.g. speaker, USB headset) volume.
*
* @see #getSystemService(String)
* @see android.media.AudioDeviceVolumeManager
*/
public static final String AUDIO_DEVICE_VOLUME_SERVICE = "audio_device_volume";
/**
* Use with {@link #getSystemService(String)} to retrieve a {@link
* android.media.MediaTranscodingManager} for transcoding media.

View File

@@ -41,8 +41,7 @@ import java.util.concurrent.Executor;
*/
public class AudioDeviceVolumeManager {
// define when using Log.*
//private static final String TAG = "AudioDeviceVolumeManager";
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;
@@ -62,11 +61,15 @@ public class AudioDeviceVolumeManager {
private static IAudioService sService;
private final @NonNull String mPackageName;
private final @Nullable String mAttributionTag;
public AudioDeviceVolumeManager(Context context) {
/**
* @hide
* Constructor
* @param context the Context for the device volume operations
*/
public AudioDeviceVolumeManager(@NonNull Context context) {
Objects.requireNonNull(context);
mPackageName = context.getApplicationContext().getOpPackageName();
mAttributionTag = context.getApplicationContext().getAttributionTag();
}
/**
@@ -308,13 +311,36 @@ public class AudioDeviceVolumeManager {
@RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
public void setDeviceVolume(@NonNull VolumeInfo vi, @NonNull AudioDeviceAttributes ada) {
try {
getService().setDeviceVolume(vi, ada, mPackageName, mAttributionTag);
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
*/
@RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
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

View File

@@ -1212,7 +1212,13 @@ public class AudioManager {
}
}
private static boolean isPublicStreamType(int streamType) {
/**
* @hide
* Checks whether a stream type is part of the public SDK
* @param streamType
* @return true if the stream type is available in SDK
*/
public static boolean isPublicStreamType(int streamType) {
switch (streamType) {
case STREAM_VOICE_CALL:
case STREAM_SYSTEM:

View File

@@ -99,7 +99,10 @@ interface IAudioService {
in String callingPackage, in String attributionTag);
void setDeviceVolume(in VolumeInfo vi, in AudioDeviceAttributes ada,
in String callingPackage, in String attributionTag);
in String callingPackage);
VolumeInfo getDeviceVolume(in VolumeInfo vi, in AudioDeviceAttributes ada,
in String callingPackage);
oneway void handleVolumeKey(in KeyEvent event, boolean isOnTv,
String callingPackage, String caller);

View File

@@ -27,7 +27,6 @@ import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.Log;
import java.util.List;
import java.util.Objects;
/**
@@ -35,8 +34,9 @@ import java.util.Objects;
* A class to represent type of volume information.
* Can be used to represent volume associated with a stream type or {@link AudioVolumeGroup}.
* Volume index is optional when used to represent a category of volume.
* Index ranges are supported too, making the representation of volume changes agnostic to the
* range (e.g. can be used to map BT A2DP absolute volume range to internal range).
* Volume ranges are supported too, making the representation of volume changes agnostic
* regarding the range of values that are supported (e.g. can be used to map BT A2DP absolute
* volume range to internal range).
*
* Note: this class is not yet part of the SystemApi but is intended to be gradually introduced
* particularly in parts of the audio framework that suffer from code ambiguity when
@@ -46,25 +46,27 @@ public final class VolumeInfo implements Parcelable {
private static final String TAG = "VolumeInfo";
private final boolean mUsesStreamType; // false implies AudioVolumeGroup is used
private final boolean mHasMuteCommand;
private final boolean mIsMuted;
private final int mVolIndex;
private final int mMinVolIndex;
private final int mMaxVolIndex;
private final int mVolGroupId;
private final int mStreamType;
private final @Nullable AudioVolumeGroup mVolGroup;
private final @AudioManager.PublicStreamTypes int mStreamType;
private static IAudioService sService;
private static VolumeInfo sDefaultVolumeInfo;
private VolumeInfo(boolean usesStreamType, boolean isMuted, int volIndex,
int minVolIndex, int maxVolIndex,
int volGroupId, int streamType) {
private VolumeInfo(boolean usesStreamType, boolean hasMuteCommand, boolean isMuted,
int volIndex, int minVolIndex, int maxVolIndex,
AudioVolumeGroup volGroup, int streamType) {
mUsesStreamType = usesStreamType;
mHasMuteCommand = hasMuteCommand;
mIsMuted = isMuted;
mVolIndex = volIndex;
mMinVolIndex = minVolIndex;
mMaxVolIndex = maxVolIndex;
mVolGroupId = volGroupId;
mVolGroup = volGroup;
mStreamType = streamType;
}
@@ -81,8 +83,10 @@ public final class VolumeInfo implements Parcelable {
/**
* Returns the associated stream type, or will throw if {@link #hasStreamType()} returned false.
* @return a stream type value, see AudioManager.STREAM_*
* @throws IllegalStateException when called on a VolumeInfo not configured for
* stream types.
*/
public int getStreamType() {
public @AudioManager.PublicStreamTypes int getStreamType() {
if (!mUsesStreamType) {
throw new IllegalStateException("VolumeInfo doesn't use stream types");
}
@@ -101,24 +105,28 @@ public final class VolumeInfo implements Parcelable {
/**
* Returns the associated volume group, or will throw if {@link #hasVolumeGroup()} returned
* false.
* @return the volume group corresponding to this VolumeInfo, or null if an error occurred
* in the volume group management
* @return the volume group corresponding to this VolumeInfo
* @throws IllegalStateException when called on a VolumeInfo not configured for
* volume groups.
*/
public @Nullable AudioVolumeGroup getVolumeGroup() {
public @NonNull AudioVolumeGroup getVolumeGroup() {
if (mUsesStreamType) {
throw new IllegalStateException("VolumeInfo doesn't use AudioVolumeGroup");
}
List<AudioVolumeGroup> volGroups = AudioVolumeGroup.getAudioVolumeGroups();
for (AudioVolumeGroup group : volGroups) {
if (group.getId() == mVolGroupId) {
return group;
}
}
return null;
return mVolGroup;
}
/**
* Returns whether this instance is conveying a mute state.
* Return whether this instance is conveying a mute state
* @return true if the muted state was explicitly set for this instance
*/
public boolean hasMuteCommand() {
return mHasMuteCommand;
}
/**
* Returns whether this instance is conveying a mute state that was explicitly set
* by {@link Builder#setMuted(boolean)}, false otherwise
* @return true if the volume state is muted
*/
public boolean isMuted() {
@@ -185,18 +193,21 @@ public final class VolumeInfo implements Parcelable {
*/
public static final class Builder {
private boolean mUsesStreamType = true; // false implies AudioVolumeGroup is used
private int mStreamType = AudioManager.STREAM_MUSIC;
private @AudioManager.PublicStreamTypes int mStreamType = AudioManager.STREAM_MUSIC;
private boolean mHasMuteCommand = false;
private boolean mIsMuted = false;
private int mVolIndex = INDEX_NOT_SET;
private int mMinVolIndex = INDEX_NOT_SET;
private int mMaxVolIndex = INDEX_NOT_SET;
private int mVolGroupId = -Integer.MIN_VALUE;
private @Nullable AudioVolumeGroup mVolGroup;
/**
* Builder constructor for stream type-based VolumeInfo
*/
public Builder(int streamType) {
// TODO validate stream type
public Builder(@AudioManager.PublicStreamTypes int streamType) {
if (!AudioManager.isPublicStreamType(streamType)) {
throw new IllegalArgumentException("Not a valid public stream type " + streamType);
}
mUsesStreamType = true;
mStreamType = streamType;
}
@@ -208,7 +219,7 @@ public final class VolumeInfo implements Parcelable {
Objects.requireNonNull(volGroup);
mUsesStreamType = false;
mStreamType = -Integer.MIN_VALUE;
mVolGroupId = volGroup.getId();
mVolGroup = volGroup;
}
/**
@@ -219,11 +230,12 @@ public final class VolumeInfo implements Parcelable {
Objects.requireNonNull(info);
mUsesStreamType = info.mUsesStreamType;
mStreamType = info.mStreamType;
mHasMuteCommand = info.mHasMuteCommand;
mIsMuted = info.mIsMuted;
mVolIndex = info.mVolIndex;
mMinVolIndex = info.mMinVolIndex;
mMaxVolIndex = info.mMaxVolIndex;
mVolGroupId = info.mVolGroupId;
mVolGroup = info.mVolGroup;
}
/**
@@ -232,6 +244,7 @@ public final class VolumeInfo implements Parcelable {
* @return the same builder instance
*/
public @NonNull Builder setMuted(boolean isMuted) {
mHasMuteCommand = true;
mIsMuted = isMuted;
return this;
}
@@ -241,7 +254,6 @@ public final class VolumeInfo implements Parcelable {
* @param volIndex a 0 or greater value, or {@link #INDEX_NOT_SET} if unknown
* @return the same builder instance
*/
// TODO should we allow muted true + volume index set? (useful when toggling mute on/off?)
public @NonNull Builder setVolumeIndex(int volIndex) {
if (volIndex != INDEX_NOT_SET && volIndex < 0) {
throw new IllegalArgumentException("Volume index cannot be negative");
@@ -296,9 +308,9 @@ public final class VolumeInfo implements Parcelable {
throw new IllegalArgumentException("Min volume index:" + mMinVolIndex
+ " greater than max index:" + mMaxVolIndex);
}
return new VolumeInfo(mUsesStreamType, mIsMuted,
return new VolumeInfo(mUsesStreamType, mHasMuteCommand, mIsMuted,
mVolIndex, mMinVolIndex, mMaxVolIndex,
mVolGroupId, mStreamType);
mVolGroup, mStreamType);
}
}
@@ -306,8 +318,8 @@ public final class VolumeInfo implements Parcelable {
// Parcelable
@Override
public int hashCode() {
return Objects.hash(mUsesStreamType, mStreamType, mIsMuted,
mVolIndex, mMinVolIndex, mMaxVolIndex, mVolGroupId);
return Objects.hash(mUsesStreamType, mHasMuteCommand, mStreamType, mIsMuted,
mVolIndex, mMinVolIndex, mMaxVolIndex, mVolGroup);
}
@Override
@@ -318,19 +330,20 @@ public final class VolumeInfo implements Parcelable {
VolumeInfo that = (VolumeInfo) o;
return ((mUsesStreamType == that.mUsesStreamType)
&& (mStreamType == that.mStreamType)
&& (mIsMuted == that.mIsMuted)
&& (mVolIndex == that.mVolIndex)
&& (mMinVolIndex == that.mMinVolIndex)
&& (mMaxVolIndex == that.mMaxVolIndex)
&& (mVolGroupId == that.mVolGroupId));
&& (mHasMuteCommand == that.mHasMuteCommand)
&& (mIsMuted == that.mIsMuted)
&& (mVolIndex == that.mVolIndex)
&& (mMinVolIndex == that.mMinVolIndex)
&& (mMaxVolIndex == that.mMaxVolIndex)
&& Objects.equals(mVolGroup, that.mVolGroup));
}
@Override
public String toString() {
return new String("VolumeInfo:"
+ (mUsesStreamType ? (" streamType:" + mStreamType)
: (" volGroupId" + mVolGroupId))
+ " muted:" + mIsMuted
: (" volGroup:" + mVolGroup))
+ (mHasMuteCommand ? (" muted:" + mIsMuted) : ("[no mute cmd]"))
+ ((mVolIndex != INDEX_NOT_SET) ? (" volIndex:" + mVolIndex) : "")
+ ((mMinVolIndex != INDEX_NOT_SET) ? (" min:" + mMinVolIndex) : "")
+ ((mMaxVolIndex != INDEX_NOT_SET) ? (" max:" + mMaxVolIndex) : ""));
@@ -345,21 +358,29 @@ public final class VolumeInfo implements Parcelable {
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeBoolean(mUsesStreamType);
dest.writeInt(mStreamType);
dest.writeBoolean(mHasMuteCommand);
dest.writeBoolean(mIsMuted);
dest.writeInt(mVolIndex);
dest.writeInt(mMinVolIndex);
dest.writeInt(mMaxVolIndex);
dest.writeInt(mVolGroupId);
if (!mUsesStreamType) {
mVolGroup.writeToParcel(dest, 0 /*ignored*/);
}
}
private VolumeInfo(@NonNull Parcel in) {
mUsesStreamType = in.readBoolean();
mStreamType = in.readInt();
mHasMuteCommand = in.readBoolean();
mIsMuted = in.readBoolean();
mVolIndex = in.readInt();
mMinVolIndex = in.readInt();
mMaxVolIndex = in.readInt();
mVolGroupId = in.readInt();
if (!mUsesStreamType) {
mVolGroup = AudioVolumeGroup.CREATOR.createFromParcel(in);
} else {
mVolGroup = null;
}
}
public static final @NonNull Parcelable.Creator<VolumeInfo> CREATOR =

View File

@@ -3599,6 +3599,18 @@ public class AudioService extends IAudioService.Stub
}
}
// TODO enforce MODIFY_AUDIO_SYSTEM_SETTINGS when defined
private void enforceModifyAudioRoutingOrSystemSettingsPermission() {
if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
!= PackageManager.PERMISSION_GRANTED
/*&& mContext.checkCallingOrSelfPermission(
android.Manifest.permission.MODIFY_AUDIO_SYSTEM_SETTINGS)
!= PackageManager.PERMISSION_DENIED*/) {
throw new SecurityException(
"Missing MODIFY_AUDIO_ROUTING or MODIFY_AUDIO_SYSTEM_SETTINGS permission");
}
}
private void enforceAccessUltrasoundPermission() {
if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.ACCESS_ULTRASOUND)
!= PackageManager.PERMISSION_GRANTED) {
@@ -3703,10 +3715,12 @@ public class AudioService extends IAudioService.Stub
}
/** @see AudioDeviceVolumeManager#setDeviceVolume(VolumeInfo, AudioDeviceAttributes)
* Part of service interface, check permissions and parameters here */
* Part of service interface, check permissions and parameters here
* Note calling package is for logging purposes only, not to be trusted
*/
public void setDeviceVolume(@NonNull VolumeInfo vi, @NonNull AudioDeviceAttributes ada,
@NonNull String callingPackage, @Nullable String attributionTag) {
enforceModifyAudioRoutingPermission();
@NonNull String callingPackage) {
enforceModifyAudioRoutingOrSystemSettingsPermission();
Objects.requireNonNull(vi);
Objects.requireNonNull(ada);
Objects.requireNonNull(callingPackage);
@@ -3719,8 +3733,20 @@ public class AudioService extends IAudioService.Stub
return;
}
int index = vi.getVolumeIndex();
if (index == VolumeInfo.INDEX_NOT_SET) {
throw new IllegalArgumentException("changing device volume requires a volume index");
if (index == VolumeInfo.INDEX_NOT_SET && !vi.hasMuteCommand()) {
throw new IllegalArgumentException(
"changing device volume requires a volume index or mute command");
}
// TODO handle unmuting of current audio device
// if a stream is not muted but the VolumeInfo is for muting, set the volume index
// for the device to min volume
if (vi.hasMuteCommand() && vi.isMuted() && !isStreamMute(vi.getStreamType())) {
setStreamVolumeWithAttributionInt(vi.getStreamType(),
mStreamStates[vi.getStreamType()].getMinIndex(),
/*flags*/ 0,
ada, callingPackage, null);
return;
}
if (vi.getMinVolumeIndex() == VolumeInfo.INDEX_NOT_SET
@@ -3742,7 +3768,7 @@ public class AudioService extends IAudioService.Stub
}
}
setStreamVolumeWithAttributionInt(vi.getStreamType(), index, /*flags*/ 0,
ada, callingPackage, attributionTag);
ada, callingPackage, null);
}
/** Retain API for unsupported app usage */
@@ -4648,6 +4674,40 @@ public class AudioService extends IAudioService.Stub
}
}
/**
* @see AudioDeviceVolumeManager#getDeviceVolume(VolumeInfo, AudioDeviceAttributes)
*/
public @NonNull VolumeInfo getDeviceVolume(@NonNull VolumeInfo vi,
@NonNull AudioDeviceAttributes ada, @NonNull String callingPackage) {
enforceModifyAudioRoutingOrSystemSettingsPermission();
Objects.requireNonNull(vi);
Objects.requireNonNull(ada);
Objects.requireNonNull(callingPackage);
if (!vi.hasStreamType()) {
Log.e(TAG, "Unsupported non-stream type based VolumeInfo", new Exception());
return getDefaultVolumeInfo();
}
int streamType = vi.getStreamType();
final VolumeInfo.Builder vib = new VolumeInfo.Builder(vi);
vib.setMinVolumeIndex((mStreamStates[streamType].mIndexMin + 5) / 10);
vib.setMaxVolumeIndex((mStreamStates[streamType].mIndexMax + 5) / 10);
synchronized (VolumeStreamState.class) {
final int index;
if (isFixedVolumeDevice(ada.getInternalType())) {
index = (mStreamStates[streamType].mIndexMax + 5) / 10;
} else {
index = (mStreamStates[streamType].getIndex(ada.getInternalType()) + 5) / 10;
}
vib.setVolumeIndex(index);
// only set as a mute command if stream muted
if (mStreamStates[streamType].mIsMuted) {
vib.setMuted(true);
}
return vib.build();
}
}
/** @see AudioManager#getStreamMaxVolume(int) */
public int getStreamMaxVolume(int streamType) {
ensureValidStreamType(streamType);
@@ -4686,7 +4746,6 @@ public class AudioService extends IAudioService.Stub
sDefaultVolumeInfo = new VolumeInfo.Builder(AudioSystem.STREAM_MUSIC)
.setMinVolumeIndex(getStreamMinVolume(AudioSystem.STREAM_MUSIC))
.setMaxVolumeIndex(getStreamMaxVolume(AudioSystem.STREAM_MUSIC))
.setMuted(false)
.build();
}
return sDefaultVolumeInfo;

View File

@@ -27,18 +27,18 @@ import android.media.AudioManager;
import android.media.AudioSystem;
import android.media.VolumeInfo;
import android.os.test.TestLooper;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import junit.framework.Assert;
import org.junit.Before;
import org.junit.Test;
public class AudioDeviceVolumeManagerTest {
private static final String TAG = "AudioDeviceVolumeManagerTest";
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;
@@ -84,14 +84,20 @@ public class AudioDeviceVolumeManagerTest {
final AudioDeviceAttributes usbDevice = new AudioDeviceAttributes(
/*native type*/ AudioSystem.DEVICE_OUT_USB_DEVICE, /*address*/ "bla");
mAudioService.setDeviceVolume(volMin, usbDevice, mPackageName, TAG);
mAudioService.setDeviceVolume(volMin, usbDevice, mPackageName);
mTestLooper.dispatchAll();
verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
AudioManager.STREAM_MUSIC, minIndex, AudioSystem.DEVICE_OUT_USB_DEVICE);
mAudioService.setDeviceVolume(volMid, usbDevice, mPackageName, TAG);
mAudioService.setDeviceVolume(volMid, usbDevice, mPackageName);
mTestLooper.dispatchAll();
verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
AudioManager.STREAM_MUSIC, midIndex, AudioSystem.DEVICE_OUT_USB_DEVICE);
final VolumeInfo vi = mAudioService.getDeviceVolume(volMin, usbDevice, mPackageName);
Assert.assertEquals("getDeviceVolume doesn't return expected value in " + vi
+ " after setting " + volMid,
(volMid.getMaxVolumeIndex() - volMid.getMinVolumeIndex()) / 2,
(vi.getMaxVolumeIndex() - vi.getMinVolumeIndex()) / 2);
}
}