diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 8d96bb7ab2749..2c139b72eab35 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -1337,12 +1337,8 @@ public class AudioManager { public void setVolumeIndexForAttributes(@NonNull AudioAttributes attr, int index, int flags) { Preconditions.checkNotNull(attr, "attr must not be null"); final IAudioService service = getService(); - try { - service.setVolumeIndexForAttributes(attr, index, flags, - getContext().getOpPackageName(), getContext().getAttributionTag()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + int groupId = getVolumeGroupIdForAttributes(attr); + setVolumeGroupVolumeIndex(groupId, index, flags); } /** @@ -1361,11 +1357,8 @@ public class AudioManager { public int getVolumeIndexForAttributes(@NonNull AudioAttributes attr) { Preconditions.checkNotNull(attr, "attr must not be null"); final IAudioService service = getService(); - try { - return service.getVolumeIndexForAttributes(attr); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + int groupId = getVolumeGroupIdForAttributes(attr); + return getVolumeGroupVolumeIndex(groupId); } /** @@ -1382,11 +1375,8 @@ public class AudioManager { public int getMaxVolumeIndexForAttributes(@NonNull AudioAttributes attr) { Preconditions.checkNotNull(attr, "attr must not be null"); final IAudioService service = getService(); - try { - return service.getMaxVolumeIndexForAttributes(attr); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + int groupId = getVolumeGroupIdForAttributes(attr); + return getVolumeGroupMaxVolumeIndex(groupId); } /** @@ -1402,9 +1392,169 @@ public class AudioManager { @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getMinVolumeIndexForAttributes(@NonNull AudioAttributes attr) { Preconditions.checkNotNull(attr, "attr must not be null"); + final IAudioService service = getService(); + int groupId = getVolumeGroupIdForAttributes(attr); + return getVolumeGroupMinVolumeIndex(groupId); + } + + /** + * Returns the volume group id associated to the given {@link AudioAttributes}. + * + * @param attributes The {@link AudioAttributes} to consider. + * @return {@link android.media.audiopolicy.AudioVolumeGroup} id supporting the given + * {@link AudioAttributes} if found, + * {@code android.media.audiopolicy.AudioVolumeGroup.DEFAULT_VOLUME_GROUP} otherwise. + * @hide + */ + public int getVolumeGroupIdForAttributes(@NonNull AudioAttributes attributes) { + Preconditions.checkNotNull(attributes, "Audio Attributes must not be null"); + return AudioProductStrategy.getVolumeGroupIdForAudioAttributes(attributes, + /* fallbackOnDefault= */ false); + } + + /** + * Sets the volume index for a particular group associated to given id. + *

Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group + * id supporting the given {@link AudioAttributes}. + * + * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider. + * @param index The volume index to set. See + * {@link #getVolumeGroupMaxVolumeIndex(id)} for the largest valid value + * {@link #getVolumeGroupMinVolumeIndex(id)} for the lowest valid value. + * @param flags One or more flags. + * @hide + */ + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public void setVolumeGroupVolumeIndex(int groupId, int index, int flags) { final IAudioService service = getService(); try { - return service.getMinVolumeIndexForAttributes(attr); + service.setVolumeGroupVolumeIndex(groupId, index, flags, + getContext().getOpPackageName(), getContext().getAttributionTag()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns the current volume index for a particular group associated to given id. + *

Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group + * id supporting the given {@link AudioAttributes}. + * + * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider. + * @return The current volume index for the stream. + * @hide + */ + @IntRange(from = 0) + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public int getVolumeGroupVolumeIndex(int groupId) { + final IAudioService service = getService(); + try { + return service.getVolumeGroupVolumeIndex(groupId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns the maximum volume index for a particular group associated to given id. + *

Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group + * id supporting the given {@link AudioAttributes}. + * + * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider. + * @return The maximum valid volume index for the {@link AudioAttributes}. + * @hide + */ + @IntRange(from = 0) + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public int getVolumeGroupMaxVolumeIndex(int groupId) { + final IAudioService service = getService(); + try { + return service.getVolumeGroupMaxVolumeIndex(groupId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns the minimum volume index for a particular group associated to given id. + *

Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group + * id supporting the given {@link AudioAttributes}. + * + * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider. + * @return The minimum valid volume index for the {@link AudioAttributes}. + * @hide + */ + @IntRange(from = 0) + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public int getVolumeGroupMinVolumeIndex(int groupId) { + final IAudioService service = getService(); + try { + return service.getVolumeGroupMinVolumeIndex(groupId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Adjusts the volume of a particular group associated to given id by one step in a direction. + *

If the volume group is associated to a stream type, it fallbacks on + * {@link AudioManager#adjustStreamVolume()} for compatibility reason. + *

Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group + * id supporting the given {@link AudioAttributes}. + * + * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider. + * @param direction The direction to adjust the volume. One of + * {@link #ADJUST_LOWER}, {@link #ADJUST_RAISE}, or + * {@link #ADJUST_SAME}. + * @param flags One or more flags. + * @throws SecurityException if the adjustment triggers a Do Not Disturb change and the caller + * is not granted notification policy access. + * @hide + */ + public void adjustVolumeGroupVolume(int groupId, int direction, int flags) { + IAudioService service = getService(); + try { + service.adjustVolumeGroupVolume(groupId, direction, flags, + getContext().getOpPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get last audible volume of the group associated to given id before it was muted. + *

Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group + * id supporting the given {@link AudioAttributes}. + * + * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider. + * @return current volume if not muted, volume before muted otherwise. + * @hide + */ + @RequiresPermission("android.permission.QUERY_AUDIO_STATE") + @IntRange(from = 0) + public int getLastAudibleVolumeGroupVolume(int groupId) { + IAudioService service = getService(); + try { + return service.getLastAudibleVolumeGroupVolume(groupId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns the current mute state for a particular volume group associated to the given id. + *

Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group + * id supporting the given {@link AudioAttributes}. + * + * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider. + * @return The mute state for the given {@link android.media.audiopolicy.AudioVolumeGroup} id. + * @see #adjustAttributesVolume(AudioAttributes, int, int) + * @hide + */ + public boolean isVolumeGroupMuted(int groupId) { + IAudioService service = getService(); + try { + return service.isVolumeGroupMuted(groupId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index ad933e02c0d52..88a0321d34da6 100755 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -126,14 +126,20 @@ interface IAudioService { List getAudioVolumeGroups(); - void setVolumeIndexForAttributes(in AudioAttributes aa, int index, int flags, - String callingPackage, in String attributionTag); + void setVolumeGroupVolumeIndex(int groupId, int index, int flags, String callingPackage, + in String attributionTag); - int getVolumeIndexForAttributes(in AudioAttributes aa); + int getVolumeGroupVolumeIndex(int groupId); - int getMaxVolumeIndexForAttributes(in AudioAttributes aa); + int getVolumeGroupMaxVolumeIndex(int groupId); - int getMinVolumeIndexForAttributes(in AudioAttributes aa); + int getVolumeGroupMinVolumeIndex(int groupId); + + int getLastAudibleVolumeGroupVolume(int groupId); + + boolean isVolumeGroupMuted(int groupId); + + void adjustVolumeGroupVolume(int groupId, int direction, int flags, String callingPackage); int getLastAudibleStreamVolume(int streamType); diff --git a/media/java/android/media/audiopolicy/AudioProductStrategy.java b/media/java/android/media/audiopolicy/AudioProductStrategy.java index 31d596765bccb..a792501a07bad 100644 --- a/media/java/android/media/audiopolicy/AudioProductStrategy.java +++ b/media/java/android/media/audiopolicy/AudioProductStrategy.java @@ -29,10 +29,10 @@ import android.text.TextUtils; import android.util.Log; import com.android.internal.annotations.GuardedBy; -import com.android.internal.util.Preconditions; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * @hide @@ -142,7 +142,7 @@ public final class AudioProductStrategy implements Parcelable { */ public static int getLegacyStreamTypeForStrategyWithAudioAttributes( @NonNull AudioAttributes audioAttributes) { - Preconditions.checkNotNull(audioAttributes, "AudioAttributes must not be null"); + Objects.requireNonNull(audioAttributes, "AudioAttributes must not be null"); for (final AudioProductStrategy productStrategy : AudioProductStrategy.getAudioProductStrategies()) { if (productStrategy.supportsAudioAttributes(audioAttributes)) { @@ -162,6 +162,30 @@ public final class AudioProductStrategy implements Parcelable { return AudioSystem.STREAM_MUSIC; } + /** + * @hide + * @param attributes the {@link AudioAttributes} to identify VolumeGroupId with + * @param fallbackOnDefault if set, allows to fallback on the default group (e.g. the group + * associated to {@link AudioManager#STREAM_MUSIC}). + * @return volume group id associated with the given {@link AudioAttributes} if found, + * default volume group id if fallbackOnDefault is set + *

By convention, the product strategy with default attributes will be associated to the + * default volume group (e.g. associated to {@link AudioManager#STREAM_MUSIC}) + * or {@link AudioVolumeGroup#DEFAULT_VOLUME_GROUP} if not found. + */ + public static int getVolumeGroupIdForAudioAttributes( + @NonNull AudioAttributes attributes, boolean fallbackOnDefault) { + Objects.requireNonNull(attributes, "attributes must not be null"); + int volumeGroupId = getVolumeGroupIdForAudioAttributesInt(attributes); + if (volumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP) { + return volumeGroupId; + } + if (fallbackOnDefault) { + return getVolumeGroupIdForAudioAttributesInt(getDefaultAttributes()); + } + return AudioVolumeGroup.DEFAULT_VOLUME_GROUP; + } + private static List initializeAudioProductStrategies() { ArrayList apsList = new ArrayList(); int status = native_list_audio_product_strategies(apsList); @@ -192,8 +216,8 @@ public final class AudioProductStrategy implements Parcelable { */ private AudioProductStrategy(@NonNull String name, int id, @NonNull AudioAttributesGroup[] aag) { - Preconditions.checkNotNull(name, "name must not be null"); - Preconditions.checkNotNull(aag, "AudioAttributesGroups must not be null"); + Objects.requireNonNull(name, "name must not be null"); + Objects.requireNonNull(aag, "AudioAttributesGroups must not be null"); mName = name; mId = id; mAudioAttributesGroups = aag; @@ -209,6 +233,15 @@ public final class AudioProductStrategy implements Parcelable { return mId; } + /** + * @hide + * @return the product strategy ID (which is the generalisation of Car Audio Usage / legacy + * routing_strategy linked to {@link AudioAttributes#getUsage()}). + */ + @NonNull public String getName() { + return mName; + } + /** * @hide * @return first {@link AudioAttributes} associated to this product strategy. @@ -243,7 +276,7 @@ public final class AudioProductStrategy implements Parcelable { */ @TestApi public int getLegacyStreamTypeForAudioAttributes(@NonNull AudioAttributes aa) { - Preconditions.checkNotNull(aa, "AudioAttributes must not be null"); + Objects.requireNonNull(aa, "AudioAttributes must not be null"); for (final AudioAttributesGroup aag : mAudioAttributesGroups) { if (aag.supportsAttributes(aa)) { return aag.getStreamType(); @@ -260,7 +293,7 @@ public final class AudioProductStrategy implements Parcelable { */ @SystemApi public boolean supportsAudioAttributes(@NonNull AudioAttributes aa) { - Preconditions.checkNotNull(aa, "AudioAttributes must not be null"); + Objects.requireNonNull(aa, "AudioAttributes must not be null"); for (final AudioAttributesGroup aag : mAudioAttributesGroups) { if (aag.supportsAttributes(aa)) { return true; @@ -293,7 +326,7 @@ public final class AudioProductStrategy implements Parcelable { */ @TestApi public int getVolumeGroupIdForAudioAttributes(@NonNull AudioAttributes aa) { - Preconditions.checkNotNull(aa, "AudioAttributes must not be null"); + Objects.requireNonNull(aa, "AudioAttributes must not be null"); for (final AudioAttributesGroup aag : mAudioAttributesGroups) { if (aag.supportsAttributes(aa)) { return aag.getVolumeGroupId(); @@ -302,6 +335,17 @@ public final class AudioProductStrategy implements Parcelable { return AudioVolumeGroup.DEFAULT_VOLUME_GROUP; } + private static int getVolumeGroupIdForAudioAttributesInt(@NonNull AudioAttributes attributes) { + Objects.requireNonNull(attributes, "attributes must not be null"); + for (AudioProductStrategy productStrategy : getAudioProductStrategies()) { + int volumeGroupId = productStrategy.getVolumeGroupIdForAudioAttributes(attributes); + if (volumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP) { + return volumeGroupId; + } + } + return AudioVolumeGroup.DEFAULT_VOLUME_GROUP; + } + @Override public int describeContents() { return 0; @@ -377,8 +421,8 @@ public final class AudioProductStrategy implements Parcelable { */ private static boolean attributesMatches(@NonNull AudioAttributes refAttr, @NonNull AudioAttributes attr) { - Preconditions.checkNotNull(refAttr, "refAttr must not be null"); - Preconditions.checkNotNull(attr, "attr must not be null"); + Objects.requireNonNull(refAttr, "reference AudioAttributes must not be null"); + Objects.requireNonNull(attr, "requester's AudioAttributes must not be null"); String refFormattedTags = TextUtils.join(";", refAttr.getTags()); String cliFormattedTags = TextUtils.join(";", attr.getTags()); if (refAttr.equals(DEFAULT_ATTRIBUTES)) { diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index d6ecbc3828b03..6084ccaeb20de 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -1253,6 +1253,20 @@ public class AudioService extends IAudioService.Stub 0 /* arg1 */, 0 /* arg2 */, null /* obj */, 0 /* delay */); } + private void initVolumeStreamStates() { + int numStreamTypes = AudioSystem.getNumStreamTypes(); + synchronized (VolumeStreamState.class) { + for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) { + VolumeStreamState streamState = mStreamStates[streamType]; + int groupId = getVolumeGroupForStreamType(streamType); + if (groupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP + && sVolumeGroupStates.indexOfKey(groupId) >= 0) { + streamState.setVolumeGroupState(sVolumeGroupStates.get(groupId)); + } + } + } + } + /** * Separating notification volume from ring is NOT of aliasing the corresponding streams * @param properties @@ -1282,6 +1296,8 @@ public class AudioService extends IAudioService.Stub // mSafeUsbMediaVolumeIndex must be initialized after createStreamStates() because it // relies on audio policy having correct ranges for volume indexes. mSafeUsbMediaVolumeIndex = getSafeUsbMediaVolumeIndex(); + // Link VGS on VSS + initVolumeStreamStates(); // Call setRingerModeInt() to apply correct mute // state on streams affected by ringer mode. @@ -3340,15 +3356,7 @@ public class AudioService extends IAudioService.Stub } else { state = direction == AudioManager.ADJUST_MUTE; } - for (int stream = 0; stream < mStreamStates.length; stream++) { - if (streamTypeAlias == mStreamVolumeAlias[stream]) { - if (!(readCameraSoundForced() - && (mStreamStates[stream].getStreamType() - == AudioSystem.STREAM_SYSTEM_ENFORCED))) { - mStreamStates[stream].mute(state); - } - } - } + muteAliasStreams(streamTypeAlias, state); } else if ((direction == AudioManager.ADJUST_RAISE) && !checkSafeMediaVolume(streamTypeAlias, aliasIndex + step, device)) { Log.e(TAG, "adjustStreamVolume() safe volume index = " + oldIndex); @@ -3363,7 +3371,7 @@ public class AudioService extends IAudioService.Stub // Unmute the stream if it was previously muted if (direction == AudioManager.ADJUST_RAISE) { // unmute immediately for volume up - streamState.mute(false); + muteAliasStreams(streamTypeAlias, false); } else if (direction == AudioManager.ADJUST_LOWER) { if (mIsSingleVolume) { sendMsg(mAudioHandler, MSG_UNMUTE_STREAM, SENDMSG_QUEUE, @@ -3489,6 +3497,42 @@ public class AudioService extends IAudioService.Stub sendVolumeUpdate(streamType, oldIndex, newIndex, flags, device); } + /** + * Loops on aliasted stream, update the mute cache attribute of each + * {@see AudioService#VolumeStreamState}, and then apply the change. + * It prevents to unnecessary {@see AudioSystem#setStreamVolume} done for each stream + * and aliases before mute change changed and after. + */ + private void muteAliasStreams(int streamAlias, boolean state) { + synchronized (VolumeStreamState.class) { + List streamsToMute = new ArrayList<>(); + for (int stream = 0; stream < mStreamStates.length; stream++) { + if (streamAlias == mStreamVolumeAlias[stream]) { + if (!(readCameraSoundForced() + && (mStreamStates[stream].getStreamType() + == AudioSystem.STREAM_SYSTEM_ENFORCED))) { + boolean changed = mStreamStates[stream].mute(state, /* apply= */ false); + if (changed) { + streamsToMute.add(stream); + } + } + } + } + streamsToMute.forEach(streamToMute -> { + mStreamStates[streamToMute].doMute(); + broadcastMuteSetting(streamToMute, state); + }); + } + } + + private void broadcastMuteSetting(int streamType, boolean isMuted) { + // Stream mute changed, fire the intent. + Intent intent = new Intent(AudioManager.STREAM_MUTE_CHANGED_ACTION); + intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, streamType); + intent.putExtra(AudioManager.EXTRA_STREAM_VOLUME_MUTED, isMuted); + sendBroadcastToAll(intent); + } + // Called after a delay when volume down is pressed while muted private void onUnmuteStream(int stream, int flags) { boolean wasMuted; @@ -3608,7 +3652,8 @@ public class AudioService extends IAudioService.Stub // except for BT SCO stream where only explicit mute is allowed to comply to BT requirements if ((streamType != AudioSystem.STREAM_BLUETOOTH_SCO) && (getDeviceForStream(stream) == device)) { - mStreamStates[stream].mute(index == 0); + // As adjustStreamVolume with muteAdjust flags mute/unmutes stream and aliased streams. + muteAliasStreams(stream, index == 0); } } @@ -3664,29 +3709,27 @@ public class AudioService extends IAudioService.Stub } - /** @see AudioManager#setVolumeIndexForAttributes(attr, int, int) */ - public void setVolumeIndexForAttributes(@NonNull AudioAttributes attr, int index, int flags, + /** @see AudioManager#setVolumeGroupVolumeIndex(int, int, int) */ + public void setVolumeGroupVolumeIndex(int groupId, int index, int flags, String callingPackage, String attributionTag) { enforceModifyAudioRoutingPermission(); - Objects.requireNonNull(attr, "attr must not be null"); - final int volumeGroup = getVolumeGroupIdForAttributes(attr); - if (sVolumeGroupStates.indexOfKey(volumeGroup) < 0) { - Log.e(TAG, ": no volume group found for attributes " + attr.toString()); + if (sVolumeGroupStates.indexOfKey(groupId) < 0) { + Log.e(TAG, ": no volume group found for id " + groupId); return; } - final VolumeGroupState vgs = sVolumeGroupStates.get(volumeGroup); + VolumeGroupState vgs = sVolumeGroupStates.get(groupId); - sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_SET_GROUP_VOL, attr, vgs.name(), - index/*val1*/, flags/*val2*/, callingPackage)); + sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_SET_GROUP_VOL, vgs.name(), + index, flags, callingPackage + ", user " + getCurrentUserId())); vgs.setVolumeIndex(index, flags); // For legacy reason, propagate to all streams associated to this volume group - for (final int groupedStream : vgs.getLegacyStreamTypes()) { + for (int groupedStream : vgs.getLegacyStreamTypes()) { try { ensureValidStreamType(groupedStream); } catch (IllegalArgumentException e) { - Log.d(TAG, "volume group " + volumeGroup + " has internal streams (" + groupedStream + Log.d(TAG, "volume group " + groupId + " has internal streams (" + groupedStream + "), do not change associated stream volume"); continue; } @@ -3698,7 +3741,7 @@ public class AudioService extends IAudioService.Stub @Nullable private AudioVolumeGroup getAudioVolumeGroupById(int volumeGroupId) { - for (final AudioVolumeGroup avg : AudioVolumeGroup.getAudioVolumeGroups()) { + for (AudioVolumeGroup avg : AudioVolumeGroup.getAudioVolumeGroups()) { if (avg.getId() == volumeGroupId) { return avg; } @@ -3708,30 +3751,42 @@ public class AudioService extends IAudioService.Stub return null; } - /** @see AudioManager#getVolumeIndexForAttributes(attr) */ - public int getVolumeIndexForAttributes(@NonNull AudioAttributes attr) { + /** @see AudioManager#getVolumeGroupVolumeIndex(int) */ + public int getVolumeGroupVolumeIndex(int groupId) { enforceModifyAudioRoutingPermission(); - Objects.requireNonNull(attr, "attr must not be null"); - final int volumeGroup = getVolumeGroupIdForAttributes(attr); - if (sVolumeGroupStates.indexOfKey(volumeGroup) < 0) { - throw new IllegalArgumentException("No volume group for attributes " + attr); + synchronized (VolumeStreamState.class) { + if (sVolumeGroupStates.indexOfKey(groupId) < 0) { + throw new IllegalArgumentException("No volume group for id " + groupId); + } + VolumeGroupState vgs = sVolumeGroupStates.get(groupId); + // Return 0 when muted, not min index since for e.g. Voice Call, it has a non zero + // min but it mutable on permission condition. + return vgs.isMuted() ? 0 : vgs.getVolumeIndex(); } - final VolumeGroupState vgs = sVolumeGroupStates.get(volumeGroup); - return vgs.getVolumeIndex(); } - /** @see AudioManager#getMaxVolumeIndexForAttributes(attr) */ - public int getMaxVolumeIndexForAttributes(@NonNull AudioAttributes attr) { + /** @see AudioManager#getVolumeGroupMaxVolumeIndex(int) */ + public int getVolumeGroupMaxVolumeIndex(int groupId) { enforceModifyAudioRoutingPermission(); - Objects.requireNonNull(attr, "attr must not be null"); - return AudioSystem.getMaxVolumeIndexForAttributes(attr); + synchronized (VolumeStreamState.class) { + if (sVolumeGroupStates.indexOfKey(groupId) < 0) { + throw new IllegalArgumentException("No volume group for id " + groupId); + } + VolumeGroupState vgs = sVolumeGroupStates.get(groupId); + return vgs.getMaxIndex(); + } } - /** @see AudioManager#getMinVolumeIndexForAttributes(attr) */ - public int getMinVolumeIndexForAttributes(@NonNull AudioAttributes attr) { + /** @see AudioManager#getVolumeGroupMinVolumeIndex(int) */ + public int getVolumeGroupMinVolumeIndex(int groupId) { enforceModifyAudioRoutingPermission(); - Objects.requireNonNull(attr, "attr must not be null"); - return AudioSystem.getMinVolumeIndexForAttributes(attr); + synchronized (VolumeStreamState.class) { + if (sVolumeGroupStates.indexOfKey(groupId) < 0) { + throw new IllegalArgumentException("No volume group for id " + groupId); + } + VolumeGroupState vgs = sVolumeGroupStates.get(groupId); + return vgs.getMinIndex(); + } } /** @see AudioDeviceVolumeManager#setDeviceVolume(VolumeInfo, AudioDeviceAttributes) @@ -3808,6 +3863,70 @@ public class AudioService extends IAudioService.Stub callingPackage, /*attributionTag*/ null); } + /** @see AudioManager#adjustVolumeGroupVolume(int, int, int) */ + public void adjustVolumeGroupVolume(int groupId, int direction, int flags, + String callingPackage) { + ensureValidDirection(direction); + if (sVolumeGroupStates.indexOfKey(groupId) < 0) { + Log.e(TAG, ": no volume group found for id " + groupId); + return; + } + VolumeGroupState vgs = sVolumeGroupStates.get(groupId); + // For compatibility reason, use stream API if group linked to a valid stream + boolean fallbackOnStream = false; + for (int stream : vgs.getLegacyStreamTypes()) { + try { + ensureValidStreamType(stream); + } catch (IllegalArgumentException e) { + Log.d(TAG, "volume group " + groupId + " has internal streams (" + stream + + "), do not change associated stream volume"); + continue; + } + // Note: Group and Stream does not share same convention, 0 is mute for stream, + // min index is acting as mute for Groups + if (vgs.isVssMuteBijective(stream)) { + adjustStreamVolume(stream, direction, flags, callingPackage); + if (isMuteAdjust(direction)) { + // will be propagated to all aliased streams + return; + } + fallbackOnStream = true; + } + } + if (fallbackOnStream) { + // Handled by at least one stream, will be propagated to group, bailing out. + return; + } + sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_ADJUST_GROUP_VOL, vgs.name(), + direction, flags, callingPackage)); + vgs.adjustVolume(direction, flags); + } + + /** @see AudioManager#getLastAudibleVolumeGroupVolume(int) */ + public int getLastAudibleVolumeGroupVolume(int groupId) { + enforceQueryStatePermission(); + synchronized (VolumeStreamState.class) { + if (sVolumeGroupStates.indexOfKey(groupId) < 0) { + Log.e(TAG, ": no volume group found for id " + groupId); + return 0; + } + VolumeGroupState vgs = sVolumeGroupStates.get(groupId); + return vgs.getVolumeIndex(); + } + } + + /** @see AudioManager#isVolumeGroupMuted(int) */ + public boolean isVolumeGroupMuted(int groupId) { + synchronized (VolumeStreamState.class) { + if (sVolumeGroupStates.indexOfKey(groupId) < 0) { + Log.e(TAG, ": no volume group found for id " + groupId); + return false; + } + VolumeGroupState vgs = sVolumeGroupStates.get(groupId); + return vgs.isMuted(); + } + } + /** @see AudioManager#setStreamVolume(int, int, int) * Part of service interface, check permissions here */ public void setStreamVolumeWithAttribution(int streamType, int index, int flags, @@ -4264,31 +4383,6 @@ public class AudioService extends IAudioService.Stub } } - - - private int getVolumeGroupIdForAttributes(@NonNull AudioAttributes attributes) { - Objects.requireNonNull(attributes, "attributes must not be null"); - int volumeGroupId = getVolumeGroupIdForAttributesInt(attributes); - if (volumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP) { - return volumeGroupId; - } - // The default volume group is the one hosted by default product strategy, i.e. - // supporting Default Attributes - return getVolumeGroupIdForAttributesInt(AudioProductStrategy.getDefaultAttributes()); - } - - private int getVolumeGroupIdForAttributesInt(@NonNull AudioAttributes attributes) { - Objects.requireNonNull(attributes, "attributes must not be null"); - for (final AudioProductStrategy productStrategy : - AudioProductStrategy.getAudioProductStrategies()) { - int volumeGroupId = productStrategy.getVolumeGroupIdForAudioAttributes(attributes); - if (volumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP) { - return volumeGroupId; - } - } - return AudioVolumeGroup.DEFAULT_VOLUME_GROUP; - } - private void dispatchAbsoluteVolumeChanged(int streamType, AbsoluteVolumeDeviceInfo deviceInfo, int index) { VolumeInfo volumeInfo = deviceInfo.getMatchingVolumeInfoForStream(streamType); @@ -4321,7 +4415,6 @@ public class AudioService extends IAudioService.Stub } } - // No ringer or zen muted stream volumes can be changed unless it'll exit dnd private boolean volumeAdjustmentAllowedByDnd(int streamTypeAlias, int flags) { switch (mNm.getZenMode()) { @@ -5013,7 +5106,7 @@ public class AudioService extends IAudioService.Stub } private void setRingerMode(int ringerMode, String caller, boolean external) { - if (mUseFixedVolume || mIsSingleVolume) { + if (mUseFixedVolume || mIsSingleVolume || mUseVolumeGroupAliases) { return; } if (caller == null || caller.length() == 0) { @@ -5838,7 +5931,7 @@ public class AudioService extends IAudioService.Stub } } - readVolumeGroupsSettings(); + readVolumeGroupsSettings(userSwitch); if (DEBUG_VOL) { Log.d(TAG, "Restoring device volume behavior"); @@ -7286,6 +7379,7 @@ public class AudioService extends IAudioService.Stub try { // if no valid attributes, this volume group is not controllable, throw exception ensureValidAttributes(avg); + sVolumeGroupStates.append(avg.getId(), new VolumeGroupState(avg)); } catch (IllegalArgumentException e) { // Volume Groups without attributes are not controllable through set/get volume // using attributes. Do not append them. @@ -7294,11 +7388,10 @@ public class AudioService extends IAudioService.Stub } continue; } - sVolumeGroupStates.append(avg.getId(), new VolumeGroupState(avg)); } for (int i = 0; i < sVolumeGroupStates.size(); i++) { final VolumeGroupState vgs = sVolumeGroupStates.valueAt(i); - vgs.applyAllVolumes(); + vgs.applyAllVolumes(/* userSwitch= */ false); } } @@ -7311,14 +7404,22 @@ public class AudioService extends IAudioService.Stub } } - private void readVolumeGroupsSettings() { - if (DEBUG_VOL) { - Log.v(TAG, "readVolumeGroupsSettings"); - } - for (int i = 0; i < sVolumeGroupStates.size(); i++) { - final VolumeGroupState vgs = sVolumeGroupStates.valueAt(i); - vgs.readSettings(); - vgs.applyAllVolumes(); + private void readVolumeGroupsSettings(boolean userSwitch) { + synchronized (mSettingsLock) { + synchronized (VolumeStreamState.class) { + if (DEBUG_VOL) { + Log.d(TAG, "readVolumeGroupsSettings userSwitch=" + userSwitch); + } + for (int i = 0; i < sVolumeGroupStates.size(); i++) { + VolumeGroupState vgs = sVolumeGroupStates.valueAt(i); + // as for STREAM_MUSIC, preserve volume from one user to the next. + if (!(userSwitch && vgs.isMusic())) { + vgs.clearIndexCache(); + vgs.readSettings(); + } + vgs.applyAllVolumes(userSwitch); + } + } } } @@ -7329,7 +7430,7 @@ public class AudioService extends IAudioService.Stub } for (int i = 0; i < sVolumeGroupStates.size(); i++) { final VolumeGroupState vgs = sVolumeGroupStates.valueAt(i); - vgs.applyAllVolumes(); + vgs.applyAllVolumes(false/*userSwitch*/); } } @@ -7342,17 +7443,34 @@ public class AudioService extends IAudioService.Stub } } + private static boolean isCallStream(int stream) { + return stream == AudioSystem.STREAM_VOICE_CALL + || stream == AudioSystem.STREAM_BLUETOOTH_SCO; + } + + private static int getVolumeGroupForStreamType(int stream) { + AudioAttributes attributes = + AudioProductStrategy.getAudioAttributesForStrategyWithLegacyStreamType(stream); + if (attributes.equals(new AudioAttributes.Builder().build())) { + return AudioVolumeGroup.DEFAULT_VOLUME_GROUP; + } + return AudioProductStrategy.getVolumeGroupIdForAudioAttributes( + attributes, /* fallbackOnDefault= */ false); + } + // NOTE: Locking order for synchronized objects related to volume management: // 1 mSettingsLock - // 2 VolumeGroupState.class + // 2 VolumeStreamState.class private class VolumeGroupState { private final AudioVolumeGroup mAudioVolumeGroup; private final SparseIntArray mIndexMap = new SparseIntArray(8); private int mIndexMin; private int mIndexMax; - private int mLegacyStreamType = AudioSystem.STREAM_DEFAULT; + private boolean mHasValidStreamType = false; private int mPublicStreamType = AudioSystem.STREAM_MUSIC; private AudioAttributes mAudioAttributes = AudioProductStrategy.getDefaultAttributes(); + private boolean mIsMuted = false; + private final String mSettingName; // No API in AudioSystem to get a device from strategy or from attributes. // Need a valid public stream type to use current API getDeviceForStream @@ -7366,20 +7484,22 @@ public class AudioService extends IAudioService.Stub Log.v(TAG, "VolumeGroupState for " + avg.toString()); } // mAudioAttributes is the default at this point - for (final AudioAttributes aa : avg.getAudioAttributes()) { + for (AudioAttributes aa : avg.getAudioAttributes()) { if (!aa.equals(mAudioAttributes)) { mAudioAttributes = aa; break; } } - final int[] streamTypes = mAudioVolumeGroup.getLegacyStreamTypes(); + int[] streamTypes = mAudioVolumeGroup.getLegacyStreamTypes(); + String streamSettingName = ""; if (streamTypes.length != 0) { // Uses already initialized MIN / MAX if a stream type is attached to group - mLegacyStreamType = streamTypes[0]; - for (final int streamType : streamTypes) { + for (int streamType : streamTypes) { if (streamType != AudioSystem.STREAM_DEFAULT && streamType < AudioSystem.getNumStreamTypes()) { mPublicStreamType = streamType; + mHasValidStreamType = true; + streamSettingName = System.VOLUME_SETTINGS_INT[mPublicStreamType]; break; } } @@ -7389,10 +7509,10 @@ public class AudioService extends IAudioService.Stub mIndexMin = AudioSystem.getMinVolumeIndexForAttributes(mAudioAttributes); mIndexMax = AudioSystem.getMaxVolumeIndexForAttributes(mAudioAttributes); } else { - Log.e(TAG, "volume group: " + mAudioVolumeGroup.name() + throw new IllegalArgumentException("volume group: " + mAudioVolumeGroup.name() + " has neither valid attributes nor valid stream types assigned"); - return; } + mSettingName = !streamSettingName.isEmpty() ? streamSettingName : ("volume_" + name()); // Load volume indexes from data base readSettings(); } @@ -7405,40 +7525,149 @@ public class AudioService extends IAudioService.Stub return mAudioVolumeGroup.name(); } + /** + * Volume group with non null minimum index are considered as non mutable, thus + * bijectivity is broken with potential associated stream type. + * VOICE_CALL stream has minVolumeIndex > 0 but can be muted directly by an + * app that has MODIFY_PHONE_STATE permission. + */ + private boolean isVssMuteBijective(int stream) { + return isStreamAffectedByMute(stream) + && (getMinIndex() == (mStreamStates[stream].mIndexMin + 5) / 10) + && (getMinIndex() == 0 || isCallStream(stream)); + } + + private boolean isMutable() { + return mIndexMin == 0 || (mHasValidStreamType && isVssMuteBijective(mPublicStreamType)); + } + /** + * Mute/unmute the volume group + * @param muted the new mute state + */ + @GuardedBy("AudioService.VolumeStreamState.class") + public boolean mute(boolean muted) { + if (!isMutable()) { + // Non mutable volume group + if (DEBUG_VOL) { + Log.d(TAG, "invalid mute on unmutable volume group " + name()); + } + return false; + } + boolean changed = (mIsMuted != muted); + // As for VSS, mute shall apply minIndex to all devices found in IndexMap and default. + if (changed) { + mIsMuted = muted; + applyAllVolumes(false /*userSwitch*/); + } + return changed; + } + + public boolean isMuted() { + return mIsMuted; + } + + public void adjustVolume(int direction, int flags) { + synchronized (VolumeStreamState.class) { + int device = getDeviceForVolume(); + int previousIndex = getIndex(device); + if (isMuteAdjust(direction) && !isMutable()) { + // Non mutable volume group + if (DEBUG_VOL) { + Log.d(TAG, "invalid mute on unmutable volume group " + name()); + } + return; + } + switch (direction) { + case AudioManager.ADJUST_TOGGLE_MUTE: { + // Note: If muted by volume 0, unmute will restore volume 0. + mute(!mIsMuted); + break; + } + case AudioManager.ADJUST_UNMUTE: + // Note: If muted by volume 0, unmute will restore volume 0. + mute(false); + break; + case AudioManager.ADJUST_MUTE: + // May be already muted by setvolume 0, prevent from setting same value + if (previousIndex != 0) { + // bypass persist + mute(true); + } + mIsMuted = true; + break; + case AudioManager.ADJUST_RAISE: + // As for stream, RAISE during mute will increment the index + setVolumeIndex(Math.min(previousIndex + 1, mIndexMax), device, flags); + break; + case AudioManager.ADJUST_LOWER: + // For stream, ADJUST_LOWER on a muted VSS is a no-op + // If we decide to unmute on ADJUST_LOWER, cannot fallback on + // adjustStreamVolume for group associated to legacy stream type + if (isMuted() && previousIndex != 0) { + mute(false); + } else { + int newIndex = Math.max(previousIndex - 1, mIndexMin); + setVolumeIndex(newIndex, device, flags); + } + break; + } + } + } + public int getVolumeIndex() { - return getIndex(getDeviceForVolume()); + synchronized (VolumeStreamState.class) { + return getIndex(getDeviceForVolume()); + } } public void setVolumeIndex(int index, int flags) { - if (mUseFixedVolume) { - return; + synchronized (VolumeStreamState.class) { + if (mUseFixedVolume) { + return; + } + setVolumeIndex(index, getDeviceForVolume(), flags); } - setVolumeIndex(index, getDeviceForVolume(), flags); } + @GuardedBy("AudioService.VolumeStreamState.class") private void setVolumeIndex(int index, int device, int flags) { - // Set the volume index - setVolumeIndexInt(index, device, flags); - - // Update local cache - mIndexMap.put(device, index); - - // update data base - post a persist volume group msg - sendMsg(mAudioHandler, - MSG_PERSIST_VOLUME_GROUP, - SENDMSG_QUEUE, - device, - 0, - this, - PERSIST_DELAY); + // Update cache & persist (muted by volume 0 shall be persisted) + updateVolumeIndex(index, device); + // setting non-zero volume for a muted stream unmutes the stream and vice versa, + boolean changed = mute(index == 0); + if (!changed) { + // Set the volume index only if mute operation is a no-op + index = getValidIndex(index); + setVolumeIndexInt(index, device, flags); + } } + @GuardedBy("AudioService.VolumeStreamState.class") + public void updateVolumeIndex(int index, int device) { + // Filter persistency if already exist and the index has not changed + if (mIndexMap.indexOfKey(device) < 0 || mIndexMap.get(device) != index) { + // Update local cache + mIndexMap.put(device, getValidIndex(index)); + + // update data base - post a persist volume group msg + sendMsg(mAudioHandler, + MSG_PERSIST_VOLUME_GROUP, + SENDMSG_QUEUE, + device, + 0, + this, + PERSIST_DELAY); + } + } + + @GuardedBy("AudioService.VolumeStreamState.class") private void setVolumeIndexInt(int index, int device, int flags) { // Reflect mute state of corresponding stream by forcing index to 0 if muted // Only set audio policy BT SCO stream volume to 0 when the stream is actually muted. // This allows RX path muting by the audio HAL only when explicitly muted but not when // index is just set to 0 to repect BT requirements - if (mStreamStates[mPublicStreamType].isFullyMuted()) { + if (mHasValidStreamType && isVssMuteBijective(mPublicStreamType) + && mStreamStates[mPublicStreamType].isFullyMuted()) { index = 0; } else if (mPublicStreamType == AudioSystem.STREAM_BLUETOOTH_SCO && index == 0) { index = 1; @@ -7447,18 +7676,16 @@ public class AudioService extends IAudioService.Stub AudioSystem.setVolumeIndexForAttributes(mAudioAttributes, index, device); } - public int getIndex(int device) { - synchronized (VolumeGroupState.class) { - int index = mIndexMap.get(device, -1); - // there is always an entry for AudioSystem.DEVICE_OUT_DEFAULT - return (index != -1) ? index : mIndexMap.get(AudioSystem.DEVICE_OUT_DEFAULT); - } + @GuardedBy("AudioService.VolumeStreamState.class") + private int getIndex(int device) { + int index = mIndexMap.get(device, -1); + // there is always an entry for AudioSystem.DEVICE_OUT_DEFAULT + return (index != -1) ? index : mIndexMap.get(AudioSystem.DEVICE_OUT_DEFAULT); } - public boolean hasIndexForDevice(int device) { - synchronized (VolumeGroupState.class) { - return (mIndexMap.get(device, -1) != -1); - } + @GuardedBy("AudioService.VolumeStreamState.class") + private boolean hasIndexForDevice(int device) { + return (mIndexMap.get(device, -1) != -1); } public int getMaxIndex() { @@ -7469,55 +7696,108 @@ public class AudioService extends IAudioService.Stub return mIndexMin; } - private boolean isValidLegacyStreamType() { - return (mLegacyStreamType != AudioSystem.STREAM_DEFAULT) - && (mLegacyStreamType < mStreamStates.length); + private boolean isValidStream(int stream) { + return (stream != AudioSystem.STREAM_DEFAULT) && (stream < mStreamStates.length); } - public void applyAllVolumes() { - synchronized (VolumeGroupState.class) { - int deviceForStream = AudioSystem.DEVICE_NONE; - int volumeIndexForStream = 0; - if (isValidLegacyStreamType()) { - // Prevent to apply settings twice when group is associated to public stream - deviceForStream = getDeviceForStream(mLegacyStreamType); - volumeIndexForStream = getStreamVolume(mLegacyStreamType); - } + public boolean isMusic() { + return mHasValidStreamType && mPublicStreamType == AudioSystem.STREAM_MUSIC; + } + + public void applyAllVolumes(boolean userSwitch) { + String caller = "from vgs"; + synchronized (VolumeStreamState.class) { // apply device specific volumes first - int index; for (int i = 0; i < mIndexMap.size(); i++) { - final int device = mIndexMap.keyAt(i); + int device = mIndexMap.keyAt(i); + int index = mIndexMap.valueAt(i); + boolean synced = false; if (device != AudioSystem.DEVICE_OUT_DEFAULT) { - index = mIndexMap.valueAt(i); - if (device == deviceForStream && volumeIndexForStream == index) { - continue; + for (int stream : getLegacyStreamTypes()) { + if (isValidStream(stream)) { + boolean streamMuted = mStreamStates[stream].mIsMuted; + int deviceForStream = getDeviceForStream(stream); + int indexForStream = + (mStreamStates[stream].getIndex(deviceForStream) + 5) / 10; + if (device == deviceForStream) { + if (indexForStream == index && (isMuted() == streamMuted) + && isVssMuteBijective(stream)) { + synced = true; + continue; + } + if (indexForStream != index) { + mStreamStates[stream].setIndex(index * 10, device, caller, + true /*hasModifyAudioSettings*/); + } + if ((isMuted() != streamMuted) && isVssMuteBijective(stream)) { + mStreamStates[stream].mute(isMuted()); + } + } + } } - if (DEBUG_VOL) { - Log.v(TAG, "applyAllVolumes: restore index " + index + " for group " - + mAudioVolumeGroup.name() + " and device " - + AudioSystem.getOutputDeviceName(device)); + if (!synced) { + if (DEBUG_VOL) { + Log.d(TAG, "applyAllVolumes: apply index " + index + ", group " + + mAudioVolumeGroup.name() + " and device " + + AudioSystem.getOutputDeviceName(device)); + } + setVolumeIndexInt(isMuted() ? 0 : index, device, 0 /*flags*/); } - setVolumeIndexInt(index, device, 0 /*flags*/); } } // apply default volume last: by convention , default device volume will be used // by audio policy manager if no explicit volume is present for a given device type - index = getIndex(AudioSystem.DEVICE_OUT_DEFAULT); - if (DEBUG_VOL) { - Log.v(TAG, "applyAllVolumes: restore default device index " + index - + " for group " + mAudioVolumeGroup.name()); - } - if (isValidLegacyStreamType()) { - int defaultStreamIndex = (mStreamStates[mLegacyStreamType] - .getIndex(AudioSystem.DEVICE_OUT_DEFAULT) + 5) / 10; - if (defaultStreamIndex == index) { - return; + int index = getIndex(AudioSystem.DEVICE_OUT_DEFAULT); + boolean synced = false; + int deviceForVolume = getDeviceForVolume(); + boolean forceDeviceSync = userSwitch && (mIndexMap.indexOfKey(deviceForVolume) < 0); + for (int stream : getLegacyStreamTypes()) { + if (isValidStream(stream)) { + boolean streamMuted = mStreamStates[stream].mIsMuted; + int defaultStreamIndex = (mStreamStates[stream].getIndex( + AudioSystem.DEVICE_OUT_DEFAULT) + 5) / 10; + if (forceDeviceSync) { + mStreamStates[stream].setIndex(index * 10, deviceForVolume, caller, + true /*hasModifyAudioSettings*/); + } + if (defaultStreamIndex == index && (isMuted() == streamMuted) + && isVssMuteBijective(stream)) { + synced = true; + continue; + } + if (defaultStreamIndex != index) { + mStreamStates[stream].setIndex( + index * 10, AudioSystem.DEVICE_OUT_DEFAULT, caller, + true /*hasModifyAudioSettings*/); + } + if ((isMuted() != streamMuted) && isVssMuteBijective(stream)) { + mStreamStates[stream].mute(isMuted()); + } } } - setVolumeIndexInt(index, AudioSystem.DEVICE_OUT_DEFAULT, 0 /*flags*/); + if (!synced) { + if (DEBUG_VOL) { + Log.d(TAG, "applyAllVolumes: apply default device index " + index + + ", group " + mAudioVolumeGroup.name()); + } + setVolumeIndexInt( + isMuted() ? 0 : index, AudioSystem.DEVICE_OUT_DEFAULT, 0 /*flags*/); + } + if (forceDeviceSync) { + if (DEBUG_VOL) { + Log.d(TAG, "applyAllVolumes: forceDeviceSync index " + index + + ", device " + AudioSystem.getOutputDeviceName(deviceForVolume) + + ", group " + mAudioVolumeGroup.name()); + } + setVolumeIndexInt(isMuted() ? 0 : index, deviceForVolume, 0); + } } } + public void clearIndexCache() { + mIndexMap.clear(); + } + private void persistVolumeGroup(int device) { if (mUseFixedVolume) { return; @@ -7526,21 +7806,19 @@ public class AudioService extends IAudioService.Stub Log.v(TAG, "persistVolumeGroup: storing index " + getIndex(device) + " for group " + mAudioVolumeGroup.name() + ", device " + AudioSystem.getOutputDeviceName(device) - + " and User=" + ActivityManager.getCurrentUser()); + + " and User=" + getCurrentUserId()); } boolean success = mSettings.putSystemIntForUser(mContentResolver, getSettingNameForDevice(device), getIndex(device), - UserHandle.USER_CURRENT); + isMusic() ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT); if (!success) { Log.e(TAG, "persistVolumeGroup failed for group " + mAudioVolumeGroup.name()); } } public void readSettings() { - synchronized (VolumeGroupState.class) { - // First clear previously loaded (previous user?) settings - mIndexMap.clear(); + synchronized (VolumeStreamState.class) { // force maximum volume on all streams if fixed volume property is set if (mUseFixedVolume) { mIndexMap.put(AudioSystem.DEVICE_OUT_DEFAULT, mIndexMax); @@ -7555,7 +7833,8 @@ public class AudioService extends IAudioService.Stub int index; String name = getSettingNameForDevice(device); index = mSettings.getSystemIntForUser( - mContentResolver, name, defaultIndex, UserHandle.USER_CURRENT); + mContentResolver, name, defaultIndex, + isMusic() ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT); if (index == -1) { continue; } @@ -7566,13 +7845,14 @@ public class AudioService extends IAudioService.Stub if (DEBUG_VOL) { Log.v(TAG, "readSettings: found stored index " + getValidIndex(index) + " for group " + mAudioVolumeGroup.name() + ", device: " + name - + ", User=" + ActivityManager.getCurrentUser()); + + ", User=" + getCurrentUserId()); } mIndexMap.put(device, getValidIndex(index)); } } } + @GuardedBy("AudioService.VolumeStreamState.class") private int getValidIndex(int index) { if (index < mIndexMin) { return mIndexMin; @@ -7583,15 +7863,17 @@ public class AudioService extends IAudioService.Stub } public @NonNull String getSettingNameForDevice(int device) { - final String suffix = AudioSystem.getOutputDeviceName(device); + String suffix = AudioSystem.getOutputDeviceName(device); if (suffix.isEmpty()) { - return mAudioVolumeGroup.name(); + return mSettingName; } - return mAudioVolumeGroup.name() + "_" + AudioSystem.getOutputDeviceName(device); + return mSettingName + "_" + AudioSystem.getOutputDeviceName(device); } private void dump(PrintWriter pw) { pw.println("- VOLUME GROUP " + mAudioVolumeGroup.name() + ":"); + pw.print(" Muted: "); + pw.println(mIsMuted); pw.print(" Min: "); pw.println(mIndexMin); pw.print(" Max: "); @@ -7601,9 +7883,9 @@ public class AudioService extends IAudioService.Stub if (i > 0) { pw.print(", "); } - final int device = mIndexMap.keyAt(i); + int device = mIndexMap.keyAt(i); pw.print(Integer.toHexString(device)); - final String deviceName = device == AudioSystem.DEVICE_OUT_DEFAULT ? "default" + String deviceName = device == AudioSystem.DEVICE_OUT_DEFAULT ? "default" : AudioSystem.getOutputDeviceName(device); if (!deviceName.isEmpty()) { pw.print(" ("); @@ -7616,7 +7898,7 @@ public class AudioService extends IAudioService.Stub pw.println(); pw.print(" Devices: "); int n = 0; - final int devices = getDeviceForVolume(); + int devices = getDeviceForVolume(); for (int device : AudioSystem.DEVICE_OUT_ALL_SET) { if ((devices & device) == device) { if (n++ > 0) { @@ -7625,6 +7907,10 @@ public class AudioService extends IAudioService.Stub pw.print(AudioSystem.getOutputDeviceName(device)); } } + pw.println(); + pw.print(" Streams: "); + Arrays.stream(getLegacyStreamTypes()) + .forEach(stream -> pw.print(AudioSystem.streamToString(stream) + " ")); } } @@ -7636,13 +7922,14 @@ public class AudioService extends IAudioService.Stub // 4 VolumeStreamState.class private class VolumeStreamState { private final int mStreamType; + private VolumeGroupState mVolumeGroupState = null; private int mIndexMin; // min index when user doesn't have permission to change audio settings private int mIndexMinNoPerm; private int mIndexMax; - private boolean mIsMuted; - private boolean mIsMutedInternally; + private boolean mIsMuted = false; + private boolean mIsMutedInternally = false; private String mVolumeIndexSettingName; @NonNull private Set mObservedDeviceSet = new TreeSet<>(); @@ -7699,6 +7986,15 @@ public class AudioService extends IAudioService.Stub mStreamDevicesChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, mStreamType); } + /** + * Associate a {@link volumeGroupState} on the {@link VolumeStreamState}. + *

It helps to synchronize the index, mute attributes on the maching + * {@link volumeGroupState} + * @param volumeGroupState matching the {@link VolumeStreamState} + */ + public void setVolumeGroupState(VolumeGroupState volumeGroupState) { + mVolumeGroupState = volumeGroupState; + } /** * Update the minimum index that can be used without MODIFY_AUDIO_SETTINGS permission * @param index minimum index expressed in "UI units", i.e. no 10x factor @@ -7957,6 +8253,9 @@ public class AudioService extends IAudioService.Stub } } if (changed) { + // If associated to volume group, update group cache + updateVolumeGroupIndex(device, /* forceMuteState= */ false); + oldIndex = (oldIndex + 5) / 10; index = (index + 5) / 10; // log base stream changes to the event log @@ -8059,6 +8358,28 @@ public class AudioService extends IAudioService.Stub } } + // If associated to volume group, update group cache + private void updateVolumeGroupIndex(int device, boolean forceMuteState) { + synchronized (VolumeStreamState.class) { + if (mVolumeGroupState != null) { + int groupIndex = (getIndex(device) + 5) / 10; + if (DEBUG_VOL) { + Log.d(TAG, "updateVolumeGroupIndex for stream " + mStreamType + + ", muted=" + mIsMuted + ", device=" + device + ", index=" + + getIndex(device) + ", group " + mVolumeGroupState.name() + + " Muted=" + mVolumeGroupState.isMuted() + ", Index=" + groupIndex + + ", forceMuteState=" + forceMuteState); + } + mVolumeGroupState.updateVolumeIndex(groupIndex, device); + // Only propage mute of stream when applicable + if (mIndexMin == 0 || isCallStream(mStreamType)) { + // For call stream, align mute only when muted, not when index is set to 0 + mVolumeGroupState.mute(forceMuteState ? mIsMuted : groupIndex == 0); + } + } + } + } + /** * Mute/unmute the stream * @param state the new mute state @@ -8067,27 +8388,10 @@ public class AudioService extends IAudioService.Stub public boolean mute(boolean state) { boolean changed = false; synchronized (VolumeStreamState.class) { - if (state != mIsMuted) { - changed = true; - mIsMuted = state; - - // Set the new mute volume. This propagates the values to - // the audio system, otherwise the volume won't be changed - // at the lower level. - sendMsg(mAudioHandler, - MSG_SET_ALL_VOLUMES, - SENDMSG_QUEUE, - 0, - 0, - this, 0); - } + changed = mute(state, true); } if (changed) { - // Stream mute changed, fire the intent. - Intent intent = new Intent(AudioManager.STREAM_MUTE_CHANGED_ACTION); - intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, mStreamType); - intent.putExtra(AudioManager.EXTRA_STREAM_VOLUME_MUTED, state); - sendBroadcastToAll(intent); + broadcastMuteSetting(mStreamType, state); } return changed; } @@ -8119,6 +8423,44 @@ public class AudioService extends IAudioService.Stub return mIsMuted || mIsMutedInternally; } + /** + * Mute/unmute the stream + * @param state the new mute state + * @param apply true to propagate to HW, or false just to update the cache. May be needed + * to mute a stream and its aliases as applyAllVolume will force settings to aliases. + * It prevents unnecessary calls to {@see AudioSystem#setStreamVolume} + * @return true if the mute state was changed + */ + public boolean mute(boolean state, boolean apply) { + synchronized (VolumeStreamState.class) { + boolean changed = state != mIsMuted; + if (changed) { + mIsMuted = state; + if (apply) { + doMute(); + } + } + return changed; + } + } + + public void doMute() { + synchronized (VolumeStreamState.class) { + // If associated to volume group, update group cache + updateVolumeGroupIndex(getDeviceForStream(mStreamType), /* forceMuteState= */ true); + + // Set the new mute volume. This propagates the values to + // the audio system, otherwise the volume won't be changed + // at the lower level. + sendMsg(mAudioHandler, + MSG_SET_ALL_VOLUMES, + SENDMSG_QUEUE, + 0, + 0, + this, 0); + } + } + public int getStreamType() { return mStreamType; } @@ -8188,6 +8530,9 @@ public class AudioService extends IAudioService.Stub pw.println(); pw.print(" Devices: "); pw.print(AudioSystem.deviceSetToString(getDeviceSetForStream(mStreamType))); + pw.println(); + pw.print(" Volume Group: "); + pw.println(mVolumeGroupState != null ? mVolumeGroupState.name() : "n/a"); } } diff --git a/services/core/java/com/android/server/audio/AudioServiceEvents.java b/services/core/java/com/android/server/audio/AudioServiceEvents.java index c2c3f028abdb0..6cbe03ed87d3e 100644 --- a/services/core/java/com/android/server/audio/AudioServiceEvents.java +++ b/services/core/java/com/android/server/audio/AudioServiceEvents.java @@ -17,7 +17,6 @@ package com.android.server.audio; import android.annotation.NonNull; -import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; import android.media.AudioManager; import android.media.AudioSystem; @@ -221,6 +220,7 @@ public class AudioServiceEvents { static final int VOL_SET_GROUP_VOL = 8; static final int VOL_MUTE_STREAM_INT = 9; static final int VOL_SET_LE_AUDIO_VOL = 10; + static final int VOL_ADJUST_GROUP_VOL = 11; final int mOp; final int mStream; @@ -228,7 +228,6 @@ public class AudioServiceEvents { final int mVal2; final String mCaller; final String mGroupName; - final AudioAttributes mAudioAttributes; /** used for VOL_ADJUST_VOL_UID, * VOL_ADJUST_SUGG_VOL, @@ -241,7 +240,6 @@ public class AudioServiceEvents { mVal2 = val2; mCaller = caller; mGroupName = null; - mAudioAttributes = null; logMetricEvent(); } @@ -254,7 +252,6 @@ public class AudioServiceEvents { mStream = -1; mCaller = null; mGroupName = null; - mAudioAttributes = null; logMetricEvent(); } @@ -267,7 +264,6 @@ public class AudioServiceEvents { mStream = -1; mCaller = null; mGroupName = null; - mAudioAttributes = null; logMetricEvent(); } @@ -280,7 +276,6 @@ public class AudioServiceEvents { // unused mCaller = null; mGroupName = null; - mAudioAttributes = null; logMetricEvent(); } @@ -293,19 +288,18 @@ public class AudioServiceEvents { // unused mCaller = null; mGroupName = null; - mAudioAttributes = null; logMetricEvent(); } - /** used for VOL_SET_GROUP_VOL */ - VolumeEvent(int op, AudioAttributes aa, String group, int index, int flags, String caller) { + /** used for VOL_SET_GROUP_VOL, + * VOL_ADJUST_GROUP_VOL */ + VolumeEvent(int op, String group, int index, int flags, String caller) { mOp = op; mStream = -1; mVal1 = index; mVal2 = flags; mCaller = caller; mGroupName = group; - mAudioAttributes = aa; logMetricEvent(); } @@ -317,7 +311,6 @@ public class AudioServiceEvents { mVal2 = 0; mCaller = null; mGroupName = null; - mAudioAttributes = null; logMetricEvent(); } @@ -359,6 +352,15 @@ public class AudioServiceEvents { .record(); return; } + case VOL_ADJUST_GROUP_VOL: + new MediaMetrics.Item(mMetricsId) + .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller) + .set(MediaMetrics.Property.DIRECTION, mVal1 > 0 ? "up" : "down") + .set(MediaMetrics.Property.EVENT, "adjustVolumeGroupVolume") + .set(MediaMetrics.Property.FLAGS, mVal2) + .set(MediaMetrics.Property.GROUP, mGroupName) + .record(); + return; case VOL_SET_STREAM_VOL: new MediaMetrics.Item(mMetricsId) .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller) @@ -410,7 +412,6 @@ public class AudioServiceEvents { return; case VOL_SET_GROUP_VOL: new MediaMetrics.Item(mMetricsId) - .set(MediaMetrics.Property.ATTRIBUTES, mAudioAttributes.toString()) .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller) .set(MediaMetrics.Property.EVENT, "setVolumeIndexForAttributes") .set(MediaMetrics.Property.FLAGS, mVal2) @@ -436,6 +437,13 @@ public class AudioServiceEvents { .append(" flags:0x").append(Integer.toHexString(mVal2)) .append(") from ").append(mCaller) .toString(); + case VOL_ADJUST_GROUP_VOL: + return new StringBuilder("adjustVolumeGroupVolume(group:") + .append(mGroupName) + .append(" dir:").append(AudioManager.adjustToString(mVal1)) + .append(" flags:0x").append(Integer.toHexString(mVal2)) + .append(") from ").append(mCaller) + .toString(); case VOL_ADJUST_STREAM_VOL: return new StringBuilder("adjustStreamVolume(stream:") .append(AudioSystem.streamToString(mStream)) @@ -484,8 +492,7 @@ public class AudioServiceEvents { .append(" stream:").append(AudioSystem.streamToString(mStream)) .toString(); case VOL_SET_GROUP_VOL: - return new StringBuilder("setVolumeIndexForAttributes(attr:") - .append(mAudioAttributes.toString()) + return new StringBuilder("setVolumeIndexForAttributes(group:") .append(" group: ").append(mGroupName) .append(" index:").append(mVal1) .append(" flags:0x").append(Integer.toHexString(mVal2))