1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.content.browser;
7 import android.content.Context;
8 import android.content.pm.PackageManager;
9 import android.media.MediaMetadataRetriever;
10 import android.net.ConnectivityManager;
11 import android.net.NetworkInfo;
12 import android.text.TextUtils;
13 import android.util.Log;
15 import com.google.common.annotations.VisibleForTesting;
17 import org.chromium.base.CalledByNative;
18 import org.chromium.base.JNINamespace;
19 import org.chromium.base.PathUtils;
22 import java.io.IOException;
24 import java.util.ArrayList;
25 import java.util.HashMap;
26 import java.util.List;
30 * Java counterpart of android MediaResourceGetter.
32 @JNINamespace("content")
33 class MediaResourceGetter {
35 private static final String TAG = "MediaResourceGetter";
36 private final MediaMetadata EMPTY_METADATA = new MediaMetadata(0,0,0,false);
38 private final MediaMetadataRetriever mRetriever = new MediaMetadataRetriever();
41 static class MediaMetadata {
42 private final int mDurationInMilliseconds;
43 private final int mWidth;
44 private final int mHeight;
45 private final boolean mSuccess;
47 MediaMetadata(int durationInMilliseconds, int width, int height, boolean success) {
48 mDurationInMilliseconds = durationInMilliseconds;
54 // TODO(andrewhayden): according to the spec, if duration is unknown
55 // then we must return NaN. If it is unbounded, then positive infinity.
56 // http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html
57 @CalledByNative("MediaMetadata")
58 int getDurationInMilliseconds() { return mDurationInMilliseconds; }
60 @CalledByNative("MediaMetadata")
61 int getWidth() { return mWidth; }
63 @CalledByNative("MediaMetadata")
64 int getHeight() { return mHeight; }
66 @CalledByNative("MediaMetadata")
67 boolean isSuccess() { return mSuccess; }
70 public String toString() {
71 return "MediaMetadata["
72 + "durationInMilliseconds=" + mDurationInMilliseconds
74 + ", height=" + mHeight
75 + ", success=" + mSuccess
80 public int hashCode() {
83 result = prime * result + mDurationInMilliseconds;
84 result = prime * result + mHeight;
85 result = prime * result + (mSuccess ? 1231 : 1237);
86 result = prime * result + mWidth;
91 public boolean equals(Object obj) {
96 if (getClass() != obj.getClass())
98 MediaMetadata other = (MediaMetadata)obj;
99 if (mDurationInMilliseconds != other.mDurationInMilliseconds)
101 if (mHeight != other.mHeight)
103 if (mSuccess != other.mSuccess)
105 if (mWidth != other.mWidth)
112 private static MediaMetadata extractMediaMetadata(final Context context,
114 final String cookies,
115 final String userAgent) {
116 return new MediaResourceGetter().extract(
117 context, url, cookies, userAgent);
121 MediaMetadata extract(final Context context, final String url,
122 final String cookies, final String userAgent) {
123 if (!androidDeviceOk(android.os.Build.MODEL, android.os.Build.VERSION.SDK_INT)) {
124 return EMPTY_METADATA;
127 if (!configure(context, url, cookies, userAgent)) {
128 Log.e(TAG, "Unable to configure metadata extractor");
129 return EMPTY_METADATA;
133 String durationString = extractMetadata(
134 MediaMetadataRetriever.METADATA_KEY_DURATION);
135 if (durationString == null) {
136 Log.w(TAG, "missing duration metadata");
137 return EMPTY_METADATA;
140 int durationMillis = 0;
142 durationMillis = Integer.parseInt(durationString);
143 } catch (NumberFormatException e) {
144 Log.w(TAG, "non-numeric duration: " + durationString);
145 return EMPTY_METADATA;
150 boolean hasVideo = "yes".equals(extractMetadata(
151 MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO));
152 Log.d(TAG, (hasVideo ? "resource has video" : "resource doesn't have video"));
154 String widthString = extractMetadata(
155 MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
156 if (widthString == null) {
157 Log.w(TAG, "missing video width metadata");
158 return EMPTY_METADATA;
161 width = Integer.parseInt(widthString);
162 } catch (NumberFormatException e) {
163 Log.w(TAG, "non-numeric width: " + widthString);
164 return EMPTY_METADATA;
167 String heightString = extractMetadata(
168 MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
169 if (heightString == null) {
170 Log.w(TAG, "missing video height metadata");
171 return EMPTY_METADATA;
174 height = Integer.parseInt(heightString);
175 } catch (NumberFormatException e) {
176 Log.w(TAG, "non-numeric height: " + heightString);
177 return EMPTY_METADATA;
180 MediaMetadata result = new MediaMetadata(durationMillis, width, height, true);
181 Log.d(TAG, "extracted valid metadata: " + result.toString());
183 } catch (RuntimeException e) {
184 Log.e(TAG, "Unable to extract medata", e);
185 return EMPTY_METADATA;
190 boolean configure(Context context, String url, String cookies, String userAgent) {
193 uri = URI.create(url);
194 } catch (IllegalArgumentException e) {
195 Log.e(TAG, "Cannot parse uri.", e);
198 String scheme = uri.getScheme();
199 if (scheme == null || scheme.equals("file") || scheme.equals("app")) {
200 File file = uriToFile(uri.getPath());
201 if (!file.exists()) {
202 Log.e(TAG, "File does not exist.");
205 if (!filePathAcceptable(file)) {
206 Log.e(TAG, "Refusing to read from unsafe file location.");
210 configure(file.getAbsolutePath());
212 } catch (RuntimeException e) {
213 Log.e(TAG, "Error configuring data source", e);
217 final String host = uri.getHost();
218 if (!isLoopbackAddress(host) && !isNetworkReliable(context)) {
219 Log.w(TAG, "non-file URI can't be read due to unsuitable network conditions");
222 Map<String, String> headersMap = new HashMap<String, String>();
223 if (!TextUtils.isEmpty(cookies)) {
224 headersMap.put("Cookie", cookies);
226 if (!TextUtils.isEmpty(userAgent)) {
227 headersMap.put("User-Agent", userAgent);
230 configure(url, headersMap);
232 } catch (RuntimeException e) {
233 Log.e(TAG, "Error configuring data source", e);
240 * @return true if the device is on an ethernet or wifi network.
241 * If anything goes wrong (e.g., permission denied while trying to access
242 * the network state), returns false.
245 boolean isNetworkReliable(Context context) {
246 if (context.checkCallingOrSelfPermission(
247 android.Manifest.permission.ACCESS_NETWORK_STATE) !=
248 PackageManager.PERMISSION_GRANTED) {
249 Log.w(TAG, "permission denied to access network state");
253 Integer networkType = getNetworkType(context);
254 if (networkType == null) {
257 switch (networkType.intValue()) {
258 case ConnectivityManager.TYPE_ETHERNET:
259 case ConnectivityManager.TYPE_WIFI:
260 Log.d(TAG, "ethernet/wifi connection detected");
262 case ConnectivityManager.TYPE_WIMAX:
263 case ConnectivityManager.TYPE_MOBILE:
265 Log.d(TAG, "no ethernet/wifi connection detected");
270 // This method covers only typcial expressions for the loopback address
271 // to resolve the hostname without a DNS loopup.
272 private boolean isLoopbackAddress(String host) {
273 return host != null && (host.equalsIgnoreCase("localhost") // typical hostname
274 || host.equals("127.0.0.1") // typical IP v4 expression
275 || host.equals("[::1]")); // typical IP v6 expression
279 * @param file the file whose path should be checked
280 * @return true if and only if the file is in a location that we consider
281 * safe to read from, such as /mnt/sdcard.
284 boolean filePathAcceptable(File file) {
287 path = file.getCanonicalPath();
288 } catch (IOException e) {
289 // Canonicalization has failed. Assume malicious, give up.
290 Log.w(TAG, "canonicalization of file path failed");
293 // In order to properly match the roots we must also canonicalize the
294 // well-known paths we are matching against. If we don't, then we can
295 // get unusual results in testing systems or possibly on rooted devices.
296 // Note that canonicalized directory paths always end with '/'.
297 List<String> acceptablePaths = canonicalize(getRawAcceptableDirectories());
298 acceptablePaths.add(getExternalStorageDirectory());
299 Log.d(TAG, "canonicalized file path: " + path);
300 for (String acceptablePath : acceptablePaths) {
301 if (path.startsWith(acceptablePath)) {
309 * Special case handling for device/OS combos that simply do not work.
310 * @param model the model of device being examined
311 * @param sdkVersion the version of the SDK installed on the device
312 * @return true if the device can be used correctly, otherwise false
315 static boolean androidDeviceOk(final String model, final int sdkVersion) {
316 return !("GT-I9100".contentEquals(model) &&
317 sdkVersion < android.os.Build.VERSION_CODES.JELLY_BEAN);
320 // The methods below can be used by unit tests to fake functionality.
322 File uriToFile(String path) {
323 return new File(path);
327 Integer getNetworkType(Context context) {
328 // TODO(qinmin): use ConnectionTypeObserver to listen to the network type change.
329 ConnectivityManager mConnectivityManager =
330 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
331 if (mConnectivityManager == null) {
332 Log.w(TAG, "no connectivity manager available");
335 NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
337 Log.d(TAG, "no active network");
340 return info.getType();
343 private List<String> getRawAcceptableDirectories() {
344 List<String> result = new ArrayList<String>();
345 result.add("/mnt/sdcard/");
346 result.add("/sdcard/");
350 private List<String> canonicalize(List<String> paths) {
351 List<String> result = new ArrayList<String>(paths.size());
353 for (String path : paths) {
354 result.add(new File(path).getCanonicalPath());
357 } catch (IOException e) {
358 // Canonicalization has failed. Assume malicious, give up.
359 Log.w(TAG, "canonicalization of file path failed");
365 String getExternalStorageDirectory() {
366 return PathUtils.getExternalStorageDirectory();
370 void configure(String url, Map<String,String> headers) {
371 mRetriever.setDataSource(url, headers);
375 void configure(String path) {
376 mRetriever.setDataSource(path);
380 String extractMetadata(int key) {
381 return mRetriever.extractMetadata(key);