/**
   * 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;
  }