summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/service/dreams/DreamOverlayConnectionHandler.java242
-rw-r--r--core/java/android/service/dreams/DreamService.java105
-rw-r--r--services/tests/mockingservicestests/src/android/service/dreams/DreamOverlayConnectionHandlerTest.java245
3 files changed, 495 insertions, 97 deletions
diff --git a/core/java/android/service/dreams/DreamOverlayConnectionHandler.java b/core/java/android/service/dreams/DreamOverlayConnectionHandler.java
new file mode 100644
index 000000000000..cafe02ad8658
--- /dev/null
+++ b/core/java/android/service/dreams/DreamOverlayConnectionHandler.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2023 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 android.service.dreams;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ObservableServiceConnection;
+import com.android.internal.util.PersistentServiceConnection;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Handles the service connection to {@link IDreamOverlay}
+ *
+ * @hide
+ */
+@VisibleForTesting
+public final class DreamOverlayConnectionHandler {
+ private static final String TAG = "DreamOverlayConnection";
+
+ private static final int MSG_ADD_CONSUMER = 1;
+ private static final int MSG_REMOVE_CONSUMER = 2;
+ private static final int MSG_OVERLAY_CLIENT_READY = 3;
+
+ private final Handler mHandler;
+ private final PersistentServiceConnection<IDreamOverlay> mConnection;
+ // Retrieved Client
+ private IDreamOverlayClient mClient;
+ // A list of pending requests to execute on the overlay.
+ private final List<Consumer<IDreamOverlayClient>> mConsumers = new ArrayList<>();
+ private final OverlayConnectionCallback mCallback;
+
+ DreamOverlayConnectionHandler(
+ Context context,
+ Looper looper,
+ Intent serviceIntent,
+ int minConnectionDurationMs,
+ int maxReconnectAttempts,
+ int baseReconnectDelayMs) {
+ this(context, looper, serviceIntent, minConnectionDurationMs, maxReconnectAttempts,
+ baseReconnectDelayMs, new Injector());
+ }
+
+ @VisibleForTesting
+ public DreamOverlayConnectionHandler(
+ Context context,
+ Looper looper,
+ Intent serviceIntent,
+ int minConnectionDurationMs,
+ int maxReconnectAttempts,
+ int baseReconnectDelayMs,
+ Injector injector) {
+ mCallback = new OverlayConnectionCallback();
+ mHandler = new Handler(looper, new OverlayHandlerCallback());
+ mConnection = injector.buildConnection(
+ context,
+ mHandler,
+ serviceIntent,
+ minConnectionDurationMs,
+ maxReconnectAttempts,
+ baseReconnectDelayMs
+ );
+ }
+
+ /**
+ * Bind to the overlay service. If binding fails, we automatically call unbind to clean
+ * up resources.
+ *
+ * @return true if binding was successful, false otherwise.
+ */
+ public boolean bind() {
+ mConnection.addCallback(mCallback);
+ final boolean success = mConnection.bind();
+ if (!success) {
+ unbind();
+ }
+ return success;
+ }
+
+ /**
+ * Unbind from the overlay service, clearing any pending callbacks.
+ */
+ public void unbind() {
+ mConnection.removeCallback(mCallback);
+ // Remove any pending messages.
+ mHandler.removeCallbacksAndMessages(null);
+ mClient = null;
+ mConsumers.clear();
+ mConnection.unbind();
+ }
+
+ /**
+ * Adds a consumer to run once the overlay service has connected. If the overlay service
+ * disconnects (eg binding dies) and then reconnects, this consumer will be re-run unless
+ * removed.
+ *
+ * @param consumer The consumer to run. This consumer is always executed asynchronously.
+ */
+ public void addConsumer(Consumer<IDreamOverlayClient> consumer) {
+ final Message msg = mHandler.obtainMessage(MSG_ADD_CONSUMER, consumer);
+ mHandler.sendMessage(msg);
+ }
+
+ /**
+ * Removes the consumer, preventing this consumer from being called again.
+ *
+ * @param consumer The consumer to remove.
+ */
+ public void removeConsumer(Consumer<IDreamOverlayClient> consumer) {
+ final Message msg = mHandler.obtainMessage(MSG_REMOVE_CONSUMER, consumer);
+ mHandler.sendMessage(msg);
+ // Clear any pending messages to add this consumer
+ mHandler.removeMessages(MSG_ADD_CONSUMER, consumer);
+ }
+
+ private final class OverlayHandlerCallback implements Handler.Callback {
+ @Override
+ public boolean handleMessage(@NonNull Message msg) {
+ switch (msg.what) {
+ case MSG_OVERLAY_CLIENT_READY:
+ onOverlayClientReady((IDreamOverlayClient) msg.obj);
+ break;
+ case MSG_ADD_CONSUMER:
+ onAddConsumer((Consumer<IDreamOverlayClient>) msg.obj);
+ break;
+ case MSG_REMOVE_CONSUMER:
+ onRemoveConsumer((Consumer<IDreamOverlayClient>) msg.obj);
+ break;
+ }
+ return true;
+ }
+ }
+
+ private void onOverlayClientReady(IDreamOverlayClient client) {
+ mClient = client;
+ for (Consumer<IDreamOverlayClient> consumer : mConsumers) {
+ consumer.accept(mClient);
+ }
+ }
+
+ private void onAddConsumer(Consumer<IDreamOverlayClient> consumer) {
+ if (mClient != null) {
+ consumer.accept(mClient);
+ }
+ mConsumers.add(consumer);
+ }
+
+ private void onRemoveConsumer(Consumer<IDreamOverlayClient> consumer) {
+ mConsumers.remove(consumer);
+ }
+
+ private final class OverlayConnectionCallback implements
+ ObservableServiceConnection.Callback<IDreamOverlay> {
+
+ private final IDreamOverlayClientCallback mClientCallback =
+ new IDreamOverlayClientCallback.Stub() {
+ @Override
+ public void onDreamOverlayClient(IDreamOverlayClient client) {
+ final Message msg =
+ mHandler.obtainMessage(MSG_OVERLAY_CLIENT_READY, client);
+ mHandler.sendMessage(msg);
+ }
+ };
+
+ @Override
+ public void onConnected(
+ ObservableServiceConnection<IDreamOverlay> connection,
+ IDreamOverlay service) {
+ try {
+ service.getClient(mClientCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "could not get DreamOverlayClient", e);
+ }
+ }
+
+ @Override
+ public void onDisconnected(ObservableServiceConnection<IDreamOverlay> connection,
+ int reason) {
+ mClient = null;
+ // Cancel any pending messages about the overlay being ready, since it is no
+ // longer ready.
+ mHandler.removeMessages(MSG_OVERLAY_CLIENT_READY);
+ }
+ }
+
+ /**
+ * 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 PersistentServiceConnection<IDreamOverlay> buildConnection(
+ Context context,
+ Handler handler,
+ Intent serviceIntent,
+ int minConnectionDurationMs,
+ int maxReconnectAttempts,
+ int baseReconnectDelayMs) {
+ final Executor executor = handler::post;
+ final int flags = Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE;
+ return new PersistentServiceConnection<>(
+ context,
+ executor,
+ handler,
+ IDreamOverlay.Stub::asInterface,
+ serviceIntent,
+ flags,
+ minConnectionDurationMs,
+ maxReconnectAttempts,
+ baseReconnectDelayMs
+ );
+ }
+ }
+}
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 356a566fa913..9107c5f4bbdb 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -68,8 +68,6 @@ 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;
@@ -77,8 +75,6 @@ import org.xmlpull.v1.XmlPullParserException;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
@@ -234,7 +230,6 @@ public class DreamService extends Service implements Window.Callback {
private boolean mCanDoze;
private boolean mDozing;
private boolean mWindowless;
- private boolean mOverlayFinishing;
private int mDozeScreenState = Display.STATE_UNKNOWN;
private int mDozeScreenBrightness = PowerManager.BRIGHTNESS_DEFAULT;
@@ -246,88 +241,7 @@ public class DreamService extends Service implements Window.Callback {
private DreamServiceWrapper mDreamServiceWrapper;
private Runnable mDispatchAfterOnAttachedToWindow;
- private OverlayConnection mOverlayConnection;
-
- private static class OverlayConnection extends PersistentServiceConnection<IDreamOverlay> {
- // Retrieved Client
- private IDreamOverlayClient mClient;
-
- // A list of pending requests to execute on the overlay.
- private final ArrayList<Consumer<IDreamOverlayClient>> mConsumers = new ArrayList<>();
-
- private final IDreamOverlayClientCallback mClientCallback =
- new IDreamOverlayClientCallback.Stub() {
- @Override
- public void onDreamOverlayClient(IDreamOverlayClient client) {
- mClient = client;
-
- for (Consumer<IDreamOverlayClient> consumer : mConsumers) {
- consumer.accept(mClient);
- }
- }
- };
-
- private final Callback<IDreamOverlay> mCallback = new Callback<IDreamOverlay>() {
- @Override
- public void onConnected(ObservableServiceConnection<IDreamOverlay> connection,
- IDreamOverlay service) {
- try {
- service.getClient(mClientCallback);
- } catch (RemoteException e) {
- Log.e(TAG, "could not get DreamOverlayClient", e);
- }
- }
-
- @Override
- public void onDisconnected(ObservableServiceConnection<IDreamOverlay> connection,
- int reason) {
- mClient = null;
- }
- };
-
- 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 boolean bind() {
- addCallback(mCallback);
- return super.bind();
- }
-
- @Override
- public void unbind() {
- removeCallback(mCallback);
- super.unbind();
- }
-
- public void addConsumer(Consumer<IDreamOverlayClient> consumer) {
- execute(() -> {
- mConsumers.add(consumer);
- if (mClient != null) {
- consumer.accept(mClient);
- }
- });
- }
-
- public void removeConsumer(Consumer<IDreamOverlayClient> consumer) {
- execute(() -> mConsumers.remove(consumer));
- }
-
- public void clearConsumers() {
- execute(() -> mConsumers.clear());
- }
- }
+ private DreamOverlayConnectionHandler mOverlayConnection;
private final IDreamOverlayCallback mOverlayCallback = new IDreamOverlayCallback.Stub() {
@Override
@@ -1030,18 +944,18 @@ public class DreamService extends Service implements Window.Callback {
final Resources resources = getResources();
final Intent overlayIntent = new Intent().setComponent(overlayComponent);
- mOverlayConnection = new OverlayConnection(
+ mOverlayConnection = new DreamOverlayConnectionHandler(
/* context= */ this,
- getMainExecutor(),
- mHandler,
- IDreamOverlay.Stub::asInterface,
+ Looper.getMainLooper(),
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();
+ if (!mOverlayConnection.bind()) {
+ // Binding failed.
+ mOverlayConnection = null;
+ }
}
return mDreamServiceWrapper;
@@ -1069,9 +983,7 @@ public class DreamService extends Service implements Window.Callback {
// If there is an active overlay connection, signal that the dream is ending before
// continuing. Note that the overlay cannot rely on the unbound state, since another dream
// might have bound to it in the meantime.
- if (mOverlayConnection != null && !mOverlayFinishing) {
- // Set mOverlayFinish to true to only allow this consumer to be added once.
- mOverlayFinishing = true;
+ if (mOverlayConnection != null) {
mOverlayConnection.addConsumer(overlay -> {
try {
overlay.endDream();
@@ -1082,7 +994,6 @@ public class DreamService extends Service implements Window.Callback {
Log.e(mTag, "could not inform overlay of dream end:" + e);
}
});
- mOverlayConnection.clearConsumers();
return;
}
diff --git a/services/tests/mockingservicestests/src/android/service/dreams/DreamOverlayConnectionHandlerTest.java b/services/tests/mockingservicestests/src/android/service/dreams/DreamOverlayConnectionHandlerTest.java
new file mode 100644
index 000000000000..22d7e7300bba
--- /dev/null
+++ b/services/tests/mockingservicestests/src/android/service/dreams/DreamOverlayConnectionHandlerTest.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2023 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 android.service.dreams;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.test.TestLooper;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.util.ObservableServiceConnection;
+import com.android.internal.util.PersistentServiceConnection;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DreamOverlayConnectionHandlerTest {
+ private static final int MIN_CONNECTION_DURATION_MS = 100;
+ private static final int MAX_RECONNECT_ATTEMPTS = 3;
+ private static final int BASE_RECONNECT_DELAY_MS = 50;
+
+ @Mock
+ private Context mContext;
+ @Mock
+ private PersistentServiceConnection<IDreamOverlay> mConnection;
+ @Mock
+ private Intent mServiceIntent;
+ @Mock
+ private IDreamOverlay mOverlayService;
+ @Mock
+ private IDreamOverlayClient mOverlayClient;
+
+ private TestLooper mTestLooper;
+ private DreamOverlayConnectionHandler mDreamOverlayConnectionHandler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mTestLooper = new TestLooper();
+ mDreamOverlayConnectionHandler = new DreamOverlayConnectionHandler(
+ mContext,
+ mTestLooper.getLooper(),
+ mServiceIntent,
+ MIN_CONNECTION_DURATION_MS,
+ MAX_RECONNECT_ATTEMPTS,
+ BASE_RECONNECT_DELAY_MS,
+ new TestInjector(mConnection));
+ }
+
+ @Test
+ public void consumerShouldRunImmediatelyWhenClientAvailable() throws RemoteException {
+ mDreamOverlayConnectionHandler.bind();
+ connectService();
+ provideClient();
+
+ final Consumer<IDreamOverlayClient> consumer = Mockito.mock(Consumer.class);
+ mDreamOverlayConnectionHandler.addConsumer(consumer);
+ mTestLooper.dispatchAll();
+ verify(consumer).accept(mOverlayClient);
+ }
+
+ @Test
+ public void consumerShouldRunAfterClientAvailable() throws RemoteException {
+ mDreamOverlayConnectionHandler.bind();
+ connectService();
+
+ final Consumer<IDreamOverlayClient> consumer = Mockito.mock(Consumer.class);
+ mDreamOverlayConnectionHandler.addConsumer(consumer);
+ mTestLooper.dispatchAll();
+ // No client yet, so we shouldn't have executed
+ verify(consumer, never()).accept(mOverlayClient);
+
+ provideClient();
+ mTestLooper.dispatchAll();
+ verify(consumer).accept(mOverlayClient);
+ }
+
+ @Test
+ public void consumerShouldNeverRunIfClientConnectsAndDisconnects() throws RemoteException {
+ mDreamOverlayConnectionHandler.bind();
+ connectService();
+
+ final Consumer<IDreamOverlayClient> consumer = Mockito.mock(Consumer.class);
+ mDreamOverlayConnectionHandler.addConsumer(consumer);
+ mTestLooper.dispatchAll();
+ // No client yet, so we shouldn't have executed
+ verify(consumer, never()).accept(mOverlayClient);
+
+ provideClient();
+ // Service disconnected before looper could handle the message.
+ disconnectService();
+ mTestLooper.dispatchAll();
+ verify(consumer, never()).accept(mOverlayClient);
+ }
+
+ @Test
+ public void consumerShouldNeverRunIfUnbindCalled() throws RemoteException {
+ mDreamOverlayConnectionHandler.bind();
+ connectService();
+ provideClient();
+
+ final Consumer<IDreamOverlayClient> consumer = Mockito.mock(Consumer.class);
+ mDreamOverlayConnectionHandler.addConsumer(consumer);
+ mDreamOverlayConnectionHandler.unbind();
+ mTestLooper.dispatchAll();
+ // We unbinded immediately after adding consumer, so should never have run.
+ verify(consumer, never()).accept(mOverlayClient);
+ }
+
+ @Test
+ public void consumersOnlyRunOnceIfUnbound() throws RemoteException {
+ mDreamOverlayConnectionHandler.bind();
+ connectService();
+ provideClient();
+
+ AtomicInteger counter = new AtomicInteger();
+ // Add 10 consumers in a row which call unbind within the consumer.
+ for (int i = 0; i < 10; i++) {
+ mDreamOverlayConnectionHandler.addConsumer(client -> {
+ counter.getAndIncrement();
+ mDreamOverlayConnectionHandler.unbind();
+ });
+ }
+ mTestLooper.dispatchAll();
+ // Only the first consumer should have run, since we unbinded.
+ assertThat(counter.get()).isEqualTo(1);
+ }
+
+ @Test
+ public void consumerShouldRunAgainAfterReconnect() throws RemoteException {
+ mDreamOverlayConnectionHandler.bind();
+ connectService();
+ provideClient();
+
+ final Consumer<IDreamOverlayClient> consumer = Mockito.mock(Consumer.class);
+ mDreamOverlayConnectionHandler.addConsumer(consumer);
+ mTestLooper.dispatchAll();
+ verify(consumer, times(1)).accept(mOverlayClient);
+
+ disconnectService();
+ mTestLooper.dispatchAll();
+ // No new calls should happen when service disconnected.
+ verify(consumer, times(1)).accept(mOverlayClient);
+
+ connectService();
+ provideClient();
+ mTestLooper.dispatchAll();
+ // We should trigger the consumer again once the server reconnects.
+ verify(consumer, times(2)).accept(mOverlayClient);
+ }
+
+ @Test
+ public void consumerShouldNeverRunIfRemovedImmediately() throws RemoteException {
+ mDreamOverlayConnectionHandler.bind();
+ connectService();
+ provideClient();
+
+ final Consumer<IDreamOverlayClient> consumer = Mockito.mock(Consumer.class);
+ mDreamOverlayConnectionHandler.addConsumer(consumer);
+ mDreamOverlayConnectionHandler.removeConsumer(consumer);
+ mTestLooper.dispatchAll();
+ verify(consumer, never()).accept(mOverlayClient);
+ }
+
+ private void connectService() {
+ final ObservableServiceConnection.Callback<IDreamOverlay> callback =
+ captureConnectionCallback();
+ callback.onConnected(mConnection, mOverlayService);
+ }
+
+ private void disconnectService() {
+ final ObservableServiceConnection.Callback<IDreamOverlay> callback =
+ captureConnectionCallback();
+ callback.onDisconnected(mConnection, /* reason= */ 0);
+ }
+
+ private void provideClient() throws RemoteException {
+ final IDreamOverlayClientCallback callback = captureClientCallback();
+ callback.onDreamOverlayClient(mOverlayClient);
+ }
+
+ private ObservableServiceConnection.Callback<IDreamOverlay> captureConnectionCallback() {
+ ArgumentCaptor<ObservableServiceConnection.Callback<IDreamOverlay>>
+ callbackCaptor =
+ ArgumentCaptor.forClass(ObservableServiceConnection.Callback.class);
+ verify(mConnection).addCallback(callbackCaptor.capture());
+ return callbackCaptor.getValue();
+ }
+
+ private IDreamOverlayClientCallback captureClientCallback() throws RemoteException {
+ ArgumentCaptor<IDreamOverlayClientCallback> callbackCaptor =
+ ArgumentCaptor.forClass(IDreamOverlayClientCallback.class);
+ verify(mOverlayService, atLeastOnce()).getClient(callbackCaptor.capture());
+ return callbackCaptor.getValue();
+ }
+
+ static class TestInjector extends DreamOverlayConnectionHandler.Injector {
+ private final PersistentServiceConnection<IDreamOverlay> mConnection;
+
+ TestInjector(PersistentServiceConnection<IDreamOverlay> connection) {
+ mConnection = connection;
+ }
+
+ @Override
+ public PersistentServiceConnection<IDreamOverlay> buildConnection(Context context,
+ Handler handler, Intent serviceIntent, int minConnectionDurationMs,
+ int maxReconnectAttempts, int baseReconnectDelayMs) {
+ return mConnection;
+ }
+ }
+}