The service I am using to obtain images, like many such sites does not have a cache control header indicating how long the image should be cached. Volley uses an http cache control header by default to decide how long to cache images on disk. How could I override this default behavior and keep such images for a set period of time?
Thanks
I needed to change the default caching strategy to a "cache all" policy, without taking into account the HTTP headers.
You want to cache for a set period of time. There are several ways you can do this, since there are many places in the code that "touch" the network response. I suggest an edit to the HttpHeaderParser (parseCacheHeaders method at line 39):
Cache.Entry entry = new Cache.Entry();
entry.data = response.data;
entry.etag = serverEtag;
entry.softTtl = softExpire;
entry.ttl = now; // **Edited**
entry.serverDate = serverDate;
entry.responseHeaders = headers;
and another to Cache.Entry class:
/** True if the entry is expired. */
public boolean isExpired() {
return this.ttl + GLOBAL_TTL < System.currentTimeMillis();
}
/** True if a refresh is needed from the original data source. */
public boolean refreshNeeded() {
return this.softTtl + GLOBAL_TTL < System.currentTimeMillis();
}
where GLOBAL_TTL is a constant representing the time you want each image to live in the cache.
Related
I have the following requirements for image download:
ignoring SSL errors (yes I am aware of the risks)
using a session cookie
I tried to adapt Picasso 2.4.0 to do that, below is my approach:
public static Picasso getPicasso(Context context) {
/* an OkHttpClient that ignores SSL errors */
final OkHttpClient client = getUnsafeOkHttpClient();
return new Picasso.Builder(context)
.downloader(new OkHttpDownloader(client) {
#Override
public Response load(Uri uri, boolean localCacheOnly) throws IOException {
final String RESPONSE_SOURCE_ANDROID = "X-Android-Response-Source";
final String RESPONSE_SOURCE_OKHTTP = "OkHttp-Response-Source";
HttpURLConnection connection = openConnection(uri);
connection.setRequestProperty("Cookie", getCookieHandler().
getCookieStore().getCookies().get(0).toString());
connection.setUseCaches(true);
if (localCacheOnly)
connection.setRequestProperty("Cache-Control", "only-if-cached,max-age=" + Integer.MAX_VALUE);
int responseCode = connection.getResponseCode();
if (responseCode == 401)
relogin();
else if (responseCode >= 300) {
connection.disconnect();
throw new ResponseException(responseCode + " " + connection.getResponseMessage());
}
String responseSource = connection.getHeaderField(RESPONSE_SOURCE_OKHTTP);
if (responseSource == null)
responseSource = connection.getHeaderField(RESPONSE_SOURCE_ANDROID);
long contentLength = connection.getHeaderFieldInt("Content-Length", -1);
boolean fromCache = parseResponseSourceHeader(responseSource);
return new Response(connection.getInputStream(), fromCache, contentLength);
}
}).build();
}
The only thing that I changed from the original source is adding a Cookie for the HttpURLConnection. I also copied (unchanged) the parseResponseSourceHeader() method since it has private access.
Note that the approach given here does NOT work (response code 401).
The image loading basically works, but there are major issues:
caching doesn't work (fromCache is always false and Picasso always reloads an image which has already been downloaded)
there's no "Content-Length" header, so contentLength is always -1
though the cache doesn't work, the RAM usage increases when loading next image (into exactly the same or any other ImageView), it seems the Bitmap object stays somewhere in the memory
when used inside the BaseAdapter of a GridView, it seems that Picasso tries to load all (or at least as many as the number of times getView() was called) images at the same time. Those images appear, then the app freezes and closes with the following (OOM?) log:
A/Looper﹕ Could not create wake pipe. errno=24
or
A/Looper﹕ Could not create epoll instance. errno=24
The described issues occur no matter if I use a custom Target of just an ImageView.
It seems I have broken some of Picasso mechanisms by overriding the load() method of the OkHttpDownloader, but I'm not getting what's wrong since I did minimal changes. Any suggestions are appreciated.
In case someone has a similar problem: it was a really lame mistake of mine. I was creating multiple Picasso instances which is complete nonsense. After ensuring the singleton pattern with a helper class that returns a single Picasso instance everything works as intended.
My application generates ListFragments based on user type (determined by the DeviceID) and then fetch images from a web server. These images will then be displayed as a list.
My question is how can I cache those images as well as fragments (generated dynamically) to be displayed in offline mode. For instance when user opens the application without having an active internet connection, it should display the images within the fragments generated dynamically last time.
At the moment my app just download the images from a web service each time.
code for generating fragments dynamically in the MainActivity each time when the application loads.
//generating the views based on JSON data
try {
JSONObject resultObject = new JSONObject(result.toString());
boolean success = resultObject.getBoolean("success");
JSONArray jArray = resultObject.getJSONArray("data");
if (success == true) {
//save menu
for (int i = 0; i < jArray.length(); i++) {
postObject = jArray.getJSONObject(i);
if (postObject.has("ev_count")) {
categoriesSet.put("Events", "Events");
mTabsAdapter.addTab(bar.newTab().setText("Events"), EventsFragment.class, null);
}
if (postObject.has("pl_count")) {
categoriesSet.put("Places", "Places");
mTabsAdapter.addTab(bar.newTab().setText("Places"), PlacesFragment.class, null);
}
if (postObject.has("gn_count")) {
categoriesSet.put("General", "General");
mTabsAdapter.addTab(bar.newTab().setText("General"), GeneralFragment.class, null);
}
}
}
//saving values to the shared preferences (hashmap as a string)
categoriesPreferences = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = categoriesPreferences.edit();
editor.putString("categories", categoriesSet.toString());
editor.commit();
}catch (JSONException e) {
Log.e("ALLOCATE_DAT_ERROR", e.toString());
}
I would just advice to use some image downloading library which also often handles caching for you..
Here is a short list of some of them:
Volley - directly from google, its more like a whole networking stack, but it allows you to easily download Images (see NetworkImageView) and also to cache them (you need to provide a cache implemtantion - google will help you)
Picasso - nice library from square with very straightforward API - I would advice you to go with it, it might be the easiest way to go
Universal Image Loader - another option, it has really a lot of options and is also easy to use
Here is a simple sample code which should advice you how to use volley to load and cache images.
NetworkImageView mage = (NetworkImageView) view.findViewById(...);
image.setImageUrl("http://someurl.com/image.png",mImageLoader);
You need an ImageLoader instance then..
public ImageLoader getImageLoader() {
if (mImageLoader == null) {
mImageLoader = new ImageLoader(getImageRequestQueue(), new DiskBitmapCache(getCacheDir(), 50 * 1024 * 1024));
}
return mImageLoader;
}
imageRequestQueue is standard queue you should have already initialised somewhere in your app if already using volley for networking stuff
As a DiskCache you can use this
I want to clear the request queue each 30 minutes for example.
So What is the best way to clear volley cache automatically?
Override methods by extending the volley cache class?
Or build a timer which will clear the cache every times i need?
Google Volley provides 2 ways to clear an item from the Cache:
AppController.getInstance().getRequestQueue().getCache().remove(key);
and
AppController.getInstance().getRequestQueue().getCache().invalidate(key, fullExpire);
Remove means you are removing the actual cached data.
Invalidate means you are just marking the data as invalid. So volley will check with the server whether the data is still valid. The full expire determines whether to use the data before volley has validated it with the server.
To clear cache in each 30 minutes use below code:-
you can use volley's serverDate to get the date for when the response was originally received as
AppController.getInstance().getRequestQueue().getCache().get(url).serverDate
So in your code use getMinutesDifference function as
public static long getMinutesDifference(long timeStart,long timeStop){
long diff = timeStop - timeStart;
long diffMinutes = diff / (60 * 1000);
return diffMinutes;
}
and Call this function in your code as
Calendar calendar = Calendar.getInstance();
long serverDate = AppController.getInstance().getRequestQueue().getCache().get(url).serverDate;
if(getMinutesDifference(serverDate, calendar.getTimeInMillis()) >=30){
AppController.getInstance().getRequestQueue().getCache().invalidate(url, true);
}
It will invalidate the cache,if previous url response >=30 minutes.
Easy way to do that is override onRequestFinished method and clear cache. Or you can run inside the timer after 30 min.
final RequestQueue requestQueue = Volley.newRequestQueue(this);
requestQueue.add(stringRequest);
requestQueue.addRequestFinishedListener(new RequestQueue.RequestFinishedListener<Object>() {
#Override
public void onRequestFinished(Request<Object> request) {
requestQueue.getCache().clear();
}
});
I was trying to remove bitmap from cache by using remove(key) but it was not working, so I have check url received by putBitmap(String url, Bitmap bitmap). I found url has some prefix like #W0#H#S7http... this is because volley call getCacheKey(String url, int maxWidth, int maxHeight, ScaleType scaleType) for each url. SO if you wants to remove url from cache then you also have to call this function for getting key for url.
String key = mImageLoader.getCacheKey(url, 0, 0, ImageView.ScaleType.CENTER_INSIDE);
mRequestQueue.getCache().remove(key);
Pass 0,0 and ImageView.ScaleType.CENTER_INSIDE if you are using imageLoader.get(String requestUrl,ImageLoader.ImageListener listener) else pass min height and width and scale type you are using.
NOTE getCacheKey() is private function of ImageLoader class so you have to change it to public for using it inside you app.
I am using Google's Volley Library as my design for getting network data;
I have set up a RequestQueue
requestQueue = new RequestQueue(new DiskBasedCache(new File(context.getCacheDir(),
DEFAULT_CACHE_DIR)), new BasicNetwork(new
HttpClientStack(AndroidHttpClient.newInstance(userAgent))));
I have also subclassed Request, and have data coming back from the network just fine. My issue is with caching: in parseNetworkResponse() which is overridden in my subclass of Request, when I call
return Response.success(list, HttpHeaderParser.parseCacheHeaders(response));
HttpHeaderParser.parseCacheHeaders(response) returns null since the server is set up for "no caching" in its response header... Regardless I still would like to cache this data for a variable set number of hours (24 hours probably), How can I do this by creating a volley Cache.Entry... It is my understanding that the URL is used as the cache key value (and I would like it to be the URL).
To sum up, since HttpHeaderParser.parseCacheHeaders(response) returns null, I would like to create a new Cache.Entry that is set up for expiring after 24 hours, and the cache key being the URL of the request.
Any thoughts?
Thanks!
I've had the same issue and ended up with this solution:
#Override
protected Response<String> parseNetworkResponse(NetworkResponse response) {
// Create a FakeCache that invalidate the data after 24 hour
Cache.Entry mFakeCache = HttpHeaderParser.parseCacheHeaders(response);
mFakeCache.etag = null;
mFakeCache.softTtl = System.currentTimeMillis() + 86400 * 1000;
mFakeCache.ttl = mFakeCache.softTtl;
return Response.success(response.data, mFakeCache);
}
I'm trying to cache JSON requests from a server, however, they are incorrectly using the Cache-Control header, amongst others (everything expires in the past). I want to override it so that calls are cached for say, 3 hours, regardless of what the server requests. Is that possible? The documentation for Volley is Scarce.
You might subclass the JsonObjectRequest class and overwrite parseNetworkResponse. You will notice the call to HttpHeaderParser.parseCacheHeaders - it's a good place to start :] just wrap this call or replace it and provide your own dummy Cache header configuration object [with your proprietary clientside cache time] to Response.success.
In my implementation it looks like this:
parseNetworkResponse
return Response.success(payload, enforceClientCaching(HttpHeaderParser.parseCacheHeaders(response), response));
with enforceClientCaching related members being
protected static final int defaultClientCacheExpiry = 1000 * 60 * 60; // milliseconds; = 1 hour
protected Cache.Entry enforceClientCaching(Cache.Entry entry, NetworkResponse response) {
if (getClientCacheExpiry() == null) return entry;
long now = System.currentTimeMillis();
if (entry == null) {
entry = new Cache.Entry();
entry.data = response.data;
entry.etag = response.headers.get("ETag");
entry.softTtl = now + getClientCacheExpiry();
entry.ttl = entry.softTtl;
entry.serverDate = now;
entry.responseHeaders = response.headers;
} else if (entry.isExpired()) {
entry.softTtl = now + getClientCacheExpiry();
entry.ttl = entry.softTtl;
}
return entry;
}
protected Integer getClientCacheExpiry() {
return defaultClientCacheExpiry;
}
It handles 2 cases:
no Cache headers were set
Server cache entry indicates expired item
So if the server starts sending correct cache headers with expiry in the future, it will still work.