Skip to content

Commit

Permalink
Get Implementation in AsyncCache that takes in a callback function (g…
Browse files Browse the repository at this point in the history
…oogle#344)

Co-authored-by: Scott Phillips <[email protected]>
  • Loading branch information
sphill99 and Scott Phillips authored Jun 25, 2020
1 parent 504e41a commit a791d09
Show file tree
Hide file tree
Showing 7 changed files with 585 additions and 383 deletions.
28 changes: 28 additions & 0 deletions src/main/java/com/android/volley/AsyncCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.android.volley;

import androidx.annotation.Nullable;

/** Asynchronous equivalent to the {@link Cache} interface. */
public abstract class AsyncCache {

public interface OnGetCompleteCallback {
/**
* Invoked when the read from the cache is complete.
*
* @param entry The entry read from the cache, or null if the read failed or the key did not
* exist in the cache.
*/
void onGetComplete(@Nullable Cache.Entry entry);
}

/**
* Retrieves an entry from the cache and sends it back through the {@link
* OnGetCompleteCallback#onGetComplete} function
*
* @param key Cache key
* @param callback Callback that will be notified when the information has been retrieved
*/
public abstract void get(String key, OnGetCompleteCallback callback);

// TODO(#181): Implement the rest.
}
146 changes: 146 additions & 0 deletions src/main/java/com/android/volley/toolbox/CacheHeader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package com.android.volley.toolbox;

import androidx.annotation.Nullable;
import com.android.volley.Cache;
import com.android.volley.Header;
import com.android.volley.VolleyLog;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.List;

/** Handles holding onto the cache headers for an entry. */
class CacheHeader {
/** Magic number for current version of cache file format. */
private static final int CACHE_MAGIC = 0x20150306;

/**
* The size of the data identified by this CacheHeader on disk (both header and data).
*
* <p>Must be set by the caller after it has been calculated.
*
* <p>This is not serialized to disk.
*/
long size;

/** The key that identifies the cache entry. */
final String key;

/** ETag for cache coherence. */
@Nullable final String etag;

/** Date of this response as reported by the server. */
final long serverDate;

/** The last modified date for the requested object. */
final long lastModified;

/** TTL for this record. */
final long ttl;

/** Soft TTL for this record. */
final long softTtl;

/** Headers from the response resulting in this cache entry. */
final List<Header> allResponseHeaders;

private CacheHeader(
String key,
String etag,
long serverDate,
long lastModified,
long ttl,
long softTtl,
List<Header> allResponseHeaders) {
this.key = key;
this.etag = "".equals(etag) ? null : etag;
this.serverDate = serverDate;
this.lastModified = lastModified;
this.ttl = ttl;
this.softTtl = softTtl;
this.allResponseHeaders = allResponseHeaders;
}

/**
* Instantiates a new CacheHeader object.
*
* @param key The key that identifies the cache entry
* @param entry The cache entry.
*/
CacheHeader(String key, Cache.Entry entry) {
this(
key,
entry.etag,
entry.serverDate,
entry.lastModified,
entry.ttl,
entry.softTtl,
getAllResponseHeaders(entry));
}

private static List<Header> getAllResponseHeaders(Cache.Entry entry) {
// If the entry contains all the response headers, use that field directly.
if (entry.allResponseHeaders != null) {
return entry.allResponseHeaders;
}

// Legacy fallback - copy headers from the map.
return HttpHeaderParser.toAllHeaderList(entry.responseHeaders);
}

/**
* Reads the header from a CountingInputStream and returns a CacheHeader object.
*
* @param is The InputStream to read from.
* @throws IOException if fails to read header
*/
static CacheHeader readHeader(DiskBasedCache.CountingInputStream is) throws IOException {
int magic = DiskBasedCacheUtility.readInt(is);
if (magic != CACHE_MAGIC) {
// don't bother deleting, it'll get pruned eventually
throw new IOException();
}
String key = DiskBasedCacheUtility.readString(is);
String etag = DiskBasedCacheUtility.readString(is);
long serverDate = DiskBasedCacheUtility.readLong(is);
long lastModified = DiskBasedCacheUtility.readLong(is);
long ttl = DiskBasedCacheUtility.readLong(is);
long softTtl = DiskBasedCacheUtility.readLong(is);
List<Header> allResponseHeaders = DiskBasedCacheUtility.readHeaderList(is);
return new CacheHeader(
key, etag, serverDate, lastModified, ttl, softTtl, allResponseHeaders);
}

/** Creates a cache entry for the specified data. */
Cache.Entry toCacheEntry(byte[] data) {
Cache.Entry e = new Cache.Entry();
e.data = data;
e.etag = etag;
e.serverDate = serverDate;
e.lastModified = lastModified;
e.ttl = ttl;
e.softTtl = softTtl;
e.responseHeaders = HttpHeaderParser.toHeaderMap(allResponseHeaders);
e.allResponseHeaders = Collections.unmodifiableList(allResponseHeaders);
return e;
}

/** Writes the contents of this CacheHeader to the specified OutputStream. */
boolean writeHeader(OutputStream os) {
try {
DiskBasedCacheUtility.writeInt(os, CACHE_MAGIC);
DiskBasedCacheUtility.writeString(os, key);
DiskBasedCacheUtility.writeString(os, etag == null ? "" : etag);
DiskBasedCacheUtility.writeLong(os, serverDate);
DiskBasedCacheUtility.writeLong(os, lastModified);
DiskBasedCacheUtility.writeLong(os, ttl);
DiskBasedCacheUtility.writeLong(os, softTtl);
DiskBasedCacheUtility.writeHeaderList(allResponseHeaders, os);
os.flush();
return true;
} catch (IOException e) {
VolleyLog.d("%s", e.toString());
return false;
}
}
}
98 changes: 98 additions & 0 deletions src/main/java/com/android/volley/toolbox/DiskBasedAsyncCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.android.volley.toolbox;

import android.os.Build;
import androidx.annotation.RequiresApi;
import com.android.volley.AsyncCache;
import com.android.volley.VolleyLog;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* AsyncCache implementation that uses Java NIO's AsynchronousFileChannel to perform asynchronous
* disk reads and writes.
*/
@RequiresApi(Build.VERSION_CODES.O)
public class DiskBasedAsyncCache extends AsyncCache {

/** Map of the Key, CacheHeader pairs */
private final Map<String, CacheHeader> mEntries = new LinkedHashMap<>(16, .75f, true);

/** The supplier for the root directory to use for the cache. */
private final DiskBasedCacheUtility.FileSupplier mRootDirectorySupplier;

/** Total amount of space currently used by the cache in bytes. */
private long mTotalSize = 0;

/**
* Constructs an instance of the DiskBasedAsyncCache at the specified directory.
*
* @param rootDirectory The root directory of the cache.
*/
public DiskBasedAsyncCache(final File rootDirectory) {
mRootDirectorySupplier =
new DiskBasedCacheUtility.FileSupplier() {
@Override
public File get() {
return rootDirectory;
}
};
}

/** Returns the cache entry with the specified key if it exists, null otherwise. */
@Override
public void get(String key, final OnGetCompleteCallback callback) {
final CacheHeader entry = mEntries.get(key);
// if the entry does not exist, return null.
if (entry == null) {
callback.onGetComplete(null);
return;
}
final File file = getFileForKey(key);
final int size = (int) file.length();
Path path = Paths.get(file.getPath());
try (AsynchronousFileChannel afc =
AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
final ByteBuffer buffer = ByteBuffer.allocate(size);
afc.read(
/* destination= */ buffer,
/* position= */ 0,
/* attachment= */ null,
new CompletionHandler<Integer, Void>() {
@Override
public void completed(Integer result, Void v) {
// if the file size changes, return null
if (size != result) {
VolleyLog.e(
"File changed while reading: %s", file.getAbsolutePath());
callback.onGetComplete(null);
return;
}
byte[] data = buffer.array();
callback.onGetComplete(entry.toCacheEntry(data));
}

@Override
public void failed(Throwable exc, Void v) {
VolleyLog.e(exc, "Failed to read file %s", file.getAbsolutePath());
callback.onGetComplete(null);
}
});
} catch (IOException e) {
VolleyLog.e(e, "Failed to read file %s", file.getAbsolutePath());
callback.onGetComplete(null);
}
}

/** Returns a file object for the given cache key. */
File getFileForKey(String key) {
return new File(mRootDirectorySupplier.get(), DiskBasedCacheUtility.getFilenameForKey(key));
}
}
Loading

0 comments on commit a791d09

Please sign in to comment.