• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2020 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.base;
6 
7 import androidx.annotation.NonNull;
8 import androidx.annotation.Nullable;
9 
10 import java.lang.ref.WeakReference;
11 import java.util.ArrayList;
12 import java.util.Objects;
13 
14 import javax.annotation.concurrent.GuardedBy;
15 
16 /**
17  * Class allowing to wrap lambdas, such as {@link Callback} or {@link Runnable} with a cancelable
18  * version of the same, and cancel them in bulk when {@link #destroy()} is called. Use an instance
19  * of this class to wrap lambdas passed to other objects, and later use {@link #destroy()} to
20  * prevent future invocations of these lambdas.
21  *
22  * <p>Besides helping with lifecycle management, this also prevents holding onto object references
23  * after callbacks have been canceled.
24  *
25  * <p>Example usage:
26  *
27  * <pre>{@code
28  * public class Foo {
29  *    private CallbackController mCallbackController = new CallbackController();
30  *    private SomeDestructibleClass mDestructible = new SomeDestructibleClass();
31  *
32  *    // Classic destroy, with clean up of cancelables.
33  *    public void destroy() {
34  *        // This call makes sure all tracked lambdas are destroyed.
35  *        // It is recommended to be done at the top of the destroy methods, to ensure calls from
36  *        // other threads don't use already destroyed resources.
37  *        if (mCallbackController != null) {
38  *            mCallbackController.destroy();
39  *            mCallbackController = null;
40  *        }
41  *
42  *        if (mDestructible != null) {
43  *            mDestructible.destroy();
44  *            mDestructible = null;
45  *        }
46  *    }
47  *
48  *    // Sets up Bar instance by providing it with a set of dangerous callbacks all of which could
49  *    // cause a NullPointerException if invoked after destroy().
50  *    public void setUpBar(Bar bar) {
51  *        // Notice all callbacks below would fail post destroy, if they were not canceled.
52  *        bar.setDangerousLambda(mCallbackController.makeCancelable(() -> mDestructible.method()));
53  *        bar.setDangerousRunnable(mCallbackController.makeCancelable(this::dangerousRunnable));
54  *        bar.setDangerousOtherCallback(
55  *                mCallbackController.makeCancelable(baz -> mDestructible.setBaz(baz)));
56  *        bar.setDangerousCallback(mCallbackController.makeCancelable(this::setBaz));
57  *    }
58  *
59  *    private void dangerousRunnable() {
60  *        mDestructible.method();
61  *    }
62  *
63  *    private void setBaz(Baz baz) {
64  *        mDestructible.setBaz(baz);
65  *    }
66  * }
67  * }</pre>
68  *
69  * <p>It does not matter if the lambda is intended to be invoked once or more times, as it is only
70  * weakly referred from this class. When the lambda is no longer needed, it can be safely garbage
71  * collected. All invocations after {@link #destroy()} will be ignored.
72  *
73  * <p>Each instance of this class in only meant for a single {@link #destroy()} call. After it is
74  * destroyed, the owning class should create a new instance instead:
75  *
76  * <pre>{@code
77  * // Somewhere inside Foo.
78  * mCallbackController.destroy();  // Invalidates all current callbacks.
79  * mCallbackController = new CallbackController();  // Allows to start handing out new callbacks.
80  * }</pre>
81  */
82 @SuppressWarnings({"NoSynchronizedThisCheck", "NoSynchronizedMethodCheck"})
83 public final class CallbackController {
84     /** Interface for cancelable objects tracked by this class. */
85     private interface Cancelable {
86         /** Cancels the object, preventing its execution, when triggered. */
cancel()87         void cancel();
88     }
89 
90     /** Class wrapping a {@link Callback} interface with a {@link Cancelable} interface. */
91     private class CancelableCallback<T> implements Cancelable, Callback<T> {
92         @GuardedBy("CallbackController.this")
93         private Callback<T> mCallback;
94 
CancelableCallback(@onNull Callback<T> callback)95         private CancelableCallback(@NonNull Callback<T> callback) {
96             mCallback = callback;
97         }
98 
99         @Override
100         @SuppressWarnings("GuardedBy")
cancel()101         public void cancel() {
102             mCallback = null;
103         }
104 
105         @Override
onResult(T result)106         public void onResult(T result) {
107             // Guarantees the cancelation is not going to happen, while callback is executed by
108             // another thread.
109             synchronized (CallbackController.this) {
110                 if (mCallback != null) mCallback.onResult(result);
111             }
112         }
113     }
114 
115     /** Class wrapping {@link Runnable} interface with a {@link Cancelable} interface. */
116     private class CancelableRunnable implements Cancelable, Runnable {
117         @GuardedBy("CallbackController.this")
118         private Runnable mRunnable;
119 
CancelableRunnable(@onNull Runnable runnable)120         private CancelableRunnable(@NonNull Runnable runnable) {
121             mRunnable = runnable;
122         }
123 
124         @Override
125         @SuppressWarnings("GuardedBy")
cancel()126         public void cancel() {
127             mRunnable = null;
128         }
129 
130         @Override
run()131         public void run() {
132             // Guarantees the cancelation is not going to happen, while runnable is executed by
133             // another thread.
134             synchronized (CallbackController.this) {
135                 if (mRunnable != null) mRunnable.run();
136             }
137         }
138     }
139 
140     /** A list of cancelables created and cancelable by this object. */
141     @Nullable
142     @GuardedBy("this")
143     private ArrayList<WeakReference<Cancelable>> mCancelables = new ArrayList<>();
144 
145     /**
146      * Wraps a provided {@link Callback} with a cancelable object that is tracked by this {@link
147      * CallbackController}. To cancel a resulting wrapped instance destroy the host.
148      *
149      * <p>This method must not be called after {@link #destroy()}.
150      *
151      * @param <T> The type of the callback result.
152      * @param callback A callback that will be made cancelable.
153      * @return A cancelable instance of the callback.
154      */
makeCancelable(@onNull Callback<T> callback)155     public synchronized <T> Callback<T> makeCancelable(@NonNull Callback<T> callback) {
156         checkNotCanceled();
157         CancelableCallback<T> cancelable = new CancelableCallback<>(callback);
158         addInternal(cancelable);
159         return cancelable;
160     }
161 
162     /**
163      * Wraps a provided {@link Runnable} with a cancelable object that is tracked by this {@link
164      * CallbackController}. To cancel a resulting wrapped instance destroy the host.
165      *
166      * <p>This method must not be called after {@link #destroy()}.
167      *
168      * @param runnable A runnable that will be made cancelable.
169      * @return A cancelable instance of the runnable.
170      */
makeCancelable(@onNull Runnable runnable)171     public synchronized Runnable makeCancelable(@NonNull Runnable runnable) {
172         checkNotCanceled();
173         CancelableRunnable cancelable = new CancelableRunnable(runnable);
174         addInternal(cancelable);
175         return cancelable;
176     }
177 
178     @GuardedBy("this")
addInternal(Cancelable cancelable)179     private void addInternal(Cancelable cancelable) {
180         var cancelables = mCancelables;
181         cancelables.add(new WeakReference<>(cancelable));
182         // Flush null entries.
183         if ((cancelables.size() % 1024) == 0) {
184             // This removes null entries as a side-effect.
185             // Cloning the list is inefficient, but this should rarely be hit.
186             CollectionUtil.strengthen(cancelables);
187         }
188     }
189 
190     /**
191      * Cancels all of the cancelables that have not been garbage collected yet.
192      *
193      * <p>This method must only be called once and makes the instance unusable afterwards.
194      */
destroy()195     public synchronized void destroy() {
196         checkNotCanceled();
197         for (Cancelable cancelable : CollectionUtil.strengthen(mCancelables)) {
198             cancelable.cancel();
199         }
200         mCancelables = null;
201     }
202 
203     /** If the cancelation already happened, throws an {@link IllegalStateException}. */
204     @GuardedBy("this")
checkNotCanceled()205     private void checkNotCanceled() {
206         // Use NullPointerException because it optimizes well.
207         Objects.requireNonNull(mCancelables);
208     }
209 }
210