63c829e04f06f73b83e7491965c323e7814abc33
[platform/framework/web/crosswalk.git] / src / content / public / android / java / src / org / chromium / content / browser / MediaResourceGetter.java
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.text.TextUtils;
13 import android.util.Log;
14
15 import com.google.common.annotations.VisibleForTesting;
16
17 import org.chromium.base.CalledByNative;
18 import org.chromium.base.JNINamespace;
19 import org.chromium.base.PathUtils;
20
21 import java.io.File;
22 import java.io.IOException;
23 import java.net.URI;
24 import java.util.ArrayList;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28
29 /**
30  * Java counterpart of android MediaResourceGetter.
31  */
32 @JNINamespace("content")
33 class MediaResourceGetter {
34
35     private static final String TAG = "MediaResourceGetter";
36     private final MediaMetadata EMPTY_METADATA = new MediaMetadata(0,0,0,false);
37
38     private final MediaMetadataRetriever mRetriever = new MediaMetadataRetriever();
39
40     @VisibleForTesting
41     static class MediaMetadata {
42         private final int mDurationInMilliseconds;
43         private final int mWidth;
44         private final int mHeight;
45         private final boolean mSuccess;
46
47         MediaMetadata(int durationInMilliseconds, int width, int height, boolean success) {
48             mDurationInMilliseconds = durationInMilliseconds;
49             mWidth = width;
50             mHeight = height;
51             mSuccess = success;
52         }
53
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; }
59
60         @CalledByNative("MediaMetadata")
61         int getWidth() { return mWidth; }
62
63         @CalledByNative("MediaMetadata")
64         int getHeight() { return mHeight; }
65
66         @CalledByNative("MediaMetadata")
67         boolean isSuccess() { return mSuccess; }
68
69         @Override
70         public String toString() {
71             return "MediaMetadata["
72                     + "durationInMilliseconds=" + mDurationInMilliseconds
73                     + ", width=" + mWidth
74                     + ", height=" + mHeight
75                     + ", success=" + mSuccess
76                     + "]";
77         }
78
79         @Override
80         public int hashCode() {
81             final int prime = 31;
82             int result = 1;
83             result = prime * result + mDurationInMilliseconds;
84             result = prime * result + mHeight;
85             result = prime * result + (mSuccess ? 1231 : 1237);
86             result = prime * result + mWidth;
87             return result;
88         }
89
90         @Override
91         public boolean equals(Object obj) {
92             if (this == obj)
93                 return true;
94             if (obj == null)
95                 return false;
96             if (getClass() != obj.getClass())
97                 return false;
98             MediaMetadata other = (MediaMetadata)obj;
99             if (mDurationInMilliseconds != other.mDurationInMilliseconds)
100                 return false;
101             if (mHeight != other.mHeight)
102                 return false;
103             if (mSuccess != other.mSuccess)
104                 return false;
105             if (mWidth != other.mWidth)
106                 return false;
107             return true;
108         }
109     }
110
111     @CalledByNative
112     private static MediaMetadata extractMediaMetadata(final Context context,
113                                                       final String url,
114                                                       final String cookies,
115                                                       final String userAgent) {
116         return new MediaResourceGetter().extract(
117                 context, url, cookies, userAgent);
118     }
119
120     @VisibleForTesting
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;
125         }
126
127         if (!configure(context, url, cookies, userAgent)) {
128             Log.e(TAG, "Unable to configure metadata extractor");
129             return EMPTY_METADATA;
130         }
131
132         try {
133             String durationString = extractMetadata(
134                     MediaMetadataRetriever.METADATA_KEY_DURATION);
135             if (durationString == null) {
136                 Log.w(TAG, "missing duration metadata");
137                 return EMPTY_METADATA;
138             }
139
140             int durationMillis = 0;
141             try {
142                 durationMillis = Integer.parseInt(durationString);
143             } catch (NumberFormatException e) {
144                 Log.w(TAG, "non-numeric duration: " + durationString);
145                 return EMPTY_METADATA;
146             }
147
148             int width = 0;
149             int height = 0;
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"));
153             if (hasVideo) {
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;
159                 }
160                 try {
161                     width = Integer.parseInt(widthString);
162                 } catch (NumberFormatException e) {
163                     Log.w(TAG, "non-numeric width: " + widthString);
164                     return EMPTY_METADATA;
165                 }
166
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;
172                 }
173                 try {
174                     height = Integer.parseInt(heightString);
175                 } catch (NumberFormatException e) {
176                     Log.w(TAG, "non-numeric height: " + heightString);
177                     return EMPTY_METADATA;
178                 }
179             }
180             MediaMetadata result = new MediaMetadata(durationMillis, width, height, true);
181             Log.d(TAG, "extracted valid metadata: " + result.toString());
182             return result;
183         } catch (RuntimeException e) {
184             Log.e(TAG, "Unable to extract medata", e);
185             return EMPTY_METADATA;
186         }
187     }
188
189     @VisibleForTesting
190     boolean configure(Context context, String url, String cookies, String userAgent) {
191         URI uri;
192         try {
193             uri = URI.create(url);
194         } catch (IllegalArgumentException  e) {
195             Log.e(TAG, "Cannot parse uri.", e);
196             return false;
197         }
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.");
203                 return false;
204             }
205             if (!filePathAcceptable(file)) {
206                 Log.e(TAG, "Refusing to read from unsafe file location.");
207                 return false;
208             }
209             try {
210                 configure(file.getAbsolutePath());
211                 return true;
212             } catch (RuntimeException e) {
213                 Log.e(TAG, "Error configuring data source", e);
214                 return false;
215             }
216         } else {
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");
220                 return false;
221             }
222             Map<String, String> headersMap = new HashMap<String, String>();
223             if (!TextUtils.isEmpty(cookies)) {
224                 headersMap.put("Cookie", cookies);
225             }
226             if (!TextUtils.isEmpty(userAgent)) {
227                 headersMap.put("User-Agent", userAgent);
228             }
229             try {
230                 configure(url, headersMap);
231                 return true;
232             } catch (RuntimeException e) {
233                 Log.e(TAG, "Error configuring data source", e);
234                 return false;
235             }
236         }
237     }
238
239     /**
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.
243      */
244     @VisibleForTesting
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");
250             return false;
251         }
252
253         Integer networkType = getNetworkType(context);
254         if (networkType == null) {
255             return false;
256         }
257         switch (networkType.intValue()) {
258             case ConnectivityManager.TYPE_ETHERNET:
259             case ConnectivityManager.TYPE_WIFI:
260                 Log.d(TAG, "ethernet/wifi connection detected");
261                 return true;
262             case ConnectivityManager.TYPE_WIMAX:
263             case ConnectivityManager.TYPE_MOBILE:
264             default:
265                 Log.d(TAG, "no ethernet/wifi connection detected");
266                 return false;
267         }
268     }
269
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
276     }
277
278     /**
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.
282      */
283     @VisibleForTesting
284     boolean filePathAcceptable(File file) {
285         final String path;
286         try {
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");
291             return false;
292         }
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)) {
302                 return true;
303             }
304         }
305         return false;
306     }
307
308     /**
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
313      */
314     @VisibleForTesting
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);
318     }
319
320     // The methods below can be used by unit tests to fake functionality.
321     @VisibleForTesting
322     File uriToFile(String path) {
323         return new File(path);
324     }
325
326     @VisibleForTesting
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");
333             return null;
334         }
335         NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
336         if (info == null) {
337             Log.d(TAG, "no active network");
338             return null;
339         }
340         return info.getType();
341     }
342
343     private List<String> getRawAcceptableDirectories() {
344         List<String> result = new ArrayList<String>();
345         result.add("/mnt/sdcard/");
346         result.add("/sdcard/");
347         return result;
348     }
349
350     private List<String> canonicalize(List<String> paths) {
351         List<String> result = new ArrayList<String>(paths.size());
352         try {
353             for (String path : paths) {
354                 result.add(new File(path).getCanonicalPath());
355             }
356             return result;
357         } catch (IOException e) {
358             // Canonicalization has failed. Assume malicious, give up.
359             Log.w(TAG, "canonicalization of file path failed");
360         }
361         return result;
362     }
363
364     @VisibleForTesting
365     String getExternalStorageDirectory() {
366         return PathUtils.getExternalStorageDirectory();
367     }
368
369     @VisibleForTesting
370     void configure(String url, Map<String,String> headers) {
371         mRetriever.setDataSource(url, headers);
372     }
373
374     @VisibleForTesting
375     void configure(String path) {
376         mRetriever.setDataSource(path);
377     }
378
379     @VisibleForTesting
380     String extractMetadata(int key) {
381         return mRetriever.extractMetadata(key);
382     }
383 }