2 * Copyright (C) 2013 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package android.media;
19 import android.net.NetworkUtils;
20 import android.os.IBinder;
21 import android.os.StrictMode;
22 import android.util.Log;
24 import java.io.BufferedInputStream;
25 import java.io.InputStream;
26 import java.io.IOException;
27 import java.net.CookieHandler;
28 import java.net.CookieManager;
29 import java.net.Proxy;
31 import java.net.HttpURLConnection;
32 import java.net.MalformedURLException;
33 import java.net.NoRouteToHostException;
34 import java.util.HashMap;
37 import static android.media.MediaPlayer.MEDIA_ERROR_UNSUPPORTED;
40 public class MediaHTTPConnection extends IMediaHTTPConnection.Stub {
41 private static final String TAG = "MediaHTTPConnection";
42 private static final boolean VERBOSE = false;
44 private long mCurrentOffset = -1;
45 private URL mURL = null;
46 private Map<String, String> mHeaders = null;
47 private HttpURLConnection mConnection = null;
48 private long mTotalSize = -1;
49 private InputStream mInputStream = null;
51 private boolean mAllowCrossDomainRedirect = true;
52 private boolean mAllowCrossProtocolRedirect = true;
54 // from com.squareup.okhttp.internal.http
55 private final static int HTTP_TEMP_REDIRECT = 307;
56 private final static int MAX_REDIRECTS = 20;
58 public MediaHTTPConnection() {
59 if (CookieHandler.getDefault() == null) {
60 CookieHandler.setDefault(new CookieManager());
67 public IBinder connect(String uri, String headers) {
69 Log.d(TAG, "connect: uri=" + uri + ", headers=" + headers);
74 mAllowCrossDomainRedirect = true;
76 mHeaders = convertHeaderStringToMap(headers);
77 } catch (MalformedURLException e) {
81 return native_getIMemory();
84 private boolean parseBoolean(String val) {
86 return Long.parseLong(val) != 0;
87 } catch (NumberFormatException e) {
88 return "true".equalsIgnoreCase(val) ||
89 "yes".equalsIgnoreCase(val);
93 /* returns true iff header is internal */
94 private boolean filterOutInternalHeaders(String key, String val) {
95 if ("android-allow-cross-domain-redirect".equalsIgnoreCase(key)) {
96 mAllowCrossDomainRedirect = parseBoolean(val);
97 // cross-protocol redirects are also controlled by this flag
98 mAllowCrossProtocolRedirect = mAllowCrossDomainRedirect;
105 private Map<String, String> convertHeaderStringToMap(String headers) {
106 HashMap<String, String> map = new HashMap<String, String>();
108 String[] pairs = headers.split("\r\n");
109 for (String pair : pairs) {
110 int colonPos = pair.indexOf(":");
112 String key = pair.substring(0, colonPos);
113 String val = pair.substring(colonPos + 1);
115 if (!filterOutInternalHeaders(key, val)) {
125 public void disconnect() {
126 teardownConnection();
131 private void teardownConnection() {
132 if (mConnection != null) {
135 mConnection.disconnect();
142 private static final boolean isLocalHost(URL url) {
147 String host = url.getHost();
154 if (host.equalsIgnoreCase("localhost")) {
157 if (NetworkUtils.numericToInetAddress(host).isLoopbackAddress()) {
160 } catch (IllegalArgumentException iex) {
165 private void seekTo(long offset) throws IOException {
166 teardownConnection();
170 int redirectCount = 0;
174 // do not use any proxy for localhost (127.0.0.1)
175 boolean noProxy = isLocalHost(url);
179 mConnection = (HttpURLConnection)url.openConnection(Proxy.NO_PROXY);
181 mConnection = (HttpURLConnection)url.openConnection();
184 // handle redirects ourselves if we do not allow cross-domain redirect
185 mConnection.setInstanceFollowRedirects(mAllowCrossDomainRedirect);
187 if (mHeaders != null) {
188 for (Map.Entry<String, String> entry : mHeaders.entrySet()) {
189 mConnection.setRequestProperty(
190 entry.getKey(), entry.getValue());
195 mConnection.setRequestProperty(
196 "Range", "bytes=" + offset + "-");
199 response = mConnection.getResponseCode();
200 if (response != HttpURLConnection.HTTP_MULT_CHOICE &&
201 response != HttpURLConnection.HTTP_MOVED_PERM &&
202 response != HttpURLConnection.HTTP_MOVED_TEMP &&
203 response != HttpURLConnection.HTTP_SEE_OTHER &&
204 response != HTTP_TEMP_REDIRECT) {
205 // not a redirect, or redirect handled by HttpURLConnection
209 if (++redirectCount > MAX_REDIRECTS) {
210 throw new NoRouteToHostException("Too many redirects: " + redirectCount);
213 String method = mConnection.getRequestMethod();
214 if (response == HTTP_TEMP_REDIRECT &&
215 !method.equals("GET") && !method.equals("HEAD")) {
216 // "If the 307 status code is received in response to a
217 // request other than GET or HEAD, the user agent MUST NOT
218 // automatically redirect the request"
219 throw new NoRouteToHostException("Invalid redirect");
221 String location = mConnection.getHeaderField("Location");
222 if (location == null) {
223 throw new NoRouteToHostException("Invalid redirect");
225 url = new URL(mURL /* TRICKY: don't use url! */, location);
226 if (!url.getProtocol().equals("https") &&
227 !url.getProtocol().equals("http")) {
228 throw new NoRouteToHostException("Unsupported protocol redirect");
230 boolean sameProtocol = mURL.getProtocol().equals(url.getProtocol());
231 if (!mAllowCrossProtocolRedirect && !sameProtocol) {
232 throw new NoRouteToHostException("Cross-protocol redirects are disallowed");
234 boolean sameHost = mURL.getHost().equals(url.getHost());
235 if (!mAllowCrossDomainRedirect && !sameHost) {
236 throw new NoRouteToHostException("Cross-domain redirects are disallowed");
239 if (response != HTTP_TEMP_REDIRECT) {
240 // update effective URL, unless it is a Temporary Redirect
245 if (mAllowCrossDomainRedirect) {
246 // remember the current, potentially redirected URL if redirects
247 // were handled by HttpURLConnection
248 mURL = mConnection.getURL();
251 if (response == HttpURLConnection.HTTP_PARTIAL) {
252 // Partial content, we cannot just use getContentLength
253 // because what we want is not just the length of the range
254 // returned but the size of the full content if available.
256 String contentRange =
257 mConnection.getHeaderField("Content-Range");
260 if (contentRange != null) {
261 // format is "bytes xxx-yyy/zzz
262 // where "zzz" is the total number of bytes of the
263 // content or '*' if unknown.
265 int lastSlashPos = contentRange.lastIndexOf('/');
266 if (lastSlashPos >= 0) {
268 contentRange.substring(lastSlashPos + 1);
271 mTotalSize = Long.parseLong(total);
272 } catch (NumberFormatException e) {
276 } else if (response != HttpURLConnection.HTTP_OK) {
277 throw new IOException();
279 mTotalSize = mConnection.getContentLength();
282 if (offset > 0 && response != HttpURLConnection.HTTP_PARTIAL) {
283 // Some servers simply ignore "Range" requests and serve
284 // data from the start of the content.
285 throw new IOException();
289 new BufferedInputStream(mConnection.getInputStream());
291 mCurrentOffset = offset;
292 } catch (IOException e) {
303 public int readAt(long offset, int size) {
304 return native_readAt(offset, size);
307 private int readAt(long offset, byte[] data, int size) {
308 StrictMode.ThreadPolicy policy =
309 new StrictMode.ThreadPolicy.Builder().permitAll().build();
311 StrictMode.setThreadPolicy(policy);
314 if (offset != mCurrentOffset) {
318 int n = mInputStream.read(data, 0, size);
321 // InputStream signals EOS using a -1 result, our semantics
322 // are to return a 0-length read.
329 Log.d(TAG, "readAt " + offset + " / " + size + " => " + n);
333 } catch (NoRouteToHostException e) {
334 Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
335 return MEDIA_ERROR_UNSUPPORTED;
336 } catch (IOException e) {
338 Log.d(TAG, "readAt " + offset + " / " + size + " => -1");
341 } catch (Exception e) {
343 Log.d(TAG, "unknown exception " + e);
344 Log.d(TAG, "readAt " + offset + " / " + size + " => -1");
351 public long getSize() {
352 if (mConnection == null) {
355 } catch (IOException e) {
364 public String getMIMEType() {
365 if (mConnection == null) {
368 } catch (IOException e) {
369 return "application/octet-stream";
373 return mConnection.getContentType();
377 public String getUri() {
378 return mURL.toString();
382 protected void finalize() {
386 private static native final void native_init();
387 private native final void native_setup();
388 private native final void native_finalize();
390 private native final IBinder native_getIMemory();
391 private native final int native_readAt(long offset, int size);
394 System.loadLibrary("media_jni");
398 private long mNativeContext;