TouchHandler for hiding dream overlay complications.

This changelist introduces HideComplicationTouchHandler, which is
responsible for monitoring touches on the dream overlay and determining
when complications should be hidden. When a touch ends, the
DreamTouchHandler also handles restoring complication visibility.

Test: atest HideComplicationTouchHandler
Fixes: 214292772
Change-Id: Ib80ed9d6c351503ef5f9910b9870c4a5672b96d2
This commit is contained in:
Bryce Lee
2022-02-04 13:03:39 -08:00
parent 6c6f6bd6d5
commit d3237afb49
13 changed files with 508 additions and 10 deletions

View File

@@ -701,4 +701,10 @@
<!-- How long in milliseconds before full burn-in protection is achieved. -->
<integer name="config_dreamOverlayMillisUntilFullJitter">240000</integer>
<integer name="complicationFadeOutMs">500</integer>
<integer name="complicationFadeInMs">500</integer>
<integer name="complicationRestoreMs">1000</integer>
</resources>

View File

@@ -185,6 +185,19 @@ public interface Complication {
void requestExitDream();
}
/**
* The implementation of this interface is in charge of managing the visible state of
* the shown complication.
*/
interface VisibilityController {
/**
* Called to set the visibility of all shown and future complications.
* @param visibility The desired future visibility.
* @param animate whether the change should be animated.
*/
void setVisibility(@View.Visibility int visibility, boolean animate);
}
/**
* Returned through {@link Complication#createView(ComplicationViewModel)}, {@link ViewHolder}
* is a container for a single {@link Complication} instance. The {@link Host} guarantees that

View File

@@ -16,17 +16,23 @@
package com.android.systemui.dreams.complication;
import static com.android.systemui.dreams.complication.dagger.ComplicationHostViewModule.COMPLICATIONS_FADE_IN_DURATION;
import static com.android.systemui.dreams.complication.dagger.ComplicationHostViewModule.COMPLICATIONS_FADE_OUT_DURATION;
import static com.android.systemui.dreams.complication.dagger.ComplicationHostViewModule.COMPLICATION_MARGIN;
import static com.android.systemui.dreams.complication.dagger.ComplicationHostViewModule.SCOPED_COMPLICATIONS_LAYOUT;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.Constraints;
import com.android.systemui.R;
import com.android.systemui.dreams.dagger.DreamOverlayComponent;
import com.android.systemui.touch.TouchInsetManager;
import java.util.ArrayList;
@@ -43,7 +49,8 @@ import javax.inject.Named;
* their layout parameters and attributes. The management of this set is done by
* {@link ComplicationHostViewController}.
*/
public class ComplicationLayoutEngine {
@DreamOverlayComponent.DreamOverlayScope
public class ComplicationLayoutEngine implements Complication.VisibilityController {
public static final String TAG = "ComplicationLayoutEngine";
/**
@@ -454,15 +461,45 @@ public class ComplicationLayoutEngine {
private final HashMap<ComplicationId, ViewEntry> mEntries = new HashMap<>();
private final HashMap<Integer, PositionGroup> mPositions = new HashMap<>();
private final TouchInsetManager.TouchInsetSession mSession;
private final int mFadeInDuration;
private final int mFadeOutDuration;
private ViewPropertyAnimator mViewPropertyAnimator;
/** */
@Inject
public ComplicationLayoutEngine(@Named(SCOPED_COMPLICATIONS_LAYOUT) ConstraintLayout layout,
@Named(COMPLICATION_MARGIN) int margin,
TouchInsetManager.TouchInsetSession session) {
TouchInsetManager.TouchInsetSession session,
@Named(COMPLICATIONS_FADE_IN_DURATION) int fadeInDuration,
@Named(COMPLICATIONS_FADE_OUT_DURATION) int fadeOutDuration) {
mLayout = layout;
mMargin = margin;
mSession = session;
mFadeInDuration = fadeInDuration;
mFadeOutDuration = fadeOutDuration;
}
@Override
public void setVisibility(int visibility, boolean animate) {
final boolean appearing = visibility == View.VISIBLE;
if (mViewPropertyAnimator != null) {
mViewPropertyAnimator.cancel();
}
if (appearing) {
mLayout.setVisibility(View.VISIBLE);
}
mViewPropertyAnimator = mLayout.animate()
.alpha(appearing ? 1f : 0f)
.setDuration(appearing ? mFadeInDuration : mFadeOutDuration)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mLayout.setVisibility(visibility);
}
});
}
/**
@@ -477,6 +514,8 @@ public class ComplicationLayoutEngine {
*/
public void addComplication(ComplicationId id, View view,
ComplicationLayoutParams lp, @Complication.Category int category) {
Log.d(TAG, "engine: " + this + " addComplication");
// If the complication is present, remove.
if (mEntries.containsKey(id)) {
removeComplication(id);

View File

@@ -38,6 +38,9 @@ import dagger.Provides;
public abstract class ComplicationHostViewModule {
public static final String SCOPED_COMPLICATIONS_LAYOUT = "scoped_complications_layout";
public static final String COMPLICATION_MARGIN = "complication_margin";
public static final String COMPLICATIONS_FADE_OUT_DURATION = "complications_fade_out_duration";
public static final String COMPLICATIONS_FADE_IN_DURATION = "complications_fade_in_duration";
public static final String COMPLICATIONS_RESTORE_TIMEOUT = "complication_restore_timeout";
/**
* Generates a {@link ConstraintLayout}, which can host
@@ -60,4 +63,34 @@ public abstract class ComplicationHostViewModule {
static int providesComplicationPadding(@Main Resources resources) {
return resources.getDimensionPixelSize(R.dimen.dream_overlay_complication_margin);
}
/**
* Provides the fade out duration for complications.
*/
@Provides
@Named(COMPLICATIONS_FADE_OUT_DURATION)
@DreamOverlayComponent.DreamOverlayScope
static int providesComplicationsFadeOutDuration(@Main Resources resources) {
return resources.getInteger(R.integer.complicationFadeOutMs);
}
/**
* Provides the fade in duration for complications.
*/
@Provides
@Named(COMPLICATIONS_FADE_IN_DURATION)
@DreamOverlayComponent.DreamOverlayScope
static int providesComplicationsFadeInDuration(@Main Resources resources) {
return resources.getInteger(R.integer.complicationFadeInMs);
}
/**
* Provides the timeout for restoring complication visibility.
*/
@Provides
@Named(COMPLICATIONS_RESTORE_TIMEOUT)
@DreamOverlayComponent.DreamOverlayScope
static int providesComplicationsRestoreTimeout(@Main Resources resources) {
return resources.getInteger(R.integer.complicationRestoreMs);
}
}

View File

@@ -21,7 +21,9 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStore;
import com.android.systemui.dreams.complication.Complication;
import com.android.systemui.dreams.complication.ComplicationCollectionViewModel;
import com.android.systemui.dreams.complication.ComplicationLayoutEngine;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@@ -62,4 +64,13 @@ public interface ComplicationModule {
return provider.get(ComplicationCollectionViewModel.class);
}
/**
* Provides the visibility controller for display complications.
*/
@Provides
static Complication.VisibilityController providesVisibilityController(
ComplicationLayoutEngine engine) {
return engine;
}
}

View File

@@ -21,7 +21,6 @@ import android.content.Context;
import com.android.settingslib.dream.DreamBackend;
import com.android.systemui.dreams.complication.dagger.DreamPreviewComplicationComponent;
import com.android.systemui.dreams.complication.dagger.RegisteredComplicationsModule;
import com.android.systemui.dreams.touch.dagger.DreamTouchModule;
import dagger.Module;
import dagger.Provides;
@@ -30,7 +29,6 @@ import dagger.Provides;
* Dagger Module providing Communal-related functionality.
*/
@Module(includes = {
DreamTouchModule.class,
RegisteredComplicationsModule.class,
},
subcomponents = {

View File

@@ -26,6 +26,7 @@ import com.android.systemui.dreams.DreamOverlayContainerViewController;
import com.android.systemui.dreams.complication.Complication;
import com.android.systemui.dreams.complication.dagger.ComplicationModule;
import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor;
import com.android.systemui.dreams.touch.dagger.DreamTouchModule;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@@ -39,6 +40,7 @@ import dagger.Subcomponent;
* Dagger subcomponent for {@link DreamOverlayModule}.
*/
@Subcomponent(modules = {
DreamTouchModule.class,
DreamOverlayModule.class,
ComplicationModule.class,
})

View File

@@ -0,0 +1,137 @@
/*
* Copyright (C) 2022 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.touch;
import static com.android.systemui.dreams.complication.dagger.ComplicationHostViewModule.COMPLICATIONS_RESTORE_TIMEOUT;
import android.os.Handler;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.complication.Complication;
import com.android.systemui.touch.TouchInsetManager;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import javax.inject.Inject;
import javax.inject.Named;
/**
* {@link HideComplicationTouchHandler} is responsible for hiding the overlay complications from
* visibility whenever there is touch interactions outside the overlay. The overlay interaction
* scope includes touches to the complication plus any touch entry region for gestures as specified
* to the {@link DreamOverlayTouchMonitor}.
*
* This {@link DreamTouchHandler} is also responsible for fading in the complications at the end
* of the {@link com.android.systemui.dreams.touch.DreamTouchHandler.TouchSession}.
*/
public class HideComplicationTouchHandler implements DreamTouchHandler {
private static final String TAG = "HideComplicationHandler";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final Complication.VisibilityController mVisibilityController;
private final int mRestoreTimeout;
private final Handler mHandler;
private final Executor mExecutor;
private final TouchInsetManager mTouchInsetManager;
private final Runnable mRestoreComplications = new Runnable() {
@Override
public void run() {
mVisibilityController.setVisibility(View.VISIBLE, true);
}
};
@Inject
HideComplicationTouchHandler(Complication.VisibilityController visibilityController,
@Named(COMPLICATIONS_RESTORE_TIMEOUT) int restoreTimeout,
TouchInsetManager touchInsetManager,
@Main Executor executor,
@Main Handler handler) {
mVisibilityController = visibilityController;
mRestoreTimeout = restoreTimeout;
mHandler = handler;
mTouchInsetManager = touchInsetManager;
mExecutor = executor;
}
@Override
public void onSessionStart(TouchSession session) {
if (DEBUG) {
Log.d(TAG, "onSessionStart");
}
// If other sessions are interested in this touch, do not fade out elements.
if (session.getActiveSessionCount() > 1) {
if (DEBUG) {
Log.d(TAG, "multiple active touch sessions, not fading");
}
session.pop();
return;
}
session.registerInputListener(ev -> {
if (!(ev instanceof MotionEvent)) {
return;
}
final MotionEvent motionEvent = (MotionEvent) ev;
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
if (DEBUG) {
Log.d(TAG, "ACTION_DOWN received");
}
final ListenableFuture<Boolean> touchCheck = mTouchInsetManager
.checkWithinTouchRegion(Math.round(motionEvent.getX()),
Math.round(motionEvent.getY()));
touchCheck.addListener(() -> {
try {
if (!touchCheck.get()) {
mHandler.removeCallbacks(mRestoreComplications);
mVisibilityController.setVisibility(View.INVISIBLE, true);
} else {
// If a touch occurred inside the dream overlay touch insets, do not
// handle the touch.
session.pop();
}
} catch (InterruptedException | ExecutionException exception) {
Log.e(TAG, "could not check TouchInsetManager:" + exception);
}
}, mExecutor);
} else if (motionEvent.getAction() == MotionEvent.ACTION_CANCEL
|| motionEvent.getAction() == MotionEvent.ACTION_UP) {
// End session and initiate delayed reappearance of the complications.
session.pop();
mHandler.postDelayed(mRestoreComplications, mRestoreTimeout);
}
});
}
@Override
public void onSessionEnd(TouchSession session) {
if (DEBUG) {
Log.d(TAG, "onSessionEnd");
}
}
}

View File

@@ -23,6 +23,7 @@ import dagger.Module;
*/
@Module(includes = {
BouncerSwipeModule.class,
HideComplicationModule.class,
}, subcomponents = {
InputSessionComponent.class,
})

View File

@@ -0,0 +1,40 @@
/*
* 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.touch.dagger;
import com.android.systemui.dreams.touch.DreamTouchHandler;
import com.android.systemui.dreams.touch.HideComplicationTouchHandler;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoSet;
/**
* Module for {@link HideComplicationTouchHandler}.
*/
@Module
public class HideComplicationModule {
/**
* Provides {@link HideComplicationTouchHandler} for inclusion in touch handling over the dream.
*/
@Provides
@IntoSet
public static DreamTouchHandler providesHideComplicationTouchHandler(
HideComplicationTouchHandler touchHandler) {
return touchHandler;
}
}

View File

@@ -21,6 +21,10 @@ import android.graphics.Region;
import android.view.View;
import android.view.ViewRootImpl;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.Executor;
@@ -141,6 +145,18 @@ public class TouchInsetManager {
return new TouchInsetSession(this, mExecutor);
}
/**
* Checks to see if the given point coordinates fall within an inset region.
*/
public ListenableFuture<Boolean> checkWithinTouchRegion(int x, int y) {
return CallbackToFutureAdapter.getFuture(completer -> {
mExecutor.execute(() -> completer.set(
mDefinedRegions.values().stream().anyMatch(region -> region.contains(x, y))));
return "DreamOverlayTouchMonitor::checkWithinTouchRegion";
});
}
private void updateTouchInset() {
final ViewRootImpl viewRootImpl = mRootView.getViewRootImpl();

View File

@@ -117,7 +117,7 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase {
mLayout);
final ComplicationLayoutEngine engine =
new ComplicationLayoutEngine(mLayout, 0, mTouchSession);
new ComplicationLayoutEngine(mLayout, 0, mTouchSession, 0, 0);
addComplication(engine, firstViewInfo);
// Ensure the view is added to the top end corner
@@ -145,7 +145,7 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase {
mLayout);
final ComplicationLayoutEngine engine =
new ComplicationLayoutEngine(mLayout, 0, mTouchSession);
new ComplicationLayoutEngine(mLayout, 0, mTouchSession, 0, 0);
addComplication(engine, firstViewInfo);
// Ensure the view is added to the top end corner
@@ -162,7 +162,7 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase {
@Test
public void testDirectionLayout() {
final ComplicationLayoutEngine engine =
new ComplicationLayoutEngine(mLayout, 0, mTouchSession);
new ComplicationLayoutEngine(mLayout, 0, mTouchSession, 0, 0);
final ViewInfo firstViewInfo = new ViewInfo(
new ComplicationLayoutParams(
@@ -211,7 +211,7 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase {
@Test
public void testPositionLayout() {
final ComplicationLayoutEngine engine =
new ComplicationLayoutEngine(mLayout, 0, mTouchSession);
new ComplicationLayoutEngine(mLayout, 0, mTouchSession, 0, 0);
final ViewInfo firstViewInfo = new ViewInfo(
new ComplicationLayoutParams(
@@ -299,7 +299,7 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase {
public void testMargin() {
final int margin = 5;
final ComplicationLayoutEngine engine =
new ComplicationLayoutEngine(mLayout, margin, mTouchSession);
new ComplicationLayoutEngine(mLayout, margin, mTouchSession, 0, 0);
final ViewInfo firstViewInfo = new ViewInfo(
new ComplicationLayoutParams(
@@ -374,7 +374,7 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase {
@Test
public void testRemoval() {
final ComplicationLayoutEngine engine =
new ComplicationLayoutEngine(mLayout, 0, mTouchSession);
new ComplicationLayoutEngine(mLayout, 0, mTouchSession, 0, 0);
final ViewInfo firstViewInfo = new ViewInfo(
new ComplicationLayoutParams(

View File

@@ -0,0 +1,202 @@
/*
* Copyright (C) 2022 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.touch;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.os.Handler;
import android.testing.AndroidTestingRunner;
import android.view.MotionEvent;
import android.view.View;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dreams.complication.Complication;
import com.android.systemui.shared.system.InputChannelCompat;
import com.android.systemui.touch.TouchInsetManager;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
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;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class HideComplicationTouchHandlerTest extends SysuiTestCase {
private static final int RESTORE_TIMEOUT = 1000;
@Mock
Complication.VisibilityController mVisibilityController;
@Mock
TouchInsetManager mTouchInsetManager;
@Mock
Handler mHandler;
@Mock
MotionEvent mMotionEvent;
@Mock
DreamTouchHandler.TouchSession mSession;
FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
}
/**
* Ensures no actions are taken when there multiple sessions.
*/
@Test
public void testSessionEndOnMultipleSessions() {
final HideComplicationTouchHandler touchHandler = new HideComplicationTouchHandler(
mVisibilityController,
RESTORE_TIMEOUT,
mTouchInsetManager,
mFakeExecutor,
mHandler);
// Report multiple active sessions.
when(mSession.getActiveSessionCount()).thenReturn(2);
// Start session.
touchHandler.onSessionStart(mSession);
// Verify session end.
verify(mSession).pop();
// Verify no interaction with visibility controller.
verify(mVisibilityController, never()).setVisibility(anyInt(), anyBoolean());
}
/**
* Ensures no actions are taken when there multiple sessions.
*/
@Test
public void testSessionEndWithTouchInInset() {
final HideComplicationTouchHandler touchHandler = new HideComplicationTouchHandler(
mVisibilityController,
RESTORE_TIMEOUT,
mTouchInsetManager,
mFakeExecutor,
mHandler);
// Report one session
when(mSession.getActiveSessionCount()).thenReturn(1);
// Start session
touchHandler.onSessionStart(mSession);
// Capture input listener
final ArgumentCaptor<InputChannelCompat.InputEventListener> inputEventListenerCaptor =
ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class);
verify(mSession).registerInputListener(inputEventListenerCaptor.capture());
// Report touch within the inset
when(mTouchInsetManager.checkWithinTouchRegion(anyInt(), anyInt())).thenReturn(
CallbackToFutureAdapter.getFuture(completer -> {
completer.set(true);
return "testSessionEndWithTouchInInset";
})
);
inputEventListenerCaptor.getValue().onInputEvent(mMotionEvent);
mFakeExecutor.runAllReady();
// Verify session ended.
verify(mSession).pop();
// Verify no interaction with visibility controller.
verify(mVisibilityController, never()).setVisibility(anyInt(), anyBoolean());
}
/**
* Make sure visibility changes are triggered from session events.
*/
@Test
public void testSessionLifecycle() {
final HideComplicationTouchHandler touchHandler = new HideComplicationTouchHandler(
mVisibilityController,
RESTORE_TIMEOUT,
mTouchInsetManager,
mFakeExecutor,
mHandler);
// Report one session
when(mSession.getActiveSessionCount()).thenReturn(1);
// Start session
touchHandler.onSessionStart(mSession);
// Capture input listener
final ArgumentCaptor<InputChannelCompat.InputEventListener> inputEventListenerCaptor =
ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class);
verify(mSession).registerInputListener(inputEventListenerCaptor.capture());
// Report touch outside the inset
when(mTouchInsetManager.checkWithinTouchRegion(anyInt(), anyInt())).thenReturn(
CallbackToFutureAdapter.getFuture(completer -> {
completer.set(false);
return "testSessionEndWithTouchInInset";
})
);
// Send down event.
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_DOWN);
inputEventListenerCaptor.getValue().onInputEvent(mMotionEvent);
mFakeExecutor.runAllReady();
// Verify callback to restore visibility cancelled.
verify(mHandler).removeCallbacks(any());
// Verify visibility controller told to hide complications.
verify(mVisibilityController).setVisibility(eq(View.INVISIBLE), anyBoolean());
Mockito.clearInvocations(mVisibilityController, mHandler);
// Send up event.
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_UP);
inputEventListenerCaptor.getValue().onInputEvent(mMotionEvent);
mFakeExecutor.runAllReady();
// Verify visibility controller told to show complications.
ArgumentCaptor<Runnable> delayRunnableCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(mHandler).postDelayed(delayRunnableCaptor.capture(),
eq(Long.valueOf(RESTORE_TIMEOUT)));
delayRunnableCaptor.getValue().run();
verify(mVisibilityController).setVisibility(eq(View.VISIBLE), anyBoolean());
// Verify session ended.
verify(mSession).pop();
}
}