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