How it is was written previously, Picasso uses a cache of the underlying Http client.
HttpUrlConnection's built-in cache isn't working in truly offline mode and If using of OkHttpClient is unwanted by some reasons, it is possible to use your own implementation of disk-cache (of course based on DiskLruCache
).
One of ways is subclassing com.squareup.picasso.UrlConnectionDownloader
and programm whole logic at:
@Override
public Response load(final Uri uri, int networkPolicy) throws IOException {
...
}
And then use your implementation like this:
new Picasso.Builder(context).downloader(<your_downloader>).build();
Here is my implementation of UrlConnectionDownloader
, that works with disk-cache and ships to Picasso bitmaps even in total offline mode:
public class PicassoBitmapDownloader extends UrlConnectionDownloader {
private static final int MIN_DISK_CACHE_SIZE = 5 * 1024 * 1024; // 5MB
private static final int MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB
@NonNull private Context context;
@Nullable private DiskLruCache diskCache;
public class IfModifiedResponse extends Response {
private final String ifModifiedSinceDate;
public IfModifiedResponse(InputStream stream, boolean loadedFromCache, long contentLength, String ifModifiedSinceDate) {
super(stream, loadedFromCache, contentLength);
this.ifModifiedSinceDate = ifModifiedSinceDate;
}
public String getIfModifiedSinceDate() {
return ifModifiedSinceDate;
}
}
public PicassoBitmapDownloader(@NonNull Context context) {
super(context);
this.context = context;
}
@Override
public Response load(final Uri uri, int networkPolicy) throws IOException {
final String key = getKey(uri);
{
Response cachedResponse = getCachedBitmap(key);
if (cachedResponse != null) {
return cachedResponse;
}
}
IfModifiedResponse response = _load(uri);
if (cacheBitmap(key, response.getInputStream(), response.getIfModifiedSinceDate())) {
IfModifiedResponse cachedResponse = getCachedBitmap(key);
if (cachedResponse != null) {return cachedResponse;
}
}
return response;
}
@NonNull
protected IfModifiedResponse _load(Uri uri) throws IOException {
HttpURLConnection connection = openConnection(uri);
int responseCode = connection.getResponseCode();
if (responseCode >= 300) {
connection.disconnect();
throw new ResponseException(responseCode + " " + connection.getResponseMessage(),
0, responseCode);
}
long contentLength = connection.getHeaderFieldInt("Content-Length", -1);
String lastModified = connection.getHeaderField(Constants.HEADER_LAST_MODIFIED);
return new IfModifiedResponse(connection.getInputStream(), false, contentLength, lastModified);
}
@Override
protected HttpURLConnection openConnection(Uri path) throws IOException {
HttpURLConnection conn = super.openConnection(path);
DiskLruCache diskCache = getDiskCache();
DiskLruCache.Snapshot snapshot = diskCache == null ? null : diskCache.get(getKey(path));
if (snapshot != null) {
String ifModifiedSince = snapshot.getString(1);
if (!isEmpty(ifModifiedSince)) {
conn.addRequestProperty(Constants.HEADER_IF_MODIFIED_SINCE, ifModifiedSince);
}
}
return conn;
}
@Override public void shutdown() {
try {
if (diskCache != null) {
diskCache.flush();
diskCache.close();
}
}
catch (IOException e) {
e.printStackTrace();
}
super.shutdown();
}
public boolean cacheBitmap(@Nullable String key, @Nullable InputStream inputStream, @Nullable String ifModifiedSince) {
if (inputStream == null || isEmpty(key)) {
return false;
}
OutputStream outputStream = null;
DiskLruCache.Editor edit = null;
try {
DiskLruCache diskCache = getDiskCache();
edit = diskCache == null ? null : diskCache.edit(key);
outputStream = edit == null ? null : new BufferedOutputStream(edit.newOutputStream(0));
if (outputStream == null) {
return false;
}
ChatUtils.copy(inputStream, outputStream);
outputStream.flush();
edit.set(1, ifModifiedSince == null ? "" : ifModifiedSince);
edit.commit();
return true;
}
catch (Exception e) {
e.printStackTrace();
}
finally {
if (edit != null) {
edit.abortUnlessCommitted();
}
ChatUtils.closeQuietly(outputStream);
}
return false;
}
@Nullable
public IfModifiedResponse getCachedBitmap(String key) {
try {
DiskLruCache diskCache = getDiskCache();
DiskLruCache.Snapshot snapshot = diskCache == null ? null : diskCache.get(key);
InputStream inputStream = snapshot == null ? null : snapshot.getInputStream(0);
if (inputStream == null) {
return null;
}
return new IfModifiedResponse(inputStream, true, snapshot.getLength(0), snapshot.getString(1));
}
catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Nullable
synchronized public DiskLruCache getDiskCache() {
if (diskCache == null) {
try {
File file = new File(context.getCacheDir() + "/images");
if (!file.exists()) {
//noinspection ResultOfMethodCallIgnored
file.mkdirs();
}
long maxSize = calculateDiskCacheSize(file);
diskCache = DiskLruCache.open(file, BuildConfig.VERSION_CODE, 2, maxSize);
}
catch (Exception e) {
e.printStackTrace();
}
}
return diskCache;
}
@NonNull
private String getKey(@NonNull Uri uri) {
String key = md5(uri.toString());
return isEmpty(key) ? String.valueOf(uri.hashCode()) : key;
}
@Nullable
public static String md5(final String toEncrypt) {
try {
final MessageDigest digest = MessageDigest.getInstance("md5");
digest.update(toEncrypt.getBytes());
final byte[] bytes = digest.digest();
final StringBuilder sb = new StringBuilder();
for (byte aByte : bytes) {
sb.append(String.format("%02X", aByte));
}
return sb.toString().toLowerCase();
}
catch (Exception e) {
e.printStackTrace();
return null;
}
}
static long calculateDiskCacheSize(File dir) {
long available = ChatUtils.bytesAvailable(dir);
// Target 2% of the total space.
long size = available / 50;
// Bound inside min/max size for disk cache.
return Math.max(Math.min(size, MAX_DISK_CACHE_SIZE), MIN_DISK_CACHE_SIZE);
}
}