Home | History | Annotate | Download | only in toolbox
      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      */
     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      */
     77     public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
     78         mHttpStack = httpStack;
     79         mPool = pool;
     80     }
     81 
     82     @Override
     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() == null ? null : request.getCacheEntry().data,
    102                             responseHeaders, true);
    103                 }
    104 
    105                 // Some responses such as 204s do not have content.  We must check.
    106                 if (httpResponse.getEntity() != null) {
    107                   responseContents = entityToBytes(httpResponse.getEntity());
    108                 } else {
    109                   // Add 0 byte response as a way of honestly representing a
    110                   // no-content request.
    111                   responseContents = new byte[0];
    112                 }
    113 
    114                 // if the request is slow, log it.
    115                 long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
    116                 logSlowRequests(requestLifetime, request, responseContents, statusLine);
    117 
    118                 if (statusCode < 200 || statusCode > 299) {
    119                     throw new IOException();
    120                 }
    121                 return new NetworkResponse(statusCode, responseContents, responseHeaders, false);
    122             } catch (SocketTimeoutException e) {
    123                 attemptRetryOnException("socket", request, new TimeoutError());
    124             } catch (ConnectTimeoutException e) {
    125                 attemptRetryOnException("connection", request, new TimeoutError());
    126             } catch (MalformedURLException e) {
    127                 throw new RuntimeException("Bad URL " + request.getUrl(), e);
    128             } catch (IOException e) {
    129                 int statusCode = 0;
    130                 NetworkResponse networkResponse = null;
    131                 if (httpResponse != null) {
    132                     statusCode = httpResponse.getStatusLine().getStatusCode();
    133                 } else {
    134                     throw new NoConnectionError(e);
    135                 }
    136                 VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
    137                 if (responseContents != null) {
    138                     networkResponse = new NetworkResponse(statusCode, responseContents,
    139                             responseHeaders, false);
    140                     if (statusCode == HttpStatus.SC_UNAUTHORIZED ||
    141                             statusCode == HttpStatus.SC_FORBIDDEN) {
    142                         attemptRetryOnException("auth",
    143                                 request, new AuthFailureError(networkResponse));
    144                     } else {
    145                         // TODO: Only throw ServerError for 5xx status codes.
    146                         throw new ServerError(networkResponse);
    147                     }
    148                 } else {
    149                     throw new NetworkError(networkResponse);
    150                 }
    151             }
    152         }
    153     }
    154 
    155     /**
    156      * Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete.
    157      */
    158     private void logSlowRequests(long requestLifetime, Request<?> request,
    159             byte[] responseContents, StatusLine statusLine) {
    160         if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) {
    161             VolleyLog.d("HTTP response for request=<%s> [lifetime=%d], [size=%s], " +
    162                     "[rc=%d], [retryCount=%s]", request, requestLifetime,
    163                     responseContents != null ? responseContents.length : "null",
    164                     statusLine.getStatusCode(), request.getRetryPolicy().getCurrentRetryCount());
    165         }
    166     }
    167 
    168     /**
    169      * Attempts to prepare the request for a retry. If there are no more attempts remaining in the
    170      * request's retry policy, a timeout exception is thrown.
    171      * @param request The request to use.
    172      */
    173     private static void attemptRetryOnException(String logPrefix, Request<?> request,
    174             VolleyError exception) throws VolleyError {
    175         RetryPolicy retryPolicy = request.getRetryPolicy();
    176         int oldTimeout = request.getTimeoutMs();
    177 
    178         try {
    179             retryPolicy.retry(exception);
    180         } catch (VolleyError e) {
    181             request.addMarker(
    182                     String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
    183             throw e;
    184         }
    185         request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
    186     }
    187 
    188     private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
    189         // If there's no cache entry, we're done.
    190         if (entry == null) {
    191             return;
    192         }
    193 
    194         if (entry.etag != null) {
    195             headers.put("If-None-Match", entry.etag);
    196         }
    197 
    198         if (entry.serverDate > 0) {
    199             Date refTime = new Date(entry.serverDate);
    200             headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
    201         }
    202     }
    203 
    204     protected void logError(String what, String url, long start) {
    205         long now = SystemClock.elapsedRealtime();
    206         VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start), url);
    207     }
    208 
    209     /** Reads the contents of HttpEntity into a byte[]. */
    210     private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError {
    211         PoolingByteArrayOutputStream bytes =
    212                 new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength());
    213         byte[] buffer = null;
    214         try {
    215             InputStream in = entity.getContent();
    216             if (in == null) {
    217                 throw new ServerError();
    218             }
    219             buffer = mPool.getBuf(1024);
    220             int count;
    221             while ((count = in.read(buffer)) != -1) {
    222                 bytes.write(buffer, 0, count);
    223             }
    224             return bytes.toByteArray();
    225         } finally {
    226             try {
    227                 // Close the InputStream and release the resources by "consuming the content".
    228                 entity.consumeContent();
    229             } catch (IOException e) {
    230                 // This can happen if there was an exception above that left the entity in
    231                 // an invalid state.
    232                 VolleyLog.v("Error occured when calling consumingContent");
    233             }
    234             mPool.returnBuf(buffer);
    235             bytes.close();
    236         }
    237     }
    238 
    239     /**
    240      * Converts Headers[] to Map<String, String>.
    241      */
    242     private static Map<String, String> convertHeaders(Header[] headers) {
    243         Map<String, String> result = new HashMap<String, String>();
    244         for (int i = 0; i < headers.length; i++) {
    245             result.put(headers[i].getName(), headers[i].getValue());
    246         }
    247         return result;
    248     }
    249 }
    250