diff --git a/firebase-crashlytics/CHANGELOG.md b/firebase-crashlytics/CHANGELOG.md index 2c75af37754..1af84131def 100644 --- a/firebase-crashlytics/CHANGELOG.md +++ b/firebase-crashlytics/CHANGELOG.md @@ -5,6 +5,9 @@ - [fixed] Fixed race condition that caused logs from background threads to not be attached to reports in some cases [#8034] - [changed] Updated `firebase-sessions` dependency to v3.0.6 +- [changed] `didCrashOnPreviousExecution()` now also returns `true` when the previous run ended + with an ANR (Application Not Responding), in addition to JVM and native crashes. ANR detection + requires API level 30 (Android R) or above. [#4201] # 20.0.5 diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCoreInitializationTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCoreInitializationTest.java index 9110bdb2588..9eb04b9737c 100644 --- a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCoreInitializationTest.java +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCoreInitializationTest.java @@ -265,6 +265,18 @@ public void testOnPreExecute_didNotCrashOnPreviousExecution() { assertFalse(getCrashMarkerFile().exists()); } + @Test + public void testOnPreExecute_didNotANROnPreviousExecution() { + // Without any ApplicationExitInfo entries indicating an ANR, didCrashOnPreviousExecution + // should return false. + final CrashlyticsCore crashlyticsCore = builder().build(); + setupBuildIdRequired(String.valueOf(false)); + setupAppData(BUILD_ID); + + assertTrue(crashlyticsCore.onPreExecute(appData, mockSettingsController)); + assertFalse(crashlyticsCore.didCrashOnPreviousExecution()); + } + private void setupBuildIdRequired(String booleanValue) { setupResource( RES_ID_REQUIRE_BUILD_ID, diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/FirebaseCrashlytics.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/FirebaseCrashlytics.java index 46f992aaf12..178d13ff3bc 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/FirebaseCrashlytics.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/FirebaseCrashlytics.java @@ -454,9 +454,11 @@ public void deleteUnsentReports() { // endregion /** - * Checks whether the app crashed on its previous run. + * Checks whether the app crashed on its previous run. Returns {@code true} for JVM crashes, + * native crashes, and ANRs (Application Not Responding). ANR detection requires Android API 30 + * (R) or above. * - * @return true if a crash was recorded during the previous run of the app. + * @return true if a crash or ANR was recorded during the previous run of the app. */ public boolean didCrashOnPreviousExecution() { return core.didCrashOnPreviousExecution(); diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java index b464819e18c..6e7fbdf4146 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java @@ -119,6 +119,10 @@ class CrashlyticsController { // A token to make sure that checkForUnsentReports only gets called once. final AtomicBoolean checkForUnsentReportsCalled = new AtomicBoolean(false); + // Set during initialization's session finalization when the previous session ended with an ANR, + // so didCrashOnPreviousExecution() reports ANRs alongside JVM and native crashes. + private volatile boolean didPreviousExecutionEndWithAnr = false; + CrashlyticsController( Context context, IdManager idManager, @@ -930,6 +934,11 @@ private void writeApplicationExitInfoEventIfRelevant(String sessionId) { // Passes the latest applicationExitInfo to ReportCoordinator, which persists it if it // happened during the session. if (applicationExitInfoList.size() != 0) { + // Record whether the previous session ended with an ANR so didCrashOnPreviousExecution() + // can report it, without re-querying the system or blocking the main thread. + if (reportingCoordinator.didRelevantAnrOccur(sessionId, applicationExitInfoList)) { + didPreviousExecutionEndWithAnr = true; + } final LogFileManager relevantSessionLogManager = new LogFileManager(fileStore, sessionId); final UserMetadata relevantUserMetadata = UserMetadata.loadFromExistingSession(sessionId, fileStore, crashlyticsWorkers); @@ -943,5 +952,11 @@ private void writeApplicationExitInfoEventIfRelevant(String sessionId) { .v("ANR feature enabled, but device is API " + android.os.Build.VERSION.SDK_INT); } } + + /** Whether the previous execution ended with an ANR, detected during session finalization. */ + boolean didPreviousExecutionEndWithAnr() { + return didPreviousExecutionEndWithAnr; + } + // endregion } diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCore.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCore.java index f8ea00c767d..aef6b53bc54 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCore.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCore.java @@ -499,7 +499,8 @@ private void checkForPreviousCrash() { } public boolean didCrashOnPreviousExecution() { - return didCrashOnPreviousExecution; + return didCrashOnPreviousExecution + || (controller != null && controller.didPreviousExecutionEndWithAnr()); } // endregion diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinator.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinator.java index 50533be05b1..d7bbc12304b 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinator.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinator.java @@ -439,6 +439,13 @@ public static String convertInputStreamToString(InputStream inputStream) throws } } + /** Returns true if an ANR ApplicationExitInfo occurred during the given session. */ + @RequiresApi(api = Build.VERSION_CODES.R) + public boolean didRelevantAnrOccur( + String sessionId, List applicationExitInfoList) { + return findRelevantApplicationExitInfo(sessionId, applicationExitInfoList) != null; + } + /** Finds the first ANR ApplicationExitInfo within the session. */ @RequiresApi(api = Build.VERSION_CODES.R) private @Nullable ApplicationExitInfo findRelevantApplicationExitInfo( diff --git a/firebase-crashlytics/src/test/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerRobolectricTest.java b/firebase-crashlytics/src/test/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerRobolectricTest.java index ed21115c075..e76b963dd9e 100644 --- a/firebase-crashlytics/src/test/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerRobolectricTest.java +++ b/firebase-crashlytics/src/test/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerRobolectricTest.java @@ -15,6 +15,8 @@ package com.google.firebase.crashlytics.internal.common; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -133,6 +135,47 @@ public void testDoCloseSession_enabledAnrs_persistsAppExitInfoIfItExists() throw any(UserMetadata.class)); } + @Test + public void testFinalizeSessions_relevantAnr_setsDidPreviousExecutionEndWithAnr() + throws Exception { + final String sessionIdPrevious = "sessionIdPrevious"; + final String sessionId = "sessionId"; + final CrashlyticsController controller = createController(); + addAppExitInfo(ApplicationExitInfo.REASON_ANR); + List testApplicationExitInfo = getApplicationExitInfoList(); + + when(mockSessionReportingCoordinator.listSortedOpenSessionIds()) + .thenReturn(new TreeSet<>(Arrays.asList(sessionId, sessionIdPrevious))); + when(mockSessionReportingCoordinator.didRelevantAnrOccur( + eq(sessionIdPrevious), eq(testApplicationExitInfo))) + .thenReturn(true); + mockSettingsProvider(true, false); + crashlyticsWorkers.common.submit(() -> controller.finalizeSessions(mockSettingsProvider)); + // cannot use await since it check preconditions if blocking main thread + Thread.sleep(100); + + assertTrue(controller.didPreviousExecutionEndWithAnr()); + } + + @Test + public void testFinalizeSessions_noRelevantAnr_leavesDidPreviousExecutionEndWithAnrFalse() + throws Exception { + final String sessionIdPrevious = "sessionIdPrevious"; + final String sessionId = "sessionId"; + final CrashlyticsController controller = createController(); + addAppExitInfo(ApplicationExitInfo.REASON_EXIT_SELF); + + when(mockSessionReportingCoordinator.listSortedOpenSessionIds()) + .thenReturn(new TreeSet<>(Arrays.asList(sessionId, sessionIdPrevious))); + // didRelevantAnrOccur returns false by default for the mock. + mockSettingsProvider(true, false); + crashlyticsWorkers.common.submit(() -> controller.finalizeSessions(mockSettingsProvider)); + // cannot use await since it check preconditions if blocking main thread + Thread.sleep(100); + + assertFalse(controller.didPreviousExecutionEndWithAnr()); + } + @Test public void testDoCloseSession_disabledAnrs_doesNotPersistsAppExitInfo() throws Exception { final String sessionId = "sessionId"; diff --git a/firebase-crashlytics/src/test/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinatorRobolectricTest.java b/firebase-crashlytics/src/test/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinatorRobolectricTest.java index 67be4920dad..805d2bd483c 100644 --- a/firebase-crashlytics/src/test/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinatorRobolectricTest.java +++ b/firebase-crashlytics/src/test/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinatorRobolectricTest.java @@ -164,6 +164,55 @@ public void testAppExitInfoEvent_notPersistIfAppExitInfoNotAnrButWithinSession() verify(reportPersistence, never()).persistEvent(any(), eq(sessionId), eq(true)); } + @Test + @SdkSuppress(minSdkVersion = VERSION_CODES.R) + public void testDidRelevantAnrOccur_returnsTrueForAnrWithinSession() { + final long sessionStartTimestamp = 0; + final String sessionId = "testSessionId"; + when(reportPersistence.getStartTimestampMillis(sessionId)).thenReturn(sessionStartTimestamp); + + addAppExitInfo(ApplicationExitInfo.REASON_ANR); + List testApplicationExitInfoList = getAppExitInfoList(); + + assertTrue(reportingCoordinator.didRelevantAnrOccur(sessionId, testApplicationExitInfoList)); + } + + @Test + @SdkSuppress(minSdkVersion = VERSION_CODES.R) + public void testDidRelevantAnrOccur_returnsFalseForAnrBeforeSession() { + // ANR timestamp is 0; session starts at 10, so ANR was before the session. + final long sessionStartTimestamp = 10; + final String sessionId = "testSessionId"; + when(reportPersistence.getStartTimestampMillis(sessionId)).thenReturn(sessionStartTimestamp); + + addAppExitInfo(ApplicationExitInfo.REASON_ANR); + List testApplicationExitInfoList = getAppExitInfoList(); + + assertFalse(reportingCoordinator.didRelevantAnrOccur(sessionId, testApplicationExitInfoList)); + } + + @Test + @SdkSuppress(minSdkVersion = VERSION_CODES.R) + public void testDidRelevantAnrOccur_returnsFalseForNonAnrWithinSession() { + final long sessionStartTimestamp = 0; + final String sessionId = "testSessionId"; + when(reportPersistence.getStartTimestampMillis(sessionId)).thenReturn(sessionStartTimestamp); + + addAppExitInfo(ApplicationExitInfo.REASON_EXIT_SELF); + List testApplicationExitInfoList = getAppExitInfoList(); + + assertFalse(reportingCoordinator.didRelevantAnrOccur(sessionId, testApplicationExitInfoList)); + } + + @Test + @SdkSuppress(minSdkVersion = VERSION_CODES.R) + public void testDidRelevantAnrOccur_returnsFalseForEmptyList() { + final String sessionId = "testSessionId"; + when(reportPersistence.getStartTimestampMillis(sessionId)).thenReturn(0L); + + assertFalse(reportingCoordinator.didRelevantAnrOccur(sessionId, List.of())); + } + @Test public void testconvertInputStreamToString_worksSuccessfully() throws IOException { String stackTrace = "-----stacktrace---------";