/*
* 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:
*
* - 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(@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");
}
}
}