/* * 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.annotation.SuppressLint; import android.media.permission.ClearCallingIdentityContext; import android.media.permission.SafeCloseable; import android.util.Log; import android.util.Pair; import com.android.internal.annotations.GuardedBy; 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)); } } } /** * Interface to be implemented by stub implementation for the events received from a server * to the class managing the listener API. * For an example see {@link AudioManager#ModeDispatcherStub} which registers with AudioService. */ interface DispatcherStub { /** * Register/unregister the stub as a listener of the events to be forwarded to the listeners * managed by LazyListenerManager. * @param register true for registering, false to unregister */ void register(boolean register); } /** * Class to manage a list of listeners and their callback, and the associated stub which * receives the events to be forwarded to the listeners. * The list of listeners and the stub and its registration are lazily initialized and registered * @param the listener class */ static class LazyListenerManager { private final Object mListenerLock = new Object(); @GuardedBy("mListenerLock") private @Nullable ArrayList> mListeners; @GuardedBy("mListenerLock") private @Nullable DispatcherStub mDispatcherStub; LazyListenerManager() { // nothing to initialize as instances of dispatcher and list of listeners // are lazily initialized } /** * Add a new listener / executor pair for the configured listener * @param executor Executor for the callback * @param listener the listener to register * @param methodName the name of the method calling this utility method for easier to read * exception messages * @param newStub how to build a new instance of the stub receiving the events when the * number of listeners goes from 0 to 1, not called until then. */ void addListener(@NonNull Executor executor, @NonNull T listener, String methodName, @NonNull java.util.function.Supplier newStub) { synchronized (mListenerLock) { final Pair>, DispatcherStub> res = CallbackUtil.addListener(methodName, executor, listener, mListeners, mDispatcherStub, newStub, stub -> stub.register(true)); mListeners = res.first; mDispatcherStub = res.second; } } /** * Remove a previously registered listener * @param listener the listener to unregister * @param methodName the name of the method calling this utility method for easier to read * exception messages */ void removeListener(@NonNull T listener, String methodName) { synchronized (mListenerLock) { final Pair>, DispatcherStub> res = CallbackUtil.removeListener(methodName, listener, mListeners, mDispatcherStub, stub -> stub.register(false)); mListeners = res.first; mDispatcherStub = res.second; } } /** * Call the registered listeners with the given callback method * @param callback the listener method to invoke */ @SuppressLint("GuardedBy") // lock applied inside callListeners method void callListeners(CallbackMethod callback) { CallbackUtil.callListeners(mListeners, mListenerLock, callback); } } }