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