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
- [feature] Added `didANRKillOnPreviousExecution()` to `FirebaseCrashlytics`, allowing apps to
detect whether they were killed by an ANR in the previous run. Requires API level 30 (Android R)
or above; always returns `false` on older versions. [#4201]

# 20.0.5

Expand Down
1 change: 1 addition & 0 deletions firebase-crashlytics/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.google.firebase.crashlytics {
public class FirebaseCrashlytics {
method public com.google.android.gms.tasks.Task<java.lang.Boolean!> checkForUnsentReports();
method public void deleteUnsentReports();
method public boolean didANRKillOnPreviousExecution();
method public boolean didCrashOnPreviousExecution();
method public static com.google.firebase.crashlytics.FirebaseCrashlytics getInstance();
method public boolean isCrashlyticsCollectionEnabled();
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, didANROnPreviousExecution
// should return false.
final CrashlyticsCore crashlyticsCore = builder().build();
setupBuildIdRequired(String.valueOf(false));
setupAppData(BUILD_ID);

assertTrue(crashlyticsCore.onPreExecute(appData, mockSettingsController));
assertFalse(crashlyticsCore.didANROnPreviousExecution());
}

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 @@ -462,6 +462,20 @@ public boolean didCrashOnPreviousExecution() {
return core.didCrashOnPreviousExecution();
}

/**
* Checks whether the app was terminated due to an ANR (Application Not Responding) on its
* previous run. Requires Android API 30 (R) or above; returns {@code false} on older versions.
*
* The result is computed lazily on the first call and cached, so the first invocation may
* briefly block the calling thread (up to a few seconds) while the system is queried. Call from
* a background thread if responsiveness matters.
*
* @return true if an ANR was recorded during the previous run of the app.
*/
public boolean didANRKillOnPreviousExecution() {
return core.didANROnPreviousExecution();
}

/**
* Indicates whether or not automatic data collection is enabled.
*
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);

// Captured before the previous session is finalized, so didANROnPreviousExecution() can
// still resolve the correct session ID after the user calls it later.
@Nullable private volatile String previousExecutionSessionId;

CrashlyticsController(
Context context,
IdManager idManager,
Expand Down Expand Up @@ -943,5 +947,39 @@ private void writeApplicationExitInfoEventIfRelevant(String sessionId) {
.v("ANR feature enabled, but device is API " + android.os.Build.VERSION.SDK_INT);
}
}

/**
* Captures the previous run's session ID on the common worker so {@link
* #didANROnPreviousExecution} can resolve it later. SDK-internal: the call site (currently
* {@link CrashlyticsCore#onPreExecute}) must queue this before {@link #enableExceptionHandling}
* (which opens a new session) and before {@link #finalizeSessions} (which removes the previous
* one); after either runs, {@link #getCurrentSessionId} no longer refers to the previous run.
*/
void capturePreviousExecutionSessionId() {
crashlyticsWorkers.common.submit(() -> previousExecutionSessionId = getCurrentSessionId());
}

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

// endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ public class CrashlyticsCore {
private CrashlyticsFileMarker crashMarker;
private boolean didCrashOnPreviousExecution;

private final Object didANROnPreviousExecutionLock = new Object();
@Nullable private volatile Boolean didANROnPreviousExecutionCached;

private CrashlyticsController controller;
private final IdManager idManager;
private final FileStore fileStore;
Expand Down Expand Up @@ -197,6 +200,10 @@ public boolean onPreExecute(AppData appData, SettingsProvider settingsProvider)

checkForPreviousCrash();

// Must run before enableExceptionHandling and finalizeSessions, since both alter the
// set of open sessions. didANROnPreviousExecution() uses the captured ID later.
controller.capturePreviousExecutionSessionId();

controller.enableExceptionHandling(
sessionIdentifier, Thread.getDefaultUncaughtExceptionHandler(), settingsProvider);

Expand Down Expand Up @@ -502,6 +509,33 @@ public boolean didCrashOnPreviousExecution() {
return didCrashOnPreviousExecution;
}

public boolean didANROnPreviousExecution() {
Boolean cached = didANROnPreviousExecutionCached;
if (cached != null) {
return cached;
}
synchronized (didANROnPreviousExecutionLock) {
if (didANROnPreviousExecutionCached != null) {
return didANROnPreviousExecutionCached;
}
Future<Boolean> future =
crashlyticsWorkers
.common
.getExecutor()
.submit(() -> controller.didANROnPreviousExecution());

Boolean result;
try {
result = future.get(DEFAULT_MAIN_HANDLER_TIMEOUT_SEC, TimeUnit.SECONDS);
} catch (Exception ex) {
Logger.getLogger().v("Error checking for previous ANR: " + ex.getMessage());
result = false;
}
didANROnPreviousExecutionCached = Boolean.TRUE.equals(result);
return didANROnPreviousExecutionCached;
}
}

// endregion

// region Static utilities
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 @@ -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