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 5package org.chromium.android_webview.test; 6 7import android.test.suitebuilder.annotation.SmallTest; 8import android.view.View; 9import android.view.ViewGroup.LayoutParams; 10import android.widget.LinearLayout; 11 12import org.chromium.android_webview.AwContents; 13import org.chromium.android_webview.AwContentsClient; 14import org.chromium.android_webview.AwLayoutSizer; 15import org.chromium.android_webview.test.util.CommonResources; 16import org.chromium.base.test.util.Feature; 17import org.chromium.content.browser.test.util.CallbackHelper; 18import org.chromium.ui.gfx.DeviceDisplayInfo; 19 20import java.util.concurrent.atomic.AtomicReference; 21 22/** 23 * Tests for certain edge cases related to integrating with the Android view system. 24 */ 25public class AndroidViewIntegrationTest extends AwTestBase { 26 private static final int CONTENT_SIZE_CHANGE_STABILITY_TIMEOUT_MS = 1000; 27 28 private static class OnContentSizeChangedHelper extends CallbackHelper { 29 private int mWidth; 30 private int mHeight; 31 32 public int getWidth() { 33 assert getCallCount() > 0; 34 return mWidth; 35 } 36 37 public int getHeight() { 38 assert getCallCount() > 0; 39 return mHeight; 40 } 41 42 public void onContentSizeChanged(int widthCss, int heightCss) { 43 mWidth = widthCss; 44 mHeight = heightCss; 45 notifyCalled(); 46 } 47 } 48 49 private OnContentSizeChangedHelper mOnContentSizeChangedHelper = 50 new OnContentSizeChangedHelper(); 51 private CallbackHelper mOnPageScaleChangedHelper = new CallbackHelper(); 52 53 private class TestAwLayoutSizer extends AwLayoutSizer { 54 @Override 55 public void onContentSizeChanged(int widthCss, int heightCss) { 56 super.onContentSizeChanged(widthCss, heightCss); 57 if (mOnContentSizeChangedHelper != null) 58 mOnContentSizeChangedHelper.onContentSizeChanged(widthCss, heightCss); 59 } 60 61 @Override 62 public void onPageScaleChanged(float pageScaleFactor) { 63 super.onPageScaleChanged(pageScaleFactor); 64 if (mOnPageScaleChangedHelper != null) 65 mOnPageScaleChangedHelper.notifyCalled(); 66 } 67 } 68 69 @Override 70 protected TestDependencyFactory createTestDependencyFactory() { 71 return new TestDependencyFactory() { 72 @Override 73 public AwLayoutSizer createLayoutSizer() { 74 return new TestAwLayoutSizer(); 75 } 76 }; 77 } 78 79 final LinearLayout.LayoutParams mWrapContentLayoutParams = 80 new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 81 82 private AwTestContainerView createCustomTestContainerViewOnMainSync( 83 final AwContentsClient awContentsClient, final int visibility) throws Exception { 84 final AtomicReference<AwTestContainerView> testContainerView = 85 new AtomicReference<AwTestContainerView>(); 86 getInstrumentation().runOnMainSync(new Runnable() { 87 @Override 88 public void run() { 89 testContainerView.set(createAwTestContainerView(awContentsClient)); 90 testContainerView.get().setLayoutParams(mWrapContentLayoutParams); 91 testContainerView.get().setVisibility(visibility); 92 } 93 }); 94 return testContainerView.get(); 95 } 96 97 private AwTestContainerView createDetachedTestContainerViewOnMainSync( 98 final AwContentsClient awContentsClient) { 99 final AtomicReference<AwTestContainerView> testContainerView = 100 new AtomicReference<AwTestContainerView>(); 101 getInstrumentation().runOnMainSync(new Runnable() { 102 @Override 103 public void run() { 104 testContainerView.set(createDetachedAwTestContainerView(awContentsClient)); 105 } 106 }); 107 return testContainerView.get(); 108 } 109 110 private void assertZeroHeight(final AwTestContainerView testContainerView) throws Throwable { 111 // Make sure the test isn't broken by the view having a non-zero height. 112 getInstrumentation().runOnMainSync(new Runnable() { 113 @Override 114 public void run() { 115 assertEquals(0, testContainerView.getHeight()); 116 } 117 }); 118 } 119 120 private int getRootLayoutWidthOnMainThread() throws Exception { 121 final AtomicReference<Integer> width = new AtomicReference<Integer>(); 122 getInstrumentation().runOnMainSync(new Runnable() { 123 @Override 124 public void run() { 125 width.set(Integer.valueOf(getActivity().getRootLayoutWidth())); 126 } 127 }); 128 return width.get(); 129 } 130 131 /** 132 * This checks for issues related to loading content into a 0x0 view. 133 * 134 * A 0x0 sized view is common if the WebView is set to wrap_content and newly created. The 135 * expected behavior is for the WebView to expand after some content is loaded. 136 * In Chromium it would be valid to not load or render content into a WebContents with a 0x0 137 * view (since the user can't see it anyway) and only do so after the view's size is non-zero. 138 * Such behavior is unacceptable for the WebView and this test is to ensure that such behavior 139 * is not re-introduced. 140 */ 141 @SmallTest 142 @Feature({"AndroidWebView"}) 143 public void testZeroByZeroViewLoadsContent() throws Throwable { 144 final TestAwContentsClient contentsClient = new TestAwContentsClient(); 145 final AwTestContainerView testContainerView = createCustomTestContainerViewOnMainSync( 146 contentsClient, View.VISIBLE); 147 assertZeroHeight(testContainerView); 148 149 final int contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount(); 150 final int pageScaleChangeCallCount = mOnPageScaleChangedHelper.getCallCount(); 151 loadUrlAsync(testContainerView.getAwContents(), CommonResources.ABOUT_HTML); 152 mOnPageScaleChangedHelper.waitForCallback(pageScaleChangeCallCount); 153 mOnContentSizeChangedHelper.waitForCallback(contentSizeChangeCallCount); 154 assertTrue(mOnContentSizeChangedHelper.getHeight() > 0); 155 } 156 157 /** 158 * Check that a content size change notification is issued when the view is invisible. 159 * 160 * This makes sure that any optimizations related to the view's visibility don't inhibit 161 * the ability to load pages. Many applications keep the WebView hidden when it's loading. 162 */ 163 @SmallTest 164 @Feature({"AndroidWebView"}) 165 public void testInvisibleViewLoadsContent() throws Throwable { 166 final TestAwContentsClient contentsClient = new TestAwContentsClient(); 167 final AwTestContainerView testContainerView = createCustomTestContainerViewOnMainSync( 168 contentsClient, View.INVISIBLE); 169 assertZeroHeight(testContainerView); 170 171 final int contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount(); 172 final int pageScaleChangeCallCount = mOnPageScaleChangedHelper.getCallCount(); 173 loadUrlAsync(testContainerView.getAwContents(), CommonResources.ABOUT_HTML); 174 mOnPageScaleChangedHelper.waitForCallback(pageScaleChangeCallCount); 175 mOnContentSizeChangedHelper.waitForCallback(contentSizeChangeCallCount); 176 assertTrue(mOnContentSizeChangedHelper.getHeight() > 0); 177 178 getInstrumentation().runOnMainSync(new Runnable() { 179 @Override 180 public void run() { 181 assertEquals(View.INVISIBLE, testContainerView.getVisibility()); 182 } 183 }); 184 } 185 186 /** 187 * Check that a content size change notification is sent even if the WebView is off screen. 188 */ 189 @SmallTest 190 @Feature({"AndroidWebView"}) 191 public void testDisconnectedViewLoadsContent() throws Throwable { 192 final TestAwContentsClient contentsClient = new TestAwContentsClient(); 193 final AwTestContainerView testContainerView = 194 createDetachedTestContainerViewOnMainSync(contentsClient); 195 assertZeroHeight(testContainerView); 196 197 final int contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount(); 198 final int pageScaleChangeCallCount = mOnPageScaleChangedHelper.getCallCount(); 199 loadUrlAsync(testContainerView.getAwContents(), CommonResources.ABOUT_HTML); 200 mOnPageScaleChangedHelper.waitForCallback(pageScaleChangeCallCount); 201 mOnContentSizeChangedHelper.waitForCallback(contentSizeChangeCallCount); 202 assertTrue(mOnContentSizeChangedHelper.getHeight() > 0); 203 } 204 205 private String makeHtmlPageOfSize(int widthCss, int heightCss, boolean heightPercent) { 206 String content = "<div class=\"normal\">a</div>"; 207 if (heightPercent) 208 content += "<div class=\"heightPercent\"></div>"; 209 return CommonResources.makeHtmlPageFrom( 210 "<style type=\"text/css\">" + 211 "body { margin:0px; padding:0px; } " + 212 ".normal { " + 213 "width:" + widthCss + "px; " + 214 "height:" + heightCss + "px; " + 215 "background-color: red; " + 216 "} " + 217 ".heightPercent { " + 218 "height: 150%; " + 219 "background-color: blue; " + 220 "} " + 221 "</style>", content); 222 } 223 224 private void waitForContentSizeToChangeTo(OnContentSizeChangedHelper helper, int callCount, 225 int widthCss, int heightCss) throws Exception { 226 final int maxSizeChangeNotificationsToWaitFor = 5; 227 for (int i = 1; i <= maxSizeChangeNotificationsToWaitFor; i++) { 228 helper.waitForCallback(callCount, i); 229 if ((heightCss == -1 || helper.getHeight() == heightCss) && 230 (widthCss == -1 || helper.getWidth() == widthCss)) { 231 break; 232 } 233 // This means that we hit the max number of iterations but the expected contents size 234 // wasn't reached. 235 assertTrue(i != maxSizeChangeNotificationsToWaitFor); 236 } 237 } 238 239 private void loadPageOfSizeAndWaitForSizeChange(AwContents awContents, 240 OnContentSizeChangedHelper helper, int widthCss, int heightCss, 241 boolean heightPercent) throws Exception { 242 243 final String htmlData = makeHtmlPageOfSize(widthCss, heightCss, heightPercent); 244 final int contentSizeChangeCallCount = helper.getCallCount(); 245 loadDataAsync(awContents, htmlData, "text/html", false); 246 247 waitForContentSizeToChangeTo(helper, contentSizeChangeCallCount, widthCss, heightCss); 248 } 249 250 @SmallTest 251 @Feature({"AndroidWebView"}) 252 public void testSizeUpdateWhenDetached() throws Throwable { 253 final TestAwContentsClient contentsClient = new TestAwContentsClient(); 254 final AwTestContainerView testContainerView = createDetachedTestContainerViewOnMainSync( 255 contentsClient); 256 assertZeroHeight(testContainerView); 257 258 final int contentWidthCss = 142; 259 final int contentHeightCss = 180; 260 261 loadPageOfSizeAndWaitForSizeChange(testContainerView.getAwContents(), 262 mOnContentSizeChangedHelper, contentWidthCss, contentHeightCss, false); 263 } 264 265 public void waitForNoLayoutsPending() throws InterruptedException { 266 // This is to make sure that there are no more pending size change notifications. Ideally 267 // we'd assert that the renderer is idle (has no pending layout passes) but that would 268 // require quite a bit of plumbing, so we just wait a bit and make sure the size hadn't 269 // changed. 270 Thread.sleep(CONTENT_SIZE_CHANGE_STABILITY_TIMEOUT_MS); 271 } 272 273 @SmallTest 274 @Feature({"AndroidWebView"}) 275 public void testAbsolutePositionContributesToContentSize() throws Throwable { 276 final TestAwContentsClient contentsClient = new TestAwContentsClient(); 277 final AwTestContainerView testContainerView = createDetachedTestContainerViewOnMainSync( 278 contentsClient); 279 assertZeroHeight(testContainerView); 280 281 final int widthCss = 142; 282 final int heightCss = 180; 283 284 final String htmlData = CommonResources.makeHtmlPageFrom( 285 "<style type=\"text/css\">" + 286 "body { margin:0px; padding:0px; } " + 287 "div { " + 288 "position: absolute; " + 289 "width:" + widthCss + "px; " + 290 "height:" + heightCss + "px; " + 291 "background-color: red; " + 292 "} " + 293 "</style>", "<div>a</div>"); 294 295 final int contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount(); 296 loadDataAsync(testContainerView.getAwContents(), htmlData, "text/html", false); 297 298 waitForContentSizeToChangeTo(mOnContentSizeChangedHelper, contentSizeChangeCallCount, 299 widthCss, heightCss); 300 } 301 302 @SmallTest 303 @Feature({"AndroidWebView"}) 304 public void testViewSizedCorrectlyInWrapContentMode() throws Throwable { 305 final TestAwContentsClient contentsClient = new TestAwContentsClient(); 306 final AwTestContainerView testContainerView = createCustomTestContainerViewOnMainSync( 307 contentsClient, View.VISIBLE); 308 assertZeroHeight(testContainerView); 309 310 final double deviceDIPScale = 311 DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale(); 312 313 final int contentWidthCss = 142; 314 final int contentHeightCss = 180; 315 316 // In wrap-content mode the AwLayoutSizer will size the view to be as wide as the parent 317 // view. 318 final int expectedWidthCss = 319 (int) Math.ceil(getRootLayoutWidthOnMainThread() / deviceDIPScale); 320 final int expectedHeightCss = contentHeightCss; 321 322 loadPageOfSizeAndWaitForSizeChange(testContainerView.getAwContents(), 323 mOnContentSizeChangedHelper, expectedWidthCss, expectedHeightCss, false); 324 325 waitForNoLayoutsPending(); 326 assertEquals(expectedWidthCss, mOnContentSizeChangedHelper.getWidth()); 327 assertEquals(expectedHeightCss, mOnContentSizeChangedHelper.getHeight()); 328 } 329 330 @SmallTest 331 @Feature({"AndroidWebView"}) 332 public void testViewSizedCorrectlyInWrapContentModeWithDynamicContents() throws Throwable { 333 final TestAwContentsClient contentsClient = new TestAwContentsClient(); 334 final AwTestContainerView testContainerView = createCustomTestContainerViewOnMainSync( 335 contentsClient, View.VISIBLE); 336 assertZeroHeight(testContainerView); 337 338 final double deviceDIPScale = 339 DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale(); 340 341 final int contentWidthCss = 142; 342 final int contentHeightCss = 180; 343 344 final int expectedWidthCss = 345 (int) Math.ceil(getRootLayoutWidthOnMainThread() / deviceDIPScale); 346 final int expectedHeightCss = contentHeightCss; 347 348 loadPageOfSizeAndWaitForSizeChange(testContainerView.getAwContents(), 349 mOnContentSizeChangedHelper, expectedWidthCss, contentHeightCss, true); 350 351 waitForNoLayoutsPending(); 352 assertEquals(expectedWidthCss, mOnContentSizeChangedHelper.getWidth()); 353 assertEquals(expectedHeightCss, mOnContentSizeChangedHelper.getHeight()); 354 } 355 356 @SmallTest 357 @Feature({"AndroidWebView"}) 358 public void testReceivingSizeAfterLoadUpdatesLayout() throws Throwable { 359 final TestAwContentsClient contentsClient = new TestAwContentsClient(); 360 final AwTestContainerView testContainerView = createDetachedTestContainerViewOnMainSync( 361 contentsClient); 362 final AwContents awContents = testContainerView.getAwContents(); 363 364 final double deviceDIPScale = 365 DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale(); 366 final int physicalWidth = 600; 367 final int spanWidth = 42; 368 final int expectedWidthCss = 369 (int) Math.ceil(physicalWidth / deviceDIPScale); 370 371 StringBuilder htmlBuilder = new StringBuilder("<html><body style='margin:0px;'>"); 372 final String spanBlock = 373 "<span style='width: " + spanWidth + "px; display: inline-block;'>a</span>"; 374 for (int i = 0; i < 10; ++i) { 375 htmlBuilder.append(spanBlock); 376 } 377 htmlBuilder.append("</body></html>"); 378 379 int contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount(); 380 loadDataAsync(awContents, htmlBuilder.toString(), "text/html", false); 381 // Because we're loading the contents into a detached WebView its layout size is 0x0 and as 382 // a result of that the paragraph will be formated such that each word is on a separate 383 // line. 384 waitForContentSizeToChangeTo(mOnContentSizeChangedHelper, contentSizeChangeCallCount, 385 spanWidth, -1); 386 387 final int narrowLayoutHeight = mOnContentSizeChangedHelper.getHeight(); 388 389 contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount(); 390 getInstrumentation().runOnMainSync(new Runnable() { 391 @Override 392 public void run() { 393 testContainerView.onSizeChanged(physicalWidth, 0, 0, 0); 394 } 395 }); 396 mOnContentSizeChangedHelper.waitForCallback(contentSizeChangeCallCount); 397 398 // As a result of calling the onSizeChanged method the layout size should be updated to 399 // match the width of the webview and the text we previously loaded should reflow making the 400 // contents width match the WebView width. 401 assertEquals(expectedWidthCss, mOnContentSizeChangedHelper.getWidth()); 402 assertTrue(mOnContentSizeChangedHelper.getHeight() < narrowLayoutHeight); 403 assertTrue(mOnContentSizeChangedHelper.getHeight() > 0); 404 } 405} 406