Enabling and Disabling Cache : Retrofit ,Okhttp Implementation - android

I have an Implementation of Retrofit with OkHTTP as a client configured with an Interceptor to enable Cache Feature. Well the cache Works fine , But I have trouble disabling Cache using the Mechanisms I already have. I do have a solution for this : Not using the Code Containing cache mechanism and use a normal retrofit implementation each time I want to disable Cache.
Here Is the Full Code . Service Generator to turn a retrofit Interface to a reusable client. and Request A Request/response Interceptor
package com.example.infinite.adapter;
import android.content.Context;
import android.util.Log;
import com.example.infinite.settings.Config;
import com.example.infinite.utils.ConnectionDetector;
import com.example.infinite.utils.SharedPreferenceEasy;
import com.squareup.okhttp.Cache;
import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import retrofit.RequestInterceptor;
import retrofit.RestAdapter;
import retrofit.android.MainThreadExecutor;
import retrofit.client.OkClient;
/**
* Created by user on 5/18/2015.
* Create REST Adapter for Given Class
*/
public class ServiceGenerator {
protected static RestAdapter mRestAdapter;
static String secret = "Schhh!!";
private static Context mContext;
private static final Interceptor mCacheControlInterceptor = new Interceptor() {
#Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// Add Cache Control only for GET methods
if (request.method().equals("GET")) {
//see if cache is enabled
if (enableCache) {
if (new ConnectionDetector(mContext).isConnectingToInternet()) {
// 1 day
request.newBuilder()
.header("Cache-control", "only-if-cached")
.header("Accept", "application/json")
.header("Authorization", "Basic " + secret)
.build();
Log.d("REQUEST CACHE", "Only-if-Cached");
} else {
// upto 4 weeks stale
request.newBuilder()
.header("Cache-control", "public, max-stale=2419200")
.header("Accept", "application/json")
.header("Authorization", "Basic " + secret)
.build();
Log.d("REQUEST CACHE", "MAX STALE ");
}
}else
{
//NO CACHE
request.newBuilder()
.header("Cache-control" ,"public,max-age=0")
.header("Accept", "application/json")
.header("Authorization", "Basic " + secret)
.build();
Log.d("REQUEST CACHE","NO CACHE ");
}
}
//s-maxage for requests with authentications
Response response = chain.proceed(request);
// Re-write response CC header to force use of cache & set header such that it is cached.
if(enableCache) {
Log.d("RESPONSE CACHE","MAXAGE-CACHE ");
return response.newBuilder()
.header("Cache-control", "public, max-age=86400") // 1 day
.header("Accept", "application/json")
//Authentication Header , do you want to see it in the response
.header("Authorization", "Basic " + secret)
.build();
}else{
Log.d("RESPONSE CACHE","NO-CACHE ");
//Say that you don't want the result to be storedcontinue
return response.newBuilder()
.header("Cache-control", " public,max-age=0, no-cache, no-store,must-revalidate")
.header("Pragma","no-cache")
.build();
}
}
};
/**
* #param serviceClass Service class To create the adaptor for
* #param baseUrl Base URL of the API
* #param context Used to create a Cache
* #param <S>
* #return Rest Adapter
*/
public static <S> S createService(Class<S> serviceClass, String baseUrl, Context context) {
mContext = context;
enableCache = new SharedPreferenceEasy(context).RetreiveBoolean(Config.KEY_PREF_NETWK_CACHE,true);
// Create Cache
Cache cache = null;
cache = new Cache(new File(mContext.getCacheDir(), "http"), SIZE_OF_CACHE);
// Create OkHttpClient
OkHttpClient okHttpClient = new OkHttpClient();
if(enableCache) {
okHttpClient.setCache(cache);
}
okHttpClient.setConnectTimeout(30, TimeUnit.SECONDS);
okHttpClient.setReadTimeout(30, TimeUnit.SECONDS);
// Add Cache-Control Interceptor
okHttpClient.networkInterceptors().add(mCacheControlInterceptor);
// Create Executor
Executor executor = Executors.newCachedThreadPool();
RestAdapter.Builder builder = new RestAdapter.Builder()
.setEndpoint(baseUrl)
.setExecutors(executor, new MainThreadExecutor())
.setClient(new OkClient(okHttpClient))
.setLogLevel(RestAdapter.LogLevel.FULL);
RestAdapter adapter = builder.build();
return adapter.create(serviceClass);
}
}
I want to acheive a compulsive request from Network when cache is disabled from my app's settings. if(enableCache)

Related

HTTP FAILED: java.net.UnknownHostException: Unable to resolve host "www.reddit.com": No address associated with hostname

hello im trying to caching the GET request that im doing from reddit service. I tested with another api and works but i can't find the reason why i can't have control of the cache on this one. I can see how cache data goes up in app settings when i can fecth the json with internet on. But when i turn off internet connection ( airplane mode ) i get this error:
01-17 06:00:05.524 17363-17429/com.amirgb.reddittestapp.debug D/OkHttp: --> GET https://www.reddit.com/reddits.json http/1.1
01-17 06:00:05.524 17363-17429/com.amirgb.reddittestapp.debug D/OkHttp: --> END GET
01-17 06:00:05.531 17363-17429/com.amirgb.reddittestapp.debug D/OkHttp: <-- HTTP FAILED: java.net.UnknownHostException: Unable to resolve host "www.reddit.com": No address associated with hostname
Im testing on a moto g 4 plus api 23 i tried with https://itunes.apple.com/us/rss/topfreeapplications/limit=20/json service and cache does work, so maybe there is a difference in reddit header? .
My restclient class:
import android.content.Context;
import android.widget.ImageView;
import com.amirgb.reddittestapp.BuildConfig;
import com.amirgb.reddittestapp.R;
import com.amirgb.reddittestapp.RedditTestApplication;
import com.amirgb.reddittestapp.model.RedditResponse;
import com.amirgb.reddittestapp.util.NetWorkUtils;
import java.io.File;
import java.util.concurrent.TimeUnit;
import okhttp3.Cache;
import okhttp3.CacheControl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public final class RedditApiService {
private static IRedditApi mRedditApi;
private static OkHttpClient mClient;
/**
* Inits retrofit
*/
static {
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
File httpCacheDirectory = new File(RedditTestApplication.getInstance().getCacheDir(), "reddit");
Cache cache = new Cache(httpCacheDirectory, 10 * 1024 * 1024);
mClient = new OkHttpClient.Builder()
.cache(cache)
.addInterceptor(chain -> {
Request request = chain.request();
if (NetWorkUtils.isNetworkAvailable(RedditTestApplication.getInstance())) {
request = request.newBuilder()
.cacheControl(new CacheControl.Builder()
.maxAge(0, TimeUnit.SECONDS)
.maxStale(365, TimeUnit.DAYS).build())
.build();
}
Response originalResponse = chain.proceed(request);
if (NetWorkUtils.isNetworkAvailable(RedditTestApplication.getInstance())) {
int maxAge = 60 * 60;
return originalResponse.newBuilder()
.header("Cache-Control", "public, max-age=" + maxAge)
.build();
} else {
int maxStale = 60 * 60 * 24 * 28;
return originalResponse.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
.build();
}
})
.connectTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.addInterceptor(interceptor)
.build();
Retrofit mRetrofit = new Retrofit.Builder()
.baseUrl(IRedditApi.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(mClient)
.build();
mRedditApi = mRetrofit.create(IRedditApi.class);
}
/**
* Get subjects from service api
*
* #param callback
*/
public static void getSubjects(Callback<RedditResponse> callback) {
Call<RedditResponse> call = mRedditApi.getSubjects();
call.enqueue(callback);
}
My networks util method:
public static boolean isNetworkAvailable(Context context) {
ConnectivityManager cm =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
return activeNetwork != null &&
activeNetwork.isConnectedOrConnecting();
}
My retrofit interface:
public interface IRedditApi {
String BASE_URL = "https://www.reddit.com";
#GET("/reddits.json")
Call<RedditResponse> getSubjects();}
Reddit reponse header:
accept-ranges →bytes
access-control-allow-origin →*
access-control-expose-headers →X-Reddit-Tracking, X-Moose
cache-control →max-age=0, must-revalidate
content-encoding →gzip
content-length →79113
content-type →application/json; charset=UTF-8
date →Tue, 17 Jan 2017 09:16:52 GMT
status →200
strict-transport-security →max-age=15552000; includeSubDomains; preload
vary →accept-encoding
via →1.1 varnish
x-cache →MISS
x-cache-hits →0
x-content-type-options →nosniff
x-frame-options →SAMEORIGIN
x-moose →majestic
x-reddit-tracking →https://pixel.redditmedia.com/pixel/of_destiny.png?v=W9eORuZE%2BzXXBRZc%2BikbQ8aTR0a7BSLeoE9DDYvuCihjMAifTK%2Bdsr%2F0gX8rDES98kR5xK2vVWc%3D
x-served-by →cache-dfw1841-DFW
x-timer →S1484644612.225905,VS0,VE254
x-ua-compatible →IE=edge
x-xss-protection →1; mode=block

Why does my Web request get Truncated when using the newest versions of Retrofit / OkHttp

Good day all, I recently updated my libraries and upgraded Retrofit and OkHttp. The new versions I am using include these imports / versions:
1) Gson - compile 'com.google.code.gson:gson:2.7'
2) OkHttp - compile 'com.squareup.okhttp3:okhttp:3.4.1'
3) OkHttp Logging - compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'
4) Retrofit - compile 'com.squareup.retrofit2:retrofit:2.1.0'
5) Retrofit Gson Converter - compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta3'
I had to change the structure of some of my actual calls, but other than that, my code remained mostly the same as it did from previous versions of these libraries (Retrofit / OkHttp 1).
The problem I am having is that when I send an outbound call, it is essentially 'ignoring' the annotations for the paths.
My Retrofit Client Class :
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.SerializedName;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.GsonConverterFactory;
import retrofit2.Retrofit;
public class RetrofitClient {
private static RetrofitService serviceClient;
private static final String BASE_URL = "api.myapiurl.com";
private static HttpLoggingInterceptor.Level httpLogLevel = HttpLoggingInterceptor.Level.BODY;
static {
buildAClient();
}
public static RetrofitService getServiceClient(){
return serviceClient;
}
private static void buildAClient(){
Interceptor interceptor = new Interceptor() {
#Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request.Builder builder = new Request.Builder();
Request original = chain.request();
builder.url(BASE_URL);
builder.addHeader("Content-Type", "application/json");
builder.method(original.method(), original.body());
Request newRequest = builder.build();
return chain.proceed(newRequest);
}
};
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(httpLogLevel);
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.readTimeout(60, TimeUnit.SECONDS);
builder.writeTimeout(60, TimeUnit.SECONDS);
builder.addInterceptor(interceptor);
builder.addInterceptor(logging);
builder = configureClient(builder);
OkHttpClient client = builder.build();
Retrofit.Builder myBuilder = new Retrofit.Builder();
myBuilder.baseUrl(BASE_URL);
Gson gson = new GsonBuilder()
.setLenient()
.setPrettyPrinting()
.create();
GsonConverterFactory factory = GsonConverterFactory.create(gson);
myBuilder.addConverterFactory(factory);
myBuilder.client(client);
Retrofit retrofit = myBuilder.build();
serviceClient = retrofit.create(RetrofitService.class);
}
/**
* {#link okhttp3.OkHttpClient.Builder} <-- sslSocketFactory
*/
private static OkHttpClient.Builder configureClient(final OkHttpClient.Builder builder) {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:"
+ Arrays.toString(trustManagers));
}
X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{trustManager}, null);
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
builder.sslSocketFactory(sslSocketFactory, trustManager);
return builder;
}
}
My Retrofit Service Interface:
public interface RetrofitService {
//Version Strings
public static final String VERSION2 = "/v2";
//Returns a list of Order Objects
#GET(VERSION2 + "/orders/{orderId}/getOrder")
Call<Order> getOrder(#Path("orderId") String orderId,
#Query("key") String apiKey
//I'm aware this is bad practice ^^, already spoke with server dev
);
}
The Actual outbound call I am making is this:
public static Order getSingleOrder(String orderId, String apiKey) {
Call<Order> call = myService.getOrder(orderId, apiKey);
Order toReturn = null;
try {
Response response = call.execute();
toReturn = (Order) response.body();
} catch (IOException ioe){
ioe.printStackTrace();
}
return toReturn;
}
While all of this worked just fine in the past, it is no longer working now with the updated libraries. The issue is that it is "ignoring" the path so that instead of sending this call:
GET REQUEST: http://api.myapiurl.com/orders/12345/getOrder?key=54321
It is now making this request:
GET REQUEST: http://api.myapiurl.com/
and that's it. Anything after the .com in the api base string is never getting sent. I am reading through my logcat and the subsequent OkHttp logging statements to see where the call is failing:
Sample:
--> GET http://api.myapiurl.com/ http/1.1
--> END GET
Does anyone have any idea why this is happening when it worked fine before I updated?
Thanks for the help all!
You are resetting your url in your interceptor --
Request.Builder builder = new Request.Builder();
Request original = chain.request();
builder.url(BASE_URL); <--!!! resets the request url to the base URL
builder.addHeader("Content-Type", "application/json");
builder.method(original.method(), original.body());
Request newRequest = builder.build();
return chain.proceed(newRequest);
try grabbing a builder from the existing request instead --
Request original = chain.request();
Request.Builder builder = original.newBuilder();
builder.addHeader("Content-Type", "application/json");
Request newRequest = builder.build();
return chain.proceed(newRequest);

Set dynamic base url using Retrofit 2.0 and Dagger 2

I'm trying to perform a login action using Retrofit 2.0 using Dagger 2
Here's how I set up Retrofit dependency
#Provides
#Singleton
Retrofit provideRetrofit(Gson gson, OkHttpClient client) {
Retrofit retrofit = new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(gson)
.client(client)
.baseUrl(application.getUrl())
.build();
return retrofit;
}
Here's the API interface.
interface LoginAPI {
#GET(relative_path)
Call<Boolean> logMe();
}
I have three different base urls users can log into. So I can't set a static url while setting up Retrofit dependency. I created a setUrl() and getUrl() methods on Application class. Upon user login, I set the url onto Application before invoking the API call.
I use lazy injection for retrofit like this
Lazy<Retrofit> retrofit
That way, Dagger injects dependency only when I can call
retrofit.get()
This part works well. I got the url set to retrofit dependency. However, the problem arises when the user types in a wrong base url (say, mywifi.domain.com), understands it's the wrong one and changes it(say to mydata.domain.com). Since Dagger already created the dependency for retrofit, it won't do again.
So I have to reopen the app and type in the correct url.
I read different posts for setting up dynamic urls on Retrofit using Dagger. Nothing really worked out well in my case. Do I miss anything?
Support for this use-case was removed in Retrofit2. The recommendation is to use an OkHttp interceptor instead.
HostSelectionInterceptor made by swankjesse
import java.io.IOException;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
/** An interceptor that allows runtime changes to the URL hostname. */
public final class HostSelectionInterceptor implements Interceptor {
private volatile String host;
public void setHost(String host) {
this.host = host;
}
#Override public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
String host = this.host;
if (host != null) {
//HttpUrl newUrl = request.url().newBuilder()
// .host(host)
// .build();
HttpUrl newUrl = HttpUrl.parse(host);
request = request.newBuilder()
.url(newUrl)
.build();
}
return chain.proceed(request);
}
public static void main(String[] args) throws Exception {
HostSelectionInterceptor interceptor = new HostSelectionInterceptor();
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(interceptor)
.build();
Request request = new Request.Builder()
.url("http://www.coca-cola.com/robots.txt")
.build();
okhttp3.Call call1 = okHttpClient.newCall(request);
okhttp3.Response response1 = call1.execute();
System.out.println("RESPONSE FROM: " + response1.request().url());
System.out.println(response1.body().string());
interceptor.setHost("www.pepsi.com");
okhttp3.Call call2 = okHttpClient.newCall(request);
okhttp3.Response response2 = call2.execute();
System.out.println("RESPONSE FROM: " + response2.request().url());
System.out.println(response2.body().string());
}
}
Or you can either replace your Retrofit instance (and possibly store the instance in a RetrofitHolder in which you can modify the instance itself, and provide the holder through Dagger)...
public class RetrofitHolder {
Retrofit retrofit;
//getter, setter
}
Or re-use your current Retrofit instance and hack the new URL in with reflection, because screw the rules. Retrofit has a baseUrl parameter which is private final, therefore you can access it only with reflection.
Field field = Retrofit.class.getDeclaredField("baseUrl");
field.setAccessible(true);
okhttp3.HttpUrl newHttpUrl = HttpUrl.parse(newUrl);
field.set(retrofit, newHttpUrl);
Retrofit2 library comes with a #Url annotation. You can override baseUrl like this:
API interface:
public interface UserService {
#GET
public Call<ResponseBody> profilePicture(#Url String url);
}
And call the API like this:
Retrofit retrofit = Retrofit.Builder()
.baseUrl("https://your.api.url/");
.build();
UserService service = retrofit.create(UserService.class);
service.profilePicture("https://s3.amazon.com/profile-picture/path");
For more details refer to this link: https://futurestud.io/tutorials/retrofit-2-how-to-use-dynamic-urls-for-requests
This worked for me in Kotlin
class HostSelectionInterceptor: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val host: String = SharedPreferencesManager.getServeIpAddress()
val newUrl = request.url().newBuilder()
.host(host)
.build()
request = request.newBuilder()
.url(newUrl)
.build()
return chain.proceed(request)
}
}
Add the interceptor to OkHttpClient builder
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(HostSelectionInterceptor())
.cache(null)
.build()
This might be late but Retrofit allows you to use dynamic URLs while making the network call itself using #Url annotation.
I am also using Dagger2 to inject the Retrofit instance in my repositories and this solution is working fine for me.
This will use the base url
provided by you while creating the instance of Retrofit.
#GET("/product/123")
fun fetchDataFromNetwork(): Call<Product>
This ignore the base url
and use the url you will be providing this call at run time.
#GET()
fun fetchDataFromNetwork(#Url url : String): Call<Product> //
Thanks to #EpicPandaForce for help. If someone is facing IllegalArgumentException, this is my working code.
public class HostSelectionInterceptor implements Interceptor {
private volatile String host;
public void setHost(String host) {
this.host = HttpUrl.parse(host).host();
}
#Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
String reqUrl = request.url().host();
String host = this.host;
if (host != null) {
HttpUrl newUrl = request.url().newBuilder()
.host(host)
.build();
request = request.newBuilder()
.url(newUrl)
.build();
}
return chain.proceed(request);
}
}
For latest Retrofit library, you can simply use singleton instance and change it with retrofitInstance.newBuilder().baseUrl(newUrl). No need to create another instance.
Dynamic url using Retrofit 2 and Dagger 2
You are able to instantiate new object using un-scoped provide method.
#Provides
LoginAPI provideAPI(Gson gson, OkHttpClient client, BaseUrlHolder baseUrlHolder) {
Retrofit retrofit = new Retrofit.Builder().addConverterFactory(GsonConverterFactory.create(gson)
.client(client)
.baseUrl(baseUrlHolder.get())
.build();
return retrofit.create(LoginAPI.class);
}
#AppScope
#Provides
BaseUrlHolder provideBaseUrlHolder() {
return new BaseUrlHolder("https://www.default.com")
}
public class BaseUrlHolder {
public String baseUrl;
public BaseUrlHolder(String baseUrl) {
this.baseUrl = baseUrl;
}
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
}
Now you can change base url via getting baseUrlHolder from the component
App.appComponent.getBaseUrlHolder().set("https://www.changed.com");
this.loginApi = App.appComponent.getLoginApi();
Please look into my workaround for Dagger dynamic URL.
Step1: Create an Interceptor
import android.util.Patterns;
import com.nfs.ascent.mdaas.repo.network.ApiConfig;
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
public class DomainURLInterceptor implements Interceptor {
#Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
String requestUrl = original.url().toString();
String PROTOCOL = "(?i:http|https|rtsp)://";
String newURL = requestUrl.replaceFirst(PROTOCOL, "")
.replaceFirst(Patterns.DOMAIN_NAME.toString(), "");
newURL = validateBackSlash(newURL) ? ApiConfig.BASE_URL.concat(newURL) : newURL.replaceFirst("/", ApiConfig.BASE_URL);
original = original.newBuilder()
.url(newURL)
.build();
return chain.proceed(original);
}
private boolean validateBackSlash(String str) {
if (!str.substring(str.length() - 1).equals("/")) {
return true;
}
return false;
}
}
Step 2:
add your newly created interceptor in your module
#Provides
#Singlton
DomainURLInterceptor getChangeURLInterceptor() {
return new DomainURLInterceptor();
}
step 3:
add interceptor into list of HttpClient interceptors
#Provides
#Singlton
OkHttpClient provideHttpClient() {
return new OkHttpClient.Builder()
.addInterceptor(getChangeURLInterceptor())
.readTimeout(ApiConfig.API_CONNECTION_TIMEOUT, TimeUnit.SECONDS)
.connectTimeout(ApiConfig.API_CONNECTION_TIMEOUT, TimeUnit.SECONDS)
.build();
}
step 4:
#Provides
#Singlton
Retrofit provideRetrofit() {
return new Retrofit.Builder()
.baseUrl(ApiConfig.BASE_URL) // this is default URl,
.addConverterFactory(provideConverterFactory())
.client(provideHttpClient())
.build();
}
Note: if the user has to change the Base URL from settings, remember to validate the newly created URL with below method:
public final static boolean isValidUrl(CharSequence target) {
if (target == null) {
return false;
} else {
return Patterns.WEB_URL.matcher(target).matches();
}
}

HTTP Caching with Retrofit 2.0.x

I'm trying to cache some responses in my app using Retrofit 2.0, but I'm missing something.
I installed a caching file as follows:
private static File httpCacheDir;
private static Cache cache;
try {
httpCacheDir = new File(getApplicationContext().getCacheDir(), "http");
httpCacheDir.setReadable(true);
long httpCacheSize = 10 * 1024 * 1024; // 10 MiB
HttpResponseCache.install(httpCacheDir, httpCacheSize);
cache = new Cache(httpCacheDir, httpCacheSize);
Log.i("HTTP Caching", "HTTP response cache installation success");
} catch (IOException e) {
Log.i("HTTP Caching", "HTTP response cache installation failed:" + e);
}
public static Cache getCache() {
return cache;
}
which creates a file in /data/user/0/<PackageNmae>/cache/http
, then prepared a network interceptor as follows:
public class CachingControlInterceptor implements Interceptor {
#Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// Add Cache Control only for GET methods
if (request.method().equals("GET")) {
if (ConnectivityUtil.checkConnectivity(getContext())) {
// 1 day
request.newBuilder()
.header("Cache-Control", "only-if-cached")
.build();
} else {
// 4 weeks stale
request.newBuilder()
.header("Cache-Control", "public, max-stale=2419200")
.build();
}
}
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.header("Cache-Control", "max-age=86400")
.build();
}
}
my Retrofit and OkHttpClient instance:
OkHttpClient client = new OkHttpClient();
client.setCache(getCache());
client.interceptors().add(new MainInterceptor());
client.interceptors().add(new LoggingInceptor());
client.networkInterceptors().add(new CachingControlInterceptor());
Retrofit restAdapter = new Retrofit.Builder()
.client(client)
.baseUrl(Constants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
productsService = restAdapter.create(ProductsService.class);
where ProductsService.class contains:
#Headers("Cache-Control: max-age=86400")
#GET("categories/")
Call<PagedResponse<Category>> listCategories();
and
Call<PagedResponse<Category>> call = getRestClient().getProductsService().listCategories();
call.enqueue(new GenericCallback<PagedResponse<Category>>() {
// whatever
// GenericCallback<T> implements Callback<T>
}
});
The question here is: How to make it access cached responses when device being offline?
Header of backend response are:
Allow → GET, HEAD, OPTIONS
Cache-Control → max-age=86400, must-revalidate
Connection → keep-alive
Content-Encoding → gzip
Content-Language → en
Content-Type → application/json; charset=utf-8
Date → Thu, 17 Dec 2015 09:42:49 GMT
Server → nginx
Transfer-Encoding → chunked
Vary → Accept-Encoding, Cookie, Accept-Language
X-Frame-Options → SAMEORIGIN
x-content-type-options → nosniff
x-xss-protection → 1; mode=block
Finally I get the answer.
Network Interceptor should be as follow:
public class CachingControlInterceptor implements Interceptor {
#Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// Add Cache Control only for GET methods
if (request.method().equals("GET")) {
if (ConnectivityUtil.checkConnectivity(YaootaApplication.getContext())) {
// 1 day
request = request.newBuilder()
.header("Cache-Control", "only-if-cached")
.build();
} else {
// 4 weeks stale
request = request.newBuilder()
.header("Cache-Control", "public, max-stale=2419200")
.build();
}
}
Response originalResponse = chain.proceed(request);
return originalResponse.newBuilder()
.header("Cache-Control", "max-age=600")
.build();
}
}
then installing cache file is that simple
long SIZE_OF_CACHE = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(new File(context.getCacheDir(), "http"), SIZE_OF_CACHE);
OkHttpClient client = new OkHttpClient();
client.cache(cache);
client.networkInterceptors().add(new CachingControlInterceptor());
In your CachingControlInterceptor, you create new requests, but never actually use them. You call newBuilder and ignore the result, so the header modification is never actually sent any where. Try assigning those values to request and then instead of calling proceed on chain.request() call it on request.
public class CachingControlInterceptor implements Interceptor {
#Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// Add Cache Control only for GET methods
if (request.method().equals("GET")) {
if (ConnectivityUtil.checkConnectivity(getContext())) {
// 1 day
request = request.newBuilder()
.header("Cache-Control", "only-if-cached")
.build();
} else {
// 4 weeks stale
request = request.newBuilder()
.header("Cache-Control", "public, max-stale=2419200")
.build();
}
}
Response originalResponse = chain.proceed(request);
return originalResponse.newBuilder()
.header("Cache-Control", "max-age=600")
.build();
}
}
you can also try:
public class CachingInterceptor implements Interceptor {
#Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
request = new Request.Builder()
.cacheControl(new CacheControl.Builder()
.maxAge(1, TimeUnit.DAYS)
.minFresh(4, TimeUnit.HOURS)
.maxStale(8, TimeUnit.HOURS)
.build())
.url(request.url())
.build();
return chain.proceed(request);
}
}
I finally discovered the solution that worked for me in Retrofit 2.x and OkHttp 3.x
I had to implement two Interceptors, one of them is responsible to rewrite the Request headers and the other to rewrite the Response headers.
First, make sure you delete any old cache. (root explorer /data/data/com.yourapp/cache
Instantiate the client builder:
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder()
.cache(cache)
.addInterceptor(new RewriteRequestInterceptor())
.addNetworkInterceptor(new RewriteResponseCacheControlInterceptor())
Create the RewriteRequestInterceptor
public class RewriteRequestInterceptor implements Interceptor {
#Override
public Response intercept(Chain chain) throws IOException {
int maxStale = 60 * 60 * 24 * 5;
Request request;
if (NetworkUtils.isNetworkAvailable()) {
request = chain.request();
} else {
request = chain.request().newBuilder().header("Cache-Control", "max-stale=" + maxStale).build();
}
return chain.proceed(request);
}
}
Create the RewriteResponseCacheControlInterceptor
public class RewriteResponseCacheControlInterceptor implements Interceptor {
#Override
public Response intercept(Chain chain) throws IOException {
int maxStale = 60 * 60 * 24 * 5;
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder().header("Cache-Control", "public, max-age=120, max-stale=" + maxStale).build();
}
}
It's important to make sure you add the ResponseCacheControlInterceptor as a Network Interceptor, and the RewriteRequestInterceptor as a Interceptor (as I did in the 2nd step).
OkHttpClient client = new OkHttpClient.Builder().cache(cache).build();

Retrofit + OkHTTP - response cache not working

I know there has been a lot of similar questions, but I have read them all and none of them really helped.
So, here is my problem:
I am using retrofit + okhttp to fetch some data from API and I'd like to cache them. Unfortunately, I don't have admin access to the API server so I can't modify headers returned by the server. (currently, server returns Cache-control: private)
So I decided to use okhttp header spoofing to insert appropriate cache headers. Sadly, no matter what I do, caching doesn't seem to work.
I initialise the api service like this:
int cacheSize = 10 * 1024 * 1024; // 10 MiB
File cacheFile = new File(context.getCacheDir(), "thumbs");
final Cache cache = new Cache(cacheFile, cacheSize);
OkHttpClient client = new OkHttpClient();
client.setCache(cache);
client.interceptors().add(new Interceptor() {
#Override
public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.removeHeader("Access-Control-Allow-Origin")
.removeHeader("Vary")
.removeHeader("Age")
.removeHeader("Via")
.removeHeader("C3-Request")
.removeHeader("C3-Domain")
.removeHeader("C3-Date")
.removeHeader("C3-Hostname")
.removeHeader("C3-Cache-Control")
.removeHeader("X-Varnish-back")
.removeHeader("X-Varnish")
.removeHeader("X-Cache")
.removeHeader("X-Cache-Hit")
.removeHeader("X-Varnish-front")
.removeHeader("Connection")
.removeHeader("Accept-Ranges")
.removeHeader("Transfer-Encoding")
.header("Cache-Control", "public, max-age=60")
//.header("Expires", "Mon, 27 Apr 2015 08:15:14 GMT")
.build();
}
});
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(API_ROOT)
.setLogLevel(RestAdapter.LogLevel.HEADERS_AND_ARGS)
.setClient(new OkClient(client))
.setConverter(new SimpleXMLConverter(false))
.setRequestInterceptor(new RequestInterceptor() {
#Override
public void intercept(RequestFacade request) {
if (Network.isConnected(context)) {
int maxAge = 60; // read from cache for 2 minutes
request.addHeader("Cache-Control", "public, max-age=" + maxAge);
} else {
int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
request.addHeader("Cache-Control",
"public, only-if-cached, max-stale=" + maxStale);
}
}
})
.build();
api = restAdapter.create(ApiService.class);
Of course, it's not necessary to remove all these headers, but I wanted to make the response as clean as possible to rule out some interference from these extra headers.
As you can see, I tried to also spoof Expires and Date header (I tried removing them, setting them so that there is exactly max-age differnece between them and also setting Expires far into future). I also experimented with various Cache-control values, but no luck.
I made sure the cacheFile exists, isDirectory and is writeable by the application.
These are the request and response headers as logged directly by retrofit:
Request:
Cache-Control: public, max-age=60
---> END HTTP (no body)
Response:
Date: Mon, 27 Apr 2015 08:41:10 GMT
Server: Apache/2.2.22 (Ubuntu)
Expires: Mon, 27 Apr 2015 08:46:10 GMT
Content-Type: text/xml; charset=UTF-8
OkHttp-Selected-Protocol: http/1.1
OkHttp-Sent-Millis: 1430124070000
OkHttp-Received-Millis: 1430124070040
Cache-Control: public, max-age=60
<--- END HTTP (-1-byte body)
<--- BODY: ...
And, finally one strange incident: At some point, the cache worked for a few minutes. I was getting reasonable hit counts, even offline requests returned cached values. (It happened while using the exact setting posted here) But when I restarted the app, everything was back to "normal" (constant hit count 0).
Co if anyone has any idea what could be the problem here, I'd be really glad for any help :)
Use networkInterceptors() instead of interceptors(). That in combination with your strategy of removing any headers that are somewhat related to caching will work. That's the short answer.
When you use interceptors to change headers it does not make any adjustments before CacheStrategy.isCacheable() is called. It's worthwhile to look at the CacheStrategy and CacheControl classes to see how OKHttp handles cache-related headers. It's also worthwhile to do ctrl+f "cache" on http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
I am not sure if the networkInterceptors() and interceptors() documentation is just unclear or if there is a bug. Once I look into that more, I will update this answer.
One more thing to add here, Apart from Brendan Weinstein's answer just to confirm OkHttp3 cache will not work with post requests.
After a full day, I found that my offline caching was not working just because I was using POST in the API type. The moment I changed it to GET, it worked!
#GET("/ws/audioInactive.php")
Call<List<GetAudioEntity>> getAudios();
My entire Retrofit class.
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.limnet.iatia.App;
import com.limnet.iatia.netio.entity.registration.APIInterfaceProviderIMPL;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import okhttp3.Cache;
import okhttp3.CacheControl;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class RHTRetroClient {
public static final String BASE_URL = "https://abc.pro";
private static Retrofit retrofit = null;
private static RHTRetroClient mInstance;
private static final long cacheSize = 10 * 1024 * 1024; // 10 MB
public static final String HEADER_CACHE_CONTROL = "Cache-Control";
public static final String HEADER_PRAGMA = "Pragma";
private RHTRetroClient() {
Gson gson = new GsonBuilder()
.setLenient()
.create();
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
Cache cache = new Cache(new File(App.getAppContext().getCacheDir(), "soundbites"),cacheSize);
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.addInterceptor(httpLoggingInterceptor()) // used if network off OR on
.addNetworkInterceptor(networkInterceptor()) // only used when network is on
.addInterceptor(offlineInterceptor())
.build();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
}
/**
* This interceptor will be called both if the network is available and if the network is not available
*
* #return
*/
private static Interceptor offlineInterceptor() {
return new Interceptor() {
#Override
public Response intercept(Chain chain) throws IOException {
Log.d("rht", "offline interceptor: called.");
Request request = chain.request();
// prevent caching when network is on. For that we use the "networkInterceptor"
if (!App.hasNetwork()) {
CacheControl cacheControl = new CacheControl.Builder()
.maxStale(7, TimeUnit.DAYS)
.build();
request = request.newBuilder()
.removeHeader(HEADER_PRAGMA)
.removeHeader(HEADER_CACHE_CONTROL)
.cacheControl(cacheControl)
.build();
}
return chain.proceed(request);
}
};
}
/**
* This interceptor will be called ONLY if the network is available
*
* #return
*/
private static Interceptor networkInterceptor() {
return new Interceptor() {
#Override
public Response intercept(Chain chain) throws IOException {
Log.d("rht", "network interceptor: called.");
Response response = chain.proceed(chain.request());
CacheControl cacheControl = new CacheControl.Builder()
.maxAge(5, TimeUnit.SECONDS)
.build();
return response.newBuilder()
.removeHeader(HEADER_PRAGMA)
.removeHeader(HEADER_CACHE_CONTROL)
.header(HEADER_CACHE_CONTROL, cacheControl.toString())
.build();
}
};
}
private static HttpLoggingInterceptor httpLoggingInterceptor() {
HttpLoggingInterceptor httpLoggingInterceptor =
new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
#Override
public void log(String message) {
Log.d("rht", "log: http log: " + message);
}
});
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
return httpLoggingInterceptor;
}
public static synchronized RHTRetroClient getInstance() {
if (mInstance == null) {
mInstance = new RHTRetroClient();
}
return mInstance;
}
public APIInterfaceProviderIMPL getAPIInterfaceProvider() {
return retrofit.create(APIInterfaceProviderIMPL.class);
}
}
Check if there is a Pragma header in your response. Caching with max-age will not work if Pragma: no-cache header is present.
If it does have Pragma header, remove it by doing the following in your Interceptor:
override fun intercept(chain: Interceptor.Chain): Response {
val cacheControl = CacheControl.Builder()
.maxAge(1, TimeUnit.MINUTES)
.build()
return originalResponse.newBuilder()
.header("Cache-Control", cacheControl.toString())
.removeHeader("Pragma") // Caching doesnt work if this header is not removed
.build()
}

Categories

Resources