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
Merged-In: Ib9a7e8675e408c1724567335a3c186fe5c9f5f59
This commit is contained in:
Lucas Silva
2022-10-04 10:11:36 -04:00
parent 3e5c74adf4
commit 7bbcdc1c01
8 changed files with 1011 additions and 76 deletions

View File

@@ -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;
// A list of pending requests to execute on the overlay.
private final ArrayList<Consumer<IDreamOverlay>> mConsumers = new ArrayList<>();
private boolean mBound;
OverlayConnection() {
mRequests = new ArrayDeque<>();
}
public void bind(Context context, @Nullable ComponentName overlayService,
ComponentName dreamService) {
if (overlayService == null) {
return;
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;
}
public void request(Consumer<IDreamOverlay> request) {
mRequests.push(request);
evaluate();
}
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);
}
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);
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// Store Overlay and execute pending requests.
mOverlay = IDreamOverlay.Stub.asInterface(service);
evaluate();
public boolean bind() {
addCallback(mCallback);
return super.bind();
}
@Override
public void onServiceDisconnected(ComponentName name) {
// Clear Overlay binder to prevent further request processing.
mOverlay = null;
public void unbind() {
removeCallback(mCallback);
super.unbind();
}
public void addConsumer(Consumer<IDreamOverlay> consumer) {
mConsumers.add(consumer);
if (mOverlay != null) {
consumer.accept(mOverlay);
}
}
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),
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);
}
}
});
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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.

View File

@@ -2239,6 +2239,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" />

View File

@@ -34,6 +34,7 @@ android_test {
"mockito-target-minus-junit4",
"androidx.test.ext.junit",
"truth-prebuilt",
"servicestests-utils",
],
libs: [

View File

@@ -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();
}
}
}
}

View File

@@ -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();
}
}
}
}