| 1 | // Copyright 2013 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.android_webview.test; |
| 6 | |
| 7 | import android.test.suitebuilder.annotation.SmallTest; |
| 8 | import android.view.View; |
| 9 | import android.view.View.MeasureSpec; |
| 10 | import android.view.ViewGroup.LayoutParams; |
| 11 | import android.widget.LinearLayout; |
| 12 | import android.util.Log; |
| 13 | |
| 14 | import org.chromium.android_webview.AwContents; |
| 15 | import org.chromium.android_webview.AwContentsClient; |
| 16 | import org.chromium.android_webview.AwLayoutSizer; |
| 17 | import org.chromium.android_webview.test.util.CommonResources; |
| 18 | import org.chromium.base.test.util.Feature; |
| 19 | import org.chromium.content.browser.ContentViewCore; |
| 20 | import org.chromium.content.browser.test.util.CallbackHelper; |
| 21 | import org.chromium.ui.gfx.DeviceDisplayInfo; |
| 22 | |
| 23 | import java.util.concurrent.atomic.AtomicReference; |
| 24 | import java.util.concurrent.TimeoutException; |
| 25 | |
| 26 | /** |
| 27 | * Tests for certain edge cases related to integrating with the Android view system. |
| 28 | */ |
| 29 | public class AndroidViewIntegrationTest extends AwTestBase { |
| 30 | final int CONTENT_SIZE_CHANGE_STABILITY_TIMEOUT_MS = 1000; |
| 31 | |
| 32 | private static class OnContentSizeChangedHelper extends CallbackHelper { |
| 33 | private int mWidth; |
| 34 | private int mHeight; |
| 35 | |
| 36 | public int getWidth() { |
| 37 | assert(getCallCount() > 0); |
| 38 | return mWidth; |
| 39 | } |
| 40 | |
| 41 | public int getHeight() { |
| 42 | assert(getCallCount() > 0); |
| 43 | return mHeight; |
| 44 | } |
| 45 | |
| 46 | public void onContentSizeChanged(int widthCss, int heightCss) { |
| 47 | mWidth = widthCss; |
| 48 | mHeight = heightCss; |
| 49 | notifyCalled(); |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | private OnContentSizeChangedHelper mOnContentSizeChangedHelper = |
| 54 | new OnContentSizeChangedHelper(); |
| 55 | private CallbackHelper mOnPageScaleChangedHelper = new CallbackHelper(); |
| 56 | |
| 57 | private class TestAwLayoutSizer extends AwLayoutSizer { |
| 58 | @Override |
| 59 | public void onContentSizeChanged(int widthCss, int heightCss) { |
| 60 | super.onContentSizeChanged(widthCss, heightCss); |
| 61 | mOnContentSizeChangedHelper.onContentSizeChanged(widthCss, heightCss); |
| 62 | } |
| 63 | |
| 64 | @Override |
| 65 | public void onPageScaleChanged(double pageScaleFactor) { |
| 66 | super.onPageScaleChanged(pageScaleFactor); |
| 67 | mOnPageScaleChangedHelper.notifyCalled(); |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | @Override |
| 72 | protected TestDependencyFactory createTestDependencyFactory() { |
| 73 | return new TestDependencyFactory() { |
| 74 | @Override |
| 75 | public AwLayoutSizer createLayoutSizer() { |
| 76 | return new TestAwLayoutSizer(); |
| 77 | } |
| 78 | }; |
| 79 | } |
| 80 | |
| 81 | final LinearLayout.LayoutParams wrapContentLayoutParams = |
| 82 | new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); |
| 83 | |
| 84 | private AwTestContainerView createCustomTestContainerViewOnMainSync( |
| 85 | final AwContentsClient awContentsClient, final int visibility) throws Exception { |
| 86 | final AtomicReference<AwTestContainerView> testContainerView = |
| 87 | new AtomicReference<AwTestContainerView>(); |
| 88 | getInstrumentation().runOnMainSync(new Runnable() { |
| 89 | @Override |
| 90 | public void run() { |
| 91 | testContainerView.set(createAwTestContainerView(awContentsClient)); |
| 92 | testContainerView.get().setLayoutParams(wrapContentLayoutParams); |
| 93 | testContainerView.get().setVisibility(visibility); |
| 94 | } |
| 95 | }); |
| 96 | return testContainerView.get(); |
| 97 | } |
| 98 | |
| 99 | private AwTestContainerView createDetachedTestContainerViewOnMainSync( |
| 100 | final AwContentsClient awContentsClient) { |
| 101 | final AtomicReference<AwTestContainerView> testContainerView = |
| 102 | new AtomicReference<AwTestContainerView>(); |
| 103 | getInstrumentation().runOnMainSync(new Runnable() { |
| 104 | @Override |
| 105 | public void run() { |
| 106 | testContainerView.set(createDetachedAwTestContainerView(awContentsClient)); |
| 107 | } |
| 108 | }); |
| 109 | return testContainerView.get(); |
| 110 | } |
| 111 | |
| 112 | private void assertZeroHeight(final AwTestContainerView testContainerView) throws Throwable { |
| 113 | // Make sure the test isn't broken by the view having a non-zero height. |
| 114 | getInstrumentation().runOnMainSync(new Runnable() { |
| 115 | @Override |
| 116 | public void run() { |
| 117 | assertEquals(0, testContainerView.getHeight()); |
| 118 | } |
| 119 | }); |
| 120 | } |
| 121 | |
| 122 | private int getRootLayoutWidthOnMainThread() throws Exception { |
| 123 | final AtomicReference<Integer> width = new AtomicReference<Integer>(); |
| 124 | getInstrumentation().runOnMainSync(new Runnable() { |
| 125 | @Override |
| 126 | public void run() { |
| 127 | width.set(Integer.valueOf(getActivity().getRootLayoutWidth())); |
| 128 | } |
| 129 | }); |
| 130 | return width.get(); |
| 131 | } |
| 132 | |
| 133 | /** |
| 134 | * This checks for issues related to loading content into a 0x0 view. |
| 135 | * |
| 136 | * A 0x0 sized view is common if the WebView is set to wrap_content and newly created. The |
| 137 | * expected behavior is for the WebView to expand after some content is loaded. |
| 138 | * In Chromium it would be valid to not load or render content into a WebContents with a 0x0 |
| 139 | * view (since the user can't see it anyway) and only do so after the view's size is non-zero. |
| 140 | * Such behavior is unacceptable for the WebView and this test is to ensure that such behavior |
| 141 | * is not re-introduced. |
| 142 | */ |
| 143 | @SmallTest |
| 144 | @Feature({"AndroidWebView"}) |
| 145 | public void testZeroByZeroViewLoadsContent() throws Throwable { |
| 146 | final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| 147 | final AwTestContainerView testContainerView = createCustomTestContainerViewOnMainSync( |
| 148 | contentsClient, View.VISIBLE); |
| 149 | assertZeroHeight(testContainerView); |
| 150 | |
| 151 | final int contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount(); |
| 152 | final int pageScaleChangeCallCount = mOnPageScaleChangedHelper.getCallCount(); |
| 153 | loadUrlAsync(testContainerView.getAwContents(), CommonResources.ABOUT_HTML); |
| 154 | mOnPageScaleChangedHelper.waitForCallback(pageScaleChangeCallCount); |
| 155 | mOnContentSizeChangedHelper.waitForCallback(contentSizeChangeCallCount); |
| 156 | assertTrue(mOnContentSizeChangedHelper.getHeight() > 0); |
| 157 | } |
| 158 | |
| 159 | /** |
| 160 | * Check that a content size change notification is issued when the view is invisible. |
| 161 | * |
| 162 | * This makes sure that any optimizations related to the view's visibility don't inhibit |
| 163 | * the ability to load pages. Many applications keep the WebView hidden when it's loading. |
| 164 | */ |
| 165 | @SmallTest |
| 166 | @Feature({"AndroidWebView"}) |
| 167 | public void testInvisibleViewLoadsContent() throws Throwable { |
| 168 | final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| 169 | final AwTestContainerView testContainerView = createCustomTestContainerViewOnMainSync( |
| 170 | contentsClient, View.INVISIBLE); |
| 171 | assertZeroHeight(testContainerView); |
| 172 | |
| 173 | final int contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount(); |
| 174 | final int pageScaleChangeCallCount = mOnPageScaleChangedHelper.getCallCount(); |
| 175 | loadUrlAsync(testContainerView.getAwContents(), CommonResources.ABOUT_HTML); |
| 176 | mOnPageScaleChangedHelper.waitForCallback(pageScaleChangeCallCount); |
| 177 | mOnContentSizeChangedHelper.waitForCallback(contentSizeChangeCallCount); |
| 178 | assertTrue(mOnContentSizeChangedHelper.getHeight() > 0); |
| 179 | |
| 180 | getInstrumentation().runOnMainSync(new Runnable() { |
| 181 | @Override |
| 182 | public void run() { |
| 183 | assertEquals(View.INVISIBLE, testContainerView.getVisibility()); |
| 184 | } |
| 185 | }); |
| 186 | } |
| 187 | |
| 188 | /** |
| 189 | * Check that a content size change notification is sent even if the WebView is off screen. |
| 190 | */ |
| 191 | @SmallTest |
| 192 | @Feature({"AndroidWebView"}) |
| 193 | public void testDisconnectedViewLoadsContent() throws Throwable { |
| 194 | final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| 195 | final AwTestContainerView testContainerView = |
| 196 | createDetachedTestContainerViewOnMainSync(contentsClient); |
| 197 | assertZeroHeight(testContainerView); |
| 198 | |
| 199 | final int contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount(); |
| 200 | final int pageScaleChangeCallCount = mOnPageScaleChangedHelper.getCallCount(); |
| 201 | loadUrlAsync(testContainerView.getAwContents(), CommonResources.ABOUT_HTML); |
| 202 | mOnPageScaleChangedHelper.waitForCallback(pageScaleChangeCallCount); |
| 203 | mOnContentSizeChangedHelper.waitForCallback(contentSizeChangeCallCount); |
| 204 | assertTrue(mOnContentSizeChangedHelper.getHeight() > 0); |
| 205 | } |
| 206 | |
| 207 | private String makeHtmlPageOfSize(int widthCss, int heightCss) { |
| 208 | return CommonResources.makeHtmlPageFrom( |
| 209 | "<style type=\"text/css\">" + |
| 210 | "body { margin:0px; padding:0px; } " + |
| 211 | "div { " + |
| 212 | "width:" + widthCss + "px; " + |
| 213 | "height:" + heightCss + "px; " + |
| 214 | "background-color: red; " + |
| 215 | "} " + |
| 216 | "</style>", "<div/>"); |
| 217 | } |
| 218 | |
| 219 | private void waitForContentSizeToChangeTo(OnContentSizeChangedHelper helper, int callCount, |
| 220 | int widthCss, int heightCss) throws Exception { |
| 221 | final int maxSizeChangeNotificationsToWaitFor = 5; |
| 222 | for (int i = 1; i <= maxSizeChangeNotificationsToWaitFor; i++) { |
| 223 | helper.waitForCallback(callCount, i); |
| 224 | if ((heightCss == -1 || helper.getHeight() == heightCss) && |
| 225 | (widthCss == -1 || helper.getWidth() == widthCss)) { |
| 226 | break; |
| 227 | } |
| 228 | // This means that we hit the max number of iterations but the expected contents size |
| 229 | // wasn't reached. |
| 230 | assertTrue(i != maxSizeChangeNotificationsToWaitFor); |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | private void loadPageOfSizeAndWaitForSizeChange(AwContents awContents, |
| 235 | OnContentSizeChangedHelper helper, int widthCss, int heightCss) throws Exception { |
| 236 | |
| 237 | final String htmlData = makeHtmlPageOfSize(widthCss, heightCss); |
| 238 | final int contentSizeChangeCallCount = helper.getCallCount(); |
| 239 | loadDataAsync(awContents, htmlData, "text/html", false); |
| 240 | |
| 241 | waitForContentSizeToChangeTo(helper, contentSizeChangeCallCount, widthCss, heightCss); |
| 242 | } |
| 243 | |
| 244 | @SmallTest |
| 245 | @Feature({"AndroidWebView"}) |
| 246 | public void testPreferredSizeUpdateWhenDetached() throws Throwable { |
| 247 | final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| 248 | final AwTestContainerView testContainerView = createDetachedTestContainerViewOnMainSync( |
| 249 | contentsClient); |
| 250 | assertZeroHeight(testContainerView); |
| 251 | |
| 252 | final int contentWidthCss = 142; |
| 253 | final int contentHeightCss = 180; |
| 254 | |
| 255 | loadPageOfSizeAndWaitForSizeChange(testContainerView.getAwContents(), |
| 256 | mOnContentSizeChangedHelper, contentWidthCss, contentHeightCss); |
| 257 | } |
| 258 | |
| 259 | @SmallTest |
| 260 | @Feature({"AndroidWebView"}) |
| 261 | public void testViewSizedCorrectlyInWrapContentMode() throws Throwable { |
| 262 | final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| 263 | final AwTestContainerView testContainerView = createCustomTestContainerViewOnMainSync( |
| 264 | contentsClient, View.VISIBLE); |
| 265 | assertZeroHeight(testContainerView); |
| 266 | |
| 267 | final double deviceDIPScale = |
| 268 | DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale(); |
| 269 | |
| 270 | final int contentWidthCss = 142; |
| 271 | final int contentHeightCss = 180; |
| 272 | |
| 273 | // In wrap-content mode the AwLayoutSizer will size the view to be as wide as the parent |
| 274 | // view. |
| 275 | final int expectedWidthCss = (int) (getRootLayoutWidthOnMainThread() / deviceDIPScale); |
| 276 | final int expectedHeightCss = contentHeightCss; |
| 277 | |
| 278 | loadPageOfSizeAndWaitForSizeChange(testContainerView.getAwContents(), |
| 279 | mOnContentSizeChangedHelper, expectedWidthCss, expectedHeightCss); |
| 280 | |
| 281 | // This is to make sure that there are no more pending size change notifications. Ideally |
| 282 | // we'd assert that the renderer is idle (has no pending layout passes) but that would |
| 283 | // require quite a bit of plumbing, so we just wait a bit and make sure the size hadn't |
| 284 | // changed. |
| 285 | Thread.sleep(CONTENT_SIZE_CHANGE_STABILITY_TIMEOUT_MS); |
| 286 | assertEquals(expectedWidthCss, mOnContentSizeChangedHelper.getWidth()); |
| 287 | assertEquals(expectedHeightCss, mOnContentSizeChangedHelper.getHeight()); |
| 288 | } |
| 289 | } |