Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1788)

Unified Diff: content/public/android/java/src/org/chromium/content/browser/MediaResourceGetter.java

Issue 23684004: Fix broken duration logic in MediaResourceGetter for non-video media. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: No more findbugs warnings. Created 6 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | content/public/android/javatests/src/org/chromium/content/browser/MediaResourceGetterTest.java » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: content/public/android/java/src/org/chromium/content/browser/MediaResourceGetter.java
diff --git a/content/public/android/java/src/org/chromium/content/browser/MediaResourceGetter.java b/content/public/android/java/src/org/chromium/content/browser/MediaResourceGetter.java
index 5ea3df0d193d4a762239faae29e9b859b753eb16..5e334820aeadc2ddb147b9039c406b963a3017c2 100644
--- a/content/public/android/java/src/org/chromium/content/browser/MediaResourceGetter.java
+++ b/content/public/android/java/src/org/chromium/content/browser/MediaResourceGetter.java
@@ -13,12 +13,18 @@ import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
+import com.google.common.annotations.VisibleForTesting;
+
import org.chromium.base.CalledByNative;
import org.chromium.base.JNINamespace;
import org.chromium.base.PathUtils;
import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
/**
* Java counterpart of android MediaResourceGetter.
@@ -27,112 +33,336 @@ import java.util.HashMap;
class MediaResourceGetter {
private static final String TAG = "MediaResourceGetter";
+ private final MediaMetadata EMPTY_METADATA = new MediaMetadata(0,0,0,false);
+
+ private final MediaMetadataRetriever mRetriever = new MediaMetadataRetriever();
- private static class MediaMetadata {
+ @VisibleForTesting
+ static class MediaMetadata {
private final int mDurationInMilliseconds;
private final int mWidth;
private final int mHeight;
private final boolean mSuccess;
- private MediaMetadata(int durationInMilliseconds, int width, int height, boolean success) {
+ MediaMetadata(int durationInMilliseconds, int width, int height, boolean success) {
mDurationInMilliseconds = durationInMilliseconds;
mWidth = width;
mHeight = height;
mSuccess = success;
}
+ // TODO(andrewhayden): according to the spec, if duration is unknown
+ // then we must return NaN. If it is unbounded, then positive infinity.
+ // http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html
@CalledByNative("MediaMetadata")
- private int getDurationInMilliseconds() { return mDurationInMilliseconds; }
+ int getDurationInMilliseconds() { return mDurationInMilliseconds; }
@CalledByNative("MediaMetadata")
- private int getWidth() { return mWidth; }
+ int getWidth() { return mWidth; }
@CalledByNative("MediaMetadata")
- private int getHeight() { return mHeight; }
+ int getHeight() { return mHeight; }
@CalledByNative("MediaMetadata")
- private boolean isSuccess() { return mSuccess; }
+ boolean isSuccess() { return mSuccess; }
+
+ @Override
+ public String toString() {
+ return "MediaMetadata["
+ + "durationInMilliseconds=" + mDurationInMilliseconds
+ + ", width=" + mWidth
+ + ", height=" + mHeight
+ + ", success=" + mSuccess
+ + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + mDurationInMilliseconds;
+ result = prime * result + mHeight;
+ result = prime * result + (mSuccess ? 1231 : 1237);
+ result = prime * result + mWidth;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ MediaMetadata other = (MediaMetadata)obj;
+ if (mDurationInMilliseconds != other.mDurationInMilliseconds)
+ return false;
+ if (mHeight != other.mHeight)
+ return false;
+ if (mSuccess != other.mSuccess)
+ return false;
+ if (mWidth != other.mWidth)
+ return false;
+ return true;
+ }
}
@CalledByNative
- private static MediaMetadata extractMediaMetadata(Context context, String url, String cookies,
- String userAgent) {
- int durationInMilliseconds = 0;
- int width = 0;
- int height = 0;
- boolean success = false;
- if ("GT-I9100".contentEquals(android.os.Build.MODEL)
- && android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) {
- return new MediaMetadata(0, 0, 0, success);
+ private static MediaMetadata extractMediaMetadata(final Context context,
+ final String url,
+ final String cookies,
+ final String userAgent) {
+ return new MediaResourceGetter().extract(
+ context, url, cookies, userAgent);
+ }
+
+ @VisibleForTesting
+ MediaMetadata extract(final Context context, final String url,
+ final String cookies, final String userAgent) {
+ if (!androidDeviceOk(android.os.Build.MODEL, android.os.Build.VERSION.SDK_INT)) {
+ return EMPTY_METADATA;
}
- // TODO(qinmin): use ConnectionTypeObserver to listen to the network type change.
- ConnectivityManager mConnectivityManager =
- (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- if (mConnectivityManager != null) {
- if (context.checkCallingOrSelfPermission(
- android.Manifest.permission.ACCESS_NETWORK_STATE) !=
- PackageManager.PERMISSION_GRANTED) {
- return new MediaMetadata(0, 0, 0, false);
- }
- NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
- if (info == null) {
- return new MediaMetadata(durationInMilliseconds, width, height, success);
- }
- switch (info.getType()) {
- case ConnectivityManager.TYPE_ETHERNET:
- case ConnectivityManager.TYPE_WIFI:
- break;
- case ConnectivityManager.TYPE_WIMAX:
- case ConnectivityManager.TYPE_MOBILE:
- default:
- return new MediaMetadata(durationInMilliseconds, width, height, success);
- }
+ if (!configure(context, url, cookies, userAgent)) {
+ Log.e(TAG, "Unable to configure metadata extractor");
+ return EMPTY_METADATA;
}
- MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
- Uri uri = Uri.parse(url);
- String scheme = uri.getScheme();
- if (scheme == null || scheme.equals("file")) {
- File file = new File(uri.getPath());
- String path = file.getAbsolutePath();
- if (file.exists() && (path.startsWith("/mnt/sdcard/") ||
- path.startsWith("/sdcard/") ||
- path.startsWith(PathUtils.getExternalStorageDirectory()) ||
- path.startsWith(context.getCacheDir().getAbsolutePath()))) {
- retriever.setDataSource(path);
- } else {
- return new MediaMetadata(durationInMilliseconds, width, height, success);
+ String durationString = extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_DURATION);
+ if (durationString == null) {
+ Log.w(TAG, "missing duration metadata");
+ return EMPTY_METADATA;
+ }
+
+ int durationMillis = 0;
+ try {
+ durationMillis = Integer.parseInt(durationString);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "non-numeric duration: " + durationString);
+ return EMPTY_METADATA;
+ }
+
+ int width = 0;
+ int height = 0;
+ boolean hasVideo = "yes".equals(extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO));
+ Log.d(TAG, (hasVideo ? "resource has video" : "resource doesn't have video"));
+ if (hasVideo) {
+ String widthString = extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
+ if (widthString == null) {
+ Log.w(TAG, "missing video width metadata");
+ return EMPTY_METADATA;
}
- } else {
- HashMap<String, String> headersMap = new HashMap<String, String>();
- if (!TextUtils.isEmpty(cookies)) {
- headersMap.put("Cookie", cookies);
+ try {
+ width = Integer.parseInt(widthString);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "non-numeric width: " + widthString);
+ return EMPTY_METADATA;
}
- if (!TextUtils.isEmpty(userAgent)) {
- headersMap.put("User-Agent", userAgent);
+
+ String heightString = extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
+ if (heightString == null) {
+ Log.w(TAG, "missing video height metadata");
+ return EMPTY_METADATA;
+ }
+ try {
+ height = Integer.parseInt(heightString);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "non-numeric height: " + heightString);
+ return EMPTY_METADATA;
}
- retriever.setDataSource(url, headersMap);
- }
- String duration = retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_DURATION);
- String videoWidth = retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
- String videoHeight = retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
- if (duration == null || videoWidth == null || videoHeight == null) {
- return new MediaMetadata(durationInMilliseconds, width, height, success);
}
- durationInMilliseconds = Integer.parseInt(duration);
- width = Integer.parseInt(videoWidth);
- height = Integer.parseInt(videoHeight);
- success = true;
- } catch (IllegalArgumentException e) {
- Log.e(TAG, "Invalid url: " + e);
+ MediaMetadata result = new MediaMetadata(durationMillis, width, height, true);
+ Log.d(TAG, "extracted valid metadata: " + result.toString());
+ return result;
} catch (RuntimeException e) {
- Log.e(TAG, "Invalid url: " + e);
+ Log.e(TAG, "Unable to extract medata", e);
+ return EMPTY_METADATA;
+ }
+ }
+
+ @VisibleForTesting
+ boolean configure(Context context, String url, String cookies, String userAgent) {
+ Uri uri = Uri.parse(url);
+ String scheme = uri.getScheme();
+ if (scheme == null || scheme.equals("file")) {
+ File file = uriToFile(uri.getPath());
+ if (!file.exists()) {
+ Log.e(TAG, "File does not exist.");
+ return false;
+ }
+ if (!filePathAcceptable(file)) {
+ Log.e(TAG, "Refusing to read from unsafe file location.");
+ return false;
+ }
+ try {
+ configure(file.getAbsolutePath());
+ return true;
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Error configuring data source", e);
+ return false;
+ }
+ } else {
+ if (!isNetworkReliable(context)) {
+ Log.w(TAG, "non-file URI can't be read due to unsuitable network conditions");
+ return false;
+ }
+ Map<String, String> headersMap = new HashMap<String, String>();
+ if (!TextUtils.isEmpty(cookies)) {
+ headersMap.put("Cookie", cookies);
+ }
+ if (!TextUtils.isEmpty(userAgent)) {
+ headersMap.put("User-Agent", userAgent);
+ }
+ try {
+ configure(url, headersMap);
+ return true;
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Error configuring data source", e);
+ return false;
+ }
}
- return new MediaMetadata(durationInMilliseconds, width, height, success);
+ }
+
+ /**
+ * @return true if the device is on an ethernet or wifi network.
+ * If anything goes wrong (e.g., permission denied while trying to access
+ * the network state), returns false.
+ */
+ @VisibleForTesting
+ boolean isNetworkReliable(Context context) {
+ if (context.checkCallingOrSelfPermission(
+ android.Manifest.permission.ACCESS_NETWORK_STATE) !=
+ PackageManager.PERMISSION_GRANTED) {
+ Log.w(TAG, "permission denied to access network state");
+ return false;
+ }
+
+ Integer networkType = getNetworkType(context);
+ if (networkType == null) {
+ return false;
+ }
+ switch (networkType.intValue()) {
+ case ConnectivityManager.TYPE_ETHERNET:
+ case ConnectivityManager.TYPE_WIFI:
+ Log.d(TAG, "ethernet/wifi connection detected");
+ return true;
+ case ConnectivityManager.TYPE_WIMAX:
+ case ConnectivityManager.TYPE_MOBILE:
+ default:
+ Log.d(TAG, "no ethernet/wifi connection detected");
+ return false;
+ }
+ }
+
+ /**
+ * @param file the file whose path should be checked
+ * @return true if and only if the file is in a location that we consider
+ * safe to read from, such as /mnt/sdcard.
+ */
+ @VisibleForTesting
+ boolean filePathAcceptable(File file) {
+ final String path;
+ try {
+ path = file.getCanonicalPath();
+ } catch (IOException e) {
+ // Canonicalization has failed. Assume malicious, give up.
+ Log.w(TAG, "canonicalization of file path failed");
+ return false;
+ }
+ // In order to properly match the roots we must also canonicalize the
+ // well-known paths we are matching against. If we don't, then we can
+ // get unusual results in testing systems or possibly on rooted devices.
+ // Note that canonicalized directory paths always end with '/'.
+ List<String> acceptablePaths = canonicalize(getRawAcceptableDirectories());
+ acceptablePaths.add(getExternalStorageDirectory());
+ Log.d(TAG, "canonicalized file path: " + path);
+ for (String acceptablePath : acceptablePaths) {
+ if (path.startsWith(acceptablePath)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Special case handling for device/OS combos that simply do not work.
+ * @param model the model of device being examined
+ * @param sdkVersion the version of the SDK installed on the device
+ * @return true if the device can be used correctly, otherwise false
+ */
+ @VisibleForTesting
+ static boolean androidDeviceOk(final String model, final int sdkVersion) {
+ return !("GT-I9100".contentEquals(model) &&
+ sdkVersion < android.os.Build.VERSION_CODES.JELLY_BEAN);
+ }
+
+ // The methods below can be used by unit tests to fake functionality.
+ @VisibleForTesting
+ File uriToFile(String path) {
+ return new File(path);
+ }
+
+ @VisibleForTesting
+ Integer getNetworkType(Context context) {
+ // TODO(qinmin): use ConnectionTypeObserver to listen to the network type change.
+ ConnectivityManager mConnectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (mConnectivityManager == null) {
+ Log.w(TAG, "no connectivity manager available");
+ return null;
+ }
+ NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
+ if (info == null) {
+ Log.d(TAG, "no active network");
+ return null;
+ }
+ return info.getType();
+ }
+
+ private List<String> getRawAcceptableDirectories() {
+ List<String> result = new ArrayList<String>();
+ result.add("/mnt/sdcard/");
+ result.add("/sdcard/");
+ return result;
+ }
+
+ private List<String> canonicalize(List<String> paths) {
+ List<String> result = new ArrayList<String>(paths.size());
+ try {
+ for (String path : paths) {
+ result.add(new File(path).getCanonicalPath());
+ }
+ return result;
+ } catch (IOException e) {
+ // Canonicalization has failed. Assume malicious, give up.
+ Log.w(TAG, "canonicalization of file path failed");
+ }
+ return result;
+ }
+
+ @VisibleForTesting
+ String getExternalStorageDirectory() {
+ return PathUtils.getExternalStorageDirectory();
+ }
+
+ @VisibleForTesting
+ void configure(String url, Map<String,String> headers) {
+ mRetriever.setDataSource(url, headers);
+ }
+
+ @VisibleForTesting
+ void configure(String path) {
+ mRetriever.setDataSource(path);
+ }
+
+ @VisibleForTesting
+ String extractMetadata(int key) {
+ return mRetriever.extractMetadata(key);
}
}
« no previous file with comments | « no previous file | content/public/android/javatests/src/org/chromium/content/browser/MediaResourceGetterTest.java » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698