diff options
Diffstat (limited to 'src/main/java/com/android/volley/toolbox/BasicNetwork.java')
-rw-r--r-- | src/main/java/com/android/volley/toolbox/BasicNetwork.java | 266 |
1 files changed, 266 insertions, 0 deletions
diff --git a/src/main/java/com/android/volley/toolbox/BasicNetwork.java b/src/main/java/com/android/volley/toolbox/BasicNetwork.java new file mode 100644 index 0000000..bc1cfdb --- /dev/null +++ b/src/main/java/com/android/volley/toolbox/BasicNetwork.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import android.os.SystemClock; + +import com.android.volley.AuthFailureError; +import com.android.volley.Cache; +import com.android.volley.Cache.Entry; +import com.android.volley.Network; +import com.android.volley.NetworkError; +import com.android.volley.NetworkResponse; +import com.android.volley.NoConnectionError; +import com.android.volley.Request; +import com.android.volley.RetryPolicy; +import com.android.volley.ServerError; +import com.android.volley.TimeoutError; +import com.android.volley.VolleyError; +import com.android.volley.VolleyLog; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.impl.cookie.DateUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.SocketTimeoutException; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +/** + * A network performing Volley requests over an {@link HttpStack}. + */ +public class BasicNetwork implements Network { + protected static final boolean DEBUG = VolleyLog.DEBUG; + + private static int SLOW_REQUEST_THRESHOLD_MS = 3000; + + private static int DEFAULT_POOL_SIZE = 4096; + + protected final HttpStack mHttpStack; + + protected final ByteArrayPool mPool; + + /** + * @param httpStack HTTP stack to be used + */ + public BasicNetwork(HttpStack httpStack) { + // If a pool isn't passed in, then build a small default pool that will give us a lot of + // benefit and not use too much memory. + this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE)); + } + + /** + * @param httpStack HTTP stack to be used + * @param pool a buffer pool that improves GC performance in copy operations + */ + public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) { + mHttpStack = httpStack; + mPool = pool; + } + + @Override + public NetworkResponse performRequest(Request<?> request) throws VolleyError { + long requestStart = SystemClock.elapsedRealtime(); + while (true) { + HttpResponse httpResponse = null; + byte[] responseContents = null; + Map<String, String> responseHeaders = Collections.emptyMap(); + try { + // Gather headers. + Map<String, String> headers = new HashMap<String, String>(); + addCacheHeaders(headers, request.getCacheEntry()); + httpResponse = mHttpStack.performRequest(request, headers); + StatusLine statusLine = httpResponse.getStatusLine(); + int statusCode = statusLine.getStatusCode(); + + responseHeaders = convertHeaders(httpResponse.getAllHeaders()); + // Handle cache validation. + if (statusCode == HttpStatus.SC_NOT_MODIFIED) { + + Entry entry = request.getCacheEntry(); + if (entry == null) { + return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null, + responseHeaders, true, + SystemClock.elapsedRealtime() - requestStart); + } + + // A HTTP 304 response does not have all header fields. We + // have to use the header fields from the cache entry plus + // the new ones from the response. + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5 + entry.responseHeaders.putAll(responseHeaders); + return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data, + entry.responseHeaders, true, + SystemClock.elapsedRealtime() - requestStart); + } + + // Some responses such as 204s do not have content. We must check. + if (httpResponse.getEntity() != null) { + responseContents = entityToBytes(httpResponse.getEntity()); + } else { + // Add 0 byte response as a way of honestly representing a + // no-content request. + responseContents = new byte[0]; + } + + // if the request is slow, log it. + long requestLifetime = SystemClock.elapsedRealtime() - requestStart; + logSlowRequests(requestLifetime, request, responseContents, statusLine); + + if (statusCode < 200 || statusCode > 299) { + throw new IOException(); + } + return new NetworkResponse(statusCode, responseContents, responseHeaders, false, + SystemClock.elapsedRealtime() - requestStart); + } catch (SocketTimeoutException e) { + attemptRetryOnException("socket", request, new TimeoutError()); + } catch (ConnectTimeoutException e) { + attemptRetryOnException("connection", request, new TimeoutError()); + } catch (MalformedURLException e) { + throw new RuntimeException("Bad URL " + request.getUrl(), e); + } catch (IOException e) { + int statusCode = 0; + NetworkResponse networkResponse = null; + if (httpResponse != null) { + statusCode = httpResponse.getStatusLine().getStatusCode(); + } else { + throw new NoConnectionError(e); + } + VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl()); + if (responseContents != null) { + networkResponse = new NetworkResponse(statusCode, responseContents, + responseHeaders, false, SystemClock.elapsedRealtime() - requestStart); + if (statusCode == HttpStatus.SC_UNAUTHORIZED || + statusCode == HttpStatus.SC_FORBIDDEN) { + attemptRetryOnException("auth", + request, new AuthFailureError(networkResponse)); + } else { + // TODO: Only throw ServerError for 5xx status codes. + throw new ServerError(networkResponse); + } + } else { + throw new NetworkError(networkResponse); + } + } + } + } + + /** + * Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete. + */ + private void logSlowRequests(long requestLifetime, Request<?> request, + byte[] responseContents, StatusLine statusLine) { + if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) { + VolleyLog.d("HTTP response for request=<%s> [lifetime=%d], [size=%s], " + + "[rc=%d], [retryCount=%s]", request, requestLifetime, + responseContents != null ? responseContents.length : "null", + statusLine.getStatusCode(), request.getRetryPolicy().getCurrentRetryCount()); + } + } + + /** + * Attempts to prepare the request for a retry. If there are no more attempts remaining in the + * request's retry policy, a timeout exception is thrown. + * @param request The request to use. + */ + private static void attemptRetryOnException(String logPrefix, Request<?> request, + VolleyError exception) throws VolleyError { + RetryPolicy retryPolicy = request.getRetryPolicy(); + int oldTimeout = request.getTimeoutMs(); + + try { + retryPolicy.retry(exception); + } catch (VolleyError e) { + request.addMarker( + String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout)); + throw e; + } + request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout)); + } + + private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) { + // If there's no cache entry, we're done. + if (entry == null) { + return; + } + + if (entry.etag != null) { + headers.put("If-None-Match", entry.etag); + } + + if (entry.serverDate > 0) { + Date refTime = new Date(entry.serverDate); + headers.put("If-Modified-Since", DateUtils.formatDate(refTime)); + } + } + + protected void logError(String what, String url, long start) { + long now = SystemClock.elapsedRealtime(); + VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start), url); + } + + /** Reads the contents of HttpEntity into a byte[]. */ + private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError { + PoolingByteArrayOutputStream bytes = + new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength()); + byte[] buffer = null; + try { + InputStream in = entity.getContent(); + if (in == null) { + throw new ServerError(); + } + buffer = mPool.getBuf(1024); + int count; + while ((count = in.read(buffer)) != -1) { + bytes.write(buffer, 0, count); + } + return bytes.toByteArray(); + } finally { + try { + // Close the InputStream and release the resources by "consuming the content". + entity.consumeContent(); + } catch (IOException e) { + // This can happen if there was an exception above that left the entity in + // an invalid state. + VolleyLog.v("Error occured when calling consumingContent"); + } + mPool.returnBuf(buffer); + bytes.close(); + } + } + + /** + * Converts Headers[] to Map<String, String>. + */ + protected static Map<String, String> convertHeaders(Header[] headers) { + Map<String, String> result = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER); + for (int i = 0; i < headers.length; i++) { + result.put(headers[i].getName(), headers[i].getValue()); + } + return result; + } +} |