Skip to content
Merged
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 @@ -302,6 +302,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 setupLegacyBuildIdRequired(String booleanValue) {
setupResource(
RES_ID_LEGACY_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 @@ -318,7 +318,8 @@ boolean didCrashOnPreviousExecution() {
// Before the first session of this execution is opened, the current session ID still refers
// to the previous execution's last session, which is what we want.
final String sessionId = getCurrentSessionId();
return sessionId != null && nativeComponent.hasCrashDataForSession(sessionId);
return sessionId != null
&& (nativeComponent.hasCrashDataForSession(sessionId) || hasAnrForSession(sessionId));
}

Logger.getLogger().v("Found previous crash marker.");
Expand Down Expand Up @@ -943,5 +944,17 @@ private void writeApplicationExitInfoEventIfRelevant(String sessionId) {
.v("ANR feature enabled, but device is API " + android.os.Build.VERSION.SDK_INT);
}
}

private boolean hasAnrForSession(String sessionId) {
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
ActivityManager activityManager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ApplicationExitInfo> applicationExitInfoList =
activityManager.getHistoricalProcessExitReasons(null, 0, 0);
return reportingCoordinator.didRelevantAnrOccur(sessionId, applicationExitInfoList);
}
return false;
}

// endregion
}
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 @@ -47,6 +49,7 @@
import java.util.Collections;
import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand Down Expand Up @@ -133,6 +136,45 @@ public void testDoCloseSession_enabledAnrs_persistsAppExitInfoIfItExists() throw
any(UserMetadata.class));
}

@Test
public void testDidCrashOnPreviousExecution_relevantAnr_returnsTrue() throws Exception {
final String previousSessionId = "previousSessionId";
final CrashlyticsController controller = createController();
addAppExitInfo(ApplicationExitInfo.REASON_ANR);

when(mockSessionReportingCoordinator.listSortedOpenSessionIds())
.thenReturn(new TreeSet<>(Collections.singletonList(previousSessionId)));
when(mockSessionReportingCoordinator.didRelevantAnrOccur(eq(previousSessionId), any()))
.thenReturn(true);

AtomicBoolean didCrashOnPrevious = new AtomicBoolean(false);
crashlyticsWorkers.common.submit(
() -> didCrashOnPrevious.set(controller.didCrashOnPreviousExecution()));
// cannot use await since it check preconditions if blocking main thread
Thread.sleep(100);
Comment thread
jrodiz marked this conversation as resolved.

assertTrue(didCrashOnPrevious.get());
}

@Test
public void testDidCrashOnPreviousExecution_noRelevantAnr_returnsFalse() throws Exception {
final String previousSessionId = "previousSessionId";
final CrashlyticsController controller = createController();
addAppExitInfo(ApplicationExitInfo.REASON_EXIT_SELF);

when(mockSessionReportingCoordinator.listSortedOpenSessionIds())
.thenReturn(new TreeSet<>(Collections.singletonList(previousSessionId)));
// didRelevantAnrOccur returns false by default for the mock.

AtomicBoolean didCrashOnPrevious = new AtomicBoolean(false);
crashlyticsWorkers.common.submit(
() -> didCrashOnPrevious.set(controller.didCrashOnPreviousExecution()));
// cannot use await since it check preconditions if blocking main thread
Thread.sleep(100);

assertFalse(didCrashOnPrevious.get());
}

@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