1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.media; 18 19 import static android.media.MediaPlayer.MEDIA_ERROR_UNSUPPORTED; 20 21 import android.annotation.UnsupportedAppUsage; 22 import android.net.NetworkUtils; 23 import android.os.IBinder; 24 import android.os.StrictMode; 25 import android.util.Log; 26 27 import com.android.internal.annotations.GuardedBy; 28 import java.io.BufferedInputStream; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.net.CookieHandler; 32 import java.net.HttpURLConnection; 33 import java.net.MalformedURLException; 34 import java.net.NoRouteToHostException; 35 import java.net.ProtocolException; 36 import java.net.Proxy; 37 import java.net.URL; 38 import java.net.UnknownServiceException; 39 import java.util.HashMap; 40 import java.util.Map; 41 import java.util.concurrent.atomic.AtomicInteger; 42 43 /** @hide */ 44 public class MediaHTTPConnection extends IMediaHTTPConnection.Stub { 45 private static final String TAG = "MediaHTTPConnection"; 46 private static final boolean VERBOSE = false; 47 48 // connection timeout - 30 sec 49 private static final int CONNECT_TIMEOUT_MS = 30 * 1000; 50 51 @GuardedBy("this") 52 @UnsupportedAppUsage 53 private long mCurrentOffset = -1; 54 55 @GuardedBy("this") 56 @UnsupportedAppUsage 57 private URL mURL = null; 58 59 @GuardedBy("this") 60 @UnsupportedAppUsage 61 private Map<String, String> mHeaders = null; 62 63 // volatile so that disconnect() can be called without acquiring a lock. 64 // All other access is @GuardedBy("this"). 65 @UnsupportedAppUsage 66 private volatile HttpURLConnection mConnection = null; 67 68 @GuardedBy("this") 69 @UnsupportedAppUsage 70 private long mTotalSize = -1; 71 72 @GuardedBy("this") 73 private InputStream mInputStream = null; 74 75 @GuardedBy("this") 76 @UnsupportedAppUsage 77 private boolean mAllowCrossDomainRedirect = true; 78 79 @GuardedBy("this") 80 @UnsupportedAppUsage 81 private boolean mAllowCrossProtocolRedirect = true; 82 83 // from com.squareup.okhttp.internal.http 84 private final static int HTTP_TEMP_REDIRECT = 307; 85 private final static int MAX_REDIRECTS = 20; 86 87 // The number of threads that are currently running disconnect() (possibly 88 // not yet holding the synchronized lock). 89 private final AtomicInteger mNumDisconnectingThreads = new AtomicInteger(0); 90 91 @UnsupportedAppUsage MediaHTTPConnection()92 public MediaHTTPConnection() { 93 CookieHandler cookieHandler = CookieHandler.getDefault(); 94 if (cookieHandler == null) { 95 Log.w(TAG, "MediaHTTPConnection: Unexpected. No CookieHandler found."); 96 } 97 98 native_setup(); 99 } 100 101 @Override 102 @UnsupportedAppUsage connect(String uri, String headers)103 public synchronized IBinder connect(String uri, String headers) { 104 if (VERBOSE) { 105 Log.d(TAG, "connect: uri=" + uri + ", headers=" + headers); 106 } 107 108 try { 109 disconnect(); 110 mAllowCrossDomainRedirect = true; 111 mURL = new URL(uri); 112 mHeaders = convertHeaderStringToMap(headers); 113 } catch (MalformedURLException e) { 114 return null; 115 } 116 117 return native_getIMemory(); 118 } 119 parseBoolean(String val)120 private static boolean parseBoolean(String val) { 121 try { 122 return Long.parseLong(val) != 0; 123 } catch (NumberFormatException e) { 124 return "true".equalsIgnoreCase(val) || 125 "yes".equalsIgnoreCase(val); 126 } 127 } 128 129 /* returns true iff header is internal */ filterOutInternalHeaders(String key, String val)130 private synchronized boolean filterOutInternalHeaders(String key, String val) { 131 if ("android-allow-cross-domain-redirect".equalsIgnoreCase(key)) { 132 mAllowCrossDomainRedirect = parseBoolean(val); 133 // cross-protocol redirects are also controlled by this flag 134 mAllowCrossProtocolRedirect = mAllowCrossDomainRedirect; 135 } else { 136 return false; 137 } 138 return true; 139 } 140 convertHeaderStringToMap(String headers)141 private synchronized Map<String, String> convertHeaderStringToMap(String headers) { 142 HashMap<String, String> map = new HashMap<String, String>(); 143 144 String[] pairs = headers.split("\r\n"); 145 for (String pair : pairs) { 146 int colonPos = pair.indexOf(":"); 147 if (colonPos >= 0) { 148 String key = pair.substring(0, colonPos); 149 String val = pair.substring(colonPos + 1); 150 151 if (!filterOutInternalHeaders(key, val)) { 152 map.put(key, val); 153 } 154 } 155 } 156 157 return map; 158 } 159 160 @Override 161 @UnsupportedAppUsage disconnect()162 public void disconnect() { 163 mNumDisconnectingThreads.incrementAndGet(); 164 try { 165 HttpURLConnection connectionToDisconnect = mConnection; 166 // Call disconnect() before blocking for the lock in order to ensure that any 167 // other thread that is blocked in readAt() will return quickly. 168 if (connectionToDisconnect != null) { 169 connectionToDisconnect.disconnect(); 170 } 171 synchronized (this) { 172 // It's possible that while we were waiting to acquire the lock, another thread 173 // concurrently started a new connection; if so, we're disconnecting that one 174 // here, too. 175 teardownConnection(); 176 mHeaders = null; 177 mURL = null; 178 } 179 } finally { 180 mNumDisconnectingThreads.decrementAndGet(); 181 } 182 } 183 teardownConnection()184 private synchronized void teardownConnection() { 185 if (mConnection != null) { 186 if (mInputStream != null) { 187 try { 188 mInputStream.close(); 189 } catch (IOException e) { 190 } 191 mInputStream = null; 192 } 193 194 mConnection.disconnect(); 195 mConnection = null; 196 197 mCurrentOffset = -1; 198 } 199 } 200 isLocalHost(URL url)201 private static final boolean isLocalHost(URL url) { 202 if (url == null) { 203 return false; 204 } 205 206 String host = url.getHost(); 207 208 if (host == null) { 209 return false; 210 } 211 212 try { 213 if (host.equalsIgnoreCase("localhost")) { 214 return true; 215 } 216 if (NetworkUtils.numericToInetAddress(host).isLoopbackAddress()) { 217 return true; 218 } 219 } catch (IllegalArgumentException iex) { 220 } 221 return false; 222 } 223 seekTo(long offset)224 private synchronized void seekTo(long offset) throws IOException { 225 teardownConnection(); 226 227 try { 228 int response; 229 int redirectCount = 0; 230 231 URL url = mURL; 232 233 // do not use any proxy for localhost (127.0.0.1) 234 boolean noProxy = isLocalHost(url); 235 236 while (true) { 237 // If another thread is concurrently disconnect()ing, there's a race 238 // between them and us. Therefore, we check mNumDisconnectingThreads shortly 239 // (not atomically) before & after writing mConnection. This guarantees that 240 // we won't "lose" a disconnect by creating a new connection that might 241 // miss the disconnect. 242 // 243 // Note that throwing an instanceof IOException is also what this thread 244 // would have done if another thread disconnect()ed the connection while 245 // this thread was blocked reading from that connection further down in this 246 // loop. 247 if (mNumDisconnectingThreads.get() > 0) { 248 throw new IOException("concurrently disconnecting"); 249 } 250 if (noProxy) { 251 mConnection = (HttpURLConnection)url.openConnection(Proxy.NO_PROXY); 252 } else { 253 mConnection = (HttpURLConnection)url.openConnection(); 254 } 255 // If another thread is concurrently disconnecting, throwing IOException will 256 // cause us to release the lock, giving the other thread a chance to acquire 257 // it. It also ensures that the catch block will run, which will tear down 258 // the connection even if the other thread happens to already be on its way 259 // out of disconnect(). 260 if (mNumDisconnectingThreads.get() > 0) { 261 throw new IOException("concurrently disconnecting"); 262 } 263 // If we get here without having thrown, we know that other threads 264 // will see our write to mConnection. Any disconnect() on that mConnection 265 // instance will cause our read from/write to that connection instance below 266 // to encounter an instanceof IOException. 267 mConnection.setConnectTimeout(CONNECT_TIMEOUT_MS); 268 269 // handle redirects ourselves if we do not allow cross-domain redirect 270 mConnection.setInstanceFollowRedirects(mAllowCrossDomainRedirect); 271 272 if (mHeaders != null) { 273 for (Map.Entry<String, String> entry : mHeaders.entrySet()) { 274 mConnection.setRequestProperty( 275 entry.getKey(), entry.getValue()); 276 } 277 } 278 279 if (offset > 0) { 280 mConnection.setRequestProperty( 281 "Range", "bytes=" + offset + "-"); 282 } 283 284 response = mConnection.getResponseCode(); 285 if (response != HttpURLConnection.HTTP_MULT_CHOICE && 286 response != HttpURLConnection.HTTP_MOVED_PERM && 287 response != HttpURLConnection.HTTP_MOVED_TEMP && 288 response != HttpURLConnection.HTTP_SEE_OTHER && 289 response != HTTP_TEMP_REDIRECT) { 290 // not a redirect, or redirect handled by HttpURLConnection 291 break; 292 } 293 294 if (++redirectCount > MAX_REDIRECTS) { 295 throw new NoRouteToHostException("Too many redirects: " + redirectCount); 296 } 297 298 String method = mConnection.getRequestMethod(); 299 if (response == HTTP_TEMP_REDIRECT && 300 !method.equals("GET") && !method.equals("HEAD")) { 301 // "If the 307 status code is received in response to a 302 // request other than GET or HEAD, the user agent MUST NOT 303 // automatically redirect the request" 304 throw new NoRouteToHostException("Invalid redirect"); 305 } 306 String location = mConnection.getHeaderField("Location"); 307 if (location == null) { 308 throw new NoRouteToHostException("Invalid redirect"); 309 } 310 url = new URL(mURL /* TRICKY: don't use url! */, location); 311 if (!url.getProtocol().equals("https") && 312 !url.getProtocol().equals("http")) { 313 throw new NoRouteToHostException("Unsupported protocol redirect"); 314 } 315 boolean sameProtocol = mURL.getProtocol().equals(url.getProtocol()); 316 if (!mAllowCrossProtocolRedirect && !sameProtocol) { 317 throw new NoRouteToHostException("Cross-protocol redirects are disallowed"); 318 } 319 boolean sameHost = mURL.getHost().equals(url.getHost()); 320 if (!mAllowCrossDomainRedirect && !sameHost) { 321 throw new NoRouteToHostException("Cross-domain redirects are disallowed"); 322 } 323 324 if (response != HTTP_TEMP_REDIRECT) { 325 // update effective URL, unless it is a Temporary Redirect 326 mURL = url; 327 } 328 } 329 330 if (mAllowCrossDomainRedirect) { 331 // remember the current, potentially redirected URL if redirects 332 // were handled by HttpURLConnection 333 mURL = mConnection.getURL(); 334 } 335 336 if (response == HttpURLConnection.HTTP_PARTIAL) { 337 // Partial content, we cannot just use getContentLength 338 // because what we want is not just the length of the range 339 // returned but the size of the full content if available. 340 341 String contentRange = 342 mConnection.getHeaderField("Content-Range"); 343 344 mTotalSize = -1; 345 if (contentRange != null) { 346 // format is "bytes xxx-yyy/zzz 347 // where "zzz" is the total number of bytes of the 348 // content or '*' if unknown. 349 350 int lastSlashPos = contentRange.lastIndexOf('/'); 351 if (lastSlashPos >= 0) { 352 String total = 353 contentRange.substring(lastSlashPos + 1); 354 355 try { 356 mTotalSize = Long.parseLong(total); 357 } catch (NumberFormatException e) { 358 } 359 } 360 } 361 } else if (response != HttpURLConnection.HTTP_OK) { 362 throw new IOException(); 363 } else { 364 mTotalSize = mConnection.getContentLength(); 365 } 366 367 if (offset > 0 && response != HttpURLConnection.HTTP_PARTIAL) { 368 // Some servers simply ignore "Range" requests and serve 369 // data from the start of the content. 370 throw new ProtocolException(); 371 } 372 373 mInputStream = 374 new BufferedInputStream(mConnection.getInputStream()); 375 376 mCurrentOffset = offset; 377 } catch (IOException e) { 378 mTotalSize = -1; 379 teardownConnection(); 380 mCurrentOffset = -1; 381 382 throw e; 383 } 384 } 385 386 @Override 387 @UnsupportedAppUsage readAt(long offset, int size)388 public synchronized int readAt(long offset, int size) { 389 return native_readAt(offset, size); 390 } 391 readAt(long offset, byte[] data, int size)392 private synchronized int readAt(long offset, byte[] data, int size) { 393 StrictMode.ThreadPolicy policy = 394 new StrictMode.ThreadPolicy.Builder().permitAll().build(); 395 396 StrictMode.setThreadPolicy(policy); 397 398 try { 399 if (offset != mCurrentOffset) { 400 seekTo(offset); 401 } 402 403 int n = mInputStream.read(data, 0, size); 404 405 if (n == -1) { 406 // InputStream signals EOS using a -1 result, our semantics 407 // are to return a 0-length read. 408 n = 0; 409 } 410 411 mCurrentOffset += n; 412 413 if (VERBOSE) { 414 Log.d(TAG, "readAt " + offset + " / " + size + " => " + n); 415 } 416 417 return n; 418 } catch (ProtocolException e) { 419 Log.w(TAG, "readAt " + offset + " / " + size + " => " + e); 420 return MEDIA_ERROR_UNSUPPORTED; 421 } catch (NoRouteToHostException e) { 422 Log.w(TAG, "readAt " + offset + " / " + size + " => " + e); 423 return MEDIA_ERROR_UNSUPPORTED; 424 } catch (UnknownServiceException e) { 425 Log.w(TAG, "readAt " + offset + " / " + size + " => " + e); 426 return MEDIA_ERROR_UNSUPPORTED; 427 } catch (IOException e) { 428 if (VERBOSE) { 429 Log.d(TAG, "readAt " + offset + " / " + size + " => -1"); 430 } 431 return -1; 432 } catch (Exception e) { 433 if (VERBOSE) { 434 Log.d(TAG, "unknown exception " + e); 435 Log.d(TAG, "readAt " + offset + " / " + size + " => -1"); 436 } 437 return -1; 438 } 439 } 440 441 @Override getSize()442 public synchronized long getSize() { 443 if (mConnection == null) { 444 try { 445 seekTo(0); 446 } catch (IOException e) { 447 return -1; 448 } 449 } 450 451 return mTotalSize; 452 } 453 454 @Override 455 @UnsupportedAppUsage getMIMEType()456 public synchronized String getMIMEType() { 457 if (mConnection == null) { 458 try { 459 seekTo(0); 460 } catch (IOException e) { 461 return "application/octet-stream"; 462 } 463 } 464 465 return mConnection.getContentType(); 466 } 467 468 @Override 469 @UnsupportedAppUsage getUri()470 public synchronized String getUri() { 471 return mURL.toString(); 472 } 473 474 @Override finalize()475 protected void finalize() { 476 native_finalize(); 477 } 478 native_init()479 private static native final void native_init(); native_setup()480 private native final void native_setup(); native_finalize()481 private native final void native_finalize(); 482 native_getIMemory()483 private native final IBinder native_getIMemory(); native_readAt(long offset, int size)484 private native final int native_readAt(long offset, int size); 485 486 static { 487 System.loadLibrary("media_jni"); native_init()488 native_init(); 489 } 490 491 private long mNativeContext; 492 493 } 494