Merge "Change the default character encoding for JSON responses to UTF-8"
diff --git a/pom.xml b/pom.xml
index 90061b1..47252cb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,6 +32,11 @@
<artifactId>robolectric</artifactId>
<version>2.2</version>
</dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <version>1.9.5</version>
+ </dependency>
</dependencies>
<build>
@@ -57,6 +62,13 @@
<target>${java.version}</target>
</configuration>
</plugin>
+
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>emma-maven-plugin</artifactId>
+ <version>1.0-alpha-3</version>
+ </plugin>
+
</plugins>
</pluginManagement>
</build>
diff --git a/src/main/java/com/android/volley/Cache.java b/src/main/java/com/android/volley/Cache.java
index eafd118..f1ec757 100644
--- a/src/main/java/com/android/volley/Cache.java
+++ b/src/main/java/com/android/volley/Cache.java
@@ -74,6 +74,9 @@
/** Date of this response as reported by the server. */
public long serverDate;
+ /** The last modified date for the requested object. */
+ public long lastModified;
+
/** TTL for this record. */
public long ttl;
diff --git a/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java b/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java
index 371fd83..bdf7091 100644
--- a/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java
+++ b/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java
@@ -30,7 +30,7 @@
* tokens of a specified type for a specified account.
*/
public class AndroidAuthenticator implements Authenticator {
- private final Context mContext;
+ private final AccountManager mAccountManager;
private final Account mAccount;
private final String mAuthTokenType;
private final boolean mNotifyAuthFailure;
@@ -54,7 +54,13 @@
*/
public AndroidAuthenticator(Context context, Account account, String authTokenType,
boolean notifyAuthFailure) {
- mContext = context;
+ this(AccountManager.get(context), account, authTokenType, notifyAuthFailure);
+ }
+
+ // Visible for testing. Allows injection of a mock AccountManager.
+ AndroidAuthenticator(AccountManager accountManager, Account account,
+ String authTokenType, boolean notifyAuthFailure) {
+ mAccountManager = accountManager;
mAccount = account;
mAuthTokenType = authTokenType;
mNotifyAuthFailure = notifyAuthFailure;
@@ -71,8 +77,7 @@
@SuppressWarnings("deprecation")
@Override
public String getAuthToken() throws AuthFailureError {
- final AccountManager accountManager = AccountManager.get(mContext);
- AccountManagerFuture<Bundle> future = accountManager.getAuthToken(mAccount,
+ AccountManagerFuture<Bundle> future = mAccountManager.getAuthToken(mAccount,
mAuthTokenType, mNotifyAuthFailure, null, null);
Bundle result;
try {
@@ -97,6 +102,6 @@
@Override
public void invalidateAuthToken(String authToken) {
- AccountManager.get(mContext).invalidateAuthToken(mAccount.type, authToken);
+ mAccountManager.invalidateAuthToken(mAccount.type, authToken);
}
}
diff --git a/src/main/java/com/android/volley/toolbox/BasicNetwork.java b/src/main/java/com/android/volley/toolbox/BasicNetwork.java
index bc1cfdb..4b1603b 100644
--- a/src/main/java/com/android/volley/toolbox/BasicNetwork.java
+++ b/src/main/java/com/android/volley/toolbox/BasicNetwork.java
@@ -212,8 +212,8 @@
headers.put("If-None-Match", entry.etag);
}
- if (entry.serverDate > 0) {
- Date refTime = new Date(entry.serverDate);
+ if (entry.lastModified > 0) {
+ Date refTime = new Date(entry.lastModified);
headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
}
}
diff --git a/src/main/java/com/android/volley/toolbox/DiskBasedCache.java b/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
index b283788..036b55a 100644
--- a/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
+++ b/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
@@ -349,6 +349,9 @@
/** Date of this response as reported by the server. */
public long serverDate;
+ /** The last modified date for the requested object. */
+ public long lastModified;
+
/** TTL for this record. */
public long ttl;
@@ -370,6 +373,7 @@
this.size = entry.data.length;
this.etag = entry.etag;
this.serverDate = entry.serverDate;
+ this.lastModified = entry.lastModified;
this.ttl = entry.ttl;
this.softTtl = entry.softTtl;
this.responseHeaders = entry.responseHeaders;
@@ -396,6 +400,13 @@
entry.ttl = readLong(is);
entry.softTtl = readLong(is);
entry.responseHeaders = readStringStringMap(is);
+
+ try {
+ entry.lastModified = readLong(is);
+ } catch (EOFException e) {
+ // the old cache entry format doesn't know lastModified
+ }
+
return entry;
}
@@ -407,6 +418,7 @@
e.data = data;
e.etag = etag;
e.serverDate = serverDate;
+ e.lastModified = lastModified;
e.ttl = ttl;
e.softTtl = softTtl;
e.responseHeaders = responseHeaders;
@@ -426,6 +438,7 @@
writeLong(os, ttl);
writeLong(os, softTtl);
writeStringStringMap(responseHeaders, os);
+ writeLong(os, lastModified);
os.flush();
return true;
} catch (IOException e) {
diff --git a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
index 601ac0f..7306052 100644
--- a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
+++ b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
@@ -42,9 +42,12 @@
Map<String, String> headers = response.headers;
long serverDate = 0;
+ long lastModified = 0;
long serverExpires = 0;
long softExpire = 0;
+ long finalExpire = 0;
long maxAge = 0;
+ long staleWhileRevalidate = 0;
boolean hasCacheControl = false;
String serverEtag = null;
@@ -68,6 +71,11 @@
maxAge = Long.parseLong(token.substring(8));
} catch (Exception e) {
}
+ } else if (token.startsWith("stale-while-revalidate=")) {
+ try {
+ staleWhileRevalidate = Long.parseLong(token.substring(23));
+ } catch (Exception e) {
+ }
} else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
maxAge = 0;
}
@@ -79,23 +87,31 @@
serverExpires = parseDateAsEpoch(headerValue);
}
+ headerValue = headers.get("Last-Modified");
+ if (headerValue != null) {
+ lastModified = parseDateAsEpoch(headerValue);
+ }
+
serverEtag = headers.get("ETag");
// Cache-Control takes precedence over an Expires header, even if both exist and Expires
// is more restrictive.
if (hasCacheControl) {
softExpire = now + maxAge * 1000;
+ finalExpire = softExpire + staleWhileRevalidate * 1000;
} else if (serverDate > 0 && serverExpires >= serverDate) {
// Default semantic for Expire header in HTTP specification is softExpire.
softExpire = now + (serverExpires - serverDate);
+ finalExpire = softExpire;
}
Cache.Entry entry = new Cache.Entry();
entry.data = response.data;
entry.etag = serverEtag;
entry.softTtl = softExpire;
- entry.ttl = entry.softTtl;
+ entry.ttl = finalExpire;
entry.serverDate = serverDate;
+ entry.lastModified = lastModified;
entry.responseHeaders = headers;
return entry;
diff --git a/src/main/java/com/android/volley/toolbox/ImageLoader.java b/src/main/java/com/android/volley/toolbox/ImageLoader.java
index 5348dc6..151e022 100644
--- a/src/main/java/com/android/volley/toolbox/ImageLoader.java
+++ b/src/main/java/com/android/volley/toolbox/ImageLoader.java
@@ -20,6 +20,7 @@
import android.os.Handler;
import android.os.Looper;
import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
@@ -171,6 +172,15 @@
}
/**
+ * Equivalent to calling {@link #get(String, ImageListener, int, int, ScaleType)} with
+ * {@code Scaletype == ScaleType.CENTER_INSIDE}.
+ */
+ public ImageContainer get(String requestUrl, ImageListener imageListener,
+ int maxWidth, int maxHeight) {
+ return get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
+ }
+
+ /**
* Issues a bitmap request with the given URL if that image is not available
* in the cache, and returns a bitmap container that contains all of the data
* relating to the request (as well as the default image if the requested
@@ -179,11 +189,13 @@
* @param imageListener The listener to call when the remote image is loaded
* @param maxWidth The maximum width of the returned image.
* @param maxHeight The maximum height of the returned image.
+ * @param scaleType The ImageViews ScaleType used to calculate the needed image size.
* @return A container object that contains all of the properties of the request, as well as
* the currently available image (default if remote is not loaded).
*/
public ImageContainer get(String requestUrl, ImageListener imageListener,
- int maxWidth, int maxHeight) {
+ int maxWidth, int maxHeight, ScaleType scaleType) {
+
// only fulfill requests that were initiated from the main thread.
throwIfNotOnMainThread();
@@ -215,7 +227,8 @@
// The request is not already in flight. Send the new request to the network and
// track it.
- Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, cacheKey);
+ Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
+ cacheKey);
mRequestQueue.add(newRequest);
mInFlightRequests.put(cacheKey,
@@ -223,14 +236,14 @@
return imageContainer;
}
- protected Request<Bitmap> makeImageRequest(String requestUrl, int maxWidth, int maxHeight, final String cacheKey) {
+ protected Request<Bitmap> makeImageRequest(String requestUrl, int maxWidth, int maxHeight,
+ ScaleType scaleType, final String cacheKey) {
return new ImageRequest(requestUrl, new Listener<Bitmap>() {
@Override
public void onResponse(Bitmap response) {
onGetImageSuccess(cacheKey, response);
}
- }, maxWidth, maxHeight,
- Config.RGB_565, new ErrorListener() {
+ }, maxWidth, maxHeight, scaleType, Config.RGB_565, new ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onGetImageError(cacheKey, error);
diff --git a/src/main/java/com/android/volley/toolbox/ImageRequest.java b/src/main/java/com/android/volley/toolbox/ImageRequest.java
index 2ebe015..27c1fe2 100644
--- a/src/main/java/com/android/volley/toolbox/ImageRequest.java
+++ b/src/main/java/com/android/volley/toolbox/ImageRequest.java
@@ -26,6 +26,7 @@
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
+import android.widget.ImageView.ScaleType;
/**
* A canned request for getting an image at a given URL and calling
@@ -45,6 +46,7 @@
private final Config mDecodeConfig;
private final int mMaxWidth;
private final int mMaxHeight;
+ private ScaleType mScaleType;
/** Decoding lock so that we don't decode more than one image at a time (to avoid OOM's) */
private static final Object sDecodeLock = new Object();
@@ -63,20 +65,32 @@
* @param maxWidth Maximum width to decode this bitmap to, or zero for none
* @param maxHeight Maximum height to decode this bitmap to, or zero for
* none
+ * @param scaleType The ImageViews ScaleType used to calculate the needed image size.
* @param decodeConfig Format to decode the bitmap to
* @param errorListener Error listener, or null to ignore errors
*/
public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
- Config decodeConfig, Response.ErrorListener errorListener) {
- super(Method.GET, url, errorListener);
+ ScaleType scaleType, Config decodeConfig, Response.ErrorListener errorListener) {
+ super(Method.GET, url, errorListener);
setRetryPolicy(
new DefaultRetryPolicy(IMAGE_TIMEOUT_MS, IMAGE_MAX_RETRIES, IMAGE_BACKOFF_MULT));
mListener = listener;
mDecodeConfig = decodeConfig;
mMaxWidth = maxWidth;
mMaxHeight = maxHeight;
+ mScaleType = scaleType;
}
+ /**
+ * For API compatibility with the pre-ScaleType variant of the constructor. Equivalent to
+ * the normal constructor with {@code ScaleType.CENTER_INSIDE}.
+ */
+ @Deprecated
+ public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
+ Config decodeConfig, Response.ErrorListener errorListener) {
+ this(url, listener, maxWidth, maxHeight,
+ ScaleType.CENTER_INSIDE, decodeConfig, errorListener);
+ }
@Override
public Priority getPriority() {
return Priority.LOW;
@@ -92,14 +106,24 @@
* maintain aspect ratio with primary dimension
* @param actualPrimary Actual size of the primary dimension
* @param actualSecondary Actual size of the secondary dimension
+ * @param scaleType The ScaleType used to calculate the needed image size.
*/
private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
- int actualSecondary) {
+ int actualSecondary, ScaleType scaleType) {
+
// If no dominant value at all, just return the actual.
- if (maxPrimary == 0 && maxSecondary == 0) {
+ if ((maxPrimary == 0) && (maxSecondary == 0)) {
return actualPrimary;
}
+ // If ScaleType.FIT_XY fill the whole rectangle, ignore ratio.
+ if (scaleType == ScaleType.FIT_XY) {
+ if (maxPrimary == 0) {
+ return actualPrimary;
+ }
+ return maxPrimary;
+ }
+
// If primary is unspecified, scale primary to match secondary's scaling ratio.
if (maxPrimary == 0) {
double ratio = (double) maxSecondary / (double) actualSecondary;
@@ -112,7 +136,16 @@
double ratio = (double) actualSecondary / (double) actualPrimary;
int resized = maxPrimary;
- if (resized * ratio > maxSecondary) {
+
+ // If ScaleType.CENTER_CROP fill the whole rectangle, preserve aspect ratio.
+ if (scaleType == ScaleType.CENTER_CROP) {
+ if ((resized * ratio) < maxSecondary) {
+ resized = (int) (maxSecondary / ratio);
+ }
+ return resized;
+ }
+
+ if ((resized * ratio) > maxSecondary) {
resized = (int) (maxSecondary / ratio);
}
return resized;
@@ -150,9 +183,9 @@
// Then compute the dimensions we would ideally like to decode to.
int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
- actualWidth, actualHeight);
+ actualWidth, actualHeight, mScaleType);
int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
- actualHeight, actualWidth);
+ actualHeight, actualWidth, mScaleType);
// Decode to the nearest power of two scaling factor.
decodeOptions.inJustDecodeBounds = false;
diff --git a/src/main/java/com/android/volley/toolbox/NetworkImageView.java b/src/main/java/com/android/volley/toolbox/NetworkImageView.java
index 692e988..324dbc0 100644
--- a/src/main/java/com/android/volley/toolbox/NetworkImageView.java
+++ b/src/main/java/com/android/volley/toolbox/NetworkImageView.java
@@ -103,6 +103,7 @@
void loadImageIfNecessary(final boolean isInLayoutPass) {
int width = getWidth();
int height = getHeight();
+ ScaleType scaleType = getScaleType();
boolean wrapWidth = false, wrapHeight = false;
if (getLayoutParams() != null) {
@@ -177,7 +178,7 @@
setImageResource(mDefaultImageId);
}
}
- }, maxWidth, maxHeight);
+ }, maxWidth, maxHeight, scaleType);
// update the ImageContainer to be the new bitmap container.
mImageContainer = newContainer;
diff --git a/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java b/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java
new file mode 100644
index 0000000..e878658
--- /dev/null
+++ b/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerFuture;
+import android.accounts.AuthenticatorException;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import com.android.volley.AuthFailureError;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import static org.mockito.Mockito.*;
+
+@RunWith(RobolectricTestRunner.class)
+public class AndroidAuthenticatorTest {
+ private AccountManager mAccountManager;
+ private Account mAccount;
+ private AccountManagerFuture<Bundle> mFuture;
+ private AndroidAuthenticator mAuthenticator;
+
+ @Before
+ public void setUp() {
+ mAccountManager = mock(AccountManager.class);
+ mFuture = mock(AccountManagerFuture.class);
+ mAccount = new Account("coolperson", "cooltype");
+ mAuthenticator = new AndroidAuthenticator(mAccountManager, mAccount, "cooltype", false);
+ }
+
+ @Test(expected = AuthFailureError.class)
+ public void failedGetAuthToken() throws Exception {
+ when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)).thenReturn(mFuture);
+ when(mFuture.getResult()).thenThrow(new AuthenticatorException("sadness!"));
+ mAuthenticator.getAuthToken();
+ }
+
+ @Test(expected = AuthFailureError.class)
+ public void resultContainsIntent() throws Exception {
+ Intent intent = new Intent();
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(AccountManager.KEY_INTENT, intent);
+ when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)).thenReturn(mFuture);
+ when(mFuture.getResult()).thenReturn(bundle);
+ when(mFuture.isDone()).thenReturn(true);
+ when(mFuture.isCancelled()).thenReturn(false);
+ mAuthenticator.getAuthToken();
+ }
+
+ @Test(expected = AuthFailureError.class)
+ public void missingAuthToken() throws Exception {
+ Bundle bundle = new Bundle();
+ when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)).thenReturn(mFuture);
+ when(mFuture.getResult()).thenReturn(bundle);
+ when(mFuture.isDone()).thenReturn(true);
+ when(mFuture.isCancelled()).thenReturn(false);
+ mAuthenticator.getAuthToken();
+ }
+
+ @Test
+ public void invalidateAuthToken() throws Exception {
+ mAuthenticator.invalidateAuthToken("monkey");
+ verify(mAccountManager).invalidateAuthToken("cooltype", "monkey");
+ }
+
+ @Test
+ public void goodToken() throws Exception {
+ Bundle bundle = new Bundle();
+ bundle.putString(AccountManager.KEY_AUTHTOKEN, "monkey");
+ when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)).thenReturn(mFuture);
+ when(mFuture.getResult()).thenReturn(bundle);
+ when(mFuture.isDone()).thenReturn(true);
+ when(mFuture.isCancelled()).thenReturn(false);
+ Assert.assertEquals("monkey", mAuthenticator.getAuthToken());
+ }
+
+ @Test
+ public void publicMethods() throws Exception {
+ // Catch-all test to find API-breaking changes.
+ Context context = mock(Context.class);
+ new AndroidAuthenticator(context, mAccount, "cooltype");
+ new AndroidAuthenticator(context, mAccount, "cooltype", true);
+ Assert.assertSame(mAccount, mAuthenticator.getAccount());
+ }
+}
diff --git a/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java b/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
index 4b2955d..d9d49e9 100644
--- a/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
+++ b/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java
@@ -34,6 +34,7 @@
Cache.Entry e = new Cache.Entry();
e.data = new byte[8];
e.serverDate = 1234567L;
+ e.lastModified = 13572468L;
e.ttl = 9876543L;
e.softTtl = 8765432L;
e.etag = "etag";
@@ -48,6 +49,7 @@
assertEquals(first.key, second.key);
assertEquals(first.serverDate, second.serverDate);
+ assertEquals(first.lastModified, second.lastModified);
assertEquals(first.ttl, second.ttl);
assertEquals(first.softTtl, second.softTtl);
assertEquals(first.etag, second.etag);
@@ -121,4 +123,43 @@
assertEquals(DiskBasedCache.readStringStringMap(bais), emptyKey);
assertEquals(DiskBasedCache.readStringStringMap(bais), emptyValue);
}
+
+ // Test deserializing the old format into the new one.
+ public void testCacheHeaderSerializationOldToNewFormat() throws Exception {
+
+ final int CACHE_MAGIC = 0x20140623;
+ final String key = "key";
+ final String etag = "etag";
+ final long serverDate = 1234567890l;
+ final long ttl = 1357924680l;
+ final long softTtl = 2468013579l;
+
+ Map<String, String> responseHeaders = new HashMap<String, String>();
+ responseHeaders.put("first", "thing");
+ responseHeaders.put("second", "item");
+
+ // write old sytle header (without lastModified)
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DiskBasedCache.writeInt(baos, CACHE_MAGIC);
+ DiskBasedCache.writeString(baos, key);
+ DiskBasedCache.writeString(baos, etag == null ? "" : etag);
+ DiskBasedCache.writeLong(baos, serverDate);
+ DiskBasedCache.writeLong(baos, ttl);
+ DiskBasedCache.writeLong(baos, softTtl);
+ DiskBasedCache.writeStringStringMap(responseHeaders, baos);
+
+ // read / test new style header (with lastModified)
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ CacheHeader cacheHeader = CacheHeader.readHeader(bais);
+
+ assertEquals(cacheHeader.key, key);
+ assertEquals(cacheHeader.etag, etag);
+ assertEquals(cacheHeader.serverDate, serverDate);
+ assertEquals(cacheHeader.ttl, ttl);
+ assertEquals(cacheHeader.softTtl, softTtl);
+ assertEquals(cacheHeader.responseHeaders, responseHeaders);
+
+ // the old format doesn't know lastModified
+ assertEquals(cacheHeader.lastModified, 0);
+ }
}
diff --git a/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java b/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
index ae8257a..f9230c6 100644
--- a/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
+++ b/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java
@@ -40,6 +40,7 @@
private static long ONE_MINUTE_MILLIS = 1000L * 60;
private static long ONE_HOUR_MILLIS = 1000L * 60 * 60;
+ private static long ONE_DAY_MILLIS = ONE_HOUR_MILLIS * 24;
private NetworkResponse response;
private Map<String, String> headers;
@@ -55,6 +56,7 @@
assertNotNull(entry);
assertNull(entry.etag);
assertEquals(0, entry.serverDate);
+ assertEquals(0, entry.lastModified);
assertEquals(0, entry.ttl);
assertEquals(0, entry.softTtl);
}
@@ -82,6 +84,7 @@
@Test public void parseCacheHeaders_normalExpire() {
long now = System.currentTimeMillis();
headers.put("Date", rfc1123Date(now));
+ headers.put("Last-Modified", rfc1123Date(now - ONE_DAY_MILLIS));
headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS));
Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
@@ -89,6 +92,7 @@
assertNotNull(entry);
assertNull(entry.etag);
assertEqualsWithin(entry.serverDate, now, ONE_MINUTE_MILLIS);
+ assertEqualsWithin(entry.lastModified, (now - ONE_DAY_MILLIS), ONE_MINUTE_MILLIS);
assertTrue(entry.softTtl >= (now + ONE_HOUR_MILLIS));
assertTrue(entry.ttl == entry.softTtl);
}
@@ -135,6 +139,24 @@
assertEquals(entry.softTtl, entry.ttl);
}
+ @Test public void testParseCacheHeaders_staleWhileRevalidate() {
+ long now = System.currentTimeMillis();
+ headers.put("Date", rfc1123Date(now));
+ headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS));
+
+ // - max-age (entry.softTtl) indicates that the asset is fresh for 1 day
+ // - stale-while-revalidate (entry.ttl) indicates that the asset may
+ // continue to be served stale for up to additional 7 days
+ headers.put("Cache-Control", "max-age=86400, stale-while-revalidate=604800");
+
+ Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response);
+
+ assertNotNull(entry);
+ assertNull(entry.etag);
+ assertEqualsWithin(now + 24 * ONE_HOUR_MILLIS, entry.softTtl, ONE_MINUTE_MILLIS);
+ assertEqualsWithin(now + 8 * 24 * ONE_HOUR_MILLIS, entry.ttl, ONE_MINUTE_MILLIS);
+ }
+
@Test public void parseCacheHeaders_cacheControlNoCache() {
long now = System.currentTimeMillis();
headers.put("Date", rfc1123Date(now));
diff --git a/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java b/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java
new file mode 100644
index 0000000..47c55a6
--- /dev/null
+++ b/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.volley.toolbox;
+
+import android.graphics.Bitmap;
+import com.android.volley.Request;
+import com.android.volley.RequestQueue;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import static org.mockito.Mockito.*;
+
+@RunWith(RobolectricTestRunner.class)
+public class ImageLoaderTest {
+ private RequestQueue mRequestQueue;
+ private ImageLoader.ImageCache mImageCache;
+ private ImageLoader mImageLoader;
+
+ @Before
+ public void setUp() {
+ mRequestQueue = mock(RequestQueue.class);
+ mImageCache = mock(ImageLoader.ImageCache.class);
+ mImageLoader = new ImageLoader(mRequestQueue, mImageCache);
+ }
+
+ @Test
+ public void isCachedChecksCache() throws Exception {
+ when(mImageCache.getBitmap(anyString())).thenReturn(null);
+ Assert.assertFalse(mImageLoader.isCached("http://foo", 0, 0));
+ }
+
+ @Test
+ public void getWithCacheHit() throws Exception {
+ Bitmap bitmap = Bitmap.createBitmap(1, 1, null);
+ ImageLoader.ImageListener listener = mock(ImageLoader.ImageListener.class);
+ when(mImageCache.getBitmap(anyString())).thenReturn(bitmap);
+ ImageLoader.ImageContainer ic = mImageLoader.get("http://foo", listener);
+ Assert.assertSame(bitmap, ic.getBitmap());
+ verify(listener).onResponse(ic, true);
+ }
+
+ @Test
+ public void getWithCacheMiss() throws Exception {
+ when(mImageCache.getBitmap(anyString())).thenReturn(null);
+ ImageLoader.ImageListener listener = mock(ImageLoader.ImageListener.class);
+ // Ask for the image to be loaded.
+ mImageLoader.get("http://foo", listener);
+ // Second pass to test deduping logic.
+ mImageLoader.get("http://foo", listener);
+ // Response callback should be called both times.
+ verify(listener, times(2)).onResponse(any(ImageLoader.ImageContainer.class), eq(true));
+ // But request should be enqueued only once.
+ verify(mRequestQueue, times(1)).add(any(Request.class));
+ }
+
+ @Test
+ public void publicMethods() throws Exception {
+ // Catch API breaking changes.
+ ImageLoader.getImageListener(null, -1, -1);
+ mImageLoader.setBatchedResponseDelay(1000);
+ }
+}
+
diff --git a/src/test/java/com/android/volley/toolbox/ImageRequestTest.java b/src/test/java/com/android/volley/toolbox/ImageRequestTest.java
index 2f4495a..bd98e7d 100644
--- a/src/test/java/com/android/volley/toolbox/ImageRequestTest.java
+++ b/src/test/java/com/android/volley/toolbox/ImageRequestTest.java
@@ -18,7 +18,7 @@
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
-
+import android.widget.ImageView.ScaleType;
import com.android.volley.NetworkResponse;
import com.android.volley.Response;
import org.junit.Test;
@@ -47,30 +47,84 @@
ShadowBitmapFactory.provideWidthAndHeightHints("fake", 1024, 500);
NetworkResponse jpeg = new NetworkResponse(jpegBytes);
- // No resize
- verifyResize(jpeg, 0, 0, 1024, 500);
+ // Scale the image uniformly (maintain the image's aspect ratio) so that
+ // both dimensions (width and height) of the image will be equal to or
+ // less than the corresponding dimension of the view.
+ ScaleType scalteType = ScaleType.CENTER_INSIDE;
// Exact sizes
- verifyResize(jpeg, 512, 250, 512, 250); // exactly half
- verifyResize(jpeg, 511, 249, 509, 249); // just under half
- verifyResize(jpeg, 1080, 500, 1024, 500); // larger
- verifyResize(jpeg, 500, 500, 500, 244); // keep same ratio
+ verifyResize(jpeg, 512, 250, scalteType, 512, 250); // exactly half
+ verifyResize(jpeg, 511, 249, scalteType, 509, 249); // just under half
+ verifyResize(jpeg, 1080, 500, scalteType, 1024, 500); // larger
+ verifyResize(jpeg, 500, 500, scalteType, 500, 244); // keep same ratio
// Specify only width, preserve aspect ratio
- verifyResize(jpeg, 512, 0, 512, 250);
- verifyResize(jpeg, 800, 0, 800, 390);
- verifyResize(jpeg, 1024, 0, 1024, 500);
+ verifyResize(jpeg, 512, 0, scalteType, 512, 250);
+ verifyResize(jpeg, 800, 0, scalteType, 800, 390);
+ verifyResize(jpeg, 1024, 0, scalteType, 1024, 500);
// Specify only height, preserve aspect ratio
- verifyResize(jpeg, 0, 250, 512, 250);
- verifyResize(jpeg, 0, 391, 800, 391);
- verifyResize(jpeg, 0, 500, 1024, 500);
+ verifyResize(jpeg, 0, 250, scalteType, 512, 250);
+ verifyResize(jpeg, 0, 391, scalteType, 800, 391);
+ verifyResize(jpeg, 0, 500, scalteType, 1024, 500);
+
+ // No resize
+ verifyResize(jpeg, 0, 0, scalteType, 1024, 500);
+
+
+ // Scale the image uniformly (maintain the image's aspect ratio) so that
+ // both dimensions (width and height) of the image will be equal to or
+ // larger than the corresponding dimension of the view.
+ scalteType = ScaleType.CENTER_CROP;
+
+ // Exact sizes
+ verifyResize(jpeg, 512, 250, scalteType, 512, 250);
+ verifyResize(jpeg, 511, 249, scalteType, 511, 249);
+ verifyResize(jpeg, 1080, 500, scalteType, 1024, 500);
+ verifyResize(jpeg, 500, 500, scalteType, 1024, 500);
+
+ // Specify only width
+ verifyResize(jpeg, 512, 0, scalteType, 512, 250);
+ verifyResize(jpeg, 800, 0, scalteType, 800, 390);
+ verifyResize(jpeg, 1024, 0, scalteType, 1024, 500);
+
+ // Specify only height
+ verifyResize(jpeg, 0, 250, scalteType, 512, 250);
+ verifyResize(jpeg, 0, 391, scalteType, 800, 391);
+ verifyResize(jpeg, 0, 500, scalteType, 1024, 500);
+
+ // No resize
+ verifyResize(jpeg, 0, 0, scalteType, 1024, 500);
+
+
+ // Scale in X and Y independently, so that src matches dst exactly. This
+ // may change the aspect ratio of the src.
+ scalteType = ScaleType.FIT_XY;
+
+ // Exact sizes
+ verifyResize(jpeg, 512, 250, scalteType, 512, 250);
+ verifyResize(jpeg, 511, 249, scalteType, 511, 249);
+ verifyResize(jpeg, 1080, 500, scalteType, 1024, 500);
+ verifyResize(jpeg, 500, 500, scalteType, 500, 500);
+
+ // Specify only width
+ verifyResize(jpeg, 512, 0, scalteType, 512, 500);
+ verifyResize(jpeg, 800, 0, scalteType, 800, 500);
+ verifyResize(jpeg, 1024, 0, scalteType, 1024, 500);
+
+ // Specify only height
+ verifyResize(jpeg, 0, 250, scalteType, 1024, 250);
+ verifyResize(jpeg, 0, 391, scalteType, 1024, 391);
+ verifyResize(jpeg, 0, 500, scalteType, 1024, 500);
+
+ // No resize
+ verifyResize(jpeg, 0, 0, scalteType, 1024, 500);
}
private void verifyResize(NetworkResponse networkResponse, int maxWidth, int maxHeight,
- int expectedWidth, int expectedHeight) {
- ImageRequest request = new ImageRequest(
- "", null, maxWidth, maxHeight, Config.RGB_565, null);
+ ScaleType scaleType, int expectedWidth, int expectedHeight) {
+ ImageRequest request = new ImageRequest("", null, maxWidth, maxHeight, scaleType,
+ Config.RGB_565, null);
Response<Bitmap> response = request.parseNetworkResponse(networkResponse);
assertNotNull(response);
assertTrue(response.isSuccess());
diff --git a/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java b/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java
index 48c81b6..bc2cc29 100644
--- a/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java
+++ b/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java
@@ -1,6 +1,7 @@
package com.android.volley.toolbox;
import android.view.ViewGroup.LayoutParams;
+import android.widget.ImageView.ScaleType;
import org.junit.Before;
import org.junit.Test;
@@ -43,7 +44,7 @@
public int lastMaxHeight;
public ImageContainer get(String requestUrl, ImageListener imageListener, int maxWidth,
- int maxHeight) {
+ int maxHeight, ScaleType scaleType) {
lastRequestUrl = requestUrl;
lastMaxWidth = maxWidth;
lastMaxHeight = maxHeight;
diff --git a/src/test/java/com/android/volley/utils/CacheTestUtils.java b/src/test/java/com/android/volley/utils/CacheTestUtils.java
index cd2b8e7..898d055 100644
--- a/src/test/java/com/android/volley/utils/CacheTestUtils.java
+++ b/src/test/java/com/android/volley/utils/CacheTestUtils.java
@@ -24,7 +24,7 @@
entry.data = new byte[random.nextInt(1024)];
}
entry.etag = String.valueOf(random.nextLong());
- entry.serverDate = random.nextLong();
+ entry.lastModified = random.nextLong();
entry.ttl = isExpired ? 0 : Long.MAX_VALUE;
entry.softTtl = needsRefresh ? 0 : Long.MAX_VALUE;
return entry;
diff --git a/src/test/resources/org.robolectric.Config.properties b/src/test/resources/org.robolectric.Config.properties
new file mode 100644
index 0000000..9daf692
--- /dev/null
+++ b/src/test/resources/org.robolectric.Config.properties
@@ -0,0 +1 @@
+manifest=src/main/AndroidManifest.xml