1 package com.airbnb.lottie.network; 2 3 import android.content.Context; 4 import android.util.Pair; 5 6 import androidx.annotation.NonNull; 7 import androidx.annotation.Nullable; 8 import androidx.annotation.RestrictTo; 9 import androidx.annotation.WorkerThread; 10 11 import com.airbnb.lottie.LottieComposition; 12 import com.airbnb.lottie.LottieCompositionFactory; 13 import com.airbnb.lottie.LottieResult; 14 import com.airbnb.lottie.utils.Logger; 15 16 import java.io.File; 17 import java.io.FileInputStream; 18 import java.io.IOException; 19 import java.io.InputStream; 20 import java.util.zip.GZIPInputStream; 21 import java.util.zip.ZipInputStream; 22 23 @RestrictTo(RestrictTo.Scope.LIBRARY) 24 public class NetworkFetcher { 25 26 @Nullable 27 private final NetworkCache networkCache; 28 @NonNull 29 private final LottieNetworkFetcher fetcher; 30 NetworkFetcher(@ullable NetworkCache networkCache, @NonNull LottieNetworkFetcher fetcher)31 public NetworkFetcher(@Nullable NetworkCache networkCache, @NonNull LottieNetworkFetcher fetcher) { 32 this.networkCache = networkCache; 33 this.fetcher = fetcher; 34 } 35 36 @NonNull 37 @WorkerThread fetchSync(Context context, @NonNull String url, @Nullable String cacheKey)38 public LottieResult<LottieComposition> fetchSync(Context context, @NonNull String url, @Nullable String cacheKey) { 39 LottieComposition result = fetchFromCache(context, url, cacheKey); 40 if (result != null) { 41 return new LottieResult<>(result); 42 } 43 44 Logger.debug("Animation for " + url + " not found in cache. Fetching from network."); 45 46 return fetchFromNetwork(context, url, cacheKey); 47 } 48 49 @Nullable 50 @WorkerThread fetchFromCache(Context context, @NonNull String url, @Nullable String cacheKey)51 private LottieComposition fetchFromCache(Context context, @NonNull String url, @Nullable String cacheKey) { 52 if (cacheKey == null || networkCache == null) { 53 return null; 54 } 55 Pair<FileExtension, InputStream> cacheResult = networkCache.fetch(url); 56 if (cacheResult == null) { 57 return null; 58 } 59 60 FileExtension extension = cacheResult.first; 61 InputStream inputStream = cacheResult.second; 62 LottieResult<LottieComposition> result; 63 switch (extension) { 64 case ZIP: 65 result = LottieCompositionFactory.fromZipStreamSync(context, new ZipInputStream(inputStream), cacheKey); 66 break; 67 case GZIP: 68 try { 69 result = LottieCompositionFactory.fromJsonInputStreamSync(new GZIPInputStream(inputStream), cacheKey); 70 } catch (IOException e) { 71 result = new LottieResult<>(e); 72 } 73 break; 74 default: 75 result = LottieCompositionFactory.fromJsonInputStreamSync(inputStream, cacheKey); 76 } 77 if (result.getValue() != null) { 78 return result.getValue(); 79 } 80 return null; 81 } 82 83 @NonNull 84 @WorkerThread fetchFromNetwork(Context context, @NonNull String url, @Nullable String cacheKey)85 private LottieResult<LottieComposition> fetchFromNetwork(Context context, @NonNull String url, @Nullable String cacheKey) { 86 Logger.debug("Fetching " + url); 87 88 LottieFetchResult fetchResult = null; 89 try { 90 fetchResult = fetcher.fetchSync(url); 91 if (fetchResult.isSuccessful()) { 92 InputStream inputStream = fetchResult.bodyByteStream(); 93 String contentType = fetchResult.contentType(); 94 LottieResult<LottieComposition> result = fromInputStream(context, url, inputStream, contentType, cacheKey); 95 Logger.debug("Completed fetch from network. Success: " + (result.getValue() != null)); 96 return result; 97 } else { 98 return new LottieResult<>(new IllegalArgumentException(fetchResult.error())); 99 } 100 } catch (Exception e) { 101 return new LottieResult<>(e); 102 } finally { 103 if (fetchResult != null) { 104 try { 105 fetchResult.close(); 106 } catch (IOException e) { 107 Logger.warning("LottieFetchResult close failed ", e); 108 } 109 } 110 } 111 } 112 113 @NonNull fromInputStream(Context context, @NonNull String url, @NonNull InputStream inputStream, @Nullable String contentType, @Nullable String cacheKey)114 private LottieResult<LottieComposition> fromInputStream(Context context, @NonNull String url, @NonNull InputStream inputStream, @Nullable String contentType, 115 @Nullable String cacheKey) throws IOException { 116 FileExtension extension; 117 LottieResult<LottieComposition> result; 118 if (contentType == null) { 119 // Assume JSON for best effort parsing. If it fails, it will just deliver the parse exception 120 // in the result which is more useful than failing here. 121 contentType = "application/json"; 122 } 123 if (contentType.contains("application/zip") || 124 contentType.contains("application/x-zip") || 125 contentType.contains("application/x-zip-compressed") || 126 url.split("\\?")[0].endsWith(".lottie")) { 127 Logger.debug("Handling zip response."); 128 extension = FileExtension.ZIP; 129 result = fromZipStream(context, url, inputStream, cacheKey); 130 } else if (contentType.contains("application/gzip") || 131 contentType.contains("application/x-gzip") || 132 url.split("\\?")[0].endsWith(".tgs")) { 133 Logger.debug("Handling gzip response."); 134 extension = FileExtension.GZIP; 135 result = fromGzipStream(url, inputStream, cacheKey); 136 } else { 137 Logger.debug("Received json response."); 138 extension = FileExtension.JSON; 139 result = fromJsonStream(url, inputStream, cacheKey); 140 } 141 142 if (cacheKey != null && result.getValue() != null && networkCache != null) { 143 networkCache.renameTempFile(url, extension); 144 } 145 146 return result; 147 } 148 149 @NonNull fromZipStream(Context context, @NonNull String url, @NonNull InputStream inputStream, @Nullable String cacheKey)150 private LottieResult<LottieComposition> fromZipStream(Context context, @NonNull String url, @NonNull InputStream inputStream, @Nullable String cacheKey) 151 throws IOException { 152 if (cacheKey == null || networkCache == null) { 153 return LottieCompositionFactory.fromZipStreamSync(context, new ZipInputStream(inputStream), null); 154 } 155 File file = networkCache.writeTempCacheFile(url, inputStream, FileExtension.ZIP); 156 return LottieCompositionFactory.fromZipStreamSync(context, new ZipInputStream(new FileInputStream(file)), url); 157 } 158 159 @NonNull fromGzipStream(@onNull String url, @NonNull InputStream inputStream, @Nullable String cacheKey)160 private LottieResult<LottieComposition> fromGzipStream(@NonNull String url, @NonNull InputStream inputStream, @Nullable String cacheKey) 161 throws IOException { 162 if (cacheKey == null || networkCache == null) { 163 return LottieCompositionFactory.fromJsonInputStreamSync(new GZIPInputStream(inputStream), null); 164 } 165 File file = networkCache.writeTempCacheFile(url, inputStream, FileExtension.GZIP); 166 return LottieCompositionFactory.fromJsonInputStreamSync(new GZIPInputStream(new FileInputStream(file)), url); 167 } 168 169 @NonNull fromJsonStream(@onNull String url, @NonNull InputStream inputStream, @Nullable String cacheKey)170 private LottieResult<LottieComposition> fromJsonStream(@NonNull String url, @NonNull InputStream inputStream, @Nullable String cacheKey) 171 throws IOException { 172 if (cacheKey == null || networkCache == null) { 173 return LottieCompositionFactory.fromJsonInputStreamSync(inputStream, null); 174 } 175 File file = networkCache.writeTempCacheFile(url, inputStream, FileExtension.JSON); 176 return LottieCompositionFactory.fromJsonInputStreamSync(new FileInputStream(file.getAbsolutePath()), url); 177 } 178 } 179