1 /* 2 * Copyright (C) 2011 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 com.android.volley.toolbox; 18 19 import android.os.SystemClock; 20 21 import com.android.volley.AuthFailureError; 22 import com.android.volley.Cache; 23 import com.android.volley.Cache.Entry; 24 import com.android.volley.Network; 25 import com.android.volley.NetworkError; 26 import com.android.volley.NetworkResponse; 27 import com.android.volley.NoConnectionError; 28 import com.android.volley.Request; 29 import com.android.volley.RetryPolicy; 30 import com.android.volley.ServerError; 31 import com.android.volley.TimeoutError; 32 import com.android.volley.VolleyError; 33 import com.android.volley.VolleyLog; 34 35 import org.apache.http.Header; 36 import org.apache.http.HttpEntity; 37 import org.apache.http.HttpResponse; 38 import org.apache.http.HttpStatus; 39 import org.apache.http.StatusLine; 40 import org.apache.http.conn.ConnectTimeoutException; 41 import org.apache.http.impl.cookie.DateUtils; 42 43 import java.io.IOException; 44 import java.io.InputStream; 45 import java.net.MalformedURLException; 46 import java.net.SocketTimeoutException; 47 import java.util.Collections; 48 import java.util.Date; 49 import java.util.HashMap; 50 import java.util.Map; 51 import java.util.TreeMap; 52 53 /** 54 * A network performing Volley requests over an {@link HttpStack}. 55 */ 56 public class BasicNetwork implements Network { 57 protected static final boolean DEBUG = VolleyLog.DEBUG; 58 59 private static int SLOW_REQUEST_THRESHOLD_MS = 3000; 60 61 private static int DEFAULT_POOL_SIZE = 4096; 62 63 protected final HttpStack mHttpStack; 64 65 protected final ByteArrayPool mPool; 66 67 /** 68 * @param httpStack HTTP stack to be used 69 */ BasicNetwork(HttpStack httpStack)70 public BasicNetwork(HttpStack httpStack) { 71 // If a pool isn't passed in, then build a small default pool that will give us a lot of 72 // benefit and not use too much memory. 73 this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE)); 74 } 75 76 /** 77 * @param httpStack HTTP stack to be used 78 * @param pool a buffer pool that improves GC performance in copy operations 79 */ BasicNetwork(HttpStack httpStack, ByteArrayPool pool)80 public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) { 81 mHttpStack = httpStack; 82 mPool = pool; 83 } 84 85 @Override performRequest(Request<?> request)86 public NetworkResponse performRequest(Request<?> request) throws VolleyError { 87 long requestStart = SystemClock.elapsedRealtime(); 88 while (true) { 89 HttpResponse httpResponse = null; 90 byte[] responseContents = null; 91 Map<String, String> responseHeaders = Collections.emptyMap(); 92 try { 93 // Gather headers. 94 Map<String, String> headers = new HashMap<String, String>(); 95 addCacheHeaders(headers, request.getCacheEntry()); 96 httpResponse = mHttpStack.performRequest(request, headers); 97 StatusLine statusLine = httpResponse.getStatusLine(); 98 int statusCode = statusLine.getStatusCode(); 99 100 responseHeaders = convertHeaders(httpResponse.getAllHeaders()); 101 // Handle cache validation. 102 if (statusCode == HttpStatus.SC_NOT_MODIFIED) { 103 104 Entry entry = request.getCacheEntry(); 105 if (entry == null) { 106 return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null, 107 responseHeaders, true, 108 SystemClock.elapsedRealtime() - requestStart); 109 } 110 111 // A HTTP 304 response does not have all header fields. We 112 // have to use the header fields from the cache entry plus 113 // the new ones from the response. 114 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5 115 entry.responseHeaders.putAll(responseHeaders); 116 return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data, 117 entry.responseHeaders, true, 118 SystemClock.elapsedRealtime() - requestStart); 119 } 120 121 // Some responses such as 204s do not have content. We must check. 122 if (httpResponse.getEntity() != null) { 123 responseContents = entityToBytes(httpResponse.getEntity()); 124 } else { 125 // Add 0 byte response as a way of honestly representing a 126 // no-content request. 127 responseContents = new byte[0]; 128 } 129 130 // if the request is slow, log it. 131 long requestLifetime = SystemClock.elapsedRealtime() - requestStart; 132 logSlowRequests(requestLifetime, request, responseContents, statusLine); 133 134 if (statusCode < 200 || statusCode > 299) { 135 throw new IOException(); 136 } 137 return new NetworkResponse(statusCode, responseContents, responseHeaders, false, 138 SystemClock.elapsedRealtime() - requestStart); 139 } catch (SocketTimeoutException e) { 140 attemptRetryOnException("socket", request, new TimeoutError()); 141 } catch (ConnectTimeoutException e) { 142 attemptRetryOnException("connection", request, new TimeoutError()); 143 } catch (MalformedURLException e) { 144 throw new RuntimeException("Bad URL " + request.getUrl(), e); 145 } catch (IOException e) { 146 int statusCode = 0; 147 NetworkResponse networkResponse = null; 148 if (httpResponse != null) { 149 statusCode = httpResponse.getStatusLine().getStatusCode(); 150 } else { 151 throw new NoConnectionError(e); 152 } 153 VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl()); 154 if (responseContents != null) { 155 networkResponse = new NetworkResponse(statusCode, responseContents, 156 responseHeaders, false, SystemClock.elapsedRealtime() - requestStart); 157 if (statusCode == HttpStatus.SC_UNAUTHORIZED || 158 statusCode == HttpStatus.SC_FORBIDDEN) { 159 attemptRetryOnException("auth", 160 request, new AuthFailureError(networkResponse)); 161 } else { 162 // TODO: Only throw ServerError for 5xx status codes. 163 throw new ServerError(networkResponse); 164 } 165 } else { 166 throw new NetworkError(networkResponse); 167 } 168 } 169 } 170 } 171 172 /** 173 * Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete. 174 */ logSlowRequests(long requestLifetime, Request<?> request, byte[] responseContents, StatusLine statusLine)175 private void logSlowRequests(long requestLifetime, Request<?> request, 176 byte[] responseContents, StatusLine statusLine) { 177 if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) { 178 VolleyLog.d("HTTP response for request=<%s> [lifetime=%d], [size=%s], " + 179 "[rc=%d], [retryCount=%s]", request, requestLifetime, 180 responseContents != null ? responseContents.length : "null", 181 statusLine.getStatusCode(), request.getRetryPolicy().getCurrentRetryCount()); 182 } 183 } 184 185 /** 186 * Attempts to prepare the request for a retry. If there are no more attempts remaining in the 187 * request's retry policy, a timeout exception is thrown. 188 * @param request The request to use. 189 */ attemptRetryOnException(String logPrefix, Request<?> request, VolleyError exception)190 private static void attemptRetryOnException(String logPrefix, Request<?> request, 191 VolleyError exception) throws VolleyError { 192 RetryPolicy retryPolicy = request.getRetryPolicy(); 193 int oldTimeout = request.getTimeoutMs(); 194 195 try { 196 retryPolicy.retry(exception); 197 } catch (VolleyError e) { 198 request.addMarker( 199 String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout)); 200 throw e; 201 } 202 request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout)); 203 } 204 addCacheHeaders(Map<String, String> headers, Cache.Entry entry)205 private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) { 206 // If there's no cache entry, we're done. 207 if (entry == null) { 208 return; 209 } 210 211 if (entry.etag != null) { 212 headers.put("If-None-Match", entry.etag); 213 } 214 215 if (entry.lastModified > 0) { 216 Date refTime = new Date(entry.lastModified); 217 headers.put("If-Modified-Since", DateUtils.formatDate(refTime)); 218 } 219 } 220 logError(String what, String url, long start)221 protected void logError(String what, String url, long start) { 222 long now = SystemClock.elapsedRealtime(); 223 VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start), url); 224 } 225 226 /** Reads the contents of HttpEntity into a byte[]. */ entityToBytes(HttpEntity entity)227 private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError { 228 PoolingByteArrayOutputStream bytes = 229 new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength()); 230 byte[] buffer = null; 231 try { 232 InputStream in = entity.getContent(); 233 if (in == null) { 234 throw new ServerError(); 235 } 236 buffer = mPool.getBuf(1024); 237 int count; 238 while ((count = in.read(buffer)) != -1) { 239 bytes.write(buffer, 0, count); 240 } 241 return bytes.toByteArray(); 242 } finally { 243 try { 244 // Close the InputStream and release the resources by "consuming the content". 245 entity.consumeContent(); 246 } catch (IOException e) { 247 // This can happen if there was an exception above that left the entity in 248 // an invalid state. 249 VolleyLog.v("Error occured when calling consumingContent"); 250 } 251 mPool.returnBuf(buffer); 252 bytes.close(); 253 } 254 } 255 256 /** 257 * Converts Headers[] to Map<String, String>. 258 */ convertHeaders(Header[] headers)259 protected static Map<String, String> convertHeaders(Header[] headers) { 260 Map<String, String> result = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER); 261 for (int i = 0; i < headers.length; i++) { 262 result.put(headers[i].getName(), headers[i].getValue()); 263 } 264 return result; 265 } 266 } 267