/* * 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.annotation.SuppressLint; import android.system.SystemCleaner; import android.util.Pair; import android.view.inputmethod.CancellableHandwritingGesture; import android.view.inputmethod.HandwritingGesture; import java.lang.ref.Cleaner; import java.lang.ref.Reference; import java.util.ArrayList; 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: *

*

Caveats: *

* * *

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(@NonNull 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(@NonNull IBinder token); private static final ThreadLocal>> sScope = new ThreadLocal<>(); /** * Beams a {@link CancellationSignal} through an existing Binder interface. * @param gesture {@link HandwritingGesture} that supports * {@link CancellableHandwritingGesture cancellation} requesting cancellation token. * @return {@link IBinder} token. MUST be {@link MustClose#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} or if * {@link HandwritingGesture} isn't {@link CancellableHandwritingGesture cancellable}. */ @NonNull public MustClose beamScopeIfNeeded(@NonNull HandwritingGesture gesture) { if (!(gesture instanceof CancellableHandwritingGesture)) { return null; } sScope.set(Pair.create(this, new ArrayList<>())); return () -> { var tokens = sScope.get().second; sScope.remove(); for (int i = tokens.size() - 1; i >= 0; i--) { if (tokens.get(i) != null) { tokens.get(i).close(); } } }; } /** * An {@link AutoCloseable} interface with {@link AutoCloseable#close()} callback. */ public interface MustClose extends AutoCloseable { @Override void close(); } /** * Beams a {@link CancellationSignal} token from existing scope created by previous call to * {@link #beamScopeIfNeeded()} * @param cs {@link CancellationSignal} for which token should be returned. * @return {@link IBinder} token. */ @NonNull public static IBinder beamFromScope(@NonNull CancellationSignal cs) { var state = sScope.get(); if (state != null) { var token = state.first.beam(cs); state.second.add(token); return token; } return null; } 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, MustClose { @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 @SuppressLint("VisiblySynchronized") 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}. */ @SuppressLint("VisiblySynchronized") 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}. */ @SuppressLint("VisiblySynchronized") 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"); } } }