summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Andy Hung <hunga@google.com> 2020-01-22 06:24:31 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2020-01-22 06:24:31 +0000
commit6f0d16df8c75c89fdfa7f9cbac6d7b74fe040295 (patch)
tree6d85885fa426a5f72874c11c52528021aecfb84e
parente68e6deb3e7e59994b0306064dd49c93af81d542 (diff)
parentb7149a1d19ccb5aa933d0709eb2ec46e1d7ee10a (diff)
Merge "AudioTrack: Add Codec format change listener"
-rw-r--r--api/current.txt37
-rw-r--r--media/java/android/media/AudioMetadata.java406
-rw-r--r--media/java/android/media/AudioTrack.java79
-rw-r--r--media/java/android/media/Utils.java282
4 files changed, 802 insertions, 2 deletions
diff --git a/api/current.txt b/api/current.txt
index 74242ba42426..54e1bad2a5b3 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -24144,6 +24144,37 @@ package android.media {
method public void onAudioFocusChange(int);
}
+ public final class AudioMetadata {
+ method @NonNull public static android.media.AudioMetadata.Map createMap();
+ }
+
+ public static class AudioMetadata.Format {
+ field @NonNull public static final android.media.AudioMetadata.Key<java.lang.Boolean> KEY_ATMOS_PRESENT;
+ field @NonNull public static final android.media.AudioMetadata.Key<java.lang.Integer> KEY_AUDIO_ENCODING;
+ field @NonNull public static final android.media.AudioMetadata.Key<java.lang.Integer> KEY_BIT_RATE;
+ field @NonNull public static final android.media.AudioMetadata.Key<java.lang.Integer> KEY_BIT_WIDTH;
+ field @NonNull public static final android.media.AudioMetadata.Key<java.lang.Integer> KEY_CHANNEL_MASK;
+ field @NonNull public static final android.media.AudioMetadata.Key<java.lang.String> KEY_MIME;
+ field @NonNull public static final android.media.AudioMetadata.Key<java.lang.Integer> KEY_SAMPLE_RATE;
+ }
+
+ public static interface AudioMetadata.Key<T> {
+ method @NonNull public String getName();
+ method @NonNull public Class<T> getValueClass();
+ }
+
+ public static interface AudioMetadata.Map extends android.media.AudioMetadata.ReadMap {
+ method @Nullable public <T> T remove(@NonNull android.media.AudioMetadata.Key<T>);
+ method @Nullable public <T> T set(@NonNull android.media.AudioMetadata.Key<T>, @NonNull T);
+ }
+
+ public static interface AudioMetadata.ReadMap {
+ method public <T> boolean containsKey(@NonNull android.media.AudioMetadata.Key<T>);
+ method @NonNull public android.media.AudioMetadata.Map dup();
+ method @Nullable public <T> T get(@NonNull android.media.AudioMetadata.Key<T>);
+ method public int size();
+ }
+
public final class AudioPlaybackCaptureConfiguration {
method @NonNull public int[] getExcludeUids();
method @NonNull public int[] getExcludeUsages();
@@ -24329,6 +24360,7 @@ package android.media {
ctor @Deprecated public AudioTrack(int, int, int, int, int, int) throws java.lang.IllegalArgumentException;
ctor @Deprecated public AudioTrack(int, int, int, int, int, int, int) throws java.lang.IllegalArgumentException;
ctor public AudioTrack(android.media.AudioAttributes, android.media.AudioFormat, int, int, int) throws java.lang.IllegalArgumentException;
+ method public void addOnCodecFormatChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioTrack.OnCodecFormatChangedListener);
method public void addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener, android.os.Handler);
method @Deprecated public void addOnRoutingChangedListener(android.media.AudioTrack.OnRoutingChangedListener, android.os.Handler);
method public int attachAuxEffect(int);
@@ -24372,6 +24404,7 @@ package android.media {
method public void registerStreamEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioTrack.StreamEventCallback);
method public void release();
method public int reloadStaticData();
+ method public void removeOnCodecFormatChangedListener(@NonNull android.media.AudioTrack.OnCodecFormatChangedListener);
method public void removeOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener);
method @Deprecated public void removeOnRoutingChangedListener(android.media.AudioTrack.OnRoutingChangedListener);
method public int setAuxEffectSendLevel(@FloatRange(from=0.0) float);
@@ -24445,6 +24478,10 @@ package android.media {
field public static final String USAGE = "android.media.audiotrack.usage";
}
+ public static interface AudioTrack.OnCodecFormatChangedListener {
+ method public void onCodecFormatChanged(@NonNull android.media.AudioTrack, @Nullable android.media.AudioMetadata.ReadMap);
+ }
+
public static interface AudioTrack.OnPlaybackPositionUpdateListener {
method public void onMarkerReached(android.media.AudioTrack);
method public void onPeriodicNotification(android.media.AudioTrack);
diff --git a/media/java/android/media/AudioMetadata.java b/media/java/android/media/AudioMetadata.java
new file mode 100644
index 000000000000..7245aab41eec
--- /dev/null
+++ b/media/java/android/media/AudioMetadata.java
@@ -0,0 +1,406 @@
+/*
+ * 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Pair;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * AudioMetadata class is used to manage typed key-value pairs for
+ * configuration and capability requests within the Audio Framework.
+ */
+public final class AudioMetadata {
+ /**
+ * Key interface for the map.
+ *
+ * The presence of this {@code Key} interface on an object allows
+ * it to be used to reference metadata in the Audio Framework.
+ *
+ * @param <T> type of value associated with {@code Key}.
+ */
+ // Conceivably metadata keys exposing multiple interfaces
+ // could be eligible to work in multiple framework domains.
+ public interface Key<T> {
+ /**
+ * Returns the internal name of the key.
+ */
+ @NonNull
+ String getName();
+
+ /**
+ * Returns the class type of the associated value.
+ */
+ @NonNull
+ Class<T> getValueClass();
+
+ // TODO: consider adding bool isValid(@NonNull T value)
+
+ /**
+ * Do not allow non-framework apps to create their own keys
+ * by implementing this interface; keep a method hidden.
+ *
+ * @hide
+ */
+ boolean isFromFramework();
+ }
+
+ /**
+ * A read only {@code Map} interface of {@link Key} value pairs.
+ *
+ * Using a {@link Key} interface, look up the corresponding value.
+ */
+ public interface ReadMap {
+ /**
+ * Returns true if the key exists in the map.
+ *
+ * @param key interface for requesting the value.
+ * @param <T> type of value.
+ * @return true if key exists in the Map.
+ */
+ <T> boolean containsKey(@NonNull Key<T> key);
+
+ /**
+ * Returns a copy of the map.
+ *
+ * This is intended for safe conversion between a {@link ReadMap}
+ * interface and a {@link Map} interface.
+ * Currently only simple objects are used for key values which
+ * means a shallow copy is sufficient.
+ *
+ * @return a Map copied from the existing map.
+ */
+ @NonNull
+ Map dup(); // lint checker doesn't like clone().
+
+ /**
+ * Returns the value associated with the key.
+ *
+ * @param key interface for requesting the value.
+ * @param <T> type of value.
+ * @return returns the value of associated with key or null if it doesn't exist.
+ */
+ @Nullable
+ <T> T get(@NonNull Key<T> key);
+
+ /**
+ * Returns a {@code Set} of keys associated with the map.
+ * @hide
+ */
+ @NonNull
+ Set<Key<?>> keySet();
+
+ /**
+ * Returns the number of elements in the map.
+ */
+ int size();
+ }
+
+ /**
+ * A writeable {@link Map} interface of {@link Key} value pairs.
+ * This interface is not guaranteed to be thread-safe
+ * unless the supplier for the {@code Map} states it as thread safe.
+ */
+ // TODO: Create a wrapper like java.util.Collections.synchronizedMap?
+ public interface Map extends ReadMap {
+ /**
+ * Removes the value associated with the key.
+ * @param key interface for storing the value.
+ * @param <T> type of value.
+ * @return the value of the key, null if it doesn't exist.
+ */
+ @Nullable
+ <T> T remove(@NonNull Key<T> key);
+
+ /**
+ * Sets a value for the key.
+ *
+ * @param key interface for storing the value.
+ * @param <T> type of value.
+ * @param value a non-null value of type T.
+ * @return the previous value associated with key or null if it doesn't exist.
+ */
+ // See automatic Kotlin overloading for Java interoperability.
+ // https://kotlinlang.org/docs/reference/java-interop.html#operators
+ // See also Kotlin set for overloaded operator indexing.
+ // https://kotlinlang.org/docs/reference/operator-overloading.html#indexed
+ // Also the Kotlin mutable-list set.
+ // https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-mutable-list/set.html
+ @Nullable
+ <T> T set(@NonNull Key<T> key, @NonNull T value);
+ }
+
+ /**
+ * Creates a {@link Map} suitable for adding keys.
+ * @return an empty {@link Map} instance.
+ */
+ @NonNull
+ public static Map createMap() {
+ return new BaseMap();
+ }
+
+ /**
+ * A container class for AudioMetadata Format keys.
+ *
+ * @see AudioTrack.OnCodecFormatChangedListener
+ */
+ public static class Format {
+ // The key name strings used here must match that of the native framework, but are
+ // allowed to change between API releases. This due to the Java specification
+ // on what is a compile time constant.
+ //
+ // Key<?> are final variables but not constant variables (per Java spec 4.12.4) because
+ // the keys are not a primitive type nor a String initialized by a constant expression.
+ // Hence (per Java spec 13.1.3), they are not resolved at compile time,
+ // rather are picked up by applications at run time.
+ //
+ // So the contractual API behavior of AudioMetadata.Key<> are different than Strings
+ // initialized by a constant expression (for example MediaFormat.KEY_*).
+
+ // See MediaFormat
+ /**
+ * A key representing the bitrate of the encoded stream used in
+ *
+ * If the stream is variable bitrate, this is the average bitrate of the stream.
+ * The unit is bits per second.
+ *
+ * An Integer value.
+ *
+ * @see MediaFormat#KEY_BIT_RATE
+ */
+ @NonNull public static final Key<Integer> KEY_BIT_RATE =
+ createKey("bitrate", Integer.class);
+
+ /**
+ * A key representing the audio channel mask of the stream.
+ *
+ * An Integer value.
+ *
+ * @see AudioTrack#getChannelConfiguration()
+ * @see MediaFormat#KEY_CHANNEL_MASK
+ */
+ @NonNull public static final Key<Integer> KEY_CHANNEL_MASK =
+ createKey("channel-mask", Integer.class);
+
+
+ /**
+ * A key representing the codec mime string.
+ *
+ * A String value.
+ *
+ * @see MediaFormat#KEY_MIME
+ */
+ @NonNull public static final Key<String> KEY_MIME = createKey("mime", String.class);
+
+ /**
+ * A key representing the audio sample rate in Hz of the stream.
+ *
+ * An Integer value.
+ *
+ * @see AudioFormat#getSampleRate()
+ * @see MediaFormat#KEY_SAMPLE_RATE
+ */
+ @NonNull public static final Key<Integer> KEY_SAMPLE_RATE =
+ createKey("sample-rate", Integer.class);
+
+ // Unique to Audio
+
+ /**
+ * A key representing the bit width of an element of decoded data.
+ *
+ * An Integer value.
+ */
+ @NonNull public static final Key<Integer> KEY_BIT_WIDTH =
+ createKey("bit-width", Integer.class);
+
+ /**
+ * A key representing the presence of Atmos in an E-AC3 stream.
+ *
+ * A Boolean value which is true if Atmos is present in an E-AC3 stream.
+ */
+ @NonNull public static final Key<Boolean> KEY_ATMOS_PRESENT =
+ createKey("atmos-present", Boolean.class);
+
+ /**
+ * A key representing the audio encoding used for the stream.
+ * This is the same encoding used in {@link AudioFormat#getEncoding()}.
+ *
+ * An Integer value.
+ *
+ * @see AudioFormat#getEncoding()
+ */
+ @NonNull public static final Key<Integer> KEY_AUDIO_ENCODING =
+ createKey("audio-encoding", Integer.class);
+
+ private Format() {} // delete constructor
+ }
+
+ /////////////////////////////////////////////////////////////////////////
+ // Hidden methods and functions.
+
+ /**
+ * Returns a Key object with the correct interface for the AudioMetadata.
+ *
+ * An interface with the same name and type will be treated as
+ * identical for the purposes of value storage, even though
+ * other methods or hidden parameters may return different values.
+ *
+ * @param name The name of the key.
+ * @param type The class type of the value represented by the key.
+ * @param <T> The type of value.
+ * @return a new key interface.
+ *
+ * Creating keys is currently only allowed by the Framework.
+ * @hide
+ */
+ @NonNull
+ public static <T> Key<T> createKey(String name, Class<T> type) {
+ // Implementation specific.
+ return new Key<T>() {
+ private final String mName = name;
+ private final Class<T> mType = type;
+
+ @Override
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+
+ @Override
+ @NonNull
+ public Class<T> getValueClass() {
+ return mType;
+ }
+
+ // hidden interface method to prevent user class implements the of Key interface.
+ @Override
+ public boolean isFromFramework() {
+ return true;
+ }
+ };
+ }
+
+ /**
+ * @hide
+ *
+ * AudioMetadata is based on interfaces in order to allow multiple inheritance
+ * and maximum flexibility in implementation.
+ *
+ * Here, we provide a simple implementation of {@link Map} interface;
+ * Note that the Keys are not specific to this Map implementation.
+ *
+ * It is possible to require the keys to be of a certain class
+ * before allowing a set or get operation.
+ */
+ public static class BaseMap implements Map {
+ @Override
+ public <T> boolean containsKey(@NonNull Key<T> key) {
+ Pair<Key<?>, Object> valuePair = mHashMap.get(pairFromKey(key));
+ return valuePair != null;
+ }
+
+ @Override
+ @NonNull
+ public Map dup() {
+ BaseMap map = new BaseMap();
+ map.mHashMap.putAll(this.mHashMap);
+ return map;
+ }
+
+ @Override
+ @Nullable
+ public <T> T get(@NonNull Key<T> key) {
+ Pair<Key<?>, Object> valuePair = mHashMap.get(pairFromKey(key));
+ return (T) getValueFromValuePair(valuePair);
+ }
+
+ @Override
+ @NonNull
+ public Set<Key<?>> keySet() {
+ HashSet<Key<?>> set = new HashSet();
+ for (Pair<Key<?>, Object> pair : mHashMap.values()) {
+ set.add(pair.first);
+ }
+ return set;
+ }
+
+ @Override
+ @Nullable
+ public <T> T remove(@NonNull Key<T> key) {
+ Pair<Key<?>, Object> valuePair = mHashMap.remove(pairFromKey(key));
+ return (T) getValueFromValuePair(valuePair);
+ }
+
+ @Override
+ @Nullable
+ public <T> T set(@NonNull Key<T> key, @NonNull T value) {
+ Objects.requireNonNull(value);
+ Pair<Key<?>, Object> valuePair = mHashMap
+ .put(pairFromKey(key), new Pair<Key<?>, Object>(key, value));
+ return (T) getValueFromValuePair(valuePair);
+ }
+
+ @Override
+ public int size() {
+ return mHashMap.size();
+ }
+
+ /*
+ * Implementation specific.
+ *
+ * To store the value in the HashMap we need to convert the Key interface
+ * to a hashcode() / equals() compliant Pair.
+ */
+ @NonNull
+ private static <T> Pair<String, Class<?>> pairFromKey(@NonNull Key<T> key) {
+ Objects.requireNonNull(key);
+ return new Pair<String, Class<?>>(key.getName(), key.getValueClass());
+ }
+
+ /*
+ * Implementation specific.
+ *
+ * We store in a Pair (valuePair) the key along with the Object value.
+ * This helper returns the Object value from the value pair.
+ */
+ @Nullable
+ private static Object getValueFromValuePair(@Nullable Pair<Key<?>, Object> valuePair) {
+ if (valuePair == null) {
+ return null;
+ }
+ return valuePair.second;
+ }
+
+ /*
+ * Implementation specific.
+ *
+ * We use a HashMap to back the AudioMetadata BaseMap object.
+ * This is not locked, so concurrent reads are permitted if all threads
+ * have a ReadMap; this is risky with a Map.
+ */
+ private final HashMap<Pair<String, Class<?>>, Pair<Key<?>, Object>> mHashMap =
+ new HashMap();
+ }
+
+ // Delete the constructor as there is nothing to implement here.
+ private AudioMetadata() {}
+}
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index 4dbc79b54199..f566f646f1e0 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -188,6 +188,10 @@ public class AudioTrack extends PlayerBase
// Events:
// to keep in sync with frameworks/av/include/media/AudioTrack.h
+ // Note: To avoid collisions with other event constants,
+ // do not define an event here that is the same value as
+ // AudioSystem.NATIVE_EVENT_ROUTING_CHANGE.
+
/**
* Event id denotes when playback head has reached a previously set marker.
*/
@@ -210,6 +214,14 @@ public class AudioTrack extends PlayerBase
* back (after stop is called) for an offloaded track.
*/
private static final int NATIVE_EVENT_STREAM_END = 7;
+ /**
+ * Event id denotes when the codec format changes.
+ *
+ * Note: Similar to a device routing change (AudioSystem.NATIVE_EVENT_ROUTING_CHANGE),
+ * this event comes from the AudioFlinger Thread / Output Stream management
+ * (not from buffer indications as above).
+ */
+ private static final int NATIVE_EVENT_CODEC_FORMAT_CHANGE = 100;
private final static String TAG = "android.media.AudioTrack";
@@ -3409,6 +3421,67 @@ public class AudioTrack extends PlayerBase
}
}
+ //--------------------------------------------------------------------------
+ // Codec notifications
+ //--------------------
+
+ // OnCodecFormatChangedListener notifications uses an instance
+ // of ListenerList to manage its listeners.
+
+ private final Utils.ListenerList<AudioMetadata.ReadMap> mCodecFormatChangedListeners =
+ new Utils.ListenerList();
+
+ /**
+ * Interface definition for a listener for codec format changes.
+ */
+ public interface OnCodecFormatChangedListener {
+ /**
+ * Called when the compressed codec format changes.
+ *
+ * @param audioTrack is the {@code AudioTrack} instance associated with the codec.
+ * @param info is a {@link AudioMetadata.ReadMap} of values which contains decoded format
+ * changes reported by the codec. Not all hardware
+ * codecs indicate codec format changes. Acceptable keys are taken from
+ * {@code AudioMetadata.Format.KEY_*} range, with the associated value type.
+ */
+ void onCodecFormatChanged(
+ @NonNull AudioTrack audioTrack, @Nullable AudioMetadata.ReadMap info);
+ }
+
+ /**
+ * Adds an {@link OnCodecFormatChangedListener} to receive notifications of
+ * codec format change events on this {@code AudioTrack}.
+ *
+ * @param executor Specifies the {@link Executor} object to control execution.
+ *
+ * @param listener The {@link OnCodecFormatChangedListener} interface to receive
+ * notifications of codec events.
+ */
+ public void addOnCodecFormatChangedListener(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OnCodecFormatChangedListener listener) { // NPE checks done by ListenerList.
+ mCodecFormatChangedListeners.add(
+ listener, /* key for removal */
+ executor,
+ (int eventCode, AudioMetadata.ReadMap readMap) -> {
+ // eventCode is unused by this implementation.
+ listener.onCodecFormatChanged(this, readMap);
+ }
+ );
+ }
+
+ /**
+ * Removes an {@link OnCodecFormatChangedListener} which has been previously added
+ * to receive codec format change events.
+ *
+ * @param listener The previously added {@link OnCodecFormatChangedListener} interface
+ * to remove.
+ */
+ public void removeOnCodecFormatChangedListener(
+ @NonNull OnCodecFormatChangedListener listener) {
+ mCodecFormatChangedListeners.remove(listener); // NPE checks done by ListenerList.
+ }
+
//---------------------------------------------------------
// Interface definitions
//--------------------
@@ -3745,6 +3818,12 @@ public class AudioTrack extends PlayerBase
return;
}
+ if (what == NATIVE_EVENT_CODEC_FORMAT_CHANGE) {
+ track.mCodecFormatChangedListeners.notify(
+ 0 /* eventCode, unused */, (AudioMetadata.ReadMap) obj);
+ return;
+ }
+
if (what == NATIVE_EVENT_CAN_WRITE_MORE_DATA
|| what == NATIVE_EVENT_NEW_IAUDIOTRACK
|| what == NATIVE_EVENT_STREAM_END) {
diff --git a/media/java/android/media/Utils.java b/media/java/android/media/Utils.java
index d942bb653127..7a4e7b897de1 100644
--- a/media/java/android/media/Utils.java
+++ b/media/java/android/media/Utils.java
@@ -16,12 +16,17 @@
package android.media;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
+import android.os.Binder;
import android.os.Environment;
import android.os.FileUtils;
+import android.os.Handler;
import android.provider.OpenableColumns;
import android.util.Log;
import android.util.Pair;
@@ -29,14 +34,26 @@ import android.util.Range;
import android.util.Rational;
import android.util.Size;
+import com.android.internal.annotations.GuardedBy;
+
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Arrays;
import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Objects;
import java.util.Vector;
+import java.util.concurrent.Executor;
-// package private
-class Utils {
+/**
+ * Media Utilities
+ *
+ * This class is hidden but public to allow CTS testing and verification
+ * of the static methods and classes.
+ *
+ * @hide
+ */
+public class Utils {
private static final String TAG = "Utils";
/**
@@ -381,4 +398,265 @@ class Utils {
// it already represents the file's name.
return uri.toString();
}
+
+ /**
+ * {@code ListenerList} is a helper class that delivers events to listeners.
+ *
+ * It is written to isolate the <strong>mechanics</strong> of event delivery from the
+ * <strong>details</strong> of those events.
+ *
+ * The {@code ListenerList} is parameterized on the generic type {@code V}
+ * of the object delivered by {@code notify()}.
+ * This gives compile time type safety over run-time casting of a general {@code Object},
+ * much like {@code HashMap&lt;String, Object&gt;} does not give type safety of the
+ * stored {@code Object} value and may allow
+ * permissive storage of {@code Object}s that are not expected by users of the
+ * {@code HashMap}, later resulting in run-time cast exceptions that
+ * could have been caught by replacing
+ * {@code Object} with a more precise type to enforce a compile time contract.
+ *
+ * The {@code ListenerList} is implemented as a single method callback
+ * - or a "listener" according to Android style guidelines.
+ *
+ * The {@code ListenerList} can be trivially extended by a suitable lambda to implement
+ * a <strong> multiple method abstract class</strong> "callback",
+ * in which the generic type {@code V} could be an {@code Object}
+ * to encapsulate the details of the parameters of each callback method, and
+ * {@code instanceof} could be used to disambiguate which callback method to use.
+ * A {@link Bundle} could alternatively encapsulate those generic parameters,
+ * perhaps more conveniently.
+ * Again, this is a detail of the event, not the mechanics of the event delivery,
+ * which this class is concerned with.
+ *
+ * For details on how to use this class to implement a <strong>single listener</strong>
+ * {@code ListenerList}, see notes on {@link #add}.
+ *
+ * For details on how to optimize this class to implement
+ * a listener based on {@link Handler}s
+ * instead of {@link Executor}s, see{@link #ListenerList(boolean, boolean, boolean)}.
+ *
+ * This is a TestApi for CTS Unit Testing, not exposed for general Application use.
+ * @hide
+ *
+ * @param <V> The class of the object returned to the listener.
+ */
+ @TestApi
+ public static class ListenerList<V> {
+ /**
+ * The Listener interface for callback.
+ *
+ * @param <V> The class of the object returned to the listener
+ */
+ public interface Listener<V> {
+ /**
+ * General event listener interface which is managed by the {@code ListenerList}.
+ *
+ * @param eventCode is an integer representing the event type. This is an
+ * implementation defined parameter.
+ * @param info is the object returned to the listener. It is expected
+ * that the listener makes a private copy of the {@code info} object before
+ * modification, as it is the same instance passed to all listeners.
+ * This is an implementation defined parameter that may be null.
+ */
+ void onEvent(int eventCode, @Nullable V info);
+ }
+
+ private interface ListenerWithCancellation<V> extends Listener<V> {
+ void cancel();
+ }
+
+ /**
+ * Default {@code ListenerList} constructor for {@link Executor} based implementation.
+ *
+ * TODO: consider adding a "name" for debugging if this is used for
+ * multiple listener implementations.
+ */
+ public ListenerList() {
+ this(true /* restrictSingleCallerOnEvent */,
+ true /* clearCallingIdentity */,
+ false /* forceRemoveConsistency*/);
+ }
+
+ /**
+ * Specific {@code ListenerList} constructor for customization.
+ *
+ * See the internal notes for the corresponding private variables on the behavior of
+ * the boolean configuration parameters.
+ *
+ * {@code ListenerList(true, true, false)} is the default and used for
+ * {@link Executor} based notification implementation.
+ *
+ * {@code ListenerList(false, false, false)} may be used for as an optimization
+ * where the {@link Executor} is actually a {@link Handler} post.
+ *
+ * @param restrictSingleCallerOnEvent whether the listener will only be called by
+ * a single thread at a time.
+ * @param clearCallingIdentity whether the binder calling identity on
+ * {@link #notify} is cleared.
+ * @param forceRemoveConsistency whether remove() guarantees no more callbacks to
+ * the listener immediately after the call.
+ */
+ public ListenerList(boolean restrictSingleCallerOnEvent,
+ boolean clearCallingIdentity,
+ boolean forceRemoveConsistency) {
+ mRestrictSingleCallerOnEvent = restrictSingleCallerOnEvent;
+ mClearCallingIdentity = clearCallingIdentity;
+ mForceRemoveConsistency = forceRemoveConsistency;
+ }
+
+ /**
+ * Adds a listener to the {@code ListenerList}.
+ *
+ * The {@code ListenerList} is most often used to hold {@code multiple} listeners.
+ *
+ * Per Android style, for a single method Listener interface, the add and remove
+ * would be wrapped in "addSomeListener" or "removeSomeListener";
+ * or a lambda implemented abstract class callback, wrapped in
+ * "registerSomeCallback" or "unregisterSomeCallback".
+ *
+ * We allow a general {@code key} to be attached to add and remove that specific
+ * listener. It could be the {@code listener} object itself.
+ *
+ * For some implementations, there may be only a {@code single} listener permitted.
+ *
+ * Per Android style, for a single listener {@code ListenerList},
+ * the naming of the wrapping call to {@link #add} would be
+ * "setSomeListener" with a nullable listener, which would be null
+ * to call {@link #remove}.
+ *
+ * In that case, the caller may use this {@link #add} with a single constant object for
+ * the {@code key} to enforce only one Listener in the {@code ListenerList}.
+ * Likewise on remove it would use that
+ * same single constant object to remove the listener.
+ * That {@code key} object could be the {@code ListenerList} itself for convenience.
+ *
+ * @param key is a unique object that is used to identify the listener
+ * when {@code remove()} is called. It can be the listener itself.
+ * @param executor is used to execute the callback.
+ * @param listener is the {@link AudioTrack.ListenerList.Listener}
+ * interface to be called upon {@link notify}.
+ */
+ public void add(
+ @NonNull Object key, @NonNull Executor executor, @NonNull Listener<V> listener) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(listener);
+
+ // construct wrapper outside of lock.
+ ListenerWithCancellation<V> listenerWithCancellation =
+ new ListenerWithCancellation<V>() {
+ private final Object mLock = new Object(); // our lock is per Listener.
+ private volatile boolean mCancelled = false; // atomic rmw not needed.
+
+ @Override
+ public void onEvent(int eventCode, V info) {
+ executor.execute(() -> {
+ // Note deep execution of locking and cancellation
+ // so this works after posting on different threads.
+ if (mRestrictSingleCallerOnEvent || mForceRemoveConsistency) {
+ synchronized (mLock) {
+ if (mCancelled) return;
+ listener.onEvent(eventCode, info);
+ }
+ } else {
+ if (mCancelled) return;
+ listener.onEvent(eventCode, info);
+ }
+ });
+ }
+
+ @Override
+ public void cancel() {
+ if (mForceRemoveConsistency) {
+ synchronized (mLock) {
+ mCancelled = true;
+ }
+ } else {
+ mCancelled = true;
+ }
+ }
+ };
+
+ synchronized (mListeners) {
+ // TODO: consider an option to check the existence of the key
+ // and throw an ISE if it exists.
+ mListeners.put(key, listenerWithCancellation); // replaces old value
+ }
+ }
+
+ /**
+ * Removes a listener from the {@code ListenerList}.
+ *
+ * @param key the unique object associated with the listener during {@link #add}.
+ */
+ public void remove(@NonNull Object key) {
+ Objects.requireNonNull(key);
+
+ ListenerWithCancellation<V> listener;
+ synchronized (mListeners) {
+ listener = mListeners.get(key);
+ if (listener == null) { // TODO: consider an option to throw ISE Here.
+ return;
+ }
+ mListeners.remove(key); // removes if exist
+ }
+
+ // cancel outside of lock
+ listener.cancel();
+ }
+
+ /**
+ * Notifies all listeners on the List.
+ *
+ * @param eventCode to pass to all listeners.
+ * @param info to pass to all listeners. This is an implemention defined parameter
+ * which may be {@code null}.
+ */
+ public void notify(int eventCode, @Nullable V info) {
+ Object[] listeners; // note we can't cast an object array to a listener array
+ synchronized (mListeners) {
+ if (mListeners.size() == 0) {
+ return;
+ }
+ listeners = mListeners.values().toArray(); // guarantees a copy.
+ }
+
+ // notify outside of lock.
+ final Long identity = mClearCallingIdentity ? Binder.clearCallingIdentity() : null;
+ try {
+ for (Object object : listeners) {
+ final ListenerWithCancellation<V> listener =
+ (ListenerWithCancellation<V>) object;
+ listener.onEvent(eventCode, info);
+ }
+ } finally {
+ if (identity != null) {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
+
+ @GuardedBy("mListeners")
+ private HashMap<Object, ListenerWithCancellation<V>> mListeners = new HashMap<>();
+
+ // An Executor may run in multiple threads, whereas a Handler runs on a single Looper.
+ // Should be true for an Executor to avoid concurrent calling into the same listener,
+ // can be false for a Handler as a Handler forces single thread caller for each listener.
+ private final boolean mRestrictSingleCallerOnEvent; // default true
+
+ // An Executor may run in the calling thread, whereas a handler will post to the Looper.
+ // Should be true for an Executor to prevent privilege escalation,
+ // can be false for a Handler as its thread is not the calling binder thread.
+ private final boolean mClearCallingIdentity; // default true
+
+ // Guaranteeing no listener callbacks after removal requires taking the same lock for the
+ // remove as the callback; this is a reversal in calling layers,
+ // hence the risk of lock order inversion is great.
+ //
+ // Set to true only if you can control the caller's listen and remove methods and/or
+ // the threading of the Executor used for each listener.
+ // When set to false, we do not lock, but still do a best effort to cancel messages
+ // on the fly.
+ private final boolean mForceRemoveConsistency; // default false
+ }
}