1 /* 2 * Copyright (C) 2022 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.junit.Assert.*; 20 21 import android.app.Instrumentation; 22 import android.app.UiAutomation; 23 import android.content.Context; 24 import android.os.StrictMode; 25 import android.os.StrictMode.ThreadPolicy; 26 import android.view.MotionEvent; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.webkit.WebView; 30 import android.widget.FrameLayout; 31 32 import androidx.annotation.NonNull; 33 import androidx.annotation.Nullable; 34 import androidx.test.InstrumentationRegistry; 35 36 import org.apache.http.util.EncodingUtils; 37 38 import java.io.ByteArrayInputStream; 39 import java.security.cert.CertificateFactory; 40 import java.security.cert.X509Certificate; 41 import java.util.List; 42 43 import javax.net.ssl.X509TrustManager; 44 45 /** 46 * This class contains all the environmental variables that need to be configured for WebView tests 47 * to either run inside the SDK Runtime or within an Activity. 48 */ 49 public final class SharedWebViewTestEnvironment { 50 @Nullable private final Context mContext; 51 @Nullable private final WebView mWebView; 52 @Nullable private final FrameLayout mRootLayout; 53 private final IHostAppInvoker mHostAppInvoker; 54 SharedWebViewTestEnvironment( Context context, WebView webView, IHostAppInvoker hostAppInvoker, FrameLayout rootLayout)55 private SharedWebViewTestEnvironment( 56 Context context, 57 WebView webView, 58 IHostAppInvoker hostAppInvoker, 59 FrameLayout rootLayout) { 60 mContext = context; 61 mWebView = webView; 62 mHostAppInvoker = hostAppInvoker; 63 mRootLayout = rootLayout; 64 } 65 66 @Nullable getContext()67 public Context getContext() { 68 return mContext; 69 } 70 71 @Nullable getWebView()72 public WebView getWebView() { 73 return mWebView; 74 } 75 76 /** 77 * Some tests require adding a content view to the root view at runtime. This method mimics the 78 * behaviour of Activity.addContentView() 79 */ 80 @Nullable addContentView(View view, ViewGroup.LayoutParams params)81 public void addContentView(View view, ViewGroup.LayoutParams params) { 82 view.setLayoutParams(params); 83 mRootLayout.addView(view); 84 } 85 86 /** 87 * Apache Utils can't be statically linked so we can't use them directly inside the SDK Runtime. 88 * Use this method instead of EncodingUtils.getBytes. 89 */ getEncodingBytes(String data, String charset)90 public byte[] getEncodingBytes(String data, String charset) { 91 return ExceptionWrapper.unwrap(() -> { 92 return mHostAppInvoker.getEncodingBytes(data, charset); 93 }); 94 } 95 96 /** Invokes waitForIdleSync on the {@link Instrumentation} in the activity. */ waitForIdleSync()97 public void waitForIdleSync() { 98 ExceptionWrapper.unwrap(() -> { 99 mHostAppInvoker.waitForIdleSync(); 100 }); 101 } 102 103 /** Invokes sendKeyDownUpSync on the {@link Instrumentation} in the activity. */ sendKeyDownUpSync(int keyCode)104 public void sendKeyDownUpSync(int keyCode) { 105 ExceptionWrapper.unwrap(() -> { 106 mHostAppInvoker.sendKeyDownUpSync(keyCode); 107 }); 108 } 109 110 /** Invokes sendPointerSync on the {@link Instrumentation} in the activity. */ sendPointerSync(MotionEvent event)111 public void sendPointerSync(MotionEvent event) { 112 ExceptionWrapper.unwrap(() -> { 113 mHostAppInvoker.sendPointerSync(event); 114 }); 115 } 116 117 /** Returns a web server that can be used for web based testing. */ getWebServer()118 public SharedSdkWebServer getWebServer() { 119 return ExceptionWrapper.unwrap(() -> { 120 return new SharedSdkWebServer(mHostAppInvoker.getWebServer()); 121 }); 122 } 123 124 /** Returns a web server that has been started and can be used for web based testing. */ 125 public SharedSdkWebServer getSetupWebServer(@SslMode int sslMode) { 126 return getSetupWebServer(sslMode, null, 0, 0); 127 } 128 129 /** Returns a web server that has been started and can be used for web based testing. */ 130 public SharedSdkWebServer getSetupWebServer( 131 @SslMode int sslMode, @Nullable byte[] acceptedIssuerDer, int keyResId, int certResId) { 132 SharedSdkWebServer webServer = getWebServer(); 133 webServer.start(sslMode, acceptedIssuerDer, keyResId, certResId); 134 return webServer; 135 } 136 137 /** 138 * Use this builder to create a {@link SharedWebViewTestEnvironment}. The {@link 139 * SharedWebViewTestEnvironment} can not be built directly. 140 */ 141 public static final class Builder { 142 private Context mContext; 143 private WebView mWebView; 144 145 private FrameLayout mRootLayout; 146 private IHostAppInvoker mHostAppInvoker; 147 148 /** Provide a {@link Context} the tests should use for your environment. */ 149 public Builder setContext(@NonNull Context context) { 150 mContext = context; 151 return this; 152 } 153 154 /** Provide a {@link WebView} the tests should use for your environment. */ 155 public Builder setWebView(@NonNull WebView webView) { 156 mWebView = webView; 157 return this; 158 } 159 160 /** 161 * Provide a {@link IHostAppInvoker} the tests should use for your environment. 162 * 163 * <p>This can be created with {@link createHostAppInvoker}. 164 * 165 * <p>Note: This is required. 166 */ 167 public Builder setHostAppInvoker(@NonNull IHostAppInvoker hostAppInvoker) { 168 mHostAppInvoker = hostAppInvoker; 169 return this; 170 } 171 172 /** Provide a {@link FrameLayout} the tests should use for your environment. */ 173 public Builder setRootLayout(@NonNull FrameLayout rootLayout) { 174 mRootLayout = rootLayout; 175 return this; 176 } 177 178 /** Build a new SharedWebViewTestEnvironment. */ 179 public SharedWebViewTestEnvironment build() { 180 if (mHostAppInvoker == null) { 181 throw new NullPointerException("The host app invoker is required"); 182 } 183 return new SharedWebViewTestEnvironment( 184 mContext, mWebView, mHostAppInvoker, mRootLayout); 185 } 186 } 187 188 /** 189 * UiAutomation sends events at device level which lets us get around issues with sending 190 * instrumented events to the SDK Runtime but we don't want this for the regular tests. If 191 * something like a dialog pops up while an input event is being sent, the instrumentation would 192 * treat that as an issue while the UiAutomation input event would just send it through. 193 * 194 * <p>So by default, we disable this use and only use it in the SDK Sandbox. 195 * 196 * <p>This API is used for regular activity based tests. 197 */ 198 public static IHostAppInvoker.Stub createHostAppInvoker(Context applicationContext) { 199 return createHostAppInvoker(applicationContext, false); 200 } 201 /** 202 * This will generate a new {@link IHostAppInvoker} binder node. This should be called from 203 * wherever the activity exists for test cases. 204 */ 205 public static IHostAppInvoker.Stub createHostAppInvoker( 206 Context applicationContext, boolean allowUiAutomation) { 207 return new IHostAppInvoker.Stub() { 208 private Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation(); 209 private UiAutomation mUiAutomation; 210 211 public void waitForIdleSync() { 212 ExceptionWrapper.wrap(() -> { 213 mInstrumentation.waitForIdleSync(); 214 }); 215 } 216 217 public void sendKeyDownUpSync(int keyCode) { 218 ExceptionWrapper.wrap(() -> { 219 mInstrumentation.sendKeyDownUpSync(keyCode); 220 }); 221 } 222 223 public void sendPointerSync(MotionEvent event) { 224 ExceptionWrapper.wrap(() -> { 225 if (allowUiAutomation) { 226 sendPointerSyncWithUiAutomation(event); 227 } else { 228 sendPointerSyncWithInstrumentation(event); 229 } 230 }); 231 } 232 233 public byte[] getEncodingBytes(String data, String charset) { 234 return ExceptionWrapper.wrap(() -> { 235 return EncodingUtils.getBytes(data, charset); 236 }); 237 } 238 239 public IWebServer getWebServer() { 240 return new IWebServer.Stub() { 241 private CtsTestServer mWebServer; 242 243 public void start( 244 @SslMode int sslMode, 245 @Nullable byte[] acceptedIssuerDer, 246 int keyResId, 247 int certResId) { 248 ExceptionWrapper.wrap(() -> { 249 assertNull(mWebServer); 250 final X509Certificate[] acceptedIssuerCerts; 251 if (acceptedIssuerDer != null) { 252 CertificateFactory certFactory = 253 CertificateFactory.getInstance("X.509"); 254 acceptedIssuerCerts = new X509Certificate[] { 255 (X509Certificate) certFactory.generateCertificate( 256 new ByteArrayInputStream(acceptedIssuerDer)) 257 }; 258 } else { 259 acceptedIssuerCerts = null; 260 } 261 X509TrustManager trustManager = 262 new CtsTestServer.CtsTrustManager() { 263 @Override 264 public X509Certificate[] getAcceptedIssuers() { 265 return acceptedIssuerCerts; 266 } 267 }; 268 mWebServer = new CtsTestServer( 269 applicationContext, sslMode, trustManager, keyResId, certResId); 270 }); 271 } 272 273 public void shutdown() { 274 if (mWebServer == null) { 275 return; 276 } 277 ExceptionWrapper.wrap(() -> { 278 ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); 279 ThreadPolicy tmpPolicy = 280 new ThreadPolicy.Builder(oldPolicy) 281 .permitNetwork() 282 .build(); 283 StrictMode.setThreadPolicy(tmpPolicy); 284 mWebServer.shutdown(); 285 mWebServer = null; 286 StrictMode.setThreadPolicy(oldPolicy); 287 }); 288 } 289 290 public void resetRequestState() { 291 ExceptionWrapper.wrap(() -> { 292 assertNotNull("The WebServer needs to be started", mWebServer); 293 mWebServer.resetRequestState(); 294 }); 295 } 296 297 public String setResponse( 298 String path, String responseString, List<HttpHeader> responseHeaders) { 299 return ExceptionWrapper.wrap(() -> { 300 assertNotNull("The WebServer needs to be started", mWebServer); 301 return mWebServer.setResponse( 302 path, responseString, HttpHeader.asPairList(responseHeaders)); 303 }); 304 } 305 306 public String getAbsoluteUrl(String path) { 307 return ExceptionWrapper.wrap(() -> { 308 assertNotNull("The WebServer needs to be started", mWebServer); 309 return mWebServer.getAbsoluteUrl(path); 310 }); 311 } 312 313 public String getUserAgentUrl() { 314 return ExceptionWrapper.wrap(() -> { 315 assertNotNull("The WebServer needs to be started", mWebServer); 316 return mWebServer.getUserAgentUrl(); 317 }); 318 } 319 320 public String getDelayedAssetUrl(String path) { 321 return ExceptionWrapper.wrap(() -> { 322 assertNotNull("The WebServer needs to be started", mWebServer); 323 return mWebServer.getDelayedAssetUrl(path); 324 }); 325 } 326 327 public String getRedirectingAssetUrl(String path) { 328 return ExceptionWrapper.wrap(() -> { 329 assertNotNull("The WebServer needs to be started", mWebServer); 330 return mWebServer.getRedirectingAssetUrl(path); 331 }); 332 } 333 334 public String getAssetUrl(String path) { 335 return ExceptionWrapper.wrap(() -> { 336 assertNotNull("The WebServer needs to be started", mWebServer); 337 return mWebServer.getAssetUrl(path); 338 }); 339 } 340 341 public String getAuthAssetUrl(String path) { 342 return ExceptionWrapper.wrap(() -> { 343 assertNotNull("The WebServer needs to be started", mWebServer); 344 return mWebServer.getAuthAssetUrl(path); 345 }); 346 } 347 348 public String getBinaryUrl(String mimeType, int contentLength) { 349 return ExceptionWrapper.wrap(() -> { 350 assertNotNull("The WebServer needs to be started", mWebServer); 351 return mWebServer.getBinaryUrl(mimeType, contentLength); 352 }); 353 } 354 355 public String getAppCacheUrl() { 356 return ExceptionWrapper.wrap(() -> { 357 assertNotNull("The WebServer needs to be started", mWebServer); 358 return mWebServer.getAppCacheUrl(); 359 }); 360 } 361 362 public int getRequestCount() { 363 return ExceptionWrapper.wrap(() -> { 364 assertNotNull("The WebServer needs to be started", mWebServer); 365 return mWebServer.getRequestCount(); 366 }); 367 } 368 369 public int getRequestCountWithPath(String path) { 370 return ExceptionWrapper.wrap(() -> { 371 assertNotNull("The WebServer needs to be started", mWebServer); 372 return mWebServer.getRequestCount(path); 373 }); 374 } 375 376 public boolean wasResourceRequested(String url) { 377 return ExceptionWrapper.wrap(() -> { 378 assertNotNull("The WebServer needs to be started", mWebServer); 379 return mWebServer.wasResourceRequested(url); 380 }); 381 } 382 383 public HttpRequest getLastRequest(String path) { 384 return ExceptionWrapper.wrap(() -> { 385 assertNotNull("The WebServer needs to be started", mWebServer); 386 return toHttpRequest(path, mWebServer.getLastRequest(path)); 387 }); 388 } 389 390 public HttpRequest getLastAssetRequest(String url) { 391 return ExceptionWrapper.wrap(() -> { 392 assertNotNull("The WebServer needs to be started", mWebServer); 393 return toHttpRequest(url, mWebServer.getLastAssetRequest(url)); 394 }); 395 } 396 397 public String getCookieUrl(String path) { 398 return ExceptionWrapper.wrap(() -> { 399 assertNotNull("The WebServer needs to be started", mWebServer); 400 return mWebServer.getCookieUrl(path); 401 }); 402 } 403 404 public String getSetCookieUrl( 405 String path, String key, String value, String attributes) { 406 return ExceptionWrapper.wrap(() -> { 407 assertNotNull("The WebServer needs to be started", mWebServer); 408 return mWebServer.getSetCookieUrl(path, key, value, attributes); 409 }); 410 } 411 412 public String getLinkedScriptUrl(String path, String url) { 413 return ExceptionWrapper.wrap(() -> { 414 assertNotNull("The WebServer needs to be started", mWebServer); 415 return mWebServer.getLinkedScriptUrl(path, url); 416 }); 417 } 418 419 private HttpRequest toHttpRequest( 420 String url, org.apache.http.HttpRequest apacheRequest) { 421 if (apacheRequest == null) { 422 return null; 423 } 424 425 return new HttpRequest(url, apacheRequest); 426 } 427 }; 428 } 429 430 private void sendPointerSyncWithInstrumentation(MotionEvent event) { 431 mInstrumentation.sendPointerSync(event); 432 } 433 434 private void sendPointerSyncWithUiAutomation(MotionEvent event) { 435 if (mUiAutomation == null) { 436 mUiAutomation = mInstrumentation.getUiAutomation(); 437 438 if (mUiAutomation == null) { 439 fail("Could not retrieve UI automation"); 440 } 441 } 442 443 if (!mUiAutomation.injectInputEvent(event, true)) { 444 fail("Could not inject motion event"); 445 } 446 } 447 }; 448 } 449 } 450