• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4 
5 package org.chromium.content.browser;
6 
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.os.ParcelFileDescriptor;
13 import android.text.TextUtils;
14 import android.util.Log;
15 
16 import com.google.common.annotations.VisibleForTesting;
17 
18 import org.chromium.base.CalledByNative;
19 import org.chromium.base.JNINamespace;
20 import org.chromium.base.PathUtils;
21 
22 import java.io.File;
23 import java.io.IOException;
24 import java.net.URI;
25 import java.util.ArrayList;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29 
30 /**
31  * Java counterpart of android MediaResourceGetter.
32  */
33 @JNINamespace("content")
34 class MediaResourceGetter {
35 
36     private static final String TAG = "MediaResourceGetter";
37     private final MediaMetadata EMPTY_METADATA = new MediaMetadata(0,0,0,false);
38 
39     private final MediaMetadataRetriever mRetriever = new MediaMetadataRetriever();
40 
41     @VisibleForTesting
42     static class MediaMetadata {
43         private final int mDurationInMilliseconds;
44         private final int mWidth;
45         private final int mHeight;
46         private final boolean mSuccess;
47 
MediaMetadata(int durationInMilliseconds, int width, int height, boolean success)48         MediaMetadata(int durationInMilliseconds, int width, int height, boolean success) {
49             mDurationInMilliseconds = durationInMilliseconds;
50             mWidth = width;
51             mHeight = height;
52             mSuccess = success;
53         }
54 
55         // TODO(andrewhayden): according to the spec, if duration is unknown
56         // then we must return NaN. If it is unbounded, then positive infinity.
57         // http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html
58         @CalledByNative("MediaMetadata")
getDurationInMilliseconds()59         int getDurationInMilliseconds() { return mDurationInMilliseconds; }
60 
61         @CalledByNative("MediaMetadata")
getWidth()62         int getWidth() { return mWidth; }
63 
64         @CalledByNative("MediaMetadata")
getHeight()65         int getHeight() { return mHeight; }
66 
67         @CalledByNative("MediaMetadata")
isSuccess()68         boolean isSuccess() { return mSuccess; }
69 
70         @Override
toString()71         public String toString() {
72             return "MediaMetadata["
73                     + "durationInMilliseconds=" + mDurationInMilliseconds
74                     + ", width=" + mWidth
75                     + ", height=" + mHeight
76                     + ", success=" + mSuccess
77                     + "]";
78         }
79 
80         @Override
hashCode()81         public int hashCode() {
82             final int prime = 31;
83             int result = 1;
84             result = prime * result + mDurationInMilliseconds;
85             result = prime * result + mHeight;
86             result = prime * result + (mSuccess ? 1231 : 1237);
87             result = prime * result + mWidth;
88             return result;
89         }
90 
91         @Override
equals(Object obj)92         public boolean equals(Object obj) {
93             if (this == obj)
94                 return true;
95             if (obj == null)
96                 return false;
97             if (getClass() != obj.getClass())
98                 return false;
99             MediaMetadata other = (MediaMetadata)obj;
100             if (mDurationInMilliseconds != other.mDurationInMilliseconds)
101                 return false;
102             if (mHeight != other.mHeight)
103                 return false;
104             if (mSuccess != other.mSuccess)
105                 return false;
106             if (mWidth != other.mWidth)
107                 return false;
108             return true;
109         }
110     }
111 
112     @CalledByNative
extractMediaMetadata(final Context context, final String url, final String cookies, final String userAgent)113     private static MediaMetadata extractMediaMetadata(final Context context,
114                                                       final String url,
115                                                       final String cookies,
116                                                       final String userAgent) {
117         return new MediaResourceGetter().extract(
118                 context, url, cookies, userAgent);
119     }
120 
121     @CalledByNative
extractMediaMetadataFromFd(int fd, long offset, long length)122     private static MediaMetadata extractMediaMetadataFromFd(int fd,
123                                                             long offset,
124                                                             long length) {
125         return new MediaResourceGetter().extract(fd, offset, length);
126     }
127 
128     @VisibleForTesting
extract(int fd, long offset, long length)129     MediaMetadata extract(int fd, long offset, long length) {
130         if (!androidDeviceOk(android.os.Build.MODEL, android.os.Build.VERSION.SDK_INT)) {
131             return EMPTY_METADATA;
132         }
133 
134         configure(fd, offset, length);
135         return doExtractMetadata();
136     }
137 
138     @VisibleForTesting
extract(final Context context, final String url, final String cookies, final String userAgent)139     MediaMetadata extract(final Context context, final String url,
140                           final String cookies, final String userAgent) {
141         if (!androidDeviceOk(android.os.Build.MODEL, android.os.Build.VERSION.SDK_INT)) {
142             return EMPTY_METADATA;
143         }
144 
145         if (!configure(context, url, cookies, userAgent)) {
146             Log.e(TAG, "Unable to configure metadata extractor");
147             return EMPTY_METADATA;
148         }
149         return doExtractMetadata();
150     }
151 
doExtractMetadata()152     private MediaMetadata doExtractMetadata() {
153         try {
154             String durationString = extractMetadata(
155                     MediaMetadataRetriever.METADATA_KEY_DURATION);
156             if (durationString == null) {
157                 Log.w(TAG, "missing duration metadata");
158                 return EMPTY_METADATA;
159             }
160 
161             int durationMillis = 0;
162             try {
163                 durationMillis = Integer.parseInt(durationString);
164             } catch (NumberFormatException e) {
165                 Log.w(TAG, "non-numeric duration: " + durationString);
166                 return EMPTY_METADATA;
167             }
168 
169             int width = 0;
170             int height = 0;
171             boolean hasVideo = "yes".equals(extractMetadata(
172                     MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO));
173             Log.d(TAG, (hasVideo ? "resource has video" : "resource doesn't have video"));
174             if (hasVideo) {
175                 String widthString = extractMetadata(
176                         MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
177                 if (widthString == null) {
178                     Log.w(TAG, "missing video width metadata");
179                     return EMPTY_METADATA;
180                 }
181                 try {
182                     width = Integer.parseInt(widthString);
183                 } catch (NumberFormatException e) {
184                     Log.w(TAG, "non-numeric width: " + widthString);
185                     return EMPTY_METADATA;
186                 }
187 
188                 String heightString = extractMetadata(
189                         MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
190                 if (heightString == null) {
191                     Log.w(TAG, "missing video height metadata");
192                     return EMPTY_METADATA;
193                 }
194                 try {
195                     height = Integer.parseInt(heightString);
196                 } catch (NumberFormatException e) {
197                     Log.w(TAG, "non-numeric height: " + heightString);
198                     return EMPTY_METADATA;
199                 }
200             }
201             MediaMetadata result = new MediaMetadata(durationMillis, width, height, true);
202             Log.d(TAG, "extracted valid metadata: " + result.toString());
203             return result;
204         } catch (RuntimeException e) {
205             Log.e(TAG, "Unable to extract medata", e);
206             return EMPTY_METADATA;
207         }
208     }
209 
210     @VisibleForTesting
configure(Context context, String url, String cookies, String userAgent)211     boolean configure(Context context, String url, String cookies, String userAgent) {
212         URI uri;
213         try {
214             uri = URI.create(url);
215         } catch (IllegalArgumentException  e) {
216             Log.e(TAG, "Cannot parse uri.", e);
217             return false;
218         }
219         String scheme = uri.getScheme();
220         if (scheme == null || scheme.equals("file")) {
221             File file = uriToFile(uri.getPath());
222             if (!file.exists()) {
223                 Log.e(TAG, "File does not exist.");
224                 return false;
225             }
226             if (!filePathAcceptable(file)) {
227                 Log.e(TAG, "Refusing to read from unsafe file location.");
228                 return false;
229             }
230             try {
231                 configure(file.getAbsolutePath());
232                 return true;
233             } catch (RuntimeException e) {
234                 Log.e(TAG, "Error configuring data source", e);
235                 return false;
236             }
237         } else {
238             final String host = uri.getHost();
239             if (!isLoopbackAddress(host) && !isNetworkReliable(context)) {
240                 Log.w(TAG, "non-file URI can't be read due to unsuitable network conditions");
241                 return false;
242             }
243             Map<String, String> headersMap = new HashMap<String, String>();
244             if (!TextUtils.isEmpty(cookies)) {
245                 headersMap.put("Cookie", cookies);
246             }
247             if (!TextUtils.isEmpty(userAgent)) {
248                 headersMap.put("User-Agent", userAgent);
249             }
250             try {
251                 configure(url, headersMap);
252                 return true;
253             } catch (RuntimeException e) {
254                 Log.e(TAG, "Error configuring data source", e);
255                 return false;
256             }
257         }
258     }
259 
260     /**
261      * @return true if the device is on an ethernet or wifi network.
262      * If anything goes wrong (e.g., permission denied while trying to access
263      * the network state), returns false.
264      */
265     @VisibleForTesting
isNetworkReliable(Context context)266     boolean isNetworkReliable(Context context) {
267         if (context.checkCallingOrSelfPermission(
268                 android.Manifest.permission.ACCESS_NETWORK_STATE) !=
269                 PackageManager.PERMISSION_GRANTED) {
270             Log.w(TAG, "permission denied to access network state");
271             return false;
272         }
273 
274         Integer networkType = getNetworkType(context);
275         if (networkType == null) {
276             return false;
277         }
278         switch (networkType.intValue()) {
279             case ConnectivityManager.TYPE_ETHERNET:
280             case ConnectivityManager.TYPE_WIFI:
281                 Log.d(TAG, "ethernet/wifi connection detected");
282                 return true;
283             case ConnectivityManager.TYPE_WIMAX:
284             case ConnectivityManager.TYPE_MOBILE:
285             default:
286                 Log.d(TAG, "no ethernet/wifi connection detected");
287                 return false;
288         }
289     }
290 
291     // This method covers only typcial expressions for the loopback address
292     // to resolve the hostname without a DNS loopup.
isLoopbackAddress(String host)293     private boolean isLoopbackAddress(String host) {
294         return host != null && (host.equalsIgnoreCase("localhost")  // typical hostname
295                 || host.equals("127.0.0.1")  // typical IP v4 expression
296                 || host.equals("[::1]"));  // typical IP v6 expression
297     }
298 
299     /**
300      * @param file the file whose path should be checked
301      * @return true if and only if the file is in a location that we consider
302      * safe to read from, such as /mnt/sdcard.
303      */
304     @VisibleForTesting
filePathAcceptable(File file)305     boolean filePathAcceptable(File file) {
306         final String path;
307         try {
308             path = file.getCanonicalPath();
309         } catch (IOException e) {
310             // Canonicalization has failed. Assume malicious, give up.
311             Log.w(TAG, "canonicalization of file path failed");
312             return false;
313         }
314         // In order to properly match the roots we must also canonicalize the
315         // well-known paths we are matching against. If we don't, then we can
316         // get unusual results in testing systems or possibly on rooted devices.
317         // Note that canonicalized directory paths always end with '/'.
318         List<String> acceptablePaths = canonicalize(getRawAcceptableDirectories());
319         acceptablePaths.add(getExternalStorageDirectory());
320         Log.d(TAG, "canonicalized file path: " + path);
321         for (String acceptablePath : acceptablePaths) {
322             if (path.startsWith(acceptablePath)) {
323                 return true;
324             }
325         }
326         return false;
327     }
328 
329     /**
330      * Special case handling for device/OS combos that simply do not work.
331      * @param model the model of device being examined
332      * @param sdkVersion the version of the SDK installed on the device
333      * @return true if the device can be used correctly, otherwise false
334      */
335     @VisibleForTesting
androidDeviceOk(final String model, final int sdkVersion)336     static boolean androidDeviceOk(final String model, final int sdkVersion) {
337         return !("GT-I9100".contentEquals(model) &&
338                  sdkVersion < android.os.Build.VERSION_CODES.JELLY_BEAN);
339     }
340 
341     // The methods below can be used by unit tests to fake functionality.
342     @VisibleForTesting
uriToFile(String path)343     File uriToFile(String path) {
344         return new File(path);
345     }
346 
347     @VisibleForTesting
getNetworkType(Context context)348     Integer getNetworkType(Context context) {
349         // TODO(qinmin): use ConnectionTypeObserver to listen to the network type change.
350         ConnectivityManager mConnectivityManager =
351                 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
352         if (mConnectivityManager == null) {
353             Log.w(TAG, "no connectivity manager available");
354             return null;
355         }
356         NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
357         if (info == null) {
358             Log.d(TAG, "no active network");
359             return null;
360         }
361         return info.getType();
362     }
363 
getRawAcceptableDirectories()364     private List<String> getRawAcceptableDirectories() {
365         List<String> result = new ArrayList<String>();
366         result.add("/mnt/sdcard/");
367         result.add("/sdcard/");
368         return result;
369     }
370 
canonicalize(List<String> paths)371     private List<String> canonicalize(List<String> paths) {
372         List<String> result = new ArrayList<String>(paths.size());
373         try {
374             for (String path : paths) {
375                 result.add(new File(path).getCanonicalPath());
376             }
377             return result;
378         } catch (IOException e) {
379             // Canonicalization has failed. Assume malicious, give up.
380             Log.w(TAG, "canonicalization of file path failed");
381         }
382         return result;
383     }
384 
385     @VisibleForTesting
getExternalStorageDirectory()386     String getExternalStorageDirectory() {
387         return PathUtils.getExternalStorageDirectory();
388     }
389 
390     @VisibleForTesting
configure(int fd, long offset, long length)391     void configure(int fd, long offset, long length) {
392         ParcelFileDescriptor parcelFd = ParcelFileDescriptor.adoptFd(fd);
393         try {
394             mRetriever.setDataSource(parcelFd.getFileDescriptor(),
395                     offset, length);
396         } finally {
397             try {
398                 parcelFd.close();
399             } catch (IOException e) {
400                 Log.e(TAG, "Failed to close file descriptor: " + e);
401             }
402         }
403     }
404 
405     @VisibleForTesting
configure(String url, Map<String,String> headers)406     void configure(String url, Map<String,String> headers) {
407         mRetriever.setDataSource(url, headers);
408     }
409 
410     @VisibleForTesting
configure(String path)411     void configure(String path) {
412         mRetriever.setDataSource(path);
413     }
414 
415     @VisibleForTesting
extractMetadata(int key)416     String extractMetadata(int key) {
417         return mRetriever.extractMetadata(key);
418     }
419 }
420