Add support for dotLottie  (#1660)

Fix parsing .lottie files
the dotLottie format  (dotlottie.io) is a zip file with animation.json bundled along with image resources, and manifest

**Changes**
* update zipStreamSync() to handle dotLottie use case
  * ignore manifest.json otherwise lottie will try to use it

* update network loader to check for .lottie and treat it as a .zip

* add support for .lottie and in rawRes
  * use magic header to determine files and use zipStreamSync() instead of fromJsonSync()
  * this has the bonus side effect of allowing .zip files in rawRes as well (which doesn't appear to have been supported before)
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieCompositionFactory.java b/lottie/src/main/java/com/airbnb/lottie/LottieCompositionFactory.java
index 0c686f7..344bf98 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieCompositionFactory.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieCompositionFactory.java
@@ -9,7 +9,7 @@
 import com.airbnb.lottie.model.LottieCompositionCache;
 import com.airbnb.lottie.parser.LottieCompositionMoshiParser;
 import com.airbnb.lottie.parser.moshi.JsonReader;
-
+import com.airbnb.lottie.utils.Logger;
 import com.airbnb.lottie.utils.Utils;
 
 import org.json.JSONObject;
@@ -27,8 +27,10 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RawRes;
 import androidx.annotation.WorkerThread;
+import okio.BufferedSource;
+import okio.Okio;
 
-import static com.airbnb.lottie.parser.moshi.JsonReader.*;
+import static com.airbnb.lottie.parser.moshi.JsonReader.of;
 import static com.airbnb.lottie.utils.Utils.closeQuietly;
 import static okio.Okio.buffer;
 import static okio.Okio.source;
@@ -49,6 +51,13 @@
    */
   private static final Map<String, LottieTask<LottieComposition>> taskCache = new HashMap<>();
 
+  /**
+   * reference magic bytes for zip compressed files.
+   * useful to determine if an InputStream is a zip file or not
+   */
+  private static final byte[] MAGIC = new byte[] { 0x50, 0x4b, 0x03, 0x04 };
+
+
   private LottieCompositionFactory() {
   }
 
@@ -181,7 +190,7 @@
   @WorkerThread
   public static LottieResult<LottieComposition> fromAssetSync(Context context, String fileName, @Nullable String cacheKey) {
     try {
-      if (fileName.endsWith(".zip")) {
+      if (fileName.endsWith(".zip") || fileName.endsWith(".lottie")) {
         return fromZipStreamSync(new ZipInputStream(context.getAssets().open(fileName)), cacheKey);
       }
       return fromJsonInputStreamSync(context.getAssets().open(fileName), cacheKey);
@@ -253,7 +262,11 @@
   @WorkerThread
   public static LottieResult<LottieComposition> fromRawResSync(Context context, @RawRes int rawRes, @Nullable String cacheKey) {
     try {
-      return fromJsonInputStreamSync(context.getResources().openRawResource(rawRes), cacheKey);
+      BufferedSource source = Okio.buffer(source(context.getResources().openRawResource(rawRes)));
+      if (isZipCompressed(source)) {
+        return fromZipStreamSync(new ZipInputStream(source.inputStream()), cacheKey);
+      }
+      return fromJsonInputStreamSync(source.inputStream(), cacheKey);
     } catch (Resources.NotFoundException e) {
       return new LottieResult<>(e);
     }
@@ -423,6 +436,8 @@
         final String entryName = entry.getName();
         if (entryName.contains("__MACOSX")) {
           inputStream.closeEntry();
+        } else if (entry.getName().equalsIgnoreCase("manifest.json")) { //ignore .lottie manifest
+          inputStream.closeEntry();
         } else if (entry.getName().contains(".json")) {
           com.airbnb.lottie.parser.moshi.JsonReader reader = of(buffer(source(inputStream)));
           composition = LottieCompositionFactory.fromJsonReaderSyncInternal(reader, null, false).getValue();
@@ -465,6 +480,26 @@
     return new LottieResult<>(composition);
   }
 
+  /**
+   * Check if a given InputStream points to a .zip compressed file
+   */
+  private static Boolean isZipCompressed(BufferedSource inputSource) {
+
+    try {
+      BufferedSource peek = inputSource.peek();
+      for (byte b: MAGIC) {
+        if(peek.readByte() != b)
+          return false;
+      }
+      peek.close();
+      return true;
+    } catch (Exception e) {
+      Logger.error("Failed to check zip file header", e);
+      return false;
+    }
+
+  }
+
   @Nullable
   private static LottieImageAsset findImageAssetForFileName(LottieComposition composition, String fileName) {
     for (LottieImageAsset asset : composition.getImages().values()) {
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 21fbc34..75a8ab6 100644
--- a/lottie/src/main/java/com/airbnb/lottie/network/NetworkFetcher.java
+++ b/lottie/src/main/java/com/airbnb/lottie/network/NetworkFetcher.java
@@ -108,7 +108,7 @@
       // in the result which is more useful than failing here.
       contentType = "application/json";
     }
-    if (contentType.contains("application/zip")) {
+    if (contentType.contains("application/zip") || url.split("\\?")[0].endsWith(".lottie")) {
       Logger.debug("Handling zip response.");
       extension = FileExtension.ZIP;
       result = fromZipStream(url, inputStream, cacheKey);