MediaResourceGetter.java revision c5cede9ae108bb15f6b7a8aea21c7e1fefa2834c
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.net.Uri; 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.util.ArrayList; 25import java.util.HashMap; 26import java.util.List; 27import java.util.Map; 28 29/** 30 * Java counterpart of android MediaResourceGetter. 31 */ 32@JNINamespace("content") 33class 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 = Uri.parse(url); 192 String scheme = uri.getScheme(); 193 if (scheme == null || scheme.equals("file")) { 194 File file = uriToFile(uri.getPath()); 195 if (!file.exists()) { 196 Log.e(TAG, "File does not exist."); 197 return false; 198 } 199 if (!filePathAcceptable(file)) { 200 Log.e(TAG, "Refusing to read from unsafe file location."); 201 return false; 202 } 203 try { 204 configure(file.getAbsolutePath()); 205 return true; 206 } catch (RuntimeException e) { 207 Log.e(TAG, "Error configuring data source", e); 208 return false; 209 } 210 } else { 211 if (!isNetworkReliable(context)) { 212 Log.w(TAG, "non-file URI can't be read due to unsuitable network conditions"); 213 return false; 214 } 215 Map<String, String> headersMap = new HashMap<String, String>(); 216 if (!TextUtils.isEmpty(cookies)) { 217 headersMap.put("Cookie", cookies); 218 } 219 if (!TextUtils.isEmpty(userAgent)) { 220 headersMap.put("User-Agent", userAgent); 221 } 222 try { 223 configure(url, headersMap); 224 return true; 225 } catch (RuntimeException e) { 226 Log.e(TAG, "Error configuring data source", e); 227 return false; 228 } 229 } 230 } 231 232 /** 233 * @return true if the device is on an ethernet or wifi network. 234 * If anything goes wrong (e.g., permission denied while trying to access 235 * the network state), returns false. 236 */ 237 @VisibleForTesting 238 boolean isNetworkReliable(Context context) { 239 if (context.checkCallingOrSelfPermission( 240 android.Manifest.permission.ACCESS_NETWORK_STATE) != 241 PackageManager.PERMISSION_GRANTED) { 242 Log.w(TAG, "permission denied to access network state"); 243 return false; 244 } 245 246 Integer networkType = getNetworkType(context); 247 if (networkType == null) { 248 return false; 249 } 250 switch (networkType.intValue()) { 251 case ConnectivityManager.TYPE_ETHERNET: 252 case ConnectivityManager.TYPE_WIFI: 253 Log.d(TAG, "ethernet/wifi connection detected"); 254 return true; 255 case ConnectivityManager.TYPE_WIMAX: 256 case ConnectivityManager.TYPE_MOBILE: 257 default: 258 Log.d(TAG, "no ethernet/wifi connection detected"); 259 return false; 260 } 261 } 262 263 /** 264 * @param file the file whose path should be checked 265 * @return true if and only if the file is in a location that we consider 266 * safe to read from, such as /mnt/sdcard. 267 */ 268 @VisibleForTesting 269 boolean filePathAcceptable(File file) { 270 final String path; 271 try { 272 path = file.getCanonicalPath(); 273 } catch (IOException e) { 274 // Canonicalization has failed. Assume malicious, give up. 275 Log.w(TAG, "canonicalization of file path failed"); 276 return false; 277 } 278 // In order to properly match the roots we must also canonicalize the 279 // well-known paths we are matching against. If we don't, then we can 280 // get unusual results in testing systems or possibly on rooted devices. 281 // Note that canonicalized directory paths always end with '/'. 282 List<String> acceptablePaths = canonicalize(getRawAcceptableDirectories()); 283 acceptablePaths.add(getExternalStorageDirectory()); 284 Log.d(TAG, "canonicalized file path: " + path); 285 for (String acceptablePath : acceptablePaths) { 286 if (path.startsWith(acceptablePath)) { 287 return true; 288 } 289 } 290 return false; 291 } 292 293 /** 294 * Special case handling for device/OS combos that simply do not work. 295 * @param model the model of device being examined 296 * @param sdkVersion the version of the SDK installed on the device 297 * @return true if the device can be used correctly, otherwise false 298 */ 299 @VisibleForTesting 300 static boolean androidDeviceOk(final String model, final int sdkVersion) { 301 return !("GT-I9100".contentEquals(model) && 302 sdkVersion < android.os.Build.VERSION_CODES.JELLY_BEAN); 303 } 304 305 // The methods below can be used by unit tests to fake functionality. 306 @VisibleForTesting 307 File uriToFile(String path) { 308 return new File(path); 309 } 310 311 @VisibleForTesting 312 Integer getNetworkType(Context context) { 313 // TODO(qinmin): use ConnectionTypeObserver to listen to the network type change. 314 ConnectivityManager mConnectivityManager = 315 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 316 if (mConnectivityManager == null) { 317 Log.w(TAG, "no connectivity manager available"); 318 return null; 319 } 320 NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); 321 if (info == null) { 322 Log.d(TAG, "no active network"); 323 return null; 324 } 325 return info.getType(); 326 } 327 328 private List<String> getRawAcceptableDirectories() { 329 List<String> result = new ArrayList<String>(); 330 result.add("/mnt/sdcard/"); 331 result.add("/sdcard/"); 332 return result; 333 } 334 335 private List<String> canonicalize(List<String> paths) { 336 List<String> result = new ArrayList<String>(paths.size()); 337 try { 338 for (String path : paths) { 339 result.add(new File(path).getCanonicalPath()); 340 } 341 return result; 342 } catch (IOException e) { 343 // Canonicalization has failed. Assume malicious, give up. 344 Log.w(TAG, "canonicalization of file path failed"); 345 } 346 return result; 347 } 348 349 @VisibleForTesting 350 String getExternalStorageDirectory() { 351 return PathUtils.getExternalStorageDirectory(); 352 } 353 354 @VisibleForTesting 355 void configure(String url, Map<String,String> headers) { 356 mRetriever.setDataSource(url, headers); 357 } 358 359 @VisibleForTesting 360 void configure(String path) { 361 mRetriever.setDataSource(path); 362 } 363 364 @VisibleForTesting 365 String extractMetadata(int key) { 366 return mRetriever.extractMetadata(key); 367 } 368} 369