/* * 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: *
* new FadeManagerConfiguration.Builder() * .setFadeState(FADE_STATE_DISABLED).build() ** Can be used to disable fading
* new FadeManagerConfiguration.Builder() * .setFadeState(FADE_STATE_ENABLED_DEFAULT).build() ** Can be used to enable default fading configurations
* new FadeManagerConfiguration.Builder(fade out duration, fade in duration) * .setFadeState(FADE_STATE_ENABLED_DEFAULT).build() ** Can be used to enable default fadeability lists with configurable fade in and out duration *
* 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()* Achieves full customization of fadeability lists and configurations
* new FadeManagerConfiguration.Builder(fadeManagerConfiguration) * .addFadeableUsage(new usage) * .... * .build()* Helps with recreating a new instance from another to simply change/add on top of the * existing ones
Notes: *
* 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 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 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 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:
* 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
* 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