diff --git a/.editorconfig b/.editorconfig index 6d83688d..eab196de 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,3 +11,13 @@ insert_final_newline = true [.idea/codeStyles/*.xml] indent_size = 2 insert_final_newline = false + +[*.{kt,kts}] +max_line_length = 120 +ktlint_code_style = android_studio +ktlint_standard_import-ordering = disabled +ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = unset +ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = unset +ktlint_standard_trailing-comma-on-call-site = disabled +ktlint_standard_trailing-comma-on-declaration-site = disabled +ktlint_standard_function-signature = disabled diff --git a/.github/actions/get-avd-info/action.yml b/.github/actions/get-avd-info/action.yml index a65c0e6e..293841eb 100644 --- a/.github/actions/get-avd-info/action.yml +++ b/.github/actions/get-avd-info/action.yml @@ -5,18 +5,22 @@ inputs: description: The API level for which to retrieve AVD info required: true outputs: - arch: - description: CPU architecture of the system image - value: ${{ steps.get-avd-arch.outputs.arch }} target: description: Target of the system image value: ${{ steps.get-avd-target.outputs.target }} + arch: + description: CPU architecture of the system image + value: ${{ steps.get-avd-arch.outputs.arch }} runs: using: "composite" steps: - - id: get-avd-arch - run: echo "::set-output name=arch::$(if [ ${{ inputs.api-level }} -ge 30 ]; then echo x86_64; else echo x86; fi)" - shell: bash + # Prefer ATD system images available in API 30+ as they are optimized for headless tests. + # Google Play services is required and is available in the google_atd and google_apis images. + # Note, API 27 does not provide a google_apis system image. - id: get-avd-target - run: echo "::set-output name=target::$(if [ ${{ inputs.api-level }} -ge 32 ]; then echo google_apis; else echo default; fi)" + run: echo "target=$(if [ ${{ inputs.api-level }} -eq 27 ]; then echo default; elif [ ${{ inputs.api-level }} -ge 30 ]; then echo google_atd; else echo google_apis; fi)" >> $GITHUB_OUTPUT + shell: bash + # Prefer x86_64 architecture + - id: get-avd-arch + run: echo "arch=x86_64" >> $GITHUB_OUTPUT shell: bash diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d3942ece..44276cb9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,11 +17,11 @@ jobs: - name: Checkout the code uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 with: - java-version: '11' - distribution: 'temurin' + java-version: '17' + distribution: 'corretto' cache: gradle - name: Clear gradle cache @@ -63,7 +63,7 @@ jobs: - name: Upload reports uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 with: - name: Test-Reports + name: analyze-reports path: app/build/reports if: always() @@ -76,11 +76,11 @@ jobs: - name: Checkout the code uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 with: - java-version: '11' - distribution: 'temurin' + java-version: '17' + distribution: 'corretto' cache: gradle - name: Clear gradle cache @@ -106,28 +106,28 @@ jobs: - name: Upload reports uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 with: - name: Test-Reports + name: unit-tests-reports path: app/build/reports if: always() instrumentation-tests: - name: Instrumentation tests on ${{ matrix.target }} API ${{ matrix.api-level }} - runs-on: macos-latest - timeout-minutes: 30 + name: Instrumentation tests on API ${{ matrix.api-level }} + runs-on: ubuntu-latest + timeout-minutes: 15 needs: unit-tests strategy: - fail-fast: true + fail-fast: false matrix: - api-level: [ 21, 32 ] + api-level: [ 26, 27, 28, 29, 30, 31, 32, 33, 34 ] steps: - name: Checkout the code uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 with: - java-version: '11' - distribution: 'temurin' + java-version: '17' + distribution: 'corretto' cache: gradle - name: Clear gradle cache @@ -145,7 +145,15 @@ jobs: - name: Check Gradle wrapper uses: gradle/wrapper-validation-action@216d1ad2b3710bf005dc39237337b9673fd8fcd5 - # API 30+ emulators only have x86_64 system images + - name: Enable KVM + 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: Create directory for AVD + run: mkdir -p ~/.android/avd + - name: Get AVD info uses: ./.github/actions/get-avd-info id: avd-info @@ -168,29 +176,29 @@ jobs: uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2 with: api-level: ${{ matrix.api-level }} - arch: ${{ steps.avd-info.outputs.arch }} target: ${{ steps.avd-info.outputs.target }} - disable-animations: false + arch: ${{ steps.avd-info.outputs.arch }} + disable-animations: true force-avd-creation: false ram-size: 4096M emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none script: echo "Generated AVD snapshot" - name: Run instrumentation tests - uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2 + uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d with: api-level: ${{ matrix.api-level }} - arch: ${{ steps.avd-info.outputs.arch }} target: ${{ steps.avd-info.outputs.target }} + arch: ${{ steps.avd-info.outputs.arch }} disable-animations: true force-avd-creation: false ram-size: 4096M - emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-snapshot-save - script: mv .github/debug.keystore ~/.android; ./gradlew connectedDebugAndroidTest + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + script: ./gradlew connectedCheck && killall -INT crashpad_handler || true - name: Upload reports uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 with: - name: Test-Reports + name: instrumentation-tests-reports-api${{ matrix.api-level }} path: app/build/reports if: always() diff --git a/.github/workflows/master.yaml b/.github/workflows/master.yaml index b62bed1f..eb7771ec 100644 --- a/.github/workflows/master.yaml +++ b/.github/workflows/master.yaml @@ -34,11 +34,11 @@ jobs: GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} run: echo $GOOGLE_SERVICES_JSON > app/google-services.json - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 with: - java-version: '11' - distribution: 'temurin' + java-version: '17' + distribution: 'corretto' cache: gradle - name: Clear gradle cache @@ -98,6 +98,13 @@ jobs: name: bisq-release.aab path: app/build/outputs/bundle/release + - name: Determine latest build-tools version + shell: bash + run: | + LATEST_BUILD_TOOLS_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1) + echo "LATEST_BUILD_TOOLS_VERSION=$LATEST_BUILD_TOOLS_VERSION" >> $GITHUB_ENV + echo Latest build tools version is: $LATEST_BUILD_TOOLS_VERSION + - name: Sign APK uses: r0adkll/sign-android-release@dbeba6b98a60b0fd540c02443c7f428cdedf0e7f id: sign_apk @@ -107,6 +114,8 @@ jobs: alias: ${{ secrets.KEY_ALIAS }} keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD }} + env: + BUILD_TOOLS_VERSION: ${{ env.LATEST_BUILD_TOOLS_VERSION }} - name: Sign AAB uses: r0adkll/sign-android-release@dbeba6b98a60b0fd540c02443c7f428cdedf0e7f @@ -117,6 +126,8 @@ jobs: alias: ${{ secrets.KEY_ALIAS }} keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD }} + env: + BUILD_TOOLS_VERSION: ${{ env.LATEST_BUILD_TOOLS_VERSION }} - name: Upload signed APK uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 824b3aa4..c87441f2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -19,7 +19,7 @@ jobs: - name: Read VERSION file id: get_version - run: echo "::set-output name=version::$(cat VERSION/VERSION)" + run: echo "version=$(cat VERSION/VERSION)" >> $GITHUB_OUTPUT - name: Create release id: create_release diff --git a/README.md b/README.md index 56b33b1a..82a856ec 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,46 @@ # Bisq Notification Android App + Since Bisq is a desktop-based application, this Android app enables you to pair it with your desktop application and receive important notifications such as trade updates and offer alerts when you are not near your computer. -## How to Run -In order to pair the app and receive notifications, you will need to create your own +## Prerequisites + +In order to pair the app and receive notifications, you will need to obtain an appropriate `google-services.json` file and place it under the app/ directory. Refer to [firebase documentation](https://firebase.google.com/docs/android/setup#add-config-file) for more information. +> Note, the `google-services.json` file needs to correspond to the `fcmServiceAccountKey.json` +> used by the [bisq-relay](https://github.com/bisq-network/bisq-relay) service. + +## Updating Dependencies + +Whenever dependencies are updated/changed, it is necessary to update the following: + +- [gradle/verification-metadata.xml](gradle/verification-metadata.xml) - can be updated using the + following command: + +```shell +./gradlew --write-verification-metadata sha256 build :app:processDebugResources :app:connectedDebugAndroidTest +``` + +- [gradle.lockfile](gradle.lockfile) - can be updated using the following command: + +```shell +./gradlew dependencies --write-locks +``` + ## Architectural Design + For information on the architectural design, refer to the [Bisq Remote Specification](https://github.com/bisq-network/bisqremote/wiki/Specification). ## Screenshots -![Welcome](images/welcome.png) -![Scan Pairing Code](images/scan_pairing_code.png) -![Pairing Success](images/pairing_success.png) -![Notification List](images/notification_list.png) -![Offer Taken Details](images/offer_taken_details.png) -![Settings](images/settings.png) + + + + + + + diff --git a/app/build.gradle b/app/build.gradle index 4d72ee48..1a867113 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,24 +1,15 @@ apply plugin: 'com.android.application' +apply plugin: 'com.google.devtools.ksp' apply plugin: 'com.google.gms.google-services' +apply plugin: 'io.gitlab.arturbosch.detekt' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' apply plugin: 'org.jlleitschuh.gradle.ktlint' -apply plugin: 'io.gitlab.arturbosch.detekt' repositories { google() mavenCentral() } -// This is primarily to be able to run unit and instrumented tests in GitHub workflows, -// since google-services.json is unavailable and will cause the GoogleServices task to fail -if (!project.file('google-services.json').exists()) { - android.applicationVariants.all { variant -> - def googleTask = tasks.findByName("process${variant.name.capitalize()}GoogleServices") - googleTask.enabled = false - } -} - Properties props = new Properties() props.load(new FileInputStream("$project.rootDir/version.properties")) props.each { prop -> @@ -43,13 +34,14 @@ def getVersionName = { -> android { namespace 'bisq.android' - compileSdkVersion 32 + compileSdk 34 + defaultConfig { applicationId "com.joachimneumann.bisq" versionCode getVersionCode() versionName getVersionName() - minSdkVersion 21 - targetSdkVersion 32 + minSdkVersion 26 + targetSdkVersion 34 multiDexEnabled true setProperty("archivesBaseName", "bisq") @@ -60,10 +52,18 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' } + testOptions { animationsDisabled = true execution 'ANDROIDX_TEST_ORCHESTRATOR' + + packagingOptions { + jniLibs { + useLegacyPackaging true + } + } } + buildTypes { debug { minifyEnabled false @@ -72,81 +72,199 @@ android { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } - applicationVariants.all { - variant -> - variant.outputs.each { - output -> - def name = "bisq-${variant.name}.apk" - output.outputFileName = name - } - } } - sourceSets { - androidTest { - java.srcDirs += "$projectDir/src/testCommon/java" + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { output -> + output.outputFileName = "bisq-${variant.name}.apk" } - test { - java.srcDirs += "$projectDir/src/testCommon/java" + + // Must disable the GoogleServices task when running unit and instrumented tests in GitHub + // workflows, since google-services.json is unavailable and will cause it to fail. + if (!project.file('google-services.json').exists()) { + tasks.configureEach { Task task -> + if (task.name.matches("process${variant.name.capitalize()}GoogleServices")) { + task.enabled = false + } + } } } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + packagingOptions { + resources.excludes.add("META-INF/*") + } } dependencies { - implementation 'androidx.test.uiautomator:uiautomator:2.2.0' - detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.20.0") + detektPlugins('io.gitlab.arturbosch.detekt:detekt-formatting:1.23.5') - implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation 'androidx.core:core-ktx:1.12.0' - implementation 'androidx.appcompat:appcompat:1.4.2' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.preference:preference-ktx:1.2.1' + + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'androidx.constraintlayout:constraintlayout:2.2.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.media:media:1.6.0' - implementation 'androidx.recyclerview:recyclerview:1.2.1' + implementation 'androidx.media:media:1.7.0' + implementation 'androidx.recyclerview:recyclerview:1.3.2' - implementation 'com.google.android.material:material:1.6.1' + implementation 'com.google.android.material:material:1.12.0' - implementation 'com.google.zxing:core:3.4.1' + implementation 'com.google.zxing:core:3.5.3' - implementation platform('com.google.firebase:firebase-bom:30.1.0') - implementation 'com.google.firebase:firebase-analytics-ktx:21.0.0' - implementation 'com.google.firebase:firebase-messaging:23.0.5' + implementation platform('com.google.firebase:firebase-bom:33.7.0') + implementation 'com.google.firebase:firebase-analytics-ktx:22.1.2' + implementation 'com.google.firebase:firebase-messaging:24.1.0' - implementation 'com.google.code.gson:gson:2.9.0' + implementation 'com.google.code.gson:gson:2.10.1' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' - implementation 'androidx.room:room-ktx:2.4.2' - implementation 'androidx.room:room-runtime:2.4.2' - annotationProcessor 'androidx.room:room-compiler:2.4.2' - kapt 'androidx.room:room-compiler:2.4.2' + implementation 'androidx.room:room-ktx:2.6.1' + implementation 'androidx.room:room-runtime:2.6.1' + annotationProcessor 'androidx.room:room-compiler:2.6.1' + ksp 'androidx.room:room-compiler:2.6.1' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1' - implementation "androidx.lifecycle:lifecycle-common-java8:2.4.1" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1" + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.7' + implementation 'androidx.lifecycle:lifecycle-common-java8:2.8.7' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation "androidx.multidex:multidex:2.0.1" + implementation 'androidx.multidex:multidex:2.0.1' testImplementation 'junit:junit:4.13.2' + testImplementation 'org.assertj:assertj-core:3.25.3' testImplementation 'org.hamcrest:hamcrest:2.2' - testImplementation "io.mockk:mockk:1.12.3" - testImplementation 'org.powermock:powermock:1.6.5' - testImplementation 'org.powermock:powermock-module-junit4:2.0.2' - testImplementation 'org.powermock:powermock-api-mockito2:2.0.2' - - androidTestImplementation "junit:junit:4.13.2" - androidTestImplementation "androidx.test:core-ktx:1.4.0" - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation "androidx.test.ext:junit-ktx:1.1.3" - androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" - androidTestImplementation "androidx.test.espresso:espresso-intents:3.4.0" - androidTestImplementation "androidx.test.espresso:espresso-contrib:3.4.0" - androidTestImplementation "androidx.test.uiautomator:uiautomator:2.2.0" - androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0" - androidTestImplementation "io.mockk:mockk-android:1.12.3" + testImplementation 'io.mockk:mockk:1.13.9' + testImplementation 'org.powermock:powermock:1.6.6' + testImplementation 'org.powermock:powermock-module-junit4:2.0.9' + testImplementation 'org.powermock:powermock-api-mockito2:2.0.9' + testImplementation(project(':testCommon')) + + androidTestImplementation 'junit:junit:4.13.2' + androidTestImplementation 'org.assertj:assertj-core:3.25.3' + androidTestImplementation 'androidx.test:core-ktx: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' + androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1' + androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.6.1' + androidTestImplementation 'androidx.test:rules:1.6.1' + androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0' + androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3' androidTestImplementation 'org.awaitility:awaitility-kotlin:4.2.0' + androidTestImplementation 'io.mockk:mockk-android:1.13.9' + androidTestImplementation(project(':testCommon')) { + // Prevent conflict with mockk-android + exclude group: 'io.mockk', module: 'mockk' + } + androidTestImplementation 'org.objenesis:objenesis:3.3' + + androidTestUtil 'androidx.test:orchestrator:1.5.1' +} + +def androidTestsReportsDirectory() { + return "${layout.buildDirectory.get()}/reports/androidTests/connected/debug" +} - androidTestUtil 'androidx.test:orchestrator:1.4.1' +tasks.register('embedScreenshotsIntoReport') { + group = "reporting" + doFirst { + def failureScreenshotsDirectory = + new File(androidTestsReportsDirectory(), "screenshots/failures") + + if (!failureScreenshotsDirectory.exists()) { + return + } + + println "Embedding failure screenshots into report from ${failureScreenshotsDirectory}" + + failureScreenshotsDirectory.eachFile { failedTestClassDirectory -> + def failedTestClassName = failedTestClassDirectory.name + + failedTestClassDirectory.eachFile { failedTestFile -> + def failedTestName = failedTestFile.name + def failedTestNameWithoutExtension = + failedTestName.take(failedTestName.lastIndexOf('.')) + def failedTestClassJunitReportFile = + new File(androidTestsReportsDirectory(), "${failedTestClassName}.html") + + if (!failedTestClassJunitReportFile.exists()) { + println "Could not find JUnit report file for test class " + + "'${failedTestClassJunitReportFile}'" + return + } + + def failedTestJunitReportContent = failedTestClassJunitReportFile.text + + def patternToFind = "