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