From 7856a35e205bf690a05a1a775b23cd6ab186ac0a Mon Sep 17 00:00:00 2001 From: Lucas Silva Date: Tue, 4 Oct 2022 10:11:36 -0400 Subject: Update DreamService -> DreamOverlayService connection to handle crashes This change implements PersistentServiceConnection and ObservableServiceConnection helper classes in framework for better handling of crashes. Fixes: 247103878 Test: manually by crashing systemui using adb while dreaming and verifying that the overlay is visible Test: atest com.android.internal.util.PersistentServiceConnectionTest Test: atest com.android.internal.util.ObservableServiceConnectionTest Change-Id: Ib9a7e8675e408c1724567335a3c186fe5c9f5f59 --- core/java/android/service/dreams/DreamService.java | 174 ++++++++------ .../internal/util/ObservableServiceConnection.java | 258 +++++++++++++++++++++ .../internal/util/PersistentServiceConnection.java | 200 ++++++++++++++++ core/res/res/values/config.xml | 8 + core/res/res/values/symbols.xml | 3 + core/tests/utiltests/Android.bp | 1 + .../util/ObservableServiceConnectionTest.java | 226 ++++++++++++++++++ .../util/PersistentServiceConnectionTest.java | 215 +++++++++++++++++ 8 files changed, 1010 insertions(+), 75 deletions(-) create mode 100644 core/java/com/android/internal/util/ObservableServiceConnection.java create mode 100644 core/java/com/android/internal/util/PersistentServiceConnection.java create mode 100644 core/tests/utiltests/src/com/android/internal/util/ObservableServiceConnectionTest.java create mode 100644 core/tests/utiltests/src/com/android/internal/util/PersistentServiceConnectionTest.java 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 { // Overlay set during onBind. private IDreamOverlay mOverlay; - // A Queue of pending requests to execute on the overlay. - private final ArrayDeque> 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> mConsumers = new ArrayList<>(); + + private final Callback mCallback = new Callback() { + @Override + public void onConnected(ObservableServiceConnection connection, + IDreamOverlay service) { + mOverlay = service; + for (Consumer 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 connection, + int reason) { + mOverlay = null; } + }; - context.unbindService(this); - mBound = false; + OverlayConnection(Context context, + Executor executor, + Handler handler, + ServiceTransformer 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 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 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 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 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 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 The type of the wrapper over the resulting service. + */ +public class ObservableServiceConnection implements ServiceConnection { + /** + * An interface for converting the service proxy into a given internal wrapper type. + * + * @param The type of the wrapper over the resulting service. + */ + public interface ServiceTransformer { + /** + * 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 The wrapper type. + */ + public interface Callback { + /** + * 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 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 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 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, ObservableServiceConnection, T> + mCallbackRegistry = new CallbackRegistry<>( + new NotifierCallback, ObservableServiceConnection, T>() { + @Override + public void onNotifyCallback(Callback callback, + ObservableServiceConnection 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 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 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 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 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 extends ObservableServiceConnection { + private final Callback mConnectionCallback = new Callback() { + private long mConnectedTime; + + @Override + public void onConnected(ObservableServiceConnection connection, T service) { + mConnectedTime = mInjector.uptimeMillis(); + } + + @Override + public void onDisconnected(ObservableServiceConnection 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 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 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 @@ false + + 1000 + + 3 + + 10000 +