package im.amomo.volley; import android.os.SystemClock; import android.text.format.DateUtils; import com.android.volley.AuthFailureError; import com.android.volley.Cache; 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 com.android.volley.toolbox.ByteArrayPool; import com.squareup.okhttp.Response; import org.apache.http.conn.ConnectTimeoutException; import java.io.IOException; import java.lang.ref.SoftReference; import java.net.MalformedURLException; import java.net.SocketTimeoutException; import java.text.SimpleDateFormat; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.TimeZone; import java.util.TreeMap; import okio.Buffer; import okio.GzipSource; /** * Created by GoogolMo on 11/26/13. */ public class OkNetwork implements Network { protected static final boolean DEBUG = VolleyLog.DEBUG; private static final int SLOW_REQUEST_THRESHOLD_MS = 3000; private static final int DEFAULT_POOL_SIZE = 4096; protected final OkStack mHttpStack; public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz"; protected final ByteArrayPool mPool; /** * @param httpStack HTTP stack to be used */ public OkNetwork(OkStack 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 OkNetwork(OkStack httpStack, ByteArrayPool pool) { mHttpStack = httpStack; mPool = pool; } @Override public NetworkResponse performRequest(Request<?> request) throws VolleyError { long requestStart = SystemClock.elapsedRealtime(); while (true) { Response 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); int statusCode = httpResponse.code(); responseHeaders = new TreeMap<String, String>(); for (String field : httpResponse.headers() .names()) { responseHeaders.put(field, httpResponse.headers() .get(field)); } // Handle cache validation. if (statusCode == 304) { return new NetworkResponse(304, request.getCacheEntry().data, responseHeaders, true); } if (httpResponse.body() != null) { if (responseGzip(responseHeaders)) { Buffer buffer = new Buffer(); GzipSource gzipSource = new GzipSource(httpResponse.body() .source()); while (gzipSource.read(buffer, Integer.MAX_VALUE) != -1) { } responseContents = buffer.readByteArray(); } else { responseContents = httpResponse.body() .bytes(); } } else { responseContents = new byte[0]; } // // Some responses such as 204s do not have content. We must check. // if (httpResponse.getEntity() != null) { // responseContents = entityToBytes(httpResponse.getEntity() // , responseGzip(responseHeaders)); // } 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, httpResponse); if (statusCode < 200 || statusCode > 299) { throw new IOException(); } return new NetworkResponse(statusCode, responseContents, responseHeaders, false); } 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; NetworkResponse networkResponse = null; if (httpResponse != null) { statusCode = httpResponse.code(); } 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); if (statusCode == 401 || statusCode == 403) { attemptRetryOnException("auth", request, new AuthFailureError(networkResponse)); } else { // TODO: Only throw ServerError for 5xx status codes. throw new ServerError(networkResponse); } } else { throw new NetworkError(networkResponse); } } } } private static boolean responseGzip(Map<String, String> headers) { for (Map.Entry<String, String> entry : headers.entrySet()) { if (entry.getKey() .toLowerCase() .equals(im.amomo.volley.OkRequest.HEADER_CONTENT_ENCODING.toLowerCase()) && entry.getValue() .toLowerCase() .equals(im.amomo.volley.OkRequest.ENCODING_GZIP.toLowerCase())) { return true; } } return false; } /** * Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete. */ private void logSlowRequests(long requestLifetime, Request<?> request, byte[] responseContents, Response response) { 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", response.code(), 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); final SimpleDateFormat formatter = DateFormatHolder.formatFor(PATTERN_RFC1036); headers.put("If-Modified-Since", formatter.format(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); } /** * A factory for {@link SimpleDateFormat}s. The instances are stored in a * threadlocal way because SimpleDateFormat is not threadsafe as noted in * {@link SimpleDateFormat its javadoc}. * */ final static class DateFormatHolder { private static final ThreadLocal<SoftReference<Map<String, SimpleDateFormat>>> THREADLOCAL_FORMATS = new ThreadLocal<SoftReference<Map<String, SimpleDateFormat>>>() { @Override protected SoftReference<Map<String, SimpleDateFormat>> initialValue() { return new SoftReference<Map<String, SimpleDateFormat>>( new HashMap<String, SimpleDateFormat>()); } }; /** * creates a {@link SimpleDateFormat} for the requested format string. * * @param pattern * a non-{@code null} format String according to * {@link SimpleDateFormat}. The format is not checked against * {@code null} since all paths go through * {@link DateUtils}. * @return the requested format. This simple dateformat should not be used * to {@link SimpleDateFormat#applyPattern(String) apply} to a * different pattern. */ public static SimpleDateFormat formatFor(final String pattern) { final SoftReference<Map<String, SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get(); Map<String, SimpleDateFormat> formats = ref.get(); if (formats == null) { formats = new HashMap<>(); THREADLOCAL_FORMATS.set( new SoftReference<>(formats)); } SimpleDateFormat format = formats.get(pattern); if (format == null) { format = new SimpleDateFormat(pattern, Locale.US); format.setTimeZone(TimeZone.getTimeZone("GMT")); formats.put(pattern, format); } return format; } public static void clearThreadLocal() { THREADLOCAL_FORMATS.remove(); } } }