diff --git a/bugsnag-plugin-android-exitinfo/detekt-baseline.xml b/bugsnag-plugin-android-exitinfo/detekt-baseline.xml
index 43db92ae23..741c10dd01 100644
--- a/bugsnag-plugin-android-exitinfo/detekt-baseline.xml
+++ b/bugsnag-plugin-android-exitinfo/detekt-baseline.xml
@@ -28,6 +28,7 @@
MaxLineLength:TraceParserNativeStackframeTest.kt$TraceParserNativeStackframeTest.Companion$"void* std::__1::__thread_proxy<std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, android::uirenderer::CommonPool::CommonPool()::\$_0> >(void*) (.__uniq.99815402873434996937524029735804459536)"
MaxLineLength:TraceParserTest.kt$TraceParserTest$"void* std::__1::__thread_proxy<std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, void (android::AsyncWorker::*)(), android::AsyncWorker*> >(void*)"
NestedBlockDepth:TraceParser.kt$TraceParser$private fun parseThreadAttributes(line: String)
+ ReturnCount:EventSynthesizer.kt$EventSynthesizer$fun createEventWithExitInfo(appExitInfo: ApplicationExitInfo): Event?
ReturnCount:TraceParser.kt$TraceParser$@VisibleForTesting internal fun parseNativeFrame(line: String): Stackframe?
SwallowedException:BugsnagExitInfoPlugin.kt$BugsnagExitInfoPlugin$e: Exception
SwallowedException:ExitInfoCallback.kt$ExitInfoCallback$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
index a9252572f4..881a6c3af6 100644
--- 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
@@ -4,7 +4,7 @@ import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.bugsnag.android.internal.ImmutableConfig
import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNull
+import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Test
import java.io.File
@@ -71,7 +71,7 @@ internal class BugsnagExitInfoPluginStoreTest {
fun writableFileWithEmptyExitInfo() {
exitInfoPluginStore.persist(12345, emptySet())
val firstPid = exitInfoPluginStore.load().first
- assertNull(firstPid)
+ assertNotNull(firstPid)
exitInfoPluginStore = ExitInfoPluginStore(immutableConfig)
val (storedPid, storageExitInfoKeys) = exitInfoPluginStore.load()
assertEquals(12345, storedPid)
@@ -85,8 +85,8 @@ internal class BugsnagExitInfoPluginStoreTest {
exitInfoPluginStore.persist(expectedPid, expectedExitInfoKeys)
val (storedPid, storageExitInfoKeys) = exitInfoPluginStore.load()
- assertNull(storedPid)
- assertEquals(emptySet(), storageExitInfoKeys)
+ assertNotNull(storedPid)
+ assertEquals(expectedExitInfoKeys, storageExitInfoKeys)
exitInfoPluginStore = ExitInfoPluginStore(immutableConfig)
val (storedPid2, storageExitInfoKeys2) = exitInfoPluginStore.load()
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 cd5a30eb57..915c933135 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
@@ -14,6 +14,9 @@ class BugsnagExitInfoPlugin @JvmOverloads constructor(
private val configuration = configuration.copy()
+ private var reportUnmatchedAnrs: Boolean = true
+ private var reportUnmatchedNativeCrashes: Boolean = true
+
@SuppressLint("VisibleForTests")
override fun load(client: Client) {
if (!configuration.disableProcessStateSummaryOverride) {
@@ -38,6 +41,7 @@ class BugsnagExitInfoPlugin @JvmOverloads constructor(
val exitInfoPluginStore =
ExitInfoPluginStore(client.immutableConfig)
+ addAllExitInfoAtFirstRun(client, exitInfoPluginStore)
val (oldPid, exitInfoKeys) = exitInfoPluginStore.load()
exitInfoPluginStore.persist(android.os.Process.myPid(), exitInfoKeys)
@@ -59,6 +63,22 @@ class BugsnagExitInfoPlugin @JvmOverloads constructor(
client.addOnSend(exitInfoCallback)
}
+ private fun addAllExitInfoAtFirstRun(
+ client: Client,
+ exitInfoPluginStore: ExitInfoPluginStore
+ ) {
+ if (exitInfoPluginStore.isFirstRun || exitInfoPluginStore.legacyStore) {
+ val am: ActivityManager =
+ client.appContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ val allExitInfo: List =
+ am.getHistoricalProcessExitReasons(client.appContext.packageName, 0, 100)
+
+ allExitInfo.forEach { exitInfo ->
+ exitInfoPluginStore.addExitInfoKey(ExitInfoKey(exitInfo.pid, exitInfo.timestamp))
+ }
+ }
+ }
+
private fun createExitInfoCallback(
client: Client,
oldPid: Int?,
@@ -82,7 +102,9 @@ class BugsnagExitInfoPlugin @JvmOverloads constructor(
val eventSynthesizer = EventSynthesizer(
traceEventEnhancer,
tombstoneEventEnhancer,
- exitInfoPluginStore
+ exitInfoPluginStore,
+ reportUnmatchedAnrs,
+ reportUnmatchedNativeCrashes
)
val context = client.appContext
val am: ActivityManager = context.safeGetActivityManager() ?: return
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
index 55e33b525f..6dd6e9d634 100644
--- 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
@@ -13,6 +13,8 @@ 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_NATIVE
import android.os.Build
import androidx.annotation.RequiresApi
@@ -20,7 +22,9 @@ import androidx.annotation.RequiresApi
internal class EventSynthesizer(
private val anrEventEnhancer: (Event, ApplicationExitInfo) -> Unit,
private val nativeEnhancer: (Event, ApplicationExitInfo) -> Unit,
- private val exitInfoPluginStore: ExitInfoPluginStore
+ private val exitInfoPluginStore: ExitInfoPluginStore,
+ private val reportUnmatchedAnrs: Boolean,
+ private val reportUnmatchedNativeCrashes: Boolean
) {
fun createEventWithExitInfo(appExitInfo: ApplicationExitInfo): Event? {
val (_, knownExitInfoKeys) = exitInfoPluginStore.load()
@@ -31,31 +35,37 @@ internal class EventSynthesizer(
)
when (appExitInfo.reason) {
- ApplicationExitInfo.REASON_ANR
+ REASON_ANR
-> {
- val newAnrEvent = InternalHooks.createEmptyANR(exitInfoKey.timestamp)
- addExitInfoMetadata(newAnrEvent, appExitInfo)
- anrEventEnhancer(newAnrEvent, appExitInfo)
- val thread =
- newAnrEvent.threads.find { it.name == "main" }
- ?: newAnrEvent.threads.firstOrNull()
- val error = newAnrEvent.addError("ANR", appExitInfo.description)
- thread?.let { error.stacktrace.addAll(it.stacktrace) }
+ if (reportUnmatchedAnrs) {
+ val newAnrEvent = InternalHooks.createEmptyANR(exitInfoKey.timestamp)
+ addExitInfoMetadata(newAnrEvent, appExitInfo)
+ anrEventEnhancer(newAnrEvent, appExitInfo)
+ val thread =
+ newAnrEvent.threads.find { it.name == "main" }
+ ?: newAnrEvent.threads.firstOrNull()
+ val error = newAnrEvent.addError("ANR", appExitInfo.description)
+ thread?.let { error.stacktrace.addAll(it.stacktrace) }
- return newAnrEvent
+ return newAnrEvent
+ }
+ return null
}
- ApplicationExitInfo.REASON_CRASH_NATIVE
+ REASON_CRASH_NATIVE
-> {
- val newNativeEvent = InternalHooks.createEmptyCrash(exitInfoKey.timestamp)
- addExitInfoMetadata(newNativeEvent, appExitInfo)
- nativeEnhancer(newNativeEvent, appExitInfo)
- val thread =
- newNativeEvent.threads.find { it.name == "main" }
- ?: newNativeEvent.threads.firstOrNull()
- val error = newNativeEvent.addError("Native", appExitInfo.description)
- thread?.let { error.stacktrace.addAll(it.stacktrace) }
- return newNativeEvent
+ if (reportUnmatchedNativeCrashes) {
+ val newNativeEvent = InternalHooks.createEmptyCrash(exitInfoKey.timestamp)
+ addExitInfoMetadata(newNativeEvent, appExitInfo)
+ nativeEnhancer(newNativeEvent, appExitInfo)
+ val thread =
+ newNativeEvent.threads.find { it.name == "main" }
+ ?: newNativeEvent.threads.firstOrNull()
+ val error = newNativeEvent.addError("Native", appExitInfo.description)
+ thread?.let { error.stacktrace.addAll(it.stacktrace) }
+ return newNativeEvent
+ }
+ return null
}
else -> return null
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 b704a6de2d..6c401f851d 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
@@ -49,8 +49,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
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 493ec38650..c949977e31 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
@@ -10,7 +10,10 @@ 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()
- private val isFirstRun: Boolean = !file.exists()
+ internal val isFirstRun: Boolean = !file.exists()
+
+ internal var legacyStore: Boolean = false
+ private set
fun persist(currentPid: Int, exitInfoKeys: Set) {
lock.writeLock().withLock {
@@ -31,9 +34,6 @@ internal class ExitInfoPluginStore(config: ImmutableConfig) {
}
fun load(): Pair> {
- if (isFirstRun) {
- return null to emptySet()
- }
return tryLoadJson() ?: (tryLoadLegacy() to emptySet())
}
@@ -62,6 +62,7 @@ internal class ExitInfoPluginStore(config: ImmutableConfig) {
private fun tryLoadLegacy(): Int? {
try {
val content = file.readText()
+ legacyStore = true
if (content.isEmpty()) {
logger.w("PID is empty")
return null
@@ -75,7 +76,7 @@ internal class ExitInfoPluginStore(config: ImmutableConfig) {
fun addExitInfoKey(exitInfoKey: ExitInfoKey) {
val (oldPid, exitInfoKeys) = load()
- val newExitInfoKeys = exitInfoKeys.toMutableSet().plus(exitInfoKey)
+ val newExitInfoKeys = exitInfoKeys + exitInfoKey
oldPid?.let { persist(it, newExitInfoKeys) }
}
}
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..c06f4fb295 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
@@ -1,15 +1,23 @@
package com.example.bugsnag.android
+import android.app.ActivityManager
import android.app.Application
+import android.app.ApplicationExitInfo
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
import com.bugsnag.android.Bugsnag
+import com.bugsnag.android.BugsnagExitInfoPlugin
import com.bugsnag.android.Configuration
import com.bugsnag.android.okhttp.BugsnagOkHttpPlugin
import okhttp3.OkHttpClient
import java.io.File
+@RequiresApi(Build.VERSION_CODES.R)
class ExampleApplication : Application() {
private val bugsnagOkHttpPlugin = BugsnagOkHttpPlugin()
+ private val exitInfoPlugin = BugsnagExitInfoPlugin()
val httpClient = OkHttpClient.Builder()
.eventListener(bugsnagOkHttpPlugin)
.build()
@@ -36,6 +44,36 @@ class ExampleApplication : Application() {
config.setUser("123456", "joebloggs@example.com", "Joe Bloggs")
config.addMetadata("user", "age", 31)
config.addPlugin(bugsnagOkHttpPlugin)
+ config.addPlugin(exitInfoPlugin)
+
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ (getSystemService(ACTIVITY_SERVICE) as ActivityManager)
+ .getHistoricalProcessExitReasons(packageName, 0, 1000).forEach { r ->
+ val reason = when(r.reason) {
+ ApplicationExitInfo.REASON_UNKNOWN -> "unknown reason (${r.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 (${r.reason})"
+ }
+ Log.i("ExitReason", "${r.description} / $reason")
+ }}
+
+
// Configure the persistence directory when running MultiProcessActivity in a separate
// process to ensure the two Bugsnag clients are independent
@@ -50,4 +88,4 @@ class ExampleApplication : Application() {
performNativeBugsnagSetup()
}
-}
+}
\ No newline at end of file
diff --git a/features/smoke_tests/04_unhandled.feature b/features/smoke_tests/04_unhandled.feature
index 6eb54cffb8..041c29d7e4 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