/** * There is a bug in com.android.uiautomator.core.UiDevice#dumpWindowHierarchy(java.lang.String) * that sometimes manifest itself with an Exception. This method protects against it, making a * couple of attempts at getting the dump and if all of them fail, throwing an {@link * UiAutomatorDaemonException}. * * <p>Example stack trace of possible NPE:<br> * <code> * java.lang.NullPointerException: null<br/> * at com.android.uiautomator.core.AccessibilityNodeInfoDumper.childNafCheck(AccessibilityNodeInfoDumper.java:200)<br/> * at com.android.uiautomator.core.AccessibilityNodeInfoDumper.nafCheck(AccessibilityNodeInfoDumper.java:180)<br/> * at com.android.uiautomator.core.AccessibilityNodeInfoDumper.dumpNodeRec(AccessibilityNodeInfoDumper.java:104)<br/> * at com.android.uiautomator.core.AccessibilityNodeInfoDumper.dumpNodeRec(AccessibilityNodeInfoDumper.java:129)<br/> * (...)<br/> * at com.android.uiautomator.core.AccessibilityNodeInfoDumper.dumpWindowToFile(AccessibilityNodeInfoDumper.java:89)<br/> * at com.android.uiautomator.core.UiDevice.dumpWindowHierarchy(UiDevice.java:<obsolete line number here>) * </code> * * <p>Example stack trace of possible IllegalArgumentException: of an exception that occurred on * Snapchat 5.0.27.3 (July 3, 2014): * * <p><code> * java.lang.IllegalArgumentException: Illegal character (d83d)<br/> * at org.kxml2.io.KXmlSerializer.reportInvalidCharacter(KXmlSerializer.java:144) ~[na:na]<br/> * at org.kxml2.io.KXmlSerializer.writeEscaped(KXmlSerializer.java:130) ~[na:na]<br/> * at org.kxml2.io.KXmlSerializer.attribute(KXmlSerializer.java:465) ~[na:na]<br/> * at com.android.uiautomator.core.AccessibilityNodeInfoDumper.dumpNodeRec(AccessibilityNodeInfoDumper.java:111) ~[na:na]<br/> * (...)<br/> * at com.android.uiautomator.core.AccessibilityNodeInfoDumper.dumpWindowToFile(AccessibilityNodeInfoDumper.java:89) ~[na:na]<br/> * at com.android.uiautomator.core.UiDevice.dumpWindowHierarchy(UiDevice.java:768) ~[na:na]<br/> * at org.droidmate.uiautomatordaemon.UiAutomatorDaemonDriver.tryDumpWindowHierarchy(UiAutomatorDaemonDriver.java:420) ~[na:na]<br/> * (...)<br/> * </code> * * <p>Exploration log snippet of the IllegalArgumentException: * * <p><code> * 2015-02-20 19:44:21.113 DEBUG o.d.e.VerifiableDeviceActionsExecutor - Performing verifiable device action: <click on LC? 0 Wdgt:View/""/""/[570,233], no expectations><br/> * 2015-02-20 19:44:23.958 DEBUG o.d.exploration.ApiLogcatLogsReader - Current API logs read count: 0<br/> * 2015-02-20 19:44:24.040 ERROR o.d.e.ExplorationOutputCollector - Abrupt exploration end. Caught exception thrown during exploration of com.snapchat.android. Exception message: Device returned DeviceResponse with non-null throwable, indicating something exploded on the A(V)D. The exception is given as a cause of this one. If it doesn't have enough information, try inspecting the logcat output of the A(V)D. * </code> * * <p>Discussion: https://groups.google.com/forum/#!topic/appium-discuss/pkDcLx0LyWQ * * <p>Issue tracker: https://code.google.com/p/android/issues/detail?id=68419 */ private void dumpWindowHierarchyProtectingAgainstException(File windowDumpFile) throws UiAutomatorDaemonException { int dumpAttempts = 5; int dumpAttemptsLeft = dumpAttempts; boolean dumpSucceeded; do { dumpSucceeded = tryDumpWindowHierarchy(windowDumpFile); dumpAttemptsLeft--; if (!dumpSucceeded) { if (dumpAttemptsLeft == 1) { Log.w( uiaDaemon_logcatTag, "UiDevice.dumpWindowHierarchy() failed. Attempts left: 1. Pressing home screen button."); // Countermeasure for "Illegal character (d83d)". See the doc of this method and // https://hg.st.cs.uni-saarland.de/issues/981 this.device.pressHome(); } else { Log.w( uiaDaemon_logcatTag, "UiDevice.dumpWindowHierarchy() failed. Attempts left: " + dumpAttemptsLeft); } try { Thread.sleep(2000); } catch (InterruptedException e) { Log.e( uiaDaemon_logcatTag, "Sleeping between tryDumpWindowHierarchy attempts was interrupted!"); } } } while (!dumpSucceeded && dumpAttemptsLeft > 0); if (dumpAttemptsLeft <= 0) { Log.w( uiaDaemon_logcatTag, "UiDevice.dumpWindowHierarchy() failed. No attempts left. Throwing UiAutomatorDaemonException."); throw new UiAutomatorDaemonException( String.format( "All %d tryDumpWindowHierarchy(%s) attempts exhausted.", dumpAttempts, windowDumpFile)); } }
@TargetApi(Build.VERSION_CODES.FROYO) private DeviceResponse performAction(DeviceCommand deviceCommand) throws UiAutomatorDaemonException { Log.v(uiaDaemon_logcatTag, "Performing GUI action"); GuiAction action = deviceCommand.guiAction; // Log.e(uiaDaemon_logcatTag, "==========="); // Log.e(uiaDaemon_logcatTag, action.guiActionCommand); // Log.e(uiaDaemon_logcatTag, "==========="); if (action.guiActionCommand != null) { // Explanation for turning off the 'IfCanBeSwitch' inspection: // the ant script used for building this source uses Java 1.5 in which switch over strings is // not supported. //noinspection IfCanBeSwitch if (action.guiActionCommand.equals(guiActionCommand_pressBack)) { d(uiaDaemon_logcatTag, "Pressing 'back' button."); this.device.pressBack(); waitForGuiToStabilize(); } else if (action.guiActionCommand.equals(guiActionCommand_pressHome)) { d(uiaDaemon_logcatTag, "Pressing 'home' button."); this.device.pressHome(); waitForGuiToStabilize(); } else if (action.guiActionCommand.equals(guiActionCommand_turnWifiOn)) { turnWifiOn(); } else if (action.guiActionCommand.startsWith(guiActionCommand_loadXPrivacyConfig)) { loadXPrivacyConfig(action.guiActionCommand); } else if (action.guiActionCommand.equals(guiActionCommand_launchApp)) { launchApp(action.resourceId); } else { throw new UiAutomatorDaemonException( String.format("Unrecognized GUI action command: %s", action.guiActionCommand)); } } else if (deviceCommand.guiAction.resourceId != null) { d( uiaDaemon_logcatTag, String.format( "Setting text of widget with resource ID %s to %s.", deviceCommand.guiAction.resourceId, deviceCommand.guiAction.textToEnter)); try { boolean enterResult = this.device .findObject(new UiSelector().resourceId(deviceCommand.guiAction.resourceId)) .setText(deviceCommand.guiAction.textToEnter); if (enterResult) waitForGuiToStabilize(); if (!enterResult) Log.w( uiaDaemon_logcatTag, String.format( "Failed to enter text in widget with resource id: %s", deviceCommand.guiAction.resourceId)); } catch (UiObjectNotFoundException e) { throw new AssertionError( "Assertion error: UIObject not found. ResourceId: " + deviceCommand.guiAction.resourceId); } } else { int clickXCoor = deviceCommand.guiAction.clickXCoor; int clickYCoor = deviceCommand.guiAction.clickYCoor; d( uiaDaemon_logcatTag, String.format("Clicking on (x,y) coordinates of (%d,%d)", clickXCoor, clickYCoor)); if (clickXCoor < 0) throw new AssertionError("assert clickXCoor >= 0"); if (clickYCoor < 0) throw new AssertionError("assert clickYCoor >= 0"); if (clickXCoor > this.device.getDisplayWidth()) throw new AssertionError("assert clickXCoor <= device.getDisplayWidth()"); if (clickYCoor > this.device.getDisplayHeight()) throw new AssertionError("assert clickXCoor <= device.getDisplayHeight()"); // WISH return clickResult in deviceResponse, so we can try to click again on 'app has // stopped' and other dialog boxes. Right now there is just last chance attempt in // org.droidmate.exploration.VerifiableDeviceActionsExecutor.executeAndVerify() boolean clickResult; clickResult = click(deviceCommand, clickXCoor, clickYCoor); if (!clickResult) { d( uiaDaemon_logcatTag, (String.format( "The operation device.click(%d, %d) failed (the 'click' method returned 'false'). Retrying after 2 seconds.", clickXCoor, clickYCoor))); try { Thread.sleep(2000); } catch (InterruptedException e) { Log.w( uiaDaemon_logcatTag, "InterruptedException while sleeping before repeating a click."); } clickResult = click(deviceCommand, clickXCoor, clickYCoor); // WISH what does it actually mean that click failed? if (!clickResult) { Log.w( uiaDaemon_logcatTag, (String.format( "The operation ui.getUiDevice().click(%d, %d) failed for the second time. Giving up.", clickXCoor, clickYCoor))); } else d(uiaDaemon_logcatTag, "The click retry attempt succeeded."); } } DeviceResponse deviceResponse = new DeviceResponse(); deviceResponse.isNaturalOrientation = this.device.isNaturalOrientation(); return deviceResponse; }