diff options
| author | 2020-01-22 06:24:31 +0000 | |
|---|---|---|
| committer | 2020-01-22 06:24:31 +0000 | |
| commit | 6f0d16df8c75c89fdfa7f9cbac6d7b74fe040295 (patch) | |
| tree | 6d85885fa426a5f72874c11c52528021aecfb84e | |
| parent | e68e6deb3e7e59994b0306064dd49c93af81d542 (diff) | |
| parent | b7149a1d19ccb5aa933d0709eb2ec46e1d7ee10a (diff) | |
Merge "AudioTrack: Add Codec format change listener"
| -rw-r--r-- | api/current.txt | 37 | ||||
| -rw-r--r-- | media/java/android/media/AudioMetadata.java | 406 | ||||
| -rw-r--r-- | media/java/android/media/AudioTrack.java | 79 | ||||
| -rw-r--r-- | media/java/android/media/Utils.java | 282 |
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<String, Object>} 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 + } } |