From 91f0baa89866da65317883def45f1fa677644e69 Mon Sep 17 00:00:00 2001 From: Jean-Michel Trivi Date: Thu, 11 Nov 2021 16:31:32 -0800 Subject: [PATCH] Utilities for managing event listeners from AudioService Part 1/2: individual static helpers for managing listeners, more to come to encompass all functionality inside one class. Implement utility classes for: - storing information about a listener containing callback and Executor to use (see ListenerInfo class) - managing listeners in a list: remove / retrieve / check presence of a listener in a given list (see removeListener / getListenerInfo / hasListener methods) - managing listeners in a list, while managing the lifecycle of the list, and associated stub for receiving events (for instance for AudioManager to receive events from AudioService), including lazy initialization of the listener list and stub to minimize memory usage The utility class is used to refactor the management of the following listeners: Spatializer.OnSpatializerStateChangedListener Spatializer.OnHeadTrackingModeChangedListener AudioManager.OnModeChangedListener AudioManager.OnCommunicationDeviceChangedListener Bug: 206040617 Test: atest AudioModeListenerTest AudioCommunicationDeviceTest \ SpatializerTest Change-Id: Ia37f1b19603282e0f14e6e5afc1226891e3c1d78 --- media/java/android/media/AudioManager.java | 265 +++++------------- media/java/android/media/CallbackUtil.java | 224 +++++++++++++++ media/java/android/media/Spatializer.java | 301 +++++---------------- 3 files changed, 359 insertions(+), 431 deletions(-) create mode 100644 media/java/android/media/CallbackUtil.java diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index c6622238a7921..e6cba66039073 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -40,6 +40,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.media.AudioAttributes.AttributeSystemUsage; +import android.media.CallbackUtil.ListenerInfo; import android.media.audiopolicy.AudioPolicy; import android.media.audiopolicy.AudioPolicy.AudioPolicyFocusListener; import android.media.audiopolicy.AudioProductStrategy; @@ -2900,79 +2901,33 @@ public class AudioManager { * List is lazy-initialized on first registration */ @GuardedBy("mModeListenerLock") - private @Nullable ArrayList mModeListeners; + private @Nullable ArrayList> mModeListeners; @GuardedBy("mModeListenerLock") private ModeDispatcherStub mModeDispatcherStub; - private final class ModeDispatcherStub - extends IAudioModeDispatcher.Stub { + private final class ModeDispatcherStub extends IAudioModeDispatcher.Stub { + + public void register(boolean register) { + try { + if (register) { + getService().registerModeDispatcher(this); + } else { + getService().unregisterModeDispatcher(this); + } + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } @Override + @SuppressLint("GuardedBy") // lock applied inside callListeners method public void dispatchAudioModeChanged(int mode) { - // make a shallow copy of listeners so callback is not executed under lock - final ArrayList modeListeners; - synchronized (mModeListenerLock) { - if (mModeListeners == null || mModeListeners.size() == 0) { - return; - } - modeListeners = (ArrayList) mModeListeners.clone(); - } - final long ident = Binder.clearCallingIdentity(); - try { - for (ModeListenerInfo info : modeListeners) { - info.mExecutor.execute(() -> - info.mListener.onModeChanged(mode)); - } - } finally { - Binder.restoreCallingIdentity(ident); - } + CallbackUtil.callListeners(mModeListeners, mModeListenerLock, + (listener) -> listener.onModeChanged(mode)); } } - private static class ModeListenerInfo { - final @NonNull OnModeChangedListener mListener; - final @NonNull Executor mExecutor; - - ModeListenerInfo(OnModeChangedListener listener, Executor exe) { - mListener = listener; - mExecutor = exe; - } - } - - @GuardedBy("mModeListenerLock") - private boolean hasModeListener(OnModeChangedListener listener) { - return getModeListenerInfo(listener) != null; - } - - @GuardedBy("mModeListenerLock") - private @Nullable ModeListenerInfo getModeListenerInfo( - OnModeChangedListener listener) { - if (mModeListeners == null) { - return null; - } - for (ModeListenerInfo info : mModeListeners) { - if (info.mListener == listener) { - return info; - } - } - return null; - } - - - @GuardedBy("mModeListenerLock") - /** - * @return true if the listener was removed from the list - */ - private boolean removeModeListener(OnModeChangedListener listener) { - final ModeListenerInfo infoToRemove = getModeListenerInfo(listener); - if (infoToRemove != null) { - mModeListeners.remove(infoToRemove); - return true; - } - return false; - } - /** * Adds a listener to be notified of changes to the audio mode. * See {@link #getMode()} @@ -2982,30 +2937,14 @@ public class AudioManager { public void addOnModeChangedListener( @NonNull @CallbackExecutor Executor executor, @NonNull OnModeChangedListener listener) { - Objects.requireNonNull(executor); - Objects.requireNonNull(listener); synchronized (mModeListenerLock) { - if (hasModeListener(listener)) { - throw new IllegalArgumentException("attempt to call addOnModeChangedListener() " - + "on a previously registered listener"); - } - // lazy initialization of the list of strategy-preferred device listener - if (mModeListeners == null) { - mModeListeners = new ArrayList<>(); - } - final int oldCbCount = mModeListeners.size(); - mModeListeners.add(new ModeListenerInfo(listener, executor)); - if (oldCbCount == 0) { - // register binder for callbacks - if (mModeDispatcherStub == null) { - mModeDispatcherStub = new ModeDispatcherStub(); - } - try { - getService().registerModeDispatcher(mModeDispatcherStub); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } + final Pair>, ModeDispatcherStub> res = + CallbackUtil.addListener("addOnModeChangedListener", + executor, listener, mModeListeners, mModeDispatcherStub, + () -> new ModeDispatcherStub(), + stub -> stub.register(true)); + mModeListeners = res.first; + mModeDispatcherStub = res.second; } } @@ -3015,23 +2954,13 @@ public class AudioManager { * @param listener */ public void removeOnModeChangedListener(@NonNull OnModeChangedListener listener) { - Objects.requireNonNull(listener); synchronized (mModeListenerLock) { - if (!removeModeListener(listener)) { - throw new IllegalArgumentException("attempt to call removeOnModeChangedListener() " - + "on an unregistered listener"); - } - if (mModeListeners.size() == 0) { - // unregister binder for callbacks - try { - getService().unregisterModeDispatcher(mModeDispatcherStub); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } finally { - mModeDispatcherStub = null; - mModeListeners = null; - } - } + final Pair>, ModeDispatcherStub> res = + CallbackUtil.removeListener("removeOnModeChangedListener", + listener, mModeListeners, mModeDispatcherStub, + stub -> stub.register(false)); + mModeListeners = res.first; + mModeDispatcherStub = res.second; } } @@ -7646,31 +7575,15 @@ public class AudioManager { public void addOnCommunicationDeviceChangedListener( @NonNull @CallbackExecutor Executor executor, @NonNull OnCommunicationDeviceChangedListener listener) { - Objects.requireNonNull(executor); - Objects.requireNonNull(listener); synchronized (mCommDevListenerLock) { - if (hasCommDevListener(listener)) { - throw new IllegalArgumentException( - "attempt to call addOnCommunicationDeviceChangedListener() " - + "on a previously registered listener"); - } - // lazy initialization of the list of strategy-preferred device listener - if (mCommDevListeners == null) { - mCommDevListeners = new ArrayList<>(); - } - final int oldCbCount = mCommDevListeners.size(); - mCommDevListeners.add(new CommDevListenerInfo(listener, executor)); - if (oldCbCount == 0 && mCommDevListeners.size() > 0) { - // register binder for callbacks - if (mCommDevDispatcherStub == null) { - mCommDevDispatcherStub = new CommunicationDeviceDispatcherStub(); - } - try { - getService().registerCommunicationDeviceDispatcher(mCommDevDispatcherStub); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } + final Pair>, + CommunicationDeviceDispatcherStub> res = + CallbackUtil.addListener("addOnCommunicationDeviceChangedListener", + executor, listener, mCommDevListeners, mCommDevDispatcherStub, + () -> new CommunicationDeviceDispatcherStub(), + stub -> stub.register(true)); + mCommDevListeners = res.first; + mCommDevDispatcherStub = res.second; } } @@ -7681,25 +7594,14 @@ public class AudioManager { */ public void removeOnCommunicationDeviceChangedListener( @NonNull OnCommunicationDeviceChangedListener listener) { - Objects.requireNonNull(listener); synchronized (mCommDevListenerLock) { - if (!removeCommDevListener(listener)) { - throw new IllegalArgumentException( - "attempt to call removeOnCommunicationDeviceChangedListener() " - + "on an unregistered listener"); - } - if (mCommDevListeners.size() == 0) { - // unregister binder for callbacks - try { - getService().unregisterCommunicationDeviceDispatcher( - mCommDevDispatcherStub); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } finally { - mCommDevDispatcherStub = null; - mCommDevListeners = null; - } - } + final Pair>, + CommunicationDeviceDispatcherStub> res = + CallbackUtil.removeListener("removeOnCommunicationDeviceChangedListener", + listener, mCommDevListeners, mCommDevDispatcherStub, + stub -> stub.register(false)); + mCommDevListeners = res.first; + mCommDevDispatcherStub = res.second; } } @@ -7709,17 +7611,8 @@ public class AudioManager { * List is lazy-initialized on first registration */ @GuardedBy("mCommDevListenerLock") - private @Nullable ArrayList mCommDevListeners; - - private static class CommDevListenerInfo { - final @NonNull OnCommunicationDeviceChangedListener mListener; - final @NonNull Executor mExecutor; - - CommDevListenerInfo(OnCommunicationDeviceChangedListener listener, Executor exe) { - mListener = listener; - mExecutor = exe; - } - } + private @Nullable + ArrayList> mCommDevListeners; @GuardedBy("mCommDevListenerLock") private CommunicationDeviceDispatcherStub mCommDevDispatcherStub; @@ -7727,59 +7620,25 @@ public class AudioManager { private final class CommunicationDeviceDispatcherStub extends ICommunicationDeviceDispatcher.Stub { - @Override - public void dispatchCommunicationDeviceChanged(int portId) { - // make a shallow copy of listeners so callback is not executed under lock - final ArrayList commDevListeners; - synchronized (mCommDevListenerLock) { - if (mCommDevListeners == null || mCommDevListeners.size() == 0) { - return; - } - commDevListeners = (ArrayList) mCommDevListeners.clone(); - } - AudioDeviceInfo device = getDeviceForPortId(portId, GET_DEVICES_OUTPUTS); - final long ident = Binder.clearCallingIdentity(); + public void register(boolean register) { try { - for (CommDevListenerInfo info : commDevListeners) { - info.mExecutor.execute(() -> - info.mListener.onCommunicationDeviceChanged(device)); + if (register) { + getService().registerCommunicationDeviceDispatcher(this); + } else { + getService().unregisterCommunicationDeviceDispatcher(this); } - } finally { - Binder.restoreCallingIdentity(ident); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); } } - } - @GuardedBy("mCommDevListenerLock") - private @Nullable CommDevListenerInfo getCommDevListenerInfo( - OnCommunicationDeviceChangedListener listener) { - if (mCommDevListeners == null) { - return null; + @Override + @SuppressLint("GuardedBy") // lock applied inside callListeners method + public void dispatchCommunicationDeviceChanged(int portId) { + AudioDeviceInfo device = getDeviceForPortId(portId, GET_DEVICES_OUTPUTS); + CallbackUtil.callListeners(mCommDevListeners, mCommDevListenerLock, + (listener) -> listener.onCommunicationDeviceChanged(device)); } - for (CommDevListenerInfo info : mCommDevListeners) { - if (info.mListener == listener) { - return info; - } - } - return null; - } - - @GuardedBy("mCommDevListenerLock") - private boolean hasCommDevListener(OnCommunicationDeviceChangedListener listener) { - return getCommDevListenerInfo(listener) != null; - } - - @GuardedBy("mCommDevListenerLock") - /** - * @return true if the listener was removed from the list - */ - private boolean removeCommDevListener(OnCommunicationDeviceChangedListener listener) { - final CommDevListenerInfo infoToRemove = getCommDevListenerInfo(listener); - if (infoToRemove != null) { - mCommDevListeners.remove(infoToRemove); - return true; - } - return false; } //--------------------------------------------------------- @@ -7838,4 +7697,4 @@ public class AudioManager { return mHandler; } } -} +} \ No newline at end of file diff --git a/media/java/android/media/CallbackUtil.java b/media/java/android/media/CallbackUtil.java new file mode 100644 index 0000000000000..ac39317ed1d5f --- /dev/null +++ b/media/java/android/media/CallbackUtil.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2021 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.annotation.NonNull; +import android.annotation.Nullable; +import android.media.permission.ClearCallingIdentityContext; +import android.media.permission.SafeCloseable; +import android.util.Log; +import android.util.Pair; + +import java.util.ArrayList; +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * @hide + * A utility class to implement callback listeners and their management. + * This is meant to be used for lazily-initialized listener lists and stubs for event reception, + * typically received from server (e.g. AudioService). + */ + +/*package*/ class CallbackUtil { + + private static final String TAG = "CallbackUtil"; + + /** + * Container class to store a listener and associated Executor + * @param the type of the listener + */ + static class ListenerInfo { + final @NonNull T mListener; + final @NonNull Executor mExecutor; + + ListenerInfo(@NonNull T listener, @NonNull Executor exe) { + mListener = listener; + mExecutor = exe; + } + } + + /** + * Finds the listener information (listener + Executor) in a given list of listeners + * @param listener the listener to find + * @param listeners the list of listener informations, can be null if not instantiated yet + * @param the type of the listeners + * @return null if the listener is not in the given list of listener informations + */ + static @Nullable ListenerInfo getListenerInfo( + @NonNull T listener, @Nullable ArrayList> listeners) { + if (listeners == null) { + return null; + } + for (ListenerInfo info : listeners) { + if (info.mListener == listener) { + return info; + } + } + return null; + } + + /** + * Returns true if the given listener is present in the list of listener informations + * @param listener the listener to find + * @param listeners the list of listener informations, can be null if not instantiated yet + * @param the type of the listeners + * @return true if the listener is in the list + */ + static boolean hasListener(@NonNull T listener, + @Nullable ArrayList> listeners) { + return getListenerInfo(listener, listeners) != null; + } + + /** + * Removes the given listener from the list of listener informations + * @param listener the listener to remove + * @param listeners the list of listener informations, can be null if not instantiated yet + * @param the type of the listeners + * @return true if the listener was found and removed from the list, false otherwise + */ + static boolean removeListener(@NonNull T listener, + @Nullable ArrayList> listeners) { + final ListenerInfo infoToRemove = getListenerInfo(listener, listeners); + if (infoToRemove != null) { + listeners.remove(infoToRemove); + return true; + } + return false; + } + + /** + * Adds a listener and associated Executor in the list of listeners. + * This method handles the lazy initialization of both the list of listeners and the stub + * used to receive the events that will be forwarded to the listener, see the returned pair + * for the updated references. + * @param methodName the name of the method calling this, for inclusion in the + * string in case of IllegalArgumentException + * @param executor the Executor for the listener + * @param listener the listener to add + * @param listeners the list of listener informations, can be null if not instantiated yet + * @param dispatchStub the stub that receives the events to be forwarded to the listeners, + * can be null if not instantiated yet + * @param newStub the function to create a new stub if needed + * @param registerStub the function for the stub registration if needed + * @param the type of the listener interface + * @param the type of the event receiver stub + * @return a pair of the listener list and the event receiver stub which may have been + * initialized if needed (e.g. on the first ever addition of a listener) + */ + static Pair>, S> addListener(String methodName, + @NonNull Executor executor, + @NonNull T listener, + @Nullable ArrayList> listeners, + @Nullable S dispatchStub, + @NonNull java.util.function.Supplier newStub, + @NonNull java.util.function.Consumer registerStub) { + Objects.requireNonNull(executor); + Objects.requireNonNull(listener); + + if (hasListener(listener, listeners)) { + throw new IllegalArgumentException("attempt to call " + methodName + + "on a previously registered listener"); + } + // lazy initialization of the list of strategy-preferred device listener + if (listeners == null) { + listeners = new ArrayList<>(); + } + if (listeners.size() == 0) { + // register binder for callbacks + if (dispatchStub == null) { + try { + dispatchStub = newStub.get(); + } catch (Exception e) { + Log.e(TAG, "Exception while creating stub in " + methodName, e); + return new Pair<>(null, null); + } + } + registerStub.accept(dispatchStub); + } + listeners.add(new ListenerInfo(listener, executor)); + return new Pair(listeners, dispatchStub); + } + + /** + * Removes a listener from the list of listeners. + * This method handles the freeing of both the list of listeners and the stub + * used to receive the events that will be forwarded to the listener,see the returned pair + * for the updated references. + * @param methodName the name of the method calling this, for inclusion in the + * string in case of IllegalArgumentException + * @param listener the listener to remove + * @param listeners the list of listener informations, can be null if not instantiated yet + * @param dispatchStub the stub that receives the events to be forwarded to the listeners, + * can be null if not instantiated yet + * @param unregisterStub the function to unregister the stub if needed + * @param the type of the listener interface + * @param the type of the event receiver stub + * @return a pair of the listener list and the event receiver stub which may have been + * changed if needed (e.g. on the removal of the last listener) + */ + static Pair>, S> removeListener(String methodName, + @NonNull T listener, + @Nullable ArrayList> listeners, + @Nullable S dispatchStub, + @NonNull java.util.function.Consumer unregisterStub) { + Objects.requireNonNull(listener); + + if (!removeListener(listener, listeners)) { + throw new IllegalArgumentException("attempt to call " + methodName + + "on an unregistered listener"); + } + if (listeners.size() == 0) { + unregisterStub.accept(dispatchStub); + return new Pair<>(null, null); + } else { + return new Pair<>(listeners, dispatchStub); + } + } + + interface CallbackMethod { + void callbackMethod(T listener); + } + + /** + * Exercise the callback of the listeners + * @param listeners the list of listeners + * @param listenerLock the lock guarding the list of listeners + * @param callback the function to call for each listener + * @param the type of the listener interface + */ + static void callListeners( + @Nullable ArrayList> listeners, + @NonNull Object listenerLock, + @NonNull CallbackMethod callback) { + Objects.requireNonNull(listenerLock); + // make a shallow copy of listeners so callback is not executed under lock + final ArrayList> listenersShallowCopy; + synchronized (listenerLock) { + if (listeners == null || listeners.size() == 0) { + return; + } + listenersShallowCopy = (ArrayList>) listeners.clone(); + } + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + for (ListenerInfo info : listenersShallowCopy) { + info.mExecutor.execute(() -> callback.callbackMethod(info.mListener)); + } + } + + } +} diff --git a/media/java/android/media/Spatializer.java b/media/java/android/media/Spatializer.java index e6fff392c2643..c0793ec58e537 100644 --- a/media/java/android/media/Spatializer.java +++ b/media/java/android/media/Spatializer.java @@ -22,11 +22,14 @@ import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.SuppressLint; import android.annotation.SystemApi; +import android.media.CallbackUtil.ListenerInfo; import android.media.permission.ClearCallingIdentityContext; import android.media.permission.SafeCloseable; import android.os.RemoteException; import android.util.Log; +import android.util.Pair; import com.android.internal.annotations.GuardedBy; @@ -373,32 +376,15 @@ public class Spatializer { public void addOnSpatializerStateChangedListener( @NonNull @CallbackExecutor Executor executor, @NonNull OnSpatializerStateChangedListener listener) { - Objects.requireNonNull(executor); - Objects.requireNonNull(listener); synchronized (mStateListenerLock) { - if (hasSpatializerStateListener(listener)) { - throw new IllegalArgumentException( - "Called addOnSpatializerStateChangedListener() " - + "on a previously registered listener"); - } - // lazy initialization of the list of strategy-preferred device listener - if (mStateListeners == null) { - mStateListeners = new ArrayList<>(); - } - mStateListeners.add(new StateListenerInfo(listener, executor)); - if (mStateListeners.size() == 1) { - // register binder for callbacks - if (mInfoDispatcherStub == null) { - mInfoDispatcherStub = - new SpatializerInfoDispatcherStub(); - } - try { - mAm.getService().registerSpatializerCallback( - mInfoDispatcherStub); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } + final Pair>, + SpatializerInfoDispatcherStub> res = + CallbackUtil.addListener("addOnSpatializerStateChangedListener", + executor, listener, mStateListeners, mInfoDispatcherStub, + () -> new SpatializerInfoDispatcherStub(), + stub -> stub.register(true)); + mStateListeners = res.first; + mInfoDispatcherStub = res.second; } } @@ -410,24 +396,14 @@ public class Spatializer { */ public void removeOnSpatializerStateChangedListener( @NonNull OnSpatializerStateChangedListener listener) { - Objects.requireNonNull(listener); synchronized (mStateListenerLock) { - if (!removeStateListener(listener)) { - throw new IllegalArgumentException( - "Called removeOnSpatializerStateChangedListener() " - + "on an unregistered listener"); - } - if (mStateListeners.size() == 0) { - // unregister binder for callbacks - try { - mAm.getService().unregisterSpatializerCallback(mInfoDispatcherStub); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } finally { - mInfoDispatcherStub = null; - mStateListeners = null; - } - } + final Pair>, + SpatializerInfoDispatcherStub> res = + CallbackUtil.removeListener("removeOnSpatializerStateChangedListener", + listener, mStateListeners, mInfoDispatcherStub, + stub -> stub.register(false)); + mStateListeners = res.first; + mInfoDispatcherStub = res.second; } } @@ -489,93 +465,41 @@ public class Spatializer { * List is lazy-initialized on first registration */ @GuardedBy("mStateListenerLock") - private @Nullable ArrayList mStateListeners; + private @Nullable ArrayList> mStateListeners; @GuardedBy("mStateListenerLock") private @Nullable SpatializerInfoDispatcherStub mInfoDispatcherStub; private final class SpatializerInfoDispatcherStub extends ISpatializerCallback.Stub { + public void register(boolean register) { + try { + if (register) { + mAm.getService().registerSpatializerCallback(this); + } else { + mAm.getService().unregisterSpatializerCallback(this); + } + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + @Override + @SuppressLint("GuardedBy") // lock applied inside callListeners method public void dispatchSpatializerEnabledChanged(boolean enabled) { - // make a shallow copy of listeners so callback is not executed under lock - final ArrayList stateListeners; - synchronized (mStateListenerLock) { - if (mStateListeners == null || mStateListeners.size() == 0) { - return; - } - stateListeners = (ArrayList) mStateListeners.clone(); - } - try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { - for (StateListenerInfo info : stateListeners) { - info.mExecutor.execute(() -> - info.mListener.onSpatializerEnabledChanged(Spatializer.this, enabled)); - } - } + CallbackUtil.callListeners(mStateListeners, mStateListenerLock, + (listener) -> listener.onSpatializerEnabledChanged( + Spatializer.this, enabled)); } @Override + @SuppressLint("GuardedBy") // lock applied inside callListeners method public void dispatchSpatializerAvailableChanged(boolean available) { - // make a shallow copy of listeners so callback is not executed under lock - final ArrayList stateListeners; - synchronized (mStateListenerLock) { - if (mStateListeners == null || mStateListeners.size() == 0) { - return; - } - stateListeners = (ArrayList) mStateListeners.clone(); - } - try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { - for (StateListenerInfo info : stateListeners) { - info.mExecutor.execute(() -> - info.mListener.onSpatializerAvailableChanged( - Spatializer.this, available)); - } - } + CallbackUtil.callListeners(mStateListeners, mStateListenerLock, + (listener) -> listener.onSpatializerAvailableChanged( + Spatializer.this, available)); } } - private static class StateListenerInfo { - final @NonNull OnSpatializerStateChangedListener mListener; - final @NonNull Executor mExecutor; - - StateListenerInfo(@NonNull OnSpatializerStateChangedListener listener, - @NonNull Executor exe) { - mListener = listener; - mExecutor = exe; - } - } - - @GuardedBy("mStateListenerLock") - private boolean hasSpatializerStateListener(OnSpatializerStateChangedListener listener) { - return getStateListenerInfo(listener) != null; - } - - @GuardedBy("mStateListenerLock") - private @Nullable StateListenerInfo getStateListenerInfo( - OnSpatializerStateChangedListener listener) { - if (mStateListeners == null) { - return null; - } - for (StateListenerInfo info : mStateListeners) { - if (info.mListener == listener) { - return info; - } - } - return null; - } - - @GuardedBy("mStateListenerLock") - /** - * @return true if the listener was removed from the list - */ - private boolean removeStateListener(OnSpatializerStateChangedListener listener) { - final StateListenerInfo infoToRemove = getStateListenerInfo(listener); - if (infoToRemove != null) { - mStateListeners.remove(infoToRemove); - return true; - } - return false; - } - /** * @hide @@ -688,33 +612,15 @@ public class Spatializer { public void addOnHeadTrackingModeChangedListener( @NonNull @CallbackExecutor Executor executor, @NonNull OnHeadTrackingModeChangedListener listener) { - Objects.requireNonNull(executor); - Objects.requireNonNull(listener); synchronized (mHeadTrackingListenerLock) { - if (hasListener(listener, mHeadTrackingListeners)) { - throw new IllegalArgumentException( - "Called addOnHeadTrackingModeChangedListener() " - + "on a previously registered listener"); - } - // lazy initialization of the list of strategy-preferred device listener - if (mHeadTrackingListeners == null) { - mHeadTrackingListeners = new ArrayList<>(); - } - mHeadTrackingListeners.add( - new ListenerInfo(listener, executor)); - if (mHeadTrackingListeners.size() == 1) { - // register binder for callbacks - if (mHeadTrackingDispatcherStub == null) { - mHeadTrackingDispatcherStub = - new SpatializerHeadTrackingDispatcherStub(); - } - try { - mAm.getService().registerSpatializerHeadTrackingCallback( - mHeadTrackingDispatcherStub); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } + final Pair>, + SpatializerHeadTrackingDispatcherStub> res = CallbackUtil.addListener( + "addOnHeadTrackingModeChangedListener", executor, listener, + mHeadTrackingListeners, mHeadTrackingDispatcherStub, + () -> new SpatializerHeadTrackingDispatcherStub(), + stub -> stub.register(true)); + mHeadTrackingListeners = res.first; + mHeadTrackingDispatcherStub = res.second; } } @@ -728,25 +634,14 @@ public class Spatializer { @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public void removeOnHeadTrackingModeChangedListener( @NonNull OnHeadTrackingModeChangedListener listener) { - Objects.requireNonNull(listener); synchronized (mHeadTrackingListenerLock) { - if (!removeListener(listener, mHeadTrackingListeners)) { - throw new IllegalArgumentException( - "Called removeOnHeadTrackingModeChangedListener() " - + "on an unregistered listener"); - } - if (mHeadTrackingListeners.size() == 0) { - // unregister binder for callbacks - try { - mAm.getService().unregisterSpatializerHeadTrackingCallback( - mHeadTrackingDispatcherStub); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } finally { - mHeadTrackingDispatcherStub = null; - mHeadTrackingListeners = null; - } - } + final Pair>, + SpatializerHeadTrackingDispatcherStub> res = CallbackUtil.removeListener( + "removeOnHeadTrackingModeChangedListener", listener, + mHeadTrackingListeners, mHeadTrackingDispatcherStub, + stub -> stub.register(false)); + mHeadTrackingListeners = res.first; + mHeadTrackingDispatcherStub = res.second; } } @@ -930,45 +825,6 @@ public class Spatializer { } } - //----------------------------------------------------------------------------- - // callback helper definitions - - private static class ListenerInfo { - final @NonNull T mListener; - final @NonNull Executor mExecutor; - - ListenerInfo(T listener, Executor exe) { - mListener = listener; - mExecutor = exe; - } - } - - private static ListenerInfo getListenerInfo( - T listener, ArrayList> listeners) { - if (listeners == null) { - return null; - } - for (ListenerInfo info : listeners) { - if (info.mListener == listener) { - return info; - } - } - return null; - } - - private static boolean hasListener(T listener, ArrayList> listeners) { - return getListenerInfo(listener, listeners) != null; - } - - private static boolean removeListener(T listener, ArrayList> listeners) { - final ListenerInfo infoToRemove = getListenerInfo(listener, listeners); - if (infoToRemove != null) { - listeners.remove(infoToRemove); - return true; - } - return false; - } - //----------------------------------------------------------------------------- // head tracking callback management and stub @@ -986,42 +842,31 @@ public class Spatializer { private final class SpatializerHeadTrackingDispatcherStub extends ISpatializerHeadTrackingModeCallback.Stub { - @Override - public void dispatchSpatializerActualHeadTrackingModeChanged(int mode) { - // make a shallow copy of listeners so callback is not executed under lock - final ArrayList> headTrackingListeners; - synchronized (mHeadTrackingListenerLock) { - if (mHeadTrackingListeners == null || mHeadTrackingListeners.size() == 0) { - return; - } - headTrackingListeners = (ArrayList>) - mHeadTrackingListeners.clone(); - } - try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { - for (ListenerInfo info : headTrackingListeners) { - info.mExecutor.execute(() -> info.mListener - .onHeadTrackingModeChanged(Spatializer.this, mode)); + public void register(boolean register) { + try { + if (register) { + mAm.getService().registerSpatializerHeadTrackingCallback(this); + } else { + mAm.getService().unregisterSpatializerHeadTrackingCallback(this); } + } catch (RemoteException e) { + e.rethrowFromSystemServer(); } } @Override + @SuppressLint("GuardedBy") // lock applied inside callListeners method + public void dispatchSpatializerActualHeadTrackingModeChanged(int mode) { + CallbackUtil.callListeners(mHeadTrackingListeners, mHeadTrackingListenerLock, + (listener) -> listener.onHeadTrackingModeChanged(Spatializer.this, mode)); + } + + @Override + @SuppressLint("GuardedBy") // lock applied inside callListeners method public void dispatchSpatializerDesiredHeadTrackingModeChanged(int mode) { - // make a shallow copy of listeners so callback is not executed under lock - final ArrayList> headTrackingListeners; - synchronized (mHeadTrackingListenerLock) { - if (mHeadTrackingListeners == null || mHeadTrackingListeners.size() == 0) { - return; - } - headTrackingListeners = (ArrayList>) - mHeadTrackingListeners.clone(); - } - try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { - for (ListenerInfo info : headTrackingListeners) { - info.mExecutor.execute(() -> info.mListener - .onDesiredHeadTrackingModeChanged(Spatializer.this, mode)); - } - } + CallbackUtil.callListeners(mHeadTrackingListeners, mHeadTrackingListenerLock, + (listener) -> listener.onDesiredHeadTrackingModeChanged( + Spatializer.this, mode)); } }