| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.content.browser.androidoverlay; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| |
| import android.app.Activity; |
| import android.app.Dialog; |
| import android.os.Binder; |
| import android.os.IBinder; |
| import android.view.Surface; |
| import android.view.SurfaceHolder; |
| import android.view.WindowManager; |
| |
| // TODO(liberato): prior to M, this was ...policy.impl.PhoneWindow |
| import com.android.internal.policy.PhoneWindow; |
| |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| 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.shadows.ShadowDialog; |
| import org.robolectric.shadows.ShadowPhoneWindow; |
| import org.robolectric.shadows.ShadowSurfaceView; |
| |
| import org.chromium.base.test.BaseRobolectricTestRunner; |
| import org.chromium.gfx.mojom.Rect; |
| import org.chromium.media.mojom.AndroidOverlayConfig; |
| |
| /** |
| * Tests for DialogOverlayCore. |
| */ |
| @RunWith(BaseRobolectricTestRunner.class) |
| @Config(manifest = Config.NONE) |
| public class DialogOverlayCoreTest { |
| private Activity mActivity; |
| |
| AndroidOverlayConfig mConfig = new AndroidOverlayConfig(); |
| |
| // Should we request a panel? |
| boolean mAsPanel; |
| |
| // DialogCore under test. |
| DialogOverlayCore mCore; |
| |
| // The dialog that we've provided to |mCore|. |
| Dialog mDialog; |
| |
| // Fake window token that we'll send to |mCore|. |
| IBinder mWindowToken = new Binder(); |
| |
| // Surface that will be provided by |mDialog|. |
| Surface mSurface = new Surface(); |
| |
| // SurfaceHolder that will be provided by |mDialog|. |
| SurfaceHolder mHolder = new MyFakeSurfaceHolder(mSurface); |
| |
| /** |
| * Robolectric shadow for PhoneWindow. This one keeps track of takeSurface() calls. |
| * TODO(liberato): the @Impl specifies 'minSdk=M' in the robolectric source. |
| */ |
| @Implements(value = PhoneWindow.class, isInAndroidSdk = false) |
| public static class MyPhoneWindowShadow extends ShadowPhoneWindow { |
| public MyPhoneWindowShadow() {} |
| |
| private SurfaceHolder.Callback2 mCallback; |
| private WindowManager.LayoutParams mLayoutParams; |
| public boolean mDidUpdateParams; |
| |
| @Implementation |
| public void takeSurface(SurfaceHolder.Callback2 callback) { |
| mCallback = callback; |
| } |
| |
| @Implementation |
| public void setAttributes(WindowManager.LayoutParams layoutParams) { |
| mLayoutParams = layoutParams; |
| mDidUpdateParams = true; |
| } |
| } |
| |
| /** |
| * The default fake surface holder doesn't let us provide a surface. |
| */ |
| public static class MyFakeSurfaceHolder extends ShadowSurfaceView.FakeSurfaceHolder { |
| private Surface mSurface; |
| |
| // @param surface The Surface that we'll provide via getSurface. |
| public MyFakeSurfaceHolder(Surface surface) { |
| mSurface = surface; |
| } |
| |
| @Override |
| public Surface getSurface() { |
| return mSurface; |
| } |
| } |
| |
| @Before |
| public void setUp() { |
| mActivity = Robolectric.buildActivity(Activity.class).setup().get(); |
| |
| mConfig = new AndroidOverlayConfig(); |
| mConfig.rect = new Rect(); |
| mConfig.rect.x = 0; |
| mConfig.rect.y = 1; |
| mConfig.rect.width = 2; |
| mConfig.rect.height = 3; |
| } |
| |
| public void createOverlay() { |
| mCore = new DialogOverlayCore(); |
| mCore.initialize(mActivity, mConfig, mHost, mAsPanel); |
| mDialog = mCore.getDialog(); |
| |
| // Nothing should be called yet. |
| checkOverlayDidntCall(); |
| |
| // The dialog should not be shown yet. |
| checkDialogIsNotShown(); |
| } |
| |
| // Make sure that the overlay didn't provide us with a surface, or notify us that it was |
| // destroyed, or wait for cleanup. |
| void checkOverlayDidntCall() { |
| assertEquals(null, mHost.surface()); |
| assertEquals(0, mHost.destroyedCount()); |
| assertEquals(0, mHost.waitCloseCount()); |
| assertEquals(0, mHost.enforceCloseCount()); |
| } |
| |
| // Return the SurfaceHolder callback that was provided to takeSurface(), if any. |
| SurfaceHolder.Callback2 holderCallback() { |
| return ((MyPhoneWindowShadow) Shadows.shadowOf(mDialog.getWindow())).mCallback; |
| } |
| |
| // Return the LayoutPararms that was most recently provided to the dialog. |
| WindowManager.LayoutParams layoutParams() { |
| return ((MyPhoneWindowShadow) Shadows.shadowOf(mDialog.getWindow())).mLayoutParams; |
| } |
| |
| MyPhoneWindowShadow getShadowWindow() { |
| return ((MyPhoneWindowShadow) Shadows.shadowOf(mDialog.getWindow())); |
| } |
| |
| /** |
| * Host impl that counts calls to it. |
| */ |
| class HostMock implements DialogOverlayCore.Host { |
| private Surface mSurface; |
| private int mDestroyedCount; |
| private int mWaitCloseCount; |
| private int mEnforceCloseCount; |
| |
| @Override |
| public void onSurfaceReady(Surface surface) { |
| mSurface = surface; |
| } |
| |
| @Override |
| public void onOverlayDestroyed() { |
| mDestroyedCount++; |
| } |
| |
| @Override |
| public void waitForClose() { |
| mWaitCloseCount++; |
| } |
| |
| @Override |
| public void enforceClose() { |
| mEnforceCloseCount++; |
| } |
| |
| public Surface surface() { |
| return mSurface; |
| } |
| |
| public int destroyedCount() { |
| return mDestroyedCount; |
| } |
| |
| public int waitCloseCount() { |
| return mWaitCloseCount; |
| } |
| |
| public int enforceCloseCount() { |
| return mEnforceCloseCount; |
| } |
| }; |
| |
| HostMock mHost = new HostMock(); |
| |
| // Send a window token and provide the surface, so that the overlay is ready for use. |
| void sendTokenAndSurface() { |
| mCore.onWindowToken(mWindowToken); |
| // Make sure that somebody called takeSurface. |
| assertNotNull(holderCallback()); |
| |
| checkDialogIsShown(); |
| |
| // Provide the Android Surface. |
| holderCallback().surfaceCreated(mHolder); |
| |
| // The host should have been told about the surface. |
| assertEquals(mSurface, mHost.surface()); |
| } |
| |
| // Verify that the dialog has been shown. |
| void checkDialogIsShown() { |
| assertEquals(mDialog, ShadowDialog.getShownDialogs().get(0)); |
| } |
| |
| // Verify that the dialog is not currently shown. Note that dismiss() doesn't remove it from |
| // the shown dialog list in Robolectric, so we check for "was never shown or was dismissed". |
| void checkDialogIsNotShown() { |
| assertTrue(ShadowDialog.getShownDialogs().size() == 0 |
| || Shadows.shadowOf(mDialog).hasBeenDismissed()); |
| } |
| |
| // Verify that |mCore| signaled that the overlay was lost to|mHost|. |
| void checkOverlayWasDestroyed() { |
| // |mCore| should have notified the host that it has been destroyed, and also waited for |
| // the host to signal that the client released it. |
| assertEquals(1, mHost.destroyedCount()); |
| checkDialogIsNotShown(); |
| } |
| |
| // Check that releasing an overlay before getting a window token works. |
| @Test |
| @Config(shadows = {MyPhoneWindowShadow.class}) |
| public void testReleaseImmediately() { |
| // Release the overlay. |mCore| shouldn't notify us, since we released it. |
| createOverlay(); |
| mCore.release(); |
| checkOverlayDidntCall(); |
| checkDialogIsNotShown(); |
| } |
| |
| // Create a dialog, then send it a token. Verify that it's shown. |
| @Test |
| @Config(shadows = {MyPhoneWindowShadow.class}) |
| public void testTokenThenRelease() { |
| createOverlay(); |
| mCore.onWindowToken(mWindowToken); |
| checkDialogIsShown(); |
| |
| // Release the surface. |mHost| shouldn't be notified, nor should it wait for cleanup. |
| // Note: it might be okay if it checks for cleanup, since cleanup would be complete after |
| // we call release(). However, it's not needed, so we enforce that it isn't. |
| mCore.release(); |
| checkOverlayDidntCall(); |
| checkDialogIsNotShown(); |
| } |
| |
| // Create a dialog, send a token, send a surface, then release it. |
| @Test |
| @Config(shadows = {MyPhoneWindowShadow.class}) |
| public void testSurfaceThenRelease() { |
| createOverlay(); |
| sendTokenAndSurface(); |
| |
| mCore.release(); |
| assertEquals(0, mHost.destroyedCount()); |
| assertEquals(0, mHost.waitCloseCount()); |
| assertEquals(0, mHost.enforceCloseCount()); |
| checkDialogIsNotShown(); |
| } |
| |
| // Create a dialog, send a surface, then destroy the surface. |
| @Test |
| @Config(shadows = {MyPhoneWindowShadow.class}) |
| public void testSurfaceThenDestroy() { |
| createOverlay(); |
| sendTokenAndSurface(); |
| |
| // Destroy the surface. |
| holderCallback().surfaceDestroyed(mHolder); |
| // |mCore| should have waited for cleanup during surfaceDestroyed. |
| assertEquals(1, mHost.waitCloseCount()); |
| // Since we waited for cleanup, also pretend that the release was posted during the wait and |
| // will arrive after the wait completes. |
| mCore.release(); |
| assertEquals(1, mHost.enforceCloseCount()); |
| |
| checkOverlayWasDestroyed(); |
| } |
| |
| // Test that we're notified when the window token changes. |
| @Test |
| @Config(shadows = {MyPhoneWindowShadow.class}) |
| public void testChangeWindowToken() { |
| createOverlay(); |
| sendTokenAndSurface(); |
| |
| // Change the window token. |
| mCore.onWindowToken(new Binder()); |
| |
| checkOverlayWasDestroyed(); |
| } |
| |
| // Test that we're notified when the window token is lost. |
| @Test |
| @Config(shadows = {MyPhoneWindowShadow.class}) |
| public void testLoseWindowToken() { |
| createOverlay(); |
| sendTokenAndSurface(); |
| |
| // Remove the window token. |
| mCore.onWindowToken(null); |
| |
| checkOverlayWasDestroyed(); |
| } |
| |
| // Test that the layout params reflect TYPE_APPLICATION_MEDIA, and that it its geometry matches |
| // what we requested. |
| @Test |
| @Config(shadows = {MyPhoneWindowShadow.class}) |
| public void testOverlayTypeAndGeometry() { |
| createOverlay(); |
| mCore.onWindowToken(mWindowToken); |
| assertEquals(WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA, layoutParams().type); |
| assertEquals(mConfig.rect.x, layoutParams().x); |
| assertEquals(mConfig.rect.y, layoutParams().y); |
| assertEquals(mConfig.rect.width, layoutParams().width); |
| assertEquals(mConfig.rect.height, layoutParams().height); |
| } |
| |
| // Test that the layout params reflect TYPE_APPLICATION_PANEL when we request it. |
| @Test |
| @Config(shadows = {MyPhoneWindowShadow.class}) |
| public void testOverlayAsPanel() { |
| mAsPanel = true; |
| createOverlay(); |
| mCore.onWindowToken(mWindowToken); |
| assertEquals(layoutParams().type, WindowManager.LayoutParams.TYPE_APPLICATION_PANEL); |
| } |
| |
| @Test |
| @Config(shadows = {MyPhoneWindowShadow.class}) |
| public void testNoParamsUpdateForSamePositionRect() { |
| createOverlay(); |
| mCore.onWindowToken(mWindowToken); |
| assertTrue(getShadowWindow().mDidUpdateParams); |
| |
| // Update with the same rect, it should not update the window params. |
| getShadowWindow().mDidUpdateParams = false; |
| mCore.layoutSurface(mConfig.rect); |
| assertFalse(getShadowWindow().mDidUpdateParams); |
| } |
| } |