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);