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 | } |