diff --git a/.circleci/config.yml b/.circleci/config.yml index aca01a1e..29704536 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,7 @@ version: 2.1 orbs: codecov: codecov/codecov@3.2.4 + ruby: circleci/ruby@2.0.0 commands: restore_gradle_cache: @@ -21,26 +22,31 @@ commands: - ~/.gradle/caches key: v1-gradle-cache-{{ arch }}-{{ checksum "build.gradle" }} -jobs: - android-test: +executors: + android-machine: machine: image: android:2022.12.1 + android-docker: + docker: + - image: cimg/android:2023.02 + +jobs: + android-test: + executor: android-machine resource_class: large steps: - - run: lsb_release -a - checkout - - run: python3 scripts/generateKsPropFile.py - - run: python3 scripts/generateGoogleServicesJson.py - restore_gradle_cache - - run: ./gradlew tasks + - ruby/install-deps: + with-cache: true + - run: + name: Fastlane - run all tests with coverage report + command: | + bundle exec fastlane testDev - save_gradle_cache - - run: sdkmanager --list - - run: (yes || true) | sdkmanager "tools" "platform-tools" "build-tools;33.0.2" "platforms;android-33" "system-images;android-30;google_atd;x86" - - run: (yes || true) | sdkmanager --licenses - - run: ./gradlew assembleDebug --stacktrace - - run: ./gradlew pixel2api30DebugAndroidTest --stacktrace - - run: ./gradlew testDebugUnitTest --stacktrace - - run: ./gradlew jacocoTestReport --stacktrace + - run: + name: Analyze on SonarCloud + command: ./gradlew lintDebug sonar - codecov/upload: file: app/build/mergedReportDir/jacocoTestReport/jacocoTestReport.xml - store_test_results: @@ -48,7 +54,25 @@ jobs: - store_artifacts: path: app/build/mergedReportDir + distribute-dev: + executor: android-docker + resource_class: large + steps: + - checkout + - restore_gradle_cache + - run: + name: Prepare Fastlane + command: sudo bundle update + - run: + name: Distribute to Firebase AppTester + command: bundle exec fastlane distDev + - store_artifacts: + path: /home/circleci/project/app/build/outputs/apk/debug/app-debug.apk + destination: fastlane-output-debug + workflows: workflow: jobs: - - android-test + - android-test: + context: SonarCloud + - distribute-dev diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 86605917..6a96ad37 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -12,24 +12,31 @@ jobs: with: ruby-version: '3.0' bundler-cache: true - - name: set up JDK 11 uses: actions/setup-java@v1 with: distribution: 'zulu' java-version: '11' - - name: set up Python - uses: actions/setup-python@v4 + - name: Cache Gradle wrapper + uses: actions/cache@v3 + with: + path: ~/.gradle/wrapper + key: ${{ runner.OS }}-gradle-wrapper-cache-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.OS }}-gradle-wrapper-cache- + - name: Cache Gradle caches + uses: actions/cache@v3 with: - python-version: '3.10' + path: ~/.gradle/caches + key: ${{ runner.OS }}-gradle-caches-cache-${{ hashFiles('build.gradle') }} + restore-keys: | + ${{ runner.OS }}-gradle-caches-cache- - name: generate ksProp file - run: python scripts/generateKsPropFile.py - - name: create google-services.json file - run: cat app/google-services.json | base64 - - name: update google-services.json file + run: ./gradlew generateKsPropFile + - name: generate google-services.json file + run: ./gradlew generateGoogleServicesJson env: - DATA: ${{ secrets.GOOGLE_SERVICES_JSON }} - run: echo $DATA > app/google-services.json + GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} - name: setup fastlane run: bundle install - name: run unit tests @@ -49,18 +56,12 @@ jobs: with: distribution: 'zulu' java-version: '11' - - name: set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.10' - name: generate ksProp file - run: python scripts/generateKsPropFile.py - - name: create google-services.json file - run: cat app/google-services.json | base64 - - name: update google-services.json file + run: ./gradlew generateKsPropFile + - name: generate google-services.json file + run: ./gradlew generateGoogleServicesJson env: - DATA: ${{ secrets.GOOGLE_SERVICES_JSON }} - run: echo $DATA > app/google-services.json + GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} - name: Lint run: bash ./gradlew lintDebug - name: Lint results @@ -70,7 +71,7 @@ jobs: path: app/build/reports/lint-results-debug.html ui-test: - runs-on: macOS-latest + runs-on: macos-latest steps: - name: checkout uses: actions/checkout@v2 @@ -79,18 +80,12 @@ jobs: with: distribution: 'zulu' java-version: '11' - - name: set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.10' - name: generate ksProp file - run: python scripts/generateKsPropFile.py - - name: create google-services.json file - run: cat app/google-services.json | base64 - - name: update google-services.json file + run: ./gradlew generateKsPropFile + - name: generate google-services.json file + run: ./gradlew generateGoogleServicesJson env: - DATA: ${{ secrets.GOOGLE_SERVICES_JSON }} - run: echo $DATA > app/google-services.json + GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} - name: run tests uses: reactivecircus/android-emulator-runner@v2 with: diff --git a/Gemfile b/Gemfile index adc90d98..2ccf2ecb 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ source "https://rubygems.org" -gem "fastlane" \ No newline at end of file +gem "fastlane" +plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock index 944b5730..7a0078bc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,24 +1,24 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.5) + CFPropertyList (3.0.6) rexml addressable (2.8.1) public_suffix (>= 2.0.2, < 6.0) artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.642.0) - aws-sdk-core (3.158.0) + aws-partitions (1.725.0) + aws-sdk-core (3.170.0) aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.525.0) - aws-sigv4 (~> 1.1) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.5) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.58.0) - aws-sdk-core (~> 3, >= 3.127.0) + aws-sdk-kms (1.63.0) + aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.114.0) - aws-sdk-core (~> 3, >= 3.127.0) + aws-sdk-s3 (1.119.1) + aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.5.2) @@ -36,8 +36,8 @@ GEM unf (>= 0.0.5, < 1.0.0) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.93.0) - faraday (1.10.2) + excon (0.99.0) + faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -66,7 +66,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.6) - fastlane (2.210.1) + fastlane (2.212.1) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -105,10 +105,11 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) + fastlane-plugin-firebase_app_distribution (0.5.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.29.0) - google-apis-core (>= 0.9.0, < 2.a) - google-apis-core (0.9.0) + google-apis-androidpublisher_v3 (0.35.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.0) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -117,10 +118,10 @@ GEM retriable (>= 2.0, < 4.a) rexml webrick - google-apis-iamcredentials_v1 (0.15.0) - google-apis-core (>= 0.9.0, < 2.a) - google-apis-playcustomapp_v1 (0.11.0) - google-apis-core (>= 0.9.0, < 2.a) + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.19.0) google-apis-core (>= 0.9.0, < 2.a) google-cloud-core (1.6.0) @@ -128,8 +129,8 @@ GEM google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.0) - google-cloud-storage (1.43.0) + google-cloud-errors (1.3.1) + google-cloud-storage (1.44.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) @@ -137,7 +138,7 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.2.0) + googleauth (1.3.0) faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -148,11 +149,11 @@ GEM http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) - jmespath (1.6.1) - json (2.6.2) - jwt (2.5.0) + jmespath (1.6.2) + json (2.6.3) + jwt (2.7.0) memoist (0.16.2) - mini_magick (4.11.0) + mini_magick (4.12.0) mini_mime (1.1.2) multi_json (1.15.0) multipart-post (2.0.0) @@ -160,8 +161,8 @@ GEM naturally (2.2.1) optparse (0.1.1) os (1.1.4) - plist (3.6.0) - public_suffix (5.0.0) + plist (3.7.0) + public_suffix (5.0.1) rake (13.0.6) representable (3.2.0) declarative (< 0.1.0) @@ -178,7 +179,7 @@ GEM faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - simctl (1.6.8) + simctl (1.6.10) CFPropertyList naturally terminal-notifier (2.0.0) @@ -195,7 +196,7 @@ GEM unf_ext (0.0.8.2) unf_ext (0.0.8.2-x64-mingw32) unicode-display_width (1.8.0) - webrick (1.7.0) + webrick (1.8.1) word_wrap (1.0.0) xcodeproj (1.22.0) CFPropertyList (>= 2.3.3, < 4.0) @@ -217,6 +218,7 @@ PLATFORMS DEPENDENCIES fastlane + fastlane-plugin-firebase_app_distribution BUNDLED WITH - 2.3.7 + 2.4.7 diff --git a/README.md b/README.md index e0102fa7..01708181 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,22 @@ # arduino-usb-terminal - Terminal-like app to send commands to Arduino through USB +Simplify testing your IoT projects by using your Android device to send commands to Arduino through USB. ![Android CI](https://github.com/k4biri/arduino-usb-terminal/workflows/Android%20CI/badge.svg) [![superus8r](https://circleci.com/gh/superus8r/arduino-usb-terminal.svg?style=shield)](https://circleci.com/gh/superus8r/arduino-usb-terminal) [![codecov](https://codecov.io/gh/superus8r/arduino-usb-terminal/branch/develop/graph/badge.svg?token=RYIUU345QG)](https://codecov.io/gh/superus8r/arduino-usb-terminal) +[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=superus8r_arduino-usb-terminal&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=superus8r_arduino-usb-terminal) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=superus8r_arduino-usb-terminal&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=superus8r_arduino-usb-terminal) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=superus8r_arduino-usb-terminal&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=superus8r_arduino-usb-terminal) + + Ever thought of testing your Arduino project on the go without using a lap top? + + Sometimes, you just want to send simple commands to an Arduino through USB without getting that 1.4 kilogram laptop out of the bag! 😉 - This app simplifies testing your Arduino components that work with direct usb commands by giving you the ability to send custom commands and view the returned message from your Arduino device. + Especially if the Android phone in your pocket has enough resources to do that! - This is being done as a hobby, and for experimenting, so probably there might be some flaws; As an example, the vendor ID of Arduino is hardcoded to only work with Arduino devices, but this is my use case and please feel free to change it to match your needs. + This is being done as a hobby, and for experimenting, so probably there might be some flaws; As an example, the vendor ID of Arduino is set to only work with Arduino devices, but this is my use case and please feel free to change it to match your needs. Or if you have a great idea to make this dynamic wile keeping the app simple, please feel free to open a pull request! # Build and Run The app is available for free on Google Play Store (Arduino USB Terminal). @@ -17,32 +24,60 @@ Please read the **Sentry Reports** part on this page before running the project to avoid build failures. Get it on Google Play + + + # Tests + You can run all the instrumented tests using a Gradle Managed Device in one line: +``` +./gradlew pixel2api30DebugAndroidTest +``` +This command will run an Android emulator on the background and run all the tests. +After a successful run, the test coverage file will be available in your build folder: +``` +app/build/outputs/managed_device_code_coverage/pixel2api30/coverage.ec +``` +To create a unified coverage report you can use the Jacoco task: +``` +./gradlew jacocoTestReport +// Jacoco HTML and XML output will be under the following path: +app/build/mergedReportDir/jacocoTestReport +``` +Gradle automatically recognizes your environment and configures, runs, and closes the emulator in the background. +This has been tested on environments with Arm (Apple M1) and Intel CPUs. + +More info about Gradle managed devices in official Android testing docs: [Scale your tests with Gradle Managed Devices](https://developer.android.com/studio/test/gradle-managed-devices) + + - ## Terminal - A Simple terminal page which does what it is supposed to do interacting with an Arduino manually through the USB cable. + ## Firebase Crashlytics Reports + The project uses Firebase Crashlytics for the crash reports, therefore you will need to create a free Firebase project to use it. + - Once you create a Firebase project, Firebase will provide you with a config file (`google-services.json`). + - Place your `google-services.json` file under the `app/` directory and build the project to activate it. - ## Joystick - The Joystick is removed for the first release. + More info on [Firebase official docs for getting started with Firebase Crashlytics](https://firebase.google.com/docs/crashlytics/get-started?platform=android) - ## Tests - Currently, there are some basic tests to run on the CI, but needs improvements. - UI tests will be completed once the project is migrated to Jetpack Compose. + If this is not needed, you can remove the crashlytics dependency in project leve and app level build.gradle files. + - remove `classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.4"` from `build.gradle` file + - remove `implementation("com.google.firebase:firebase-crashlytics-ktx")` from `app/build.gradle.kts` file + + +## Sonar Cloud Analysis +The project uses Sonar Cloud manual analysis to detect code smells and potential bugs +To run the manual analysis locally, use the gradle `sonar` task: +``` +./gradlew sonar +``` +Since this uses the gradle scanner, the sonar properties are defined in root project's `build.gradle` file. +On one hand, the Sonar scanner requires local paths for `sonar.sources` and `sonar.binaries` properties, on the other hand it requires absolute path for `sonar.androidLint.reportPaths` and `sonar.coverage.jacoco.xmlReportPaths`. +More info on official Sonar docs: [SonarScanner for Gradle](https://docs.sonarcloud.io/advanced-setup/ci-based-analysis/sonarscanner-for-gradle/) + - ## Sentry Reports - The project uses Sentry for the crash reports, if this is not needed, you can remove the following line in `AndroidManifest.xml`: - `` - But if it is needed, you need to [create a Sentry dsn value](https://docs.sentry.io/platforms/android/) to put under the following path: - `app/src/main/res/values/api_keys.xml` - The file contents might look like similar to this: - ` - - YOUR_SENTRY_SPECIFIC_VALUE - ` + ## Knows Issues +- Please feel free inform me about new issues - ### Knows Issues - _Still unknown! :) Suggestions and PRs are welcome! :) +--- ### More comes as the project evolves... \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 23b09bd3..253fb968 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,7 +15,7 @@ repositories { } android { - compileSdkVersion(33) + compileSdk = 33 compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 @@ -24,29 +24,30 @@ android { defaultConfig { applicationId = "org.kabiri.android.usbterminal" - minSdkVersion(23) - targetSdkVersion(33) - versionCode = 13 - versionName = "0.9.12" + minSdk = 23 + targetSdk = 33 + versionCode = System.getenv("CIRCLE_BUILD_NUM")?.toIntOrNull() ?: 13 + versionName = "0.9.12${System.getenv("CIRCLE_BUILD_NUM") ?: ""}" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } signingConfigs { - // read release credentials from keystore.properties file - val ksProp = Properties() - // load keys inside the ksProp file - loadKeyStore(ksProp) - create("release") { - keyAlias = ksProp.getProperty("release.keyAlias") - keyPassword = ksProp.getProperty("release.keyPassword") - storeFile = file(ksProp.getProperty("release.file")) - storePassword = ksProp.getProperty("release.storePassword") + + val ksName = "keystore.properties" + val ksProp = loadKeyStore(ksName) + ksProp?.let { + create("release") { + keyAlias = ksProp.getProperty("release.keyAlias") + keyPassword = ksProp.getProperty("release.keyPassword") + storeFile = file(ksProp.getProperty("release.file")) + storePassword = ksProp.getProperty("release.storePassword") + } } } buildTypes { named("release") { - signingConfig = signingConfigs.getByName("release") + signingConfig = signingConfigs.findByName("release") isMinifyEnabled = false proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } @@ -103,12 +104,63 @@ tasks.register("jacocoTestReport") { ))}) } -fun loadKeyStore(ksProp: Properties) { - val ksPropFile = file("keystore.properties") - if (ksPropFile.exists()) { +sonarqube { + properties { + property("sonar.projectKey", System.getenv("SONAR_PROJECT_KEY")) + property("sonar.organization", System.getenv("SONAR_ORGANIZATION")) + property("sonar.host.url", System.getenv("SONAR_HOST_URL")) + } +} + +tasks.register("generateGoogleServicesJson") { + doLast { + val jsonFileName = "google-services.json" + val fileContent = System.getenv("GOOGLE_SERVICES_JSON") + File(projectDir, jsonFileName).apply { + createNewFile(); writeText(fileContent) + println("generated $jsonFileName") + } + } +} + +tasks.register("generateKsPropFile") { + doLast { + val configFileName = "keystore.properties" + File(projectDir, configFileName).apply { + createNewFile() + writeText(""" + # Gradle signing properties for app module + release.file=${System.getenv("KS_PATH")} + release.storePassword=${System.getenv("KS_PASSWORD")} + release.keyAlias=${System.getenv("KS_KEY_ALIAS")} + release.keyPassword=${System.getenv("KS_KEY_PASSWORD")} + """.trimIndent()) + println("generated ${this.path}") + } + } +} + +tasks.register("generateAppDistKey") { + doLast { + val jsonFileName = "app-dist-key.json" + val fileContent = System.getenv("GOOGLE_APP_DIST_FASTLANE_SERVICE_ACCOUNT") + File(rootDir, jsonFileName).apply { + createNewFile() + writeText(fileContent) + println("generated ${this.path}") + } + } +} + +fun loadKeyStore(name: String): Properties? { + val ksProp = Properties() + val ksPropFile = file(name) + return if (ksPropFile.exists()) { ksProp.load(FileInputStream(ksPropFile)) + ksProp } else { println("ERROR: local keystore file not found") + null } } @@ -116,7 +168,6 @@ val firebase_bom_version: String by project val hilt_version: String by project dependencies { -// implementation fileTree("libs") { include(setOf("*.jar")) } implementation("androidx.appcompat:appcompat:1.6.0") implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c87c749d..521743c2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@