// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.compositor;
import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
import static org.hamcrest.Matchers.lessThan;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
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.times;
import static org.mockito.Mockito.verify;
import android.app.Activity;
import android.graphics.PixelFormat;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.widget.FrameLayout;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowLooper;
import org.robolectric.shadows.ShadowSurfaceView;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;
import java.util.Set;
/** Unit tests for the CompositorSurfaceManagerImpl. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
@LooperMode(LooperMode.Mode.LEGACY)
public class CompositorSurfaceManagerImplTest {
@Mock private CompositorSurfaceManager.SurfaceManagerCallbackTarget mCallback;
private CompositorSurfaceManager mManager;
private FrameLayout mLayout;
// surfaceChanged parameters chosen by most recent sendSurfaceChanged.
private int mActualFormat;
private int mWidth;
private int mHeight;
/**
* Implementation of a SurfaceView shadow that provides additional functionality for controlling
* the state of the underlying (fake) Surface.
*/
@Implements(SurfaceView.class)
public static class MyShadowSurfaceView extends ShadowSurfaceView {
private final MyFakeSurfaceHolder mHolder = new MyFakeSurfaceHolder();
/** Robolectric's FakeSurfaceHolder doesn't keep track of the format, etc. */
public static class MyFakeSurfaceHolder extends ShadowSurfaceView.FakeSurfaceHolder {
/** Fake surface that lets us control whether it's valid or not. */
public static class MyFakeSurface extends Surface {
public boolean valid;
@Override
public boolean isValid() {
return valid;
}
}
private int mFormat = PixelFormat.UNKNOWN;
private final MyFakeSurface mSurface = new MyFakeSurface();
@Override
@Implementation
public void setFormat(int format) {
mFormat = format;
}
public int getFormat() {
return mFormat;
}
// Return a surface that we can control if it's valid or not.
@Override
public Surface getSurface() {
return getFakeSurface();
}
public MyFakeSurface getFakeSurface() {
return mSurface;
}
}
public MyShadowSurfaceView() {}
@Override
@Implementation
public SurfaceHolder getHolder() {
return getMyFakeSurfaceHolder();
}
@Override
public FakeSurfaceHolder getFakeSurfaceHolder() {
return getMyFakeSurfaceHolder();
}
public MyFakeSurfaceHolder getMyFakeSurfaceHolder() {
return mHolder;
}
}
@Before
public void beforeTest() {
MockitoAnnotations.initMocks(this);
Activity activity = Robolectric.buildActivity(Activity.class).setup().get();
mLayout = new FrameLayout(activity);
mManager = new CompositorSurfaceManagerImpl(mLayout, mCallback);
}
private void runDelayedTasks() {
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
}
/** Return the callback for |view|, or null. Will get mad if there's more than one. */
private SurfaceHolder.Callback callbackFor(SurfaceView view) {
MyShadowSurfaceView viewShadow = (MyShadowSurfaceView) Shadows.shadowOf(view);
ShadowSurfaceView.FakeSurfaceHolder viewHolder = viewShadow.getFakeSurfaceHolder();
Set<SurfaceHolder.Callback> callbacks = viewHolder.getCallbacks();
// Zero or one is okay.
assertThat(callbacks.size(), lessThan(2));
if (callbacks.size() == 1) return callbacks.iterator().next();
return null;
}
private MyShadowSurfaceView.MyFakeSurfaceHolder fakeHolderFor(SurfaceView view) {
MyShadowSurfaceView viewShadow = (MyShadowSurfaceView) Shadows.shadowOf(view);
return viewShadow.getMyFakeSurfaceHolder();
}
private void setSurfaceValid(SurfaceView view, boolean valid) {
fakeHolderFor(view).getFakeSurface().valid = valid;
}
/** Find and return the SurfaceView with format |format|. */
private SurfaceView findSurface(int format) {
final int childCount = mLayout.getChildCount();
for (int i = 0; i < childCount; i++) {
final SurfaceView child = (SurfaceView) mLayout.getChildAt(i);
if (fakeHolderFor(child).getFormat() == format) return child;
}
return null;
}
/**
* Request the pixel format |format|, and return the SurfaceView for it if it's attached. You
* are responsible for sending surfaceCreated / Changed to |mManager| if you want it to think
* that Android has provided the Surface.
*/
private SurfaceView requestSurface(int format) {
mManager.requestSurface(format);
runDelayedTasks();
return findSurface(format);
}
/**
* Request format |format|, and send created / changed callbacks to |mManager| as if Android
* had provided the underlying Surface.
*/
private SurfaceView requestThenCreateSurface(int format) {
SurfaceView view = requestSurface(format);
setSurfaceValid(view, true);
callbackFor(view).surfaceCreated(view.getHolder());
sendSurfaceChanged(view, format, 320, 240);
return view;
}
/** Send a surfaceChanged event with the given parameters. */
private void sendSurfaceChanged(SurfaceView view, int format, int width, int height) {
mActualFormat =
(format == PixelFormat.OPAQUE) ? PixelFormat.RGB_565 : PixelFormat.RGBA_8888;
mWidth = width;
mHeight = height;
callbackFor(view).surfaceChanged(view.getHolder(), mActualFormat, mWidth, mHeight);
}
@Test
@Feature("Compositor")
@Config(shadows = {MyShadowSurfaceView.class})
public void testRequestOpaqueSurface() {
// Request a SurfaceView, and test in detail that it worked.
SurfaceView opaque = requestSurface(PixelFormat.OPAQUE);
verify(mCallback, times(0)).surfaceCreated(ArgumentMatchers.<Surface>any());
verify(mCallback, times(0))
.surfaceChanged(ArgumentMatchers.<Surface>any(), anyInt(), anyInt(), anyInt());
verify(mCallback, times(0)).surfaceDestroyed(any(), anyBoolean());
// Check that there's an opaque SurfaceView .
assertEquals(1, mLayout.getChildCount());
assertTrue(fakeHolderFor(opaque).getFormat() == PixelFormat.OPAQUE);
// Verify that we are notified when the surface is created.
callbackFor(opaque).surfaceCreated(opaque.getHolder());
verify(mCallback, times(1)).surfaceCreated(eq(opaque.getHolder().getSurface()));
verify(mCallback, times(0)).surfaceDestroyed(any(), anyBoolean());
// Verify that we are notified when the surface is changed.
sendSurfaceChanged(opaque, PixelFormat.OPAQUE, 320, 240);
verify(mCallback, times(1)).surfaceCreated(eq(opaque.getHolder().getSurface()));
verify(mCallback, times(1))
.surfaceChanged(
eq(opaque.getHolder().getSurface()),
eq(mActualFormat),
eq(mWidth),
eq(mHeight));
verify(mCallback, times(0)).surfaceDestroyed(any(), anyBoolean());
// Verify that we are notified when the surface is destroyed.
callbackFor(opaque).surfaceDestroyed(opaque.getHolder());
verify(mCallback, times(1)).surfaceCreated(eq(opaque.getHolder().getSurface()));
verify(mCallback, times(1))
.surfaceChanged(eq(opaque.getHolder().getSurface()), anyInt(), anyInt(), anyInt());
verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder().getSurface(), true);
}
@Test
@Feature("Compositor")
@Config(shadows = {MyShadowSurfaceView.class})
public void testRequestOpaqueThenTranslucentSurface() {
// Request opaque then translucent.
SurfaceView opaque = requestThenCreateSurface(PixelFormat.OPAQUE);
SurfaceView translucent = requestThenCreateSurface(PixelFormat.TRANSLUCENT);
// Verify that we received a destroy for |opaque| and created / changed for |translucent|.
verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder().getSurface(), false);
verify(mCallback, times(1)).surfaceCreated(translucent.getHolder().getSurface());
verify(mCallback, times(1))
.surfaceChanged(
eq(translucent.getHolder().getSurface()), anyInt(), anyInt(), anyInt());
// Both views should be present.
assertEquals(2, mLayout.getChildCount());
// Only the translucent surface should be left. Note that the old view is still valid.
mManager.doneWithUnownedSurface();
runDelayedTasks();
assertEquals(1, mLayout.getChildCount());
assertNotNull(findSurface(PixelFormat.TRANSLUCENT));
}
@Test
@Feature("Compositor")
@Config(shadows = {MyShadowSurfaceView.class})
public void testRequestSameSurface() {
// Request an opaque surface, get it, then request it again. Verify that we get synthetic
// create / destroy callbacks.
SurfaceView opaque = requestThenCreateSurface(PixelFormat.OPAQUE);
verify(mCallback, times(1)).surfaceCreated(eq(opaque.getHolder().getSurface()));
verify(mCallback, times(1))
.surfaceChanged(eq(opaque.getHolder().getSurface()), anyInt(), anyInt(), anyInt());
verify(mCallback, times(0)).surfaceDestroyed(any(), anyBoolean());
// Surface is currently valid. Request again. We should get back a destroy and create.
assertEquals(opaque, requestSurface(PixelFormat.OPAQUE));
verify(mCallback, times(2)).surfaceCreated(opaque.getHolder().getSurface());
verify(mCallback, times(2))
.surfaceChanged(
eq(opaque.getHolder().getSurface()),
eq(mActualFormat),
eq(mWidth),
eq(mHeight));
verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder().getSurface(), false);
assertEquals(1, mLayout.getChildCount());
}
@Test
@Feature("Compositor")
@Config(shadows = {MyShadowSurfaceView.class})
public void testRequestSameSurfaceBeforeReady() {
// Request an opaque surface, then request it again before the first one shows up.
SurfaceView opaque = requestSurface(PixelFormat.OPAQUE);
verify(mCallback, times(0)).surfaceCreated(opaque.getHolder().getSurface());
verify(mCallback, times(0))
.surfaceChanged(eq(opaque.getHolder().getSurface()), anyInt(), anyInt(), anyInt());
verify(mCallback, times(0)).surfaceDestroyed(any(), anyBoolean());
// Request again. We shouldn't get any callbacks, since the surface is still pending.
assertEquals(opaque, requestSurface(PixelFormat.OPAQUE));
verify(mCallback, times(0)).surfaceCreated(opaque.getHolder().getSurface());
verify(mCallback, times(0))
.surfaceChanged(eq(opaque.getHolder().getSurface()), anyInt(), anyInt(), anyInt());
verify(mCallback, times(0)).surfaceDestroyed(any(), anyBoolean());
// Only the opaque view should be attached.
assertEquals(1, mLayout.getChildCount());
// When the surface is created, we should get notified created / changed, but not destroyed.
callbackFor(opaque).surfaceCreated(opaque.getHolder());
verify(mCallback, times(1)).surfaceCreated(opaque.getHolder().getSurface());
sendSurfaceChanged(opaque, PixelFormat.RGB_565, 320, 240);
verify(mCallback, times(1))
.surfaceChanged(
eq(opaque.getHolder().getSurface()),
eq(mActualFormat),
eq(mWidth),
eq(mHeight));
verify(mCallback, times(0)).surfaceDestroyed(any(), anyBoolean());
}
@Test
@Feature("Compositor")
@Config(shadows = {MyShadowSurfaceView.class})
public void testRequestDifferentSurfacesBeforeReady() {
// Request an opaque surface, then request the translucent one before the it one shows up.
SurfaceView opaque = requestSurface(PixelFormat.OPAQUE);
verify(mCallback, times(0)).surfaceCreated(opaque.getHolder().getSurface());
verify(mCallback, times(0))
.surfaceChanged(eq(opaque.getHolder().getSurface()), anyInt(), anyInt(), anyInt());
verify(mCallback, times(0)).surfaceDestroyed(any(), anyBoolean());
// Request translucent. We should get no callbacks, but both views should be attached.
SurfaceView translucent = requestSurface(PixelFormat.TRANSLUCENT);
verify(mCallback, times(0)).surfaceCreated(opaque.getHolder().getSurface());
verify(mCallback, times(0)).surfaceCreated(translucent.getHolder().getSurface());
assertEquals(2, mLayout.getChildCount());
// If the opaque surface arrives, we shouldn't hear about it. It should be detached, since
// we've requested the other one.
callbackFor(opaque).surfaceCreated(opaque.getHolder());
runDelayedTasks();
assertEquals(1, mLayout.getChildCount());
verify(mCallback, times(0)).surfaceCreated(opaque.getHolder().getSurface());
verify(mCallback, times(0)).surfaceCreated(translucent.getHolder().getSurface());
verify(mCallback, times(0)).surfaceDestroyed(any(), anyBoolean());
// When we create the translucent surface, we should be notified.
callbackFor(translucent).surfaceCreated(translucent.getHolder());
verify(mCallback, times(0)).surfaceCreated(opaque.getHolder().getSurface());
verify(mCallback, times(1)).surfaceCreated(translucent.getHolder().getSurface());
}
@Test
@Feature("Compositor")
@Config(shadows = {MyShadowSurfaceView.class})
public void testPendingSurfaceChangedCallback() {
// Request an opaque surface, and request it again between 'created' and 'changed'. We
// should get a synthetic 'created', but a real 'changed' callback.
SurfaceView opaque = requestSurface(PixelFormat.OPAQUE);
callbackFor(opaque).surfaceCreated(opaque.getHolder());
runDelayedTasks();
// Sanity check.
verify(mCallback, times(1)).surfaceCreated(opaque.getHolder().getSurface());
verify(mCallback, times(0))
.surfaceChanged(eq(opaque.getHolder().getSurface()), anyInt(), anyInt(), anyInt());
// Re-request while 'changed' is still pending. We should get a synthetic 'destroyed' and
// synthetic 'created'.
assertEquals(opaque, requestSurface(PixelFormat.OPAQUE));
verify(mCallback, times(2)).surfaceCreated(opaque.getHolder().getSurface());
verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder().getSurface(), false);
verify(mCallback, times(0))
.surfaceChanged(eq(opaque.getHolder().getSurface()), anyInt(), anyInt(), anyInt());
// Send 'changed', and expect that we'll receive it.
sendSurfaceChanged(opaque, PixelFormat.OPAQUE, 320, 240);
verify(mCallback, times(1))
.surfaceChanged(
eq(opaque.getHolder().getSurface()),
eq(mActualFormat),
eq(mWidth),
eq(mHeight));
}
@Test
@Feature("Compositor")
@Config(shadows = {MyShadowSurfaceView.class})
public void testRecreateSurface() {
// See if recreateSurface destroys / re-creates the surface.
// should get a synthetic 'created', but a real 'changed' callback.
SurfaceView opaque = requestThenCreateSurface(PixelFormat.OPAQUE);
verify(mCallback, times(1)).surfaceCreated(opaque.getHolder().getSurface());
assertEquals(1, mLayout.getChildCount());
// We should be notified that the surface was destroyed via synthetic callback, and the
// surface should be detached.
mManager.recreateSurface();
verify(mCallback, times(1)).surfaceCreated(opaque.getHolder().getSurface());
verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder().getSurface(), true);
assertEquals(0, mLayout.getChildCount());
// When the surface really is destroyed, it should be re-attached. We should not be
// notified again, though.
callbackFor(opaque).surfaceDestroyed(opaque.getHolder());
verify(mCallback, times(1)).surfaceCreated(opaque.getHolder().getSurface());
verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder().getSurface(), true);
assertEquals(1, mLayout.getChildCount());
// When the surface is re-created, we should be notified.
callbackFor(opaque).surfaceCreated(opaque.getHolder());
verify(mCallback, times(2)).surfaceCreated(opaque.getHolder().getSurface());
verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder().getSurface(), true);
assertEquals(1, mLayout.getChildCount());
}
@Test
@Feature("Compositor")
@Config(shadows = {MyShadowSurfaceView.class})
public void testRequestSurfaceDuringDestruction() {
// If we re-request a surface while we're tearing it down, it should be re-attached and
// given back to us once the destruction completes.
SurfaceView opaque = requestThenCreateSurface(PixelFormat.OPAQUE);
SurfaceView translucent = requestThenCreateSurface(PixelFormat.TRANSLUCENT);
mManager.doneWithUnownedSurface();
// The transparent surface should be attached, and the opaque one detached.
assertEquals(1, mLayout.getChildCount());
assertNotNull(findSurface(PixelFormat.TRANSLUCENT));
// Re-request the opaque surface. Nothing should happen until it's destroyed. It should
// not be re-attached, since that is also deferred until destruction.
assertEquals(null, requestSurface(PixelFormat.OPAQUE));
assertEquals(1, mLayout.getChildCount());
assertNotNull(findSurface(PixelFormat.TRANSLUCENT));
// When the opaque surface is destroyed, then it should be re-attached. No callbacks should
// have arrived yet, except for initial creation and (synthetic) destroyed when we got the
// translucent surface.
callbackFor(opaque).surfaceDestroyed(opaque.getHolder());
assertEquals(2, mLayout.getChildCount());
verify(mCallback, times(1)).surfaceCreated(opaque.getHolder().getSurface());
verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder().getSurface(), false);
verify(mCallback, times(0)).surfaceDestroyed(translucent.getHolder().getSurface(), true);
// When the opaque surface becomes available, we'll get the synthetic destroy for the
// translucent one that we lost ownership of, and the real create for the opaque one.
callbackFor(opaque).surfaceCreated(opaque.getHolder());
assertEquals(2, mLayout.getChildCount());
verify(mCallback, times(2)).surfaceCreated(opaque.getHolder().getSurface());
verify(mCallback, times(1)).surfaceDestroyed(translucent.getHolder().getSurface(), false);
verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder().getSurface(), false);
}
}