Skip to content

Instantly share code, notes, and snippets.

@1zaman
Last active March 2, 2021 20:31
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 1zaman/94892c192ca318e2dd6a to your computer and use it in GitHub Desktop.
Save 1zaman/94892c192ca318e2dd6a to your computer and use it in GitHub Desktop.
Delegator class to use as the HTTP stack layer for Volley and Retrofit, that enables some features that are not supported in OkHttp's API: 1) Disabling retries on non-idempotent methods (see https://github.com/square/okhttp/issues/1043). 2) Automatically serving the response from the cache when offline (see https://github.com/square/okhttp/issue…
package com.example.android;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.res.Configuration;
import android.os.StatFs;
import android.support.annotation.NonNull;
import com.android.volley.AuthFailureError;
import com.android.volley.Request;
import com.android.volley.Request.Method;
import com.android.volley.toolbox.HttpStack;
import com.squareup.okhttp.Cache;
import com.squareup.okhttp.CacheControl;
import com.squareup.okhttp.Call;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.ResponseBody;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolVersion;
import org.apache.http.StatusLine;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.message.BasicStatusLine;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import okio.BufferedSink;
import retrofit.client.Client;
import retrofit.mime.TypedInput;
import retrofit.mime.TypedOutput;
// Derived from HurlStack and OkClient
public class OkHttpDelegator implements HttpStack, Client {
private static final long DEFAULT_CONNECT_TIMEOUT_SECONDS = 50;
private static final long DEFAULT_READ_TIMEOUT_SECONDS = 60;
private static final String CACHE_DIR_NAME = "http-cache";
private static final int MIN_CACHE_SIZE = 1 * 1024 * 1024; // 5MB
private static final int MAX_CACHE_SIZE = 10 * 1024 * 1024; // 10MB
private static OkHttpDelegator instance;
private boolean isOffline = true;
public static synchronized OkHttpDelegator getInstance(Context context) {
if (instance == null) {
instance = new OkHttpDelegator(context);
}
return instance;
}
private static OkHttpClient generateDefaultClient(Context context) {
OkHttpClient client = new OkHttpClient();
client.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
client.setReadTimeout(DEFAULT_READ_TIMEOUT_SECONDS, TimeUnit.SECONDS);
File cacheDir = new File(context.getApplicationContext().getCacheDir(), CACHE_DIR_NAME);
if (!cacheDir.exists()) {
// noinspection ResultOfMethodCallIgnored
cacheDir.mkdirs();
}
if (cacheDir.isDirectory()) {
long maxSize;
try {
StatFs statFs = new StatFs(cacheDir.getAbsolutePath());
@SuppressWarnings("deprecation")
long available = ((long) statFs.getBlockCount()) * statFs.getBlockSize();
// Target 2% of the total space.
long target = available / 50;
// Bound inside min/max size for disk cache.
maxSize = Math.max(Math.min(target, MAX_CACHE_SIZE), MIN_CACHE_SIZE);
} catch (IllegalArgumentException e) {
maxSize = MIN_CACHE_SIZE;
}
client.setCache(new Cache(cacheDir, maxSize));
}
return client;
}
private static OkHttpClient getClient(OkHttpClient client, boolean shouldRetry) {
if (client.getRetryOnConnectionFailure() != shouldRetry) {
client = client.clone();
client.setRetryOnConnectionFailure(shouldRetry);
}
return client;
}
private final OkHttpClient mRetryingClient, mNonRetryingClient;
private OkHttpDelegator(Context context) {
this(generateDefaultClient(context));
isOffline = false;
context.registerComponentCallbacks(new ComponentCallbacks2() {
@Override
public void onTrimMemory(int level) {
if (level == TRIM_MEMORY_UI_HIDDEN) {
isOffline = false;
}
}
@Override public void onConfigurationChanged(Configuration newConfig) {}
@Override public void onLowMemory() {}
});
}
private OkHttpDelegator(@NonNull OkHttpClient client) {
this(getClient(client, true), getClient(client, false));
}
private OkHttpDelegator(@NonNull OkHttpClient retryingClient, @NonNull OkHttpClient nonRetryingClient) {
mRetryingClient = retryingClient;
mNonRetryingClient = nonRetryingClient;
}
private void notifyOffline() {
if (!isOffline) {
// Dialog prompt here
isOffline = true;
}
}
private Call buildCall(Request<?> request, Map<String, String> additionalHeaders, CacheControl cacheControl)
throws AuthFailureError {
int method = request.getMethod();
final byte[] body;
switch (method) {
case Method.DEPRECATED_GET_OR_POST:
// noinspection deprecation
body = request.getPostBody();
if (body == null) {
method = Method.GET;
} else {
method = Method.POST;
}
break;
case Method.PUT:
case Method.POST:
case Method.PATCH:
body = request.getBody();
break;
default:
body = null;
}
RequestBody requestBody;
if (body == null) {
requestBody = null;
} else {
final MediaType mediaType = MediaType.parse(request.getBodyContentType());
requestBody = new RequestBody() {
@Override
public MediaType contentType() {
return mediaType;
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
sink.outputStream().write(body);
}
@Override
public long contentLength() {
return body.length;
}
};
}
String methodString;
switch (method) {
case Method.GET:
methodString = "GET";
break;
case Method.DELETE:
methodString = "DELETE";
break;
case Method.POST:
methodString = "POST";
break;
case Method.PUT:
methodString = "PUT";
break;
case Method.HEAD:
methodString = "HEAD";
break;
case Method.OPTIONS:
methodString = "OPTIONS";
break;
case Method.TRACE:
methodString = "TRACE";
break;
case Method.PATCH:
methodString = "PATCH";
break;
default:
throw new IllegalStateException("Unknown method type.");
}
com.squareup.okhttp.Request.Builder builder = new com.squareup.okhttp.Request.Builder()
.url(request.getUrl())
.method(methodString, requestBody);
for (Map.Entry<String, String> header : request.getHeaders().entrySet()) {
String value = header.getValue();
if (value == null) value = "";
builder.addHeader(header.getKey(), value);
}
for (Map.Entry<String, String> header : additionalHeaders.entrySet()) {
String value = header.getValue();
if (value == null) value = "";
builder.addHeader(header.getKey(), value);
}
if (cacheControl != null) {
builder.cacheControl(cacheControl);
}
com.squareup.okhttp.Request clientRequest = builder.build();
OkHttpClient client;
switch (method) {
case Method.POST:
case Method.PATCH:
client = mNonRetryingClient;
break;
default:
client = mRetryingClient;
}
client = client.clone();
int timeoutMs = request.getTimeoutMs();
client.setConnectTimeout(timeoutMs, TimeUnit.MILLISECONDS);
client.setReadTimeout(timeoutMs, TimeUnit.MILLISECONDS);
return client.newCall(clientRequest);
}
@Override
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError {
Call call = buildCall(request, additionalHeaders, null);
Response response;
try {
response = call.execute();
} catch (IOException e) {
notifyOffline();
// noinspection unchecked
call = buildCall(request, Collections.EMPTY_MAP, CacheControl.FORCE_CACHE);
response = call.execute();
}
int responseCode = response.code();
if (responseCode == -1) {
throw new IOException("Could not retrieve response code from HttpUrlConnection.");
}
StatusLine responseStatus = new BasicStatusLine(
new ProtocolVersion("HTTP", 1, 1),
responseCode, response.message());
BasicHttpResponse httpResponse = new BasicHttpResponse(responseStatus);
BasicHttpEntity entity = new BasicHttpEntity();
ResponseBody responseBody = response.body();
entity.setContent(responseBody.byteStream());
entity.setContentLength(responseBody.contentLength());
entity.setContentEncoding(response.header("Content-Encoding"));
MediaType contentType = responseBody.contentType();
if (contentType != null) {
entity.setContentType(contentType.toString());
}
httpResponse.setEntity(entity);
Headers headers = response.headers();
for (int i = 0, size = headers.size(); i < size; i++) {
Header h = new BasicHeader(headers.name(i), headers.value(i));
httpResponse.addHeader(h);
}
return httpResponse;
}
private Call buildCall(retrofit.client.Request request, CacheControl cacheControl) {
RequestBody requestBody;
final TypedOutput body = request.getBody();
if (body == null) {
requestBody = null;
} else {
final MediaType mediaType = MediaType.parse(body.mimeType());
requestBody = new RequestBody() {
@Override
public MediaType contentType() {
return mediaType;
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
body.writeTo(sink.outputStream());
}
@Override
public long contentLength() {
return body.length();
}
};
}
com.squareup.okhttp.Request.Builder builder = new com.squareup.okhttp.Request.Builder()
.url(request.getUrl())
.method(request.getMethod(), requestBody);
List<retrofit.client.Header> headers = request.getHeaders();
for (int i = 0, size = headers.size(); i < size; i++) {
retrofit.client.Header header = headers.get(i);
String value = header.getValue();
if (value == null) value = "";
builder.addHeader(header.getName(), value);
}
if (cacheControl != null) {
builder.cacheControl(cacheControl);
}
com.squareup.okhttp.Request clientRequest = builder.build();
String method = request.getMethod();
OkHttpClient client = method.equals("POST") || method.equals("PATCH") ?
mNonRetryingClient : mRetryingClient;
return client.newCall(clientRequest);
}
@Override
public retrofit.client.Response execute(retrofit.client.Request request) throws IOException {
Call call = buildCall(request, null);
Response response;
try {
response = call.execute();
} catch (IOException e) {
notifyOffline();
call = buildCall(request, CacheControl.FORCE_CACHE);
response = call.execute();
}
List<retrofit.client.Header> headerList; {
Headers headers = response.headers();
int size = headers.size();
headerList = new ArrayList<retrofit.client.Header>(size);
for (int i = 0; i < size; i++) {
headerList.add(new retrofit.client.Header(headers.name(i), headers.value(i)));
}
}
TypedInput responseBody; {
final ResponseBody body = response.body();
if (body.contentLength() == 0) {
responseBody = null;
} else {
responseBody = new TypedInput() {
@Override
public String mimeType() {
MediaType mediaType = body.contentType();
return mediaType == null ? null : mediaType.toString();
}
@Override
public long length() {
try {
return body.contentLength();
} catch (IOException e) {
e.printStackTrace();
return -1;
}
}
@Override
public InputStream in() throws IOException {
return body.byteStream();
}
};
}
}
return new retrofit.client.Response(response.request().urlString(),
response.code(), response.message(), headerList, responseBody);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment