Merge remote-tracking branch 'origin/master' into gpeal/network-cache-key
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
index ed780c1..0102049 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
@@ -425,6 +425,19 @@
   }
 
   /**
+   * Load a lottie animation from a url. The url can be a json file or a zip file. Use a zip file if you have images. Simply zip them together and lottie
+   * will unzip and link the images automatically.
+   *
+   * Under the hood, Lottie uses Java HttpURLConnection because it doesn't require any transitive networking dependencies. It will download the file
+   * to the application cache under a temporary name. If the file successfully parses to a composition, it will rename the temporary file to one that
+   * can be accessed immediately for subsequent requests. If the file does not parse to a composition, the temporary file will be deleted.
+   */
+  public void setAnimationFromUrl(String url, @Nullable String cacheKey) {
+    LottieTask<LottieComposition> task = LottieCompositionFactory.fromUrl(getContext(), url, cacheKey);
+    setCompositionTask(task);
+  }
+
+  /**
    * Set a default failure listener that will be called if any of the setAnimation APIs fail for any reason.
    * This can be used to replace the default behavior.
    *
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieCompositionFactory.java b/lottie/src/main/java/com/airbnb/lottie/LottieCompositionFactory.java
index a1d394f..0ae8125 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieCompositionFactory.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieCompositionFactory.java
@@ -8,6 +8,7 @@
 import android.os.Build;
 
 import com.airbnb.lottie.model.LottieCompositionCache;
+import com.airbnb.lottie.network.NetworkCache;
 import com.airbnb.lottie.network.NetworkFetcher;
 import com.airbnb.lottie.parser.LottieCompositionMoshiParser;
 import com.airbnb.lottie.parser.moshi.JsonReader;
@@ -61,6 +62,12 @@
     LottieCompositionCache.getInstance().resize(size);
   }
 
+  public static void clearCache(Context context) {
+    taskCache.clear();
+    LottieCompositionCache.getInstance().clear();
+    new NetworkCache(context).clear();
+  }
+
   /**
    * Fetch an animation from an http url. Once it is downloaded once, Lottie will cache the file to disk for
    * future use. Because of this, you may call `fromUrl` ahead of time to warm the cache if you think you
@@ -77,11 +84,11 @@
    * future use. Because of this, you may call `fromUrl` ahead of time to warm the cache if you think you
    * might need an animation in the future.
    */
-  public static LottieTask<LottieComposition> fromUrl(final Context context, final String url, @Nullable String cacheKey) {
+  public static LottieTask<LottieComposition> fromUrl(final Context context, final String url, @Nullable final String cacheKey) {
     return cache(cacheKey, new Callable<LottieResult<LottieComposition>>() {
       @Override
       public LottieResult<LottieComposition> call() {
-        return NetworkFetcher.fetchSync(context, url);
+        return NetworkFetcher.fetchSync(context, url, cacheKey);
       }
     });
   }
@@ -93,7 +100,18 @@
    */
   @WorkerThread
   public static LottieResult<LottieComposition> fromUrlSync(Context context, String url) {
-    return NetworkFetcher.fetchSync(context, url);
+    return fromUrlSync(context, url, url);
+  }
+
+
+  /**
+   * Fetch an animation from an http url. Once it is downloaded once, Lottie will cache the file to disk for
+   * future use. Because of this, you may call `fromUrl` ahead of time to warm the cache if you think you
+   * might need an animation in the future.
+   */
+  @WorkerThread
+  public static LottieResult<LottieComposition> fromUrlSync(Context context, String url, @Nullable String cacheKey) {
+    return NetworkFetcher.fetchSync(context, url, cacheKey);
   }
 
   /**
diff --git a/lottie/src/main/java/com/airbnb/lottie/network/NetworkCache.java b/lottie/src/main/java/com/airbnb/lottie/network/NetworkCache.java
index dc8e5bd..71efef2 100644
--- a/lottie/src/main/java/com/airbnb/lottie/network/NetworkCache.java
+++ b/lottie/src/main/java/com/airbnb/lottie/network/NetworkCache.java
@@ -18,13 +18,24 @@
 /**
  * Helper class to save and restore animations fetched from an URL to the app disk cache.
  */
-class NetworkCache {
+public class NetworkCache {
   private final Context appContext;
-  private final String url;
 
-  NetworkCache(Context appContext, String url) {
+  public NetworkCache(Context appContext) {
     this.appContext = appContext.getApplicationContext();
-    this.url = url;
+  }
+
+  public void clear() {
+    File parentDir = parentDir();
+    if (parentDir.exists()) {
+      File[] files = parentDir.listFiles();
+      if (files != null && files.length > 0) {
+        for (File file : parentDir.listFiles()) {
+          file.delete();
+        }
+      }
+      parentDir.delete();
+    }
   }
 
   /**
@@ -36,8 +47,8 @@
    */
   @Nullable
   @WorkerThread
-  Pair<FileExtension, InputStream> fetch() {
-    File cachedFile = null;
+  Pair<FileExtension, InputStream> fetch(String url) {
+    File cachedFile;
     try {
       cachedFile = getCachedFile(url);
     } catch (FileNotFoundException e) {
@@ -70,9 +81,9 @@
    * to an composition, {@link #renameTempFile(FileExtension)} should be called to move the file
    * to its final location for future cache hits.
    */
-  File writeTempCacheFile(InputStream stream, FileExtension extension) throws IOException {
+  File writeTempCacheFile(String url, InputStream stream, FileExtension extension) throws IOException {
     String fileName = filenameForUrl(url, extension, true);
-    File file = new File(appContext.getCacheDir(), fileName);
+    File file = new File(parentDir(), fileName);
     try {
       OutputStream output = new FileOutputStream(file);
       //noinspection TryFinallyCanBeTryWithResources
@@ -98,9 +109,9 @@
    * If the file created by {@link #writeTempCacheFile(InputStream, FileExtension)} was successfully parsed,
    * this should be called to remove the temporary part of its name which will allow it to be a cache hit in the future.
    */
-  void renameTempFile(FileExtension extension) {
+  void renameTempFile(String url, FileExtension extension) {
     String fileName = filenameForUrl(url, extension, true);
-    File file = new File(appContext.getCacheDir(), fileName);
+    File file = new File(parentDir(), fileName);
     String newFileName = file.getAbsolutePath().replace(".temp", "");
     File newFile = new File(newFileName);
     boolean renamed = file.renameTo(newFile);
@@ -116,17 +127,28 @@
    */
   @Nullable
   private File getCachedFile(String url) throws FileNotFoundException {
-    File jsonFile = new File(appContext.getCacheDir(), filenameForUrl(url, FileExtension.JSON, false));
+    File jsonFile = new File(parentDir(), filenameForUrl(url, FileExtension.JSON, false));
     if (jsonFile.exists()) {
       return jsonFile;
     }
-    File zipFile = new File(appContext.getCacheDir(), filenameForUrl(url, FileExtension.ZIP, false));
+    File zipFile = new File(parentDir(), filenameForUrl(url, FileExtension.ZIP, false));
     if (zipFile.exists()) {
       return zipFile;
     }
     return null;
   }
 
+  private File parentDir() {
+    File file = new File(appContext.getCacheDir(), "lottie_network_cache");
+    if (file.isFile()) {
+      file.delete();
+    }
+    if (!file.exists()) {
+      file.mkdirs();
+    }
+    return file;
+  }
+
   private static String filenameForUrl(String url, FileExtension extension, boolean isTemp) {
     return "lottie_cache_" + url.replaceAll("\\W+", "") + (isTemp ? extension.tempExtension(): extension.extension);
   }
diff --git a/lottie/src/main/java/com/airbnb/lottie/network/NetworkFetcher.java b/lottie/src/main/java/com/airbnb/lottie/network/NetworkFetcher.java
index 028ee95..2f74633 100644
--- a/lottie/src/main/java/com/airbnb/lottie/network/NetworkFetcher.java
+++ b/lottie/src/main/java/com/airbnb/lottie/network/NetworkFetcher.java
@@ -25,16 +25,20 @@
   private final Context appContext;
   private final String url;
 
-  private final NetworkCache networkCache;
+  @Nullable private final NetworkCache networkCache;
 
-  public static LottieResult<LottieComposition> fetchSync(Context context, String url) {
-    return new NetworkFetcher(context, url).fetchSync();
+  public static LottieResult<LottieComposition> fetchSync(Context context, String url, @Nullable String cacheKey) {
+    return new NetworkFetcher(context, url, cacheKey).fetchSync();
   }
 
-  private NetworkFetcher(Context context, String url) {
+  private NetworkFetcher(Context context, String url, @Nullable String cacheKey) {
     appContext = context.getApplicationContext();
     this.url = url;
-    networkCache = new NetworkCache(appContext, url);
+    if (cacheKey == null) {
+      networkCache = null;
+    } else {
+      networkCache = new NetworkCache(appContext);
+    }
   }
 
   @WorkerThread
@@ -54,7 +58,10 @@
   @Nullable
   @WorkerThread
   private LottieComposition fetchFromCache() {
-    Pair<FileExtension, InputStream> cacheResult = networkCache.fetch();
+    if (networkCache == null) {
+      return null;
+    }
+    Pair<FileExtension, InputStream> cacheResult = networkCache.fetch(url);
     if (cacheResult == null) {
       return null;
     }
@@ -83,7 +90,7 @@
   }
 
   @WorkerThread
-  private LottieResult fetchFromNetworkInternal() throws IOException {
+  private LottieResult<LottieComposition> fetchFromNetworkInternal() throws IOException {
     Logger.debug("Fetching " + url);
 
 
@@ -95,7 +102,7 @@
 
       if (connection.getErrorStream() != null || connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
         String error = getErrorFromConnection(connection);
-        return new LottieResult<>(new IllegalArgumentException("Unable to fetch " + url + ". Failed with " + connection.getResponseCode() + "\n" + error));
+        return new LottieResult<LottieComposition>(new IllegalArgumentException("Unable to fetch " + url + ". Failed with " + connection.getResponseCode() + "\n" + error));
       }
 
       LottieResult<LottieComposition> result = getResultFromConnection(connection);
@@ -134,7 +141,7 @@
   private LottieResult<LottieComposition> getResultFromConnection(HttpURLConnection connection) throws IOException {
     File file;
     FileExtension extension;
-    LottieResult<LottieComposition> result = null;
+    LottieResult<LottieComposition> result;
     String contentType = connection.getContentType();
     if (contentType == null) {
       // Assume JSON for best effort parsing. If it fails, it will just deliver the parse exception
@@ -144,17 +151,25 @@
     if (contentType.contains("application/zip")) {
       Logger.debug("Handling zip response.");
       extension = FileExtension.ZIP;
-      file = networkCache.writeTempCacheFile(connection.getInputStream(), extension);
-      result = LottieCompositionFactory.fromZipStreamSync(new ZipInputStream(new FileInputStream(file)), url);
+      if (networkCache == null) {
+        result = LottieCompositionFactory.fromZipStreamSync(new ZipInputStream(connection.getInputStream()), null);
+      } else {
+        file = networkCache.writeTempCacheFile(url, connection.getInputStream(), extension);
+        result = LottieCompositionFactory.fromZipStreamSync(new ZipInputStream(new FileInputStream(file)), url);
+      }
     } else {
       Logger.debug("Received json response.");
       extension = FileExtension.JSON;
-      file = networkCache.writeTempCacheFile(connection.getInputStream(), extension);
-      result = LottieCompositionFactory.fromJsonInputStreamSync(new FileInputStream(new File(file.getAbsolutePath())), url);
+      if (networkCache == null) {
+        result = LottieCompositionFactory.fromJsonInputStreamSync(connection.getInputStream(), null);
+      } else {
+        file = networkCache.writeTempCacheFile(url, connection.getInputStream(), extension);
+        result = LottieCompositionFactory.fromJsonInputStreamSync(new FileInputStream(new File(file.getAbsolutePath())), url);
+      }
     }
 
-    if (result.getValue() != null) {
-      networkCache.renameTempFile(extension);
+    if (networkCache != null && result.getValue() != null) {
+      networkCache.renameTempFile(url, extension);
     }
     return result;
   }
diff --git a/lottie/src/test/java/com/airbnb/lottie/LottieCompositionFactoryTest.java b/lottie/src/test/java/com/airbnb/lottie/LottieCompositionFactoryTest.java
index 7693858..7fee47d 100644
--- a/lottie/src/test/java/com/airbnb/lottie/LottieCompositionFactoryTest.java
+++ b/lottie/src/test/java/com/airbnb/lottie/LottieCompositionFactoryTest.java
@@ -9,9 +9,9 @@
 import org.robolectric.RuntimeEnvironment;
 
 import java.io.FileNotFoundException;
-import java.io.StringReader;
+import java.io.IOException;
+import java.io.InputStream;
 
-import static com.airbnb.lottie.parser.moshi.JsonReader.of;
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
@@ -20,6 +20,7 @@
 import static okio.Okio.source;
 import static org.junit.Assert.assertTrue;
 
+@SuppressWarnings("ReferenceEquality")
 public class LottieCompositionFactoryTest extends BaseTest {
     private static final String JSON = "{\"v\":\"4.11.1\",\"fr\":60,\"ip\":0,\"op\":180,\"w\":300,\"h\":300,\"nm\":\"Comp 1\",\"ddd\":0,\"assets\":[]," +
             "\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Shape Layer 1\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0," +
@@ -90,7 +91,7 @@
 
     @Test
     public void testNullMultipleTimesAsync() {
-        JsonReader reader = JsonReader.of(buffer(source(new StringInputStream(JSON))));
+        JsonReader reader = JsonReader.of(buffer(source(getNeverCompletingInputStream())));
         LottieTask<LottieComposition> task1 = LottieCompositionFactory.fromJsonReader(reader, null);
         LottieTask<LottieComposition> task2 = LottieCompositionFactory.fromJsonReader(reader, null);
         assertFalse(task1 == task2);
@@ -98,7 +99,7 @@
 
     @Test
     public void testNullMultipleTimesSync() {
-        JsonReader reader = JsonReader.of(buffer(source(new StringInputStream(JSON))));
+        JsonReader reader = JsonReader.of(buffer(source(getNeverCompletingInputStream())));
         LottieResult<LottieComposition> task1 = LottieCompositionFactory.fromJsonReaderSync(reader, null);
         LottieResult<LottieComposition> task2 = LottieCompositionFactory.fromJsonReaderSync(reader, null);
         assertFalse(task1 == task2);
@@ -106,7 +107,7 @@
 
     @Test
     public void testCacheWorks() {
-        JsonReader reader = JsonReader.of(buffer(source(new StringInputStream(JSON))));
+        JsonReader reader = JsonReader.of(buffer(source(getNeverCompletingInputStream())));
         LottieTask<LottieComposition> task1 = LottieCompositionFactory.fromJsonReader(reader, "foo");
         LottieTask<LottieComposition> task2 = LottieCompositionFactory.fromJsonReader(reader, "foo");
         assertTrue(task1 == task2);
@@ -114,7 +115,7 @@
 
     @Test
     public void testZeroCacheWorks() {
-        JsonReader reader = JsonReader.of(buffer(source(new StringInputStream(JSON))));
+        JsonReader reader = JsonReader.of(buffer(source(getNeverCompletingInputStream())));
         LottieCompositionFactory.setMaxCacheSize(1);
         LottieResult<LottieComposition> taskFoo1 = LottieCompositionFactory.fromJsonReaderSync(reader, "foo");
         LottieResult<LottieComposition> taskBar = LottieCompositionFactory.fromJsonReaderSync(reader, "bar");
@@ -126,4 +127,12 @@
     public void testCannotSetCacheSizeToZero() {
         LottieCompositionFactory.setMaxCacheSize(0);
     }
+
+    private static InputStream getNeverCompletingInputStream() {
+        return new InputStream() {
+            @Override public int read() throws IOException {
+                return 100;
+            }
+        };
+    }
 }