1 /* 2 * Copyright 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.webkit.cts; 18 19 import static org.hamcrest.MatcherAssert.assertThat; 20 import static org.hamcrest.Matchers.greaterThan; 21 import static org.hamcrest.Matchers.greaterThanOrEqualTo; 22 import static org.hamcrest.Matchers.lessThan; 23 import static org.hamcrest.Matchers.lessThanOrEqualTo; 24 25 import android.net.http.SslError; 26 import android.os.StrictMode; 27 import android.os.StrictMode.ThreadPolicy; 28 import android.platform.test.annotations.AppModeFull; 29 import android.test.ActivityInstrumentationTestCase2; 30 import android.webkit.SslErrorHandler; 31 import android.webkit.WebSettings; 32 import android.webkit.WebView; 33 import android.webkit.WebViewClient; 34 import android.webkit.cts.WebViewSyncLoader.WaitForLoadedClient; 35 36 import com.android.compatibility.common.util.NullWebViewUtils; 37 import com.android.compatibility.common.util.PollingCheck; 38 39 import java.util.concurrent.BlockingQueue; 40 import java.util.concurrent.LinkedBlockingQueue; 41 42 /** 43 * Test WebView zooming behaviour 44 */ 45 @AppModeFull 46 public class WebViewZoomTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> { 47 private WebView mWebView; 48 private WebViewOnUiThread mOnUiThread; 49 private ScaleChangedWebViewClient mWebViewClient; 50 private CtsTestServer mWebServer; 51 52 /** 53 * Epsilon used in page scale value comparisons. 54 */ 55 private static final float PAGE_SCALE_EPSILON = 0.0001f; 56 57 public WebViewZoomTest() { 58 super("com.android.cts.webkit", WebViewCtsActivity.class); 59 } 60 61 @Override 62 protected void setUp() throws Exception { 63 super.setUp(); 64 final WebViewCtsActivity activity = getActivity(); 65 mWebView = activity.getWebView(); 66 if (mWebView == null) 67 return; 68 mOnUiThread = new WebViewOnUiThread(mWebView); 69 mOnUiThread.requestFocus(); 70 71 new PollingCheck() { 72 @Override 73 protected boolean check() { 74 return activity.hasWindowFocus(); 75 } 76 }.run(); 77 78 mWebViewClient = new ScaleChangedWebViewClient(); 79 mOnUiThread.setWebViewClient(mWebViewClient); 80 81 // Pinch zoom is not supported in wrap_content layouts. 82 mOnUiThread.setLayoutHeightToMatchParent(); 83 84 } 85 86 @Override 87 protected void tearDown() throws Exception { 88 if (mOnUiThread != null) { 89 mOnUiThread.cleanUp(); 90 } 91 if (mWebServer != null) { 92 stopWebServer(); 93 } 94 super.tearDown(); 95 } 96 97 private void stopWebServer() throws Exception { 98 assertNotNull(mWebServer); 99 ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); 100 ThreadPolicy tmpPolicy = new ThreadPolicy.Builder(oldPolicy) 101 .permitNetwork() 102 .build(); 103 StrictMode.setThreadPolicy(tmpPolicy); 104 mWebServer.shutdown(); 105 mWebServer = null; 106 StrictMode.setThreadPolicy(oldPolicy); 107 } 108 109 private void setUpPage() throws Exception { 110 assertFalse(mWebViewClient.onScaleChangedCalled()); 111 assertNull(mWebServer); 112 // Pass CtsTestserver.SslMode.TRUST_ANY_CLIENT to make the server serve https URLs yet do 113 // not ask client for client authentication. 114 mWebServer = new CtsTestServer(getActivity(), CtsTestServer.SslMode.TRUST_ANY_CLIENT); 115 mOnUiThread.loadUrlAndWaitForCompletion( 116 mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL)); 117 pollingCheckForCanZoomIn(); 118 } 119 120 public void testZoomIn() throws Throwable { 121 if (!NullWebViewUtils.isWebViewAvailable()) { 122 return; 123 } 124 125 setUpPage(); 126 127 assertTrue(mOnUiThread.zoomIn()); 128 mWebViewClient.waitForNextScaleChange(); 129 } 130 131 @SuppressWarnings("deprecation") 132 public void testGetZoomControls() { 133 if (!NullWebViewUtils.isWebViewAvailable()) { 134 return; 135 } 136 WebSettings settings = mOnUiThread.getSettings(); 137 assertTrue(settings.supportZoom()); 138 assertNotNull( 139 "Should be able to get zoom controls when zoom is enabled", 140 WebkitUtils.onMainThreadSync(() -> { return mWebView.getZoomControls(); })); 141 142 // disable zoom support 143 settings.setSupportZoom(false); 144 assertFalse(settings.supportZoom()); 145 assertNull( 146 "Should not be able to get zoom controls when zoom is disabled", 147 WebkitUtils.onMainThreadSync(() -> { return mWebView.getZoomControls(); })); 148 } 149 150 public void testInvokeZoomPicker() throws Exception { 151 if (!NullWebViewUtils.isWebViewAvailable()) { 152 return; 153 } 154 WebSettings settings = mOnUiThread.getSettings(); 155 assertTrue(settings.supportZoom()); 156 setUpPage(); 157 WebkitUtils.onMainThreadSync(() -> mWebView.invokeZoomPicker()); 158 } 159 160 public void testZoom_canNotZoomInPastMaximum() { 161 if (!NullWebViewUtils.isWebViewAvailable()) { 162 return; 163 } 164 float currScale = mOnUiThread.getScale(); 165 // Zoom in until maximum scale, in default increments. 166 while (mOnUiThread.zoomIn()) { 167 currScale = mWebViewClient.expectZoomIn(currScale); 168 } 169 170 assertFalse(mOnUiThread.zoomIn()); 171 assertNoScaleChange(currScale); 172 } 173 174 public void testZoom_canNotZoomOutPastMinimum() { 175 if (!NullWebViewUtils.isWebViewAvailable()) { 176 return; 177 } 178 float currScale = mOnUiThread.getScale(); 179 // Zoom in until maximum scale, in default increments. 180 while (mOnUiThread.zoomOut()) { 181 currScale = mWebViewClient.expectZoomOut(currScale); 182 } 183 184 assertFalse(mOnUiThread.zoomOut()); 185 assertNoScaleChange(currScale); 186 } 187 188 public void testCanZoomWhileZoomSupportedFalse() throws Throwable { 189 // setZoomSupported disables user controls, but not zooming via API 190 if (!NullWebViewUtils.isWebViewAvailable()) { 191 return; 192 } 193 194 setUpPage(); 195 196 WebSettings settings = mOnUiThread.getSettings(); 197 settings.setSupportZoom(false); 198 assertFalse(settings.supportZoom()); 199 200 float currScale = mOnUiThread.getScale(); 201 202 assertTrue("Zoom out should succeed although zoom support is disabled in web settings", 203 mOnUiThread.zoomIn()); 204 currScale = mWebViewClient.expectZoomIn(currScale); 205 206 assertTrue("Zoom out should succeed although zoom support is disabled in web settings", 207 mOnUiThread.zoomOut()); 208 currScale = mWebViewClient.expectZoomOut(currScale); 209 } 210 211 public void testZoomByPowerOfTwoIncrements() throws Throwable { 212 // setZoomSupported disables user controls, but not zooming via API 213 if (!NullWebViewUtils.isWebViewAvailable()) { 214 return; 215 } 216 217 setUpPage(); 218 float currScale = mOnUiThread.getScale(); 219 220 mOnUiThread.zoomBy(1.25f); // zoom in 221 currScale = mWebViewClient.expectZoomBy(currScale, 1.25f); 222 223 mOnUiThread.zoomBy(0.75f); // zoom out 224 currScale = mWebViewClient.expectZoomBy(currScale, 0.75f); 225 } 226 227 public void testZoomByNonPowerOfTwoIncrements() throws Throwable { 228 if (!NullWebViewUtils.isWebViewAvailable()) { 229 return; 230 } 231 232 setUpPage(); 233 234 float currScale = mOnUiThread.getScale(); 235 236 // Zoom in until maximum scale, in specified increments designed so that the last zoom will 237 // be less than expected. 238 while (mOnUiThread.canZoomIn()) { 239 mOnUiThread.zoomBy(1.7f); 240 currScale = mWebViewClient.expectZoomBy(currScale, 1.7f); 241 } 242 243 // At this point, zooming in should do nothing. 244 mOnUiThread.zoomBy(1.7f); 245 assertNoScaleChange(currScale); 246 247 // Zoom out until minimum scale, in specified increments designed so that the last zoom will 248 // be less than requested. 249 while (mOnUiThread.canZoomOut()) { 250 mOnUiThread.zoomBy(0.7f); 251 currScale = mWebViewClient.expectZoomBy(currScale, 0.7f); 252 } 253 254 // At this point, zooming out should do nothing. 255 mOnUiThread.zoomBy(0.7f); 256 assertNoScaleChange(currScale); 257 } 258 259 public void testScaleChangeCallbackMatchesGetScale() throws Throwable { 260 if (!NullWebViewUtils.isWebViewAvailable()) { 261 return; 262 } 263 assertFalse(mWebViewClient.onScaleChangedCalled()); 264 265 setUpPage(); 266 267 assertFalse(mWebViewClient.onScaleChangedCalled()); 268 assertTrue(mOnUiThread.zoomIn()); 269 ScaleChangedState state = mWebViewClient.waitForNextScaleChange(); 270 assertEquals( 271 "Expected onScaleChanged arg 2 (new scale) to equal view.getScale()", 272 state.mNewScale, mOnUiThread.getScale()); 273 } 274 275 private void assertNoScaleChange(float currScale) { 276 // We sleep to assert to the best of our ability 277 // that a scale change does *not* happen. 278 try { 279 Thread.sleep(500); 280 assertFalse(mWebViewClient.onScaleChangedCalled()); 281 assertEquals(currScale, mOnUiThread.getScale()); 282 } catch (InterruptedException e) { 283 fail("Interrupted"); 284 } 285 } 286 287 private static final class ScaleChangedState { 288 public float mOldScale; 289 public float mNewScale; 290 public boolean mCanZoomIn; 291 public boolean mCanZoomOut; 292 293 ScaleChangedState(WebView view, float oldScale, float newScale) { 294 mOldScale = oldScale; 295 mNewScale = newScale; 296 mCanZoomIn = view.canZoomIn(); 297 mCanZoomOut = view.canZoomOut(); 298 } 299 } 300 301 private void pollingCheckForCanZoomIn() { 302 new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) { 303 @Override 304 protected boolean check() { 305 return mOnUiThread.canZoomIn(); 306 } 307 }.run(); 308 } 309 310 private final class ScaleChangedWebViewClient extends WaitForLoadedClient { 311 private BlockingQueue<ScaleChangedState> mCallQueue; 312 313 public ScaleChangedWebViewClient() { 314 super(mOnUiThread); 315 mCallQueue = new LinkedBlockingQueue<>(); 316 } 317 318 @Override 319 public void onScaleChanged(WebView view, float oldScale, float newScale) { 320 super.onScaleChanged(view, oldScale, newScale); 321 mCallQueue.add(new ScaleChangedState(view, oldScale, newScale)); 322 } 323 324 public float expectZoomBy(float currentScale, float scaleAmount) { 325 assertTrue(scaleAmount != 1.0f); 326 327 float nextScale = currentScale * scaleAmount; 328 ScaleChangedState state = waitForNextScaleChange(); 329 assertEquals(currentScale, state.mOldScale); 330 331 // Check that we zoomed in the expected direction wrt. the current scale. 332 if (scaleAmount > 1.0f) { 333 assertThat( 334 "Expected new scale > current scale when zooming in", 335 state.mNewScale, greaterThan(currentScale)); 336 } else { 337 assertThat( 338 "Expected new scale < current scale when zooming out", 339 state.mNewScale, lessThan(currentScale)); 340 } 341 342 // If we hit the zoom limit, then the new scale should be between the old scale 343 // and the expected new scale. Otherwise it should equal the expected new scale. 344 if (Math.abs(nextScale - state.mNewScale) > PAGE_SCALE_EPSILON) { 345 if (scaleAmount > 1.0f) { 346 assertFalse(state.mCanZoomIn); 347 assertThat( 348 "Expected new scale <= requested scale when zooming in past limit", 349 state.mNewScale, lessThanOrEqualTo(nextScale)); 350 } else { 351 assertFalse(state.mCanZoomOut); 352 assertThat( 353 "Expected new scale >= requested scale when zooming out past limit", 354 state.mNewScale, greaterThanOrEqualTo(nextScale)); 355 } 356 } 357 358 return state.mNewScale; 359 } 360 361 public float expectZoomOut(float currentScale) { 362 ScaleChangedState state = waitForNextScaleChange(); 363 assertEquals(currentScale, state.mOldScale); 364 assertThat(state.mNewScale, lessThan(currentScale)); 365 return state.mNewScale; 366 } 367 368 public float expectZoomIn(float currentScale) { 369 ScaleChangedState state = waitForNextScaleChange(); 370 assertEquals(currentScale, state.mOldScale); 371 assertThat(state.mNewScale, greaterThan(currentScale)); 372 return state.mNewScale; 373 } 374 375 public ScaleChangedState waitForNextScaleChange() { 376 return WebkitUtils.waitForNextQueueElement(mCallQueue); 377 } 378 379 public boolean onScaleChangedCalled() { 380 return mCallQueue.size() > 0; 381 } 382 383 public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { 384 // We know the CtsTestServer gave us fake credential, so we ignore the SSL error. 385 handler.proceed(); 386 } 387 } 388 } 389