Chromium Code Reviews| 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 |