Loading core/java/android/os/CancellationSignalBeamer.java 0 → 100644 +325 −0 Original line number Diff line number Diff line /* * 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. * * <p><strong>Important:</strong> For this to work, the following invariants must be held up: * <ul> * <li>A call to beam() <strong>MUST</strong> 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. * <li>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. * <li>A {@link CancellationSignal} <strong>SHOULD</strong> be used only once, as there * can only be a single {@link android.os.CancellationSignal.OnCancelListener OnCancelListener}. * * </ul> * <p>Caveats: * <ul> * <li>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. * <li>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()}). * <li>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. * </ul> * * * <p>Usage: * <pre> * // 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)) * } * } * * </pre> * * @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 <em>after</em> * 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 <em>after</em> 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<IBinder, CancellationSignal> 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"); } } } core/tests/coretests/src/android/os/CancellationSignalBeamerTest.java 0 → 100644 +224 −0 Original line number Diff line number Diff line /* * 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); } Loading
core/java/android/os/CancellationSignalBeamer.java 0 → 100644 +325 −0 Original line number Diff line number Diff line /* * 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. * * <p><strong>Important:</strong> For this to work, the following invariants must be held up: * <ul> * <li>A call to beam() <strong>MUST</strong> 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. * <li>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. * <li>A {@link CancellationSignal} <strong>SHOULD</strong> be used only once, as there * can only be a single {@link android.os.CancellationSignal.OnCancelListener OnCancelListener}. * * </ul> * <p>Caveats: * <ul> * <li>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. * <li>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()}). * <li>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. * </ul> * * * <p>Usage: * <pre> * // 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)) * } * } * * </pre> * * @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 <em>after</em> * 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 <em>after</em> 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<IBinder, CancellationSignal> 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"); } } }
core/tests/coretests/src/android/os/CancellationSignalBeamerTest.java 0 → 100644 +224 −0 Original line number Diff line number Diff line /* * 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); }