From 814a2d60ef71f5da499ae2b33b5d85403a91aa1e Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Thu, 30 Sep 2021 16:49:50 -0700 Subject: [PATCH] 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 --- .../systemui/dreams/DreamOverlayService.java | 32 +++- .../dreams/DreamOverlayStateController.java | 150 ++++++++++++++++++ .../dreams/DreamOverlayServiceTest.java | 35 +++- .../DreamOverlayStateControllerTest.java | 86 ++++++++++ 4 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index d37914a63a390..8f0ea2fb2f878 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -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(); } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java new file mode 100644 index 0000000000000..d248a9e174f51 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java @@ -0,0 +1,150 @@ +/* + * 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 { + // 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 mCallbacks = new ArrayList<>(); + private final HashMap 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 getOverlays() { + return mOverlays.values(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java index 6223048f0ce5e..53bfeee9135ab 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java @@ -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 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()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java new file mode 100644 index 0000000000000..4e97be37603e0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java @@ -0,0 +1,86 @@ +/* + * 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 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(); + } +}