OLD | NEW |
(Empty) | |
| 1 // Copyright 2017 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package org.chromium.chrome.browser.compositor; |
| 6 |
| 7 import android.app.Activity; |
| 8 import android.graphics.PixelFormat; |
| 9 import android.view.Surface; |
| 10 import android.view.SurfaceHolder; |
| 11 import android.view.SurfaceView; |
| 12 import android.widget.FrameLayout; |
| 13 |
| 14 import static org.hamcrest.Matchers.lessThan; |
| 15 import static org.junit.Assert.assertEquals; |
| 16 import static org.junit.Assert.assertNotNull; |
| 17 import static org.junit.Assert.assertThat; |
| 18 import static org.junit.Assert.assertTrue; |
| 19 import static org.mockito.ArgumentMatchers.anyInt; |
| 20 import static org.mockito.ArgumentMatchers.eq; |
| 21 import static org.mockito.Mockito.times; |
| 22 import static org.mockito.Mockito.verify; |
| 23 |
| 24 import org.junit.Before; |
| 25 import org.junit.Test; |
| 26 import org.junit.runner.RunWith; |
| 27 import org.mockito.ArgumentMatchers; |
| 28 import org.mockito.Mock; |
| 29 import org.mockito.MockitoAnnotations; |
| 30 import org.robolectric.Robolectric; |
| 31 import org.robolectric.Shadows; |
| 32 import org.robolectric.annotation.Config; |
| 33 import org.robolectric.annotation.Implementation; |
| 34 import org.robolectric.annotation.Implements; |
| 35 import org.robolectric.shadows.ShadowLooper; |
| 36 import org.robolectric.shadows.ShadowSurfaceView; |
| 37 |
| 38 import org.chromium.base.test.util.Feature; |
| 39 import org.chromium.testing.local.LocalRobolectricTestRunner; |
| 40 |
| 41 import java.util.Set; |
| 42 |
| 43 /** |
| 44 * Unit tests for the CompositorSurfaceManager. |
| 45 */ |
| 46 @RunWith(LocalRobolectricTestRunner.class) |
| 47 @Config(manifest = Config.NONE) |
| 48 public class CompositorSurfaceManagerTest { |
| 49 @Mock |
| 50 private SurfaceHolder.Callback mCallback; |
| 51 |
| 52 private CompositorSurfaceManager mManager; |
| 53 |
| 54 private FrameLayout mLayout; |
| 55 |
| 56 /** |
| 57 * Implementation of a SurfaceView shadow that provides additional functiona
lity for controlling |
| 58 * the state of the underlying (fake) Surface. |
| 59 */ |
| 60 @Implements(SurfaceView.class) |
| 61 public static class MyShadowSurfaceView extends ShadowSurfaceView { |
| 62 private final MyFakeSurfaceHolder mHolder = new MyFakeSurfaceHolder(); |
| 63 |
| 64 /** |
| 65 * Robolectric's FakeSurfaceHolder doesn't keep track of the format, etc
. |
| 66 */ |
| 67 public static class MyFakeSurfaceHolder extends ShadowSurfaceView.FakeSu
rfaceHolder { |
| 68 /** |
| 69 * Fake surface that lets us control whether it's valid or not. |
| 70 */ |
| 71 public static class MyFakeSurface extends Surface { |
| 72 public boolean valid = false; |
| 73 |
| 74 @Override |
| 75 public boolean isValid() { |
| 76 return valid; |
| 77 } |
| 78 } |
| 79 |
| 80 private int mFormat = PixelFormat.UNKNOWN; |
| 81 private final MyFakeSurface mSurface = new MyFakeSurface(); |
| 82 |
| 83 @Implementation |
| 84 public void setFormat(int format) { |
| 85 mFormat = format; |
| 86 } |
| 87 |
| 88 public int getFormat() { |
| 89 return mFormat; |
| 90 } |
| 91 |
| 92 // Return a surface that we can control if it's valid or not. |
| 93 @Override |
| 94 public Surface getSurface() { |
| 95 return getFakeSurface(); |
| 96 } |
| 97 |
| 98 public MyFakeSurface getFakeSurface() { |
| 99 return mSurface; |
| 100 } |
| 101 } |
| 102 |
| 103 public MyShadowSurfaceView() {} |
| 104 |
| 105 @Implementation |
| 106 public SurfaceHolder getHolder() { |
| 107 return getMyFakeSurfaceHolder(); |
| 108 } |
| 109 |
| 110 @Override |
| 111 public FakeSurfaceHolder getFakeSurfaceHolder() { |
| 112 return getMyFakeSurfaceHolder(); |
| 113 } |
| 114 |
| 115 public MyFakeSurfaceHolder getMyFakeSurfaceHolder() { |
| 116 return mHolder; |
| 117 } |
| 118 } |
| 119 |
| 120 @Before |
| 121 public void beforeTest() { |
| 122 MockitoAnnotations.initMocks(this); |
| 123 Activity activity = Robolectric.buildActivity(Activity.class).setup().ge
t(); |
| 124 mLayout = new FrameLayout(activity); |
| 125 mManager = new CompositorSurfaceManager(mLayout, mCallback); |
| 126 } |
| 127 |
| 128 private void runDelayedTasks() { |
| 129 ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); |
| 130 } |
| 131 |
| 132 /** |
| 133 * Return the callback for |view|, or null. Will get mad if there's more th
an one. |
| 134 */ |
| 135 private SurfaceHolder.Callback callbackFor(SurfaceView view) { |
| 136 MyShadowSurfaceView viewShadow = (MyShadowSurfaceView) Shadows.shadowOf(
view); |
| 137 ShadowSurfaceView.FakeSurfaceHolder viewHolder = viewShadow.getFakeSurfa
ceHolder(); |
| 138 Set<SurfaceHolder.Callback> callbacks = viewHolder.getCallbacks(); |
| 139 // Zero or one is okay. |
| 140 assertThat(callbacks.size(), lessThan(2)); |
| 141 |
| 142 if (callbacks.size() == 1) return callbacks.iterator().next(); |
| 143 |
| 144 return null; |
| 145 } |
| 146 |
| 147 private MyShadowSurfaceView.MyFakeSurfaceHolder fakeHolderFor(SurfaceView vi
ew) { |
| 148 MyShadowSurfaceView viewShadow = (MyShadowSurfaceView) Shadows.shadowOf(
view); |
| 149 return viewShadow.getMyFakeSurfaceHolder(); |
| 150 } |
| 151 |
| 152 private void setSurfaceValid(SurfaceView view, boolean valid) { |
| 153 fakeHolderFor(view).getFakeSurface().valid = valid; |
| 154 } |
| 155 |
| 156 /** |
| 157 * Find and return the SurfaceView with format |format|. |
| 158 */ |
| 159 private SurfaceView findSurface(int format) { |
| 160 final int childCount = mLayout.getChildCount(); |
| 161 for (int i = 0; i < childCount; i++) { |
| 162 final SurfaceView child = (SurfaceView) mLayout.getChildAt(i); |
| 163 if (fakeHolderFor(child).getFormat() == format) return child; |
| 164 } |
| 165 |
| 166 return null; |
| 167 } |
| 168 |
| 169 /** |
| 170 * Request the pixel format |format|, and return the SurfaceView for it if i
t's attached. You |
| 171 * are responsible for sending surfaceCreated / Changed to |mManager| if you
want it to think |
| 172 * that Android has provided the Surface. |
| 173 */ |
| 174 private SurfaceView requestSurface(int format) { |
| 175 mManager.requestSurface(format); |
| 176 runDelayedTasks(); |
| 177 |
| 178 return findSurface(format); |
| 179 } |
| 180 |
| 181 /** |
| 182 * Request format |format|, and send created / changed callbacks to |mManage
r| as if Android |
| 183 * had provided the underlying Surface. |
| 184 */ |
| 185 private SurfaceView requestThenCreateSurface(int format) { |
| 186 SurfaceView view = requestSurface(format); |
| 187 setSurfaceValid(view, true); |
| 188 callbackFor(view).surfaceCreated(view.getHolder()); |
| 189 final int actualFormat = |
| 190 (format == PixelFormat.OPAQUE) ? PixelFormat.RGB_565 : PixelForm
at.RGBA_8888; |
| 191 final int width = 320; |
| 192 final int height = 240; |
| 193 callbackFor(view).surfaceChanged(view.getHolder(), actualFormat, width,
height); |
| 194 |
| 195 return view; |
| 196 } |
| 197 |
| 198 @Test |
| 199 @Feature("Compositor") |
| 200 @Config(shadows = {MyShadowSurfaceView.class}) |
| 201 public void testRequestOpaqueSurface() { |
| 202 // Request a SurfaceView, and test in detail that it worked. |
| 203 SurfaceView opaque = requestSurface(PixelFormat.OPAQUE); |
| 204 verify(mCallback, times(0)).surfaceCreated(ArgumentMatchers.<SurfaceHold
er>any()); |
| 205 verify(mCallback, times(0)) |
| 206 .surfaceChanged( |
| 207 ArgumentMatchers.<SurfaceHolder>any(), anyInt(), anyInt(
), anyInt()); |
| 208 verify(mCallback, times(0)).surfaceDestroyed(ArgumentMatchers.<SurfaceHo
lder>any()); |
| 209 |
| 210 // Check that there's an opaque SurfaceView . |
| 211 assertEquals(1, mLayout.getChildCount()); |
| 212 assertTrue(fakeHolderFor(opaque).getFormat() == PixelFormat.OPAQUE); |
| 213 |
| 214 // Verify that we are notified when the surface is created. |
| 215 callbackFor(opaque).surfaceCreated(opaque.getHolder()); |
| 216 verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
| 217 verify(mCallback, times(0)).surfaceDestroyed(ArgumentMatchers.<SurfaceHo
lder>any()); |
| 218 |
| 219 // Verify that we are notified when the surface is changed. |
| 220 final int format = PixelFormat.RGB_565; |
| 221 final int width = 320; |
| 222 final int height = 240; |
| 223 callbackFor(opaque).surfaceChanged(opaque.getHolder(), format, width, he
ight); |
| 224 verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
| 225 verify(mCallback, times(1)).surfaceChanged(opaque.getHolder(), format, w
idth, height); |
| 226 verify(mCallback, times(0)).surfaceDestroyed(ArgumentMatchers.<SurfaceHo
lder>any()); |
| 227 |
| 228 // Verify that we are notified when the surface is destroyed. |
| 229 callbackFor(opaque).surfaceDestroyed(opaque.getHolder()); |
| 230 verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
| 231 verify(mCallback, times(1)).surfaceChanged(opaque.getHolder(), format, w
idth, height); |
| 232 verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder()); |
| 233 } |
| 234 |
| 235 @Test |
| 236 @Feature("Compositor") |
| 237 @Config(shadows = {MyShadowSurfaceView.class}) |
| 238 public void testRequestOpaqueThenTranslucentSurface() { |
| 239 // Request opaque then translucent. |
| 240 SurfaceView opaque = requestThenCreateSurface(PixelFormat.OPAQUE); |
| 241 SurfaceView translucent = requestThenCreateSurface(PixelFormat.TRANSLUCE
NT); |
| 242 |
| 243 // Verify that we received a destroy for |opaque| and created / changed
for |translucent|. |
| 244 verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder()); |
| 245 verify(mCallback, times(1)).surfaceCreated(translucent.getHolder()); |
| 246 verify(mCallback, times(1)) |
| 247 .surfaceChanged(eq(translucent.getHolder()), anyInt(), anyInt(),
anyInt()); |
| 248 |
| 249 // Both views should be present. |
| 250 assertEquals(2, mLayout.getChildCount()); |
| 251 |
| 252 // Only the translucent surface should be left. Note that the old view
is still valid. |
| 253 mManager.doneWithUnownedSurface(); |
| 254 runDelayedTasks(); |
| 255 assertEquals(1, mLayout.getChildCount()); |
| 256 assertNotNull(findSurface(PixelFormat.TRANSLUCENT)); |
| 257 } |
| 258 |
| 259 @Test |
| 260 @Feature("Compositor") |
| 261 @Config(shadows = {MyShadowSurfaceView.class}) |
| 262 public void testRequestSameSurface() { |
| 263 // Request an opaque surface, get it, then request it again. Verify tha
t we get synthetic |
| 264 // create / destroy callbacks. |
| 265 SurfaceView opaque = requestThenCreateSurface(PixelFormat.OPAQUE); |
| 266 verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
| 267 verify(mCallback, times(1)) |
| 268 .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyI
nt()); |
| 269 verify(mCallback, times(0)).surfaceDestroyed(opaque.getHolder()); |
| 270 |
| 271 // Surface is curerntly valid. Request again. We should get back a des
troy and create. |
| 272 assertEquals(opaque, requestSurface(PixelFormat.OPAQUE)); |
| 273 verify(mCallback, times(2)).surfaceCreated(opaque.getHolder()); |
| 274 verify(mCallback, times(2)) |
| 275 .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyI
nt()); |
| 276 verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder()); |
| 277 assertEquals(1, mLayout.getChildCount()); |
| 278 } |
| 279 |
| 280 @Test |
| 281 @Feature("Compositor") |
| 282 @Config(shadows = {MyShadowSurfaceView.class}) |
| 283 public void testRequestSameSurfaceBeforeReady() { |
| 284 // Request an opaque surface, then request it again before the first one
shows up. |
| 285 SurfaceView opaque = requestSurface(PixelFormat.OPAQUE); |
| 286 verify(mCallback, times(0)).surfaceCreated(opaque.getHolder()); |
| 287 verify(mCallback, times(0)) |
| 288 .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyI
nt()); |
| 289 verify(mCallback, times(0)).surfaceDestroyed(opaque.getHolder()); |
| 290 |
| 291 // Request again. We shouldn't get any callbacks, since the surface is
still pending. |
| 292 assertEquals(opaque, requestSurface(PixelFormat.OPAQUE)); |
| 293 verify(mCallback, times(0)).surfaceCreated(opaque.getHolder()); |
| 294 verify(mCallback, times(0)) |
| 295 .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyI
nt()); |
| 296 verify(mCallback, times(0)).surfaceDestroyed(opaque.getHolder()); |
| 297 |
| 298 // Only the opaque view should be attached. |
| 299 assertEquals(1, mLayout.getChildCount()); |
| 300 |
| 301 // When the surface is created, we should get notified created / changed
, but not destroyed. |
| 302 callbackFor(opaque).surfaceCreated(opaque.getHolder()); |
| 303 verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
| 304 |
| 305 callbackFor(opaque).surfaceChanged(opaque.getHolder(), PixelFormat.RGB_5
65, 320, 240); |
| 306 verify(mCallback, times(1)) |
| 307 .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyI
nt()); |
| 308 verify(mCallback, times(0)).surfaceDestroyed(opaque.getHolder()); |
| 309 } |
| 310 |
| 311 @Test |
| 312 @Feature("Compositor") |
| 313 @Config(shadows = {MyShadowSurfaceView.class}) |
| 314 public void testRequestDifferentSurfacesBeforeReady() { |
| 315 // Request an opaque surface, then request the translucent one before th
e it one shows up. |
| 316 SurfaceView opaque = requestSurface(PixelFormat.OPAQUE); |
| 317 verify(mCallback, times(0)).surfaceCreated(opaque.getHolder()); |
| 318 verify(mCallback, times(0)) |
| 319 .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyI
nt()); |
| 320 verify(mCallback, times(0)).surfaceDestroyed(opaque.getHolder()); |
| 321 |
| 322 // Request translucent. We should get no callbacks, but both views shou
ld be attached. |
| 323 SurfaceView translucent = requestSurface(PixelFormat.TRANSLUCENT); |
| 324 verify(mCallback, times(0)).surfaceCreated(opaque.getHolder()); |
| 325 verify(mCallback, times(0)).surfaceCreated(translucent.getHolder()); |
| 326 assertEquals(2, mLayout.getChildCount()); |
| 327 |
| 328 // If the opaque surface arrives, we shouldn't hear about it. It should
be detached, since |
| 329 // we've requested the other one. |
| 330 callbackFor(opaque).surfaceCreated(opaque.getHolder()); |
| 331 runDelayedTasks(); |
| 332 assertEquals(1, mLayout.getChildCount()); |
| 333 verify(mCallback, times(0)).surfaceCreated(opaque.getHolder()); |
| 334 verify(mCallback, times(0)).surfaceCreated(translucent.getHolder()); |
| 335 verify(mCallback, times(0)).surfaceDestroyed(opaque.getHolder()); |
| 336 |
| 337 // When we create the translucent surface, we should be notified. |
| 338 callbackFor(translucent).surfaceCreated(translucent.getHolder()); |
| 339 verify(mCallback, times(0)).surfaceCreated(opaque.getHolder()); |
| 340 verify(mCallback, times(1)).surfaceCreated(translucent.getHolder()); |
| 341 } |
| 342 |
| 343 @Test |
| 344 @Feature("Compositor") |
| 345 @Config(shadows = {MyShadowSurfaceView.class}) |
| 346 public void testPendingSurfaceChangedCallback() { |
| 347 // Request an opaque surface, and request it again between 'created' and
'changed'. We |
| 348 // should get a synthetic 'created', but a real 'changed' callback. |
| 349 SurfaceView opaque = requestSurface(PixelFormat.OPAQUE); |
| 350 callbackFor(opaque).surfaceCreated(opaque.getHolder()); |
| 351 runDelayedTasks(); |
| 352 |
| 353 // Sanity check. |
| 354 verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
| 355 verify(mCallback, times(0)) |
| 356 .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyI
nt()); |
| 357 |
| 358 // Re-request while 'changed' is still pending. We should get a synthet
ic 'destroyed' and |
| 359 // synthetic 'created'. |
| 360 assertEquals(opaque, requestSurface(PixelFormat.OPAQUE)); |
| 361 verify(mCallback, times(2)).surfaceCreated(opaque.getHolder()); |
| 362 verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder()); |
| 363 verify(mCallback, times(0)) |
| 364 .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyI
nt()); |
| 365 |
| 366 // Send 'changed', and expect that we'll receive it. |
| 367 callbackFor(opaque).surfaceChanged(opaque.getHolder(), PixelFormat.RGB_5
65, 320, 240); |
| 368 verify(mCallback, times(1)) |
| 369 .surfaceChanged(eq(opaque.getHolder()), anyInt(), anyInt(), anyI
nt()); |
| 370 } |
| 371 |
| 372 @Test |
| 373 @Feature("Compositor") |
| 374 @Config(shadows = {MyShadowSurfaceView.class}) |
| 375 public void testJellyBeanWorkaround() { |
| 376 // See if recreateSurfaceForJellyBean destroys / re-creates the surface. |
| 377 // should get a synthetic 'created', but a real 'changed' callback. |
| 378 SurfaceView opaque = requestThenCreateSurface(PixelFormat.OPAQUE); |
| 379 verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
| 380 assertEquals(1, mLayout.getChildCount()); |
| 381 |
| 382 // We should be notified that the surface was destroyed via synthetic ca
llback, and the |
| 383 // surface should be detached. |
| 384 mManager.recreateSurfaceForJellyBean(); |
| 385 verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
| 386 verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder()); |
| 387 assertEquals(0, mLayout.getChildCount()); |
| 388 |
| 389 // When the surface really is destroyed, it should be re-attached. We s
hould not be |
| 390 // notified again, though. |
| 391 callbackFor(opaque).surfaceDestroyed(opaque.getHolder()); |
| 392 verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
| 393 verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder()); |
| 394 assertEquals(1, mLayout.getChildCount()); |
| 395 |
| 396 // When the surface is re-created, we should be notified. |
| 397 callbackFor(opaque).surfaceCreated(opaque.getHolder()); |
| 398 verify(mCallback, times(2)).surfaceCreated(opaque.getHolder()); |
| 399 verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder()); |
| 400 assertEquals(1, mLayout.getChildCount()); |
| 401 } |
| 402 |
| 403 @Test |
| 404 @Feature("Compositor") |
| 405 @Config(shadows = {MyShadowSurfaceView.class}) |
| 406 public void testRequestSurfaceDuringDestruction() { |
| 407 // If we re-request a surface while we're tearing it down, it should be
re-attached and |
| 408 // given back to us once the destruction completes. |
| 409 SurfaceView opaque = requestThenCreateSurface(PixelFormat.OPAQUE); |
| 410 SurfaceView translucent = requestThenCreateSurface(PixelFormat.TRANSLUCE
NT); |
| 411 mManager.doneWithUnownedSurface(); |
| 412 |
| 413 // The transparent surface should be attached, and the opaque one detach
ed. |
| 414 assertEquals(1, mLayout.getChildCount()); |
| 415 assertNotNull(findSurface(PixelFormat.TRANSLUCENT)); |
| 416 |
| 417 // Re-request the opaque surface. Nothing should happen until it's dest
royed. It should |
| 418 // not be re-attached, since that is also deferred until destruction. |
| 419 assertEquals(null, requestSurface(PixelFormat.OPAQUE)); |
| 420 assertEquals(1, mLayout.getChildCount()); |
| 421 assertNotNull(findSurface(PixelFormat.TRANSLUCENT)); |
| 422 |
| 423 // When the opaque surface is destroyed, then it should be re-attached.
No callbacks shoud |
| 424 // have arrived yet, except for initial creation and (synthetic) destroy
ed when we got the |
| 425 // translucent surface. |
| 426 callbackFor(opaque).surfaceDestroyed(opaque.getHolder()); |
| 427 assertEquals(2, mLayout.getChildCount()); |
| 428 verify(mCallback, times(1)).surfaceCreated(opaque.getHolder()); |
| 429 verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder()); |
| 430 verify(mCallback, times(0)).surfaceDestroyed(translucent.getHolder()); |
| 431 |
| 432 // When the opaque surface becomes available, we'll get the synthetic de
stroy for the |
| 433 // translucent one that we lost ownership of, and the real create for th
e opaque one. |
| 434 callbackFor(opaque).surfaceCreated(opaque.getHolder()); |
| 435 assertEquals(2, mLayout.getChildCount()); |
| 436 verify(mCallback, times(2)).surfaceCreated(opaque.getHolder()); |
| 437 verify(mCallback, times(1)).surfaceDestroyed(opaque.getHolder()); |
| 438 verify(mCallback, times(1)).surfaceDestroyed(translucent.getHolder()); |
| 439 } |
| 440 } |
OLD | NEW |