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

Side by Side 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: Add unit tests and restructure the class to be testable. Created 7 years, 3 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 unified diff | 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 »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 package org.chromium.content.browser; 5 package org.chromium.content.browser;
6 6
7 import android.content.Context; 7 import android.content.Context;
8 import android.content.pm.PackageManager; 8 import android.content.pm.PackageManager;
9 import android.media.MediaMetadataRetriever; 9 import android.media.MediaMetadataRetriever;
10 import android.net.ConnectivityManager; 10 import android.net.ConnectivityManager;
11 import android.net.NetworkInfo; 11 import android.net.NetworkInfo;
12 import android.net.Uri; 12 import android.net.Uri;
13 import android.text.TextUtils; 13 import android.text.TextUtils;
14 import android.util.Log; 14 import android.util.Log;
15 15
16 import com.google.common.annotations.VisibleForTesting;
17
16 import org.chromium.base.CalledByNative; 18 import org.chromium.base.CalledByNative;
17 import org.chromium.base.JNINamespace; 19 import org.chromium.base.JNINamespace;
18 import org.chromium.base.PathUtils; 20 import org.chromium.base.PathUtils;
19 21
20 import java.io.File; 22 import java.io.File;
23 import java.io.IOException;
21 import java.util.HashMap; 24 import java.util.HashMap;
25 import java.util.Map;
22 26
23 /** 27 /**
24 * Java counterpart of android MediaResourceGetter. 28 * Java counterpart of android MediaResourceGetter.
25 */ 29 */
26 @JNINamespace("content") 30 @JNINamespace("content")
27 class MediaResourceGetter { 31 class MediaResourceGetter {
28 32
29 private static final String TAG = "MediaResourceGetter"; 33 private static final String TAG = "MediaResourceGetter";
30 34 private static final MediaMetadata EMPTY_METADATA = new MediaMetadata(0,0,0, false);
qinmin 2013/08/30 16:53:34 don't use static final on non-primitive objects. t
Andrew Hayden (chromium.org) 2013/08/30 17:10:09 I will make it non-static.
31 private static class MediaMetadata { 35
36 private final MediaMetadataRetriever mRetriever = new MediaMetadataRetriever ();
37
38 @VisibleForTesting
39 static class MediaMetadata {
32 private final int mDurationInMilliseconds; 40 private final int mDurationInMilliseconds;
33 private final int mWidth; 41 private final int mWidth;
34 private final int mHeight; 42 private final int mHeight;
35 private final boolean mSuccess; 43 private final boolean mSuccess;
36 44
37 private MediaMetadata(int durationInMilliseconds, int width, int height, boolean success) { 45 MediaMetadata(int durationInMilliseconds, int width, int height, boolean success) {
38 mDurationInMilliseconds = durationInMilliseconds; 46 mDurationInMilliseconds = durationInMilliseconds;
39 mWidth = width; 47 mWidth = width;
40 mHeight = height; 48 mHeight = height;
41 mSuccess = success; 49 mSuccess = success;
42 } 50 }
43 51
44 @CalledByNative("MediaMetadata") 52 // TODO(andrewhayden): according to the spec, if duration is unknown
45 private int getDurationInMilliseconds() { return mDurationInMilliseconds ; } 53 // then we must return NaN. If it is unbounded, then positive infinity.
46 54 // http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html
47 @CalledByNative("MediaMetadata") 55 @CalledByNative("MediaMetadata")
48 private int getWidth() { return mWidth; } 56 int getDurationInMilliseconds() { return mDurationInMilliseconds; }
49 57
50 @CalledByNative("MediaMetadata") 58 @CalledByNative("MediaMetadata")
51 private int getHeight() { return mHeight; } 59 int getWidth() { return mWidth; }
52 60
53 @CalledByNative("MediaMetadata") 61 @CalledByNative("MediaMetadata")
54 private boolean isSuccess() { return mSuccess; } 62 int getHeight() { return mHeight; }
63
64 @CalledByNative("MediaMetadata")
65 boolean isSuccess() { return mSuccess; }
66
67 @Override
68 public String toString() {
69 return "MediaMetadata["
70 + "durationInMilliseconds=" + mDurationInMilliseconds
71 + ", width=" + mWidth
72 + ", height=" + mHeight
73 + ", success=" + mSuccess
74 + "]";
75 }
76
77 @Override
78 public int hashCode() {
79 final int prime = 31;
80 int result = 1;
81 result = prime * result + mDurationInMilliseconds;
qinmin 2013/08/30 16:53:34 why we need this hash function? mDurationInMillise
Andrew Hayden (chromium.org) 2013/08/30 17:10:09 Java best practices dictate always generating hash
82 result = prime * result + mHeight;
83 result = prime * result + (mSuccess ? 1231 : 1237);
84 result = prime * result + mWidth;
85 return result;
86 }
87
88 @Override
89 public boolean equals(Object obj) {
90 if (this == obj)
91 return true;
92 if (obj == null)
93 return false;
94 if (getClass() != obj.getClass())
95 return false;
96 MediaMetadata other = (MediaMetadata)obj;
97 if (mDurationInMilliseconds != other.mDurationInMilliseconds)
98 return false;
99 if (mHeight != other.mHeight)
100 return false;
101 if (mSuccess != other.mSuccess)
102 return false;
103 if (mWidth != other.mWidth)
104 return false;
105 return true;
106 }
55 } 107 }
56 108
57 @CalledByNative 109 @CalledByNative
58 private static MediaMetadata extractMediaMetadata(Context context, String ur l, String cookies) { 110 private static MediaMetadata extractMediaMetadata(Context context, String ur l, String cookies) {
59 int durationInMilliseconds = 0; 111 return new MediaResourceGetter().extract(context, url, cookies);
60 int width = 0; 112 }
61 int height = 0; 113
62 boolean success = false; 114 @VisibleForTesting
115 MediaMetadata extract(Context context, String url, String cookies) {
116 if (!configure(context, url, cookies)) {
117 Log.e(TAG, "Unable to configure metadata extractor");
118 return EMPTY_METADATA;
119 }
120
121 try {
122 String durationString = extractMetadata(
123 MediaMetadataRetriever.METADATA_KEY_DURATION);
124 if (durationString == null) {
125 Log.w(TAG, "missing duration metadata");
126 return EMPTY_METADATA;
127 }
128
129 int durationMillis = 0;
130 try {
131 durationMillis = Integer.parseInt(durationString);
132 } catch (NumberFormatException e) {
133 Log.w(TAG, "non-numeric duration: " + durationString);
134 return EMPTY_METADATA;
135 }
136
137 int width = 0;
138 int height = 0;
139 boolean hasVideo = "yes".equals(extractMetadata(
140 MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO));
141 Log.d(TAG, (hasVideo ? "resource has video" : "resource doesn't have video"));
142 if (hasVideo) {
143 String widthString = extractMetadata(
144 MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
145 if (widthString == null) {
146 Log.w(TAG, "missing video width metadata");
147 return EMPTY_METADATA;
148 }
149 try {
150 width = Integer.parseInt(widthString);
151 } catch (NumberFormatException e) {
152 Log.w(TAG, "non-numeric width: " + widthString);
153 return EMPTY_METADATA;
154 }
155
156 String heightString = extractMetadata(
157 MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
158 if (heightString == null) {
159 Log.w(TAG, "missing video height metadata");
160 return EMPTY_METADATA;
161 }
162 try {
163 height = Integer.parseInt(heightString);
164 } catch (NumberFormatException e) {
165 Log.w(TAG, "non-numeric height: " + heightString);
166 return EMPTY_METADATA;
167 }
168 }
169 MediaMetadata result = new MediaMetadata(durationMillis, width, heig ht, true);
170 Log.d(TAG, "extracted valid metadata: " + result.toString());
171 return result;
172 } catch (RuntimeException e) {
173 Log.e(TAG, "Unable to extract medata", e);
174 return EMPTY_METADATA;
175 }
176 }
177
178 @VisibleForTesting
179 boolean configure(Context context, String url, String cookies) {
180 Uri uri = Uri.parse(url);
181 String scheme = uri.getScheme();
182 if (scheme == null || scheme.equals("file")) {
183 File file = uriToFile(uri.getPath());
184 if (!file.exists()) {
185 Log.e(TAG, "File does not exist.");
186 return false;
187 }
188 if (!filePathAcceptable(file)) {
189 Log.e(TAG, "Refusing to read from unsafe file location.");
190 return false;
191 }
192 try {
193 configure(file.getAbsolutePath());
194 return true;
195 } catch (RuntimeException e) {
196 Log.e(TAG, "Error configuring data source", e);
197 return false;
198 }
199 } else {
200 if (!networkEnvironmentOk(context)) {
201 Log.w(TAG, "non-file URI can't be read due to unsuitable network conditions");
202 return false;
203 }
204 Map<String, String> headersMap = new HashMap<String, String>();
205 if (!TextUtils.isEmpty(cookies)) {
206 headersMap.put("Cookie", cookies);
207 }
208 try {
209 configure(url, headersMap);
210 return true;
211 } catch (RuntimeException e) {
212 Log.e(TAG, "Error configuring data source", e);
213 return false;
214 }
215 }
216 }
217
218 /**
219 * @return true if the device is on an ethernet or wifi network.
220 * If anything goes wrong (e.g., permission denied while trying to access
221 * the network state), returns false.
222 */
223 @VisibleForTesting
224 boolean networkEnvironmentOk(Context context) {
qinmin 2013/08/30 16:53:34 the functionName is very misleading. what about Is
Andrew Hayden (chromium.org) 2013/08/30 17:10:09 Works for me. Good suggestion, thanks. Long term I
qinmin 2013/08/30 17:25:19 This won't impact streaming. It only saves some un
225 if (context.checkCallingOrSelfPermission(
226 android.Manifest.permission.ACCESS_NETWORK_STATE) !=
227 PackageManager.PERMISSION_GRANTED) {
228 Log.w(TAG, "permission denied to access network state");
229 return false;
230 }
231
232 Integer networkType = getNetworkType(context);
233 if (networkType == null) {
234 return false;
235 }
236 switch (networkType.intValue()) {
237 case ConnectivityManager.TYPE_ETHERNET:
238 case ConnectivityManager.TYPE_WIFI:
239 Log.d(TAG, "ethernet/wifi connection detected");
240 return true;
241 case ConnectivityManager.TYPE_WIMAX:
242 case ConnectivityManager.TYPE_MOBILE:
243 default:
244 Log.d(TAG, "no ethernet/wifi connection detected");
245 return false;
246 }
247 }
248
249 /**
250 * @param file the file whose path should be checked
251 * @return true if and only if the file is in a location that we consider
252 * safe to read from, such as /mnt/sdcard.
253 */
254 @VisibleForTesting
255 boolean filePathAcceptable(File file) {
256 try {
257 // We must canonicalize the file. However, in order to properly
258 // match the roots we must also canonicalize the well-known
259 // paths we are matching against. If we don't, then we can
260 // get unusual results in testing systems or possibly on rooted
261 // devices.
262 //
263 // Note that canonicalized directory paths always end with '/'.
264 final String canonicalMntSdcard = new File("/mnt/sdcard/").getCanoni calPath();
265 final String canonicalSdcard = new File("/sdcard/").getCanonicalPath ();
266 String path = file.getCanonicalPath();
267 Log.d(TAG, "canonicalized file path: " + path);
268 return path.startsWith(canonicalMntSdcard) ||
269 path.startsWith(canonicalSdcard) ||
270 path.startsWith(getExternalStorageDirectory());
271 } catch (IOException e) {
272 // Canonicalization has failed. Assume malicious, give up.
273 Log.w(TAG, "canonicalization of file path failed");
274 return false;
275 }
276 }
277
278 // The methods below can be used by unit tests to fake functionality.
279 @VisibleForTesting
280 File uriToFile(String path) {
281 return new File(path);
282 }
283
284 @VisibleForTesting
285 Integer getNetworkType(Context context) {
63 // TODO(qinmin): use ConnectionTypeObserver to listen to the network typ e change. 286 // TODO(qinmin): use ConnectionTypeObserver to listen to the network typ e change.
64 ConnectivityManager mConnectivityManager = 287 ConnectivityManager mConnectivityManager =
65 (ConnectivityManager) context.getSystemService(Context.CONNECTIV ITY_SERVICE); 288 (ConnectivityManager) context.getSystemService(Context.CONNECTIV ITY_SERVICE);
66 if (mConnectivityManager != null) { 289 if (mConnectivityManager == null) {
67 if (context.checkCallingOrSelfPermission( 290 Log.w(TAG, "no connectivity manager available");
68 android.Manifest.permission.ACCESS_NETWORK_STATE) != 291 return null;
69 PackageManager.PERMISSION_GRANTED) { 292 }
70 return new MediaMetadata(0, 0, 0, false); 293 NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
71 } 294 if (info == null) {
72 295 Log.d(TAG, "no active network");
73 NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); 296 return null;
74 if (info == null) { 297 }
75 return new MediaMetadata(durationInMilliseconds, width, height, success); 298 return info.getType();
76 } 299 }
77 switch (info.getType()) { 300
78 case ConnectivityManager.TYPE_ETHERNET: 301 @VisibleForTesting
79 case ConnectivityManager.TYPE_WIFI: 302 String getExternalStorageDirectory() {
80 break; 303 return PathUtils.getExternalStorageDirectory();
81 case ConnectivityManager.TYPE_WIMAX: 304 }
82 case ConnectivityManager.TYPE_MOBILE: 305
83 default: 306 @VisibleForTesting
84 return new MediaMetadata(durationInMilliseconds, width, heig ht, success); 307 void configure(String url, Map<String,String> headers) {
85 } 308 mRetriever.setDataSource(url, headers);
86 } 309 }
87 310
88 MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 311 @VisibleForTesting
89 try { 312 void configure(String path) {
90 Uri uri = Uri.parse(url); 313 mRetriever.setDataSource(path);
91 String scheme = uri.getScheme(); 314 }
92 if (scheme == null || scheme.equals("file")) { 315
93 File file = new File(uri.getPath()); 316 @VisibleForTesting
94 String path = file.getAbsolutePath(); 317 String extractMetadata(int key) {
95 if (file.exists() && (path.startsWith("/mnt/sdcard/") || 318 return mRetriever.extractMetadata(key);
96 path.startsWith("/sdcard/") || 319 }
97 path.startsWith(PathUtils.getExternalStorageDirectory()) )) { 320 }
98 retriever.setDataSource(path);
99 } else {
100 Log.e(TAG, "Unable to read file: " + url);
101 return new MediaMetadata(durationInMilliseconds, width, heig ht, success);
102 }
103 } else {
104 HashMap<String, String> headersMap = new HashMap<String, String> ();
105 if (!TextUtils.isEmpty(cookies)) {
106 headersMap.put("Cookie", cookies);
107 }
108 retriever.setDataSource(url, headersMap);
109 }
110 String duration = retriever.extractMetadata(
111 MediaMetadataRetriever.METADATA_KEY_DURATION);
112 String videoWidth = retriever.extractMetadata(
113 MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
114 String videoHeight = retriever.extractMetadata(
115 MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
116 if (duration == null || videoWidth == null || videoHeight == null) {
117 return new MediaMetadata(durationInMilliseconds, width, height, success);
118 }
119 durationInMilliseconds = Integer.parseInt(duration);
120 width = Integer.parseInt(videoWidth);
121 height = Integer.parseInt(videoHeight);
122 success = true;
123 } catch (IllegalArgumentException e) {
124 Log.e(TAG, "Invalid url: " + e);
125 } catch (RuntimeException e) {
126 Log.e(TAG, "Invalid url: " + e);
127 }
128 return new MediaMetadata(durationInMilliseconds, width, height, success) ;
129 }
130 }
OLDNEW
« 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