| /* |
| * Copyright (C) 2014 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.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; |
| 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; |
| |
| /** |
| * 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"; |
| |
| /** |
| * Sorts distinct (non-intersecting) range array in ascending order. |
| * @throws java.lang.IllegalArgumentException if ranges are not distinct |
| */ |
| public static <T extends Comparable<? super T>> void sortDistinctRanges(Range<T>[] ranges) { |
| Arrays.sort(ranges, new Comparator<Range<T>>() { |
| @Override |
| public int compare(Range<T> lhs, Range<T> rhs) { |
| if (lhs.getUpper().compareTo(rhs.getLower()) < 0) { |
| return -1; |
| } else if (lhs.getLower().compareTo(rhs.getUpper()) > 0) { |
| return 1; |
| } |
| throw new IllegalArgumentException( |
| "sample rate ranges must be distinct (" + lhs + " and " + rhs + ")"); |
| } |
| }); |
| } |
| |
| /** |
| * Returns the intersection of two sets of non-intersecting ranges |
| * @param one a sorted set of non-intersecting ranges in ascending order |
| * @param another another sorted set of non-intersecting ranges in ascending order |
| * @return the intersection of the two sets, sorted in ascending order |
| */ |
| public static <T extends Comparable<? super T>> |
| Range<T>[] intersectSortedDistinctRanges(Range<T>[] one, Range<T>[] another) { |
| int ix = 0; |
| Vector<Range<T>> result = new Vector<Range<T>>(); |
| for (Range<T> range: another) { |
| while (ix < one.length && |
| one[ix].getUpper().compareTo(range.getLower()) < 0) { |
| ++ix; |
| } |
| while (ix < one.length && |
| one[ix].getUpper().compareTo(range.getUpper()) < 0) { |
| result.add(range.intersect(one[ix])); |
| ++ix; |
| } |
| if (ix == one.length) { |
| break; |
| } |
| if (one[ix].getLower().compareTo(range.getUpper()) <= 0) { |
| result.add(range.intersect(one[ix])); |
| } |
| } |
| return result.toArray(new Range[result.size()]); |
| } |
| |
| /** |
| * Returns the index of the range that contains a value in a sorted array of distinct ranges. |
| * @param ranges a sorted array of non-intersecting ranges in ascending order |
| * @param value the value to search for |
| * @return if the value is in one of the ranges, it returns the index of that range. Otherwise, |
| * the return value is {@code (-1-index)} for the {@code index} of the range that is |
| * immediately following {@code value}. |
| */ |
| public static <T extends Comparable<? super T>> |
| int binarySearchDistinctRanges(Range<T>[] ranges, T value) { |
| return Arrays.binarySearch(ranges, Range.create(value, value), |
| new Comparator<Range<T>>() { |
| @Override |
| public int compare(Range<T> lhs, Range<T> rhs) { |
| if (lhs.getUpper().compareTo(rhs.getLower()) < 0) { |
| return -1; |
| } else if (lhs.getLower().compareTo(rhs.getUpper()) > 0) { |
| return 1; |
| } |
| return 0; |
| } |
| }); |
| } |
| |
| /** |
| * Returns greatest common divisor |
| */ |
| static int gcd(int a, int b) { |
| if (a == 0 && b == 0) { |
| return 1; |
| } |
| if (b < 0) { |
| b = -b; |
| } |
| if (a < 0) { |
| a = -a; |
| } |
| while (a != 0) { |
| int c = b % a; |
| b = a; |
| a = c; |
| } |
| return b; |
| } |
| |
| /** Returns the equivalent factored range {@code newrange}, where for every |
| * {@code e}: {@code newrange.contains(e)} implies that {@code range.contains(e * factor)}, |
| * and {@code !newrange.contains(e)} implies that {@code !range.contains(e * factor)}. |
| */ |
| static Range<Integer>factorRange(Range<Integer> range, int factor) { |
| if (factor == 1) { |
| return range; |
| } |
| return Range.create(divUp(range.getLower(), factor), range.getUpper() / factor); |
| } |
| |
| /** Returns the equivalent factored range {@code newrange}, where for every |
| * {@code e}: {@code newrange.contains(e)} implies that {@code range.contains(e * factor)}, |
| * and {@code !newrange.contains(e)} implies that {@code !range.contains(e * factor)}. |
| */ |
| static Range<Long>factorRange(Range<Long> range, long factor) { |
| if (factor == 1) { |
| return range; |
| } |
| return Range.create(divUp(range.getLower(), factor), range.getUpper() / factor); |
| } |
| |
| private static Rational scaleRatio(Rational ratio, int num, int den) { |
| int common = gcd(num, den); |
| num /= common; |
| den /= common; |
| return new Rational( |
| (int)(ratio.getNumerator() * (double)num), // saturate to int |
| (int)(ratio.getDenominator() * (double)den)); // saturate to int |
| } |
| |
| static Range<Rational> scaleRange(Range<Rational> range, int num, int den) { |
| if (num == den) { |
| return range; |
| } |
| return Range.create( |
| scaleRatio(range.getLower(), num, den), |
| scaleRatio(range.getUpper(), num, den)); |
| } |
| |
| static Range<Integer> alignRange(Range<Integer> range, int align) { |
| return range.intersect( |
| divUp(range.getLower(), align) * align, |
| (range.getUpper() / align) * align); |
| } |
| |
| static int divUp(int num, int den) { |
| return (num + den - 1) / den; |
| } |
| |
| static long divUp(long num, long den) { |
| return (num + den - 1) / den; |
| } |
| |
| /** |
| * Returns least common multiple |
| */ |
| private static long lcm(int a, int b) { |
| if (a == 0 || b == 0) { |
| throw new IllegalArgumentException("lce is not defined for zero arguments"); |
| } |
| return (long)a * b / gcd(a, b); |
| } |
| |
| static Range<Integer> intRangeFor(double v) { |
| return Range.create((int)v, (int)Math.ceil(v)); |
| } |
| |
| static Range<Long> longRangeFor(double v) { |
| return Range.create((long)v, (long)Math.ceil(v)); |
| } |
| |
| static Size parseSize(Object o, Size fallback) { |
| if (o == null) { |
| return fallback; |
| } |
| try { |
| return Size.parseSize((String) o); |
| } catch (ClassCastException e) { |
| } catch (NumberFormatException e) { |
| } |
| Log.w(TAG, "could not parse size '" + o + "'"); |
| return fallback; |
| } |
| |
| static int parseIntSafely(Object o, int fallback) { |
| if (o == null) { |
| return fallback; |
| } |
| try { |
| String s = (String)o; |
| return Integer.parseInt(s); |
| } catch (ClassCastException e) { |
| } catch (NumberFormatException e) { |
| } |
| Log.w(TAG, "could not parse integer '" + o + "'"); |
| return fallback; |
| } |
| |
| static Range<Integer> parseIntRange(Object o, Range<Integer> fallback) { |
| if (o == null) { |
| return fallback; |
| } |
| try { |
| String s = (String)o; |
| int ix = s.indexOf('-'); |
| if (ix >= 0) { |
| return Range.create( |
| Integer.parseInt(s.substring(0, ix), 10), |
| Integer.parseInt(s.substring(ix + 1), 10)); |
| } |
| int value = Integer.parseInt(s); |
| return Range.create(value, value); |
| } catch (ClassCastException e) { |
| } catch (NumberFormatException e) { |
| } catch (IllegalArgumentException e) { |
| } |
| Log.w(TAG, "could not parse integer range '" + o + "'"); |
| return fallback; |
| } |
| |
| static Range<Long> parseLongRange(Object o, Range<Long> fallback) { |
| if (o == null) { |
| return fallback; |
| } |
| try { |
| String s = (String)o; |
| int ix = s.indexOf('-'); |
| if (ix >= 0) { |
| return Range.create( |
| Long.parseLong(s.substring(0, ix), 10), |
| Long.parseLong(s.substring(ix + 1), 10)); |
| } |
| long value = Long.parseLong(s); |
| return Range.create(value, value); |
| } catch (ClassCastException e) { |
| } catch (NumberFormatException e) { |
| } catch (IllegalArgumentException e) { |
| } |
| Log.w(TAG, "could not parse long range '" + o + "'"); |
| return fallback; |
| } |
| |
| static Range<Rational> parseRationalRange(Object o, Range<Rational> fallback) { |
| if (o == null) { |
| return fallback; |
| } |
| try { |
| String s = (String)o; |
| int ix = s.indexOf('-'); |
| if (ix >= 0) { |
| return Range.create( |
| Rational.parseRational(s.substring(0, ix)), |
| Rational.parseRational(s.substring(ix + 1))); |
| } |
| Rational value = Rational.parseRational(s); |
| return Range.create(value, value); |
| } catch (ClassCastException e) { |
| } catch (NumberFormatException e) { |
| } catch (IllegalArgumentException e) { |
| } |
| Log.w(TAG, "could not parse rational range '" + o + "'"); |
| return fallback; |
| } |
| |
| static Pair<Size, Size> parseSizeRange(Object o) { |
| if (o == null) { |
| return null; |
| } |
| try { |
| String s = (String)o; |
| int ix = s.indexOf('-'); |
| if (ix >= 0) { |
| return Pair.create( |
| Size.parseSize(s.substring(0, ix)), |
| Size.parseSize(s.substring(ix + 1))); |
| } |
| Size value = Size.parseSize(s); |
| return Pair.create(value, value); |
| } catch (ClassCastException e) { |
| } catch (NumberFormatException e) { |
| } catch (IllegalArgumentException e) { |
| } |
| Log.w(TAG, "could not parse size range '" + o + "'"); |
| return null; |
| } |
| |
| /** |
| * Creates a unique file in the specified external storage with the desired name. If the name is |
| * taken, the new file's name will have '(%d)' to avoid overwriting files. |
| * |
| * @param context {@link Context} to query the file name from. |
| * @param subdirectory One of the directories specified in {@link android.os.Environment} |
| * @param fileName desired name for the file. |
| * @param mimeType MIME type of the file to create. |
| * @return the File object in the storage, or null if an error occurs. |
| */ |
| public static File getUniqueExternalFile(Context context, String subdirectory, String fileName, |
| String mimeType) { |
| File externalStorage = Environment.getExternalStoragePublicDirectory(subdirectory); |
| // Make sure the storage subdirectory exists |
| externalStorage.mkdirs(); |
| |
| File outFile = null; |
| try { |
| // Ensure the file has a unique name, as to not override any existing file |
| outFile = FileUtils.buildUniqueFile(externalStorage, mimeType, fileName); |
| } catch (FileNotFoundException e) { |
| // This might also be reached if the number of repeated files gets too high |
| Log.e(TAG, "Unable to get a unique file name: " + e); |
| return null; |
| } |
| return outFile; |
| } |
| |
| /** |
| * Returns a file's display name from its {@link android.content.ContentResolver.SCHEME_FILE} |
| * or {@link android.content.ContentResolver.SCHEME_CONTENT} Uri. The display name of a file |
| * includes its extension. |
| * |
| * @param context Context trying to resolve the file's display name. |
| * @param uri Uri of the file. |
| * @return the file's display name, or the uri's string if something fails or the uri isn't in |
| * the schemes specified above. |
| */ |
| static String getFileDisplayNameFromUri(Context context, Uri uri) { |
| String scheme = uri.getScheme(); |
| |
| if (ContentResolver.SCHEME_FILE.equals(scheme)) { |
| return uri.getLastPathSegment(); |
| } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) { |
| // We need to query the ContentResolver to get the actual file name as the Uri masks it. |
| // This means we want the name used for display purposes only. |
| String[] proj = { |
| OpenableColumns.DISPLAY_NAME |
| }; |
| try (Cursor cursor = context.getContentResolver().query(uri, proj, null, null, null)) { |
| if (cursor != null && cursor.getCount() != 0) { |
| cursor.moveToFirst(); |
| return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); |
| } |
| } |
| } |
| |
| // This will only happen if the Uri isn't either SCHEME_CONTENT or SCHEME_FILE, so we assume |
| // 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. |
| */ |
| 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 |
| } |
| |
| /** |
| * Convert a Bluetooth MAC address to an anonymized one when exposed to a non privileged app |
| * Must match the implementation of BluetoothUtils.toAnonymizedAddress() |
| * @param address MAC address to be anonymized |
| * @return anonymized MAC address |
| */ |
| public static @Nullable String anonymizeBluetoothAddress(@Nullable String address) { |
| if (address == null) { |
| return null; |
| } |
| if (address.length() != "AA:BB:CC:DD:EE:FF".length()) { |
| return address; |
| } |
| return "XX:XX:XX:XX" + address.substring("XX:XX:XX:XX".length()); |
| } |
| |
| /** |
| * Convert a Bluetooth MAC address to an anonymized one if the internal device type corresponds |
| * to a Bluetooth. |
| * @param deviceType the internal type of the audio device |
| * @param address MAC address to be anonymized |
| * @return anonymized MAC address |
| */ |
| public static @Nullable String anonymizeBluetoothAddress( |
| int deviceType, @Nullable String address) { |
| if (!AudioSystem.isBluetoothDevice(deviceType)) { |
| return address; |
| } |
| return anonymizeBluetoothAddress(address); |
| } |
| } |