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/)
+
+
+
+
+
+
+
+
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"
}