Loading media/java/android/media/IAudioService.aidl +4 −0 Original line number Diff line number Diff line Loading @@ -709,6 +709,10 @@ interface IAudioService { @EnforcePermission("MODIFY_AUDIO_ROUTING") List<AudioFocusInfo> getFocusStack(); @EnforcePermission("MODIFY_AUDIO_ROUTING") oneway void sendFocusLossAndUpdate(in AudioFocusInfo focusLoser, in IAudioPolicyCallback apcb); @EnforcePermission("MODIFY_AUDIO_ROUTING") boolean sendFocusLoss(in AudioFocusInfo focusLoser, in IAudioPolicyCallback apcb); @EnforcePermission("MODIFY_AUDIO_ROUTING") Loading media/java/android/media/audiopolicy/AudioPolicy.java +23 −0 Original line number Diff line number Diff line Loading @@ -926,6 +926,29 @@ public class AudioPolicy { } } /** * @hide * Causes the given audio focus owner to lose audio focus with * {@link android.media.AudioManager#AUDIOFOCUS_LOSS}, and be removed from the focus stack. * Unlike {@link #sendFocusLoss(AudioFocusInfo)}, the method causes the focus stack * to be reevaluated as the discarded focus owner may have been at the top of stack, * and now the new owner needs to be notified of the gain. * @param focusLoser identifies the focus owner to discard from the focus stack * @throws IllegalStateException if used on an unregistered policy, or a registered policy * with no {@link AudioPolicyFocusListener} set * @see #getFocusStack() * @see #sendFocusLoss(AudioFocusInfo) */ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void sendFocusLossAndUpdate(@NonNull AudioFocusInfo focusLoser) throws IllegalStateException { try { getService().sendFocusLossAndUpdate(Objects.requireNonNull(focusLoser), cb()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Send AUDIOFOCUS_LOSS to a specific stack entry, causing it to be notified of the focus * loss, and for it to exit the focus stack (its focus listener will not be invoked after that). Loading services/core/java/com/android/server/audio/AudioService.java +23 −3 Original line number Diff line number Diff line Loading @@ -13383,19 +13383,39 @@ public class AudioService extends IAudioService.Stub } @android.annotation.EnforcePermission(MODIFY_AUDIO_ROUTING) /** @see AudioPolicy#getFocusStack() */ /* @see AudioPolicy#getFocusStack() */ public List<AudioFocusInfo> getFocusStack() { super.getFocusStack_enforcePermission(); return mMediaFocusControl.getFocusStack(); } /** @see AudioPolicy#sendFocusLoss */ /** * @param focusLoser non-null entry that may be in the stack * @see AudioPolicy#sendFocusLossAndUpdate(AudioFocusInfo) */ @android.annotation.EnforcePermission(MODIFY_AUDIO_ROUTING) public void sendFocusLossAndUpdate(@NonNull AudioFocusInfo focusLoser, @NonNull IAudioPolicyCallback apcb) { super.sendFocusLossAndUpdate_enforcePermission(); Objects.requireNonNull(apcb); if (!mAudioPolicies.containsKey(apcb.asBinder())) { throw new IllegalStateException("Only registered AudioPolicy can change focus"); } if (!mAudioPolicies.get(apcb.asBinder()).mHasFocusListener) { throw new IllegalStateException("AudioPolicy must have focus listener to change focus"); } mMediaFocusControl.sendFocusLossAndUpdate(Objects.requireNonNull(focusLoser)); } /* @see AudioPolicy#sendFocusLoss(AudioFocusInfo) */ @android.annotation.EnforcePermission(MODIFY_AUDIO_ROUTING) public boolean sendFocusLoss(@NonNull AudioFocusInfo focusLoser, @NonNull IAudioPolicyCallback apcb) { super.sendFocusLoss_enforcePermission(); Objects.requireNonNull(focusLoser); Objects.requireNonNull(apcb); enforceModifyAudioRoutingPermission(); if (!mAudioPolicies.containsKey(apcb.asBinder())) { throw new IllegalStateException("Only registered AudioPolicy can change focus"); } services/core/java/com/android/server/audio/MediaFocusControl.java +31 −0 Original line number Diff line number Diff line Loading @@ -279,6 +279,37 @@ public class MediaFocusControl implements PlayerFocusEnforcer { return true; } /** * Like {@link #sendFocusLoss(AudioFocusInfo)} but if the loser was at the top of stack, * make the next entry gain focus with {@link AudioManager#AUDIOFOCUS_GAIN}. * @param focusInfo the focus owner to discard * @see AudioPolicy#sendFocusLossAndUpdate(AudioFocusInfo) */ protected void sendFocusLossAndUpdate(@NonNull AudioFocusInfo focusInfo) { synchronized (mAudioFocusLock) { if (mFocusStack.isEmpty()) { return; } final FocusRequester currentFocusOwner = mFocusStack.peek(); if (currentFocusOwner.toAudioFocusInfo().equals(focusInfo)) { // focus loss is for the top of the stack currentFocusOwner.handleFocusLoss(AudioManager.AUDIOFOCUS_LOSS, null, false /*forceDuck*/); currentFocusOwner.release(); mFocusStack.pop(); // is there a new focus owner? if (!mFocusStack.isEmpty()) { mFocusStack.peek().handleFocusGain(AudioManager.AUDIOFOCUS_GAIN); } } else { // focus loss if for another entry that's not at the top of the stack, // just remove it from the stack and make it lose focus sendFocusLoss(focusInfo); } } } /** * Return a copy of the focus stack for external consumption (composed of AudioFocusInfo * instead of FocusRequester instances) Loading services/tests/servicestests/src/com/android/server/audio/MediaFocusControlTest.java 0 → 100644 +128 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.audio; import android.annotation.NonNull; import android.content.Context; import android.media.AudioAttributes; import android.media.AudioFocusInfo; import android.media.AudioManager; import android.os.Binder; import android.os.IBinder; import androidx.test.InstrumentationRegistry; import androidx.test.filters.MediumTest; import androidx.test.runner.AndroidJUnit4; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.util.List; @MediumTest @RunWith(AndroidJUnit4.class) public class MediaFocusControlTest { private static final String TAG = "MediaFocusControlTest"; private Context mContext; private MediaFocusControl mMediaFocusControl; private final IBinder mICallBack = new Binder(); private static class NoopPlayerFocusEnforcer implements PlayerFocusEnforcer { public boolean duckPlayers(@NonNull FocusRequester winner, @NonNull FocusRequester loser, boolean forceDuck) { return true; } public void restoreVShapedPlayers(@NonNull FocusRequester winner) { } public void mutePlayersForCall(int[] usagesToMute) { } public void unmutePlayersForCall() { } public boolean fadeOutPlayers(@NonNull FocusRequester winner, @NonNull FocusRequester loser) { return true; } public void forgetUid(int uid) { } public long getFadeOutDurationMillis(@NonNull AudioAttributes aa) { return 100; } public long getFadeInDelayForOffendersMillis(@NonNull AudioAttributes aa) { return 100; } public boolean shouldEnforceFade() { return false; } } @Before public void setUp() throws Exception { mContext = InstrumentationRegistry.getTargetContext(); mMediaFocusControl = new MediaFocusControl(mContext, new NoopPlayerFocusEnforcer()); } private static final AudioAttributes MEDIA_ATTRIBUTES = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA).build(); private static final AudioAttributes ALARM_ATTRIBUTES = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ALARM).build(); private static final int MEDIA_UID = 10300; private static final int ALARM_UID = 10301; /** * Test {@link MediaFocusControl#sendFocusLossAndUpdate(AudioFocusInfo)} */ @Test public void testSendFocusLossAndUpdate() throws Exception { // simulate a media app requesting focus, followed by an alarm mMediaFocusControl.requestAudioFocus(MEDIA_ATTRIBUTES, AudioManager.AUDIOFOCUS_GAIN, mICallBack, null /*focusDispatcher*/, "clientMedia", "packMedia", AudioManager.AUDIOFOCUS_FLAG_TEST /*flags*/, 35 /*sdk*/, false/*forceDuck*/, MEDIA_UID, true /*permissionOverridesCheck*/); final AudioFocusInfo alarm = new AudioFocusInfo(ALARM_ATTRIBUTES, ALARM_UID, "clientAlarm", "packAlarm", AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, 0/*lossReceived*/, AudioManager.AUDIOFOCUS_FLAG_TEST /*flags*/, 35 /*sdk*/); mMediaFocusControl.requestAudioFocus(alarm.getAttributes(), alarm.getGainRequest(), mICallBack, null /*focusDispatcher*/, alarm.getClientId(), alarm.getPackageName(), alarm.getFlags(), alarm.getSdkTarget(), false/*forceDuck*/, alarm.getClientUid(), true /*permissionOverridesCheck*/); // verify stack is in expected state List<AudioFocusInfo> stack = mMediaFocusControl.getFocusStack(); Assert.assertEquals("focus stack should have 2 entries", 2, stack.size()); Assert.assertEquals("focus loser should have received LOSS_TRANSIENT_CAN_DUCK", AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK, stack.get(0).getLossReceived()); // make alarm app lose focus and check stack mMediaFocusControl.sendFocusLossAndUpdate(alarm); stack = mMediaFocusControl.getFocusStack(); Assert.assertEquals("focus stack should have 1 entry after sendFocusLossAndUpdate", 1, stack.size()); Assert.assertEquals("new top of stack should be media app", MEDIA_UID, stack.get(0).getClientUid()); } } Loading
media/java/android/media/IAudioService.aidl +4 −0 Original line number Diff line number Diff line Loading @@ -709,6 +709,10 @@ interface IAudioService { @EnforcePermission("MODIFY_AUDIO_ROUTING") List<AudioFocusInfo> getFocusStack(); @EnforcePermission("MODIFY_AUDIO_ROUTING") oneway void sendFocusLossAndUpdate(in AudioFocusInfo focusLoser, in IAudioPolicyCallback apcb); @EnforcePermission("MODIFY_AUDIO_ROUTING") boolean sendFocusLoss(in AudioFocusInfo focusLoser, in IAudioPolicyCallback apcb); @EnforcePermission("MODIFY_AUDIO_ROUTING") Loading
media/java/android/media/audiopolicy/AudioPolicy.java +23 −0 Original line number Diff line number Diff line Loading @@ -926,6 +926,29 @@ public class AudioPolicy { } } /** * @hide * Causes the given audio focus owner to lose audio focus with * {@link android.media.AudioManager#AUDIOFOCUS_LOSS}, and be removed from the focus stack. * Unlike {@link #sendFocusLoss(AudioFocusInfo)}, the method causes the focus stack * to be reevaluated as the discarded focus owner may have been at the top of stack, * and now the new owner needs to be notified of the gain. * @param focusLoser identifies the focus owner to discard from the focus stack * @throws IllegalStateException if used on an unregistered policy, or a registered policy * with no {@link AudioPolicyFocusListener} set * @see #getFocusStack() * @see #sendFocusLoss(AudioFocusInfo) */ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void sendFocusLossAndUpdate(@NonNull AudioFocusInfo focusLoser) throws IllegalStateException { try { getService().sendFocusLossAndUpdate(Objects.requireNonNull(focusLoser), cb()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Send AUDIOFOCUS_LOSS to a specific stack entry, causing it to be notified of the focus * loss, and for it to exit the focus stack (its focus listener will not be invoked after that). Loading
services/core/java/com/android/server/audio/AudioService.java +23 −3 Original line number Diff line number Diff line Loading @@ -13383,19 +13383,39 @@ public class AudioService extends IAudioService.Stub } @android.annotation.EnforcePermission(MODIFY_AUDIO_ROUTING) /** @see AudioPolicy#getFocusStack() */ /* @see AudioPolicy#getFocusStack() */ public List<AudioFocusInfo> getFocusStack() { super.getFocusStack_enforcePermission(); return mMediaFocusControl.getFocusStack(); } /** @see AudioPolicy#sendFocusLoss */ /** * @param focusLoser non-null entry that may be in the stack * @see AudioPolicy#sendFocusLossAndUpdate(AudioFocusInfo) */ @android.annotation.EnforcePermission(MODIFY_AUDIO_ROUTING) public void sendFocusLossAndUpdate(@NonNull AudioFocusInfo focusLoser, @NonNull IAudioPolicyCallback apcb) { super.sendFocusLossAndUpdate_enforcePermission(); Objects.requireNonNull(apcb); if (!mAudioPolicies.containsKey(apcb.asBinder())) { throw new IllegalStateException("Only registered AudioPolicy can change focus"); } if (!mAudioPolicies.get(apcb.asBinder()).mHasFocusListener) { throw new IllegalStateException("AudioPolicy must have focus listener to change focus"); } mMediaFocusControl.sendFocusLossAndUpdate(Objects.requireNonNull(focusLoser)); } /* @see AudioPolicy#sendFocusLoss(AudioFocusInfo) */ @android.annotation.EnforcePermission(MODIFY_AUDIO_ROUTING) public boolean sendFocusLoss(@NonNull AudioFocusInfo focusLoser, @NonNull IAudioPolicyCallback apcb) { super.sendFocusLoss_enforcePermission(); Objects.requireNonNull(focusLoser); Objects.requireNonNull(apcb); enforceModifyAudioRoutingPermission(); if (!mAudioPolicies.containsKey(apcb.asBinder())) { throw new IllegalStateException("Only registered AudioPolicy can change focus"); }
services/core/java/com/android/server/audio/MediaFocusControl.java +31 −0 Original line number Diff line number Diff line Loading @@ -279,6 +279,37 @@ public class MediaFocusControl implements PlayerFocusEnforcer { return true; } /** * Like {@link #sendFocusLoss(AudioFocusInfo)} but if the loser was at the top of stack, * make the next entry gain focus with {@link AudioManager#AUDIOFOCUS_GAIN}. * @param focusInfo the focus owner to discard * @see AudioPolicy#sendFocusLossAndUpdate(AudioFocusInfo) */ protected void sendFocusLossAndUpdate(@NonNull AudioFocusInfo focusInfo) { synchronized (mAudioFocusLock) { if (mFocusStack.isEmpty()) { return; } final FocusRequester currentFocusOwner = mFocusStack.peek(); if (currentFocusOwner.toAudioFocusInfo().equals(focusInfo)) { // focus loss is for the top of the stack currentFocusOwner.handleFocusLoss(AudioManager.AUDIOFOCUS_LOSS, null, false /*forceDuck*/); currentFocusOwner.release(); mFocusStack.pop(); // is there a new focus owner? if (!mFocusStack.isEmpty()) { mFocusStack.peek().handleFocusGain(AudioManager.AUDIOFOCUS_GAIN); } } else { // focus loss if for another entry that's not at the top of the stack, // just remove it from the stack and make it lose focus sendFocusLoss(focusInfo); } } } /** * Return a copy of the focus stack for external consumption (composed of AudioFocusInfo * instead of FocusRequester instances) Loading
services/tests/servicestests/src/com/android/server/audio/MediaFocusControlTest.java 0 → 100644 +128 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.audio; import android.annotation.NonNull; import android.content.Context; import android.media.AudioAttributes; import android.media.AudioFocusInfo; import android.media.AudioManager; import android.os.Binder; import android.os.IBinder; import androidx.test.InstrumentationRegistry; import androidx.test.filters.MediumTest; import androidx.test.runner.AndroidJUnit4; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.util.List; @MediumTest @RunWith(AndroidJUnit4.class) public class MediaFocusControlTest { private static final String TAG = "MediaFocusControlTest"; private Context mContext; private MediaFocusControl mMediaFocusControl; private final IBinder mICallBack = new Binder(); private static class NoopPlayerFocusEnforcer implements PlayerFocusEnforcer { public boolean duckPlayers(@NonNull FocusRequester winner, @NonNull FocusRequester loser, boolean forceDuck) { return true; } public void restoreVShapedPlayers(@NonNull FocusRequester winner) { } public void mutePlayersForCall(int[] usagesToMute) { } public void unmutePlayersForCall() { } public boolean fadeOutPlayers(@NonNull FocusRequester winner, @NonNull FocusRequester loser) { return true; } public void forgetUid(int uid) { } public long getFadeOutDurationMillis(@NonNull AudioAttributes aa) { return 100; } public long getFadeInDelayForOffendersMillis(@NonNull AudioAttributes aa) { return 100; } public boolean shouldEnforceFade() { return false; } } @Before public void setUp() throws Exception { mContext = InstrumentationRegistry.getTargetContext(); mMediaFocusControl = new MediaFocusControl(mContext, new NoopPlayerFocusEnforcer()); } private static final AudioAttributes MEDIA_ATTRIBUTES = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA).build(); private static final AudioAttributes ALARM_ATTRIBUTES = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ALARM).build(); private static final int MEDIA_UID = 10300; private static final int ALARM_UID = 10301; /** * Test {@link MediaFocusControl#sendFocusLossAndUpdate(AudioFocusInfo)} */ @Test public void testSendFocusLossAndUpdate() throws Exception { // simulate a media app requesting focus, followed by an alarm mMediaFocusControl.requestAudioFocus(MEDIA_ATTRIBUTES, AudioManager.AUDIOFOCUS_GAIN, mICallBack, null /*focusDispatcher*/, "clientMedia", "packMedia", AudioManager.AUDIOFOCUS_FLAG_TEST /*flags*/, 35 /*sdk*/, false/*forceDuck*/, MEDIA_UID, true /*permissionOverridesCheck*/); final AudioFocusInfo alarm = new AudioFocusInfo(ALARM_ATTRIBUTES, ALARM_UID, "clientAlarm", "packAlarm", AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, 0/*lossReceived*/, AudioManager.AUDIOFOCUS_FLAG_TEST /*flags*/, 35 /*sdk*/); mMediaFocusControl.requestAudioFocus(alarm.getAttributes(), alarm.getGainRequest(), mICallBack, null /*focusDispatcher*/, alarm.getClientId(), alarm.getPackageName(), alarm.getFlags(), alarm.getSdkTarget(), false/*forceDuck*/, alarm.getClientUid(), true /*permissionOverridesCheck*/); // verify stack is in expected state List<AudioFocusInfo> stack = mMediaFocusControl.getFocusStack(); Assert.assertEquals("focus stack should have 2 entries", 2, stack.size()); Assert.assertEquals("focus loser should have received LOSS_TRANSIENT_CAN_DUCK", AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK, stack.get(0).getLossReceived()); // make alarm app lose focus and check stack mMediaFocusControl.sendFocusLossAndUpdate(alarm); stack = mMediaFocusControl.getFocusStack(); Assert.assertEquals("focus stack should have 1 entry after sendFocusLossAndUpdate", 1, stack.size()); Assert.assertEquals("new top of stack should be media app", MEDIA_UID, stack.get(0).getClientUid()); } }