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