Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a97cd681 authored by Jean-Michel Trivi's avatar Jean-Michel Trivi
Browse files

Muting notifications due to focus is conditional on recording

Legacy behavior is to mute notification while an app has
requested audio focus with GAIN_TRANSIENT_EXCLUSIVE.
The new behavior under flag is to enforce that the muting
happens only when the focus requester is also recording audio.

Implementation uses a new AudioManager method that contains
all audio framework-side evaluation of whether the notification
should play, and avoid code duplication in
NotificationManagerService and NotificationAttentionHelper.

Annotated AudioAttributes field in NotificationRecord
to document that they are never null (so always available
to be consumed by AudioManager.shouldNotificationSoundPlay).

Updated unit tests to also mock AudioManager's response
for shouldNotificationPlaySound such that it's compliant
with the ringer mode and volume set on the mock in the
existing test. This way the test is independent of the flag
state.

Flag: android.media.audio.focus_exclusive_with_recording
Test: atest android.media.audio.cts.AudioFocusTest
Test: atest BuzzBeepBlinkTest NotificationAttentionHelperTest
Bug: 316414750
Change-Id: I54147c0dcccd7785589f4c9882582f42cf1a2d65
parent 3f6fad2a
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -1871,6 +1871,7 @@ package android.media {
    method public void setRampingRingerEnabled(boolean);
    method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public void setRs2Value(float);
    method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setTestDeviceConnectionState(@NonNull android.media.AudioDeviceAttributes, boolean);
    method @FlaggedApi("android.media.audio.focus_exclusive_with_recording") @RequiresPermission(android.Manifest.permission.QUERY_AUDIO_STATE) public boolean shouldNotificationSoundPlay(@NonNull android.media.AudioAttributes);
  }

  public static final class AudioRecord.MetricsConstants {
+23 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
import static android.content.Context.DEVICE_ID_DEFAULT;
import static android.media.audio.Flags.autoPublicVolumeApiHardening;
import static android.media.audio.Flags.automaticBtDeviceType;
import static android.media.audio.Flags.FLAG_FOCUS_EXCLUSIVE_WITH_RECORDING;
import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API;
import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;

@@ -10081,6 +10082,28 @@ public class AudioManager {
        }
    }

    /**
     * @hide
     * Checks whether a notification sound should be played or not, as reported by the state
     * of the audio framework. Querying whether playback should proceed is favored over
     * playing and letting the sound be muted or not.
     * @param aa the {@link AudioAttributes} of the notification about to maybe play
     * @return true if the audio framework state is such that the notification should be played
     *    because at time of checking, and the notification will be heard,
     *    false otherwise
     */
    @TestApi
    @FlaggedApi(FLAG_FOCUS_EXCLUSIVE_WITH_RECORDING)
    @RequiresPermission(android.Manifest.permission.QUERY_AUDIO_STATE)
    public boolean shouldNotificationSoundPlay(@NonNull final AudioAttributes aa) {
        final IAudioService service = getService();
        try {
            return service.shouldNotificationSoundPlay(Objects.requireNonNull(aa));
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    //====================================================================
    // Mute await connection

+4 −0
Original line number Diff line number Diff line
@@ -775,4 +775,8 @@ interface IAudioService {
    @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)")
    FadeManagerConfiguration getFadeManagerConfigurationForFocusLoss();

    @EnforcePermission("QUERY_AUDIO_STATE")
    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.QUERY_AUDIO_STATE)")
    boolean shouldNotificationSoundPlay(in AudioAttributes aa);
}
+40 −0
Original line number Diff line number Diff line
@@ -13588,6 +13588,46 @@ public class AudioService extends IAudioService.Stub
        }
    }
    /**
     * @see AudioManager#shouldNotificationSoundPlay(AudioAttributes)
     */
    @android.annotation.EnforcePermission(
            android.Manifest.permission.QUERY_AUDIO_STATE)
    public boolean shouldNotificationSoundPlay(@NonNull final AudioAttributes aa) {
        super.shouldNotificationSoundPlay_enforcePermission();
        Objects.requireNonNull(aa);
        // don't play notifications if the stream volume associated with the
        // AudioAttributes of the notification record is 0 (non-zero volume implies
        // not silenced by SILENT or VIBRATE ringer mode)
        final int stream = AudioAttributes.toLegacyStreamType(aa);
        final boolean mutingFromVolume = getStreamVolume(stream) == 0;
        if (mutingFromVolume) {
            if (DEBUG_VOL) {
                Slog.d(TAG, "notification should not play due to muted stream " + stream);
            }
            return false;
        }
        // don't play notifications if there is a user of GAIN_TRANSIENT_EXCLUSIVE audio focus
        // and the focus owner is recording
        final int uid = mMediaFocusControl.getExclusiveFocusOwnerUid();
        if (uid == -1) { // return value is -1 if focus isn't GAIN_TRANSIENT_EXCLUSIVE
            return true;
        }
        // is the owner of GAIN_TRANSIENT_EXCLUSIVE focus also recording?
        final boolean mutingFromFocusAndRecording = mRecordMonitor.isRecordingActiveForUid(uid);
        if (mutingFromFocusAndRecording) {
            if (DEBUG_VOL) {
                Slog.d(TAG, "notification should not play due to exclusive focus owner recording "
                        + " uid:" + uid);
            }
            return false;
        }
        return true;
    }
    //======================
    // Audioserver state dispatch
    //======================
+17 −0
Original line number Diff line number Diff line
@@ -296,6 +296,23 @@ public class MediaFocusControl implements PlayerFocusEnforcer {
        }
    }

    /**
     * Return the UID of the focus owner that has focus with exclusive focus gain
     * @return -1 if nobody has exclusive focus, the UID of the owner otherwise
     */
    protected int getExclusiveFocusOwnerUid() {
        synchronized (mAudioFocusLock) {
            if (mFocusStack.empty()) {
                return -1;
            }
            final FocusRequester owner = mFocusStack.peek();
            if (owner.getGainRequest() != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) {
                return -1;
            }
            return owner.getClientUid();
        }
    }

    /**
     * Send AUDIOFOCUS_LOSS to a specific stack entry.
     * Note this method is supporting an external API, and is restricted to LOSS in order to
Loading