OSDN Git Service

Merge changes Id25696e4,I939a12a2 into klp-dev am: 77b5526229 am: 2169197e28
[android-x86/frameworks-base.git] / media / java / android / media / MediaHTTPConnection.java
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 android.net.NetworkUtils;
20 import android.os.IBinder;
21 import android.os.StrictMode;
22 import android.util.Log;
23
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;
30 import java.net.URL;
31 import java.net.HttpURLConnection;
32 import java.net.MalformedURLException;
33 import java.net.NoRouteToHostException;
34 import java.util.HashMap;
35 import java.util.Map;
36
37 import static android.media.MediaPlayer.MEDIA_ERROR_UNSUPPORTED;
38
39 /** @hide */
40 public class MediaHTTPConnection extends IMediaHTTPConnection.Stub {
41     private static final String TAG = "MediaHTTPConnection";
42     private static final boolean VERBOSE = false;
43
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;
50
51     private boolean mAllowCrossDomainRedirect = true;
52     private boolean mAllowCrossProtocolRedirect = true;
53
54     // from com.squareup.okhttp.internal.http
55     private final static int HTTP_TEMP_REDIRECT = 307;
56     private final static int MAX_REDIRECTS = 20;
57
58     public MediaHTTPConnection() {
59         if (CookieHandler.getDefault() == null) {
60             CookieHandler.setDefault(new CookieManager());
61         }
62
63         native_setup();
64     }
65
66     @Override
67     public IBinder connect(String uri, String headers) {
68         if (VERBOSE) {
69             Log.d(TAG, "connect: uri=" + uri + ", headers=" + headers);
70         }
71
72         try {
73             disconnect();
74             mAllowCrossDomainRedirect = true;
75             mURL = new URL(uri);
76             mHeaders = convertHeaderStringToMap(headers);
77         } catch (MalformedURLException e) {
78             return null;
79         }
80
81         return native_getIMemory();
82     }
83
84     private boolean parseBoolean(String val) {
85         try {
86             return Long.parseLong(val) != 0;
87         } catch (NumberFormatException e) {
88             return "true".equalsIgnoreCase(val) ||
89                 "yes".equalsIgnoreCase(val);
90         }
91     }
92
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;
99         } else {
100             return false;
101         }
102         return true;
103     }
104
105     private Map<String, String> convertHeaderStringToMap(String headers) {
106         HashMap<String, String> map = new HashMap<String, String>();
107
108         String[] pairs = headers.split("\r\n");
109         for (String pair : pairs) {
110             int colonPos = pair.indexOf(":");
111             if (colonPos >= 0) {
112                 String key = pair.substring(0, colonPos);
113                 String val = pair.substring(colonPos + 1);
114
115                 if (!filterOutInternalHeaders(key, val)) {
116                     map.put(key, val);
117                 }
118             }
119         }
120
121         return map;
122     }
123
124     @Override
125     public void disconnect() {
126         teardownConnection();
127         mHeaders = null;
128         mURL = null;
129     }
130
131     private void teardownConnection() {
132         if (mConnection != null) {
133             mInputStream = null;
134
135             mConnection.disconnect();
136             mConnection = null;
137
138             mCurrentOffset = -1;
139         }
140     }
141
142     private static final boolean isLocalHost(URL url) {
143         if (url == null) {
144             return false;
145         }
146
147         String host = url.getHost();
148
149         if (host == null) {
150             return false;
151         }
152
153         try {
154             if (host.equalsIgnoreCase("localhost")) {
155                 return true;
156             }
157             if (NetworkUtils.numericToInetAddress(host).isLoopbackAddress()) {
158                 return true;
159             }
160         } catch (IllegalArgumentException iex) {
161         }
162         return false;
163     }
164
165     private void seekTo(long offset) throws IOException {
166         teardownConnection();
167
168         try {
169             int response;
170             int redirectCount = 0;
171
172             URL url = mURL;
173
174             // do not use any proxy for localhost (127.0.0.1)
175             boolean noProxy = isLocalHost(url);
176
177             while (true) {
178                 if (noProxy) {
179                     mConnection = (HttpURLConnection)url.openConnection(Proxy.NO_PROXY);
180                 } else {
181                     mConnection = (HttpURLConnection)url.openConnection();
182                 }
183
184                 // handle redirects ourselves if we do not allow cross-domain redirect
185                 mConnection.setInstanceFollowRedirects(mAllowCrossDomainRedirect);
186
187                 if (mHeaders != null) {
188                     for (Map.Entry<String, String> entry : mHeaders.entrySet()) {
189                         mConnection.setRequestProperty(
190                                 entry.getKey(), entry.getValue());
191                     }
192                 }
193
194                 if (offset > 0) {
195                     mConnection.setRequestProperty(
196                             "Range", "bytes=" + offset + "-");
197                 }
198
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
206                     break;
207                 }
208
209                 if (++redirectCount > MAX_REDIRECTS) {
210                     throw new NoRouteToHostException("Too many redirects: " + redirectCount);
211                 }
212
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");
220                 }
221                 String location = mConnection.getHeaderField("Location");
222                 if (location == null) {
223                     throw new NoRouteToHostException("Invalid redirect");
224                 }
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");
229                 }
230                 boolean sameProtocol = mURL.getProtocol().equals(url.getProtocol());
231                 if (!mAllowCrossProtocolRedirect && !sameProtocol) {
232                     throw new NoRouteToHostException("Cross-protocol redirects are disallowed");
233                 }
234                 boolean sameHost = mURL.getHost().equals(url.getHost());
235                 if (!mAllowCrossDomainRedirect && !sameHost) {
236                     throw new NoRouteToHostException("Cross-domain redirects are disallowed");
237                 }
238
239                 if (response != HTTP_TEMP_REDIRECT) {
240                     // update effective URL, unless it is a Temporary Redirect
241                     mURL = url;
242                 }
243             }
244
245             if (mAllowCrossDomainRedirect) {
246                 // remember the current, potentially redirected URL if redirects
247                 // were handled by HttpURLConnection
248                 mURL = mConnection.getURL();
249             }
250
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.
255
256                 String contentRange =
257                     mConnection.getHeaderField("Content-Range");
258
259                 mTotalSize = -1;
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.
264
265                     int lastSlashPos = contentRange.lastIndexOf('/');
266                     if (lastSlashPos >= 0) {
267                         String total =
268                             contentRange.substring(lastSlashPos + 1);
269
270                         try {
271                             mTotalSize = Long.parseLong(total);
272                         } catch (NumberFormatException e) {
273                         }
274                     }
275                 }
276             } else if (response != HttpURLConnection.HTTP_OK) {
277                 throw new IOException();
278             } else {
279                 mTotalSize = mConnection.getContentLength();
280             }
281
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();
286             }
287
288             mInputStream =
289                 new BufferedInputStream(mConnection.getInputStream());
290
291             mCurrentOffset = offset;
292         } catch (IOException e) {
293             mTotalSize = -1;
294             mInputStream = null;
295             mConnection = null;
296             mCurrentOffset = -1;
297
298             throw e;
299         }
300     }
301
302     @Override
303     public int readAt(long offset, int size) {
304         return native_readAt(offset, size);
305     }
306
307     private int readAt(long offset, byte[] data, int size) {
308         StrictMode.ThreadPolicy policy =
309             new StrictMode.ThreadPolicy.Builder().permitAll().build();
310
311         StrictMode.setThreadPolicy(policy);
312
313         try {
314             if (offset != mCurrentOffset) {
315                 seekTo(offset);
316             }
317
318             int n = mInputStream.read(data, 0, size);
319
320             if (n == -1) {
321                 // InputStream signals EOS using a -1 result, our semantics
322                 // are to return a 0-length read.
323                 n = 0;
324             }
325
326             mCurrentOffset += n;
327
328             if (VERBOSE) {
329                 Log.d(TAG, "readAt " + offset + " / " + size + " => " + n);
330             }
331
332             return n;
333         } catch (NoRouteToHostException e) {
334             Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
335             return MEDIA_ERROR_UNSUPPORTED;
336         } catch (IOException e) {
337             if (VERBOSE) {
338                 Log.d(TAG, "readAt " + offset + " / " + size + " => -1");
339             }
340             return -1;
341         } catch (Exception e) {
342             if (VERBOSE) {
343                 Log.d(TAG, "unknown exception " + e);
344                 Log.d(TAG, "readAt " + offset + " / " + size + " => -1");
345             }
346             return -1;
347         }
348     }
349
350     @Override
351     public long getSize() {
352         if (mConnection == null) {
353             try {
354                 seekTo(0);
355             } catch (IOException e) {
356                 return -1;
357             }
358         }
359
360         return mTotalSize;
361     }
362
363     @Override
364     public String getMIMEType() {
365         if (mConnection == null) {
366             try {
367                 seekTo(0);
368             } catch (IOException e) {
369                 return "application/octet-stream";
370             }
371         }
372
373         return mConnection.getContentType();
374     }
375
376     @Override
377     public String getUri() {
378         return mURL.toString();
379     }
380
381     @Override
382     protected void finalize() {
383         native_finalize();
384     }
385
386     private static native final void native_init();
387     private native final void native_setup();
388     private native final void native_finalize();
389
390     private native final IBinder native_getIMemory();
391     private native final int native_readAt(long offset, int size);
392
393     static {
394         System.loadLibrary("media_jni");
395         native_init();
396     }
397
398     private long mNativeContext;
399
400 }