Commit 5a00bc83 authored by Filip Gorski's avatar Filip Gorski Committed by Commit Bot

[Android] CallbackController for canceling Callbacks and Runnables

This patch adds CallbackController that can wrap Callbacks
and Runnables in order to protect them from being invoked after
an object they refer to has been destroyed. This late invocation
can come for example from a UI which has received a callback
to signal UI events, like changing focus.

Detailed documentation in JavaDoc.

Change-Id: I012f647c4b93ca378a1987da593d77a5890dafef
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2267409Reviewed-by: default avatarSky Malice <skym@chromium.org>
Reviewed-by: default avatarDavid Trainor <dtrainor@chromium.org>
Reviewed-by: default avatarTommy Nyquist <nyquist@chromium.org>
Commit-Queue: Filip Gorski <fgorski@chromium.org>
Cr-Commit-Position: refs/heads/master@{#787884}
parent 16a3428c
......@@ -3458,6 +3458,7 @@ if (is_android) {
"android/java/src/org/chromium/base/BuildInfo.java",
"android/java/src/org/chromium/base/BundleUtils.java",
"android/java/src/org/chromium/base/Callback.java",
"android/java/src/org/chromium/base/CallbackController.java",
"android/java/src/org/chromium/base/CollectionUtil.java",
"android/java/src/org/chromium/base/CommandLine.java",
"android/java/src/org/chromium/base/CommandLineInitUtil.java",
......@@ -3793,6 +3794,7 @@ if (is_android) {
junit_binary("base_junit_tests") {
sources = [
"android/junit/src/org/chromium/base/ApplicationStatusTest.java",
"android/junit/src/org/chromium/base/CallbackControllerTest.java",
"android/junit/src/org/chromium/base/DiscardableReferencePoolTest.java",
"android/junit/src/org/chromium/base/FileUtilsTest.java",
"android/junit/src/org/chromium/base/LifetimeAssertTest.java",
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
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("mReadWriteLock")
private Callback<T> mCallback;
private CancelableCallback(@NonNull Callback<T> callback) {
mCallback = callback;
}
@Override
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(mReadWriteLock.readLock())) {
if (mCallback != null) mCallback.onResult(result);
}
}
}
/** Class wrapping {@link Runnable} interface with a {@link Cancelable} interface. */
private class CancelableRunnable implements Cancelable, Runnable {
@GuardedBy("mReadWriteLock")
private Runnable mRunnable;
private CancelableRunnable(@NonNull Runnable runnable) {
mRunnable = runnable;
}
@Override
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(mReadWriteLock.readLock())) {
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("mReadWriteLock")
private ArrayList<WeakReference<Cancelable>> mCancelables = new ArrayList<>();
/** Ensures thread safety of creating cancelables and canceling them. */
private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock(/*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(mReadWriteLock.writeLock())) {
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(mReadWriteLock.writeLock())) {
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() {
try (AutoCloseableLock acl = AutoCloseableLock.lock(mReadWriteLock.writeLock())) {
checkNotCanceled();
for (WeakReference<Cancelable> cancelableWeakReference : mCancelables) {
Cancelable cancelable = cancelableWeakReference.get();
if (cancelable != null) cancelable.cancel();
}
mCancelables = null;
}
}
/** If the cancelation already happened, throws an {@link IllegalStateException}. */
@GuardedBy("mReadWriteLock")
private void checkNotCanceled() {
if (mCancelables == null) {
throw new IllegalStateException("This CallbackController has already been destroyed.");
}
}
}
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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 static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.chromium.base.test.BaseRobolectricTestRunner;
/**
* Test class for {@link CallbackController}, which also describes typical usage.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class CallbackControllerTest {
private boolean mExecutionCompleted;
@Test
public void testInstanceCallback() {
CallbackController mCallbackController = new CallbackController();
Callback<Boolean> wrapped = mCallbackController.makeCancelable(this::setExecutionCompleted);
mExecutionCompleted = false;
wrapped.onResult(true);
assertTrue(mExecutionCompleted);
// Execution possible multiple times.
mExecutionCompleted = false;
wrapped.onResult(true);
assertTrue(mExecutionCompleted);
// Won't trigger after CallbackController is destroyed.
mExecutionCompleted = false;
mCallbackController.destroy();
wrapped.onResult(true);
assertFalse(mExecutionCompleted);
}
@Test
public void testlInstanceRunnable() {
CallbackController mCallbackController = new CallbackController();
Runnable wrapped = mCallbackController.makeCancelable(this::completeExection);
mExecutionCompleted = false;
wrapped.run();
assertTrue(mExecutionCompleted);
// Execution possible multiple times.
mExecutionCompleted = false;
wrapped.run();
assertTrue(mExecutionCompleted);
// Won't trigger after CallbackController is destroyed.
mExecutionCompleted = false;
mCallbackController.destroy();
wrapped.run();
assertFalse(mExecutionCompleted);
}
@Test
public void testLambdaCallback() {
CallbackController mCallbackController = new CallbackController();
Callback<Boolean> wrapped =
mCallbackController.makeCancelable(value -> setExecutionCompleted(value));
mExecutionCompleted = false;
wrapped.onResult(true);
assertTrue(mExecutionCompleted);
// Execution possible multiple times.
mExecutionCompleted = false;
wrapped.onResult(true);
assertTrue(mExecutionCompleted);
// Won't trigger after CallbackController is destroyed.
mExecutionCompleted = false;
mCallbackController.destroy();
wrapped.onResult(true);
assertFalse(mExecutionCompleted);
}
@Test
public void testLambdaRunnable() {
Runnable runnable = () -> setExecutionCompleted(true);
CallbackController mCallbackController = new CallbackController();
Runnable wrapped = mCallbackController.makeCancelable(() -> completeExection());
mExecutionCompleted = false;
wrapped.run();
assertTrue(mExecutionCompleted);
// Execution possible multiple times.
mExecutionCompleted = false;
wrapped.run();
assertTrue(mExecutionCompleted);
// Won't trigger after CallbackController is destroyed.
mExecutionCompleted = false;
mCallbackController.destroy();
wrapped.run();
assertFalse(mExecutionCompleted);
}
private void completeExection() {
setExecutionCompleted(true);
}
private void setExecutionCompleted(boolean completed) {
mExecutionCompleted = completed;
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment