Pull out LottieComposition factory methods into inner class (#137)

This is part of issue #3. Pulled static factory methods out of LottieComposition into a static inner class. Made all fields final and initialized them all in the constructor.
This will make it easier for follow up refactors where we'll modularize the parsing logic and make it pluggable.
Also upgraded the Espresso tests to JUnit 4 so it no longer uses the deprecated ActivityInstrumentationTestCase2
diff --git a/LottieSample/build.gradle b/LottieSample/build.gradle
index 74ac10f..53f4ae6 100644
--- a/LottieSample/build.gradle
+++ b/LottieSample/build.gradle
@@ -10,6 +10,7 @@
         targetSdkVersion 25
         versionCode 3
         versionName "1.0.3"
+        testInstrumentationRunner "com.airbnb.lottie.TestRunner"
     }
     buildTypes {
         release {
diff --git a/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.java b/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.java
index dc1a246..4fead43 100644
--- a/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.java
+++ b/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.java
@@ -1,52 +1,55 @@
 package com.airbnb.lottie;
 
-
-import android.test.ActivityInstrumentationTestCase2;
+import android.support.test.filters.LargeTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
 
 import com.airbnb.lottie.samples.MainActivity;
 
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
 /**
- * Run these with: ./gradlew --daemon recordMode screenshotTests
- * If you run that command, it completes successfully, and nothing shows up in git, then you haven't broken anything!
+ * Run these with: ./gradlew recordMode screenshotTests
+ * If you run that command, it completes successfully, and nothing shows up in git, then you
+ * haven't broken anything!
  */
-public class LottieTest extends ActivityInstrumentationTestCase2<MainActivity> {
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class LottieTest {
+  @Rule public ActivityTestRule<MainActivity> activityRule = new ActivityTestRule<>(
+      MainActivity.class);
 
-  public LottieTest() {
-    super(MainActivity.class);
-  }
-
-  @Test
-  public void testAll() {
-    TestRobot.testAnimation(getActivity(), "9squares-AlBoardman.json");
-    TestRobot.testAnimation(getActivity(), "EmptyState.json");
-    TestRobot.testAnimation(getActivity(), "HamburgerArrow.json");
-    TestRobot.testAnimation(getActivity(), "LottieLogo1.json");
-    TestRobot.testAnimation(getActivity(), "LottieLogo2.json");
-    TestRobot.testAnimation(getActivity(), "MotionCorpse-Jrcanest.json");
-    TestRobot.testAnimation(getActivity(), "PinJump.json");
-    TestRobot.testAnimation(getActivity(), "TwitterHeart.json");
-    TestRobot.testAnimation(getActivity(), "Tests/Hosts.json");
-    TestRobot.testAnimation(getActivity(), "Tests/LightBulb.json", null,
+  @Test public void testAll() {
+    TestRobot.testAnimation(activityRule.getActivity(), "9squares-AlBoardman.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "EmptyState.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "HamburgerArrow.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "LottieLogo1.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "LottieLogo2.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "MotionCorpse-Jrcanest.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "PinJump.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "TwitterHeart.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "Tests/Hosts.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "Tests/LightBulb.json", null,
         new float[]{0f, 0.05f, 0.10f, 0.2f, 0.3f, 0.4f, 0.5f, 1f});
-    TestRobot.testAnimation(getActivity(), "Tests/LoopPlayOnce.json");
-    TestRobot.testAnimation(getActivity(), "Tests/Alarm.json");
-    TestRobot.testAnimation(getActivity(), "Tests/CheckSwitch.json");
-    TestRobot.testAnimation(getActivity(), "Tests/EllipseTrimPath.json");
-    TestRobot.testAnimation(getActivity(), "Tests/SplitDimensions.json");
-    TestRobot.testAnimation(getActivity(), "Tests/TrimPathsFull.json");
-    TestRobot.testAnimation(getActivity(), "Tests/Laugh4.json");
-    TestRobot.testAnimation(getActivity(), "Tests/Star.json");
-    TestRobot.testAnimation(getActivity(), "Tests/Polygon.json");
-    TestRobot.testAnimation(getActivity(), "Tests/AllSet.json");
-    TestRobot.testAnimation(getActivity(), "Tests/City.json");
-    TestRobot.testAnimation(getActivity(), "Tests/PreCompMadness.json");
-    TestRobot.testAnimation(getActivity(), "Tests/MatteParentPrecomp.json");
-    TestRobot.testAnimation(getActivity(), "Tests/Image.json", "Tests/weaccept");
-    TestRobot.testStatic(getActivity(), "Tests/TrimPathFill.json");
-    TestRobot.testStatic(getActivity(), "Tests/Mask_26.json");
-    TestRobot.testStatic(getActivity(), "Tests/MatteInv.json");
-    TestRobot.testStatic(getActivity(), "Tests/MaskInv.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "Tests/LoopPlayOnce.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "Tests/Alarm.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "Tests/CheckSwitch.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "Tests/EllipseTrimPath.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "Tests/SplitDimensions.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "Tests/TrimPathsFull.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "Tests/Laugh4.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "Tests/Star.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "Tests/Polygon.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "Tests/AllSet.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "Tests/City.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "Tests/PreCompMadness.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "Tests/MatteParentPrecomp.json");
+    TestRobot.testAnimation(activityRule.getActivity(), "Tests/Image.json", "Tests/weaccept");
+    TestRobot.testStatic(activityRule.getActivity(), "Tests/TrimPathFill.json");
+    TestRobot.testStatic(activityRule.getActivity(), "Tests/Mask_26.json");
+    TestRobot.testStatic(activityRule.getActivity(), "Tests/MatteInv.json");
+    TestRobot.testStatic(activityRule.getActivity(), "Tests/MaskInv.json");
   }
 }
diff --git a/LottieSample/src/androidTest/java/com/airbnb/lottie/TestRobot.java b/LottieSample/src/androidTest/java/com/airbnb/lottie/TestRobot.java
index 7c8046f..b1582fc 100644
--- a/LottieSample/src/androidTest/java/com/airbnb/lottie/TestRobot.java
+++ b/LottieSample/src/androidTest/java/com/airbnb/lottie/TestRobot.java
@@ -26,7 +26,7 @@
       float[] progress) {
     final LottieAnimationView view = new LottieAnimationView(activity);
     view.setImageAssetsFolder(imageAssetsFolder);
-    view.setComposition(LottieComposition.fromFileSync(activity, fileName));
+    view.setComposition(LottieComposition.Factory.fromFileSync(activity, fileName));
     ViewHelpers.setupView(view)
         .layout();
 
diff --git a/LottieSample/src/androidTest/java/com/airbnb/lottie/TestRunner.java b/LottieSample/src/androidTest/java/com/airbnb/lottie/TestRunner.java
index 2eaf80f..c4eb3a1 100644
--- a/LottieSample/src/androidTest/java/com/airbnb/lottie/TestRunner.java
+++ b/LottieSample/src/androidTest/java/com/airbnb/lottie/TestRunner.java
@@ -1,7 +1,18 @@
 package com.airbnb.lottie;
 
+import android.os.Bundle;
 import android.support.test.runner.AndroidJUnitRunner;
 
+import com.facebook.testing.screenshot.ScreenshotRunner;
 
 public class TestRunner extends AndroidJUnitRunner {
+  @Override public void onCreate(Bundle arguments) {
+    ScreenshotRunner.onCreate(this, arguments);
+    super.onCreate(arguments);
+  }
+
+  @Override public void finish(int resultCode, Bundle results) {
+    ScreenshotRunner.onDestroy();
+    super.finish(resultCode, results);
+  }
 }
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 e64cebb..44f55f5 100644
--- a/LottieSample/src/main/java/com/airbnb/lottie/samples/AnimationFragment.java
+++ b/LottieSample/src/main/java/com/airbnb/lottie/samples/AnimationFragment.java
@@ -28,6 +28,7 @@
 
 import com.airbnb.lottie.LottieAnimationView;
 import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.OnCompositionLoadedListener;
 
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -150,8 +151,8 @@
       case RC_ASSET:
         final String assetName = data.getStringExtra(EXTRA_ANIMATION_NAME);
         animationView.setImageAssetsFolder(assetFolders.get(assetName));
-        LottieComposition.fromAssetFileName(getContext(), assetName,
-            new LottieComposition.OnCompositionLoadedListener() {
+        LottieComposition.Factory.fromAssetFileName(getContext(), assetName,
+            new OnCompositionLoadedListener() {
               @Override
               public void onCompositionLoaded(LottieComposition composition) {
                 setComposition(composition, assetName);
@@ -288,8 +289,8 @@
       return;
     }
 
-    LottieComposition
-        .fromInputStream(getContext(), fis, new LottieComposition.OnCompositionLoadedListener() {
+    LottieComposition.Factory
+        .fromInputStream(getContext(), fis, new OnCompositionLoadedListener() {
           @Override
           public void onCompositionLoaded(LottieComposition composition) {
             setComposition(composition, uri.getPath());
@@ -324,8 +325,8 @@
 
         try {
           JSONObject json = new JSONObject(response.body().string());
-          LottieComposition
-              .fromJson(getResources(), json, new LottieComposition.OnCompositionLoadedListener() {
+          LottieComposition.Factory
+              .fromJson(getResources(), json, new OnCompositionLoadedListener() {
                 @Override
                 public void onCompositionLoaded(LottieComposition composition) {
                   setComposition(composition, "Network Animation");
diff --git a/LottieSample/src/main/java/com/airbnb/lottie/samples/LottieFontViewGroup.java b/LottieSample/src/main/java/com/airbnb/lottie/samples/LottieFontViewGroup.java
index 874fe27..b6c395f 100644
--- a/LottieSample/src/main/java/com/airbnb/lottie/samples/LottieFontViewGroup.java
+++ b/LottieSample/src/main/java/com/airbnb/lottie/samples/LottieFontViewGroup.java
@@ -14,6 +14,7 @@
 
 import com.airbnb.lottie.LottieAnimationView;
 import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.OnCompositionLoadedListener;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -43,8 +44,8 @@
 
   private void init() {
     setFocusableInTouchMode(true);
-    LottieComposition.fromAssetFileName(getContext(), "Mobilo/BlinkingCursor.json",
-        new LottieComposition.OnCompositionLoadedListener() {
+    LottieComposition.Factory.fromAssetFileName(getContext(), "Mobilo/BlinkingCursor.json",
+        new OnCompositionLoadedListener() {
           @Override
           public void onCompositionLoaded(LottieComposition composition) {
             cursorView = new LottieAnimationView(getContext());
@@ -180,8 +181,8 @@
     if (compositionMap.containsKey(fileName)) {
       addComposition(compositionMap.get(fileName));
     } else {
-      LottieComposition.fromAssetFileName(getContext(), fileName,
-          new LottieComposition.OnCompositionLoadedListener() {
+      LottieComposition.Factory.fromAssetFileName(getContext(), fileName,
+          new OnCompositionLoadedListener() {
             @Override
             public void onCompositionLoaded(LottieComposition composition) {
               compositionMap.put(fileName, composition);
diff --git a/README.md b/README.md
index 542b3db..20c04bc 100644
--- a/README.md
+++ b/README.md
@@ -102,7 +102,7 @@
 Under the hood, `LottieAnimationView` uses `LottieDrawable` to render its animations. If you need to, you can use the drawable form directly:
 ```java
 LottieDrawable drawable = new LottieDrawable();
-LottieComposition.fromAssetFileName(getContext(), "hello-world.json", (composition) -> {
+LottieComposition.Factory.fromAssetFileName(getContext(), "hello-world.json", (composition) -> {
     drawable.setComposition(composition);
 });
 ```
diff --git a/lottie/src/main/java/com/airbnb/lottie/Cancellable.java b/lottie/src/main/java/com/airbnb/lottie/Cancellable.java
new file mode 100644
index 0000000..6eeac5e
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/Cancellable.java
@@ -0,0 +1,5 @@
+package com.airbnb.lottie;
+
+interface Cancellable {
+  void cancel();
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/CompositionLoader.java b/lottie/src/main/java/com/airbnb/lottie/CompositionLoader.java
new file mode 100644
index 0000000..3036c03
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/CompositionLoader.java
@@ -0,0 +1,10 @@
+package com.airbnb.lottie;
+
+import android.os.AsyncTask;
+
+abstract class CompositionLoader<Params> extends AsyncTask<Params, Void, LottieComposition>
+    implements Cancellable {
+  @Override public void cancel() {
+    cancel(true);
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/FileCompositionLoader.java b/lottie/src/main/java/com/airbnb/lottie/FileCompositionLoader.java
new file mode 100644
index 0000000..33b69af
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/FileCompositionLoader.java
@@ -0,0 +1,23 @@
+package com.airbnb.lottie;
+
+import android.content.res.Resources;
+
+import java.io.InputStream;
+
+final class FileCompositionLoader extends CompositionLoader<InputStream> {
+  private final Resources res;
+  private final OnCompositionLoadedListener loadedListener;
+
+  FileCompositionLoader(Resources res, OnCompositionLoadedListener loadedListener) {
+    this.res = res;
+    this.loadedListener = loadedListener;
+  }
+
+  @Override protected LottieComposition doInBackground(InputStream... params) {
+    return LottieComposition.Factory.fromInputStream(res, params[0]);
+  }
+
+  @Override protected void onPostExecute(LottieComposition composition) {
+    loadedListener.onCompositionLoaded(composition);
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/JsonCompositionLoader.java b/lottie/src/main/java/com/airbnb/lottie/JsonCompositionLoader.java
new file mode 100644
index 0000000..25de60c
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/JsonCompositionLoader.java
@@ -0,0 +1,23 @@
+package com.airbnb.lottie;
+
+import android.content.res.Resources;
+
+import org.json.JSONObject;
+
+final class JsonCompositionLoader extends CompositionLoader<JSONObject> {
+  private final Resources res;
+  private final OnCompositionLoadedListener loadedListener;
+
+  JsonCompositionLoader(Resources res, OnCompositionLoadedListener loadedListener) {
+    this.res = res;
+    this.loadedListener = loadedListener;
+  }
+
+  @Override protected LottieComposition doInBackground(JSONObject... params) {
+    return LottieComposition.Factory.fromJsonSync(res, params[0]);
+  }
+
+  @Override protected void onPostExecute(LottieComposition composition) {
+    loadedListener.onCompositionLoaded(composition);
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
index 2c5dc8f..9196da0 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
@@ -55,8 +55,8 @@
   private static final Map<String, WeakReference<LottieComposition>> weakRefCache =
       new HashMap<>();
 
-  private final LottieComposition.OnCompositionLoadedListener loadedListener =
-      new LottieComposition.OnCompositionLoadedListener() {
+  private final OnCompositionLoadedListener loadedListener =
+      new OnCompositionLoadedListener() {
         @Override
         public void onCompositionLoaded(LottieComposition composition) {
           setComposition(composition);
@@ -67,7 +67,7 @@
   private final LottieDrawable lottieDrawable = new LottieDrawable();
   private String animationName;
 
-  @Nullable private LottieComposition.Cancellable compositionLoader;
+  @Nullable private Cancellable compositionLoader;
   /**
    * Can be null because it is created async
    */
@@ -205,8 +205,8 @@
     this.animationName = animationName;
     lottieDrawable.cancelAnimation();
     cancelLoaderTask();
-    compositionLoader = LottieComposition.fromAssetFileName(getContext(), animationName,
-        new LottieComposition.OnCompositionLoadedListener() {
+    compositionLoader = LottieComposition.Factory.fromAssetFileName(getContext(), animationName,
+        new OnCompositionLoadedListener() {
           @Override
           public void onCompositionLoaded(LottieComposition composition) {
             if (cacheStrategy == CacheStrategy.Strong) {
@@ -229,7 +229,7 @@
    */
   public void setAnimation(final JSONObject json) {
     cancelLoaderTask();
-    compositionLoader = LottieComposition.fromJson(getResources(), json, loadedListener);
+    compositionLoader = LottieComposition.Factory.fromJson(getResources(), json, loadedListener);
   }
 
   private void cancelLoaderTask() {
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java b/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
index 544b679..1497774 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
@@ -3,7 +3,6 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Rect;
-import android.os.AsyncTask;
 import android.support.annotation.Nullable;
 import android.support.v4.util.LongSparseArray;
 
@@ -18,6 +17,8 @@
 import java.util.List;
 import java.util.Map;
 
+import static com.airbnb.lottie.Utils.closeQuietly;
+
 /**
  * After Effects/Bodymovin composition model. This is the serialized model from which the
  * animation will be created.
@@ -25,14 +26,6 @@
  * {@link com.airbnb.lottie.LottieDrawable}.
  */
 public class LottieComposition {
-  public interface OnCompositionLoadedListener {
-    void onCompositionLoaded(LottieComposition composition);
-  }
-
-  interface Cancellable {
-    void cancel();
-  }
-
   /**
    * The largest bitmap drawing cache can be is 8,294,400 bytes. There are 4 bytes per pixel
    * leaving ~2.3M pixels available.
@@ -42,158 +35,25 @@
    */
   private static final int MAX_PIXELS = 1000;
 
-  /**
-   * Loads a composition from a file stored in /assets.
-   */
-  public static Cancellable fromAssetFileName(Context context, String fileName,
-      OnCompositionLoadedListener loadedListener) {
-    InputStream stream;
-    try {
-      stream = context.getAssets().open(fileName);
-    } catch (IOException e) {
-      throw new IllegalStateException("Unable to find file " + fileName, e);
-    }
-    return fromInputStream(context, stream, loadedListener);
-  }
-
-  /**
-   * Loads a composition from an arbitrary input stream.
-   * <p>
-   * ex: fromInputStream(context, new FileInputStream(filePath), (composition) -> {});
-   */
-  public static Cancellable fromInputStream(Context context, InputStream stream,
-      OnCompositionLoadedListener loadedListener) {
-    FileCompositionLoader loader =
-        new FileCompositionLoader(context.getResources(), loadedListener);
-    loader.execute(stream);
-    return loader;
-  }
-
-  static LottieComposition fromFileSync(Context context, String fileName) {
-    InputStream file;
-    try {
-      file = context.getAssets().open(fileName);
-    } catch (IOException e) {
-      throw new IllegalStateException("Unable to find file " + fileName, e);
-    }
-    return fromInputStream(context.getResources(), file);
-  }
-
-  /**
-   * Loads a composition from a raw json object. This is useful for animations loaded from the
-   * network.
-   */
-  public static Cancellable fromJson(Resources res, JSONObject json,
-      OnCompositionLoadedListener loadedListener) {
-    JsonCompositionLoader loader = new JsonCompositionLoader(res, loadedListener);
-    loader.execute(json);
-    return loader;
-  }
-
-  @SuppressWarnings("WeakerAccess")
-  static LottieComposition fromInputStream(Resources res, InputStream file) {
-    try {
-      int size = file.available();
-      byte[] buffer = new byte[size];
-      //noinspection ResultOfMethodCallIgnored
-      file.read(buffer);
-      file.close();
-      String json = new String(buffer, "UTF-8");
-
-      JSONObject jsonObject = new JSONObject(json);
-      return LottieComposition.fromJsonSync(res, jsonObject);
-    } catch (IOException e) {
-      throw new IllegalStateException("Unable to find file.", e);
-    } catch (JSONException e) {
-      throw new IllegalStateException("Unable to load JSON.", e);
-    }
-  }
-
-  @SuppressWarnings("WeakerAccess")
-  static LottieComposition fromJsonSync(Resources res, JSONObject json) {
-    LottieComposition composition = new LottieComposition(res);
-
-    int width = json.optInt("w", -1);
-    int height = json.optInt("h", -1);
-    if (width != -1 && height != -1) {
-      int scaledWidth = (int) (width * composition.scale);
-      int scaledHeight = (int) (height * composition.scale);
-      if (Math.max(scaledWidth, scaledHeight) > MAX_PIXELS) {
-        float factor = (float) MAX_PIXELS / (float) Math.max(scaledWidth, scaledHeight);
-        scaledWidth *= factor;
-        scaledHeight *= factor;
-        composition.scale *= factor;
-      }
-      composition.bounds = new Rect(0, 0, scaledWidth, scaledHeight);
-    }
-
-    composition.startFrame = json.optLong("ip", 0);
-    composition.endFrame = json.optLong("op", 0);
-    composition.frameRate = json.optInt("fr", 0);
-
-    JSONArray jsonLayers = json.optJSONArray("layers");
-    for (int i = 0; i < jsonLayers.length(); i++) {
-      Layer layer = new Layer(jsonLayers.optJSONObject(i), composition);
-      addLayer(composition, layer);
-    }
-
-    JSONArray assetsJson = json.optJSONArray("assets");
-    parsePrecomps(composition, assetsJson);
-    parseImages(composition, assetsJson);
-
-    return composition;
-  }
-
-  private static void parsePrecomps(LottieComposition composition, JSONArray assetsJson) {
-    for (int i = 0; i < assetsJson.length(); i++) {
-      JSONObject assetJson = assetsJson.optJSONObject(i);
-      JSONArray layersJson = assetJson.optJSONArray("layers");
-      if (layersJson == null) {
-        continue;
-      }
-      List<Layer> layers = new ArrayList<>(layersJson.length());
-      LongSparseArray<Layer> layerMap = new LongSparseArray<>();
-      for (int j = 0; j < layersJson.length(); j++) {
-        Layer layer = new Layer(layersJson.optJSONObject(j), composition);
-        layerMap.put(layer.getId(), layer);
-        layers.add(layer);
-      }
-      String id = assetJson.optString("id");
-      composition.precomps.put(id, layers);
-    }
-  }
-
-  private static void parseImages(LottieComposition composition, JSONArray assetsJson) {
-    for (int i = 0; i < assetsJson.length(); i++) {
-      JSONObject assetJson = assetsJson.optJSONObject(i);
-      if (!assetJson.has("p")) {
-        continue;
-      }
-      ImageAsset image = new ImageAsset(assetJson);
-      composition.images.put(image.getId(), image);
-    }
-  }
-
-  private static void addLayer(LottieComposition composition, Layer layer) {
-    composition.layers.add(layer);
-    composition.layerMap.put(layer.getId(), layer);
-  }
-
   private final Map<String, List<Layer>> precomps = new HashMap<>();
   private final Map<String, ImageAsset> images = new HashMap<>();
   private final LongSparseArray<Layer> layerMap = new LongSparseArray<>();
   private final List<Layer> layers = new ArrayList<>();
-  private Rect bounds;
-  private long startFrame;
-  private long endFrame;
-  private int frameRate;
-  private float scale;
+  private final Rect bounds;
+  private final long startFrame;
+  private final long endFrame;
+  private final int frameRate;
+  private final float scale;
 
-  private LottieComposition(Resources res) {
-    scale = res.getDisplayMetrics().density;
+  private LottieComposition(Rect bounds, long startFrame, long endFrame, int frameRate,
+      float scale) {
+    this.bounds = bounds;
+    this.startFrame = startFrame;
+    this.endFrame = endFrame;
+    this.frameRate = frameRate;
+    this.scale = scale;
   }
 
-
   Layer layerModelForId(long id) {
     return layerMap.get(id);
   }
@@ -245,54 +105,150 @@
     return sb.toString();
   }
 
-  private static final class FileCompositionLoader extends CompositionLoader<InputStream> {
-
-    private final Resources res;
-    private final OnCompositionLoadedListener loadedListener;
-
-    FileCompositionLoader(Resources res, OnCompositionLoadedListener loadedListener) {
-      this.res = res;
-      this.loadedListener = loadedListener;
+  public static class Factory {
+    /**
+     * Loads a composition from a file stored in /assets.
+     */
+    public static Cancellable fromAssetFileName(Context context, String fileName,
+        OnCompositionLoadedListener loadedListener) {
+      InputStream stream;
+      try {
+        stream = context.getAssets().open(fileName);
+      } catch (IOException e) {
+        throw new IllegalStateException("Unable to find file " + fileName, e);
+      }
+      return fromInputStream(context, stream, loadedListener);
     }
 
-    @Override
-    protected LottieComposition doInBackground(InputStream... params) {
-      return fromInputStream(res, params[0]);
+    /**
+     * Loads a composition from an arbitrary input stream.
+     * <p>
+     * ex: fromInputStream(context, new FileInputStream(filePath), (composition) -> {});
+     */
+    public static Cancellable fromInputStream(Context context, InputStream stream,
+        OnCompositionLoadedListener loadedListener) {
+      FileCompositionLoader loader =
+          new FileCompositionLoader(context.getResources(), loadedListener);
+      loader.execute(stream);
+      return loader;
     }
 
-    @Override
-    protected void onPostExecute(LottieComposition composition) {
-      loadedListener.onCompositionLoaded(composition);
-    }
-  }
-
-  private static final class JsonCompositionLoader extends CompositionLoader<JSONObject> {
-
-    private final Resources res;
-    private final OnCompositionLoadedListener loadedListener;
-
-    JsonCompositionLoader(Resources res, OnCompositionLoadedListener loadedListener) {
-      this.res = res;
-      this.loadedListener = loadedListener;
+    static LottieComposition fromFileSync(Context context, String fileName) {
+      InputStream file;
+      try {
+        file = context.getAssets().open(fileName);
+      } catch (IOException e) {
+        throw new IllegalStateException("Unable to find file " + fileName, e);
+      }
+      return fromInputStream(context.getResources(), file);
     }
 
-    @Override
-    protected LottieComposition doInBackground(JSONObject... params) {
-      return fromJsonSync(res, params[0]);
+    /**
+     * Loads a composition from a raw json object. This is useful for animations loaded from the
+     * network.
+     */
+    public static Cancellable fromJson(Resources res, JSONObject json,
+        OnCompositionLoadedListener loadedListener) {
+      JsonCompositionLoader loader = new JsonCompositionLoader(res, loadedListener);
+      loader.execute(json);
+      return loader;
     }
 
-    @Override
-    protected void onPostExecute(LottieComposition composition) {
-      loadedListener.onCompositionLoaded(composition);
+    @SuppressWarnings("WeakerAccess")
+    static LottieComposition fromInputStream(Resources res, InputStream stream) {
+      try {
+        // TODO: It's not correct to use available() to allocate the byte array.
+        int size = stream.available();
+        byte[] buffer = new byte[size];
+        //noinspection ResultOfMethodCallIgnored
+        stream.read(buffer);
+        String json = new String(buffer, "UTF-8");
+        JSONObject jsonObject = new JSONObject(json);
+        return fromJsonSync(res, jsonObject);
+      } catch (IOException e) {
+        throw new IllegalStateException("Unable to find file.", e);
+      } catch (JSONException e) {
+        throw new IllegalStateException("Unable to load JSON.", e);
+      } finally {
+        closeQuietly(stream);
+      }
     }
-  }
 
-  private abstract static class CompositionLoader<Params>
-      extends AsyncTask<Params, Void, LottieComposition>
-      implements Cancellable {
+    @SuppressWarnings("WeakerAccess")
+    static LottieComposition fromJsonSync(Resources res, JSONObject json) {
+      Rect bounds = null;
+      float scale = res.getDisplayMetrics().density;
+      int width = json.optInt("w", -1);
+      int height = json.optInt("h", -1);
 
-    @Override public void cancel() {
-      cancel(true);
+      if (width != -1 && height != -1) {
+        int scaledWidth = (int) (width * scale);
+        int scaledHeight = (int) (height * scale);
+        if (Math.max(scaledWidth, scaledHeight) > MAX_PIXELS) {
+          float factor = (float) MAX_PIXELS / (float) Math.max(scaledWidth, scaledHeight);
+          scaledWidth *= factor;
+          scaledHeight *= factor;
+          scale *= factor;
+        }
+        bounds = new Rect(0, 0, scaledWidth, scaledHeight);
+      }
+
+      long startFrame = json.optLong("ip", 0);
+      long endFrame = json.optLong("op", 0);
+      int frameRate = json.optInt("fr", 0);
+      LottieComposition composition =
+          new LottieComposition(bounds, startFrame, endFrame, frameRate, scale);
+      JSONArray assetsJson = json.optJSONArray("assets");
+      parseImages(assetsJson, composition);
+      parsePrecomps(assetsJson, composition);
+      parseLayers(json, composition);
+      return composition;
+    }
+
+    private static void parseLayers(JSONObject json, LottieComposition composition) {
+      JSONArray jsonLayers = json.optJSONArray("layers");
+      int length = jsonLayers.length();
+      for (int i = 0; i < length; i++) {
+        Layer layer = new Layer(jsonLayers.optJSONObject(i), composition);
+        addLayer(composition.layers, composition.layerMap, layer);
+      }
+    }
+
+    private static void parsePrecomps(JSONArray assetsJson, LottieComposition composition) {
+      int length = assetsJson.length();
+      for (int i = 0; i < length; i++) {
+        JSONObject assetJson = assetsJson.optJSONObject(i);
+        JSONArray layersJson = assetJson.optJSONArray("layers");
+        if (layersJson == null) {
+          continue;
+        }
+        List<Layer> layers = new ArrayList<>(layersJson.length());
+        LongSparseArray<Layer> layerMap = new LongSparseArray<>();
+        for (int j = 0; j < layersJson.length(); j++) {
+          Layer layer = new Layer(layersJson.optJSONObject(j), composition);
+          layerMap.put(layer.getId(), layer);
+          layers.add(layer);
+        }
+        String id = assetJson.optString("id");
+        composition.precomps.put(id, layers);
+      }
+    }
+
+    private static void parseImages(JSONArray assetsJson, LottieComposition composition) {
+      int length = assetsJson.length();
+      for (int i = 0; i < length; i++) {
+        JSONObject assetJson = assetsJson.optJSONObject(i);
+        if (!assetJson.has("p")) {
+          continue;
+        }
+        ImageAsset image = new ImageAsset(assetJson);
+        composition.images.put(image.getId(), image);
+      }
+    }
+
+    private static void addLayer(List<Layer> layers, LongSparseArray<Layer> layerMap, Layer layer) {
+      layers.add(layer);
+      layerMap.put(layer.getId(), layer);
     }
   }
 }
\ No newline at end of file
diff --git a/lottie/src/main/java/com/airbnb/lottie/OnCompositionLoadedListener.java b/lottie/src/main/java/com/airbnb/lottie/OnCompositionLoadedListener.java
new file mode 100644
index 0000000..92d3c47
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/OnCompositionLoadedListener.java
@@ -0,0 +1,5 @@
+package com.airbnb.lottie;
+
+public interface OnCompositionLoadedListener {
+  void onCompositionLoaded(LottieComposition composition);
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/Utils.java b/lottie/src/main/java/com/airbnb/lottie/Utils.java
index 4a8dd2d..789d410 100644
--- a/lottie/src/main/java/com/airbnb/lottie/Utils.java
+++ b/lottie/src/main/java/com/airbnb/lottie/Utils.java
@@ -3,6 +3,8 @@
 import android.graphics.Path;
 import android.graphics.PointF;
 
+import java.io.Closeable;
+
 final class Utils {
   private static PointF emptyPoint;
 
@@ -27,4 +29,15 @@
     }
     return path;
   }
+
+  static void closeQuietly(Closeable closeable) {
+    if (closeable != null) {
+      try {
+        closeable.close();
+      } catch (RuntimeException rethrown) {
+        throw rethrown;
+      } catch (Exception ignored) {
+      }
+    }
+  }
 }