-
Notifications
You must be signed in to change notification settings - Fork 751
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
Changes from all commits
34c5cac
e638859
63ca3b5
71c3851
51f1fce
17859ac
396e6be
771780b
28a4a22
d29af11
890d0f1
6838149
7dcfec9
88c66be
7253e55
b8d30ef
b10b298
7d412c7
12393fe
1f2fafc
02487b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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} There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
} |
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you use the |
||
|
||
/** | ||
* 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment looks intended for |
||
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; | ||
} | ||
} | ||
} |
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As for |
||
|
||
/** 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)); | ||
} | ||
} |
There was a problem hiding this comment.
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: