summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/service/dreams/DreamService.java174
-rw-r--r--core/java/com/android/internal/util/ObservableServiceConnection.java258
-rw-r--r--core/java/com/android/internal/util/PersistentServiceConnection.java200
-rw-r--r--core/res/res/values/config.xml8
-rw-r--r--core/res/res/values/symbols.xml3
-rw-r--r--core/tests/utiltests/Android.bp1
-rw-r--r--core/tests/utiltests/src/com/android/internal/util/ObservableServiceConnectionTest.java226
-rw-r--r--core/tests/utiltests/src/com/android/internal/util/PersistentServiceConnectionTest.java215
8 files changed, 1010 insertions, 75 deletions
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 13913268cad0..3b7698e3954b 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -31,9 +31,9 @@ import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.graphics.drawable.Drawable;
@@ -68,6 +68,8 @@ import android.view.accessibility.AccessibilityEvent;
import com.android.internal.R;
import com.android.internal.util.DumpUtils;
+import com.android.internal.util.ObservableServiceConnection;
+import com.android.internal.util.PersistentServiceConnection;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -75,7 +77,8 @@ import org.xmlpull.v1.XmlPullParserException;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
-import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
@@ -225,6 +228,7 @@ public class DreamService extends Service implements Window.Callback {
/**
* The default value for whether to show complications on the overlay.
+ *
* @hide
*/
public static final boolean DEFAULT_SHOW_COMPLICATIONS = false;
@@ -251,77 +255,66 @@ public class DreamService extends Service implements Window.Callback {
private DreamServiceWrapper mDreamServiceWrapper;
private Runnable mDispatchAfterOnAttachedToWindow;
- private final OverlayConnection mOverlayConnection;
+ private OverlayConnection mOverlayConnection;
- private static class OverlayConnection implements ServiceConnection {
+ private static class OverlayConnection extends PersistentServiceConnection<IDreamOverlay> {
// Overlay set during onBind.
private IDreamOverlay mOverlay;
- // A Queue of pending requests to execute on the overlay.
- private final ArrayDeque<Consumer<IDreamOverlay>> mRequests;
-
- private boolean mBound;
-
- OverlayConnection() {
- mRequests = new ArrayDeque<>();
- }
-
- public void bind(Context context, @Nullable ComponentName overlayService,
- ComponentName dreamService) {
- if (overlayService == null) {
- return;
+ // A list of pending requests to execute on the overlay.
+ private final ArrayList<Consumer<IDreamOverlay>> mConsumers = new ArrayList<>();
+
+ private final Callback<IDreamOverlay> mCallback = new Callback<IDreamOverlay>() {
+ @Override
+ public void onConnected(ObservableServiceConnection<IDreamOverlay> connection,
+ IDreamOverlay service) {
+ mOverlay = service;
+ for (Consumer<IDreamOverlay> consumer : mConsumers) {
+ consumer.accept(mOverlay);
+ }
}
- final ServiceInfo serviceInfo = fetchServiceInfo(context, dreamService);
-
- final Intent overlayIntent = new Intent();
- overlayIntent.setComponent(overlayService);
- overlayIntent.putExtra(EXTRA_SHOW_COMPLICATIONS,
- fetchShouldShowComplications(context, serviceInfo));
- overlayIntent.putExtra(EXTRA_DREAM_COMPONENT, dreamService);
-
- context.bindService(overlayIntent,
- this, Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE);
- mBound = true;
- }
-
- public void unbind(Context context) {
- if (!mBound) {
- return;
+ @Override
+ public void onDisconnected(ObservableServiceConnection<IDreamOverlay> connection,
+ int reason) {
+ mOverlay = null;
}
+ };
- context.unbindService(this);
- mBound = false;
+ OverlayConnection(Context context,
+ Executor executor,
+ Handler handler,
+ ServiceTransformer<IDreamOverlay> transformer,
+ Intent serviceIntent,
+ int flags,
+ int minConnectionDurationMs,
+ int maxReconnectAttempts,
+ int baseReconnectDelayMs) {
+ super(context, executor, handler, transformer, serviceIntent, flags,
+ minConnectionDurationMs,
+ maxReconnectAttempts, baseReconnectDelayMs);
}
- public void request(Consumer<IDreamOverlay> request) {
- mRequests.push(request);
- evaluate();
+ @Override
+ public boolean bind() {
+ addCallback(mCallback);
+ return super.bind();
}
- private void evaluate() {
- if (mOverlay == null) {
- return;
- }
-
- // Any new requests that arrive during this loop will be processed synchronously after
- // the loop exits.
- while (!mRequests.isEmpty()) {
- final Consumer<IDreamOverlay> request = mRequests.pop();
- request.accept(mOverlay);
- }
+ @Override
+ public void unbind() {
+ removeCallback(mCallback);
+ super.unbind();
}
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- // Store Overlay and execute pending requests.
- mOverlay = IDreamOverlay.Stub.asInterface(service);
- evaluate();
+ public void addConsumer(Consumer<IDreamOverlay> consumer) {
+ mConsumers.add(consumer);
+ if (mOverlay != null) {
+ consumer.accept(mOverlay);
+ }
}
- @Override
- public void onServiceDisconnected(ComponentName name) {
- // Clear Overlay binder to prevent further request processing.
- mOverlay = null;
+ public void removeConsumer(Consumer<IDreamOverlay> consumer) {
+ mConsumers.remove(consumer);
}
}
@@ -336,7 +329,6 @@ public class DreamService extends Service implements Window.Callback {
public DreamService() {
mDreamManager = IDreamManager.Stub.asInterface(ServiceManager.getService(DREAM_SERVICE));
- mOverlayConnection = new OverlayConnection();
}
/**
@@ -532,7 +524,7 @@ public class DreamService extends Service implements Window.Callback {
return mWindow;
}
- /**
+ /**
* Inflates a layout resource and set it to be the content view for this Dream.
* Behaves similarly to {@link android.app.Activity#setContentView(int)}.
*
@@ -996,13 +988,33 @@ public class DreamService extends Service implements Window.Callback {
public final IBinder onBind(Intent intent) {
if (mDebug) Slog.v(mTag, "onBind() intent = " + intent);
mDreamServiceWrapper = new DreamServiceWrapper();
+ final ComponentName overlayComponent = intent.getParcelableExtra(
+ EXTRA_DREAM_OVERLAY_COMPONENT, ComponentName.class);
// Connect to the overlay service if present.
- if (!mWindowless) {
- mOverlayConnection.bind(
+ if (!mWindowless && overlayComponent != null) {
+ final Resources resources = getResources();
+ final ComponentName dreamService = new ComponentName(this, getClass());
+
+ final ServiceInfo serviceInfo = fetchServiceInfo(this, dreamService);
+ final Intent overlayIntent = new Intent()
+ .setComponent(overlayComponent)
+ .putExtra(EXTRA_SHOW_COMPLICATIONS,
+ fetchShouldShowComplications(this, serviceInfo))
+ .putExtra(EXTRA_DREAM_COMPONENT, dreamService);
+
+ mOverlayConnection = new OverlayConnection(
/* context= */ this,
- intent.getParcelableExtra(EXTRA_DREAM_OVERLAY_COMPONENT, android.content.ComponentName.class),
- new ComponentName(this, getClass()));
+ getMainExecutor(),
+ mHandler,
+ IDreamOverlay.Stub::asInterface,
+ overlayIntent,
+ /* flags= */ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
+ resources.getInteger(R.integer.config_minDreamOverlayDurationMs),
+ resources.getInteger(R.integer.config_dreamOverlayMaxReconnectAttempts),
+ resources.getInteger(R.integer.config_dreamOverlayReconnectTimeoutMs));
+
+ mOverlayConnection.bind();
}
return mDreamServiceWrapper;
@@ -1011,7 +1023,9 @@ public class DreamService extends Service implements Window.Callback {
@Override
public boolean onUnbind(Intent intent) {
// We must unbind from any overlay connection if we are unbound before finishing.
- mOverlayConnection.unbind(this);
+ if (mOverlayConnection != null) {
+ mOverlayConnection.unbind();
+ }
return super.onUnbind(intent);
}
@@ -1040,7 +1054,9 @@ public class DreamService extends Service implements Window.Callback {
}
mFinished = true;
- mOverlayConnection.unbind(this);
+ if (mOverlayConnection != null) {
+ mOverlayConnection.unbind();
+ }
if (mDreamToken == null) {
Slog.w(mTag, "Finish was called before the dream was attached.");
@@ -1337,19 +1353,24 @@ public class DreamService extends Service implements Window.Callback {
mWindow.getDecorView().addOnAttachStateChangeListener(
new View.OnAttachStateChangeListener() {
+ private Consumer<IDreamOverlay> mDreamStartOverlayConsumer;
+
@Override
public void onViewAttachedToWindow(View v) {
mDispatchAfterOnAttachedToWindow.run();
- // Request the DreamOverlay be told to dream with dream's window parameters
- // once the window has been attached.
- mOverlayConnection.request(overlay -> {
- try {
- overlay.startDream(mWindow.getAttributes(), mOverlayCallback);
- } catch (RemoteException e) {
- Log.e(mTag, "could not send window attributes:" + e);
- }
- });
+ if (mOverlayConnection != null) {
+ // Request the DreamOverlay be told to dream with dream's window
+ // parameters once the window has been attached.
+ mDreamStartOverlayConsumer = overlay -> {
+ try {
+ overlay.startDream(mWindow.getAttributes(), mOverlayCallback);
+ } catch (RemoteException e) {
+ Log.e(mTag, "could not send window attributes:" + e);
+ }
+ };
+ mOverlayConnection.addConsumer(mDreamStartOverlayConsumer);
+ }
}
@Override
@@ -1362,6 +1383,9 @@ public class DreamService extends Service implements Window.Callback {
mActivity = null;
finish();
}
+ if (mOverlayConnection != null && mDreamStartOverlayConsumer != null) {
+ mOverlayConnection.removeConsumer(mDreamStartOverlayConsumer);
+ }
}
});
}
diff --git a/core/java/com/android/internal/util/ObservableServiceConnection.java b/core/java/com/android/internal/util/ObservableServiceConnection.java
new file mode 100644
index 000000000000..3165d293bd91
--- /dev/null
+++ b/core/java/com/android/internal/util/ObservableServiceConnection.java
@@ -0,0 +1,258 @@
+/*
+ * 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 com.android.internal.util;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.CallbackRegistry.NotifierCallback;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
+
+/**
+ * {@link ObservableServiceConnection} is a concrete implementation of {@link ServiceConnection}
+ * that enables monitoring the status of a binder connection. It also aides in automatically
+ * converting a proxy into an internal wrapper type.
+ *
+ * @param <T> The type of the wrapper over the resulting service.
+ */
+public class ObservableServiceConnection<T> implements ServiceConnection {
+ /**
+ * An interface for converting the service proxy into a given internal wrapper type.
+ *
+ * @param <T> The type of the wrapper over the resulting service.
+ */
+ public interface ServiceTransformer<T> {
+ /**
+ * Called to convert the service proxy to the wrapper type.
+ *
+ * @param service The service proxy to create the wrapper type from.
+ * @return The wrapper type.
+ */
+ T convert(IBinder service);
+ }
+
+ /**
+ * An interface for listening to the connection status.
+ *
+ * @param <T> The wrapper type.
+ */
+ public interface Callback<T> {
+ /**
+ * Invoked when the service has been successfully connected to.
+ *
+ * @param connection The {@link ObservableServiceConnection} instance that is now connected
+ * @param service The service proxy converted into the typed wrapper.
+ */
+ void onConnected(ObservableServiceConnection<T> connection, T service);
+
+ /**
+ * Invoked when the service has been disconnected.
+ *
+ * @param connection The {@link ObservableServiceConnection} that is now disconnected.
+ * @param reason The reason for the disconnection.
+ */
+ void onDisconnected(ObservableServiceConnection<T> connection,
+ @DisconnectReason int reason);
+ }
+
+ /**
+ * Default state, service has not yet disconnected.
+ */
+ public static final int DISCONNECT_REASON_NONE = 0;
+ /**
+ * Disconnection was due to the resulting binding being {@code null}.
+ */
+ public static final int DISCONNECT_REASON_NULL_BINDING = 1;
+ /**
+ * Disconnection was due to the remote end disconnecting.
+ */
+ public static final int DISCONNECT_REASON_DISCONNECTED = 2;
+ /**
+ * Disconnection due to the binder dying.
+ */
+ public static final int DISCONNECT_REASON_BINDING_DIED = 3;
+ /**
+ * Disconnection from an explicit unbinding.
+ */
+ public static final int DISCONNECT_REASON_UNBIND = 4;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ DISCONNECT_REASON_NONE,
+ DISCONNECT_REASON_NULL_BINDING,
+ DISCONNECT_REASON_DISCONNECTED,
+ DISCONNECT_REASON_BINDING_DIED,
+ DISCONNECT_REASON_UNBIND
+ })
+ public @interface DisconnectReason {
+ }
+
+ private final Object mLock = new Object();
+ private final Context mContext;
+ private final Executor mExecutor;
+ private final ServiceTransformer<T> mTransformer;
+ private final Intent mServiceIntent;
+ private final int mFlags;
+
+ @GuardedBy("mLock")
+ private T mService;
+ @GuardedBy("mLock")
+ private boolean mBoundCalled = false;
+ @GuardedBy("mLock")
+ private int mLastDisconnectReason = DISCONNECT_REASON_NONE;
+
+ private final CallbackRegistry<Callback<T>, ObservableServiceConnection<T>, T>
+ mCallbackRegistry = new CallbackRegistry<>(
+ new NotifierCallback<Callback<T>, ObservableServiceConnection<T>, T>() {
+ @Override
+ public void onNotifyCallback(Callback<T> callback,
+ ObservableServiceConnection<T> sender,
+ int disconnectReason, T service) {
+ mExecutor.execute(() -> {
+ synchronized (mLock) {
+ if (service != null) {
+ callback.onConnected(sender, service);
+ } else if (mLastDisconnectReason != DISCONNECT_REASON_NONE) {
+ callback.onDisconnected(sender, disconnectReason);
+ }
+ }
+ });
+ }
+ });
+
+ /**
+ * Default constructor for {@link ObservableServiceConnection}.
+ *
+ * @param context The context from which the service will be bound with.
+ * @param executor The executor for connection callbacks to be delivered on
+ * @param transformer A {@link ObservableServiceConnection.ServiceTransformer} for transforming
+ * the resulting service into a desired type.
+ */
+ public ObservableServiceConnection(@NonNull Context context,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull ServiceTransformer<T> transformer,
+ Intent serviceIntent,
+ int flags) {
+ mContext = context;
+ mExecutor = executor;
+ mTransformer = transformer;
+ mServiceIntent = serviceIntent;
+ mFlags = flags;
+ }
+
+ /**
+ * Initiate binding to the service.
+ *
+ * @return {@code true} if initiating binding succeed, {@code false} if the binding failed or
+ * if this service is already bound. Regardless of the return value, you should later call
+ * {@link #unbind()} to release the connection.
+ */
+ public boolean bind() {
+ synchronized (mLock) {
+ if (mBoundCalled) {
+ return false;
+ }
+ final boolean bindResult =
+ mContext.bindService(mServiceIntent, mFlags, mExecutor, this);
+ mBoundCalled = true;
+ return bindResult;
+ }
+ }
+
+ /**
+ * Disconnect from the service if bound.
+ */
+ public void unbind() {
+ onDisconnected(DISCONNECT_REASON_UNBIND);
+ }
+
+ /**
+ * Adds a callback for receiving connection updates.
+ *
+ * @param callback The {@link Callback} to receive future updates.
+ */
+ public void addCallback(Callback<T> callback) {
+ mCallbackRegistry.add(callback);
+ mExecutor.execute(() -> {
+ synchronized (mLock) {
+ if (mService != null) {
+ callback.onConnected(this, mService);
+ } else if (mLastDisconnectReason != DISCONNECT_REASON_NONE) {
+ callback.onDisconnected(this, mLastDisconnectReason);
+ }
+ }
+ });
+ }
+
+ /**
+ * Removes previously added callback from receiving future connection updates.
+ *
+ * @param callback The {@link Callback} to be removed.
+ */
+ public void removeCallback(Callback<T> callback) {
+ synchronized (mLock) {
+ mCallbackRegistry.remove(callback);
+ }
+ }
+
+ private void onDisconnected(@DisconnectReason int reason) {
+ synchronized (mLock) {
+ if (!mBoundCalled) {
+ return;
+ }
+ mBoundCalled = false;
+ mLastDisconnectReason = reason;
+ mContext.unbindService(this);
+ mService = null;
+ mCallbackRegistry.notifyCallbacks(this, reason, null);
+ }
+ }
+
+ @Override
+ public final void onServiceConnected(ComponentName name, IBinder service) {
+ synchronized (mLock) {
+ mService = mTransformer.convert(service);
+ mLastDisconnectReason = DISCONNECT_REASON_NONE;
+ mCallbackRegistry.notifyCallbacks(this, mLastDisconnectReason, mService);
+ }
+ }
+
+ @Override
+ public final void onServiceDisconnected(ComponentName name) {
+ onDisconnected(DISCONNECT_REASON_DISCONNECTED);
+ }
+
+ @Override
+ public final void onBindingDied(ComponentName name) {
+ onDisconnected(DISCONNECT_REASON_BINDING_DIED);
+ }
+
+ @Override
+ public final void onNullBinding(ComponentName name) {
+ onDisconnected(DISCONNECT_REASON_NULL_BINDING);
+ }
+}
diff --git a/core/java/com/android/internal/util/PersistentServiceConnection.java b/core/java/com/android/internal/util/PersistentServiceConnection.java
new file mode 100644
index 000000000000..d2017347bd64
--- /dev/null
+++ b/core/java/com/android/internal/util/PersistentServiceConnection.java
@@ -0,0 +1,200 @@
+/*
+ * 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 com.android.internal.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.SystemClock;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.concurrent.Executor;
+
+/**
+ * {@link PersistentServiceConnection} is a concrete implementation of {@link ServiceConnection}
+ * that maintains the binder connection by handling reconnection when a failure occurs.
+ *
+ * @param <T> The transformed connection type handled by the service.
+ *
+ * When the target process is killed (by OOM-killer, force-stopped, crash, etc..) then this class
+ * will trigger a reconnection to the target. This should be used carefully.
+ *
+ * NOTE: This class does *not* handle package-updates -- i.e. even if the binding dies due to
+ * the target package being updated, this class won't reconnect. This is because this class doesn't
+ * know what to do when the service component has gone missing, for example. If the user of this
+ * class wants to restore the connection, then it should call {@link #unbind()} and {@link #bind}
+ * explicitly.
+ */
+public class PersistentServiceConnection<T> extends ObservableServiceConnection<T> {
+ private final Callback<T> mConnectionCallback = new Callback<T>() {
+ private long mConnectedTime;
+
+ @Override
+ public void onConnected(ObservableServiceConnection<T> connection, T service) {
+ mConnectedTime = mInjector.uptimeMillis();
+ }
+
+ @Override
+ public void onDisconnected(ObservableServiceConnection<T> connection,
+ @DisconnectReason int reason) {
+ if (reason == DISCONNECT_REASON_UNBIND) return;
+ synchronized (mLock) {
+ if ((mInjector.uptimeMillis() - mConnectedTime) > mMinConnectionDurationMs) {
+ mReconnectAttempts = 0;
+ bindInternalLocked();
+ } else {
+ scheduleConnectionAttemptLocked();
+ }
+ }
+ }
+ };
+
+ private final Object mLock = new Object();
+ private final Injector mInjector;
+ private final Handler mHandler;
+ private final int mMinConnectionDurationMs;
+ private final int mMaxReconnectAttempts;
+ private final int mBaseReconnectDelayMs;
+ @GuardedBy("mLock")
+ private int mReconnectAttempts;
+ @GuardedBy("mLock")
+ private Object mCancelToken;
+
+ private final Runnable mConnectRunnable = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mLock) {
+ mCancelToken = null;
+ bindInternalLocked();
+ }
+ }
+ };
+
+ /**
+ * Default constructor for {@link PersistentServiceConnection}.
+ *
+ * @param context The context from which the service will be bound with.
+ * @param executor The executor for connection callbacks to be delivered on
+ * @param transformer A {@link ServiceTransformer} for transforming
+ */
+ public PersistentServiceConnection(Context context,
+ Executor executor,
+ Handler handler,
+ ServiceTransformer<T> transformer,
+ Intent serviceIntent,
+ int flags,
+ int minConnectionDurationMs,
+ int maxReconnectAttempts,
+ int baseReconnectDelayMs) {
+ this(context,
+ executor,
+ handler,
+ transformer,
+ serviceIntent,
+ flags,
+ minConnectionDurationMs,
+ maxReconnectAttempts,
+ baseReconnectDelayMs,
+ new Injector());
+ }
+
+ @VisibleForTesting
+ public PersistentServiceConnection(
+ Context context,
+ Executor executor,
+ Handler handler,
+ ServiceTransformer<T> transformer,
+ Intent serviceIntent,
+ int flags,
+ int minConnectionDurationMs,
+ int maxReconnectAttempts,
+ int baseReconnectDelayMs,
+ Injector injector) {
+ super(context, executor, transformer, serviceIntent, flags);
+ mHandler = handler;
+ mMinConnectionDurationMs = minConnectionDurationMs;
+ mMaxReconnectAttempts = maxReconnectAttempts;
+ mBaseReconnectDelayMs = baseReconnectDelayMs;
+ mInjector = injector;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean bind() {
+ synchronized (mLock) {
+ addCallback(mConnectionCallback);
+ mReconnectAttempts = 0;
+ return bindInternalLocked();
+ }
+ }
+
+ @GuardedBy("mLock")
+ private boolean bindInternalLocked() {
+ return super.bind();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void unbind() {
+ synchronized (mLock) {
+ removeCallback(mConnectionCallback);
+ cancelPendingConnectionAttemptLocked();
+ super.unbind();
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void cancelPendingConnectionAttemptLocked() {
+ if (mCancelToken != null) {
+ mHandler.removeCallbacksAndMessages(mCancelToken);
+ mCancelToken = null;
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void scheduleConnectionAttemptLocked() {
+ cancelPendingConnectionAttemptLocked();
+
+ if (mReconnectAttempts >= mMaxReconnectAttempts) {
+ return;
+ }
+
+ final long reconnectDelayMs =
+ (long) Math.scalb(mBaseReconnectDelayMs, mReconnectAttempts);
+
+ mCancelToken = new Object();
+ mHandler.postDelayed(mConnectRunnable, mCancelToken, reconnectDelayMs);
+ mReconnectAttempts++;
+ }
+
+ /**
+ * Injector for testing
+ */
+ @VisibleForTesting
+ public static class Injector {
+ /**
+ * Returns milliseconds since boot, not counting time spent in deep sleep. Can be overridden
+ * in tests with a fake clock.
+ */
+ public long uptimeMillis() {
+ return SystemClock.uptimeMillis();
+ }
+ }
+}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 47faf2a2ebf7..38344788a100 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -553,6 +553,14 @@
<!-- If this is true, then keep dreaming when undocking. -->
<bool name="config_keepDreamingWhenUndocking">false</bool>
+ <!-- The timeout (in ms) to wait before attempting to reconnect to the dream overlay service if
+ it becomes disconnected -->
+ <integer name="config_dreamOverlayReconnectTimeoutMs">1000</integer> <!-- 1 second -->
+ <!-- The maximum number of times to attempt reconnecting to the dream overlay service -->
+ <integer name="config_dreamOverlayMaxReconnectAttempts">3</integer>
+ <!-- The duration after which the dream overlay connection should be considered stable -->
+ <integer name="config_minDreamOverlayDurationMs">10000</integer> <!-- 10 seconds -->
+
<!-- Auto-rotation behavior -->
<!-- If true, enables auto-rotation features using the accelerometer.
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b42db13e1ac2..2fbf8034c27e 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2238,6 +2238,9 @@
<java-symbol type="array" name="config_supportedDreamComplications" />
<java-symbol type="array" name="config_disabledDreamComponents" />
<java-symbol type="bool" name="config_dismissDreamOnActivityStart" />
+ <java-symbol type="integer" name="config_dreamOverlayReconnectTimeoutMs" />
+ <java-symbol type="integer" name="config_dreamOverlayMaxReconnectAttempts" />
+ <java-symbol type="integer" name="config_minDreamOverlayDurationMs" />
<java-symbol type="string" name="config_loggable_dream_prefix" />
<java-symbol type="string" name="config_dozeComponent" />
<java-symbol type="string" name="enable_explore_by_touch_warning_title" />
diff --git a/core/tests/utiltests/Android.bp b/core/tests/utiltests/Android.bp
index adc3676f7b93..3798da592cd5 100644
--- a/core/tests/utiltests/Android.bp
+++ b/core/tests/utiltests/Android.bp
@@ -34,6 +34,7 @@ android_test {
"mockito-target-minus-junit4",
"androidx.test.ext.junit",
"truth-prebuilt",
+ "servicestests-utils",
],
libs: [
diff --git a/core/tests/utiltests/src/com/android/internal/util/ObservableServiceConnectionTest.java b/core/tests/utiltests/src/com/android/internal/util/ObservableServiceConnectionTest.java
new file mode 100644
index 000000000000..d124ad9ddfb0
--- /dev/null
+++ b/core/tests/utiltests/src/com/android/internal/util/ObservableServiceConnectionTest.java
@@ -0,0 +1,226 @@
+/*
+ * 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 com.android.internal.util;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.util.ObservableServiceConnection.ServiceTransformer;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayDeque;
+import java.util.Objects;
+import java.util.Queue;
+import java.util.concurrent.Executor;
+
+@SmallTest
+public class ObservableServiceConnectionTest {
+ private static final ComponentName COMPONENT_NAME =
+ new ComponentName("test.package", "component");
+
+ public static class Foo {
+ int mValue;
+
+ Foo(int value) {
+ mValue = value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Foo)) return false;
+ Foo foo = (Foo) o;
+ return mValue == foo.mValue;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mValue);
+ }
+ }
+
+
+ @Mock
+ private Context mContext;
+ @Mock
+ private Intent mIntent;
+ @Mock
+ private Foo mResult;
+ @Mock
+ private IBinder mBinder;
+ @Mock
+ private ServiceTransformer<Foo> mTransformer;
+ @Mock
+ private ObservableServiceConnection.Callback<Foo> mCallback;
+ private final FakeExecutor mExecutor = new FakeExecutor();
+ private ObservableServiceConnection<Foo> mConnection;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mConnection = new ObservableServiceConnection<>(
+ mContext,
+ mExecutor,
+ mTransformer,
+ mIntent,
+ /* flags= */ Context.BIND_AUTO_CREATE);
+ }
+
+ @After
+ public void tearDown() {
+ mExecutor.clearAll();
+ }
+
+ @Test
+ public void testConnect() {
+ // Register twice to ensure only one callback occurs.
+ mConnection.addCallback(mCallback);
+ mConnection.addCallback(mCallback);
+
+ mExecutor.runAll();
+ mConnection.bind();
+
+ // Ensure that no callbacks happen before connection.
+ verify(mCallback, never()).onConnected(any(), any());
+ verify(mCallback, never()).onDisconnected(any(), anyInt());
+
+ when(mTransformer.convert(mBinder)).thenReturn(mResult);
+ mConnection.onServiceConnected(COMPONENT_NAME, mBinder);
+
+ mExecutor.runAll();
+ verify(mCallback, times(1)).onConnected(mConnection, mResult);
+ }
+
+ @Test
+ public void testDisconnectBeforeBind() {
+ mConnection.addCallback(mCallback);
+ mExecutor.runAll();
+ mConnection.onServiceDisconnected(COMPONENT_NAME);
+ mExecutor.runAll();
+ // Disconnects before binds should be ignored.
+ verify(mCallback, never()).onDisconnected(eq(mConnection), anyInt());
+ }
+
+ @Test
+ public void testDisconnect() {
+ mConnection.addCallback(mCallback);
+ mExecutor.runAll();
+ mConnection.bind();
+ mConnection.onServiceDisconnected(COMPONENT_NAME);
+
+ // Ensure the callback doesn't get triggered until the executor runs.
+ verify(mCallback, never()).onDisconnected(eq(mConnection), anyInt());
+ mExecutor.runAll();
+ // Ensure proper disconnect reason reported.
+ verify(mCallback, times(1)).onDisconnected(mConnection,
+ ObservableServiceConnection.DISCONNECT_REASON_DISCONNECTED);
+ // Verify unbound from service.
+ verify(mContext, times(1)).unbindService(mConnection);
+
+ clearInvocations(mContext);
+ // Ensure unbind after disconnect has no effect on the connection
+ mConnection.unbind();
+ verify(mContext, never()).unbindService(mConnection);
+ }
+
+ @Test
+ public void testBindingDied() {
+ mConnection.addCallback(mCallback);
+ mExecutor.runAll();
+ mConnection.bind();
+ mConnection.onBindingDied(COMPONENT_NAME);
+
+ // Ensure the callback doesn't get triggered until the executor runs.
+ verify(mCallback, never()).onDisconnected(eq(mConnection), anyInt());
+ mExecutor.runAll();
+ // Ensure proper disconnect reason reported.
+ verify(mCallback, times(1)).onDisconnected(mConnection,
+ ObservableServiceConnection.DISCONNECT_REASON_BINDING_DIED);
+ // Verify unbound from service.
+ verify(mContext, times(1)).unbindService(mConnection);
+ }
+
+ @Test
+ public void testNullBinding() {
+ mConnection.addCallback(mCallback);
+ mExecutor.runAll();
+ mConnection.bind();
+ mConnection.onNullBinding(COMPONENT_NAME);
+
+ // Ensure the callback doesn't get triggered until the executor runs.
+ verify(mCallback, never()).onDisconnected(eq(mConnection), anyInt());
+ mExecutor.runAll();
+ // Ensure proper disconnect reason reported.
+ verify(mCallback, times(1)).onDisconnected(mConnection,
+ ObservableServiceConnection.DISCONNECT_REASON_NULL_BINDING);
+ // Verify unbound from service.
+ verify(mContext, times(1)).unbindService(mConnection);
+ }
+
+ @Test
+ public void testUnbind() {
+ mConnection.addCallback(mCallback);
+ mExecutor.runAll();
+ mConnection.bind();
+ mConnection.unbind();
+
+ // Ensure the callback doesn't get triggered until the executor runs.
+ verify(mCallback, never()).onDisconnected(eq(mConnection), anyInt());
+ mExecutor.runAll();
+ verify(mCallback).onDisconnected(mConnection,
+ ObservableServiceConnection.DISCONNECT_REASON_UNBIND);
+ }
+
+ static class FakeExecutor implements Executor {
+ private final Queue<Runnable> mQueue = new ArrayDeque<>();
+
+ @Override
+ public void execute(Runnable command) {
+ mQueue.add(command);
+ }
+
+ public void runAll() {
+ while (!mQueue.isEmpty()) {
+ mQueue.remove().run();
+ }
+ }
+
+ public void clearAll() {
+ while (!mQueue.isEmpty()) {
+ mQueue.remove();
+ }
+ }
+ }
+}
diff --git a/core/tests/utiltests/src/com/android/internal/util/PersistentServiceConnectionTest.java b/core/tests/utiltests/src/com/android/internal/util/PersistentServiceConnectionTest.java
new file mode 100644
index 000000000000..fee46545ac62
--- /dev/null
+++ b/core/tests/utiltests/src/com/android/internal/util/PersistentServiceConnectionTest.java
@@ -0,0 +1,215 @@
+/*
+ * 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 com.android.internal.util;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+
+import com.android.internal.util.ObservableServiceConnection.ServiceTransformer;
+import com.android.server.testutils.OffsettableClock;
+import com.android.server.testutils.TestHandler;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayDeque;
+import java.util.Queue;
+import java.util.concurrent.Executor;
+
+public class PersistentServiceConnectionTest {
+ private static final ComponentName COMPONENT_NAME =
+ new ComponentName("test.package", "component");
+ private static final int MAX_RETRIES = 2;
+ private static final int RETRY_DELAY_MS = 1000;
+ private static final int CONNECTION_MIN_DURATION_MS = 5000;
+ private PersistentServiceConnection<Proxy> mConnection;
+
+ public static class Proxy {
+ }
+
+ @Mock
+ private Context mContext;
+ @Mock
+ private Intent mIntent;
+ @Mock
+ private Proxy mResult;
+ @Mock
+ private IBinder mBinder;
+ @Mock
+ private ServiceTransformer<Proxy> mTransformer;
+ @Mock
+ private ObservableServiceConnection.Callback<Proxy> mCallback;
+ private TestHandler mHandler;
+ private final FakeExecutor mFakeExecutor = new FakeExecutor();
+ private OffsettableClock mClock;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ mClock = new OffsettableClock.Stopped();
+ mHandler = spy(new TestHandler(null, mClock));
+
+ mConnection = new PersistentServiceConnection<>(
+ mContext,
+ mFakeExecutor,
+ mHandler,
+ mTransformer,
+ mIntent,
+ /* flags= */ Context.BIND_AUTO_CREATE,
+ CONNECTION_MIN_DURATION_MS,
+ MAX_RETRIES,
+ RETRY_DELAY_MS,
+ new TestInjector(mClock));
+
+ mClock.fastForward(1000);
+ mConnection.addCallback(mCallback);
+ when(mTransformer.convert(mBinder)).thenReturn(mResult);
+ }
+
+ @After
+ public void tearDown() {
+ mFakeExecutor.clearAll();
+ }
+
+ @Test
+ public void testConnect() {
+ mConnection.bind();
+ mConnection.onServiceConnected(COMPONENT_NAME, mBinder);
+ mFakeExecutor.runAll();
+ // Ensure that we did not schedule a retry
+ verify(mHandler, never()).postDelayed(any(), anyLong());
+ }
+
+ @Test
+ public void testRetryOnBindFailure() {
+ mConnection.bind();
+
+ verify(mContext, times(1)).bindService(
+ eq(mIntent),
+ anyInt(),
+ eq(mFakeExecutor),
+ eq(mConnection));
+
+ // After disconnect, a reconnection should be attempted after the RETRY_DELAY_MS
+ mConnection.onServiceDisconnected(COMPONENT_NAME);
+ mFakeExecutor.runAll();
+ advanceTime(RETRY_DELAY_MS);
+ verify(mContext, times(2)).bindService(
+ eq(mIntent),
+ anyInt(),
+ eq(mFakeExecutor),
+ eq(mConnection));
+
+ // Reconnect attempt #2
+ mConnection.onServiceDisconnected(COMPONENT_NAME);
+ mFakeExecutor.runAll();
+ advanceTime(RETRY_DELAY_MS * 2);
+ verify(mContext, times(3)).bindService(
+ eq(mIntent),
+ anyInt(),
+ eq(mFakeExecutor),
+ eq(mConnection));
+
+ // There should be no more reconnect attempts, since the maximum is 2
+ mConnection.onServiceDisconnected(COMPONENT_NAME);
+ mFakeExecutor.runAll();
+ advanceTime(RETRY_DELAY_MS * 4);
+ verify(mContext, times(3)).bindService(
+ eq(mIntent),
+ anyInt(),
+ eq(mFakeExecutor),
+ eq(mConnection));
+ }
+
+ @Test
+ public void testManualUnbindDoesNotReconnect() {
+ mConnection.bind();
+
+ verify(mContext, times(1)).bindService(
+ eq(mIntent),
+ anyInt(),
+ eq(mFakeExecutor),
+ eq(mConnection));
+
+ mConnection.unbind();
+ // Ensure that disconnection after unbind does not reconnect.
+ mConnection.onServiceDisconnected(COMPONENT_NAME);
+ mFakeExecutor.runAll();
+ advanceTime(RETRY_DELAY_MS);
+
+ verify(mContext, times(1)).bindService(
+ eq(mIntent),
+ anyInt(),
+ eq(mFakeExecutor),
+ eq(mConnection));
+ }
+
+ private void advanceTime(long millis) {
+ mClock.fastForward(millis);
+ mHandler.timeAdvance();
+ }
+
+ static class TestInjector extends PersistentServiceConnection.Injector {
+ private final OffsettableClock mClock;
+
+ TestInjector(OffsettableClock clock) {
+ mClock = clock;
+ }
+
+ @Override
+ public long uptimeMillis() {
+ return mClock.now();
+ }
+ }
+
+ static class FakeExecutor implements Executor {
+ private final Queue<Runnable> mQueue = new ArrayDeque<>();
+
+ @Override
+ public void execute(Runnable command) {
+ mQueue.add(command);
+ }
+
+ public void runAll() {
+ while (!mQueue.isEmpty()) {
+ mQueue.remove().run();
+ }
+ }
+
+ public void clearAll() {
+ while (!mQueue.isEmpty()) {
+ mQueue.remove();
+ }
+ }
+ }
+}