/* * 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 > void sortDistinctRanges(Range[] ranges) { Arrays.sort(ranges, new Comparator>() { @Override public int compare(Range lhs, Range 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 > Range[] intersectSortedDistinctRanges(Range[] one, Range[] another) { int ix = 0; Vector> result = new Vector>(); for (Range 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 > int binarySearchDistinctRanges(Range[] ranges, T value) { return Arrays.binarySearch(ranges, Range.create(value, value), new Comparator>() { @Override public int compare(Range lhs, Range 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 RangefactorRange(Range 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 RangefactorRange(Range 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 scaleRange(Range 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 alignRange(Range 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 intRangeFor(double v) { return Range.create((int)v, (int)Math.ceil(v)); } static Range 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 parseIntRange(Object o, Range 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 parseLongRange(Object o, Range 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 parseRationalRange(Object o, Range 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 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 mechanics of event delivery from the * details 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 multiple method abstract class "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 single listener * {@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 The class of the object returned to the listener. */ public static class ListenerList { /** * The Listener interface for callback. * * @param The class of the object returned to the listener */ public interface Listener { /** * 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 extends Listener { 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 listener) { Objects.requireNonNull(key); Objects.requireNonNull(executor); Objects.requireNonNull(listener); // construct wrapper outside of lock. ListenerWithCancellation listenerWithCancellation = new ListenerWithCancellation() { 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 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 listener = (ListenerWithCancellation) object; listener.onEvent(eventCode, info); } } finally { if (identity != null) { Binder.restoreCallingIdentity(identity); } } } @GuardedBy("mListeners") private HashMap> 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); } }