diff options
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(); + } + } + } +} |