Add an image asset delegate

This allows the user to provide their own bitmaps from wherever
they need to retrieve them from.
Fixes #177
diff --git a/LottieSample/src/main/java/com/airbnb/lottie/samples/AnimationFragment.java b/LottieSample/src/main/java/com/airbnb/lottie/samples/AnimationFragment.java
index acb6172..1087bfc 100644
--- a/LottieSample/src/main/java/com/airbnb/lottie/samples/AnimationFragment.java
+++ b/LottieSample/src/main/java/com/airbnb/lottie/samples/AnimationFragment.java
@@ -6,6 +6,8 @@
 import android.app.AlertDialog;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
@@ -26,8 +28,10 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+import com.airbnb.lottie.ImageAssetDelegate;
 import com.airbnb.lottie.LottieAnimationView;
 import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.LottieImageAsset;
 import com.airbnb.lottie.OnCompositionLoadedListener;
 
 import org.json.JSONException;
@@ -150,7 +154,23 @@
     switch (requestCode) {
       case RC_ASSET:
         final String assetName = data.getStringExtra(EXTRA_ANIMATION_NAME);
-        animationView.setImageAssetsFolder(assetFolders.get(assetName));
+        // animationView.setImageAssetsFolder(assetFolders.get(assetName));
+        animationView.setImageAssetDelegate(new ImageAssetDelegate() {
+          @Override public Bitmap fetchBitmap(LottieImageAsset asset) {
+            InputStream is;
+            try {
+              is = getContext().getAssets().open("Images/WeAccept/" + asset.getFileName());
+            } catch (IOException e) {
+              Log.w("Gabe", "Unable to open asset.", e);
+              return null;
+            }
+            BitmapFactory.Options opts = new BitmapFactory.Options();
+            opts.inScaled = true;
+            opts.inDensity = 160;
+            Bitmap bitmap = BitmapFactory.decodeStream(is, null, opts);
+            return bitmap;
+          }
+        });
         LottieComposition.Factory.fromAssetFileName(getContext(), assetName,
             new OnCompositionLoadedListener() {
               @Override
diff --git a/lottie/src/main/java/com/airbnb/lottie/ImageAsset.java b/lottie/src/main/java/com/airbnb/lottie/ImageAsset.java
deleted file mode 100644
index de90a51..0000000
--- a/lottie/src/main/java/com/airbnb/lottie/ImageAsset.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.airbnb.lottie;
-
-import org.json.JSONObject;
-
-class ImageAsset {
-  private final int width;
-  private final int height;
-  private final String id;
-  private final String fileName;
-
-  private ImageAsset(int width, int height, String id, String fileName) {
-    this.width = width;
-    this.height = height;
-    this.id = id;
-    this.fileName = fileName;
-  }
-
-  static class Factory {
-    private Factory() {
-    }
-
-    static ImageAsset newInstance(JSONObject imageJson) {
-      return new ImageAsset(imageJson.optInt("w"), imageJson.optInt("h"), imageJson.optString("id"),
-          imageJson.optString("p"));
-    }
-  }
-
-  int getWidth() {
-    return width;
-  }
-
-  int getHeight() {
-    return height;
-  }
-
-  String getId() {
-    return id;
-  }
-
-  String getFileName() {
-    return fileName;
-  }
-}
diff --git a/lottie/src/main/java/com/airbnb/lottie/ImageAssetBitmapManager.java b/lottie/src/main/java/com/airbnb/lottie/ImageAssetBitmapManager.java
index 26c7e0e..fe38da3 100644
--- a/lottie/src/main/java/com/airbnb/lottie/ImageAssetBitmapManager.java
+++ b/lottie/src/main/java/com/airbnb/lottie/ImageAssetBitmapManager.java
@@ -4,6 +4,7 @@
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.View;
@@ -19,19 +20,17 @@
 class ImageAssetBitmapManager {
   private final Context context;
   private String imagesFolder;
-  private final Map<String, ImageAsset> imageAssets;
+  @Nullable private ImageAssetDelegate assetDelegate;
+  private final Map<String, LottieImageAsset> imageAssets;
   private final Map<String, Bitmap> bitmaps = new HashMap<>();
 
   ImageAssetBitmapManager(Drawable.Callback callback, String imagesFolder,
-      Map<String, ImageAsset> imageAssets) {
+      ImageAssetDelegate assetDelegate, Map<String, LottieImageAsset> imageAssets) {
     assertNotNull(callback);
 
-    if (TextUtils.isEmpty(imagesFolder)) {
-      throw new IllegalStateException("You must specify an image assets folder by calling " +
-          "setImageAssetsFolder on LottieAnimationView or LottieDrawable.");
-    }
     this.imagesFolder = imagesFolder;
-    if (this.imagesFolder.charAt(this.imagesFolder.length() - 1) != '/') {
+    if (!TextUtils.isEmpty(imagesFolder) &&
+        this.imagesFolder.charAt(this.imagesFolder.length() - 1) != '/') {
       this.imagesFolder += '/';
     }
 
@@ -44,15 +43,26 @@
 
     context = ((View) callback).getContext();
     this.imageAssets = imageAssets;
+    setAssetDelegate(assetDelegate);
+  }
+
+  void setAssetDelegate(@Nullable ImageAssetDelegate assetDelegate) {
+    this.assetDelegate = assetDelegate;
   }
 
   Bitmap bitmapForId(String id) {
     Bitmap bitmap = bitmaps.get(id);
     if (bitmap == null) {
-      ImageAsset imageAsset = imageAssets.get(id);
+      LottieImageAsset imageAsset = imageAssets.get(id);
       if (imageAsset == null) {
         return null;
       }
+      if (assetDelegate != null) {
+        bitmap = assetDelegate.fetchBitmap(imageAsset);
+        bitmaps.put(id, bitmap);
+        return bitmap;
+      }
+
       InputStream is;
       try {
         if (TextUtils.isEmpty(imagesFolder)) {
diff --git a/lottie/src/main/java/com/airbnb/lottie/ImageAssetDelegate.java b/lottie/src/main/java/com/airbnb/lottie/ImageAssetDelegate.java
new file mode 100644
index 0000000..ed46ee7
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/ImageAssetDelegate.java
@@ -0,0 +1,12 @@
+package com.airbnb.lottie;
+
+import android.graphics.Bitmap;
+
+/**
+ * Delegate to handle the loading of bitmaps that are not packaged in the assets of your app.
+ *
+ * @see LottieDrawable#setImageAssetDelegate(ImageAssetDelegate)
+ */
+public interface ImageAssetDelegate {
+  Bitmap fetchBitmap(LottieImageAsset asset);
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
index 7f0fd5f..495efa1 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
@@ -360,6 +360,15 @@
     lottieDrawable.setSpeed(speed);
   }
 
+  /**
+   * Use this if you can't bundle images with your app. This may be useful if you download the
+   * animations from the network or have the images saved to an SD Card. In that case, Lottie
+   * will defer the loading of the bitmap to this delegate.
+   */
+  public void setImageAssetDelegate(ImageAssetDelegate assetDelegate) {
+    lottieDrawable.setImageAssetDelegate(assetDelegate);
+  }
+
   void setScale(float scale) {
     lottieDrawable.setScale(scale);
     setImageDrawable(null);
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java b/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
index 6d23f5e..161c727 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
@@ -29,7 +29,7 @@
 public class LottieComposition {
 
   private final Map<String, List<Layer>> precomps = new HashMap<>();
-  private final Map<String, ImageAsset> images = new HashMap<>();
+  private final Map<String, LottieImageAsset> images = new HashMap<>();
   private final LongSparseArray<Layer> layerMap = new LongSparseArray<>();
   private final List<Layer> layers = new ArrayList<>();
   private final Rect bounds;
@@ -77,7 +77,7 @@
     return !images.isEmpty();
   }
 
-  Map<String, ImageAsset> getImages() {
+  Map<String, LottieImageAsset> getImages() {
     return images;
   }
 
@@ -239,7 +239,7 @@
         if (!assetJson.has("p")) {
           continue;
         }
-        ImageAsset image = ImageAsset.Factory.newInstance(assetJson);
+        LottieImageAsset image = LottieImageAsset.Factory.newInstance(assetJson);
         composition.images.put(image.getId(), image);
       }
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
index 03dbf82..e8b9519 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
@@ -32,6 +32,7 @@
 
   @Nullable private ImageAssetBitmapManager imageAssetBitmapManager;
   @Nullable private String imageAssetsFolder;
+  @Nullable private ImageAssetDelegate imageAssetDelegate;
   private boolean playAnimationWhenLayerAdded;
   private boolean reverseAnimationWhenLayerAdded;
   private boolean systemAnimationsAreDisabled;
@@ -278,6 +279,19 @@
     updateBounds();
   }
 
+  /**
+   * Use this if you can't bundle images with your app. This may be useful if you download the
+   * animations from the network or have the images saved to an SD Card. In that case, Lottie
+   * will defer the loading of the bitmap to this delegate.
+   */
+  @SuppressWarnings({"unused", "WeakerAccess"}) public void setImageAssetDelegate(
+      @SuppressWarnings("NullableProblems") ImageAssetDelegate assetDelegate) {
+    this.imageAssetDelegate = assetDelegate;
+    if (imageAssetBitmapManager != null) {
+      imageAssetBitmapManager.setAssetDelegate(assetDelegate);
+    }
+  }
+
   @SuppressWarnings("WeakerAccess") public float getScale() {
     return scale;
   }
@@ -345,7 +359,7 @@
 
     if (imageAssetBitmapManager == null) {
       imageAssetBitmapManager = new ImageAssetBitmapManager(getCallback(),
-          imageAssetsFolder, composition.getImages());
+          imageAssetsFolder, imageAssetDelegate, composition.getImages());
     }
 
     return imageAssetBitmapManager;
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieImageAsset.java b/lottie/src/main/java/com/airbnb/lottie/LottieImageAsset.java
new file mode 100644
index 0000000..2cfb2f5
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieImageAsset.java
@@ -0,0 +1,46 @@
+package com.airbnb.lottie;
+
+import org.json.JSONObject;
+
+/**
+ * Data class describing an image asset exported by bodymovin.
+ */
+public class LottieImageAsset {
+  private final int width;
+  private final int height;
+  private final String id;
+  private final String fileName;
+
+  private LottieImageAsset(int width, int height, String id, String fileName) {
+    this.width = width;
+    this.height = height;
+    this.id = id;
+    this.fileName = fileName;
+  }
+
+  static class Factory {
+    private Factory() {
+    }
+
+    static LottieImageAsset newInstance(JSONObject imageJson) {
+      return new LottieImageAsset(imageJson.optInt("w"), imageJson.optInt("h"), imageJson.optString("id"),
+          imageJson.optString("p"));
+    }
+  }
+
+  public int getWidth() {
+    return width;
+  }
+
+  public int getHeight() {
+    return height;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  public String getFileName() {
+    return fileName;
+  }
+}