• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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