| /* |
| * 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 <T> the type of the listener |
| */ |
| static class ListenerInfo<T> { |
| 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 <T> the type of the listeners |
| * @return null if the listener is not in the given list of listener informations |
| */ |
| static <T> @Nullable ListenerInfo<T> getListenerInfo( |
| @NonNull T listener, @Nullable ArrayList<ListenerInfo<T>> listeners) { |
| if (listeners == null) { |
| return null; |
| } |
| for (ListenerInfo<T> 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 <T> the type of the listeners |
| * @return true if the listener is in the list |
| */ |
| static <T> boolean hasListener(@NonNull T listener, |
| @Nullable ArrayList<ListenerInfo<T>> 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 <T> the type of the listeners |
| * @return true if the listener was found and removed from the list, false otherwise |
| */ |
| static <T> boolean removeListener(@NonNull T listener, |
| @Nullable ArrayList<ListenerInfo<T>> listeners) { |
| final ListenerInfo<T> 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 <T> the type of the listener interface |
| * @param <S> 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 <T, S> Pair<ArrayList<ListenerInfo<T>>, S> addListener(String methodName, |
| @NonNull Executor executor, |
| @NonNull T listener, |
| @Nullable ArrayList<ListenerInfo<T>> listeners, |
| @Nullable S dispatchStub, |
| @NonNull java.util.function.Supplier<S> newStub, |
| @NonNull java.util.function.Consumer<S> 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<T>(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 <T> the type of the listener interface |
| * @param <S> 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 <T, S> Pair<ArrayList<ListenerInfo<T>>, S> removeListener(String methodName, |
| @NonNull T listener, |
| @Nullable ArrayList<ListenerInfo<T>> listeners, |
| @Nullable S dispatchStub, |
| @NonNull java.util.function.Consumer<S> 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<T> { |
| 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 <T> the type of the listener interface |
| */ |
| static <T> void callListeners( |
| @Nullable ArrayList<ListenerInfo<T>> listeners, |
| @NonNull Object listenerLock, |
| @NonNull CallbackMethod<T> callback) { |
| Objects.requireNonNull(listenerLock); |
| // make a shallow copy of listeners so callback is not executed under lock |
| final ArrayList<ListenerInfo<T>> listenersShallowCopy; |
| synchronized (listenerLock) { |
| if (listeners == null || listeners.size() == 0) { |
| return; |
| } |
| listenersShallowCopy = (ArrayList<ListenerInfo<T>>) listeners.clone(); |
| } |
| try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { |
| for (ListenerInfo<T> 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 <T> the listener class |
| */ |
| static class LazyListenerManager<T> { |
| private final Object mListenerLock = new Object(); |
| |
| @GuardedBy("mListenerLock") |
| private @Nullable ArrayList<ListenerInfo<T>> 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<DispatcherStub> newStub) { |
| synchronized (mListenerLock) { |
| final Pair<ArrayList<ListenerInfo<T>>, 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<ArrayList<ListenerInfo<T>>, 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<T> callback) { |
| CallbackUtil.callListeners(mListeners, mListenerLock, callback); |
| } |
| } |
| } |