1 package com.airbnb.lottie; 2 3 import android.os.Handler; 4 import android.os.Looper; 5 6 import androidx.annotation.Nullable; 7 import androidx.annotation.RestrictTo; 8 9 import com.airbnb.lottie.utils.Logger; 10 import com.airbnb.lottie.utils.LottieThreadFactory; 11 12 import java.util.ArrayList; 13 import java.util.LinkedHashSet; 14 import java.util.List; 15 import java.util.Set; 16 import java.util.concurrent.Callable; 17 import java.util.concurrent.ExecutionException; 18 import java.util.concurrent.Executor; 19 import java.util.concurrent.Executors; 20 import java.util.concurrent.FutureTask; 21 22 /** 23 * Helper to run asynchronous tasks with a result. 24 * Results can be obtained with {@link #addListener(LottieListener)}. 25 * Failures can be obtained with {@link #addFailureListener(LottieListener)}. 26 * <p> 27 * A task will produce a single result or a single failure. 28 */ 29 @SuppressWarnings("UnusedReturnValue") 30 public class LottieTask<T> { 31 32 /** 33 * Set this to change the executor that LottieTasks are run on. This will be the executor that composition parsing and url 34 * fetching happens on. 35 * <p> 36 * You may change this to run deserialization synchronously for testing. 37 */ 38 @SuppressWarnings("WeakerAccess") 39 public static Executor EXECUTOR = Executors.newCachedThreadPool(new LottieThreadFactory()); 40 41 /* Preserve add order. */ 42 private final Set<LottieListener<T>> successListeners = new LinkedHashSet<>(1); 43 private final Set<LottieListener<Throwable>> failureListeners = new LinkedHashSet<>(1); 44 private final Handler handler = new Handler(Looper.getMainLooper()); 45 46 @Nullable private volatile LottieResult<T> result = null; 47 48 @RestrictTo(RestrictTo.Scope.LIBRARY) LottieTask(Callable<LottieResult<T>> runnable)49 public LottieTask(Callable<LottieResult<T>> runnable) { 50 this(runnable, false); 51 } 52 LottieTask(T result)53 public LottieTask(T result) { 54 setResult(new LottieResult<>(result)); 55 } 56 57 /** 58 * runNow is only used for testing. 59 */ LottieTask(Callable<LottieResult<T>> runnable, boolean runNow)60 @RestrictTo(RestrictTo.Scope.LIBRARY) LottieTask(Callable<LottieResult<T>> runnable, boolean runNow) { 61 if (runNow) { 62 try { 63 setResult(runnable.call()); 64 } catch (Throwable e) { 65 setResult(new LottieResult<>(e)); 66 } 67 } else { 68 EXECUTOR.execute(new LottieFutureTask<T>(this, runnable)); 69 } 70 } 71 setResult(@ullable LottieResult<T> result)72 private void setResult(@Nullable LottieResult<T> result) { 73 if (this.result != null) { 74 throw new IllegalStateException("A task may only be set once."); 75 } 76 this.result = result; 77 notifyListeners(); 78 } 79 80 /** 81 * Add a task listener. If the task has completed, the listener will be called synchronously. 82 * 83 * @return the task for call chaining. 84 */ addListener(LottieListener<T> listener)85 public synchronized LottieTask<T> addListener(LottieListener<T> listener) { 86 LottieResult<T> result = this.result; 87 if (result != null && result.getValue() != null) { 88 listener.onResult(result.getValue()); 89 } 90 91 successListeners.add(listener); 92 return this; 93 } 94 95 /** 96 * Remove a given task listener. The task will continue to execute so you can re-add 97 * a listener if necessary. 98 * 99 * @return the task for call chaining. 100 */ removeListener(LottieListener<T> listener)101 public synchronized LottieTask<T> removeListener(LottieListener<T> listener) { 102 successListeners.remove(listener); 103 return this; 104 } 105 106 /** 107 * Add a task failure listener. This will only be called in the even that an exception 108 * occurs. If an exception has already occurred, the listener will be called immediately. 109 * 110 * @return the task for call chaining. 111 */ addFailureListener(LottieListener<Throwable> listener)112 public synchronized LottieTask<T> addFailureListener(LottieListener<Throwable> listener) { 113 LottieResult<T> result = this.result; 114 if (result != null && result.getException() != null) { 115 listener.onResult(result.getException()); 116 } 117 118 failureListeners.add(listener); 119 return this; 120 } 121 122 /** 123 * Remove a given task failure listener. The task will continue to execute so you can re-add 124 * a listener if necessary. 125 * 126 * @return the task for call chaining. 127 */ removeFailureListener(LottieListener<Throwable> listener)128 public synchronized LottieTask<T> removeFailureListener(LottieListener<Throwable> listener) { 129 failureListeners.remove(listener); 130 return this; 131 } 132 133 @Nullable getResult()134 public LottieResult<T> getResult() { 135 return result; 136 } 137 notifyListeners()138 private void notifyListeners() { 139 // Listeners should be called on the main thread. 140 if (Looper.myLooper() == Looper.getMainLooper()) { 141 notifyListenersInternal(); 142 } else { 143 handler.post(this::notifyListenersInternal); 144 } 145 } 146 notifyListenersInternal()147 private void notifyListenersInternal() { 148 // Local reference in case it gets set on a background thread. 149 LottieResult<T> result = LottieTask.this.result; 150 if (result == null) { 151 return; 152 } 153 if (result.getValue() != null) { 154 notifySuccessListeners(result.getValue()); 155 } else { 156 notifyFailureListeners(result.getException()); 157 } 158 } 159 notifySuccessListeners(T value)160 private synchronized void notifySuccessListeners(T value) { 161 // Allows listeners to remove themselves in onResult. 162 // Otherwise we risk ConcurrentModificationException. 163 List<LottieListener<T>> listenersCopy = new ArrayList<>(successListeners); 164 for (LottieListener<T> l : listenersCopy) { 165 l.onResult(value); 166 } 167 } 168 notifyFailureListeners(Throwable e)169 private synchronized void notifyFailureListeners(Throwable e) { 170 // Allows listeners to remove themselves in onResult. 171 // Otherwise we risk ConcurrentModificationException. 172 List<LottieListener<Throwable>> listenersCopy = new ArrayList<>(failureListeners); 173 if (listenersCopy.isEmpty()) { 174 Logger.warning("Lottie encountered an error but no failure listener was added:", e); 175 return; 176 } 177 178 for (LottieListener<Throwable> l : listenersCopy) { 179 l.onResult(e); 180 } 181 } 182 183 private static class LottieFutureTask<T> extends FutureTask<LottieResult<T>> { 184 185 private LottieTask<T> lottieTask; 186 LottieFutureTask(LottieTask<T> task, Callable<LottieResult<T>> callable)187 LottieFutureTask(LottieTask<T> task, Callable<LottieResult<T>> callable) { 188 super(callable); 189 lottieTask = task; 190 } 191 192 @Override done()193 protected void done() { 194 try { 195 if (isCancelled()) { 196 // We don't need to notify and listeners if the task is cancelled. 197 return; 198 } 199 200 try { 201 lottieTask.setResult(get()); 202 } catch (InterruptedException | ExecutionException e) { 203 lottieTask.setResult(new LottieResult<>(e)); 204 } 205 } finally { 206 // LottieFutureTask can be held in memory for up to 60 seconds after the task is done, which would 207 // result in holding on to the associated LottieTask instance and leaking its listeners. To avoid 208 // that, we clear our the reference to the LottieTask instance. 209 // 210 // How is LottieFutureTask held for up to 60 seconds? It's a bug in how the VM cleans up stack 211 // local variables. When you have a loop that polls a blocking queue and assigns the result 212 // to a local variable, after looping the local variable will still reference the previous value 213 // until the queue returns the next result. 214 // 215 // Executors.newCachedThreadPool() relies on a SynchronousQueue and creates a cached thread pool 216 // with a default keep alice of 60 seconds. After a given worker thread runs a task, that thread 217 // will wait for up to 60 seconds for a new task to come, and while waiting it's also accidentally 218 // keeping a reference to the previous task. 219 // 220 // See commit d577e728e9bccbafc707af3060ea914caa73c14f in AOSP for how that was fixed for Looper. 221 lottieTask = null; 222 } 223 } 224 } 225 } 226