Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get Implementation in AsyncCache that takes in a callback function #344

Merged
merged 21 commits into from
Jun 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Could you add a Javadoc for this method? It doesn't have to be super-detailed, but one thing I think would be good to include is when/why entry would be null, and what we do in the event of an error. Something like:

/**
 * Invoked when the read from the cache is complete.
 *
 * @param entry The entry read from the cache, or null if the cache did not contain an entry for the given key or
 *         if the entry couldn't be read.
 */

}

/**
* Retrieves an entry from the cache and sends it back through the {@link
* OnGetCompleteCallback#onGetComplete} function
*
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "sends it back"

Also would be good to link to the callback – {@link OnGetCompleteCallback#onGetComplete}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed this. I realized I never added the DiskBasedCacheUtilityTest class to the commit, since it was untracked. Should have just fixed that as well.

* @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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you use the CACHE_MAGIC from DiskBasedCacheUtility instead? This way, we avoid duplicate code and potential mismatches in the future if one is modified but not the other.


/**
* 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.
*/
Comment on lines +17 to +23
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment looks intended for long size; below

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for AsyncCache, make sure to add javadoc to describe what the class does and how to use it.


/** 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