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

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

AudioPolicy: add API to make a focus owner lose focus

Add a new AudioPolicy method that allows a registered policy
to force a focus owner lose focus. If it took focus from another
focus stack entry, that one will now regain focus.

Flag: android.multiuser.add_ui_for_sounds_from_background_users
Bug: 355596432
Bug: 355576058
Test: atest com.android.server.audio.MediaFocusControlTest#testSendFocusLossAndUpdate
Change-Id: Ib739f95c0f2792ec9ead59862f52cd4e11d33594
parent 24484f8e
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -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")
+23 −0
Original line number Diff line number Diff line
@@ -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).
+23 −3
Original line number Diff line number Diff line
@@ -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");
        }
+31 −0
Original line number Diff line number Diff line
@@ -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)
+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());
    }
}