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