diff --git a/.buildkite/block.step.yml b/.buildkite/block.step.yml index 371c7491fa..6aba4948d4 100644 --- a/.buildkite/block.step.yml +++ b/.buildkite/block.step.yml @@ -1,7 +1,12 @@ +agents: + queue: 'macos' + steps: - - block: 'Trigger full build' + - label: + block: 'Trigger full build' key: 'trigger-full-build' - label: 'Upload the full test pipeline' depends_on: 'trigger-full-build' - command: buildkite-agent pipeline upload .buildkite/pipeline.full.yml \ No newline at end of file + command: buildkite-agent pipeline upload .buildkite/pipeline.full.yml + timeout_in_minutes: 5 diff --git a/.buildkite/pipeline.full.yml b/.buildkite/pipeline.full.yml index d8bc9d5f46..cfeac04440 100644 --- a/.buildkite/pipeline.full.yml +++ b/.buildkite/pipeline.full.yml @@ -1,3 +1,6 @@ +agents: + queue: 'opensource' + steps: - label: ':android: Build minimal fixture APK' key: "fixture-minimal" @@ -10,7 +13,7 @@ steps: JAVA_VERSION: 17 - label: ':android: Build Example App' - timeout_in_minutes: 5 + timeout_in_minutes: 10 agents: queue: macos-14 command: 'make example-app' @@ -230,7 +233,7 @@ steps: - label: ':browserstack: Android 7 NDK r19 end-to-end tests - ANRs' depends_on: "fixture-r19" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -328,7 +331,7 @@ steps: - label: ':browserstack: Android 8 NDK r19 end-to-end tests - ANRs' depends_on: "fixture-r19" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -428,7 +431,7 @@ steps: - label: ':bitbar: Android 9 NDK r21 end-to-end tests - ANRs' depends_on: "fixture-r21" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -529,7 +532,7 @@ steps: - label: ':browserstack: Android 10 NDK r21 end-to-end tests - ANRs' depends_on: "fixture-r21" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -634,7 +637,7 @@ steps: - label: ':browserstack: Android 11 NDK r21 end-to-end tests - ANRs' depends_on: "fixture-r21" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -735,7 +738,7 @@ steps: - label: ':browserstack: Android 13 NDK r21 end-to-end tests - ANRs' depends_on: "fixture-r21" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -836,7 +839,7 @@ steps: - label: ':browserstack: Android 14 NDK r21 end-to-end tests - ANRs' depends_on: "fixture-r21" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -891,6 +894,7 @@ steps: - label: 'Publish :rocket:' if: build.branch == "master" depends_on: 'android-common' + timeout_in_minutes: 30 env: BUILDKITE_PLUGIN_S3_SECRETS_BUCKET_PREFIX: bugsnag-android-publish plugins: diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 55dd175187..1fe389f929 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -1,3 +1,6 @@ +agents: + queue: 'opensource' + steps: - label: 'Audit current licenses' @@ -105,7 +108,7 @@ steps: # - label: ':bitbar: Android 7 NDK r19 smoke tests' depends_on: "fixture-r19" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -139,7 +142,7 @@ steps: - label: ':browserstack: Android 7 NDK r19 ANR smoke tests' depends_on: "fixture-r19" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -170,7 +173,7 @@ steps: - label: ':bitbar: Android 8 NDK r19 smoke tests' depends_on: "fixture-r19" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -202,7 +205,7 @@ steps: - label: ':browserstack: Android 8 NDK r19 ANR smoke tests' depends_on: "fixture-r19" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -231,7 +234,7 @@ steps: - label: ':bitbar: Android 9 NDK r21 smoke tests' depends_on: "fixture-r21" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -265,7 +268,7 @@ steps: - label: ':browserstack: Android 9 NDK r21 ANR smoke tests' depends_on: "fixture-r21" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -292,7 +295,7 @@ steps: - label: ':bitbar: Android 10 NDK r21 smoke tests' depends_on: "fixture-r21" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -326,7 +329,7 @@ steps: - label: ':browserstack: Android 10 NDK r21 ANR smoke tests' depends_on: "fixture-r21" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -358,7 +361,7 @@ steps: - label: ':bitbar: Android 11 NDK r21 smoke tests' key: 'android-11-smoke' depends_on: "fixture-r21" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -393,7 +396,7 @@ steps: - label: ':browserstack: Android 11 NDK r21 ANR smoke tests' key: 'android-11-anr-smoke' depends_on: "fixture-r21" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -532,7 +535,7 @@ steps: - label: ':bitbar: Android 13 NDK r21 smoke tests' depends_on: "fixture-r21" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -566,7 +569,7 @@ steps: - label: ':browserstack: Android 13 NDK r21 ANR smoke tests' depends_on: "fixture-r21" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -597,7 +600,7 @@ steps: - label: ':bitbar: Android 14 NDK r21 smoke tests' depends_on: "fixture-r21" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -631,7 +634,7 @@ steps: - label: ':browserstack: Android 14 NDK r21 ANR smoke tests' depends_on: "fixture-r21" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: @@ -660,9 +663,8 @@ steps: concurrency_group: 'browserstack-app' concurrency_method: eager - - label: 'Conditionally include device farms/full tests' agents: - queue: macos-14 + queue: macos command: sh -c .buildkite/pipeline_trigger.sh - + timeout_in_minutes: 10 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 47ab8e0caf..92ce2072e2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -10,6 +10,7 @@ # supported CodeQL languages. # name: "CodeQL" +permissions: read-all on: push: @@ -26,7 +27,7 @@ env: jobs: analyze: name: Analyze - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-22.04' }} permissions: actions: read contents: read @@ -42,49 +43,52 @@ jobs: # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - submodules: recursive - - uses: gradle/wrapper-validation-action@v1 + - name: Checkout repository + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 #v3.6.0 + with: + submodules: recursive + - uses: gradle/wrapper-validation-action@56b90f209b02bf6d1deae490e9ef18b21a389cd4 #v1.1.0 - - uses: actions/setup-java@v3 - with: - distribution: 'zulu' - java-version: 11 + - uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 #v3.13.0 + with: + distribution: 'zulu' + java-version: 11 - - name: Gradle cache - uses: gradle/gradle-build-action@v2 + - name: Gradle cache + uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa #v2.12.0 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@396bb3e45325a47dd9ef434068033c6d5bb0d11a #v3.27.3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v3 + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild +# uses: github/codeql-action/autobuild@396bb3e45325a47dd9ef434068033c6d5bb0d11a #v3.27.3 - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + run: | + echo "Run, Build Application using script" + ./gradlew --no-daemon assemble - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@396bb3e45325a47dd9ef434068033c6d5bb0d11a #v3.27.3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/downstream_updates.yml b/.github/workflows/downstream_updates.yml index 20de33f516..68e06e182e 100644 --- a/.github/workflows/downstream_updates.yml +++ b/.github/workflows/downstream_updates.yml @@ -1,4 +1,5 @@ name: downstream-updates +permissions: read-all on: release: diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000000..151d6452cd --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,73 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: OpenSSF Scorecard +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '0 0 * * 0' + push: + branches: [ "next" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/.github/workflows/signing.yml b/.github/workflows/signing.yml new file mode 100644 index 0000000000..ec28f82d84 --- /dev/null +++ b/.github/workflows/signing.yml @@ -0,0 +1,32 @@ +name: Sign release assets + +permissions: read-all + +on: + release: + types: [released] + workflow_dispatch: + inputs: + tag: + description: 'Tag to sign' + required: true + type: string +jobs: + sign-assets: + runs-on: ubuntu-latest + steps: + - name: Install gpg + run: | + sudo apt-get update + sudo apt-get install -y gnupg + - name: Import GPG key + run: | + echo "${{ secrets.PLATFORMS_GPG_KEY_BASE64 }}" | base64 --decode | gpg --batch --import + - name: Sign assets + uses: bugsnag/platforms-release-signer@main + with: + github_token: ${{ secrets.PLATFORMS_SIGNING_GITHUB_TOKEN }} + full_repository: ${{ github.repository }} + release_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.event.release.tag_name }} + key_id: ${{ secrets.PLATFORMS_GPG_KEY_ID }} + key_passphrase: ${{ secrets.PLATFORMS_GPG_KEY_PASSPHRASE }} diff --git a/CHANGELOG.md b/CHANGELOG.md index e1e3b868c0..b35eb2b223 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 6.11.0 (2025-01-22) + +### Enhancements + +* Introduced a new option in the `exitinfo` plugin for generating ANRs that does not have a matching Events (such as background ANRs) + [#2116](https://github.com/bugsnag/bugsnag-android/pull/2116) +* Add original error class and message to metadata for link errors loading BugSnag NDK libraries + [#2126](https://github.com/bugsnag/bugsnag-android/pull/2126) + ## 6.10.0 (2024-11-14) ### Enhancements diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000..2d92311226 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,7 @@ +bugsnag-android/ @lemnik @YYChen01988 +bugsnag-android-core/ @lemnik @YYChen01988 +bugsnag-plugin-android-anr/ @lemnik @YYChen01988 +bugsnag-plugin-android-exitinfo/ @lemnik @YYChen01988 +bugsnag-plugin-android-ndk/ @lemnik @YYChen01988 +bugsnag-plugin-android-okhttp/ @lemnik @YYChen01988 +bugsnag-plugin-react-native/ @lemnik @YYChen01988 diff --git a/Makefile b/Makefile index 2ee1038902..83b0cd90cd 100644 --- a/Makefile +++ b/Makefile @@ -67,20 +67,13 @@ example-app: @cd ./examples/sdk-app-example/ && ./gradlew clean assembleRelease bump: -ifneq ($(shell git diff --staged),) - @git diff --staged - @$(error You have uncommitted changes. Push or discard them to continue) +ifneq ($(VERSION),) + @echo "Bumping version to $(VERSION)" + @./scripts/bump-version.sh $(VERSION) +else + @echo "Please provide a version number" + @./scripts/bump-version.sh endif -ifeq ($(VERSION),) - @$(error VERSION is not defined. Run with `make VERSION=number bump`) -endif - @echo Bumping the version number to $(VERSION) - @sed -i '' "s/bugsnag-android:.*\"/bugsnag-android:$(VERSION)\"/" examples/sdk-app-example/app/build.gradle - @sed -i '' "s/bugsnag-plugin-android-okhttp:.*\"/bugsnag-plugin-android-okhttp:$(VERSION)\"/" examples/sdk-app-example/app/build.gradle - @sed -i '' "s/VERSION_NAME=.*/VERSION_NAME=$(VERSION)/" gradle.properties - @sed -i '' "s/var version: String = .*/var version: String = \"$(VERSION)\",/"\ - bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt - @sed -i '' "s/## TBD/## $(VERSION) ($(shell date '+%Y-%m-%d'))/" CHANGELOG.md .PHONY: check check: diff --git a/README.md b/README.md index 4bd18fbfaa..e860535bcc 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,18 @@ -# Bugsnag error monitoring & exception reporter for Android -[![Documentation](https://img.shields.io/badge/documentation-latest-blue.svg)](https://docs.bugsnag.com/platforms/android/) +
+ + + + SmartBear BugSnag logo + + +

Error monitoring & exception reporter for Android

+
-Get comprehensive [Android crash reports](https://www.bugsnag.com/platforms/android/) to quickly debug errors. +[![Documentation](https://img.shields.io/badge/documentation-latest-blue.svg)](https://docs.bugsnag.com/platforms/android/) +[![Build status](https://badge.buildkite.com/ff6aa35c92e06a739cb095b58762dffab8011c7f05a1ce86e1.svg)](https://buildkite.com/bugsnag/bugsnag-android) +[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/bugsnag/bugsnag-android/badge)](https://scorecard.dev/viewer/?uri=github.com/bugsnag/bugsnag-android) -Bugsnag's [Android crash reporting](https://www.bugsnag.com/platforms/android/) -library automatically detects crashes in your Android apps, collecting -diagnostic information and immediately notifying your development team, helping -you to understand and resolve issues as fast as possible. +Detect crashes in your Android applications: collecting diagnostic information and immediately notifying your development team, helping you to understand and resolve issues as fast as possible. ## Features @@ -30,17 +36,10 @@ you to understand and resolve issues as fast as possible. * [Search open and closed issues](https://github.com/bugsnag/bugsnag-android/issues?utf8=✓&q=is%3Aissue) for similar problems * [Report a bug or request a feature](https://github.com/bugsnag/bugsnag-android/issues/new) - ## Contributing -All contributors are welcome! For information on how to build, test -and release `bugsnag-android`, see our -[contributing guide](https://github.com/bugsnag/bugsnag-android/blob/master/CONTRIBUTING.md). - -Bugsnag employees should start by reading [the docs](docs/README.md). +All contributors are welcome! For information on how to build, test and release `bugsnag-android`, see our [contributing guide](https://github.com/bugsnag/bugsnag-android/blob/main/CONTRIBUTING.md). ## License -The Bugsnag Android notifier is free software released under the MIT License. -See the [LICENSE](https://github.com/bugsnag/bugsnag-android/blob/master/LICENSE) -for details. +The BugSnag Android SDK is free software released under the MIT License. See the [LICENSE](https://github.com/bugsnag/bugsnag-android/blob/main/LICENSE) for details. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..445bb0b0db --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,12 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | --------------------- | +| 5.x | Critical patches only | +| 6.x | Yes | + +## Reporting a Vulnerability + +If you find a vulnerability in this SDK, please report it to our [Support team](mailto:support@bugsnag.com) for review. \ No newline at end of file diff --git a/bugsnag-android-core/api/bugsnag-android-core.api b/bugsnag-android-core/api/bugsnag-android-core.api index b73cae03e9..a1371e4fec 100644 --- a/bugsnag-android-core/api/bugsnag-android-core.api +++ b/bugsnag-android-core/api/bugsnag-android-core.api @@ -509,6 +509,7 @@ public class com/bugsnag/android/NativeInterface { public static fun addMetadata (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V public static fun addMetadata (Ljava/lang/String;Ljava/util/Map;)V public static fun clearMetadata (Ljava/lang/String;Ljava/lang/String;)V + public static fun createEmptyEvent ()Lcom/bugsnag/android/Event; public static fun createEvent (Ljava/lang/Throwable;Lcom/bugsnag/android/Client;Lcom/bugsnag/android/SeverityReason;)Lcom/bugsnag/android/Event; public static fun deliverReport (Ljava/io/File;)V public static fun deliverReport ([B[B[BLjava/lang/String;Z)V diff --git a/bugsnag-android-core/build.gradle.kts b/bugsnag-android-core/build.gradle.kts index 52c5d28871..12024316a9 100644 --- a/bugsnag-android-core/build.gradle.kts +++ b/bugsnag-android-core/build.gradle.kts @@ -2,35 +2,77 @@ import kotlinx.validation.ApiValidationExtension import org.jetbrains.dokka.gradle.DokkaTask plugins { - id("bugsnag-build-plugin") + loadDefaultPlugins() } -bugsnagBuildOptions { - usesNdk = true +android { + compileSdk = Versions.Android.Build.compileSdkVersion + namespace = "com.bugsnag.android.core" - // pick up dsl-json by adding to the default sourcesets - android { - sourceSets { - named("main") { - java.srcDirs("dsl-json/library/src/main/java") - } - named("test") { - java.srcDirs( - "dsl-json/library/src/test/java", - "src/sharedTest/java" - ) - } - named("androidTest") { - java.srcDirs( - "src/sharedTest/java" - ) - } + defaultConfig { + minSdk = Versions.Android.Build.minSdkVersion + ndkVersion = Versions.Android.Build.ndk + + consumerProguardFiles("proguard-rules.pro") + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + externalNativeBuild.cmake.arguments += BugsnagDefaults.cmakeArguments + + configureAbis(ndk.abiFilters) + } + + lint { + isAbortOnError = true + isWarningsAsErrors = true + isCheckAllWarnings = true + baseline(File(project.projectDir, "lint-baseline.xml")) + disable("GradleDependency", "NewerVersionAvailable") + } + + buildFeatures { + aidl = false + renderScript = false + shaders = false + resValues = false + buildConfig = false + } + + compileOptions { + sourceCompatibility = Versions.java + targetCompatibility = Versions.java + } + + testOptions { + unitTests { + isReturnDefaultValues = true + } + } + + sourceSets { + named("main") { + java.srcDirs("dsl-json/library/src/main/java") + } + + named("test") { + java.srcDirs( + "dsl-json/library/src/test/java", + "src/sharedTest/java" + ) + } + named("androidTest") { + java.srcDirs( + "src/sharedTest/java" + ) } } + + externalNativeBuild.cmake.path = project.file("CMakeLists.txt") + externalNativeBuild.cmake.version = Versions.Android.Build.cmakeVersion } -apply(plugin = "com.android.library") -apply(plugin = "org.jetbrains.dokka") +dependencies { + addCommonModuleDependencies() +} tasks.getByName("dokkaHtml") { dokkaSourceSets { @@ -47,3 +89,9 @@ tasks.getByName("dokkaHtml") { plugins.withId("org.jetbrains.kotlinx.binary-compatibility-validator") { project.extensions.getByType(ApiValidationExtension::class.java).ignoredPackages.add("com.bugsnag.android.repackaged.dslplatform.json") } + +apply(from = rootProject.file("gradle/detekt.gradle")) +apply(from = rootProject.file("gradle/license-check.gradle")) +apply(from = rootProject.file("gradle/release.gradle")) + +configureCheckstyle() diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt index a74579eea9..c1eb409bc8 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt @@ -64,6 +64,13 @@ internal class AppDataCollector( ) } + fun generateHistoricAppWithState(): AppWithState { + return AppWithState( + config, binaryArch, packageName, releaseStage, versionName, codeBundleId, + null, null, null, null + ) + } + @SuppressLint("SwitchIntDef") @Suppress("DEPRECATION") private fun getProcessImportance(): String? { diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt index b818523837..49d585534b 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt @@ -91,6 +91,20 @@ internal class DeviceDataCollector( Date(now) ) + fun generateHistoricDeviceWithState(timeStamp: Long) = + DeviceWithState( + buildInfo, + checkIsRooted(), + deviceIdStore.get()?.internalDeviceId, + locale, + null, + runtimeVersions.toMutableMap(), + null, + null, + getOrientationAsString(), + Date(timeStamp) + ) + fun getDeviceMetadata(): Map { val map = HashMap() populateBatteryInfo(into = map) diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.kt index 75300b3907..5d68837b05 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.kt @@ -40,6 +40,10 @@ internal class EventStore( private val callbackState: CallbackState override val logger: Logger + var onEventStoreEmptyCallback: () -> Unit = {} + var onDiscardEventCallback: (EventPayload) -> Unit = {} + private var isEmptyEventCallbackCalled: Boolean = false + /** * Flush startup crashes synchronously on the main thread. Startup crashes block the main thread * when being sent (subject to [Configuration.setSendLaunchCrashesSynchronously]) @@ -51,7 +55,10 @@ internal class EventStore( val future = try { bgTaskService.submitTask( TaskType.ERROR_REQUEST, - Runnable { flushLaunchCrashReport() } + Runnable { + flushLaunchCrashReport() + notifyEventQueueEmpty() + } ) } catch (exc: RejectedExecutionException) { logger.d("Failed to flush launch crash reports, continuing.", exc) @@ -135,6 +142,7 @@ internal class EventStore( logger.d("No regular events to flush to Bugsnag.") } flushReports(storedFiles) + notifyEventQueueEmpty() } ) } catch (exception: RejectedExecutionException) { @@ -188,11 +196,13 @@ internal class EventStore( logger.w( "Discarding over-sized event (${eventFile.length()}) after failed delivery" ) + discardEvents(eventFile) deleteStoredFiles(setOf(eventFile)) } else if (isTooOld(eventFile)) { logger.w( "Discarding historical event (from ${getCreationDate(eventFile)}) after failed delivery" ) + discardEvents(eventFile) deleteStoredFiles(setOf(eventFile)) } else { cancelQueuedFiles(setOf(eventFile)) @@ -258,6 +268,26 @@ internal class EventStore( return Date(findTimestampInFilename(file)) } + private fun notifyEventQueueEmpty() { + if (isEmpty() && !isEmptyEventCallbackCalled) { + onEventStoreEmptyCallback() + isEmptyEventCallbackCalled = true + } + } + + private fun discardEvents(eventFile: File) { + val eventFilenameInfo = fromFile(eventFile, config) + onDiscardEventCallback( + EventPayload( + eventFilenameInfo.apiKey, + null, + eventFile, + notifier, + config + ) + ) + } + companion object { private const val LAUNCH_CRASH_TIMEOUT_MS: Long = 2000 val EVENT_COMPARATOR: Comparator = Comparator { lhs, rhs -> diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.kt index 69f7c77632..f7ede678cb 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.kt @@ -47,6 +47,11 @@ internal abstract class FileStore( return true } + /** + * Test whether this `FileStore` is definitely empty + */ + fun isEmpty(): Boolean = queuedFiles.isEmpty() && storageDir.list().isNullOrEmpty() + fun enqueueContentForDelivery(content: String?, filename: String) { if (!isStorageDirValid(storageDir)) { return diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java index de0da4a21c..3ea633712c 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java @@ -51,7 +51,8 @@ private static Client getClient() { * will populate the Error and then pass the Event object to * {@link Client#populateAndNotifyAndroidEvent(Event, OnErrorCallback)}. */ - private static Event createEmptyEvent() { + @NonNull + public static Event createEmptyEvent() { Client client = getClient(); return new Event( diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt index e8c0693cdd..a916dce798 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt @@ -7,7 +7,7 @@ import java.io.IOException */ class Notifier @JvmOverloads constructor( var name: String = "Android Bugsnag Notifier", - var version: String = "6.10.0", + var version: String = "6.11.0", var url: String = "https://bugsnag.com" ) : JsonStream.Streamable { diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/EmptyEventCallbackTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/EmptyEventCallbackTest.kt new file mode 100644 index 0000000000..6c802250a3 --- /dev/null +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/EmptyEventCallbackTest.kt @@ -0,0 +1,156 @@ +package com.bugsnag.android + +import com.bugsnag.android.BugsnagTestUtils.generateConfiguration +import com.bugsnag.android.BugsnagTestUtils.generateEvent +import com.bugsnag.android.FileStore.Delegate +import com.bugsnag.android.internal.BackgroundTaskService +import com.bugsnag.android.internal.ImmutableConfig +import com.bugsnag.android.internal.convertToImmutableConfig +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` +import java.io.File +import java.nio.file.Files +import java.util.concurrent.CountDownLatch + +class EmptyEventCallbackTest { + + private lateinit var storageDir: File + private lateinit var errorDir: File + private lateinit var backgroundTaskService: BackgroundTaskService + + @Before + fun setUp() { + storageDir = Files.createTempDirectory("tmp").toFile() + storageDir.deleteRecursively() + errorDir = File(storageDir, "bugsnag/errors") + backgroundTaskService = BackgroundTaskService() + } + + @After + fun tearDown() { + storageDir.deleteRecursively() + backgroundTaskService.shutdown() + } + + @Test + fun emptyQueuedEventTriggerEventStoreEmptyCallback() { + val config = generateConfiguration().apply { + maxPersistedEvents = 0 + persistenceDirectory = storageDir + } + val eventStore = createEventStore(convertToImmutableConfig(config)) + eventStore.write(generateEvent()) + + val callbackLatch = CountDownLatch(1) + eventStore.onEventStoreEmptyCallback = { callbackLatch.countDown() } + eventStore.flushAsync() + callbackLatch.await() + + assertTrue(eventStore.isEmpty()) + } + + @Test + fun testFailedDeliveryEvents() { + val mockDelivery = mock(Delivery::class.java) + `when`(mockDelivery.deliver(any(), any())) + .thenReturn( + DeliveryStatus.DELIVERED, + DeliveryStatus.FAILURE + ) + + val config = generateConfiguration().apply { + maxPersistedEvents = 3 + persistenceDirectory = storageDir + delivery = mockDelivery + } + val eventStore = createEventStore(convertToImmutableConfig(config)) + repeat(3) { + eventStore.write(generateEvent()) + } + + // the EventStore should not be considered empty with 3 events in it + assertFalse(eventStore.isEmpty()) + + var eventStoreEmptyCount = 0 + eventStore.onEventStoreEmptyCallback = { eventStoreEmptyCount++ } + eventStore.flushAsync() + backgroundTaskService.shutdown() + + assertTrue( + "there should be no undelivered payloads in the EventStore", + eventStore.isEmpty() + ) + + assertEquals( + "onEventStoreEmptyCallback have been called even with a failed (deleted) payload", + 1, + eventStoreEmptyCount + ) + } + + @Test + fun testUndeliveredEvents() { + val mockDelivery = mock(Delivery::class.java) + `when`(mockDelivery.deliver(any(), any())) + .thenReturn( + DeliveryStatus.DELIVERED, + DeliveryStatus.FAILURE, + DeliveryStatus.UNDELIVERED + ) + + val config = generateConfiguration().apply { + maxPersistedEvents = 3 + persistenceDirectory = storageDir + delivery = mockDelivery + } + val eventStore = createEventStore(convertToImmutableConfig(config)) + repeat(3) { + eventStore.write(generateEvent()) + } + + // the EventStore should not be considered empty with 3 events in it + assertFalse(eventStore.isEmpty()) + + var eventStoreEmptyCount = 0 + eventStore.onEventStoreEmptyCallback = { eventStoreEmptyCount++ } + eventStore.flushAsync() + backgroundTaskService.shutdown() + + // the last payload should not have been delivered + assertFalse( + "there should be one undelivered payload in the EventStore", + eventStore.isEmpty() + ) + + assertEquals( + "onEventStoreEmptyCallback should not be called when there are undelivered payloads", + 0, + eventStoreEmptyCount + ) + } + + private fun createEventStore(config: ImmutableConfig): EventStore { + return EventStore( + config, + NoopLogger, + Notifier(), + backgroundTaskService, + object : Delegate { + override fun onErrorIOFailure( + exception: Exception?, + errorFile: File?, + context: String? + ) { + } + }, + CallbackState() + ) + } +} diff --git a/bugsnag-android/build.gradle.kts b/bugsnag-android/build.gradle.kts index bd5c5a6162..bd71784906 100644 --- a/bugsnag-android/build.gradle.kts +++ b/bugsnag-android/build.gradle.kts @@ -1,10 +1,16 @@ plugins { - id("bugsnag-build-plugin") - id("com.android.library") + load(Versions.Plugins.AGP) + load(Versions.Plugins.licenseCheck) } -bugsnagBuildOptions { - compilesCode = false +android { + compileSdk = Versions.Android.Build.compileSdkVersion + namespace = "com.bugsnag.android" + + defaultConfig { + minSdk = Versions.Android.Build.minSdkVersion + ndkVersion = Versions.Android.Build.ndk + } } dependencies { @@ -12,3 +18,6 @@ dependencies { add("api", project(":bugsnag-plugin-android-anr")) add("api", project(":bugsnag-plugin-android-ndk")) } + +apply(from = rootProject.file("gradle/license-check.gradle")) +apply(from = rootProject.file("gradle/release.gradle")) diff --git a/bugsnag-benchmarks/build.gradle b/bugsnag-benchmarks/build.gradle index 3c794e9101..ea774a67a7 100644 --- a/bugsnag-benchmarks/build.gradle +++ b/bugsnag-benchmarks/build.gradle @@ -1,6 +1,6 @@ plugins { id "com.android.library" - id "androidx.benchmark" + id "androidx.benchmark" version "1.1.1" id "kotlin-android" } diff --git a/bugsnag-plugin-android-anr/build.gradle.kts b/bugsnag-plugin-android-anr/build.gradle.kts index 9f778ce9f9..dbae9dc178 100644 --- a/bugsnag-plugin-android-anr/build.gradle.kts +++ b/bugsnag-plugin-android-anr/build.gradle.kts @@ -1,13 +1,70 @@ plugins { - id("bugsnag-build-plugin") + loadDefaultPlugins() } -bugsnagBuildOptions { - usesNdk = true -} +android { + compileSdk = Versions.Android.Build.compileSdkVersion + namespace = "com.bugsnag.android.anr" + + defaultConfig { + minSdk = Versions.Android.Build.minSdkVersion + ndkVersion = Versions.Android.Build.ndk + + consumerProguardFiles("proguard-rules.pro") + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + externalNativeBuild.cmake.arguments += listOf( + "-DANDROID_CPP_FEATURES=exceptions", + "-DANDROID_STL=c++_static" + ) + + configureAbis(ndk.abiFilters) + } + + lint { + isAbortOnError = true + isWarningsAsErrors = true + isCheckAllWarnings = true + baseline(File(project.projectDir, "lint-baseline.xml")) + disable("GradleDependency", "NewerVersionAvailable") + } + + buildFeatures { + aidl = false + renderScript = false + shaders = false + resValues = false + buildConfig = false + } -apply(plugin = "com.android.library") + compileOptions { + sourceCompatibility = Versions.java + targetCompatibility = Versions.java + } + + testOptions { + unitTests { + isReturnDefaultValues = true + } + } + + sourceSets { + named("test") { + java.srcDir(SHARED_TEST_SRC_DIR) + } + } + + externalNativeBuild.cmake.path = project.file("CMakeLists.txt") + externalNativeBuild.cmake.version = Versions.Android.Build.cmakeVersion +} dependencies { + addCommonModuleDependencies() add("api", project(":bugsnag-android-core")) } + +apply(from = rootProject.file("gradle/detekt.gradle")) +apply(from = rootProject.file("gradle/license-check.gradle")) +apply(from = rootProject.file("gradle/release.gradle")) + +configureCheckstyle() diff --git a/bugsnag-plugin-android-exitinfo/api/bugsnag-plugin-android-exitinfo.api b/bugsnag-plugin-android-exitinfo/api/bugsnag-plugin-android-exitinfo.api index 5a3a43aded..60c47da3b9 100644 --- a/bugsnag-plugin-android-exitinfo/api/bugsnag-plugin-android-exitinfo.api +++ b/bugsnag-plugin-android-exitinfo/api/bugsnag-plugin-android-exitinfo.api @@ -9,14 +9,17 @@ public final class com/bugsnag/android/BugsnagExitInfoPlugin : com/bugsnag/andro public final class com/bugsnag/android/ExitInfoPluginConfiguration { public fun ()V public fun (ZZZ)V - public synthetic fun (ZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (ZZZZ)V + public synthetic fun (ZZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z public final fun getDisableProcessStateSummaryOverride ()Z public final fun getIncludeLogcat ()Z public final fun getListOpenFds ()Z + public final fun getReportUnmatchedANR ()Z public fun hashCode ()I public final fun setDisableProcessStateSummaryOverride (Z)V public final fun setIncludeLogcat (Z)V public final fun setListOpenFds (Z)V + public final fun setReportUnmatchedANR (Z)V } diff --git a/bugsnag-plugin-android-exitinfo/build.gradle.kts b/bugsnag-plugin-android-exitinfo/build.gradle.kts index 6a5fdc1b5a..65b1171c28 100644 --- a/bugsnag-plugin-android-exitinfo/build.gradle.kts +++ b/bugsnag-plugin-android-exitinfo/build.gradle.kts @@ -1,20 +1,66 @@ plugins { - id("bugsnag-build-plugin") - id("com.android.library") - id("com.google.protobuf") version "0.9.4" + loadDefaultPlugins() + load(Versions.Plugins.protobuf) +} + +android { + compileSdk = Versions.Android.Build.compileSdkVersion + namespace = "com.bugsnag.android.exitinfo" + + defaultConfig { + minSdk = Versions.Android.Build.minSdkVersion + ndkVersion = Versions.Android.Build.ndk + + consumerProguardFiles("proguard-rules.pro") + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + lint { + isAbortOnError = true + isWarningsAsErrors = true + isCheckAllWarnings = true + baseline(File(project.projectDir, "lint-baseline.xml")) + disable("GradleDependency", "NewerVersionAvailable") + } + + buildFeatures { + aidl = false + renderScript = false + shaders = false + resValues = false + buildConfig = false + } + + compileOptions { + sourceCompatibility = Versions.java + targetCompatibility = Versions.java + } + + testOptions { + unitTests { + isReturnDefaultValues = true + } + } + + sourceSets { + named("test") { + java.srcDir(SHARED_TEST_SRC_DIR) + } + } + + libraryVariants.configureEach { + processJavaResourcesProvider { + exclude("**/*.proto") + } + } } dependencies { + addCommonModuleDependencies() api(project(":bugsnag-android-core")) implementation("com.google.protobuf:protobuf-javalite:3.24.2") } -android.libraryVariants.configureEach { - processJavaResourcesProvider { - exclude("**/*.proto") - } -} - protobuf { protoc { artifact = "com.google.protobuf:protoc:3.24.2" @@ -31,3 +77,9 @@ protobuf { } apiValidation.ignoredPackages += "com.bugsnag.android.repackaged.server.os" + +apply(from = rootProject.file("gradle/detekt.gradle")) +apply(from = rootProject.file("gradle/license-check.gradle")) +apply(from = rootProject.file("gradle/release.gradle")) + +configureCheckstyle() diff --git a/bugsnag-plugin-android-exitinfo/detekt-baseline.xml b/bugsnag-plugin-android-exitinfo/detekt-baseline.xml index 09e64143ef..44290b68f8 100644 --- a/bugsnag-plugin-android-exitinfo/detekt-baseline.xml +++ b/bugsnag-plugin-android-exitinfo/detekt-baseline.xml @@ -2,11 +2,13 @@ - CyclomaticComplexMethod:ExitInfoCallback.kt$ExitInfoCallback$@SuppressLint("SwitchIntDef") @Suppress("DEPRECATION") private fun importanceDescriptionOf(exitInfo: ApplicationExitInfo) - CyclomaticComplexMethod:ExitInfoCallback.kt$ExitInfoCallback$private fun exitReasonOf(exitInfo: ApplicationExitInfo) + CyclomaticComplexMethod:CodeStrings.kt$@RequiresApi(Build.VERSION_CODES.R) @SuppressLint("SwitchIntDef") @Suppress("DEPRECATION") internal fun importanceDescriptionOf(exitInfo: ApplicationExitInfo) + CyclomaticComplexMethod:CodeStrings.kt$@RequiresApi(Build.VERSION_CODES.R) internal fun exitReasonOf(exitInfo: ApplicationExitInfo) LongParameterList:TombstoneParser.kt$TombstoneParser$( exitInfo: ApplicationExitInfo, listOpenFds: Boolean, includeLogcat: Boolean, threadConsumer: (BugsnagThread) -> Unit, fileDescriptorConsumer: (Int, String, String) -> Unit, logcatConsumer: (String) -> Unit ) + MagicNumber:BugsnagExitInfoPlugin.kt$BugsnagExitInfoPlugin$100 MagicNumber:TraceParser.kt$TraceParser$16 MagicNumber:TraceParser.kt$TraceParser$3 + MaxLineLength:ExitInfoCallbackTest.kt$ExitInfoCallbackTest$exitInfoCallback = ExitInfoCallback(context, nativeEnhancer, anrEventEnhancer, null, ApplicationExitInfoMatcher(context, 100)) MaxLineLength:TraceParserInvalidStackframesTest.kt$TraceParserInvalidStackframesTest.Companion$"#01 pc 0000000000000c5c /data/app/~~sKQbJGqVJA5glcnvEjZCMg==/com.example.bugsnag.android-fVuoJh5GpAL7sRAeI3vjSw==/lib/arm64/libentrypoint.so " MaxLineLength:TraceParserInvalidStackframesTest.kt$TraceParserInvalidStackframesTest.Companion$"#01 pc 0000000000000c5c /data/app/~~sKQbJGqVJA5glcnvEjZCMg==/com.example.bugsnag.android-fVuoJh5GpAL7sRAeI3vjSw==/lib/arm64/libentrypoint.so (Java_com_example_bugsnag_android_BaseCrashyActivity_anrFromCXX+20" MaxLineLength:TraceParserInvalidStackframesTest.kt$TraceParserInvalidStackframesTest.Companion$"#01 pc 0000000000000c5c /data/app/~~sKQbJGqVJA5glcnvEjZCMg==/com.example.bugsnag.android-fVuoJh5GpAL7sRAeI3vjSw==/lib/arm64/libentrypoint.so (Java_com_example_bugsnag_android_BaseCrashyActivity_anrFromCXX+20) (" @@ -28,5 +30,6 @@ ReturnCount:TraceParser.kt$TraceParser$@VisibleForTesting internal fun parseNativeFrame(line: String): Stackframe? SwallowedException:BugsnagExitInfoPlugin.kt$BugsnagExitInfoPlugin$e: Exception SwallowedException:ExitInfoCallback.kt$ExitInfoCallback$exc: Throwable + SwallowedException:ExitInfoPluginStore.kt$ExitInfoPluginStore$exc: Throwable diff --git a/bugsnag-plugin-android-exitinfo/src/androidTest/java/com/bugsnag/android/BugsnagExitInfoPluginStoreTest.kt b/bugsnag-plugin-android-exitinfo/src/androidTest/java/com/bugsnag/android/BugsnagExitInfoPluginStoreTest.kt new file mode 100644 index 0000000000..8320df8199 --- /dev/null +++ b/bugsnag-plugin-android-exitinfo/src/androidTest/java/com/bugsnag/android/BugsnagExitInfoPluginStoreTest.kt @@ -0,0 +1,118 @@ +package com.bugsnag.android + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.bugsnag.android.internal.ImmutableConfig +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import java.io.File + +internal class BugsnagExitInfoPluginStoreTest { + + private lateinit var file: File + private lateinit var exitInfoPluginStore: ExitInfoPluginStore + private lateinit var immutableConfig: ImmutableConfig + + @Before + fun setUp() { + immutableConfig = TestHooks.convertToImmutableConfig( + generateConfiguration().apply { + persistenceDirectory = ApplicationProvider.getApplicationContext().cacheDir + } + ) + file = File(immutableConfig.persistenceDirectory.value, "bugsnag-exit-reasons") + file.delete() + } + + /** + * Null should be returned for non-existent files + */ + @Test + fun readNonExistentFile() { + exitInfoPluginStore = ExitInfoPluginStore(immutableConfig) + assertEquals(0, exitInfoPluginStore.currentPid) + assertEquals(0, exitInfoPluginStore.previousPid) + assertEquals(emptySet(), exitInfoPluginStore.exitInfoKeys) + } + + /** + * Null should be returned for empty files + */ + @Test + fun readEmptyFile() { + file.createNewFile() + exitInfoPluginStore = ExitInfoPluginStore(immutableConfig) + + assertEquals(0, exitInfoPluginStore.currentPid) + assertEquals(0, exitInfoPluginStore.previousPid) + assertEquals(emptySet(), exitInfoPluginStore.exitInfoKeys) + } + + /** + * Null should be returned for invalid file contents + */ + @Test + fun readInvalidFileContents() { + file.writeText("{\"hamster\": 2}") + exitInfoPluginStore = ExitInfoPluginStore(immutableConfig) + + assertEquals(0, exitInfoPluginStore.currentPid) + assertEquals(0, exitInfoPluginStore.previousPid) + assertEquals(emptySet(), exitInfoPluginStore.exitInfoKeys) + } + + /** + * A valid PID should be returned for legacy files, which contained only the PID of the previous run + */ + @Test + fun readLegacyFileContents() { + file.writeText("12345") + exitInfoPluginStore = ExitInfoPluginStore(immutableConfig) + assertEquals(12345, exitInfoPluginStore.previousPid) + assertEquals(0, exitInfoPluginStore.currentPid) + } + + @Test + fun addExitInfoKey() { + file.writeText("12345") + exitInfoPluginStore = ExitInfoPluginStore(immutableConfig) + exitInfoPluginStore.currentPid = 54321 + + assertEquals(12345, exitInfoPluginStore.previousPid) + + val expectedExitInfoKey = ExitInfoKey(111, 100L) + exitInfoPluginStore.addExitInfoKey(expectedExitInfoKey) + + // reload the ExitInfoPluginStore + exitInfoPluginStore = ExitInfoPluginStore(immutableConfig) + + assertEquals(54321, exitInfoPluginStore.previousPid) + assertEquals(0, exitInfoPluginStore.currentPid) + assertEquals(setOf(expectedExitInfoKey), exitInfoPluginStore.exitInfoKeys) + } + + private fun generateConfiguration(): Configuration { + val configuration = Configuration("5d1ec5bd39a74caa1267142706a7fb21") + configuration.delivery = generateDelivery() + return configuration + } + + private fun generateDelivery(): Delivery { + return object : Delivery { + override fun deliver( + payload: EventPayload, + deliveryParams: DeliveryParams + ): DeliveryStatus { + return DeliveryStatus.DELIVERED + } + + override fun deliver( + payload: Session, + deliveryParams: DeliveryParams + ): DeliveryStatus { + return DeliveryStatus.DELIVERED + } + } + } +} diff --git a/bugsnag-plugin-android-exitinfo/src/androidTest/java/com/bugsnag/android/TestHooks.java b/bugsnag-plugin-android-exitinfo/src/androidTest/java/com/bugsnag/android/TestHooks.java new file mode 100644 index 0000000000..a51c3a8ce2 --- /dev/null +++ b/bugsnag-plugin-android-exitinfo/src/androidTest/java/com/bugsnag/android/TestHooks.java @@ -0,0 +1,17 @@ +package com.bugsnag.android; + +import com.bugsnag.android.internal.ImmutableConfig; +import com.bugsnag.android.internal.ImmutableConfigKt; + +import androidx.annotation.NonNull; + +public class TestHooks { + private TestHooks() { + } + + public static ImmutableConfig convertToImmutableConfig( + @NonNull Configuration configuration + ) { + return ImmutableConfigKt.convertToImmutableConfig(configuration); + } +} diff --git a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ApplicationExitInfoMatcher.kt b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ApplicationExitInfoMatcher.kt new file mode 100644 index 0000000000..c03bae2907 --- /dev/null +++ b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ApplicationExitInfoMatcher.kt @@ -0,0 +1,41 @@ +package com.bugsnag.android + +import android.app.ActivityManager +import android.app.ApplicationExitInfo +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi + +@RequiresApi(Build.VERSION_CODES.R) +internal class ApplicationExitInfoMatcher( + private val context: Context, + private val pid: Int +) { + fun matchExitInfo(event: Event): ApplicationExitInfo? { + val am: ActivityManager = + context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val allExitInfo: List = + am.getHistoricalProcessExitReasons(context.packageName, MATCH_ALL, MAX_EXIT_INFO) + val sessionIdBytes: ByteArray = + event.session?.id?.toByteArray() ?: return null + val exitInfo: ApplicationExitInfo = + findExitInfoBySessionId(allExitInfo, sessionIdBytes) + ?: findExitInfoByPid(allExitInfo) ?: return null + return exitInfo + } + + internal fun findExitInfoBySessionId( + allExitInfo: List, + sessionIdBytes: ByteArray + ) = allExitInfo.find { + it.processStateSummary?.contentEquals(sessionIdBytes) == true + } + + internal fun findExitInfoByPid(allExitInfo: List) = + allExitInfo.find { it.pid == pid } + + internal companion object { + const val MATCH_ALL = 0 + const val MAX_EXIT_INFO = 100 + } +} diff --git a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/BugsnagExitInfoPlugin.kt b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/BugsnagExitInfoPlugin.kt index a1179ef8c3..cbf11895fa 100644 --- a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/BugsnagExitInfoPlugin.kt +++ b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/BugsnagExitInfoPlugin.kt @@ -1,9 +1,15 @@ package com.bugsnag.android +import android.annotation.SuppressLint import android.app.ActivityManager +import android.app.Application +import android.app.ApplicationExitInfo import android.content.Context import android.os.Build +import android.os.Process import androidx.annotation.RequiresApi +import com.bugsnag.android.ApplicationExitInfoMatcher.Companion.MATCH_ALL +import com.bugsnag.android.ApplicationExitInfoMatcher.Companion.MAX_EXIT_INFO @RequiresApi(Build.VERSION_CODES.R) class BugsnagExitInfoPlugin @JvmOverloads constructor( @@ -12,6 +18,7 @@ class BugsnagExitInfoPlugin @JvmOverloads constructor( private val configuration = configuration.copy() + @SuppressLint("VisibleForTests") override fun load(client: Client) { if (!configuration.disableProcessStateSummaryOverride) { client.addOnSession( @@ -23,25 +30,119 @@ class BugsnagExitInfoPlugin @JvmOverloads constructor( ) } + val tombstoneEventEnhancer = TombstoneEventEnhancer( + client.logger, + configuration.listOpenFds, + configuration.includeLogcat + ) + val traceEventEnhancer = TraceEventEnhancer( + client.logger, + client.immutableConfig.projectPackages + ) + val exitInfoPluginStore = ExitInfoPluginStore(client.immutableConfig) - val oldPid = exitInfoPluginStore.load() - exitInfoPluginStore.persist(android.os.Process.myPid()) - - val exitInfoCallback = ExitInfoCallback( - client.appContext, - oldPid, - TombstoneEventEnhancer( - client.logger, - configuration.listOpenFds, - configuration.includeLogcat - ), - TraceEventEnhancer( - client.logger, - client.immutableConfig.projectPackages - ) + addAllExitInfoAtFirstRun(client, exitInfoPluginStore) + exitInfoPluginStore.currentPid = Process.myPid() + + val exitInfoMatcher = ApplicationExitInfoMatcher( + context = client.appContext, + pid = exitInfoPluginStore.previousPid ) + val exitInfoCallback = createExitInfoCallback( + client, + exitInfoPluginStore, + tombstoneEventEnhancer, + traceEventEnhancer, + exitInfoMatcher + ) client.addOnSend(exitInfoCallback) + + if (client.appContext.isPrimaryProcess()) { + configureEventSynthesizer( + client, + exitInfoPluginStore, + traceEventEnhancer, + exitInfoMatcher + ) + } + } + + private fun configureEventSynthesizer( + client: Client, + exitInfoPluginStore: ExitInfoPluginStore, + traceEventEnhancer: TraceEventEnhancer, + exitInfoMatcher: ApplicationExitInfoMatcher + ) { + InternalHooks.setEventStoreEmptyCallback(client) { + synthesizeNewEvents( + client, + exitInfoPluginStore, + traceEventEnhancer + ) + } + + InternalHooks.setDiscardEventCallback(client) { eventPayload -> + val exitInfo = eventPayload.event?.let { exitInfoMatcher.matchExitInfo(it) } + exitInfo?.let { + exitInfoPluginStore.addExitInfoKey(ExitInfoKey(exitInfo)) + } + } + } + + private fun addAllExitInfoAtFirstRun( + client: Client, + exitInfoPluginStore: ExitInfoPluginStore + ) { + if (exitInfoPluginStore.isFirstRun || exitInfoPluginStore.legacyStore) { + val am: ActivityManager = client.appContext.safeGetActivityManager() ?: return + val allExitInfo: List = + am.getHistoricalProcessExitReasons( + client.appContext.packageName, + MATCH_ALL, + MAX_EXIT_INFO + ) + + allExitInfo.forEach { exitInfo -> + exitInfoPluginStore.addExitInfoKey(ExitInfoKey(exitInfo.pid, exitInfo.timestamp)) + } + } + } + + private fun createExitInfoCallback( + client: Client, + exitInfoPluginStore: ExitInfoPluginStore, + tombstoneEventEnhancer: TombstoneEventEnhancer, + traceEventEnhancer: TraceEventEnhancer, + applicationExitInfoMatcher: ApplicationExitInfoMatcher + ): ExitInfoCallback = ExitInfoCallback( + client.appContext, + tombstoneEventEnhancer, + traceEventEnhancer, + exitInfoPluginStore, + applicationExitInfoMatcher + ) + + private fun synthesizeNewEvents( + client: Client, + exitInfoPluginStore: ExitInfoPluginStore, + traceEventEnhancer: TraceEventEnhancer + ) { + val eventSynthesizer = EventSynthesizer( + traceEventEnhancer, + exitInfoPluginStore, + configuration.reportUnmatchedANR + ) + val context = client.appContext + val am: ActivityManager = context.safeGetActivityManager() ?: return + val allExitInfo: List = + am.getHistoricalProcessExitReasons(context.packageName, MATCH_ALL, MAX_EXIT_INFO) + allExitInfo.forEach { + val newEvent = eventSynthesizer.createEventWithExitInfo(it) + if (newEvent != null) { + InternalHooks.deliver(client, newEvent) + } + } } override fun unload() = Unit @@ -51,4 +152,8 @@ class BugsnagExitInfoPlugin @JvmOverloads constructor( } catch (e: Exception) { null } + + private fun Context.isPrimaryProcess(): Boolean { + return Application.getProcessName() == packageName + } } diff --git a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/CodeStrings.kt b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/CodeStrings.kt new file mode 100644 index 0000000000..d41da49d41 --- /dev/null +++ b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/CodeStrings.kt @@ -0,0 +1,83 @@ +package com.bugsnag.android + +import android.annotation.SuppressLint +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CANT_SAVE_STATE +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_EMPTY +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE_PRE_26 +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING_PRE_28 +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE +import android.app.ActivityManager.RunningAppProcessInfo.REASON_PROVIDER_IN_USE +import android.app.ActivityManager.RunningAppProcessInfo.REASON_SERVICE_IN_USE +import android.app.ApplicationExitInfo +import android.app.ApplicationExitInfo.REASON_ANR +import android.app.ApplicationExitInfo.REASON_CRASH +import android.app.ApplicationExitInfo.REASON_CRASH_NATIVE +import android.app.ApplicationExitInfo.REASON_DEPENDENCY_DIED +import android.app.ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE +import android.app.ApplicationExitInfo.REASON_EXIT_SELF +import android.app.ApplicationExitInfo.REASON_FREEZER +import android.app.ApplicationExitInfo.REASON_INITIALIZATION_FAILURE +import android.app.ApplicationExitInfo.REASON_LOW_MEMORY +import android.app.ApplicationExitInfo.REASON_OTHER +import android.app.ApplicationExitInfo.REASON_PACKAGE_STATE_CHANGE +import android.app.ApplicationExitInfo.REASON_PACKAGE_UPDATED +import android.app.ApplicationExitInfo.REASON_PERMISSION_CHANGE +import android.app.ApplicationExitInfo.REASON_SIGNALED +import android.app.ApplicationExitInfo.REASON_USER_REQUESTED +import android.app.ApplicationExitInfo.REASON_USER_STOPPED +import android.os.Build +import androidx.annotation.RequiresApi + +private const val IMPORTANCE_CANT_SAVE_STATE_PRE_26 = 170 + +@RequiresApi(Build.VERSION_CODES.R) +internal fun exitReasonOf(exitInfo: ApplicationExitInfo) = + when (exitInfo.reason) { + ApplicationExitInfo.REASON_UNKNOWN -> "unknown reason (${exitInfo.reason})" + REASON_EXIT_SELF -> "exit self" + REASON_SIGNALED -> "signaled" + REASON_LOW_MEMORY -> "low memory" + REASON_CRASH -> "crash" + REASON_CRASH_NATIVE -> "crash native" + REASON_ANR -> "ANR" + REASON_INITIALIZATION_FAILURE -> "initialization failure" + REASON_PERMISSION_CHANGE -> "permission change" + REASON_EXCESSIVE_RESOURCE_USAGE -> "excessive resource usage" + REASON_USER_REQUESTED -> "user requested" + REASON_USER_STOPPED -> "user stopped" + REASON_DEPENDENCY_DIED -> "dependency died" + REASON_OTHER -> "other" + REASON_FREEZER -> "freezer" + REASON_PACKAGE_STATE_CHANGE -> "package state change" + REASON_PACKAGE_UPDATED -> "package updated" + else -> "unknown reason (${exitInfo.reason})" + } + +@RequiresApi(Build.VERSION_CODES.R) +@SuppressLint("SwitchIntDef") +@Suppress("DEPRECATION") +internal fun importanceDescriptionOf(exitInfo: ApplicationExitInfo) = + when (exitInfo.importance) { + IMPORTANCE_FOREGROUND -> "foreground" + IMPORTANCE_FOREGROUND_SERVICE -> "foreground service" + IMPORTANCE_TOP_SLEEPING -> "top sleeping" + IMPORTANCE_TOP_SLEEPING_PRE_28 -> "top sleeping" + IMPORTANCE_VISIBLE -> "visible" + IMPORTANCE_PERCEPTIBLE -> "perceptible" + IMPORTANCE_PERCEPTIBLE_PRE_26 -> "perceptible" + IMPORTANCE_CANT_SAVE_STATE, IMPORTANCE_CANT_SAVE_STATE_PRE_26 -> "can't save state" + IMPORTANCE_SERVICE -> "service" + IMPORTANCE_CACHED -> "cached/background" + IMPORTANCE_GONE -> "gone" + IMPORTANCE_EMPTY -> "empty" + REASON_PROVIDER_IN_USE -> "provider in use" + REASON_SERVICE_IN_USE -> "service in use" + else -> "unknown importance (${exitInfo.importance})" + } diff --git a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/EventSynthesizer.kt b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/EventSynthesizer.kt new file mode 100644 index 0000000000..9e0e5ff8ac --- /dev/null +++ b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/EventSynthesizer.kt @@ -0,0 +1,83 @@ +package com.bugsnag.android + +import android.app.ApplicationExitInfo +import android.app.ApplicationExitInfo.REASON_ANR +import android.os.Build +import androidx.annotation.RequiresApi + +@RequiresApi(Build.VERSION_CODES.R) +internal class EventSynthesizer( + private val anrEventEnhancer: (Event, ApplicationExitInfo) -> Unit, + private val exitInfoPluginStore: ExitInfoPluginStore, + private val reportUnmatchedANRs: Boolean, +) { + fun createEventWithExitInfo(appExitInfo: ApplicationExitInfo): Event? { + val knownExitInfoKeys = exitInfoPluginStore.exitInfoKeys + val exitInfoKey = ExitInfoKey(appExitInfo) + + if (knownExitInfoKeys.contains(exitInfoKey)) { + return null + } + + exitInfoPluginStore.addExitInfoKey(exitInfoKey) + + return when (appExitInfo.reason) { + REASON_ANR -> { + createEventWithUnmatchedANR(exitInfoKey, appExitInfo) + } + + else -> null + } + } + + private fun createEventWithUnmatchedANR( + exitInfoKey: ExitInfoKey, + appExitInfo: ApplicationExitInfo + ): Event? { + if (reportUnmatchedANRs) { + val newAnrEvent = InternalHooks.createEmptyANR(exitInfoKey.timestamp) + ?: return null + addExitInfoMetadata(newAnrEvent, appExitInfo) + anrEventEnhancer(newAnrEvent, appExitInfo) + val thread = getErrorThread(newAnrEvent) + val error = newAnrEvent.addError("ANR", appExitInfo.description) + thread?.let { error.stacktrace.addAll(it.stacktrace) } + + return newAnrEvent + } else { + return null + } + } + + private fun getErrorThread(newNativeEvent: Event): Thread? { + val thread = newNativeEvent.threads.find { it.name == "main" } + ?: newNativeEvent.threads.firstOrNull() + return thread + } + + private fun addExitInfoMetadata( + newEvent: Event, + appExitInfo: ApplicationExitInfo + ) { + newEvent.addMetadata("exitInfo", "Description", appExitInfo.description) + newEvent.addMetadata( + "exitInfo", + "Importance", + importanceDescriptionOf(appExitInfo) + ) + + val pss = appExitInfo.pss + if (pss > 0) { + newEvent.addMetadata( + "exitInfo", "Proportional Set Size (PSS)", "$pss kB" + ) + } + + val rss = appExitInfo.rss + if (rss > 0) { + newEvent.addMetadata( + "exitInfo", "Resident Set Size (RSS)", "$rss kB" + ) + } + } +} diff --git a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoCallback.kt b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoCallback.kt index 0feefbf78c..182e8c68a9 100644 --- a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoCallback.kt +++ b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoCallback.kt @@ -1,40 +1,31 @@ package com.bugsnag.android -import android.annotation.SuppressLint import android.app.ActivityManager -import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED -import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CANT_SAVE_STATE -import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_EMPTY -import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND -import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE -import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE -import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE -import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE_PRE_26 -import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE -import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING -import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING_PRE_28 -import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE -import android.app.ActivityManager.RunningAppProcessInfo.REASON_PROVIDER_IN_USE -import android.app.ActivityManager.RunningAppProcessInfo.REASON_SERVICE_IN_USE import android.app.ApplicationExitInfo import android.content.Context import android.os.Build import androidx.annotation.RequiresApi +import com.bugsnag.android.ApplicationExitInfoMatcher.Companion.MATCH_ALL @RequiresApi(Build.VERSION_CODES.R) internal class ExitInfoCallback( private val context: Context, - private val pid: Int?, private val nativeEnhancer: (Event, ApplicationExitInfo) -> Unit, - private val anrEventEnhancer: (Event, ApplicationExitInfo) -> Unit + private val anrEventEnhancer: (Event, ApplicationExitInfo) -> Unit, + private val exitInfoPluginStore: ExitInfoPluginStore?, + private val applicationExitInfoMatcher: ApplicationExitInfoMatcher?, ) : OnSendCallback { override fun onSend(event: Event): Boolean { - val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - val allExitInfo = am.getHistoricalProcessExitReasons(context.packageName, 0, MAX_EXIT_INFO) - val sessionIdBytes = event.session?.id?.toByteArray() ?: return true - val exitInfo = findExitInfoBySessionId(allExitInfo, sessionIdBytes) - ?: findExitInfoByPid(allExitInfo) ?: return true + val am: ActivityManager = + context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val allExitInfo: List = + am.getHistoricalProcessExitReasons(context.packageName, MATCH_ALL, MAX_EXIT_INFO) + val sessionIdBytes: ByteArray = event.session?.id?.toByteArray() ?: return true + val exitInfo: ApplicationExitInfo = + applicationExitInfoMatcher?.findExitInfoBySessionId(allExitInfo, sessionIdBytes) + ?: applicationExitInfoMatcher?.findExitInfoByPid(allExitInfo) ?: return true + exitInfoPluginStore?.addExitInfoKey(ExitInfoKey(exitInfo.pid, exitInfo.timestamp)) try { val reason = exitReasonOf(exitInfo) @@ -47,8 +38,10 @@ internal class ExitInfoCallback( exitInfo.reason == ApplicationExitInfo.REASON_SIGNALED ) { nativeEnhancer(event, exitInfo) + exitInfoPluginStore?.addExitInfoKey(ExitInfoKey(exitInfo)) } else if (exitInfo.reason == ApplicationExitInfo.REASON_ANR) { anrEventEnhancer(event, exitInfo) + exitInfoPluginStore?.addExitInfoKey(ExitInfoKey(exitInfo)) } } catch (exc: Throwable) { return true @@ -56,60 +49,7 @@ internal class ExitInfoCallback( return true } - private fun exitReasonOf(exitInfo: ApplicationExitInfo) = when (exitInfo.reason) { - ApplicationExitInfo.REASON_UNKNOWN -> "unknown reason (${exitInfo.reason})" - ApplicationExitInfo.REASON_EXIT_SELF -> "exit self" - ApplicationExitInfo.REASON_SIGNALED -> "signaled" - ApplicationExitInfo.REASON_LOW_MEMORY -> "low memory" - ApplicationExitInfo.REASON_CRASH -> "crash" - ApplicationExitInfo.REASON_CRASH_NATIVE -> "crash native" - ApplicationExitInfo.REASON_ANR -> "ANR" - ApplicationExitInfo.REASON_INITIALIZATION_FAILURE -> "initialization failure" - ApplicationExitInfo.REASON_PERMISSION_CHANGE -> "permission change" - ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE -> "excessive resource usage" - ApplicationExitInfo.REASON_USER_REQUESTED -> "user requested" - ApplicationExitInfo.REASON_USER_STOPPED -> "user stopped" - ApplicationExitInfo.REASON_DEPENDENCY_DIED -> "dependency died" - ApplicationExitInfo.REASON_OTHER -> "other" - ApplicationExitInfo.REASON_FREEZER -> "freezer" - ApplicationExitInfo.REASON_PACKAGE_STATE_CHANGE -> "package state change" - ApplicationExitInfo.REASON_PACKAGE_UPDATED -> "package updated" - else -> "unknown reason (${exitInfo.reason})" - } - - @SuppressLint("SwitchIntDef") - @Suppress("DEPRECATION") - private fun importanceDescriptionOf(exitInfo: ApplicationExitInfo) = when (exitInfo.importance) { - IMPORTANCE_FOREGROUND -> "foreground" - IMPORTANCE_FOREGROUND_SERVICE -> "foreground service" - IMPORTANCE_TOP_SLEEPING -> "top sleeping" - IMPORTANCE_TOP_SLEEPING_PRE_28 -> "top sleeping" - IMPORTANCE_VISIBLE -> "visible" - IMPORTANCE_PERCEPTIBLE -> "perceptible" - IMPORTANCE_PERCEPTIBLE_PRE_26 -> "perceptible" - IMPORTANCE_CANT_SAVE_STATE -> "can't save state" - IMPORTANCE_CANT_SAVE_STATE_PRE_26 -> "can't save state" - IMPORTANCE_SERVICE -> "service" - IMPORTANCE_CACHED -> "cached/background" - IMPORTANCE_GONE -> "gone" - IMPORTANCE_EMPTY -> "empty" - REASON_PROVIDER_IN_USE -> "provider in use" - REASON_SERVICE_IN_USE -> "service in use" - else -> "unknown importance (${exitInfo.importance})" - } - - private fun findExitInfoBySessionId( - allExitInfo: List, - sessionIdBytes: ByteArray - ) = allExitInfo.find { - it.processStateSummary?.contentEquals(sessionIdBytes) == true - } - - private fun findExitInfoByPid(allExitInfo: List) = - allExitInfo.find { it.pid == pid } - internal companion object { private const val MAX_EXIT_INFO = 100 - private const val IMPORTANCE_CANT_SAVE_STATE_PRE_26 = 170 } } diff --git a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoKey.kt b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoKey.kt new file mode 100644 index 0000000000..a0e6afdae4 --- /dev/null +++ b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoKey.kt @@ -0,0 +1,18 @@ +package com.bugsnag.android + +import android.app.ApplicationExitInfo +import android.os.Build +import androidx.annotation.RequiresApi + +internal data class ExitInfoKey(val pid: Int, val timestamp: Long) : JsonStream.Streamable { + @RequiresApi(Build.VERSION_CODES.R) + constructor(exitInfo: ApplicationExitInfo) : + this(exitInfo.pid, exitInfo.timestamp) + + override fun toStream(stream: JsonStream) { + stream.beginObject() + .name("pid").value(pid) + .name("timestamp").value(timestamp.toString()) + .endObject() + } +} diff --git a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoPluginConfiguration.kt b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoPluginConfiguration.kt index a9882bd84b..8ec5c3dff9 100644 --- a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoPluginConfiguration.kt +++ b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoPluginConfiguration.kt @@ -16,23 +16,54 @@ class ExitInfoPluginConfiguration( * [processStateSummary](ActivityManager.setProcessStateSummary) field. This can set to `true` * to stop `BugsnagExitInfoPlugin` overwriting the field if it is being used by the app. */ - var disableProcessStateSummaryOverride: Boolean = false + var disableProcessStateSummaryOverride: Boolean = false, + + /** + * Report [ApplicationExitInfo] ANRs that do not appear to correspond with BugSnag [Event]s + * as synthesized errors. These will appear on your dashboard without BugSnag data such as + * breadcrumbs and metadata, but will report crashes that BugSnag is otherwise unable to catch + * such as background ANRs. + */ + var reportUnmatchedANR: Boolean = false ) { - constructor() : this(true, false, false) + constructor( + listOpenFds: Boolean, + includeLogcat: Boolean, + disableProcessStateSummaryOverride: Boolean + ) : this( + listOpenFds, + includeLogcat, + disableProcessStateSummaryOverride, + true + ) + + constructor() : this( + listOpenFds = true, + includeLogcat = false, + reportUnmatchedANR = false, + disableProcessStateSummaryOverride = false + ) internal fun copy() = - ExitInfoPluginConfiguration(listOpenFds, includeLogcat, disableProcessStateSummaryOverride) + ExitInfoPluginConfiguration( + listOpenFds, + includeLogcat, + disableProcessStateSummaryOverride, + reportUnmatchedANR, + ) override fun equals(other: Any?): Boolean { return other is ExitInfoPluginConfiguration && listOpenFds == other.listOpenFds && includeLogcat == other.includeLogcat && + reportUnmatchedANR == other.reportUnmatchedANR && disableProcessStateSummaryOverride == other.disableProcessStateSummaryOverride } override fun hashCode(): Int { var result = listOpenFds.hashCode() result = 31 * result + includeLogcat.hashCode() + result = 31 * result + reportUnmatchedANR.hashCode() result = 31 * result + disableProcessStateSummaryOverride.hashCode() return result } diff --git a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoPluginStore.kt b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoPluginStore.kt index 18f0dd895f..b587df17fd 100644 --- a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoPluginStore.kt +++ b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoPluginStore.kt @@ -1,6 +1,7 @@ package com.bugsnag.android import com.bugsnag.android.internal.ImmutableConfig +import org.json.JSONObject import java.io.File import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.withLock @@ -9,39 +10,99 @@ internal class ExitInfoPluginStore(config: ImmutableConfig) { private val file: File = File(config.persistenceDirectory.value, "bugsnag-exit-reasons") private val logger: Logger = config.logger private val lock = ReentrantReadWriteLock() + internal val isFirstRun: Boolean = !file.exists() - fun persist(pid: Int) { - lock.writeLock().withLock { - try { - val text = pid.toString() - file.writeText(text) - } catch (exc: Throwable) { - logger.w("Unexpectedly failed to persist PID.", exc) + internal var legacyStore: Boolean = false + private set + + var previousPid: Int = 0 + private set + + var currentPid: Int = 0 + + private var _exitInfoKeys = HashSet() + val exitInfoKeys: Set get() = _exitInfoKeys + + init { + load() + } + + private fun load() { + lock.readLock().withLock { + val json = tryLoadJson() + if (json != null) { + if (previousPid == 0) { + previousPid = json.first ?: 0 + } else { + currentPid = json.first ?: 0 + } + + _exitInfoKeys = json.second + } else { + val legacy = tryLoadLegacy() + if (legacy != null) { + previousPid = legacy + } } } } - fun load(): Int? { - return lock.readLock().withLock { + private fun persist() { + lock.writeLock().withLock { try { - loadImpl() + file.writer().buffered().use { writer -> + JsonStream(writer).use { json -> + json.beginObject() + .name("pid").value(currentPid) + .name("exitInfoKeys") + json.value(exitInfoKeys) + json.endObject() + } + } } catch (exc: Throwable) { - logger.w("Unexpectedly failed to load PID.", exc) - null + logger.w("Unexpectedly failed to persist PID.", exc) } } } - private fun loadImpl(): Int? { - if (!file.exists()) { + private fun tryLoadJson(): Pair>? { + try { + val fileContents = file.readText() + val jsonObject = JSONObject(fileContents) + val currentPid = jsonObject.getInt("pid") + val exitInfoKeys = HashSet() + val jsonArray = jsonObject.getJSONArray("exitInfoKeys") + for (i in 0 until jsonArray.length()) { + val exitInfoKeyObject = jsonArray.getJSONObject(i) + exitInfoKeys.add( + ExitInfoKey( + exitInfoKeyObject.getString("pid").toInt(), + exitInfoKeyObject.getString("timestamp").toLong() + ) + ) + } + return Pair(currentPid, exitInfoKeys) + } catch (exc: Throwable) { return null } + } - val content = file.readText() - if (content.isEmpty()) { - logger.w("PID is empty") + private fun tryLoadLegacy(): Int? { + try { + val content = file.readText() + if (content.isEmpty()) { + logger.w("PID is empty") + return null + } + return content.toIntOrNull() + } catch (exc: Throwable) { + logger.w("Unexpectedly failed to load PID.", exc) return null } - return content.toIntOrNull() + } + + fun addExitInfoKey(exitInfoKey: ExitInfoKey) { + _exitInfoKeys.add(exitInfoKey) + persist() } } diff --git a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/InternalHooks.java b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/InternalHooks.java new file mode 100644 index 0000000000..8976269c41 --- /dev/null +++ b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/InternalHooks.java @@ -0,0 +1,78 @@ +package com.bugsnag.android; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import kotlin.Unit; +import kotlin.jvm.functions.Function0; +import kotlin.jvm.functions.Function1; + +class InternalHooks { + + private InternalHooks() { + } + + public static void setEventStoreEmptyCallback(Client client, Function0 callback) { + client.eventStore.setOnEventStoreEmptyCallback(callback); + } + + public static void setDiscardEventCallback( + Client client, + Function1 callback) { + client.eventStore.setOnDiscardEventCallback(callback); + } + + static void deliver(@NonNull Client client, @NonNull Event event) { + client.deliveryDelegate.deliver(event); + } + + @Nullable + static Event createEmptyANR(long exitInfoTimeStamp) { + try { + Client client = Bugsnag.getClient(); + DeviceDataCollector deviceDataCollector = client.getDeviceDataCollector(); + + if (deviceDataCollector == null) { + return null; + } + + AppDataCollector appDataCollector = client.getAppDataCollector(); + if (appDataCollector == null) { + return null; + } + + Event event = NativeInterface.createEmptyEvent(); + event.setDevice(deviceDataCollector.generateHistoricDeviceWithState(exitInfoTimeStamp)); + event.setApp(appDataCollector.generateHistoricAppWithState()); + event.updateSeverityReason(SeverityReason.REASON_ANR); + return event; + } catch (Exception ex) { + return null; + } + } + + @Nullable + static Event createEmptyCrash(long exitInfoTimeStamp) { + try { + Client client = Bugsnag.getClient(); + DeviceDataCollector deviceDataCollector = client.getDeviceDataCollector(); + + if (deviceDataCollector == null) { + return null; + } + + AppDataCollector appDataCollector = client.getAppDataCollector(); + if (appDataCollector == null) { + return null; + } + + Event event = NativeInterface.createEmptyEvent(); + event.setDevice(deviceDataCollector.generateHistoricDeviceWithState(exitInfoTimeStamp)); + event.setApp(appDataCollector.generateHistoricAppWithState()); + event.updateSeverityReason(SeverityReason.REASON_SIGNAL); + return event; + } catch (Exception ex) { + return null; + } + } +} diff --git a/bugsnag-plugin-android-exitinfo/src/test/java/com/bugsnag/android/BugsnagExitInfoPluginStoreTest.kt b/bugsnag-plugin-android-exitinfo/src/test/java/com/bugsnag/android/BugsnagExitInfoPluginStoreTest.kt deleted file mode 100644 index 70248a215e..0000000000 --- a/bugsnag-plugin-android-exitinfo/src/test/java/com/bugsnag/android/BugsnagExitInfoPluginStoreTest.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.bugsnag.android - -import com.bugsnag.android.internal.ImmutableConfig -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Before -import org.junit.Test -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` -import java.io.File -import java.nio.file.Files - -internal class BugsnagExitInfoPluginStoreTest { - - private lateinit var file: File - private lateinit var exitInfoPluginStore: ExitInfoPluginStore - private lateinit var storageDir: File - - private val immutableConfig = mock(ImmutableConfig::class.java) - private val logger = mock(Logger::class.java) - - @Before - fun setUp() { - storageDir = Files.createTempDirectory("tmp").toFile() - `when`(immutableConfig.persistenceDirectory).thenReturn(lazy { storageDir }) - `when`(immutableConfig.logger).thenReturn(logger) - file = File(immutableConfig.persistenceDirectory.value, "bugsnag-exit-reasons") - file.delete() - exitInfoPluginStore = ExitInfoPluginStore(immutableConfig) - } - - /** - * Null should be returned for non-existent files - */ - @Test - fun readNonExistentFile() { - assertNull(exitInfoPluginStore.load()) - } - - /** - * Null should be returned for empty files - */ - @Test - fun readEmptyFile() { - file.createNewFile() - assertNull(exitInfoPluginStore.load()) - } - - /** - * Null should be returned for invalid file contents - */ - @Test - fun readInvalidFileContents() { - file.writeText("{\"hamster\": 2}") - assertNull(exitInfoPluginStore.load()) - } - - /** - * Information should be returned for valid files - */ - @Test - fun readValidFileContents() { - file.writeText("12345") - val info = requireNotNull(exitInfoPluginStore.load()) - assertEquals(12345, info) - } - - @Test - fun writableFile() { - exitInfoPluginStore.persist(12345) - val pid = file.readText() - assertEquals("12345", pid) - } - - @Test - fun nonWritableFile() { - file.apply { - delete() - createNewFile() - setWritable(false) - } - assertNull(exitInfoPluginStore.load()) - } -} diff --git a/bugsnag-plugin-android-exitinfo/src/test/java/com/bugsnag/android/ExitInfoCallbackTest.kt b/bugsnag-plugin-android-exitinfo/src/test/java/com/bugsnag/android/ExitInfoCallbackTest.kt index f6ffe3ef34..9591624444 100644 --- a/bugsnag-plugin-android-exitinfo/src/test/java/com/bugsnag/android/ExitInfoCallbackTest.kt +++ b/bugsnag-plugin-android-exitinfo/src/test/java/com/bugsnag/android/ExitInfoCallbackTest.kt @@ -45,7 +45,7 @@ internal class ExitInfoCallbackTest { @Before fun setUp() { - exitInfoCallback = ExitInfoCallback(context, 100, nativeEnhancer, anrEventEnhancer) + exitInfoCallback = ExitInfoCallback(context, nativeEnhancer, anrEventEnhancer, null, ApplicationExitInfoMatcher(context, 100)) exitInfos = listOf(exitInfo1) `when`(context.getSystemService(any())).thenReturn(am) `when`(am.getHistoricalProcessExitReasons(any(), anyInt(), anyInt())) diff --git a/bugsnag-plugin-android-ndk/build.gradle.kts b/bugsnag-plugin-android-ndk/build.gradle.kts index 1006109c80..51a08ab826 100644 --- a/bugsnag-plugin-android-ndk/build.gradle.kts +++ b/bugsnag-plugin-android-ndk/build.gradle.kts @@ -1,15 +1,67 @@ plugins { - id("bugsnag-build-plugin") + loadDefaultPlugins() } -bugsnagBuildOptions { - usesNdk = true - publishesPrefab = "bugsnag-ndk" -} +android { + compileSdk = Versions.Android.Build.compileSdkVersion + namespace = "com.bugsnag.android.ndk" + + defaultConfig { + minSdk = Versions.Android.Build.minSdkVersion + ndkVersion = Versions.Android.Build.ndk + + consumerProguardFiles("proguard-rules.pro") + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + externalNativeBuild.cmake.arguments += BugsnagDefaults.cmakeArguments + + configureAbis(ndk.abiFilters) + } -apply(plugin = "com.android.library") + lint { + isAbortOnError = true + isWarningsAsErrors = true + isCheckAllWarnings = true + baseline(File(project.projectDir, "lint-baseline.xml")) + disable("GradleDependency", "NewerVersionAvailable") + } + + buildFeatures { + aidl = false + renderScript = false + shaders = false + resValues = false + buildConfig = false + } + + compileOptions { + sourceCompatibility = Versions.java + targetCompatibility = Versions.java + } + + testOptions { + unitTests { + isReturnDefaultValues = true + } + } + + sourceSets { + named("test") { + java.srcDir(SHARED_TEST_SRC_DIR) + } + } + + buildFeatures.prefabPublishing = true + prefab.create("bugsnag-ndk") { + headers = "src/main/jni/include" + } + + externalNativeBuild.cmake.path = project.file("CMakeLists.txt") + externalNativeBuild.cmake.version = Versions.Android.Build.cmakeVersion +} dependencies { + addCommonModuleDependencies() add("api", project(":bugsnag-android-core")) } @@ -24,3 +76,9 @@ afterEvaluate { } } } + +apply(from = rootProject.file("gradle/detekt.gradle")) +apply(from = rootProject.file("gradle/license-check.gradle")) +apply(from = rootProject.file("gradle/release.gradle")) + +configureCheckstyle() diff --git a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/NdkPlugin.kt b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/NdkPlugin.kt index 80ff9efad0..2882b55b8f 100644 --- a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/NdkPlugin.kt +++ b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/NdkPlugin.kt @@ -46,6 +46,9 @@ internal class NdkPlugin : Plugin { private fun performOneTimeSetup(client: Client) { libraryLoader.loadLibrary("bugsnag-ndk", client) { val error = it.errors[0] + it.addMetadata("LinkError", "errorClass", error.errorClass) + it.addMetadata("LinkError", "errorMessage", error.errorMessage) + error.errorClass = "NdkLinkError" error.errorMessage = LOAD_ERR_MSG true diff --git a/bugsnag-plugin-android-okhttp/build.gradle.kts b/bugsnag-plugin-android-okhttp/build.gradle.kts index 9bfba99091..7ac8784704 100644 --- a/bugsnag-plugin-android-okhttp/build.gradle.kts +++ b/bugsnag-plugin-android-okhttp/build.gradle.kts @@ -1,9 +1,56 @@ plugins { - id("bugsnag-build-plugin") - id("com.android.library") + loadDefaultPlugins() +} + +android { + compileSdk = Versions.Android.Build.compileSdkVersion + namespace = "com.bugsnag.android.okhttp" + + defaultConfig { + minSdk = Versions.Android.Build.minSdkVersion + ndkVersion = Versions.Android.Build.ndk + + consumerProguardFiles("proguard-rules.pro") + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + lint { + isAbortOnError = true + isWarningsAsErrors = true + isCheckAllWarnings = true + baseline(File(project.projectDir, "lint-baseline.xml")) + disable("GradleDependency", "NewerVersionAvailable") + } + + buildFeatures { + aidl = false + renderScript = false + shaders = false + resValues = false + buildConfig = false + } + + compileOptions { + sourceCompatibility = Versions.java + targetCompatibility = Versions.java + } + + testOptions { + unitTests { + isReturnDefaultValues = true + } + } + + sourceSets { + named("test") { + java.srcDir(SHARED_TEST_SRC_DIR) + } + } } dependencies { + addCommonModuleDependencies() + add("api", project(":bugsnag-android-core")) add("compileOnly", "com.squareup.okhttp3:okhttp:4.9.1") { @@ -14,3 +61,9 @@ dependencies { exclude(group = "org.jetbrains.kotlin") } } + +apply(from = rootProject.file("gradle/detekt.gradle")) +apply(from = rootProject.file("gradle/license-check.gradle")) +apply(from = rootProject.file("gradle/release.gradle")) + +configureCheckstyle() diff --git a/bugsnag-plugin-react-native/build.gradle.kts b/bugsnag-plugin-react-native/build.gradle.kts index da00b5fbe3..8aaf78009f 100644 --- a/bugsnag-plugin-react-native/build.gradle.kts +++ b/bugsnag-plugin-react-native/build.gradle.kts @@ -1,8 +1,60 @@ plugins { - id("bugsnag-build-plugin") - id("com.android.library") + loadDefaultPlugins() +} + +android { + compileSdk = Versions.Android.Build.compileSdkVersion + namespace = "com.bugsnag.android.reactnative" + + defaultConfig { + minSdk = Versions.Android.Build.minSdkVersion + ndkVersion = Versions.Android.Build.ndk + + consumerProguardFiles("proguard-rules.pro") + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + lint { + isAbortOnError = true + isWarningsAsErrors = true + isCheckAllWarnings = true + baseline(File(project.projectDir, "lint-baseline.xml")) + disable("GradleDependency", "NewerVersionAvailable") + } + + buildFeatures { + aidl = false + renderScript = false + shaders = false + resValues = false + buildConfig = false + } + + compileOptions { + sourceCompatibility = Versions.java + targetCompatibility = Versions.java + } + + testOptions { + unitTests { + isReturnDefaultValues = true + } + } + + sourceSets { + named("test") { + java.srcDir(SHARED_TEST_SRC_DIR) + } + } } dependencies { + addCommonModuleDependencies() add("api", project(":bugsnag-android-core")) } + +apply(from = rootProject.file("gradle/detekt.gradle")) +apply(from = rootProject.file("gradle/license-check.gradle")) +apply(from = rootProject.file("gradle/release.gradle")) + +configureCheckstyle() diff --git a/build.gradle.kts b/build.gradle.kts index 07f756315d..871f1073c5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,23 +1,8 @@ -buildscript { - repositories { - google() - mavenCentral() - maven(url = "https://plugins.gradle.org/m2/") - } - - dependencies { - classpath("com.android.tools.build:gradle:7.0.4") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10") - classpath("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.1") - classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.0") - classpath("org.jlleitschuh.gradle:ktlint-gradle:10.2.0") - classpath("androidx.benchmark:benchmark-gradle-plugin:1.1.1") - } -} +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - id("com.github.hierynomus.license") version "0.16.1" - id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.13.1" apply false + load(Versions.Plugins.AGP) apply false + load(Versions.Plugins.kotlin) apply false } allprojects { @@ -31,4 +16,19 @@ allprojects { options.compilerArgs.addAll(listOf("-Xlint:all", "-Werror")) } } -} \ No newline at end of file +} + +subprojects { + tasks.withType(KotlinCompile::class.java).configureEach { + kotlinOptions { + allWarningsAsErrors = true + apiVersion = Versions.kotlinLang + languageVersion = Versions.kotlinLang + freeCompilerArgs += listOf( + "-Xno-call-assertions", + "-Xno-receiver-assertions", + "-Xno-param-assertions" + ) + } + } +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 901628b472..bac8645f5d 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,24 +1,11 @@ plugins { - `java-gradle-plugin` `kotlin-dsl` } -gradlePlugin { - plugins { - register("bugsnag-build-plugin") { - id = "bugsnag-build-plugin" - implementationClass = "com.bugsnag.android.BugsnagBuildPlugin" - } - } -} - repositories { - google() mavenCentral() } dependencies { compileOnly(gradleApi()) - implementation("com.android.tools.build:gradle:7.0.2") - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10") } diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 0000000000..ac180651c0 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "bugsnag-buildSrc" \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/com/bugsnag/android/Checkstyle.kt b/buildSrc/src/main/kotlin/Checkstyle.kt similarity index 95% rename from buildSrc/src/main/kotlin/com/bugsnag/android/Checkstyle.kt rename to buildSrc/src/main/kotlin/Checkstyle.kt index 989a9ae6ad..d8bd874b75 100644 --- a/buildSrc/src/main/kotlin/com/bugsnag/android/Checkstyle.kt +++ b/buildSrc/src/main/kotlin/Checkstyle.kt @@ -1,5 +1,3 @@ -package com.bugsnag.android - import org.gradle.api.Project import org.gradle.api.plugins.quality.Checkstyle import org.gradle.api.plugins.quality.CheckstyleExtension diff --git a/buildSrc/src/main/kotlin/ProjectDefaults.kt b/buildSrc/src/main/kotlin/ProjectDefaults.kt new file mode 100644 index 0000000000..38da13d88c --- /dev/null +++ b/buildSrc/src/main/kotlin/ProjectDefaults.kt @@ -0,0 +1,36 @@ +import org.gradle.api.Project +import org.gradle.plugin.use.PluginDependenciesSpec + +const val SHARED_TEST_SRC_DIR = "../bugsnag-android-core/src/sharedTest/java" + +object BugsnagDefaults { + val cmakeArguments + get() = listOf( + "-DANDROID_CPP_FEATURES=exceptions", + "-DANDROID_STL=c++_static" + ) +} + +fun Project.configureAbis(abiFilters: MutableSet) { + val override: String? = project.findProperty("ABI_FILTERS") as String? + val abis = override?.split(",") ?: mutableSetOf( + "arm64-v8a", + "armeabi-v7a", + "x86", + "x86_64" + ) + + abiFilters.clear() + abiFilters.addAll(abis) +} + +fun PluginDependenciesSpec.loadDefaultPlugins() { + load(Versions.Plugins.AGP) + load(Versions.Plugins.kotlin) + load(Versions.Plugins.ktlint) + load(Versions.Plugins.dokka) + load(Versions.Plugins.detekt) + load(Versions.Plugins.licenseCheck) + load(Versions.Plugins.binaryCompatibilityCheck) + load(Versions.Plugins.checkstyle) +} diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt new file mode 100644 index 0000000000..ad9e74a7e6 --- /dev/null +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -0,0 +1,84 @@ +import org.gradle.api.JavaVersion +import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.plugin.use.PluginDependenciesSpec +import org.gradle.plugin.use.PluginDependencySpec + +/** + * Controls the versions of plugins, dependencies, and build targets used by the SDK. + */ +object Versions { + val java = JavaVersion.VERSION_1_8 + val kotlin = "1.6.0" + val kotlinLang = "1.5" + + object Plugins { + val AGP = ModuleWithVersion("com.android.library", "7.0.4") + val kotlin = ModuleWithVersion("org.jetbrains.kotlin.android", Versions.kotlin) + val detekt = ModuleWithVersion("io.gitlab.arturbosch.detekt", "1.23.1") + val ktlint = ModuleWithVersion("org.jlleitschuh.gradle.ktlint", "10.2.0") + val dokka = ModuleWithVersion("org.jetbrains.dokka", "1.9.20") + val binaryCompatibilityCheck = ModuleWithVersion( + "org.jetbrains.kotlinx.binary-compatibility-validator", "0.13.1") + val licenseCheck = ModuleWithVersion("com.github.hierynomus.license", "0.16.1") + + val protobuf = ModuleWithVersion("com.google.protobuf", "0.9.4") + + val checkstyle = ModuleWithVersion("checkstyle") + } + + object Android { + // dependencies + val supportLib = "1.1.0" + val supportTestLib = "1.2.0" + val espressoTestLib = "3.1.0" + val junitTestLib = "4.12" + val mockitoTestLib = "2.28.2" + + object Build { + // Note minSdkVersion must be >=21 for 64 bit architectures + val minSdkVersion = 14 + val compileSdkVersion = 34 + + val ndk = "23.1.7779620" + val cmakeVersion = "3.22.1" + } + } + + data class ModuleWithVersion( + val id: String, + val version: String? = null, + ) +} + +fun PluginDependenciesSpec.load(module: Versions.ModuleWithVersion): PluginDependencySpec { + var plugin = id(module.id) + if (module.version != null) { + plugin = plugin.version(module.version) + } + + return plugin +} + +fun DependencyHandler.addCommonModuleDependencies() { + // needs to be kept as 'compile' for license checking to work + // as otherwise the downloadLicenses task misses these deps + add("api", "androidx.annotation:annotation:${Versions.Android.supportLib}") + add("api", "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}") + + add("testImplementation", "junit:junit:${Versions.Android.junitTestLib}") + add("testImplementation", "org.mockito:mockito-core:${Versions.Android.mockitoTestLib}") + add("testImplementation", "org.mockito:mockito-inline:${Versions.Android.mockitoTestLib}") + add("testImplementation", "androidx.test:core:${Versions.Android.supportTestLib}") + + add( + "androidTestImplementation", + "org.mockito:mockito-android:${Versions.Android.mockitoTestLib}" + ) + add("androidTestImplementation", "androidx.test:core:${Versions.Android.supportTestLib}") + add("androidTestImplementation", "androidx.test:runner:${Versions.Android.supportTestLib}") + add("androidTestImplementation", "androidx.test:rules:${Versions.Android.supportTestLib}") + add( + "androidTestImplementation", + "androidx.test.espresso:espresso-core:${Versions.Android.espressoTestLib}" + ) +} diff --git a/buildSrc/src/main/kotlin/com/bugsnag/android/BugsnagBuildPlugin.kt b/buildSrc/src/main/kotlin/com/bugsnag/android/BugsnagBuildPlugin.kt deleted file mode 100644 index efbf6784c9..0000000000 --- a/buildSrc/src/main/kotlin/com/bugsnag/android/BugsnagBuildPlugin.kt +++ /dev/null @@ -1,234 +0,0 @@ -package com.bugsnag.android - -import com.android.build.gradle.BaseExtension -import com.android.build.gradle.LibraryExtension -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.kotlin.dsl.apply -import org.gradle.kotlin.dsl.dependencies -import org.jetbrains.kotlin.gradle.dsl.KotlinCompile -import java.io.File - -/** - * A plugin which shares build logic between subprojects. This is the recommended way of - * sharing logic in Gradle builds as it cuts down on repetition, and avoids the pitfalls - * of the `allProjects {}` DSL. - * - * The Android Library plugin must be applied after this plugin. - * - * https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.html - */ -class BugsnagBuildPlugin : Plugin { - - override fun apply(project: Project) { - val bugsnag = project.extensions.create( - "bugsnagBuildOptions", - BugsnagBuildPluginExtension::class.java, - project.objects - ) - - // configure AGP with the information it needs to build the project - project.pluginManager.withPlugin("com.android.library") { - configureProject(project, bugsnag) - } - } - - private fun configureProject( - project: Project, - bugsnag: BugsnagBuildPluginExtension - ) { - // load 3rd party gradle plugins - project.applyPlugins(bugsnag) - - val android = project.extensions.getByType(LibraryExtension::class.java) - android.apply { - configureDefaults() - configureAndroidLint(project) - configureTests() - configureAndroidPackagingOptions() - - if (bugsnag.usesNdk) { - configureNdk(project) - - bugsnag.publishesPrefab?.let { prefabModuleName -> - configurePrefabPublishing(prefabModuleName) - } - } - - bugsnag.androidConfiguration.forEach { config -> - config(android) - } - } - - // add 3rd party dependencies to the project - project.addExternalDependencies() - - // apply legacy groovy scripts to configure 3rd party plugins - project.apply(from = project.file("../gradle/release.gradle")) - project.apply(from = project.file("../gradle/license-check.gradle")) - - if (bugsnag.compilesCode) { - project.configureKotlinOptions() - project.configureCheckstyle() - - project.apply(from = project.file("../gradle/detekt.gradle")) - } - } - - private fun LibraryExtension.configurePrefabPublishing(prefabModuleName: String) { - buildFeatures.prefabPublishing = true - prefab.create(prefabModuleName) { - headers = "src/main/jni/include" - } - } - - /** - * Configures the Android NDK, if it is enabled - */ - private fun BaseExtension.configureNdk(project: Project) { - defaultConfig { - externalNativeBuild.cmake.arguments += listOf( - "-DANDROID_CPP_FEATURES=exceptions", - "-DANDROID_STL=c++_static" - ) - - - val override: String? = project.findProperty("ABI_FILTERS") as String? - val abis = override?.split(",") ?: mutableSetOf( - "arm64-v8a", - "armeabi-v7a", - "x86", - "x86_64" - ) - ndk.setAbiFilters(abis) - } - externalNativeBuild.cmake.path = project.file("CMakeLists.txt") - externalNativeBuild.cmake.version = Versions.cmakeVersion - } - - /** - * Configures the compile and package options for the Android artefacts - */ - private fun BaseExtension.configureAndroidPackagingOptions() { - buildFeatures.apply { - aidl = false - renderScript = false - shaders = false - resValues = false - buildConfig = false - } - - compileOptions { - sourceCompatibility = Versions.java - targetCompatibility = Versions.java - } - } - - /** - * Configures the Android Lint static analysis tool - */ - private fun BaseExtension.configureAndroidLint(project: Project) { - lintOptions { - isAbortOnError = true - isWarningsAsErrors = true - isCheckAllWarnings = true - baseline(File(project.projectDir, "lint-baseline.xml")) - disable("GradleDependency", "NewerVersionAvailable") - } - } - - /** - * Configures options for how the unit test target behaves. - */ - private fun BaseExtension.configureTests() { - testOptions { - unitTests { - isReturnDefaultValues = true - } - } - - sourceSets { - named("test") { - java.srcDir(SHARED_TEST_SRC_DIR) - } - } - } - - /** - * Adds external libraries to the dependencies for each project, such as the Kotlin stdlib. - */ - private fun Project.addExternalDependencies() { - project.dependencies { - // needs to be kept as 'compile' for license checking to work - // as otherwise the downloadLicenses task misses these deps - add("api", "androidx.annotation:annotation:${Versions.supportLib}") - add("api", "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}") - - add("testImplementation", "junit:junit:${Versions.junitTestLib}") - add("testImplementation", "org.mockito:mockito-core:${Versions.mockitoTestLib}") - add("testImplementation", "org.mockito:mockito-inline:${Versions.mockitoTestLib}") - add("testImplementation", "androidx.test:core:${Versions.supportTestLib}") - - add( - "androidTestImplementation", - "org.mockito:mockito-android:${Versions.mockitoTestLib}" - ) - add("androidTestImplementation", "androidx.test:core:${Versions.supportTestLib}") - add("androidTestImplementation", "androidx.test:runner:${Versions.supportTestLib}") - add("androidTestImplementation", "androidx.test:rules:${Versions.supportTestLib}") - add( - "androidTestImplementation", - "androidx.test.espresso:espresso-core:${Versions.espressoTestLib}" - ) - } - } - - private fun Project.configureKotlinOptions() { - tasks.withType(KotlinCompile::class.java).configureEach { - kotlinOptions { - allWarningsAsErrors = true - apiVersion = Versions.kotlinLang - languageVersion = Versions.kotlinLang - freeCompilerArgs += listOf( - "-Xno-call-assertions", - "-Xno-receiver-assertions", - "-Xno-param-assertions" - ) - } - } - } - - /** - * Configures Android project defaults such as minSdkVersion. - */ - private fun BaseExtension.configureDefaults() { - defaultConfig { - setCompileSdkVersion(Versions.compileSdkVersion) - minSdk = Versions.minSdkVersion - ndkVersion = Versions.ndk - consumerProguardFiles("proguard-rules.pro") - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - } - - /** - * Applies gradle plugins to the given project which are always required (e.g. - * kotlin-android). - */ - private fun Project.applyPlugins(bugsnag: BugsnagBuildPluginExtension) { - val plugins = project.plugins - plugins.apply("com.github.hierynomus.license") - - if (bugsnag.compilesCode) { - plugins.apply("checkstyle") - plugins.apply("kotlin-android") - plugins.apply("io.gitlab.arturbosch.detekt") - plugins.apply("org.jlleitschuh.gradle.ktlint") - plugins.apply("org.jetbrains.kotlinx.binary-compatibility-validator") - } - } - - companion object { - private const val SHARED_TEST_SRC_DIR = "../bugsnag-android-core/src/sharedTest/java" - } -} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/com/bugsnag/android/BugsnagBuildPluginExtension.kt b/buildSrc/src/main/kotlin/com/bugsnag/android/BugsnagBuildPluginExtension.kt deleted file mode 100644 index d2637be468..0000000000 --- a/buildSrc/src/main/kotlin/com/bugsnag/android/BugsnagBuildPluginExtension.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.bugsnag.android - -import com.android.build.api.dsl.LibraryExtension -import org.gradle.api.model.ObjectFactory - -/** - * Controls how the Bugsnag Build Plugin should be applied to a given module. This interface - * allows for unwanted behaviour to be switched off - e.g., modules which don't use the NDK - * can disable it. - */ -open class BugsnagBuildPluginExtension(@Suppress("UNUSED_PARAMETER") objects: ObjectFactory) { - - internal val androidConfiguration = ArrayList Unit>() - - /** - * Whether this project compiles code or not. If this is set to false then unnecessary - * plugins are not applied, which speeds up the build. By default this is enabled. - */ - open var compilesCode: Boolean = true - - /** - * Whether the project uses the Android NDK or not. By default this is disabled. - */ - open var usesNdk: Boolean = false - - /** - * The project publishes a native prefab with the specified name. By default this is disabled. - */ - open var publishesPrefab: String? = null - - fun android(config: LibraryExtension.() -> Unit) { - androidConfiguration.add(config) - } -} diff --git a/buildSrc/src/main/kotlin/com/bugsnag/android/Versions.kt b/buildSrc/src/main/kotlin/com/bugsnag/android/Versions.kt deleted file mode 100644 index 0184e19bfc..0000000000 --- a/buildSrc/src/main/kotlin/com/bugsnag/android/Versions.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.bugsnag.android - -import org.gradle.api.JavaVersion - -/** - * Controls the versions of plugins, dependencies, and build targets used by the SDK. - */ -object Versions { - // Note minSdkVersion must be >=21 for 64 bit architectures - val minSdkVersion = 14 - val compileSdkVersion = 34 - val ndk = "23.1.7779620" - val java = JavaVersion.VERSION_1_8 - val kotlin = "1.5.10" - val kotlinLang = "1.5" - val cmakeVersion = "3.22.1" - - // plugins - val androidGradlePlugin = "7.0.4" - val detektPlugin = "1.23.1" - val ktlintPlugin = "10.2.0" - val dokkaPlugin = "1.9.0" - val benchmarkPlugin = "1.1.1" - - // dependencies - val supportLib = "1.1.0" - val supportTestLib = "1.2.0" - val espressoTestLib = "3.1.0" - val junitTestLib = "4.13.2" - val mockitoTestLib = "4.11.0" -} \ No newline at end of file diff --git a/dockerfiles/Dockerfile.android-common b/dockerfiles/Dockerfile.android-common index db6b623797..a578127e1b 100644 --- a/dockerfiles/Dockerfile.android-common +++ b/dockerfiles/Dockerfile.android-common @@ -1,4 +1,4 @@ -FROM ubuntu:20.04 +FROM ubuntu:20.04@sha256:8e5c4f0285ecbb4ead070431d29b576a530d3166df73ec44affc1cd27555141b RUN apt-get update > /dev/n RUN DEBIAN_FRONTEND=noninteractive apt-get install -y wget maven gnupg1 cppcheck libncurses5 jq clang-format unzip curl git diff --git a/examples/sdk-app-example/app/build.gradle b/examples/sdk-app-example/app/build.gradle index 604e26a0f0..aa8b9c7f9d 100644 --- a/examples/sdk-app-example/app/build.gradle +++ b/examples/sdk-app-example/app/build.gradle @@ -42,8 +42,8 @@ android { } dependencies { - implementation "com.bugsnag:bugsnag-android:6.10.0" - implementation "com.bugsnag:bugsnag-plugin-android-okhttp:6.10.0" + implementation "com.bugsnag:bugsnag-android:6.11.0" + implementation "com.bugsnag:bugsnag-plugin-android-okhttp:6.11.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "androidx.appcompat:appcompat:1.6.1" implementation "com.google.android.material:material:1.11.0" diff --git a/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/ExampleApplication.kt b/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/ExampleApplication.kt index 668ef475c0..e43650bba6 100644 --- a/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/ExampleApplication.kt +++ b/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/ExampleApplication.kt @@ -50,4 +50,4 @@ class ExampleApplication : Application() { performNativeBugsnagSetup() } -} +} \ No newline at end of file diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/BugsnagConfig.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/BugsnagConfig.kt index 7a594a9f78..a1bdbc5244 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/BugsnagConfig.kt +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/BugsnagConfig.kt @@ -22,7 +22,9 @@ fun prepareConfig( ): Configuration { val config = Configuration(apiKey) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - config.addPlugin(BugsnagExitInfoPlugin()) + config.addPlugin( + BugsnagExitInfoPlugin() + ) } if (notify.isNotEmpty() && sessions.isNotEmpty()) { diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/MultiThreadedStartupScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/MultiThreadedStartupScenario.kt deleted file mode 100644 index aea3d67d10..0000000000 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/MultiThreadedStartupScenario.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.bugsnag.android.mazerunner.scenarios - -import android.content.Context -import com.bugsnag.android.Bugsnag -import com.bugsnag.android.Configuration -import kotlin.concurrent.thread - -class MultiThreadedStartupScenario( - config: Configuration, - context: Context, - eventMetadata: String? -) : Scenario(config, context, eventMetadata) { - override fun startBugsnag(startBugsnagOnly: Boolean) {} - - override fun startScenario() { - val startThread = thread(name = "AsyncStart") { - measureBugsnagStartupDuration(context, config) - } - - thread(name = "leaveBreadcrumb") { - // simulate the start of some startup work, but not enough for Bugsnag.start to complete - Thread.sleep(1L) - try { - Bugsnag.leaveBreadcrumb("I'm leaving a breadcrumb on another thread") - Bugsnag.notify(Exception("Scenario complete")) - } catch (e: Exception) { - measureBugsnagStartupDuration(context, config) - Bugsnag.notify(e) - } - } - - // make sure we wait before returning - startThread.join() - } -} diff --git a/features/full_tests/threaded_startup.feature b/features/full_tests/threaded_startup.feature deleted file mode 100644 index ab383ea02f..0000000000 --- a/features/full_tests/threaded_startup.feature +++ /dev/null @@ -1,10 +0,0 @@ -Feature: Switching automatic error detection on/off for Unity - - Background: - Given I clear all persistent data - - Scenario: Starting Bugsnag & calling it on separate threads - When I run "MultiThreadedStartupScenario" and relaunch the crashed app - And I configure Bugsnag for "MultiThreadedStartupScenario" - Then I wait to receive an error - And the error is correct for "MultiThreadedStartupScenario" or I allow a retry diff --git a/features/smoke_tests/04_unhandled.feature b/features/smoke_tests/04_unhandled.feature index 73158a07c4..23c7f5f372 100644 --- a/features/smoke_tests/04_unhandled.feature +++ b/features/smoke_tests/04_unhandled.feature @@ -214,6 +214,16 @@ Feature: Unhandled smoke tests And the event "metaData.opaque.array.1" equals "b" And the event "metaData.opaque.array.2" equals "c" + + @skip_below_android_12 + Scenario: Signal raised with exit info + When I set the screen orientation to portrait + And I run "CXXSignalSmokeScenario" and relaunch the crashed app + And I configure Bugsnag for "CXXSignalSmokeScenario" + And I wait to receive an error + And the event "metaData.Open FileDescriptors" is not null + And the event "metaData.app.exitReason" equals "crash native" + @debug-safe Scenario: C++ exception thrown with overwritten config When I set the screen orientation to portrait diff --git a/features/support/env.rb b/features/support/env.rb index d2eff5d3a0..df8670cf39 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -40,6 +40,10 @@ skip_this_scenario("Skipping scenario") if Maze.config.os_version < 11 end +Before('@skip_below_android_12') do |scenario| + skip_this_scenario("Skipping scenario") if Maze.config.os_version < 12 +end + Before('@skip_below_android_9') do |scenario| skip_this_scenario("Skipping scenario") if Maze.config.os_version < 9 end diff --git a/gradle.properties b/gradle.properties index f60eadcec2..ca0958c82b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ org.gradle.jvmargs=-Xmx4096m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects org.gradle.parallel=true -VERSION_NAME=6.10.0 +VERSION_NAME=6.11.0 GROUP=com.bugsnag POM_SCM_URL=https://github.com/bugsnag/bugsnag-android POM_SCM_CONNECTION=scm:git@github.com:bugsnag/bugsnag-android.git diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh new file mode 100755 index 0000000000..08fc9566d3 --- /dev/null +++ b/scripts/bump-version.sh @@ -0,0 +1,28 @@ +#!/bin/bash -e + +BRANCH=$(git rev-parse --abbrev-ref HEAD) + +if [[ "$1" != "" ]]; then + VERSION=$1 +elif [[ "$BRANCH" =~ ^release/v.*$ ]]; then + VERSION=${BRANCH#release/v} +else + echo "Error: Current branch '$BRANCH' does not appear to be a release branch." + echo "Please specify VERSION manually:" + echo "$(basename $0) " + exit 1 +fi + +if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: VERSION '$VERSION' is not in a valid format (e.g., 1.2.3)." + exit 1 +fi + +echo Bumping the version number to $VERSION +sed -i '' "s/bugsnag-android:.*\"/bugsnag-android:$VERSION\"/" examples/sdk-app-example/app/build.gradle +sed -i '' "s/bugsnag-plugin-android-okhttp:.*\"/bugsnag-plugin-android-okhttp:$VERSION\"/" examples/sdk-app-example/app/build.gradle +sed -i '' "s/VERSION_NAME=.*/VERSION_NAME=$VERSION/" gradle.properties +sed -i '' "s/var version: String = .*/var version: String = \"$VERSION\",/"\ + bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt +sed -i '' "s/## TBD/## $VERSION ($(date '+%Y-%m-%d'))/" CHANGELOG.md + diff --git a/scripts/copy-build-files.rb b/scripts/copy-build-files.rb index c2bc1107c4..e5a7928ab2 100755 --- a/scripts/copy-build-files.rb +++ b/scripts/copy-build-files.rb @@ -8,6 +8,8 @@ FileUtils.mkdir_p destination FileUtils.cp "features/fixtures/mazerunner/app/build/outputs/apk/#{build_mode}/fixture-#{ndk_version}.apk", "build/fixture-#{ndk_version}.apk" +mapping_txt = "features/fixtures/mazerunner/app/build/outputs/mapping/#{build_mode}/mapping.txt" +FileUtils.cp mapping_txt, "#{destination}/mapping.txt" if File.exist? mapping_txt fixture_dir = 'features/fixtures/mazerunner' cxx_base = "#{fixture_dir}/cxx-scenarios/build/intermediates" diff --git a/settings.gradle.kts b/settings.gradle.kts index a093eafc48..cc2590528a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,11 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} + plugins { id("com.gradle.enterprise") version "3.5" }