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