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();
}
}
}