LayoutTestsExecutor.java revision bcf114c2bbef4dd4af266a635a74076d568d125c
1/* 2 * Copyright (C) 2010 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 17package com.android.dumprendertree2; 18 19import android.app.Activity; 20import android.content.ComponentName; 21import android.content.Context; 22import android.content.Intent; 23import android.content.ServiceConnection; 24import android.net.Uri; 25import android.os.Bundle; 26import android.os.Environment; 27import android.os.Handler; 28import android.os.IBinder; 29import android.os.Message; 30import android.os.Messenger; 31import android.os.PowerManager; 32import android.os.Process; 33import android.os.RemoteException; 34import android.os.PowerManager.WakeLock; 35import android.util.Log; 36import android.view.Window; 37import android.webkit.ConsoleMessage; 38import android.webkit.JsPromptResult; 39import android.webkit.JsResult; 40import android.webkit.WebChromeClient; 41import android.webkit.WebSettings; 42import android.webkit.WebView; 43import android.webkit.WebViewClient; 44import android.webkit.GeolocationPermissions; 45import android.webkit.WebStorage.QuotaUpdater; 46 47import java.io.File; 48import java.lang.Thread.UncaughtExceptionHandler; 49import java.net.MalformedURLException; 50import java.net.URL; 51import java.util.HashMap; 52import java.util.Iterator; 53import java.util.List; 54import java.util.Map; 55 56/** 57 * This activity executes the test. It contains WebView and logic of LayoutTestController 58 * functions. It runs in a separate process and sends the results of running the test 59 * to ManagerService. The reason why is to handle crashing (test that crashes brings down 60 * whole process with it). 61 */ 62public class LayoutTestsExecutor extends Activity { 63 64 private enum CurrentState { 65 IDLE, 66 RENDERING_PAGE, 67 WAITING_FOR_ASYNCHRONOUS_TEST, 68 OBTAINING_RESULT; 69 70 public boolean isRunningState() { 71 return this == CurrentState.RENDERING_PAGE || 72 this == CurrentState.WAITING_FOR_ASYNCHRONOUS_TEST; 73 } 74 } 75 76 /** TODO: make it a setting */ 77 static final String TESTS_ROOT_DIR_PATH = 78 Environment.getExternalStorageDirectory() + 79 File.separator + "android" + 80 File.separator + "LayoutTests"; 81 82 private static final String LOG_TAG = "LayoutTestExecutor"; 83 84 public static final String EXTRA_TESTS_LIST = "TestsList"; 85 public static final String EXTRA_TEST_INDEX = "TestIndex"; 86 87 private static final int MSG_ACTUAL_RESULT_OBTAINED = 0; 88 private static final int MSG_TEST_TIMED_OUT = 1; 89 90 private static final int DEFAULT_TIME_OUT_MS = 15 * 1000; 91 92 /** A list of tests that remain to run since last crash */ 93 private List<String> mTestsList; 94 95 /** 96 * This is a number of currently running test. It is 0-based and doesn't reset after 97 * the crash. Initial index is passed to LayoutTestsExecuter in the intent that starts 98 * it. 99 */ 100 private int mCurrentTestIndex; 101 102 /** The total number of tests to run, doesn't reset after crash */ 103 private int mTotalTestCount; 104 105 private WebView mCurrentWebView; 106 private String mCurrentTestRelativePath; 107 private String mCurrentTestUri; 108 private CurrentState mCurrentState = CurrentState.IDLE; 109 110 private boolean mCurrentTestTimedOut; 111 private AbstractResult mCurrentResult; 112 private AdditionalTextOutput mCurrentAdditionalTextOutput; 113 114 private LayoutTestController mLayoutTestController = new LayoutTestController(this); 115 private boolean mCanOpenWindows; 116 private boolean mDumpDatabaseCallbacks; 117 private boolean mIsGeolocationPermissionSet; 118 private boolean mGeolocationPermission; 119 private Map<GeolocationPermissions.Callback, String> mPendingGeolocationPermissionCallbacks; 120 121 private EventSender mEventSender = new EventSender(); 122 123 private WakeLock mScreenDimLock; 124 125 /** COMMUNICATION WITH ManagerService */ 126 127 private Messenger mManagerServiceMessenger; 128 129 private ServiceConnection mServiceConnection = new ServiceConnection() { 130 131 @Override 132 public void onServiceConnected(ComponentName name, IBinder service) { 133 mManagerServiceMessenger = new Messenger(service); 134 startTests(); 135 } 136 137 @Override 138 public void onServiceDisconnected(ComponentName name) { 139 /** TODO */ 140 } 141 }; 142 143 private final Handler mResultHandler = new Handler() { 144 @Override 145 public void handleMessage(Message msg) { 146 switch (msg.what) { 147 case MSG_ACTUAL_RESULT_OBTAINED: 148 onActualResultsObtained(); 149 break; 150 151 case MSG_TEST_TIMED_OUT: 152 onTestTimedOut(); 153 break; 154 155 default: 156 break; 157 } 158 } 159 }; 160 161 /** WEBVIEW CONFIGURATION */ 162 163 private WebViewClient mWebViewClient = new WebViewClient() { 164 @Override 165 public void onPageFinished(WebView view, String url) { 166 /** Some tests fire up many page loads, we don't want to detect them */ 167 if (!url.equals(mCurrentTestUri)) { 168 return; 169 } 170 171 if (mCurrentState == CurrentState.RENDERING_PAGE) { 172 onTestFinished(); 173 } 174 } 175 }; 176 177 private WebChromeClient mWebChromeClient = new WebChromeClient() { 178 @Override 179 public void onExceededDatabaseQuota(String url, String databaseIdentifier, 180 long currentQuota, long estimatedSize, long totalUsedQuota, 181 QuotaUpdater quotaUpdater) { 182 /** TODO: This should be recorded as part of the text result */ 183 /** TODO: The quota should also probably be reset somehow for every test? */ 184 if (mDumpDatabaseCallbacks) { 185 getCurrentAdditionalTextOutput().appendExceededDbQuotaMessage(url, 186 databaseIdentifier); 187 } 188 quotaUpdater.updateQuota(currentQuota + 5 * 1024 * 1024); 189 } 190 191 @Override 192 public boolean onJsAlert(WebView view, String url, String message, JsResult result) { 193 getCurrentAdditionalTextOutput().appendJsAlert(message); 194 result.confirm(); 195 return true; 196 } 197 198 @Override 199 public boolean onJsConfirm(WebView view, String url, String message, JsResult result) { 200 getCurrentAdditionalTextOutput().appendJsConfirm(message); 201 result.confirm(); 202 return true; 203 } 204 205 @Override 206 public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, 207 JsPromptResult result) { 208 getCurrentAdditionalTextOutput().appendJsPrompt(message, defaultValue); 209 result.confirm(); 210 return true; 211 } 212 213 @Override 214 public boolean onConsoleMessage(ConsoleMessage consoleMessage) { 215 getCurrentAdditionalTextOutput().appendConsoleMessage(consoleMessage); 216 return true; 217 } 218 219 @Override 220 public boolean onCreateWindow(WebView view, boolean dialog, boolean userGesture, 221 Message resultMsg) { 222 WebView.WebViewTransport transport = (WebView.WebViewTransport)resultMsg.obj; 223 /** By default windows cannot be opened, so just send null back. */ 224 WebView newWindowWebView = null; 225 226 if (mCanOpenWindows) { 227 /** 228 * We never display the new window, just create the view and allow it's content to 229 * execute and be recorded by the executor. 230 */ 231 newWindowWebView = new WebView(LayoutTestsExecutor.this); 232 setupWebView(newWindowWebView); 233 } 234 235 transport.setWebView(newWindowWebView); 236 resultMsg.sendToTarget(); 237 return true; 238 } 239 240 @Override 241 public void onGeolocationPermissionsShowPrompt(String origin, 242 GeolocationPermissions.Callback callback) { 243 if (mIsGeolocationPermissionSet) { 244 callback.invoke(origin, mGeolocationPermission, false); 245 return; 246 } 247 if (mPendingGeolocationPermissionCallbacks == null) { 248 mPendingGeolocationPermissionCallbacks = 249 new HashMap<GeolocationPermissions.Callback, String>(); 250 } 251 mPendingGeolocationPermissionCallbacks.put(callback, origin); 252 } 253 }; 254 255 /** IMPLEMENTATION */ 256 257 @Override 258 protected void onCreate(Bundle savedInstanceState) { 259 super.onCreate(savedInstanceState); 260 261 /** 262 * It detects the crash by catching all the uncaught exceptions. However, we 263 * still have to kill the process, because after catching the exception the 264 * activity remains in a strange state, where intents don't revive it. 265 * However, we send the message to the service to speed up the rebooting 266 * (we don't have to wait for time-out to kick in). 267 */ 268 Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { 269 @Override 270 public void uncaughtException(Thread thread, Throwable e) { 271 Log.w(LOG_TAG, 272 "onTestCrashed(): " + mCurrentTestRelativePath + " thread=" + thread, e); 273 274 try { 275 Message serviceMsg = 276 Message.obtain(null, ManagerService.MSG_CURRENT_TEST_CRASHED); 277 278 mManagerServiceMessenger.send(serviceMsg); 279 } catch (RemoteException e2) { 280 Log.e(LOG_TAG, "mCurrentTestRelativePath=" + mCurrentTestRelativePath, e2); 281 } 282 283 Process.killProcess(Process.myPid()); 284 } 285 }); 286 287 requestWindowFeature(Window.FEATURE_PROGRESS); 288 289 Intent intent = getIntent(); 290 mTestsList = intent.getStringArrayListExtra(EXTRA_TESTS_LIST); 291 mCurrentTestIndex = intent.getIntExtra(EXTRA_TEST_INDEX, -1); 292 mTotalTestCount = mCurrentTestIndex + mTestsList.size(); 293 294 PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); 295 mScreenDimLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK 296 | PowerManager.ON_AFTER_RELEASE, "WakeLock in LayoutTester"); 297 mScreenDimLock.acquire(); 298 299 bindService(new Intent(this, ManagerService.class), mServiceConnection, 300 Context.BIND_AUTO_CREATE); 301 } 302 303 private void reset() { 304 WebView previousWebView = mCurrentWebView; 305 306 resetLayoutTestController(); 307 308 mCurrentTestTimedOut = false; 309 mCurrentResult = null; 310 mCurrentAdditionalTextOutput = null; 311 312 mCurrentWebView = new WebView(this); 313 setupWebView(mCurrentWebView); 314 315 mEventSender.reset(mCurrentWebView); 316 317 setContentView(mCurrentWebView); 318 if (previousWebView != null) { 319 Log.d(LOG_TAG + "::reset", "previousWebView != null"); 320 previousWebView.destroy(); 321 } 322 } 323 324 private void setupWebView(WebView webView) { 325 webView.setWebViewClient(mWebViewClient); 326 webView.setWebChromeClient(mWebChromeClient); 327 webView.addJavascriptInterface(mLayoutTestController, "layoutTestController"); 328 webView.addJavascriptInterface(mEventSender, "eventSender"); 329 330 /** 331 * Setting a touch interval of -1 effectively disables the optimisation in WebView 332 * that stops repeated touch events flooding WebCore. The Event Sender only sends a 333 * single event rather than a stream of events (like what would generally happen in 334 * a real use of touch events in a WebView) and so if the WebView drops the event, 335 * the test will fail as the test expects one callback for every touch it synthesizes. 336 */ 337 webView.setTouchInterval(-1); 338 339 webView.clearCache(true); 340 341 WebSettings webViewSettings = webView.getSettings(); 342 webViewSettings.setAppCacheEnabled(true); 343 webViewSettings.setAppCachePath(getApplicationContext().getCacheDir().getPath()); 344 webViewSettings.setAppCacheMaxSize(Long.MAX_VALUE); 345 webViewSettings.setJavaScriptEnabled(true); 346 webViewSettings.setJavaScriptCanOpenWindowsAutomatically(true); 347 webViewSettings.setSupportMultipleWindows(true); 348 webViewSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL); 349 webViewSettings.setDatabaseEnabled(true); 350 webViewSettings.setDatabasePath(getDir("databases", 0).getAbsolutePath()); 351 webViewSettings.setDomStorageEnabled(true); 352 webViewSettings.setWorkersEnabled(false); 353 webViewSettings.setXSSAuditorEnabled(false); 354 355 // This is asynchronous, but it gets processed by WebCore before it starts loading pages. 356 mCurrentWebView.useMockDeviceOrientation(); 357 } 358 359 private void startTests() { 360 try { 361 Message serviceMsg = 362 Message.obtain(null, ManagerService.MSG_FIRST_TEST); 363 364 Bundle bundle = new Bundle(); 365 if (!mTestsList.isEmpty()) { 366 bundle.putString("firstTest", mTestsList.get(0)); 367 bundle.putInt("index", mCurrentTestIndex); 368 } 369 370 serviceMsg.setData(bundle); 371 mManagerServiceMessenger.send(serviceMsg); 372 } catch (RemoteException e) { 373 Log.e(LOG_TAG, "mCurrentTestRelativePath=" + mCurrentTestRelativePath, e); 374 } 375 376 runNextTest(); 377 } 378 379 private void runNextTest() { 380 assert mCurrentState == CurrentState.IDLE : "mCurrentState = " + mCurrentState.name(); 381 382 if (mTestsList.isEmpty()) { 383 onAllTestsFinished(); 384 return; 385 } 386 387 mCurrentTestRelativePath = mTestsList.remove(0); 388 389 Log.i(LOG_TAG, "runNextTest(): Start: " + mCurrentTestRelativePath + 390 " (" + mCurrentTestIndex + ")"); 391 392 mCurrentTestUri = FileFilter.getUrl(mCurrentTestRelativePath).toString(); 393 394 reset(); 395 396 /** Start time-out countdown and the test */ 397 mCurrentState = CurrentState.RENDERING_PAGE; 398 mResultHandler.sendEmptyMessageDelayed(MSG_TEST_TIMED_OUT, DEFAULT_TIME_OUT_MS); 399 mCurrentWebView.loadUrl(mCurrentTestUri); 400 } 401 402 private void onTestTimedOut() { 403 assert mCurrentState.isRunningState() : "mCurrentState = " + mCurrentState.name(); 404 405 Log.w(LOG_TAG, "onTestTimedOut(): " + mCurrentTestRelativePath); 406 mCurrentTestTimedOut = true; 407 408 /** 409 * While it is theoretically possible that the test times out because 410 * of webview becoming unresponsive, it is very unlikely. Therefore it's 411 * assumed that obtaining results (that calls various webview methods) 412 * will not itself hang. 413 */ 414 obtainActualResultsFromWebView(); 415 } 416 417 private void onTestFinished() { 418 assert mCurrentState.isRunningState() : "mCurrentState = " + mCurrentState.name(); 419 420 Log.i(LOG_TAG, "onTestFinished(): " + mCurrentTestRelativePath); 421 obtainActualResultsFromWebView(); 422 } 423 424 private void obtainActualResultsFromWebView() { 425 /** 426 * If the result has not been set by the time the test finishes we create 427 * a default type of result. 428 */ 429 if (mCurrentResult == null) { 430 /** TODO: Default type should be RenderTreeResult. We don't support it now. */ 431 mCurrentResult = new TextResult(mCurrentTestRelativePath); 432 } 433 434 mCurrentState = CurrentState.OBTAINING_RESULT; 435 436 mCurrentResult.obtainActualResults(mCurrentWebView, 437 mResultHandler.obtainMessage(MSG_ACTUAL_RESULT_OBTAINED)); 438 } 439 440 private void onActualResultsObtained() { 441 assert mCurrentState == CurrentState.OBTAINING_RESULT 442 : "mCurrentState = " + mCurrentState.name(); 443 444 Log.i(LOG_TAG, "onActualResultsObtained(): " + mCurrentTestRelativePath); 445 mCurrentState = CurrentState.IDLE; 446 447 mResultHandler.removeMessages(MSG_TEST_TIMED_OUT); 448 reportResultToService(); 449 mCurrentTestIndex++; 450 updateProgressBar(); 451 runNextTest(); 452 } 453 454 private void reportResultToService() { 455 if (mCurrentAdditionalTextOutput != null) { 456 mCurrentResult.setAdditionalTextOutputString(mCurrentAdditionalTextOutput.toString()); 457 } 458 459 try { 460 Message serviceMsg = 461 Message.obtain(null, ManagerService.MSG_PROCESS_ACTUAL_RESULTS); 462 463 Bundle bundle = mCurrentResult.getBundle(); 464 bundle.putInt("testIndex", mCurrentTestIndex); 465 if (mCurrentTestTimedOut) { 466 bundle.putString("resultCode", AbstractResult.ResultCode.FAIL_TIMED_OUT.name()); 467 } 468 if (!mTestsList.isEmpty()) { 469 bundle.putString("nextTest", mTestsList.get(0)); 470 } 471 472 serviceMsg.setData(bundle); 473 mManagerServiceMessenger.send(serviceMsg); 474 } catch (RemoteException e) { 475 Log.e(LOG_TAG, "mCurrentTestRelativePath=" + mCurrentTestRelativePath, e); 476 } 477 } 478 479 private void updateProgressBar() { 480 getWindow().setFeatureInt(Window.FEATURE_PROGRESS, 481 mCurrentTestIndex * Window.PROGRESS_END / mTotalTestCount); 482 setTitle(mCurrentTestIndex * 100 / mTotalTestCount + "% " + 483 "(" + mCurrentTestIndex + "/" + mTotalTestCount + ")"); 484 } 485 486 private void onAllTestsFinished() { 487 mScreenDimLock.release(); 488 489 try { 490 Message serviceMsg = 491 Message.obtain(null, ManagerService.MSG_ALL_TESTS_FINISHED); 492 mManagerServiceMessenger.send(serviceMsg); 493 } catch (RemoteException e) { 494 Log.e(LOG_TAG, "mCurrentTestRelativePath=" + mCurrentTestRelativePath, e); 495 } 496 497 unbindService(mServiceConnection); 498 } 499 500 private AdditionalTextOutput getCurrentAdditionalTextOutput() { 501 if (mCurrentAdditionalTextOutput == null) { 502 mCurrentAdditionalTextOutput = new AdditionalTextOutput(); 503 } 504 return mCurrentAdditionalTextOutput; 505 } 506 507 /** LAYOUT TEST CONTROLLER */ 508 509 private static final int MSG_WAIT_UNTIL_DONE = 0; 510 private static final int MSG_NOTIFY_DONE = 1; 511 private static final int MSG_DUMP_AS_TEXT = 2; 512 private static final int MSG_DUMP_CHILD_FRAMES_AS_TEXT = 3; 513 private static final int MSG_SET_CAN_OPEN_WINDOWS = 4; 514 private static final int MSG_DUMP_DATABASE_CALLBACKS = 5; 515 private static final int MSG_SET_GEOLOCATION_PERMISSION = 6; 516 517 Handler mLayoutTestControllerHandler = new Handler() { 518 @Override 519 public void handleMessage(Message msg) { 520 assert mCurrentState.isRunningState() : "mCurrentState = " + mCurrentState.name(); 521 522 switch (msg.what) { 523 case MSG_WAIT_UNTIL_DONE: 524 mCurrentState = CurrentState.WAITING_FOR_ASYNCHRONOUS_TEST; 525 break; 526 527 case MSG_NOTIFY_DONE: 528 if (mCurrentState == CurrentState.WAITING_FOR_ASYNCHRONOUS_TEST) { 529 onTestFinished(); 530 } 531 break; 532 533 case MSG_DUMP_AS_TEXT: 534 if (mCurrentResult == null) { 535 mCurrentResult = new TextResult(mCurrentTestRelativePath); 536 } 537 538 assert mCurrentResult instanceof TextResult 539 : "mCurrentResult instanceof" + mCurrentResult.getClass().getName(); 540 541 break; 542 543 case MSG_DUMP_CHILD_FRAMES_AS_TEXT: 544 /** If dumpAsText was not called we assume that the result should be text */ 545 if (mCurrentResult == null) { 546 mCurrentResult = new TextResult(mCurrentTestRelativePath); 547 } 548 549 assert mCurrentResult instanceof TextResult 550 : "mCurrentResult instanceof" + mCurrentResult.getClass().getName(); 551 552 ((TextResult)mCurrentResult).setDumpChildFramesAsText(true); 553 break; 554 555 case MSG_SET_CAN_OPEN_WINDOWS: 556 mCanOpenWindows = true; 557 break; 558 559 case MSG_DUMP_DATABASE_CALLBACKS: 560 mDumpDatabaseCallbacks = true; 561 break; 562 563 case MSG_SET_GEOLOCATION_PERMISSION: 564 mIsGeolocationPermissionSet = true; 565 mGeolocationPermission = msg.arg1 == 1; 566 567 if (mPendingGeolocationPermissionCallbacks != null) { 568 Iterator<GeolocationPermissions.Callback> iter = 569 mPendingGeolocationPermissionCallbacks.keySet().iterator(); 570 while (iter.hasNext()) { 571 GeolocationPermissions.Callback callback = iter.next(); 572 String origin = mPendingGeolocationPermissionCallbacks.get(callback); 573 callback.invoke(origin, mGeolocationPermission, false); 574 } 575 mPendingGeolocationPermissionCallbacks = null; 576 } 577 break; 578 579 default: 580 assert false : "msg.what=" + msg.what; 581 break; 582 } 583 } 584 }; 585 586 private void resetLayoutTestController() { 587 mCanOpenWindows = false; 588 mDumpDatabaseCallbacks = false; 589 mIsGeolocationPermissionSet = false; 590 mPendingGeolocationPermissionCallbacks = null; 591 } 592 593 public void waitUntilDone() { 594 Log.i(LOG_TAG, mCurrentTestRelativePath + ": waitUntilDone() called"); 595 mLayoutTestControllerHandler.sendEmptyMessage(MSG_WAIT_UNTIL_DONE); 596 } 597 598 public void notifyDone() { 599 Log.i(LOG_TAG, mCurrentTestRelativePath + ": notifyDone() called"); 600 mLayoutTestControllerHandler.sendEmptyMessage(MSG_NOTIFY_DONE); 601 } 602 603 public void dumpAsText(boolean enablePixelTest) { 604 Log.i(LOG_TAG, mCurrentTestRelativePath + ": dumpAsText(" + enablePixelTest + ") called"); 605 /** TODO: Implement */ 606 if (enablePixelTest) { 607 Log.w(LOG_TAG, "enablePixelTest not implemented, switching to false"); 608 } 609 mLayoutTestControllerHandler.sendEmptyMessage(MSG_DUMP_AS_TEXT); 610 } 611 612 public void dumpChildFramesAsText() { 613 Log.i(LOG_TAG, mCurrentTestRelativePath + ": dumpChildFramesAsText() called"); 614 mLayoutTestControllerHandler.sendEmptyMessage(MSG_DUMP_CHILD_FRAMES_AS_TEXT); 615 } 616 617 public void setCanOpenWindows() { 618 Log.i(LOG_TAG, mCurrentTestRelativePath + ": setCanOpenWindows() called"); 619 mLayoutTestControllerHandler.sendEmptyMessage(MSG_SET_CAN_OPEN_WINDOWS); 620 } 621 622 public void dumpDatabaseCallbacks() { 623 Log.i(LOG_TAG, mCurrentTestRelativePath + ": dumpDatabaseCallbacks() called"); 624 mLayoutTestControllerHandler.sendEmptyMessage(MSG_DUMP_DATABASE_CALLBACKS); 625 } 626 627 public void setGeolocationPermission(boolean allow) { 628 Log.i(LOG_TAG, mCurrentTestRelativePath + ": setGeolocationPermission(" + allow + 629 ") called"); 630 Message msg = mLayoutTestControllerHandler.obtainMessage(MSG_SET_GEOLOCATION_PERMISSION); 631 msg.arg1 = allow ? 1 : 0; 632 msg.sendToTarget(); 633 } 634 635 public void setMockDeviceOrientation(boolean canProvideAlpha, double alpha, 636 boolean canProvideBeta, double beta, boolean canProvideGamma, double gamma) { 637 Log.i(LOG_TAG, mCurrentTestRelativePath + ": setMockDeviceOrientation(" + canProvideAlpha + 638 ", " + alpha + ", " + canProvideBeta + ", " + beta + ", " + canProvideGamma + 639 ", " + gamma + ")"); 640 mCurrentWebView.setMockDeviceOrientation(canProvideAlpha, alpha, canProvideBeta, beta, 641 canProvideGamma, gamma); 642 } 643} 644