OLD | NEW |
---|---|
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 } | |
OLD | NEW |