/* * 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 android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION; import android.annotation.DurationMillisLong; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; 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. * *

Configurations are provided through: *

* *

Fade manager configuration can be created in one of the following ways: *

* @hide */ @SystemApi @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; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = false, prefix = "FADE_STATE", value = { FADE_STATE_DISABLED, FADE_STATE_ENABLED_DEFAULT, }) 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 @DurationMillisLong long DURATION_NOT_SET = 0; /** Defines the default fade out duration */ private static final @DurationMillisLong long DEFAULT_FADE_OUT_DURATION_MS = 2_000; /** Defines the default fade in duration */ private static final @DurationMillisLong long DEFAULT_FADE_IN_DURATION_MS = 1_000; /** Map of Usage to Fade volume shaper configs wrapper */ private final SparseArray mUsageToFadeWrapperMap; /** Map of AudioAttributes to Fade volume shaper configs wrapper */ private final ArrayMap 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 mUnfadeableAudioAttributes; /** fade state */ private final @FadeStateEnum int mFadeState; /** fade out duration from builder - used for creating default fade out volume shaper */ private final @DurationMillisLong long mFadeOutDurationMillis; /** fade in duration from builder - used for creating default fade in volume shaper */ private final @DurationMillisLong long mFadeInDurationMillis; /** delay after which the offending players are faded back in */ private final @DurationMillisLong long mFadeInDelayForOffendersMillis; private FadeManagerConfiguration(int fadeState, @DurationMillisLong long fadeOutDurationMillis, @DurationMillisLong long fadeInDurationMillis, @DurationMillisLong long offendersFadeInDelayMillis, @NonNull SparseArray usageToFadeWrapperMap, @NonNull ArrayMap attrToFadeWrapperMap, @NonNull IntArray fadeableUsages, @NonNull IntArray unfadeableContentTypes, @NonNull IntArray unfadeablePlayerTypes, @NonNull IntArray unfadeableUids, @NonNull List 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 */ @FadeStateEnum public int getFadeState() { return mFadeState; } /** * Get the list of usages that can be faded * * @return list of {@link android.media.AudioAttributes usages} that shall be faded * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} */ @NonNull public List getFadeableUsages() { ensureFadingIsEnabled(); return convertIntArrayToIntegerList(mFadeableUsages); } /** * Get the list of {@link android.media.AudioPlaybackConfiguration player types} that can be * faded * * @return list of {@link android.media.AudioPlaybackConfiguration player types} * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} */ @NonNull public List getUnfadeablePlayerTypes() { ensureFadingIsEnabled(); return convertIntArrayToIntegerList(mUnfadeablePlayerTypes); } /** * Get the list of {@link android.media.AudioAttributes content types} that can be faded * * @return list of {@link android.media.AudioAttributes content types} * @throws IllegalStateExceptionif if the fade state is set to {@link #FADE_STATE_DISABLED} */ @NonNull public List 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 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 getUnfadeableAudioAttributes() { ensureFadingIsEnabled(); return mUnfadeableAudioAttributes; } /** * Get the duration used to fade out players with {@link android.media.AudioAttributes usage} * * @param usage the {@link android.media.AudioAttributes usage} * @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} */ @IntRange(from = 0) @DurationMillisLong public long getFadeOutDurationForUsage(@AudioAttributes.AttributeUsage 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 usage} * * @param usage the {@link android.media.AudioAttributes usage} * @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} */ @IntRange(from = 0) @DurationMillisLong public long getFadeInDurationForUsage(@AudioAttributes.AttributeUsage 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 usage} * * @param usage the {@link android.media.AudioAttributes usage} * @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( @AudioAttributes.AttributeUsage 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 usage} * * @param usage the {@link android.media.AudioAttributes usage} * @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( @AudioAttributes.AttributeUsage 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} */ @IntRange(from = 0) @DurationMillisLong 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} */ @IntRange(from = 0) @DurationMillisLong 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 getAudioAttributesWithVolumeShaperConfigs() { return getAudioAttributesInternal(); } /** * Get the delay after which the offending players are faded back in * * Players are categorized as offending if they do not honor audio focus state changes. For * example - when an app loses audio focus, it is expected that the app stops any active * player in favor of the app(s) that gained audio focus. However, if the app do not stop the * audio playback, such players are termed as offenders. * * @return delay in milliseconds */ @IntRange(from = 0) @DurationMillisLong 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 usage} * @return {@code true} if usage is fadeable, {@code false} when the fade state is set to * {@link #FADE_STATE_DISABLED} or if the usage is not fadeable. */ 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 content type} * @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(@AudioPlaybackConfiguration.PlayerType 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); } /** * Returns the default fade out duration (in milliseconds) */ public static @IntRange(from = 1) @DurationMillisLong long getDefaultFadeOutDurationMillis() { return DEFAULT_FADE_OUT_DURATION_MS; } /** * Returns the default fade in duration (in milliseconds) */ public static @IntRange(from = 1) @DurationMillisLong long getDefaultFadeInDurationMillis() { return DEFAULT_FADE_IN_DURATION_MS; } @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 {@link #FADE_STATE_DISABLED} or {@link #FADE_STATE_ENABLED_DEFAULT} * @return human-readable string * @hide */ @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"; 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 usageToWrapperMap = in.createTypedSparseArray(FadeVolumeShaperConfigsWrapper.CREATOR); ArrayMap 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 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 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 getAudioAttributesInternal() { List 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) || AudioAttributes.isHiddenUsage(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 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 convertIntArrayToIntegerList(IntArray intArray) { if (intArray == null) { return new ArrayList<>(); } ArrayList 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. * *

Notes: *

    *
  • When fade state is set to {@link #FADE_STATE_ENABLED_DEFAULT}, the builder expects at * least one valid usage to be set/added. Failure to do so will result in an exception * during {@link #build()}
  • *
  • 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)}
  • *
  • It is recommended to set volume shaper configurations individually for fade out and * fade in
  • *
  • For any incomplete volume shaper configurations, 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)}
  • *
  • 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)}
  • *
* */ @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; /** * 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 @DurationMillisLong long mFadeInDelayForOffendersMillis = DEFAULT_DELAY_FADE_IN_OFFENDERS_MS; private @DurationMillisLong long mFadeOutDurationMillis; private @DurationMillisLong long mFadeInDurationMillis; private long mBuilderFieldsSet; private SparseArray mUsageToFadeWrapperMap = new SparseArray<>(); private ArrayMap 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 mUnfadeableAudioAttributes = new ArrayList<>(); /** * Constructs a new Builder with {@link #DEFAULT_FADE_OUT_DURATION_MS} and * {@link #DEFAULT_FADE_IN_DURATION_MS} 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(@IntRange(from = 1) @DurationMillisLong long fadeOutDurationMillis, @IntRange(from = 1) @DurationMillisLong 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; copyUsageToFadeWrapperMapInternal(fmc.mUsageToFadeWrapperMap); mAttrToFadeWrapperMap = new ArrayMap( 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 #FADE_STATE_DISABLED} or * {@link #FADE_STATE_ENABLED_DEFAULT} 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 usage} *

* 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 usage} 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( @AudioAttributes.AttributeUsage 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 usage} *

* 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 usage} * @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( @AudioAttributes.AttributeUsage 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 usage} *

* 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. *

* 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 usage} 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(@AudioAttributes.AttributeUsage int usage, @IntRange(from = 0) @DurationMillisLong 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 usage} *

* 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. *

* Note: 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 usage} 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(@AudioAttributes.AttributeUsage int usage, @IntRange(from = 0) @DurationMillisLong 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} *

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

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}. *

* 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. *

* Note: 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, @IntRange(from = 0) @DurationMillisLong 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} *

* 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. *

* Note: 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, @IntRange(from = 0) @DurationMillisLong 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 usage} that can be faded * *

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

Warning: 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 usages} * @return the same Builder instance * @throws IllegalArgumentException if the usages are invalid * @see #getFadeableUsages() */ @NonNull public Builder setFadeableUsages(@NonNull List 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 usage} to the fadeable list * * @param usage the {@link android.media.AudioAttributes usage} * @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; } /** * Clears the fadeable {@link android.media.AudioAttributes usage} list * *

This can be used to reset the list when using a copy constructor * * @return the same Builder instance * @see #getFadeableUsages() * @see #setFadeableUsages(List) */ @NonNull public Builder clearFadeableUsages() { setFlag(IS_FADEABLE_USAGES_FIELD_SET); mFadeableUsages.clear(); return this; } /** * Set the list of {@link android.media.AudioAttributes content type} that can not be faded * *

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. * *

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 content types} * @return the same Builder instance * @throws IllegalArgumentException if the content types are invalid * @see #getUnfadeableContentTypes() */ @NonNull public Builder setUnfadeableContentTypes(@NonNull List 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 content type} to unfadeable list * * @param contentType the {@link android.media.AudioAttributes content type} * @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; } /** * Clears the unfadeable {@link android.media.AudioAttributes content type} list * *

This can be used to reset the list when using a copy constructor * * @return the same Builder instance * @see #setUnfadeableContentTypes(List) * @see #getUnfadeableContentTypes() */ @NonNull public Builder clearUnfadeableContentTypes() { setFlag(IS_UNFADEABLE_CONTENT_TYPE_FIELD_SET); mUnfadeableContentTypes.clear(); return this; } /** * Set the uids that cannot be faded * *

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. * * @param uids list of uids * @return the same Builder instance * @see #getUnfadeableUids() */ @NonNull public Builder setUnfadeableUids(@NonNull List 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; } /** * Clears the unfadeable uid list * *

This can be used to reset the list when using a copy constructor. * * @return the same Builder instance * @see #setUnfadeableUids(List) * @see #getUnfadeableUids() */ @NonNull public Builder clearUnfadeableUids() { mUnfadeableUids.clear(); return this; } /** * Set the list of {@link android.media.AudioAttributes} that can not be faded * *

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. * *

Note: 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: *

         *    AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build() 
* is a generic audio attribute for {@link android.media.AudioAttributes.USAGE_MEDIA}. * It is an undefined behavior to have an {@link android.media.AudioAttributes usage} 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 * @see #getUnfadeableAudioAttributes() */ @NonNull public Builder setUnfadeableAudioAttributes(@NonNull List 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 * @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; } /** * Clears the unfadeable {@link android.media.AudioAttributes} list. * *

This can be used to reset the list when using a copy constructor. * * @return the same Builder instance * @see #getUnfadeableAudioAttributes() */ @NonNull public Builder clearUnfadeableAudioAttributes() { mUnfadeableAudioAttributes.clear(); return this; } /** * Set the delay after which the offending faded out player will be faded in. * *

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( @IntRange(from = 0) @DurationMillisLong 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 copyUsageToFadeWrapperMapInternal( SparseArray usageToFadeWrapperMap) { for (int index = 0; index < usageToFadeWrapperMap.size(); index++) { mUsageToFadeWrapperMap.put(usageToFadeWrapperMap.keyAt(index), new FadeVolumeShaperConfigsWrapper(usageToFadeWrapperMap.valueAt(index))); } } private void validateFadeState(int state) { switch(state) { case FADE_STATE_DISABLED: case FADE_STATE_ENABLED_DEFAULT: break; default: throw new IllegalArgumentException("Unknown fade state: " + state); } } private void validateUsages(List usages) { for (int index = 0; index < usages.size(); index++) { validateUsage(usages.get(index)); } } private void validateContentTypes(List 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() {} FadeVolumeShaperConfigsWrapper(@NonNull FadeVolumeShaperConfigsWrapper wrapper) { Objects.requireNonNull(wrapper, "Fade volume shaper configs wrapper cannot be null"); this.mFadeOutVolShaperConfig = wrapper.mFadeOutVolShaperConfig; this.mFadeInVolShaperConfig = wrapper.mFadeInVolShaperConfig; } 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 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]; } }; } }