Merge "Update DreamService -> DreamOverlayService connection to handle crashes" into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
2ab63d23d3
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -34,6 +34,7 @@ android_test {
|
||||
"mockito-target-minus-junit4",
|
||||
"androidx.test.ext.junit",
|
||||
"truth-prebuilt",
|
||||
"servicestests-utils",
|
||||
],
|
||||
|
||||
libs: [
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user