├── .github └── workflows │ └── android.yml ├── .gitignore ├── .idea ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── encodings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── kotlinc.xml ├── misc.xml ├── other.xml ├── runConfigurations │ └── _UiAutomatorHelper.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── lint.xml ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── dtmilano │ │ └── android │ │ └── culebratester2 │ │ ├── UiAutomatorHelper.kt │ │ └── utils │ │ └── SelectorUtilsTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ ├── com │ │ │ └── dtmilano │ │ │ │ └── android │ │ │ │ └── culebratester2 │ │ │ │ ├── ApplicationComponent.kt │ │ │ │ ├── CulebraTesterApplication.kt │ │ │ │ ├── CulebraTesterServer.kt │ │ │ │ ├── Holder.kt │ │ │ │ ├── KtorApplication.kt │ │ │ │ ├── LauncherActivity.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MainActivityOld.kt │ │ │ │ ├── ObjectStore.kt │ │ │ │ ├── OnboardingActivity.kt │ │ │ │ ├── WelcomeActivity.kt │ │ │ │ ├── WindowHierarchyDumpToJson.kt │ │ │ │ ├── location │ │ │ │ ├── Am.kt │ │ │ │ ├── Configurator.kt │ │ │ │ ├── Culebra.kt │ │ │ │ ├── Device.kt │ │ │ │ ├── Help.kt │ │ │ │ ├── ObjectStore.kt │ │ │ │ ├── TargetContext.kt │ │ │ │ ├── UiDevice.kt │ │ │ │ ├── UiObject.kt │ │ │ │ ├── UiObject2.kt │ │ │ │ └── Until.kt │ │ │ │ ├── ui │ │ │ │ └── theme │ │ │ │ │ ├── Color.kt │ │ │ │ │ ├── Shape.kt │ │ │ │ │ ├── Theme.kt │ │ │ │ │ └── Type.kt │ │ │ │ └── utils │ │ │ │ ├── JavaSelectorUtils.java │ │ │ │ ├── LocaleUtils.kt │ │ │ │ ├── PackageUtils.kt │ │ │ │ ├── SelectorBundles.kt │ │ │ │ └── SelectorUtils.kt │ │ └── io │ │ │ ├── ktor │ │ │ └── swagger │ │ │ │ └── experimental │ │ │ │ └── SwaggerUtils.kt │ │ │ └── swagger │ │ │ └── server │ │ │ └── models │ │ │ ├── AnyValue.kt │ │ │ ├── BooleanResponse.kt │ │ │ ├── ClickBody.kt │ │ │ ├── CulebraInfo.kt │ │ │ ├── CurrentPackageName.kt │ │ │ ├── DisplayHeight.kt │ │ │ ├── DisplayRealSize.kt │ │ │ ├── DisplayRotationEnum.kt │ │ │ ├── DisplayRotationEnumCompanion.kt │ │ │ ├── DisplayRotationResponse.kt │ │ │ ├── DisplaySizeDp.kt │ │ │ ├── DisplayWidth.kt │ │ │ ├── Help.kt │ │ │ ├── Inline_response_200.kt │ │ │ ├── Inline_response_200_1.kt │ │ │ ├── LastTraversedText.kt │ │ │ ├── Locale.kt │ │ │ ├── NumberResponse.kt │ │ │ ├── ObjectRef.kt │ │ │ ├── Oid_setText_body.kt │ │ │ ├── Pattern.kt │ │ │ ├── PerformTwoPointerGestureBody.kt │ │ │ ├── Pixel.kt │ │ │ ├── Point.kt │ │ │ ├── ProductName.kt │ │ │ ├── Rect.kt │ │ │ ├── Selector.kt │ │ │ ├── SelectorCompanion.kt │ │ │ ├── StatusCode.kt │ │ │ ├── StatusResponse.kt │ │ │ ├── StringResponse.kt │ │ │ ├── SwipeBody.kt │ │ │ ├── Text.kt │ │ │ ├── Timeout.kt │ │ │ ├── WindowHierarchy.kt │ │ │ └── WindowHierarchyChild.kt │ └── res │ │ ├── drawable-hdpi │ │ └── culebra.png │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xxhdpi │ │ ├── culebra.png │ │ └── monitor.png │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_onboarding.xml │ │ ├── activity_welcome.xml │ │ └── content_main.xml │ │ ├── menu │ │ └── menu_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── culebra.png │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── culebra.png │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── culebra.png │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── culebra.png │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── culebra.png │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── dtmilano │ └── android │ └── culebratester2 │ ├── KtorApplicationQuitTest.kt │ ├── KtorApplicationTest.kt │ ├── ObjectStoreTest.kt │ └── utils │ └── SelectorUtilsTest.kt ├── build.gradle ├── clean-openapi-yaml ├── culebratester2 ├── dump-window-hierarchy ├── fix-swagger-generated-files ├── get-api-version ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── local.properties.SAMPLE ├── openapi.yaml ├── publish ├── settings.gradle ├── simple-calculator-test ├── swagger-codegen-config.json └── take-screenshot /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: set up JDK 11 13 | uses: actions/setup-java@v1 14 | with: 15 | java-version: 11 16 | - name: Build with Gradle 17 | run: ./gradlew build assembleAndroidTest 18 | - name: Upload APK 19 | uses: actions/upload-artifact@v4 20 | with: 21 | name: app 22 | path: app/build/outputs/apk/debug/app-debug.apk 23 | - name: Upload Instrumentation 24 | uses: actions/upload-artifact@v4 25 | with: 26 | name: instrumentation 27 | path: app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # IntelliJ 36 | *.iml 37 | .idea/workspace.xml 38 | .idea/tasks.xml 39 | .idea/gradle.xml 40 | .idea/assetWizardSettings.xml 41 | .idea/dictionaries 42 | .idea/libraries 43 | .idea/caches 44 | .idea/modules.xml 45 | .idea/navEditor.xml 46 | .idea/deploymentTargetDropDown.xml 47 | 48 | # Keystore files 49 | # Uncomment the following line if you do not want to check your keystore files in. 50 | *.jks 51 | 52 | # External native build folder generated in Android Studio 2.2 and later 53 | .externalNativeBuild 54 | 55 | # Google Services (e.g. APIs or Firebase) 56 | google-services.json 57 | 58 | # Freeline 59 | freeline.py 60 | freeline/ 61 | freeline_project_description.json 62 | 63 | # fastlane 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | fastlane/readme.md 69 | 70 | # MacOS 71 | .DS_Store 72 | 73 | # Other 74 | .cxx 75 | 76 | # backup 77 | .BAK 78 | 79 | # Culebratester scripts 80 | generate-culebratester-* 81 | levenshtein 82 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | CulebraTester2 -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 12 | 13 | 14 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | xmlns:android 23 | 24 | ^$ 25 | 26 | 27 | 28 |
29 |
30 | 31 | 32 | 33 | xmlns:.* 34 | 35 | ^$ 36 | 37 | 38 | BY_NAME 39 | 40 |
41 |
42 | 43 | 44 | 45 | .*:id 46 | 47 | http://schemas.android.com/apk/res/android 48 | 49 | 50 | 51 |
52 |
53 | 54 | 55 | 56 | .*:name 57 | 58 | http://schemas.android.com/apk/res/android 59 | 60 | 61 | 62 |
63 |
64 | 65 | 66 | 67 | name 68 | 69 | ^$ 70 | 71 | 72 | 73 |
74 |
75 | 76 | 77 | 78 | style 79 | 80 | ^$ 81 | 82 | 83 | 84 |
85 |
86 | 87 | 88 | 89 | .* 90 | 91 | ^$ 92 | 93 | 94 | BY_NAME 95 | 96 |
97 |
98 | 99 | 100 | 101 | .* 102 | 103 | http://schemas.android.com/apk/res/android 104 | 105 | 106 | ANDROID_ATTRIBUTE_ORDER 107 | 108 |
109 |
110 | 111 | 112 | 113 | .* 114 | 115 | .* 116 | 117 | 118 | BY_NAME 119 | 120 |
121 |
122 |
123 |
124 | 125 | 127 |
128 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | -------------------------------------------------------------------------------- /.idea/other.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/runConfigurations/_UiAutomatorHelper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 53 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # CulebraTester2-public 4 | CulebraTester: Snaky Android Testing 5 | 6 | ### Welcome to CulebraTester2. 7 | Android testing can be complicated, time-consuming, and tedious. 8 | What if it didn’t have to be? 9 | 10 | **CulebraTester2** provides an API that facilitates the creation of tests and test automation tools and UI's. 11 | Not sure what we mean? 12 | 13 | Continue reading and see how you can run this early preview. 14 | 15 | 16 | ⚠️ Warning | 17 | :------------------| 18 | **This is an alpha version of CulebraTester2 expect changes** | 19 | 20 | 21 | 22 | # How to run CulebraTester2 ? 23 | 1. Have your device or emulator connected to `adb` 24 | 1. Install APKs 25 | 1. Download prebuilt **app** and **instrumentation** APKs from [Github Actions](https://github.com/dtmilano/CulebraTester2-public/wiki/Prebuilt-APKs) 26 | 1. or build from source and install 27 | 1. Copy `local.properties.SAMPLE` to `local.properties` and adapt the values to your environment 28 | 1. `./culebratester2 install` (or run `./gradlew installDebug installDebugAndroidTest`) 29 | 1. Start server `bash <(curl -sL https://git.io/JT5nc) start-server` 30 | 1. alternatively if you checked out the source you can run `./culebratester2 start-server` instead 31 | 1. Open http://localhost:9987/ with a browser or `curl` 32 | 1. You should see `CulebraTester2: Go to http://localhost:/help for usage details.` 33 | 1. If the previous request worked, you can try something more ambitious like http://localhost:9987/v2/uiDevice/screenshot 34 | 1. Take a look at [CulebraTester2 API](https://mrin9.github.io/OpenAPI-Viewer/#/load/https%3A%2F%2Fraw.githubusercontent.com%2Fdtmilano%2FCulebraTester2-public%2Fmaster%2Fopenapi.yaml) or its spec [`openapi.yaml`](https://github.com/dtmilano/CulebraTester2-public/blob/master/openapi.yaml) for more info 35 | 1. When you are done testing, navigating to http://localhost:9987/quit will terminate the server 36 | 37 | # Want to learn more? 38 | Detailed information can be found in the [CulebraTester2 wiki](https://github.com/dtmilano/CulebraTester2-public/wiki) wiki 39 | 40 | # AndroidViewClient 41 | **CulebraTester2** is the new backend for [AndroidViewClient/culebra](https://github.com/dtmilano/AndroidViewClient). 42 | 43 | It can be used similarly to other backends with the following command options 44 | 45 | ``` 46 | -h, --use-uiautomator-helper use UiAutomatorHelper Android app 47 | ``` 48 | 49 | for example 50 | 51 | ```sh 52 | $ dump -ah emulator-5554 | jq 53 | ⚠️ CulebraTester2 server should have been started and port redirected. 54 | ``` 55 | ```json 56 | { 57 | "id": "hierarchy", 58 | "text": "Window Hierarchy", 59 | "timestamp": "2020-10-12T02:18:45.639Z", 60 | "children": [ 61 | { 62 | "id": 0, 63 | "parent": -1, 64 | "text": "", 65 | "package": "com.android.systemui", 66 | "checkable": false, 67 | "clickable": false, 68 | "index": 0, 69 | "content_description": "", 70 | "focusable": false, 71 | ... 72 | ``` 73 | 74 | or set `useuiautomatorhelper=True` when you create a `ViewClient` object. 75 | 76 | # culebra 77 | **CulebraTester2** is a new implementation in Kotlin of [culebra](culebra.dtmilano.com). 78 | 79 | A Python client implementation can be found at **[CulebraTester2-client](https://github.com/dtmilano/CulebraTester2-client)**. 80 | 81 | This previous version API specification can be found at [here](https://github.com/dtmilano/CulebraTester-public/wiki/RESTful-API). 82 | 83 | # Example 84 | The script [simple-calculator-test](https://github.com/dtmilano/CulebraTester2-public/blob/master/simple-calculator-test) shows a rudimentary usage of this API by 85 | - starting Calculator activity 86 | - finding one of the digit buttons (can be specified or a random one is selected) 87 | - clicking on that button 88 | 89 | # UI 90 | We mentioned **CulebraTester2** provides an API that facilitates the creation of test automation tools and UIs. 91 | 92 | Here we are, this is in the making. 93 | 94 | CulebraTester2-ui preview 96 | 97 | # Communication 98 | Found issues? Use https://github.com/dtmilano/CulebraTester2-public/issues 99 | 100 | Have questions? Use https://stackoverflow.com/questions/tagged/androidviewclient. 101 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | // this is in documentation https://developer.android.com/training/dependency-injection/hilt-android#kts 5 | //kotlin("kapt") 6 | // but this one works 7 | id("kotlin-kapt") 8 | id("dagger.hilt.android.plugin") 9 | } 10 | 11 | ext { 12 | compose_version = '1.1.1' 13 | ktor_version = '1.6.8' 14 | } 15 | 16 | android { 17 | compileSdk 33 18 | 19 | defaultConfig { 20 | applicationId "com.dtmilano.android.culebratester2" 21 | minSdk 26 22 | targetSdk 33 23 | versionCode 20074 24 | versionName "2.0.74-alpha" 25 | 26 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 27 | vectorDrawables { 28 | useSupportLibrary true 29 | } 30 | } 31 | 32 | 33 | buildTypes { 34 | release { 35 | minifyEnabled false 36 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 37 | } 38 | } 39 | 40 | packagingOptions { 41 | resources { 42 | excludes += ['/META-INF/{AL2.0,LGPL2.1}', 'META-INF/DEPENDENCIES', 'META-INF/io.netty.versions.properties', 'META-INF/INDEX.LIST', 'META-INF/ktor-http.kotlin_module', 'META-INF/ktor-utils.kotlin_module', 'META-INF/kotlinx-coroutines-core.kotlin_module', 'META-INF/ktor-io.kotlin_module', 'META-INF/ktor-http-cio.kotlin_module', 'META-INF/ktor-client-core.kotlin_module'] 43 | } 44 | } 45 | 46 | compileOptions { 47 | sourceCompatibility JavaVersion.VERSION_1_8 48 | targetCompatibility JavaVersion.VERSION_1_8 49 | } 50 | 51 | kotlinOptions { 52 | jvmTarget = '1.8' 53 | useIR = true 54 | } 55 | 56 | buildFeatures { 57 | viewBinding true 58 | compose true 59 | } 60 | 61 | testOptions { 62 | unitTests { 63 | returnDefaultValues = true 64 | includeAndroidResources = true 65 | } 66 | } 67 | 68 | composeOptions { 69 | kotlinCompilerExtensionVersion compose_version 70 | } 71 | 72 | lintOptions { 73 | disable "JvmStaticProvidesInObjectDetector", "FieldSiteTargetOnQualifierAnnotation", "ModuleCompanionObjects", "ModuleCompanionObjectsNotInModuleParent" 74 | } 75 | } 76 | 77 | dependencies { 78 | 79 | implementation 'androidx.core:core-ktx:1.8.0' 80 | implementation 'androidx.appcompat:appcompat:1.4.2' 81 | implementation 'com.google.android.material:material:1.6.1' 82 | implementation "com.google.dagger:hilt-android:$hilt_android_version" 83 | //kapt "com.google.dagger:hilt-compiler:$hilt_android_version" 84 | implementation("io.ktor:ktor:$ktor_version") 85 | implementation 'androidx.test.uiautomator:uiautomator:2.2.0' 86 | implementation "androidx.compose.ui:ui:$compose_version" 87 | implementation "androidx.compose.material:material:$compose_version" 88 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 89 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' 90 | implementation 'androidx.activity:activity-compose:1.4.0' 91 | testImplementation 'junit:junit:4.13.2' 92 | testImplementation('androidx.test:core:1.5.0') 93 | androidTestImplementation 'androidx.test.ext:junit:1.1.4' 94 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 95 | implementation('com.google.dagger:hilt-android:2.42') 96 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" 97 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" 98 | kapt("com.google.dagger:hilt-android-compiler:$hilt_android_version") 99 | implementation("io.ktor:ktor-server-core:$ktor_version") 100 | implementation("io.ktor:ktor-server-netty:$ktor_version") 101 | implementation("ch.qos.logback:logback-classic:1.2.11") 102 | implementation("io.ktor:ktor-jackson:$ktor_version") 103 | implementation("io.ktor:ktor-locations:$ktor_version") 104 | implementation("io.ktor:ktor-gson:$ktor_version") 105 | testImplementation("io.ktor:ktor-server-tests:$ktor_version") 106 | androidTestImplementation 'com.github.stefanbirkner:system-rules:1.19.0' 107 | testImplementation 'com.github.stefanbirkner:system-rules:1.19.0' 108 | testImplementation('org.mockito:mockito-core:4.6.0') 109 | testImplementation('org.mockito.kotlin:mockito-kotlin:4.0.0') 110 | testImplementation('org.jetbrains.kotlin:kotlin-test:1.6.10') 111 | androidTestImplementation('org.jetbrains.kotlin:kotlin-test:1.6.10') 112 | testImplementation 'org.robolectric:robolectric:4.8.1' 113 | implementation group: 'commons-io', name: 'commons-io', version: '2.6' 114 | } 115 | 116 | // Allow references to generated code 117 | kapt { 118 | correctErrorTypes true 119 | } 120 | -------------------------------------------------------------------------------- /app/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/dtmilano/android/culebratester2/UiAutomatorHelper.kt: -------------------------------------------------------------------------------- 1 | package com.dtmilano.android.culebratester2 2 | 3 | import android.app.Instrumentation 4 | import android.content.Context.WINDOW_SERVICE 5 | import android.view.WindowManager 6 | import androidx.test.ext.junit.runners.AndroidJUnit4 7 | import androidx.test.platform.app.InstrumentationRegistry 8 | import androidx.test.uiautomator.UiDevice 9 | import io.ktor.application.* 10 | import io.ktor.locations.* 11 | import org.junit.After 12 | import org.junit.Before 13 | import org.junit.Test 14 | import org.junit.runner.RunWith 15 | import java.io.File 16 | import java.lang.ref.WeakReference 17 | 18 | /** 19 | * The port. 20 | */ 21 | private const val PORT = 9987 22 | 23 | @KtorExperimentalLocationsAPI 24 | @RunWith(AndroidJUnit4::class) 25 | class UiAutomatorHelper { 26 | private var appComponent: ApplicationComponent = CulebraTesterApplication().appComponent 27 | private lateinit var instrumentation: Instrumentation 28 | 29 | @Before 30 | fun setUp() { 31 | // Injecting the Instrumentation instance is required 32 | // for your test to run with AndroidJUnitRunner. 33 | instrumentation = InstrumentationRegistry.getInstrumentation() 34 | 35 | val holder = appComponent.holder().instance 36 | holder.targetContext = WeakReference(instrumentation.targetContext) 37 | holder.uiDevice = UiDevice.getInstance(instrumentation) 38 | holder.cacheDir = instrumentation.targetContext.cacheDir 39 | holder.windowManager = 40 | instrumentation.targetContext.getSystemService(WINDOW_SERVICE) as WindowManager 41 | holder.uiAutomation = instrumentation.uiAutomation 42 | } 43 | 44 | @Test 45 | fun uiAutomatorHelper() { 46 | val ktorApplicationConfigurationFile = createKtorApplicationConfigurationFile() 47 | val args = arrayOf("-config=${ktorApplicationConfigurationFile!!.absolutePath}") 48 | io.ktor.server.netty.EngineMain.main(args) 49 | } 50 | 51 | /** 52 | * Creates a temporary file containing the configuration. 53 | */ 54 | private fun createKtorApplicationConfigurationFile(): File? { 55 | val tempDir = instrumentation.targetContext.cacheDir 56 | val tempFile = File.createTempFile("ktor-application", "conf", tempDir) 57 | val applicationConfiguration = "ktor {\n" + 58 | " deployment {\n" + 59 | " port = ${PORT}\n" + 60 | " port = \${?PORT}\n" + 61 | "\n" + 62 | " shutdown.url = \"/ktor/application/shutdown\"\n" + 63 | " }\n" + 64 | " application {\n" + 65 | " modules = [ com.dtmilano.android.culebratester2.KtorApplicationKt.module ]\n" + 66 | " }\n" + 67 | "}" 68 | tempFile.writeText(applicationConfiguration) 69 | return tempFile 70 | } 71 | 72 | @After 73 | fun tearDown() { 74 | // TODO: should stop ktor server here 75 | } 76 | 77 | /** 78 | * Quits the server. 79 | */ 80 | fun quit() { 81 | // TODO: should stop ktor server here 82 | } 83 | 84 | // WARNING: 85 | // won't be found because this is inside the test application code and not the main app 86 | @Suppress("unused") 87 | fun Application.module(testing: Boolean = false) { 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/dtmilano/android/culebratester2/utils/SelectorUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.dtmilano.android.culebratester2.utils 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import org.junit.After 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Assert.assertNotNull 7 | import org.junit.Before 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | 11 | @RunWith(AndroidJUnit4::class) 12 | class SelectorUtilsTest { 13 | 14 | @Before 15 | fun setUp() { 16 | } 17 | 18 | @After 19 | fun tearDown() { 20 | } 21 | 22 | @Test 23 | fun test_tokenize() { 24 | val selector = "clickable@true" 25 | val delimiters = "(?<=[^\\\\])@" 26 | val tokens = tokenize(selector, delimiters).contentToString() 27 | assertEquals("[clickable, true]", tokens) 28 | } 29 | 30 | @Test 31 | fun test_tokenize_complex_selector() { 32 | val selector = 33 | "clickable@true,depth@3,desc@something with spaces and \\@ special chars@index@3,text@$.*" 34 | val delimiters = "(?<=[^\\\\])@" 35 | val tokens = tokenize(selector, delimiters).contentToString() 36 | assertEquals( 37 | "[clickable, true,depth, 3,desc, something with spaces and \\@ special chars, index, 3,text, \$.*]", 38 | tokens 39 | ) 40 | } 41 | 42 | @Test 43 | fun test_unescapeSelectorChars() { 44 | val str = "\\@\\," 45 | assertEquals("@,", unescapeSelectorChars(str)) 46 | } 47 | 48 | @Test 49 | fun test_unescapeSelectorChars_multiple_occurrences() { 50 | val str = "\\@\\@\\,\\,\\@" 51 | assertEquals("@@,,@", unescapeSelectorChars(str)) 52 | } 53 | 54 | @Test 55 | fun test_uiSelectorBundleFromString_missing_value() { 56 | val str = "clickable" 57 | val bundle = uiSelectorBundleFromString(str) 58 | assertEquals(str, bundle.selectorStr) 59 | assertNotNull(bundle.selector) 60 | } 61 | 62 | @Test 63 | fun test_uiSelectorBundleFromString_clickable_true() { 64 | val str = "clickable@true" 65 | val bundle = uiSelectorBundleFromString(str) 66 | assertEquals(str, bundle.selectorStr) 67 | assertNotNull(bundle.selector) 68 | } 69 | 70 | @Test 71 | fun test_uiSelectorBundleFromString_selector_with_spaces_and_special_chars() { 72 | val selector = "desc@something with spaces and \\@ special chars" 73 | val bundle = uiSelectorBundleFromString(selector) 74 | assertEquals(selector, bundle.selectorStr) 75 | assertNotNull(bundle.selector) 76 | } 77 | 78 | @Test 79 | fun test_uiSelectorBundleFromString_selector_with_spaces_and_special_chars_followed_by_another() { 80 | val selector = "desc@something with spaces and \\@ special chars,index@3" 81 | val bundle = uiSelectorBundleFromString(selector) 82 | assertEquals(selector, bundle.selectorStr) 83 | assertNotNull(bundle.selector) 84 | } 85 | 86 | @Test 87 | fun test_uiSelectorBundleFromString_complex_selector() { 88 | val selector = 89 | "clickable@true,depth@3,desc@something with spaces and \\@ special chars@index@3,text@$.*" 90 | val bundle = uiSelectorBundleFromString(selector) 91 | assertEquals(selector, bundle.selectorStr) 92 | assertNotNull(bundle.selector) 93 | } 94 | 95 | @Test(expected = KotlinNullPointerException::class) 96 | fun test_bySelectorBundleFromString_missing_value() { 97 | val selector = "clickable" 98 | val bundle = bySelectorBundleFromString(selector) 99 | } 100 | 101 | @Test 102 | fun test_bySelectorBundleFromString_clickable_true() { 103 | val str = "clickable@true" 104 | val bundle = bySelectorBundleFromString(str) 105 | assertEquals(str, bundle.selectorStr) 106 | assertNotNull(bundle.selector) 107 | } 108 | 109 | @Test 110 | fun test_bySelectorBundleFromString_selector_with_spaces_and_special_chars() { 111 | val selector = "desc@something with spaces and \\@ special chars" 112 | val bundle = bySelectorBundleFromString(selector) 113 | assertEquals(selector, bundle.selectorStr) 114 | assertNotNull(bundle.selector) 115 | } 116 | 117 | @Test 118 | fun test_bySelectorBundleFromString_selector_with_spaces_and_special_chars_followed_by_another() { 119 | val selector = "desc@something with spaces and \\@ special chars,depth@3" 120 | val bundle = bySelectorBundleFromString(selector) 121 | assertEquals(selector, bundle.selectorStr) 122 | assertNotNull(bundle.selector) 123 | } 124 | 125 | @Test 126 | fun test_bySelectorBundleFromString_complex_selector() { 127 | val selector = 128 | "clickable@true,depth@3,desc@something with spaces and \\@ special chars,text@$.*" 129 | val bundle = bySelectorBundleFromString(selector) 130 | assertEquals(selector, bundle.selectorStr) 131 | assertNotNull(bundle.selector) 132 | } 133 | 134 | @Test 135 | fun eventConditionFromString() { 136 | } 137 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 9 | 11 | 13 | 14 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 39 | 44 | 45 | 51 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/dtmilano/android/culebratester2/ApplicationComponent.kt: -------------------------------------------------------------------------------- 1 | package com.dtmilano.android.culebratester2 2 | 3 | import com.dtmilano.android.culebratester2.location.* 4 | import dagger.Component 5 | import io.ktor.locations.KtorExperimentalLocationsAPI 6 | import javax.inject.Singleton 7 | 8 | @KtorExperimentalLocationsAPI 9 | @Singleton 10 | @Component 11 | interface ApplicationComponent { 12 | // Factory to create instances of the ApplicationComponent 13 | @Component.Factory 14 | interface Factory { 15 | // With @BindsInstance, the Context passed in will be available in the graph 16 | //fun create(@BindsInstance context: Context): ApplicationComponent 17 | fun create(): ApplicationComponent 18 | } 19 | 20 | fun holder(): HolderHolder 21 | fun objectStore(): ObjectStore 22 | fun inject(objectStore: com.dtmilano.android.culebratester2.location.ObjectStore) 23 | fun inject(list: com.dtmilano.android.culebratester2.location.ObjectStore.List) 24 | fun inject(startActivity: TargetContext.StartActivity) 25 | fun inject(dumpWindowHierarchy: UiDevice.DumpWindowHierarchy) 26 | fun inject(screenshot: UiDevice.Screenshot) 27 | fun inject(get: UiDevice.Click.Get) 28 | fun inject(currentPackageName: UiDevice.CurrentPackageName) 29 | fun inject(displayHeight: UiDevice.DisplayHeight) 30 | fun inject(displayRotation: UiDevice.DisplayRotation) 31 | fun inject(displaySizeDp: UiDevice.DisplaySizeDp) 32 | fun inject(displayWidth: UiDevice.DisplayWidth) 33 | fun inject(findObject: UiDevice.FindObject) 34 | fun inject(get: UiDevice.FindObject.Get) 35 | fun inject(post: UiDevice.FindObject.Post) 36 | fun inject(lastTraversedText: UiDevice.LastTraversedText) 37 | fun inject(pressBack: UiDevice.PressBack) 38 | fun inject(pressDelete: UiDevice.PressDelete) 39 | fun inject(pressEnter: UiDevice.PressEnter) 40 | fun inject(pressHome: UiDevice.PressHome) 41 | fun inject(pressKeyCode: UiDevice.PressKeyCode) 42 | fun inject(productName: UiDevice.ProductName) 43 | fun inject(waitForIdle: UiDevice.WaitForIdle) 44 | fun inject(waitForWindowUpdate: UiDevice.WaitForWindowUpdate) 45 | fun inject(displayRealSize: Device.DisplayRealSize) 46 | fun inject(click: UiObject2.Click) 47 | fun inject(dump: UiObject2.Dump) 48 | fun inject(get: UiObject2.SetText.Get) 49 | fun inject(post: UiObject2.SetText.Post) 50 | fun inject(longClick: UiObject2.LongClick) 51 | fun inject(get: UiDevice.FindObjects.Get) 52 | fun inject(getText: UiObject2.GetText) 53 | fun inject(pressRecentApps: UiDevice.PressRecentApps) 54 | fun inject(get: UiDevice.Swipe.Get) 55 | fun inject(post: UiDevice.Swipe.Post) 56 | fun inject(clear: com.dtmilano.android.culebratester2.location.ObjectStore.Clear) 57 | fun inject(remove: com.dtmilano.android.culebratester2.location.ObjectStore.Remove) 58 | fun inject(clear: UiObject2.Clear) 59 | fun inject(wait: UiDevice.Wait) 60 | fun inject(newWindow: Until.NewWindow) 61 | fun inject(clickAndWait: UiObject2.ClickAndWait) 62 | fun inject(clearLastTraversedText: UiDevice.ClearLastTraversedText) 63 | fun inject(drag: UiDevice.Drag) 64 | fun inject(freezeRotation: UiDevice.FreezeRotation) 65 | fun inject(get: UiDevice.HasObject.Get) 66 | fun inject(post: UiDevice.HasObject.Post) 67 | fun inject(waitForNewToast: Device.WaitForNewToast) 68 | fun inject(pixel: UiDevice.Pixel) 69 | fun inject(post: UiDevice.Click.Post) 70 | fun inject(unfreezeRotation: UiDevice.UnfreezeRotation) 71 | fun inject(isNaturalOrientation: UiDevice.IsNaturalOrientation) 72 | fun inject(isScreenOn: UiDevice.IsScreenOn) 73 | fun inject(query: Culebra.Help.Query) 74 | fun inject(dumpsys: Device.Dumpsys) 75 | fun inject(getContentDescription: UiObject2.GetContentDescription) 76 | fun inject(post: UiDevice.FindObjects.Post) 77 | fun inject(get: Until.FindObject.Get) 78 | fun inject(post: Until.FindObject.Post) 79 | fun inject(get: Until.FindObjects.Get) 80 | fun inject(post: Until.FindObjects.Post) 81 | fun inject(get: UiObject2.FindObject.Get) 82 | fun inject(post: UiObject2.FindObject.Post) 83 | fun inject(pressDPadCenter: UiDevice.PressDPadCenter) 84 | fun inject(pressDPadDown: UiDevice.PressDPadDown) 85 | fun inject(pressDPadLeft: UiDevice.PressDPadLeft) 86 | fun inject(pressDPadRight: UiDevice.PressDPadRight) 87 | fun inject(pressDPadUp: UiDevice.PressDPadUp) 88 | fun inject(get: Device.Locale.Get) 89 | fun inject(post: Device.Locale.Post) 90 | fun inject(post: UiObject.PerformTwoPointerGesture.Post) 91 | fun inject(pinchIn: UiObject.PinchIn) 92 | fun inject(pinchOut: UiObject.PinchOut) 93 | fun inject(exists: UiObject.Exists) 94 | fun inject(waitForExists: UiObject.WaitForExists) 95 | fun inject(dump: UiObject.Dump) 96 | fun inject(getChildCount: UiObject.GetChildCount) 97 | fun inject(dump: Until.Dump) 98 | fun inject(click: UiObject.Click) 99 | fun inject(clickAndWaitForNewWindow: UiObject.ClickAndWaitForNewWindow) 100 | fun inject(clearTextField: UiObject.ClearTextField) 101 | fun inject(getContentDescription: UiObject.GetContentDescription) 102 | fun inject(getClassName: UiObject.GetClassName) 103 | fun inject(getChildCount: UiObject2.GetChildCount) 104 | fun inject(getChildren: UiObject2.GetChildren) 105 | fun inject(getBounds: UiObject.GetBounds) 106 | fun inject(getChild: UiObject.GetChild) 107 | fun inject(getFromParent: UiObject.GetFromParent) 108 | // fun inject(uiAutomatorHelper: UiAutomatorHelper) 109 | } 110 | -------------------------------------------------------------------------------- /app/src/main/java/com/dtmilano/android/culebratester2/CulebraTesterApplication.kt: -------------------------------------------------------------------------------- 1 | package com.dtmilano.android.culebratester2 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | import android.preference.PreferenceManager 7 | import dagger.hilt.android.HiltAndroidApp 8 | import io.ktor.locations.* 9 | 10 | @HiltAndroidApp 11 | class CulebraTesterApplication : Application() { 12 | companion object { 13 | 14 | /** 15 | * Preferences store whether the welcome activity was showed. 16 | */ 17 | const val PREFERENCE_ONBOARDING_SHOWED = 18 | "com.dtmilano.android.culebratester2.PREFERENCE_ONBOARDING_SHOWED" 19 | 20 | /** 21 | * Gets the default shared preferences for this application. 22 | * 23 | * @param context the context 24 | * @return the shared preferences 25 | */ 26 | fun getPreferences(context: Context): SharedPreferences { 27 | return PreferenceManager.getDefaultSharedPreferences(context.applicationContext) 28 | } 29 | } 30 | 31 | // Instance of the ApplicationComponent that will be used by all the Activities in the project 32 | @KtorExperimentalLocationsAPI 33 | val appComponent: ApplicationComponent by lazy { 34 | initializeComponent() 35 | } 36 | 37 | @KtorExperimentalLocationsAPI 38 | fun initializeComponent(): ApplicationComponent { 39 | // Creates an instance of ApplicationComponent using its factory method 40 | // We pass the applicationContext that will be used as Context in the graph 41 | //return DaggerApplicationComponent.factory().create(applicationContext) 42 | return DaggerApplicationComponent.factory().create() 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dtmilano/android/culebratester2/CulebraTesterServer.kt: -------------------------------------------------------------------------------- 1 | package com.dtmilano.android.culebratester2 2 | 3 | class CulebraTesterServer { 4 | init { 5 | println("==== CulebraTesterServer ====") 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/dtmilano/android/culebratester2/Holder.kt: -------------------------------------------------------------------------------- 1 | package com.dtmilano.android.culebratester2 2 | 3 | import android.app.UiAutomation 4 | import android.content.Context 5 | import android.view.WindowManager 6 | import androidx.test.uiautomator.UiDevice 7 | import java.io.File 8 | import java.lang.ref.WeakReference 9 | import javax.inject.Inject 10 | import javax.inject.Singleton 11 | 12 | /** 13 | * Holds some important objects accessible from app and tests. 14 | */ 15 | object Holder { 16 | lateinit var targetContext: WeakReference 17 | lateinit var windowManager: WindowManager 18 | lateinit var cacheDir: File 19 | lateinit var uiDevice: UiDevice 20 | lateinit var uiAutomation: UiAutomation 21 | } 22 | 23 | // Didn't work, dagger was injecting different references even though this is annotated with 24 | // singleton, as a workaround, HolderHolder was used 25 | //@Singleton 26 | //class Holder @Inject constructor() { 27 | // lateinit var targetContext: WeakReference 28 | // lateinit var windowManager: WindowManager 29 | // lateinit var cacheDir: File 30 | // lateinit var uiDevice: UiDevice 31 | //} 32 | 33 | @Singleton 34 | class HolderHolder @Inject constructor() { 35 | val instance = Holder 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/dtmilano/android/culebratester2/LauncherActivity.kt: -------------------------------------------------------------------------------- 1 | package com.dtmilano.android.culebratester2 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | 7 | class LauncherActivity : AppCompatActivity() { 8 | 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | 12 | if (shouldShowOnboarding(this)) { 13 | OnboardingActivity.start(this) 14 | } else { 15 | MainActivityOld.start(this) 16 | } 17 | } 18 | 19 | companion object { 20 | fun shouldShowOnboarding(context: Context): Boolean { 21 | val preferences = CulebraTesterApplication.getPreferences(context) 22 | return !(preferences.contains(CulebraTesterApplication.PREFERENCE_ONBOARDING_SHOWED) && preferences.getBoolean( 23 | CulebraTesterApplication.PREFERENCE_ONBOARDING_SHOWED, false)) 24 | } 25 | 26 | fun onboardingShowed(context: Context) { 27 | val preferences = CulebraTesterApplication.getPreferences(context) 28 | preferences.edit().putBoolean(CulebraTesterApplication.PREFERENCE_ONBOARDING_SHOWED, true).apply() 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/dtmilano/android/culebratester2/MainActivity.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("MainActivityKt") 2 | 3 | package com.dtmilano.android.culebratester2 4 | 5 | import android.os.Bundle 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.setContent 8 | import androidx.compose.foundation.Image 9 | import androidx.compose.foundation.background 10 | import androidx.compose.foundation.layout.* 11 | import androidx.compose.material.MaterialTheme 12 | import androidx.compose.material.Surface 13 | import androidx.compose.material.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.alpha 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.platform.LocalContext 20 | import androidx.compose.ui.res.painterResource 21 | import androidx.compose.ui.res.stringResource 22 | import androidx.compose.ui.text.font.FontFamily 23 | import androidx.compose.ui.text.font.FontWeight 24 | import androidx.compose.ui.text.style.TextAlign 25 | import androidx.compose.ui.tooling.preview.Preview 26 | import androidx.compose.ui.tooling.preview.PreviewParameter 27 | import androidx.compose.ui.tooling.preview.PreviewParameterProvider 28 | import androidx.compose.ui.unit.dp 29 | import androidx.compose.ui.unit.sp 30 | import com.dtmilano.android.culebratester2.ui.theme.CulebraTester2Theme 31 | import com.dtmilano.android.culebratester2.utils.PackageUtils 32 | 33 | class MainActivity : ComponentActivity() { 34 | override fun onCreate(savedInstanceState: Bundle?) { 35 | super.onCreate(savedInstanceState) 36 | setContent { 37 | Content() 38 | } 39 | } 40 | } 41 | 42 | @Composable 43 | private fun checkInstrumentationPresent(): Boolean { 44 | val context = LocalContext.current 45 | val instrumentationInfo = PackageUtils.isInstrumentationPresent(context = context) 46 | return (instrumentationInfo != null) 47 | } 48 | 49 | @Preview(showBackground = true) 50 | @Composable 51 | private fun Content() { 52 | CulebraTester2Theme { 53 | Surface(color = MaterialTheme.colors.background) { 54 | Column( 55 | modifier = Modifier.fillMaxSize(), 56 | verticalArrangement = Arrangement.Center, 57 | horizontalAlignment = Alignment.CenterHorizontally 58 | ) { 59 | MonitorImage() 60 | Message(checkInstrumentationPresent()) 61 | } 62 | } 63 | } 64 | } 65 | 66 | @Preview(showBackground = true) 67 | @Composable 68 | fun MonitorImage() { 69 | Surface(color = MaterialTheme.colors.background) { 70 | Image( 71 | painter = painterResource(id = R.drawable.monitor), 72 | contentDescription = "monitor" 73 | ) 74 | } 75 | } 76 | 77 | class IsInstrumentationPresentPreviewParameterProvider : PreviewParameterProvider { 78 | override val values = sequenceOf( 79 | true, 80 | false 81 | ) 82 | } 83 | 84 | @Preview(showBackground = true) 85 | @Composable 86 | fun MessagePreview( 87 | @PreviewParameter(IsInstrumentationPresentPreviewParameterProvider::class) isInstrumentationPresent: Boolean 88 | ) { 89 | Message(isInstrumentationPresent) 90 | } 91 | 92 | 93 | @Composable 94 | fun Message(isInstrumentationPresent: Boolean = false) { 95 | Column(Modifier.absolutePadding(24.dp, 6.dp, 24.dp, 6.dp)) { 96 | if (isInstrumentationPresent) { 97 | Text( 98 | text = stringResource(id = R.string.msg_instrumentation_installed), 99 | Modifier 100 | .fillMaxWidth(), 101 | textAlign = TextAlign.Center, 102 | fontSize = 24.sp, 103 | fontWeight = FontWeight.Light 104 | ) 105 | Spacer(modifier = Modifier.height(12.dp)) 106 | Text( 107 | text = stringResource(id = R.string.msg_start_server), 108 | Modifier 109 | .fillMaxWidth(), 110 | textAlign = TextAlign.Center 111 | ) 112 | Spacer(modifier = Modifier.height(12.dp)) 113 | Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { 114 | Text( 115 | text = stringResource(id = R.string.msg_start_server_command), 116 | Modifier 117 | .background(Color.DarkGray) 118 | .absolutePadding(12.dp, 12.dp, 12.dp, 12.dp), 119 | fontFamily = FontFamily.Monospace, 120 | textAlign = TextAlign.Center 121 | ) 122 | } 123 | Spacer(modifier = Modifier.height(12.dp)) 124 | Text( 125 | text = stringResource(id = R.string.msg_activity_will_exit), 126 | Modifier 127 | .fillMaxWidth() 128 | .alpha(0.7f), 129 | textAlign = TextAlign.Center, 130 | fontSize = 12.sp 131 | ) 132 | } else { 133 | Text( 134 | text = stringResource(id = R.string.msg_instrumentation_not_installed), 135 | Modifier 136 | .fillMaxWidth(), 137 | textAlign = TextAlign.Center 138 | ) 139 | } 140 | } 141 | } 142 | 143 | @Preview(showBackground = true) 144 | @Composable 145 | fun DefaultPreview() { 146 | CulebraTester2Theme { 147 | Content() 148 | } 149 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dtmilano/android/culebratester2/MainActivityOld.kt: -------------------------------------------------------------------------------- 1 | package com.dtmilano.android.culebratester2 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.view.Menu 7 | import android.view.MenuItem 8 | import androidx.appcompat.app.AppCompatActivity 9 | import com.dtmilano.android.culebratester2.databinding.ActivityMainBinding 10 | import com.dtmilano.android.culebratester2.databinding.ContentMainBinding 11 | import com.dtmilano.android.culebratester2.utils.PackageUtils 12 | import com.google.android.material.snackbar.Snackbar 13 | import dagger.hilt.android.AndroidEntryPoint 14 | 15 | 16 | @AndroidEntryPoint 17 | class MainActivityOld : AppCompatActivity() { 18 | private lateinit var main: ActivityMainBinding 19 | private lateinit var content: ContentMainBinding 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | 24 | main = ActivityMainBinding.inflate(layoutInflater) 25 | val view = main.root 26 | setContentView(view) 27 | 28 | setSupportActionBar(main.toolbar) 29 | 30 | content.mainMessage.text = getString(R.string.main_message_default) 31 | 32 | main.fab.setOnClickListener { _view -> 33 | Snackbar.make(_view, "Replace with your own action", Snackbar.LENGTH_LONG) 34 | .setAction("Action", null).show() 35 | } 36 | 37 | } 38 | 39 | override fun onResume() { 40 | super.onResume() 41 | checkInstrumentationPresent() 42 | } 43 | 44 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 45 | // Inflate the menu; this adds items to the action bar if it is present. 46 | menuInflater.inflate(R.menu.menu_main, menu) 47 | return true 48 | } 49 | 50 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 51 | // Handle action bar item clicks here. The action bar will 52 | // automatically handle clicks on the Home/Up button, so long 53 | // as you specify a parent activity in AndroidManifest.xml. 54 | return when (item.itemId) { 55 | R.id.action_settings -> true 56 | else -> super.onOptionsItemSelected(item) 57 | } 58 | } 59 | 60 | private fun checkInstrumentationPresent() { 61 | val instrumentationInfo = PackageUtils.isInstrumentationPresent(this) 62 | if (instrumentationInfo != null) { 63 | // Do something 64 | // We are resuming and instrumentation is installed, so we should start the tests, however it's not 65 | // possible from the Activity 66 | content.mainMessage.append("\n" + getString(R.string.msg_instrumentation_installed)) 67 | } else { 68 | val snackbar = Snackbar.make( 69 | main.coordinator, 70 | R.string.msg_instrumentation_not_installed, 71 | Snackbar.LENGTH_INDEFINITE 72 | ).setAction(R.string.action_install) { 73 | // FIXME: can we install from github? 74 | } 75 | snackbar.show() 76 | } 77 | } 78 | 79 | companion object { 80 | /** 81 | * Convenience function to start the MainActivity. 82 | */ 83 | fun start(context: Context) { 84 | val main = Intent(Intent.ACTION_MAIN) 85 | main.setClass(context, MainActivityOld::class.java) 86 | context.startActivity(main) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/dtmilano/android/culebratester2/ObjectStore.kt: -------------------------------------------------------------------------------- 1 | package com.dtmilano.android.culebratester2 2 | 3 | import java.util.* 4 | import java.util.concurrent.ConcurrentSkipListMap 5 | import javax.inject.Inject 6 | import javax.inject.Singleton 7 | 8 | /** 9 | * Created by diego on 2016-02-26. 10 | */ 11 | @Singleton 12 | //class ObjectStore private constructor() { 13 | class ObjectStore @Inject constructor() { 14 | private val nextOid: Int 15 | get() = try { 16 | objectMap.lastKey() 17 | } catch (e: NoSuchElementException) { 18 | 0 19 | } + 1 20 | 21 | fun size(): Int { 22 | return objectMap.size 23 | } 24 | 25 | fun lastKey(): Int { 26 | return objectMap.lastKey() 27 | } 28 | 29 | operator fun get(oid: Int): Any? { 30 | return objectMap[oid] 31 | } 32 | 33 | fun remove(oid: Int) { 34 | objectMap.remove(oid) 35 | } 36 | 37 | fun list(): SortedMap { 38 | return objectMap 39 | } 40 | 41 | fun put(obj: Any): Int { 42 | nextOid.let { put(it, obj); return@put it } 43 | } 44 | 45 | fun clear() { 46 | objectMap.clear() 47 | } 48 | 49 | private fun put(oid: Int, obj: Any) { 50 | objectMap[oid] = obj 51 | } 52 | 53 | companion object { 54 | //val instance = ObjectStore() 55 | 56 | private val objectMap = ConcurrentSkipListMap() 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/dtmilano/android/culebratester2/OnboardingActivity.kt: -------------------------------------------------------------------------------- 1 | package com.dtmilano.android.culebratester2 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.os.Handler 8 | import android.view.MotionEvent 9 | import android.view.View 10 | import androidx.appcompat.app.AppCompatActivity 11 | import com.dtmilano.android.culebratester2.databinding.ActivityOnboardingBinding 12 | 13 | /** 14 | * An example full-screen activity that shows and hides the system UI (i.e. 15 | * status bar and navigation/system bar) with user interaction. 16 | */ 17 | class OnboardingActivity : AppCompatActivity() { 18 | private lateinit var onboarding: ActivityOnboardingBinding 19 | 20 | //private lateinit var fullscreenContent: View 21 | //private lateinit var fullscreenContentControls: LinearLayout 22 | private val hideHandler = Handler() 23 | 24 | @SuppressLint("InlinedApi") 25 | private val hidePart2Runnable = Runnable { 26 | // Delayed removal of status and navigation bar 27 | 28 | // Note that some of these constants are new as of API 16 (Jelly Bean) 29 | // and API 19 (KitKat). It is safe to use them, as they are inlined 30 | // at compile-time and do nothing on earlier devices. 31 | onboarding.fullscreenContent.systemUiVisibility = 32 | View.SYSTEM_UI_FLAG_LOW_PROFILE or 33 | View.SYSTEM_UI_FLAG_FULLSCREEN or 34 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE or 35 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or 36 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or 37 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 38 | } 39 | private val showPart2Runnable = Runnable { 40 | // Delayed display of UI elements 41 | supportActionBar?.show() 42 | onboarding.fullscreenContentControls.visibility = View.VISIBLE 43 | } 44 | private var isFullscreen: Boolean = false 45 | 46 | private val hideRunnable = Runnable { hide() } 47 | 48 | /** 49 | * Touch listener to use for in-layout UI controls to delay hiding the 50 | * system UI. This is to prevent the jarring behavior of controls going away 51 | * while interacting with activity UI. 52 | */ 53 | private val delayHideTouchListener = View.OnTouchListener { view, motionEvent -> 54 | when (motionEvent.action) { 55 | MotionEvent.ACTION_DOWN -> if (AUTO_HIDE) { 56 | delayedHide(AUTO_HIDE_DELAY_MILLIS) 57 | } 58 | MotionEvent.ACTION_UP -> view.performClick() 59 | else -> { 60 | } 61 | } 62 | false 63 | } 64 | 65 | @SuppressLint("ClickableViewAccessibility") 66 | override fun onCreate(savedInstanceState: Bundle?) { 67 | super.onCreate(savedInstanceState) 68 | 69 | onboarding = ActivityOnboardingBinding.inflate(layoutInflater) 70 | val view = onboarding.root 71 | setContentView(view) 72 | 73 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 74 | 75 | isFullscreen = true 76 | 77 | // Set up the user interaction to manually show or hide the system UI. 78 | //fullscreenContent = findViewById(R.id.fullscreen_content) 79 | //fullscreenContent.setOnClickListener { toggle() } 80 | onboarding.fullscreenContent.setOnClickListener { toggle() } 81 | 82 | //fullscreenContentControls = findViewById(R.id.fullscreen_content_controls) 83 | 84 | // Upon interacting with UI controls, delay any scheduled hide() 85 | // operations to prevent the jarring behavior of controls going away 86 | // while interacting with the UI. 87 | //findViewById