Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ccd8a87a authored by Lucas Silva's avatar Lucas Silva Committed by Automerger Merge Worker
Browse files

Merge "Fix DreamService -> DreamOverlayService synchronization issues." into...

Merge "Fix DreamService -> DreamOverlayService synchronization issues." into tm-qpr-dev am: 64754c34 am: a85d0b74

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/22105563



Change-Id: I6fdd81768f5aa06c1e73da7f24d4cfe46e4940d7
Signed-off-by: default avatarAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
parents 56baab25 a85d0b74
Loading
Loading
Loading
Loading
+242 −0
Original line number Diff line number Diff line
/*
 * 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
            );
        }
    }
}
+8 −97
Original line number Diff line number Diff line
@@ -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;
        }

+245 −0
Original line number Diff line number Diff line
/*
 * 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;
        }
    }
}