blob: 167d128a76f80ec85a4c5e1d192f292645aa5814 [file] [log] [blame]
/*
* Copyright (C) 2019 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 com.android.internal.infra;
import android.annotation.CheckResult;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.IInterface;
import android.os.RemoteException;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.DebugUtils;
import android.util.Log;
import com.android.internal.util.function.pooled.PooledLambda;
import java.io.PrintWriter;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Function;
/**
* Takes care of managing a {@link ServiceConnection} and auto-disconnecting from the service upon
* a certain timeout.
*
* <p>
* The requests are always processed in the order they are scheduled.
*
* <p>
* Use {@link ServiceConnector.Impl} to construct an instance.
*
* @param <I> the type of the {@link IInterface ipc interface} for the remote service
*/
public interface ServiceConnector<I extends IInterface> {
/**
* Schedules to run a given job when service is connected, without providing any means to track
* the job's completion.
*
* <p>
* This is slightly more efficient than {@link #post(VoidJob)} as it doesn't require an extra
* allocation of a {@link AndroidFuture} for progress tracking.
*
* @return whether a job was successfully scheduled
*/
boolean run(@NonNull VoidJob<I> job);
/**
* Schedules to run a given job when service is connected.
*
* <p>
* You can choose to wait for the job synchronously using {@link AndroidFuture#get} or
* attach a listener to it using one of the options such as
* {@link AndroidFuture#whenComplete}
* You can also {@link AndroidFuture#cancel cancel} the pending job.
*
* @return a {@link AndroidFuture} tracking the job's completion
*
* @see #postForResult(Job) for a variant of this that also propagates an arbitrary result
* back to the caller
* @see CompletableFuture for more options on what you can do with a result of an asynchronous
* operation, including more advanced operations such as
* {@link CompletableFuture#thenApply transforming} its result,
* {@link CompletableFuture#thenCombine joining}
* results of multiple async operation into one,
* {@link CompletableFuture#thenCompose composing} results of
* multiple async operations that depend on one another, and more.
*/
@CheckResult(suggest = "#fireAndForget")
AndroidFuture<Void> post(@NonNull VoidJob<I> job);
/**
* Variant of {@link #post(VoidJob)} that also propagates an arbitrary result back to the
* caller asynchronously.
*
* @param <R> the type of the result this job produces
*
* @see #post(VoidJob)
*/
@CheckResult(suggest = "#fireAndForget")
<R> AndroidFuture<R> postForResult(@NonNull Job<I, R> job);
/**
* Schedules a job that is itself asynchronous, that is job returns a result in the form of a
* {@link CompletableFuture}
*
* <p>
* This takes care of "flattening" the nested futures that would have resulted from 2
* asynchronous operations performed in sequence.
*
* <p>
* Like with other options, {@link AndroidFuture#cancel cancelling} the resulting future
* will remove the job from the queue, preventing it from running if it hasn't yet started.
*
* @see #postForResult
* @see #post
*/
<R> AndroidFuture<R> postAsync(@NonNull Job<I, CompletableFuture<R>> job);
/**
* Requests to connect to the service without posting any meaningful job to run.
*
* <p>
* This returns a {@link AndroidFuture} tracking the progress of binding to the service,
* which can be used to schedule calls to the service once it's connected.
*
* <p>
* Avoid caching the resulting future as the instance may change due to service disconnecting
* and reconnecting.
*/
AndroidFuture<I> connect();
/**
* Request to unbind from the service as soon as possible.
*
* <p>
* If there are any pending jobs remaining they will be
* {@link AndroidFuture#cancel cancelled}.
*/
void unbind();
/**
* A request to be run when the service is
* {@link ServiceConnection#onServiceConnected connected}.
*
* @param <II> type of the {@link IInterface ipc interface} to be used
* @param <R> type of the return value
*
* @see VoidJob for a variant that doesn't produce any return value
*/
@FunctionalInterface
interface Job<II, R> {
/**
* Perform the remote call using the provided {@link IInterface ipc interface instance}.
*
* Avoid caching the provided {@code service} instance as it may become invalid when service
* disconnects.
*
* @return the result of this operation to be propagated to the original caller.
* If you do not need to provide a result you can implement {@link VoidJob} instead
*/
R run(@NonNull II service) throws Exception;
}
/**
* Variant of {@link Job} that doesn't return a result
*
* @param <II> see {@link Job}
*/
@FunctionalInterface
interface VoidJob<II> extends Job<II, Void> {
/** @see Job#run */
void runNoResult(II service) throws Exception;
@Override
default Void run(II service) throws Exception {
runNoResult(service);
return null;
}
}
/**
* Implementation of {@link ServiceConnector}
*
* <p>
* For allocation-efficiency reasons this implements a bunch of interfaces that are not meant to
* be a public API of {@link ServiceConnector}.
* For this reason prefer to use {@link ServiceConnector} instead of
* {@link ServiceConnector.Impl} as the field type when storing an instance.
*
* <p>
* In some rare cases you may want to extend this class, overriding certain methods for further
* flexibility.
* If you do, it would typically be one of the {@code protected} methods on this class.
*
* @param <I> see {@link ServiceConnector}
*/
class Impl<I extends IInterface> extends ArrayDeque<Job<I, ?>>
implements ServiceConnector<I>, ServiceConnection, IBinder.DeathRecipient, Runnable {
static final boolean DEBUG = false;
static final String LOG_TAG = "ServiceConnector.Impl";
private static final long DEFAULT_DISCONNECT_TIMEOUT_MS = 15_000;
private static final long DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
private final @NonNull Queue<Job<I, ?>> mQueue = this;
private final @NonNull List<CompletionAwareJob<I, ?>> mUnfinishedJobs = new ArrayList<>();
private final @NonNull ServiceConnection mServiceConnection = this;
private final @NonNull Runnable mTimeoutDisconnect = this;
protected final @NonNull Context mContext;
private final @NonNull Intent mIntent;
private final int mBindingFlags;
private final int mUserId;
private final @Nullable Function<IBinder, I> mBinderAsInterface;
private volatile I mService = null;
private boolean mBinding = false;
private boolean mUnbinding = false;
private CompletionAwareJob<I, I> mServiceConnectionFutureCache = null;
/**
* Creates an instance of {@link ServiceConnector}
*
* See {@code protected} methods for optional parameters you can override.
*
* @param context to be used for {@link Context#bindServiceAsUser binding} and
* {@link Context#unbindService unbinding}
* @param intent to be used for {@link Context#bindServiceAsUser binding}
* @param bindingFlags to be used for {@link Context#bindServiceAsUser binding}
* @param userId to be used for {@link Context#bindServiceAsUser binding}
* @param binderAsInterface to be used for converting an {@link IBinder} provided in
* {@link ServiceConnection#onServiceConnected} into a specific
* {@link IInterface}.
* Typically this is {@code IMyInterface.Stub::asInterface}
*/
public Impl(@NonNull Context context, @NonNull Intent intent,
@Context.BindServiceFlags int bindingFlags, @UserIdInt int userId,
@Nullable Function<IBinder, I> binderAsInterface) {
mContext = context;
mIntent = intent;
mBindingFlags = bindingFlags;
mUserId = userId;
mBinderAsInterface = binderAsInterface;
}
/**
* {@link Handler} on which {@link Job}s will be called
*/
protected Handler getJobHandler() {
return Handler.getMain();
}
/**
* Gets the amount of time spent without any calls before the service is automatically
* {@link Context#unbindService unbound}
*
* @return amount of time in ms, or non-positive (<=0) value to disable automatic unbinding
*/
protected long getAutoDisconnectTimeoutMs() {
return DEFAULT_DISCONNECT_TIMEOUT_MS;
}
/**
* Gets the amount of time to wait for a request to complete, before finishing it with a
* {@link java.util.concurrent.TimeoutException}
*
* <p>
* This includes time spent connecting to the service, if any.
*
* @return amount of time in ms
*/
protected long getRequestTimeoutMs() {
return DEFAULT_REQUEST_TIMEOUT_MS;
}
/**
* {@link Context#bindServiceAsUser Binds} to the service.
*
* <p>
* If overridden, implementation must use at least the provided {@link ServiceConnection}
*/
protected boolean bindService(
@NonNull ServiceConnection serviceConnection, @NonNull Handler handler) {
if (DEBUG) {
logTrace();
}
return mContext.bindServiceAsUser(mIntent, serviceConnection,
Context.BIND_AUTO_CREATE | mBindingFlags,
handler, UserHandle.of(mUserId));
}
/**
* Gets the binder interface.
* Typically {@code IMyInterface.Stub.asInterface(service)}.
*
* <p>
* Can be overridden instead of provided as a constructor parameter to save a singleton
* allocation
*/
protected I binderAsInterface(@NonNull IBinder service) {
return mBinderAsInterface.apply(service);
}
/**
* Called when service was {@link Context#unbindService unbound}
*
* <p>
* Can be overridden to perform some cleanup on service disconnect
*/
protected void onServiceUnbound() {
if (DEBUG) {
logTrace();
}
}
/**
* Called when the service just connected or is about to disconnect
*/
protected void onServiceConnectionStatusChanged(@NonNull I service, boolean isConnected) {}
@Override
public boolean run(@NonNull VoidJob<I> job) {
if (DEBUG) {
Log.d(LOG_TAG, "Wrapping fireAndForget job to take advantage of its mDebugName");
return !post(job).isCompletedExceptionally();
}
return enqueue(job);
}
@Override
public AndroidFuture<Void> post(@NonNull VoidJob<I> job) {
return postForResult((Job) job);
}
@Override
public <R> CompletionAwareJob<I, R> postForResult(@NonNull Job<I, R> job) {
CompletionAwareJob<I, R> task = new CompletionAwareJob<>();
task.mDelegate = Objects.requireNonNull(job);
enqueue(task);
return task;
}
@Override
public <R> AndroidFuture<R> postAsync(@NonNull Job<I, CompletableFuture<R>> job) {
CompletionAwareJob<I, R> task = new CompletionAwareJob<>();
task.mDelegate = Objects.requireNonNull((Job) job);
task.mAsync = true;
enqueue(task);
return task;
}
@Override
public synchronized AndroidFuture<I> connect() {
if (mServiceConnectionFutureCache == null) {
mServiceConnectionFutureCache = new CompletionAwareJob<>();
mServiceConnectionFutureCache.mDelegate = s -> s;
I service = mService;
if (service != null) {
mServiceConnectionFutureCache.complete(service);
} else {
enqueue(mServiceConnectionFutureCache);
}
}
return mServiceConnectionFutureCache;
}
private void enqueue(@NonNull CompletionAwareJob<I, ?> task) {
if (!enqueue((Job<I, ?>) task)) {
task.completeExceptionally(new IllegalStateException(
"Failed to post a job to handler. Likely "
+ getJobHandler().getLooper() + " is exiting"));
}
}
private boolean enqueue(@NonNull Job<I, ?> job) {
cancelTimeout();
return getJobHandler().sendMessage(PooledLambda.obtainMessage(
ServiceConnector.Impl::enqueueJobThread, this, job));
}
void enqueueJobThread(@NonNull Job<I, ?> job) {
if (DEBUG) {
Log.i(LOG_TAG, "post(" + job + ", this = " + this + ")");
}
cancelTimeout();
if (mUnbinding) {
completeExceptionally(job,
new IllegalStateException("Service is unbinding. Ignoring " + job));
} else if (!mQueue.offer(job)) {
completeExceptionally(job,
new IllegalStateException("Failed to add to queue: " + job));
} else if (isBound()) {
processQueue();
} else if (!mBinding) {
if (bindService(mServiceConnection, getJobHandler())) {
mBinding = true;
} else {
completeExceptionally(job,
new IllegalStateException("Failed to bind to service " + mIntent));
}
}
}
private void cancelTimeout() {
if (DEBUG) {
logTrace();
}
Handler.getMain().removeCallbacks(mTimeoutDisconnect);
}
void completeExceptionally(@NonNull Job<?, ?> job, @NonNull Throwable ex) {
CompletionAwareJob task = castOrNull(job, CompletionAwareJob.class);
boolean taskChanged = false;
if (task != null) {
taskChanged = task.completeExceptionally(ex);
}
if (task == null || (DEBUG && taskChanged)) {
Log.e(LOG_TAG, "Job failed: " + job, ex);
}
}
static @Nullable <BASE, T extends BASE> T castOrNull(
@Nullable BASE instance, @NonNull Class<T> cls) {
return cls.isInstance(instance) ? (T) instance : null;
}
private void processQueue() {
if (DEBUG) {
logTrace();
}
Job<I, ?> job;
while ((job = mQueue.poll()) != null) {
CompletionAwareJob task = castOrNull(job, CompletionAwareJob.class);
try {
I service = mService;
if (service == null) {
return;
}
Object result = job.run(service);
if (DEBUG) {
Log.i(LOG_TAG, "complete(" + job + ", result = " + result + ")");
}
if (task != null) {
if (task.mAsync) {
mUnfinishedJobs.add(task);
((CompletionStage) result).whenComplete(task);
} else {
task.complete(result);
}
}
} catch (Throwable e) {
completeExceptionally(job, e);
}
}
maybeScheduleUnbindTimeout();
}
private void maybeScheduleUnbindTimeout() {
if (mUnfinishedJobs.isEmpty() && mQueue.isEmpty()) {
scheduleUnbindTimeout();
}
}
private void scheduleUnbindTimeout() {
if (DEBUG) {
logTrace();
}
long timeout = getAutoDisconnectTimeoutMs();
if (timeout > 0) {
Handler.getMain().postDelayed(mTimeoutDisconnect, timeout);
} else if (DEBUG) {
Log.i(LOG_TAG, "Not scheduling unbind for permanently bound " + this);
}
}
private boolean isBound() {
return mService != null;
}
@Override
public void unbind() {
if (DEBUG) {
logTrace();
}
mUnbinding = true;
getJobHandler().sendMessage(PooledLambda.obtainMessage(Impl::unbindJobThread, this));
}
void unbindJobThread() {
cancelTimeout();
I service = mService;
boolean wasBound = service != null;
if (wasBound) {
onServiceConnectionStatusChanged(service, false);
mContext.unbindService(mServiceConnection);
service.asBinder().unlinkToDeath(this, 0);
mService = null;
}
mBinding = false;
mUnbinding = false;
synchronized (this) {
if (mServiceConnectionFutureCache != null) {
mServiceConnectionFutureCache.cancel(true);
mServiceConnectionFutureCache = null;
}
}
cancelPendingJobs();
if (wasBound) {
onServiceUnbound();
}
}
protected void cancelPendingJobs() {
Job<I, ?> job;
while ((job = mQueue.poll()) != null) {
if (DEBUG) {
Log.i(LOG_TAG, "cancel(" + job + ")");
}
CompletionAwareJob task = castOrNull(job, CompletionAwareJob.class);
if (task != null) {
task.cancel(/* mayInterruptWhileRunning= */ false);
}
}
}
@Override
public void onServiceConnected(@NonNull ComponentName name, @NonNull IBinder binder) {
if (mUnbinding) {
Log.i(LOG_TAG, "Ignoring onServiceConnected due to ongoing unbinding: " + this);
return;
}
if (DEBUG) {
logTrace();
}
I service = binderAsInterface(binder);
mService = service;
mBinding = false;
try {
binder.linkToDeath(ServiceConnector.Impl.this, 0);
} catch (RemoteException e) {
Log.e(LOG_TAG, "onServiceConnected " + name + ": ", e);
}
onServiceConnectionStatusChanged(service, true);
processQueue();
}
@Override
public void onServiceDisconnected(@NonNull ComponentName name) {
if (DEBUG) {
logTrace();
}
mBinding = true;
I service = mService;
if (service != null) {
onServiceConnectionStatusChanged(service, false);
mService = null;
}
}
@Override
public void onBindingDied(@NonNull ComponentName name) {
if (DEBUG) {
logTrace();
}
binderDied();
}
@Override
public void binderDied() {
if (DEBUG) {
logTrace();
}
mService = null;
unbind();
}
@Override
public void run() {
onTimeout();
}
private void onTimeout() {
if (DEBUG) {
logTrace();
}
unbind();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("ServiceConnector@")
.append(System.identityHashCode(this) % 1000).append("(")
.append(mIntent).append(", user: ").append(mUserId)
.append(")[").append(stateToString());
if (!mQueue.isEmpty()) {
sb.append(", ").append(mQueue.size()).append(" pending job(s)");
if (DEBUG) {
sb.append(": ").append(super.toString());
}
}
if (!mUnfinishedJobs.isEmpty()) {
sb.append(", ").append(mUnfinishedJobs.size()).append(" unfinished async job(s)");
}
return sb.append("]").toString();
}
public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
String tab = " ";
pw.append(prefix).append("ServiceConnector:").println();
pw.append(prefix).append(tab).append(String.valueOf(mIntent)).println();
pw.append(prefix).append(tab)
.append("userId: ").append(String.valueOf(mUserId)).println();
pw.append(prefix).append(tab)
.append("State: ").append(stateToString()).println();
pw.append(prefix).append(tab)
.append("Pending jobs: ").append(String.valueOf(mQueue.size())).println();
if (DEBUG) {
for (Job<I, ?> pendingJob : mQueue) {
pw.append(prefix).append(tab).append(tab)
.append(String.valueOf(pendingJob)).println();
}
}
pw.append(prefix).append(tab)
.append("Unfinished async jobs: ")
.append(String.valueOf(mUnfinishedJobs.size())).println();
}
private String stateToString() {
if (mBinding) {
return "Binding...";
} else if (mUnbinding) {
return "Unbinding...";
} else if (isBound()) {
return "Bound";
} else {
return "Unbound";
}
}
private void logTrace() {
Log.i(LOG_TAG,
TextUtils.join(" -> ",
DebugUtils.callersWithin(ServiceConnector.class, /* offset= */ 1))
+ "(" + this + ")");
}
/**
* {@link Job} + {@link AndroidFuture}
*/
class CompletionAwareJob<II, R> extends AndroidFuture<R>
implements Job<II, R>, BiConsumer<R, Throwable> {
Job<II, R> mDelegate;
boolean mAsync = false;
private String mDebugName;
{
long requestTimeout = getRequestTimeoutMs();
if (requestTimeout > 0) {
orTimeout(requestTimeout, TimeUnit.MILLISECONDS);
}
if (DEBUG) {
mDebugName = Arrays.stream(Thread.currentThread().getStackTrace())
.skip(2)
.filter(st ->
!st.getClassName().contains(ServiceConnector.class.getName()))
.findFirst()
.get()
.getMethodName();
}
}
@Override
public R run(@NonNull II service) throws Exception {
return mDelegate.run(service);
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
if (mayInterruptIfRunning) {
Log.w(LOG_TAG, "mayInterruptIfRunning not supported - ignoring");
}
boolean wasRemoved = mQueue.remove(this);
return super.cancel(mayInterruptIfRunning) || wasRemoved;
}
@Override
public String toString() {
if (DEBUG) {
return mDebugName;
}
return mDelegate + " wrapped into " + super.toString();
}
@Override
public void accept(@Nullable R res, @Nullable Throwable err) {
if (err != null) {
completeExceptionally(err);
} else {
complete(res);
}
}
@Override
protected void onCompleted(R res, Throwable err) {
super.onCompleted(res, err);
if (mUnfinishedJobs.remove(this)) {
maybeScheduleUnbindTimeout();
}
}
}
}
}