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

Commit 814a2d60 authored by Bryce Lee's avatar Bryce Lee
Browse files

Centralized Dream Overlay State.

This changelist introduces DreamOverlayStateController,
a singleton for managing dream overlay state. The controller
currently maintains the collection of overlays (by way of
DreamOverlayProviders) to show over a dream. Entities, such
as the DreamOverlayService, can register for updates to the
collection.

Bug: 201676403
Test: atest DreamOverlayServiceTest
Test: atest DreamOverlayStateControllerTest
Change-Id: I096dbea4b99fbbe7f5ec74d954db5b8af80ef68b
parent 92fdf1d2
Loading
Loading
Loading
Loading
+31 −1
Original line number Diff line number Diff line
@@ -52,11 +52,21 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
    private final Context mContext;
    // The Executor ensures actions and ui updates happen on the same thread.
    private final Executor mExecutor;
    // The state controller informs the service of updates to the overlays present.
    private final DreamOverlayStateController mStateController;

    // The window is populated once the dream informs the service it has begun dreaming.
    private Window mWindow;
    private ConstraintLayout mLayout;

    private final DreamOverlayStateController.Callback mOverlayStateCallback =
            new DreamOverlayStateController.Callback() {
        @Override
        public void onOverlayChanged() {
            mExecutor.execute(() -> reloadOverlaysLocked());
        }
    };

    // The service listens to view changes in order to declare that input occurring in areas outside
    // the overlay should be passed through to the dream underneath.
    private View.OnAttachStateChangeListener mRootViewAttachListener =
@@ -101,6 +111,16 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
        mExecutor.execute(() -> addOverlayWindowLocked(layoutParams));
    }

    private void reloadOverlaysLocked() {
        if (mLayout == null) {
            return;
        }
        mLayout.removeAllViews();
        for (OverlayProvider overlayProvider : mStateController.getOverlays()) {
            addOverlay(overlayProvider);
        }
    }

    /**
     * Inserts {@link Window} to host dream overlays into the dream's parent window. Must be called
     * from the main executing thread. The window attributes closely mirror those that are set by
@@ -134,6 +154,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ

        final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
        windowManager.addView(mWindow.getDecorView(), mWindow.getAttributes());
        mExecutor.execute(this::reloadOverlaysLocked);
    }

    @VisibleForTesting
@@ -158,8 +179,17 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
    }

    @Inject
    public DreamOverlayService(Context context, @Main Executor executor) {
    public DreamOverlayService(Context context, @Main Executor executor,
            DreamOverlayStateController overlayStateController) {
        mContext = context;
        mExecutor = executor;
        mStateController = overlayStateController;
        mStateController.addCallback(mOverlayStateCallback);
    }

    @Override
    public void onDestroy() {
        mStateController.removeCallback(mOverlayStateCallback);
        super.onDestroy();
    }
}
+150 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.systemui.dreams;

import androidx.annotation.NonNull;

import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.policy.CallbackController;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Objects;

import javax.inject.Inject;

/**
 * {@link DreamOverlayStateController} is the source of truth for Dream overlay configurations.
 * Clients can register as listeners for changes to the overlay composition and can query for the
 * overlays on-demand.
 */
@SysUISingleton
public class DreamOverlayStateController implements
        CallbackController<DreamOverlayStateController.Callback> {
    // A counter for guaranteeing unique overlay tokens within the scope of this state controller.
    private int mNextOverlayTokenId = 0;

    /**
     * {@link OverlayToken} provides a unique key for identifying {@link OverlayProvider}
     * instances registered with {@link DreamOverlayStateController}.
     */
    public static class OverlayToken {
        private final int mId;

        private OverlayToken(int id) {
            mId = id;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof OverlayToken)) return false;
            OverlayToken that = (OverlayToken) o;
            return mId == that.mId;
        }

        @Override
        public int hashCode() {
            return Objects.hash(mId);
        }
    }

    /**
     * Callback for dream overlay events.
     */
    public interface Callback {
        /**
         * Called when the visibility of the communal view changes.
         */
        default void onOverlayChanged() {
        }
    }

    private final ArrayList<Callback> mCallbacks = new ArrayList<>();
    private final HashMap<OverlayToken, OverlayProvider> mOverlays = new HashMap<>();

    @VisibleForTesting
    @Inject
    public DreamOverlayStateController() {
    }

    /**
     * Adds an overlay to be presented on top of dreams.
     * @param provider The {@link OverlayProvider} providing the dream.
     * @return The {@link OverlayToken} tied to the supplied {@link OverlayProvider}.
     */
    public OverlayToken addOverlay(OverlayProvider provider) {
        final OverlayToken token = new OverlayToken(mNextOverlayTokenId++);
        mOverlays.put(token, provider);
        notifyCallbacks();
        return token;
    }

    /**
     * Removes an overlay from being shown on dreams.
     * @param token The {@link OverlayToken} associated with the {@link OverlayProvider} to be
     *              removed.
     * @return The removed {@link OverlayProvider}, {@code null} if not found.
     */
    public OverlayProvider removeOverlay(OverlayToken token) {
        final OverlayProvider removedOverlay = mOverlays.remove(token);

        if (removedOverlay != null) {
            notifyCallbacks();
        }

        return removedOverlay;
    }

    private void notifyCallbacks() {
        for (Callback callback : mCallbacks) {
            callback.onOverlayChanged();
        }
    }

    @Override
    public void addCallback(@NonNull Callback callback) {
        Objects.requireNonNull(callback, "Callback must not be null. b/128895449");
        if (mCallbacks.contains(callback)) {
            return;
        }

        mCallbacks.add(callback);

        if (mOverlays.isEmpty()) {
            return;
        }

        callback.onOverlayChanged();
    }

    @Override
    public void removeCallback(@NonNull Callback callback) {
        Objects.requireNonNull(callback, "Callback must not be null. b/128895449");
        mCallbacks.remove(callback);
    }

    /**
     * Returns all registered {@link OverlayProvider} instances.
     * @return A collection of {@link OverlayProvider}.
     */
    public Collection<OverlayProvider> getOverlays() {
        return mOverlays.values();
    }
}
+34 −1
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.systemui.dreams;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Intent;
import android.os.IBinder;
@@ -48,6 +49,8 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Arrays;

@SmallTest
@RunWith(AndroidTestingRunner.class)
public class DreamOverlayServiceTest extends SysuiTestCase {
@@ -72,6 +75,9 @@ public class DreamOverlayServiceTest extends SysuiTestCase {
    @Mock
    OverlayProvider mProvider;

    @Mock
    DreamOverlayStateController mDreamOverlayStateController;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
@@ -80,7 +86,8 @@ public class DreamOverlayServiceTest extends SysuiTestCase {

    @Test
    public void testInteraction() throws Exception {
        final DreamOverlayService service = new DreamOverlayService(mContext, mMainExecutor);
        final DreamOverlayService service = new DreamOverlayService(mContext, mMainExecutor,
                mDreamOverlayStateController);
        final IBinder proxy = service.onBind(new Intent());
        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
        clearInvocations(mWindowManager);
@@ -117,4 +124,30 @@ public class DreamOverlayServiceTest extends SysuiTestCase {
        // Ensure service informs dream host of exit.
        verify(mDreamOverlayCallback).onExitRequested();
    }

    @Test
    public void testListening() throws Exception {
        final DreamOverlayService service = new DreamOverlayService(mContext, mMainExecutor,
                mDreamOverlayStateController);

        final IBinder proxy = service.onBind(new Intent());
        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);

        // Inform the overlay service of dream starting.
        overlay.startDream(mWindowParams, mDreamOverlayCallback);
        mMainExecutor.runAllReady();

        // Verify overlay service registered as listener with DreamOverlayStateController
        // and inform callback of addition.
        final ArgumentCaptor<DreamOverlayStateController.Callback> callbackCapture =
                ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);

        verify(mDreamOverlayStateController).addCallback(callbackCapture.capture());
        when(mDreamOverlayStateController.getOverlays()).thenReturn(Arrays.asList(mProvider));
        callbackCapture.getValue().onOverlayChanged();
        mMainExecutor.runAllReady();

        // Verify provider is asked to create overlay.
        verify(mProvider).onCreateOverlay(any(), any(), any());
    }
}
+86 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.systemui.dreams;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.testing.AndroidTestingRunner;

import androidx.test.filters.SmallTest;

import com.android.systemui.SysuiTestCase;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Collection;

@SmallTest
@RunWith(AndroidTestingRunner.class)
public class DreamOverlayStateControllerTest extends SysuiTestCase {
    @Mock
    DreamOverlayStateController.Callback mCallback;

    @Mock
    OverlayProvider mProvider;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testCallback() {
        final DreamOverlayStateController stateController = new DreamOverlayStateController();
        stateController.addCallback(mCallback);

        // Add overlay and verify callback is notified.
        final DreamOverlayStateController.OverlayToken token =
                stateController.addOverlay(mProvider);

        verify(mCallback, times(1)).onOverlayChanged();

        final Collection<OverlayProvider> providers = stateController.getOverlays();
        assertEquals(providers.size(), 1);
        assertTrue(providers.contains(mProvider));

        clearInvocations(mCallback);

        // Remove overlay and verify callback is notified.
        stateController.removeOverlay(token);
        verify(mCallback, times(1)).onOverlayChanged();
        assertTrue(providers.isEmpty());
    }

    @Test
    public void testNotifyOnCallbackAdd() {
        final DreamOverlayStateController stateController = new DreamOverlayStateController();
        final DreamOverlayStateController.OverlayToken token =
                stateController.addOverlay(mProvider);

        // Verify callback occurs on add when an overlay is already present.
        stateController.addCallback(mCallback);
        verify(mCallback, times(1)).onOverlayChanged();
    }
}