blob: 60fe1a39d4b25cabd9016667de1256eec6c05183 [file] [log] [blame]
/*
* Copyright (C) 2016 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 com.google.android.exoplayer2.util;
import static android.content.Context.UI_MODE_SERVICE;
import android.Manifest.permission;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.UiModeManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Point;
import android.media.AudioFormat;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Parcel;
import android.os.SystemClock;
import android.security.NetworkSecurityPolicy;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.view.Display;
import android.view.SurfaceView;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.upstream.DataSource;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Formatter;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import org.checkerframework.checker.initialization.qual.UnknownInitialization;
import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.PolyNull;
/**
* Miscellaneous utility methods.
*/
public final class Util {
/**
* Like {@link android.os.Build.VERSION#SDK_INT}, but in a place where it can be conveniently
* overridden for local testing.
*/
public static final int SDK_INT = Build.VERSION.SDK_INT;
/**
* Like {@link Build#DEVICE}, but in a place where it can be conveniently overridden for local
* testing.
*/
public static final String DEVICE = Build.DEVICE;
/**
* Like {@link Build#MANUFACTURER}, but in a place where it can be conveniently overridden for
* local testing.
*/
public static final String MANUFACTURER = Build.MANUFACTURER;
/**
* Like {@link Build#MODEL}, but in a place where it can be conveniently overridden for local
* testing.
*/
public static final String MODEL = Build.MODEL;
/**
* A concise description of the device that it can be useful to log for debugging purposes.
*/
public static final String DEVICE_DEBUG_INFO = DEVICE + ", " + MODEL + ", " + MANUFACTURER + ", "
+ SDK_INT;
/** An empty byte array. */
public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
private static final String TAG = "Util";
private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile(
"(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]"
+ "(\\d\\d):(\\d\\d):(\\d\\d)([\\.,](\\d+))?"
+ "([Zz]|((\\+|\\-)(\\d?\\d):?(\\d\\d)))?");
private static final Pattern XS_DURATION_PATTERN =
Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?"
+ "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$");
private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})");
// Replacement map of ISO language codes used for normalization.
@Nullable private static HashMap<String, String> languageTagReplacementMap;
private Util() {}
/**
* Converts the entirety of an {@link InputStream} to a byte array.
*
* @param inputStream the {@link InputStream} to be read. The input stream is not closed by this
* method.
* @return a byte array containing all of the inputStream's bytes.
* @throws IOException if an error occurs reading from the stream.
*/
public static byte[] toByteArray(InputStream inputStream) throws IOException {
byte[] buffer = new byte[1024 * 4];
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
return outputStream.toByteArray();
}
/**
* Calls {@link Context#startForegroundService(Intent)} if {@link #SDK_INT} is 26 or higher, or
* {@link Context#startService(Intent)} otherwise.
*
* @param context The context to call.
* @param intent The intent to pass to the called method.
* @return The result of the called method.
*/
@Nullable
public static ComponentName startForegroundService(Context context, Intent intent) {
if (Util.SDK_INT >= 26) {
return context.startForegroundService(intent);
} else {
return context.startService(intent);
}
}
/**
* Checks whether it's necessary to request the {@link permission#READ_EXTERNAL_STORAGE}
* permission read the specified {@link Uri}s, requesting the permission if necessary.
*
* @param activity The host activity for checking and requesting the permission.
* @param uris {@link Uri}s that may require {@link permission#READ_EXTERNAL_STORAGE} to read.
* @return Whether a permission request was made.
*/
public static boolean maybeRequestReadExternalStoragePermission(Activity activity, Uri... uris) {
if (Util.SDK_INT < 23) {
return false;
}
for (Uri uri : uris) {
if (isLocalFileUri(uri)) {
return requestExternalStoragePermission(activity);
}
}
return false;
}
/**
* Checks whether it's necessary to request the {@link permission#READ_EXTERNAL_STORAGE}
* permission for the specified {@link MediaItem media items}, requesting the permission if
* necessary.
*
* @param activity The host activity for checking and requesting the permission.
* @param mediaItems {@link MediaItem Media items}s that may require {@link
* permission#READ_EXTERNAL_STORAGE} to read.
* @return Whether a permission request was made.
*/
public static boolean maybeRequestReadExternalStoragePermission(
Activity activity, MediaItem... mediaItems) {
if (Util.SDK_INT < 23) {
return false;
}
for (MediaItem mediaItem : mediaItems) {
if (mediaItem.playbackProperties == null) {
continue;
}
if (isLocalFileUri(mediaItem.playbackProperties.sourceUri)) {
return requestExternalStoragePermission(activity);
}
for (int i = 0; i < mediaItem.playbackProperties.subtitles.size(); i++) {
if (isLocalFileUri(mediaItem.playbackProperties.subtitles.get(i).uri)) {
return requestExternalStoragePermission(activity);
}
}
}
return false;
}
/**
* Returns whether it may be possible to load the URIs of the given media items based on the
* network security policy's cleartext traffic permissions.
*
* @param mediaItems A list of {@link MediaItem media items}.
* @return Whether it may be possible to load the URIs of the given media items.
*/
public static boolean checkCleartextTrafficPermitted(MediaItem... mediaItems) {
if (Util.SDK_INT < 24) {
// We assume cleartext traffic is permitted.
return true;
}
for (MediaItem mediaItem : mediaItems) {
if (mediaItem.playbackProperties == null) {
continue;
}
if (isTrafficRestricted(mediaItem.playbackProperties.sourceUri)) {
return false;
}
for (int i = 0; i < mediaItem.playbackProperties.subtitles.size(); i++) {
if (isTrafficRestricted(mediaItem.playbackProperties.subtitles.get(i).uri)) {
return false;
}
}
}
return true;
}
/**
* Returns true if the URI is a path to a local file or a reference to a local file.
*
* @param uri The uri to test.
*/
public static boolean isLocalFileUri(Uri uri) {
String scheme = uri.getScheme();
return TextUtils.isEmpty(scheme) || "file".equals(scheme);
}
/**
* Tests two objects for {@link Object#equals(Object)} equality, handling the case where one or
* both may be null.
*
* @param o1 The first object.
* @param o2 The second object.
* @return {@code o1 == null ? o2 == null : o1.equals(o2)}.
*/
public static boolean areEqual(@Nullable Object o1, @Nullable Object o2) {
return o1 == null ? o2 == null : o1.equals(o2);
}
/**
* Tests whether an {@code items} array contains an object equal to {@code item}, according to
* {@link Object#equals(Object)}.
*
* <p>If {@code item} is null then true is returned if and only if {@code items} contains null.
*
* @param items The array of items to search.
* @param item The item to search for.
* @return True if the array contains an object equal to the item being searched for.
*/
public static boolean contains(@NullableType Object[] items, @Nullable Object item) {
for (Object arrayItem : items) {
if (areEqual(arrayItem, item)) {
return true;
}
}
return false;
}
/**
* Removes an indexed range from a List.
*
* <p>Does nothing if the provided range is valid and {@code fromIndex == toIndex}.
*
* @param list The List to remove the range from.
* @param fromIndex The first index to be removed (inclusive).
* @param toIndex The last index to be removed (exclusive).
* @throws IllegalArgumentException If {@code fromIndex} &lt; 0, {@code toIndex} &gt; {@code
* list.size()}, or {@code fromIndex} &gt; {@code toIndex}.
*/
public static <T> void removeRange(List<T> list, int fromIndex, int toIndex) {
if (fromIndex < 0 || toIndex > list.size() || fromIndex > toIndex) {
throw new IllegalArgumentException();
} else if (fromIndex != toIndex) {
// Checking index inequality prevents an unnecessary allocation.
list.subList(fromIndex, toIndex).clear();
}
}
/**
* Casts a nullable variable to a non-null variable without runtime null check.
*
* <p>Use {@link Assertions#checkNotNull(Object)} to throw if the value is null.
*/
@SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"})
@EnsuresNonNull("#1")
public static <T> T castNonNull(@Nullable T value) {
return value;
}
/** Casts a nullable type array to a non-null type array without runtime null check. */
@SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"})
@EnsuresNonNull("#1")
public static <T> T[] castNonNullTypeArray(@NullableType T[] value) {
return value;
}
/**
* Copies and optionally truncates an array. Prevents null array elements created by {@link
* Arrays#copyOf(Object[], int)} by ensuring the new length does not exceed the current length.
*
* @param input The input array.
* @param length The output array length. Must be less or equal to the length of the input array.
* @return The copied array.
*/
@SuppressWarnings({"nullness:argument.type.incompatible", "nullness:return.type.incompatible"})
public static <T> T[] nullSafeArrayCopy(T[] input, int length) {
Assertions.checkArgument(length <= input.length);
return Arrays.copyOf(input, length);
}
/**
* Copies a subset of an array.
*
* @param input The input array.
* @param from The start the range to be copied, inclusive
* @param to The end of the range to be copied, exclusive.
* @return The copied array.
*/
@SuppressWarnings({"nullness:argument.type.incompatible", "nullness:return.type.incompatible"})
public static <T> T[] nullSafeArrayCopyOfRange(T[] input, int from, int to) {
Assertions.checkArgument(0 <= from);
Assertions.checkArgument(to <= input.length);
return Arrays.copyOfRange(input, from, to);
}
/**
* Creates a new array containing {@code original} with {@code newElement} appended.
*
* @param original The input array.
* @param newElement The element to append.
* @return The new array.
*/
public static <T> T[] nullSafeArrayAppend(T[] original, T newElement) {
@NullableType T[] result = Arrays.copyOf(original, original.length + 1);
result[original.length] = newElement;
return castNonNullTypeArray(result);
}
/**
* Creates a new array containing the concatenation of two non-null type arrays.
*
* @param first The first array.
* @param second The second array.
* @return The concatenated result.
*/
@SuppressWarnings({"nullness:assignment.type.incompatible"})
public static <T> T[] nullSafeArrayConcatenation(T[] first, T[] second) {
T[] concatenation = Arrays.copyOf(first, first.length + second.length);
System.arraycopy(
/* src= */ second,
/* srcPos= */ 0,
/* dest= */ concatenation,
/* destPos= */ first.length,
/* length= */ second.length);
return concatenation;
}
/**
* Creates a {@link Handler} on the current {@link Looper} thread.
*
* <p>If the current thread doesn't have a {@link Looper}, the application's main thread {@link
* Looper} is used.
*/
public static Handler createHandler() {
return createHandler(/* callback= */ null);
}
/**
* Creates a {@link Handler} with the specified {@link Handler.Callback} on the current {@link
* Looper} thread. The method accepts partially initialized objects as callback under the
* assumption that the Handler won't be used to send messages until the callback is fully
* initialized.
*
* <p>If the current thread doesn't have a {@link Looper}, the application's main thread {@link
* Looper} is used.
*
* @param callback A {@link Handler.Callback}. May be a partially initialized class, or null if no
* callback is required.
* @return A {@link Handler} with the specified callback on the current {@link Looper} thread.
*/
public static Handler createHandler(@Nullable Handler.@UnknownInitialization Callback callback) {
return createHandler(getLooper(), callback);
}
/**
* Creates a {@link Handler} with the specified {@link Handler.Callback} on the specified {@link
* Looper} thread. The method accepts partially initialized objects as callback under the
* assumption that the Handler won't be used to send messages until the callback is fully
* initialized.
*
* @param looper A {@link Looper} to run the callback on.
* @param callback A {@link Handler.Callback}. May be a partially initialized class, or null if no
* callback is required.
* @return A {@link Handler} with the specified callback on the current {@link Looper} thread.
*/
@SuppressWarnings({"nullness:argument.type.incompatible", "nullness:return.type.incompatible"})
public static Handler createHandler(
Looper looper, @Nullable Handler.@UnknownInitialization Callback callback) {
return new Handler(looper, callback);
}
/**
* Returns the {@link Looper} associated with the current thread, or the {@link Looper} of the
* application's main thread if the current thread doesn't have a {@link Looper}.
*/
public static Looper getLooper() {
Looper myLooper = Looper.myLooper();
return myLooper != null ? myLooper : Looper.getMainLooper();
}
/**
* Instantiates a new single threaded executor whose thread has the specified name.
*
* @param threadName The name of the thread.
* @return The executor.
*/
public static ExecutorService newSingleThreadExecutor(final String threadName) {
return Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, threadName));
}
/**
* Closes a {@link DataSource}, suppressing any {@link IOException} that may occur.
*
* @param dataSource The {@link DataSource} to close.
*/
public static void closeQuietly(@Nullable DataSource dataSource) {
try {
if (dataSource != null) {
dataSource.close();
}
} catch (IOException e) {
// Ignore.
}
}
/**
* Closes a {@link Closeable}, suppressing any {@link IOException} that may occur. Both {@link
* java.io.OutputStream} and {@link InputStream} are {@code Closeable}.
*
* @param closeable The {@link Closeable} to close.
*/
public static void closeQuietly(@Nullable Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException e) {
// Ignore.
}
}
/**
* Reads an integer from a {@link Parcel} and interprets it as a boolean, with 0 mapping to false
* and all other values mapping to true.
*
* @param parcel The {@link Parcel} to read from.
* @return The read value.
*/
public static boolean readBoolean(Parcel parcel) {
return parcel.readInt() != 0;
}
/**
* Writes a boolean to a {@link Parcel}. The boolean is written as an integer with value 1 (true)
* or 0 (false).
*
* @param parcel The {@link Parcel} to write to.
* @param value The value to write.
*/
public static void writeBoolean(Parcel parcel, boolean value) {
parcel.writeInt(value ? 1 : 0);
}
/**
* Returns the language tag for a {@link Locale}.
*
* <p>For API levels &ge; 21, this tag is IETF BCP 47 compliant. Use {@link
* #normalizeLanguageCode(String)} to retrieve a normalized IETF BCP 47 language tag for all API
* levels if needed.
*
* @param locale A {@link Locale}.
* @return The language tag.
*/
public static String getLocaleLanguageTag(Locale locale) {
return SDK_INT >= 21 ? getLocaleLanguageTagV21(locale) : locale.toString();
}
/**
* Returns a normalized IETF BCP 47 language tag for {@code language}.
*
* @param language A case-insensitive language code supported by {@link
* Locale#forLanguageTag(String)}.
* @return The all-lowercase normalized code, or null if the input was null, or {@code
* language.toLowerCase()} if the language could not be normalized.
*/
public static @PolyNull String normalizeLanguageCode(@PolyNull String language) {
if (language == null) {
return null;
}
// Locale data (especially for API < 21) may produce tags with '_' instead of the
// standard-conformant '-'.
String normalizedTag = language.replace('_', '-');
if (normalizedTag.isEmpty() || "und".equals(normalizedTag)) {
// Tag isn't valid, keep using the original.
normalizedTag = language;
}
normalizedTag = Util.toLowerInvariant(normalizedTag);
String mainLanguage = Util.splitAtFirst(normalizedTag, "-")[0];
if (languageTagReplacementMap == null) {
languageTagReplacementMap = createIsoLanguageReplacementMap();
}
@Nullable String replacedLanguage = languageTagReplacementMap.get(mainLanguage);
if (replacedLanguage != null) {
normalizedTag =
replacedLanguage + normalizedTag.substring(/* beginIndex= */ mainLanguage.length());
mainLanguage = replacedLanguage;
}
if ("no".equals(mainLanguage) || "i".equals(mainLanguage) || "zh".equals(mainLanguage)) {
normalizedTag = maybeReplaceGrandfatheredLanguageTags(normalizedTag);
}
return normalizedTag;
}
/**
* Returns a new {@link String} constructed by decoding UTF-8 encoded bytes.
*
* @param bytes The UTF-8 encoded bytes to decode.
* @return The string.
*/
public static String fromUtf8Bytes(byte[] bytes) {
return new String(bytes, Charset.forName(C.UTF8_NAME));
}
/**
* Returns a new {@link String} constructed by decoding UTF-8 encoded bytes in a subarray.
*
* @param bytes The UTF-8 encoded bytes to decode.
* @param offset The index of the first byte to decode.
* @param length The number of bytes to decode.
* @return The string.
*/
public static String fromUtf8Bytes(byte[] bytes, int offset, int length) {
return new String(bytes, offset, length, Charset.forName(C.UTF8_NAME));
}
/**
* Returns a new byte array containing the code points of a {@link String} encoded using UTF-8.
*
* @param value The {@link String} whose bytes should be obtained.
* @return The code points encoding using UTF-8.
*/
public static byte[] getUtf8Bytes(String value) {
return value.getBytes(Charset.forName(C.UTF8_NAME));
}
/**
* Splits a string using {@code value.split(regex, -1}). Note: this is is similar to {@link
* String#split(String)} but empty matches at the end of the string will not be omitted from the
* returned array.
*
* @param value The string to split.
* @param regex A delimiting regular expression.
* @return The array of strings resulting from splitting the string.
*/
public static String[] split(String value, String regex) {
return value.split(regex, /* limit= */ -1);
}
/**
* Splits the string at the first occurrence of the delimiter {@code regex}. If the delimiter does
* not match, returns an array with one element which is the input string. If the delimiter does
* match, returns an array with the portion of the string before the delimiter and the rest of the
* string.
*
* @param value The string.
* @param regex A delimiting regular expression.
* @return The string split by the first occurrence of the delimiter.
*/
public static String[] splitAtFirst(String value, String regex) {
return value.split(regex, /* limit= */ 2);
}
/**
* Returns whether the given character is a carriage return ('\r') or a line feed ('\n').
*
* @param c The character.
* @return Whether the given character is a linebreak.
*/
public static boolean isLinebreak(int c) {
return c == '\n' || c == '\r';
}
/**
* Converts text to lower case using {@link Locale#US}.
*
* @param text The text to convert.
* @return The lower case text, or null if {@code text} is null.
*/
public static @PolyNull String toLowerInvariant(@PolyNull String text) {
return text == null ? text : text.toLowerCase(Locale.US);
}
/**
* Converts text to upper case using {@link Locale#US}.
*
* @param text The text to convert.
* @return The upper case text, or null if {@code text} is null.
*/
public static @PolyNull String toUpperInvariant(@PolyNull String text) {
return text == null ? text : text.toUpperCase(Locale.US);
}
/**
* Formats a string using {@link Locale#US}.
*
* @see String#format(String, Object...)
*/
public static String formatInvariant(String format, Object... args) {
return String.format(Locale.US, format, args);
}
/**
* Divides a {@code numerator} by a {@code denominator}, returning the ceiled result.
*
* @param numerator The numerator to divide.
* @param denominator The denominator to divide by.
* @return The ceiled result of the division.
*/
public static int ceilDivide(int numerator, int denominator) {
return (numerator + denominator - 1) / denominator;
}
/**
* Divides a {@code numerator} by a {@code denominator}, returning the ceiled result.
*
* @param numerator The numerator to divide.
* @param denominator The denominator to divide by.
* @return The ceiled result of the division.
*/
public static long ceilDivide(long numerator, long denominator) {
return (numerator + denominator - 1) / denominator;
}
/**
* Constrains a value to the specified bounds.
*
* @param value The value to constrain.
* @param min The lower bound.
* @param max The upper bound.
* @return The constrained value {@code Math.max(min, Math.min(value, max))}.
*/
public static int constrainValue(int value, int min, int max) {
return Math.max(min, Math.min(value, max));
}
/**
* Constrains a value to the specified bounds.
*
* @param value The value to constrain.
* @param min The lower bound.
* @param max The upper bound.
* @return The constrained value {@code Math.max(min, Math.min(value, max))}.
*/
public static long constrainValue(long value, long min, long max) {
return Math.max(min, Math.min(value, max));
}
/**
* Constrains a value to the specified bounds.
*
* @param value The value to constrain.
* @param min The lower bound.
* @param max The upper bound.
* @return The constrained value {@code Math.max(min, Math.min(value, max))}.
*/
public static float constrainValue(float value, float min, float max) {
return Math.max(min, Math.min(value, max));
}
/**
* Returns the sum of two arguments, or a third argument if the result overflows.
*
* @param x The first value.
* @param y The second value.
* @param overflowResult The return value if {@code x + y} overflows.
* @return {@code x + y}, or {@code overflowResult} if the result overflows.
*/
public static long addWithOverflowDefault(long x, long y, long overflowResult) {
long result = x + y;
// See Hacker's Delight 2-13 (H. Warren Jr).
if (((x ^ result) & (y ^ result)) < 0) {
return overflowResult;
}
return result;
}
/**
* Returns the difference between two arguments, or a third argument if the result overflows.
*
* @param x The first value.
* @param y The second value.
* @param overflowResult The return value if {@code x - y} overflows.
* @return {@code x - y}, or {@code overflowResult} if the result overflows.
*/
public static long subtractWithOverflowDefault(long x, long y, long overflowResult) {
long result = x - y;
// See Hacker's Delight 2-13 (H. Warren Jr).
if (((x ^ y) & (x ^ result)) < 0) {
return overflowResult;
}
return result;
}
/**
* Returns the index of the first occurrence of {@code value} in {@code array}, or {@link
* C#INDEX_UNSET} if {@code value} is not contained in {@code array}.
*
* @param array The array to search.
* @param value The value to search for.
* @return The index of the first occurrence of value in {@code array}, or {@link C#INDEX_UNSET}
* if {@code value} is not contained in {@code array}.
*/
public static int linearSearch(int[] array, int value) {
for (int i = 0; i < array.length; i++) {
if (array[i] == value) {
return i;
}
}
return C.INDEX_UNSET;
}
/**
* Returns the index of the largest element in {@code array} that is less than (or optionally
* equal to) a specified {@code value}.
*
* <p>The search is performed using a binary search algorithm, so the array must be sorted. If the
* array contains multiple elements equal to {@code value} and {@code inclusive} is true, the
* index of the first one will be returned.
*
* @param array The array to search.
* @param value The value being searched for.
* @param inclusive If the value is present in the array, whether to return the corresponding
* index. If false then the returned index corresponds to the largest element strictly less
* than the value.
* @param stayInBounds If true, then 0 will be returned in the case that the value is smaller than
* the smallest element in the array. If false then -1 will be returned.
* @return The index of the largest element in {@code array} that is less than (or optionally
* equal to) {@code value}.
*/
public static int binarySearchFloor(
int[] array, int value, boolean inclusive, boolean stayInBounds) {
int index = Arrays.binarySearch(array, value);
if (index < 0) {
index = -(index + 2);
} else {
while (--index >= 0 && array[index] == value) {}
if (inclusive) {
index++;
}
}
return stayInBounds ? Math.max(0, index) : index;
}
/**
* Returns the index of the largest element in {@code array} that is less than (or optionally
* equal to) a specified {@code value}.
* <p>
* The search is performed using a binary search algorithm, so the array must be sorted. If the
* array contains multiple elements equal to {@code value} and {@code inclusive} is true, the
* index of the first one will be returned.
*
* @param array The array to search.
* @param value The value being searched for.
* @param inclusive If the value is present in the array, whether to return the corresponding
* index. If false then the returned index corresponds to the largest element strictly less
* than the value.
* @param stayInBounds If true, then 0 will be returned in the case that the value is smaller than
* the smallest element in the array. If false then -1 will be returned.
* @return The index of the largest element in {@code array} that is less than (or optionally
* equal to) {@code value}.
*/
public static int binarySearchFloor(long[] array, long value, boolean inclusive,
boolean stayInBounds) {
int index = Arrays.binarySearch(array, value);
if (index < 0) {
index = -(index + 2);
} else {
while (--index >= 0 && array[index] == value) {}
if (inclusive) {
index++;
}
}
return stayInBounds ? Math.max(0, index) : index;
}
/**
* Returns the index of the largest element in {@code list} that is less than (or optionally equal
* to) a specified {@code value}.
*
* <p>The search is performed using a binary search algorithm, so the list must be sorted. If the
* list contains multiple elements equal to {@code value} and {@code inclusive} is true, the index
* of the first one will be returned.
*
* @param <T> The type of values being searched.
* @param list The list to search.
* @param value The value being searched for.
* @param inclusive If the value is present in the list, whether to return the corresponding
* index. If false then the returned index corresponds to the largest element strictly less
* than the value.
* @param stayInBounds If true, then 0 will be returned in the case that the value is smaller than
* the smallest element in the list. If false then -1 will be returned.
* @return The index of the largest element in {@code list} that is less than (or optionally equal
* to) {@code value}.
*/
public static <T extends Comparable<? super T>> int binarySearchFloor(
List<? extends Comparable<? super T>> list,
T value,
boolean inclusive,
boolean stayInBounds) {
int index = Collections.binarySearch(list, value);
if (index < 0) {
index = -(index + 2);
} else {
while (--index >= 0 && list.get(index).compareTo(value) == 0) {}
if (inclusive) {
index++;
}
}
return stayInBounds ? Math.max(0, index) : index;
}
/**
* Returns the index of the largest element in {@code longArray} that is less than (or optionally
* equal to) a specified {@code value}.
*
* <p>The search is performed using a binary search algorithm, so the array must be sorted. If the
* array contains multiple elements equal to {@code value} and {@code inclusive} is true, the
* index of the first one will be returned.
*
* @param longArray The array to search.
* @param value The value being searched for.
* @param inclusive If the value is present in the array, whether to return the corresponding
* index. If false then the returned index corresponds to the largest element strictly less
* than the value.
* @param stayInBounds If true, then 0 will be returned in the case that the value is smaller than
* the smallest element in the array. If false then -1 will be returned.
* @return The index of the largest element in {@code array} that is less than (or optionally
* equal to) {@code value}.
*/
public static int binarySearchFloor(
LongArray longArray, long value, boolean inclusive, boolean stayInBounds) {
int lowIndex = 0;
int highIndex = longArray.size() - 1;
while (lowIndex <= highIndex) {
int midIndex = (lowIndex + highIndex) >>> 1;
if (longArray.get(midIndex) < value) {
lowIndex = midIndex + 1;
} else {
highIndex = midIndex - 1;
}
}
if (inclusive && highIndex + 1 < longArray.size() && longArray.get(highIndex + 1) == value) {
highIndex++;
} else if (stayInBounds && highIndex == -1) {
highIndex = 0;
}
return highIndex;
}
/**
* Returns the index of the smallest element in {@code array} that is greater than (or optionally
* equal to) a specified {@code value}.
*
* <p>The search is performed using a binary search algorithm, so the array must be sorted. If the
* array contains multiple elements equal to {@code value} and {@code inclusive} is true, the
* index of the last one will be returned.
*
* @param array The array to search.
* @param value The value being searched for.
* @param inclusive If the value is present in the array, whether to return the corresponding
* index. If false then the returned index corresponds to the smallest element strictly
* greater than the value.
* @param stayInBounds If true, then {@code (a.length - 1)} will be returned in the case that the
* value is greater than the largest element in the array. If false then {@code a.length} will
* be returned.
* @return The index of the smallest element in {@code array} that is greater than (or optionally
* equal to) {@code value}.
*/
public static int binarySearchCeil(
int[] array, int value, boolean inclusive, boolean stayInBounds) {
int index = Arrays.binarySearch(array, value);
if (index < 0) {
index = ~index;
} else {
while (++index < array.length && array[index] == value) {}
if (inclusive) {
index--;
}
}
return stayInBounds ? Math.min(array.length - 1, index) : index;
}
/**
* Returns the index of the smallest element in {@code array} that is greater than (or optionally
* equal to) a specified {@code value}.
*
* <p>The search is performed using a binary search algorithm, so the array must be sorted. If the
* array contains multiple elements equal to {@code value} and {@code inclusive} is true, the
* index of the last one will be returned.
*
* @param array The array to search.
* @param value The value being searched for.
* @param inclusive If the value is present in the array, whether to return the corresponding
* index. If false then the returned index corresponds to the smallest element strictly
* greater than the value.
* @param stayInBounds If true, then {@code (a.length - 1)} will be returned in the case that the
* value is greater than the largest element in the array. If false then {@code a.length} will
* be returned.
* @return The index of the smallest element in {@code array} that is greater than (or optionally
* equal to) {@code value}.
*/
public static int binarySearchCeil(
long[] array, long value, boolean inclusive, boolean stayInBounds) {
int index = Arrays.binarySearch(array, value);
if (index < 0) {
index = ~index;
} else {
while (++index < array.length && array[index] == value) {}
if (inclusive) {
index--;
}
}
return stayInBounds ? Math.min(array.length - 1, index) : index;
}
/**
* Returns the index of the smallest element in {@code list} that is greater than (or optionally
* equal to) a specified value.
*
* <p>The search is performed using a binary search algorithm, so the list must be sorted. If the
* list contains multiple elements equal to {@code value} and {@code inclusive} is true, the index
* of the last one will be returned.
*
* @param <T> The type of values being searched.
* @param list The list to search.
* @param value The value being searched for.
* @param inclusive If the value is present in the list, whether to return the corresponding
* index. If false then the returned index corresponds to the smallest element strictly
* greater than the value.
* @param stayInBounds If true, then {@code (list.size() - 1)} will be returned in the case that
* the value is greater than the largest element in the list. If false then {@code
* list.size()} will be returned.
* @return The index of the smallest element in {@code list} that is greater than (or optionally
* equal to) {@code value}.
*/
public static <T extends Comparable<? super T>> int binarySearchCeil(
List<? extends Comparable<? super T>> list,
T value,
boolean inclusive,
boolean stayInBounds) {
int index = Collections.binarySearch(list, value);
if (index < 0) {
index = ~index;
} else {
int listSize = list.size();
while (++index < listSize && list.get(index).compareTo(value) == 0) {}
if (inclusive) {
index--;
}
}
return stayInBounds ? Math.min(list.size() - 1, index) : index;
}
/**
* Compares two long values and returns the same value as {@code Long.compare(long, long)}.
*
* @param left The left operand.
* @param right The right operand.
* @return 0, if left == right, a negative value if left &lt; right, or a positive value if left
* &gt; right.
*/
public static int compareLong(long left, long right) {
return left < right ? -1 : left == right ? 0 : 1;
}
/**
* Parses an xs:duration attribute value, returning the parsed duration in milliseconds.
*
* @param value The attribute value to decode.
* @return The parsed duration in milliseconds.
*/
public static long parseXsDuration(String value) {
Matcher matcher = XS_DURATION_PATTERN.matcher(value);
if (matcher.matches()) {
boolean negated = !TextUtils.isEmpty(matcher.group(1));
// Durations containing years and months aren't completely defined. We assume there are
// 30.4368 days in a month, and 365.242 days in a year.
String years = matcher.group(3);
double durationSeconds = (years != null) ? Double.parseDouble(years) * 31556908 : 0;
String months = matcher.group(5);
durationSeconds += (months != null) ? Double.parseDouble(months) * 2629739 : 0;
String days = matcher.group(7);
durationSeconds += (days != null) ? Double.parseDouble(days) * 86400 : 0;
String hours = matcher.group(10);
durationSeconds += (hours != null) ? Double.parseDouble(hours) * 3600 : 0;
String minutes = matcher.group(12);
durationSeconds += (minutes != null) ? Double.parseDouble(minutes) * 60 : 0;
String seconds = matcher.group(14);
durationSeconds += (seconds != null) ? Double.parseDouble(seconds) : 0;
long durationMillis = (long) (durationSeconds * 1000);
return negated ? -durationMillis : durationMillis;
} else {
return (long) (Double.parseDouble(value) * 3600 * 1000);
}
}
/**
* Parses an xs:dateTime attribute value, returning the parsed timestamp in milliseconds since the
* epoch.
*
* @param value The attribute value to decode.
* @return The parsed timestamp in milliseconds since the epoch.
* @throws ParserException if an error occurs parsing the dateTime attribute value.
*/
// incompatible types in argument.
// dereference of possibly-null reference matcher.group(9)
@SuppressWarnings({"nullness:argument.type.incompatible", "nullness:dereference.of.nullable"})
public static long parseXsDateTime(String value) throws ParserException {
Matcher matcher = XS_DATE_TIME_PATTERN.matcher(value);
if (!matcher.matches()) {
throw new ParserException("Invalid date/time format: " + value);
}
int timezoneShift;
if (matcher.group(9) == null) {
// No time zone specified.
timezoneShift = 0;
} else if (matcher.group(9).equalsIgnoreCase("Z")) {
timezoneShift = 0;
} else {
timezoneShift = ((Integer.parseInt(matcher.group(12)) * 60
+ Integer.parseInt(matcher.group(13))));
if ("-".equals(matcher.group(11))) {
timezoneShift *= -1;
}
}
Calendar dateTime = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
dateTime.clear();
// Note: The month value is 0-based, hence the -1 on group(2)
dateTime.set(Integer.parseInt(matcher.group(1)),
Integer.parseInt(matcher.group(2)) - 1,
Integer.parseInt(matcher.group(3)),
Integer.parseInt(matcher.group(4)),
Integer.parseInt(matcher.group(5)),
Integer.parseInt(matcher.group(6)));
if (!TextUtils.isEmpty(matcher.group(8))) {
final BigDecimal bd = new BigDecimal("0." + matcher.group(8));
// we care only for milliseconds, so movePointRight(3)
dateTime.set(Calendar.MILLISECOND, bd.movePointRight(3).intValue());
}
long time = dateTime.getTimeInMillis();
if (timezoneShift != 0) {
time -= timezoneShift * 60000;
}
return time;
}
/**
* Scales a large timestamp.
* <p>
* Logically, scaling consists of a multiplication followed by a division. The actual operations
* performed are designed to minimize the probability of overflow.
*
* @param timestamp The timestamp to scale.
* @param multiplier The multiplier.
* @param divisor The divisor.
* @return The scaled timestamp.
*/
public static long scaleLargeTimestamp(long timestamp, long multiplier, long divisor) {
if (divisor >= multiplier && (divisor % multiplier) == 0) {
long divisionFactor = divisor / multiplier;
return timestamp / divisionFactor;
} else if (divisor < multiplier && (multiplier % divisor) == 0) {
long multiplicationFactor = multiplier / divisor;
return timestamp * multiplicationFactor;
} else {
double multiplicationFactor = (double) multiplier / divisor;
return (long) (timestamp * multiplicationFactor);
}
}
/**
* Applies {@link #scaleLargeTimestamp(long, long, long)} to a list of unscaled timestamps.
*
* @param timestamps The timestamps to scale.
* @param multiplier The multiplier.
* @param divisor The divisor.
* @return The scaled timestamps.
*/
public static long[] scaleLargeTimestamps(List<Long> timestamps, long multiplier, long divisor) {
long[] scaledTimestamps = new long[timestamps.size()];
if (divisor >= multiplier && (divisor % multiplier) == 0) {
long divisionFactor = divisor / multiplier;
for (int i = 0; i < scaledTimestamps.length; i++) {
scaledTimestamps[i] = timestamps.get(i) / divisionFactor;
}
} else if (divisor < multiplier && (multiplier % divisor) == 0) {
long multiplicationFactor = multiplier / divisor;
for (int i = 0; i < scaledTimestamps.length; i++) {
scaledTimestamps[i] = timestamps.get(i) * multiplicationFactor;
}
} else {
double multiplicationFactor = (double) multiplier / divisor;
for (int i = 0; i < scaledTimestamps.length; i++) {
scaledTimestamps[i] = (long) (timestamps.get(i) * multiplicationFactor);
}
}
return scaledTimestamps;
}
/**
* Applies {@link #scaleLargeTimestamp(long, long, long)} to an array of unscaled timestamps.
*
* @param timestamps The timestamps to scale.
* @param multiplier The multiplier.
* @param divisor The divisor.
*/
public static void scaleLargeTimestampsInPlace(long[] timestamps, long multiplier, long divisor) {
if (divisor >= multiplier && (divisor % multiplier) == 0) {
long divisionFactor = divisor / multiplier;
for (int i = 0; i < timestamps.length; i++) {
timestamps[i] /= divisionFactor;
}
} else if (divisor < multiplier && (multiplier % divisor) == 0) {
long multiplicationFactor = multiplier / divisor;
for (int i = 0; i < timestamps.length; i++) {
timestamps[i] *= multiplicationFactor;
}
} else {
double multiplicationFactor = (double) multiplier / divisor;
for (int i = 0; i < timestamps.length; i++) {
timestamps[i] = (long) (timestamps[i] * multiplicationFactor);
}
}
}
/**
* Returns the duration of media that will elapse in {@code playoutDuration}.
*
* @param playoutDuration The duration to scale.
* @param speed The playback speed.
* @return The scaled duration, in the same units as {@code playoutDuration}.
*/
public static long getMediaDurationForPlayoutDuration(long playoutDuration, float speed) {
if (speed == 1f) {
return playoutDuration;
}
return Math.round((double) playoutDuration * speed);
}
/**
* Returns the playout duration of {@code mediaDuration} of media.
*
* @param mediaDuration The duration to scale.
* @return The scaled duration, in the same units as {@code mediaDuration}.
*/
public static long getPlayoutDurationForMediaDuration(long mediaDuration, float speed) {
if (speed == 1f) {
return mediaDuration;
}
return Math.round((double) mediaDuration / speed);
}
/**
* Converts a list of integers to a primitive array.
*
* @param list A list of integers.
* @return The list in array form, or null if the input list was null.
*/
public static int @PolyNull [] toArray(@PolyNull List<Integer> list) {
if (list == null) {
return null;
}
int length = list.size();
int[] intArray = new int[length];
for (int i = 0; i < length; i++) {
intArray[i] = list.get(i);
}
return intArray;
}
/**
* Converts an array of primitive ints to a list of integers.
*
* @param ints The ints.
* @return The input array in list form.
*/
public static List<Integer> toList(int... ints) {
if (ints == null) {
return new ArrayList<>();
}
List<Integer> integers = new ArrayList<>();
for (int anInt : ints) {
integers.add(anInt);
}
return integers;
}
/**
* Returns the integer equal to the big-endian concatenation of the characters in {@code string}
* as bytes. The string must be no more than four characters long.
*
* @param string A string no more than four characters long.
*/
public static int getIntegerCodeForString(String string) {
int length = string.length();
Assertions.checkArgument(length <= 4);
int result = 0;
for (int i = 0; i < length; i++) {
result <<= 8;
result |= string.charAt(i);
}
return result;
}
/**
* Converts an integer to a long by unsigned conversion.
*
* <p>This method is equivalent to {@link Integer#toUnsignedLong(int)} for API 26+.
*/
public static long toUnsignedLong(int x) {
// x is implicitly casted to a long before the bit operation is executed but this does not
// impact the method correctness.
return x & 0xFFFFFFFFL;
}
/**
* Return the long that is composed of the bits of the 2 specified integers.
*
* @param mostSignificantBits The 32 most significant bits of the long to return.
* @param leastSignificantBits The 32 least significant bits of the long to return.
* @return a long where its 32 most significant bits are {@code mostSignificantBits} bits and its
* 32 least significant bits are {@code leastSignificantBits}.
*/
public static long toLong(int mostSignificantBits, int leastSignificantBits) {
return (toUnsignedLong(mostSignificantBits) << 32) | toUnsignedLong(leastSignificantBits);
}
/**
* Returns a byte array containing values parsed from the hex string provided.
*
* @param hexString The hex string to convert to bytes.
* @return A byte array containing values parsed from the hex string provided.
*/
public static byte[] getBytesFromHexString(String hexString) {
byte[] data = new byte[hexString.length() / 2];
for (int i = 0; i < data.length; i++) {
int stringOffset = i * 2;
data[i] = (byte) ((Character.digit(hexString.charAt(stringOffset), 16) << 4)
+ Character.digit(hexString.charAt(stringOffset + 1), 16));
}
return data;
}
/**
* Returns a string containing a lower-case hex representation of the bytes provided.
*
* @param bytes The byte data to convert to hex.
* @return A String containing the hex representation of {@code bytes}.
*/
public static String toHexString(byte[] bytes) {
StringBuilder result = new StringBuilder(bytes.length * 2);
for (int i = 0; i < bytes.length; i++) {
result
.append(Character.forDigit((bytes[i] >> 4) & 0xF, 16))
.append(Character.forDigit(bytes[i] & 0xF, 16));
}
return result.toString();
}
/**
* Returns a string with comma delimited simple names of each object's class.
*
* @param objects The objects whose simple class names should be comma delimited and returned.
* @return A string with comma delimited simple names of each object's class.
*/
public static String getCommaDelimitedSimpleClassNames(Object[] objects) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < objects.length; i++) {
stringBuilder.append(objects[i].getClass().getSimpleName());
if (i < objects.length - 1) {
stringBuilder.append(", ");
}
}
return stringBuilder.toString();
}
/**
* Returns a user agent string based on the given application name and the library version.
*
* @param context A valid context of the calling application.
* @param applicationName String that will be prefix'ed to the generated user agent.
* @return A user agent string generated using the applicationName and the library version.
*/
public static String getUserAgent(Context context, String applicationName) {
String versionName;
try {
String packageName = context.getPackageName();
PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
versionName = info.versionName;
} catch (NameNotFoundException e) {
versionName = "?";
}
return applicationName + "/" + versionName + " (Linux;Android " + Build.VERSION.RELEASE
+ ") " + ExoPlayerLibraryInfo.VERSION_SLASHY;
}
/**
* Returns a copy of {@code codecs} without the codecs whose track type doesn't match {@code
* trackType}.
*
* @param codecs A codec sequence string, as defined in RFC 6381.
* @param trackType One of {@link C}{@code .TRACK_TYPE_*}.
* @return A copy of {@code codecs} without the codecs whose track type doesn't match {@code
* trackType}. If this ends up empty, or {@code codecs} is null, return null.
*/
public static @Nullable String getCodecsOfType(@Nullable String codecs, int trackType) {
String[] codecArray = splitCodecs(codecs);
if (codecArray.length == 0) {
return null;
}
StringBuilder builder = new StringBuilder();
for (String codec : codecArray) {
if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) {
if (builder.length() > 0) {
builder.append(",");
}
builder.append(codec);
}
}
return builder.length() > 0 ? builder.toString() : null;
}
/**
* Splits a codecs sequence string, as defined in RFC 6381, into individual codec strings.
*
* @param codecs A codec sequence string, as defined in RFC 6381.
* @return The split codecs, or an array of length zero if the input was empty or null.
*/
public static String[] splitCodecs(@Nullable String codecs) {
if (TextUtils.isEmpty(codecs)) {
return new String[0];
}
return split(codecs.trim(), "(\\s*,\\s*)");
}
/**
* Converts a sample bit depth to a corresponding PCM encoding constant.
*
* @param bitDepth The bit depth. Supported values are 8, 16, 24 and 32.
* @return The corresponding encoding. One of {@link C#ENCODING_PCM_8BIT},
* {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and
* {@link C#ENCODING_PCM_32BIT}. If the bit depth is unsupported then
* {@link C#ENCODING_INVALID} is returned.
*/
@C.PcmEncoding
public static int getPcmEncoding(int bitDepth) {
switch (bitDepth) {
case 8:
return C.ENCODING_PCM_8BIT;
case 16:
return C.ENCODING_PCM_16BIT;
case 24:
return C.ENCODING_PCM_24BIT;
case 32:
return C.ENCODING_PCM_32BIT;
default:
return C.ENCODING_INVALID;
}
}
/**
* Returns whether {@code encoding} is one of the linear PCM encodings.
*
* @param encoding The encoding of the audio data.
* @return Whether the encoding is one of the PCM encodings.
*/
public static boolean isEncodingLinearPcm(@C.Encoding int encoding) {
return encoding == C.ENCODING_PCM_8BIT
|| encoding == C.ENCODING_PCM_16BIT
|| encoding == C.ENCODING_PCM_16BIT_BIG_ENDIAN
|| encoding == C.ENCODING_PCM_24BIT
|| encoding == C.ENCODING_PCM_32BIT
|| encoding == C.ENCODING_PCM_FLOAT;
}
/**
* Returns whether {@code encoding} is high resolution (&gt; 16-bit) PCM.
*
* @param encoding The encoding of the audio data.
* @return Whether the encoding is high resolution PCM.
*/
public static boolean isEncodingHighResolutionPcm(@C.PcmEncoding int encoding) {
return encoding == C.ENCODING_PCM_24BIT
|| encoding == C.ENCODING_PCM_32BIT
|| encoding == C.ENCODING_PCM_FLOAT;
}
/**
* Returns the audio track channel configuration for the given channel count, or {@link
* AudioFormat#CHANNEL_INVALID} if output is not poossible.
*
* @param channelCount The number of channels in the input audio.
* @return The channel configuration or {@link AudioFormat#CHANNEL_INVALID} if output is not
* possible.
*/
public static int getAudioTrackChannelConfig(int channelCount) {
switch (channelCount) {
case 1:
return AudioFormat.CHANNEL_OUT_MONO;
case 2:
return AudioFormat.CHANNEL_OUT_STEREO;
case 3:
return AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
case 4:
return AudioFormat.CHANNEL_OUT_QUAD;
case 5:
return AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
case 6:
return AudioFormat.CHANNEL_OUT_5POINT1;
case 7:
return AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
case 8:
if (Util.SDK_INT >= 23) {
return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
} else if (Util.SDK_INT >= 21) {
// Equal to AudioFormat.CHANNEL_OUT_7POINT1_SURROUND, which is hidden before Android M.
return AudioFormat.CHANNEL_OUT_5POINT1
| AudioFormat.CHANNEL_OUT_SIDE_LEFT
| AudioFormat.CHANNEL_OUT_SIDE_RIGHT;
} else {
// 8 ch output is not supported before Android L.
return AudioFormat.CHANNEL_INVALID;
}
default:
return AudioFormat.CHANNEL_INVALID;
}
}
/**
* Returns the frame size for audio with {@code channelCount} channels in the specified encoding.
*
* @param pcmEncoding The encoding of the audio data.
* @param channelCount The channel count.
* @return The size of one audio frame in bytes.
*/
public static int getPcmFrameSize(@C.PcmEncoding int pcmEncoding, int channelCount) {
switch (pcmEncoding) {
case C.ENCODING_PCM_8BIT:
return channelCount;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
return channelCount * 2;
case C.ENCODING_PCM_24BIT:
return channelCount * 3;
case C.ENCODING_PCM_32BIT:
case C.ENCODING_PCM_FLOAT:
return channelCount * 4;
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
throw new IllegalArgumentException();
}
}
/**
* Returns the {@link C.AudioUsage} corresponding to the specified {@link C.StreamType}.
*/
@C.AudioUsage
public static int getAudioUsageForStreamType(@C.StreamType int streamType) {
switch (streamType) {
case C.STREAM_TYPE_ALARM:
return C.USAGE_ALARM;
case C.STREAM_TYPE_DTMF:
return C.USAGE_VOICE_COMMUNICATION_SIGNALLING;
case C.STREAM_TYPE_NOTIFICATION:
return C.USAGE_NOTIFICATION;
case C.STREAM_TYPE_RING:
return C.USAGE_NOTIFICATION_RINGTONE;
case C.STREAM_TYPE_SYSTEM:
return C.USAGE_ASSISTANCE_SONIFICATION;
case C.STREAM_TYPE_VOICE_CALL:
return C.USAGE_VOICE_COMMUNICATION;
case C.STREAM_TYPE_USE_DEFAULT:
case C.STREAM_TYPE_MUSIC:
default:
return C.USAGE_MEDIA;
}
}
/**
* Returns the {@link C.AudioContentType} corresponding to the specified {@link C.StreamType}.
*/
@C.AudioContentType
public static int getAudioContentTypeForStreamType(@C.StreamType int streamType) {
switch (streamType) {
case C.STREAM_TYPE_ALARM:
case C.STREAM_TYPE_DTMF:
case C.STREAM_TYPE_NOTIFICATION:
case C.STREAM_TYPE_RING:
case C.STREAM_TYPE_SYSTEM:
return C.CONTENT_TYPE_SONIFICATION;
case C.STREAM_TYPE_VOICE_CALL:
return C.CONTENT_TYPE_SPEECH;
case C.STREAM_TYPE_USE_DEFAULT:
case C.STREAM_TYPE_MUSIC:
default:
return C.CONTENT_TYPE_MUSIC;
}
}
/**
* Returns the {@link C.StreamType} corresponding to the specified {@link C.AudioUsage}.
*/
@C.StreamType
public static int getStreamTypeForAudioUsage(@C.AudioUsage int usage) {
switch (usage) {
case C.USAGE_MEDIA:
case C.USAGE_GAME:
case C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:
return C.STREAM_TYPE_MUSIC;
case C.USAGE_ASSISTANCE_SONIFICATION:
return C.STREAM_TYPE_SYSTEM;
case C.USAGE_VOICE_COMMUNICATION:
return C.STREAM_TYPE_VOICE_CALL;
case C.USAGE_VOICE_COMMUNICATION_SIGNALLING:
return C.STREAM_TYPE_DTMF;
case C.USAGE_ALARM:
return C.STREAM_TYPE_ALARM;
case C.USAGE_NOTIFICATION_RINGTONE:
return C.STREAM_TYPE_RING;
case C.USAGE_NOTIFICATION:
case C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST:
case C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT:
case C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED:
case C.USAGE_NOTIFICATION_EVENT:
return C.STREAM_TYPE_NOTIFICATION;
case C.USAGE_ASSISTANCE_ACCESSIBILITY:
case C.USAGE_ASSISTANT:
case C.USAGE_UNKNOWN:
default:
return C.STREAM_TYPE_DEFAULT;
}
}
/**
* Derives a DRM {@link UUID} from {@code drmScheme}.
*
* @param drmScheme A UUID string, or {@code "widevine"}, {@code "playready"} or {@code
* "clearkey"}.
* @return The derived {@link UUID}, or {@code null} if one could not be derived.
*/
public static @Nullable UUID getDrmUuid(String drmScheme) {
switch (toLowerInvariant(drmScheme)) {
case "widevine":
return C.WIDEVINE_UUID;
case "playready":
return C.PLAYREADY_UUID;
case "clearkey":
return C.CLEARKEY_UUID;
default:
try {
return UUID.fromString(drmScheme);
} catch (RuntimeException e) {
return null;
}
}
}
/**
* Makes a best guess to infer the type from a {@link Uri}.
*
* @param uri The {@link Uri}.
* @param overrideExtension If not null, used to infer the type.
* @return The content type.
*/
@C.ContentType
public static int inferContentType(Uri uri, @Nullable String overrideExtension) {
return TextUtils.isEmpty(overrideExtension)
? inferContentType(uri)
: inferContentType("." + overrideExtension);
}
/**
* Makes a best guess to infer the type from a {@link Uri}.
*
* @param uri The {@link Uri}.
* @return The content type.
*/
@C.ContentType
public static int inferContentType(Uri uri) {
String path = uri.getPath();
return path == null ? C.TYPE_OTHER : inferContentType(path);
}
/**
* Makes a best guess to infer the type from a file name.
*
* @param fileName Name of the file. It can include the path of the file.
* @return The content type.
*/
@C.ContentType
public static int inferContentType(String fileName) {
fileName = toLowerInvariant(fileName);
if (fileName.endsWith(".mpd")) {
return C.TYPE_DASH;
} else if (fileName.endsWith(".m3u8")) {
return C.TYPE_HLS;
} else if (fileName.matches(".*\\.ism(l)?(/manifest(\\(.+\\))?)?")) {
return C.TYPE_SS;
} else {
return C.TYPE_OTHER;
}
}
/**
* Returns the specified millisecond time formatted as a string.
*
* @param builder The builder that {@code formatter} will write to.
* @param formatter The formatter.
* @param timeMs The time to format as a string, in milliseconds.
* @return The time formatted as a string.
*/
public static String getStringForTime(StringBuilder builder, Formatter formatter, long timeMs) {
if (timeMs == C.TIME_UNSET) {
timeMs = 0;
}
long totalSeconds = (timeMs + 500) / 1000;
long seconds = totalSeconds % 60;
long minutes = (totalSeconds / 60) % 60;
long hours = totalSeconds / 3600;
builder.setLength(0);
return hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString()
: formatter.format("%02d:%02d", minutes, seconds).toString();
}
/**
* Escapes a string so that it's safe for use as a file or directory name on at least FAT32
* filesystems. FAT32 is the most restrictive of all filesystems still commonly used today.
*
* <p>For simplicity, this only handles common characters known to be illegal on FAT32:
* &lt;, &gt;, :, ", /, \, |, ?, and *. % is also escaped since it is used as the escape
* character. Escaping is performed in a consistent way so that no collisions occur and
* {@link #unescapeFileName(String)} can be used to retrieve the original file name.
*
* @param fileName File name to be escaped.
* @return An escaped file name which will be safe for use on at least FAT32 filesystems.
*/
public static String escapeFileName(String fileName) {
int length = fileName.length();
int charactersToEscapeCount = 0;
for (int i = 0; i < length; i++) {
if (shouldEscapeCharacter(fileName.charAt(i))) {
charactersToEscapeCount++;
}
}
if (charactersToEscapeCount == 0) {
return fileName;
}
int i = 0;
StringBuilder builder = new StringBuilder(length + charactersToEscapeCount * 2);
while (charactersToEscapeCount > 0) {
char c = fileName.charAt(i++);
if (shouldEscapeCharacter(c)) {
builder.append('%').append(Integer.toHexString(c));
charactersToEscapeCount--;
} else {
builder.append(c);
}
}
if (i < length) {
builder.append(fileName, i, length);
}
return builder.toString();
}
private static boolean shouldEscapeCharacter(char c) {
switch (c) {
case '<':
case '>':
case ':':
case '"':
case '/':
case '\\':
case '|':
case '?':
case '*':
case '%':
return true;
default:
return false;
}
}
/**
* Unescapes an escaped file or directory name back to its original value.
*
* <p>See {@link #escapeFileName(String)} for more information.
*
* @param fileName File name to be unescaped.
* @return The original value of the file name before it was escaped, or null if the escaped
* fileName seems invalid.
*/
public static @Nullable String unescapeFileName(String fileName) {
int length = fileName.length();
int percentCharacterCount = 0;
for (int i = 0; i < length; i++) {
if (fileName.charAt(i) == '%') {
percentCharacterCount++;
}
}
if (percentCharacterCount == 0) {
return fileName;
}
int expectedLength = length - percentCharacterCount * 2;
StringBuilder builder = new StringBuilder(expectedLength);
Matcher matcher = ESCAPED_CHARACTER_PATTERN.matcher(fileName);
int startOfNotEscaped = 0;
while (percentCharacterCount > 0 && matcher.find()) {
char unescapedCharacter =
(char) Integer.parseInt(Assertions.checkNotNull(matcher.group(1)), 16);
builder.append(fileName, startOfNotEscaped, matcher.start()).append(unescapedCharacter);
startOfNotEscaped = matcher.end();
percentCharacterCount--;
}
if (startOfNotEscaped < length) {
builder.append(fileName, startOfNotEscaped, length);
}
if (builder.length() != expectedLength) {
return null;
}
return builder.toString();
}
/**
* A hacky method that always throws {@code t} even if {@code t} is a checked exception,
* and is not declared to be thrown.
*/
public static void sneakyThrow(Throwable t) {
sneakyThrowInternal(t);
}
@SuppressWarnings("unchecked")
private static <T extends Throwable> void sneakyThrowInternal(Throwable t) throws T {
throw (T) t;
}
/** Recursively deletes a directory and its content. */
public static void recursiveDelete(File fileOrDirectory) {
File[] directoryFiles = fileOrDirectory.listFiles();
if (directoryFiles != null) {
for (File child : directoryFiles) {
recursiveDelete(child);
}
}
fileOrDirectory.delete();
}
/** Creates an empty directory in the directory returned by {@link Context#getCacheDir()}. */
public static File createTempDirectory(Context context, String prefix) throws IOException {
File tempFile = createTempFile(context, prefix);
tempFile.delete(); // Delete the temp file.
tempFile.mkdir(); // Create a directory with the same name.
return tempFile;
}
/** Creates a new empty file in the directory returned by {@link Context#getCacheDir()}. */
public static File createTempFile(Context context, String prefix) throws IOException {
return File.createTempFile(prefix, null, context.getCacheDir());
}
/**
* Returns the result of updating a CRC-32 with the specified bytes in a "most significant bit
* first" order.
*
* @param bytes Array containing the bytes to update the crc value with.
* @param start The index to the first byte in the byte range to update the crc with.
* @param end The index after the last byte in the byte range to update the crc with.
* @param initialValue The initial value for the crc calculation.
* @return The result of updating the initial value with the specified bytes.
*/
public static int crc32(byte[] bytes, int start, int end, int initialValue) {
for (int i = start; i < end; i++) {
initialValue = (initialValue << 8)
^ CRC32_BYTES_MSBF[((initialValue >>> 24) ^ (bytes[i] & 0xFF)) & 0xFF];
}
return initialValue;
}
/**
* Returns the result of updating a CRC-8 with the specified bytes in a "most significant bit
* first" order.
*
* @param bytes Array containing the bytes to update the crc value with.
* @param start The index to the first byte in the byte range to update the crc with.
* @param end The index after the last byte in the byte range to update the crc with.
* @param initialValue The initial value for the crc calculation.
* @return The result of updating the initial value with the specified bytes.
*/
public static int crc8(byte[] bytes, int start, int end, int initialValue) {
for (int i = start; i < end; i++) {
initialValue = CRC8_BYTES_MSBF[initialValue ^ (bytes[i] & 0xFF)];
}
return initialValue;
}
/**
* Returns the {@link C.NetworkType} of the current network connection.
*
* @param context A context to access the connectivity manager.
* @return The {@link C.NetworkType} of the current network connection.
*/
@C.NetworkType
public static int getNetworkType(Context context) {
if (context == null) {
// Note: This is for backward compatibility only (context used to be @Nullable).
return C.NETWORK_TYPE_UNKNOWN;
}
NetworkInfo networkInfo;
ConnectivityManager connectivityManager =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivityManager == null) {
return C.NETWORK_TYPE_UNKNOWN;
}
try {
networkInfo = connectivityManager.getActiveNetworkInfo();
} catch (SecurityException e) {
// Expected if permission was revoked.
return C.NETWORK_TYPE_UNKNOWN;
}
if (networkInfo == null || !networkInfo.isConnected()) {
return C.NETWORK_TYPE_OFFLINE;
}
switch (networkInfo.getType()) {
case ConnectivityManager.TYPE_WIFI:
return C.NETWORK_TYPE_WIFI;
case ConnectivityManager.TYPE_WIMAX:
return C.NETWORK_TYPE_4G;
case ConnectivityManager.TYPE_MOBILE:
case ConnectivityManager.TYPE_MOBILE_DUN:
case ConnectivityManager.TYPE_MOBILE_HIPRI:
return getMobileNetworkType(networkInfo);
case ConnectivityManager.TYPE_ETHERNET:
return C.NETWORK_TYPE_ETHERNET;
default: // VPN, Bluetooth, Dummy.
return C.NETWORK_TYPE_OTHER;
}
}
/**
* Returns the upper-case ISO 3166-1 alpha-2 country code of the current registered operator's MCC
* (Mobile Country Code), or the country code of the default Locale if not available.
*
* @param context A context to access the telephony service. If null, only the Locale can be used.
* @return The upper-case ISO 3166-1 alpha-2 country code, or an empty String if unavailable.
*/
public static String getCountryCode(@Nullable Context context) {
if (context != null) {
TelephonyManager telephonyManager =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
if (telephonyManager != null) {
String countryCode = telephonyManager.getNetworkCountryIso();
if (!TextUtils.isEmpty(countryCode)) {
return toUpperInvariant(countryCode);
}
}
}
return toUpperInvariant(Locale.getDefault().getCountry());
}
/**
* Returns a non-empty array of normalized IETF BCP 47 language tags for the system languages
* ordered by preference.
*/
public static String[] getSystemLanguageCodes() {
String[] systemLocales = getSystemLocales();
for (int i = 0; i < systemLocales.length; i++) {
systemLocales[i] = normalizeLanguageCode(systemLocales[i]);
}
return systemLocales;
}
/**
* Uncompresses the data in {@code input}.
*
* @param input Wraps the compressed input data.
* @param output Wraps an output buffer to be used to store the uncompressed data. If {@code
* output.data} isn't big enough to hold the uncompressed data, a new array is created. If
* {@code true} is returned then the output's position will be set to 0 and its limit will be
* set to the length of the uncompressed data.
* @param inflater If not null, used to uncompressed the input. Otherwise a new {@link Inflater}
* is created.
* @return Whether the input is uncompressed successfully.
*/
public static boolean inflate(
ParsableByteArray input, ParsableByteArray output, @Nullable Inflater inflater) {
if (input.bytesLeft() <= 0) {
return false;
}
byte[] outputData = output.data;
if (outputData.length < input.bytesLeft()) {
outputData = new byte[2 * input.bytesLeft()];
}
if (inflater == null) {
inflater = new Inflater();
}
inflater.setInput(input.data, input.getPosition(), input.bytesLeft());
try {
int outputSize = 0;
while (true) {
outputSize += inflater.inflate(outputData, outputSize, outputData.length - outputSize);
if (inflater.finished()) {
output.reset(outputData, outputSize);
return true;
}
if (inflater.needsDictionary() || inflater.needsInput()) {
return false;
}
if (outputSize == outputData.length) {
outputData = Arrays.copyOf(outputData, outputData.length * 2);
}
}
} catch (DataFormatException e) {
return false;
} finally {
inflater.reset();
}
}
/**
* Returns whether the app is running on a TV device.
*
* @param context Any context.
* @return Whether the app is running on a TV device.
*/
public static boolean isTv(Context context) {
// See https://developer.android.com/training/tv/start/hardware.html#runtime-check.
UiModeManager uiModeManager =
(UiModeManager) context.getApplicationContext().getSystemService(UI_MODE_SERVICE);
return uiModeManager != null
&& uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
}
/**
* Gets the size of the current mode of the default display, in pixels.
*
* <p>Note that due to application UI scaling, the number of pixels made available to applications
* (as reported by {@link Display#getSize(Point)} may differ from the mode's actual resolution (as
* reported by this function). For example, applications running on a display configured with a 4K
* mode may have their UI laid out and rendered in 1080p and then scaled up. Applications can take
* advantage of the full mode resolution through a {@link SurfaceView} using full size buffers.
*
* @param context Any context.
* @return The size of the current mode, in pixels.
*/
public static Point getCurrentDisplayModeSize(Context context) {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
return getCurrentDisplayModeSize(context, windowManager.getDefaultDisplay());
}
/**
* Gets the size of the current mode of the specified display, in pixels.
*
* <p>Note that due to application UI scaling, the number of pixels made available to applications
* (as reported by {@link Display#getSize(Point)} may differ from the mode's actual resolution (as
* reported by this function). For example, applications running on a display configured with a 4K
* mode may have their UI laid out and rendered in 1080p and then scaled up. Applications can take
* advantage of the full mode resolution through a {@link SurfaceView} using full size buffers.
*
* @param context Any context.
* @param display The display whose size is to be returned.
* @return The size of the current mode, in pixels.
*/
public static Point getCurrentDisplayModeSize(Context context, Display display) {
if (Util.SDK_INT <= 29 && display.getDisplayId() == Display.DEFAULT_DISPLAY && isTv(context)) {
// On Android TVs it is common for the UI to be configured for a lower resolution than
// SurfaceViews can output. Before API 26 the Display object does not provide a way to
// identify this case, and up to and including API 28 many devices still do not correctly set
// their hardware compositor output size.
// Sony Android TVs advertise support for 4k output via a system feature.
if ("Sony".equals(Util.MANUFACTURER)
&& Util.MODEL.startsWith("BRAVIA")
&& context.getPackageManager().hasSystemFeature("com.sony.dtv.hardware.panel.qfhd")) {
return new Point(3840, 2160);
}
// Otherwise check the system property for display size. From API 28 treble may prevent the
// system from writing sys.display-size so we check vendor.display-size instead.
String displaySize =
Util.SDK_INT < 28
? getSystemProperty("sys.display-size")
: getSystemProperty("vendor.display-size");
// If we managed to read the display size, attempt to parse it.
if (!TextUtils.isEmpty(displaySize)) {
try {
String[] displaySizeParts = split(displaySize.trim(), "x");
if (displaySizeParts.length == 2) {
int width = Integer.parseInt(displaySizeParts[0]);
int height = Integer.parseInt(displaySizeParts[1]);
if (width > 0 && height > 0) {
return new Point(width, height);
}
}
} catch (NumberFormatException e) {
// Do nothing.
}
Log.e(TAG, "Invalid display size: " + displaySize);
}
}
Point displaySize = new Point();
if (Util.SDK_INT >= 23) {
getDisplaySizeV23(display, displaySize);
} else if (Util.SDK_INT >= 17) {
getDisplaySizeV17(display, displaySize);
} else {
getDisplaySizeV16(display, displaySize);
}
return displaySize;
}
/**
* Returns a string representation of a {@code TRACK_TYPE_*} constant defined in {@link C}.
*
* @param trackType A {@code TRACK_TYPE_*} constant,
* @return A string representation of this constant.
*/
public static String getTrackTypeString(int trackType) {
switch (trackType) {
case C.TRACK_TYPE_AUDIO:
return "audio";
case C.TRACK_TYPE_DEFAULT:
return "default";
case C.TRACK_TYPE_METADATA:
return "metadata";
case C.TRACK_TYPE_CAMERA_MOTION:
return "camera motion";
case C.TRACK_TYPE_NONE:
return "none";
case C.TRACK_TYPE_TEXT:
return "text";
case C.TRACK_TYPE_VIDEO:
return "video";
default:
return trackType >= C.TRACK_TYPE_CUSTOM_BASE ? "custom (" + trackType + ")" : "?";
}
}
/**
* Returns the current time in milliseconds since the epoch.
*
* @param elapsedRealtimeEpochOffsetMs The offset between {@link SystemClock#elapsedRealtime()}
* and the time since the Unix epoch, or {@link C#TIME_UNSET} if unknown.
* @return The Unix time in milliseconds since the epoch.
*/
public static long getNowUnixTimeMs(long elapsedRealtimeEpochOffsetMs) {
return elapsedRealtimeEpochOffsetMs == C.TIME_UNSET
? System.currentTimeMillis()
: SystemClock.elapsedRealtime() + elapsedRealtimeEpochOffsetMs;
}
@Nullable
private static String getSystemProperty(String name) {
try {
@SuppressLint("PrivateApi")
Class<?> systemProperties = Class.forName("android.os.SystemProperties");
Method getMethod = systemProperties.getMethod("get", String.class);
return (String) getMethod.invoke(systemProperties, name);
} catch (Exception e) {
Log.e(TAG, "Failed to read system property " + name, e);
return null;
}
}
@RequiresApi(23)
private static void getDisplaySizeV23(Display display, Point outSize) {
Display.Mode mode = display.getMode();
outSize.x = mode.getPhysicalWidth();
outSize.y = mode.getPhysicalHeight();
}
@RequiresApi(17)
private static void getDisplaySizeV17(Display display, Point outSize) {
display.getRealSize(outSize);
}
private static void getDisplaySizeV16(Display display, Point outSize) {
display.getSize(outSize);
}
private static String[] getSystemLocales() {
Configuration config = Resources.getSystem().getConfiguration();
return SDK_INT >= 24
? getSystemLocalesV24(config)
: new String[] {getLocaleLanguageTag(config.locale)};
}
@RequiresApi(24)
private static String[] getSystemLocalesV24(Configuration config) {
return Util.split(config.getLocales().toLanguageTags(), ",");
}
@RequiresApi(21)
private static String getLocaleLanguageTagV21(Locale locale) {
return locale.toLanguageTag();
}
private static @C.NetworkType int getMobileNetworkType(NetworkInfo networkInfo) {
switch (networkInfo.getSubtype()) {
case TelephonyManager.NETWORK_TYPE_EDGE:
case TelephonyManager.NETWORK_TYPE_GPRS:
return C.NETWORK_TYPE_2G;
case TelephonyManager.NETWORK_TYPE_1xRTT:
case TelephonyManager.NETWORK_TYPE_CDMA:
case TelephonyManager.NETWORK_TYPE_EVDO_0:
case TelephonyManager.NETWORK_TYPE_EVDO_A:
case TelephonyManager.NETWORK_TYPE_EVDO_B:
case TelephonyManager.NETWORK_TYPE_HSDPA:
case TelephonyManager.NETWORK_TYPE_HSPA:
case TelephonyManager.NETWORK_TYPE_HSUPA:
case TelephonyManager.NETWORK_TYPE_IDEN:
case TelephonyManager.NETWORK_TYPE_UMTS:
case TelephonyManager.NETWORK_TYPE_EHRPD:
case TelephonyManager.NETWORK_TYPE_HSPAP:
case TelephonyManager.NETWORK_TYPE_TD_SCDMA:
return C.NETWORK_TYPE_3G;
case TelephonyManager.NETWORK_TYPE_LTE:
return C.NETWORK_TYPE_4G;
case TelephonyManager.NETWORK_TYPE_NR:
return C.NETWORK_TYPE_5G;
case TelephonyManager.NETWORK_TYPE_IWLAN:
return C.NETWORK_TYPE_WIFI;
case TelephonyManager.NETWORK_TYPE_GSM:
case TelephonyManager.NETWORK_TYPE_UNKNOWN:
default: // Future mobile network types.
return C.NETWORK_TYPE_CELLULAR_UNKNOWN;
}
}
private static HashMap<String, String> createIsoLanguageReplacementMap() {
String[] iso2Languages = Locale.getISOLanguages();
HashMap<String, String> replacedLanguages =
new HashMap<>(
/* initialCapacity= */ iso2Languages.length + additionalIsoLanguageReplacements.length);
for (String iso2 : iso2Languages) {
try {
// This returns the ISO 639-2/T code for the language.
String iso3 = new Locale(iso2).getISO3Language();
if (!TextUtils.isEmpty(iso3)) {
replacedLanguages.put(iso3, iso2);
}
} catch (MissingResourceException e) {
// Shouldn't happen for list of known languages, but we don't want to throw either.
}
}
// Add additional replacement mappings.
for (int i = 0; i < additionalIsoLanguageReplacements.length; i += 2) {
replacedLanguages.put(
additionalIsoLanguageReplacements[i], additionalIsoLanguageReplacements[i + 1]);
}
return replacedLanguages;
}
@RequiresApi(api = Build.VERSION_CODES.M)
private static boolean requestExternalStoragePermission(Activity activity) {
if (activity.checkSelfPermission(permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
activity.requestPermissions(
new String[] {permission.READ_EXTERNAL_STORAGE}, /* requestCode= */ 0);
return true;
}
return false;
}
@RequiresApi(api = Build.VERSION_CODES.N)
private static boolean isTrafficRestricted(Uri uri) {
return "http".equals(uri.getScheme())
&& !NetworkSecurityPolicy.getInstance()
.isCleartextTrafficPermitted(Assertions.checkNotNull(uri.getHost()));
}
private static String maybeReplaceGrandfatheredLanguageTags(String languageTag) {
for (int i = 0; i < isoGrandfatheredTagReplacements.length; i += 2) {
if (languageTag.startsWith(isoGrandfatheredTagReplacements[i])) {
return isoGrandfatheredTagReplacements[i + 1]
+ languageTag.substring(/* beginIndex= */ isoGrandfatheredTagReplacements[i].length());
}
}
return languageTag;
}
// Additional mapping from ISO3 to ISO2 language codes.
private static final String[] additionalIsoLanguageReplacements =
new String[] {
// Bibliographical codes defined in ISO 639-2/B, replaced by terminological code defined in
// ISO 639-2/T. See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes.
"alb", "sq",
"arm", "hy",
"baq", "eu",
"bur", "my",
"tib", "bo",
"chi", "zh",
"cze", "cs",
"dut", "nl",
"ger", "de",
"gre", "el",
"fre", "fr",
"geo", "ka",
"ice", "is",
"mac", "mk",
"mao", "mi",
"may", "ms",
"per", "fa",
"rum", "ro",
"scc", "hbs-srp",
"slo", "sk",
"wel", "cy",
// Deprecated 2-letter codes, replaced by modern equivalent (including macrolanguage)
// See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes, "ISO 639:1988"
"id", "ms-ind",
"iw", "he",
"heb", "he",
"ji", "yi",
// Individual macrolanguage codes mapped back to full macrolanguage code.
// See https://en.wikipedia.org/wiki/ISO_639_macrolanguage
"in", "ms-ind",
"ind", "ms-ind",
"nb", "no-nob",
"nob", "no-nob",
"nn", "no-nno",
"nno", "no-nno",
"tw", "ak-twi",
"twi", "ak-twi",
"bs", "hbs-bos",
"bos", "hbs-bos",
"hr", "hbs-hrv",
"hrv", "hbs-hrv",
"sr", "hbs-srp",
"srp", "hbs-srp",
"cmn", "zh-cmn",
"hak", "zh-hak",
"nan", "zh-nan",
"hsn", "zh-hsn"
};
// "Grandfathered tags", replaced by modern equivalents (including macrolanguage)
// See https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry.
private static final String[] isoGrandfatheredTagReplacements =
new String[] {
"i-lux", "lb",
"i-hak", "zh-hak",
"i-navajo", "nv",
"no-bok", "no-nob",
"no-nyn", "no-nno",
"zh-guoyu", "zh-cmn",
"zh-hakka", "zh-hak",
"zh-min-nan", "zh-nan",
"zh-xiang", "zh-hsn"
};
/**
* Allows the CRC-32 calculation to be done byte by byte instead of bit per bit in the order "most
* significant bit first".
*/
private static final int[] CRC32_BYTES_MSBF = {
0X00000000, 0X04C11DB7, 0X09823B6E, 0X0D4326D9, 0X130476DC, 0X17C56B6B, 0X1A864DB2,
0X1E475005, 0X2608EDB8, 0X22C9F00F, 0X2F8AD6D6, 0X2B4BCB61, 0X350C9B64, 0X31CD86D3,
0X3C8EA00A, 0X384FBDBD, 0X4C11DB70, 0X48D0C6C7, 0X4593E01E, 0X4152FDA9, 0X5F15ADAC,
0X5BD4B01B, 0X569796C2, 0X52568B75, 0X6A1936C8, 0X6ED82B7F, 0X639B0DA6, 0X675A1011,
0X791D4014, 0X7DDC5DA3, 0X709F7B7A, 0X745E66CD, 0X9823B6E0, 0X9CE2AB57, 0X91A18D8E,
0X95609039, 0X8B27C03C, 0X8FE6DD8B, 0X82A5FB52, 0X8664E6E5, 0XBE2B5B58, 0XBAEA46EF,
0XB7A96036, 0XB3687D81, 0XAD2F2D84, 0XA9EE3033, 0XA4AD16EA, 0XA06C0B5D, 0XD4326D90,
0XD0F37027, 0XDDB056FE, 0XD9714B49, 0XC7361B4C, 0XC3F706FB, 0XCEB42022, 0XCA753D95,
0XF23A8028, 0XF6FB9D9F, 0XFBB8BB46, 0XFF79A6F1, 0XE13EF6F4, 0XE5FFEB43, 0XE8BCCD9A,
0XEC7DD02D, 0X34867077, 0X30476DC0, 0X3D044B19, 0X39C556AE, 0X278206AB, 0X23431B1C,
0X2E003DC5, 0X2AC12072, 0X128E9DCF, 0X164F8078, 0X1B0CA6A1, 0X1FCDBB16, 0X018AEB13,
0X054BF6A4, 0X0808D07D, 0X0CC9CDCA, 0X7897AB07, 0X7C56B6B0, 0X71159069, 0X75D48DDE,
0X6B93DDDB, 0X6F52C06C, 0X6211E6B5, 0X66D0FB02, 0X5E9F46BF, 0X5A5E5B08, 0X571D7DD1,
0X53DC6066, 0X4D9B3063, 0X495A2DD4, 0X44190B0D, 0X40D816BA, 0XACA5C697, 0XA864DB20,
0XA527FDF9, 0XA1E6E04E, 0XBFA1B04B, 0XBB60ADFC, 0XB6238B25, 0XB2E29692, 0X8AAD2B2F,
0X8E6C3698, 0X832F1041, 0X87EE0DF6, 0X99A95DF3, 0X9D684044, 0X902B669D, 0X94EA7B2A,
0XE0B41DE7, 0XE4750050, 0XE9362689, 0XEDF73B3E, 0XF3B06B3B, 0XF771768C, 0XFA325055,
0XFEF34DE2, 0XC6BCF05F, 0XC27DEDE8, 0XCF3ECB31, 0XCBFFD686, 0XD5B88683, 0XD1799B34,
0XDC3ABDED, 0XD8FBA05A, 0X690CE0EE, 0X6DCDFD59, 0X608EDB80, 0X644FC637, 0X7A089632,
0X7EC98B85, 0X738AAD5C, 0X774BB0EB, 0X4F040D56, 0X4BC510E1, 0X46863638, 0X42472B8F,
0X5C007B8A, 0X58C1663D, 0X558240E4, 0X51435D53, 0X251D3B9E, 0X21DC2629, 0X2C9F00F0,
0X285E1D47, 0X36194D42, 0X32D850F5, 0X3F9B762C, 0X3B5A6B9B, 0X0315D626, 0X07D4CB91,
0X0A97ED48, 0X0E56F0FF, 0X1011A0FA, 0X14D0BD4D, 0X19939B94, 0X1D528623, 0XF12F560E,
0XF5EE4BB9, 0XF8AD6D60, 0XFC6C70D7, 0XE22B20D2, 0XE6EA3D65, 0XEBA91BBC, 0XEF68060B,
0XD727BBB6, 0XD3E6A601, 0XDEA580D8, 0XDA649D6F, 0XC423CD6A, 0XC0E2D0DD, 0XCDA1F604,
0XC960EBB3, 0XBD3E8D7E, 0XB9FF90C9, 0XB4BCB610, 0XB07DABA7, 0XAE3AFBA2, 0XAAFBE615,
0XA7B8C0CC, 0XA379DD7B, 0X9B3660C6, 0X9FF77D71, 0X92B45BA8, 0X9675461F, 0X8832161A,
0X8CF30BAD, 0X81B02D74, 0X857130C3, 0X5D8A9099, 0X594B8D2E, 0X5408ABF7, 0X50C9B640,
0X4E8EE645, 0X4A4FFBF2, 0X470CDD2B, 0X43CDC09C, 0X7B827D21, 0X7F436096, 0X7200464F,
0X76C15BF8, 0X68860BFD, 0X6C47164A, 0X61043093, 0X65C52D24, 0X119B4BE9, 0X155A565E,
0X18197087, 0X1CD86D30, 0X029F3D35, 0X065E2082, 0X0B1D065B, 0X0FDC1BEC, 0X3793A651,
0X3352BBE6, 0X3E119D3F, 0X3AD08088, 0X2497D08D, 0X2056CD3A, 0X2D15EBE3, 0X29D4F654,
0XC5A92679, 0XC1683BCE, 0XCC2B1D17, 0XC8EA00A0, 0XD6AD50A5, 0XD26C4D12, 0XDF2F6BCB,
0XDBEE767C, 0XE3A1CBC1, 0XE760D676, 0XEA23F0AF, 0XEEE2ED18, 0XF0A5BD1D, 0XF464A0AA,
0XF9278673, 0XFDE69BC4, 0X89B8FD09, 0X8D79E0BE, 0X803AC667, 0X84FBDBD0, 0X9ABC8BD5,
0X9E7D9662, 0X933EB0BB, 0X97FFAD0C, 0XAFB010B1, 0XAB710D06, 0XA6322BDF, 0XA2F33668,
0XBCB4666D, 0XB8757BDA, 0XB5365D03, 0XB1F740B4
};
/**
* Allows the CRC-8 calculation to be done byte by byte instead of bit per bit in the order "most
* significant bit first".
*/
private static final int[] CRC8_BYTES_MSBF = {
0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A,
0x2D, 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53,
0x5A, 0x5D, 0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4,
0xC3, 0xCA, 0xCD, 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1,
0xB4, 0xB3, 0xBA, 0xBD, 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1,
0xF6, 0xE3, 0xE4, 0xED, 0xEA, 0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88,
0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A, 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F,
0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A, 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42,
0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A, 0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B,
0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4, 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2,
0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4, 0x69, 0x6E, 0x67, 0x60, 0x75,
0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44, 0x19, 0x1E, 0x17, 0x10,
0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34, 0x4E, 0x49, 0x40,
0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63, 0x3E, 0x39,
0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13, 0xAE,
0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4,
0xF3
};
}