chromium/base/android/java/src/org/chromium/base/CallbackController.java

// 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);
    }
}