Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 91f0baa8 authored by Jean-Michel Trivi's avatar Jean-Michel Trivi
Browse files

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
parent 3ae8743e
Loading
Loading
Loading
Loading
+59 −200
Original line number Diff line number Diff line
@@ -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,77 +2901,31 @@ public class AudioManager {
     * List is lazy-initialized on first registration
     */
    @GuardedBy("mModeListenerLock")
    private @Nullable ArrayList<ModeListenerInfo> mModeListeners;
    private @Nullable ArrayList<ListenerInfo<OnModeChangedListener>> mModeListeners;

    @GuardedBy("mModeListenerLock")
    private ModeDispatcherStub mModeDispatcherStub;

    private final class ModeDispatcherStub
            extends IAudioModeDispatcher.Stub {
    private final class ModeDispatcherStub extends IAudioModeDispatcher.Stub {

        @Override
        public void dispatchAudioModeChanged(int mode) {
            // make a shallow copy of listeners so callback is not executed under lock
            final ArrayList<ModeListenerInfo> modeListeners;
            synchronized (mModeListenerLock) {
                if (mModeListeners == null || mModeListeners.size() == 0) {
                    return;
                }
                modeListeners = (ArrayList<ModeListenerInfo>) mModeListeners.clone();
            }
            final long ident = Binder.clearCallingIdentity();
        public void register(boolean register) {
            try {
                for (ModeListenerInfo info : modeListeners) {
                    info.mExecutor.execute(() ->
                            info.mListener.onModeChanged(mode));
                }
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }
    }

    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;
                if (register) {
                    getService().registerModeDispatcher(this);
                } else {
                    getService().unregisterModeDispatcher(this);
                }
            } catch (RemoteException e) {
                e.rethrowFromSystemServer();
            }
        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;
        @Override
        @SuppressLint("GuardedBy") // lock applied inside callListeners method
        public void dispatchAudioModeChanged(int mode) {
            CallbackUtil.callListeners(mModeListeners, mModeListenerLock,
                    (listener) -> listener.onModeChanged(mode));
        }
        return false;
    }

    /**
@@ -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<ArrayList<ListenerInfo<OnModeChangedListener>>, 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<ArrayList<ListenerInfo<OnModeChangedListener>>, 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<ArrayList<ListenerInfo<OnCommunicationDeviceChangedListener>>,
                    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<ArrayList<ListenerInfo<OnCommunicationDeviceChangedListener>>,
                    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<CommDevListenerInfo> 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<ListenerInfo<OnCommunicationDeviceChangedListener>> 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<CommDevListenerInfo> commDevListeners;
            synchronized (mCommDevListenerLock) {
                if (mCommDevListeners == null || mCommDevListeners.size() == 0) {
                    return;
                }
                commDevListeners = (ArrayList<CommDevListenerInfo>) 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));
                }
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }
    }

    @GuardedBy("mCommDevListenerLock")
    private @Nullable CommDevListenerInfo getCommDevListenerInfo(
            OnCommunicationDeviceChangedListener listener) {
        if (mCommDevListeners == null) {
            return null;
        }
        for (CommDevListenerInfo info : mCommDevListeners) {
            if (info.mListener == listener) {
                return info;
                if (register) {
                    getService().registerCommunicationDeviceDispatcher(this);
                } else {
                    getService().unregisterCommunicationDeviceDispatcher(this);
                }
            } catch (RemoteException e) {
                e.rethrowFromSystemServer();
            }
        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;
        @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));
        }
        return false;
    }

    //---------------------------------------------------------
+224 −0
Original line number Diff line number Diff line
/*
 * 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 <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));
            }
        }

    }
}
+72 −227

File changed.

Preview size limit exceeded, changes collapsed.