diff --git a/core/java/android/os/CancellationSignalBeamer.java b/core/java/android/os/CancellationSignalBeamer.java
new file mode 100644
index 0000000000000..afb5ff7bf6265
--- /dev/null
+++ b/core/java/android/os/CancellationSignalBeamer.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.system.SystemCleaner;
+
+import java.lang.ref.Cleaner;
+import java.lang.ref.Reference;
+import java.util.HashMap;
+
+/**
+ * A transport for {@link CancellationSignal}, but unlike
+ * {@link CancellationSignal#createTransport()} doesn't require pre-creating the transport in the
+ * target process. Instead, cancellation is forwarded over the same IPC surface as the cancellable
+ * request.
+ *
+ *
Important: For this to work, the following invariants must be held up:
+ *
+ * - A call to beam() MUST result in a call to close() on the result
+ * (otherwise, the token will be leaked and cancellation isn't propagated), and that call
+ * must happen after the call using the
+ * token is sent (otherwise, any concurrent cancellation may be lost). It is strongly
+ * recommended to use try-with-resources on the token.
+ *
- The cancel(), forget() and cancellable operations transporting the token must either
+ * all be oneway on the same binder, or all be non-oneway to guarantee proper ordering.
+ *
- A {@link CancellationSignal} SHOULD be used only once, as there
+ * can only be a single {@link android.os.CancellationSignal.OnCancelListener OnCancelListener}.
+ *
+ *
+ * Caveats:
+ *
+ * - Cancellation is only ever dispatched after the token is closed, and thus after the
+ * call performing the cancellable operation (if the invariants are followed). The operation
+ * must therefore not block the incoming binder thread, or cancellation won't be possible.
+ *
- Consequently, in the unlikely event that the sender dies right after beaming an already
+ * cancelled {@link CancellationSignal}, the cancellation may be lost (unlike with
+ * {@link CancellationSignal#createTransport()}).
+ *
- The forwarding OnCancelListener is set in the implied finally phase of try-with-resources
+ * / when closing the token. If the receiver is in the same process, and the signal is
+ * already cancelled, this may invoke the target's OnCancelListener during that phase.
+ *
+ *
+ *
+ * Usage:
+ *
+ * // Sender:
+ *
+ * class FooManager {
+ * var mCancellationSignalSender = new CancellationSignalBeamer.Sender() {
+ * @Override
+ * public void onCancel(IBinder token) { remoteIFooService.onCancelToken(token); }
+ *
+ * @Override
+ * public void onForget(IBinder token) { remoteIFooService.onForgetToken(token); }
+ * };
+ *
+ * public void doCancellableOperation(..., CancellationSignal cs) {
+ * try (var csToken = mCancellationSignalSender.beam(cs)) {
+ * remoteIFooService.doCancellableOperation(..., csToken);
+ * }
+ * }
+ * }
+ *
+ * // Receiver:
+ *
+ * class FooManagerService extends IFooService.Stub {
+ * var mCancellationSignalReceiver = new CancellationSignalBeamer.Receiver();
+ *
+ * @Override
+ * public void doCancellableOperation(..., IBinder csToken) {
+ * CancellationSignal cs = mCancellationSignalReceiver.unbeam(csToken))
+ * // ...
+ * }
+ *
+ * @Override
+ * public void onCancelToken(..., IBinder csToken) {
+ * mCancellationSignalReceiver.cancelToken(csToken))
+ * }
+ *
+ * @Override
+ * public void onForgetToken(..., IBinder csToken) {
+ * mCancellationSignalReceiver.forgetToken(csToken))
+ * }
+ * }
+ *
+ *
+ *
+ * @hide
+ */
+public class CancellationSignalBeamer {
+
+ static final Cleaner sCleaner = SystemCleaner.cleaner();
+
+ /** The sending side of an {@link CancellationSignalBeamer} */
+ public abstract static class Sender {
+
+ /**
+ * Beams a {@link CancellationSignal} through an existing Binder interface.
+ *
+ * @param cs the {@code CancellationSignal} to beam, or {@code null}.
+ * @return an {@link IBinder} token. MUST be {@link CloseableToken#close}d after
+ * the binder call transporting it to the remote process, best with
+ * try-with-resources. {@code null} if {@code cs} was {@code null}.
+ */
+ // TODO(b/254888024): @MustBeClosed
+ @Nullable
+ public CloseableToken beam(@Nullable CancellationSignal cs) {
+ if (cs == null) {
+ return null;
+ }
+ return new Token(this, cs);
+ }
+
+ /**
+ * A {@link #beam}ed {@link CancellationSignal} was closed.
+ *
+ * MUST be forwarded to {@link Receiver#cancel} with proper ordering. See
+ * {@link CancellationSignalBeamer} for details.
+ */
+ public abstract void onCancel(IBinder token);
+
+ /**
+ * A {@link #beam}ed {@link CancellationSignal} was GC'd.
+ *
+ * MUST be forwarded to {@link Receiver#forget} with proper ordering. See
+ * {@link CancellationSignalBeamer} for details.
+ */
+ public abstract void onForget(IBinder token);
+
+ private static class Token extends Binder implements CloseableToken, Runnable {
+
+ private final Sender mSender;
+ private Preparer mPreparer;
+
+ private Token(Sender sender, CancellationSignal signal) {
+ mSender = sender;
+ mPreparer = new Preparer(sender, signal, this);
+ }
+
+ @Override
+ public void close() {
+ Preparer preparer = mPreparer;
+ mPreparer = null;
+ if (preparer != null) {
+ preparer.setup();
+ }
+ }
+
+ @Override
+ public void run() {
+ mSender.onForget(this);
+ }
+
+ private static class Preparer implements CancellationSignal.OnCancelListener {
+ private final Sender mSender;
+ private final CancellationSignal mSignal;
+ private final Token mToken;
+
+ private Preparer(Sender sender, CancellationSignal signal, Token token) {
+ mSender = sender;
+ mSignal = signal;
+ mToken = token;
+ }
+
+ void setup() {
+ sCleaner.register(this, mToken);
+ mSignal.setOnCancelListener(this);
+ }
+
+ @Override
+ public void onCancel() {
+ try {
+ mSender.onCancel(mToken);
+ } finally {
+ // Make sure we dispatch onCancel before the cleaner can run.
+ Reference.reachabilityFence(this);
+ }
+ }
+ }
+ }
+
+ /**
+ * A {@link #beam}ed {@link CancellationSignal} ready for sending over Binder.
+ *
+ * MUST be closed after it is sent over binder, ideally through try-with-resources.
+ */
+ public interface CloseableToken extends IBinder, AutoCloseable {
+ @Override
+ void close(); // No throws
+ }
+ }
+
+ /** The receiving side of a {@link CancellationSignalBeamer}. */
+ public static class Receiver implements IBinder.DeathRecipient {
+ private final HashMap mTokenMap = new HashMap<>();
+ private final boolean mCancelOnSenderDeath;
+
+ /**
+ * Constructs a new {@code Receiver}.
+ *
+ * @param cancelOnSenderDeath if true, {@link CancellationSignal}s obtained from
+ * {@link #unbeam} are automatically {@link #cancel}led if the sender token
+ * {@link Binder#linkToDeath dies}; otherwise they are simnply dropped. Note: if the
+ * sending process drops all references to the {@link CancellationSignal} before
+ * process death, the cancellation is not guaranteed.
+ */
+ public Receiver(boolean cancelOnSenderDeath) {
+ mCancelOnSenderDeath = cancelOnSenderDeath;
+ }
+
+ /**
+ * Unbeams a token that was obtained via {@link Sender#beam} and turns it back into a
+ * {@link CancellationSignal}.
+ *
+ * A subsequent call to {@link #cancel} with the same token will cancel the returned
+ * {@code CancellationSignal}.
+ *
+ * @param token a token that was obtained from {@link Sender}, possibly in a remote process.
+ * @return a {@link CancellationSignal} linked to the given token.
+ */
+ @Nullable
+ public CancellationSignal unbeam(@Nullable IBinder token) {
+ if (token == null) {
+ return null;
+ }
+ synchronized (this) {
+ CancellationSignal cs = mTokenMap.get(token);
+ if (cs != null) {
+ return cs;
+ }
+
+ cs = new CancellationSignal();
+ mTokenMap.put(token, cs);
+ try {
+ token.linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ dead(token);
+ }
+ return cs;
+ }
+ }
+
+ /**
+ * Forgets state associated with the given token (if any).
+ *
+ * Subsequent calls to {@link #cancel} or binder death notifications on the token will not
+ * have any effect.
+ *
+ * This MUST be invoked when forwarding {@link Sender#onForget}, otherwise the token and
+ * {@link CancellationSignal} will leak if the token was ever {@link #unbeam}ed.
+ *
+ * Optionally, the receiving service logic may also invoke this if it can guarantee that
+ * the unbeamed CancellationSignal isn't needed anymore (i.e. the cancellable operation
+ * using the CancellationSignal has been fully completed).
+ *
+ * @param token the token to forget. No-op if {@code null}.
+ */
+ public void forget(@Nullable IBinder token) {
+ synchronized (this) {
+ if (mTokenMap.remove(token) != null) {
+ token.unlinkToDeath(this, 0);
+ }
+ }
+ }
+
+ /**
+ * Cancels the {@link CancellationSignal} associated with the given token (if any).
+ *
+ * This MUST be invoked when forwarding {@link Sender#onCancel}, otherwise the token and
+ * {@link CancellationSignal} will leak if the token was ever {@link #unbeam}ed.
+ *
+ * Optionally, the receiving service logic may also invoke this if it can guarantee that
+ * the unbeamed CancellationSignal isn't needed anymore (i.e. the cancellable operation
+ * using the CancellationSignal has been fully completed).
+ *
+ * @param token the token to forget. No-op if {@code null}.
+ */
+ public void cancel(@Nullable IBinder token) {
+ CancellationSignal cs;
+ synchronized (this) {
+ cs = mTokenMap.get(token);
+ if (cs != null) {
+ forget(token);
+ } else {
+ return;
+ }
+ }
+ cs.cancel();
+ }
+
+ private void dead(@NonNull IBinder token) {
+ if (mCancelOnSenderDeath) {
+ cancel(token);
+ } else {
+ forget(token);
+ }
+ }
+
+ @Override
+ public void binderDied(@NonNull IBinder who) {
+ dead(who);
+ }
+
+ @Override
+ public void binderDied() {
+ throw new RuntimeException("unreachable");
+ }
+ }
+}
diff --git a/core/tests/coretests/src/android/os/CancellationSignalBeamerTest.java b/core/tests/coretests/src/android/os/CancellationSignalBeamerTest.java
new file mode 100644
index 0000000000000..42c97f3876a6a
--- /dev/null
+++ b/core/tests/coretests/src/android/os/CancellationSignalBeamerTest.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import static android.os.CancellationSignalBeamer.Sender;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.os.CancellationSignalBeamer.Receiver;
+import android.util.PollingCheck;
+import android.util.PollingCheck.PollingCheckCondition;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.ref.Cleaner;
+import java.lang.ref.Reference;
+import java.util.concurrent.CountDownLatch;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CancellationSignalBeamerTest {
+
+ private CancellationSignal mSenderSignal = new CancellationSignal();
+ private CancellationSignal mReceivedSignal;
+ private Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
+
+ @Test
+ public void testBeam_null() {
+ try (var token = mSender.beam(null)) {
+ assertThat(token).isNull();
+ invokeGenericService(token);
+ }
+ assertThat(mReceivedSignal).isNull();
+ }
+
+ @Test
+ public void testBeam_nonNull() {
+ try (var token = mSender.beam(mSenderSignal)) {
+ assertThat(token).isNotNull();
+ invokeGenericService(token);
+ }
+ assertThat(mReceivedSignal).isNotNull();
+ }
+
+ @Test
+ public void testBeam_async() {
+ IBinder outerToken;
+ try (var token = mSender.beam(mSenderSignal)) {
+ assertThat(token).isNotNull();
+ outerToken = token;
+ }
+ invokeGenericService(outerToken);
+ assertThat(mReceivedSignal).isNotNull();
+ }
+
+ @Test
+ public void testCancelOnSentSignal_cancelsReceivedSignal() {
+ try (var token = mSender.beam(mSenderSignal)) {
+ invokeGenericService(token);
+ }
+ mSenderSignal.cancel();
+ assertThat(mReceivedSignal.isCanceled()).isTrue();
+ }
+
+ @Test
+ public void testSendingCancelledSignal_cancelsReceivedSignal() {
+ mSenderSignal.cancel();
+ try (var token = mSender.beam(mSenderSignal)) {
+ invokeGenericService(token);
+ }
+ assertThat(mReceivedSignal.isCanceled()).isTrue();
+ }
+
+ @Test
+ public void testUnbeam_null() {
+ assertThat(mReceiver.unbeam(null)).isNull();
+ }
+
+ @Test
+ public void testForget_null() {
+ mReceiver.forget(null);
+ }
+
+ @Test
+ public void testCancel_null() {
+ mReceiver.cancel(null);
+ }
+
+ @Test
+ public void testForget_withUnknownToken() {
+ mReceiver.forget(new Binder());
+ }
+
+ @Test
+ public void testCancel_withUnknownToken() {
+ mReceiver.cancel(new Binder());
+ }
+
+ @Test
+ public void testBinderDied_withUnknownToken() {
+ mReceiver.binderDied(new Binder());
+ }
+
+ @Test
+ public void testReceiverWithCancelOnSenderDead_cancelsOnSenderDeath() {
+ var receiver = new Receiver(true /* cancelOnSenderDeath */);
+ var token = new Binder();
+ var signal = receiver.unbeam(token);
+ receiver.binderDied(token);
+ assertThat(signal.isCanceled()).isTrue();
+ }
+
+ @Test
+ public void testReceiverWithoutCancelOnSenderDead_doesntCancelOnSenderDeath() {
+ var receiver = new Receiver(false /* cancelOnSenderDeath */);
+ var token = new Binder();
+ var signal = receiver.unbeam(token);
+ receiver.binderDied(token);
+ assertThat(signal.isCanceled()).isFalse();
+ }
+
+ @Test
+ public void testDroppingSentSignal_dropsReceivedSignal() throws Exception {
+ // In a multiprocess scenario, sending token over Binder might leak the token
+ // on both ends if we create a reference cycle. Simulate that worst-case scenario
+ // here by leaking it directly, then test that cleanup of the signals still works.
+ var receivedSignalCleaned = new CountDownLatch(1);
+ var tokenRef = new Object[1];
+ // Reference the cancellation signals in a separate method scope, so we don't
+ // accidentally leak them on the stack / in a register.
+ Runnable r = () -> {
+ try (var token = mSender.beam(mSenderSignal)) {
+ tokenRef[0] = token;
+ invokeGenericService(token);
+ }
+ mSenderSignal = null;
+
+ Cleaner.create().register(mReceivedSignal, receivedSignalCleaned::countDown);
+ mReceivedSignal = null;
+ };
+ r.run();
+
+ waitForWithGc(() -> receivedSignalCleaned.getCount() == 0);
+
+ Reference.reachabilityFence(tokenRef[0]);
+ }
+
+ @Test
+ public void testRepeatedBeaming_doesntLeak() throws Exception {
+ var receivedSignalCleaned = new CountDownLatch(1);
+ var tokenRef = new Object[1];
+ // Reference the cancellation signals in a separate method scope, so we don't
+ // accidentally leak them on the stack / in a register.
+ Runnable r = () -> {
+ try (var token = mSender.beam(mSenderSignal)) {
+ tokenRef[0] = token;
+ invokeGenericService(token);
+ }
+ // Beaming again leaves mReceivedSignal dangling, so it should be collected.
+ mSender.beam(mSenderSignal).close();
+
+ Cleaner.create().register(mReceivedSignal, receivedSignalCleaned::countDown);
+ mReceivedSignal = null;
+ };
+ r.run();
+
+ waitForWithGc(() -> receivedSignalCleaned.getCount() == 0);
+
+ Reference.reachabilityFence(tokenRef[0]);
+ }
+
+ private void waitForWithGc(PollingCheckCondition condition) throws IOException {
+ try {
+ PollingCheck.waitFor(() -> {
+ Runtime.getRuntime().gc();
+ return condition.canProceed();
+ });
+ } catch (AssertionError e) {
+ File heap = new File(mContext.getExternalFilesDir(null), "dump.hprof");
+ Debug.dumpHprofData(heap.getAbsolutePath());
+ throw e;
+ }
+ }
+
+ private void invokeGenericService(IBinder cancellationSignalToken) {
+ mReceivedSignal = mReceiver.unbeam(cancellationSignalToken);
+ }
+
+ private final Sender mSender = new Sender() {
+ @Override
+ public void onCancel(IBinder token) {
+ mReceiver.cancel(token);
+ }
+
+ @Override
+ public void onForget(IBinder token) {
+ mReceiver.forget(token);
+ }
+ };
+
+ private final Receiver mReceiver = new Receiver(false);
+}