blob: 2dc19a6c0fb8541d987f42831abd052d18bdfacb [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.annotation.concurrent.GuardedBy;
/**
* Class allowing to wrap lambdas, such as {@link Callback} or {@link Runnable} with a cancelable
* version of the same, and cancel them in bulk when {@link #destroy()} is called. Use an instance
* of this class to wrap lambdas passed to other objects, and later use {@link #destroy()} to
* prevent future invocations of these lambdas.
* <p>
* Example usage:
* {@code
* public class Foo {
* private CallbackController mCallbackController = new CallbackController();
* private SomeDestructibleClass mDestructible = new SomeDestructibleClass();
*
* // Classic destroy, with clean up of cancelables.
* public void destroy() {
* // This call makes sure all tracked lambdas are destroyed.
* // It is recommended to be done at the top of the destroy methods, to ensure calls from
* // other threads don't use already destroyed resources.
* if (mCallbackController != null) {
* mCallbackController.destroy();
* mCallbackController = null;
* }
*
* if (mDestructible != null) {
* mDestructible.destroy();
* mDestructible = null;
* }
* }
*
* // Sets up Bar instance by providing it with a set of dangerous callbacks all of which could
* // cause a NullPointerException if invoked after destroy().
* public void setUpBar(Bar bar) {
* // Notice all callbacks below would fail post destroy, if they were not canceled.
* bar.setDangerousLambda(mCallbackController.makeCancelable(() -> mDestructible.method()));
* bar.setDangerousRunnable(mCallbackController.makeCancelable(this::dangerousRunnable));
* bar.setDangerousOtherCallback(
* mCallbackController.makeCancelable(baz -> mDestructible.setBaz(baz)));
* bar.setDangerousCallback(mCallbackController.makeCancelable(this::setBaz));
* }
*
* private void dangerousRunnable() {
* mDestructible.method();
* }
*
* private void setBaz(Baz baz) {
* mDestructible.setBaz(baz);
* }
* }
* }
* <p>
* It does not matter if the lambda is intended to be invoked once or more times, as it is only
* weakly referred from this class. When the lambda is no longer needed, it can be safely garbage
* collected. All invocations after {@link #destroy()} will be ignored.
* <p>
* Each instance of this class in only meant for a single {@link
* #destroy()} call. After it is destroyed, the owning class should create a new instance instead:
* {@code
* // Somewhere inside Foo.
* mCallbackController.destroy(); // Invalidates all current callbacks.
* mCallbackController = new CallbackController(); // Allows to start handing out new callbacks.
* }
*/
public final class CallbackController {
/** Interface for cancelable objects tracked by this class. */
private interface Cancelable {
/** Cancels the object, preventing its execution, when triggered. */
void cancel();
}
/** Class wrapping a {@link Callback} interface with a {@link Cancelable} interface. */
private class CancelableCallback<T> implements Cancelable, Callback<T> {
@GuardedBy("mReentrantLock")
private Callback<T> mCallback;
private CancelableCallback(@NonNull Callback<T> callback) {
mCallback = callback;
}
@Override
@SuppressWarnings("GuardedBy")
public void cancel() {
mCallback = null;
}
@Override
public void onResult(T result) {
// Guarantees the cancelation is not going to happen, while callback is executed by
// another thread.
try (AutoCloseableLock acl = AutoCloseableLock.lock(mReentrantLock)) {
if (mCallback != null) mCallback.onResult(result);
}
}
}
/** Class wrapping {@link Runnable} interface with a {@link Cancelable} interface. */
private class CancelableRunnable implements Cancelable, Runnable {
@GuardedBy("mReentrantLock")
private Runnable mRunnable;
private CancelableRunnable(@NonNull Runnable runnable) {
mRunnable = runnable;
}
@Override
@SuppressWarnings("GuardedBy")
public void cancel() {
mRunnable = null;
}
@Override
public void run() {
// Guarantees the cancelation is not going to happen, while runnable is executed by
// another thread.
try (AutoCloseableLock acl = AutoCloseableLock.lock(mReentrantLock)) {
if (mRunnable != null) mRunnable.run();
}
}
}
/** Class wrapping the locking logic to reduce repetitive code. */
private static class AutoCloseableLock implements AutoCloseable {
private final Lock mLock;
private boolean mIsLocked;
private AutoCloseableLock(Lock lock, boolean isLocked) {
mLock = lock;
mIsLocked = isLocked;
}
static AutoCloseableLock lock(Lock l) {
l.lock();
return new AutoCloseableLock(l, true);
}
@Override
public void close() {
if (!mIsLocked) throw new IllegalStateException("mLock isn't locked.");
mIsLocked = false;
mLock.unlock();
}
}
/** A list of cancelables created and cancelable by this object. */
@Nullable
@GuardedBy("mReentrantLock")
private ArrayList<WeakReference<Cancelable>> mCancelables = new ArrayList<>();
/** Ensures thread safety of creating cancelables and canceling them. */
private final ReentrantLock mReentrantLock = new ReentrantLock(/*fair=*/true);
/**
* Wraps a provided {@link Callback} with a cancelable object that is tracked by this
* {@link CallbackController}. To cancel a resulting wrapped instance destroy the host.
* <p>
* This method must not be called after {@link #destroy()}.
*
* @param <T> The type of the callback result.
* @param callback A callback that will be made cancelable.
* @return A cancelable instance of the callback.
*/
public <T> Callback<T> makeCancelable(@NonNull Callback<T> callback) {
try (AutoCloseableLock acl = AutoCloseableLock.lock(mReentrantLock)) {
checkNotCanceled();
CancelableCallback<T> cancelable = new CancelableCallback<>(callback);
mCancelables.add(new WeakReference<>(cancelable));
return cancelable;
}
}
/**
* Wraps a provided {@link Runnable} with a cancelable object that is tracked by this
* {@link CallbackController}. To cancel a resulting wrapped instance destroy the host.
* <p>
* This method must not be called after {@link #destroy()}.
*
* @param runnable A runnable that will be made cancelable.
* @return A cancelable instance of the runnable.
*/
public Runnable makeCancelable(@NonNull Runnable runnable) {
try (AutoCloseableLock acl = AutoCloseableLock.lock(mReentrantLock)) {
checkNotCanceled();
CancelableRunnable cancelable = new CancelableRunnable(runnable);
mCancelables.add(new WeakReference<>(cancelable));
return cancelable;
}
}
/**
* Cancels all of the cancelables that have not been garbage collected yet.
* <p>
* This method must only be called once and makes the instance unusable afterwards.
*/
public void destroy() {
// This is likely an invalid state. A callback is currently being run, which means the
// owning class using this controller is likely in the middle of a method call. But if
// destroy is called on the controller, it is also likely being called on the owning class.
// After destroy completes, the owning class will be in invalid state to be used, and the
// rest of the method call will be operating with the owning class in an invalid state. If
// something has a valid use case for this, remove this assert.
assert !mReentrantLock.isHeldByCurrentThread();
try (AutoCloseableLock acl = AutoCloseableLock.lock(mReentrantLock)) {
checkNotCanceled();
for (Cancelable cancelable : CollectionUtil.strengthen(mCancelables)) {
cancelable.cancel();
}
mCancelables = null;
}
}
/** If the cancelation already happened, throws an {@link IllegalStateException}. */
@GuardedBy("mReentrantLock")
private void checkNotCanceled() {
if (mCancelables == null) {
throw new IllegalStateException("This CallbackController has already been destroyed.");
}
}
}