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
This commit is contained in:
Bryce Lee
2021-09-30 16:49:50 -07:00
parent 92fdf1d25b
commit 814a2d60ef
4 changed files with 301 additions and 2 deletions

View File

@@ -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();
}
}

View File

@@ -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<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();
}
}

View File

@@ -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());
}
}

View File

@@ -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<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();
}
}