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