LiHaoyu 5 lat temu
commit
7e4ab2c9c0
84 zmienionych plików z 6399 dodań i 0 usunięć
  1. 31 0
      .gitignore
  2. 7 0
      Commands/appium.sh
  3. 1 0
      Commands/batteryTemp.sh
  4. 1 0
      Commands/checkappium.sh
  5. 1 0
      Commands/checkdevice.sh
  6. 1 0
      Commands/compile.sh
  7. 2 0
      Commands/cpuinfo.sh
  8. 1 0
      Commands/dot.sh
  9. 1 0
      Commands/getPid.sh
  10. 1 0
      Commands/getUid.sh
  11. 3 0
      Commands/logcat.sh
  12. 1 0
      Commands/meminfo.sh
  13. 2 0
      Commands/network.sh
  14. 1 0
      Commands/newAppium.sh
  15. 14 0
      Commands/stopAppium.sh
  16. 18 0
      HELP.md
  17. 8 0
      configs/fatherComponent.conf
  18. 31 0
      configs/ignore.conf
  19. 88 0
      configs/login.txt
  20. 0 0
      mockData/reportData.json
  21. 286 0
      mvnw
  22. 161 0
      mvnw.cmd
  23. 114 0
      pom.xml
  24. 13 0
      src/main/java/net/mooctest/www/android_auto_test/AndroidAutoTestApplication.java
  25. 421 0
      src/main/java/net/mooctest/www/android_auto_test/Obversers/DeviceObserver.java
  26. 81 0
      src/main/java/net/mooctest/www/android_auto_test/Obversers/ScreenShotThread.java
  27. 339 0
      src/main/java/net/mooctest/www/android_auto_test/Obversers/monitor/CPUUtils.java
  28. 176 0
      src/main/java/net/mooctest/www/android_auto_test/Obversers/monitor/LogLine.java
  29. 247 0
      src/main/java/net/mooctest/www/android_auto_test/Obversers/monitor/LogMonitor.java
  30. 119 0
      src/main/java/net/mooctest/www/android_auto_test/Obversers/monitor/SMMonitor.java
  31. 28 0
      src/main/java/net/mooctest/www/android_auto_test/Scripts/AbstractBaseScript.java
  32. 906 0
      src/main/java/net/mooctest/www/android_auto_test/Scripts/DefaultScript.java
  33. 57 0
      src/main/java/net/mooctest/www/android_auto_test/Scripts/DemoScript.java
  34. 33 0
      src/main/java/net/mooctest/www/android_auto_test/common/BeanFactory.java
  35. 20 0
      src/main/java/net/mooctest/www/android_auto_test/common/DeviceStatusRunner.java
  36. 52 0
      src/main/java/net/mooctest/www/android_auto_test/common/FuckingTest.java
  37. 35 0
      src/main/java/net/mooctest/www/android_auto_test/common/constant/Consts.java
  38. 49 0
      src/main/java/net/mooctest/www/android_auto_test/common/constant/enums/DeviceRunningStatus.java
  39. 32 0
      src/main/java/net/mooctest/www/android_auto_test/common/constant/enums/DeviceStatus.java
  40. 53 0
      src/main/java/net/mooctest/www/android_auto_test/common/constant/enums/TraceStatus.java
  41. 10 0
      src/main/java/net/mooctest/www/android_auto_test/common/exceptions/ApkDownloadException.java
  42. 10 0
      src/main/java/net/mooctest/www/android_auto_test/common/exceptions/ApkParseException.java
  43. 10 0
      src/main/java/net/mooctest/www/android_auto_test/common/exceptions/AppiumDriverInitException.java
  44. 14 0
      src/main/java/net/mooctest/www/android_auto_test/common/exceptions/HttpNotFoundException.java
  45. 11 0
      src/main/java/net/mooctest/www/android_auto_test/common/exceptions/NoFreeDeviceException.java
  46. 10 0
      src/main/java/net/mooctest/www/android_auto_test/common/exceptions/TraceTimeoutException.java
  47. 33 0
      src/main/java/net/mooctest/www/android_auto_test/controller/AutoTestController.java
  48. 30 0
      src/main/java/net/mooctest/www/android_auto_test/dao/RedisMappers/DeviceRunningStatusMap.java
  49. 32 0
      src/main/java/net/mooctest/www/android_auto_test/dao/RedisMappers/Trace2DeviceMap.java
  50. 30 0
      src/main/java/net/mooctest/www/android_auto_test/dao/RedisMappers/TraceStatusMap.java
  51. 15 0
      src/main/java/net/mooctest/www/android_auto_test/models/Action.java
  52. 44 0
      src/main/java/net/mooctest/www/android_auto_test/models/Activity.java
  53. 45 0
      src/main/java/net/mooctest/www/android_auto_test/models/Component.java
  54. 16 0
      src/main/java/net/mooctest/www/android_auto_test/models/Device.java
  55. 53 0
      src/main/java/net/mooctest/www/android_auto_test/models/IgnoreComponent.java
  56. 32 0
      src/main/java/net/mooctest/www/android_auto_test/services/ApkService.java
  57. 30 0
      src/main/java/net/mooctest/www/android_auto_test/services/AutoTestService.java
  58. 28 0
      src/main/java/net/mooctest/www/android_auto_test/services/DeviceService.java
  59. 148 0
      src/main/java/net/mooctest/www/android_auto_test/services/Impl/ApkServiceImpl.java
  60. 148 0
      src/main/java/net/mooctest/www/android_auto_test/services/Impl/AutoTestServiceImpl.java
  61. 63 0
      src/main/java/net/mooctest/www/android_auto_test/services/Impl/DeviceServiceImpl.java
  62. 85 0
      src/main/java/net/mooctest/www/android_auto_test/services/Impl/OssServiceImpl.java
  63. 63 0
      src/main/java/net/mooctest/www/android_auto_test/services/Impl/TraceServiceImpl.java
  64. 17 0
      src/main/java/net/mooctest/www/android_auto_test/services/OssService.java
  65. 18 0
      src/main/java/net/mooctest/www/android_auto_test/services/TraceService.java
  66. 133 0
      src/main/java/net/mooctest/www/android_auto_test/utils/AddressUtil.java
  67. 172 0
      src/main/java/net/mooctest/www/android_auto_test/utils/AppiumManager.java
  68. 497 0
      src/main/java/net/mooctest/www/android_auto_test/utils/CoverageTest.java
  69. 22 0
      src/main/java/net/mooctest/www/android_auto_test/utils/DeviceDaemon.java
  70. 234 0
      src/main/java/net/mooctest/www/android_auto_test/utils/DeviceUtil.java
  71. 65 0
      src/main/java/net/mooctest/www/android_auto_test/utils/DoXml.java
  72. 61 0
      src/main/java/net/mooctest/www/android_auto_test/utils/DoubleUtils.java
  73. 9 0
      src/main/java/net/mooctest/www/android_auto_test/utils/DriverUtil.java
  74. 89 0
      src/main/java/net/mooctest/www/android_auto_test/utils/FileUtil.java
  75. 72 0
      src/main/java/net/mooctest/www/android_auto_test/utils/InputFinder.java
  76. 115 0
      src/main/java/net/mooctest/www/android_auto_test/utils/OsUtil.java
  77. 144 0
      src/main/java/net/mooctest/www/android_auto_test/utils/ParseXml.java
  78. 106 0
      src/main/java/net/mooctest/www/android_auto_test/utils/PrintUtil.java
  79. 172 0
      src/main/java/net/mooctest/www/android_auto_test/utils/TraceDaemon.java
  80. 10 0
      src/main/java/net/mooctest/www/android_auto_test/vo/DeviceStatusResult.java
  81. 13 0
      src/main/java/net/mooctest/www/android_auto_test/vo/TraceMetaInfo.java
  82. 16 0
      src/main/java/net/mooctest/www/android_auto_test/vo/TraceStatusResult.java
  83. 26 0
      src/main/resources/application.yaml
  84. 13 0
      src/test/java/net/mooctest/www/android_auto_test/AndroidAutoTestApplicationTests.java

+ 31 - 0
.gitignore

@@ -0,0 +1,31 @@
+.idea
+.mvn
+apks
+lib
+tasks
+target
+.DS_Store
+
+*.iml
+
+
+# Created by .ignore support plugin (hsz.mobi)
+### Java template
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# Package Files #
+*.jar
+#*.war
+#*.nar
+#*.ear
+#*.zip
+#*.tar.gz
+#*.rar
+
+!/tasks/Classifier.arff
+!/tasks/Label2ReasonAndSolution.json
+!/tasks/AutoTestReportGenerator.jar

+ 7 - 0
Commands/appium.sh

@@ -0,0 +1,7 @@
+
+
+perl /home/homer/programming-environment/node/bin/appium  -a 127.0.0.1 -p $1 -U $2 --log-timestamp --local-timezone --log-no-colors --log-level info:error --log $3  --session-override -bp $5 | tee $4 
+
+#perl /home/ise/programming-environmen/node/bin/appium  -a 127.0.0.1 -p $1 -U $2 --log-level info:error --log $3  --session-override -bp $5 | tee $4 
+# 
+

+ 1 - 0
Commands/batteryTemp.sh

@@ -0,0 +1 @@
+adb -s $1 shell dumpsys battery | grep temperature

+ 1 - 0
Commands/checkappium.sh

@@ -0,0 +1 @@
+netstat -apn | grep $1

+ 1 - 0
Commands/checkdevice.sh

@@ -0,0 +1 @@
+adb devices | grep $1

+ 1 - 0
Commands/compile.sh

@@ -0,0 +1 @@
+javac -classpath jar/apkUtil.jar:jar/java-client-1.2.1.jar:jar/selenium-server-standalone-2.44.0.jar  $1

+ 2 - 0
Commands/cpuinfo.sh

@@ -0,0 +1,2 @@
+adb -s $1 shell dumpsys cpuinfo | grep $2
+

+ 1 - 0
Commands/dot.sh

@@ -0,0 +1 @@
+dot -T png $1 -o $2

+ 1 - 0
Commands/getPid.sh

@@ -0,0 +1 @@
+adb -s $1 shell ps | grep $2

+ 1 - 0
Commands/getUid.sh

@@ -0,0 +1 @@
+adb -s $1 shell dumpsys package $2 | grep userId= | cut -d '=' -f 2

+ 3 - 0
Commands/logcat.sh

@@ -0,0 +1,3 @@
+adb -s $1 logcat -c # clear buffer
+## select the application pid and ActivityManager , ActivityThread
+adb -s $1 logcat -v long | grep -A 2 -e $2 -e ActivityManager -e ActivityThread > $3

+ 1 - 0
Commands/meminfo.sh

@@ -0,0 +1 @@
+adb -s $1 shell dumpsys meminfo | grep $2

+ 2 - 0
Commands/network.sh

@@ -0,0 +1,2 @@
+adb -s $1 shell cat /proc/net/xt_qtaguid/stats | grep $2
+

+ 1 - 0
Commands/newAppium.sh

@@ -0,0 +1 @@
+appium  -a 127.0.0.1 -p $1 -U $2 --log-timestamp --local-timezone --log-no-colors --log-level info:error --log $3  --session-override -bp $5 | tee $4

+ 14 - 0
Commands/stopAppium.sh

@@ -0,0 +1,14 @@
+result=$(echo `netstat -nlp 2>/dev/null | grep node | grep $1`)
+arr=(${result//,/ })  
+for i in ${arr[@]}  
+do  
+	if [[ $i == *node ]]
+	then
+		tmp=$i
+	fi
+done
+process_id=${tmp%/*}
+if [ -n $process_id ]
+then
+	kill $process_id 2>/dev/null	#不显示标准错误输出
+fi

+ 18 - 0
HELP.md

@@ -0,0 +1,18 @@
+# Getting Started
+
+### Reference Documentation
+For further reference, please consider the following sections:
+
+* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
+* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.2.0.RELEASE/maven-plugin/)
+* [Spring Configuration Processor](https://docs.spring.io/spring-boot/docs/2.2.0.RELEASE/reference/htmlsingle/#configuration-metadata-annotation-processor)
+* [Spring Web](https://docs.spring.io/spring-boot/docs/2.2.0.RELEASE/reference/htmlsingle/#boot-features-developing-web-applications)
+* [Spring Boot DevTools](https://docs.spring.io/spring-boot/docs/2.2.0.RELEASE/reference/htmlsingle/#using-boot-devtools)
+
+### Guides
+The following guides illustrate how to use some features concretely:
+
+* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/)
+* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/)
+* [Building REST services with Spring](https://spring.io/guides/tutorials/bookmarks/)
+

+ 8 - 0
configs/fatherComponent.conf

@@ -0,0 +1,8 @@
+//android.widget.ImageView[contains(@content-desc,'更多选项')]#.ui.activitys.MainActivity
+//android.widget.ImageButton[contains(@index,'0')]#.activity.MainActivity
+com.example.jingbin.cloudreader:id/ll_title_menu#.MainActivity
+//android.widget.ImageButton[contains(@content-desc,'Open navigation drawer')]#.modules.main.ui.MainActivity
+//android.widget.ImageButton[contains(@content-desc,'打开')]#.ui.main.activity.MainActivity
+//android.widget.ImageButton[contains(@index,'0')]#.activities.MainActivity
+//android.widget.ImageButton[contains(@content-desc,'打开导航菜单')]#.MainActivity
+com.hotbitmapgg.ohmybilibili:id/navigation_layout#com.hotbitmapgg.bilibili.module.common.MainActivity

+ 31 - 0
configs/ignore.conf

@@ -0,0 +1,31 @@
+*equals*
+id#com.xdf.ucan:id/title_right_image_btn
+id#com.jianshu.haruki:id/titlebar_navigation
+id#com.tencent.mm:id/gq
+id#com.tencent.mm:id/h6
+id#com.baidu.homework:id/sll_qq
+id#com.baidu.homework:id/sll_weChat
+id#com.baidu.homework:id/sll_phone
+text#返回
+content_desc#转到上一层级
+id#com.jianshu.haruki:id/title_wrapper
+id#com.jianshu.haruki:id/iv_qq
+id#com.jianshu.haruki:id/iv_close
+id#com.xdf.ucan:id/iv_change
+id#com.tencent.research.drop:id/btn_order
+id#com.tencent.research.drop:id/btn_menu
+id#com.myzaker.ZAKER_Phone:id/action_subscribe
+id#com.tuniu.app.ui:id/layout_dot_menu
+id#name.gudong.translate:id/sp_translate_way
+id#net.luoo.LuooFM:id/bt_top_bar_close
+*startsWith*
+//id#android:id
+
+*contains*
+id#back
+id#close
+id#return
+text#不同意
+text#更新
+text#下载
+text#安装

+ 88 - 0
configs/login.txt

@@ -0,0 +1,88 @@
+package nju.edu.cn;
+
+//import io.appium.java_client.AndroidKeyCode;
+import io.appium.java_client.AppiumDriver;
+import io.appium.java_client.android.AndroidDriver;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.OutputType;
+import org.openqa.selenium.TakesScreenshot;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.CapabilityType;
+import org.openqa.selenium.remote.DesiredCapabilities;
+import org.openqa.selenium.server.browserlaunchers.Sleeper;
+
+import com.sun.prism.Texture;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.sql.Time;
+import java.util.List;
+public class SecondTest {
+
+    private AndroidDriver driver;
+    @Before
+    public void setUp() throws Exception {
+        File classpathRoot = new File(System.getProperty("user.dir"));
+        File appDir = new File(classpathRoot, "apps");
+        File app = new File(appDir, "UpocStudent.apk");
+        DesiredCapabilities capabilities = new DesiredCapabilities();
+        capabilities.setCapability("browserName", "");
+        capabilities.setCapability("platformName", "Android");
+        capabilities.setCapability("deviceName", "Android Emulator");
+        capabilities.setCapability("newCommandTimeout", 10);
+        capabilities.setCapability("app", app.getAbsolutePath());
+        capabilities.setCapability("appPackage", "com.xdf.ucan");
+        capabilities.setCapability("appActivity", ".ui.login.StartActivity");
+
+        driver = new AndroidDriver(new URL("http://127.0.0.1:4723/wd/hub"), capabilities);
+    }
+
+    @Test
+    public void addContact(){
+    	//UpocStudent
+    	WebElement el1 = driver.findElementById("com.xdf.ucan:id/editText1");
+    	el1.sendKeys("15861819028");
+    	WebElement el2 = driver.findElementById("com.xdf.ucan:id/editText2");
+    	el2.sendKeys("00zoosong");
+    	//Jlb
+    	WebElement el3 = driver.findElementById("com.yao.club:id/et_username");
+    	el3.sendKeys("张三");
+    	WebElement el4 = driver.findElementById("com.yao.club:id/et_pwd");
+    	el4.sendKeys("123");
+    	//Jchat
+    	WebElement el5 = driver.findElementById("io.jchat.android:id/login_userName");
+    	el5.sendKeys("1234567");
+    	WebElement el6 = driver.findElementById("io.jchat.android:id/login_passWord");
+    	el6.sendKeys("1234567");
+    	//GuDong
+    	WebElement el7 = driver.findElementById("android:id/input");
+    	el7.sendKeys("test");
+        //Bilibili
+        WebElement el8 = driver.findElementById("com.hotbitmapgg.ohmybilibili:id/et_username");
+        el8.sendKeys("123");
+        WebElement el9 = driver.findElementById("com.hotbitmapgg.ohmybilibili:id/et_password");
+        el9.sendKeys("123");
+        //Jianshi
+        WebElement el10 = driver.findElementById("com.wingjay.android.jianshi:id/email");
+        el10.sendKeys("test@demo.com");
+        WebElement el11 = driver.findElementById("com.wingjay.android.jianshi:id/password");
+        el11.sendKeys("test123");
+        WebElement el12 = driver.findElementById("com.wingjay.android.jianshi:id/edit_title");
+        el12.sendKeys("demo");
+        WebElement el13 = driver.findElementById("com.wingjay.android.jianshi:id/edit_content");
+        el13.sendKeys("Hello World!");
+  }
+
+    @After
+    public void tearDown() throws Exception {
+        driver.quit();
+    }
+}
+
+

Plik diff jest za duży
+ 0 - 0
mockData/reportData.json


+ 286 - 0
mvnw

@@ -0,0 +1,286 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven2 Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+#   JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+#   M2_HOME - location of maven2's installed home dir
+#   MAVEN_OPTS - parameters passed to the Java VM when running Maven
+#     e.g. to debug Maven itself, use
+#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+  if [ -f /etc/mavenrc ] ; then
+    . /etc/mavenrc
+  fi
+
+  if [ -f "$HOME/.mavenrc" ] ; then
+    . "$HOME/.mavenrc"
+  fi
+
+fi
+
+# OS specific support.  $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+  CYGWIN*) cygwin=true ;;
+  MINGW*) mingw=true;;
+  Darwin*) darwin=true
+    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+    if [ -z "$JAVA_HOME" ]; then
+      if [ -x "/usr/libexec/java_home" ]; then
+        export JAVA_HOME="`/usr/libexec/java_home`"
+      else
+        export JAVA_HOME="/Library/Java/Home"
+      fi
+    fi
+    ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+  if [ -r /etc/gentoo-release ] ; then
+    JAVA_HOME=`java-config --jre-home`
+  fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+  ## resolve links - $0 may be a link to maven's home
+  PRG="$0"
+
+  # need this for relative symlinks
+  while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+      PRG="$link"
+    else
+      PRG="`dirname "$PRG"`/$link"
+    fi
+  done
+
+  saveddir=`pwd`
+
+  M2_HOME=`dirname "$PRG"`/..
+
+  # make it fully qualified
+  M2_HOME=`cd "$M2_HOME" && pwd`
+
+  cd "$saveddir"
+  # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --unix "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME="`(cd "$M2_HOME"; pwd)`"
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+  # TODO classpath?
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+  javaExecutable="`which javac`"
+  if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+    # readlink(1) is not available as standard on Solaris 10.
+    readLink=`which readlink`
+    if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+      if $darwin ; then
+        javaHome="`dirname \"$javaExecutable\"`"
+        javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+      else
+        javaExecutable="`readlink -f \"$javaExecutable\"`"
+      fi
+      javaHome="`dirname \"$javaExecutable\"`"
+      javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+      JAVA_HOME="$javaHome"
+      export JAVA_HOME
+    fi
+  fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+  if [ -n "$JAVA_HOME"  ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+      # IBM's JDK on AIX uses strange locations for the executables
+      JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+      JAVACMD="$JAVA_HOME/bin/java"
+    fi
+  else
+    JAVACMD="`which java`"
+  fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+  echo "Error: JAVA_HOME is not defined correctly." >&2
+  echo "  We cannot execute $JAVACMD" >&2
+  exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+  echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+  if [ -z "$1" ]
+  then
+    echo "Path not specified to find_maven_basedir"
+    return 1
+  fi
+
+  basedir="$1"
+  wdir="$1"
+  while [ "$wdir" != '/' ] ; do
+    if [ -d "$wdir"/.mvn ] ; then
+      basedir=$wdir
+      break
+    fi
+    # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+    if [ -d "${wdir}" ]; then
+      wdir=`cd "$wdir/.."; pwd`
+    fi
+    # end of workaround
+  done
+  echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+  if [ -f "$1" ]; then
+    echo "$(tr -s '\n' ' ' < "$1")"
+  fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+  exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Found .mvn/wrapper/maven-wrapper.jar"
+    fi
+else
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+    fi
+    jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
+    while IFS="=" read key value; do
+      case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+      esac
+    done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Downloading from: $jarUrl"
+    fi
+    wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+
+    if command -v wget > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found wget ... using wget"
+        fi
+        wget "$jarUrl" -O "$wrapperJarPath"
+    elif command -v curl > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found curl ... using curl"
+        fi
+        curl -o "$wrapperJarPath" "$jarUrl"
+    else
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Falling back to using Java to download"
+        fi
+        javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+        if [ -e "$javaClass" ]; then
+            if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Compiling MavenWrapperDownloader.java ..."
+                fi
+                # Compiling the Java class
+                ("$JAVA_HOME/bin/javac" "$javaClass")
+            fi
+            if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                # Running the downloader
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Running MavenWrapperDownloader.java ..."
+                fi
+                ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+            fi
+        fi
+    fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+  echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --path --windows "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+  [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+    MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+  $MAVEN_OPTS \
+  -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+  "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

+ 161 - 0
mvnw.cmd

@@ -0,0 +1,161 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements.  See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership.  The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License.  You may obtain a copy of the License at
+@REM
+@REM    https://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied.  See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven2 Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM     e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on"  echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
+FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO (
+	IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+    echo Found %WRAPPER_JAR%
+) else (
+    echo Couldn't find %WRAPPER_JAR%, downloading it ...
+	echo Downloading from: %DOWNLOAD_URL%
+    powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"
+    echo Finished downloading %WRAPPER_JAR%
+)
+@REM End of extension
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%

+ 114 - 0
pom.xml

@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.2.0.RELEASE</version>
+        <relativePath/> <!-- lookup parent from repository -->
+    </parent>
+    <groupId>net.mooctest.www</groupId>
+    <artifactId>android_auto_test</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <name>android_auto_test</name>
+    <description>Demo project for Spring Boot</description>
+
+    <properties>
+        <java.version>1.8</java.version>
+    </properties>
+
+    <dependencies>
+        <!-- https://mvnrepository.com/artifact/org.jdom/jdom2 -->
+        <dependency>
+            <groupId>org.jdom</groupId>
+            <artifactId>jdom2</artifactId>
+            <version>2.0.6</version>
+        </dependency>
+
+        <dependency>
+            <groupId>apkUtil</groupId>
+            <artifactId>apkUtil</artifactId>
+            <version>1.0.0</version>
+            <scope>system</scope>
+            <systemPath>${project.basedir}/lib/apkUtil.jar</systemPath>
+        </dependency>
+
+        <dependency>
+            <groupId>io.appium</groupId>
+            <artifactId>java-client</artifactId>
+            <version>6.1.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+            <scope>runtime</scope>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.junit.vintage</groupId>
+                    <artifactId>junit-vintage-engine</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.6</version>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.35</version>
+        </dependency>
+        <dependency>
+            <groupId>com.aliyun.oss</groupId>
+            <artifactId>aliyun-sdk-oss</artifactId>
+            <version>2.5.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi</artifactId>
+            <version>3.15</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <includeSystemScope>true</includeSystemScope>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 13 - 0
src/main/java/net/mooctest/www/android_auto_test/AndroidAutoTestApplication.java

@@ -0,0 +1,13 @@
+package net.mooctest.www.android_auto_test;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class AndroidAutoTestApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(AndroidAutoTestApplication.class, args);
+	}
+
+}

+ 421 - 0
src/main/java/net/mooctest/www/android_auto_test/Obversers/DeviceObserver.java

@@ -0,0 +1,421 @@
+package net.mooctest.www.android_auto_test.Obversers;
+
+import com.google.gson.Gson;
+import net.mooctest.www.android_auto_test.Obversers.monitor.SMMonitor;
+import net.mooctest.www.android_auto_test.utils.AddressUtil;
+import net.mooctest.www.android_auto_test.utils.DeviceUtil;
+import net.mooctest.www.android_auto_test.utils.OsUtil;
+import net.mooctest.www.android_auto_test.utils.PrintUtil;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import static java.util.regex.Pattern.*;
+
+/**
+ * @author henrylee
+ */
+public class DeviceObserver extends Thread {
+    public static final String TAG = Thread.currentThread() .getStackTrace()[1].getClassName();
+    private String cpuInfo = "0";
+    private String memInfo = "0";
+    private String networkInfo = "0,0";
+    private String powerInfo = "0";
+    private String batteryTempInfo = "0";
+    private String maxCpuInfo = "0";
+    private String maxMemInfo = "0";
+    private String maxNetWorkInfo = "0";
+    private String maxBatteryTempInfo = "0";
+    private String udid;
+    private String traceId;
+    private String packageName;
+    private int pid;
+    private volatile boolean isEnd = false;
+    private SMMonitor mSmMonitor;
+
+    private FileWriter cpuWriter;
+    private FileWriter memWriter;
+    private FileWriter networkWriter;
+    private FileWriter smWriter;
+    private FileWriter batteryTempWriter;
+
+    private List<Double> cpuList = new LinkedList<>();
+    private List<Integer> memList = new LinkedList<>();
+    private List<String> networkList = new LinkedList<>();
+    private List<Integer> smList = new LinkedList<>();
+    private List<Integer> batteryTempList = new LinkedList<>();
+
+    public DeviceObserver(String udid, String packageName,String traceId) {
+        super();
+        setDeviceUdid(udid);
+        setApkPackage(packageName);
+        this.traceId = traceId;
+    }
+
+    private void saveData() {
+        try {
+            File cpuLog = new File(AddressUtil.getCpuFilePath(udid, traceId));
+            File memLog = new File(AddressUtil.getMemFilePath(udid, traceId));
+            File networkLog = new File(AddressUtil.getNetworkFilePath(udid, traceId));
+            File smLog = new File(AddressUtil.getSMFilePath(udid, traceId));
+            File batteryTempLog = new File(AddressUtil.getBatteryFilePath(udid, traceId));
+            if (cpuLog.exists()) {
+                cpuLog.delete();
+            }
+            cpuLog.createNewFile();
+            if (memLog.exists()) {
+                memLog.delete();
+            }
+            memLog.createNewFile();
+            if (networkLog.exists()) {
+                networkLog.delete();
+            }
+            if (smLog.exists()) {
+                smLog.delete();
+            }
+            if (batteryTempLog.exists()) {
+                batteryTempLog.delete();
+            }
+            networkLog.createNewFile();
+            cpuLog.createNewFile();
+            memLog.createNewFile();
+            smLog.createNewFile();
+            batteryTempLog.createNewFile();
+
+            Gson gson = new Gson();
+            String cpuData = gson.toJson(cpuList);
+            String memData = gson.toJson(memList);
+            String networkData = gson.toJson(networkList);
+            String smData = gson.toJson(smList);
+            String batteryData = gson.toJson(batteryTempList);
+
+            cpuWriter = new FileWriter(cpuLog);
+            memWriter = new FileWriter(memLog);
+            networkWriter = new FileWriter(networkLog);
+            smWriter = new FileWriter(smLog);
+            batteryTempWriter = new FileWriter(batteryTempLog);
+
+            cpuWriter.write(cpuData);
+            memWriter.write(memData);
+            networkWriter.write(networkData);
+            smWriter.write(smData);
+            batteryTempWriter.write(batteryData);
+
+            cpuWriter.flush();
+            memWriter.flush();
+            networkWriter.flush();
+            smWriter.flush();
+            batteryTempWriter.flush();
+
+            cpuWriter.close();
+            memWriter.close();
+            networkWriter.close();
+            smWriter.close();
+            batteryTempWriter.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            //关闭
+        }
+    }
+
+    public void setDeviceUdid(String udid) {
+        this.udid = udid;
+    }
+
+    public void setApkPackage(String packageName) {
+        this.packageName = packageName;
+    }
+
+    @Override
+    public void run() {
+        PrintUtil.print("run", TAG, udid);
+        mSmMonitor = new SMMonitor();
+        pid = getPid(udid, packageName);
+        if (pid != -1) {
+            mSmMonitor.startSMMonitor(udid, pid);
+        }
+        while (!isEnd) {
+            PrintUtil.print("run while", TAG, udid);
+            try {
+                getCpuInfo(udid, packageName);
+                getMemInfo(udid, packageName);
+                getNetworkInfo(udid, packageName);
+                getSmInfo();
+                getBatteryTempInfo(udid);
+                saveData();
+                Thread.sleep(5000);
+            } catch (InterruptedException e) {
+                PrintUtil.printException(TAG, udid, e);
+            }
+        }
+    }
+
+    public void setStopFlag() {
+        isEnd = true;
+        if (mSmMonitor != null) {
+            mSmMonitor.stop();
+        }
+        saveData();
+//        if (mLogMonitor != null) {
+//            mLogMonitor.stop();
+//        }
+//        if (logThread != null) {
+//            logThread.stop();
+//        }
+    }
+
+
+    private int getPid(String udid, String apkPackage) {
+        return DeviceUtil.getAppPidOnDevice(udid, apkPackage);
+    }
+
+    private void getSmInfo() {
+        int skippedFrameCount = mSmMonitor.getSkippedFrameCount();
+        if (skippedFrameCount == 0) {
+            smList.add(60);
+        } else {
+            int sm = 60 - skippedFrameCount / 5;
+            smList.add(sm);
+        }
+    }
+
+    private void getCpuInfo(String udid, String apkPackage) {
+//        PrintUtil.print("getCpuInfo", TAG, udid);
+        //数据出现小数时先用int截断
+        double tempCpuInfo = getCpuRate(String.valueOf(pid), udid);
+        if(tempCpuInfo < 0 || Double.isNaN(tempCpuInfo)) {
+            PrintUtil.print("CpuInfo is invalid , use previous data " + cpuInfo + "%", TAG, udid);
+        } else {
+            //四舍五入两位小数
+            cpuInfo = String.format("%.2f",tempCpuInfo);
+//            PrintUtil.print(cpuInfo + "%", TAG, udid);
+            if(tempCpuInfo > Double.parseDouble(maxCpuInfo)) {
+                maxCpuInfo = String.format("%.2f",tempCpuInfo);
+            }
+        }
+        cpuList.add(Double.parseDouble(cpuInfo));
+    }
+
+    private void getMemInfo(String udid, String apkPackage) {
+//        PrintUtil.print("getMemInfo", TAG, udid);
+        String command;
+        command = OsUtil.getCmd() + " Commands/meminfo.sh " + udid + " " + apkPackage;
+        String result = OsUtil.runCommand(command);
+
+        String[] units = {"kB", "K"};
+        boolean ok = false;
+        for (String unit: units){
+            if (result != null && result.contains(unit)) {
+                //只看第一行PSS值
+                result = result.split("\n")[0];
+                //去除字符串中的空格、回车、换行符、制表符
+                result = result.replaceAll("\\s*","");
+                if(result.indexOf(unit) < result.indexOf("(")) {
+                    result = result.substring(0,result.indexOf(unit));
+                } else {
+                    //处理不同的结果格式
+                    result = result.substring(0,result.indexOf("("));
+                }
+                result = result.replaceAll(",", "");
+                memInfo = result;
+                ok = true;
+//                PrintUtil.print(Integer.parseInt(memInfo) + unit, TAG, udid);
+                break;
+            }
+        }
+        if (ok) {
+            if(Integer.parseInt(memInfo) > Integer.parseInt(maxMemInfo)) {
+                maxMemInfo = memInfo;
+            }
+        } else {
+            PrintUtil.print("MemInfo is invalid , use previous data " + memInfo + "kB", TAG, udid);
+        }
+        memList.add(Integer.parseInt(memInfo));
+    }
+
+    private void getNetworkInfo(String udid, String apkPackage) {
+//        PrintUtil.print("getNetWorkInfo", TAG, udid);
+        String command;
+        command = OsUtil.getCmd() + " Commands/getUid.sh " + udid + " " + apkPackage;
+
+        String uid = OsUtil.runCommand(command).replaceAll("\\s*","");	//获得应用Uid
+        PrintUtil.print("Uid is: " + uid, TAG, udid);
+        int index = 0;
+        for(;index < uid.length();index++) {
+            if(uid.charAt(index) >= '0' && uid.charAt(index) <= '9') {
+                continue;	//过滤uid=10198 gids=xxxx这一类结果
+            } else {
+                break;
+            }
+        }
+        uid = uid.substring(0,index);
+        String snd = OsUtil.runCommand("adb -s " + udid + " shell cat /proc/uid_stat/" + uid + "/tcp_snd").replaceAll("\\s*","");
+        String rcv = OsUtil.runCommand("adb -s " + udid + " shell cat /proc/uid_stat/" + uid + "/tcp_rcv").replaceAll("\\s*","");
+        boolean dataErrorFlag = false;
+        try {
+            Integer.parseInt(snd);
+            Integer.parseInt(rcv);
+        } catch (Exception e) {
+            dataErrorFlag = true;
+        }
+        if(!dataErrorFlag) {
+            networkInfo = snd + "," + rcv;
+//            PrintUtil.print("snd: " + snd + ",rcv: " + rcv + " bytes", TAG, udid);
+        }else {
+            PrintUtil.print("NetWorkInfo is invalid , use previous data " + networkInfo + " bytes", TAG, udid);
+        }
+        networkList.add(networkInfo);	//if get data error, will write last time data(networkInfo)
+    }
+
+    private void getBatteryTempInfo(String udid){
+        String command;
+        command = OsUtil.getCmd() + " Commands/batteryTemp.sh " + udid;
+        String result = OsUtil.runCommand(command);
+        //去除字符串中的空格、回车、换行符、制表符
+        if(result != null) {
+            result = result.replaceAll("\\s*","");
+        }
+        result = result.substring(result.indexOf(":")+1);
+        batteryTempInfo = result;
+        try {
+            if(Integer.parseInt(batteryTempInfo) > Integer.parseInt(maxBatteryTempInfo)) {
+                maxBatteryTempInfo = batteryTempInfo;
+            }
+            batteryTempList.add(Integer.parseInt(batteryTempInfo));
+        } catch (NumberFormatException e){
+            e.printStackTrace();
+        }
+
+    }
+
+    public static double getCpuRate(String pid, String udid){
+        double result = -1;
+        try {
+            int processCpuTime1 = processCpuTime(pid, udid);
+            try {
+                TimeUnit.SECONDS.sleep(1);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            int processCpuTime2 = processCpuTime(pid, udid);
+            int processCpuTime3 = processCpuTime2 - processCpuTime1;
+            int totalCpuTime1 = totalCpuTime(udid);
+            try {
+                TimeUnit.SECONDS.sleep(1);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            int totalCpuTime2 = totalCpuTime(udid);
+            int totalCpuTime3 = (totalCpuTime2 - totalCpuTime1) * getCpuKernel(udid);
+            result = 100 * ((double)processCpuTime3) / (double)totalCpuTime3;
+        } catch (Exception e) {
+            PrintUtil.printErr("getCpuRateError: " + getStackTrace(e), TAG, udid);
+        }
+        return result;
+    }
+    public static int getCpuKernel(String udid) {
+        int kelNum = 0;
+        String cmd = "adb -s " + udid +" shell ls /sys/devices/system/cpu/ ";
+        String msg = OsUtil.runCommand(cmd);
+        String[] filename = msg.split("\n");
+        for(String file : filename){
+            if(file.startsWith("cpu")){
+                boolean flag = true;
+                for(int i = 3 ; i < file.length() ; i++){
+                    if(file.charAt(i) < '0' || file.charAt(i) > '9'){
+                        flag = false;
+                        break;
+                    }
+                }
+                if(flag) {
+                    kelNum++;
+                }
+            }
+        }
+        return kelNum == 0 ? 2 : kelNum;
+    }
+    public static int totalCpuTime(String udid) {
+        int result = 0;
+        int user, nice, system, idle, iowait, irq, softirq = 0;
+        String cmd = "adb -s " + udid +" shell cat /proc/stat";
+        String msg = OsUtil.runCommand(cmd);
+        String[] tmp = msg.split(" +");
+        for(String i : tmp){
+            if(i.equals("cpu")){
+                user = Integer.parseInt(tmp[1]);
+                nice = Integer.parseInt(tmp[2]);
+                system = Integer.parseInt(tmp[3]);
+                idle = Integer.parseInt(tmp[4]);
+                iowait = Integer.parseInt(tmp[5]);
+                irq = Integer.parseInt(tmp[6]);
+                softirq = Integer.parseInt(tmp[7]);
+                result = user + nice + system + idle + iowait + irq + softirq;
+                return result;
+            }
+        }
+        return result;
+    }
+    public static int processCpuTime(String pid, String udid) {
+        int result = 0;
+        int utime, stime, cutime, cstime = 0;
+        String cmd = "adb -s "+ udid + " shell cat /proc/" + pid +"/stat";
+        String msg = OsUtil.runCommand(cmd);
+        try{
+            String[] res = msg.split(" +");
+            utime = Integer.parseInt(res[13]);
+            stime = Integer.parseInt(res[14]);
+            cutime = Integer.parseInt(res[15]);
+            cstime = Integer.parseInt(res[16]);
+            result = utime + stime + cutime + cstime;
+        }catch (Exception e){
+            PrintUtil.printException(TAG, udid, e);
+        }
+        return result;
+    }
+    private static String getStackTrace(Throwable t){
+        StringWriter sw = new StringWriter();
+        PrintWriter pw = new PrintWriter(sw);
+        try{
+            t.printStackTrace(pw);
+            return sw.toString();
+        }
+        finally{
+            pw.close();
+        }
+    }
+    public int getPid(){
+        return pid;
+    }
+    public String getMaxCpuInfo() {
+        return maxCpuInfo;
+    }
+
+    public String getMaxMemInfo() {
+        return maxMemInfo;
+    }
+
+    public String getMaxNetworkInfo() {
+        return maxNetWorkInfo;
+    }
+
+    public String getMaxBatteryTempInfo() {
+        return maxBatteryTempInfo;
+    }
+
+    public boolean isNumeric(String str){
+        Pattern pattern = compile("[0-9]*");
+        Matcher isNum = pattern.matcher(str);
+        if( !isNum.matches() ){
+            return false;
+        }
+        return true;
+    }
+}
+

+ 81 - 0
src/main/java/net/mooctest/www/android_auto_test/Obversers/ScreenShotThread.java

@@ -0,0 +1,81 @@
+package net.mooctest.www.android_auto_test.Obversers;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import net.mooctest.www.android_auto_test.utils.DeviceUtil;
+import org.apache.commons.io.FileUtils;
+import net.mooctest.www.android_auto_test.utils.AddressUtil;
+import net.mooctest.www.android_auto_test.utils.PrintUtil;
+import org.openqa.selenium.OutputType;
+import org.openqa.selenium.TakesScreenshot;
+import io.appium.java_client.AppiumDriver;
+import org.openqa.selenium.WebDriverException;
+import org.openqa.selenium.NoSuchSessionException;
+
+
+/**
+ * @author henrylee
+ */
+public class ScreenShotThread extends Thread {
+    public static final String TAG = Thread.currentThread() .getStackTrace()[1].getClassName();
+    private AppiumDriver driver;
+    private String traceId;
+    private String udid;
+    private volatile boolean isEnd = false;
+    public ScreenShotThread(AppiumDriver driver, String udid, String traceId){
+        this.driver = driver;
+        this.udid = udid;
+        this.traceId = traceId;
+    }
+    public void setStopFlag() {
+        isEnd = true;
+    }
+
+    @Override
+    public void run() {
+        try {
+            takeScreenShot(driver, udid);
+        } catch (NoSuchSessionException e) {
+            PrintUtil.printErr("ScreenShotRunner takeScreenShot session not found " + udid, TAG);
+        } catch (WebDriverException e) {
+            PrintUtil.printErr("ScreenShotRunner webdriver exception " + udid + " " + e.getMessage(), TAG);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+    private void takeScreenShot(AppiumDriver driver,String udid) throws NoSuchSessionException{
+        while(!isEnd){
+            File screenShotFile = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE);
+            try {
+                String path = AddressUtil.getScreenShotFilePath(traceId, udid, DeviceUtil.getDeviceTimeStamp(udid));
+                FileUtils.copyFile(screenShotFile, new File(path));
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+            FileReader fr;
+            try{
+                fr = new FileReader(screenShotFile);
+                if(fr.read() == -1){
+                    screenShotFile = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE);
+                    try {
+                        String path = AddressUtil.getScreenShotFilePath(traceId, udid, DeviceUtil.getDeviceTimeStamp(udid));
+                        FileUtils.copyFile(screenShotFile, new File(path));
+                    } catch (IOException e) {
+                        e.printStackTrace();
+                    }
+                    if(fr.read() == -1){
+                        screenShotFile.delete();
+                    }
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            try {
+                Thread.sleep(2000);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+}

+ 339 - 0
src/main/java/net/mooctest/www/android_auto_test/Obversers/monitor/CPUUtils.java

@@ -0,0 +1,339 @@
+package net.mooctest.www.android_auto_test.Obversers.monitor;
+
+import net.mooctest.www.android_auto_test.utils.DoubleUtils;
+import net.mooctest.www.android_auto_test.utils.FileUtil;
+
+import java.io.*;
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by homer on 17-2-11.
+ */
+public class CPUUtils {
+    public static Map<String, CPUUtils> cpuInfoMap = new HashMap<String, CPUUtils>();
+    private static boolean initCpu = true;
+    private static double o_cpu = 0.0;
+    private static double o_idle = 0.0;
+
+    private double p_jif = 0.0;
+    private double pCpu = 0.0;
+    private double aCpu = 0.0;
+    private double o_pCpu = 0.0;
+    private double o_aCpu = 0.0;
+
+    /**
+     * 获取进程的CPU使用时间片
+     *
+     * @return 进程的CPU使用时间片
+     */
+    public long getJif() {
+        return (long)p_jif;
+    }
+
+    /**
+     * 获取CPU使用率
+     *
+     * @return CPU使用率
+     */
+    public static double getCpuUsage() {
+        double usage = 0.0;
+        if (initCpu) {
+            initCpu = false;
+            RandomAccessFile reader = null;
+            try {
+                reader = new RandomAccessFile("/proc/stat",
+                        "r");
+                String load;
+                load = reader.readLine();
+                String[] toks = load.split(" ");
+                o_idle = Double.parseDouble(toks[5]);
+                o_cpu = Double.parseDouble(toks[2])
+                        + Double.parseDouble(toks[3])
+                        + Double.parseDouble(toks[4])
+                        + Double.parseDouble(toks[6])
+                        + Double.parseDouble(toks[8])
+                        + Double.parseDouble(toks[7]);
+
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+            finally
+            {
+                FileUtil.closeRandomAccessFile(reader);
+            }
+        } else {
+            RandomAccessFile reader = null;
+            try {
+                reader = new RandomAccessFile("/proc/stat", "r");
+                String load;
+                load = reader.readLine();
+                String[] toks = load.split(" ");
+                double c_idle = Double.parseDouble(toks[5]);
+                double c_cpu = Double.parseDouble(toks[2])
+                        + Double.parseDouble(toks[3])
+                        + Double.parseDouble(toks[4])
+                        + Double.parseDouble(toks[6])
+                        + Double.parseDouble(toks[8])
+                        + Double.parseDouble(toks[7]);
+                if (0 != ((c_cpu + c_idle) - (o_cpu + o_idle))) {
+                    // double value = (100.00 * ((c_cpu - o_cpu) ) / ((c_cpu +
+                    // c_idle) - (o_cpu + o_idle)));
+                    usage = DoubleUtils.div((100.00 * ((c_cpu - o_cpu))),
+                            ((c_cpu + c_idle) - (o_cpu + o_idle)), 2);
+                    // Log.d("CPU", "usage: " + usage);
+                    if (usage < 0) {
+                        usage = 0;
+                    }
+                    else if (usage > 100)
+                    {
+                        usage = 100;
+                    }
+                    // BigDecimal b = new BigDecimal(Double.toString(value));
+
+                    // usage = b.setScale(2,
+                    // BigDecimal.ROUND_HALF_UP).doubleValue();
+                    // Log.d("CPU", "usage: " + usage);
+                }
+                o_cpu = c_cpu;
+                o_idle = c_idle;
+            } catch (IOException e) {
+                e.printStackTrace();
+            } finally {
+                FileUtil.closeRandomAccessFile(reader);
+            }
+        }
+        return usage;
+    }
+
+    public static double getCpuUsage0() {
+        try {
+            RandomAccessFile reader = new RandomAccessFile("/proc/stat", "r");
+            String load = reader.readLine();
+            String[] toks = load.split(" ");
+            double idle1 = Double.parseDouble(toks[5]);
+            double cpu1 = Double.parseDouble(toks[2])
+                    + Double.parseDouble(toks[3]) + Double.parseDouble(toks[4])
+                    + Double.parseDouble(toks[6]) + Double.parseDouble(toks[8])
+                    + Double.parseDouble(toks[7]);
+            // 2:user 3:nice 4:system 6:iowait 7:irq 8:softirq
+            try {
+                Thread.sleep(360);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            reader.seek(0);
+            load = reader.readLine();
+            reader.close();
+            toks = load.split(" ");
+            double idle2 = Double.parseDouble(toks[5]);
+            double cpu2 = Double.parseDouble(toks[2])
+                    + Double.parseDouble(toks[3]) + Double.parseDouble(toks[4])
+                    + Double.parseDouble(toks[6]) + Double.parseDouble(toks[8])
+                    + Double.parseDouble(toks[7]);
+            double value = DoubleUtils.div((100.00 * ((cpu2 - cpu1))),
+                    (cpu2 + idle2) - (cpu1 + idle1), 2);
+            // BigDecimal b = new BigDecimal(Double.toString(value));
+
+            // double res = b.setScale(2,
+            // BigDecimal.ROUND_HALF_UP).doubleValue();
+
+            return value;
+        } catch (IOException ex) {
+            ex.printStackTrace();
+        }
+        return 0;
+    }
+
+    public static double getCpuUsage1() {
+        try {
+            RandomAccessFile reader = new RandomAccessFile("/proc/stat", "r");
+            String load = reader.readLine();
+            String[] toks = load.split(" ");
+
+            double user1 = Double.parseDouble(toks[2]);
+            double system1 = Double.parseDouble(toks[4]);
+            double irq1 = Double.parseDouble(toks[7]);
+            double idle1 = Double.parseDouble(toks[5]);
+            try {
+                Thread.sleep(360);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            reader.seek(0);
+            load = reader.readLine();
+            reader.close();
+            toks = load.split(" ");
+            double user2 = Double.parseDouble(toks[2]);
+            double system2 = Double.parseDouble(toks[4]);
+            double irq2 = Double.parseDouble(toks[7]);
+            double idle2 = Double.parseDouble(toks[5]);
+
+            double user_pass = user2 - user1;
+            double system_pass = system2 - system1;
+            double irq_pass = irq2 - irq1;
+            double idle_pass = idle2 - idle1;
+            double usage = (user_pass + system_pass + irq_pass) * 100.00
+                    / (user_pass + irq_pass + system_pass + idle_pass);
+            BigDecimal b = new BigDecimal(usage);
+            double res = b.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
+            return res;
+        } catch (FileNotFoundException e) {
+            e.printStackTrace();
+        } catch (IOException e1) {
+            e1.printStackTrace();
+        }
+
+        return 0;
+
+    }
+
+    public String[] getProcessCpuAction(int pid) {
+        String cpuPath = "/proc/" + pid + "/stat";
+        String cpu = "";
+        String[] result = new String[3];
+
+        File f = new File(cpuPath);
+        if (!f.exists() || !f.canRead())
+        {
+			/*
+			 * 进程信息可能无法读取,
+			 * 同时发现此类进程的PSS信息也是无法获取的,用PS命令会发现此类进程的PPid是1
+			 * 即/init,而其他进程的PPid是zygote,
+			 * 说明此类进程是直接new出来的,不是Android系统维护的
+			 */
+            return result;
+        }
+
+        FileReader fr = null;
+        BufferedReader localBufferedReader = null;
+
+        try {
+            fr = new FileReader(f);
+            localBufferedReader = new BufferedReader(fr, 8192);
+            cpu = localBufferedReader.readLine();
+            if (null != cpu) {
+                String[] cpuSplit = cpu.split(" ");
+                result[0] = cpuSplit[1];
+                result[1] = cpuSplit[13];
+                result[2] = cpuSplit[14];
+            }
+        }catch (IOException e) {
+            e.printStackTrace();
+        }
+        FileUtil.closeReader(localBufferedReader);
+        return result;
+    }
+
+    public String[] getCpuAction() {
+        String cpuPath = "/proc/stat";
+        String cpu = "";
+        String[] result = new String[7];
+
+        File f = new File(cpuPath);
+        if (!f.exists() || !f.canRead())
+        {
+            return result;
+        }
+
+        FileReader fr = null;
+        BufferedReader localBufferedReader = null;
+
+        try {
+            fr = new FileReader(f);
+            localBufferedReader = new BufferedReader(fr, 8192);
+            cpu = localBufferedReader.readLine();
+            if (null != cpu) {
+                result = cpu.split(" ");
+
+            }
+        } catch (FileNotFoundException e) {
+            e.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        FileUtil.closeReader(localBufferedReader);
+        return result;
+    }
+
+    public CPUUtils() {
+        initCpuData();
+    }
+
+    private void initCpuData() {
+        pCpu = o_pCpu = 0.0;
+        aCpu = o_aCpu = 0.0;
+
+    }
+
+    public String getProcessCpuUsage(int pid) {
+
+        String result = "";
+        String[] result1 = null;
+        String[] result2 = null;
+        if (pid >= 0) {
+
+            result1 = getProcessCpuAction(pid);
+            if (null != result1) {
+                pCpu = Double.parseDouble(result1[1])
+                        + Double.parseDouble(result1[2]);
+            }
+            result2 = getCpuAction();
+            if (null != result2) {
+                aCpu = 0.0;
+                for (int i = 2; i < result2.length; i++) {
+
+                    aCpu += Double.parseDouble(result2[i]);
+                }
+            }
+            double usage = 0.0;
+            if ((aCpu - o_aCpu) != 0) {
+                usage = DoubleUtils.div(((pCpu - o_pCpu) * 100.00),
+                        (aCpu - o_aCpu), 2);
+                if (usage < 0) {
+                    usage = 0;
+                }
+                else if (usage > 100)
+                {
+                    usage = 100;
+                }
+
+            }
+            o_pCpu = pCpu;
+            o_aCpu = aCpu;
+            result = String.valueOf(usage) + "%";
+        }
+        p_jif = pCpu;
+        return result;
+    }
+
+    public static void getCpuUsageByCmd() {
+        Process process;
+        StringBuilder sb = new StringBuilder();
+        String line = "";
+        String cmd = "dumpsys cpuinfo";
+        try {
+            process = Runtime.getRuntime().exec(
+                    new String[] { "sh", "-c", cmd });
+            BufferedReader br = new BufferedReader(new InputStreamReader(
+                    process.getInputStream()));
+            while (((line = br.readLine()) != null)) {
+                // 去掉空白行数据
+                line = line.trim();
+                if (line.equals("")) {
+                    continue;
+                }
+                sb.append(line);
+                sb.append("\r\n");
+            }
+            try {
+                process.waitFor();
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+}

+ 176 - 0
src/main/java/net/mooctest/www/android_auto_test/Obversers/monitor/LogLine.java

@@ -0,0 +1,176 @@
+package net.mooctest.www.android_auto_test.Obversers.monitor;
+
+import org.apache.http.util.TextUtils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Created by homer on 17-2-11.
+ */
+public class LogLine {
+    public static final String LOGCAT_DATE_FORMAT = "MM-dd HH:mm:ss.SSS";
+
+    private static final int TIMESTAMP_LENGTH = 19;
+
+    private static Pattern logPattern = Pattern.compile(
+            // log level
+            "(\\w)" +
+                    "/" +
+                    // tag
+                    "([^(]+)" +
+                    "\\(\\s*" +
+                    // pid
+                    "(\\d+)" +
+                    // optional weird number that only occurs on ZTE blade
+                    "(?:\\*\\s*\\d+)?" +
+                    "\\): ");
+
+    private int logLevel;
+    private String tag;
+    private String logOutput;
+    private int processId = -1;
+    private String timestamp;
+    private boolean expanded = false;
+    private boolean highlighted = false;
+
+    public CharSequence getOriginalLine() {
+
+        if (logLevel == -1) { // starter line like "begin of log etc. etc."
+            return logOutput;
+        }
+
+        StringBuilder stringBuilder = new StringBuilder();
+
+        if (timestamp != null) {
+            stringBuilder.append(timestamp).append(' ');
+        }
+
+        stringBuilder.append(convertLogLevelToChar(logLevel))
+                .append('/')
+                .append(tag)
+                .append('(')
+                .append(processId)
+                .append("): ")
+                .append(logOutput);
+
+        return stringBuilder;
+    }
+
+    public int getLogLevel() {
+        return logLevel;
+    }
+    public void setLogLevel(int logLevel) {
+        this.logLevel = logLevel;
+    }
+    public String getTag() {
+        return tag;
+    }
+    public void setTag(String tag) {
+        this.tag = tag;
+    }
+    public String getLogOutput() {
+        return logOutput;
+    }
+    public void setLogOutput(String logOutput) {
+        this.logOutput = logOutput;
+    }
+
+    public int getProcessId() {
+        return processId;
+    }
+    public void setProcessId(int processId) {
+        this.processId = processId;
+    }
+
+    public String getTimestamp() {
+        return timestamp;
+    }
+    public void setTimestamp(String timestamp) {
+        this.timestamp = timestamp;
+    }
+    public boolean isExpanded() {
+        return expanded;
+    }
+    public void setExpanded(boolean expanded) {
+        this.expanded = expanded;
+    }
+
+    public boolean isHighlighted() {
+        return highlighted;
+    }
+
+    public void setHighlighted(boolean highlighted) {
+        this.highlighted = highlighted;
+    }
+
+    public static LogLine newLogLine(String originalLine, boolean expanded) {
+
+        LogLine logLine = new LogLine();
+        logLine.setExpanded(expanded);
+
+        int startIdx = 0;
+
+        // if the first char is a digit, then this starts out with a timestamp
+        // otherwise, it's a legacy log or the beginning of the log output or something
+        if (!TextUtils.isEmpty(originalLine)
+                && Character.isDigit(originalLine.charAt(0))
+                && originalLine.length() >= TIMESTAMP_LENGTH) {
+            String timestamp = originalLine.substring(0, TIMESTAMP_LENGTH - 1);
+            logLine.setTimestamp(timestamp);
+            startIdx = TIMESTAMP_LENGTH; // cut off timestamp
+        }
+
+        Matcher matcher = logPattern.matcher(originalLine);
+
+        if (matcher.find(startIdx)) {
+            char logLevelChar = matcher.group(1).charAt(0);
+
+            logLine.setLogLevel(convertCharToLogLevel(logLevelChar));
+            logLine.setTag(matcher.group(2));
+            logLine.setProcessId(Integer.parseInt(matcher.group(3)));
+
+            logLine.setLogOutput(originalLine.substring(matcher.end()));
+
+        } else {
+            logLine.setLogOutput(originalLine);
+            logLine.setLogLevel(-1);
+        }
+
+        return logLine;
+
+    }
+    public static int convertCharToLogLevel(char logLevelChar) {
+
+        switch (logLevelChar) {
+            case 'D':
+                return 3;
+            case 'E':
+                return 6;
+            case 'I':
+                return 4;
+            case 'V':
+                return 2;
+            case 'W':
+                return 5;
+        }
+        return -1;
+    }
+
+    public static char convertLogLevelToChar(int logLevel) {
+
+        switch (logLevel) {
+            case 3:
+                return 'D';
+            case 6:
+                return 'E';
+            case 4:
+                return 'I';
+            case 2:
+                return 'V';
+            case 5:
+                return 'W';
+        }
+        return ' ';
+    }
+}

+ 247 - 0
src/main/java/net/mooctest/www/android_auto_test/Obversers/monitor/LogMonitor.java

@@ -0,0 +1,247 @@
+package net.mooctest.www.android_auto_test.Obversers.monitor;
+
+import net.mooctest.www.android_auto_test.models.Device;
+import net.mooctest.www.android_auto_test.utils.AddressUtil;
+import net.mooctest.www.android_auto_test.utils.DeviceUtil;
+import net.mooctest.www.android_auto_test.utils.FileUtil;
+import org.apache.http.util.TextUtils;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+/**
+ * @author henrylee
+ */
+public class LogMonitor implements Runnable{
+    // 全部
+    public static final int LOG_VERBOSE = 0;
+    // 调试
+    public static final int LOG_DEBUG = 1;
+    // 信息
+    public static final int LOG_INFO = 2;
+    // 警告
+    public static final int LOG_WARNING = 3;
+    // 错误
+    public static final int LOG_ERROR = 4;
+    // 显示的log级别,起到开关的作用
+    public static int Log_Level = LOG_VERBOSE;
+
+    // 因为Activity的生命周期不太靠谱,所以在这里保存上次的文件名
+    private static String lastSaveLog = "GTLog";
+
+    // 日志的日期部分长度是19
+    private static final int TIMESTAMP_LENGTH = 19;
+    // 去掉日期,logcat的匹配模式
+    private static Pattern logPattern = Pattern.compile(
+            // level
+            "(\\w)" +
+                    "/" +
+                    // tag
+                    "([^(]+)" +
+                    "\\(\\s*" +
+                    // pid
+                    "(\\d+)" +
+                    // optional weird number that only occurs on ZTE blade
+                    "(?:\\*\\s*\\d+)?" +
+                    "\\): ");
+
+    private Process logcatProcess;
+    private BufferedReader bufferedReader;
+    private String lastLine;
+    protected boolean recordingMode;
+    private File mTargetFile;
+    private FileWriter mWriter;
+    private String traceId;
+    private String udid;
+    private String deviceYear;
+    private volatile boolean stoped = false;
+
+    public LogMonitor(String udid, String traceId) {
+        super();
+        this.udid = udid;
+        this.traceId = traceId;
+        try {
+            init(udid);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void stop() {
+        logcatProcess.destroy();
+        this.stoped = true;
+
+    }
+    private void init(String udid) throws IOException {
+    	deviceYear = DeviceUtil.getDeviceYear(udid);
+    	if(deviceYear.charAt(0) < '1' || deviceYear.charAt(0) > '9') {
+    	    deviceYear = "";
+        }
+        logcatProcess = getLogcatProcess(udid);
+        bufferedReader = new BufferedReader(new InputStreamReader(logcatProcess.getInputStream()));
+
+        String logPath = AddressUtil.getLogCatLogPath(traceId, udid);
+        File file = new File(logPath);
+        FileUtil.newFile(file);
+        mTargetFile = new File(logPath);
+        mWriter = new FileWriter(mTargetFile);
+    }
+    @Override
+    public void run() {
+        while (!stoped) {
+            try {
+                String line = readLine();
+                if (line != null) {
+                    logCat(line);
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        try {
+            bufferedReader.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public String readLine() throws IOException {
+        String line = bufferedReader.readLine();
+        if (recordingMode && lastLine != null) {
+            // still skipping past the 'last line'
+            if (lastLine.equals(line) || isAfterLastTime(line)) {
+                // indicates we've passed the last line
+                lastLine = null;
+            }
+        }
+        return line;
+    }
+
+    private boolean isAfterLastTime(String line) {
+        // doing a string comparison is sufficient to determine whether this line is chronologically
+        // after the last line, because the format they use is exactly the same and
+        // lists larger time period before smaller ones
+        return isDatedLogLine(lastLine) && isDatedLogLine(line) && line.compareTo(lastLine) > 0;
+
+    }
+
+    private boolean isDatedLogLine(String line) {
+        // 18 is the size of the logcat timestamp
+        return ((line != null && !line.isEmpty()) && line.length() >= 18 && Character.isDigit(line.charAt(0)));
+    }
+
+    private static List<String> getLogcatArgs(String udid) {
+    	int SDK_INT = Integer.parseInt(DeviceUtil.getApiLevel(udid));
+    	List<String> args = new ArrayList<String>();
+    	if(SDK_INT >= 23){
+    		//SDK > Android 6.0
+           args = new ArrayList<>(Arrays.asList("adb", "-s", udid, "logcat", "-b", "main", "-b", "system", "-b", "crash", "-b", "events", "-b", "radio", "-v", "time"));
+    	}else {
+            args = new ArrayList<>(Arrays.asList("adb", "-s", udid, "logcat", "-b", "main", "-b", "system", "-b", "events", "-b", "radio", "-v", "time"));
+		}
+        return args;
+    }
+
+    public static Process getLogcatProcess(String udid) throws IOException {
+        List<String> args = getLogcatArgs(udid);
+        ProcessBuilder pb = new ProcessBuilder(args);
+        pb.redirectErrorStream(true);
+        Process process  = pb.start();
+//        Process process = OSUtil.exec(args);
+        return process;
+    }
+
+    public  void logCat(String originalLine)
+    {
+        // 先解析日志
+        String sTime = null;
+        long pid = -1;
+        char level = 'I';
+        String tag = null;
+        String msg = null;
+
+        int startIdx = 0;
+        if (!TextUtils.isEmpty(originalLine)
+                && Character.isDigit(originalLine.charAt(0))
+                && originalLine.length() >= TIMESTAMP_LENGTH) {
+            String timestamp = originalLine.substring(0, TIMESTAMP_LENGTH - 1);
+            sTime = timestamp;
+            // cut off timestamp
+            startIdx = TIMESTAMP_LENGTH;
+        }
+
+        Matcher matcher = logPattern.matcher(originalLine);
+        if (matcher.find(startIdx)) {
+            level = matcher.group(1).charAt(0);
+            int gtLevel = LOG_VERBOSE;
+            switch (level)
+            {
+                case 'V':
+                    gtLevel = LOG_VERBOSE;
+                    break;
+                case 'D':
+                    gtLevel = LOG_DEBUG;
+                    break;
+                case 'I':
+                    gtLevel = LOG_INFO;
+                    break;
+                case 'W':
+                    gtLevel = LOG_WARNING;
+                    break;
+                case 'E':
+                    gtLevel = LOG_ERROR;
+                    break;
+                default: break;
+            }
+
+            tag = matcher.group(2);
+            pid = Integer.parseInt(matcher.group(3));
+            msg = originalLine.substring(matcher.end());
+
+            log(pid, gtLevel, tag, msg, sTime);
+        }
+    }
+
+    public  void log(long pid, int level, String tag, String msg, String sTime) {
+        if (level < Log_Level) {
+            return;
+        }
+        if (level > LOG_ERROR || null == tag || null == msg)
+        {
+            return;
+        }
+        char sLevel = 'V';
+        switch (level) {
+            case LOG_VERBOSE:
+                sLevel = 'V';
+                break;
+            case LOG_DEBUG:
+                sLevel = 'D';
+                break;
+            case LOG_INFO:
+                sLevel = 'I';
+                break;
+            case LOG_WARNING:
+                sLevel = 'W';
+                break;
+            case LOG_ERROR:
+                sLevel = 'E';
+                break;
+            default: break;
+        }
+        try {
+            mWriter = new FileWriter(mTargetFile,true);
+//            mWriter.write(pid + " " + sLevel + " " + tag + " " + msg + " " + sTime + "\n");
+            mWriter.write(pid + "||" + sLevel + "||" + tag + "||" + msg + "||" + deviceYear + sTime + "\n");
+            mWriter.flush();
+            mWriter.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+}

+ 119 - 0
src/main/java/net/mooctest/www/android_auto_test/Obversers/monitor/SMMonitor.java

@@ -0,0 +1,119 @@
+package net.mooctest.www.android_auto_test.Obversers.monitor;
+
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Created by homer on 17-2-10.
+ */
+public class SMMonitor {
+    boolean killed = false;
+    private int mPid;
+    private AtomicInteger count = new AtomicInteger(0);
+    private String udid;
+    public void startSMMonitor(String udid, int pid) {
+//        if (!isEnableForMonitor()) {
+//            //进行设置,重启
+//        }
+//        //使用日志来收集frame的time
+//        //计算
+        this.udid = udid;
+        this.mPid = pid;
+        new Thread(new SMMonitorRunnable()).start();
+    }
+
+    public void stop() {
+        killed = true;
+    }
+
+    public int getSkippedFrameCount() {
+        return count.getAndSet(0);
+    }
+    private boolean isEnableForMonitor() {
+        String cmd = "adb shell getprop debug.choreographer.skipwarning";
+        ProcessBuilder execBuilder = new ProcessBuilder(cmd);
+        execBuilder.redirectErrorStream(true);
+        boolean flag = false;
+        try {
+            Process p = execBuilder.start();
+            InputStream is = p.getInputStream();
+            InputStreamReader isr = new InputStreamReader(is);
+            BufferedReader br = new BufferedReader(isr);
+            String line;
+            while((line = br.readLine()) != null) {
+                if (line.compareTo("1") == 0) {
+                    flag = true;
+                    break;
+                }
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return flag;
+    }
+
+    class SMMonitorRunnable implements Runnable {
+        Process dumpLogcatProcess = null;
+        BufferedReader reader = null;
+
+        @Override
+        public void run() {
+            List<String> args = new ArrayList<>(Arrays.asList("adb", "-s", udid, "shell", "logcat", "-v", "time", "Choreographer:I", "*:S"));
+            try {
+                ProcessBuilder processBuilder = new ProcessBuilder(args);
+                processBuilder.redirectErrorStream(true);
+                dumpLogcatProcess = processBuilder.start();
+                reader = new BufferedReader(new InputStreamReader(dumpLogcatProcess.getInputStream()), 8192);
+
+                String line;
+
+                while((line = reader.readLine()) != null && !killed) {
+                    if (!line.contains("uch work on its main t")) {
+                        continue;
+                    }
+                    int pID = LogLine.newLogLine(line, false).getProcessId();
+                    if (pID != mPid) {
+                        continue;
+                    }
+                    line = line.substring(50, line.length() - 71);
+                    Integer value = Integer.parseInt(line.trim());
+                    //全部加到count中去。
+                    count.addAndGet(value);
+                    //SM = (60* totalSeconds - totalSkippedFrames) / totalSeconds;
+
+                }
+                if (dumpLogcatProcess != null) {
+                    dumpLogcatProcess.destroy();
+                }
+                reader.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            } finally {
+                killProcess();
+            }
+        }
+
+        public void killProcess() {
+            if (!killed) {
+                if (dumpLogcatProcess != null) {
+                    dumpLogcatProcess.destroy();
+                }
+                if (reader != null) {
+                    try {
+                        reader.close();
+                    } catch (IOException e) {
+                        e.printStackTrace();
+                    }
+                }
+                killed = true;
+            }
+        }
+    }
+}

+ 28 - 0
src/main/java/net/mooctest/www/android_auto_test/Scripts/AbstractBaseScript.java

@@ -0,0 +1,28 @@
+package net.mooctest.www.android_auto_test.Scripts;
+
+import com.sinaapp.msdxblog.apkUtil.entity.ApkInfo;
+import io.appium.java_client.AppiumDriver;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author henrylee
+ */
+@NoArgsConstructor
+public abstract class AbstractBaseScript {
+    boolean stopFlag;
+
+    void prepare(){}
+    void preRun(){}
+    abstract void run();
+    void afterRun(){}
+    public void endScript(){
+        this.stopFlag = true;
+    }
+
+    public void runAndroidAutoTest() {
+        prepare();
+        preRun();
+        run();
+        afterRun();
+    }
+}

+ 906 - 0
src/main/java/net/mooctest/www/android_auto_test/Scripts/DefaultScript.java

@@ -0,0 +1,906 @@
+package net.mooctest.www.android_auto_test.Scripts;
+
+import com.google.gson.Gson;
+import com.sinaapp.msdxblog.apkUtil.entity.ApkInfo;
+import io.appium.java_client.TouchAction;
+import io.appium.java_client.android.AndroidDriver;
+import io.appium.java_client.android.AndroidTouchAction;
+import io.appium.java_client.android.nativekey.AndroidKey;
+import io.appium.java_client.android.nativekey.KeyEvent;
+import io.appium.java_client.touch.TapOptions;
+import io.appium.java_client.touch.WaitOptions;
+import io.appium.java_client.touch.offset.PointOption;
+import net.mooctest.www.android_auto_test.common.constant.Consts;
+import net.mooctest.www.android_auto_test.models.Action;
+import net.mooctest.www.android_auto_test.models.Activity;
+import net.mooctest.www.android_auto_test.models.Component;
+import net.mooctest.www.android_auto_test.models.IgnoreComponent;
+import net.mooctest.www.android_auto_test.utils.*;
+import org.apache.commons.io.FileUtils;
+import org.openqa.selenium.*;
+
+import java.io.*;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+import static java.nio.charset.StandardCharsets.*;
+
+/**
+ * @author henrylee
+ */
+public class DefaultScript extends AbstractBaseScript {
+
+    public static final String TAG = Thread.currentThread() .getStackTrace()[1].getClassName();
+
+    private String udid;
+    private AndroidDriver driver;
+    private ApkInfo apkInfo;
+    private String traceId;
+
+    private BufferedWriter scriptWriter;
+    private BufferedWriter myTestLogWriter;
+    private BufferedWriter actionWriter;
+    private List<Activity> activityList = new ArrayList<>();
+
+    private List<String> fatherComponentList = new ArrayList<>();
+    private HashMap<String, IgnoreComponent> ignoreMap;
+
+    private boolean hasTakenScreenshot = false;
+
+    public DefaultScript(String udid, AndroidDriver driver, ApkInfo apkInfo, String traceId) {
+        this.traceId = traceId;
+        this.udid = udid;
+        this.driver = driver;
+        this.apkInfo = apkInfo;
+    }
+
+    private void initWriter(File testScript, File testAction, File myTestLog){
+        try {
+            scriptWriter = new BufferedWriter(new FileWriter(testScript,true));
+            myTestLogWriter = new BufferedWriter(new FileWriter(myTestLog,true));
+            actionWriter = new BufferedWriter(new FileWriter(testAction, true));
+        } catch (IOException e) {
+            PrintUtil.printException(TAG, udid, e);
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public void prepare(){
+        File myTestLog = new File(AddressUtil.getMyTestLogPath(traceId, udid));
+        File testScript = new File(AddressUtil.getTestScript(traceId, udid));
+        File testAction = new File(AddressUtil.getTestAction(traceId, udid));
+        FileUtil.newFiles(Arrays.asList(myTestLog, testAction, testScript));
+        initWriter(testScript, testAction, myTestLog);
+        File pageXmlDir = new File(AddressUtil.getPageXmlDir(traceId, udid));
+        File pageSourceDir = new File(AddressUtil.getPageSourceDir(traceId, udid));
+        File pageImgDir = new File(AddressUtil.getPageImgDir(traceId, udid));
+        FileUtil.newDirs(Arrays.asList(pageXmlDir, pageImgDir, pageSourceDir));
+        loadConfiguration();
+    }
+
+    @Override
+    public void run(){
+        PrintUtil.print("Mock run!", TAG, udid);
+        String rootActivity = "root";
+        DFS(rootActivity, 0);
+    }
+
+    /**
+     * 深度优先遍历APP
+     * @param depth 遍历深度
+     */
+    private void DFS(String fatherActivity, int depth){
+        if (this.stopFlag){
+            return;
+        }
+        handleAndroidAlertOnLaunch();
+        threadSleep(2);
+        List<Component> componentList = getCurrentClickableComList(depth, true);
+        if (componentList.size() == 0){
+            handleNoClickableComWhenFirstLaunch(componentList, depth);
+        }
+        // 处理了启动时的系统弹框,处理了第一次进入APP时的滑动页面
+        componentList = getCurrentClickableComList(depth, false);
+
+        //TODO 处理进入应用需要同意用户协议等内容的场景
+        // 应该在获取了componentList之后,再处理outOfPackage的情况
+        boolean isOutOfAppPackage = isOutOfAppPackage(componentList, depth);
+        if (isOutOfAppPackage){
+            PrintUtil.print("Have tried 2 times, still not in appPackage", TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+            PrintUtil.print("Change isEnd to true", TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+            this.endScript();
+            return;
+        } else if (driver.currentActivity().equals(fatherActivity)){
+            return;
+        }
+        String currentActivityName = driver.currentActivity();
+        int currentHash = driver.getPageSource().hashCode();
+        int index = activityIndexOfActivityList(currentActivityName);
+        if (index < 0){
+            //如果页面没出现过,则说明是新页面
+            threadSleep(2);
+            takeScreenShotAndSavePageImg();
+            savePageXml();
+            Activity currentActivity = new Activity(currentActivityName, fatherActivity, currentHash, componentList);
+            activityList.add(currentActivity);
+            index = activityList.size() - 1;
+        }else {
+            // 如果页面出现过,需要更新一些页面属性
+            Activity activity = activityList.get(index);
+            String oldFather = activity.getFatherActivity();
+            if (!oldFather.equals(fatherActivity)){
+                String msg = String.format("Previous Activity is %s, but %s's father is %s, change father.", fatherActivity, currentActivityName, oldFather);
+                PrintUtil.print(msg, TAG, udid, myTestLogWriter, PrintUtil.ANSI_GREEN);
+                activity.setFatherActivity(fatherActivity);
+            }
+            int oldHash = activity.getHash();
+            if (oldHash != currentHash){
+                activity.setHash(currentHash);
+                List<Component> oldComponent = activity.getComList();
+                componentList = setTestedComInList(oldComponent, componentList);
+                activity.setComList(componentList);
+            }
+        }
+        Activity activity4Test = activityList.get(index);
+        if (activity4Test.isDone()){
+            PrintUtil.print("Going to return because this page is over", TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+            returnPrevActivity();
+            return;
+        }
+        // 后面就是处理activity的父子关系,以及遍历点击了
+        for (int comIndex=0;comIndex<componentList.size();++comIndex){
+            if (this.stopFlag){
+                return;
+            }
+            PrintUtil.print(String.format("%s's index is %s/%s.", currentActivityName, comIndex, componentList.size()), TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+            Component component = componentList.get(comIndex);
+            //检查组件是否已被测试or已被配置忽略
+            if(judgeTestedComponent(component) || judgeIgnoreComponent(component)) {
+                continue;
+            }
+            if (component.getFatherComponent() != null){
+                executeClickFatherComponent(component.getFatherComponent());
+            }
+            String locator = component.getLocator();
+            if (locator == null){
+                continue;
+            }
+            WebElement elementForTest = null;
+            PrintUtil.print("Try to locate " + locator, TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+            try {
+                if(locator.startsWith("//")) {
+                    elementForTest = driver.findElementByXPath(locator);
+                }
+                else {
+                    elementForTest = driver.findElementById(locator);
+                }
+            }catch(Exception exception){
+                //定位失败,处理一下安卓弹窗,尝试定位该页面下一个控件
+                PrintUtil.print("Can't locate component " + component.getLocator(), TAG, udid, myTestLogWriter, PrintUtil.ANSI_RED);
+                handleAndroidAlert();
+                continue;
+            }
+            component.setHasBeenTested(true);
+            if (judgeFatherComponent(component, currentActivityName)){
+                executeClick(component, elementForTest);
+                List<Component> comListAfterClickFatherCom = getCurrentClickableComList(depth, true);
+                for(int i = 0;i < comListAfterClickFatherCom.size();i++){
+                    if(!componentList.contains(comListAfterClickFatherCom.get(i))){
+                        comListAfterClickFatherCom.get(i).setFatherComponent(component);
+                    }
+                }
+                componentList =  setTestedComInList(componentList, comListAfterClickFatherCom);
+                PrintUtil.print("After set father component", TAG, udid, myTestLogWriter, PrintUtil.ANSI_GREEN);
+                printComponentList(componentList);
+                executeKeyEvent("Click Return button because handled father components", AndroidKey.BACK);
+                comIndex = -1;
+                activityList.get(index).setComList(componentList);
+                //因为处理完父控件的子控件后又返回了,所以相当于处于未点击父控件之前的页面,所以不更新pageHash
+                continue;
+            }
+
+            String fatherOfCurrentActivity = activity4Test.getFatherActivity();
+            if (component.getClassname().contains("EditText")){
+                executeInput(component, elementForTest);
+            }else {
+                //其他控件,则点击
+                executeClick(component, elementForTest);
+                if(stopFlag) {
+                    return;
+                }
+                //只在进入视频播放页面第一次时截图并新建节点加入遍历生成树,之后都是直接按返回键(因为获取视频播放页面的PageSource会崩溃)
+                if(driver.currentActivity().equals(".ui.video.VideoPlayerActivity")){
+                    if(!hasTakenScreenshot){
+                        takeScreenShotAndSavePageImg();
+                        hasTakenScreenshot = true;
+                    }
+                    executeKeyEvent("Click Return button cause only enter VideoPlayerActivity", AndroidKey.BACK);
+                }
+                //TODO 点击后先看还在不在app中,不在立刻返回
+                //点击控件后马上检查是否有android消息弹窗或警告窗出现(至多只会出现一种弹窗),若有(这种情况一般是点了页面上的返回按钮,返回了主页面)就点击取消按钮
+                handleAndroidAlert();
+                headleAndroidMessage();
+                String activityAfterClick = driver.currentActivity();
+                int pageHashAfterClick = driver.getPageSource().hashCode();
+                if(stopFlag) {
+                    return;
+                }
+                // 页面完全没变化
+                if (pageHashAfterClick == currentHash){
+                    PrintUtil.print("The page has not changed", TAG, udid, myTestLogWriter, PrintUtil.ANSI_GREEN);
+                    continue;
+                }
+                //页面的pageSource的hash值变了,但是activity没变
+                if (activityAfterClick.equals(currentActivityName)){
+                    PrintUtil.print("The UI components have changed", TAG, udid, myTestLogWriter, PrintUtil.ANSI_GREEN);
+                    List<Component> comListAfterClick = getCurrentClickableComList(depth, true);
+                    if (comListAfterClick.hashCode() == componentList.hashCode()){
+                        PrintUtil.print("The component list hash code has not changed, continue to test current comList", TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+                    } else {
+                        componentList = setTestedComInList(componentList, comListAfterClick);
+                        //将此Activity的comList(去除先前页面已有且测试过的控件)和hash更新,index重置,继续遍历此Activity控件,而不是开始新一轮深搜
+                        comIndex = -1;
+                        activityList.get(index).setComList(componentList);
+                        activityList.get(index).setHash(pageHashAfterClick);
+                    }
+                    printComponentList(componentList);
+                    continue;
+                }
+                //activity和pageSource都变化了
+                PrintUtil.print("The page has changed", TAG, udid, myTestLogWriter, PrintUtil.ANSI_GREEN);
+                PrintUtil.print("Current activity is " + activityAfterClick, TAG, udid, myTestLogWriter, PrintUtil.ANSI_GREEN);
+                if(activityAfterClick.equals(fatherOfCurrentActivity)){
+                    //如果改变后的当前页面为之前页面的父页面,返回(即不认为是开始了一次新的一轮深搜)
+                    PrintUtil.print("I'm going to return because of my dad.\r\n", TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+                    return;
+                }
+                //页面发生改变,进入的是子页面
+                if(stopFlag) {
+                    return;
+                }
+                //TODO 某个屌炸天的DFS逻辑
+                DFS(currentActivityName, depth+1);
+                //点击此控件所跳转到的子页面开展的新一轮深搜已完成并返回(也按了返回键见line),理论上此时页面应该是点击该控件前的页面
+                if(stopFlag) {
+                    return;
+                }
+                PrintUtil.print("Has returned", TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+                //返回之后马上检查是否有android消息弹窗出现,若有(这种情况一般是返回了MainActivity)就点击取消按钮
+                //并检查是否在AppPackage中,不在就结束遍历
+                headleAndroidMessage();
+                boolean outOfAppPackageFlag = judgeOutOfAppPackage(componentList);
+                if(outOfAppPackageFlag) {
+                    //如果返回后的页面不在AppPackage中
+                    PrintUtil.print("Change isEnd to true because not in AppPackage", TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+                    stopFlag = true;
+                    return;
+                }
+                //准备继续遍历这个页面接下来的控件
+                try{
+                    PrintUtil.print("Current activity is " + driver.currentActivity(), TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+                }catch(Exception e){
+                    PrintUtil.printException(TAG, udid, e);
+                    return;
+                }
+                List<Component> comListAfterReturn = getCurrentClickableComList(depth, true);
+                if(componentList.hashCode() != comListAfterReturn.hashCode()){
+                    //此时的页面和之前的页面有区别,就用现在的页面更新了之前页面comList,且去除先前页面已有且测试过的控件,继续遍历这个页面剩下的控件
+                    PrintUtil.print("Update the activity's component , because current comList is different with before", TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+                    componentList = setTestedComInList(componentList, comListAfterReturn);
+                    comIndex = -1;
+                    activityList.get(index).setComList(comListAfterReturn);
+                    activityList.get(index).setHash(driver.currentActivity().hashCode());
+                    printComponentList(componentList);
+                }
+                PrintUtil.print("After return, current index is " + comIndex + "/" + componentList.size(), TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+            }
+        }
+        //本页面所有控件已遍历完
+        PrintUtil.print("Going to return because this page has done.\r\n", TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+        activityList.get(index).setDone(true);
+        returnPrevActivity();
+    }
+
+    private List<Component> getCurrentClickableComList(int i, boolean printFlag){
+        String pageSource = driver.getPageSource();
+        String path = savePageSource(i, pageSource);
+        ParseXml xmlParser = new ParseXml();
+        List<Component> comList = xmlParser.run(path);
+        if (comList.size() == 0){
+            comList = getCurrentComList(i);
+        }
+        if(printFlag){
+            printComponentList(comList);
+        }
+        return comList;
+    }
+    private List<Component> getCurrentComList(int depth){
+        List<Component> comList = parseAndSavePageSource(depth);
+        printComponentList(comList);
+        return comList;
+    }
+    private void executeInput(Component component, WebElement element){
+        String inputValue = null;
+        PrintUtil.print("This is an editText , trying to find input value", TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+        InputFinder finder = new InputFinder();
+        try {
+            inputValue = finder.getInputValue(AddressUtil.getHumanScriptPath(),component.getResource_id());
+        } catch (IOException e) {
+            PrintUtil.print("Find input value error!", TAG, udid, myTestLogWriter, PrintUtil.ANSI_RED);
+            e.printStackTrace();
+        }
+        if(inputValue != null){
+            PrintUtil.print("Input " + component.getLocator() + " use vaule " + inputValue, TAG, udid, myTestLogWriter, PrintUtil.ANSI_GREEN);
+            String activityBeforeAction = driver.currentActivity();
+            String timeBeforeAction = DeviceUtil.getDeviceTime(udid);
+            //找到了该控件的输入值,进行输入
+            element.sendKeys(inputValue);
+            String timeAfterAction = DeviceUtil.getDeviceTime(udid);
+            String activityAfterAction = driver.currentActivity();
+            try {
+                String type = "INPUT";
+                String msg = "Input " + component.getLocator() + " use vaule " + inputValue;
+                if(!component.getLocator().contains("bounds")) {
+                    msg = msg + ", bounds: \'" + component.getBounds() + "\'";
+                }
+                recordAction(new Action(timeBeforeAction, timeAfterAction, type, msg, activityBeforeAction, activityAfterAction));
+                String locator = component.getLocator();
+                if(locator.startsWith("//")) {
+                    scriptWriter.write("driver.findElementByXPath(\"" + locator + "\").sendKeys(\"" + inputValue + "\");\n");
+                } else {
+                    scriptWriter.write("driver.findElementById(\"" + locator + "\").sendKeys(\"" + inputValue + "\");\n");
+                }
+                scriptWriter.flush();
+            } catch (IOException e1) {
+                PrintUtil.printException(TAG, udid, e1);
+                e1.printStackTrace();
+            }
+        }else {PrintUtil.print("Can't find input value for this editText", TAG, udid, myTestLogWriter, PrintUtil.ANSI_RED);}
+    }
+    private void executeClick(Component component, WebElement element){
+        String locator = component.getLocator();
+        PrintUtil.print("Click component " + locator, TAG, udid, myTestLogWriter, PrintUtil.ANSI_GREEN);
+        String activityBeforeAction = driver.currentActivity();
+        String timeBeforeAction = DeviceUtil.getDeviceTime(udid);
+        String componentType = component.getClassname();
+        // 如果不是TextView的组件,则默认是button
+        if (!componentType.contains("TextView")){
+            element.click();
+        } else {
+            // 如果是TextView的组件,则定位绝对位置去点击
+            Point p = element.getLocation();
+            int x = p.x + element.getSize().getWidth() / 2;
+            int y = p.y + element.getSize().getHeight() / 4;
+            new TouchAction<>(driver).tap(PointOption.point(x, y)).perform();
+        }
+
+        String timeAfterAction = DeviceUtil.getDeviceTime(udid);
+        threadSleep(6);
+        String activityAfterAction = driver.currentActivity();
+        try {
+            String type = "CLICK";
+            String msg = "Click widget " + locator;
+            if(!locator.contains("bounds")) {
+                msg = msg + ", bounds: \'" + component.getBounds() + "\'";
+            }
+            recordAction(new Action(timeBeforeAction, timeAfterAction, type, msg, activityBeforeAction, activityAfterAction));
+            if(locator.startsWith("//")) {
+                scriptWriter.write("driver.findElementByXPath(\"" + locator + "\").click();\n");
+            } else {
+                scriptWriter.write("driver.findElementById(\"" + locator + "\").click();\n");
+            }
+            scriptWriter.flush();
+        } catch (IOException e) {
+            PrintUtil.printException(TAG, udid, e);
+            e.printStackTrace();
+        }
+    }
+    private void executeSwipe(){
+        int screenWidth = driver.manage().window().getSize().width;
+        int screenHeight = driver.manage().window().getSize().height;
+        int startx = screenWidth * 3 / 4;
+        int starty = screenHeight / 3;
+        int endx = screenWidth / 4;
+        int endy = starty;
+        try {
+            threadSleep(1);
+            String activityBeforeAction = driver.currentActivity();
+            String timeBeforeAction = DeviceUtil.getDeviceTime(udid);
+            // 这里是appium1.2.1和appium更高版本(7.3.0)在我们项目中最大的区别
+            // 1.2.1提供封装好的swipe方法,而7.3.0没有
+            // TODO 解决appium 1.2.1 maven依赖冲突的问题
+            AndroidTouchAction touchAction = new AndroidTouchAction(driver);
+            touchAction.press(PointOption.point(startx, starty))
+                    .waitAction(WaitOptions.waitOptions(Duration.ofMillis(200)))
+                    .moveTo(PointOption.point(endx, endy))
+                    .release().perform();
+            String timeAfterAction = DeviceUtil.getDeviceTime(udid);
+            threadSleep(3);
+            String activityAfterAction = driver.currentActivity();
+            String type = "SWIPE";
+            String msg = String.format("startX: %s, startY: %s, endX: %s, endY: %s, duration: 500ms", startx, starty, endx, endy);
+            recordAction(new Action(timeBeforeAction, timeAfterAction, type, msg, activityBeforeAction, activityAfterAction));
+            String script = String.format("AndroidTouchAction touchAction = new AndroidTouchAction(driver);\n" +
+                    "        touchAction.press(PointOption.point(%s, %s))\n" +
+                    "                    .waitAction(WaitOptions.waitOptions(Duration.ofMillis(%s)))\n" +
+                    "                    .moveTo(PointOption.point(%s, %s))\n" +
+                    "                    .release().perform();", startx, starty, endx, endy, 500);
+            scriptWriter.write(script);
+            scriptWriter.flush();
+        } catch (InvalidElementStateException e){
+            PrintUtil.print("WTF How to solve this exception!", TAG);
+        } catch (Exception e1) {
+            PrintUtil.printException(TAG, udid, e1);
+            e1.printStackTrace();
+        }
+    }
+    private void executeKeyEvent(String msg, AndroidKey androidKey){
+        String activityBeforeAction;
+        String activityAfterAction;
+        String timeBeforeAction;
+        String timeAfterAction;
+        try{
+            PrintUtil.print(msg, TAG, udid, myTestLogWriter, PrintUtil.ANSI_GREEN);
+            activityBeforeAction = driver.currentActivity();
+            timeBeforeAction = DeviceUtil.getDeviceTime(udid);
+            driver.pressKey(new KeyEvent(androidKey));
+            timeAfterAction = DeviceUtil.getDeviceTime(udid);
+            threadSleep(2);
+            activityAfterAction = driver.currentActivity();
+        }catch(Exception e){
+            PrintUtil.printException(TAG, udid, e);
+            return;
+        }
+        try {
+            String type = "CLICK";
+            recordAction(new Action(timeBeforeAction, timeAfterAction, type, msg, activityBeforeAction, activityAfterAction));
+            scriptWriter.write("driver.sendKeyEvent(" + androidKey.getCode() + ");\n");
+            scriptWriter.flush();
+        } catch (IOException e) {
+            PrintUtil.printException(TAG, udid, e);
+            e.printStackTrace();
+        }
+    }
+    private void executeClickFatherComponent(Component component){
+        String locator = component.getLocator();
+        PrintUtil.print("Click father component " + locator, TAG, udid, myTestLogWriter, PrintUtil.ANSI_GREEN);
+        WebElement element;
+        try {
+            if(locator.startsWith("//")) {
+                element = driver.findElementByXPath(locator);
+            } else {
+                element = driver.findElementById(locator);
+            }
+        } catch (Exception e) {
+            PrintUtil.print("Can't locate father component " + locator, TAG, udid, myTestLogWriter, PrintUtil.ANSI_RED);
+            return;
+        }
+        executeClick(component, element);
+    }
+
+    private boolean isOutOfAppPackage(List<Component> componentList, int depth){
+        boolean isOutOfPackage = judgeOutOfAppPackage(componentList);
+        List<Component> tempComs;
+        int threshold = 3;
+        int attemptCount = 0;
+        while (isOutOfPackage && attemptCount < threshold){
+            executeKeyEvent("Click Return button because not in appPackage and ready to check again", AndroidKey.BACK);
+            tempComs = getCurrentClickableComList(depth, false);
+            isOutOfPackage = judgeOutOfAppPackage(tempComs);
+            attemptCount ++;
+        }
+        return isOutOfPackage;
+    }
+
+    private boolean judgeOutOfAppPackage(List<Component> comList){
+        for(Component component : comList){
+            if(component.getPackagename().equals(apkInfo.getPackageName())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean handleAndroidAlert(){
+        boolean haveAlert = false;
+        List<Component> components = parseAndSavePageSource(-1);
+        for (Component com: components){
+            String resourceId = com.getResource_id();
+            if (resourceId == null){
+                continue;
+            }
+            String packageName = com.getPackagename();
+            boolean isComponentAlert = (resourceId.contains("alertTitle") || resourceId.contains("event_title"))
+                    || packageName != null && (packageName.contains("packageinstaller") || packageName.contains("permissioncontroller"));
+            if (isComponentAlert){
+                haveAlert = true;
+                break;
+            }
+        }
+        if (haveAlert){
+            clickAndroidAlert();
+            threadSleep(2);
+            return true;
+        }else {
+            return false;
+        }
+    }
+    private void headleAndroidMessage(){
+        boolean isAndroidMsg = false;
+        List<Component> components = parseAndSavePageSource(-1);
+        for (Component component : components) {
+            if ("android:id/message".equals(component.getResource_id())) {
+                //有androidMsg弹窗
+                isAndroidMsg = true;
+                break;
+            }
+        }
+        if(isAndroidMsg) {
+            clickAndroidMsg();
+        }
+    }
+    private void handleAndroidAlertOnLaunch() {
+        PrintUtil.print("Handle AndroidAlert On Launch", TAG, udid, myTestLogWriter, PrintUtil.ANSI_RED);
+        int index = 0;
+        while (true){
+            PrintUtil.print("Try to handle AndroidAlert times " + index, TAG, udid, myTestLogWriter, PrintUtil.ANSI_RED);
+            if (index >= 6){
+                PrintUtil.print("Can't handle AndroidAlert On Launch", TAG, udid, myTestLogWriter, PrintUtil.ANSI_RED);
+                break;
+            }
+            boolean goOn = handleAndroidAlert();
+            if (goOn) {
+                PrintUtil.print("Handled AndroidAlert " + index, TAG, udid, myTestLogWriter, PrintUtil.ANSI_RED);
+                index++;
+            }else {
+                PrintUtil.print("No AndroidAlert. Continue test.", TAG, udid, myTestLogWriter, PrintUtil.ANSI_RED);
+                break;
+            }
+        }
+    }
+
+    private List<Component> handleNoClickableComWhenFirstLaunch(List<Component> coms, int depth){
+        int swipeCount = 0;
+        while (coms.size() == 0 && swipeCount < 8){
+            PrintUtil.print("Ready to swipe because there is no clickable component", TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+            executeSwipe();
+            coms = getCurrentClickableComList(depth, true);
+            swipeCount++;
+        }
+        return coms;
+    }
+
+    private void clickAndroidAlert(){
+        for (String label: Consts.ANDROID_ALERT_LABELS) {
+            String printMessage = "Click button1 for AndroidAlert";
+            String actionType = "CLICK";
+            String actionMsg = "Click Confirm button for AndroidAlert";
+            String scriptFormat = "driver.findElementById(%s).click();\n";
+            if (!label.contains("android")){
+                printMessage = "Click allow button for AndroidAlert";
+                scriptFormat = "driver.findElementByXPath(\"//android.widget.Button[contains(@text,'%s')]\").click();\n";
+            }
+            try {
+                PrintUtil.print(printMessage, TAG, udid, myTestLogWriter, PrintUtil.ANSI_GREEN);
+                String activityBeforeAction = driver.currentActivity();
+                String timeBeforeAction = DeviceUtil.getDeviceTime(udid);
+                if (label.contains("android")) {
+                    driver.findElementById(label).click();
+                }else {
+                    driver.findElementByXPath("//android.widget.Button[contains(@text,'" + label + "')]").click();
+                }
+                String timeAfterAction = DeviceUtil.getDeviceTime(udid);
+                String activityAfterAction = driver.currentActivity();
+                // save to action file
+                recordAction(new Action(timeBeforeAction, timeAfterAction, actionType, actionMsg, activityBeforeAction, activityAfterAction));
+                scriptWriter.write(String.format(scriptFormat, label));
+                scriptWriter.flush();
+            } catch (NoSuchElementException e){
+                PrintUtil.print("Can't locate AndroidAlert component", TAG, udid, myTestLogWriter, PrintUtil.ANSI_RED);
+                continue;
+            } catch (Exception e) {
+                PrintUtil.printException(TAG, udid, e);
+            }
+            break;
+        }
+    }
+    private void clickAndroidMsg(){
+        try{
+            PrintUtil.print("Click cancel button for AndroidMsg", TAG, udid, myTestLogWriter, PrintUtil.ANSI_GREEN);
+            String activityBeforeAction = driver.currentActivity();
+            String timeBeforeAction = DeviceUtil.getDeviceTime(udid);
+            driver.findElementById("android:id/button2").click();
+            String timeAfterAction = DeviceUtil.getDeviceTime(udid);
+            String activityAfterAction = driver.currentActivity();
+            try {
+                String type = "CLICK";
+                String msg = "Click Cancel button for AndroidMsg";
+                recordAction(new Action(timeBeforeAction, timeAfterAction, type, msg, activityBeforeAction, activityAfterAction));
+                scriptWriter.write("driver.findElementById(\"android:id/button2\").click();\n");
+                scriptWriter.flush();
+            } catch (IOException e1) {
+                PrintUtil.printException(TAG, udid, e1);
+            }
+        } catch (Exception e) {
+            PrintUtil.print("Can't locate AndroidMsg component", TAG, udid, myTestLogWriter, PrintUtil.ANSI_RED);
+        }
+        threadSleep(2);
+    }
+
+    private String savePageSource(int depth, String content){
+        String path = AddressUtil.getPageSourcePath(traceId, udid, depth);
+        try {
+            FileUtil.saveFile(content, path);
+        }catch (IOException e) {
+            PrintUtil.printException(TAG, udid, e);
+            e.printStackTrace();
+        }
+        return path;
+    }
+    private void recordAction(Action action){
+        try {
+            actionWriter.write(new Gson().toJson(action) + "\n");
+            actionWriter.flush();
+        } catch (IOException e) {
+            PrintUtil.printException(TAG, udid, e);
+            e.printStackTrace();
+        }
+    }
+    private void threadSleep(int seconds){
+        try {
+            PrintUtil.print(String.format("Sleep %s seconds.", String.valueOf(seconds)), TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+            Thread.sleep(seconds * 1000);
+        } catch (InterruptedException e) {
+            PrintUtil.printException(TAG, udid, e);
+        }
+        try {
+            String script = String.format(" try {\nThread.sleep(%s);\n} catch (InterruptedException e) {\ne.printStackTrace();\n}\n", String.valueOf(seconds*1000));
+            scriptWriter.write(script);
+            scriptWriter.flush();
+        } catch (IOException e) {
+            PrintUtil.printException(TAG, udid, e);
+        }
+    }
+
+    private void printComponentList(List<Component> comList){
+        for(Component component: comList){
+            String father = "Null";
+            if(component.getFatherComponent() != null ){
+                father = component.getFatherComponent().getLocator();
+            }
+            String msg = String.format("%s, Tested: %s, Father: %s.", component.getLocator(), component.isHasBeenTested(), father);
+            PrintUtil.print(msg,  TAG, udid, myTestLogWriter, PrintUtil.ANSI_YELLOW);
+        }
+    }
+
+    private void takeScreenShotAndSavePageImg(){
+        int count = 0;
+        FileReader fr;
+        File newFile = new File(AddressUtil.getPageImgPath(traceId, udid, driver.currentActivity()));
+        while (count < 10){
+            try {
+                File screenShotFile = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE);
+                FileUtils.copyFile(screenShotFile, newFile);
+                fr = new FileReader(newFile);
+                if (fr.read() != -1){
+                    PrintUtil.print("Screenshot saved successful.", TAG, udid, myTestLogWriter, PrintUtil.ANSI_GREEN);
+                    return;
+                }
+            } catch (IOException e) {
+                PrintUtil.print("Screenshot error ", TAG, udid, myTestLogWriter, PrintUtil.ANSI_RED);
+                PrintUtil.printException(TAG, udid, e);
+                e.printStackTrace();
+            } finally {
+                count++;
+            }
+        }
+        PrintUtil.print("Screenshot saved failed.", TAG, udid, myTestLogWriter, PrintUtil.ANSI_RED);
+    }
+    private void savePageXml(){
+        String activityName = driver.currentActivity();
+        String xmlPath = AddressUtil.getPageXmlPath(traceId, udid, activityName);
+        String pageXml = driver.getPageSource();
+        FileOutputStream writerStream;
+        try {
+            writerStream = new FileOutputStream(xmlPath);
+            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(writerStream, UTF_8));
+            writer.write(pageXml);
+            writer.close();
+        } catch (IOException e) {
+            PrintUtil.printException(TAG, udid, e);
+            e.printStackTrace();
+        }
+    }
+    private void returnPrevActivity(){
+        String returnStartActivity = driver.currentActivity();
+        String currentActivity = returnStartActivity;
+        int returnCount = 0;
+        //按返回键直到activity发生改变
+        while(currentActivity.equals(returnStartActivity)) {
+            returnCount++;
+            if(returnCount >= 3 ) {
+                //按3次返回页面都没变化,就连按两次退出应用
+                // TODO 这里的逻辑需要处理下,因为点了三次不退出,也有可能
+                executeKeyEvent("Click Home button because has tried more than 3 times", AndroidKey.HOME);
+                break;
+            }
+            executeKeyEvent("Click Return button because this page has done", AndroidKey.BACK);
+            currentActivity = driver.currentActivity();
+            PrintUtil.print("Current activity is " + currentActivity, TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+            PrintUtil.print("Return start activity is" + returnStartActivity, TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+        }
+    }
+
+    private List<Component> parseAndSavePageSource(int depth){
+        String pageSource = driver.getPageSource();
+        String path = savePageSource(depth, pageSource);
+        DoXml parseXml = new DoXml();
+        return parseXml.run(path);
+    }
+
+    /**
+     * @description
+     * 	作用:更新当前页面控件集
+     *	被调用时刻:1、当前测试页面UI变化 2、从其他页面返回后发现当前页面变化 3、进入已测试过页面发现页面变化
+     *	更新机制:先把comList4Update中的所有控件放插入到newComList的最前面(但这些控件若在comList中出现过,那么hasBeenTested属性设置为comList中对应的,即该控件可能已被测试过),放在newComList的最前面是因为comList4Update中控件代表了实时的页面控件集,优先进行这些控件的测试;
+     *	接下来再把comList中其他的和comList4Update不相交的控件插入newComList
+     *	--采用这种机制的原因是为了避免一个Activity上会出现多个页面覆盖的问题(为了保留住该页面上所有出现过的控件的hasBeenTested属性)--
+     *	比如某Acticity上初始页面是一组控件 <a,b,c,d>,点击控件a后,页面上弹出了一个新的组件覆盖了此页面(Activity没变,<a,b,c,d>被完全覆盖住),这时候页面上可以获取到的控件为<e,f,g,h>,那么如果直接将此Activity控件集更新为<e,f,g,h>,那在测试完<e,f,g,h>,如果又回到之前的初始页面也就是<a,b,c,d>,理应继续测试控件b,但由于此时控件集里只有<e,f,g,h>的测试状态,所以会认为,控件a并没有被测试过,所以会继续测试控件a,导致程序陷入一个死循环
+     * @param comList 变化前页面控件集
+     * @param comList4Update 当前实时解析的页面控件集
+     * @return
+     */
+    private List<Component> setTestedComInList(List<Component> comList, List<Component> comList4Update){
+        List<Component> newComList = new ArrayList<Component>();
+        for(Component component : comList4Update){
+            if(comList.contains(component)){
+                int index = comList.indexOf(component);
+                component.setHasBeenTested(comList.get(index).isHasBeenTested());
+                if(comList.get(index).getFatherComponent() != null) {
+                    component.setFatherComponent(comList.get(index).getFatherComponent());
+                }
+            }
+            newComList.add(component);
+        }
+        for(Component component : comList){
+            if(!comList4Update.contains(component)) {
+                newComList.add(component);
+            }
+        }
+        return newComList;
+    }
+
+    private int activityIndexOfActivityList(String activityName){
+        for (int i=0;i<activityList.size();++i){
+            if (activityList.get(i).getName().equals(activityName)){
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private void loadConfiguration(){
+        this.ignoreMap = new HashMap<>(Consts.IGNORE_KEYWORD.length);
+        for (String keyword: Consts.IGNORE_KEYWORD){
+            ignoreMap.put(keyword, new IgnoreComponent(keyword));
+        }
+        readIgnoreComponent();
+        readFatherComponent();
+    }
+    //TODO 优化蠢逼代码
+    private void readIgnoreComponent(){
+        BufferedReader br = null;
+        try {
+            br = new BufferedReader(new FileReader(AddressUtil.getIgnoreConfigurationPath()));
+            /**
+             * component in ignore.conf file will be ignored.
+             * component identifier can only be id/content-desc/text, and use method equals/startsWith/contains
+             * must write in order equals, startsWith, contains
+             */
+        } catch (FileNotFoundException e) {
+            PrintUtil.printException(TAG, udid, e);
+            return;
+        }
+        String strLine = "";
+        try {
+            String mode = "unknown";
+            while((strLine = br.readLine()) != null){
+                if (strLine.isEmpty() || strLine.startsWith("//")){
+                    continue;
+                }
+                if ("*equals*".equals(strLine)){
+                    mode = "equals";
+                    continue;
+                }else if ("*startsWith*".equals(strLine)){
+                    mode = "startsWith";
+                    continue;
+                }else if ("*contains*".equals(strLine)){
+                    mode = "contains";
+                    continue;
+                }
+                String[] temp = strLine.split("#");
+                if (temp.length != 2){
+                    continue;
+                }
+                String modeKeyword = temp[0];
+                String componentSign = temp[1];
+                IgnoreComponent ic = ignoreMap.get(modeKeyword);
+                if (ic != null){
+                    ic.add(mode, componentSign);
+                }
+            }
+        } catch (IOException e) {
+            PrintUtil.printException(TAG, udid, e);
+        }
+    }
+    private void readFatherComponent(){
+        BufferedReader br = null;
+        try {
+            br = new BufferedReader(new FileReader(AddressUtil.getFatherComponentConfigurationPath()));
+        } catch (FileNotFoundException e) {
+            PrintUtil.printException(TAG, udid, e);
+            return;
+        }
+        String strLine = "";
+        try {
+            while((strLine = br.readLine()) != null){
+                if(strLine.contains("#")) {
+                    fatherComponentList.add(strLine);
+                }
+            }
+        } catch (IOException e) {
+            PrintUtil.printException(TAG, udid, e);
+        }
+    }
+
+    private boolean judgeIgnoreComponent(Component component){
+        boolean ignoreFlag = false;
+        String contentDesc = component.getContent_desc();
+        String resourceId = component.getResource_id();
+        String text = component.getText();
+        try {
+            ignoreFlag = ignoreMap.get("content_desc").check(contentDesc)
+                    || ignoreMap.get("id").check(resourceId)
+                    || ignoreMap.get("text").check(text);
+        } catch (Exception e) {
+            PrintUtil.printException(TAG, udid, e);
+        }
+        if(ignoreFlag) {
+            PrintUtil.print(component.getLocator() + " is a component in ignore file, skip to next one", TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+        }
+        return ignoreFlag;
+    }
+    private boolean judgeTestedComponent(Component component){
+        if(component.isHasBeenTested()){
+            PrintUtil.print(component.getLocator() + " has been tested, skip to next one", TAG, udid, myTestLogWriter, PrintUtil.ANSI_BLUE);
+            return true;
+        }
+        return false;
+    }
+
+    private boolean judgeFatherComponent(Component component, String currentActivity){
+        for(String fatherComponent : fatherComponentList){
+            String locator = fatherComponent.split("#")[0];
+            String activity = fatherComponent.split("#")[1];
+            if(locator.equals(component.getLocator()) && currentActivity.contains(activity)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void afterRun(){
+        try {
+            scriptWriter.close();
+            myTestLogWriter.close();
+            actionWriter.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+}

+ 57 - 0
src/main/java/net/mooctest/www/android_auto_test/Scripts/DemoScript.java

@@ -0,0 +1,57 @@
+package net.mooctest.www.android_auto_test.Scripts;
+
+import com.sinaapp.msdxblog.apkUtil.entity.ApkInfo;
+import io.appium.java_client.AppiumDriver;
+import net.mooctest.www.android_auto_test.models.Activity;
+import net.mooctest.www.android_auto_test.utils.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author henrylee
+ */
+public class DemoScript extends AbstractBaseScript {
+
+    public static final String TAG = Thread.currentThread() .getStackTrace()[1].getClassName();
+
+    private String udid;
+    private AppiumDriver driver;
+    private ApkInfo apkInfo;
+    private String traceId;
+
+    List<Activity> activityList = new ArrayList<>();
+    private String fatherActivity;
+
+    public DemoScript(String udid, AppiumDriver driver, ApkInfo apkInfo, String traceId) {
+        this.udid = udid;
+        this.driver = driver;
+        this.apkInfo = apkInfo;
+        this.traceId = traceId;
+    }
+
+    @Override
+    public void run(){
+        PrintUtil.print("Demo Monkey run run run!", TAG, udid);
+        DFS(0);
+    }
+
+    /**
+     * 深度优先遍历APP
+     * @param depth 遍历深度
+     */
+    private void DFS(int depth){
+        if (this.stopFlag){
+            return;
+        }
+        if (depth == 100){
+            return;
+        }
+        try {
+            Thread.sleep(10000);
+            DFS(depth + 1);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+}

+ 33 - 0
src/main/java/net/mooctest/www/android_auto_test/common/BeanFactory.java

@@ -0,0 +1,33 @@
+package net.mooctest.www.android_auto_test.common;
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+
+@Component
+public class BeanFactory implements ApplicationContextAware {
+    private static ApplicationContext applicationContext;
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        this.applicationContext = applicationContext;
+    }
+
+    public static ApplicationContext getApplicationContext(){
+        return applicationContext;
+    }
+
+    /**
+     * 获取某个Bean的对象
+     * @param clazz
+     */
+    public static Object getBean(Class<?> clazz) {
+        try {
+            return applicationContext.getBean(clazz);
+        } catch (Exception e) {
+
+        }
+        return null;
+    }
+}

+ 20 - 0
src/main/java/net/mooctest/www/android_auto_test/common/DeviceStatusRunner.java

@@ -0,0 +1,20 @@
+package net.mooctest.www.android_auto_test.common;
+
+import net.mooctest.www.android_auto_test.utils.DeviceDaemon;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author henrylee
+ */
+@Component
+@Order(value = 1)
+public class DeviceStatusRunner implements CommandLineRunner {
+
+    @Override
+    public void run(String... args) throws Exception {
+//        DeviceDaemon deviceDaemon = new DeviceDaemon();
+//        deviceDaemon.start();
+    }
+}

+ 52 - 0
src/main/java/net/mooctest/www/android_auto_test/common/FuckingTest.java

@@ -0,0 +1,52 @@
+package net.mooctest.www.android_auto_test.common;
+
+import com.aliyun.oss.OSSClient;
+import com.aliyun.oss.model.ListObjectsRequest;
+import com.aliyun.oss.model.OSSObjectSummary;
+import com.aliyun.oss.model.ObjectListing;
+import org.apache.poi.hssf.usermodel.HSSFRow;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+
+import java.io.FileOutputStream;
+
+public class FuckingTest {
+    public static void main(String args[]){
+        String accessKeyId = "IvS323TIcWUT57MG";
+        String endPoint = "http://oss-cn-hangzhou.aliyuncs.com";
+        String accessKeySecret = "dYml7rvT8stQkoSjMYlfRTxNj9dEsI";
+        String bucketName = "mooctest-share";
+        OSSClient client = new OSSClient(endPoint, accessKeyId, accessKeySecret);
+        ObjectListing list = client.listObjects(new ListObjectsRequest(bucketName)
+                .withPrefix("mooctest-accept-test-dataset/APP扫描")
+                .withMaxKeys(1000));
+        Workbook wb = new HSSFWorkbook();
+        Sheet sheet1 = wb.createSheet("APKS");
+        String path = "/Users/henrylee/Desktop/text.xls";
+        int index = 1;
+        for (int i=0;i<list.getObjectSummaries().size();++i) {
+            OSSObjectSummary ossObjectSummary = list.getObjectSummaries().get(i);
+            String name = ossObjectSummary.getKey();
+            if (!name.endsWith(".apk")){
+                continue;
+            }
+            String[] t = name.split("/");
+            String n = t[t.length-1];
+            HSSFRow row = (HSSFRow) sheet1.createRow(index);
+            row.createCell(0).setCellValue(index);
+            row.createCell(1).setCellValue(n);
+            row.createCell(4).setCellValue("http://mooctest-share.oss-cn-hangzhou.aliyuncs.com/" + name);
+            index++;
+        }
+        FileOutputStream outputStream = null;
+        try {
+            outputStream = new FileOutputStream(path);
+            wb.write(outputStream);
+            outputStream.flush();
+            outputStream.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+}

+ 35 - 0
src/main/java/net/mooctest/www/android_auto_test/common/constant/Consts.java

@@ -0,0 +1,35 @@
+package net.mooctest.www.android_auto_test.common.constant;
+
+/**
+ * @author henrylee
+ */
+public class Consts {
+    /**
+     * 线程name前缀
+     */
+    public static final String AUTO_TEST_THREAD_NAME_PREFIX = "COVERAGE_TEST_THREAD";
+
+    public static final String DAEMON_THREAD_NAME_PREFIX = "DAEMON_THREAD_";
+
+    public static final String[] ANDROID_ALERT_LABELS = {"确定", "确认", "允许", "始终允许", "同意", "继续", "同意并继续", "始终", "下一步", "取消", "继续使用", "android:id/button1"};
+
+    public static final String TRACE_DATA_PATH = "trace_data/";
+
+    /**
+     * 下载的重试次数
+     */
+    public static final int DOWNLOAD_MAX_ATTEMPTS = 10;
+
+    public static final int UPLOAD_OSS_MAX_ATTEMPTS = 5;
+
+    /**
+     * 监控线程的监控间隔,单位秒
+     */
+    public static final int TRACE_CHECK_TERMINAL = 10;
+
+    public static final int INIT_DRIVER_TIMES = 3;
+
+    public static final String[] IGNORE_KEYWORD = {"id", "text", "content_desc"};
+
+    public static final String REPORT_FILE_NAME = "reportData.json";
+}

+ 49 - 0
src/main/java/net/mooctest/www/android_auto_test/common/constant/enums/DeviceRunningStatus.java

@@ -0,0 +1,49 @@
+package net.mooctest.www.android_auto_test.common.constant.enums;
+
+import lombok.Getter;
+
+/**
+ * @author henrylee
+ */
+@Getter
+public enum DeviceRunningStatus {
+    /**
+     * 设备初始化状态,包括启动apppium和监控logcat线程
+     */
+    INIT(0, "初始化"),
+    /**
+     * 设备准备中,包括初始化driver,各种监控线程
+     */
+    PRAPARE(1, "准备中"),
+    /**
+     * 设备执行自动化测试状态
+     */
+    RUNNING(2, "运行中"),
+    /**
+     * 收尾工作,包括结束监控线程、还原设备状态、停止appium
+     */
+    ENDING(3, "结束中"),
+    /**
+     * 收尾工作结束
+     */
+    FINISH(4, "完成"),
+
+    PENDING(5, "设备离线,重试中");
+
+    private int code;
+    private String description;
+
+    DeviceRunningStatus(int code, String des){
+        this.code = code;
+        this.description = des;
+    }
+
+    public static DeviceRunningStatus getStatusByCode(int code){
+        for (DeviceRunningStatus ds:values()){
+            if (ds.code == code){
+                return ds;
+            }
+        }
+        return null;
+    }
+}

+ 32 - 0
src/main/java/net/mooctest/www/android_auto_test/common/constant/enums/DeviceStatus.java

@@ -0,0 +1,32 @@
+package net.mooctest.www.android_auto_test.common.constant.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum DeviceStatus {
+    /**
+     * 设备在线
+     */
+    ONLINE(1, "在线"),
+    /**
+     * 设备离线
+     */
+    OFFLINE(0, "离线");
+
+    private int code;
+    private String description;
+
+    private DeviceStatus(int code, String des){
+        this.code = code;
+        this.description = des;
+    }
+
+    public static DeviceStatus getStatusByCode(int code){
+        for (DeviceStatus ts: values()){
+            if (ts.code == code){
+                return ts;
+            }
+        }
+        return null;
+    }
+}

+ 53 - 0
src/main/java/net/mooctest/www/android_auto_test/common/constant/enums/TraceStatus.java

@@ -0,0 +1,53 @@
+package net.mooctest.www.android_auto_test.common.constant.enums;
+
+import lombok.Getter;
+
+/**
+ * @author henrylee
+ */
+@Getter
+public enum TraceStatus {
+    /**
+     * 初始化,包括下载apk
+     */
+    INIT(0, "初始化"),
+    /**
+     * 执行中,至少有一台device的状态不是Finish {@link DeviceRunningStatus#FINISH}
+     */
+    RUNNING(1, "执行中"),
+    /**
+     * 生成报告中
+     */
+    GEN_REPORT(2, "报告生成中"),
+    /**
+     * 报告生成结束
+     */
+    FINISH(3, "完成"),
+
+    /**
+     * APK下载失败
+     */
+    DOWNLOAD_FAILED(4, "下载失败"),
+
+    /**
+     * APK包解析失败
+     */
+    BAGGING_FAILED(5, "APK解包失败");
+
+    private int code;
+    private String description;
+
+    private TraceStatus(int code, String des){
+        this.code = code;
+        this.description = des;
+    }
+
+    public static TraceStatus getStatusByCode(int code){
+        for (TraceStatus ts: values()){
+            if (ts.code == code){
+                return ts;
+            }
+        }
+        return null;
+    }
+}

+ 10 - 0
src/main/java/net/mooctest/www/android_auto_test/common/exceptions/ApkDownloadException.java

@@ -0,0 +1,10 @@
+package net.mooctest.www.android_auto_test.common.exceptions;
+
+/**
+ * @author henrylee
+ */
+public class ApkDownloadException extends RuntimeException {
+    public ApkDownloadException(String msg){
+        super(msg);
+    }
+}

+ 10 - 0
src/main/java/net/mooctest/www/android_auto_test/common/exceptions/ApkParseException.java

@@ -0,0 +1,10 @@
+package net.mooctest.www.android_auto_test.common.exceptions;
+
+/**
+ * @author henrylee
+ */
+public class ApkParseException extends RuntimeException{
+    public ApkParseException(String msg){
+        super(msg);
+    }
+}

+ 10 - 0
src/main/java/net/mooctest/www/android_auto_test/common/exceptions/AppiumDriverInitException.java

@@ -0,0 +1,10 @@
+package net.mooctest.www.android_auto_test.common.exceptions;
+
+/**
+ * @author henrylee
+ */
+public class AppiumDriverInitException extends RuntimeException {
+    public AppiumDriverInitException(String message){
+        super(message);
+    }
+}

+ 14 - 0
src/main/java/net/mooctest/www/android_auto_test/common/exceptions/HttpNotFoundException.java

@@ -0,0 +1,14 @@
+package net.mooctest.www.android_auto_test.common.exceptions;
+
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.http.HttpStatus;
+
+/**
+ * @author henrylee
+ */
+@ResponseStatus(code = HttpStatus.NOT_FOUND)
+public class HttpNotFoundException extends RuntimeException{
+    public HttpNotFoundException(String message){
+        super(message);
+    }
+}

+ 11 - 0
src/main/java/net/mooctest/www/android_auto_test/common/exceptions/NoFreeDeviceException.java

@@ -0,0 +1,11 @@
+package net.mooctest.www.android_auto_test.common.exceptions;
+
+
+/**
+ * @author henrylee
+ */
+public class NoFreeDeviceException extends RuntimeException{
+    public NoFreeDeviceException(String msg){
+        super(msg);
+    }
+}

+ 10 - 0
src/main/java/net/mooctest/www/android_auto_test/common/exceptions/TraceTimeoutException.java

@@ -0,0 +1,10 @@
+package net.mooctest.www.android_auto_test.common.exceptions;
+
+/**
+ * @author henrylee
+ */
+public class TraceTimeoutException extends RuntimeException {
+    public TraceTimeoutException(){
+        super();
+    }
+}

+ 33 - 0
src/main/java/net/mooctest/www/android_auto_test/controller/AutoTestController.java

@@ -0,0 +1,33 @@
+package net.mooctest.www.android_auto_test.controller;
+
+import net.mooctest.www.android_auto_test.vo.TraceMetaInfo;
+import net.mooctest.www.android_auto_test.vo.TraceStatusResult;
+import net.mooctest.www.android_auto_test.services.AutoTestService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+
+/**
+ * @author henrylee
+ */
+@RestController
+public class AutoTestController {
+
+    @Autowired
+    AutoTestService autoTestService;
+
+    @RequestMapping(value = "/api/v1/runTest", method = RequestMethod.POST)
+    public TraceStatusResult runTest(@RequestBody TraceMetaInfo traceInfo){
+        return autoTestService.executeAutoTestTask(traceInfo);
+    }
+
+    @RequestMapping(value = "/api/v1/result", method = RequestMethod.GET)
+    public TraceStatusResult getTaskResult(@RequestParam(name = "traceId") String traceId){
+        return autoTestService.getResult(traceId);
+    }
+
+    @RequestMapping(value = "/api/v1/result", method = RequestMethod.DELETE)
+    public boolean stopTraceTasks(@RequestParam(name = "traceId") String traceId){
+        return autoTestService.stopTrace(traceId);
+    }
+}

+ 30 - 0
src/main/java/net/mooctest/www/android_auto_test/dao/RedisMappers/DeviceRunningStatusMap.java

@@ -0,0 +1,30 @@
+package net.mooctest.www.android_auto_test.dao.RedisMappers;
+
+import net.mooctest.www.android_auto_test.common.constant.enums.DeviceRunningStatus;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * @author henrylee
+ */
+@Component
+public class DeviceRunningStatusMap {
+    private final String DEVICE_RUNNING_STATUS_PREFIX = "DEVICE_RUNNING_STATUS_";
+
+    @Resource(name = "redisTemplate")
+    RedisTemplate<Object, Object> redisTemplate;
+
+    public DeviceRunningStatus get(String udid, String traceId){
+        redisTemplate.setKeySerializer(new StringRedisSerializer());
+        return (DeviceRunningStatus) redisTemplate.opsForValue().get(DEVICE_RUNNING_STATUS_PREFIX + traceId + "_" + udid);
+    }
+
+    public void put(String udid, String traceId, DeviceRunningStatus status){
+        redisTemplate.setKeySerializer(new StringRedisSerializer());
+        redisTemplate.opsForValue().set(DEVICE_RUNNING_STATUS_PREFIX + traceId + "_" + udid, status);
+    }
+
+}

+ 32 - 0
src/main/java/net/mooctest/www/android_auto_test/dao/RedisMappers/Trace2DeviceMap.java

@@ -0,0 +1,32 @@
+package net.mooctest.www.android_auto_test.dao.RedisMappers;
+
+import com.alibaba.fastjson.JSON;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+/**
+ * @author henrylee
+ */
+@Component
+public class Trace2DeviceMap {
+    private final String TRACE_DEVICE_PREFIX = "TRACE_DEVICES_";
+
+    @Resource(name="redisTemplate")
+    RedisTemplate<Object, Object> redisTemplate;
+
+    public List<String> get(String traceId){
+        redisTemplate.setKeySerializer(new StringRedisSerializer());
+        String re = (String) redisTemplate.opsForValue().get(TRACE_DEVICE_PREFIX + traceId);
+        return JSON.parseArray(re, String.class);
+    }
+
+    public void put(String traceId, List<String> udids){
+        redisTemplate.setKeySerializer(new StringRedisSerializer());
+        String list = JSON.toJSONString(udids);
+        redisTemplate.opsForValue().set(TRACE_DEVICE_PREFIX + traceId, list);
+    }
+}

+ 30 - 0
src/main/java/net/mooctest/www/android_auto_test/dao/RedisMappers/TraceStatusMap.java

@@ -0,0 +1,30 @@
+package net.mooctest.www.android_auto_test.dao.RedisMappers;
+
+
+import net.mooctest.www.android_auto_test.common.constant.enums.TraceStatus;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * @author henrylee
+ */
+@Component
+public class TraceStatusMap {
+    private final String TRACE_STATUS_PREFIX = "TRACE_STATUS_";
+
+    @Resource(name = "redisTemplate")
+    RedisTemplate<Object, Object> redisTemplate;
+
+    public TraceStatus get(String traceId){
+        redisTemplate.setKeySerializer(new StringRedisSerializer());
+        return (TraceStatus) redisTemplate.opsForValue().get(TRACE_STATUS_PREFIX + traceId);
+    }
+
+    public void put(String traceId, TraceStatus status){
+        redisTemplate.setKeySerializer(new StringRedisSerializer());
+        redisTemplate.opsForValue().set(TRACE_STATUS_PREFIX + traceId, status);
+    }
+}

+ 15 - 0
src/main/java/net/mooctest/www/android_auto_test/models/Action.java

@@ -0,0 +1,15 @@
+package net.mooctest.www.android_auto_test.models;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class Action {
+    private String timeBeforeAction;
+    private String timeAfterAction;
+    private String type;
+    private String message;
+    private String activityBeforeAction;
+    private String activityAfterAction;
+}

+ 44 - 0
src/main/java/net/mooctest/www/android_auto_test/models/Activity.java

@@ -0,0 +1,44 @@
+package net.mooctest.www.android_auto_test.models;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author henrylee
+ */
+@Data
+public class Activity {
+    private String name;
+    private String fatherActivity;
+    private int hash;
+    private List<Component> comList;
+    private boolean isDone;
+
+    public Activity(String name, String fatherActivity, int hash, List<Component> components){
+        this.name = name;
+        this.fatherActivity = fatherActivity;
+        this.hash = hash;
+        this.comList = components;
+    }
+
+    @Override
+    public int hashCode(){
+        return this.name.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object ojb){
+        if (ojb == null){
+            return false;
+        }
+        if (this == ojb){
+            return true;
+        }
+        if (ojb instanceof Activity){
+            return this.name.equals(((Activity)ojb).getName());
+        }
+        return false;
+    }
+}

+ 45 - 0
src/main/java/net/mooctest/www/android_auto_test/models/Component.java

@@ -0,0 +1,45 @@
+package net.mooctest.www.android_auto_test.models;
+
+import lombok.Data;
+
+@Data
+public class Component {
+    private String locator;
+    private String index;
+    private String text;
+    private String resource_id;
+    private String classname;
+    private String packagename;
+    private String content_desc;
+    private String checkable;
+    private String checked;
+    private String clickable;
+    private String enabled;
+    private String focusable;
+    private String focused;
+    private String scrollable;
+    private String long_clickable;
+    private String password;
+    private String selected;
+    private String bounds;
+    private boolean hasBeenTested = false;
+    private Component fatherComponent;
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof Component)) {
+            return false;
+        }
+        Component com = (Component)obj;
+        return (com.locator.equals(this.locator) && com.classname.equals(this.classname) && com.packagename.equals(this.packagename));
+    }
+
+    @Override
+    public int hashCode() {
+        int result = bounds.hashCode();
+        return result;
+    }
+}

+ 16 - 0
src/main/java/net/mooctest/www/android_auto_test/models/Device.java

@@ -0,0 +1,16 @@
+package net.mooctest.www.android_auto_test.models;
+
+import lombok.Data;
+import net.mooctest.www.android_auto_test.utils.OsUtil;
+
+import java.io.Serializable;
+
+/**
+ * @author henrylee
+ */
+@Data
+public class Device{
+    private String id;
+    private String pcId;
+    private String udid;
+}

+ 53 - 0
src/main/java/net/mooctest/www/android_auto_test/models/IgnoreComponent.java

@@ -0,0 +1,53 @@
+package net.mooctest.www.android_auto_test.models;
+
+import lombok.Data;
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class IgnoreComponent {
+    private String keyword;
+    private List<String> equalList;
+    private List<String> containList;
+    private List<String> startList;
+
+    public IgnoreComponent(String keyword){
+        this.keyword = keyword;
+        equalList = new ArrayList<>();
+        containList = new ArrayList<>();
+        startList = new ArrayList<>();
+    }
+    public void add(String mode, String componentSign){
+        if ("unknown".equals(mode)){
+            return;
+        }
+        if("equals".equals(mode)){
+            equalList.add(componentSign);
+        } else if("contains".equals(mode)){
+            containList.add(componentSign);
+        } else if("startsWith".equals(mode)){
+            startList.equals(componentSign);
+        }
+    }
+    public boolean check(String content){
+        if (content == null){
+            return false;
+        }
+        for (String s: equalList){
+            if (content.equals(s)){
+                return true;
+            }
+        }
+        for (String s: containList){
+            if (content.contains(s)){
+                return true;
+            }
+        }
+        for (String s: startList){
+            if (content.startsWith(s)){
+                return true;
+            }
+        }
+        return false;
+    }
+}

+ 32 - 0
src/main/java/net/mooctest/www/android_auto_test/services/ApkService.java

@@ -0,0 +1,32 @@
+package net.mooctest.www.android_auto_test.services;
+
+import com.sinaapp.msdxblog.apkUtil.entity.ApkInfo;
+
+/**
+ * @author henrylee
+ */
+public interface ApkService {
+
+    /**
+     * @param apkPath apk文件路径
+     * @return 解析出的ApkInfo对象
+     */
+    ApkInfo parseAPK(String apkPath, String traceId);
+
+    /**
+     * @param downloadUrl apk下载地址
+     * @param traceId traceId
+     * @return
+     */
+    String downloadApk(String downloadUrl, String traceId);
+
+    /**
+     * @param traceId traceId
+     */
+    void updateTraceStartTime(String traceId);
+
+    /**
+     * @param traceId traceId
+     */
+    void updateTraceEndTime(String traceId);
+}

+ 30 - 0
src/main/java/net/mooctest/www/android_auto_test/services/AutoTestService.java

@@ -0,0 +1,30 @@
+package net.mooctest.www.android_auto_test.services;
+
+import net.mooctest.www.android_auto_test.vo.TraceMetaInfo;
+import net.mooctest.www.android_auto_test.vo.TraceStatusResult;
+
+/**
+ * @author henrylee
+ */
+public interface AutoTestService {
+    /**
+     * 执行traceInfo所描述的自动化任务
+     * @param traceInfo 单次任务trace的信息
+     * @return trace启动的信息
+     */
+    TraceStatusResult executeAutoTestTask(TraceMetaInfo traceInfo);
+
+    /**
+     * 获取某次trace的当前状态
+     * @param traceId 需要查询的traceId
+     * @return trace的状态信息
+     */
+    TraceStatusResult getResult(String traceId);
+
+    /**
+     * 停止某次traceId的所有任务,异步
+     * @param traceId 需要停止的traceId
+     * @return 成功返回true,失败抛出异常
+     */
+    boolean stopTrace(String traceId);
+}

+ 28 - 0
src/main/java/net/mooctest/www/android_auto_test/services/DeviceService.java

@@ -0,0 +1,28 @@
+package net.mooctest.www.android_auto_test.services;
+
+import com.sinaapp.msdxblog.apkUtil.entity.ApkInfo;
+import net.mooctest.www.android_auto_test.common.constant.enums.DeviceRunningStatus;
+import net.mooctest.www.android_auto_test.models.Device;
+
+import java.util.List;
+
+/**
+ * @author henrylee
+ */
+public interface DeviceService {
+    /**
+     * @return 目前adb在线的设备列表
+     */
+    List<Device> getOnlineDeviceList();
+
+    /**
+     * @param apkInfo Apk信息
+     * @param devices 备选设备
+     * @return 根据某些算法,选择合适的设备
+     */
+    List<Device> selectDeviceByApp(ApkInfo apkInfo, List<Device> devices);
+
+    void updateDeviceRunningStatus(String traceId, String udid, DeviceRunningStatus deviceRunningStatus);
+
+    DeviceRunningStatus getDeviceRunningStatus(String traceId, String udid);
+}

+ 148 - 0
src/main/java/net/mooctest/www/android_auto_test/services/Impl/ApkServiceImpl.java

@@ -0,0 +1,148 @@
+package net.mooctest.www.android_auto_test.services.Impl;
+
+import com.sinaapp.msdxblog.apkUtil.entity.ApkInfo;
+import com.sinaapp.msdxblog.apkUtil.utils.ApkUtil;
+import net.mooctest.www.android_auto_test.common.constant.Consts;
+import net.mooctest.www.android_auto_test.common.exceptions.ApkDownloadException;
+import net.mooctest.www.android_auto_test.common.exceptions.ApkParseException;
+import net.mooctest.www.android_auto_test.services.ApkService;
+import net.mooctest.www.android_auto_test.services.OssService;
+import net.mooctest.www.android_auto_test.utils.AddressUtil;
+import net.mooctest.www.android_auto_test.utils.FileUtil;
+import net.mooctest.www.android_auto_test.utils.PrintUtil;
+import org.apache.commons.io.FileUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.*;
+import java.net.URL;
+import java.text.SimpleDateFormat;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * @author henrylee
+ */
+@Service
+public class ApkServiceImpl implements ApkService {
+    public static final String TAG = Thread.currentThread() .getStackTrace()[1].getClassName();
+
+    @Autowired
+    OssService ossService;
+
+    @Override
+    public ApkInfo parseAPK(String apkPath, String traceId) {
+        ApkInfo apkInfo = null;
+        try {
+            apkInfo = new ApkUtil().getApkInfo(apkPath);
+            saveApkInfo(apkInfo, apkPath, traceId);
+        } catch (Exception e) {
+            throw new ApkParseException("APK 解析失败");
+        }
+        return apkInfo;
+    }
+
+    @Override
+    public String downloadApk(String downloadUrl, String traceId) {
+        if (!downloadUrl.endsWith(".apk")){
+            throw new ApkDownloadException("Download APK failed because this is not an apk");
+        }
+        String[] temp = downloadUrl.split("/");
+        String fileName = temp[temp.length-1];
+        String path = AddressUtil.getApkDownloadPath(traceId, fileName);
+        int downloadTimes = 0;
+        while (downloadTimes < Consts.DOWNLOAD_MAX_ATTEMPTS) {
+            try {
+                FileUtils.copyURLToFile(new URL(downloadUrl), new File(path));
+                return path;
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            // 下载失败,10s后重试
+            PrintUtil.print(String.format("Download %s failed. Retry after 10 seconds", fileName), TAG);
+            try {
+                Thread.sleep(10000);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            downloadTimes ++;
+        }
+        throw new ApkDownloadException(String.format("Download APK failed after tried %s times.", Consts.DOWNLOAD_MAX_ATTEMPTS));
+    }
+
+    @Override
+    public void updateTraceStartTime(String traceId) {
+        updateApkInfoTime(traceId, "startTime");
+    }
+
+    @Override
+    public void updateTraceEndTime(String traceId) {
+        updateApkInfoTime(traceId, "endTime");
+    }
+
+    private void updateApkInfoTime(String traceId, String key){
+        File infoFile = new File(AddressUtil.getApkInfoPath(traceId));
+        Long now = System.currentTimeMillis();
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
+        String nowStr = sdf.format(now);
+        try {
+            BufferedWriter writer = new BufferedWriter(new FileWriter(infoFile, true));
+            writer.write(key + "=" + nowStr + "\n");
+            writer.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void saveApkInfo(ApkInfo apkInfo, String apkPath, String traceId){
+        String label = apkInfo.getApplicationLable();
+        String sdkVersion = apkInfo.getSdkVersion();
+        String targetSdkVersion = apkInfo.getTargetSdkVersion();
+        String versionName = apkInfo.getVersionName();
+        String iconPath = apkInfo.getApplicationIcon();
+        String minSdkVersion = apkInfo.getMinSdkVersion();
+        String iconUrl = saveIcon(apkPath, iconPath, traceId);
+        String packageName = apkInfo.getPackageName();
+        File infoFile = new File(AddressUtil.getApkInfoPath(traceId));
+        FileUtil.newFile(infoFile);
+        try {
+            BufferedWriter writer = new BufferedWriter(new FileWriter(infoFile, true));
+            writer.write("label=" + label + "\n");
+            writer.write("sdkVersion=" + sdkVersion + "\n");
+            writer.write("targetSdkVersion=" + targetSdkVersion + "\n");
+            writer.write("versionName=" + versionName + "\n");
+            writer.write("minSdkVersion=" + minSdkVersion + "\n");
+            writer.write("iconUrl=" + iconUrl + "\n");
+            writer.write("packageName=" + packageName + "\n");
+            writer.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private String saveIcon(String apkPath, String iconPath, String traceId){
+        try {
+            ZipFile zFile = null;
+            zFile = new ZipFile(apkPath);
+            ZipEntry entry = zFile.getEntry(iconPath);
+            entry.getComment();
+            entry.getCompressedSize();
+            entry.getCrc();
+            entry.isDirectory();
+            entry.getSize();
+            entry.getMethod();
+            InputStream stream = zFile.getInputStream(entry);
+            String[] temps = iconPath.split("\\.");
+            String extName = "png";
+            try {
+                extName = temps[temps.length-1];
+            } catch (Exception e){
+                e.printStackTrace();
+            }
+            return ossService.uploadInputStreamToTraceDir(stream, traceId, "icon." + extName);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return "";
+    }
+}

+ 148 - 0
src/main/java/net/mooctest/www/android_auto_test/services/Impl/AutoTestServiceImpl.java

@@ -0,0 +1,148 @@
+package net.mooctest.www.android_auto_test.services.Impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.sinaapp.msdxblog.apkUtil.entity.ApkInfo;
+import com.sinaapp.msdxblog.apkUtil.utils.ApkUtil;
+import net.mooctest.www.android_auto_test.common.constant.Consts;
+import net.mooctest.www.android_auto_test.common.constant.enums.TraceStatus;
+import net.mooctest.www.android_auto_test.common.exceptions.ApkDownloadException;
+import net.mooctest.www.android_auto_test.common.exceptions.ApkParseException;
+import net.mooctest.www.android_auto_test.common.exceptions.HttpNotFoundException;
+import net.mooctest.www.android_auto_test.common.exceptions.NoFreeDeviceException;
+import net.mooctest.www.android_auto_test.models.Device;
+import net.mooctest.www.android_auto_test.services.*;
+import net.mooctest.www.android_auto_test.utils.PrintUtil;
+import net.mooctest.www.android_auto_test.vo.DeviceStatusResult;
+import net.mooctest.www.android_auto_test.vo.TraceMetaInfo;
+import net.mooctest.www.android_auto_test.vo.TraceStatusResult;
+import net.mooctest.www.android_auto_test.utils.CoverageTest;
+import net.mooctest.www.android_auto_test.utils.TraceDaemon;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author henrylee
+ */
+@Service
+public class AutoTestServiceImpl implements AutoTestService {
+
+    public static final String TAG = Thread.currentThread() .getStackTrace()[1].getClassName();
+
+    @Autowired
+    DeviceService deviceService;
+
+    @Autowired
+    TraceService traceService;
+
+    @Autowired
+    OssService ossService;
+
+    @Autowired
+    ApkService apkService;
+
+    @Override
+    public TraceStatusResult executeAutoTestTask(TraceMetaInfo traceInfo) {
+        String traceId = traceInfo.getTraceId();
+        RunTraceThread run = new RunTraceThread(traceInfo);
+        run.start();
+        TraceStatusResult traceResult = new TraceStatusResult();
+        traceResult.setStatusCode(TraceStatus.INIT.getCode());
+        traceResult.setDescription(TraceStatus.INIT.getDescription());
+        traceResult.setTraceId(traceId);
+        return traceResult;
+    }
+
+    private class RunTraceThread extends Thread{
+        TraceMetaInfo traceInfo;
+        RunTraceThread(TraceMetaInfo traceInfo){
+            this.traceInfo = traceInfo;
+        }
+
+        @Override
+        public void run(){
+            String traceId = traceInfo.getTraceId();
+            String downloadUrl = traceInfo.getDownloadUrl();
+            int timeout = traceInfo.getLimitTime();
+            PrintUtil.print("Start Automatic Test, TraceId: " + traceId, TAG);
+            traceService.updateTraceStatue(traceId, TraceStatus.INIT);
+            String filePath;
+            ApkInfo apkInfo;
+            try {
+                PrintUtil.print("Start download apkFile, TraceId: " + traceId, TAG);
+                filePath = apkService.downloadApk(downloadUrl, traceId);
+                PrintUtil.print("Start parse apkFile, TraceId: " + traceId, TAG);
+                apkInfo = apkService.parseAPK(filePath, traceId);
+            } catch (ApkDownloadException e){
+                PrintUtil.print(String.format("Download failed, trace %s end.", traceId), TAG);
+                traceService.updateTraceStatue(traceId, TraceStatus.DOWNLOAD_FAILED);
+                return;
+            } catch (ApkParseException e){
+                PrintUtil.print(String.format("Parse failed, trace %s end.", traceId), TAG);
+                traceService.updateTraceStatue(traceId, TraceStatus.BAGGING_FAILED);
+                return;
+            }
+            // 获取目前在线设备
+            List<Device> deviceList = deviceService.getOnlineDeviceList();
+            // 选择设备,后期可以修改为如果前端指定了设备or条件,则根据指定的来;目前是选择所有在线的
+            List<Device> selectedDevices = deviceService.selectDeviceByApp(apkInfo, deviceList);
+            if (selectedDevices == null || selectedDevices.size() == 0){
+                throw new NoFreeDeviceException("没有空闲设备");
+            }
+            traceService.setTraceDevices(traceId, selectedDevices);
+            traceService.updateTraceStatue(traceId, TraceStatus.RUNNING);
+            apkService.updateTraceStartTime(traceId);
+            List<CoverageTest> oneTraceTasks = new ArrayList<>(selectedDevices.size());
+            for (Device device: selectedDevices){
+                CoverageTest coverageTest = new CoverageTest(apkInfo, filePath, device, traceId);
+                coverageTest.setName(Consts.AUTO_TEST_THREAD_NAME_PREFIX + device.getUdid());
+                coverageTest.start();
+                oneTraceTasks.add(coverageTest);
+            }
+            //启动监控线程监控该trace中所有执行线程
+            TraceDaemon traceDaemon = new TraceDaemon(traceId,timeout, oneTraceTasks);
+            traceDaemon.setName(Consts.DAEMON_THREAD_NAME_PREFIX + traceId);
+            traceDaemon.start();
+        }
+    }
+
+    @Override
+    public TraceStatusResult getResult(String traceId){
+        TraceStatus currentStatus = traceService.getTraceStatue(traceId);
+        if (currentStatus == null){
+            throw new HttpNotFoundException(String.format("TraceId [%s] 不存在", traceId));
+        }
+        TraceStatusResult traceResult = new TraceStatusResult();
+        traceResult.setStatusCode(currentStatus.getCode());
+        traceResult.setDescription(currentStatus.getDescription());
+        traceResult.setTraceId(traceId);
+        List<DeviceStatusResult> deviceStatus = traceService.getTraceDeviceStatus(traceId);
+        JSONObject extraInfo = JSON.parseObject("{}");
+        extraInfo.put("deviceStatus", deviceStatus);
+        traceResult.setExtraInfo(extraInfo);
+        if (TraceStatus.FINISH.equals(currentStatus)){
+            String downloadUrl = ossService.getDadaJsonDownloadPath(traceId);
+            traceResult.setDownloadUrl(downloadUrl);
+        }
+        return traceResult;
+    }
+
+    @Override
+    public boolean stopTrace(String traceId){
+        ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
+        int count = threadGroup.activeCount();
+        Thread[] runningThreads = new Thread[count];
+        threadGroup.enumerate(runningThreads);
+        String targetName = Consts.DAEMON_THREAD_NAME_PREFIX + traceId;
+        for (Thread t: runningThreads){
+            if (t.getName().equals(targetName)){
+                ((TraceDaemon)t).stopImmediately();
+                return true;
+            }
+        }
+        throw new HttpNotFoundException(String.format("TraceId [%s] 不存在", traceId));
+    }
+}

+ 63 - 0
src/main/java/net/mooctest/www/android_auto_test/services/Impl/DeviceServiceImpl.java

@@ -0,0 +1,63 @@
+package net.mooctest.www.android_auto_test.services.Impl;
+
+import com.sinaapp.msdxblog.apkUtil.entity.ApkInfo;
+import net.mooctest.www.android_auto_test.common.constant.enums.DeviceRunningStatus;
+import net.mooctest.www.android_auto_test.dao.RedisMappers.DeviceRunningStatusMap;
+import net.mooctest.www.android_auto_test.models.Device;
+import net.mooctest.www.android_auto_test.services.DeviceService;
+import net.mooctest.www.android_auto_test.utils.OsUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author henrylee
+ */
+@Service
+public class DeviceServiceImpl implements DeviceService {
+
+    @Autowired
+    DeviceRunningStatusMap deviceRunningStatusMap;
+
+    @Override
+    public List<Device> getOnlineDeviceList() {
+        String command = "adb devices";
+        String comResult = OsUtil.runCommand(command);
+        String[] comResultLine = comResult.split("\n");
+        List<Device> devices = new ArrayList<>(comResultLine.length);
+        for (String line:comResultLine){
+            line = line.trim();
+            if ("".equals(line) || line.length()==0 || line.startsWith("List")){
+                continue;
+            }
+            String[] temp = line.split("\t");
+            String deviceName = temp[0];
+            String deviceStatus = temp[1];
+            if (!deviceStatus.equals("device")){
+                continue;
+            }
+            Device device = new Device();
+            device.setId(deviceName);
+            device.setUdid(deviceName);
+            devices.add(device);
+        }
+        return devices;
+    }
+
+    @Override
+    public List<Device> selectDeviceByApp(ApkInfo apkInfo, List<Device> devices) {
+        return devices;
+    }
+
+    @Override
+    public void updateDeviceRunningStatus(String traceId, String udid, DeviceRunningStatus deviceRunningStatus) {
+        deviceRunningStatusMap.put(udid, traceId, deviceRunningStatus);
+    }
+
+    @Override
+    public DeviceRunningStatus getDeviceRunningStatus(String traceId, String udid) {
+        return deviceRunningStatusMap.get(udid, traceId);
+    }
+}

+ 85 - 0
src/main/java/net/mooctest/www/android_auto_test/services/Impl/OssServiceImpl.java

@@ -0,0 +1,85 @@
+package net.mooctest.www.android_auto_test.services.Impl;
+
+import com.aliyun.oss.OSSClient;
+import com.aliyun.oss.model.GetObjectRequest;
+import net.mooctest.www.android_auto_test.common.constant.Consts;
+import net.mooctest.www.android_auto_test.services.OssService;
+import net.mooctest.www.android_auto_test.utils.AddressUtil;
+import net.mooctest.www.android_auto_test.utils.FileUtil;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.io.*;
+import java.net.URL;
+
+/**
+ * @author henrylee
+ */
+@Service
+public class OssServiceImpl implements OssService {
+
+    @Value("${oss.endPoint}")
+    private String endPoint;
+    @Value("${oss.accessKeyId}")
+    private String accessKeyId;
+    @Value("${oss.accessKeySecret}")
+    private String accessKeySecret;
+    @Value("${oss.bucketName}")
+    private String bucketName;
+
+    @Override
+    public String downloadFile(String downloadUrl, String traceId) {
+        try {
+            String[] temp = downloadUrl.split("/");
+            String fileName = temp[temp.length-1];
+            File traceDir = new File(AddressUtil.getTraceDir(traceId));
+            if (!traceDir.exists()){
+                traceDir.mkdirs();
+            }
+            File f = new File(traceDir, fileName);
+            OSSClient client = new OSSClient(endPoint, accessKeyId, accessKeySecret);
+            client.getObject(new GetObjectRequest(new URL(downloadUrl), null), f);
+            return AddressUtil.getApkDownloadPath(traceId, fileName);
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    @Override
+    public String getDadaJsonDownloadPath(String traceId) {
+        String filePath = Consts.TRACE_DATA_PATH + traceId + "/" + Consts.REPORT_FILE_NAME;
+        return buildHttpPath(filePath);
+    }
+
+    @Override
+    public String uploadFileToTraceDir(File file, String traceId, String fileName) {
+        try {
+            String filePath = Consts.TRACE_DATA_PATH + traceId + "/" + fileName;
+            InputStream is = new FileInputStream(file);
+            OSSClient client = new OSSClient(endPoint, accessKeyId, accessKeySecret);
+            client.putObject(bucketName, filePath , is);
+            client.shutdown();
+            return buildHttpPath(filePath);
+        } catch (FileNotFoundException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    @Override
+    public String uploadInputStreamToTraceDir(InputStream is, String traceId, String fileName){
+            String filePath = Consts.TRACE_DATA_PATH + traceId + "/" + fileName;
+            OSSClient client = new OSSClient(endPoint, accessKeyId, accessKeySecret);
+            client.putObject(bucketName, filePath , is);
+            client.shutdown();
+            return buildHttpPath(filePath);
+    }
+
+    private String buildHttpPath(String filePath) {
+        StringBuilder sb = new StringBuilder("http://");
+        String noHttpEndPoint = endPoint.replace("http://","");
+        sb.append(bucketName).append(".").append(noHttpEndPoint).append("/").append(filePath);
+        return sb.toString();
+    }
+}

+ 63 - 0
src/main/java/net/mooctest/www/android_auto_test/services/Impl/TraceServiceImpl.java

@@ -0,0 +1,63 @@
+package net.mooctest.www.android_auto_test.services.Impl;
+
+import net.mooctest.www.android_auto_test.common.constant.enums.DeviceRunningStatus;
+import net.mooctest.www.android_auto_test.dao.RedisMappers.DeviceRunningStatusMap;
+import net.mooctest.www.android_auto_test.dao.RedisMappers.Trace2DeviceMap;
+import net.mooctest.www.android_auto_test.dao.RedisMappers.TraceStatusMap;
+import net.mooctest.www.android_auto_test.common.constant.enums.TraceStatus;
+import net.mooctest.www.android_auto_test.models.Device;
+import net.mooctest.www.android_auto_test.services.TraceService;
+import net.mooctest.www.android_auto_test.vo.DeviceStatusResult;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class TraceServiceImpl implements TraceService {
+
+    @Autowired
+    TraceStatusMap traceStatusMap;
+
+    @Autowired
+    Trace2DeviceMap trace2DeviceMap;
+
+    @Autowired
+    DeviceRunningStatusMap deviceRunningStatusMap;
+
+
+    @Override
+    public void updateTraceStatue(String traceId, TraceStatus traceStatus) {
+        traceStatusMap.put(traceId, traceStatus);
+    }
+
+    @Override
+    public TraceStatus getTraceStatue(String traceId) {
+        return traceStatusMap.get(traceId);
+    }
+
+    @Override
+    public void setTraceDevices(String traceId, List<Device> deviceList) {
+        trace2DeviceMap.put(traceId, deviceList.stream().map(Device::getUdid).collect(Collectors.toList()));
+    }
+
+    @Override
+    public List<DeviceStatusResult> getTraceDeviceStatus(String traceId) {
+        List<String> udids = trace2DeviceMap.get(traceId);
+        List<DeviceStatusResult> result = new ArrayList<>();
+        if (udids == null){
+            return result;
+        }
+        for (String udid: udids){
+            DeviceRunningStatus deviceRunningStatus = deviceRunningStatusMap.get(udid, traceId);
+            DeviceStatusResult deviceResult = new DeviceStatusResult();
+            deviceResult.setDeviceId(udid);
+            deviceResult.setStatusCode(deviceRunningStatus.getCode());
+            deviceResult.setDescription(deviceRunningStatus.getDescription());
+            result.add(deviceResult);
+        }
+        return result;
+    }
+}

+ 17 - 0
src/main/java/net/mooctest/www/android_auto_test/services/OssService.java

@@ -0,0 +1,17 @@
+package net.mooctest.www.android_auto_test.services;
+
+import java.io.File;
+import java.io.InputStream;
+
+/**
+ * @author henrylee
+ */
+public interface OssService {
+    String downloadFile(String downloadUrl, String traceId);
+
+    String getDadaJsonDownloadPath(String traceId);
+
+    String uploadFileToTraceDir(File file, String traceId, String fileName);
+
+    String uploadInputStreamToTraceDir(InputStream is, String traceId, String fileName);
+}

+ 18 - 0
src/main/java/net/mooctest/www/android_auto_test/services/TraceService.java

@@ -0,0 +1,18 @@
+package net.mooctest.www.android_auto_test.services;
+
+import net.mooctest.www.android_auto_test.common.constant.enums.TraceStatus;
+import net.mooctest.www.android_auto_test.models.Device;
+import net.mooctest.www.android_auto_test.vo.DeviceStatusResult;
+
+import java.util.List;
+
+public interface TraceService {
+
+    void updateTraceStatue(String traceId, TraceStatus traceStatus);
+
+    TraceStatus getTraceStatue(String traceId);
+
+    void setTraceDevices(String traceId, List<Device> deviceList);
+
+    List<DeviceStatusResult> getTraceDeviceStatus(String traceId);
+}

+ 133 - 0
src/main/java/net/mooctest/www/android_auto_test/utils/AddressUtil.java

@@ -0,0 +1,133 @@
+package net.mooctest.www.android_auto_test.utils;
+
+import net.mooctest.www.android_auto_test.common.constant.Consts;
+
+import java.io.File;
+
+/**
+ * @author henrylee
+ */
+public class AddressUtil {
+    private static final String TAG = Thread.currentThread() .getStackTrace()[1].getClassName();
+
+    public static String getMockDataJsonPath(){
+        return "mockData" +  File.separator + Consts.REPORT_FILE_NAME;
+    }
+
+    public static String getDataJsonPath(String traceId){
+        return "tasks" + File.separator + traceId + File.separator + Consts.REPORT_FILE_NAME;
+    }
+
+    public static String getTraceDir(String traceId){
+        return "tasks" + File.separator + traceId + File.separator;
+    }
+
+    public static String getApkInfoPath(String traceId){
+        return "tasks" + File.separator + traceId + File.separator + "ApkInfo.log";
+    }
+
+    public static String getOneTracePath(String traceId, String udid){
+        return "tasks" + File.separator + traceId + File.separator + udid + "/";
+    }
+
+    public static String getApkDownloadPath(String traceId, String fileName){
+        return "tasks" + File.separator + traceId + File.separator + fileName;
+    }
+
+    public static String getCpuFilePath(String udid, String traceId) {
+        return getOneTracePath(traceId, udid) + "Cpu.log";
+    }
+
+    public static String getMemFilePath(String udid, String traceId) {
+        return getOneTracePath(traceId, udid) + "Memory.log";
+    }
+
+    public static String getNetworkFilePath(String udid, String traceId) {
+        return getOneTracePath(traceId, udid) + "Network.log";
+    }
+
+    public static String getSMFilePath(String udid, String traceId) {
+        return getOneTracePath(traceId, udid) + "SM.log";
+    }
+
+    public static String getBatteryFilePath(String udid, String traceId) {
+        return getOneTracePath(traceId, udid) + "Battery.log";
+    }
+
+    public static String getScreenShotPath(String traceId, String udid){
+        return getOneTracePath(traceId, udid) + "ScreenShots" + File.separator;
+    }
+
+    public static String getScreenShotFilePath(String traceId, String udid, String deviceTime) {
+        return getScreenShotPath(traceId, udid) + udid + "_" + deviceTime + ".png";
+    }
+
+    public static String getLogCatLogPath(String traceId, String udid) {
+        return getOneTracePath(traceId, udid) + "Logcat.log";
+    }
+
+    public static String getAppiumLogPath(String traceId, String udid){
+        return getOneTracePath(traceId, udid) + "AppiumLog.log";
+    }
+
+    public static String getExceptionPath(String traceId, String udid) {
+        return getOneTracePath(traceId, udid) + "ExceptionLog.log";
+    }
+
+    public static String getOperationTracePath(String traceId, String udid) {
+        return getOneTracePath(traceId, udid) + "OperationTraces.log";
+    }
+
+    public static String getDeviceInfoPath(String traceId, String udid){
+        return getOneTracePath(traceId, udid) + "DeviceInfo.log";
+    }
+
+    public static String getDeviceExecInfoPath(String traceId, String udid){
+        return getOneTracePath(traceId, udid) + "DeviceExecInfo.log";
+    }
+
+    public static String getDeviceExtraInfoPath(String traceId, String udid){
+        return getOneTracePath(traceId, udid) + "ExtraInfo.log";
+    }
+
+    public static String getTestScript(String traceId, String udid) {
+        return getOneTracePath(traceId, udid) + "TestScript.java";
+    }
+    public static String getTestAction(String traceId, String udid) {
+        return getOneTracePath(traceId, udid)  + "TestAction.log";
+    }
+    public static String getMyTestLogPath(String traceId, String udid) {
+        return getOneTracePath(traceId, udid) + "TestLog.log";
+    }
+
+    public static String getPageImgDir(String traceId, String udid){
+        return getOneTracePath(traceId, udid) + "pageImg" + File.separator;
+    }
+    public static String getPageImgPath(String traceId, String udid, String activityName){
+        return getPageImgDir(traceId, udid) + activityName + ".jpg";
+    }
+    public static String getPageSourceDir(String traceId, String udid){
+        return getOneTracePath(traceId, udid) + "pageSource" + File.separator;
+    }
+    public static String getPageSourcePath(String traceId, String udid, int order){
+        return getPageSourceDir(traceId, udid) + order + "_"+ udid + ".xml";
+    }
+    public static String getPageXmlDir(String traceId, String udid){
+        return getOneTracePath(traceId, udid) + "pageXml" + File.separator;
+    }
+    public static String getPageXmlPath(String traceId, String udid, String activityName) {
+        return getPageXmlDir(traceId, udid) + activityName + ".xml";
+    }
+
+    public static String getFatherComponentConfigurationPath(){
+        return "configs" + File.separator + "fatherComponent.conf";
+    }
+    public static String getIgnoreConfigurationPath(){
+        return "configs" + File.separator + "ignore.conf";
+    }
+
+    public static String getHumanScriptPath(){
+        return "configs" + File.separator + "login.txt";
+    }
+}
+

+ 172 - 0
src/main/java/net/mooctest/www/android_auto_test/utils/AppiumManager.java

@@ -0,0 +1,172 @@
+package net.mooctest.www.android_auto_test.utils;
+
+import org.apache.commons.exec.CommandLine;
+import org.apache.commons.exec.DefaultExecuteResultHandler;
+import org.apache.commons.exec.DefaultExecutor;
+import org.apache.commons.exec.ExecuteWatchdog;
+import java.io.*;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.HashMap;
+
+/**
+ * Created by homer on 16-10-24.
+ */
+public class AppiumManager {
+    public static final String TAG = Thread.currentThread() .getStackTrace()[1].getClassName();
+    private static final int INIT_PORT = 4730;
+    private static AppiumManager manager;
+    private HashMap<String, String> udidAndPortMap = new HashMap<>();
+    private HashMap<String, String> portAndUdidMap = new HashMap<>();
+    private int nextPort;
+
+    private AppiumManager() {
+        super();
+        nextPort = INIT_PORT;
+    }
+
+    public static AppiumManager getInstance() {
+        if (manager == null) {
+            manager = new AppiumManager();
+        }
+        return manager;
+    }
+
+    public String checkDevicePort(String udid) {
+        return udidAndPortMap.get(udid);
+    }
+    //  使用一些linux命令来去掉
+    public boolean stopAppium(String port) {
+        return true;
+    }
+
+    public void setupAppium(String udid, String appiumLogsPath, String exceptionLogsPath) {
+
+        String port = checkDevicePort(udid);
+        if (port == null) {
+            port = getFreePort();
+            udidAndPortMap.put(udid, port);
+        }
+
+        if(checkAppiumRunning(port) ){
+            PrintUtil.print("appium in " + port + " is running now", TAG);
+            return;
+        }
+        File output = new File(appiumLogsPath);
+        FileUtil.newFile(output);
+
+        Runtime r=Runtime.getRuntime();
+        try {
+            String cmd = OsUtil.getCmd();
+            int dp = (Integer.parseInt(port) + 100);
+            String comand = cmd + " Commands/newAppium.sh " + port + " " + udid + " " + exceptionLogsPath
+                    + " " + appiumLogsPath + " " + dp;
+            PrintUtil.print("the comand is " + comand, TAG);
+            r.exec(comand);
+        } catch (IOException e1) {
+            // TODO Auto-generated catch block
+            e1.printStackTrace();
+        }
+        try {
+            FileReader fr=new FileReader(new File(appiumLogsPath));
+            BufferedReader br=new BufferedReader(fr);
+            String line=br.readLine();
+            PrintUtil.print(line, TAG);
+            boolean judge=false;
+            if(line==null){
+                judge=true;
+            }else if(line.equals("")){
+                judge=true;
+            }else if(line.split(" ").length<=1){
+                judge=true;
+            }else if(line.split(" ")[1].equals("-a")){
+                judge=true;
+            }
+            PrintUtil.print("Waiting for server "+udid+" openning...", TAG);
+            while(judge){
+                line=br.readLine();
+                if(line==null){
+                    judge=true;
+                }else if(line.equals("")){
+                    judge=true;
+                }else if(line.split(" ").length<=1){
+                    judge=true;
+                }else if(line.split(" ")[1].equals("-a")){
+                    judge=true;
+                }else{
+                    judge=false;
+                }
+            }
+            PrintUtil.print("Server " + port + " " + udid + " is open", TAG);
+        } catch (FileNotFoundException e1) {
+            e1.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public String getFreePort() {
+        boolean found = false;
+        String ret = null;
+        while (!found) {
+            if (!isLocalPortUsing(nextPort)) {
+                ret = String.valueOf(nextPort);
+                found = true;
+            }
+            nextPort ++;
+        }
+        return ret;
+    }
+
+    public static void startAppium(String port , String udid, String appiumLogsPath) throws IOException, InterruptedException {
+        // 处理外部命令执行的结果,释放当前线程,不会阻塞线程
+        DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler();
+        String cmd = "cmd.exe /c appium -a 127.0.0.1 -p " + port + " -U " + udid + " --log-timestamp --local-timezone --log-no-colors > " + appiumLogsPath;
+        CommandLine commandLine = CommandLine.parse(cmd);
+
+        // 创建监控时间60s,超过60s则中断执行
+        ExecuteWatchdog dog = new ExecuteWatchdog(60 * 1000);
+        DefaultExecutor executor = new DefaultExecutor();
+
+        // 设置命令执行退出值为1,如果命令成功执行并且没有错误,则返回1
+        executor.setExitValue(1);
+        executor.setWatchdog(dog);
+        executor.execute(commandLine, resultHandler);
+        resultHandler.waitFor(5000);
+        PrintUtil.print("Appium server start", TAG);
+    }
+
+    public static boolean checkAppiumRunning(String port) {
+        boolean flag = true;
+        try {
+            flag = isPortUsing("127.0.0.1", Integer.parseInt(port));
+
+        } catch (Exception e) {
+        }
+        return flag;
+    }
+
+    public static boolean isLocalPortUsing(int port){
+        boolean flag = true;
+        try {
+            flag = isPortUsing("127.0.0.1", port);
+        } catch (Exception e) {
+        }
+        return flag;
+    }
+
+    public static boolean isPortUsing(String host,int port) throws UnknownHostException {
+        boolean flag = false;
+        InetAddress theAddress = InetAddress.getByName(host);
+        int p=Integer.valueOf(port);
+        try {
+            Socket socket = new Socket(theAddress,p);
+            flag = true;
+        } catch (IOException e) {
+
+        }
+        return flag;
+    }
+}
+

+ 497 - 0
src/main/java/net/mooctest/www/android_auto_test/utils/CoverageTest.java

@@ -0,0 +1,497 @@
+package net.mooctest.www.android_auto_test.utils;
+
+import com.sinaapp.msdxblog.apkUtil.entity.ApkInfo;
+import io.appium.java_client.android.AndroidDriver;
+import net.mooctest.www.android_auto_test.Obversers.DeviceObserver;
+import net.mooctest.www.android_auto_test.Obversers.ScreenShotThread;
+import net.mooctest.www.android_auto_test.Obversers.monitor.LogMonitor;
+import net.mooctest.www.android_auto_test.Scripts.AbstractBaseScript;
+import net.mooctest.www.android_auto_test.Scripts.DefaultScript;
+import net.mooctest.www.android_auto_test.common.BeanFactory;
+import net.mooctest.www.android_auto_test.common.constant.Consts;
+import net.mooctest.www.android_auto_test.common.exceptions.AppiumDriverInitException;
+import net.mooctest.www.android_auto_test.common.exceptions.TraceTimeoutException;
+import net.mooctest.www.android_auto_test.common.constant.enums.DeviceRunningStatus;
+import net.mooctest.www.android_auto_test.models.Device;
+import net.mooctest.www.android_auto_test.services.DeviceService;
+import org.openqa.selenium.NoSuchSessionException;
+import org.openqa.selenium.WebDriverException;
+import org.openqa.selenium.remote.CapabilityType;
+import org.openqa.selenium.remote.DesiredCapabilities;
+import org.openqa.selenium.remote.UnreachableBrowserException;
+
+import java.io.*;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.util.regex.Pattern.*;
+
+/**
+ * @author henrylee
+ */
+public class CoverageTest extends Thread{
+    public static final String TAG = Thread.currentThread() .getStackTrace()[1].getClassName();
+
+    private volatile boolean stopFlag = false;
+    private volatile boolean finalEnd = false;
+    private ScreenShotThread screenShotThread;
+    private DeviceObserver deviceObserver;
+    private String traceId;
+    private String apkPath;
+    private ApkInfo apkInfo;
+    private Device device;
+    private String udid;
+    private String logPath;
+    private String port;
+    private AndroidDriver driver;
+    private LogMonitor mLogMonitor;
+    private Thread logThread;
+    private Long startTime;
+    private Long endTime;
+    private int appPid;
+    private AbstractBaseScript script;
+    private DeviceService deviceService;
+    private String coldStartTime = "unknown";
+
+    public CoverageTest(ApkInfo apkInfo, String apkPath, Device device, String traceId){
+        deviceService = (DeviceService) BeanFactory.getBean(DeviceService.class);
+        this.apkInfo = apkInfo;
+        this.apkPath = apkPath;
+        this.device = device;
+        this.udid = this.device.getUdid();
+        this.traceId = traceId;
+        deviceService.updateDeviceRunningStatus(traceId, udid, DeviceRunningStatus.INIT);
+        createCoverageTestDir();
+        //保存设备信息到相应目录
+        DeviceUtil.saveDeviceInfo(traceId, udid);
+    }
+
+    @Override
+    public void run() {
+        deviceService.updateDeviceRunningStatus(traceId, udid, DeviceRunningStatus.PRAPARE);
+        PrintUtil.print("Start Test", this.udid, TAG);
+        try{
+            this.startTime = System.currentTimeMillis();
+            checkEndFlag();
+            // 启动appium server 和 logcat监控线程
+            startAppiumServerAndLogcat();
+            //第一次安装
+            PrintUtil.print("First install", this.udid, TAG);
+            install();
+            //覆盖安装
+            PrintUtil.print("Cover install", this.udid, TAG);
+            install();
+            //统计冷启动时间
+            measureColdStartTime();
+            PrintUtil.print("Uninstall", this.udid, TAG);
+            //卸载
+            uninstall(false);
+            checkEndFlag();
+            //初始化Appium driver
+            initDriver();
+            //开启截屏 & 监控设备状态线程
+            startScreenShotAndDeviceObserverThread();
+
+            PrintUtil.print("Device " + this.udid + ", stop 5 seconds", TAG, this.udid);
+            Thread.sleep(5000);
+            // 获取应用上APP的pid
+            PrintUtil.print(String.format("Device %s, get app %s's pid.", udid, apkInfo.getPackageName()), TAG, this.udid);
+            getAppPid();
+            PrintUtil.print(String.format("Device %s, app %s, pid is %s.", udid, apkInfo.getPackageName(), appPid), TAG, this.udid);
+            // 开始测试
+            PrintUtil.print("Device " + this.udid + ", start test", TAG, this.udid);
+            // 执行算法逻辑
+            checkEndFlag();
+            deviceService.updateDeviceRunningStatus(traceId, udid, DeviceRunningStatus.RUNNING);
+            executeScript();
+        } catch (NoSuchSessionException e) {
+            writeExecErrorLog(e);
+            PrintUtil.printErr("Device " + udid + ". SessionNotFound " + udid, TAG, udid);
+        } catch (WebDriverException e) {
+            writeExecErrorLog(e);
+            PrintUtil.printErr("Device " + udid + ". WebDriverException " + udid + " " + e.getMessage(), TAG, udid);
+        } catch (TraceTimeoutException e){
+            writeExecErrorLog(e);
+            PrintUtil.printErr("Device " + udid + ". End script by timeout", TAG, udid);
+        } catch (AppiumDriverInitException e){
+            e.printStackTrace();
+            writeExecErrorLog(e);
+            PrintUtil.printErr(String.format("Device %s. %s", udid, e.getMessage()), TAG, udid);
+        } catch (Exception e) {
+            e.printStackTrace();
+            writeExecErrorLog(e);
+            PrintUtil.printErr(String.format("Device %s. Run script err %s", udid, e.getMessage()), TAG, udid);
+        }
+        finally {
+            deviceService.updateDeviceRunningStatus(traceId, udid, DeviceRunningStatus.ENDING);
+            this.endTime = System.currentTimeMillis();
+            // 恢复输入法
+            setDefaultIME();
+            // 保存设备执行的信息
+            saveDeviceExecInfo();
+            // 获取操作trace文件
+            pullOperationTrace();
+            // 结束APP并返回主页
+            killAppAndGoHome();
+            // 停止截屏 & 监控 线程
+            stopScreenShotAndDeviceObserverThread();
+            PrintUtil.print("Device " + udid + ", finish test", TAG, udid);
+            //卸载
+            uninstall(true);
+            // 停止appium server
+            stopAppiumServer(this.port);
+            PrintUtil.print("Device " + udid + ", test has finished", TAG, udid);
+            deviceService.updateDeviceRunningStatus(traceId, udid, DeviceRunningStatus.FINISH);
+            finalEnd = true;
+        }
+    }
+
+    private void startAppiumServerAndLogcat(){
+        String port = AppiumManager.getInstance().checkDevicePort(udid);
+        if (port == null){
+            startServer();
+        }else {
+            this.port = port;
+        }
+        PrintUtil.print("Device " + udid + ", the port is " + this.port, TAG, udid);
+        checkDeviceOnline();
+        OsUtil.runCommand("adb -s " + udid + "logcat -c");
+        mLogMonitor = new LogMonitor(udid, traceId);
+        logThread = new Thread(mLogMonitor);
+        logThread.start();
+    }
+
+    private void saveDeviceExecInfo(){
+        HashMap<String, String> infos = new LinkedHashMap<>(7);
+        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
+        if (deviceObserver != null && device != null) {
+            String maxCpuRate = deviceObserver.getMaxCpuInfo();
+            String maxMem = deviceObserver.getMaxMemInfo();
+            String maxNetwork = deviceObserver.getMaxNetworkInfo();
+            String maxBatteryTemp = deviceObserver.getMaxBatteryTempInfo();
+            infos.put("MaxCpu", maxCpuRate);
+            infos.put("MaxMem", maxMem);
+            infos.put("MaxNetwork", maxNetwork);
+            infos.put("MaxBatteryTemperature", maxBatteryTemp);
+            infos.put("ColdStartTime", coldStartTime);
+            infos.put("startTime", simpleDateFormat.format(this.startTime));
+            infos.put("endTime", simpleDateFormat.format(this.endTime));
+            infos.put("AppPid", String.valueOf(appPid));
+        }
+        DeviceUtil.saveDeviceExecInfo(traceId, udid, infos);
+    }
+
+    private void createCoverageTestDir(){
+        this.logPath = "tasks" + File.separator + traceId + File.separator + udid;
+        File logDir = new File(this.logPath);
+        File screenShotDir = new File(AddressUtil.getScreenShotPath(traceId, udid));
+        File installLog = new File(this.logPath + File.separator + "Install.log");
+        File launchLog = new File(this.logPath + File.separator + "Launch.log");
+        File uninstallLog = new File(this.logPath + File.separator + "Uninstall.log");
+        File coverInstallLog = new File(this.logPath + File.separator + "CoverInstall.log");
+        File execErrorLog = new File(this.logPath + File.separator + "ExecError.log");
+        FileUtil.newDir(logDir);
+        FileUtil.newDir(screenShotDir);
+        FileUtil.newFile(installLog);
+        FileUtil.newFile(launchLog);
+        FileUtil.newFile(uninstallLog);
+        FileUtil.newFile(coverInstallLog);
+        FileUtil.newFile(execErrorLog);
+
+        // 这里先创建一些测试脚本时才会创建的内容,是为了提供给报告生成服务一个兜底的东西
+        /**
+         * 设备执行信息
+         */
+        File deviceExecFile = new File(AddressUtil.getDeviceExecInfoPath(traceId, udid));
+        FileUtil.newFile(deviceExecFile);
+        /**
+         * 具体脚本用到的文件和文件夹
+         */
+        File myTestLog = new File(AddressUtil.getMyTestLogPath(traceId, udid));
+        File testScript = new File(AddressUtil.getTestScript(traceId, udid));
+        File testAction = new File(AddressUtil.getTestAction(traceId, udid));
+        FileUtil.newFiles(Arrays.asList(myTestLog, testAction, testScript));
+        File pageXmlDir = new File(AddressUtil.getPageXmlDir(traceId, udid));
+        File pageSourceDir = new File(AddressUtil.getPageSourceDir(traceId, udid));
+        File pageImgDir = new File(AddressUtil.getPageImgDir(traceId, udid));
+        FileUtil.newDirs(Arrays.asList(pageXmlDir, pageImgDir, pageSourceDir));
+        /**
+         * 监控用到的文件和文件夹
+         */
+        File logcatFile = new File(AddressUtil.getLogCatLogPath(traceId, udid));
+        File appiumLogFile = new File(AddressUtil.getAppiumLogPath(traceId, udid));
+        File exceptionLogsPath = new File(AddressUtil.getExceptionPath(traceId, udid));
+        File cpuLog = new File(AddressUtil.getCpuFilePath(udid, traceId));
+        File memLog = new File(AddressUtil.getMemFilePath(udid, traceId));
+        File networkLog = new File(AddressUtil.getNetworkFilePath(udid, traceId));
+        File smLog = new File(AddressUtil.getSMFilePath(udid, traceId));
+        File batteryTempLog = new File(AddressUtil.getBatteryFilePath(udid, traceId));
+        FileUtil.newFiles(Arrays.asList(cpuLog, memLog, networkLog, smLog, batteryTempLog, logcatFile, appiumLogFile, exceptionLogsPath));
+    }
+
+    private void startServer() {
+        AppiumManager manager = AppiumManager.getInstance();
+        String appiumLogsPath = AddressUtil.getAppiumLogPath(traceId, udid);
+        String exceptionLogsPath = AddressUtil.getExceptionPath(traceId, udid);
+        manager.setupAppium(this.udid, appiumLogsPath, exceptionLogsPath);
+        this.port = manager.checkDevicePort(this.udid);
+        PrintUtil.print("Setup Appium " + this.udid + " " + this.port, TAG, this.udid);
+    }
+
+    public boolean isFinalEnd(){
+        return this.finalEnd;
+    }
+
+    private void startScreenShotAndDeviceObserverThread(){
+        PrintUtil.print("Device " + this.udid+ ", start screenShot and device observer thread", TAG, this.udid);
+        screenShotThread = new ScreenShotThread(driver, this.udid, this.traceId);
+        screenShotThread.start();
+        deviceObserver = new DeviceObserver(this.udid, this.apkInfo.getPackageName(), this.traceId);
+        deviceObserver.start();
+    }
+    private void stopScreenShotAndDeviceObserverThread() {
+        PrintUtil.print("Stop screenshot and device observer thread", TAG, udid);
+        if (deviceObserver != null){
+            deviceObserver.interrupt();
+            deviceObserver.setStopFlag();
+//            deviceObserver.stop();
+        }
+        if (mLogMonitor != null) {
+            mLogMonitor.stop();
+        }
+//        if (logThread != null) {
+//            logThread.stop();
+//        }
+        if (screenShotThread != null){
+            screenShotThread.interrupt();
+            screenShotThread.setStopFlag();
+//            screenShotThread.stop();
+            try {
+                screenShotThread.join();
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    private void executeScript(){
+        PrintUtil.print("use the default script " + udid, TAG, udid);
+//        script = new DemoScript(udid, driver, apkInfo, traceId);
+        script = new DefaultScript(udid, driver, apkInfo, traceId);
+        script.runAndroidAutoTest();
+    }
+
+    private void install() {
+        try {
+            checkDeviceOnline();
+            String command = "adb -s " + this.udid + " install " + this.apkPath;
+            String msg = OsUtil.runCommand(command);
+            BufferedWriter writer = new BufferedWriter(new FileWriter(new File(this.logPath + File.separator + "Install.log"), false));
+            writer.write(msg);
+            writer.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void measureColdStartTime() {
+        checkDeviceOnline();
+        String command = "adb -s " + this.udid + " shell am start -W " + this.apkInfo.getPackageName() + "/" + this.apkInfo.getLaunchableActivity();
+        PrintUtil.print(command, TAG, this.udid);
+        String msg = OsUtil.runCommand(command);
+        try {
+            BufferedWriter writer = new BufferedWriter(new FileWriter(new File(this.logPath + File.separator + "Launch.log"), false));
+            writer.write(msg);
+            writer.close();
+        } catch (IOException e) {e.printStackTrace();}
+        if (msg != null) {
+            Pattern pattern = compile("ThisTime:\\s+(\\d+)");
+            Matcher matcher = pattern.matcher(msg);
+            if (matcher.find()) {
+                this.coldStartTime = String.valueOf(Double.parseDouble(matcher.group(1)) * 0.001);
+            }
+        }
+    }
+
+    private void uninstall(Boolean isFinal) {
+        try {
+            String command = "adb -s " + this.udid + " uninstall " + this.apkInfo.getPackageName();
+            if (isFinal){
+                PrintUtil.print("Uninstall to clean the device.", TAG);
+                command = String.format("adb -s %s shell pm uninstall %s", udid, this.apkInfo.getPackageName());
+            }else {
+                checkDeviceOnline();
+            }
+            String msg = OsUtil.runCommand(command);
+            BufferedWriter writer = new BufferedWriter(new FileWriter(new File(this.logPath + File.separator + "Uninstall.log"), false));
+            writer.write(msg);
+            writer.close();
+        } catch (IOException e) {e.printStackTrace();}
+    }
+
+    private void initDriver() {
+        PrintUtil.print("Uninstall and init driver " + this.udid + " " + port, TAG, this.udid);
+        uninstallDriver();
+        File app = new File(this.apkPath);
+        DesiredCapabilities capabilities = new DesiredCapabilities();
+        capabilities.setCapability(CapabilityType.BROWSER_NAME, "");
+        capabilities.setCapability("platformName", "Android");
+        capabilities.setCapability("deviceName", "Android Emulator");
+        capabilities.setCapability("platformVersion", DeviceUtil.getPlatformVersion(udid));
+        capabilities.setCapability("app", app.getAbsolutePath());
+        capabilities.setCapability("appPackage", this.apkInfo.getPackageName());
+        capabilities.setCapability("appActivity", this.apkInfo.getLaunchableActivity());
+        capabilities.setCapability("udid",this.udid);
+        capabilities.setCapability("unicodeKeyboard","true");
+        capabilities.setCapability("resetKeyboard","true");
+        capabilities.setCapability("noSign", "true");
+        boolean success = false;
+        int index = 0;
+        while (! success && index <= Consts.INIT_DRIVER_TIMES) {
+            try {
+                driver = new AndroidDriver(new URL("http://127.0.0.1:" + port + "/wd/hub"), capabilities);
+                success = true;
+            } catch (MalformedURLException e1) {
+                writeExecErrorLog(e1);
+                e1.printStackTrace();
+                index++;
+            } catch (UnreachableBrowserException e) {
+                writeExecErrorLog(e);
+                PrintUtil.printErr("Device " + this.udid + ", restart appium", TAG, this.udid);
+                startServer();
+                index++;
+            } catch (WebDriverException e) {
+                writeExecErrorLog(e);
+                PrintUtil.printErr("init android driver  web driverException " + this.udid + e.getMessage(), TAG, this.udid);
+                index++;
+            }
+        }
+        if (driver != null) {
+            PrintUtil.print("the driver init successfully " + this.udid, TAG, this.udid);
+        } else {
+            PrintUtil.print("the driver init wrongly " + this.udid, TAG, this.udid);
+            throw new AppiumDriverInitException("Init appium driver failed!");
+        }
+    }
+    private void uninstallDriver(){
+        checkDeviceOnline();
+        String command = String.format("adb -s %s uninstall io.appium.settings", udid);
+        OsUtil.runCommand(command);
+    }
+
+    private void getAppPid(){
+        this.appPid = DeviceUtil.getAppPidOnDevice(udid, apkInfo.getPackageName());
+    }
+
+    private void setDefaultIME() {
+        PrintUtil.print("Set IME back", TAG);
+        String cmd = "adb -s " + udid + " shell ime list -s";
+        String msg = OsUtil.runCommand(cmd);
+        String[] tmp = msg.split("\n");
+        for(String i : tmp){
+            if(!i.contains("io.appium")){
+                OsUtil.runCommand("adb -s " + udid + " shell ime set " + i);
+                break;
+            }
+        }
+    }
+
+    private void pullOperationTrace(){
+        try {
+            String cmd = "adb -s " + udid + " pull /data/anr/traces.txt " + AddressUtil.getOperationTracePath(traceId, udid);
+            OsUtil.runCommand(cmd);
+        } catch (Exception e){
+            e.printStackTrace();
+        }
+
+    }
+
+    private void killAppAndGoHome(){
+        try {
+            String pressHomeButton = "adb -s " + udid + " shell input keyevent 3";
+            OsUtil.runCommand(pressHomeButton);
+            Thread.sleep(1500);
+            String killApp = "adb -s " + udid + " shell am kill " + this.apkInfo.getPackageName();
+            OsUtil.runCommand(killApp);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private static void stopAppiumServer(String Port) {
+        try {
+            OsUtil.runCommand(OsUtil.getCmd() + " Commands/stopAppium.sh " + Port);
+        } catch (Exception e){
+            e.printStackTrace();
+        }
+
+    }
+
+    private void writeExecErrorLog(Throwable e){
+        try {
+            BufferedWriter writer = new BufferedWriter(new FileWriter(new File(this.logPath + File.separator + "ExecError.log"), false));
+            writer.write(getStackTrace(e));
+            writer.close();
+        } catch (IOException e1) {e1.printStackTrace();}
+    }
+
+    private static String getStackTrace(Throwable t){
+        StringWriter sw = new StringWriter();
+        PrintWriter pw = new PrintWriter(sw);
+        try{
+            t.printStackTrace(pw);
+            return sw.toString();
+        }
+        finally{
+            pw.close();
+        }
+    }
+
+    public void endTest(){
+        this.stopFlag = true;
+        if (script != null){
+            script.endScript();
+        }
+    }
+
+    private void checkEndFlag(){
+        if (this.stopFlag){
+            throw new TraceTimeoutException();
+        }
+    }
+
+    private void checkDeviceOnline() {
+        DeviceRunningStatus formerStatus = deviceService.getDeviceRunningStatus(traceId, udid);
+        boolean isOffline = false;
+        outer: while (! stopFlag){
+            List<Device> onlineDeviceList = deviceService.getOnlineDeviceList();
+            for (Device d: onlineDeviceList){
+                if (d.getUdid().equals(this.udid)){
+                    deviceService.updateDeviceRunningStatus(traceId, udid, formerStatus);
+                    break outer;
+                }
+            }
+            if (!isOffline){
+                PrintUtil.print(String.format("Device %s is offline! Retrying!", udid), TAG);
+                deviceService.updateDeviceRunningStatus(traceId, udid, DeviceRunningStatus.PENDING);
+            }
+            isOffline = true;
+            try {
+                Thread.sleep(2000);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+        checkEndFlag();
+        if (isOffline){
+            PrintUtil.print(String.format("Device %s is back online! Go on test!", udid), TAG);
+        }
+    }
+}

+ 22 - 0
src/main/java/net/mooctest/www/android_auto_test/utils/DeviceDaemon.java

@@ -0,0 +1,22 @@
+package net.mooctest.www.android_auto_test.utils;
+
+/**
+ * @author henrylee
+ * 项目启动时启动,用于更新Device状态
+ */
+public class DeviceDaemon extends Thread {
+
+    public static final String TAG = Thread.currentThread() .getStackTrace()[1].getClassName();
+
+    @Override
+    public void run(){
+//        while (true) {
+//            try {
+//                Thread.sleep(10000);
+//                PrintUtil.print("update devices status", TAG);
+//            } catch (InterruptedException e) {
+//                e.printStackTrace();
+//            }
+//        }
+    }
+}

+ 234 - 0
src/main/java/net/mooctest/www/android_auto_test/utils/DeviceUtil.java

@@ -0,0 +1,234 @@
+package net.mooctest.www.android_auto_test.utils;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+
+public class DeviceUtil {
+
+    /**
+     * @param traceId 单次任务的traceId
+     * @param udid 设备Id
+     */
+
+    public static void saveDeviceInfo(String traceId, String udid){
+        File deviceInfoFile = new File(AddressUtil.getDeviceInfoPath(traceId, udid));
+        FileUtil.newFile(deviceInfoFile);
+        try {
+            BufferedWriter writer = new BufferedWriter(new FileWriter(deviceInfoFile, true));
+            writer.write("udid=" + udid + "\n");
+            writer.write("os=" + getPlatformVersion(udid) + "\n");
+            writer.write("deviceModel=" + getMobileModel(udid) + "\n");
+            writer.write("brand=" + getMobileBrand(udid) + "\n");
+            writer.write("deviceName=" + getMobileName(udid) + "\n");
+            writer.write("resolution=" + getMobileResolution(udid) + "\n");
+            writer.write("Power=" + getBatteryLevel(udid));
+            writer.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     *
+     * @param traceId 单次任务的traceId
+     * @param udid 设备Id
+     * @param execInfo 执行期间的信息,包括最大占用CPU、内存、网络和电池温度
+     */
+    public static void saveDeviceExecInfo(String traceId, String udid, HashMap<String, String> execInfo){
+        File deviceExecFile = new File(AddressUtil.getDeviceExecInfoPath(traceId, udid));
+        FileUtil.newFile(deviceExecFile);
+        Iterator iterator = execInfo.keySet().iterator();
+        try {
+            BufferedWriter writer = new BufferedWriter(new FileWriter(deviceExecFile, true));
+            while (iterator.hasNext()){
+                String key = (String) iterator.next();
+                String value = execInfo.getOrDefault(key, "unknown");
+                writer.write(key + "=" + value + "\n");
+            }
+            writer.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+    }
+
+    /**
+     *
+     * @param udid 设备Id
+     * @return 设备的当前电量
+     */
+
+    public static String getBatteryLevel(String udid) {
+        try {
+            String command = "adb -s " + udid + " shell dumpsys battery";
+            String result = OsUtil.runCommand(command);
+            if (result != null ) {
+                result = result.replaceAll("\\s*","");
+                int firstLevel = result.indexOf("level:") + 6;
+                if(result.indexOf("level") != result.lastIndexOf("level")) {
+                    result = result.substring(firstLevel, result.indexOf("level", firstLevel) );
+                }
+                else {
+                    result = result.substring(firstLevel, result.indexOf("scale"));
+                }
+                return result;
+            }else {
+                return "0";
+            }
+        } catch (Exception e){
+            e.printStackTrace();
+            return "0";
+        }
+
+    }
+
+    /**
+     *
+     * @param udid 设备Id
+     * @return 设备的型号
+     */
+
+    public static String getMobileModel(String udid){
+        String[] tmp = OsUtil.runCommand("adb -s " + udid + " shell getprop ro.product.model").split("\n");
+        String result = tmp[tmp.length-1];
+        return result;
+    }
+
+    /**
+     * @param udid 设备Id
+     * @return 设备的分辨率
+     */
+    public static String getMobileResolution(String udid){
+        String[] tmp = OsUtil.runCommand("adb -s " + udid + " shell wm size").split(":");
+        String result = tmp[tmp.length-1].trim();
+        return result;
+    }
+
+    /**
+     *
+     * @param udid 设备Id
+     * @return 设备的品牌
+     */
+    public static String getMobileBrand(String udid){
+        String[] tmp = OsUtil.runCommand("adb -s " + udid + " shell getprop ro.product.brand").split("\n");
+        String result = tmp[tmp.length-1];
+        return result;
+    }
+
+    /**
+     * @param udid 设备Id
+     * @return 设备的名称
+     */
+    public static String getMobileName(String udid){
+        String[] tmp = OsUtil.runCommand("adb -s " + udid + " shell getprop ro.product.name").split("\n");
+        String result = tmp[tmp.length-1];
+        return result;
+    }
+
+    /**
+     *
+     * @param udid 设备Id
+     * @return 设备的年份
+     */
+    public static String getDeviceYear(String udid){
+        String result = "";
+        String command = "adb -s " + udid + " shell date +%Y-";
+        OsUtil.runCommand("adb devices");
+        result = OsUtil.runCommand(command);
+        result = result.trim();
+        return result;
+    }
+
+    /**
+     *
+     * @param udid 设备Id
+     * @return 设备的android系统版本
+     */
+    public static String getPlatformVersion(String udid){
+        String[] tmp = OsUtil.runCommand("adb -s " + udid + " shell getprop ro.build.version.release").split("\n");
+        String result = tmp[tmp.length-1];
+        return result;
+    }
+
+    /**
+     *
+     * @param udid 设备Id
+     * @return 设备的SDK版本
+     */
+    public static String getApiLevel(String udid){
+        String[] tmp = OsUtil.runCommand("adb -s " + udid + " shell getprop ro.build.version.sdk").split("\n");
+        String result = tmp[tmp.length-1];
+        return result;
+    }
+
+    /**
+     *
+     * @param udid 设备Id
+     * @return 设备当前时间
+     */
+    public static String getDeviceTime(String udid){
+        return timeStamp2Date(getDeviceTimeStamp(udid));
+    }
+
+    /**
+     *
+     * @param udid 设备Id
+     * @return 设备当前时间戳
+     */
+    public static String getDeviceTimeStamp(String udid){
+        String result = "";
+        String command = "adb -s " + udid + " shell echo ${EPOCHREALTIME:0:14}";
+        OsUtil.runCommand("adb devices");
+        result = OsUtil.runCommand(command);
+        result = result.replace(".", "");
+        result = result.trim();
+        return result;
+    }
+
+    /**
+     * @param udid 设备ID
+     * @param packageName 应用的packageName
+     * @return
+     */
+    public static int getAppPidOnDevice(String udid, String packageName){
+        String command;
+        command = OsUtil.getCmd() + " Commands/getPid.sh " + udid + " " + packageName +"\\>";
+        //获取应用的pid
+        String output = OsUtil.runCommand(command);
+        String[] tmp = output.split("\n");
+        String pid;
+        if (tmp != null && tmp.length >=1) {
+            String firstLine = tmp[0]; //默认只看第一个
+            String[] items = firstLine.split("\\s+");
+            if (items != null && items.length >= 2) {
+                pid = items[1];
+            } else {
+                pid = "-1";
+            }
+        } else {
+            //需要重新获取
+            pid = "-1";
+        }
+        return Integer.parseInt(pid);
+    }
+
+    private static String timeStamp2Date(String time) {
+        Long timeLong = Long.parseLong(time);
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS");
+        Date date;
+        try {
+            date = sdf.parse(sdf.format(timeLong));
+            return sdf.format(date);
+        } catch (ParseException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+}

+ 65 - 0
src/main/java/net/mooctest/www/android_auto_test/utils/DoXml.java

@@ -0,0 +1,65 @@
+package net.mooctest.www.android_auto_test.utils;
+
+import net.mooctest.www.android_auto_test.models.Component;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+import org.jdom2.input.SAXBuilder;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class DoXml {
+    public List<Component> run(String path){
+        List<Component> list = null;
+        try {
+            SAXBuilder builder = new SAXBuilder();
+            Document doc = builder.build(new File(path));
+            Element foo = doc.getRootElement();
+            list=new ArrayList();
+            list=DFS(foo,list);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return list;
+    }
+    List<Component> DFS(Element e,List<Component> list){
+        List allChildren = e.getChildren();
+        if(allChildren.size() <= 0){
+            Component component = new Component();
+            component.setResource_id(e.getAttributeValue("resource-id"));
+            component.setIndex(e.getAttributeValue("index"));
+            component.setText(e.getAttributeValue("text"));
+            component.setPackagename(e.getAttributeValue("package"));
+            component.setContent_desc(e.getAttributeValue("content-desc"));
+            component.setCheckable(e.getAttributeValue("checkable"));
+            component.setChecked(e.getAttributeValue("checked"));
+            component.setClickable(e.getAttributeValue("clickable"));
+            component.setEnabled(e.getAttributeValue("enabled"));
+            component.setFocusable(e.getAttributeValue("focusable"));
+            component.setFocused(e.getAttributeValue("focused"));
+            component.setScrollable(e.getAttributeValue("scrollable"));
+            component.setLong_clickable(e.getAttributeValue("long-clickable"));
+            component.setPassword(e.getAttributeValue("password"));
+            component.setSelected(e.getAttributeValue("selected"));
+
+            String classname=e.getAttributeValue("class");
+            String bounds=e.getAttributeValue("bounds");
+            String xPath = "//" + classname + "[contains(@bounds,'"+bounds+"')]";
+            component.setBounds(bounds);
+            component.setClassname(classname);
+            component.setLocator(xPath);
+
+            list.add(component);
+            return list;
+        }else{
+            for(int i = 0;i < allChildren.size();i++) {
+                DFS((Element)allChildren.get(i),list);
+            }
+            return list;
+        }
+    }
+}
+

+ 61 - 0
src/main/java/net/mooctest/www/android_auto_test/utils/DoubleUtils.java

@@ -0,0 +1,61 @@
+package net.mooctest.www.android_auto_test.utils;
+
+import java.math.BigDecimal;
+
+/**
+ * Created by homer on 17-2-11.
+ */
+public class DoubleUtils {
+    /**
+     * double 乘法
+     *
+     * @param d1
+     * @param d2
+     * @return
+     */
+    public static double mul(double d1, double d2) {
+        BigDecimal bd1 = new BigDecimal(Double.toString(d1));
+        BigDecimal bd2 = new BigDecimal(Double.toString(d2));
+
+        try
+        {
+            return bd1.multiply(bd2).doubleValue();
+        } catch (Exception e)
+        {
+            // 根据bugly观测,在进入GTOpMulPerfActivity页时有极小概率crash,故加上异常保护
+            // @see http://bugly.qq.com/detail?app=900010910&pid=1&ii=152#stack
+            e.printStackTrace();
+            return 0;
+        }
+    }
+
+    /**
+     * double 除法
+     *
+     * @param d1
+     * @param d2
+     * @param scale
+     *            四舍五入 小数点位数
+     * @return
+     */
+    public static double div(double d1, double d2, int scale) {
+        // 当然在此之前,你要判断分母是否为0,
+        // 为0你可以根据实际需求做相应的处理
+
+        BigDecimal bd1 = new BigDecimal(Double.toString(d1));
+        BigDecimal bd2 = new BigDecimal(Double.toString(d2));
+//		return bd1.divide(bd2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
+        // 直接向下取整,保持和UI展示一致
+        try
+        {
+            return bd1.divide(bd2, scale, BigDecimal.ROUND_DOWN).doubleValue();
+        } catch (Exception e)
+        {
+            // 根据bugly观测,在进入GTOpMulPerfActivity页时有极小概率crash,故加上异常保护
+            // @see http://bugly.qq.com/detail?app=900010910&pid=1&ii=46#stack
+            e.printStackTrace();
+            return 0;
+        }
+
+    }
+}

+ 9 - 0
src/main/java/net/mooctest/www/android_auto_test/utils/DriverUtil.java

@@ -0,0 +1,9 @@
+package net.mooctest.www.android_auto_test.utils;
+
+
+/**
+ * @author henrylee
+ */
+public class DriverUtil {
+
+}

+ 89 - 0
src/main/java/net/mooctest/www/android_auto_test/utils/FileUtil.java

@@ -0,0 +1,89 @@
+package net.mooctest.www.android_auto_test.utils;
+
+import net.mooctest.www.android_auto_test.common.constant.Consts;
+import net.mooctest.www.android_auto_test.common.exceptions.ApkDownloadException;
+import net.mooctest.www.android_auto_test.services.OssService;
+import org.apache.commons.io.FileUtils;
+import java.io.*;
+import java.net.URL;
+import java.util.List;
+
+/**
+ * @author henrylee
+ */
+public class FileUtil {
+    public static final String TAG = Thread.currentThread() .getStackTrace()[1].getClassName();
+
+    public static void newFiles(List<File> files){
+        for (File f: files){
+            newFile(f);
+        }
+    }
+
+    public static void newFile(File file){
+        try{
+            if(!file.exists()) {
+                file.createNewFile();
+            } else{
+                file.delete();
+                file.createNewFile();
+            }
+        }catch(Exception e){
+            e.printStackTrace();
+        }
+    }
+
+    public static void newDirs(List<File> dirs){
+        for (File dir: dirs){
+            newDir(dir);
+        }
+    }
+    public static void newDir(File dir){
+        try{
+            if(!dir.exists()) {
+                dir.mkdirs();
+            }
+            else{
+                FileUtils.deleteQuietly(dir);
+                dir.mkdirs();
+            }
+        }catch(Exception e){
+            e.printStackTrace();
+        }
+    }
+
+    public static boolean exists(String path){
+        File file = new File(path);
+        return file.exists();
+    }
+    public static boolean isDirectory(String path) {
+        File file = new File(path);
+        return file.exists() && file.isDirectory();
+    }
+
+    public static void closeReader(Reader br){
+        if(br != null){
+            try{
+                br.close();
+            }catch (IOException e){
+                e.printStackTrace();
+            }
+        }
+    }
+    public static void closeRandomAccessFile(RandomAccessFile f){
+        if (f != null){
+            try{
+                f.close();
+            }catch (IOException e){
+                e.printStackTrace();
+            }
+        }
+    }
+    public static void saveFile(String content, String path) throws IOException{
+        FileOutputStream writerStream;
+        writerStream = new FileOutputStream(path);
+        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(writerStream, "UTF-8"));
+        writer.write(content);
+        writer.close();
+    }
+}

+ 72 - 0
src/main/java/net/mooctest/www/android_auto_test/utils/InputFinder.java

@@ -0,0 +1,72 @@
+package net.mooctest.www.android_auto_test.utils;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import com.google.common.collect.ImmutableBiMap.Builder;
+
+
+public class InputFinder {
+    public String getInputValue(String filepath,String resource_id) throws IOException {
+        if (resource_id == null){
+            return null;
+        }
+        BufferedReader bre;
+        //此时获取到的bre就是整个文件的缓存流
+        bre = new BufferedReader(new FileReader(filepath));
+        StringBuilder variablebuilder = new StringBuilder();
+        StringBuilder inputBuilder = new StringBuilder();
+        String variableName = null;
+        String inputValue = null;
+        //是否找到给定控件所对应的变量名
+        boolean flag1 = false;
+
+        String strLine = "";
+        // 判断最后一行不存在,为空结束循环
+        while ((strLine = bre.readLine())!= null) {
+            int index1 = -1;
+            //找到出现过给定控件的resource-id的那一行
+            if(strLine.contains(resource_id)) {
+                index1 = strLine.indexOf("WebElement");
+                //过滤掉WebElement类名
+                while(strLine.charAt(index1)!=' ') {
+                    index1++;
+                }
+                //过滤掉类名和变量名之间空格、回车、水平制表符
+                while(strLine.charAt(index1)==' '||strLine.charAt(index1)=='\r'||strLine.charAt(index1)=='\t') {
+                    index1++;
+                }
+                //将变量名拼成字符串
+                while(strLine.charAt(index1)!=' '&&strLine.charAt(index1)!='='&&strLine.charAt(index1)!='\t'&&strLine.charAt(index1)!='\r') {
+                    variablebuilder.append(strLine.charAt(index1));
+                    index1++;
+                }
+                variableName = variablebuilder.toString();
+                //给定控件的变量名已找到
+                flag1 = true;
+                //直接跳到读取下一行
+                continue;
+            }
+
+            int index2 = -1;
+            if(flag1) {
+                //找到像控件发送输入的那一行
+                if((index2 = strLine.indexOf(variableName+".sendKeys"))!=-1) {
+                    while(strLine.charAt(index2)!='\"') {
+                        index2++;
+                    }
+                    index2++;
+                    while(strLine.charAt(index2)!='\"') {
+                        inputBuilder.append(strLine.charAt(index2));
+                        index2++;
+                    }
+                    inputValue = inputBuilder.toString();
+
+                }
+            }
+        }
+        return inputValue;
+    }
+}
+

+ 115 - 0
src/main/java/net/mooctest/www/android_auto_test/utils/OsUtil.java

@@ -0,0 +1,115 @@
+package net.mooctest.www.android_auto_test.utils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author henrylee
+ */
+public class OsUtil {
+    public static String getCmd() {
+        return "bash";
+    }
+
+    public static void runCommandAsyn(String command) {
+        try {
+            Runtime.getRuntime().exec(command);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+    static public void executeCommand(String[] commandArr) {
+        print("Linux command: "
+                + java.util.Arrays.toString(commandArr));
+
+        try {
+            ProcessBuilder pb = new ProcessBuilder(commandArr);
+            pb.redirectErrorStream(true);
+            Process proc = pb.start();
+            BufferedReader in = new BufferedReader(new InputStreamReader(
+                    proc.getInputStream()));
+            print("Process started !");
+
+            String line;
+            while ((line = in.readLine()) != null) {
+                print(line);
+            }
+
+            proc.destroy();
+            print("Process ended !");
+        } catch (Exception x) {
+            x.printStackTrace();
+        }
+    }
+
+    public static String runCommand(String[] commands) {
+        if (commands == null || commands.length == 0) {
+            return null;
+        }
+        BufferedReader br = null;
+        String line = null;
+        InputStream is = null;
+        InputStreamReader isReader = null;
+        try {
+            ProcessBuilder pb = new ProcessBuilder(commands);
+            pb.redirectErrorStream(true);
+            Process proc = pb.start();
+            is = proc.getInputStream();
+            isReader = new InputStreamReader(is, "utf-8");
+            br = new BufferedReader(isReader);
+            String result = "";
+            while ((line = br.readLine()) != null) {
+                result += (line + "\n");
+            }
+            proc.destroy();
+            return result;
+        } catch (IOException e) {
+            return line;
+        } finally {
+            if (isReader != null) {
+                try {
+                    isReader.close();
+                } catch (IOException e) {
+                    // TODO Auto-generated catch block
+                }
+            }
+
+            if (is != null) {
+                try {
+                    is.close();
+                } catch (IOException e) {
+                    // TODO Auto-generated catch block
+                }
+            }
+            if (br != null) {
+                try {
+                    br.close();
+                } catch (IOException e) {
+                    // TODO
+                }
+            }
+        }
+    }
+    public static String runCommand(String command) {
+        return runCommand(command.split(" "));
+    }
+    private static void print(String msg) {
+        String TAG = Thread.currentThread() .getStackTrace()[1].getClassName();
+        SimpleDateFormat df=new SimpleDateFormat("MM-dd HH:mm:ss,SSS");
+        Date date = new Date(System.currentTimeMillis());
+        System.out.println(df.format(date) + " " + TAG + " - " + msg);
+    }
+    public static Process exec(List<String> args) throws IOException {
+        String[] arrays = new String[args.size()];
+
+        for (int i=0; i<args.size(); i++) {
+            arrays[i] = args.get(i);
+        }
+        return Runtime.getRuntime().exec(arrays);
+    }
+}

+ 144 - 0
src/main/java/net/mooctest/www/android_auto_test/utils/ParseXml.java

@@ -0,0 +1,144 @@
+package net.mooctest.www.android_auto_test.utils;
+
+import net.mooctest.www.android_auto_test.models.Component;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.input.SAXBuilder;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ParseXml {
+	List<String> idList = new ArrayList<String>();
+	List<String> textXpathList = new ArrayList<String>();
+	List<String> contentDescXpathList = new ArrayList<String>();
+	List<String> indexXpathList = new ArrayList<String>();
+
+	public List<Component> run(String path){
+		List<Component> list = null;
+		try {
+			SAXBuilder builder = new SAXBuilder();
+			Document doc = builder.build(new File(path));
+			Element foo = doc.getRootElement();
+			list = new ArrayList();
+			list = DFS(foo,list);
+			list = setComponentLocator(list);
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+		return list;
+	}
+
+	List<Component> DFS(Element e,List<Component> list){
+//		PrintUtil.print("ParseXml fff" + e.getName(), "TAG");
+		List allChildren = e.getChildren();
+		if(allChildren.size() <= 0) {
+			recordAllLocators(e);
+			//对于叶子节点,直接判断本节点的clickable,true则将本节点加入list
+			if(e.getAttributeValue("clickable") != null && e.getAttributeValue("clickable").equals("true")) {
+				list.add(element2Component(e));
+			}
+			return list;
+		}else {
+			for(int i = 0;i < allChildren.size();i++) {//对于父亲节点,先去找自己孩子中clickable = true 的加入list(深度优先)
+				DFS((Element)allChildren.get(i),list);
+			}
+			recordAllLocators(e);
+			//孩子已遍历完,再看本节点clickable是否为true,是则将本节点加入list
+			if(e.getAttributeValue("clickable") != null && e.getAttributeValue("clickable").equals("true")) {
+				list.add(element2Component(e));
+			}
+			return list;
+		}
+	}
+	Component element2Component(Element e){
+		Component component = new Component();
+		component.setResource_id(e.getAttributeValue("resource-id"));
+		component.setIndex(e.getAttributeValue("index"));
+		component.setText(e.getAttributeValue("text"));
+		component.setPackagename(e.getAttributeValue("package"));
+		component.setContent_desc(e.getAttributeValue("content-desc"));
+		component.setCheckable(e.getAttributeValue("checkable"));
+		component.setChecked(e.getAttributeValue("checked"));
+		component.setClickable(e.getAttributeValue("clickable"));
+		component.setEnabled(e.getAttributeValue("enabled"));
+		component.setFocusable(e.getAttributeValue("focusable"));
+		component.setFocused(e.getAttributeValue("focused"));
+		component.setScrollable(e.getAttributeValue("scrollable"));
+		component.setLong_clickable(e.getAttributeValue("long-clickable"));
+		component.setPassword(e.getAttributeValue("password"));
+		component.setSelected(e.getAttributeValue("selected"));
+		
+		//Set boundsXpath as default component locator
+		String classname = e.getAttributeValue("class");
+		String bounds = e.getAttributeValue("bounds");
+		String boundsXpath = "//" + classname + "[contains(@bounds,'" + bounds + "')]";
+		component.setBounds(bounds);
+		component.setClassname(classname);
+		component.setLocator(boundsXpath);
+		return component;
+	}
+	void recordAllLocators(Element e){
+		String classname = e.getAttributeValue("class");
+		if(e.getAttributeValue("resource-id") != null && !e.getAttributeValue("resource-id").isEmpty()){
+			idList.add(e.getAttributeValue("resource-id"));
+		}
+		if(e.getAttributeValue("text") != null && !e.getAttributeValue("text").isEmpty()){
+			textXpathList.add("//" + classname + "[contains(@text,'" + e.getAttributeValue("text") + "')]");
+		}
+		if(e.getAttributeValue("content-desc") != null && !e.getAttributeValue("content-desc").isEmpty()){
+			contentDescXpathList.add("//" + classname + "[contains(@content-desc,'" + e.getAttributeValue("content-desc") + "')]");
+		}
+		if(e.getAttributeValue("index") != null && !e.getAttributeValue("index").isEmpty()){
+			indexXpathList.add("//" + classname + "[contains(@index,'" + e.getAttributeValue("index") + "')]");
+		}
+	}
+	List<Component> setComponentLocator(List<Component> list){
+		List<Component> newList = new ArrayList<Component>();
+		for(Component component : list){
+			String id = component.getResource_id();
+			String textXpath = "//" + component.getClassname() + "[contains(@text,'" + component.getText() + "')]";
+			String contentDescXpath = "//" + component.getClassname() + "[contains(@content-desc,'" + component.getContent_desc() + "')]";
+			String indexXpath = "//" + component.getClassname() + "[contains(@index,'" + component.getIndex() + "')]";
+			if(component.getResource_id() != null && !component.getResource_id().isEmpty() && idList.indexOf(id) == idList.lastIndexOf(id)){
+				component.setLocator(id);
+			}else if(component.getText() != null && !component.getText().isEmpty() && textXpathList.indexOf(textXpath) == textXpathList.lastIndexOf(textXpath)){
+				component.setLocator(textXpath);
+			}else if(component.getContent_desc() != null && !component.getContent_desc().isEmpty() && contentDescXpathList.indexOf(contentDescXpath) == contentDescXpathList.lastIndexOf(contentDescXpath)){
+				component.setLocator(contentDescXpath);
+			}else if(component.getIndex() != null && !component.getIndex().isEmpty() && indexXpathList.indexOf(indexXpath) == indexXpathList.lastIndexOf(indexXpath)){
+				component.setLocator(indexXpath);
+			}
+			newList.add(component);
+		}
+		return newList;
+	}
+
+	String form(String str){
+		String result = "";
+		String[] array = str.split(" ");
+		for(int i = 0;i < array.length;i++){
+			if(!array[i].equals("")){
+				result = result + array[i] + " ";
+			}
+		}
+		if(result.length() <= 0){
+
+		}else{
+			result = result.substring(0, result.length() - 1);
+		}
+		return result;
+	}
+
+	boolean isInList(List l,String str){
+		for(int i = 0;i < l.size();i++){
+			String buff = (String)l.get(i);
+			if(buff.equals(str)){
+				return true;
+			}
+		}
+		return false;
+	}
+
+}

+ 106 - 0
src/main/java/net/mooctest/www/android_auto_test/utils/PrintUtil.java

@@ -0,0 +1,106 @@
+package net.mooctest.www.android_auto_test.utils;
+
+import org.springframework.stereotype.Service;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+
+public class PrintUtil {
+    public static final String ANSI_RESET = "\u001B[0m";
+    public static final String ANSI_BLACK = "\u001B[30m";
+    public static final String ANSI_RED = "\u001B[31m";
+    public static final String ANSI_GREEN = "\u001B[32m";
+    public static final String ANSI_YELLOW = "\u001B[33m";
+    public static final String ANSI_BLUE = "\u001B[34m";
+    public static final String ANSI_PURPLE = "\u001B[35m";
+    public static final String ANSI_CYAN = "\u001B[36m";
+    public static final String ANSI_WHITE = "\u001B[37m";
+    public static void printWithColor(String msg, String TAG, String color) {
+        SimpleDateFormat df = new SimpleDateFormat("MM-dd HH:mm:ss,SSS");
+        Date date = new Date(System.currentTimeMillis());
+        System.out.println(color + df.format(date) + " " + TAG + " - " + msg + ANSI_RESET);
+    }
+    public static void print(String msg, String TAG, String udid, String color) {
+        SimpleDateFormat df = new SimpleDateFormat("MM-dd HH:mm:ss,SSS");
+        Date date = new Date(System.currentTimeMillis());
+        System.out.println(color + df.format(date) + " " + TAG + " [" + udid + "] - " + msg + ANSI_RESET);
+    }
+    public static void print(String msg, String TAG, String udid, BufferedWriter bufferedWriter, String color) {
+        SimpleDateFormat df1 = new SimpleDateFormat("MM-dd HH:mm:ss,SSS");
+        SimpleDateFormat df2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS");
+        Date date = new Date(System.currentTimeMillis());
+        System.out.println(color + df1.format(date) + " " + TAG + " [" + udid + "] - " + msg + ANSI_RESET);
+        try {
+            bufferedWriter.write(df2.format(date) + " " + TAG + " - " + msg + "\n");
+            bufferedWriter.flush();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+    public static void print(String msg, String TAG) {
+        SimpleDateFormat df = new SimpleDateFormat("MM-dd HH:mm:ss,SSS");
+        Date date = new Date(System.currentTimeMillis());
+        System.out.println(df.format(date) + " " + TAG + " - " + msg);
+    }
+    public static void print(String msg, String TAG, String udid) {
+        SimpleDateFormat df = new SimpleDateFormat("MM-dd HH:mm:ss,SSS");
+        Date date = new Date(System.currentTimeMillis());
+        System.out.println(df.format(date) + " " + TAG + " [" + udid + "] - " + msg);
+    }
+    public static void print(String msg, String TAG, String udid, BufferedWriter bufferedWriter) {
+        SimpleDateFormat df1 = new SimpleDateFormat("MM-dd HH:mm:ss,SSS");
+        SimpleDateFormat df2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS");
+        Date date = new Date(System.currentTimeMillis());
+        System.out.println(df1.format(date) + " " + TAG + " [" + udid + "] - " + msg);
+        try {
+            bufferedWriter.write(df2.format(date) + " " + TAG + " - " + msg + "\n");
+            bufferedWriter.flush();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+    public static void printErr(String msg, String TAG) {
+        SimpleDateFormat df = new SimpleDateFormat("MM-dd HH:mm:ss,SSS");
+        Date date = new Date(System.currentTimeMillis());
+        System.err.println(df.format(date) + " " + TAG + " - " + msg);
+    }
+    public static void printErr(String msg, String TAG, String udid) {
+        SimpleDateFormat df = new SimpleDateFormat("MM-dd HH:mm:ss,SSS");
+        Date date = new Date(System.currentTimeMillis());
+        System.err.println(df.format(date) + " " + TAG + " [" + udid + "] - " + msg);
+    }
+    public static void printException(String TAG, String udid, Exception e) {
+        SimpleDateFormat df = new SimpleDateFormat("MM-dd HH:mm:ss,SSS");
+        Date date = new Date(System.currentTimeMillis());
+        System.err.println(df.format(date) + " " + TAG + " [" + udid + "] - " + getStackTrace(e));
+    }
+    public static String getStackTrace(Exception e) {
+        StringWriter sw = null;
+        PrintWriter pw = null;
+        try {
+            sw = new StringWriter();
+            pw = new PrintWriter(sw);
+            e.printStackTrace(pw);
+            pw.flush();
+            sw.flush();
+        } finally {
+            if (sw != null) {
+                try {
+                    sw.close();
+                } catch (IOException e1) {
+                    e1.printStackTrace();
+                }
+            }
+            if (pw != null) {
+                pw.close();
+            }
+        }
+        return sw.toString();
+    }
+}

+ 172 - 0
src/main/java/net/mooctest/www/android_auto_test/utils/TraceDaemon.java

@@ -0,0 +1,172 @@
+package net.mooctest.www.android_auto_test.utils;
+
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import net.mooctest.www.android_auto_test.common.BeanFactory;
+import net.mooctest.www.android_auto_test.common.constant.Consts;
+import net.mooctest.www.android_auto_test.common.constant.enums.TraceStatus;
+import net.mooctest.www.android_auto_test.services.ApkService;
+import net.mooctest.www.android_auto_test.services.OssService;
+import net.mooctest.www.android_auto_test.services.TraceService;
+import org.apache.commons.io.FileUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * @author henrylee
+ * 用于监控一个trace下所有设备的执行线程,以及在线程结束后,触发报告生成
+ */
+public class TraceDaemon extends Thread{
+
+    private TraceService traceService;
+    private OssService ossService;
+    private ApkService apkService;
+
+    public static final String TAG = Thread.currentThread() .getStackTrace()[1].getClassName();
+
+    private String traceId;
+    private int minutes;
+    private List<CoverageTest> deviceThreads;
+    private boolean stopImmediately = false;
+
+    public TraceDaemon(String traceId, int minutes, List<CoverageTest> deviceThreads){
+        this.traceId = traceId;
+        this.minutes = minutes;
+        this.deviceThreads = deviceThreads;
+        traceService = (TraceService) BeanFactory.getBean(TraceService.class);
+        ossService = (OssService) BeanFactory.getBean(OssService.class);
+        apkService = (ApkService) BeanFactory.getBean(ApkService.class);
+    }
+
+    @Override
+    public void run() {
+        boolean allTaskEnd = false;
+        long sleptTime = 0L;
+        long timeoutTime = 60 * 1000 * minutes;
+        // 每checkTerminal秒检查一次所有线程状态
+        while (sleptTime <= timeoutTime && !stopImmediately){
+            try {
+                Thread.sleep(Consts.TRACE_CHECK_TERMINAL * 1000);
+                // 如果所有设备线程已经结束,则跳出
+                if (allTaskEnd = allTaskEnd()){
+                    PrintUtil.print("All Devices finish. TraceId: " + traceId, TAG);
+                    break;
+                }
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            sleptTime += Consts.TRACE_CHECK_TERMINAL * 1000;
+        }
+        try {
+            if (allTaskEnd){
+                PrintUtil.print("Trace " + traceId + " is finished for all devices end test!", TAG);
+            }else if (sleptTime > timeoutTime) {
+                PrintUtil.print("Trace " + traceId + " is timeout!", TAG);
+            }else if (stopImmediately){
+                PrintUtil.print("Trace " + traceId + " is stopped manually!", TAG);
+            }
+            // 不管是所有设备结束跳出 or timeout跳出,都设置一下执行线程的结束标志位
+            for (CoverageTest coverageTest: deviceThreads){
+                coverageTest.endTest();
+            }
+            // 等待执行线程最终结束,因为结束标志位结束后,需要等待一些后置操作完成
+            for (CoverageTest coverageTest: deviceThreads){
+                coverageTest.join();
+            }
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        } finally {
+            traceService.updateTraceStatue(traceId, TraceStatus.GEN_REPORT);
+            // 上传截图到OSS
+            File traceDir = new File(AddressUtil.getTraceDir(traceId));
+            if (traceDir.exists()){
+                for (File f: traceDir.listFiles()){
+                    if (f.isDirectory()){
+                        String udid = f.getName();
+                        File screenDir = new File(AddressUtil.getScreenShotPath(traceId, udid));
+                        if (!screenDir.exists()){
+                            continue;
+                        }
+                        for (File screenShot: screenDir.listFiles()){
+                            System.out.println(screenShot);
+                            ossService.uploadFileToTraceDir(screenShot, traceId, screenShot.getName());
+                        }
+                    }
+                }
+            }
+            // TODO 生成报告
+            try {
+                PrintUtil.print("Generate bug report by AllenTian's docker", TAG);
+                String command = "java -jar tasks/AutoTestReportGenerator.jar " + traceId;
+                String result = OsUtil.runCommand(command);
+                if (!"success\n".equals(result)){
+                    PrintUtil.print(result, TAG);
+                    PrintUtil.print("Generate bug report failed, use default report.", TAG);
+                    FileUtils.copyFile(new File(AddressUtil.getMockDataJsonPath()), new File(AddressUtil.getDataJsonPath(traceId)));
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            // 上传data.json文件
+            File data = new File(AddressUtil.getDataJsonPath(traceId));
+            String path = ossService.uploadFileToTraceDir(data, traceId, Consts.REPORT_FILE_NAME);
+            int retryTimes = 0;
+            // 上传失败,重试5次
+            while (path != null && retryTimes < Consts.UPLOAD_OSS_MAX_ATTEMPTS){
+                try {
+                    Thread.sleep(3000);
+                    path = ossService.uploadFileToTraceDir(data, traceId, Consts.REPORT_FILE_NAME);
+                } catch (Exception e) {
+                    e.printStackTrace();
+                    PrintUtil.print(String.format("Trace %s upload data failed %s times.", traceId, retryTimes), TAG);
+                } finally {
+                    retryTimes++;
+                }
+            }
+            apkService.updateTraceEndTime(traceId);
+            if (path != null){
+                PrintUtil.print(String.format("Trace %s's data upload success.", traceId), TAG);
+            }else {
+                PrintUtil.print(String.format("Trace %s's data upload failed after tried 5 times.", traceId), TAG);
+            }
+            PrintUtil.print(String.format("Trace %s is done!", traceId), TAG);
+            traceService.updateTraceStatue(traceId, TraceStatus.FINISH);
+        }
+    }
+
+    /**
+     * 解析报告内容,上传截图
+     * @param dataJson data.json文件
+     */
+    private void parseDataJson(File dataJson, String traceId){
+        try {
+            String content = FileUtils.readFileToString(dataJson, "UTF-8");
+            JSONObject json = JSON.parseObject(content);
+            JSONArray bugList = json.getJSONArray("bugList");
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * @return 该trace下所有任务是否都已完成
+     */
+    private boolean allTaskEnd(){
+        for (CoverageTest coverageTest: deviceThreads){
+            if (!coverageTest.isFinalEnd()){
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public void stopImmediately(){
+        this.stopImmediately = true;
+    }
+}

+ 10 - 0
src/main/java/net/mooctest/www/android_auto_test/vo/DeviceStatusResult.java

@@ -0,0 +1,10 @@
+package net.mooctest.www.android_auto_test.vo;
+
+import lombok.Data;
+
+@Data
+public class DeviceStatusResult {
+    private String deviceId;
+    private int statusCode;
+    private String description;
+}

+ 13 - 0
src/main/java/net/mooctest/www/android_auto_test/vo/TraceMetaInfo.java

@@ -0,0 +1,13 @@
+package net.mooctest.www.android_auto_test.vo;
+
+import lombok.Data;
+
+/**
+ * @author henrylee
+ */
+@Data
+public class TraceMetaInfo {
+    private String downloadUrl;
+    private String traceId;
+    private int limitTime = 20;
+}

+ 16 - 0
src/main/java/net/mooctest/www/android_auto_test/vo/TraceStatusResult.java

@@ -0,0 +1,16 @@
+package net.mooctest.www.android_auto_test.vo;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.Data;
+
+/**
+ * @author henrylee
+ */
+@Data
+public class TraceStatusResult {
+    private String traceId;
+    private int statusCode;
+    private String description;
+    private String downloadUrl;
+    private JSONObject extraInfo;
+}

+ 26 - 0
src/main/resources/application.yaml

@@ -0,0 +1,26 @@
+demo: true
+spring.profiles.active: dev
+
+---
+# 开发环境
+spring:
+    profiles: dev
+    redis:
+        host: 127.0.0.1
+        password:
+        pool:
+            max-active: 8
+            max-idle: 8
+            max-wait: -1
+            min-idle: 0
+        port: 6379
+
+server:
+    port: 15926
+
+oss:
+    accessKeyId: IvS323TIcWUT57MG
+    endPoint: http://oss-cn-shanghai.aliyuncs.com
+    accessKeySecret: dYml7rvT8stQkoSjMYlfRTxNj9dEsI
+    bucketName: mooctest-enterprise-site
+

+ 13 - 0
src/test/java/net/mooctest/www/android_auto_test/AndroidAutoTestApplicationTests.java

@@ -0,0 +1,13 @@
+package net.mooctest.www.android_auto_test;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class AndroidAutoTestApplicationTests {
+
+	@Test
+	void contextLoads() {
+	}
+
+}

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików