/*
* 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 com.google.android.exoplayer.util;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ExoPlayerLibraryInfo;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.text.TextUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Miscellaneous utility functions.
*/
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 = android.os.Build.VERSION.SDK_INT;
/**
* Like {@link android.os.Build#DEVICE}, but in a place where it can be conveniently overridden
* for local testing.
*/
public static final String DEVICE = android.os.Build.DEVICE;
/**
* Like {@link android.os.Build#MANUFACTURER}, but in a place where it can be conveniently
* overridden for local testing.
*/
public static final String MANUFACTURER = android.os.Build.MANUFACTURER;
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 long MAX_BYTES_TO_DRAIN = 2048;
private Util() {}
/**
* Returns whether the device is an AndroidTV.
*
* @param context A context.
* @return True if the device is an AndroidTV. False otherwise.
*/
@SuppressLint("InlinedApi")
public static boolean isAndroidTv(Context context) {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
}
/**
* Returns true if the URL points to a file on the local device
*
* @param url The URL to test
*/
public static boolean isUrlLocalFile(URL url) {
return url.getProtocol().equals("file");
}
/**
* 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(Object o1, 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(Object[] items, Object item) {
for (int i = 0; i < items.length; i++) {
if (Util.areEqual(items[i], item)) {
return true;
}
}
return false;
}
/**
* 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(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, threadName);
}
});
}
/**
* Instantiates a new single threaded scheduled executor whose thread has the specified name.
*
* @param threadName The name of the thread.
* @return The executor.
*/
public static ScheduledExecutorService newSingleThreadScheduledExecutor(final String threadName) {
return Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, threadName);
}
});
}
/**
* Closes a {@link DataSource}, suppressing any {@link IOException} that may occur.
*
* @param dataSource The {@link DataSource} to close.
*/
public static void closeQuietly(DataSource dataSource) {
try {
dataSource.close();
} catch (IOException e) {
// Ignore.
}
}
/**
* Closes an {@link OutputStream}, suppressing any {@link IOException} that may occur.
*
* @param outputStream The {@link OutputStream} to close.
*/
public static void closeQuietly(OutputStream outputStream) {
try {
outputStream.close();
} catch (IOException e) {
// Ignore.
}
}
/**
* 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 String toLowerInvariant(String text) {
return text == null ? null : text.toLowerCase(Locale.US);
}
/**
* 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;
}
/**
* Returns the index of the largest value in an array that is less than (or optionally equal to)
* a specified key.
* <p>
* The search is performed using a binary search algorithm, and so the array must be sorted.
*
* @param a The array to search.
* @param key The key being searched for.
* @param inclusive If the key is present in the array, whether to return the corresponding index.
* If false then the returned index corresponds to the largest value in the array that is
* strictly less than the key.
* @param stayInBounds If true, then 0 will be returned in the case that the key is smaller than
* the smallest value in the array. If false then -1 will be returned.
*/
public static int binarySearchFloor(long[] a, long key, boolean inclusive, boolean stayInBounds) {
int index = Arrays.binarySearch(a, key);
index = index < 0 ? -(index + 2) : (inclusive ? index : (index - 1));
return stayInBounds ? Math.max(0, index) : index;
}
/**
* Returns the index of the smallest value in an array that is greater than (or optionally equal
* to) a specified key.
* <p>
* The search is performed using a binary search algorithm, and so the array must be sorted.
*
* @param a The array to search.
* @param key The key being searched for.
* @param inclusive If the key is present in the array, whether to return the corresponding index.
* If false then the returned index corresponds to the smallest value in the array that is
* strictly greater than the key.
* @param stayInBounds If true, then {@code (a.length - 1)} will be returned in the case that the
* key is greater than the largest value in the array. If false then {@code a.length} will be
* returned.
*/
public static int binarySearchCeil(long[] a, long key, boolean inclusive, boolean stayInBounds) {
int index = Arrays.binarySearch(a, key);
index = index < 0 ? ~index : (inclusive ? index : (index + 1));
return stayInBounds ? Math.min(a.length - 1, index) : index;
}
/**
* Returns the index of the largest value in an list that is less than (or optionally equal to)
* a specified key.
* <p>
* The search is performed using a binary search algorithm, and so the list must be sorted.
*
* @param list The list to search.
* @param key The key being searched for.
* @param inclusive If the key is present in the list, whether to return the corresponding index.
* If false then the returned index corresponds to the largest value in the list that is
* strictly less than the key.
* @param stayInBounds If true, then 0 will be returned in the case that the key is smaller than
* the smallest value in the list. If false then -1 will be returned.
*/
public static<T> int binarySearchFloor(List<? extends Comparable<? super T>> list, T key,
boolean inclusive, boolean stayInBounds) {
int index = Collections.binarySearch(list, key);
index = index < 0 ? -(index + 2) : (inclusive ? index : (index - 1));
return stayInBounds ? Math.max(0, index) : index;
}
/**
* Returns the index of the smallest value in an list that is greater than (or optionally equal
* to) a specified key.
* <p>
* The search is performed using a binary search algorithm, and so the list must be sorted.
*
* @param list The list to search.
* @param key The key being searched for.
* @param inclusive If the key is present in the list, whether to return the corresponding index.
* If false then the returned index corresponds to the smallest value in the list that is
* strictly greater than the key.
* @param stayInBounds If true, then {@code (list.size() - 1)} will be returned in the case that
* the key is greater than the largest value in the list. If false then {@code list.size()}
* will be returned.
*/
public static<T> int binarySearchCeil(List<? extends Comparable<? super T>> list, T key,
boolean inclusive, boolean stayInBounds) {
int index = Collections.binarySearch(list, key);
index = index < 0 ? ~index : (inclusive ? index : (index + 1));
return stayInBounds ? Math.min(list.size() - 1, index) : index;
}
/**
* Creates an integer array containing the integers from 0 to {@code length - 1}.
*
* @param length The length of the array.
* @return The array.
*/
public static int[] firstIntegersArray(int length) {
int[] firstIntegers = new int[length];
for (int i = 0; i < length; i++) {
firstIntegers[i] = i;
}
return firstIntegers;
}
/**
* Parses an xs:duration attribute value, returning the parsed duration in milliseconds.
*
* @param value The attribute value to parse.
* @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 parse.
* @return The parsed timestamp in milliseconds since the epoch.
*/
public static long parseXsDateTime(String value) throws ParseException {
Matcher matcher = XS_DATE_TIME_PATTERN.matcher(value);
if (!matcher.matches()) {
throw new ParseException("Invalid date/time format: " + value, 0);
}
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 (matcher.group(11).equals("-")) {
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);
}
}
}
/**
* 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[] toArray(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;
}
/**
* On platform API levels 19 and 20, okhttp's implementation of {@link InputStream#close} can
* block for a long time if the stream has a lot of data remaining. Call this method before
* closing the input stream to make a best effort to cause the input stream to encounter an
* unexpected end of input, working around this issue. On other platform API levels, the method
* does nothing.
*
* @param connection The connection whose {@link InputStream} should be terminated.
* @param bytesRemaining The number of bytes remaining to be read from the input stream if its
* length is known. {@link C#LENGTH_UNBOUNDED} otherwise.
*/
public static void maybeTerminateInputStream(HttpURLConnection connection, long bytesRemaining) {
if (SDK_INT != 19 && SDK_INT != 20) {
return;
}
try {
InputStream inputStream = connection.getInputStream();
if (bytesRemaining == C.LENGTH_UNBOUNDED) {
// If the input stream has already ended, do nothing. The socket may be re-used.
if (inputStream.read() == -1) {
return;
}
} else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) {
// There isn't much data left. Prefer to allow it to drain, which may allow the socket to be
// re-used.
return;
}
String className = inputStream.getClass().getName();
if (className.equals("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream")
|| className.equals(
"com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream")) {
Class<?> superclass = inputStream.getClass().getSuperclass();
Method unexpectedEndOfInput = superclass.getDeclaredMethod("unexpectedEndOfInput");
unexpectedEndOfInput.setAccessible(true);
unexpectedEndOfInput.invoke(inputStream);
}
} catch (IOException e) {
// The connection didn't ever have an input stream, or it was closed already.
} catch (Exception e) {
// Something went wrong. The device probably isn't using okhttp.
}
}
/**
* Given a {@link DataSpec} and a number of bytes already loaded, returns a {@link DataSpec}
* that represents the remainder of the data.
*
* @param dataSpec The original {@link DataSpec}.
* @param bytesLoaded The number of bytes already loaded.
* @return A {@link DataSpec} that represents the remainder of the data.
*/
public static DataSpec getRemainderDataSpec(DataSpec dataSpec, int bytesLoaded) {
if (bytesLoaded == 0) {
return dataSpec;
} else {
long remainingLength = dataSpec.length == C.LENGTH_UNBOUNDED ? C.LENGTH_UNBOUNDED
: dataSpec.length - bytesLoaded;
return new DataSpec(dataSpec.uri, dataSpec.position + bytesLoaded, remainingLength,
dataSpec.key, dataSpec.flags);
}
}
/**
* Returns the integer equal to the big-endian concatenation of the characters in {@code string}
* as bytes. {@code string} must contain four or fewer characters.
*/
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;
}
/**
* Returns the top 32 bits of a long as an integer.
*/
public static int getTopInt(long value) {
return (int) (value >>> 32);
}
/**
* Returns the bottom 32 bits of a long as an integer.
*/
public static int getBottomInt(long value) {
return (int) value;
}
/**
* Returns a long created by concatenating the bits of two integers.
*/
public static long getLong(int topInteger, int bottomInteger) {
return ((long) topInteger << 32) | (bottomInteger & 0xFFFFFFFFL);
}
/**
* Returns a hex string representation of the data provided.
*
* @param data The byte array containing the data to be turned into a hex string.
* @param beginIndex The begin index, inclusive.
* @param endIndex The end index, exclusive.
* @return A string containing the hex representation of the data provided.
*/
public static String getHexStringFromBytes(byte[] data, int beginIndex, int endIndex) {
StringBuilder dataStringBuilder = new StringBuilder(endIndex - beginIndex);
for (int i = beginIndex; i < endIndex; i++) {
dataStringBuilder.append(String.format(Locale.US, "%02X", data[i]));
}
return dataStringBuilder.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 <T> String getCommaDelimitedSimpleClassNames(T[] 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
+ ") " + "ExoPlayerLib/" + ExoPlayerLibraryInfo.VERSION;
}
/**
* Executes a post request using {@link HttpURLConnection}.
*
* @param url The request URL.
* @param data The request body, or null.
* @param requestProperties Request properties, or null.
* @return The response body.
* @throws IOException If an error occurred making the request.
*/
// TODO: Remove this and use HttpDataSource once DataSpec supports inclusion of a POST body.
public static byte[] executePost(String url, byte[] data, Map<String, String> requestProperties)
throws IOException {
HttpURLConnection urlConnection = null;
try {
urlConnection = (HttpURLConnection) new URL(url).openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setDoOutput(data != null);
urlConnection.setDoInput(true);
if (requestProperties != null) {
for (Map.Entry<String, String> requestProperty : requestProperties.entrySet()) {
urlConnection.setRequestProperty(requestProperty.getKey(), requestProperty.getValue());
}
}
// Write the request body, if there is one.
if (data != null) {
OutputStream out = urlConnection.getOutputStream();
try {
out.write(data);
} finally {
out.close();
}
}
// Read and return the response body.
InputStream inputStream = urlConnection.getInputStream();
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte scratch[] = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(scratch)) != -1) {
byteArrayOutputStream.write(scratch, 0, bytesRead);
}
return byteArrayOutputStream.toByteArray();
} finally {
inputStream.close();
}
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
}
}
}