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); |
} |
} |