OLD | NEW |
| (Empty) |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 package org.chromium.net; | |
6 | |
7 import android.content.Context; | |
8 import android.text.TextUtils; | |
9 | |
10 import java.io.FileNotFoundException; | |
11 import java.io.IOException; | |
12 import java.io.InputStream; | |
13 import java.io.OutputStream; | |
14 import java.net.HttpURLConnection; | |
15 import java.net.ProtocolException; | |
16 import java.net.URL; | |
17 import java.nio.ByteBuffer; | |
18 import java.nio.channels.ReadableByteChannel; | |
19 import java.nio.channels.WritableByteChannel; | |
20 import java.util.List; | |
21 import java.util.Map; | |
22 import java.util.Map.Entry; | |
23 import java.util.concurrent.ExecutorService; | |
24 import java.util.concurrent.Executors; | |
25 import java.util.concurrent.ThreadFactory; | |
26 import java.util.concurrent.atomic.AtomicInteger; | |
27 import java.util.zip.GZIPInputStream; | |
28 | |
29 /** | |
30 * Network request using the HttpUrlConnection implementation. | |
31 * @deprecated Use {@link UrlRequest} instead. | |
32 * {@hide as it's deprecated} | |
33 */ | |
34 @Deprecated | |
35 class HttpUrlConnectionUrlRequest implements HttpUrlRequest { | |
36 | |
37 private static final int MAX_CHUNK_SIZE = 8192; | |
38 | |
39 private static final int CONNECT_TIMEOUT = 3000; | |
40 | |
41 private static final int READ_TIMEOUT = 90000; | |
42 | |
43 private final Context mContext; | |
44 | |
45 private final String mDefaultUserAgent; | |
46 | |
47 private final String mUrl; | |
48 | |
49 private final Map<String, String> mHeaders; | |
50 | |
51 private final WritableByteChannel mSink; | |
52 | |
53 private final HttpUrlRequestListener mListener; | |
54 | |
55 private IOException mException; | |
56 | |
57 private HttpURLConnection mConnection; | |
58 | |
59 private long mOffset; | |
60 | |
61 private int mContentLength; | |
62 | |
63 private int mUploadContentLength; | |
64 | |
65 private long mContentLengthLimit; | |
66 | |
67 private boolean mCancelIfContentLengthOverLimit; | |
68 | |
69 private boolean mContentLengthOverLimit; | |
70 | |
71 private boolean mSkippingToOffset; | |
72 | |
73 private long mSize; | |
74 | |
75 private String mPostContentType; | |
76 | |
77 private byte[] mPostData; | |
78 | |
79 private ReadableByteChannel mPostDataChannel; | |
80 | |
81 private String mContentType; | |
82 | |
83 private int mHttpStatusCode; | |
84 | |
85 private String mHttpStatusText; | |
86 | |
87 private boolean mStarted; | |
88 | |
89 private boolean mCanceled; | |
90 | |
91 private String mMethod; | |
92 | |
93 private InputStream mResponseStream; | |
94 | |
95 private final Object mLock; | |
96 | |
97 private static ExecutorService sExecutorService; | |
98 | |
99 private static final Object sExecutorServiceLock = new Object(); | |
100 | |
101 HttpUrlConnectionUrlRequest(Context context, String defaultUserAgent, | |
102 String url, int requestPriority, Map<String, String> headers, | |
103 HttpUrlRequestListener listener) { | |
104 this(context, defaultUserAgent, url, requestPriority, headers, | |
105 new ChunkedWritableByteChannel(), listener); | |
106 } | |
107 | |
108 HttpUrlConnectionUrlRequest(Context context, String defaultUserAgent, | |
109 String url, int requestPriority, Map<String, String> headers, | |
110 WritableByteChannel sink, HttpUrlRequestListener listener) { | |
111 if (context == null) { | |
112 throw new NullPointerException("Context is required"); | |
113 } | |
114 if (url == null) { | |
115 throw new NullPointerException("URL is required"); | |
116 } | |
117 mContext = context; | |
118 mDefaultUserAgent = defaultUserAgent; | |
119 mUrl = url; | |
120 mHeaders = headers; | |
121 mSink = sink; | |
122 mListener = listener; | |
123 mLock = new Object(); | |
124 } | |
125 | |
126 private static ExecutorService getExecutor() { | |
127 synchronized (sExecutorServiceLock) { | |
128 if (sExecutorService == null) { | |
129 ThreadFactory threadFactory = new ThreadFactory() { | |
130 private final AtomicInteger mCount = new AtomicInteger(1); | |
131 | |
132 @Override | |
133 public Thread newThread(Runnable r) { | |
134 Thread thread = new Thread(r, | |
135 "HttpUrlConnection #" | |
136 + mCount.getAndIncrement()); | |
137 // Note that this thread is not doing actual networking. | |
138 // It's only a controller. | |
139 thread.setPriority(Thread.NORM_PRIORITY); | |
140 return thread; | |
141 } | |
142 }; | |
143 sExecutorService = Executors.newCachedThreadPool(threadFactory); | |
144 } | |
145 return sExecutorService; | |
146 } | |
147 } | |
148 | |
149 @Override | |
150 public String getUrl() { | |
151 return mUrl; | |
152 } | |
153 | |
154 @Override | |
155 public void setOffset(long offset) { | |
156 mOffset = offset; | |
157 } | |
158 | |
159 @Override | |
160 public void setContentLengthLimit(long limit, boolean cancelEarly) { | |
161 mContentLengthLimit = limit; | |
162 mCancelIfContentLengthOverLimit = cancelEarly; | |
163 } | |
164 | |
165 @Override | |
166 public void setUploadData(String contentType, byte[] data) { | |
167 validateNotStarted(); | |
168 mPostContentType = contentType; | |
169 mPostData = data; | |
170 mPostDataChannel = null; | |
171 } | |
172 | |
173 @Override | |
174 public void setUploadChannel(String contentType, | |
175 ReadableByteChannel channel, long contentLength) { | |
176 validateNotStarted(); | |
177 if (contentLength > Integer.MAX_VALUE) { | |
178 throw new IllegalArgumentException( | |
179 "Upload contentLength is too big."); | |
180 } | |
181 mUploadContentLength = (int) contentLength; | |
182 mPostContentType = contentType; | |
183 mPostDataChannel = channel; | |
184 mPostData = null; | |
185 } | |
186 | |
187 | |
188 @Override | |
189 public void setHttpMethod(String method) { | |
190 validateNotStarted(); | |
191 mMethod = method; | |
192 } | |
193 | |
194 @Override | |
195 public void disableRedirects() { | |
196 validateNotStarted(); | |
197 HttpURLConnection.setFollowRedirects(false); | |
198 } | |
199 | |
200 @Override | |
201 public void start() { | |
202 getExecutor().execute(new Runnable() { | |
203 @Override | |
204 public void run() { | |
205 startOnExecutorThread(); | |
206 } | |
207 }); | |
208 } | |
209 | |
210 private void startOnExecutorThread() { | |
211 boolean readingResponse = false; | |
212 try { | |
213 synchronized (mLock) { | |
214 if (mCanceled) { | |
215 return; | |
216 } | |
217 } | |
218 | |
219 URL url = new URL(mUrl); | |
220 mConnection = (HttpURLConnection) url.openConnection(); | |
221 // If configured, use the provided http verb. | |
222 if (mMethod != null) { | |
223 try { | |
224 mConnection.setRequestMethod(mMethod); | |
225 } catch (ProtocolException e) { | |
226 // Since request hasn't started earlier, it | |
227 // must be an illegal HTTP verb. | |
228 throw new IllegalArgumentException(e); | |
229 } | |
230 } | |
231 mConnection.setConnectTimeout(CONNECT_TIMEOUT); | |
232 mConnection.setReadTimeout(READ_TIMEOUT); | |
233 mConnection.setInstanceFollowRedirects(true); | |
234 if (mHeaders != null) { | |
235 for (Entry<String, String> header : mHeaders.entrySet()) { | |
236 mConnection.setRequestProperty(header.getKey(), | |
237 header.getValue()); | |
238 } | |
239 } | |
240 | |
241 if (mOffset != 0) { | |
242 mConnection.setRequestProperty("Range", | |
243 "bytes=" + mOffset + "-"); | |
244 } | |
245 | |
246 if (mConnection.getRequestProperty("User-Agent") == null) { | |
247 mConnection.setRequestProperty("User-Agent", mDefaultUserAgent); | |
248 } | |
249 | |
250 if (mPostData != null || mPostDataChannel != null) { | |
251 uploadData(); | |
252 } | |
253 | |
254 InputStream stream = null; | |
255 try { | |
256 // We need to open the stream before asking for the response | |
257 // code. | |
258 stream = mConnection.getInputStream(); | |
259 } catch (FileNotFoundException ex) { | |
260 // Ignore - the response has no body. | |
261 } | |
262 | |
263 mHttpStatusCode = mConnection.getResponseCode(); | |
264 mHttpStatusText = mConnection.getResponseMessage(); | |
265 mContentType = mConnection.getContentType(); | |
266 mContentLength = mConnection.getContentLength(); | |
267 if (mContentLengthLimit > 0 && mContentLength > mContentLengthLimit | |
268 && mCancelIfContentLengthOverLimit) { | |
269 onContentLengthOverLimit(); | |
270 return; | |
271 } | |
272 | |
273 mListener.onResponseStarted(this); | |
274 | |
275 mResponseStream = isError(mHttpStatusCode) ? mConnection | |
276 .getErrorStream() | |
277 : stream; | |
278 | |
279 if (mResponseStream != null | |
280 && "gzip".equals(mConnection.getContentEncoding())) { | |
281 mResponseStream = new GZIPInputStream(mResponseStream); | |
282 mContentLength = -1; | |
283 } | |
284 | |
285 if (mOffset != 0) { | |
286 // The server may ignore the request for a byte range. | |
287 if (mHttpStatusCode == HttpURLConnection.HTTP_OK) { | |
288 if (mContentLength != -1) { | |
289 mContentLength -= mOffset; | |
290 } | |
291 mSkippingToOffset = true; | |
292 } else { | |
293 mSize = mOffset; | |
294 } | |
295 } | |
296 | |
297 if (mResponseStream != null) { | |
298 readingResponse = true; | |
299 readResponseAsync(); | |
300 } | |
301 } catch (IOException e) { | |
302 mException = e; | |
303 } finally { | |
304 if (mPostDataChannel != null) { | |
305 try { | |
306 mPostDataChannel.close(); | |
307 } catch (IOException e) { | |
308 // Ignore | |
309 } | |
310 } | |
311 | |
312 // Don't call onRequestComplete yet if we are reading the response | |
313 // on a separate thread | |
314 if (!readingResponse) { | |
315 mListener.onRequestComplete(this); | |
316 } | |
317 } | |
318 } | |
319 | |
320 private void uploadData() throws IOException { | |
321 mConnection.setDoOutput(true); | |
322 if (!TextUtils.isEmpty(mPostContentType)) { | |
323 mConnection.setRequestProperty("Content-Type", mPostContentType); | |
324 } | |
325 | |
326 OutputStream uploadStream = null; | |
327 try { | |
328 if (mPostData != null) { | |
329 mConnection.setFixedLengthStreamingMode(mPostData.length); | |
330 uploadStream = mConnection.getOutputStream(); | |
331 uploadStream.write(mPostData); | |
332 } else { | |
333 mConnection.setFixedLengthStreamingMode(mUploadContentLength); | |
334 uploadStream = mConnection.getOutputStream(); | |
335 byte[] bytes = new byte[MAX_CHUNK_SIZE]; | |
336 ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); | |
337 while (mPostDataChannel.read(byteBuffer) > 0) { | |
338 byteBuffer.flip(); | |
339 uploadStream.write(bytes, 0, byteBuffer.limit()); | |
340 byteBuffer.clear(); | |
341 } | |
342 } | |
343 } finally { | |
344 if (uploadStream != null) { | |
345 uploadStream.close(); | |
346 } | |
347 } | |
348 } | |
349 | |
350 private void readResponseAsync() { | |
351 getExecutor().execute(new Runnable() { | |
352 @Override | |
353 public void run() { | |
354 readResponse(); | |
355 } | |
356 }); | |
357 } | |
358 | |
359 private void readResponse() { | |
360 try { | |
361 if (mResponseStream != null) { | |
362 readResponseStream(); | |
363 } | |
364 } catch (IOException e) { | |
365 mException = e; | |
366 } finally { | |
367 try { | |
368 mConnection.disconnect(); | |
369 } catch (ArrayIndexOutOfBoundsException t) { | |
370 // Ignore it. | |
371 } | |
372 | |
373 try { | |
374 mSink.close(); | |
375 } catch (IOException e) { | |
376 if (mException == null) { | |
377 mException = e; | |
378 } | |
379 } | |
380 } | |
381 mListener.onRequestComplete(this); | |
382 } | |
383 | |
384 private void readResponseStream() throws IOException { | |
385 byte[] buffer = new byte[MAX_CHUNK_SIZE]; | |
386 int size; | |
387 while (!isCanceled() && (size = mResponseStream.read(buffer)) != -1) { | |
388 int start = 0; | |
389 int count = size; | |
390 mSize += size; | |
391 if (mSkippingToOffset) { | |
392 if (mSize <= mOffset) { | |
393 continue; | |
394 } else { | |
395 mSkippingToOffset = false; | |
396 start = (int) (mOffset - (mSize - size)); | |
397 count -= start; | |
398 } | |
399 } | |
400 | |
401 if (mContentLengthLimit != 0 && mSize > mContentLengthLimit) { | |
402 count -= (int) (mSize - mContentLengthLimit); | |
403 if (count > 0) { | |
404 mSink.write(ByteBuffer.wrap(buffer, start, count)); | |
405 } | |
406 onContentLengthOverLimit(); | |
407 return; | |
408 } | |
409 | |
410 mSink.write(ByteBuffer.wrap(buffer, start, count)); | |
411 } | |
412 } | |
413 | |
414 @Override | |
415 public void cancel() { | |
416 synchronized (mLock) { | |
417 if (mCanceled) { | |
418 return; | |
419 } | |
420 | |
421 mCanceled = true; | |
422 } | |
423 } | |
424 | |
425 @Override | |
426 public boolean isCanceled() { | |
427 synchronized (mLock) { | |
428 return mCanceled; | |
429 } | |
430 } | |
431 | |
432 @Override | |
433 public String getNegotiatedProtocol() { | |
434 return ""; | |
435 } | |
436 | |
437 @Override | |
438 public boolean wasCached() { | |
439 return false; | |
440 } | |
441 | |
442 @Override | |
443 public int getHttpStatusCode() { | |
444 int httpStatusCode = mHttpStatusCode; | |
445 | |
446 // If we have been able to successfully resume a previously interrupted | |
447 // download, | |
448 // the status code will be 206, not 200. Since the rest of the | |
449 // application is | |
450 // expecting 200 to indicate success, we need to fake it. | |
451 if (httpStatusCode == HttpURLConnection.HTTP_PARTIAL) { | |
452 httpStatusCode = HttpURLConnection.HTTP_OK; | |
453 } | |
454 return httpStatusCode; | |
455 } | |
456 | |
457 @Override | |
458 public String getHttpStatusText() { | |
459 return mHttpStatusText; | |
460 } | |
461 | |
462 @Override | |
463 public IOException getException() { | |
464 if (mException == null && mContentLengthOverLimit) { | |
465 mException = new ResponseTooLargeException(); | |
466 } | |
467 return mException; | |
468 } | |
469 | |
470 private void onContentLengthOverLimit() { | |
471 mContentLengthOverLimit = true; | |
472 cancel(); | |
473 } | |
474 | |
475 private static boolean isError(int statusCode) { | |
476 return (statusCode / 100) != 2; | |
477 } | |
478 | |
479 /** | |
480 * Returns the response as a ByteBuffer. | |
481 */ | |
482 @Override | |
483 public ByteBuffer getByteBuffer() { | |
484 return ((ChunkedWritableByteChannel) mSink).getByteBuffer(); | |
485 } | |
486 | |
487 @Override | |
488 public byte[] getResponseAsBytes() { | |
489 return ((ChunkedWritableByteChannel) mSink).getBytes(); | |
490 } | |
491 | |
492 @Override | |
493 public long getContentLength() { | |
494 return mContentLength; | |
495 } | |
496 | |
497 @Override | |
498 public String getContentType() { | |
499 return mContentType; | |
500 } | |
501 | |
502 @Override | |
503 public String getHeader(String name) { | |
504 if (mConnection == null) { | |
505 throw new IllegalStateException("Response headers not available"); | |
506 } | |
507 Map<String, List<String>> headerFields = mConnection.getHeaderFields(); | |
508 if (headerFields != null) { | |
509 List<String> headerValues = headerFields.get(name); | |
510 if (headerValues != null) { | |
511 return TextUtils.join(", ", headerValues); | |
512 } | |
513 } | |
514 return null; | |
515 } | |
516 | |
517 @Override | |
518 public Map<String, List<String>> getAllHeaders() { | |
519 if (mConnection == null) { | |
520 throw new IllegalStateException("Response headers not available"); | |
521 } | |
522 return mConnection.getHeaderFields(); | |
523 } | |
524 | |
525 private void validateNotStarted() { | |
526 if (mStarted) { | |
527 throw new IllegalStateException("Request already started"); | |
528 } | |
529 } | |
530 } | |
OLD | NEW |