diff --git a/.gitignore b/.gitignore
new file mode 100755
index 0000000..4ca0b81
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+*.iml
+/.idea
+.gradle
+/local.properties
+.DS_Store
+/build
+/captures
+.externalNativeBuild
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..96f8a1a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,106 @@
+# Colibri
+Colibri is an android library for autotesting UI.
+
+Uses UiAutomator and Espresso.
+
+![Colibri](assets/colibri.gif)
+
+## Gradle Dependency
+
+Add it in your root build.gradle at the end of repositories:
+
+````java
+allprojects {
+ repositories {
+ ...
+ maven { url "https://jitpack.io"}
+ }
+}
+````
+
+Add the dependency:
+
+````java
+dependencies {
+ androidTestImplementation 'com.github.kernel0x.colibri:1.0.0'
+}
+````
+
+## How to use
+
+In androidTest create a class inheritable from ColibriTest or in already created class initialize class Colibri.
+
+````java
+class SampleColibriTest : ColibriTest() {
+ override fun getCondition(): Condition {
+ return Condition.Builder()
+ .randomInputText(arrayOf("borscht", "vodka", "bear"))
+ .pause(Duration(500, TimeUnit.MILLISECONDS))
+ .build()
+ }
+
+ override fun getStrategy(): Strategy {
+ return Monkey()
+ }
+
+ @Test
+ fun colibriTest() {
+ launch()
+ }
+}
+````
+OR
+
+````java
+class SampleColibriTest {
+
+ @get:Rule
+ var permissionRule = GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.READ_EXTERNAL_STORAGE)
+
+ @Test
+ fun colibriTest() {
+ Colibri.condition(Condition.Builder()
+ .randomInputText(arrayOf("borscht", "vodka", "bear"))
+ .pause(Duration(500, TimeUnit.MILLISECONDS)).build())
+ .strategy(Monkey())
+ .launch()
+ }
+}
+````
+
+Everything is simple. Now you can run the test!
+
+## How to works
+
+Colibri runs throughout the app, analyzing UI elements on each screen.
+
+You can set different testing strategies and conditions. All conditions are available in Condition.Builder()
+
+You can create custom behavior that will be executed at each step (for example for authorization). Example:
+````java
+.addCustomBehavior(CustomBehavior {
+ if (getCurrentActivity().localClassName.equals(LoginActivity::class.java.canonicalName)) {
+ try {
+ onView(withId(R.id.text_username)).perform(setTextInEditText("mylogin"))
+ onView(withId(R.id.text_password)).perform(setTextInEditText("qwerty"))
+ onView(withId(R.id.button_login)).perform(click())
+ Thread.sleep(Duration.FIVE_SECONDS.valueAsMs)
+ } catch (e: Exception) {}
+ }
+ })
+````
+
+## Features
+
+* condition configuration
+* different behavioral strategies
+* customization of behavior
+* logging and screenshots
+
+## Try it
+
+Check out the [sample project](/sample) to try it yourself! :wink:
+
+## Releases
+
+Checkout the [Releases](https://github.com/kernel0x/colibri/releases) tab for all release info.
diff --git a/assets/colibri.gif b/assets/colibri.gif
new file mode 100644
index 0000000..b726ee6
Binary files /dev/null and b/assets/colibri.gif differ
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..aa90f74
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,25 @@
+apply from: 'dependencies.gradle'
+
+buildscript {
+ ext.kotlin_version = '1.3.40'
+ repositories {
+ jcenter()
+ google()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.4.2'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ jcenter()
+ google()
+ maven { url 'https://jitpack.io' }
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/dependencies.gradle b/dependencies.gradle
new file mode 100644
index 0000000..9d9ecc8
--- /dev/null
+++ b/dependencies.gradle
@@ -0,0 +1,9 @@
+ext.versions = [
+ minSdk : 18,
+ targetSdk : 29,
+ compileSdk : 29,
+ buildTools : '29.0.0',
+
+ kotlin_version : '1.3.40'
+]
+
diff --git a/gradle.properties b/gradle.properties
new file mode 100755
index 0000000..474de07
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,2 @@
+VERSION_NAME=1.0.0
+VERSION_CODE=1
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e2597c5
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Jul 15 18:41:36 MSK 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+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
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+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
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100755
index 0000000..f955316
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/library/.gitignore b/library/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/library/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/library/build.gradle b/library/build.gradle
new file mode 100644
index 0000000..1c29ed2
--- /dev/null
+++ b/library/build.gradle
@@ -0,0 +1,23 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion versions.compileSdk
+ buildToolsVersion versions.buildTools
+
+ defaultConfig {
+ minSdkVersion versions.minSdk
+ targetSdkVersion versions.compileSdk
+ versionCode 1
+ versionName "1.0"
+ }
+}
+
+dependencies {
+ implementation 'com.android.support:appcompat-v7:28.0.0'
+ implementation 'com.android.support.test:rules:1.0.2'
+ implementation 'com.android.support.test:runner:1.0.2'
+ implementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+ implementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin_version"
+}
\ No newline at end of file
diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/library/src/androidTest/AndroidManifest.xml b/library/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..dd78e16
--- /dev/null
+++ b/library/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..b4b5c2f
--- /dev/null
+++ b/library/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
diff --git a/library/src/main/java/com/kernel/colibri/Colibri.kt b/library/src/main/java/com/kernel/colibri/Colibri.kt
new file mode 100644
index 0000000..9441ca9
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/Colibri.kt
@@ -0,0 +1,41 @@
+package com.kernel.colibri
+
+import android.support.test.InstrumentationRegistry
+import android.util.Log
+import com.kernel.colibri.core.models.Condition
+import com.kernel.colibri.core.Config.TAG
+import com.kernel.colibri.core.strategy.Monkey
+import com.kernel.colibri.core.strategy.Strategy
+
+object Colibri {
+
+ var packageName = InstrumentationRegistry.getContext().packageName.replace(".test", "")
+ private var condition = Condition.Builder().build()
+ private var strategy: Strategy = Monkey()
+
+ fun condition(condition: Condition): Colibri {
+ this.condition = condition
+ this.strategy.condition = condition
+ return this
+ }
+
+ fun strategy(strategy: Strategy): Colibri {
+ this.strategy = strategy
+ this.strategy.condition = condition
+ return this
+ }
+
+ fun packageName(packageName: String): Colibri {
+ this.packageName = packageName
+ return this
+ }
+
+ fun launch() {
+ try {
+ strategy.run()
+ } catch (e: IllegalStateException) {
+ Log.e(TAG, "Don't panic! Something went wrong:" + e.message)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/library/src/main/java/com/kernel/colibri/ColibriTest.kt b/library/src/main/java/com/kernel/colibri/ColibriTest.kt
new file mode 100755
index 0000000..3361053
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/ColibriTest.kt
@@ -0,0 +1,38 @@
+package com.kernel.colibri
+
+import android.support.test.rule.GrantPermissionRule
+import com.kernel.colibri.core.models.Condition
+import com.kernel.colibri.core.Utils.saveLogcat
+import com.kernel.colibri.core.strategy.Strategy
+import org.junit.After
+import org.junit.Rule
+
+abstract class ColibriTest {
+
+ @get:Rule
+ var permissionRule = GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.READ_EXTERNAL_STORAGE)
+
+ abstract fun getCondition(): Condition
+
+ abstract fun getStrategy(): Strategy
+
+ open fun launch() {
+ Colibri.condition(getCondition())
+ .strategy(getStrategy())
+ .launch()
+ }
+
+ open fun launch(packageName: String) {
+ Colibri.condition(getCondition())
+ .strategy(getStrategy())
+ .packageName(packageName)
+ .launch()
+ }
+
+ @After
+ @Throws(Exception::class)
+ fun tearDown() {
+ saveLogcat()
+ }
+
+}
diff --git a/library/src/main/java/com/kernel/colibri/core/Config.kt b/library/src/main/java/com/kernel/colibri/core/Config.kt
new file mode 100755
index 0000000..27a47ed
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/Config.kt
@@ -0,0 +1,25 @@
+package com.kernel.colibri.core
+
+import android.os.Environment
+import com.kernel.colibri.Colibri
+import com.kernel.colibri.core.models.Duration
+import java.io.File
+import java.util.concurrent.TimeUnit
+
+object Config {
+ const val TAG = "Colibri"
+ val FILE_OUTPUT = File(String.format("%s/%s/%s", Environment.getExternalStorageDirectory(), TAG, Colibri.packageName))
+ val LOG_FILE_NAME = "$FILE_OUTPUT/$TAG.log"
+ val TECH_LOG_FILE_NAME = "$FILE_OUTPUT/Performance.csv"
+ val COMMON_BUTTONS = arrayOf("OK", "Cancel", "Yes", "No")
+ val IGNORED_ACTIVITY = arrayOf()
+ val RANDOM_INPUT_TEXT = arrayOf()
+ val MAX_STEPS = Int.MAX_VALUE
+ val MAX_DEPTH = 50
+ var MAX_RUNTIME = Duration(5, TimeUnit.HOURS)
+ var MAX_SCREEN_LOOP = Int.MAX_VALUE
+ val TIME_PAUSE = Duration.ONE_SECOND
+ val TIME_LAUNCH = Duration.FIVE_SECONDS
+ var CAPTURE_STEPS = false
+ val MAX_SCREENSHOTS = 100
+}
diff --git a/library/src/main/java/com/kernel/colibri/core/FileLog.kt b/library/src/main/java/com/kernel/colibri/core/FileLog.kt
new file mode 100755
index 0000000..412f5a8
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/FileLog.kt
@@ -0,0 +1,56 @@
+package com.kernel.colibri.core
+
+import android.util.Log
+import com.kernel.colibri.core.Config.LOG_FILE_NAME
+import java.io.BufferedWriter
+import java.io.FileWriter
+import java.io.IOException
+import java.io.PrintWriter
+import java.text.SimpleDateFormat
+import java.util.*
+
+object FileLog {
+ @Synchronized
+ private fun write(type: Char, tag: String, msg: String) {
+ val sdf = SimpleDateFormat("MM-dd HH:mm:ss")
+ var writer: PrintWriter? = null
+ try {
+ writer = PrintWriter(BufferedWriter(FileWriter(LOG_FILE_NAME, true)))
+ writer.println(String.format("%s %c/%s: %s", sdf.format(Date()), type, tag, msg))
+ } catch (e: IOException) {
+ // well now sad
+ } finally {
+ writer?.close()
+ }
+ }
+
+ @JvmStatic
+ fun v(tag: String, msg: String): Int {
+ write('v', tag, msg)
+ return Log.v(tag, msg)
+ }
+
+ @JvmStatic
+ fun w(tag: String, msg: String): Int {
+ write('w', tag, msg)
+ return Log.w(tag, msg)
+ }
+
+ @JvmStatic
+ fun e(tag: String, msg: String): Int {
+ write('e', tag, msg)
+ return Log.e(tag, msg)
+ }
+
+ @JvmStatic
+ fun i(tag: String, msg: String): Int {
+ write('i', tag, msg)
+ return Log.i(tag, msg)
+ }
+
+ @JvmStatic
+ fun d(tag: String, msg: String): Int {
+ write('d', tag, msg)
+ return Log.d(tag, msg)
+ }
+}
diff --git a/library/src/main/java/com/kernel/colibri/core/Utils.kt b/library/src/main/java/com/kernel/colibri/core/Utils.kt
new file mode 100644
index 0000000..39d89aa
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/Utils.kt
@@ -0,0 +1,65 @@
+package com.kernel.colibri.core
+
+import android.app.Activity
+import android.support.test.InstrumentationRegistry
+import android.support.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
+import android.support.test.runner.lifecycle.Stage
+import android.view.View
+import org.hamcrest.Description
+import org.hamcrest.Matcher
+import org.hamcrest.TypeSafeMatcher
+import java.io.File
+import java.io.IOException
+import java.util.*
+
+object Utils {
+ fun getRandomItem(randomText: Array): String {
+ val rand = Random()
+ return randomText[rand.nextInt(randomText.size - 1)]
+ }
+
+ fun getCurrentActivity(): Activity? {
+ var currentActivity: Activity? = null
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ val resumedActivities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED)
+ for (activity in resumedActivities) {
+ currentActivity = activity
+ break
+ }
+ }
+ return currentActivity
+ }
+
+ fun toMatcher(v: View): Matcher {
+ return object : TypeSafeMatcher() {
+ override fun matchesSafely(item: View): Boolean {
+ return item === v
+ }
+
+ override fun describeTo(description: Description) {
+ description.appendText(v.toString())
+ }
+ }
+ }
+
+ fun removeAllDirRecursively(fileOrDirectory: File) {
+ if (fileOrDirectory.isDirectory)
+ for (child in fileOrDirectory.listFiles())
+ removeAllDirRecursively(child)
+
+ fileOrDirectory.delete()
+ }
+
+ fun saveLogcat() {
+ val file = File("${Config.FILE_OUTPUT}/${Config.TAG}.Logcat.log")
+ try {
+ val newFile = file.createNewFile()
+ if (newFile) {
+ val cmd = "logcat -d -s -v time -f " + file.absolutePath + " " + Config.TAG
+ Runtime.getRuntime().exec(cmd)
+ }
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+}
\ No newline at end of file
diff --git a/library/src/main/java/com/kernel/colibri/core/behaviors/BaseBehavior.kt b/library/src/main/java/com/kernel/colibri/core/behaviors/BaseBehavior.kt
new file mode 100644
index 0000000..bcfb4c4
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/behaviors/BaseBehavior.kt
@@ -0,0 +1,5 @@
+package com.kernel.colibri.core.behaviors
+
+abstract class BaseBehavior() : Behavior {
+
+}
\ No newline at end of file
diff --git a/library/src/main/java/com/kernel/colibri/core/behaviors/Behavior.kt b/library/src/main/java/com/kernel/colibri/core/behaviors/Behavior.kt
new file mode 100644
index 0000000..365b6d5
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/behaviors/Behavior.kt
@@ -0,0 +1,5 @@
+package com.kernel.colibri.core.behaviors
+
+interface Behavior {
+ fun run()
+}
\ No newline at end of file
diff --git a/library/src/main/java/com/kernel/colibri/core/behaviors/CustomBehavior.kt b/library/src/main/java/com/kernel/colibri/core/behaviors/CustomBehavior.kt
new file mode 100644
index 0000000..d4051ab
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/behaviors/CustomBehavior.kt
@@ -0,0 +1,7 @@
+package com.kernel.colibri.core.behaviors
+
+class CustomBehavior(private val listener: () -> Unit) : BaseBehavior() {
+ override fun run() {
+ listener.invoke()
+ }
+}
\ No newline at end of file
diff --git a/library/src/main/java/com/kernel/colibri/core/helpers/ScreenshotHelper.kt b/library/src/main/java/com/kernel/colibri/core/helpers/ScreenshotHelper.kt
new file mode 100644
index 0000000..ffe1614
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/helpers/ScreenshotHelper.kt
@@ -0,0 +1,45 @@
+package com.kernel.colibri.core.helpers
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.uiautomator.UiDevice
+import com.kernel.colibri.core.Config
+import com.kernel.colibri.core.Config.FILE_OUTPUT
+import com.kernel.colibri.core.Config.TAG
+import com.kernel.colibri.core.FileLog
+import java.io.File
+
+object ScreenshotHelper {
+ var screenshotIndex = 0
+ lateinit var lastFilename: String
+
+ fun takeScreenshots(message: String) {
+ var message = message
+ if (message.length > 50) {
+ message = message.substring(0, 49)
+ }
+
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ device.waitForIdle(Config.TIME_PAUSE.valueAsMs)
+
+ var activity = device.currentActivityName ?: "Unknown"
+ if (activity.length > 30) {
+ activity = activity.substring(0, 29)
+ }
+
+ lastFilename = if (message.length > 0) {
+ String.format("(%d) %s %s.png",
+ screenshotIndex, toValidFileName(activity), toValidFileName(message))
+ } else {
+ String.format("(%d) %s.png",
+ screenshotIndex, toValidFileName(activity))
+ }
+
+ device.takeScreenshot(File("$FILE_OUTPUT/$lastFilename"))
+ screenshotIndex++
+ FileLog.i(TAG, "{Screenshot} $lastFilename")
+ }
+
+ fun toValidFileName(input: String?): String {
+ return input?.replace("[:\\\\/*\"?|<>']".toRegex(), "_") ?: ""
+ }
+}
\ No newline at end of file
diff --git a/library/src/main/java/com/kernel/colibri/core/helpers/UiWatcherHelper.kt b/library/src/main/java/com/kernel/colibri/core/helpers/UiWatcherHelper.kt
new file mode 100755
index 0000000..bb4275d
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/helpers/UiWatcherHelper.kt
@@ -0,0 +1,125 @@
+package com.kernel.colibri.core.helpers
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.uiautomator.*
+import android.util.Log
+import com.kernel.colibri.core.Config.TAG
+import java.util.*
+
+class UiWatcherHelper {
+
+ private val errors = ArrayList()
+
+ fun registerAnrAndCrashWatchers() {
+ device.registerWatcher("ANR") { handleAnr() }
+ device.registerWatcher("ANR2") { handleAnr2() }
+ device.registerWatcher("CRASH") { handleCrash() }
+ device.registerWatcher("CRASH2") { handleCrash2() }
+ }
+
+ fun handleAnr(): Boolean {
+ val window = device.findObject(UiSelector()
+ .className("com.android.server.am.AppNotRespondingDialog"))
+ var errorText: String? = null
+ if (window.exists()) {
+ try {
+ errorText = window.text
+ } catch (e: UiObjectNotFoundException) {
+ // don't care, ignore
+ }
+
+ onAnrDetected(errorText)
+ postHandler()
+ return true
+ }
+ return false
+ }
+
+ fun handleAnr2(): Boolean {
+ val window = device.findObject(UiSelector().packageName("android")
+ .textContains("isn't responding."))
+ if (window.exists()) {
+ var errorText: String? = null
+ try {
+ errorText = window.text
+ } catch (e: UiObjectNotFoundException) {
+ // don't care, ignore
+ }
+
+ onAnrDetected(errorText)
+ postHandler()
+ return true
+ }
+ return false
+ }
+
+ fun handleCrash(): Boolean {
+ val window = device.findObject(UiSelector()
+ .className("com.android.server.am.AppErrorDialog"))
+ if (window.exists()) {
+ var errorText: String? = null
+ try {
+ errorText = window.text
+ } catch (e: UiObjectNotFoundException) {
+ // don't care, ignore
+ }
+
+ onCrashDetected(errorText)
+ postHandler()
+ return true
+ }
+ return false
+ }
+
+ fun handleCrash2(): Boolean {
+ val window = device.findObject(UiSelector().packageName("android")
+ .textContains("has stopped"))
+ if (window.exists()) {
+ var errorText: String? = null
+ try {
+ errorText = window.text
+ } catch (e: UiObjectNotFoundException) {
+ // don't care, ignore
+ }
+
+ onCrashDetected(errorText)
+ postHandler()
+ return true
+ }
+ return false
+ }
+
+ fun onAnrDetected(errorText: String?) {
+ errorText?.let { errors.add(it) }
+ }
+
+ fun onCrashDetected(errorText: String?) {
+ errorText?.let { errors.add(it) }
+ }
+
+ fun reset() {
+ errors.clear()
+ }
+
+ fun getErrors(): List {
+ return Collections.unmodifiableList(errors)
+ }
+
+ fun postHandler() {
+ val formatedOutput = String.format("UI Exception Message: %-20s\n", device.currentPackageName)
+ Log.e(TAG, formatedOutput)
+
+ val buttonOK = device.findObject(UiSelector().text("OK").enabled(true))
+
+ try {
+ buttonOK.waitForExists(5000)
+ buttonOK.click()
+ } catch (e: UiObjectNotFoundException) {
+ // don't care, ignore
+ }
+ }
+
+ companion object {
+ private var device: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ }
+}
\ No newline at end of file
diff --git a/library/src/main/java/com/kernel/colibri/core/models/Condition.kt b/library/src/main/java/com/kernel/colibri/core/models/Condition.kt
new file mode 100644
index 0000000..2f34644
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/models/Condition.kt
@@ -0,0 +1,51 @@
+package com.kernel.colibri.core.models
+
+import com.kernel.colibri.core.Config
+import com.kernel.colibri.core.behaviors.CustomBehavior
+import java.util.*
+import kotlin.collections.ArrayList
+
+class Condition(
+ val timePause: Duration,
+ val randomInputText: Array,
+ val ignoredActivity: Array,
+ val maxSteps: Int,
+ val maxDepth: Int,
+ val maxRuntime: Duration,
+ val maxScreenLoop: Int,
+ val maxScreenshots: Int,
+ val listCustomBehavior: ArrayList,
+ val captureSteps: Boolean,
+ val commonButtons: Array) {
+
+ data class Builder(
+ private var timePause: Duration = Config.TIME_PAUSE,
+ private var randomInputText: Array = Config.RANDOM_INPUT_TEXT,
+ private var ignoredActivity: Array = Config.IGNORED_ACTIVITY,
+ private var maxSteps: Int = Config.MAX_STEPS,
+ private var maxDepth: Int = Config.MAX_DEPTH,
+ private var maxRuntime: Duration = Config.MAX_RUNTIME,
+ private var maxScreenLoop: Int = Config.MAX_SCREEN_LOOP,
+ private var maxScreenshots: Int = Config.MAX_SCREENSHOTS,
+ private var listCustomBehavior: ArrayList = ArrayList(),
+ private var captureSteps: Boolean = Config.CAPTURE_STEPS,
+ private var commonButtons: Array = Config.COMMON_BUTTONS) {
+
+ fun pause(timePause: Duration) = apply { this.timePause = timePause }
+ fun randomInputText(randomInputText: Array) = apply { this.randomInputText = randomInputText }
+ fun ignoredActivity(ignoredActivity: Array) = apply { this.ignoredActivity = ignoredActivity }
+ fun maxSteps(maxSteps: Int) = apply { this.maxSteps = maxSteps }
+ fun maxDepth(maxDepth: Int) = apply { this.maxDepth = maxDepth }
+ fun maxRuntime(maxRuntime: Duration) = apply { this.maxRuntime = maxRuntime }
+ fun maxScreenLoop(maxScreenLoop: Int) = apply { this.maxScreenLoop = maxScreenLoop }
+ fun maxScreenshots(maxScreenshots: Int) = apply { this.maxScreenshots = maxScreenshots }
+ fun addCustomBehavior(customBehavior: CustomBehavior) = apply { this.listCustomBehavior.add(customBehavior) }
+ fun captureSteps(captureSteps: Boolean) = apply { this.captureSteps = captureSteps }
+ fun commonButtons(commonButtons: Array) = apply { this.commonButtons = commonButtons }
+ fun build() = Condition(timePause, randomInputText, ignoredActivity, maxSteps, maxDepth, maxRuntime, maxScreenLoop, maxScreenshots, listCustomBehavior, captureSteps, commonButtons)
+ }
+
+ override fun toString(): String {
+ return "Condition(timePause=$timePause, randomInputText=${Arrays.toString(randomInputText)}, ignoredActivity=${Arrays.toString(ignoredActivity)}, maxSteps=$maxSteps, maxDepth=$maxDepth, maxRuntime=$maxRuntime, maxScreenLoop=$maxScreenLoop, maxScreenshots=$maxScreenshots, listCustomBehavior=$listCustomBehavior, captureSteps=$captureSteps, commonButtons=${Arrays.toString(commonButtons)})"
+ }
+}
\ No newline at end of file
diff --git a/library/src/main/java/com/kernel/colibri/core/models/Duration.kt b/library/src/main/java/com/kernel/colibri/core/models/Duration.kt
new file mode 100644
index 0000000..50aec94
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/models/Duration.kt
@@ -0,0 +1,206 @@
+package com.kernel.colibri.core.models
+
+import java.util.concurrent.TimeUnit
+
+class Duration(val value: Long, val timeUnit: TimeUnit) : Comparable {
+
+ init {
+ if (value < 0) {
+ throw IllegalArgumentException("value must be >= 0, was $value")
+ }
+ }
+
+ val timeUnitAsString: String
+ get() = timeUnit.toString().toLowerCase()
+
+ val valueAsMs: Long
+ get() = if (value == NONE.toLong()) {
+ value
+ } else
+ TimeUnit.MILLISECONDS.convert(value, timeUnit)
+
+ val isForever: Boolean get() = this === FOREVER
+
+ val isZero: Boolean get() = this == ZERO
+
+ fun valueAs(timeUnit: TimeUnit) {
+ timeUnit.convert(value, timeUnit)
+ }
+
+ operator fun plus(amount: Long): Duration {
+ return Plus().apply(this, Duration(amount, timeUnit))
+ }
+
+ fun plus(amount: Long, timeUnit: TimeUnit): Duration {
+ return Plus().apply(this, Duration(amount, timeUnit))
+ }
+
+ operator fun plus(duration: Duration): Duration {
+ return Plus().apply(this, duration)
+ }
+
+ fun multiply(amount: Long): Duration {
+ return Multiply().apply(this, Duration(amount, timeUnit))
+ }
+
+ fun divide(amount: Long): Duration {
+ return Divide().apply(this, Duration(amount, timeUnit))
+ }
+
+ operator fun minus(amount: Long): Duration {
+ return Minus().apply(this, Duration(amount, timeUnit))
+ }
+
+ fun minus(amount: Long, timeUnit: TimeUnit): Duration {
+ return Minus().apply(this, Duration(amount, timeUnit))
+ }
+
+ operator fun minus(duration: Duration): Duration {
+ return Minus().apply(this, duration)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || javaClass != other.javaClass) return false
+ val duration = other as Duration
+ return valueAsMs == duration.valueAsMs
+ }
+
+ override fun hashCode(): Int {
+ var result = (value xor value.ushr(32)).toInt()
+ result = 31 * result + timeUnit.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "Duration{" +
+ "timeUnit=" + timeUnit +
+ ", value=" + value +
+ '}'.toString()
+ }
+
+ override fun compareTo(other: Duration): Int {
+ val x = valueAsMs
+ val y = other.valueAsMs
+ return if (x < y) -1 else if (x == y) 0 else 1
+ }
+
+ private abstract class BiFunction {
+ fun apply(lhs: Duration?, rhs: Duration?): Duration {
+ if (lhs == null || rhs == null) {
+ throw IllegalArgumentException("Duration cannot be null")
+ }
+
+ val specialDuration = handleSpecialCases(lhs, rhs)
+ if (specialDuration != null) {
+ return specialDuration
+ }
+
+ val newDuration: Duration
+ newDuration = when {
+ lhs.timeUnit.ordinal > rhs.timeUnit.ordinal -> {
+ val lhsConverted = rhs.timeUnit.convert(lhs.value, lhs.timeUnit)
+ Duration(apply(lhsConverted, rhs.value), rhs.timeUnit)
+ }
+ lhs.timeUnit.ordinal < rhs.timeUnit.ordinal -> {
+ val rhsConverted = lhs.timeUnit.convert(rhs.value, rhs.timeUnit)
+ Duration(apply(lhs.value, rhsConverted), lhs.timeUnit)
+ }
+ else ->
+ Duration(apply(lhs.value, rhs.value), lhs.timeUnit)
+ }
+
+ return newDuration
+ }
+
+ protected abstract fun handleSpecialCases(lhs: Duration, rhs: Duration): Duration?
+
+ internal abstract fun apply(operand1: Long, operand2: Long): Long
+ }
+
+ private class Plus : BiFunction() {
+ override fun handleSpecialCases(lhs: Duration, rhs: Duration): Duration? {
+ if (ZERO == rhs) {
+ return lhs
+ } else if (ZERO == lhs) {
+ return rhs
+ } else if (lhs === FOREVER || rhs === FOREVER) {
+ return FOREVER
+ }
+ return null
+ }
+
+ override fun apply(operand1: Long, operand2: Long): Long {
+ return operand1 + operand2
+ }
+ }
+
+ private class Minus : BiFunction() {
+ override fun handleSpecialCases(lhs: Duration, rhs: Duration): Duration? {
+ if (!lhs.isZero && rhs.isZero) {
+ return lhs
+ } else if (lhs === FOREVER) {
+ return FOREVER
+ } else if (rhs === FOREVER) {
+ return ZERO
+ } else if (FOREVER == rhs) {
+ return ZERO
+ }
+ return null
+ }
+
+ override fun apply(operand1: Long, operand2: Long): Long {
+ return operand1 - operand2
+ }
+ }
+
+ private class Multiply : BiFunction() {
+ override fun handleSpecialCases(lhs: Duration, rhs: Duration): Duration? {
+ if (lhs.isZero || rhs.isZero) {
+ return ZERO
+ } else if (lhs === FOREVER || rhs === FOREVER) {
+ return FOREVER
+ }
+ return null
+ }
+
+ override fun apply(operand1: Long, operand2: Long): Long {
+ return operand1 * operand2
+ }
+ }
+
+ private class Divide : BiFunction() {
+ override fun handleSpecialCases(lhs: Duration, rhs: Duration): Duration? {
+ if (lhs === FOREVER) {
+ return FOREVER
+ } else if (rhs === FOREVER) {
+ throw IllegalArgumentException("Cannot divide by infinity")
+ } else if (ZERO == lhs) {
+ return ZERO
+ }
+ return null
+ }
+
+ override fun apply(operand1: Long, operand2: Long): Long {
+ return operand1 / operand2
+ }
+ }
+
+ companion object {
+ val FOREVER = Duration(java.lang.Long.MAX_VALUE, TimeUnit.DAYS)
+ val ZERO = Duration(0, TimeUnit.MILLISECONDS)
+ val ONE_MILLISECOND = Duration(1, TimeUnit.MILLISECONDS)
+ val ONE_HUNDRED_MILLISECONDS = Duration(100, TimeUnit.MILLISECONDS)
+ val TWO_HUNDRED_MILLISECONDS = Duration(200, TimeUnit.MILLISECONDS)
+ val FIVE_HUNDRED_MILLISECONDS = Duration(500, TimeUnit.MILLISECONDS)
+ val ONE_SECOND = Duration(1, TimeUnit.SECONDS)
+ val TWO_SECONDS = Duration(2, TimeUnit.SECONDS)
+ val FIVE_SECONDS = Duration(5, TimeUnit.SECONDS)
+ val TEN_SECONDS = Duration(10, TimeUnit.SECONDS)
+ val ONE_MINUTE = Duration(60, TimeUnit.SECONDS)
+ val TWO_MINUTES = Duration(120, TimeUnit.SECONDS)
+ val FIVE_MINUTES = Duration(300, TimeUnit.SECONDS)
+ val TEN_MINUTES = Duration(600, TimeUnit.SECONDS)
+ private val NONE = -1
+ }
+}
\ No newline at end of file
diff --git a/library/src/main/java/com/kernel/colibri/core/models/Element.kt b/library/src/main/java/com/kernel/colibri/core/models/Element.kt
new file mode 100755
index 0000000..4fe79d6
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/models/Element.kt
@@ -0,0 +1,15 @@
+package com.kernel.colibri.core.models
+
+import android.support.test.uiautomator.UiObject
+
+/**
+ * UI element that may be interesting during testing
+ */
+class Element(var uiObject: UiObject) : Cloneable {
+ var finished = false
+
+ @Throws(CloneNotSupportedException::class)
+ override fun clone(): Any {
+ return super.clone() as Element
+ }
+}
diff --git a/library/src/main/java/com/kernel/colibri/core/models/Screen.kt b/library/src/main/java/com/kernel/colibri/core/models/Screen.kt
new file mode 100755
index 0000000..9d6b78b
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/models/Screen.kt
@@ -0,0 +1,183 @@
+package com.kernel.colibri.core.models
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.uiautomator.*
+import android.util.Log
+import com.kernel.colibri.Colibri
+import com.kernel.colibri.core.Config.TAG
+import org.hamcrest.CoreMatchers.notNullValue
+import org.junit.Assert.assertThat
+import java.util.*
+
+/**
+ * UI screen that may be interesting during testing
+ */
+class Screen {
+
+ lateinit var device: UiDevice
+ var parentScreen: Screen? = null
+ var parentElement: Element? = null
+ var packageName: String = ""
+ private lateinit var childScreenList: List
+ private lateinit var rootObject: UiObject
+ private lateinit var signature: String
+ lateinit var elementsList: MutableList
+ lateinit var name: String
+ var depth = -1
+ var id = -1
+ var loop = 0
+
+ private var finished = false
+ var isFinished: Boolean
+ get() {
+ if (finished)
+ return finished
+
+ for (child in elementsList) {
+ if (!child.finished)
+ return false
+ }
+ finished = true
+ return finished
+ }
+ set(finished) {
+ this.finished = finished
+
+ if (this.finished) {
+ for (child in elementsList) {
+ child.finished = this.finished
+ }
+ }
+ }
+
+ constructor(parent: Screen?, element: Element?) {
+ device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ val root = device.findObject(UiSelector().index(0))
+ init(parent, element, root)
+ }
+
+ constructor(parent: Screen?, element: Element?, root: UiObject) {
+ init(parent, element, root)
+ }
+
+ fun init(parent: Screen?, element: Element?, root: UiObject) {
+ assertThat(root, notNullValue())
+
+ device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ parentScreen = parent
+ parentElement = element
+ rootObject = root
+ childScreenList = ArrayList()
+ elementsList = ArrayList()
+ packageName = root.packageName
+
+ signature = ""
+ name = device.currentActivityName
+ depth = if (parentScreen == null) 0 else parentScreen!!.depth + 1
+ id = -1
+ finished = false
+
+ initSignature(rootObject)
+
+ if (!packageName.equals(Colibri.packageName, ignoreCase = true)) {
+ return
+ }
+
+ var i = 0
+ var clickable: UiObject?
+ do {
+ clickable = device.findObject(UiSelector().clickable(true).instance(i++))
+ if (clickable != null && clickable.exists())
+ elementsList.add(Element(clickable))
+ } while (clickable != null && clickable.exists())
+
+ if (elementsList.size == 0)
+ finished = true
+ }
+
+ private fun initSignature(uiObject: UiObject?) {
+ val conf = Configurator.getInstance()
+ val waitForIdleTimeout = conf.waitForIdleTimeout
+ val waitForSelectorTimeout = conf.waitForSelectorTimeout
+ val actionAcknowledgmentTimeout = conf.actionAcknowledgmentTimeout
+ val scrollAcknowledgmentTimeout = conf.scrollAcknowledgmentTimeout
+ conf.waitForIdleTimeout = 0L
+ conf.waitForSelectorTimeout = 0L
+ conf.actionAcknowledgmentTimeout = 0L
+ conf.scrollAcknowledgmentTimeout = 0L
+
+ parseSignature(uiObject)
+
+ conf.waitForIdleTimeout = waitForIdleTimeout
+ conf.waitForSelectorTimeout = waitForSelectorTimeout
+ conf.actionAcknowledgmentTimeout = actionAcknowledgmentTimeout
+ conf.scrollAcknowledgmentTimeout = scrollAcknowledgmentTimeout
+ }
+
+ private fun parseSignature(uiObject: UiObject?): Boolean {
+ if (uiObject == null || !uiObject.exists())
+ return false
+
+ if (signature.length > 160)
+ return true
+
+ try {
+ var classname = ""
+ for (tmp in uiObject.className.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
+ classname = tmp
+ }
+ signature = "$signature$classname;"
+
+ for (i in 0 until uiObject.childCount) {
+ parseSignature(uiObject.getChild(UiSelector().index(i)))
+ }
+ } catch (e: UiObjectNotFoundException) {
+ Log.e(TAG, "UiObjectNotFoundException", e)
+ return false
+ }
+
+ return true
+ }
+
+ override fun toString(): String {
+ var str = "name:" + name +
+ ", id:" + id +
+ ", depth:" + depth +
+ ", finished:" + finished +
+ ", signature:" + signature +
+ ", elements:" + elementsList.size
+ for (i in elementsList.indices) {
+ val element = elementsList[i]
+ str += " " + i + ":" + element.finished
+ }
+ return str
+ }
+
+ override fun equals(o: Any?): Boolean {
+ if (o === this) {
+ return true
+ }
+ if (o !is Screen) {
+ return false
+ }
+ val c = o as Screen?
+ return this.signature == c!!.signature
+ }
+
+ override fun hashCode(): Int {
+ var result = device.hashCode()
+ result = 31 * result + (parentScreen?.hashCode() ?: 0)
+ result = 31 * result + (parentElement?.hashCode() ?: 0)
+ result = 31 * result + packageName.hashCode()
+ result = 31 * result + childScreenList.hashCode()
+ result = 31 * result + rootObject.hashCode()
+ result = 31 * result + signature.hashCode()
+ result = 31 * result + elementsList.hashCode()
+ result = 31 * result + name.hashCode()
+ result = 31 * result + depth
+ result = 31 * result + id
+ result = 31 * result + loop
+ result = 31 * result + finished.hashCode()
+ return result
+ }
+}
diff --git a/library/src/main/java/com/kernel/colibri/core/performance/CpuInfo.kt b/library/src/main/java/com/kernel/colibri/core/performance/CpuInfo.kt
new file mode 100644
index 0000000..d5e2fc8
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/performance/CpuInfo.kt
@@ -0,0 +1,63 @@
+package com.kernel.colibri.core.performance
+
+import java.io.BufferedReader
+import java.io.FileInputStream
+import java.io.IOException
+import java.io.InputStreamReader
+
+object CpuInfo {
+
+ private var totalCpuTime1 = -1f
+ private var processCpuTime1 = -1f
+
+ private val totalCpuTime: Long
+ get() {
+ var cpuInfos: Array? = null
+ try {
+ val reader = BufferedReader(InputStreamReader(FileInputStream("/proc/stat")), 1000)
+ val load = reader.readLine()
+ reader.close()
+ cpuInfos = load.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+ } catch (ex: IOException) {
+ ex.printStackTrace()
+ }
+
+ return java.lang.Long.parseLong(cpuInfos!![2]) + java.lang.Long.parseLong(cpuInfos[3]) + java.lang.Long.parseLong(cpuInfos[4]) + java.lang.Long.parseLong(cpuInfos[6]) + java.lang.Long.parseLong(cpuInfos[5]) + java.lang.Long.parseLong(cpuInfos[7]) + java.lang.Long.parseLong(cpuInfos[8])
+ }
+
+ fun getProcessCpuRate(pid: Int): Float {
+
+ if (totalCpuTime1 == -1f) {
+ totalCpuTime1 = totalCpuTime.toFloat()
+ processCpuTime1 = getAppCpuTime(pid).toFloat()
+ try {
+ Thread.sleep(360)
+ } catch (e: Exception) {
+ }
+ }
+
+ val totalCpuTime2 = totalCpuTime.toFloat()
+ val processCpuTime2 = getAppCpuTime(pid).toFloat()
+
+ val cpuRate = 100 * (processCpuTime2 - processCpuTime1) / (totalCpuTime2 - totalCpuTime1)
+
+ totalCpuTime1 = totalCpuTime2
+ processCpuTime1 = processCpuTime2
+
+ return cpuRate
+ }
+
+ fun getAppCpuTime(pid: Int): Long {
+ var cpuInfos: Array? = null
+ try {
+ val reader = BufferedReader(InputStreamReader(FileInputStream("/proc/$pid/stat")), 1000)
+ val load = reader.readLine()
+ reader.close()
+ cpuInfos = load.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+ } catch (ex: IOException) {
+ ex.printStackTrace()
+ }
+
+ return java.lang.Long.parseLong(cpuInfos!![13]) + java.lang.Long.parseLong(cpuInfos[14]) + java.lang.Long.parseLong(cpuInfos[15]) + java.lang.Long.parseLong(cpuInfos[16])
+ }
+}
diff --git a/library/src/main/java/com/kernel/colibri/core/performance/MemInfo.kt b/library/src/main/java/com/kernel/colibri/core/performance/MemInfo.kt
new file mode 100644
index 0000000..d9ade26
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/performance/MemInfo.kt
@@ -0,0 +1,16 @@
+package com.kernel.colibri.core.performance
+
+import android.app.ActivityManager
+import android.content.Context
+import android.support.test.InstrumentationRegistry
+
+object MemInfo {
+ fun getProcessMemInfo(pid: Int): android.os.Debug.MemoryInfo {
+ val context = InstrumentationRegistry.getContext()
+ val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ val pids = IntArray(1)
+ pids[0] = pid
+ val mems = am.getProcessMemoryInfo(pids)
+ return mems[0]
+ }
+}
diff --git a/library/src/main/java/com/kernel/colibri/core/performance/TechLog.kt b/library/src/main/java/com/kernel/colibri/core/performance/TechLog.kt
new file mode 100644
index 0000000..cf5e623
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/performance/TechLog.kt
@@ -0,0 +1,147 @@
+package com.kernel.colibri.core.performance
+
+import android.app.ActivityManager
+import android.content.Context
+import android.os.Build
+import android.support.test.InstrumentationRegistry
+import android.util.Log
+
+import com.kernel.colibri.core.FileLog
+
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.OutputStreamWriter
+import java.io.PrintWriter
+import java.text.SimpleDateFormat
+import java.util.ArrayList
+import java.util.Date
+
+import android.provider.ContactsContract.Directory.PACKAGE_NAME
+import com.kernel.colibri.core.Config.TECH_LOG_FILE_NAME
+import com.kernel.colibri.core.Config.TAG
+
+object TechLog {
+
+ var memList: ArrayList = ArrayList()
+ var cpuList: ArrayList = ArrayList()
+
+ var cpuLast = 0f
+ var cpuPeak = 0f
+ var memLast = 0
+ var memPeak = 0
+
+ val averageCpu: Float
+ get() {
+ if (cpuList.size == 0)
+ return 0f
+
+ var total = 0f
+ var average = 0f
+ for (i in cpuList.indices) {
+ total += cpuList[i]
+ }
+ average = total / cpuList.size
+ return average
+ }
+
+ val averageMemory: Int
+ get() {
+ if (memList.size == 0)
+ return 0
+
+ var total = 0
+ var average = 0
+ for (i in memList.indices) {
+ total += memList[i]
+ }
+ average = total / memList.size
+ return average
+ }
+
+ fun reset() {
+ memList.clear()
+ cpuList.clear()
+ memPeak = 0
+ memLast = 0
+ cpuPeak = 0f
+ cpuLast = 0f
+ }
+
+ @Synchronized
+ fun init() {
+ reset()
+ var writer: PrintWriter? = null
+ try {
+ writer = PrintWriter(OutputStreamWriter(FileOutputStream(TECH_LOG_FILE_NAME, true), "UTF-8"))
+ writer.print("\uFEFF") // byte-order marker (BOM)
+ } catch (e: IOException) {
+ e.printStackTrace()
+ } finally {
+ writer?.close()
+ }
+ writeLog("Time,CPU%,Memory(KB),Screen")
+ }
+
+ @Synchronized
+ fun writeLog(str: String) {
+ var writer: PrintWriter? = null
+ try {
+ writer = PrintWriter(OutputStreamWriter(FileOutputStream(TECH_LOG_FILE_NAME, true), "UTF-8"))
+ writer.print(str + "\r\n")
+ } catch (e: IOException) {
+ e.printStackTrace()
+ } finally {
+ writer?.close()
+ }
+ }
+
+ fun record(screenName: String): Boolean {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ val list = InstrumentationRegistry.getInstrumentation().uiAutomation.windows
+ for (win in list) {
+ Log.i(TAG, win.javaClass.name)
+ Log.i(TAG, win.javaClass.simpleName)
+ Log.i(TAG, win.javaClass.toString())
+ }
+ }
+
+ val context = InstrumentationRegistry.getContext()
+ val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+
+ val appList = am.runningAppProcesses
+ for (app in appList) {
+ if (PACKAGE_NAME.equals(app.processName, ignoreCase = true)) {
+
+ cpuLast = CpuInfo.getProcessCpuRate(app.pid)
+ cpuList.add(cpuLast)
+ if (cpuLast > cpuPeak) cpuPeak = cpuLast
+
+ val mem = MemInfo.getProcessMemInfo(app.pid)
+ memLast = mem.totalPss
+ memList.add(memLast)
+ if (mem.totalPss > memPeak) memPeak = mem.totalPss
+
+ val log = String.format("{Tech} package:%s, cpu:%.1f%%" +
+ ", memory total pss (KB):%d, total private dirty (KB):%d, total shared (KB):%d" +
+ ", dalvik private:%d, dalvik shared:%d, dalvik pss:%d" +
+ ", native private:%d, native shared:%d, native pss:%d" +
+ ", others private:%d, others shared:%d, others pss:%d",
+ app.processName, cpuLast,
+ mem.totalPss, mem.totalPrivateDirty, mem.totalSharedDirty,
+ mem.dalvikPrivateDirty, mem.dalvikSharedDirty, mem.dalvikPss,
+ mem.nativePrivateDirty, mem.nativeSharedDirty, mem.nativePss,
+ mem.otherPrivateDirty, mem.otherSharedDirty, mem.otherPss)
+
+ FileLog.i(TAG, log)
+
+ val sdf = SimpleDateFormat("MM-dd HH:mm:ss")
+ writeLog(String.format("%s,%.1f%%,%d,%s", sdf.format(Date()), cpuLast, memLast, screenName))
+
+ break
+ }
+ }
+
+ return true
+ }
+
+}
diff --git a/library/src/main/java/com/kernel/colibri/core/strategy/BaseStrategy.kt b/library/src/main/java/com/kernel/colibri/core/strategy/BaseStrategy.kt
new file mode 100644
index 0000000..51531be
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/strategy/BaseStrategy.kt
@@ -0,0 +1,212 @@
+package com.kernel.colibri.core.strategy
+
+import android.content.Intent
+import android.os.Bundle
+import android.support.test.InstrumentationRegistry
+import android.support.test.uiautomator.*
+import android.util.Log
+import com.kernel.colibri.Colibri
+import com.kernel.colibri.core.models.Condition
+import com.kernel.colibri.core.Config
+import com.kernel.colibri.core.Config.TAG
+import com.kernel.colibri.core.Config.TIME_LAUNCH
+import com.kernel.colibri.core.FileLog
+import com.kernel.colibri.core.Utils.removeAllDirRecursively
+import com.kernel.colibri.core.helpers.ScreenshotHelper
+import com.kernel.colibri.core.helpers.UiWatcherHelper
+import com.kernel.colibri.core.models.Screen
+import com.kernel.colibri.core.performance.TechLog
+import java.util.*
+
+abstract class BaseStrategy : Strategy {
+
+ override var condition: Condition = Condition.Builder().build()
+
+ private var uiWatcherHelper = UiWatcherHelper()
+
+ val isInTargetApp: Boolean
+ get() {
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ val pkg = device.currentPackageName
+ return pkg != null && pkg.equals(Colibri.packageName, ignoreCase = true)
+ }
+
+ init {
+ removeAllDirRecursively(Config.FILE_OUTPUT)
+ if (!Config.FILE_OUTPUT.exists()) {
+ if (!Config.FILE_OUTPUT.mkdirs()) {
+ Log.d(TAG, "Failed to create screenshot folder: " + Config.FILE_OUTPUT.path)
+ }
+ }
+
+ TechLog.init()
+
+ val conf = Configurator.getInstance()
+ conf.actionAcknowledgmentTimeout = 200L
+ conf.scrollAcknowledgmentTimeout = 100L
+ conf.waitForIdleTimeout = 0L
+ conf.waitForSelectorTimeout = 0L
+ logConfiguration()
+
+ uiWatcherHelper.registerAnrAndCrashWatchers()
+ }
+
+ override fun run() {
+ launchHome()
+ pause()
+ }
+
+ protected fun pause() {
+ if (!condition.timePause.isZero) {
+ Thread.sleep(condition.timePause.valueAsMs)
+ }
+ }
+
+ fun launchTargetApp(): Boolean {
+ return launchApp(Colibri.packageName)
+ }
+
+ private fun launchApp(targetPackage: String): Boolean {
+ FileLog.i(TAG, "{Launch} $targetPackage")
+
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ val launcherPackage = device.launcherPackageName
+ if (launcherPackage.equals(targetPackage, ignoreCase = true)) {
+ launchHome()
+ return true
+ }
+ val context = InstrumentationRegistry.getContext()
+ val intent = context.packageManager.getLaunchIntentForPackage(targetPackage)
+ if (intent != null) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ context.startActivity(intent)
+ device.wait(Until.hasObject(By.pkg(Colibri.packageName).depth(0)), TIME_LAUNCH.valueAsMs)
+ } else {
+ val err = String.format("(%s) No launchable Activity.\n", targetPackage)
+ Log.e(TAG, err)
+ val bundle = Bundle()
+ bundle.putString("ERROR", err)
+ InstrumentationRegistry.getInstrumentation().finish(1, bundle)
+ }
+ return true
+ }
+
+ fun launchHome() {
+ FileLog.i(TAG, "{Press} Home")
+
+ val uidevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ uidevice.pressHome()
+ val launcherPackage = uidevice.launcherPackageName
+ uidevice.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), TIME_LAUNCH.valueAsMs)
+ }
+
+ private fun logConfiguration() {
+ val conf = Configurator.getInstance()
+
+ val log = String.format("ActionAcknowledgmentTimeout:%d," +
+ " KeyInjectionDelay:%d, " +
+ "ScrollAcknowledgmentTimeout:%d," +
+ " WaitForIdleTimeout:%d," +
+ " WaitForSelectorTimeout:%d",
+ conf.actionAcknowledgmentTimeout,
+ conf.keyInjectionDelay,
+ conf.scrollAcknowledgmentTimeout,
+ conf.waitForIdleTimeout,
+ conf.waitForSelectorTimeout)
+
+ FileLog.i(TAG, log)
+
+ FileLog.i(TAG, "TargetPackage: " + Colibri.packageName + ", " + condition.toString())
+ }
+
+ fun handleAndroidUi(): Boolean {
+ when {
+ uiWatcherHelper.handleAnr() -> {
+ ScreenshotHelper.takeScreenshots("[ANR]")
+ return true
+ }
+ uiWatcherHelper.handleAnr2() -> {
+ ScreenshotHelper.takeScreenshots("[ANR]")
+ return true
+ }
+ uiWatcherHelper.handleCrash() -> {
+ ScreenshotHelper.takeScreenshots("[CRASH]")
+ return true
+ }
+ uiWatcherHelper.handleCrash2() -> {
+ ScreenshotHelper.takeScreenshots("[CRASH]")
+ return true
+ }
+ else -> {
+ // something unknown
+ }
+ }
+
+ return false
+ }
+
+ fun handleCommonDialog(): Boolean {
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ var button: UiObject? = null
+ for (keyword in condition.commonButtons) {
+ button = device.findObject(UiSelector().text(keyword).enabled(true))
+ if (button != null && button.exists()) {
+ break
+ }
+ }
+ try {
+ if (button != null && button.exists()) {
+ button.waitForExists(5000)
+ button.click()
+ Log.i(TAG, "{Click} " + button.text + " Button succeeded")
+ return true
+ }
+ } catch (e: UiObjectNotFoundException) {
+ // don't care, ignore
+ }
+
+ return false
+ }
+
+ fun setRandomInputTextToEditText() {
+ if(condition.randomInputText.isEmpty()) return
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ var edit: UiObject?
+ var i = 0
+ do {
+ edit = device.findObject(UiSelector().className("android.widget.EditText").instance(i++))
+ if (edit != null && edit.exists()) {
+ try {
+ val rand = Random()
+ val text = condition.randomInputText[rand.nextInt(condition.randomInputText.size - 1)]
+ edit.text = text
+ } catch (e: UiObjectNotFoundException) {
+ // it happens
+ }
+
+ }
+ } while (edit != null && edit.exists())
+ }
+
+ fun isIgnoreScreen(screen: Screen): Boolean {
+ return isIgnoreScreen(screen.name)
+ }
+
+ fun isIgnoreScreen(activityName: String): Boolean {
+ condition.ignoredActivity.forEach {
+ if (it.equals(activityName, true)) return true
+ }
+ return false
+ }
+
+ fun isSameScreen(target: Screen): Boolean {
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ val root = device.findObject(UiSelector().packageName(Colibri.packageName))
+ if (root == null || !root.exists()) {
+ Log.e(TAG, "Fail to get screen root object")
+ return false
+ }
+ val current = Screen(null, null, root)
+ return current == target
+ }
+}
\ No newline at end of file
diff --git a/library/src/main/java/com/kernel/colibri/core/strategy/DepthFirst.kt b/library/src/main/java/com/kernel/colibri/core/strategy/DepthFirst.kt
new file mode 100755
index 0000000..89d4290
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/strategy/DepthFirst.kt
@@ -0,0 +1,333 @@
+package com.kernel.colibri.core.strategy
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.uiautomator.UiDevice
+import android.support.test.uiautomator.UiObjectNotFoundException
+import android.util.Log
+import com.kernel.colibri.Colibri
+import com.kernel.colibri.core.Config
+import com.kernel.colibri.core.FileLog
+import com.kernel.colibri.core.helpers.ScreenshotHelper
+import com.kernel.colibri.core.models.Element
+import com.kernel.colibri.core.models.Screen
+import com.kernel.colibri.core.performance.TechLog
+import java.util.*
+
+open class DepthFirst : BaseStrategy() {
+
+ private lateinit var device: UiDevice
+ private lateinit var startTime: Date
+ private var depth = 0
+ private var steps = 0
+ private var depthPeak = 0
+ private var loop = 0
+ private var scannedScreenList = ArrayList()
+ private var rootScreen: Screen? = null
+ private var lastScreen: Screen? = null
+ private var lastInteractedElement: Element? = null
+ private var lastInteractedLog = ""
+ private var finished = false
+
+ private val isCurrentPackage: Boolean
+ get() {
+ val currentScreen = Screen(null, null)
+ return if (currentScreen.packageName.equals(Colibri.packageName, ignoreCase = true)) isNewScreen(currentScreen) else false
+ }
+
+ override fun run() {
+ super.run()
+
+ reset()
+
+ if (!launchTargetApp())
+ return
+
+ while (!finished) {
+ steps++
+
+ if (condition.listCustomBehavior.size > 0) {
+ condition.listCustomBehavior.forEach { it.run() }
+ }
+
+ var currentScreen = Screen(lastScreen, lastInteractedElement)
+ currentScreen.id = scannedScreenList.size + 1
+
+ handleCurrentScreen(currentScreen)
+
+ if (!currentScreen.packageName.equals(Colibri.packageName, true)) {
+ FileLog.i(Config.TAG, "{Inspect} screen, in other package: " + currentScreen.packageName)
+ handleOtherPackage(currentScreen)
+ continue
+ }
+
+ var newScreen = true
+ for (screen in scannedScreenList) {
+ if (screen == currentScreen) {
+ newScreen = false
+ currentScreen = screen
+ depth = currentScreen.depth
+ break
+ }
+ }
+
+ if (depth == 0) {
+ if (rootScreen != null) {
+ Log.i(Config.TAG, "Root screen changed, may be due to app's coachmarks")
+ }
+ rootScreen = currentScreen
+ }
+
+ if (newScreen) {
+ handleNewScreen(currentScreen)
+ loop = 0
+ } else {
+ handleOldScreen(currentScreen)
+ if (++loop > condition.maxScreenLoop) {
+ Log.i(Config.TAG, "Reached max old screen loop, re-launch target app")
+ loop = 0
+ launchTargetApp()
+ continue
+ }
+ }
+
+ if (!currentScreen.isFinished) {
+ var screen: Screen? = currentScreen
+ do {
+ if (screen!!.parentElement != null)
+ screen.parentElement!!.finished = false
+ if (screen.parentScreen != null)
+ screen.parentScreen!!.isFinished = false
+ screen = screen.parentScreen
+ } while (screen != null)
+ }
+
+ setRandomInputTextToEditText()
+
+ handleNextElement(currentScreen)
+
+ if (currentScreen.isFinished) {
+ Log.d(Config.TAG, "Screen[" + currentScreen.id + "] finished")
+
+ var screen: Screen? = currentScreen
+ do {
+ if (screen!!.parentElement != null)
+ screen.parentElement!!.finished = true
+ if (screen.parentScreen != null) {
+ if (!screen.parentScreen!!.isFinished)
+ break
+ }
+ screen = screen.parentScreen
+ } while (screen != null)
+
+ if (currentScreen === rootScreen) {
+ if (isCurrentPackage) {
+ lastInteractedElement?.let { it.finished = false }
+ rootScreen!!.isFinished = false
+ } else {
+ FileLog.i(Config.TAG, "{Stop} root screen finished, id:" + rootScreen!!.id)
+ finished = true
+ }
+ } else {
+ if (isSameScreen(currentScreen)) {
+ FileLog.i(Config.TAG, "{Click} Back")
+ device.pressBack()
+ }
+ }
+ }
+
+ if (Date().time - startTime.time > condition.maxRuntime.valueAsMs) {
+ FileLog.i(Config.TAG, "{Stop} reached max run-time second: " + condition.maxRuntime.value)
+ finished = true
+ }
+
+ if (ScreenshotHelper.screenshotIndex >= Config.MAX_SCREENSHOTS - 1) {
+ FileLog.i(Config.TAG, "{Stop} reached max screenshot files.")
+ finished = true
+ }
+
+ if (steps >= condition.maxSteps) {
+ FileLog.i(Config.TAG, "{Stop} reached max screenshot files.")
+ finished = true
+ }
+
+ if (++currentScreen.loop > condition.maxScreenLoop) {
+ Log.i(Config.TAG, "Reached max screen loop, set screen finished")
+ currentScreen.isFinished = true
+ }
+ }
+
+ FileLog.i(Config.TAG, "Total executed steps:" + steps +
+ ", peak depth:" + depthPeak +
+ ", detected screens:" + scannedScreenList.size +
+ ", screenshot:" + ScreenshotHelper.screenshotIndex)
+
+ val log = String.format("CPU average:%.1f%%, CPU peak:%.1f%%, " + "Memory average (KB):%d, Memory peak (KB):%d",
+ TechLog.averageCpu, TechLog.cpuPeak,
+ TechLog.averageMemory, TechLog.memPeak)
+ FileLog.i(Config.TAG, log)
+ }
+
+ open fun handleCurrentScreen(currentScreen: Screen) {
+ TechLog.record(currentScreen.name)
+ }
+
+ private fun handleNewScreen(currentScreen: Screen) {
+ FileLog.i(Config.TAG, "{Inspect} NEW screen, $currentScreen")
+ lastInteractedLog = ""
+ lastInteractedElement = null
+ ScreenshotHelper.takeScreenshots("")
+
+ currentScreen.depth = ++depth
+ if (depth > depthPeak)
+ depthPeak = depth
+
+ var stop = false
+ if (isIgnoreScreen(currentScreen.name)) {
+ FileLog.i(Config.TAG, "{Inspect} screen, in ignored list: " + currentScreen.name)
+ stop = true
+ }
+ if (depth >= condition.maxDepth) {
+ Log.i(Config.TAG, "Has reached the MaxDepth: " + condition.maxDepth)
+ stop = true
+ }
+
+ if (stop) {
+ currentScreen.elementsList.clear()
+ currentScreen.isFinished = true
+ scannedScreenList.add(currentScreen)
+ FileLog.i(Config.TAG, "{Click} Back")
+ device.pressBack()
+ device.waitForIdle(condition.timePause.valueAsMs)
+ } else {
+ scannedScreenList.add(currentScreen)
+ }
+ }
+
+ private fun handleOldScreen(currentScreen: Screen) {
+ FileLog.i(Config.TAG, "{Inspect} OLD screen, $currentScreen")
+ if (condition.captureSteps) {
+ ScreenshotHelper.takeScreenshots(lastInteractedLog)
+ lastInteractedLog = ""
+ lastInteractedElement = null
+ }
+ }
+
+ private fun handleNextElement(currentScreen: Screen) {
+ val element = getNextElement(currentScreen) ?: return
+
+ var classname = ""
+ var text = ""
+ var desc = ""
+
+ try {
+ classname = element.uiObject.className
+ text = element.uiObject.text
+ if (text.length > 25) {
+ text = text.substring(0, 21) + "..."
+ }
+ } catch (e: UiObjectNotFoundException) {
+ // don't care, ignore
+ }
+
+ try {
+ desc = element.uiObject.contentDescription
+ } catch (e: UiObjectNotFoundException) {
+ // don't care, ignore
+ }
+
+ try {
+ val bounds = element.uiObject.bounds
+ var clazz = ""
+ for (tmp in classname.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
+ clazz = tmp
+ }
+ lastInteractedLog = when {
+ text.isNotEmpty() -> String.format("{Click} %s %s %s", text, clazz, bounds.toShortString())
+ desc.isNotEmpty() -> String.format("{Click} %s %s %s", desc, clazz, bounds.toShortString())
+ else -> String.format("{Click} %s %s", clazz, bounds.toShortString())
+ }
+
+ FileLog.i(Config.TAG, lastInteractedLog)
+ lastScreen = currentScreen
+ lastInteractedElement = element
+ element.finished = true
+ element.uiObject.click()
+ } catch (e: UiObjectNotFoundException) {
+ Log.e(Config.TAG, "UiObjectNotFoundException, failed to test a element")
+ }
+
+ }
+
+ private fun handleOtherPackage(currentScreen: Screen) {
+ if (isNewScreen(currentScreen)) {
+ ScreenshotHelper.takeScreenshots("(" + currentScreen.packageName + ")")
+ currentScreen.elementsList.clear()
+ currentScreen.isFinished = true
+ scannedScreenList.add(currentScreen)
+ }
+
+ lastInteractedLog = ""
+ lastInteractedElement = null
+
+ if (handleAndroidUi()) {
+ FileLog.i(Config.TAG, "Handle Android UI succeeded")
+ } else if (handleCommonDialog()) {
+ FileLog.i(Config.TAG, "Handle Common UI succeeded")
+ } else {
+ // not handle, try back
+ FileLog.i(Config.TAG, "{Click} Back")
+ device.pressBack()
+ if (!isInTargetApp) {
+ launchTargetApp()
+ depth = 0
+ }
+ }
+ }
+
+ private fun getNextElement(currentScreen: Screen): Element? {
+ if (currentScreen.isFinished)
+ return null
+ for (element in currentScreen.elementsList) {
+ if (!element.uiObject.exists()) {
+ element.finished = true
+ continue
+ }
+ if (!element.finished)
+ return element
+ }
+ return null
+ }
+
+ private fun isNewScreen(currentScreen: Screen): Boolean {
+ for (screen in scannedScreenList) {
+ if (screen == currentScreen) {
+ return false
+ }
+ }
+ return true
+ }
+
+ private fun reset() {
+ startTime = Date()
+ depth = 0
+ steps = 0
+ loop = 0
+ depthPeak = 0
+ rootScreen = null
+ lastScreen = null
+ lastInteractedElement = null
+ lastInteractedLog = ""
+ finished = false
+ device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ scannedScreenList = ArrayList()
+ }
+
+ private fun logAllScreenInfo() {
+ for (i in scannedScreenList.indices) {
+ val screen = scannedScreenList[i]
+ Log.d(Config.TAG, "Screen[" + (i + 1) + "] " + screen.toString())
+ }
+ Log.d(Config.TAG, "Root Screen id: " + rootScreen!!.id)
+ }
+}
+
diff --git a/library/src/main/java/com/kernel/colibri/core/strategy/Monkey.kt b/library/src/main/java/com/kernel/colibri/core/strategy/Monkey.kt
new file mode 100644
index 0000000..3059b27
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/strategy/Monkey.kt
@@ -0,0 +1,10 @@
+package com.kernel.colibri.core.strategy
+
+import com.kernel.colibri.core.models.Screen
+
+class Monkey : DepthFirst() {
+ override fun handleCurrentScreen(currentScreen: Screen) {
+ super.handleCurrentScreen(currentScreen)
+ currentScreen.elementsList.shuffle()
+ }
+}
\ No newline at end of file
diff --git a/library/src/main/java/com/kernel/colibri/core/strategy/Strategy.kt b/library/src/main/java/com/kernel/colibri/core/strategy/Strategy.kt
new file mode 100644
index 0000000..097e67b
--- /dev/null
+++ b/library/src/main/java/com/kernel/colibri/core/strategy/Strategy.kt
@@ -0,0 +1,8 @@
+package com.kernel.colibri.core.strategy
+
+import com.kernel.colibri.core.models.Condition
+
+interface Strategy {
+ var condition: Condition
+ fun run()
+}
\ No newline at end of file
diff --git a/sample/.gitignore b/sample/.gitignore
new file mode 100755
index 0000000..796b96d
--- /dev/null
+++ b/sample/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/sample/build.gradle b/sample/build.gradle
new file mode 100644
index 0000000..a301ac8
--- /dev/null
+++ b/sample/build.gradle
@@ -0,0 +1,39 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion versions.compileSdk
+ buildToolsVersion versions.buildTools
+
+ defaultConfig {
+ minSdkVersion versions.minSdk
+ targetSdkVersion versions.compileSdk
+ applicationId "com.kernel.colibri.sample"
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ debug {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation 'com.android.support:appcompat-v7:28.0.0'
+ androidTestImplementation 'com.android.support.test:rules:1.0.2'
+ androidTestImplementation 'com.android.support.test:runner:1.0.2'
+ androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+ androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'
+ androidTestImplementation 'com.github.kernel0x:colibri:1.0.0'
+ //androidTestImplementation project(':library')
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+}
diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro
new file mode 100755
index 0000000..e69de29
diff --git a/sample/src/androidTest/java/com/kernel/colibri/Sample2ColibriTest.kt b/sample/src/androidTest/java/com/kernel/colibri/Sample2ColibriTest.kt
new file mode 100644
index 0000000..c6383c9
--- /dev/null
+++ b/sample/src/androidTest/java/com/kernel/colibri/Sample2ColibriTest.kt
@@ -0,0 +1,31 @@
+package com.kernel.colibri
+
+import android.support.test.filters.LargeTest
+import android.support.test.rule.GrantPermissionRule
+import android.support.test.runner.AndroidJUnit4
+import com.kernel.colibri.core.models.Condition
+import com.kernel.colibri.core.models.Duration
+import com.kernel.colibri.core.strategy.Monkey
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.TimeUnit
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class Sample2ColibriTest {
+
+ @get:Rule
+ var permissionRule = GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.READ_EXTERNAL_STORAGE)
+
+ @Test
+ fun colibriTest() {
+ Colibri.condition(Condition.Builder()
+ .randomInputText(arrayOf("borscht", "vodka", "bear"))
+ .pause(Duration(500, TimeUnit.MILLISECONDS))
+ .build())
+ .strategy(Monkey())
+ .packageName("com.google.android.youtube")
+ .launch()
+ }
+}
diff --git a/sample/src/androidTest/java/com/kernel/colibri/SampleColibriTest.kt b/sample/src/androidTest/java/com/kernel/colibri/SampleColibriTest.kt
new file mode 100644
index 0000000..f73fd36
--- /dev/null
+++ b/sample/src/androidTest/java/com/kernel/colibri/SampleColibriTest.kt
@@ -0,0 +1,31 @@
+package com.kernel.colibri
+
+import android.support.test.filters.LargeTest
+import android.support.test.runner.AndroidJUnit4
+import com.kernel.colibri.core.models.Condition
+import com.kernel.colibri.core.models.Duration
+import com.kernel.colibri.core.strategy.Monkey
+import com.kernel.colibri.core.strategy.Strategy
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.TimeUnit
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class SampleColibriTest : ColibriTest() {
+ override fun getCondition(): Condition {
+ return Condition.Builder()
+ .randomInputText(arrayOf("borscht", "vodka", "bear"))
+ .pause(Duration(500, TimeUnit.MILLISECONDS))
+ .build()
+ }
+
+ override fun getStrategy(): Strategy {
+ return Monkey()
+ }
+
+ @Test
+ fun colibriTest() {
+ launch("com.google.android.youtube")
+ }
+}
diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml
new file mode 100755
index 0000000..383b6ce
--- /dev/null
+++ b/sample/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sample/src/main/java/com/kernel/colibri/sample/MainActivity.kt b/sample/src/main/java/com/kernel/colibri/sample/MainActivity.kt
new file mode 100644
index 0000000..2e04133
--- /dev/null
+++ b/sample/src/main/java/com/kernel/colibri/sample/MainActivity.kt
@@ -0,0 +1,12 @@
+package com.kernel.colibri.sample
+
+import android.support.v7.app.AppCompatActivity
+import android.os.Bundle
+
+class MainActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ }
+}
diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..601033c
--- /dev/null
+++ b/sample/src/main/res/layout/activity_main.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml
new file mode 100644
index 0000000..7abc06d
--- /dev/null
+++ b/sample/src/main/res/values/strings.xml
@@ -0,0 +1 @@
+
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..52baf7e
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':sample', ':library'