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