MediaHTTPConnection.java revision c446dc32742de388b95ff7cf97f4dc1576cb57c5
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 17package android.media; 18 19import android.os.IBinder; 20import android.os.StrictMode; 21import android.util.Log; 22 23import java.io.BufferedInputStream; 24import java.io.InputStream; 25import java.io.IOException; 26import java.net.CookieHandler; 27import java.net.CookieManager; 28import java.net.URL; 29import java.net.HttpURLConnection; 30import java.net.MalformedURLException; 31import java.net.NoRouteToHostException; 32import java.util.HashMap; 33import java.util.Map; 34 35import static android.media.MediaPlayer.MEDIA_ERROR_UNSUPPORTED; 36 37/** @hide */ 38public class MediaHTTPConnection extends IMediaHTTPConnection.Stub { 39 private static final String TAG = "MediaHTTPConnection"; 40 private static final boolean VERBOSE = false; 41 42 private long mCurrentOffset = -1; 43 private URL mURL = null; 44 private Map<String, String> mHeaders = null; 45 private HttpURLConnection mConnection = null; 46 private long mTotalSize = -1; 47 private InputStream mInputStream = null; 48 49 private boolean mAllowCrossDomainRedirect = true; 50 51 // from com.squareup.okhttp.internal.http 52 private final static int HTTP_TEMP_REDIRECT = 307; 53 private final static int MAX_REDIRECTS = 20; 54 55 public MediaHTTPConnection() { 56 if (CookieHandler.getDefault() == null) { 57 CookieHandler.setDefault(new CookieManager()); 58 } 59 60 native_setup(); 61 } 62 63 @Override 64 public IBinder connect(String uri, String headers) { 65 if (VERBOSE) { 66 Log.d(TAG, "connect: uri=" + uri + ", headers=" + headers); 67 } 68 69 try { 70 disconnect(); 71 mAllowCrossDomainRedirect = true; 72 mURL = new URL(uri); 73 mHeaders = convertHeaderStringToMap(headers); 74 } catch (MalformedURLException e) { 75 return null; 76 } 77 78 return native_getIMemory(); 79 } 80 81 private boolean parseBoolean(String val) { 82 try { 83 return Long.parseLong(val) != 0; 84 } catch (NumberFormatException e) { 85 return "true".equalsIgnoreCase(val) || 86 "yes".equalsIgnoreCase(val); 87 } 88 } 89 90 /* returns true iff header is internal */ 91 private boolean filterOutInternalHeaders(String key, String val) { 92 if ("android-allow-cross-domain-redirect".equalsIgnoreCase(key)) { 93 mAllowCrossDomainRedirect = parseBoolean(val); 94 } else { 95 return false; 96 } 97 return true; 98 } 99 100 private Map<String, String> convertHeaderStringToMap(String headers) { 101 HashMap<String, String> map = new HashMap<String, String>(); 102 103 String[] pairs = headers.split("\r\n"); 104 for (String pair : pairs) { 105 int colonPos = pair.indexOf(":"); 106 if (colonPos >= 0) { 107 String key = pair.substring(0, colonPos); 108 String val = pair.substring(colonPos + 1); 109 110 if (!filterOutInternalHeaders(key, val)) { 111 map.put(key, val); 112 } 113 } 114 } 115 116 return map; 117 } 118 119 @Override 120 public void disconnect() { 121 teardownConnection(); 122 mHeaders = null; 123 mURL = null; 124 } 125 126 private void teardownConnection() { 127 if (mConnection != null) { 128 mInputStream = null; 129 130 mConnection.disconnect(); 131 mConnection = null; 132 133 mCurrentOffset = -1; 134 } 135 } 136 137 private void seekTo(long offset) throws IOException { 138 teardownConnection(); 139 140 try { 141 int response; 142 int redirectCount = 0; 143 144 URL url = mURL; 145 while (true) { 146 mConnection = (HttpURLConnection)url.openConnection(); 147 // handle redirects ourselves if we do not allow cross-domain redirect 148 mConnection.setInstanceFollowRedirects(mAllowCrossDomainRedirect); 149 150 if (mHeaders != null) { 151 for (Map.Entry<String, String> entry : mHeaders.entrySet()) { 152 mConnection.setRequestProperty( 153 entry.getKey(), entry.getValue()); 154 } 155 } 156 157 if (offset > 0) { 158 mConnection.setRequestProperty( 159 "Range", "bytes=" + offset + "-"); 160 } 161 162 response = mConnection.getResponseCode(); 163 if (response != HttpURLConnection.HTTP_MULT_CHOICE && 164 response != HttpURLConnection.HTTP_MOVED_PERM && 165 response != HttpURLConnection.HTTP_MOVED_TEMP && 166 response != HttpURLConnection.HTTP_SEE_OTHER && 167 response != HTTP_TEMP_REDIRECT) { 168 // not a redirect, or redirect handled by HttpURLConnection 169 break; 170 } 171 172 if (++redirectCount > MAX_REDIRECTS) { 173 throw new NoRouteToHostException("Too many redirects: " + redirectCount); 174 } 175 176 String method = mConnection.getRequestMethod(); 177 if (response == HTTP_TEMP_REDIRECT && 178 !method.equals("GET") && !method.equals("HEAD")) { 179 // "If the 307 status code is received in response to a 180 // request other than GET or HEAD, the user agent MUST NOT 181 // automatically redirect the request" 182 throw new NoRouteToHostException("Invalid redirect"); 183 } 184 String location = mConnection.getHeaderField("Location"); 185 if (location == null) { 186 throw new NoRouteToHostException("Invalid redirect"); 187 } 188 url = new URL(mURL /* TRICKY: don't use url! */, location); 189 if (!url.getProtocol().equals("https") && 190 !url.getProtocol().equals("http")) { 191 throw new NoRouteToHostException("Unsupported protocol redirect"); 192 } 193 boolean sameHost = mURL.getHost().equals(url.getHost()); 194 if (!sameHost) { 195 throw new NoRouteToHostException("Cross-domain redirects are disallowed"); 196 } 197 198 if (response != HTTP_TEMP_REDIRECT) { 199 // update effective URL, unless it is a Temporary Redirect 200 mURL = url; 201 } 202 } 203 204 if (mAllowCrossDomainRedirect) { 205 // remember the current, potentially redirected URL if redirects 206 // were handled by HttpURLConnection 207 mURL = mConnection.getURL(); 208 } 209 210 if (response == HttpURLConnection.HTTP_PARTIAL) { 211 // Partial content, we cannot just use getContentLength 212 // because what we want is not just the length of the range 213 // returned but the size of the full content if available. 214 215 String contentRange = 216 mConnection.getHeaderField("Content-Range"); 217 218 mTotalSize = -1; 219 if (contentRange != null) { 220 // format is "bytes xxx-yyy/zzz 221 // where "zzz" is the total number of bytes of the 222 // content or '*' if unknown. 223 224 int lastSlashPos = contentRange.lastIndexOf('/'); 225 if (lastSlashPos >= 0) { 226 String total = 227 contentRange.substring(lastSlashPos + 1); 228 229 try { 230 mTotalSize = Long.parseLong(total); 231 } catch (NumberFormatException e) { 232 } 233 } 234 } 235 } else if (response != HttpURLConnection.HTTP_OK) { 236 throw new IOException(); 237 } else { 238 mTotalSize = mConnection.getContentLength(); 239 } 240 241 if (offset > 0 && response != HttpURLConnection.HTTP_PARTIAL) { 242 // Some servers simply ignore "Range" requests and serve 243 // data from the start of the content. 244 throw new IOException(); 245 } 246 247 mInputStream = 248 new BufferedInputStream(mConnection.getInputStream()); 249 250 mCurrentOffset = offset; 251 } catch (IOException e) { 252 mTotalSize = -1; 253 mInputStream = null; 254 mConnection = null; 255 mCurrentOffset = -1; 256 257 throw e; 258 } 259 } 260 261 @Override 262 public int readAt(long offset, int size) { 263 return native_readAt(offset, size); 264 } 265 266 private int readAt(long offset, byte[] data, int size) { 267 StrictMode.ThreadPolicy policy = 268 new StrictMode.ThreadPolicy.Builder().permitAll().build(); 269 270 StrictMode.setThreadPolicy(policy); 271 272 try { 273 if (offset != mCurrentOffset) { 274 seekTo(offset); 275 } 276 277 int n = mInputStream.read(data, 0, size); 278 279 if (n == -1) { 280 // InputStream signals EOS using a -1 result, our semantics 281 // are to return a 0-length read. 282 n = 0; 283 } 284 285 mCurrentOffset += n; 286 287 if (VERBOSE) { 288 Log.d(TAG, "readAt " + offset + " / " + size + " => " + n); 289 } 290 291 return n; 292 } catch (NoRouteToHostException e) { 293 Log.w(TAG, "readAt " + offset + " / " + size + " => " + e); 294 return MEDIA_ERROR_UNSUPPORTED; 295 } catch (IOException e) { 296 if (VERBOSE) { 297 Log.d(TAG, "readAt " + offset + " / " + size + " => -1"); 298 } 299 return -1; 300 } catch (Exception e) { 301 if (VERBOSE) { 302 Log.d(TAG, "unknown exception " + e); 303 Log.d(TAG, "readAt " + offset + " / " + size + " => -1"); 304 } 305 return -1; 306 } 307 } 308 309 @Override 310 public long getSize() { 311 if (mConnection == null) { 312 try { 313 seekTo(0); 314 } catch (IOException e) { 315 return -1; 316 } 317 } 318 319 return mTotalSize; 320 } 321 322 @Override 323 public String getMIMEType() { 324 if (mConnection == null) { 325 try { 326 seekTo(0); 327 } catch (IOException e) { 328 return "application/octet-stream"; 329 } 330 } 331 332 return mConnection.getContentType(); 333 } 334 335 @Override 336 public String getUri() { 337 return mURL.toString(); 338 } 339 340 @Override 341 protected void finalize() { 342 native_finalize(); 343 } 344 345 private static native final void native_init(); 346 private native final void native_setup(); 347 private native final void native_finalize(); 348 349 private native final IBinder native_getIMemory(); 350 private native final int native_readAt(long offset, int size); 351 352 static { 353 System.loadLibrary("media_jni"); 354 native_init(); 355 } 356 357 private long mNativeContext; 358 359} 360