diff --git a/api/system-current.txt b/api/system-current.txt index 24936d5784f8f..fba387b27588a 100755 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -4267,6 +4267,7 @@ package android.media { public class AudioManager { method @Deprecated public int abandonAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, android.media.AudioAttributes); + method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDeviceForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener) throws java.lang.SecurityException; method public void clearAudioServerStateCallback(); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int dispatchAudioFocusChange(@NonNull android.media.AudioFocusInfo, int, @NonNull android.media.audiopolicy.AudioPolicy); method @IntRange(from=0) public int getAdditionalOutputDeviceDelay(@NonNull android.media.AudioDeviceInfo); @@ -4283,6 +4284,7 @@ package android.media { method public boolean isHdmiSystemAudioSupported(); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int registerAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy); method public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback); + method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDeviceForStrategyChangedListener(@NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean removePreferredDeviceForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public int requestAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, @NonNull android.media.AudioAttributes, int, int) throws java.lang.IllegalArgumentException; method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_PHONE_STATE, android.Manifest.permission.MODIFY_AUDIO_ROUTING}) public int requestAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, @NonNull android.media.AudioAttributes, int, int, android.media.audiopolicy.AudioPolicy) throws java.lang.IllegalArgumentException; @@ -4309,6 +4311,10 @@ package android.media { method public void onAudioServerUp(); } + public static interface AudioManager.OnPreferredDeviceForStrategyChangedListener { + method public void onPreferredDeviceForStrategyChanged(@NonNull android.media.audiopolicy.AudioProductStrategy, @Nullable android.media.AudioDevice); + } + public abstract static class AudioManager.VolumeGroupCallback { ctor public AudioManager.VolumeGroupCallback(); method public void onAudioVolumeGroupChanged(int, int); diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 4a1088bfa8770..112bb9c312764 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -17,6 +17,7 @@ package android.media; +import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; @@ -1649,6 +1650,179 @@ public class AudioManager { } } + /** + * @hide + * Interface to be notified of changes in the preferred audio device set for a given audio + * strategy. + * @see #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDevice) + * @see #removePreferredDeviceForStrategy(AudioProductStrategy) + * @see #getPreferredDeviceForStrategy(AudioProductStrategy) + */ + @SystemApi + public interface OnPreferredDeviceForStrategyChangedListener { + /** + * Called on the listener to indicate that the preferred audio device for the given + * strategy has changed. + * @param strategy the {@link AudioProductStrategy} whose preferred device changed + * @param device null if the preferred device was removed, or the newly set + * preferred audio device + */ + void onPreferredDeviceForStrategyChanged(@NonNull AudioProductStrategy strategy, + @Nullable AudioDevice device); + } + + /** + * @hide + * Adds a listener for being notified of changes to the strategy-preferred audio device. + * @param executor + * @param listener + * @throws SecurityException if the caller doesn't hold the required permission + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public void addOnPreferredDeviceForStrategyChangedListener( + @NonNull @CallbackExecutor Executor executor, + @NonNull OnPreferredDeviceForStrategyChangedListener listener) + throws SecurityException { + Objects.requireNonNull(executor); + Objects.requireNonNull(listener); + synchronized (mPrefDevListenerLock) { + if (hasPrefDevListener(listener)) { + throw new IllegalArgumentException( + "attempt to call addOnPreferredDeviceForStrategyChangedListener() " + + "on a previously registered listener"); + } + // lazy initialization of the list of strategy-preferred device listener + if (mPrefDevListeners == null) { + mPrefDevListeners = new ArrayList<>(); + } + final int oldCbCount = mPrefDevListeners.size(); + mPrefDevListeners.add(new PrefDevListenerInfo(listener, executor)); + if (oldCbCount == 0 && mPrefDevListeners.size() > 0) { + // register binder for callbacks + if (mPrefDevDispatcherStub == null) { + mPrefDevDispatcherStub = new StrategyPreferredDeviceDispatcherStub(); + } + try { + getService().registerStrategyPreferredDeviceDispatcher(mPrefDevDispatcherStub); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + } + + /** + * @hide + * Removes a previously added listener of changes to the strategy-preferred audio device. + * @param listener + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public void removeOnPreferredDeviceForStrategyChangedListener( + @NonNull OnPreferredDeviceForStrategyChangedListener listener) { + Objects.requireNonNull(listener); + synchronized (mPrefDevListenerLock) { + if (!removePrefDevListener(listener)) { + throw new IllegalArgumentException( + "attempt to call removeOnPreferredDeviceForStrategyChangedListener() " + + "on an unregistered listener"); + } + if (mPrefDevListeners.size() == 0) { + // unregister binder for callbacks + try { + getService().unregisterStrategyPreferredDeviceDispatcher( + mPrefDevDispatcherStub); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } finally { + mPrefDevDispatcherStub = null; + mPrefDevListeners = null; + } + } + } + } + + + private final Object mPrefDevListenerLock = new Object(); + /** + * List of listeners for preferred device for strategy and their associated Executor. + * List is lazy-initialized on first registration + */ + @GuardedBy("mPrefDevListenerLock") + private @Nullable ArrayList mPrefDevListeners; + + private static class PrefDevListenerInfo { + final @NonNull OnPreferredDeviceForStrategyChangedListener mListener; + final @NonNull Executor mExecutor; + PrefDevListenerInfo(OnPreferredDeviceForStrategyChangedListener listener, Executor exe) { + mListener = listener; + mExecutor = exe; + } + } + + @GuardedBy("mPrefDevListenerLock") + private StrategyPreferredDeviceDispatcherStub mPrefDevDispatcherStub; + + private final class StrategyPreferredDeviceDispatcherStub + extends IStrategyPreferredDeviceDispatcher.Stub { + + @Override + public void dispatchPrefDeviceChanged(int strategyId, @Nullable AudioDevice device) { + // make a shallow copy of listeners so callback is not executed under lock + final ArrayList prefDevListeners; + synchronized (mPrefDevListenerLock) { + if (mPrefDevListeners == null || mPrefDevListeners.size() == 0) { + return; + } + prefDevListeners = (ArrayList) mPrefDevListeners.clone(); + } + final AudioProductStrategy strategy = + AudioProductStrategy.getAudioProductStrategyWithId(strategyId); + final long ident = Binder.clearCallingIdentity(); + try { + for (PrefDevListenerInfo info : prefDevListeners) { + info.mExecutor.execute(() -> + info.mListener.onPreferredDeviceForStrategyChanged(strategy, device)); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + + @GuardedBy("mPrefDevListenerLock") + private @Nullable PrefDevListenerInfo getPrefDevListenerInfo( + OnPreferredDeviceForStrategyChangedListener listener) { + if (mPrefDevListeners == null) { + return null; + } + for (PrefDevListenerInfo info : mPrefDevListeners) { + if (info.mListener == listener) { + return info; + } + } + return null; + } + + @GuardedBy("mPrefDevListenerLock") + private boolean hasPrefDevListener(OnPreferredDeviceForStrategyChangedListener listener) { + return getPrefDevListenerInfo(listener) != null; + } + + @GuardedBy("mPrefDevListenerLock") + /** + * @return true if the listener was removed from the list + */ + private boolean removePrefDevListener(OnPreferredDeviceForStrategyChangedListener listener) { + final PrefDevListenerInfo infoToRemove = getPrefDevListenerInfo(listener); + if (infoToRemove != null) { + mPrefDevListeners.remove(infoToRemove); + return true; + } + return false; + } + //==================================================================== // Offload query /** diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 0fbc0d2180baa..27bf3fe6f0239 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -29,6 +29,7 @@ import android.media.IAudioServerStateDispatcher; import android.media.IPlaybackConfigDispatcher; import android.media.IRecordingConfigDispatcher; import android.media.IRingtonePlayer; +import android.media.IStrategyPreferredDeviceDispatcher; import android.media.IVolumeController; import android.media.IVolumeController; import android.media.PlayerBase; @@ -286,6 +287,11 @@ interface IAudioService { int getAllowedCapturePolicy(); + void registerStrategyPreferredDeviceDispatcher(IStrategyPreferredDeviceDispatcher dispatcher); + + oneway void unregisterStrategyPreferredDeviceDispatcher( + IStrategyPreferredDeviceDispatcher dispatcher); + // WARNING: read warning at top of file, new methods that need to be used by native // code via IAudioManager.h need to be added to the top section. } diff --git a/media/java/android/media/IStrategyPreferredDeviceDispatcher.aidl b/media/java/android/media/IStrategyPreferredDeviceDispatcher.aidl new file mode 100644 index 0000000000000..6db9e52c9d470 --- /dev/null +++ b/media/java/android/media/IStrategyPreferredDeviceDispatcher.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 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 android.media; + +import android.media.AudioDevice; + +/** + * AIDL for AudioService to signal audio strategy-preferred device updates. + * + * {@hide} + */ +oneway interface IStrategyPreferredDeviceDispatcher { + + void dispatchPrefDeviceChanged(int strategyId, in AudioDevice device); + +} diff --git a/media/java/android/media/audiopolicy/AudioProductStrategy.java b/media/java/android/media/audiopolicy/AudioProductStrategy.java index 60b3fc642ee46..f9dbc50e20cff 100644 --- a/media/java/android/media/audiopolicy/AudioProductStrategy.java +++ b/media/java/android/media/audiopolicy/AudioProductStrategy.java @@ -81,6 +81,27 @@ public final class AudioProductStrategy implements Parcelable { return sAudioProductStrategies; } + /** + * @hide + * Return the AudioProductStrategy object for the given strategy ID. + * @param id the ID of the strategy to find + * @return an AudioProductStrategy on which getId() would return id, null if no such strategy + * exists. + */ + public static @Nullable AudioProductStrategy getAudioProductStrategyWithId(int id) { + synchronized (sLock) { + if (sAudioProductStrategies == null) { + sAudioProductStrategies = initializeAudioProductStrategies(); + } + for (AudioProductStrategy strategy : sAudioProductStrategies) { + if (strategy.getId() == id) { + return strategy; + } + } + } + return null; + } + /** * @hide * Create an invalid AudioProductStrategy instance for testing diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index e17c1f8f82761..566b72d728e67 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -29,6 +29,7 @@ import android.media.AudioManager; import android.media.AudioRoutesInfo; import android.media.AudioSystem; import android.media.IAudioRoutesObserver; +import android.media.IStrategyPreferredDeviceDispatcher; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -410,6 +411,16 @@ import java.io.PrintWriter; return mDeviceInventory.removePreferredDeviceForStrategySync(strategy); } + /*package*/ void registerStrategyPreferredDeviceDispatcher( + @NonNull IStrategyPreferredDeviceDispatcher dispatcher) { + mDeviceInventory.registerStrategyPreferredDeviceDispatcher(dispatcher); + } + + /*package*/ void unregisterStrategyPreferredDeviceDispatcher( + @NonNull IStrategyPreferredDeviceDispatcher dispatcher) { + mDeviceInventory.unregisterStrategyPreferredDeviceDispatcher(dispatcher); + } + //--------------------------------------------------------------------- // Communication with (to) AudioService //TODO check whether the AudioService methods are candidates to move here diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 1f998c377c7b4..b0b9572dc84a6 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -16,6 +16,7 @@ package com.android.server.audio; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; @@ -31,6 +32,7 @@ import android.media.AudioPort; import android.media.AudioRoutesInfo; import android.media.AudioSystem; import android.media.IAudioRoutesObserver; +import android.media.IStrategyPreferredDeviceDispatcher; import android.os.Binder; import android.os.RemoteCallbackList; import android.os.RemoteException; @@ -87,6 +89,10 @@ public class AudioDeviceInventory { final RemoteCallbackList mRoutesObservers = new RemoteCallbackList(); + // Monitoring of strategy-preferred device + final RemoteCallbackList mPrefDevDispatchers = + new RemoteCallbackList(); + /*package*/ AudioDeviceInventory(@NonNull AudioDeviceBroker broker) { mDeviceBroker = broker; mAudioSystem = AudioSystemAdapter.getDefaultAdapter(); @@ -470,10 +476,12 @@ public class AudioDeviceInventory { /*package*/ void onSaveSetPreferredDevice(int strategy, @NonNull AudioDevice device) { mPreferredDevices.put(strategy, device); + dispatchPreferredDevice(strategy, device); } /*package*/ void onSaveRemovePreferredDevice(int strategy) { mPreferredDevices.remove(strategy); + dispatchPreferredDevice(strategy, null); } //------------------------------------------------------------ @@ -502,6 +510,16 @@ public class AudioDeviceInventory { return status; } + /*package*/ void registerStrategyPreferredDeviceDispatcher( + @NonNull IStrategyPreferredDeviceDispatcher dispatcher) { + mPrefDevDispatchers.register(dispatcher); + } + + /*package*/ void unregisterStrategyPreferredDeviceDispatcher( + @NonNull IStrategyPreferredDeviceDispatcher dispatcher) { + mPrefDevDispatchers.unregister(dispatcher); + } + /** * Implements the communication with AudioSystem to (dis)connect a device in the native layers * @param connect true if connection @@ -1090,6 +1108,17 @@ public class AudioDeviceInventory { } } + private void dispatchPreferredDevice(int strategy, @Nullable AudioDevice device) { + final int nbDispatchers = mPrefDevDispatchers.beginBroadcast(); + for (int i = 0; i < nbDispatchers; i++) { + try { + mPrefDevDispatchers.getBroadcastItem(i).dispatchPrefDeviceChanged(strategy, device); + } catch (RemoteException e) { + } + } + mPrefDevDispatchers.finishBroadcast(); + } + //---------------------------------------------------------- // For tests only diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 342ce22066b65..fea1c10771607 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -83,6 +83,7 @@ import android.media.IAudioService; import android.media.IPlaybackConfigDispatcher; import android.media.IRecordingConfigDispatcher; import android.media.IRingtonePlayer; +import android.media.IStrategyPreferredDeviceDispatcher; import android.media.IVolumeController; import android.media.MediaExtractor; import android.media.MediaFormat; @@ -1764,6 +1765,26 @@ public class AudioService extends IAudioService.Stub } } + /** @see AudioManager#addOnPreferredDeviceForStrategyChangedListener(Executor, AudioManager.OnPreferredDeviceForStrategyChangedListener) */ + public void registerStrategyPreferredDeviceDispatcher( + @Nullable IStrategyPreferredDeviceDispatcher dispatcher) { + if (dispatcher == null) { + return; + } + enforceModifyAudioRoutingPermission(); + mDeviceBroker.registerStrategyPreferredDeviceDispatcher(dispatcher); + } + + /** @see AudioManager#removeOnPreferredDeviceForStrategyChangedListener(AudioManager.OnPreferredDeviceForStrategyChangedListener) */ + public void unregisterStrategyPreferredDeviceDispatcher( + @Nullable IStrategyPreferredDeviceDispatcher dispatcher) { + if (dispatcher == null) { + return; + } + enforceModifyAudioRoutingPermission(); + mDeviceBroker.unregisterStrategyPreferredDeviceDispatcher(dispatcher); + } + /** @see AudioManager#getDevicesForAttributes(AudioAttributes) */ public @NonNull ArrayList getDevicesForAttributes( @NonNull AudioAttributes attributes) {