diff options
12 files changed, 2557 insertions, 12 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 941d842d8ec4..0cc4207d6d7b 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -425,7 +425,10 @@ java_aconfig_library { aconfig_declarations { name: "com.android.media.flags.bettertogether-aconfig", package: "com.android.media.flags", - srcs: ["media/java/android/media/flags/media_better_together.aconfig"], + srcs: [ + "media/java/android/media/flags/media_better_together.aconfig", + "media/java/android/media/flags/fade_manager_configuration.aconfig", + ], } java_aconfig_library { diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java index bf9419fe6603..4be282b5de87 100644 --- a/media/java/android/media/AudioAttributes.java +++ b/media/java/android/media/AudioAttributes.java @@ -29,6 +29,7 @@ import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import android.util.IntArray; import android.util.Log; import android.util.SparseIntArray; import android.util.proto.ProtoOutputStream; @@ -330,7 +331,7 @@ public final class AudioAttributes implements Parcelable { * @hide * Array of all usage types exposed in the SDK that applications can use. */ - public final static int[] SDK_USAGES = { + public static final IntArray SDK_USAGES = IntArray.wrap(new int[] { USAGE_UNKNOWN, USAGE_MEDIA, USAGE_VOICE_COMMUNICATION, @@ -347,14 +348,14 @@ public final class AudioAttributes implements Parcelable { USAGE_ASSISTANCE_SONIFICATION, USAGE_GAME, USAGE_ASSISTANT, - }; + }); /** * @hide */ @TestApi public static int[] getSdkUsages() { - return SDK_USAGES; + return SDK_USAGES.toArray(); } /** @@ -567,6 +568,15 @@ public final class AudioAttributes implements Parcelable { private String mFormattedTags; private Bundle mBundle; // lazy-initialized, may be null + /** Array of all content types exposed in the SDK that applications can use */ + private static final IntArray CONTENT_TYPES = IntArray.wrap(new int[]{ + CONTENT_TYPE_UNKNOWN, + CONTENT_TYPE_SPEECH, + CONTENT_TYPE_MUSIC, + CONTENT_TYPE_MOVIE, + CONTENT_TYPE_SONIFICATION, + }); + private AudioAttributes() { } @@ -1669,6 +1679,27 @@ public final class AudioAttributes implements Parcelable { } /** + * Query if the usage is a valid sdk usage + * + * @param usage one of {@link AttributeSdkUsage} + * @return {@code true} if the usage is valid for sdk or {@code false} otherwise + * @hide + */ + public static boolean isSdkUsage(@AttributeSdkUsage int usage) { + return SDK_USAGES.contains(usage); + } + + /** + * Query if the content type is a valid sdk content type + * @param contentType one of {@link AttributeContentType} + * @return {@code true} if the content type is valid for sdk or {@code false} otherwise + * @hide + */ + public static boolean isSdkContentType(@AttributeContentType int contentType) { + return CONTENT_TYPES.contains(contentType); + } + + /** * Returns the stream type matching this {@code AudioAttributes} instance for volume control. * Use this method to derive the stream type needed to configure the volume * control slider in an {@link android.app.Activity} with diff --git a/media/java/android/media/FadeManagerConfiguration.aidl b/media/java/android/media/FadeManagerConfiguration.aidl new file mode 100644 index 000000000000..ceb4ded76dcf --- /dev/null +++ b/media/java/android/media/FadeManagerConfiguration.aidl @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +/** + * Class to encapsulate fade configurations. + * @hide + */ +parcelable FadeManagerConfiguration; diff --git a/media/java/android/media/FadeManagerConfiguration.java b/media/java/android/media/FadeManagerConfiguration.java new file mode 100644 index 000000000000..337d4b0a916c --- /dev/null +++ b/media/java/android/media/FadeManagerConfiguration.java @@ -0,0 +1,1684 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import static com.android.media.flags.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.IntArray; +import android.util.SparseArray; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Class to encapsulate fade configurations. + * + * <p>Configurations are provided through: + * <ul> + * <li>Fadeable list: a positive list of fadeable type - usage</li> + * <li>Unfadeable lists: negative list of unfadeable types - content type, uid, audio attributes + * </li> + * <li>Volume shaper configs: fade in and fade out configs per usage or audio attributes + * </li> + * </ul> + * + * <p>Fade manager configuration can be created in one of the following ways: + * <ul> + * <li>Disabled fades: + * <pre class="prettyprint"> + * new FadeManagerConfiguration.Builder() + * .setFadeState(FADE_STATE_DISABLED).build() + * </pre> + * Can be used to disable fading</li> + * <li>Default configurations including default fade duration: + * <pre class="prettyprint"> + * new FadeManagerConfiguration.Builder() + * .setFadeState(FADE_STATE_ENABLED_DEFAULT).build() + * </pre> + * Can be used to enable default fading configurations</li> + * <li>Default configurations with custom fade duration: + * <pre class="prettyprint"> + * new FadeManagerConfiguration.Builder(fade out duration, fade in duration) + * .setFadeState(FADE_STATE_ENABLED_DEFAULT).build() + * </pre> + * Can be used to enable default fadeability lists with configurable fade in and out duration + * </li> + * <li>Custom configurations and fade volume shapers: + * <pre class="prettyprint"> + * new FadeManagerConfiguration.Builder(fade out duration, fade in duration) + * .setFadeState(FADE_STATE_ENABLED_DEFAULT) + * .setFadeableUsages(list of usages) + * .setUnfadeableContentTypes(list of content types) + * .setUnfadeableUids(list of uids) + * .setUnfadeableAudioAttributes(list of audio attributes) + * .setFadeOutVolumeShaperConfigForAudioAttributes(attributes, volume shaper config) + * .setFadeInDurationForUsaeg(usage, duration) + * .... + * .build() </pre> + * Achieves full customization of fadeability lists and configurations</li> + * <li>Also provides a copy constructor from another instance of fade manager configuration + * <pre class="prettyprint"> + * new FadeManagerConfiguration.Builder(fadeManagerConfiguration) + * .addFadeableUsage(new usage) + * .... + * .build()</pre> + * Helps with recreating a new instance from another to simply change/add on top of the + * existing ones</li> + * </ul> + * TODO(b/304835727): Convert into system API so that it can be set through AudioPolicy + * + * @hide + */ + +@FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION) +public final class FadeManagerConfiguration implements Parcelable { + + public static final String TAG = "FadeManagerConfiguration"; + + /** + * Defines the disabled fade state. No player will be faded in this state. + */ + public static final int FADE_STATE_DISABLED = 0; + + /** + * Defines the enabled fade state with default configurations + */ + public static final int FADE_STATE_ENABLED_DEFAULT = 1; + + /** + * Defines the enabled state with Automotive specific configurations + */ + public static final int FADE_STATE_ENABLED_AUTO = 2; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = false, prefix = "FADE_STATE", value = { + FADE_STATE_DISABLED, + FADE_STATE_ENABLED_DEFAULT, + FADE_STATE_ENABLED_AUTO, + }) + public @interface FadeStateEnum {} + + /** + * Defines ID to be used in volume shaper for fading + */ + public static final int VOLUME_SHAPER_SYSTEM_FADE_ID = 2; + + /** + * Used to reset duration or return duration when not set + * + * @see Builder#setFadeOutDurationForUsage(int, long) + * @see Builder#setFadeInDurationForUsage(int, long) + * @see Builder#setFadeOutDurationForAudioAttributes(AudioAttributes, long) + * @see Builder#setFadeInDurationForAudioAttributes(AudioAttributes, long) + * @see #getFadeOutDurationForUsage(int) + * @see #getFadeInDurationForUsage(int) + * @see #getFadeOutDurationForAudioAttributes(AudioAttributes) + * @see #getFadeInDurationForAudioAttributes(AudioAttributes) + */ + public static final long DURATION_NOT_SET = 0; + /** Map of Usage to Fade volume shaper configs wrapper */ + private final SparseArray<FadeVolumeShaperConfigsWrapper> mUsageToFadeWrapperMap; + /** Map of AudioAttributes to Fade volume shaper configs wrapper */ + private final ArrayMap<AudioAttributes, FadeVolumeShaperConfigsWrapper> mAttrToFadeWrapperMap; + /** list of fadeable usages */ + private final @NonNull IntArray mFadeableUsages; + /** list of unfadeable content types */ + private final @NonNull IntArray mUnfadeableContentTypes; + /** list of unfadeable player types */ + private final @NonNull IntArray mUnfadeablePlayerTypes; + /** list of unfadeable uid(s) */ + private final @NonNull IntArray mUnfadeableUids; + /** list of unfadeable AudioAttributes */ + private final @NonNull List<AudioAttributes> mUnfadeableAudioAttributes; + /** fade state */ + private final @FadeStateEnum int mFadeState; + /** fade out duration from builder - used for creating default fade out volume shaper */ + private final long mFadeOutDurationMillis; + /** fade in duration from builder - used for creating default fade in volume shaper */ + private final long mFadeInDurationMillis; + /** delay after which the offending players are faded back in */ + private final long mFadeInDelayForOffendersMillis; + + private FadeManagerConfiguration(int fadeState, long fadeOutDurationMillis, + long fadeInDurationMillis, long offendersFadeInDelayMillis, + @NonNull SparseArray<FadeVolumeShaperConfigsWrapper> usageToFadeWrapperMap, + @NonNull ArrayMap<AudioAttributes, FadeVolumeShaperConfigsWrapper> attrToFadeWrapperMap, + @NonNull IntArray fadeableUsages, @NonNull IntArray unfadeableContentTypes, + @NonNull IntArray unfadeablePlayerTypes, @NonNull IntArray unfadeableUids, + @NonNull List<AudioAttributes> unfadeableAudioAttributes) { + mFadeState = fadeState; + mFadeOutDurationMillis = fadeOutDurationMillis; + mFadeInDurationMillis = fadeInDurationMillis; + mFadeInDelayForOffendersMillis = offendersFadeInDelayMillis; + mUsageToFadeWrapperMap = Objects.requireNonNull(usageToFadeWrapperMap, + "Usage to fade wrapper map cannot be null"); + mAttrToFadeWrapperMap = Objects.requireNonNull(attrToFadeWrapperMap, + "Attribute to fade wrapper map cannot be null"); + mFadeableUsages = Objects.requireNonNull(fadeableUsages, + "List of fadeable usages cannot be null"); + mUnfadeableContentTypes = Objects.requireNonNull(unfadeableContentTypes, + "List of unfadeable content types cannot be null"); + mUnfadeablePlayerTypes = Objects.requireNonNull(unfadeablePlayerTypes, + "List of unfadeable player types cannot be null"); + mUnfadeableUids = Objects.requireNonNull(unfadeableUids, + "List of unfadeable uids cannot be null"); + mUnfadeableAudioAttributes = Objects.requireNonNull(unfadeableAudioAttributes, + "List of unfadeable audio attributes cannot be null"); + } + + /** + * Get the fade state + * + * @return one of the {@link FadeStateEnum} state + */ + @FadeStateEnum + public int getFadeState() { + return mFadeState; + } + + /** + * Get the list of usages that can be faded + * + * @return list of {@link android.media.AudioAttributes.AttributeUsage} that shall be faded + * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} + */ + @NonNull + public List<Integer> getFadeableUsages() { + ensureFadingIsEnabled(); + return convertIntArrayToIntegerList(mFadeableUsages); + } + + /** + * Get the list of {@link android.media.AudioPlaybackConfiguration.PlayerType player types} + * that cannot be faded + * + * @return list of {@link android.media.AudioPlaybackConfiguration.PlayerType} + * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} + */ + @NonNull + public List<Integer> getUnfadeablePlayerTypes() { + ensureFadingIsEnabled(); + return convertIntArrayToIntegerList(mUnfadeablePlayerTypes); + } + + /** + * Get the list of {@link android.media.AudioAttributes.AttributeContentType content types} + * that cannot be faded + * + * @return list of {@link android.media.AudioAttributes.AttributeContentType} + * @throws IllegalStateExceptionif if the fade state is set to {@link #FADE_STATE_DISABLED} + */ + @NonNull + public List<Integer> getUnfadeableContentTypes() { + ensureFadingIsEnabled(); + return convertIntArrayToIntegerList(mUnfadeableContentTypes); + } + + /** + * Get the list of uids that cannot be faded + * + * @return list of uids that shall not be faded + * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} + */ + @NonNull + public List<Integer> getUnfadeableUids() { + ensureFadingIsEnabled(); + return convertIntArrayToIntegerList(mUnfadeableUids); + } + + /** + * Get the list of {@link android.media.AudioAttributes} that cannot be faded + * + * @return list of {@link android.media.AudioAttributes} that shall not be faded + * @throws IllegalStateException if fade state is set to {@link #FADE_STATE_DISABLED} + */ + @NonNull + public List<AudioAttributes> getUnfadeableAudioAttributes() { + ensureFadingIsEnabled(); + return mUnfadeableAudioAttributes; + } + + /** + * Get the duration used to fade out players with + * {@link android.media.AudioAttributes.AttributeUsage} + * + * @param usage the {@link android.media.AudioAttributes.AttributeUsage} + * @return duration in milliseconds if set for the usage or {@link #DURATION_NOT_SET} otherwise + * @throws IllegalArgumentException if the usage is invalid + * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} + */ + public long getFadeOutDurationForUsage(int usage) { + ensureFadingIsEnabled(); + validateUsage(usage); + return getDurationForVolumeShaperConfig(getVolumeShaperConfigFromWrapper( + mUsageToFadeWrapperMap.get(usage), /* isFadeIn= */ false)); + } + + /** + * Get the duration used to fade in players with + * {@link android.media.AudioAttributes.AttributeUsage} + * + * @param usage the {@link android.media.AudioAttributes.AttributeUsage} + * @return duration in milliseconds if set for the usage or {@link #DURATION_NOT_SET} otherwise + * @throws IllegalArgumentException if the usage is invalid + * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} + */ + public long getFadeInDurationForUsage(int usage) { + ensureFadingIsEnabled(); + validateUsage(usage); + return getDurationForVolumeShaperConfig(getVolumeShaperConfigFromWrapper( + mUsageToFadeWrapperMap.get(usage), /* isFadeIn= */ true)); + } + + /** + * Get the {@link android.media.VolumeShaper.Configuration} used to fade out players with + * {@link android.media.AudioAttributes.AttributeUsage} + * + * @param usage the {@link android.media.AudioAttributes.AttributeUsage} + * @return {@link android.media.VolumeShaper.Configuration} if set for the usage or + * {@code null} otherwise + * @throws IllegalArgumentException if the usage is invalid + * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} + */ + @Nullable + public VolumeShaper.Configuration getFadeOutVolumeShaperConfigForUsage(int usage) { + ensureFadingIsEnabled(); + validateUsage(usage); + return getVolumeShaperConfigFromWrapper(mUsageToFadeWrapperMap.get(usage), + /* isFadeIn= */ false); + } + + /** + * Get the {@link android.media.VolumeShaper.Configuration} used to fade in players with + * {@link android.media.AudioAttributes.AttributeUsage} + * + * @param usage the {@link android.media.AudioAttributes.AttributeUsage} of player + * @return {@link android.media.VolumeShaper.Configuration} if set for the usage or + * {@code null} otherwise + * @throws IllegalArgumentException if the usage is invalid + * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} + */ + @Nullable + public VolumeShaper.Configuration getFadeInVolumeShaperConfigForUsage(int usage) { + ensureFadingIsEnabled(); + validateUsage(usage); + return getVolumeShaperConfigFromWrapper(mUsageToFadeWrapperMap.get(usage), + /* isFadeIn= */ true); + } + + /** + * Get the duration used to fade out players with {@link android.media.AudioAttributes} + * + * @param audioAttributes {@link android.media.AudioAttributes} + * @return duration in milliseconds if set for the audio attributes or + * {@link #DURATION_NOT_SET} otherwise + * @throws NullPointerException if the audio attributes is {@code null} + * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} + */ + public long getFadeOutDurationForAudioAttributes(@NonNull AudioAttributes audioAttributes) { + ensureFadingIsEnabled(); + return getDurationForVolumeShaperConfig(getVolumeShaperConfigFromWrapper( + mAttrToFadeWrapperMap.get(audioAttributes), /* isFadeIn= */ false)); + } + + /** + * Get the duration used to fade-in players with {@link android.media.AudioAttributes} + * + * @param audioAttributes {@link android.media.AudioAttributes} + * @return duration in milliseconds if set for the audio attributes or + * {@link #DURATION_NOT_SET} otherwise + * @throws NullPointerException if the audio attributes is {@code null} + * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} + */ + public long getFadeInDurationForAudioAttributes(@NonNull AudioAttributes audioAttributes) { + ensureFadingIsEnabled(); + return getDurationForVolumeShaperConfig(getVolumeShaperConfigFromWrapper( + mAttrToFadeWrapperMap.get(audioAttributes), /* isFadeIn= */ true)); + } + + /** + * Get the {@link android.media.VolumeShaper.Configuration} used to fade out players with + * {@link android.media.AudioAttributes} + * + * @param audioAttributes {@link android.media.AudioAttributes} + * @return {@link android.media.VolumeShaper.Configuration} if set for the audio attribute or + * {@code null} otherwise + * @throws NullPointerException if the audio attributes is {@code null} + * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} + */ + @Nullable + public VolumeShaper.Configuration getFadeOutVolumeShaperConfigForAudioAttributes( + @NonNull AudioAttributes audioAttributes) { + Objects.requireNonNull(audioAttributes, "Audio attributes cannot be null"); + ensureFadingIsEnabled(); + return getVolumeShaperConfigFromWrapper(mAttrToFadeWrapperMap.get(audioAttributes), + /* isFadeIn= */ false); + } + + /** + * Get the {@link android.media.VolumeShaper.Configuration} used to fade out players with + * {@link android.media.AudioAttributes} + * + * @param audioAttributes {@link android.media.AudioAttributes} + * @return {@link android.media.VolumeShaper.Configuration} used for fading in if set for the + * audio attribute or {@code null} otherwise + * @throws NullPointerException if the audio attributes is {@code null} + * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} + */ + @Nullable + public VolumeShaper.Configuration getFadeInVolumeShaperConfigForAudioAttributes( + @NonNull AudioAttributes audioAttributes) { + Objects.requireNonNull(audioAttributes, "Audio attributes cannot be null"); + ensureFadingIsEnabled(); + return getVolumeShaperConfigFromWrapper(mAttrToFadeWrapperMap.get(audioAttributes), + /* isFadeIn= */ true); + } + + /** + * Get the list of {@link android.media.AudioAttributes} for whome the volume shaper + * configurations are defined + * + * @return list of {@link android.media.AudioAttributes} with valid volume shaper configs or + * empty list if none set. + */ + @NonNull + public List<AudioAttributes> getAudioAttributesWithVolumeShaperConfigs() { + return getAudioAttributesInternal(); + } + + /** + * Get the delay after which the offending players are faded back in + * + * @return delay in milliseconds + */ + public long getFadeInDelayForOffenders() { + return mFadeInDelayForOffendersMillis; + } + + /** + * Query if fade is enabled + * + * @return {@code true} if fading is enabled, {@code false} otherwise + */ + public boolean isFadeEnabled() { + return mFadeState != FADE_STATE_DISABLED; + } + + /** + * Query if the usage is fadeable + * + * @param usage the {@link android.media.AudioAttributes.AttributeUsage} + * @return {@code true} if usage is fadeable, {@code false} otherwise + */ + public boolean isUsageFadeable(@AudioAttributes.AttributeUsage int usage) { + if (!isFadeEnabled()) { + return false; + } + return mFadeableUsages.contains(usage); + } + + /** + * Query if the content type is unfadeable + * + * @param contentType the {@link android.media.AudioAttributes.AttributeContentType} + * @return {@code true} if content type is unfadeable or if fade state is set to + * {@link #FADE_STATE_DISABLED}, {@code false} otherwise + */ + public boolean isContentTypeUnfadeable(@AudioAttributes.AttributeContentType int contentType) { + if (!isFadeEnabled()) { + return true; + } + return mUnfadeableContentTypes.contains(contentType); + } + + /** + * Query if the player type is unfadeable + * + * @param playerType the {@link android.media.AudioPlaybackConfiguration} player type + * @return {@code true} if player type is unfadeable or if fade state is set to + * {@link #FADE_STATE_DISABLED}, {@code false} otherwise + */ + public boolean isPlayerTypeUnfadeable(int playerType) { + if (!isFadeEnabled()) { + return true; + } + return mUnfadeablePlayerTypes.contains(playerType); + } + + /** + * Query if the audio attributes is unfadeable + * + * @param audioAttributes the {@link android.media.AudioAttributes} + * @return {@code true} if audio attributes is unfadeable or if fade state is set to + * {@link #FADE_STATE_DISABLED}, {@code false} otherwise + * @throws NullPointerException if the audio attributes is {@code null} + */ + public boolean isAudioAttributesUnfadeable(@NonNull AudioAttributes audioAttributes) { + Objects.requireNonNull(audioAttributes, "Audio attributes cannot be null"); + if (!isFadeEnabled()) { + return true; + } + return mUnfadeableAudioAttributes.contains(audioAttributes); + } + + /** + * Query if the uid is unfadeable + * + * @param uid the uid of application + * @return {@code true} if uid is unfadeable or if fade state is set to + * {@link #FADE_STATE_DISABLED}, {@code false} otherwise + */ + public boolean isUidUnfadeable(int uid) { + if (!isFadeEnabled()) { + return true; + } + return mUnfadeableUids.contains(uid); + } + + @Override + public String toString() { + return "FadeManagerConfiguration { fade state = " + fadeStateToString(mFadeState) + + ", fade out duration = " + mFadeOutDurationMillis + + ", fade in duration = " + mFadeInDurationMillis + + ", offenders fade in delay = " + mFadeInDelayForOffendersMillis + + ", fade volume shapers for audio attributes = " + mAttrToFadeWrapperMap + + ", fadeable usages = " + mFadeableUsages.toString() + + ", unfadeable content types = " + mUnfadeableContentTypes.toString() + + ", unfadeable player types = " + mUnfadeablePlayerTypes.toString() + + ", unfadeable uids = " + mUnfadeableUids.toString() + + ", unfadeable audio attributes = " + mUnfadeableAudioAttributes + "}"; + } + + /** + * Convert fade state into a human-readable string + * + * @param fadeState one of the fade state in {@link FadeStateEnum} + * @return human-readable string + */ + @NonNull + public static String fadeStateToString(@FadeStateEnum int fadeState) { + switch (fadeState) { + case FADE_STATE_DISABLED: + return "FADE_STATE_DISABLED"; + case FADE_STATE_ENABLED_DEFAULT: + return "FADE_STATE_ENABLED_DEFAULT"; + case FADE_STATE_ENABLED_AUTO: + return "FADE_STATE_ENABLED_AUTO"; + default: + return "unknown fade state: " + fadeState; + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof FadeManagerConfiguration)) { + return false; + } + + FadeManagerConfiguration rhs = (FadeManagerConfiguration) o; + + return mUsageToFadeWrapperMap.contentEquals(rhs.mUsageToFadeWrapperMap) + && mAttrToFadeWrapperMap.equals(rhs.mAttrToFadeWrapperMap) + && Arrays.equals(mFadeableUsages.toArray(), rhs.mFadeableUsages.toArray()) + && Arrays.equals(mUnfadeableContentTypes.toArray(), + rhs.mUnfadeableContentTypes.toArray()) + && Arrays.equals(mUnfadeablePlayerTypes.toArray(), + rhs.mUnfadeablePlayerTypes.toArray()) + && Arrays.equals(mUnfadeableUids.toArray(), rhs.mUnfadeableUids.toArray()) + && mUnfadeableAudioAttributes.equals(rhs.mUnfadeableAudioAttributes) + && mFadeState == rhs.mFadeState + && mFadeOutDurationMillis == rhs.mFadeOutDurationMillis + && mFadeInDurationMillis == rhs.mFadeInDurationMillis + && mFadeInDelayForOffendersMillis == rhs.mFadeInDelayForOffendersMillis; + } + + @Override + public int hashCode() { + return Objects.hash(mUsageToFadeWrapperMap, mAttrToFadeWrapperMap, mFadeableUsages, + mUnfadeableContentTypes, mUnfadeablePlayerTypes, mUnfadeableAudioAttributes, + mUnfadeableUids, mFadeState, mFadeOutDurationMillis, mFadeInDurationMillis, + mFadeInDelayForOffendersMillis); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mFadeState); + dest.writeLong(mFadeOutDurationMillis); + dest.writeLong(mFadeInDurationMillis); + dest.writeLong(mFadeInDelayForOffendersMillis); + dest.writeTypedSparseArray(mUsageToFadeWrapperMap, flags); + dest.writeMap(mAttrToFadeWrapperMap); + dest.writeIntArray(mFadeableUsages.toArray()); + dest.writeIntArray(mUnfadeableContentTypes.toArray()); + dest.writeIntArray(mUnfadeablePlayerTypes.toArray()); + dest.writeIntArray(mUnfadeableUids.toArray()); + dest.writeTypedList(mUnfadeableAudioAttributes, flags); + } + + /** + * Creates fade manage configuration from parcel + * + * @hide + */ + @VisibleForTesting() + FadeManagerConfiguration(Parcel in) { + int fadeState = in.readInt(); + long fadeOutDurationMillis = in.readLong(); + long fadeInDurationMillis = in.readLong(); + long fadeInDelayForOffenders = in.readLong(); + SparseArray<FadeVolumeShaperConfigsWrapper> usageToWrapperMap = + in.createTypedSparseArray(FadeVolumeShaperConfigsWrapper.CREATOR); + ArrayMap<AudioAttributes, FadeVolumeShaperConfigsWrapper> attrToFadeWrapperMap = + new ArrayMap<>(); + in.readMap(attrToFadeWrapperMap, getClass().getClassLoader(), AudioAttributes.class, + FadeVolumeShaperConfigsWrapper.class); + int[] fadeableUsages = in.createIntArray(); + int[] unfadeableContentTypes = in.createIntArray(); + int[] unfadeablePlayerTypes = in.createIntArray(); + int[] unfadeableUids = in.createIntArray(); + List<AudioAttributes> unfadeableAudioAttributes = new ArrayList<>(); + in.readTypedList(unfadeableAudioAttributes, AudioAttributes.CREATOR); + + this.mFadeState = fadeState; + this.mFadeOutDurationMillis = fadeOutDurationMillis; + this.mFadeInDurationMillis = fadeInDurationMillis; + this.mFadeInDelayForOffendersMillis = fadeInDelayForOffenders; + this.mUsageToFadeWrapperMap = usageToWrapperMap; + this.mAttrToFadeWrapperMap = attrToFadeWrapperMap; + this.mFadeableUsages = IntArray.wrap(fadeableUsages); + this.mUnfadeableContentTypes = IntArray.wrap(unfadeableContentTypes); + this.mUnfadeablePlayerTypes = IntArray.wrap(unfadeablePlayerTypes); + this.mUnfadeableUids = IntArray.wrap(unfadeableUids); + this.mUnfadeableAudioAttributes = unfadeableAudioAttributes; + } + + @NonNull + public static final Creator<FadeManagerConfiguration> CREATOR = new Creator<>() { + @Override + @NonNull + public FadeManagerConfiguration createFromParcel(@NonNull Parcel in) { + return new FadeManagerConfiguration(in); + } + + @Override + @NonNull + public FadeManagerConfiguration[] newArray(int size) { + return new FadeManagerConfiguration[size]; + } + }; + + private long getDurationForVolumeShaperConfig(VolumeShaper.Configuration config) { + return config != null ? config.getDuration() : DURATION_NOT_SET; + } + + private VolumeShaper.Configuration getVolumeShaperConfigFromWrapper( + FadeVolumeShaperConfigsWrapper wrapper, boolean isFadeIn) { + // if no volume shaper config is available, return null + if (wrapper == null) { + return null; + } + if (isFadeIn) { + return wrapper.getFadeInVolShaperConfig(); + } + return wrapper.getFadeOutVolShaperConfig(); + } + + private List<AudioAttributes> getAudioAttributesInternal() { + List<AudioAttributes> attrs = new ArrayList<>(mAttrToFadeWrapperMap.size()); + for (int index = 0; index < mAttrToFadeWrapperMap.size(); index++) { + attrs.add(mAttrToFadeWrapperMap.keyAt(index)); + } + return attrs; + } + + private static boolean isUsageValid(int usage) { + return AudioAttributes.isSdkUsage(usage) || AudioAttributes.isSystemUsage(usage); + } + + private void ensureFadingIsEnabled() { + if (!isFadeEnabled()) { + throw new IllegalStateException("Method call not allowed when fade is disabled"); + } + } + + private static void validateUsage(int usage) { + Preconditions.checkArgument(isUsageValid(usage), "Invalid usage: %s", usage); + } + + private static IntArray convertIntegerListToIntArray(List<Integer> integerList) { + if (integerList == null) { + return new IntArray(); + } + + IntArray intArray = new IntArray(integerList.size()); + for (int index = 0; index < integerList.size(); index++) { + intArray.add(integerList.get(index)); + } + return intArray; + } + + private static List<Integer> convertIntArrayToIntegerList(IntArray intArray) { + if (intArray == null) { + return new ArrayList<>(); + } + + ArrayList<Integer> integerArrayList = new ArrayList<>(intArray.size()); + for (int index = 0; index < intArray.size(); index++) { + integerArrayList.add(intArray.get(index)); + } + return integerArrayList; + } + + /** + * Builder class for {@link FadeManagerConfiguration} objects. + * + * <p><b>Notes:</b> + * <ul> + * <li>When fade state is set to enabled, the builder expects at least one valid usage to be + * set/added. Failure to do so will result in an exception during {@link #build()}</li> + * <li>Every usage added to the fadeable list should have corresponding volume shaper + * configs defined. This can be achieved by setting either the duration or volume shaper + * config through {@link #setFadeOutDurationForUsage(int, long)} or + * {@link #setFadeOutVolumeShaperConfigForUsage(int, VolumeShaper.Configuration)}</li> + * <li> It is recommended to set volume shaper configurations individually for fade out and + * fade in</li> + * <li>For any incomplete volume shaper configs a volume shaper configuration will be + * created using either the default fade durations or the ones provided as part of the + * {@link #Builder(long, long)}</li> + * <li>Additional volume shaper configs can also configured for a given usage + * with additional attributes like content-type in order to achieve finer fade controls. + * See: + * {@link #setFadeOutVolumeShaperConfigForAudioAttributes(AudioAttributes, + * VolumeShaper.Configuration)} and + * {@link #setFadeInVolumeShaperConfigForAudioAttributes(AudioAttributes, + * VolumeShaper.Configuration)} </li> + * </ul> + * + */ + @SuppressWarnings("WeakerAccess") + public static final class Builder { + private static final int INVALID_INDEX = -1; + private static final long IS_BUILDER_USED_FIELD_SET = 1 << 0; + private static final long IS_FADEABLE_USAGES_FIELD_SET = 1 << 1; + private static final long IS_UNFADEABLE_CONTENT_TYPE_FIELD_SET = 1 << 2; + + /** duration of the fade out curve */ + private static final long DEFAULT_FADE_OUT_DURATION_MS = 2_000; + /** duration of the fade in curve */ + private static final long DEFAULT_FADE_IN_DURATION_MS = 1_000; + + /** + * delay after which a faded out player will be faded back in. This will be heard by the + * user only in the case of unmuting players that didn't respect audio focus and didn't + * stop/pause when their app lost focus. + * This is the amount of time between the app being notified of the focus loss + * (when its muted by the fade out), and the time fade in (to unmute) starts + */ + private static final long DEFAULT_DELAY_FADE_IN_OFFENDERS_MS = 2_000; + + + private static final IntArray DEFAULT_UNFADEABLE_PLAYER_TYPES = IntArray.wrap(new int[]{ + AudioPlaybackConfiguration.PLAYER_TYPE_AAUDIO, + AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL + }); + + private static final IntArray DEFAULT_UNFADEABLE_CONTENT_TYPES = IntArray.wrap(new int[]{ + AudioAttributes.CONTENT_TYPE_SPEECH + }); + + private static final IntArray DEFAULT_FADEABLE_USAGES = IntArray.wrap(new int[]{ + AudioAttributes.USAGE_GAME, + AudioAttributes.USAGE_MEDIA + }); + + private int mFadeState = FADE_STATE_ENABLED_DEFAULT; + private long mFadeInDelayForOffendersMillis = DEFAULT_DELAY_FADE_IN_OFFENDERS_MS; + private long mFadeOutDurationMillis; + private long mFadeInDurationMillis; + private long mBuilderFieldsSet; + private SparseArray<FadeVolumeShaperConfigsWrapper> mUsageToFadeWrapperMap = + new SparseArray<>(); + private ArrayMap<AudioAttributes, FadeVolumeShaperConfigsWrapper> mAttrToFadeWrapperMap = + new ArrayMap<>(); + private IntArray mFadeableUsages = new IntArray(); + private IntArray mUnfadeableContentTypes = new IntArray(); + // Player types are not yet configurable + private IntArray mUnfadeablePlayerTypes = DEFAULT_UNFADEABLE_PLAYER_TYPES; + private IntArray mUnfadeableUids = new IntArray(); + private List<AudioAttributes> mUnfadeableAudioAttributes = new ArrayList<>(); + + /** + * Constructs a new Builder with default fade out and fade in durations + */ + public Builder() { + mFadeOutDurationMillis = DEFAULT_FADE_OUT_DURATION_MS; + mFadeInDurationMillis = DEFAULT_FADE_IN_DURATION_MS; + } + + /** + * Constructs a new Builder with the provided fade out and fade in durations + * + * @param fadeOutDurationMillis duration in milliseconds used for fading out + * @param fadeInDurationMills duration in milliseconds used for fading in + */ + public Builder(long fadeOutDurationMillis, long fadeInDurationMills) { + mFadeOutDurationMillis = fadeOutDurationMillis; + mFadeInDurationMillis = fadeInDurationMills; + } + + /** + * Constructs a new Builder from the given {@link FadeManagerConfiguration} + * + * @param fmc the {@link FadeManagerConfiguration} object whose data will be reused in the + * new builder + */ + public Builder(@NonNull FadeManagerConfiguration fmc) { + mFadeState = fmc.mFadeState; + mUsageToFadeWrapperMap = fmc.mUsageToFadeWrapperMap.clone(); + mAttrToFadeWrapperMap = new ArrayMap<AudioAttributes, FadeVolumeShaperConfigsWrapper>( + fmc.mAttrToFadeWrapperMap); + mFadeableUsages = fmc.mFadeableUsages.clone(); + setFlag(IS_FADEABLE_USAGES_FIELD_SET); + mUnfadeableContentTypes = fmc.mUnfadeableContentTypes.clone(); + setFlag(IS_UNFADEABLE_CONTENT_TYPE_FIELD_SET); + mUnfadeablePlayerTypes = fmc.mUnfadeablePlayerTypes.clone(); + mUnfadeableUids = fmc.mUnfadeableUids.clone(); + mUnfadeableAudioAttributes = new ArrayList<>(fmc.mUnfadeableAudioAttributes); + mFadeOutDurationMillis = fmc.mFadeOutDurationMillis; + mFadeInDurationMillis = fmc.mFadeInDurationMillis; + } + + /** + * Set the overall fade state + * + * @param state one of the {@link FadeStateEnum} states + * @return the same Builder instance + * @throws IllegalArgumentException if the fade state is invalid + * @see #getFadeState() + */ + @NonNull + public Builder setFadeState(@FadeStateEnum int state) { + validateFadeState(state); + mFadeState = state; + return this; + } + + /** + * Set the {@link android.media.VolumeShaper.Configuration} used to fade out players with + * {@link android.media.AudioAttributes.AttributeUsage} + * <p> + * This method accepts {@code null} for volume shaper config to clear a previously set + * configuration (example, if set through + * {@link #Builder(android.media.FadeManagerConfiguration)}) + * + * @param usage the {@link android.media.AudioAttributes.AttributeUsage} of target player + * @param fadeOutVShaperConfig the {@link android.media.VolumeShaper.Configuration} used + * to fade out players with usage + * @return the same Builder instance + * @throws IllegalArgumentException if the usage is invalid + * @see #getFadeOutVolumeShaperConfigForUsage(int) + */ + @NonNull + public Builder setFadeOutVolumeShaperConfigForUsage(int usage, + @Nullable VolumeShaper.Configuration fadeOutVShaperConfig) { + validateUsage(usage); + getFadeVolShaperConfigWrapperForUsage(usage) + .setFadeOutVolShaperConfig(fadeOutVShaperConfig); + cleanupInactiveWrapperEntries(usage); + return this; + } + + /** + * Set the {@link android.media.VolumeShaper.Configuration} used to fade in players with + * {@link android.media.AudioAttributes.AttributeUsage} + * <p> + * This method accepts {@code null} for volume shaper config to clear a previously set + * configuration (example, if set through + * {@link #Builder(android.media.FadeManagerConfiguration)}) + * + * @param usage the {@link android.media.AudioAttributes.AttributeUsage} + * @param fadeInVShaperConfig the {@link android.media.VolumeShaper.Configuration} used + * to fade in players with usage + * @return the same Builder instance + * @throws IllegalArgumentException if the usage is invalid + * @see #getFadeInVolumeShaperConfigForUsage(int) + */ + @NonNull + public Builder setFadeInVolumeShaperConfigForUsage(int usage, + @Nullable VolumeShaper.Configuration fadeInVShaperConfig) { + validateUsage(usage); + getFadeVolShaperConfigWrapperForUsage(usage) + .setFadeInVolShaperConfig(fadeInVShaperConfig); + cleanupInactiveWrapperEntries(usage); + return this; + } + + /** + * Set the duration used for fading out players with + * {@link android.media.AudioAttributes.AttributeUsage} + * <p> + * A Volume shaper configuration is generated with the provided duration and default + * volume curve definitions. This config is then used to fade out players with given usage. + * <p> + * In order to clear previously set duration (example, if set through + * {@link #Builder(android.media.FadeManagerConfiguration)}), this method accepts + * {@link #DURATION_NOT_SET} and sets the corresponding fade out volume shaper config to + * {@code null} + * + * @param usage the {@link android.media.AudioAttributes.AttributeUsage} of target player + * @param fadeOutDurationMillis positive duration in milliseconds or + * {@link #DURATION_NOT_SET} + * @return the same Builder instance + * @throws IllegalArgumentException if the fade out duration is non-positive with the + * exception of {@link #DURATION_NOT_SET} + * @see #setFadeOutVolumeShaperConfigForUsage(int, VolumeShaper.Configuration) + * @see #getFadeOutDurationForUsage(int) + */ + @NonNull + public Builder setFadeOutDurationForUsage(int usage, long fadeOutDurationMillis) { + validateUsage(usage); + VolumeShaper.Configuration fadeOutVShaperConfig = + createVolShaperConfigForDuration(fadeOutDurationMillis, /* isFadeIn= */ false); + setFadeOutVolumeShaperConfigForUsage(usage, fadeOutVShaperConfig); + return this; + } + + /** + * Set the duration used for fading in players with + * {@link android.media.AudioAttributes.AttributeUsage} + * <p> + * A Volume shaper configuration is generated with the provided duration and default + * volume curve definitions. This config is then used to fade in players with given usage. + * <p> + * <b>Note: </b>In order to clear previously set duration (example, if set through + * {@link #Builder(android.media.FadeManagerConfiguration)}), this method accepts + * {@link #DURATION_NOT_SET} and sets the corresponding fade in volume shaper config to + * {@code null} + * + * @param usage the {@link android.media.AudioAttributes.AttributeUsage} of target player + * @param fadeInDurationMillis positive duration in milliseconds or + * {@link #DURATION_NOT_SET} + * @return the same Builder instance + * @throws IllegalArgumentException if the fade in duration is non-positive with the + * exception of {@link #DURATION_NOT_SET} + * @see #setFadeInVolumeShaperConfigForUsage(int, VolumeShaper.Configuration) + * @see #getFadeInDurationForUsage(int) + */ + @NonNull + public Builder setFadeInDurationForUsage(int usage, long fadeInDurationMillis) { + validateUsage(usage); + VolumeShaper.Configuration fadeInVShaperConfig = + createVolShaperConfigForDuration(fadeInDurationMillis, /* isFadeIn= */ true); + setFadeInVolumeShaperConfigForUsage(usage, fadeInVShaperConfig); + return this; + } + + /** + * Set the {@link android.media.VolumeShaper.Configuration} used to fade out players with + * {@link android.media.AudioAttributes} + * <p> + * This method accepts {@code null} for volume shaper config to clear a previously set + * configuration (example, set through + * {@link #Builder(android.media.FadeManagerConfiguration)}) + * + * @param audioAttributes the {@link android.media.AudioAttributes} + * @param fadeOutVShaperConfig the {@link android.media.VolumeShaper.Configuration} used to + * fade out players with audio attribute + * @return the same Builder instance + * @throws NullPointerException if the audio attributes is {@code null} + * @see #getFadeOutVolumeShaperConfigForAudioAttributes(AudioAttributes) + */ + @NonNull + public Builder setFadeOutVolumeShaperConfigForAudioAttributes( + @NonNull AudioAttributes audioAttributes, + @Nullable VolumeShaper.Configuration fadeOutVShaperConfig) { + Objects.requireNonNull(audioAttributes, "Audio attribute cannot be null"); + getFadeVolShaperConfigWrapperForAttr(audioAttributes) + .setFadeOutVolShaperConfig(fadeOutVShaperConfig); + cleanupInactiveWrapperEntries(audioAttributes); + return this; + } + + /** + * Set the {@link android.media.VolumeShaper.Configuration} used to fade in players with + * {@link android.media.AudioAttributes} + * + * <p>This method accepts {@code null} for volume shaper config to clear a previously set + * configuration (example, set through + * {@link #Builder(android.media.FadeManagerConfiguration)}) + * + * @param audioAttributes the {@link android.media.AudioAttributes} + * @param fadeInVShaperConfig the {@link android.media.VolumeShaper.Configuration} used to + * fade in players with audio attribute + * @return the same Builder instance + * @throws NullPointerException if the audio attributes is {@code null} + * @see #getFadeInVolumeShaperConfigForAudioAttributes(AudioAttributes) + */ + @NonNull + public Builder setFadeInVolumeShaperConfigForAudioAttributes( + @NonNull AudioAttributes audioAttributes, + @Nullable VolumeShaper.Configuration fadeInVShaperConfig) { + Objects.requireNonNull(audioAttributes, "Audio attribute cannot be null"); + getFadeVolShaperConfigWrapperForAttr(audioAttributes) + .setFadeInVolShaperConfig(fadeInVShaperConfig); + cleanupInactiveWrapperEntries(audioAttributes); + return this; + } + + /** + * Set the duration used for fading out players of type + * {@link android.media.AudioAttributes}. + * <p> + * A Volume shaper configuration is generated with the provided duration and default + * volume curve definitions. This config is then used to fade out players with given usage. + * <p> + * <b>Note: </b>In order to clear previously set duration (example, if set through + * {@link #Builder(android.media.FadeManagerConfiguration)}), this method accepts + * {@link #DURATION_NOT_SET} and sets the corresponding fade out volume shaper config to + * {@code null} + * + * @param audioAttributes the {@link android.media.AudioAttributes} for which the fade out + * duration will be set/updated/reset + * @param fadeOutDurationMillis positive duration in milliseconds or + * {@link #DURATION_NOT_SET} + * @return the same Builder instance + * @throws IllegalArgumentException if the fade out duration is non-positive with the + * exception of {@link #DURATION_NOT_SET} + * @see #getFadeOutDurationForAudioAttributes(AudioAttributes) + * @see #setFadeOutVolumeShaperConfigForAudioAttributes(AudioAttributes, + * VolumeShaper.Configuration) + */ + @NonNull + public Builder setFadeOutDurationForAudioAttributes( + @NonNull AudioAttributes audioAttributes, + long fadeOutDurationMillis) { + Objects.requireNonNull(audioAttributes, "Audio attribute cannot be null"); + VolumeShaper.Configuration fadeOutVShaperConfig = + createVolShaperConfigForDuration(fadeOutDurationMillis, /* isFadeIn= */ false); + setFadeOutVolumeShaperConfigForAudioAttributes(audioAttributes, fadeOutVShaperConfig); + return this; + } + + /** + * Set the duration used for fading in players of type + * {@link android.media.AudioAttributes}. + * <p> + * A Volume shaper configuration is generated with the provided duration and default + * volume curve definitions. This config is then used to fade in players with given usage. + * <p> + * <b>Note: </b>In order to clear previously set duration (example, if set through + * {@link #Builder(android.media.FadeManagerConfiguration)}), this method accepts + * {@link #DURATION_NOT_SET} and sets the corresponding fade in volume shaper config to + * {@code null} + * + * @param audioAttributes the {@link android.media.AudioAttributes} for which the fade in + * duration will be set/updated/reset + * @param fadeInDurationMillis positive duration in milliseconds or + * {@link #DURATION_NOT_SET} + * @return the same Builder instance + * @throws IllegalArgumentException if the fade in duration is non-positive with the + * exception of {@link #DURATION_NOT_SET} + * @see #getFadeInDurationForAudioAttributes(AudioAttributes) + * @see #setFadeInVolumeShaperConfigForAudioAttributes(AudioAttributes, + * VolumeShaper.Configuration) + */ + @NonNull + public Builder setFadeInDurationForAudioAttributes(@NonNull AudioAttributes audioAttributes, + long fadeInDurationMillis) { + Objects.requireNonNull(audioAttributes, "Audio attribute cannot be null"); + VolumeShaper.Configuration fadeInVShaperConfig = + createVolShaperConfigForDuration(fadeInDurationMillis, /* isFadeIn= */ true); + setFadeInVolumeShaperConfigForAudioAttributes(audioAttributes, fadeInVShaperConfig); + return this; + } + + /** + * Set the list of {@link android.media.AudioAttributes.AttributeUsage} that can be faded + * + * <p>This is a positive list. Players with matching usage will be considered for fading. + * Usages that are not part of this list will not be faded + * + * <p>Passing an empty list as input clears the existing list. This can be used to + * reset the list when using a copy constructor + * + * <p><b>Warning:</b> When fade state is set to enabled, the builder expects at least one + * usage to be set/added. Failure to do so will result in an exception during + * {@link #build()} + * + * @param usages List of the {@link android.media.AudioAttributes.AttributeUsage} + * @return the same Builder instance + * @throws IllegalArgumentException if the usages are invalid + * @throws NullPointerException if the usage list is {@code null} + * @see #getFadeableUsages() + */ + @NonNull + public Builder setFadeableUsages(@NonNull List<Integer> usages) { + Objects.requireNonNull(usages, "List of usages cannot be null"); + validateUsages(usages); + setFlag(IS_FADEABLE_USAGES_FIELD_SET); + mFadeableUsages.clear(); + mFadeableUsages.addAll(convertIntegerListToIntArray(usages)); + return this; + } + + /** + * Add the {@link android.media.AudioAttributes.AttributeUsage} to the fadeable list + * + * @param usage the {@link android.media.AudioAttributes.AttributeUsage} + * @return the same Builder instance + * @throws IllegalArgumentException if the usage is invalid + * @see #getFadeableUsages() + * @see #setFadeableUsages(List) + */ + @NonNull + public Builder addFadeableUsage(@AudioAttributes.AttributeUsage int usage) { + validateUsage(usage); + setFlag(IS_FADEABLE_USAGES_FIELD_SET); + if (!mFadeableUsages.contains(usage)) { + mFadeableUsages.add(usage); + } + return this; + } + + /** + * Remove the {@link android.media.AudioAttributes.AttributeUsage} from the fadeable list + * <p> + * Players of this usage type will not be faded. + * + * @param usage the {@link android.media.AudioAttributes.AttributeUsage} + * @return the same Builder instance + * @throws IllegalArgumentException if the usage is invalid + * @see #getFadeableUsages() + * @see #setFadeableUsages(List) + */ + @NonNull + public Builder clearFadeableUsage(@AudioAttributes.AttributeUsage int usage) { + validateUsage(usage); + setFlag(IS_FADEABLE_USAGES_FIELD_SET); + int index = mFadeableUsages.indexOf(usage); + if (index != INVALID_INDEX) { + mFadeableUsages.remove(index); + } + return this; + } + + /** + * Set the list of {@link android.media.AudioAttributes.AttributeContentType} that can not + * be faded + * + * <p>This is a negative list. Players with matching content type of this list will not be + * faded. Content types that are not part of this list will be considered for fading. + * + * <p>Passing an empty list as input clears the existing list. This can be used to + * reset the list when using a copy constructor + * + * @param contentTypes list of {@link android.media.AudioAttributes.AttributeContentType} + * @return the same Builder instance + * @throws IllegalArgumentException if the content types are invalid + * @throws NullPointerException if the content type list is {@code null} + * @see #getUnfadeableContentTypes() + */ + @NonNull + public Builder setUnfadeableContentTypes(@NonNull List<Integer> contentTypes) { + Objects.requireNonNull(contentTypes, "List of content types cannot be null"); + validateContentTypes(contentTypes); + setFlag(IS_UNFADEABLE_CONTENT_TYPE_FIELD_SET); + mUnfadeableContentTypes.clear(); + mUnfadeableContentTypes.addAll(convertIntegerListToIntArray(contentTypes)); + return this; + } + + /** + * Add the {@link android.media.AudioAttributes.AttributeContentType} to unfadeable list + * + * @param contentType the {@link android.media.AudioAttributes.AttributeContentType} + * @return the same Builder instance + * @throws IllegalArgumentException if the content type is invalid + * @see #setUnfadeableContentTypes(List) + * @see #getUnfadeableContentTypes() + */ + @NonNull + public Builder addUnfadeableContentType( + @AudioAttributes.AttributeContentType int contentType) { + validateContentType(contentType); + setFlag(IS_UNFADEABLE_CONTENT_TYPE_FIELD_SET); + if (!mUnfadeableContentTypes.contains(contentType)) { + mUnfadeableContentTypes.add(contentType); + } + return this; + } + + /** + * Remove the {@link android.media.AudioAttributes.AttributeContentType} from the + * unfadeable list + * + * @param contentType the {@link android.media.AudioAttributes.AttributeContentType} + * @return the same Builder instance + * @throws IllegalArgumentException if the content type is invalid + * @see #setUnfadeableContentTypes(List) + * @see #getUnfadeableContentTypes() + */ + @NonNull + public Builder clearUnfadeableContentType( + @AudioAttributes.AttributeContentType int contentType) { + validateContentType(contentType); + setFlag(IS_UNFADEABLE_CONTENT_TYPE_FIELD_SET); + int index = mUnfadeableContentTypes.indexOf(contentType); + if (index != INVALID_INDEX) { + mUnfadeableContentTypes.remove(index); + } + return this; + } + + /** + * Set the uids that cannot be faded + * + * <p>This is a negative list. Players with matching uid of this list will not be faded. + * Uids that are not part of this list shall be considered for fading + * + * <p>Passing an empty list as input clears the existing list. This can be used to + * reset the list when using a copy constructor + * + * @param uids list of uids + * @return the same Builder instance + * @throws NullPointerException if the uid list is {@code null} + * @see #getUnfadeableUids() + */ + @NonNull + public Builder setUnfadeableUids(@NonNull List<Integer> uids) { + Objects.requireNonNull(uids, "List of uids cannot be null"); + mUnfadeableUids.clear(); + mUnfadeableUids.addAll(convertIntegerListToIntArray(uids)); + return this; + } + + /** + * Add uid to unfadeable list + * + * @param uid client uid + * @return the same Builder instance + * @see #setUnfadeableUids(List) + * @see #getUnfadeableUids() + */ + @NonNull + public Builder addUnfadeableUid(int uid) { + if (!mUnfadeableUids.contains(uid)) { + mUnfadeableUids.add(uid); + } + return this; + } + + /** + * Remove the uid from unfadeable list + * + * @param uid client uid + * @return the same Builder instance + * @see #setUnfadeableUids(List) + * @see #getUnfadeableUids() + */ + @NonNull + public Builder clearUnfadeableUid(int uid) { + int index = mUnfadeableUids.indexOf(uid); + if (index != INVALID_INDEX) { + mUnfadeableUids.remove(index); + } + return this; + } + + /** + * Set the list of {@link android.media.AudioAttributes} that can not be faded + * + * <p>This is a negative list. Players with matching audio attributes of this list will not + * be faded. Audio attributes that are not part of this list shall be considered for fading. + * + * <p>Passing an empty list as input clears any existing list. This can be used to + * reset the list when using a copy constructor + * + * <p><b>Note:</b> Be cautious when adding generic audio attributes into this list as it can + * negatively impact fadeability decision if such an audio attribute and corresponding + * usage fall into opposing lists. + * For example: + * <pre class=prettyprint> + * AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build() </pre> + * is a generic audio attribute for {@link android.media.AudioAttributes.USAGE_MEDIA}. + * It is an undefined behavior to have an + * {@link android.media.AudioAttributes.AttributeUsage} in the fadeable usage list and the + * corresponding generic {@link android.media.AudioAttributes} in the unfadeable list. Such + * cases will result in an exception during {@link #build()} + * + * @param attrs list of {@link android.media.AudioAttributes} + * @return the same Builder instance + * @throws NullPointerException if the audio attributes list is {@code null} + * @see #getUnfadeableAudioAttributes() + */ + @NonNull + public Builder setUnfadeableAudioAttributes(@NonNull List<AudioAttributes> attrs) { + Objects.requireNonNull(attrs, "List of audio attributes cannot be null"); + mUnfadeableAudioAttributes.clear(); + mUnfadeableAudioAttributes.addAll(attrs); + return this; + } + + /** + * Add the {@link android.media.AudioAttributes} to the unfadeable list + * + * @param audioAttributes the {@link android.media.AudioAttributes} + * @return the same Builder instance + * @throws NullPointerException if the audio attributes is {@code null} + * @see #setUnfadeableAudioAttributes(List) + * @see #getUnfadeableAudioAttributes() + */ + @NonNull + public Builder addUnfadeableAudioAttributes(@NonNull AudioAttributes audioAttributes) { + Objects.requireNonNull(audioAttributes, "Audio attributes cannot be null"); + if (!mUnfadeableAudioAttributes.contains(audioAttributes)) { + mUnfadeableAudioAttributes.add(audioAttributes); + } + return this; + } + + /** + * Remove the {@link android.media.AudioAttributes} from the unfadeable list. + * + * @param audioAttributes the {@link android.media.AudioAttributes} + * @return the same Builder instance + * @throws NullPointerException if the audio attributes is {@code null} + * @see #getUnfadeableAudioAttributes() + */ + @NonNull + public Builder clearUnfadeableAudioAttributes(@NonNull AudioAttributes audioAttributes) { + Objects.requireNonNull(audioAttributes, "Audio attributes cannot be null"); + if (mUnfadeableAudioAttributes.contains(audioAttributes)) { + mUnfadeableAudioAttributes.remove(audioAttributes); + } + return this; + } + + /** + * Set the delay after which the offending faded out player will be faded in. + * + * <p>This is the amount of time between the app being notified of the focus loss (when its + * muted by the fade out), and the time fade in (to unmute) starts + * + * @param delayMillis delay in milliseconds + * @return the same Builder instance + * @throws IllegalArgumentException if the delay is negative + * @see #getFadeInDelayForOffenders() + */ + @NonNull + public Builder setFadeInDelayForOffenders(long delayMillis) { + Preconditions.checkArgument(delayMillis >= 0, "Delay cannot be negative"); + mFadeInDelayForOffendersMillis = delayMillis; + return this; + } + + /** + * Builds the {@link FadeManagerConfiguration} with all of the fade configurations that + * have been set. + * + * @return a new {@link FadeManagerConfiguration} object + */ + @NonNull + public FadeManagerConfiguration build() { + if (!checkNotSet(IS_BUILDER_USED_FIELD_SET)) { + throw new IllegalStateException( + "This Builder should not be reused. Use a new Builder instance instead"); + } + + setFlag(IS_BUILDER_USED_FIELD_SET); + + if (checkNotSet(IS_FADEABLE_USAGES_FIELD_SET)) { + mFadeableUsages = DEFAULT_FADEABLE_USAGES; + setVolShaperConfigsForUsages(mFadeableUsages); + } + + if (checkNotSet(IS_UNFADEABLE_CONTENT_TYPE_FIELD_SET)) { + mUnfadeableContentTypes = DEFAULT_UNFADEABLE_CONTENT_TYPES; + } + + validateFadeConfigurations(); + + return new FadeManagerConfiguration(mFadeState, mFadeOutDurationMillis, + mFadeInDurationMillis, mFadeInDelayForOffendersMillis, mUsageToFadeWrapperMap, + mAttrToFadeWrapperMap, mFadeableUsages, mUnfadeableContentTypes, + mUnfadeablePlayerTypes, mUnfadeableUids, mUnfadeableAudioAttributes); + } + + private void setFlag(long flag) { + mBuilderFieldsSet |= flag; + } + + private boolean checkNotSet(long flag) { + return (mBuilderFieldsSet & flag) == 0; + } + + private FadeVolumeShaperConfigsWrapper getFadeVolShaperConfigWrapperForUsage(int usage) { + if (!mUsageToFadeWrapperMap.contains(usage)) { + mUsageToFadeWrapperMap.put(usage, new FadeVolumeShaperConfigsWrapper()); + } + return mUsageToFadeWrapperMap.get(usage); + } + + private FadeVolumeShaperConfigsWrapper getFadeVolShaperConfigWrapperForAttr( + AudioAttributes attr) { + // if no entry, create a new one for setting/clearing + if (!mAttrToFadeWrapperMap.containsKey(attr)) { + mAttrToFadeWrapperMap.put(attr, new FadeVolumeShaperConfigsWrapper()); + } + return mAttrToFadeWrapperMap.get(attr); + } + + private VolumeShaper.Configuration createVolShaperConfigForDuration(long duration, + boolean isFadeIn) { + // used to reset the volume shaper config setting + if (duration == DURATION_NOT_SET) { + return null; + } + + VolumeShaper.Configuration.Builder builder = new VolumeShaper.Configuration.Builder() + .setId(VOLUME_SHAPER_SYSTEM_FADE_ID) + .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME) + .setDuration(duration); + + if (isFadeIn) { + builder.setCurve(/* times= */ new float[]{0.f, 0.50f, 1.0f}, + /* volumes= */ new float[]{0.f, 0.30f, 1.0f}); + } else { + builder.setCurve(/* times= */ new float[]{0.f, 0.25f, 1.0f}, + /* volumes= */ new float[]{1.f, 0.65f, 0.0f}); + } + + return builder.build(); + } + + private void cleanupInactiveWrapperEntries(int usage) { + FadeVolumeShaperConfigsWrapper fmcw = mUsageToFadeWrapperMap.get(usage); + // cleanup map entry if FadeVolumeShaperConfigWrapper is inactive + if (fmcw != null && fmcw.isInactive()) { + mUsageToFadeWrapperMap.remove(usage); + } + } + + private void cleanupInactiveWrapperEntries(AudioAttributes attr) { + FadeVolumeShaperConfigsWrapper fmcw = mAttrToFadeWrapperMap.get(attr); + // cleanup map entry if FadeVolumeShaperConfigWrapper is inactive + if (fmcw != null && fmcw.isInactive()) { + mAttrToFadeWrapperMap.remove(attr); + } + } + + private void setVolShaperConfigsForUsages(IntArray usages) { + // set default volume shaper configs for fadeable usages + for (int index = 0; index < usages.size(); index++) { + setMissingVolShaperConfigsForWrapper( + getFadeVolShaperConfigWrapperForUsage(usages.get(index))); + } + } + + private void setMissingVolShaperConfigsForWrapper(FadeVolumeShaperConfigsWrapper wrapper) { + if (!wrapper.isFadeOutConfigActive()) { + wrapper.setFadeOutVolShaperConfig(createVolShaperConfigForDuration( + mFadeOutDurationMillis, /* isFadeIn= */ false)); + } + if (!wrapper.isFadeInConfigActive()) { + wrapper.setFadeInVolShaperConfig(createVolShaperConfigForDuration( + mFadeInDurationMillis, /* isFadeIn= */ true)); + } + } + + private void validateFadeState(int state) { + switch(state) { + case FADE_STATE_DISABLED: + case FADE_STATE_ENABLED_DEFAULT: + case FADE_STATE_ENABLED_AUTO: + break; + default: + throw new IllegalArgumentException("Unknown fade state: " + state); + } + } + + private void validateUsages(List<Integer> usages) { + for (int index = 0; index < usages.size(); index++) { + validateUsage(usages.get(index)); + } + } + + private void validateContentTypes(List<Integer> contentTypes) { + for (int index = 0; index < contentTypes.size(); index++) { + validateContentType(contentTypes.get(index)); + } + } + + private void validateContentType(int contentType) { + Preconditions.checkArgument(AudioAttributes.isSdkContentType(contentType), + "Invalid content type: ", contentType); + } + + private void validateFadeConfigurations() { + validateFadeableUsages(); + validateFadeVolumeShaperConfigsWrappers(); + validateUnfadeableAudioAttributes(); + } + + /** Ensure fadeable usage list meets config requirements */ + private void validateFadeableUsages() { + // ensure at least one fadeable usage + Preconditions.checkArgumentPositive(mFadeableUsages.size(), + "Fadeable usage list cannot be empty when state set to enabled"); + // ensure all fadeable usages have volume shaper configs - both fade in and out + for (int index = 0; index < mFadeableUsages.size(); index++) { + setMissingVolShaperConfigsForWrapper( + getFadeVolShaperConfigWrapperForUsage(mFadeableUsages.get(index))); + } + } + + /** Ensure Fade volume shaper config wrappers meet requirements */ + private void validateFadeVolumeShaperConfigsWrappers() { + // ensure both fade in & out volume shaper configs are defined for all wrappers + // for usages - + for (int index = 0; index < mUsageToFadeWrapperMap.size(); index++) { + setMissingVolShaperConfigsForWrapper( + getFadeVolShaperConfigWrapperForUsage(mUsageToFadeWrapperMap.keyAt(index))); + } + + // for additional audio attributes - + for (int index = 0; index < mAttrToFadeWrapperMap.size(); index++) { + setMissingVolShaperConfigsForWrapper( + getFadeVolShaperConfigWrapperForAttr(mAttrToFadeWrapperMap.keyAt(index))); + } + } + + /** Ensure Unfadeable attributes meet configuration requirements */ + private void validateUnfadeableAudioAttributes() { + // ensure no generic AudioAttributes in unfadeable list with matching usage in fadeable + // list. failure results in an undefined behavior as the audio attributes + // shall be both fadeable (because of the usage) and unfadeable at the same time. + for (int index = 0; index < mUnfadeableAudioAttributes.size(); index++) { + AudioAttributes targetAttr = mUnfadeableAudioAttributes.get(index); + int usage = targetAttr.getSystemUsage(); + boolean isFadeableUsage = mFadeableUsages.contains(usage); + // cannot have a generic audio attribute that also is a fadeable usage + Preconditions.checkArgument( + !isFadeableUsage || (isFadeableUsage && !isGeneric(targetAttr)), + "Unfadeable audio attributes cannot be generic of the fadeable usage"); + } + } + + private static boolean isGeneric(AudioAttributes attr) { + return (attr.getContentType() == AudioAttributes.CONTENT_TYPE_UNKNOWN + && attr.getFlags() == 0x0 + && attr.getBundle() == null + && attr.getTags().isEmpty()); + } + } + + private static final class FadeVolumeShaperConfigsWrapper implements Parcelable { + // null volume shaper config refers to either init state or if its cleared/reset + private @Nullable VolumeShaper.Configuration mFadeOutVolShaperConfig; + private @Nullable VolumeShaper.Configuration mFadeInVolShaperConfig; + + FadeVolumeShaperConfigsWrapper() {} + + public void setFadeOutVolShaperConfig(@Nullable VolumeShaper.Configuration fadeOutConfig) { + mFadeOutVolShaperConfig = fadeOutConfig; + } + + public void setFadeInVolShaperConfig(@Nullable VolumeShaper.Configuration fadeInConfig) { + mFadeInVolShaperConfig = fadeInConfig; + } + + /** + * Query fade out volume shaper config + * + * @return configured fade out volume shaper config or {@code null} when initialized/reset + */ + @Nullable + public VolumeShaper.Configuration getFadeOutVolShaperConfig() { + return mFadeOutVolShaperConfig; + } + + /** + * Query fade in volume shaper config + * + * @return configured fade in volume shaper config or {@code null} when initialized/reset + */ + @Nullable + public VolumeShaper.Configuration getFadeInVolShaperConfig() { + return mFadeInVolShaperConfig; + } + + /** + * Wrapper is inactive if both fade out and in configs are cleared. + * + * @return {@code true} if configs are cleared. {@code false} if either of the configs is + * set + */ + public boolean isInactive() { + return !isFadeOutConfigActive() && !isFadeInConfigActive(); + } + + boolean isFadeOutConfigActive() { + return mFadeOutVolShaperConfig != null; + } + + boolean isFadeInConfigActive() { + return mFadeInVolShaperConfig != null; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof FadeVolumeShaperConfigsWrapper)) { + return false; + } + + FadeVolumeShaperConfigsWrapper rhs = (FadeVolumeShaperConfigsWrapper) o; + + if (mFadeInVolShaperConfig == null && rhs.mFadeInVolShaperConfig == null + && mFadeOutVolShaperConfig == null && rhs.mFadeOutVolShaperConfig == null) { + return true; + } + + boolean isEqual; + if (mFadeOutVolShaperConfig != null) { + isEqual = mFadeOutVolShaperConfig.equals(rhs.mFadeOutVolShaperConfig); + } else if (rhs.mFadeOutVolShaperConfig != null) { + return false; + } else { + isEqual = true; + } + + if (mFadeInVolShaperConfig != null) { + isEqual = isEqual && mFadeInVolShaperConfig.equals(rhs.mFadeInVolShaperConfig); + } else if (rhs.mFadeInVolShaperConfig != null) { + return false; + } + + return isEqual; + } + + @Override + public int hashCode() { + return Objects.hash(mFadeOutVolShaperConfig, mFadeInVolShaperConfig); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + mFadeOutVolShaperConfig.writeToParcel(dest, flags); + mFadeInVolShaperConfig.writeToParcel(dest, flags); + } + + /** + * Creates fade volume shaper config wrapper from parcel + * + * @hide + */ + @VisibleForTesting() + FadeVolumeShaperConfigsWrapper(Parcel in) { + mFadeOutVolShaperConfig = VolumeShaper.Configuration.CREATOR.createFromParcel(in); + mFadeInVolShaperConfig = VolumeShaper.Configuration.CREATOR.createFromParcel(in); + } + + @NonNull + public static final Creator<FadeVolumeShaperConfigsWrapper> CREATOR = new Creator<>() { + @Override + @NonNull + public FadeVolumeShaperConfigsWrapper createFromParcel(@NonNull Parcel in) { + return new FadeVolumeShaperConfigsWrapper(in); + } + + @Override + @NonNull + public FadeVolumeShaperConfigsWrapper[] newArray(int size) { + return new FadeVolumeShaperConfigsWrapper[size]; + } + }; + } +} + diff --git a/media/java/android/media/flags/fade_manager_configuration.aconfig b/media/java/android/media/flags/fade_manager_configuration.aconfig new file mode 100644 index 000000000000..100e2235a7a8 --- /dev/null +++ b/media/java/android/media/flags/fade_manager_configuration.aconfig @@ -0,0 +1,8 @@ +package: "com.android.media.flags" + +flag { + namespace: "media_solutions" + name: "enable_fade_manager_configuration" + description: "Enable Fade Manager Configuration support to determine fade properties" + bug: "307354764" +}
\ No newline at end of file diff --git a/media/tests/AudioPolicyTest/Android.bp b/media/tests/AudioPolicyTest/Android.bp index 4624dfe70756..3dc2a0a9fd7c 100644 --- a/media/tests/AudioPolicyTest/Android.bp +++ b/media/tests/AudioPolicyTest/Android.bp @@ -17,6 +17,7 @@ android_test { "guava-android-testlib", "hamcrest-library", "platform-test-annotations", + "truth", ], platform_apis: true, certificate: "platform", diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java index 94df40da16c1..e9a0d3eceba3 100644 --- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java +++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java @@ -229,7 +229,7 @@ public class AudioManagerTest { @Test public void testSetGetVolumePerAttributes() { - for (int usage : AudioAttributes.SDK_USAGES) { + for (int usage : AudioAttributes.getSdkUsages()) { if (usage == AudioAttributes.USAGE_UNKNOWN) { continue; } diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioProductStrategyTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioProductStrategyTest.java index 266faae489dd..18e8608d3b4d 100644 --- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioProductStrategyTest.java +++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioProductStrategyTest.java @@ -169,7 +169,7 @@ public class AudioProductStrategyTest { assertNotNull(audioProductStrategies); assertTrue(audioProductStrategies.size() > 0); - for (int usage : AudioAttributes.SDK_USAGES) { + for (int usage : AudioAttributes.getSdkUsages()) { AudioAttributes aaForUsage = new AudioAttributes.Builder().setUsage(usage).build(); int streamTypeFromUsage = diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java new file mode 100644 index 000000000000..fb6bd489d5d0 --- /dev/null +++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java @@ -0,0 +1,795 @@ +/* + * Copyright (C) 2023 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.audiopolicytest; + +import static com.android.media.flags.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION; + +import static org.junit.Assert.assertThrows; + +import android.media.AudioAttributes; +import android.media.AudioPlaybackConfiguration; +import android.media.FadeManagerConfiguration; +import android.media.VolumeShaper; +import android.os.Parcel; +import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.google.common.truth.Expect; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Presubmit +@RunWith(AndroidJUnit4.class) +@RequiresFlagsEnabled(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION) +public final class FadeManagerConfigurationUnitTest { + private static final long DEFAULT_FADE_OUT_DURATION_MS = 2_000; + private static final long DEFAULT_FADE_IN_DURATION_MS = 1_000; + private static final long TEST_FADE_OUT_DURATION_MS = 1_500; + private static final long TEST_FADE_IN_DURATION_MS = 750; + private static final int TEST_INVALID_USAGE = -10; + private static final int TEST_INVALID_CONTENT_TYPE = 100; + private static final int TEST_INVALID_FADE_STATE = 100; + private static final long TEST_INVALID_DURATION = -10; + private static final int TEST_UID_1 = 1010001; + private static final int TEST_UID_2 = 1000; + private static final int TEST_PARCEL_FLAGS = 0; + private static final AudioAttributes TEST_MEDIA_AUDIO_ATTRIBUTE = + createAudioAttributesForUsage(AudioAttributes.USAGE_MEDIA); + private static final AudioAttributes TEST_GAME_AUDIO_ATTRIBUTE = + createAudioAttributesForUsage(AudioAttributes.USAGE_GAME); + private static final AudioAttributes TEST_NAVIGATION_AUDIO_ATTRIBUTE = + new AudioAttributes.Builder().setUsage( + AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build(); + private static final AudioAttributes TEST_ASSISTANT_AUDIO_ATTRIBUTE = + new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_ASSISTANT) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build(); + private static final List<Integer> TEST_FADEABLE_USAGES = Arrays.asList( + AudioAttributes.USAGE_MEDIA, + AudioAttributes.USAGE_GAME + ); + private static final List<Integer> TEST_UNFADEABLE_CONTENT_TYPES = Arrays.asList( + AudioAttributes.CONTENT_TYPE_SPEECH + ); + + private static final List<Integer> TEST_UNFADEABLE_PLAYER_TYPES = Arrays.asList( + AudioPlaybackConfiguration.PLAYER_TYPE_AAUDIO, + AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL + ); + private static final VolumeShaper.Configuration TEST_DEFAULT_FADE_OUT_VOLUME_SHAPER_CONFIG = + new VolumeShaper.Configuration.Builder() + .setId(FadeManagerConfiguration.VOLUME_SHAPER_SYSTEM_FADE_ID) + .setCurve(/* times= */new float[]{0.f, 0.25f, 1.0f}, + /* volumes= */new float[]{1.f, 0.65f, 0.0f}) + .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME) + .setDuration(DEFAULT_FADE_OUT_DURATION_MS) + .build(); + private static final VolumeShaper.Configuration TEST_DEFAULT_FADE_IN_VOLUME_SHAPER_CONFIG = + new VolumeShaper.Configuration.Builder() + .setId(FadeManagerConfiguration.VOLUME_SHAPER_SYSTEM_FADE_ID) + .setCurve(/* times= */new float[]{0.f, 0.50f, 1.0f}, + /* volumes= */new float[]{0.f, 0.30f, 1.0f}) + .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME) + .setDuration(DEFAULT_FADE_IN_DURATION_MS) + .build(); + private static final VolumeShaper.Configuration TEST_FADE_OUT_VOLUME_SHAPER_CONFIG = + new VolumeShaper.Configuration.Builder() + .setId(FadeManagerConfiguration.VOLUME_SHAPER_SYSTEM_FADE_ID) + .setCurve(/* times= */new float[]{0.f, 0.25f, 1.0f}, + /* volumes= */new float[]{1.f, 0.65f, 0.0f}) + .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME) + .setDuration(TEST_FADE_OUT_DURATION_MS) + .build(); + private static final VolumeShaper.Configuration TEST_FADE_IN_VOLUME_SHAPER_CONFIG = + new VolumeShaper.Configuration.Builder() + .setId(FadeManagerConfiguration.VOLUME_SHAPER_SYSTEM_FADE_ID) + .setCurve(/* times= */new float[]{0.f, 0.50f, 1.0f}, + /* volumes= */new float[]{0.f, 0.30f, 1.0f}) + .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME) + .setDuration(TEST_FADE_IN_DURATION_MS) + .build(); + + private FadeManagerConfiguration mFmc; + + @Rule + public final Expect expect = Expect.create(); + + @Before + public void setUp() { + mFmc = new FadeManagerConfiguration.Builder().build(); + } + + + @Test + public void build() { + expect.withMessage("Fade state for default builder") + .that(mFmc.getFadeState()) + .isEqualTo(FadeManagerConfiguration.FADE_STATE_ENABLED_DEFAULT); + expect.withMessage("Fadeable usages for default builder") + .that(mFmc.getFadeableUsages()) + .containsExactlyElementsIn(TEST_FADEABLE_USAGES); + expect.withMessage("Unfadeable content types usages for default builder") + .that(mFmc.getUnfadeableContentTypes()) + .containsExactlyElementsIn(TEST_UNFADEABLE_CONTENT_TYPES); + expect.withMessage("Unfadeable player types for default builder") + .that(mFmc.getUnfadeablePlayerTypes()) + .containsExactlyElementsIn(TEST_UNFADEABLE_PLAYER_TYPES); + expect.withMessage("Unfadeable uids for default builder") + .that(mFmc.getUnfadeableUids()).isEmpty(); + expect.withMessage("Unfadeable audio attributes for default builder") + .that(mFmc.getUnfadeableAudioAttributes()).isEmpty(); + expect.withMessage("Fade out volume shaper config for media usage") + .that(mFmc.getFadeOutVolumeShaperConfigForUsage(AudioAttributes.USAGE_MEDIA)) + .isEqualTo(TEST_DEFAULT_FADE_OUT_VOLUME_SHAPER_CONFIG); + expect.withMessage("Fade out duration for game usage") + .that(mFmc.getFadeOutDurationForUsage(AudioAttributes.USAGE_GAME)) + .isEqualTo(DEFAULT_FADE_OUT_DURATION_MS); + expect.withMessage("Fade in volume shaper config for media uasge") + .that(mFmc.getFadeInVolumeShaperConfigForUsage(AudioAttributes.USAGE_MEDIA)) + .isEqualTo(TEST_DEFAULT_FADE_IN_VOLUME_SHAPER_CONFIG); + expect.withMessage("Fade in duration for game audio usage") + .that(mFmc.getFadeInDurationForUsage(AudioAttributes.USAGE_GAME)) + .isEqualTo(DEFAULT_FADE_IN_DURATION_MS); + } + + @Test + public void build_withFadeDurations_succeeds() { + FadeManagerConfiguration fmc = new FadeManagerConfiguration + .Builder(TEST_FADE_OUT_DURATION_MS, TEST_FADE_IN_DURATION_MS).build(); + + expect.withMessage("Fade state for builder with duration").that(fmc.getFadeState()) + .isEqualTo(FadeManagerConfiguration.FADE_STATE_ENABLED_DEFAULT); + expect.withMessage("Fadeable usages for builder with duration") + .that(fmc.getFadeableUsages()) + .containsExactlyElementsIn(TEST_FADEABLE_USAGES); + expect.withMessage("Unfadeable content types usages for builder with duration") + .that(fmc.getUnfadeableContentTypes()) + .containsExactlyElementsIn(TEST_UNFADEABLE_CONTENT_TYPES); + expect.withMessage("Unfadeable player types for builder with duration") + .that(fmc.getUnfadeablePlayerTypes()) + .containsExactlyElementsIn(TEST_UNFADEABLE_PLAYER_TYPES); + expect.withMessage("Unfadeable uids for builder with duration") + .that(fmc.getUnfadeableUids()).isEmpty(); + expect.withMessage("Unfadeable audio attributes for builder with duration") + .that(fmc.getUnfadeableAudioAttributes()).isEmpty(); + expect.withMessage("Fade out volume shaper config for media usage") + .that(fmc.getFadeOutVolumeShaperConfigForUsage(AudioAttributes.USAGE_MEDIA)) + .isEqualTo(TEST_FADE_OUT_VOLUME_SHAPER_CONFIG); + expect.withMessage("Fade out duration for game usage") + .that(fmc.getFadeOutDurationForUsage(AudioAttributes.USAGE_GAME)) + .isEqualTo(TEST_FADE_OUT_DURATION_MS); + expect.withMessage("Fade in volume shaper config for media audio attributes") + .that(fmc.getFadeInVolumeShaperConfigForUsage(AudioAttributes.USAGE_MEDIA)) + .isEqualTo(TEST_FADE_IN_VOLUME_SHAPER_CONFIG); + expect.withMessage("Fade in duration for game audio attributes") + .that(fmc.getFadeInDurationForUsage(AudioAttributes.USAGE_GAME)) + .isEqualTo(TEST_FADE_IN_DURATION_MS); + + } + + @Test + public void build_withFadeManagerConfiguration_succeeds() { + FadeManagerConfiguration fmcObj = new FadeManagerConfiguration + .Builder(TEST_FADE_OUT_DURATION_MS, TEST_FADE_IN_DURATION_MS).build(); + + FadeManagerConfiguration fmc = new FadeManagerConfiguration + .Builder(fmcObj).build(); + + expect.withMessage("Fade state for copy builder").that(fmc.getFadeState()) + .isEqualTo(fmcObj.getFadeState()); + expect.withMessage("Fadeable usages for copy builder") + .that(fmc.getFadeableUsages()) + .containsExactlyElementsIn(fmcObj.getFadeableUsages()); + expect.withMessage("Unfadeable content types usages for copy builder") + .that(fmc.getUnfadeableContentTypes()) + .containsExactlyElementsIn(fmcObj.getUnfadeableContentTypes()); + expect.withMessage("Unfadeable player types for copy builder") + .that(fmc.getUnfadeablePlayerTypes()) + .containsExactlyElementsIn(fmcObj.getUnfadeablePlayerTypes()); + expect.withMessage("Unfadeable uids for copy builder") + .that(fmc.getUnfadeableUids()).isEqualTo(fmcObj.getUnfadeableUids()); + expect.withMessage("Unfadeable audio attributes for copy builder") + .that(fmc.getUnfadeableAudioAttributes()) + .isEqualTo(fmcObj.getUnfadeableAudioAttributes()); + expect.withMessage("Fade out volume shaper config for media usage") + .that(fmc.getFadeOutVolumeShaperConfigForUsage(AudioAttributes.USAGE_MEDIA)) + .isEqualTo(fmcObj.getFadeOutVolumeShaperConfigForUsage( + AudioAttributes.USAGE_MEDIA)); + expect.withMessage("Fade out volume shaper config for game usage") + .that(fmc.getFadeOutVolumeShaperConfigForUsage(AudioAttributes.USAGE_GAME)) + .isEqualTo(fmcObj.getFadeOutVolumeShaperConfigForUsage( + AudioAttributes.USAGE_GAME)); + expect.withMessage("Fade in volume shaper config for media usage") + .that(fmc.getFadeInVolumeShaperConfigForUsage(AudioAttributes.USAGE_MEDIA)) + .isEqualTo(fmcObj.getFadeInVolumeShaperConfigForUsage( + AudioAttributes.USAGE_MEDIA)); + expect.withMessage("Fade in volume shaper config for game usage") + .that(fmc.getFadeInVolumeShaperConfigForUsage(AudioAttributes.USAGE_GAME)) + .isEqualTo(fmcObj.getFadeInVolumeShaperConfigForUsage( + AudioAttributes.USAGE_GAME)); + expect.withMessage("Fade out volume shaper config for media audio attributes") + .that(fmc.getFadeOutVolumeShaperConfigForAudioAttributes( + TEST_MEDIA_AUDIO_ATTRIBUTE)) + .isEqualTo(fmcObj.getFadeOutVolumeShaperConfigForAudioAttributes( + TEST_MEDIA_AUDIO_ATTRIBUTE)); + expect.withMessage("Fade out duration for game audio attributes") + .that(fmc.getFadeOutDurationForAudioAttributes(TEST_GAME_AUDIO_ATTRIBUTE)) + .isEqualTo(fmcObj.getFadeOutDurationForAudioAttributes(TEST_GAME_AUDIO_ATTRIBUTE)); + expect.withMessage("Fade in volume shaper config for media audio attributes") + .that(fmc.getFadeInVolumeShaperConfigForAudioAttributes(TEST_MEDIA_AUDIO_ATTRIBUTE)) + .isEqualTo(fmcObj.getFadeInVolumeShaperConfigForAudioAttributes( + TEST_MEDIA_AUDIO_ATTRIBUTE)); + expect.withMessage("Fade in duration for game audio attributes") + .that(fmc.getFadeInDurationForAudioAttributes(TEST_GAME_AUDIO_ATTRIBUTE)) + .isEqualTo(fmcObj.getFadeInDurationForAudioAttributes(TEST_GAME_AUDIO_ATTRIBUTE)); + } + + @Test + public void testSetFadeState_toDisable() { + final int fadeState = FadeManagerConfiguration.FADE_STATE_DISABLED; + FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder() + .setFadeState(fadeState).build(); + + expect.withMessage("Fade state when disabled").that(fmc.getFadeState()) + .isEqualTo(fadeState); + } + + @Test + public void testSetFadeState_toEnableAuto() { + final int fadeStateAuto = FadeManagerConfiguration.FADE_STATE_ENABLED_AUTO; + FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder() + .setFadeState(fadeStateAuto).build(); + + expect.withMessage("Fade state when enabled for audio").that(fmc.getFadeState()) + .isEqualTo(fadeStateAuto); + } + + @Test + public void testSetFadeState_toInvalid_fails() { + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> + new FadeManagerConfiguration.Builder() + .setFadeState(TEST_INVALID_FADE_STATE).build() + ); + + expect.withMessage("Invalid fade state exception").that(thrown) + .hasMessageThat().contains("Unknown fade state"); + } + + @Test + public void testSetFadeVolShaperConfig() { + FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder() + .setFadeOutVolumeShaperConfigForAudioAttributes(TEST_ASSISTANT_AUDIO_ATTRIBUTE, + TEST_FADE_OUT_VOLUME_SHAPER_CONFIG) + .setFadeInVolumeShaperConfigForAudioAttributes(TEST_ASSISTANT_AUDIO_ATTRIBUTE, + TEST_FADE_IN_VOLUME_SHAPER_CONFIG).build(); + + expect.withMessage("Fade out volume shaper config set for assistant audio attributes") + .that(fmc.getFadeOutVolumeShaperConfigForAudioAttributes( + TEST_ASSISTANT_AUDIO_ATTRIBUTE)) + .isEqualTo(TEST_FADE_OUT_VOLUME_SHAPER_CONFIG); + expect.withMessage("Fade in volume shaper config set for assistant audio attributes") + .that(fmc.getFadeInVolumeShaperConfigForAudioAttributes( + TEST_ASSISTANT_AUDIO_ATTRIBUTE)) + .isEqualTo(TEST_FADE_IN_VOLUME_SHAPER_CONFIG); + } + + @Test + public void testSetFadeOutVolShaperConfig_withNullAudioAttributes_fails() { + NullPointerException thrown = assertThrows(NullPointerException.class, () -> + new FadeManagerConfiguration.Builder() + .setFadeOutVolumeShaperConfigForAudioAttributes(/* audioAttributes= */ null, + TEST_FADE_OUT_VOLUME_SHAPER_CONFIG).build() + ); + + expect.withMessage("Null audio attributes for fade out exception") + .that(thrown).hasMessageThat().contains("cannot be null"); + } + + @Test + public void testSetFadeVolShaperConfig_withNullVolumeShaper_getsNull() { + FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder(mFmc) + .setFadeOutVolumeShaperConfigForAudioAttributes(TEST_MEDIA_AUDIO_ATTRIBUTE, + /* VolumeShaper.Configuration= */ null) + .setFadeInVolumeShaperConfigForAudioAttributes(TEST_MEDIA_AUDIO_ATTRIBUTE, + /* VolumeShaper.Configuration= */ null) + .clearFadeableUsage(AudioAttributes.USAGE_MEDIA).build(); + + expect.withMessage("Fade out volume shaper config set with null value") + .that(fmc.getFadeOutVolumeShaperConfigForAudioAttributes( + TEST_MEDIA_AUDIO_ATTRIBUTE)).isNull(); + } + + @Test + public void testSetFadeInVolShaperConfig_withNullAudioAttributes_fails() { + NullPointerException thrown = assertThrows(NullPointerException.class, () -> + new FadeManagerConfiguration.Builder() + .setFadeInVolumeShaperConfigForAudioAttributes(/* audioAttributes= */ null, + TEST_FADE_IN_VOLUME_SHAPER_CONFIG).build() + ); + + expect.withMessage("Null audio attributes for fade in exception") + .that(thrown).hasMessageThat().contains("cannot be null"); + } + + @Test + public void testSetFadeDuration() { + FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder() + .setFadeOutDurationForAudioAttributes(TEST_GAME_AUDIO_ATTRIBUTE, + TEST_FADE_OUT_DURATION_MS) + .setFadeInDurationForAudioAttributes(TEST_GAME_AUDIO_ATTRIBUTE, + TEST_FADE_IN_DURATION_MS).build(); + + expect.withMessage("Fade out duration set for audio attributes") + .that(fmc.getFadeOutDurationForAudioAttributes(TEST_GAME_AUDIO_ATTRIBUTE)) + .isEqualTo(TEST_FADE_OUT_DURATION_MS); + expect.withMessage("Fade in duration set for audio attributes") + .that(fmc.getFadeInDurationForAudioAttributes(TEST_GAME_AUDIO_ATTRIBUTE)) + .isEqualTo(TEST_FADE_IN_DURATION_MS); + } + + @Test + public void testSetFadeOutDuration_withNullAudioAttributes_fails() { + NullPointerException thrown = assertThrows(NullPointerException.class, () -> + new FadeManagerConfiguration.Builder().setFadeOutDurationForAudioAttributes( + /* audioAttributes= */ null, TEST_FADE_OUT_DURATION_MS).build() + ); + + expect.withMessage("Null audio attributes for fade out duration exception").that(thrown) + .hasMessageThat().contains("cannot be null"); + } + + @Test + public void testSetFadeOutDuration_withInvalidDuration_fails() { + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> + new FadeManagerConfiguration.Builder().setFadeOutDurationForAudioAttributes( + TEST_NAVIGATION_AUDIO_ATTRIBUTE, TEST_INVALID_DURATION).build() + ); + + expect.withMessage("Invalid duration for fade out exception").that(thrown) + .hasMessageThat().contains("not positive"); + } + + @Test + public void testSetFadeInDuration_withNullAudioAttributes_fails() { + NullPointerException thrown = assertThrows(NullPointerException.class, () -> + new FadeManagerConfiguration.Builder().setFadeInDurationForAudioAttributes( + /* audioAttributes= */ null, TEST_FADE_IN_DURATION_MS).build() + ); + + expect.withMessage("Null audio attributes for fade in duration exception").that(thrown) + .hasMessageThat().contains("cannot be null"); + } + + @Test + public void testSetFadeInDuration_withInvalidDuration_fails() { + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> + new FadeManagerConfiguration.Builder().setFadeInDurationForAudioAttributes( + TEST_NAVIGATION_AUDIO_ATTRIBUTE, TEST_INVALID_DURATION).build() + ); + + expect.withMessage("Invalid duration for fade in exception").that(thrown) + .hasMessageThat().contains("not positive"); + } + + @Test + public void testSetFadeableUsages() { + final List<Integer> fadeableUsages = List.of( + AudioAttributes.USAGE_VOICE_COMMUNICATION, + AudioAttributes.USAGE_ALARM, + AudioAttributes.USAGE_ASSISTANT + ); + AudioAttributes aaForVoiceComm = createAudioAttributesForUsage( + AudioAttributes.USAGE_VOICE_COMMUNICATION); + AudioAttributes aaForAlarm = createAudioAttributesForUsage(AudioAttributes.USAGE_ALARM); + AudioAttributes aaForAssistant = createAudioAttributesForUsage( + AudioAttributes.USAGE_ASSISTANT); + + + FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder() + .setFadeableUsages(fadeableUsages) + .setFadeOutVolumeShaperConfigForAudioAttributes(aaForVoiceComm, + TEST_FADE_OUT_VOLUME_SHAPER_CONFIG) + .setFadeInVolumeShaperConfigForAudioAttributes(aaForVoiceComm, + TEST_FADE_IN_VOLUME_SHAPER_CONFIG) + .setFadeOutVolumeShaperConfigForAudioAttributes(aaForAlarm, + TEST_FADE_OUT_VOLUME_SHAPER_CONFIG) + .setFadeInVolumeShaperConfigForAudioAttributes(aaForAlarm, + TEST_FADE_IN_VOLUME_SHAPER_CONFIG) + .setFadeOutVolumeShaperConfigForAudioAttributes(aaForAssistant, + TEST_FADE_OUT_VOLUME_SHAPER_CONFIG) + .setFadeInVolumeShaperConfigForAudioAttributes(aaForAssistant, + TEST_FADE_IN_VOLUME_SHAPER_CONFIG).build(); + + expect.withMessage("Fadeable usages") + .that(fmc.getFadeableUsages()).isEqualTo(fadeableUsages); + } + + @Test + public void testSetFadeableUsages_withInvalidUsage_fails() { + final List<Integer> fadeableUsages = List.of( + AudioAttributes.USAGE_VOICE_COMMUNICATION, + TEST_INVALID_USAGE, + AudioAttributes.USAGE_ANNOUNCEMENT + ); + + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> + new FadeManagerConfiguration.Builder().setFadeableUsages(fadeableUsages).build() + ); + + expect.withMessage("Fadeable usages set to invalid usage").that(thrown).hasMessageThat() + .contains("Invalid usage"); + } + + @Test + public void testSetFadeableUsages_withNullUsages_fails() { + NullPointerException thrown = assertThrows(NullPointerException.class, () -> + new FadeManagerConfiguration.Builder().setFadeableUsages(/* usages= */ null) + .build() + ); + + expect.withMessage("Fadeable usages set to null list").that(thrown).hasMessageThat() + .contains("cannot be null"); + } + + @Test + public void testSetFadeableUsages_withEmptyListClears_addsNewUsage() { + final List<Integer> fadeableUsages = List.of( + AudioAttributes.USAGE_VOICE_COMMUNICATION, + AudioAttributes.USAGE_ALARM, + AudioAttributes.USAGE_ASSISTANT + ); + FadeManagerConfiguration.Builder fmcBuilder = new FadeManagerConfiguration.Builder() + .setFadeableUsages(fadeableUsages); + + fmcBuilder.setFadeableUsages(List.of()); + + FadeManagerConfiguration fmc = fmcBuilder + .addFadeableUsage(AudioAttributes.USAGE_MEDIA) + .setFadeOutVolumeShaperConfigForAudioAttributes(TEST_MEDIA_AUDIO_ATTRIBUTE, + TEST_FADE_OUT_VOLUME_SHAPER_CONFIG) + .setFadeInVolumeShaperConfigForAudioAttributes(TEST_MEDIA_AUDIO_ATTRIBUTE, + TEST_FADE_IN_VOLUME_SHAPER_CONFIG).build(); + expect.withMessage("Fadeable usages set to empty list") + .that(fmc.getFadeableUsages()).isEqualTo(List.of(AudioAttributes.USAGE_MEDIA)); + } + + + @Test + public void testAddFadeableUsage() { + final int usageToAdd = AudioAttributes.USAGE_ASSISTANT; + AudioAttributes aaToAdd = createAudioAttributesForUsage(usageToAdd); + List<Integer> updatedUsages = new ArrayList<>(mFmc.getFadeableUsages()); + updatedUsages.add(usageToAdd); + + FadeManagerConfiguration updatedFmc = new FadeManagerConfiguration + .Builder(mFmc).addFadeableUsage(usageToAdd) + .setFadeOutVolumeShaperConfigForAudioAttributes(aaToAdd, + TEST_FADE_OUT_VOLUME_SHAPER_CONFIG) + .setFadeInVolumeShaperConfigForAudioAttributes(aaToAdd, + TEST_FADE_IN_VOLUME_SHAPER_CONFIG) + .build(); + + expect.withMessage("Fadeable usages").that(updatedFmc.getFadeableUsages()) + .containsExactlyElementsIn(updatedUsages); + } + + @Test + public void testAddFadeableUsage_withoutSetFadeableUsages() { + final int newUsage = AudioAttributes.USAGE_ASSISTANT; + AudioAttributes aaToAdd = createAudioAttributesForUsage(newUsage); + + FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder() + .addFadeableUsage(newUsage) + .setFadeOutVolumeShaperConfigForAudioAttributes(aaToAdd, + TEST_FADE_OUT_VOLUME_SHAPER_CONFIG) + .setFadeInVolumeShaperConfigForAudioAttributes(aaToAdd, + TEST_FADE_IN_VOLUME_SHAPER_CONFIG) + .build(); + + expect.withMessage("Fadeable usages").that(fmc.getFadeableUsages()) + .containsExactlyElementsIn(List.of(newUsage)); + } + + @Test + public void testAddFadeableUsage_withInvalidUsage_fails() { + List<Integer> setUsages = Arrays.asList( + AudioAttributes.USAGE_VOICE_COMMUNICATION, + AudioAttributes.USAGE_ASSISTANT + ); + AudioAttributes aaForVoiceComm = createAudioAttributesForUsage( + AudioAttributes.USAGE_VOICE_COMMUNICATION); + AudioAttributes aaForAssistant = createAudioAttributesForUsage( + AudioAttributes.USAGE_ASSISTANT); + FadeManagerConfiguration.Builder fmcBuilder = new FadeManagerConfiguration.Builder() + .setFadeableUsages(setUsages) + .setFadeOutVolumeShaperConfigForAudioAttributes(aaForVoiceComm, + TEST_FADE_OUT_VOLUME_SHAPER_CONFIG) + .setFadeInVolumeShaperConfigForAudioAttributes(aaForVoiceComm, + TEST_FADE_IN_VOLUME_SHAPER_CONFIG) + .setFadeOutVolumeShaperConfigForAudioAttributes(aaForAssistant, + TEST_FADE_OUT_VOLUME_SHAPER_CONFIG) + .setFadeInVolumeShaperConfigForAudioAttributes(aaForAssistant, + TEST_FADE_IN_VOLUME_SHAPER_CONFIG); + + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> + fmcBuilder.addFadeableUsage(TEST_INVALID_USAGE) + ); + + FadeManagerConfiguration fmc = fmcBuilder.build(); + expect.withMessage("Fadeable usages ").that(thrown).hasMessageThat() + .contains("Invalid usage"); + expect.withMessage("Fadeable usages").that(fmc.getFadeableUsages()) + .containsExactlyElementsIn(setUsages); + } + + @Test + public void testClearFadeableUsage() { + final int usageToClear = AudioAttributes.USAGE_MEDIA; + List<Integer> updatedUsages = new ArrayList<>(mFmc.getFadeableUsages()); + updatedUsages.remove((Integer) usageToClear); + + FadeManagerConfiguration updatedFmc = new FadeManagerConfiguration + .Builder(mFmc).clearFadeableUsage(usageToClear).build(); + + expect.withMessage("Clear fadeable usage").that(updatedFmc.getFadeableUsages()) + .containsExactlyElementsIn(updatedUsages); + } + + @Test + public void testClearFadeableUsage_withInvalidUsage_fails() { + FadeManagerConfiguration.Builder fmcBuilder = new FadeManagerConfiguration.Builder(mFmc); + + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> + fmcBuilder.clearFadeableUsage(TEST_INVALID_USAGE) + ); + + FadeManagerConfiguration fmc = fmcBuilder.build(); + expect.withMessage("Clear invalid usage").that(thrown).hasMessageThat() + .contains("Invalid usage"); + expect.withMessage("Fadeable usages").that(fmc.getFadeableUsages()) + .containsExactlyElementsIn(mFmc.getFadeableUsages()); + } + + @Test + public void testSetUnfadeableContentTypes() { + final List<Integer> unfadeableContentTypes = List.of( + AudioAttributes.CONTENT_TYPE_MOVIE, + AudioAttributes.CONTENT_TYPE_SONIFICATION + ); + FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder() + .setUnfadeableContentTypes(unfadeableContentTypes).build(); + + expect.withMessage("Unfadeable content types set") + .that(fmc.getUnfadeableContentTypes()).isEqualTo(unfadeableContentTypes); + } + + @Test + public void testSetUnfadeableContentTypes_withInvalidContentType_fails() { + final List<Integer> invalidUnfadeableContentTypes = List.of( + AudioAttributes.CONTENT_TYPE_MOVIE, + TEST_INVALID_CONTENT_TYPE, + AudioAttributes.CONTENT_TYPE_SONIFICATION + ); + + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> + new FadeManagerConfiguration.Builder() + .setUnfadeableContentTypes(invalidUnfadeableContentTypes).build() + ); + + expect.withMessage("Invalid content type set exception").that(thrown).hasMessageThat() + .contains("Invalid content type"); + } + + @Test + public void testSetUnfadeableContentTypes_withNullContentType_fails() { + NullPointerException thrown = assertThrows(NullPointerException.class, () -> + new FadeManagerConfiguration.Builder() + .setUnfadeableContentTypes(/* contentType= */ null).build() + ); + + expect.withMessage("Null content type set exception").that(thrown).hasMessageThat() + .contains("cannot be null"); + } + + @Test + public void testSetUnfadeableContentTypes_withEmptyList_clearsExistingList() { + final List<Integer> unfadeableContentTypes = List.of( + AudioAttributes.CONTENT_TYPE_MOVIE, + AudioAttributes.CONTENT_TYPE_SONIFICATION + ); + FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder() + .setUnfadeableContentTypes(unfadeableContentTypes).build(); + + FadeManagerConfiguration fmcWithEmptyLsit = new FadeManagerConfiguration.Builder(fmc) + .setUnfadeableContentTypes(List.of()).build(); + + expect.withMessage("Unfadeable content types for empty list") + .that(fmcWithEmptyLsit.getUnfadeableContentTypes()).isEmpty(); + } + + @Test + public void testAddUnfadeableContentType() { + final int contentTypeToAdd = AudioAttributes.CONTENT_TYPE_MOVIE; + List<Integer> upatdedContentTypes = new ArrayList<>(mFmc.getUnfadeableContentTypes()); + upatdedContentTypes.add(contentTypeToAdd); + + FadeManagerConfiguration updatedFmc = new FadeManagerConfiguration + .Builder(mFmc).addUnfadeableContentType(contentTypeToAdd).build(); + + expect.withMessage("Unfadeable content types").that(updatedFmc.getUnfadeableContentTypes()) + .containsExactlyElementsIn(upatdedContentTypes); + } + + @Test + public void testAddUnfadeableContentTypes_withoutSetUnfadeableContentTypes() { + final int newContentType = AudioAttributes.CONTENT_TYPE_MOVIE; + + FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder() + .addUnfadeableContentType(newContentType).build(); + + expect.withMessage("Unfadeable content types").that(fmc.getUnfadeableContentTypes()) + .containsExactlyElementsIn(List.of(newContentType)); + } + + @Test + public void testAddunfadeableContentTypes_withInvalidContentType_fails() { + final List<Integer> unfadeableContentTypes = List.of( + AudioAttributes.CONTENT_TYPE_MOVIE, + AudioAttributes.CONTENT_TYPE_SONIFICATION + ); + FadeManagerConfiguration.Builder fmcBuilder = new FadeManagerConfiguration.Builder() + .setUnfadeableContentTypes(unfadeableContentTypes); + + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> + fmcBuilder.addUnfadeableContentType(TEST_INVALID_CONTENT_TYPE).build() + ); + + expect.withMessage("Invalid content types exception").that(thrown).hasMessageThat() + .contains("Invalid content type"); + } + + @Test + public void testClearUnfadeableContentType() { + List<Integer> unfadeableContentTypes = new ArrayList<>(Arrays.asList( + AudioAttributes.CONTENT_TYPE_MOVIE, + AudioAttributes.CONTENT_TYPE_SONIFICATION + )); + final int contentTypeToClear = AudioAttributes.CONTENT_TYPE_MOVIE; + + FadeManagerConfiguration updatedFmc = new FadeManagerConfiguration.Builder() + .setUnfadeableContentTypes(unfadeableContentTypes) + .clearUnfadeableContentType(contentTypeToClear).build(); + + unfadeableContentTypes.remove((Integer) contentTypeToClear); + expect.withMessage("Unfadeable content types").that(updatedFmc.getUnfadeableContentTypes()) + .containsExactlyElementsIn(unfadeableContentTypes); + } + + @Test + public void testClearUnfadeableContentType_withInvalidContentType_fails() { + FadeManagerConfiguration.Builder fmcBuilder = new FadeManagerConfiguration.Builder(mFmc); + + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> + fmcBuilder.clearUnfadeableContentType(TEST_INVALID_CONTENT_TYPE).build() + ); + + expect.withMessage("Invalid content type exception").that(thrown).hasMessageThat() + .contains("Invalid content type"); + } + + @Test + public void testSetUnfadeableUids() { + final List<Integer> unfadeableUids = List.of( + TEST_UID_1, + TEST_UID_2 + ); + FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder() + .setUnfadeableUids(unfadeableUids).build(); + + expect.withMessage("Unfadeable uids set") + .that(fmc.getUnfadeableUids()).isEqualTo(unfadeableUids); + } + + @Test + public void testSetUnfadeableUids_withNullUids_fails() { + NullPointerException thrown = assertThrows(NullPointerException.class, () -> + new FadeManagerConfiguration.Builder() + .setUnfadeableUids(/* uids= */ null).build() + ); + + expect.withMessage("Null unfadeable uids").that(thrown).hasMessageThat() + .contains("cannot be null"); + } + + @Test + public void testAddUnfadeableUid() { + FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder() + .addUnfadeableUid(TEST_UID_1).build(); + + expect.withMessage("Unfadeable uids") + .that(fmc.getUnfadeableUids()).isEqualTo(List.of(TEST_UID_1)); + } + + @Test + public void testClearUnfadebaleUid() { + final List<Integer> unfadeableUids = List.of( + TEST_UID_1, + TEST_UID_2 + ); + FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder() + .setUnfadeableUids(unfadeableUids).build(); + + FadeManagerConfiguration updatedFmc = new FadeManagerConfiguration.Builder(fmc) + .clearUnfadeableUid(TEST_UID_1).build(); + + expect.withMessage("Unfadeable uids").that(updatedFmc.getUnfadeableUids()) + .isEqualTo(List.of(TEST_UID_2)); + } + + @Test + public void testSetUnfadeableAudioAttributes() { + final List<AudioAttributes> unfadeableAttrs = List.of( + TEST_ASSISTANT_AUDIO_ATTRIBUTE, + TEST_NAVIGATION_AUDIO_ATTRIBUTE + ); + + FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder() + .setUnfadeableAudioAttributes(unfadeableAttrs).build(); + + expect.withMessage("Unfadeable audio attributes") + .that(fmc.getUnfadeableAudioAttributes()).isEqualTo(unfadeableAttrs); + } + + @Test + public void testSetUnfadeableAudioAttributes_withNullAttributes_fails() { + NullPointerException thrown = assertThrows(NullPointerException.class, () -> + new FadeManagerConfiguration.Builder() + .setUnfadeableAudioAttributes(/* attrs= */ null).build() + ); + + expect.withMessage("Null audio attributes exception").that(thrown).hasMessageThat() + .contains("cannot be null"); + } + + @Test + public void testWriteToParcel_andCreateFromParcel() { + Parcel parcel = Parcel.obtain(); + + mFmc.writeToParcel(parcel, TEST_PARCEL_FLAGS); + parcel.setDataPosition(/* position= */ 0); + expect.withMessage("Fade manager configuration write to and create from parcel") + .that(mFmc) + .isEqualTo(FadeManagerConfiguration.CREATOR.createFromParcel(parcel)); + } + + private static AudioAttributes createAudioAttributesForUsage(int usage) { + if (AudioAttributes.isSystemUsage(usage)) { + return new AudioAttributes.Builder().setSystemUsage(usage).build(); + } + return new AudioAttributes.Builder().setUsage(usage).build(); + } +} diff --git a/services/core/java/com/android/server/appop/AudioRestrictionManager.java b/services/core/java/com/android/server/appop/AudioRestrictionManager.java index be870373af63..b9ccc5389337 100644 --- a/services/core/java/com/android/server/appop/AudioRestrictionManager.java +++ b/services/core/java/com/android/server/appop/AudioRestrictionManager.java @@ -43,7 +43,7 @@ public class AudioRestrictionManager { static { SparseBooleanArray audioMutedUsages = new SparseBooleanArray(); SparseBooleanArray vibrationMutedUsages = new SparseBooleanArray(); - for (int usage : AudioAttributes.SDK_USAGES) { + for (int usage : AudioAttributes.SDK_USAGES.toArray()) { final int suppressionBehavior = AudioAttributes.SUPPRESSIBLE_USAGES.get(usage); if (suppressionBehavior == AudioAttributes.SUPPRESSIBLE_NOTIFICATION || suppressionBehavior == AudioAttributes.SUPPRESSIBLE_CALL || diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 89d820050b03..d0ded63162db 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -1519,7 +1519,7 @@ public class ZenModeHelper { final boolean muteEverything = zenSilence || (zenPriorityOnly && ZenModeConfig.areAllZenBehaviorSoundsMuted(mConsolidatedPolicy)); - for (int usage : AudioAttributes.SDK_USAGES) { + for (int usage : AudioAttributes.SDK_USAGES.toArray()) { final int suppressionBehavior = AudioAttributes.SUPPRESSIBLE_USAGES.get(usage); if (suppressionBehavior == AudioAttributes.SUPPRESSIBLE_NEVER) { applyRestrictions(zenPriorityOnly, false /*mute*/, usage); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 97b6b98a0b08..a44986af88a8 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -524,7 +524,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); - for (int usage : AudioAttributes.SDK_USAGES) { + for (int usage : AudioAttributes.getSdkUsages()) { if (usage == AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) { // only mute audio, not vibrations verify(mAppOps, atLeastOnce()).setRestriction(eq(AppOpsManager.OP_PLAY_AUDIO), @@ -546,7 +546,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); - for (int usage : AudioAttributes.SDK_USAGES) { + for (int usage : AudioAttributes.getSdkUsages()) { verify(mAppOps).setRestriction( eq(AppOpsManager.OP_PLAY_AUDIO), eq(usage), anyInt(), eq(new String[]{PKG_O})); verify(mAppOps).setRestriction( @@ -561,7 +561,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); - for (int usage : AudioAttributes.SDK_USAGES) { + for (int usage : AudioAttributes.getSdkUsages()) { verify(mAppOps).setRestriction( eq(AppOpsManager.OP_PLAY_AUDIO), eq(usage), anyInt(), eq(null)); verify(mAppOps).setRestriction( @@ -576,7 +576,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); - for (int usage : AudioAttributes.SDK_USAGES) { + for (int usage : AudioAttributes.getSdkUsages()) { verify(mAppOps).setRestriction( eq(AppOpsManager.OP_PLAY_AUDIO), eq(usage), anyInt(), eq(null)); verify(mAppOps).setRestriction( |