blob: 8e8e07a5b4fb915a643fcc49d9d639919efafa12 [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.Objects;
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>Besides helping with lifecycle management, this also prevents holding onto object references
* after callbacks have been canceled.
*
* <p>Example usage:
*
* <pre>{@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);
* }
* }
* }</pre>
*
* <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:
*
* <pre>{@code
* // Somewhere inside Foo.
* mCallbackController.destroy(); // Invalidates all current callbacks.
* mCallbackController = new CallbackController(); // Allows to start handing out new callbacks.
* }</pre>
*/
@SuppressWarnings({"NoSynchronizedThisCheck", "NoSynchronizedMethodCheck"})
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("CallbackController.this")
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.
synchronized (CallbackController.this) {
if (mCallback != null) mCallback.onResult(result);
}
}
}
/** Class wrapping {@link Runnable} interface with a {@link Cancelable} interface. */
private class CancelableRunnable implements Cancelable, Runnable {
@GuardedBy("CallbackController.this")
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.
synchronized (CallbackController.this) {
if (mRunnable != null) mRunnable.run();
}
}
}
/** A list of cancelables created and cancelable by this object. */
@Nullable
@GuardedBy("this")
private ArrayList<WeakReference<Cancelable>> mCancelables = new ArrayList<>();
/**
* 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 synchronized <T> Callback<T> makeCancelable(@NonNull Callback<T> callback) {
checkNotCanceled();
CancelableCallback<T> cancelable = new CancelableCallback<>(callback);
addInternal(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 synchronized Runnable makeCancelable(@NonNull Runnable runnable) {
checkNotCanceled();
CancelableRunnable cancelable = new CancelableRunnable(runnable);
addInternal(cancelable);
return cancelable;
}
@GuardedBy("this")
private void addInternal(Cancelable cancelable) {
var cancelables = mCancelables;
cancelables.add(new WeakReference<>(cancelable));
// Flush null entries.
if ((cancelables.size() % 1024) == 0) {
// This removes null entries as a side-effect.
// Cloning the list is inefficient, but this should rarely be hit.
CollectionUtil.strengthen(cancelables);
}
}
/**
* 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 synchronized void destroy() {
checkNotCanceled();
for (Cancelable cancelable : CollectionUtil.strengthen(mCancelables)) {
cancelable.cancel();
}
mCancelables = null;
}
/** If the cancelation already happened, throws an {@link IllegalStateException}. */
@GuardedBy("this")
private void checkNotCanceled() {
// Use NullPointerException because it optimizes well.
Objects.requireNonNull(mCancelables);
}
}