diff options
| author | 2023-06-26 18:51:19 -0700 | |
|---|---|---|
| committer | 2023-08-08 16:08:28 +0000 | |
| commit | 969fc708f83a42e2bc2eb6fe9dcfb30fc37beeeb (patch) | |
| tree | 4512a906b13df0d26b1213ee7b1c0dbc04ccdf49 | |
| parent | 9567b4ad0ac98153eb24c514bc13240f7cedf64e (diff) | |
AudioService: add test APIs for audio focus and ducking
Note: all new test methods end with "ForTest" so there is
no ambiguity about the purpose of those methods.
Add test methods to protect focus-based tests from external
audio focus uses (e.g. NotificationManager playing a
notification on the DUT during a test).
Add test methods to read ducking-related timing values, and
the list of ducked UIDs.
Add more debug messages (disabled by default).
Bug: 286495078
Test: atest CtsMediaAudioTestCases:android.media.audio.cts.AudioFocusTest#testDuckedUidsAfterMediaSpeech
Test: atest CtsMediaAudioTestCases:android.media.audio.cts.AudioFocusTest#testDuckedUidsAfterMediaMusic
Change-Id: Iee8f817673647c12b70beb1750498c1be386ae76
7 files changed, 358 insertions, 1 deletions
diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 833d9c6d2420..3a4c3f2b32bd 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1877,6 +1877,8 @@ package android.media { public class AudioManager { method @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public int abandonAudioFocusForTest(@NonNull android.media.AudioFocusRequest, @NonNull String); + method @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public boolean enterAudioFocusFreezeForTest(@NonNull java.util.List<java.lang.Integer>); + method @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public boolean exitAudioFocusFreezeForTest(); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public void forceComputeCsdOnAllDevices(boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public void forceUseFrameworkMel(boolean); method @NonNull @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public android.media.AudioRecord getCallDownlinkExtractionAudioRecord(@NonNull android.media.AudioFormat); @@ -1884,6 +1886,9 @@ package android.media { method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public float getCsd(); method @Nullable public static android.media.AudioDeviceInfo getDeviceInfoFromType(int); method @IntRange(from=0) @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public long getFadeOutDurationOnFocusLossMillis(@NonNull android.media.AudioAttributes); + method @NonNull @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public java.util.List<java.lang.Integer> getFocusDuckedUidsForTest(); + method @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public long getFocusFadeOutDurationForTest(); + method @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public long getFocusUnmuteDelayAfterFadeOutForTest(); method @Nullable public static android.media.AudioHalVersionInfo getHalVersion(); method public static final int[] getPublicStreamTypes(); method @NonNull public java.util.List<java.lang.Integer> getReportedSurroundFormats(); diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index e8c9d0dbd884..9b48677ca892 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -4739,6 +4739,97 @@ public class AudioManager { /** * @hide + * Test method to return the list of UIDs currently marked as ducked because of their + * audio focus status + * @return the list of UIDs, can be empty when no app is being ducked. + */ + @TestApi + @RequiresPermission("android.permission.QUERY_AUDIO_STATE") + public @NonNull List<Integer> getFocusDuckedUidsForTest() { + try { + return getService().getFocusDuckedUidsForTest(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * Test method to return the duration of the fade out applied on the players of a focus loser + * @return the fade out duration in ms + */ + @TestApi + @RequiresPermission("android.permission.QUERY_AUDIO_STATE") + public long getFocusFadeOutDurationForTest() { + try { + return getService().getFocusFadeOutDurationForTest(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * Test method to return the length of time after a fade-out before the focus loser is unmuted + * (and is faded back in). + * @return the time gap after a fade-out completion on focus loss, and fade-in start in ms. + */ + @TestApi + @RequiresPermission("android.permission.QUERY_AUDIO_STATE") + public long getFocusUnmuteDelayAfterFadeOutForTest() { + try { + return getService().getFocusUnmuteDelayAfterFadeOutForTest(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * Test method to start preventing applications from requesting audio focus during a test, + * which could interfere with the functionality/behavior under test. + * Calling this method needs to be paired with a call to {@link #exitAudioFocusFreezeForTest} + * when the testing is done. If this is not the case (e.g. in case of a test crash), + * a death observer mechanism will ensure the system is not left in a bad state, but this should + * not be relied on when implementing tests. + * @param exemptedUids a list of UIDs that are exempt from the freeze. This would for instance + * be those of the test runner and other players used in the test, or the "fake" UIDs used + * for testing with {@link #requestAudioFocusForTest(AudioFocusRequest, String, int, int)}. + * @return true if the focus freeze mode is successfully entered, false if there was an issue, + * such as another freeze in place at the time of invocation. + * A false result should result in a test failure as this would indicate the system is not + * in a proper state with a predictable behavior for audio focus management. + */ + @TestApi + @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") + public boolean enterAudioFocusFreezeForTest(@NonNull List<Integer> exemptedUids) { + Objects.requireNonNull(exemptedUids); + try { + final int[] uids = exemptedUids.stream().mapToInt(Integer::intValue).toArray(); + return getService().enterAudioFocusFreezeForTest(mICallBack, uids); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * Test method to end preventing applications from requesting audio focus during a test. + * @return true if the focus freeze mode is successfully exited, false if there was an issue, + * such as the freeze already having ended, or not started. + */ + @TestApi + @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") + public boolean exitAudioFocusFreezeForTest() { + try { + return getService().exitAudioFocusFreezeForTest(mICallBack); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide * Request or lock audio focus. * This method is to be used by system components that have registered an * {@link android.media.audiopolicy.AudioPolicy} to request audio focus, but also to "lock" it diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index b2466e990b8f..58378941c7ef 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -520,6 +520,23 @@ interface IAudioService { long getFadeOutDurationOnFocusLossMillis(in AudioAttributes aa); + @EnforcePermission("QUERY_AUDIO_STATE") + /* Returns a List<Integer> */ + @SuppressWarnings(value = {"untyped-collection"}) + List getFocusDuckedUidsForTest(); + + @EnforcePermission("QUERY_AUDIO_STATE") + long getFocusFadeOutDurationForTest(); + + @EnforcePermission("QUERY_AUDIO_STATE") + long getFocusUnmuteDelayAfterFadeOutForTest(); + + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + boolean enterAudioFocusFreezeForTest(IBinder cb, in int[] uids); + + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + boolean exitAudioFocusFreezeForTest(IBinder cb); + void registerModeDispatcher(IAudioModeDispatcher dispatcher); oneway void unregisterModeDispatcher(IAudioModeDispatcher dispatcher); diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 76c4cfe929bb..b6411191a372 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -43,6 +43,7 @@ import static com.android.server.utils.EventLogger.Event.ALOGI; import static com.android.server.utils.EventLogger.Event.ALOGW; import android.Manifest; +import android.annotation.EnforcePermission; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; @@ -9998,6 +9999,14 @@ public class AudioService extends IAudioService.Stub return mMediaFocusControl.abandonAudioFocus(fd, clientId, aa, callingPackageName); } + /** see {@link AudioManager#getFocusDuckedUidsForTest()} */ + @Override + @EnforcePermission("QUERY_AUDIO_STATE") + public @NonNull List<Integer> getFocusDuckedUidsForTest() { + super.getFocusDuckedUidsForTest_enforcePermission(); + return mPlaybackMonitor.getFocusDuckedUids(); + } + public void unregisterAudioFocusClient(String clientId) { new MediaMetrics.Item(mMetricsId + "focus") .set(MediaMetrics.Property.CLIENT_NAME, clientId) @@ -10014,6 +10023,67 @@ public class AudioService extends IAudioService.Stub return mMediaFocusControl.getFocusRampTimeMs(focusGain, attr); } + /** + * Test method to return the duration of the fade out applied on the players of a focus loser + * @see AudioManager#getFocusFadeOutDurationForTest() + * @return the fade out duration, in ms + */ + public long getFocusFadeOutDurationForTest() { + super.getFocusFadeOutDurationForTest_enforcePermission(); + return mMediaFocusControl.getFocusFadeOutDurationForTest(); + } + + /** + * Test method to return the length of time after a fade out before the focus loser is unmuted + * (and is faded back in). + * @see AudioManager#getFocusUnmuteDelayAfterFadeOutForTest() + * @return the time gap after a fade out completion on focus loss, and fade in start, in ms + */ + @Override + @EnforcePermission("QUERY_AUDIO_STATE") + public long getFocusUnmuteDelayAfterFadeOutForTest() { + super.getFocusUnmuteDelayAfterFadeOutForTest_enforcePermission(); + return mMediaFocusControl.getFocusUnmuteDelayAfterFadeOutForTest(); + } + + /** + * Test method to start preventing applications from requesting audio focus during a test, + * which could interfere with the testing of the functionality/behavior under test. + * Calling this method needs to be paired with a call to {@link #exitAudioFocusFreezeForTest} + * when the testing is done. If this is not the case (e.g. in case of a test crash), + * a death observer mechanism will ensure the system is not left in a bad state, but this should + * not be relied on when implementing tests. + * @see AudioManager#enterAudioFocusFreezeForTest(List) + * @param cb IBinder to track the death of the client of this method + * @param exemptedUids a list of UIDs that are exempt from the freeze. This would for instance + * be those of the test runner and other players used in the test + * @return true if the focus freeze mode is successfully entered, false if there was an issue, + * such as another freeze currently used. + */ + @Override + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + public boolean enterAudioFocusFreezeForTest(IBinder cb, int[] exemptedUids) { + super.enterAudioFocusFreezeForTest_enforcePermission(); + Objects.requireNonNull(exemptedUids); + Objects.requireNonNull(cb); + return mMediaFocusControl.enterAudioFocusFreezeForTest(cb, exemptedUids); + } + + /** + * Test method to end preventing applications from requesting audio focus during a test. + * @see AudioManager#exitAudioFocusFreezeForTest() + * @param cb IBinder identifying the client of this method + * @return true if the focus freeze mode is successfully exited, false if there was an issue, + * such as the freeze already having ended, or not started. + */ + @Override + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + public boolean exitAudioFocusFreezeForTest(IBinder cb) { + super.exitAudioFocusFreezeForTest_enforcePermission(); + Objects.requireNonNull(cb); + return mMediaFocusControl.exitAudioFocusFreezeForTest(cb); + } + /** only public for mocking/spying, do not call outside of AudioService */ @VisibleForTesting public boolean hasAudioFocusUsers() { @@ -10021,6 +10091,8 @@ public class AudioService extends IAudioService.Stub } /** see {@link AudioManager#getFadeOutDurationOnFocusLossMillis(AudioAttributes)} */ + @Override + @EnforcePermission("QUERY_AUDIO_STATE") public long getFadeOutDurationOnFocusLossMillis(AudioAttributes aa) { if (!enforceQueryAudioStateForTest("fade out duration")) { return 0; diff --git a/services/core/java/com/android/server/audio/FocusRequester.java b/services/core/java/com/android/server/audio/FocusRequester.java index 88a4b0531cb1..010d5f41bc7d 100644 --- a/services/core/java/com/android/server/audio/FocusRequester.java +++ b/services/core/java/com/android/server/audio/FocusRequester.java @@ -43,7 +43,7 @@ import java.io.PrintWriter; public class FocusRequester { // on purpose not using this classe's name, as it will only be used from MediaFocusControl - private static final String TAG = "MediaFocusControl"; + private static final String TAG = "FocusRequester"; private static final boolean DEBUG = false; private AudioFocusDeathHandler mDeathHandler; // may be null @@ -340,6 +340,9 @@ public class FocusRequester { @GuardedBy("MediaFocusControl.mAudioFocusLock") boolean handleFocusLossFromGain(int focusGain, final FocusRequester frWinner, boolean forceDuck) { + if (DEBUG) { + Log.i(TAG, "handleFocusLossFromGain for " + mClientId + " gain:" + focusGain); + } final int focusLoss = focusLossForGainRequest(focusGain); handleFocusLoss(focusLoss, frWinner, forceDuck); return (focusLoss == AudioManager.AUDIOFOCUS_LOSS); @@ -378,6 +381,9 @@ public class FocusRequester { @GuardedBy("MediaFocusControl.mAudioFocusLock") void handleFocusLoss(int focusLoss, @Nullable final FocusRequester frWinner, boolean forceDuck) { + if (DEBUG) { + Log.i(TAG, "handleFocusLoss for " + mClientId + " loss:" + focusLoss); + } try { if (focusLoss != mFocusLossReceived) { mFocusLossReceived = focusLoss; @@ -427,6 +433,9 @@ public class FocusRequester { toAudioFocusInfo(), true /* wasDispatched */); mFocusLossWasNotified = true; fd.dispatchAudioFocusChange(mFocusLossReceived, mClientId); + } else if (DEBUG) { + Log.i(TAG, "NOT dispatching " + focusChangeToString(mFocusLossReceived) + + " to " + mClientId + " no IAudioFocusDispatcher"); } } } catch (android.os.RemoteException e) { diff --git a/services/core/java/com/android/server/audio/MediaFocusControl.java b/services/core/java/com/android/server/audio/MediaFocusControl.java index b2180962a96e..65f6c9b8d459 100644 --- a/services/core/java/com/android/server/audio/MediaFocusControl.java +++ b/services/core/java/com/android/server/audio/MediaFocusControl.java @@ -45,6 +45,7 @@ import com.android.server.utils.EventLogger; import java.io.PrintWriter; import java.text.DateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Iterator; @@ -122,6 +123,23 @@ public class MediaFocusControl implements PlayerFocusEnforcer { dumpMultiAudioFocus(pw); } + /** + * Test method to return the duration of the fade out applied on the players of a focus loser + * @return the fade out duration in ms + */ + public long getFocusFadeOutDurationForTest() { + return FadeOutManager.FADE_OUT_DURATION_MS; + } + + /** + * Test method to return the length of time after a fade out before the focus loser is unmuted + * (and is faded back in). + * @return the time gap after a fade out completion on focus loss, and fade in start in ms + */ + public long getFocusUnmuteDelayAfterFadeOutForTest() { + return FadeOutManager.DELAY_FADE_IN_OFFENDERS_MS; + } + //================================================================= // PlayerFocusEnforcer implementation @Override @@ -304,17 +322,26 @@ public class MediaFocusControl implements PlayerFocusEnforcer { @GuardedBy("mAudioFocusLock") private void propagateFocusLossFromGain_syncAf(int focusGain, final FocusRequester fr, boolean forceDuck) { + if (DEBUG) { + Log.i(TAG, "propagateFocusLossFromGain_syncAf gain:" + focusGain); + } final List<String> clientsToRemove = new LinkedList<String>(); // going through the audio focus stack to signal new focus, traversing order doesn't // matter as all entries respond to the same external focus gain if (!mFocusStack.empty()) { for (FocusRequester focusLoser : mFocusStack) { + if (DEBUG) { + Log.i(TAG, "propagateFocusLossFromGain_syncAf checking client:" + + focusLoser.getClientId()); + } final boolean isDefinitiveLoss = focusLoser.handleFocusLossFromGain(focusGain, fr, forceDuck); if (isDefinitiveLoss) { clientsToRemove.add(focusLoser.getClientId()); } } + } else if (DEBUG) { + Log.i(TAG, "propagateFocusLossFromGain_syncAf empty stack"); } if (mMultiAudioFocusEnabled && !mMultiAudioFocusList.isEmpty()) { @@ -370,6 +397,9 @@ public class MediaFocusControl implements PlayerFocusEnforcer { @GuardedBy("mAudioFocusLock") private void removeFocusStackEntry(String clientToRemove, boolean signal, boolean notifyFocusFollowers) { + if (DEBUG) { + Log.i(TAG, "removeFocusStackEntry client:" + clientToRemove); + } AudioFocusInfo abandonSource = null; // is the current top of the focus stack abandoning focus? (because of request, not death) if (!mFocusStack.empty() && mFocusStack.peek().hasSameClient(clientToRemove)) @@ -1000,6 +1030,24 @@ public class MediaFocusControl implements PlayerFocusEnforcer { } synchronized(mAudioFocusLock) { + // check whether a focus freeze is in place and filter + if (isFocusFrozenForTest()) { + int focusRequesterUid; + if ((flags & AudioManager.AUDIOFOCUS_FLAG_TEST) + == AudioManager.AUDIOFOCUS_FLAG_TEST) { + focusRequesterUid = testUid; + } else { + focusRequesterUid = Binder.getCallingUid(); + } + if (isFocusFrozenForTestForUid(focusRequesterUid)) { + Log.i(TAG, "requestAudioFocus: focus frozen for test for uid:" + + focusRequesterUid); + return AudioManager.AUDIOFOCUS_REQUEST_FAILED; + } + Log.i(TAG, "requestAudioFocus: focus frozen for test but uid:" + focusRequesterUid + + " is exempt"); + } + if (mFocusStack.size() > MAX_STACK_SIZE) { Log.e(TAG, "Max AudioFocus stack size reached, failing requestAudioFocus()"); return AudioManager.AUDIOFOCUS_REQUEST_FAILED; @@ -1191,6 +1239,110 @@ public class MediaFocusControl implements PlayerFocusEnforcer { return AudioManager.AUDIOFOCUS_REQUEST_GRANTED; } + /** + * Reference to the caller of {@link #enterAudioFocusFreezeForTest(IBinder, int[])} + * Will be null when there is no focus freeze for test + */ + @GuardedBy("mAudioFocusLock") + @Nullable + private IBinder mFocusFreezerForTest = null; + + /** + * The death handler for {@link #mFocusFreezerForTest} + * Will be null when there is no focus freeze for test + */ + @GuardedBy("mAudioFocusLock") + @Nullable + private IBinder.DeathRecipient mFocusFreezerDeathHandler = null; + + /** + * Array of UIDs exempt from focus freeze when focus is frozen for test, null during normal + * operations. + * Will be null when there is no focus freeze for test + */ + @GuardedBy("mAudioFocusLock") + @Nullable + private int[] mFocusFreezeExemptUids = null; + + @GuardedBy("mAudioFocusLock") + private boolean isFocusFrozenForTest() { + return (mFocusFreezerForTest != null); + } + + /** + * Checks if the given UID can request focus when a focus freeze is in place for a test. + * Focus can be requested if focus is not frozen or if it's frozen but the UID is exempt. + * @param uidToCheck + * @return true if that UID is barred from requesting focus, false if its focus request + * can proceed being processed + */ + @GuardedBy("mAudioFocusLock") + private boolean isFocusFrozenForTestForUid(int uidToCheck) { + if (isFocusFrozenForTest()) { + return false; + } + // check the list of exempts (array is not null because we're in a freeze for test + for (int uid : mFocusFreezeExemptUids) { + if (uid == uidToCheck) { + return false; + } + } + // uid was not found in the exempt list, its focus request is denied + return true; + } + + protected boolean enterAudioFocusFreezeForTest( + @NonNull IBinder cb, @NonNull int[] exemptedUids) { + Log.i(TAG, "enterAudioFocusFreezeForTest UIDs exempt:" + Arrays.toString(exemptedUids)); + synchronized (mAudioFocusLock) { + if (mFocusFreezerForTest != null) { + Log.e(TAG, "Error enterAudioFocusFreezeForTest: focus already frozen"); + return false; + } + // new focus freeze, register death handler + try { + mFocusFreezerDeathHandler = new IBinder.DeathRecipient() { + @Override + public void binderDied() { + Log.i(TAG, "Audio focus freezer died, exiting focus freeze for test"); + releaseFocusFreeze(); + } + }; + cb.linkToDeath(mFocusFreezerDeathHandler, 0); + mFocusFreezerForTest = cb; + mFocusFreezeExemptUids = exemptedUids.clone(); + } catch (RemoteException e) { + // client has already died! + mFocusFreezerForTest = null; + mFocusFreezeExemptUids = null; + return false; + } + } + return true; + } + + protected boolean exitAudioFocusFreezeForTest(@NonNull IBinder cb) { + synchronized (mAudioFocusLock) { + if (mFocusFreezerForTest != cb) { + Log.e(TAG, "Error exitAudioFocusFreezeForTest: " + + ((mFocusFreezerForTest == null) + ? "call to exit while not frozen" + : "call to exit not coming from freeze owner")); + return false; + } + mFocusFreezerForTest.unlinkToDeath(mFocusFreezerDeathHandler, 0); + releaseFocusFreeze(); + } + return true; + } + + private void releaseFocusFreeze() { + synchronized (mAudioFocusLock) { + mFocusFreezerDeathHandler = null; + mFocusFreezeExemptUids = null; + mFocusFreezerForTest = null; + } + } protected void unregisterAudioFocusClient(String clientId) { synchronized(mAudioFocusLock) { diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java index 23a0782dc8a3..54fa6fbc3bfd 100644 --- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java +++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java @@ -1208,6 +1208,17 @@ public final class PlaybackActivityMonitor } } + protected @NonNull List<Integer> getFocusDuckedUids() { + final ArrayList<Integer> duckedUids; + synchronized (mPlayerLock) { + duckedUids = new ArrayList(mDuckingManager.mDuckers.keySet()); + } + if (DEBUG) { + Log.i(TAG, "current ducked UIDs: " + duckedUids); + } + return duckedUids; + } + //================================================================= // For logging private static final class PlayerEvent extends EventLogger.Event { |