diff --git a/core/api/system-current.txt b/core/api/system-current.txt index c9b8ba2f7fe2f..06edf95b0e340 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -5875,9 +5875,11 @@ package android.media.audiopolicy { method public android.media.AudioTrack createAudioTrackSource(android.media.audiopolicy.AudioMix) throws java.lang.IllegalArgumentException; method public int detachMixes(@NonNull java.util.List); method public int getFocusDuckingBehavior(); + method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List getFocusStack(); method public int getStatus(); method public boolean removeUidDeviceAffinity(int); method public boolean removeUserIdDeviceAffinity(int); + method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean sendFocusLoss(@NonNull android.media.AudioFocusInfo) throws java.lang.IllegalStateException; method public int setFocusDuckingBehavior(int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException; method public void setRegistration(String); method public boolean setUidDeviceAffinity(int, @NonNull java.util.List); diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 147e73599cadc..7f6fb90297b32 100755 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -460,4 +460,8 @@ interface IAudioService { boolean register); void setTestDeviceConnectionState(in AudioDeviceAttributes device, boolean connected); + + List getFocusStack(); + + boolean sendFocusLoss(in AudioFocusInfo focusLoser, in IAudioPolicyCallback apcb); } diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java index 0f08d79bf3bb0..3ba1d1f0eca26 100644 --- a/media/java/android/media/audiopolicy/AudioPolicy.java +++ b/media/java/android/media/audiopolicy/AudioPolicy.java @@ -19,6 +19,7 @@ package android.media.audiopolicy; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.TestApi; import android.annotation.UserIdInt; @@ -230,7 +231,7 @@ public class AudioPolicy { * If set to {@code true}, it is mandatory to set an * {@link AudioPolicy.AudioPolicyFocusListener} in order to successfully build * an {@code AudioPolicy} instance. - * @param enforce true if the policy will govern audio focus decisions. + * @param isFocusPolicy true if the policy will govern audio focus decisions. * @return the same Builder instance. */ @NonNull @@ -722,6 +723,45 @@ public class AudioPolicy { } } + /** + * Returns the list of entries in the focus stack. + * The list is ordered with increasing rank of focus ownership, where the last entry is at the + * top of the focus stack and is the current focus owner. + * @return the ordered list of focus owners + * @see AudioManager#registerAudioPolicy(AudioPolicy) + */ + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public @NonNull List getFocusStack() { + try { + return getService().getFocusStack(); + } 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). + * This operation is only valid for a registered policy (with + * {@link AudioManager#registerAudioPolicy(AudioPolicy)}) that is also set as the policy focus + * listener (with {@link Builder#setAudioPolicyFocusListener(AudioPolicyFocusListener)}. + * @param focusLoser the stack entry that is exiting the stack through a focus loss + * @return false if the focusLoser wasn't found in the stack, true otherwise + * @throws IllegalStateException if used on an unregistered policy, or a registered policy + * with no {@link AudioPolicyFocusListener} set + * @see AudioManager#registerAudioPolicy(AudioPolicy) + * @see Builder#setAudioPolicyStatusListener(AudioPolicyStatusListener) + */ + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public boolean sendFocusLoss(@NonNull AudioFocusInfo focusLoser) throws IllegalStateException { + Objects.requireNonNull(focusLoser); + try { + return getService().sendFocusLoss(focusLoser, cb()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Create an {@link AudioRecord} instance that is associated with the given {@link AudioMix}. * Audio buffers recorded through the created instance will contain the mix of the audio diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 6238198e94889..f857064a065af 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -10142,6 +10142,27 @@ public class AudioService extends IAudioService.Stub return AudioManager.SUCCESS; } + /** @see AudioPolicy#getFocusStack() */ + public List getFocusStack() { + enforceModifyAudioRoutingPermission(); + return mMediaFocusControl.getFocusStack(); + } + + /** @see AudioPolicy#sendFocusLoss */ + public boolean sendFocusLoss(@NonNull AudioFocusInfo focusLoser, + @NonNull IAudioPolicyCallback apcb) { + Objects.requireNonNull(focusLoser); + Objects.requireNonNull(apcb); + enforceModifyAudioRoutingPermission(); + 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"); + } + return mMediaFocusControl.sendFocusLoss(focusLoser); + } + /** see AudioManager.hasRegisteredDynamicPolicy */ public boolean hasRegisteredDynamicPolicy() { synchronized (mAudioPolicies) { diff --git a/services/core/java/com/android/server/audio/MediaFocusControl.java b/services/core/java/com/android/server/audio/MediaFocusControl.java index 66f62355c7f06..69a4c23cf8678 100644 --- a/services/core/java/com/android/server/audio/MediaFocusControl.java +++ b/services/core/java/com/android/server/audio/MediaFocusControl.java @@ -27,6 +27,7 @@ import android.media.AudioManager; import android.media.AudioSystem; import android.media.IAudioFocusDispatcher; import android.media.MediaMetrics; +import android.media.audiopolicy.AudioPolicy; import android.media.audiopolicy.IAudioPolicyCallback; import android.os.Binder; import android.os.Build; @@ -221,6 +222,51 @@ public class MediaFocusControl implements PlayerFocusEnforcer { } } + /** + * Return a copy of the focus stack for external consumption (composed of AudioFocusInfo + * instead of FocusRequester instances) + * @return a SystemApi-friendly version of the focus stack, in the same order (last entry + * is top of focus stack, i.e. latest focus owner) + * @see AudioPolicy#getFocusStack() + */ + @NonNull List getFocusStack() { + synchronized (mAudioFocusLock) { + final ArrayList stack = new ArrayList<>(mFocusStack.size()); + for (FocusRequester fr : mFocusStack) { + stack.add(fr.toAudioFocusInfo()); + } + return stack; + } + } + + /** + * Send AUDIOFOCUS_LOSS to a specific stack entry. + * Note this method is supporting an external API, and is restricted to LOSS in order to + * prevent allowing the stack to be in an invalid state (e.g. entry inside stack has focus) + * @param focusLoser the stack entry that is exiting the stack through a focus loss + * @return false if the focusLoser wasn't found in the stack, true otherwise + * @see AudioPolicy#sendFocusLoss(AudioFocusInfo) + */ + boolean sendFocusLoss(@NonNull AudioFocusInfo focusLoser) { + synchronized (mAudioFocusLock) { + FocusRequester loserToRemove = null; + for (FocusRequester fr : mFocusStack) { + if (fr.getClientId().equals(focusLoser.getClientId())) { + fr.handleFocusLoss(AudioManager.AUDIOFOCUS_LOSS, null, + false /*forceDuck*/); + loserToRemove = fr; + break; + } + } + if (loserToRemove != null) { + mFocusStack.remove(loserToRemove); + loserToRemove.release(); + return true; + } + } + return false; + } + @GuardedBy("mAudioFocusLock") private void notifyTopOfAudioFocusStack() { // notify the top of the stack it gained focus