Merge "Merge 25Q1 (ab/12770256) to aosp-main-future" into aosp-main-future
diff --git a/java/Android.bp b/java/Android.bp
index 88a9a6f..b1445d1 100644
--- a/java/Android.bp
+++ b/java/Android.bp
@@ -29,6 +29,7 @@
// Prefer using framework-annotations-lib wherever possible.
visibility: [
"//frameworks/base",
+ "//frameworks/base/tools/processors/property_cache",
"//frameworks/base/tools/processors/intdef_mappings",
],
}
@@ -126,6 +127,18 @@
],
}
+filegroup {
+ name: "module-utils-future-aidls",
+ srcs: [
+ "com/android/modules/utils/AndroidFuture.aidl",
+ "com/android/modules/utils/IAndroidFuture.aidl",
+ ],
+ visibility: [
+ "//packages/modules/NeuralNetworks/framework",
+ "//frameworks/libs/modules-utils/java/com/android/modules/utils",
+ ],
+}
+
// This file group is deprecated; new users should use modules-utils-preconditions
filegroup {
name: "modules-utils-preconditions-srcs",
diff --git a/java/com/android/internal/annotations/CacheModifier.java b/java/com/android/internal/annotations/CacheModifier.java
new file mode 100644
index 0000000..3908271
--- /dev/null
+++ b/java/com/android/internal/annotations/CacheModifier.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 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.annotations;
+
+public enum CacheModifier {
+ /**
+ * This modifier is used to indicate that the annotated method should be cached in as a static
+ * field. When STATIC is not present in
+ * {@link com.android.internal.annotations.CachedProperty#modsFlagOnOrNone} then generated cache
+ * field will not be static.
+ */
+ STATIC,
+}
diff --git a/java/com/android/internal/annotations/CachedProperty.java b/java/com/android/internal/annotations/CachedProperty.java
new file mode 100644
index 0000000..9a1b161
--- /dev/null
+++ b/java/com/android/internal/annotations/CachedProperty.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 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.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The annotation that generates boilerplate code required by {@link android.os.IpcDataCache}
+ * Instead of implementing IpcDataCache and adding the same code into multiple places within the
+ * same class, annotating method with CachedProperty generate the property and making sure it is
+ * thread safe if property is defined as static.
+ *
+ * To use this annotation on method, owning class needs to be annotated with
+ * {@link com.android.internal.annotations.CachedPropertyDefaults}
+ *
+ * <p>Need static IpcDataCache use @CachedProperty() or @CachedProperty(modifiers =
+ * {Modifier.STATIC}) in front of a method which calls a binder.
+ *
+ * <p>Need NON-static IpcDataCache use @CachedProperty(modifiers = {}) in front of a method which
+ * calls a binder.
+ *
+ * <p>Need to change the max capacity of cache or give custom API name use @CachedProperty(
+ * modifiers = {}, max = 1, apiName = "my_unique_key") in front of a method which calls a binder.
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target({ElementType.METHOD})
+public @interface CachedProperty {
+ /**
+ * The module under which the cache is registered {@link android.os.IpcDataCache.Config#module}.
+ * There are some well-known modules (such as {@link android.os.IpcDataCache.MODULE_SYSTEM}
+ * but any string is permitted. New modules needs to be registered.
+ * When the module is empty, then the module will be the same value as defined in
+ * CachedPropertyDefaults.
+ */
+ String module() default "";
+
+ /**
+ * The name of the {@link android.os.IpcDataCache.Config#api}
+ * When the api is empty, the api name will be the same value as defined in
+ * class level annotation {@link com.android.internal.annotations.CachedPropertyDefaults}.
+ */
+ String api() default "";
+
+ /**
+ * The maximum number of entries in the cache {@link android.os.IpcDataCache.Config#maxEntries}
+ * When the value is -1, the value will be the same value as defined in
+ * class level annotation {@link com.android.internal.annotations.CachedPropertyDefaults}.
+ */
+ int max() default -1;
+
+ /**
+ * Specify modifiers for generating cached property. By default it will be static property.
+ * This modifiers will apply when flag is on or does not exist.
+ * TODO: Add support for flag modifiers. b/361731022
+ */
+ CacheModifier[] modsFlagOnOrNone() default { CacheModifier.STATIC };
+}
+
diff --git a/java/com/android/internal/annotations/CachedPropertyDefaults.java b/java/com/android/internal/annotations/CachedPropertyDefaults.java
new file mode 100644
index 0000000..435c82b
--- /dev/null
+++ b/java/com/android/internal/annotations/CachedPropertyDefaults.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 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.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The annotation that generates default values for {@link CachedProperty} within class.
+ * This annotation is rquired on class level when intending to use {@link CachedProperty}
+ * annotation within that class.
+ *
+ * <p>To use, annotate the class with {@code @CachedPropertyDefaults}. By default it has a maximum
+ * capacity of 4 and stores in the "system_server" module
+ * {@link android.os.IpcDataCache.MODULE_SYSTEM}. Both parameters can be overwritten and will be
+ * used as default for each property inside of annotated class, eg:
+ * {@code @CachedPropertyDefaults(module = "my_custom_module", max=32)}
+ *
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.TYPE)
+public @interface CachedPropertyDefaults {
+ /**
+ * The module name under which the {@link android.os.IpcDataCache} will be registered, by default it is
+ * "system_server".
+ */
+ String module() default "system_server";
+
+ /**
+ * The default number of entries in the {@link android.os.IpcDataCache}.
+ */
+ int max() default 32;
+}
diff --git a/java/com/android/modules/expresslog/Counter.java b/java/com/android/modules/expresslog/Counter.java
index bcacb8b..fbe5e65 100644
--- a/java/com/android/modules/expresslog/Counter.java
+++ b/java/com/android/modules/expresslog/Counter.java
@@ -49,7 +49,9 @@
public static void logIncrement(@NonNull String metricId, long amount) {
final long metricIdHash =
MetricIds.getMetricIdHash(metricId, MetricIds.METRIC_TYPE_COUNTER);
- StatsExpressLog.write(StatsExpressLog.EXPRESS_EVENT_REPORTED, metricIdHash, amount);
+ if (metricIdHash != MetricIds.INVALID_METRIC_ID) {
+ StatsExpressLog.write(StatsExpressLog.EXPRESS_EVENT_REPORTED, metricIdHash, amount);
+ }
}
/**
@@ -61,7 +63,9 @@
public static void logIncrementWithUid(@NonNull String metricId, int uid, long amount) {
final long metricIdHash =
MetricIds.getMetricIdHash(metricId, MetricIds.METRIC_TYPE_COUNTER_WITH_UID);
- StatsExpressLog.write(
- StatsExpressLog.EXPRESS_UID_EVENT_REPORTED, metricIdHash, amount, uid);
+ if (metricIdHash != MetricIds.INVALID_METRIC_ID) {
+ StatsExpressLog.write(
+ StatsExpressLog.EXPRESS_UID_EVENT_REPORTED, metricIdHash, amount, uid);
+ }
}
}
diff --git a/java/com/android/modules/expresslog/Histogram.java b/java/com/android/modules/expresslog/Histogram.java
index 4f61c85..cab72b7 100644
--- a/java/com/android/modules/expresslog/Histogram.java
+++ b/java/com/android/modules/expresslog/Histogram.java
@@ -46,9 +46,11 @@
*/
public void logSample(float sample) {
final long hash = MetricIds.getMetricIdHash(mMetricId, MetricIds.METRIC_TYPE_HISTOGRAM);
- final int binIndex = mBinOptions.getBinForSample(sample);
- StatsExpressLog.write(
- StatsExpressLog.EXPRESS_HISTOGRAM_SAMPLE_REPORTED, hash, /*count*/ 1, binIndex);
+ if (hash != MetricIds.INVALID_METRIC_ID) {
+ final int binIndex = mBinOptions.getBinForSample(sample);
+ StatsExpressLog.write(
+ StatsExpressLog.EXPRESS_HISTOGRAM_SAMPLE_REPORTED, hash, /*count*/ 1, binIndex);
+ }
}
/**
@@ -60,13 +62,12 @@
public void logSampleWithUid(int uid, float sample) {
final long hash =
MetricIds.getMetricIdHash(mMetricId, MetricIds.METRIC_TYPE_HISTOGRAM_WITH_UID);
- final int binIndex = mBinOptions.getBinForSample(sample);
- StatsExpressLog.write(
- StatsExpressLog.EXPRESS_UID_HISTOGRAM_SAMPLE_REPORTED,
- hash, /*count*/
- 1,
- binIndex,
- uid);
+ if (hash != MetricIds.INVALID_METRIC_ID) {
+ final int binIndex = mBinOptions.getBinForSample(sample);
+ StatsExpressLog.write(StatsExpressLog.EXPRESS_UID_HISTOGRAM_SAMPLE_REPORTED,
+ hash, /*count*/
+ 1, binIndex, uid);
+ }
}
/** Used by Histogram to map data sample to corresponding bin */
diff --git a/java/com/android/modules/utils/Android.bp b/java/com/android/modules/utils/Android.bp
index cb4aca1..02fc4e9 100644
--- a/java/com/android/modules/utils/Android.bp
+++ b/java/com/android/modules/utils/Android.bp
@@ -109,3 +109,18 @@
defaults: ["modules-utils-defaults"],
srcs: ["BasicShellCommandHandler.java"],
}
+
+java_library {
+ name: "modules-utils-infra",
+ defaults: ["modules-utils-defaults"],
+ srcs: [
+ "AndroidFuture.java",
+ "ServiceConnector.java",
+ ":module-utils-future-aidls",
+ ],
+ static_libs: [
+ "modules-utils-handlerexecutor",
+ "modules-utils-preconditions",
+ ],
+ min_sdk_version: "33",
+}
diff --git a/java/com/android/modules/utils/AndroidFuture.aidl b/java/com/android/modules/utils/AndroidFuture.aidl
new file mode 100644
index 0000000..9a16cfa
--- /dev/null
+++ b/java/com/android/modules/utils/AndroidFuture.aidl
@@ -0,0 +1,20 @@
+/*
+** Copyright 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.modules.utils;
+
+/** @hide */
+@JavaOnlyStableParcelable parcelable AndroidFuture<T>;
\ No newline at end of file
diff --git a/java/com/android/modules/utils/AndroidFuture.java b/java/com/android/modules/utils/AndroidFuture.java
new file mode 100644
index 0000000..887f4a8
--- /dev/null
+++ b/java/com/android/modules/utils/AndroidFuture.java
@@ -0,0 +1,650 @@
+/*
+ * 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.modules.utils;
+
+import android.annotation.CallSuper;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import java.lang.reflect.Constructor;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * A customized {@link CompletableFuture} with focus on reducing the number of allocations involved
+ * in a typical future usage scenario for Android.
+ *
+ * <p>
+ * In particular this involves allocations optimizations in:
+ * <ul>
+ * <li>{@link #thenCompose(Function)}</li>
+ * <li>{@link #thenApply(Function)}</li>
+ * <li>{@link #thenCombine(CompletionStage, BiFunction)}</li>
+ * <li>{@link #orTimeout(long, TimeUnit)}</li>
+ * <li>{@link #whenComplete(BiConsumer)}</li>
+ * </ul>
+ * As well as their *Async versions.
+ *
+ * <p>
+ * You can pass {@link AndroidFuture} across an IPC.
+ * When doing so, completing the future on the other side will propagate the completion back,
+ * effectively acting as an error-aware remote callback.
+ *
+ * <p>
+ * {@link AndroidFuture} is {@link Parcelable} iff its wrapped type {@code T} is
+ * effectively parcelable, i.e. is supported by {@link Parcel#readValue}/{@link Parcel#writeValue}.
+ *
+ * @param <T> see {@link CompletableFuture}
+ * @hide
+ */
+public class AndroidFuture<T> extends CompletableFuture<T> implements Parcelable {
+
+ private static final boolean DEBUG = false;
+ private static final String LOG_TAG = AndroidFuture.class.getSimpleName();
+ private static final Executor DIRECT_EXECUTOR = Runnable::run;
+ private static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0];
+ private static @Nullable Handler sMainHandler;
+
+ private final @NonNull Object mLock = new Object();
+ @GuardedBy("mLock")
+ private @Nullable BiConsumer<? super T, ? super Throwable> mListener;
+ @GuardedBy("mLock")
+ private @Nullable Executor mListenerExecutor = DIRECT_EXECUTOR;
+ private @NonNull Handler mTimeoutHandler = getMainHandler();
+ private final @Nullable IAndroidFuture mRemoteOrigin;
+
+ public AndroidFuture() {
+ super();
+ mRemoteOrigin = null;
+ }
+
+ AndroidFuture(Parcel in) {
+ super();
+ if (in.readBoolean()) {
+ // Done
+ if (in.readBoolean()) {
+ // Failed
+ completeExceptionally(readThrowable(in));
+ } else {
+ // Success
+ complete((T) in.readValue(null));
+ }
+ mRemoteOrigin = null;
+ } else {
+ // Not done
+ mRemoteOrigin = IAndroidFuture.Stub.asInterface(in.readStrongBinder());
+ }
+ }
+
+ @NonNull
+ private static Handler getMainHandler() {
+ // This isn't thread-safe but we are okay with it.
+ if (sMainHandler == null) {
+ sMainHandler = new Handler(Looper.getMainLooper());
+ }
+ return sMainHandler;
+ }
+
+ /**
+ * Create a completed future with the given value.
+ *
+ * @param value the value for the completed future
+ * @param <U> the type of the value
+ * @return the completed future
+ */
+ @NonNull
+ public static <U> AndroidFuture<U> completedFuture(U value) {
+ AndroidFuture<U> future = new AndroidFuture<>();
+ future.complete(value);
+ return future;
+ }
+
+ @Override
+ public boolean complete(@Nullable T value) {
+ boolean changed = super.complete(value);
+ if (changed) {
+ onCompleted(value, null);
+ }
+ return changed;
+ }
+
+ @Override
+ public boolean completeExceptionally(@NonNull Throwable ex) {
+ boolean changed = super.completeExceptionally(ex);
+ if (changed) {
+ onCompleted(null, ex);
+ }
+ return changed;
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ boolean changed = super.cancel(mayInterruptIfRunning);
+ if (changed) {
+ try {
+ get();
+ throw new IllegalStateException("Expected CancellationException");
+ } catch (CancellationException ex) {
+ onCompleted(null, ex);
+ } catch (Throwable e) {
+ throw new IllegalStateException("Expected CancellationException", e);
+ }
+ }
+ return changed;
+ }
+
+ @CallSuper
+ protected void onCompleted(@Nullable T res, @Nullable Throwable err) {
+ cancelTimeout();
+
+ if (DEBUG) {
+ Log.i(LOG_TAG, this + " completed with result " + (err == null ? res : err),
+ new RuntimeException());
+ }
+
+ BiConsumer<? super T, ? super Throwable> listener;
+ synchronized (mLock) {
+ listener = mListener;
+ mListener = null;
+ }
+
+ if (listener != null) {
+ callListenerAsync(listener, res, err);
+ }
+
+ if (mRemoteOrigin != null) {
+ try {
+ mRemoteOrigin.complete(this /* resultContainer */);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Failed to propagate completion", e);
+ }
+ }
+ }
+
+ @Override
+ public AndroidFuture<T> whenComplete(@NonNull BiConsumer<? super T, ? super Throwable> action) {
+ return whenCompleteAsync(action, DIRECT_EXECUTOR);
+ }
+
+ @Override
+ public AndroidFuture<T> whenCompleteAsync(
+ @NonNull BiConsumer<? super T, ? super Throwable> action,
+ @NonNull Executor executor) {
+ Preconditions.checkNotNull(action);
+ Preconditions.checkNotNull(executor);
+ synchronized (mLock) {
+ if (!isDone()) {
+ BiConsumer<? super T, ? super Throwable> oldListener = mListener;
+
+ if (oldListener != null && executor != mListenerExecutor) {
+ // 2 listeners with different executors
+ // Too complex - give up on saving allocations and delegate to superclass
+ super.whenCompleteAsync(action, executor);
+ return this;
+ }
+
+ mListenerExecutor = executor;
+ mListener = oldListener == null
+ ? action
+ : (res, err) -> {
+ callListener(oldListener, res, err);
+ callListener(action, res, err);
+ };
+ return this;
+ }
+ }
+
+ // isDone() == true at this point
+ T res = null;
+ Throwable err = null;
+ try {
+ res = get();
+ } catch (ExecutionException e) {
+ err = e.getCause();
+ } catch (Throwable e) {
+ err = e;
+ }
+ callListenerAsync(action, res, err);
+ return this;
+ }
+
+ private void callListenerAsync(BiConsumer<? super T, ? super Throwable> listener,
+ @Nullable T res, @Nullable Throwable err) {
+ synchronized (mLock) {
+ if (mListenerExecutor == DIRECT_EXECUTOR) {
+ callListener(listener, res, err);
+ } else {
+ mListenerExecutor.execute(() -> callListener(listener, res, err));
+ }
+ }
+ }
+
+ /**
+ * Calls the provided listener, handling any exceptions that may arise.
+ */
+ // package-private to avoid synthetic method when called from lambda
+ static <TT> void callListener(
+ @NonNull BiConsumer<? super TT, ? super Throwable> listener,
+ @Nullable TT res, @Nullable Throwable err) {
+ try {
+ try {
+ listener.accept(res, err);
+ } catch (Throwable t) {
+ if (err == null) {
+ // listener happy-case threw, but exception case might not throw, so report the
+ // same exception thrown by listener's happy-path to it again
+ listener.accept(null, t);
+ } else {
+ // listener exception-case threw
+ // give up on listener but preserve the original exception when throwing up
+ t.addSuppressed(err);
+ throw t;
+ }
+ }
+ } catch (Throwable t2) {
+ // give up on listener and log the result & exception to logcat
+ Log.e(LOG_TAG, "Failed to call whenComplete listener. res = " + res, t2);
+ }
+ }
+
+ /** @inheritDoc */
+ //@Override //TODO uncomment once java 9 APIs are exposed to frameworks
+ public AndroidFuture<T> orTimeout(long timeout, @NonNull TimeUnit unit) {
+ mTimeoutHandler.postDelayed(this::triggerTimeout, this, unit.toMillis(timeout));
+ return this;
+ }
+
+ void triggerTimeout() {
+ cancelTimeout();
+ if (!isDone()) {
+ completeExceptionally(new TimeoutException());
+ }
+ }
+
+ /**
+ * Cancel all timeouts previously set with {@link #orTimeout}, if any.
+ *
+ * @return {@code this} for chaining
+ */
+ public AndroidFuture<T> cancelTimeout() {
+ mTimeoutHandler.removeCallbacksAndMessages(this);
+ return this;
+ }
+
+ /**
+ * Specifies the handler on which timeout is to be triggered
+ */
+ public AndroidFuture<T> setTimeoutHandler(@NonNull Handler h) {
+ cancelTimeout();
+ mTimeoutHandler = Preconditions.checkNotNull(h);
+ return this;
+ }
+
+ @Override
+ public <U> AndroidFuture<U> thenCompose(
+ @NonNull Function<? super T, ? extends CompletionStage<U>> fn) {
+ return thenComposeAsync(fn, DIRECT_EXECUTOR);
+ }
+
+ @Override
+ public <U> AndroidFuture<U> thenComposeAsync(
+ @NonNull Function<? super T, ? extends CompletionStage<U>> fn,
+ @NonNull Executor executor) {
+ return new ThenComposeAsync<>(this, fn, executor);
+ }
+
+ private static class ThenComposeAsync<T, U> extends AndroidFuture<U>
+ implements BiConsumer<Object, Throwable>, Runnable {
+ private volatile T mSourceResult = null;
+ private final Executor mExecutor;
+ private volatile Function<? super T, ? extends CompletionStage<U>> mFn;
+
+ ThenComposeAsync(@NonNull AndroidFuture<T> source,
+ @NonNull Function<? super T, ? extends CompletionStage<U>> fn,
+ @NonNull Executor executor) {
+ mFn = Preconditions.checkNotNull(fn);
+ mExecutor = Preconditions.checkNotNull(executor);
+
+ // subscribe to first job completion
+ source.whenComplete(this);
+ }
+
+ @Override
+ public void accept(Object res, Throwable err) {
+ if (err != null) {
+ // first or second job failed
+ completeExceptionally(err);
+ } else if (mFn != null) {
+ // first job completed
+ mSourceResult = (T) res;
+ // subscribe to second job completion asynchronously
+ mExecutor.execute(this);
+ } else {
+ // second job completed
+ complete((U) res);
+ }
+ }
+
+ @Override
+ public void run() {
+ CompletionStage<U> secondJob;
+ try {
+ secondJob = Preconditions.checkNotNull(mFn.apply(mSourceResult));
+ } catch (Throwable t) {
+ completeExceptionally(t);
+ return;
+ } finally {
+ // Marks first job complete
+ mFn = null;
+ }
+ // subscribe to second job completion
+ secondJob.whenComplete(this);
+ }
+ }
+
+ @Override
+ public <U> AndroidFuture<U> thenApply(@NonNull Function<? super T, ? extends U> fn) {
+ return thenApplyAsync(fn, DIRECT_EXECUTOR);
+ }
+
+ @Override
+ public <U> AndroidFuture<U> thenApplyAsync(@NonNull Function<? super T, ? extends U> fn,
+ @NonNull Executor executor) {
+ return new ThenApplyAsync<>(this, fn, executor);
+ }
+
+ private static class ThenApplyAsync<T, U> extends AndroidFuture<U>
+ implements BiConsumer<T, Throwable>, Runnable {
+ private volatile T mSourceResult = null;
+ private final Executor mExecutor;
+ private final Function<? super T, ? extends U> mFn;
+
+ ThenApplyAsync(@NonNull AndroidFuture<T> source,
+ @NonNull Function<? super T, ? extends U> fn,
+ @NonNull Executor executor) {
+ mExecutor = Preconditions.checkNotNull(executor);
+ mFn = Preconditions.checkNotNull(fn);
+
+ // subscribe to job completion
+ source.whenComplete(this);
+ }
+
+ @Override
+ public void accept(T res, Throwable err) {
+ if (err != null) {
+ completeExceptionally(err);
+ } else {
+ mSourceResult = res;
+ mExecutor.execute(this);
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ complete(mFn.apply(mSourceResult));
+ } catch (Throwable t) {
+ completeExceptionally(t);
+ }
+ }
+ }
+
+ @Override
+ public <U, V> AndroidFuture<V> thenCombine(
+ @NonNull CompletionStage<? extends U> other,
+ @NonNull BiFunction<? super T, ? super U, ? extends V> combineResults) {
+ return new ThenCombine<T, U, V>(this, other, combineResults);
+ }
+
+ /** @see CompletionStage#thenCombine */
+ public AndroidFuture<T> thenCombine(@NonNull CompletionStage<Void> other) {
+ return thenCombine(other, (res, aVoid) -> res);
+ }
+
+ private static class ThenCombine<T, U, V> extends AndroidFuture<V>
+ implements BiConsumer<Object, Throwable> {
+ private volatile @Nullable T mResultT = null;
+ private volatile @NonNull CompletionStage<? extends U> mSourceU;
+ private final @NonNull BiFunction<? super T, ? super U, ? extends V> mCombineResults;
+
+ ThenCombine(CompletableFuture<T> sourceT,
+ CompletionStage<? extends U> sourceU,
+ BiFunction<? super T, ? super U, ? extends V> combineResults) {
+ mSourceU = Preconditions.checkNotNull(sourceU);
+ mCombineResults = Preconditions.checkNotNull(combineResults);
+
+ sourceT.whenComplete(this);
+ }
+
+ @Override
+ public void accept(Object res, Throwable err) {
+ if (err != null) {
+ completeExceptionally(err);
+ return;
+ }
+
+ if (mSourceU != null) {
+ // T done
+ mResultT = (T) res;
+
+ // Subscribe to the second job completion.
+ mSourceU.whenComplete((r, e) -> {
+ // Mark the first job completion by setting mSourceU to null, so that next time
+ // the execution flow goes to the else case below.
+ mSourceU = null;
+ accept(r, e);
+ });
+ } else {
+ // U done
+ try {
+ complete(mCombineResults.apply(mResultT, (U) res));
+ } catch (Throwable t) {
+ completeExceptionally(t);
+ }
+ }
+ }
+ }
+
+ /**
+ * Similar to {@link CompletableFuture#supplyAsync} but
+ * runs the given action directly.
+ *
+ * The resulting future is immediately completed.
+ */
+ public static <T> AndroidFuture<T> supply(Supplier<T> supplier) {
+ return supplyAsync(supplier, DIRECT_EXECUTOR);
+ }
+
+ /**
+ * @see CompletableFuture#supplyAsync(Supplier, Executor)
+ */
+ public static <T> AndroidFuture<T> supplyAsync(Supplier<T> supplier, Executor executor) {
+ return new SupplyAsync<>(supplier, executor);
+ }
+
+ private static class SupplyAsync<T> extends AndroidFuture<T> implements Runnable {
+ private final @NonNull Supplier<T> mSupplier;
+
+ SupplyAsync(Supplier<T> supplier, Executor executor) {
+ mSupplier = supplier;
+ executor.execute(this);
+ }
+
+ @Override
+ public void run() {
+ try {
+ complete(mSupplier.get());
+ } catch (Throwable t) {
+ completeExceptionally(t);
+ }
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ boolean done = isDone();
+ dest.writeBoolean(done);
+ if (done) {
+ T result;
+ try {
+ result = get();
+ } catch (Throwable t) {
+ dest.writeBoolean(true);
+ writeThrowable(dest, unwrapExecutionException(t));
+ return;
+ }
+ dest.writeBoolean(false);
+ dest.writeValue(result);
+ } else {
+ dest.writeStrongBinder(new IAndroidFuture.Stub() {
+ @Override
+ public void complete(AndroidFuture resultContainer) {
+ boolean changed;
+ try {
+ changed = AndroidFuture.this.complete((T) resultContainer.get());
+ } catch (Throwable t) {
+ changed = completeExceptionally(unwrapExecutionException(t));
+ }
+ if (!changed) {
+ Log.w(LOG_TAG, "Remote result " + resultContainer
+ + " ignored, as local future is already completed: "
+ + AndroidFuture.this);
+ }
+ }
+ }.asBinder());
+ }
+ }
+
+ /**
+ * Exceptions coming out of {@link #get} are wrapped in {@link ExecutionException}
+ */
+ Throwable unwrapExecutionException(Throwable t) {
+ return t instanceof ExecutionException
+ ? t.getCause()
+ : t;
+ }
+
+ /**
+ * Alternative to {@link Parcel#writeException} that stores the stack trace, in a
+ * way consistent with the binder IPC exception propagation behavior.
+ */
+ private static void writeThrowable(@NonNull Parcel parcel, @Nullable Throwable throwable) {
+ boolean hasThrowable = throwable != null;
+ parcel.writeBoolean(hasThrowable);
+ if (!hasThrowable) {
+ return;
+ }
+
+ boolean isFrameworkParcelable = throwable instanceof Parcelable
+ && throwable.getClass().getClassLoader() == Parcelable.class.getClassLoader();
+ parcel.writeBoolean(isFrameworkParcelable);
+ if (isFrameworkParcelable) {
+ parcel.writeParcelable((Parcelable) throwable,
+ Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ return;
+ }
+
+ parcel.writeString(throwable.getClass().getName());
+ parcel.writeString(throwable.getMessage());
+ StackTraceElement[] stackTrace = throwable.getStackTrace();
+ StringBuilder stackTraceBuilder = new StringBuilder();
+ int truncatedStackTraceLength = Math.min(stackTrace != null ? stackTrace.length : 0, 5);
+ for (int i = 0; i < truncatedStackTraceLength; i++) {
+ if (i > 0) {
+ stackTraceBuilder.append('\n');
+ }
+ stackTraceBuilder.append("\tat ").append(stackTrace[i]);
+ }
+ parcel.writeString(stackTraceBuilder.toString());
+ writeThrowable(parcel, throwable.getCause());
+ }
+
+ /**
+ * @see #writeThrowable
+ */
+ @SuppressWarnings("UnsafeParcelApi")
+ private static @Nullable Throwable readThrowable(@NonNull Parcel parcel) {
+ final boolean hasThrowable = parcel.readBoolean();
+ if (!hasThrowable) {
+ return null;
+ }
+
+ boolean isFrameworkParcelable = parcel.readBoolean();
+ if (isFrameworkParcelable) {
+ return parcel.readParcelable(Parcelable.class.getClassLoader());
+ }
+
+ String className = parcel.readString();
+ String message = parcel.readString();
+ String stackTrace = parcel.readString();
+ String messageWithStackTrace = message + '\n' + stackTrace;
+ Throwable throwable;
+ try {
+ Class<?> clazz = Class.forName(className, true, Parcelable.class.getClassLoader());
+ if (Throwable.class.isAssignableFrom(clazz)) {
+ Constructor<?> constructor = clazz.getConstructor(String.class);
+ throwable = (Throwable) constructor.newInstance(messageWithStackTrace);
+ } else {
+ android.util.EventLog.writeEvent(0x534e4554, "186530450", -1, "");
+ throwable = new RuntimeException(className + ": " + messageWithStackTrace);
+ }
+ } catch (Throwable t) {
+ throwable = new RuntimeException(className + ": " + messageWithStackTrace);
+ throwable.addSuppressed(t);
+ }
+ throwable.setStackTrace(EMPTY_STACK_TRACE);
+ Throwable cause = readThrowable(parcel);
+ if (cause != null) {
+ throwable.initCause(cause);
+ }
+ return throwable;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @NonNull Parcelable.Creator<AndroidFuture> CREATOR =
+ new Parcelable.Creator<AndroidFuture>() {
+ public AndroidFuture createFromParcel(Parcel parcel) {
+ return new AndroidFuture(parcel);
+ }
+
+ public AndroidFuture[] newArray(int size) {
+ return new AndroidFuture[size];
+ }
+ };
+}
diff --git a/java/com/android/modules/utils/IAndroidFuture.aidl b/java/com/android/modules/utils/IAndroidFuture.aidl
new file mode 100644
index 0000000..bbf7daa
--- /dev/null
+++ b/java/com/android/modules/utils/IAndroidFuture.aidl
@@ -0,0 +1,24 @@
+/*
+** Copyright 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.modules.utils;
+
+import com.android.modules.utils.AndroidFuture;
+
+/** @hide */
+oneway interface IAndroidFuture {
+ void complete(in AndroidFuture resultContainer);
+}
\ No newline at end of file
diff --git a/java/com/android/modules/utils/ServiceConnector.java b/java/com/android/modules/utils/ServiceConnector.java
new file mode 100644
index 0000000..f50aab3
--- /dev/null
+++ b/java/com/android/modules/utils/ServiceConnector.java
@@ -0,0 +1,852 @@
+/*
+ * 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.modules.utils;
+
+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.Looper;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.Slog;
+
+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.Executor;
+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
+ * @hide
+ */
+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();
+
+ /**
+ * Registers a {@link ServiceLifecycleCallbacks callbacks} to be invoked when the lifecycle
+ * of the managed service changes.
+ *
+ * @param callbacks The callbacks that will be run, or {@code null} to clear the existing
+ * callbacks.
+ */
+ void setServiceLifecycleCallbacks(@Nullable ServiceLifecycleCallbacks<I> callbacks);
+
+ /**
+ * 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;
+ }
+ }
+
+ /**
+ * Collection of callbacks invoked when the lifecycle of the service changes.
+ *
+ * @param <II> the type of the {@link IInterface ipc interface} for the remote service
+ * @see ServiceConnector#setServiceLifecycleCallbacks(ServiceLifecycleCallbacks)
+ */
+ interface ServiceLifecycleCallbacks<II extends IInterface> {
+ /**
+ * Called when the service has just connected and before any queued jobs are run.
+ */
+ default void onConnected(@NonNull II service) {}
+
+ /**
+ * Called just before the service is disconnected and unbound.
+ */
+ default void onDisconnected(@NonNull II service) {}
+
+ /**
+ * Called when the service Binder has died.
+ *
+ * In cases where {@link #onBinderDied()} is invoked the service becomes unbound without
+ * a callback to {@link #onDisconnected(IInterface)}.
+ */
+ default void onBinderDied() {}
+ }
+
+
+ /**
+ * 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 Handler mMainHandler = new Handler(Looper.getMainLooper());
+ private final @NonNull ServiceConnection mServiceConnection = this;
+ private final @NonNull Runnable mTimeoutDisconnect = this;
+
+ // This context contains the user information.
+ protected final @NonNull Context mContext;
+ private final @NonNull Intent mIntent;
+ private final int mBindingFlags;
+ private final @Nullable Function<IBinder, I> mBinderAsInterface;
+ private final @NonNull Handler mHandler;
+ protected final @NonNull Executor mExecutor;
+
+ @Nullable
+ private volatile ServiceLifecycleCallbacks<I> mServiceLifecycleCallbacks = null;
+ 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, int bindingFlags,
+ @UserIdInt int userId, @Nullable Function<IBinder, I> binderAsInterface) {
+ mContext = context.createContextAsUser(UserHandle.of(userId), 0);
+ mIntent = intent;
+ mBindingFlags = bindingFlags;
+ mBinderAsInterface = binderAsInterface;
+
+ mHandler = getJobHandler();
+ mExecutor = new HandlerExecutor(mHandler);
+ }
+
+ /**
+ * {@link Handler} on which {@link Job}s will be called
+ */
+ protected Handler getJobHandler() {
+ return mMainHandler;
+ }
+
+ /**
+ * 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) {
+ if (DEBUG) {
+ logTrace();
+ }
+ return mContext.bindService(mIntent, Context.BIND_AUTO_CREATE | mBindingFlags,
+ mExecutor, serviceConnection);
+ }
+
+ /**
+ * 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();
+ }
+ }
+
+ private void dispatchOnServiceConnectionStatusChanged(
+ @NonNull I service, boolean isConnected) {
+ ServiceLifecycleCallbacks<I> serviceLifecycleCallbacks = mServiceLifecycleCallbacks;
+ if (serviceLifecycleCallbacks != null) {
+ if (isConnected) {
+ serviceLifecycleCallbacks.onConnected(service);
+ } else {
+ serviceLifecycleCallbacks.onDisconnected(service);
+ }
+ }
+ onServiceConnectionStatusChanged(service, isConnected);
+ }
+
+ /**
+ * 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 "
+ + mHandler.getLooper() + " is exiting"));
+ }
+ }
+
+ private boolean enqueue(@NonNull Job<I, ?> job) {
+ cancelTimeout();
+ return mHandler.post(() -> enqueueJobThread(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)) {
+ mBinding = true;
+ } else {
+ completeExceptionally(job,
+ new IllegalStateException("Failed to bind to service " + mIntent));
+ }
+ }
+ }
+
+ private void cancelTimeout() {
+ if (DEBUG) {
+ logTrace();
+ }
+ mMainHandler.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) {
+ mMainHandler.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;
+ mHandler.post(this::unbindJobThread);
+ }
+
+ @Override
+ public void setServiceLifecycleCallbacks(@Nullable ServiceLifecycleCallbacks<I> callbacks) {
+ mServiceLifecycleCallbacks = callbacks;
+ }
+
+ void unbindJobThread() {
+ cancelTimeout();
+ I service = mService;
+ // TODO(b/224695239): This is actually checking wasConnected. Rename and/or fix
+ // implementation based on what this should actually be checking. At least the first
+ // check for calling unbind is the correct behavior, though.
+ boolean wasBound = service != null;
+ if (wasBound || mBinding) {
+ try {
+ mContext.unbindService(mServiceConnection);
+ } catch (IllegalArgumentException e) { // TODO(b/224697137): Fix the race condition
+ // that requires catching this (crashes if
+ // service isn't currently bound).
+ Slog.e(LOG_TAG, "Failed to unbind: " + e);
+ }
+ }
+ if (wasBound) {
+ dispatchOnServiceConnectionStatusChanged(service, false);
+ 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);
+ }
+ dispatchOnServiceConnectionStatusChanged(service, true);
+ processQueue();
+ }
+
+ @Override
+ public void onServiceDisconnected(@NonNull ComponentName name) {
+ if (DEBUG) {
+ logTrace();
+ }
+ mBinding = true;
+ I service = mService;
+ if (service != null) {
+ dispatchOnServiceConnectionStatusChanged(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();
+ dispatchOnBinderDied();
+ }
+
+ private void dispatchOnBinderDied() {
+ ServiceLifecycleCallbacks<I> serviceLifecycleCallbacks = mServiceLifecycleCallbacks;
+ if (serviceLifecycleCallbacks != null) {
+ serviceLifecycleCallbacks.onBinderDied();
+ }
+ }
+
+ @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(mContext.getUser().getIdentifier())
+ .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(mContext.getUser().getIdentifier())).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, "See stacktrace", new Throwable());
+ }
+
+ /**
+ * {@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;
+ {
+ // The timeout handler must be set before any calls to set timeouts on the
+ // AndroidFuture, to ensure they are posted on the proper thread.
+ setTimeoutHandler(getJobHandler());
+
+ 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();
+ }
+ }
+ }
+ }
+
+ /**
+ * A {@link ServiceConnector} that doesn't connect to anything.
+ *
+ * @param <T> the type of the {@link IInterface ipc interface} for the remote service
+ */
+ class NoOp<T extends IInterface> extends AndroidFuture<Object> implements ServiceConnector<T> {
+ {
+ completeExceptionally(new IllegalStateException("ServiceConnector is a no-op"));
+ }
+
+ @Override
+ public boolean run(@NonNull VoidJob<T> job) {
+ return false;
+ }
+
+ @Override
+ public AndroidFuture<Void> post(@NonNull VoidJob<T> job) {
+ return (AndroidFuture) this;
+ }
+
+ @Override
+ public <R> AndroidFuture<R> postForResult(@NonNull Job<T, R> job) {
+ return (AndroidFuture) this;
+ }
+
+ @Override
+ public <R> AndroidFuture<R> postAsync(@NonNull Job<T, CompletableFuture<R>> job) {
+ return (AndroidFuture) this;
+ }
+
+ @Override
+ public AndroidFuture<T> connect() {
+ return (AndroidFuture) this;
+ }
+
+ @Override
+ public void unbind() {}
+
+ @Override
+ public void setServiceLifecycleCallbacks(@Nullable ServiceLifecycleCallbacks<T> callbacks) {
+ // Do nothing.
+ }
+ }
+}