diff --git a/.aiexclude b/.aiexclude new file mode 100644 index 00000000..844da302 --- /dev/null +++ b/.aiexclude @@ -0,0 +1,89 @@ +# Built application files +*.apk +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/* + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ +app/src/main/res/values/api_keys.xml +.idea/compiler.xml +.idea/compiler.xml +.idea/compiler.xml +.idea/jarRepositories.xml +.idea/misc.xml +app/build.gradle +.idea/misc.xml +.idea/misc.xml +.idea/misc.xml +.idea/misc.xml +.idea/misc.xml +.idea/.name +.idea/codeStyles/Project.xml +.idea/misc.xml +.idea/misc.xml diff --git a/.circleci/config.yml b/.circleci/config.yml index 29704536..5aca1c2e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,10 +25,10 @@ commands: executors: android-machine: machine: - image: android:2022.12.1 + image: android:2024.01.1 android-docker: docker: - - image: cimg/android:2023.02 + - image: cimg/android:2024.01 jobs: android-test: @@ -64,15 +64,35 @@ jobs: name: Prepare Fastlane command: sudo bundle update - run: - name: Distribute to Firebase AppTester + name: Distribute to Firebase AppTester Dev command: bundle exec fastlane distDev - store_artifacts: path: /home/circleci/project/app/build/outputs/apk/debug/app-debug.apk destination: fastlane-output-debug + distribute-internal-testing: + executor: android-docker + resource_class: large + steps: + - checkout + - restore_gradle_cache + - run: + name: Prepare Fastlane + command: sudo bundle update + - run: + name: Distribute to Google PlayStore Internal Testing + command: bundle exec fastlane deploy + - run: + name: Distribute to Firebase AppTester Prod + command: bundle exec fastlane distProd + - store_artifacts: + path: /home/circleci/project/app/build/outputs/bundle/release/app-release.aab + destination: fastlane-output-release + workflows: workflow: jobs: - android-test: context: SonarCloud - distribute-dev + - distribute-internal-testing diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 6a96ad37..e03a03bd 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -1,61 +1,61 @@ name: Android CI -on: [push] +on: [ push ] jobs: unit-test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: ruby/setup-ruby@v1 - 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: 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: - path: ~/.gradle/caches - key: ${{ runner.OS }}-gradle-caches-cache-${{ hashFiles('build.gradle') }} - restore-keys: | - ${{ runner.OS }}-gradle-caches-cache- - - name: generate ksProp file - run: ./gradlew generateKsPropFile - - name: generate google-services.json file - run: ./gradlew generateGoogleServicesJson - env: - GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} - - name: setup fastlane - run: bundle install - - name: run unit tests - run: bundle exec fastlane android run_unit_tests - - name: Unit tests results - uses: actions/upload-artifact@v1 - with: - name: unit-test-results - path: app/build/reports/tests/testDebugUnitTest/index.html + - uses: actions/checkout@v3 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.1' + bundler-cache: true + - name: set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + - 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: + path: ~/.gradle/caches + key: ${{ runner.OS }}-gradle-caches-cache-${{ hashFiles('build.gradle') }} + restore-keys: | + ${{ runner.OS }}-gradle-caches-cache- + - name: generate ksProp file + run: ./gradlew generateKsPropFile + - name: generate google-services.json file + run: ./gradlew generateGoogleServicesJson + env: + GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} + - name: setup fastlane + run: bundle install + - name: run unit tests + run: bundle exec fastlane android run_unit_tests + - name: Unit tests results + uses: actions/upload-artifact@v4 + with: + name: unit-test-results + path: app/build/reports/tests/testDebugUnitTest/index.html lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - - name: set up JDK 11 + - name: set up JDK 17 uses: actions/setup-java@v1 with: distribution: 'zulu' - java-version: '11' + java-version: '17' - name: generate ksProp file run: ./gradlew generateKsPropFile - name: generate google-services.json file @@ -65,27 +65,32 @@ jobs: - name: Lint run: bash ./gradlew lintDebug - name: Lint results - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: app path: app/build/reports/lint-results-debug.html ui-test: - runs-on: macos-latest + runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v2 - - name: set up JDK 11 - uses: actions/setup-java@v1 + - name: set up JDK 17 + uses: actions/setup-java@v4 with: distribution: 'zulu' - java-version: '11' + java-version: '17' - name: generate ksProp file run: ./gradlew generateKsPropFile - name: generate google-services.json file run: ./gradlew generateGoogleServicesJson env: GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm - name: run tests uses: reactivecircus/android-emulator-runner@v2 with: diff --git a/.gitignore b/.gitignore index 7769739e..844da302 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,4 @@ app/build.gradle .idea/.name .idea/codeStyles/Project.xml .idea/misc.xml +.idea/misc.xml diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml index c14ef4bb..87ee7a8a 100644 --- a/.idea/androidTestResultsUserPreferences.xml +++ b/.idea/androidTestResultsUserPreferences.xml @@ -3,6 +3,99 @@ diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 00000000..0ff848d8 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 00000000..0fc31131 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 669e0711..49bf8ca1 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - @@ -6,7 +5,7 @@ - + diff --git a/Gemfile.lock b/Gemfile.lock index 7a0078bc..d5b4e4ee 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,43 +1,47 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml - addressable (2.8.1) - public_suffix (>= 2.0.2, < 6.0) - artifactory (3.0.15) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.2.0) - aws-partitions (1.725.0) - aws-sdk-core (3.170.0) - aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) + aws-eventstream (1.3.2) + aws-partitions (1.1095.0) + aws-sdk-core (3.222.3) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.63.0) - aws-sdk-core (~> 3, >= 3.165.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.119.1) - aws-sdk-core (~> 3, >= 3.165.0) + logger + aws-sdk-kms (1.99.0) + aws-sdk-core (~> 3, >= 3.216.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.184.0) + aws-sdk-core (~> 3, >= 3.216.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.4) - aws-sigv4 (1.5.2) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.11.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) + base64 (0.2.0) claide (1.1.0) colored (1.2) colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) declarative (0.0.20) - digest-crc (0.6.4) + digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) + domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.99.0) - faraday (1.10.3) + excon (0.112.0) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -56,17 +60,17 @@ GEM faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (1.0.1) + faraday-multipart (1.1.0) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) - fastimage (2.2.6) - fastlane (2.212.1) + fastimage (2.4.0) + fastlane (2.218.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -87,94 +91,108 @@ GEM google-apis-playcustomapp_v1 (~> 0.1) google-cloud-storage (~> 1.31) highline (~> 2.0) + http-cookie (~> 1.0.5) json (< 3.0.0) jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) - multipart-post (~> 2.0.0) + multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (~> 0.1.1) + optparse (>= 0.1.1) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.3) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (>= 1.4.5, < 2.0.0) + terminal-table (~> 3) tty-screen (>= 0.6.3, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) 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) + fastlane-plugin-firebase_app_distribution (0.9.1) + google-apis-firebaseappdistribution_v1 (~> 0.3.0) + google-apis-firebaseappdistribution_v1alpha (~> 0.2.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.35.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.0) + google-apis-androidpublisher_v3 (0.78.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-core (0.17.0) addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) + googleauth (~> 1.9) + httpclient (>= 2.8.3, < 3.a) mini_mime (~> 1.0) + mutex_m representable (~> 3.0) retriable (>= 2.0, < 4.a) - rexml - webrick - google-apis-iamcredentials_v1 (0.17.0) + google-apis-firebaseappdistribution_v1 (0.3.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-playcustomapp_v1 (0.13.0) + google-apis-firebaseappdistribution_v1alpha (0.2.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) - google-cloud-env (~> 1.0) + google-apis-iamcredentials_v1 (0.23.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.16.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.50.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.8.0) + google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (1.6.0) - faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.1) - google-cloud-storage (1.44.0) + google-cloud-env (2.3.0) + base64 (~> 0.2) + faraday (>= 1.0, < 3.a) + google-cloud-errors (1.5.0) + google-cloud-storage (1.56.0) addressable (~> 2.8) digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.19.0) + google-apis-core (~> 0.13) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (>= 0.42) google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) + googleauth (~> 1.9) mini_mime (~> 1.0) - googleauth (1.3.0) - faraday (>= 0.17.3, < 3.a) + google-logging-utils (0.2.0) + googleauth (1.14.0) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) jwt (>= 1.4, < 3.0) - memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.8) domain_name (~> 0.5) - httpclient (2.8.3) + httpclient (2.9.0) + mutex_m jmespath (1.6.2) - json (2.6.3) - jwt (2.7.0) - memoist (0.16.2) - mini_magick (4.12.0) - mini_mime (1.1.2) + json (2.11.3) + jwt (2.10.1) + base64 + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) multi_json (1.15.0) - multipart-post (2.0.0) - nanaimo (0.3.0) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) naturally (2.2.1) - optparse (0.1.1) + nkf (0.2.0) + optparse (0.6.0) os (1.1.4) - plist (3.7.0) - public_suffix (5.0.1) - rake (13.0.6) + plist (3.7.2) + public_suffix (6.0.2) + rake (13.2.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.5) + rexml (3.4.1) rouge (2.0.7) ruby2_keywords (0.0.5) - rubyzip (2.3.2) + rubyzip (2.4.1) security (0.1.3) - signet (0.17.0) + signet (0.20.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) @@ -183,28 +201,23 @@ GEM CFPropertyList naturally terminal-notifier (2.0.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) trailblazer-option (0.1.2) tty-cursor (0.7.1) - tty-screen (0.8.1) + tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unf_ext (0.0.8.2-x64-mingw32) - unicode-display_width (1.8.0) - webrick (1.8.1) + unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.22.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (~> 3.2.4) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 253fb968..5ebd0cf9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,6 +6,7 @@ plugins { id("com.google.gms.google-services") id("com.google.firebase.crashlytics") id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") id("kotlin-kapt") id("dagger.hilt.android.plugin") id("jacoco") @@ -15,19 +16,28 @@ repositories { } android { - compileSdk = 33 compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } + kotlin { + jvmToolchain(17) + } + + buildFeatures { + compose = true + buildConfig = true + } + + compileSdk = 35 defaultConfig { applicationId = "org.kabiri.android.usbterminal" - minSdk = 23 - targetSdk = 33 - versionCode = System.getenv("CIRCLE_BUILD_NUM")?.toIntOrNull() ?: 13 - versionName = "0.9.12${System.getenv("CIRCLE_BUILD_NUM") ?: ""}" + minSdk = 24 + targetSdk = 35 + versionCode = System.getenv("CIRCLE_BUILD_NUM")?.toIntOrNull() ?: 14 + versionName = "0.9.84${System.getenv("CIRCLE_BUILD_NUM") ?: ""}" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -48,7 +58,7 @@ android { buildTypes { named("release") { signingConfig = signingConfigs.findByName("release") - isMinifyEnabled = false + isMinifyEnabled = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } named("debug") { @@ -71,6 +81,13 @@ android { } } + packaging { + resources { + excludes += "META-INF/LICENSE.md" + excludes += "META-INF/LICENSE-notice.md" + } + } + namespace = "org.kabiri.android.usbterminal" } @@ -93,12 +110,12 @@ tasks.register("jacocoTestReport") { } val fileFilter = listOf("**/R.class", "**/R$*.class", "**/BuildConfig.*", "**/Manifest*.*", "**/*Test*.*", "android/**/*.*") - val debugTree = fileTree("${buildDir}/tmp/kotlin-classes/debug") { exclude(fileFilter) } - val mainSrc = "${project.projectDir}/src/main/kotlin" + val debugTree = fileTree("${layout.buildDirectory}/tmp/kotlin-classes/debug") { exclude(fileFilter) } + val mainSrc = "${layout.projectDirectory}/src/main/kotlin" sourceDirectories.from(files(setOf(mainSrc))) classDirectories.from(files(setOf(debugTree))) - executionData.from(fileTree(buildDir) { include(setOf( + executionData.from(fileTree(layout.buildDirectory) { include(setOf( "outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec", "outputs/managed_device_code_coverage/pixel2api30/coverage.ec" ))}) @@ -123,6 +140,19 @@ tasks.register("generateGoogleServicesJson") { } } +tasks.register("generateKsFile") { + doLast { + val jsonFileName = "bad.json" + val encodedFileContent = System.getenv("KS_USB_TERMINAL_PLAY_STORE_RAW") + val decodedBytes = Base64.getDecoder().decode(encodedFileContent) + File(projectDir, jsonFileName).apply { + createNewFile() + writeBytes(decodedBytes) + println("generated ${this.path}") + } + } +} + tasks.register("generateKsPropFile") { doLast { val configFileName = "keystore.properties" @@ -130,10 +160,10 @@ tasks.register("generateKsPropFile") { 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")} + release.file=${System.getenv("USB_TERMINAL_KS_PATH")} + release.storePassword=${System.getenv("USB_TERMINAL_KS_PASSWORD")} + release.keyAlias=${System.getenv("USB_TERMINAL_KS_KEY_ALIAS")} + release.keyPassword=${System.getenv("USB_TERMINAL_KS_KEY_PASSWORD")} """.trimIndent()) println("generated ${this.path}") } @@ -152,6 +182,18 @@ tasks.register("generateAppDistKey") { } } +tasks.register("generateInternalReleaseKey") { + doLast { + val jsonFileName = "internal-release-key.json" + val fileContent = System.getenv("SERVICE_ACCOUNT_USB_TERMINAL_PLAY_STORE_RAW") + File(rootDir, jsonFileName).apply { + createNewFile() + writeText(fileContent) + println("generated ${this.path}") + } + } +} + fun loadKeyStore(name: String): Properties? { val ksProp = Properties() val ksPropFile = file(name) @@ -166,11 +208,14 @@ fun loadKeyStore(name: String): Properties? { val firebase_bom_version: String by project val hilt_version: String by project +val coroutines_version: String by project +val material_version: String by project +val mockk_version: String by project dependencies { - implementation("androidx.appcompat:appcompat:1.6.0") - implementation("androidx.core:core-ktx:1.9.0") - implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.appcompat:appcompat:1.7.0") + implementation("androidx.core:core-ktx:1.16.0") + implementation("androidx.constraintlayout:constraintlayout:2.2.1") // Firebase implementation(platform("com.google.firebase:firebase-bom:$firebase_bom_version")) @@ -182,37 +227,61 @@ dependencies { kapt("com.google.dagger:hilt-compiler:$hilt_version") // Coroutines - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1") - implementation("androidx.activity:activity-compose:1.6.1") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7") implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") - - // hilt testing - // more info: - // https://developer.android.com/training/dependency-injection/hilt-testing - androidTestImplementation("com.google.dagger:hilt-android-testing:$hilt_version") - kaptAndroidTest("com.google.dagger:hilt-android-compiler:$hilt_version") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + + // Compose Bom + val composeBom = platform("androidx.compose:compose-bom:2023.06.01") + implementation(composeBom) + androidTestImplementation(composeBom) + implementation("androidx.compose.foundation:foundation") + implementation("androidx.compose.material3:material3") + // Compose - Android Studio Preview support + implementation("androidx.compose.ui:ui-tooling-preview") + debugImplementation("androidx.compose.ui:ui-tooling") + implementation("androidx.activity:activity-compose:1.10.1") + + // Other UI Libraries + implementation("com.google.android.material:material:$material_version") + + // data + implementation("androidx.datastore:datastore-preferences:1.1.4") // unit test libs testImplementation("junit:junit:4.13.2") - testImplementation("com.google.truth:truth:1.1.3") // instrumented test libs - androidTestImplementation("androidx.test:core:1.5.0") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.ext:junit-ktx:1.1.5") - // Espresso - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + androidTestImplementation("androidx.test:core:1.6.1") + androidTestImplementation("androidx.test.ext:junit:1.2.1") + androidTestImplementation("androidx.test.ext:junit-ktx:1.2.1") + androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") + // Hamcrest for view matching androidTestImplementation("org.hamcrest:hamcrest-library:2.2") - androidTestImplementation("androidx.test:runner:1.5.2") - androidTestImplementation("androidx.test:rules:1.5.0") - - /** - * This library helps to automate some parts of the USB serial connection. - * For more information, visit: https://github.com/felHR85/UsbSerial - */ - implementation("com.github.felHR85:UsbSerial:6.1.0") + androidTestImplementation("androidx.test:runner:1.6.2") + androidTestImplementation("androidx.test:rules:1.6.1") + + // coroutine testing + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version") + androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version") + + // google truth for assertions + testImplementation("com.google.truth:truth:1.1.3") + androidTestImplementation("androidx.test.ext:truth:1.6.0") + + // mockk + testImplementation("io.mockk:mockk-android:$mockk_version") + testImplementation("io.mockk:mockk-agent:$mockk_version") + androidTestImplementation("io.mockk:mockk-android:$mockk_version") + androidTestImplementation("io.mockk:mockk-agent:$mockk_version") + + // hilt testing - https://developer.android.com/training/dependency-injection/hilt-testing + androidTestImplementation("com.google.dagger:hilt-android-testing:$hilt_version") + kaptAndroidTest("com.google.dagger:hilt-android-compiler:$hilt_version") + + // Android Serial Controller + implementation("com.github.superus8r:UsbSerial:6.1.1") } \ No newline at end of file diff --git a/app/src/androidTest/java/org/kabiri/android/usbterminal/common/FrequentMocks.kt b/app/src/androidTest/java/org/kabiri/android/usbterminal/common/FrequentMocks.kt new file mode 100644 index 00000000..141f6b8e --- /dev/null +++ b/app/src/androidTest/java/org/kabiri/android/usbterminal/common/FrequentMocks.kt @@ -0,0 +1,27 @@ +package org.kabiri.android.usbterminal.common + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import org.kabiri.android.usbterminal.data.repository.IUserSettingRepository +import org.kabiri.android.usbterminal.model.UserSettingPreferences + +internal fun getFakeUserSettingRepository( + onSetBaudRate: () -> Unit = {}, + fakeUserSetting: UserSettingPreferences = UserSettingPreferences(baudRate = 123) +): IUserSettingRepository { + return object: IUserSettingRepository { + override val preferenceFlow: Flow + get() = flowOf(fakeUserSetting) + + override suspend fun setBaudRate(baudRate: Int) { + onSetBaudRate() + } + + override suspend fun clear() {} + + override suspend fun fetchInitialPreferences(): UserSettingPreferences { + return fakeUserSetting + } + + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepositoryAndroidTest.kt b/app/src/androidTest/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepositoryAndroidTest.kt new file mode 100644 index 00000000..de19d3ae --- /dev/null +++ b/app/src/androidTest/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepositoryAndroidTest.kt @@ -0,0 +1,91 @@ +package org.kabiri.android.usbterminal.data.repository + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStoreFile +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.kabiri.android.usbterminal.model.UserSettingPreferences + +private const val TEST_DATA_STORE_NAME = "test_data_store" +@RunWith(AndroidJUnit4::class) +class UserSettingRepositoryAndroidTest { + + private val testCoroutineDispatcher: TestDispatcher = StandardTestDispatcher() + private val testCoroutineScope = TestScope(testCoroutineDispatcher + Job()) + private val testContext = ApplicationProvider.getApplicationContext() + + private val testDataStore: DataStore = + PreferenceDataStoreFactory.create( + scope = testCoroutineScope, + produceFile = { testContext.preferencesDataStoreFile(TEST_DATA_STORE_NAME) } + ) + + private val repository: UserSettingRepository = UserSettingRepository(testDataStore) + + @Test + fun testFetchInitialPreferences() { + + // arrange + val expected = UserSettingPreferences() + var actual: UserSettingPreferences? = null + + // act + testCoroutineScope.runTest { + actual = repository.fetchInitialPreferences() + } + + // assert + assertThat(actual).isEqualTo(expected) + } + + @Test + fun testWriteBaudRate() { + + // arrange + val customBaudRate = 123 + val expected = UserSettingPreferences( + baudRate = customBaudRate + ) + var actual: UserSettingPreferences? = null + + // act + testCoroutineScope.runTest { + repository.setBaudRate(customBaudRate) + actual = repository.preferenceFlow.first() + } + + // assert + assertThat(actual).isEqualTo(expected) + } + + @Test + fun testClearResetsValuesToDefaults() { + + // arrange + val customBaudRate = 123 + val expected = UserSettingPreferences() + var actual: UserSettingPreferences? = null + + // act + testCoroutineScope.runTest { + repository.setBaudRate(customBaudRate) + repository.clear() + actual = repository.preferenceFlow.first() + } + + // assert + assertThat(actual).isEqualTo(expected) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/kabiri/android/usbterminal/domain/GetCustomBaudRateUseCaseAndroidTest.kt b/app/src/androidTest/java/org/kabiri/android/usbterminal/domain/GetCustomBaudRateUseCaseAndroidTest.kt new file mode 100644 index 00000000..82a33ac1 --- /dev/null +++ b/app/src/androidTest/java/org/kabiri/android/usbterminal/domain/GetCustomBaudRateUseCaseAndroidTest.kt @@ -0,0 +1,30 @@ +package org.kabiri.android.usbterminal.domain + +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.kabiri.android.usbterminal.common.getFakeUserSettingRepository +import org.kabiri.android.usbterminal.model.UserSettingPreferences + +internal class GetCustomBaudRateUseCaseAndroidTest { + + @Test + fun testGetCustomBaudRateUseCaseAndroidTestReturns() = runTest { + + // arrange + val expectedBaudRate = 1234 + val fakeUserSettings = UserSettingPreferences(baudRate = expectedBaudRate) + val fakeUserSettingRepository = + getFakeUserSettingRepository(fakeUserSetting = fakeUserSettings) + val sut = GetCustomBaudRateUseCase( + userSettingRepository = fakeUserSettingRepository + ) + + // act + val actualBaudRate = sut().first() + + // assert + assertThat(actualBaudRate).isEqualTo(expectedBaudRate) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/kabiri/android/usbterminal/domain/SetCustomBaudRateUseCaseAndroidTest.kt b/app/src/androidTest/java/org/kabiri/android/usbterminal/domain/SetCustomBaudRateUseCaseAndroidTest.kt new file mode 100644 index 00000000..6bfb8143 --- /dev/null +++ b/app/src/androidTest/java/org/kabiri/android/usbterminal/domain/SetCustomBaudRateUseCaseAndroidTest.kt @@ -0,0 +1,33 @@ +package org.kabiri.android.usbterminal.domain + +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.kabiri.android.usbterminal.common.getFakeUserSettingRepository + +@OptIn(ExperimentalCoroutinesApi::class) +internal class SetCustomBaudRateUseCaseAndroidTest { + + @Test + fun testSetCustomBaudRateUseCaseAndroidTestCallsSetBaudRateOnRepository() = runTest { + + // arrange + var isCalledSetBaudRate = false + val fakeUserSettingRepository = getFakeUserSettingRepository(onSetBaudRate = { + isCalledSetBaudRate = true + }) + val sut = SetCustomBaudRateUseCase( + userSettingRepository = fakeUserSettingRepository, + scope = this, + ) + + // act + sut.invoke(123) + advanceUntilIdle() + + // assert + assertThat(isCalledSetBaudRate).isTrue() + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a9a2fe6d..07074fa9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,20 +22,15 @@ - + - - - - - diff --git a/app/src/main/java/org/kabiri/android/usbterminal/Constants.kt b/app/src/main/java/org/kabiri/android/usbterminal/Constants.kt index b4357479..a9d918de 100644 --- a/app/src/main/java/org/kabiri/android/usbterminal/Constants.kt +++ b/app/src/main/java/org/kabiri/android/usbterminal/Constants.kt @@ -5,6 +5,6 @@ package org.kabiri.android.usbterminal */ class Constants { companion object { - const val ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION" + const val ACTION_USB_PERMISSION = "org.kabiri.android.usbterminal.USB_PERMISSION" } } \ No newline at end of file diff --git a/app/src/main/java/org/kabiri/android/usbterminal/MainActivity.kt b/app/src/main/java/org/kabiri/android/usbterminal/MainActivity.kt index a3111b18..a0d5902f 100644 --- a/app/src/main/java/org/kabiri/android/usbterminal/MainActivity.kt +++ b/app/src/main/java/org/kabiri/android/usbterminal/MainActivity.kt @@ -1,19 +1,27 @@ package org.kabiri.android.usbterminal import android.os.Bundle -import android.text.SpannableString import android.text.method.ScrollingMovementMethod import android.util.Log +import android.view.KeyEvent import android.view.Menu import android.view.MenuInflater import android.view.MenuItem +import android.view.View +import android.view.inputmethod.EditorInfo import android.widget.Button import android.widget.EditText import android.widget.TextView import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint -import org.kabiri.android.usbterminal.extensions.scrollToLastLine +import kotlinx.coroutines.launch +import org.kabiri.android.usbterminal.util.scrollToLastLine +import org.kabiri.android.usbterminal.ui.setting.SettingModalBottomSheet +import org.kabiri.android.usbterminal.ui.setting.SettingViewModel import org.kabiri.android.usbterminal.viewmodel.MainActivityViewModel @AndroidEntryPoint @@ -24,11 +32,21 @@ class MainActivity : AppCompatActivity() { } private val viewModel by viewModels() + private val settingViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + // avoid system navbar or soft keyboard overlapping the content. + val rootView = findViewById(R.id.root_view) + ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, insets -> + val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()) + view.setPadding(0, 0, 0, maxOf(systemBarsInsets.bottom, imeInsets.bottom)) + insets + } + val etInput = findViewById(R.id.etInput) val tvOutput = findViewById(R.id.tvOutput) val btEnter = findViewById