Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions firebase-crashlytics/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,8 @@ private void checkForPreviousCrash() {
}

public boolean didCrashOnPreviousExecution() {
return didCrashOnPreviousExecution;
return didCrashOnPreviousExecution
|| (controller != null && controller.didPreviousExecutionEndWithAnr());
}
Comment thread
jrodiz marked this conversation as resolved.

// endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApplicationExitInfo> applicationExitInfoList) {
return findRelevantApplicationExitInfo(sessionId, applicationExitInfoList) != null;
}

/** Finds the first ANR ApplicationExitInfo within the session. */
@RequiresApi(api = Build.VERSION_CODES.R)
private @Nullable ApplicationExitInfo findRelevantApplicationExitInfo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<ApplicationExitInfo> 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);
Comment thread
jrodiz marked this conversation as resolved.

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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApplicationExitInfo> 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<ApplicationExitInfo> 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<ApplicationExitInfo> 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---------";
Expand Down
Loading