call() throws Exception {
reportingCoordinator.persistFatalEvent(
ex, thread, currentSessionId, timestampSeconds);
- doWriteAppExceptionMarker(time.getTime());
-
- doCloseSessions();
+ doWriteAppExceptionMarker(timestampMillis);
+ doCloseSessions(settingsDataProvider);
doOpenSession();
// If automatic data collection is disabled, we'll need to wait until the next run
@@ -397,14 +397,14 @@ public Void call() throws Exception {
void writeNonFatalException(@NonNull final Thread thread, @NonNull final Throwable ex) {
// Capture and close over the current time, so that we get the exact call time,
// rather than the time at which the task executes.
- final Date time = new Date();
+ final long timestampMillis = System.currentTimeMillis();
backgroundWorker.submit(
new Runnable() {
@Override
public void run() {
if (!isHandlingException()) {
- long timestampSeconds = getTimestampSeconds(time);
+ long timestampSeconds = getTimestampSeconds(timestampMillis);
final String currentSessionId = getCurrentSessionId();
if (currentSessionId == null) {
Logger.getLogger()
@@ -536,8 +536,10 @@ private String getCurrentSessionId() {
*
* This method can not be called while the {@link CrashlyticsCore} settings lock is held. It
* will result in a deadlock!
+ *
+ * @param settingsDataProvider
*/
- boolean finalizeSessions() {
+ boolean finalizeSessions(SettingsDataProvider settingsDataProvider) {
backgroundWorker.checkRunningOnThread();
if (isHandlingException()) {
@@ -547,7 +549,7 @@ boolean finalizeSessions() {
Logger.getLogger().v("Finalizing previously open sessions.");
try {
- doCloseSessions(true);
+ doCloseSessions(true, settingsDataProvider);
} catch (Exception e) {
Logger.getLogger().e("Unable to finalize previously open sessions.", e);
return false;
@@ -578,15 +580,16 @@ private void doOpenSession() {
reportingCoordinator.onBeginSession(sessionIdentifier, startedAtSeconds);
}
- void doCloseSessions() {
- doCloseSessions(false);
+ void doCloseSessions(SettingsDataProvider settingsDataProvider) {
+ doCloseSessions(false, settingsDataProvider);
}
/**
* Not synchronized/locked. Must be executed from the single thread executor service used by this
* class.
*/
- private void doCloseSessions(boolean skipCurrentSession) {
+ private void doCloseSessions(
+ boolean skipCurrentSession, SettingsDataProvider settingsDataProvider) {
final int offset = skipCurrentSession ? 1 : 0;
List sortedOpenSessions = reportingCoordinator.listSortedOpenSessionIds();
@@ -598,6 +601,12 @@ private void doCloseSessions(boolean skipCurrentSession) {
final String mostRecentSessionIdToClose = sortedOpenSessions.get(offset);
+ if (settingsDataProvider.getSettings().getFeaturesData().collectAnrs) {
+ // TODO: Consider writing applicationExitInfo for all sessions instead of just the most recent
+ // sessionId to close.
+ writeApplicationExitInfoEventIfRelevant(mostRecentSessionIdToClose);
+ }
+
if (nativeComponent.hasCrashDataForSession(mostRecentSessionIdToClose)) {
// We only finalize the current session if it's a Java crash, so only finalize native crash
// data when we aren't including current.
@@ -675,11 +684,11 @@ private void finalizePreviousNativeSession(String previousSessionId) {
}
private static long getCurrentTimestampSeconds() {
- return getTimestampSeconds(new Date());
+ return getTimestampSeconds(System.currentTimeMillis());
}
- private static long getTimestampSeconds(Date date) {
- return date.getTime() / 1000;
+ private static long getTimestampSeconds(long timestampMillis) {
+ return timestampMillis / 1000;
}
// region Serialization to protobuf
@@ -872,4 +881,35 @@ static List getNativeSessionFiles(
}
// endregion
+
+ // region ApplicationExitInfo
+
+ /** If an ApplicationExitInfo exists relevant to the session, writes that event. */
+ private void writeApplicationExitInfoEventIfRelevant(String sessionId) {
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ ActivityManager activityManager =
+ (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ // Gets the latest app exit info.
+ List applicationExitInfoList =
+ activityManager.getHistoricalProcessExitReasons(null, 0, 1);
+
+ // Passes the latest applicationExitInfo to ReportCoordinator, which persists it if it
+ // happened during the session.
+ if (applicationExitInfoList.size() != 0) {
+ final LogFileManager relevantSessionLogManager =
+ new LogFileManager(context, logFileDirectoryProvider, sessionId);
+ final UserMetadata relevantUserMetadata = new UserMetadata();
+ relevantUserMetadata.setCustomKeys(new MetaDataStore(getFilesDir()).readKeyData(sessionId));
+ reportingCoordinator.persistAppExitInfoEvent(
+ sessionId,
+ applicationExitInfoList.get(0),
+ relevantSessionLogManager,
+ relevantUserMetadata);
+ }
+ } else {
+ Logger.getLogger()
+ .v("ANR feature enabled, but device is API " + android.os.Build.VERSION.SDK_INT);
+ }
+ }
+ // 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 f3ddc2ec519..9b724fd7a72 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
@@ -227,7 +227,7 @@ private Task doBackgroundInitialization(SettingsDataProvider settingsProvi
new RuntimeException("Collection of crash reports disabled in Crashlytics settings."));
}
- if (!controller.finalizeSessions()) {
+ if (!controller.finalizeSessions(settingsProvider)) {
Logger.getLogger().w("Previous sessions could not be finalized.");
}
diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsReportDataCapture.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsReportDataCapture.java
index f736697ae7b..4da94dbe4e2 100644
--- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsReportDataCapture.java
+++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsReportDataCapture.java
@@ -79,8 +79,8 @@ public CrashlyticsReportDataCapture(
this.stackTraceTrimmingStrategy = stackTraceTrimmingStrategy;
}
- public CrashlyticsReport captureReportData(String identifier, long timestamp) {
- return buildReportData().setSession(populateSessionData(identifier, timestamp)).build();
+ public CrashlyticsReport captureReportData(String identifier, long timestampSeconds) {
+ return buildReportData().setSession(populateSessionData(identifier, timestampSeconds)).build();
}
public Event captureEventData(
@@ -110,6 +110,20 @@ public Event captureEventData(
.build();
}
+ public Event captureAnrEventData(CrashlyticsReport.ApplicationExitInfo applicationExitInfo) {
+ // This is not the orientation of the device at the time of ANR.
+ // It's filtered out when the backend processes it.
+ // TODO: Consider setting it to 0 to mark it as unknown.
+ final int orientation = context.getResources().getConfiguration().orientation;
+
+ return Event.builder()
+ .setType("anr")
+ .setTimestamp(applicationExitInfo.getTimestamp())
+ .setApp(populateEventApplicationData(orientation, applicationExitInfo))
+ .setDevice(populateEventDeviceData(orientation))
+ .build();
+ }
+
private CrashlyticsReport.Builder buildReportData() {
return CrashlyticsReport.builder()
.setSdkVersion(BuildConfig.VERSION_NAME)
@@ -120,9 +134,9 @@ private CrashlyticsReport.Builder buildReportData() {
.setPlatform(REPORT_ANDROID_PLATFORM);
}
- private CrashlyticsReport.Session populateSessionData(String identifier, long timestamp) {
+ private CrashlyticsReport.Session populateSessionData(String identifier, long timestampSeconds) {
return CrashlyticsReport.Session.builder()
- .setStartedAt(timestamp)
+ .setStartedAt(timestampSeconds)
.setIdentifier(identifier)
.setGenerator(GENERATOR)
.setApp(populateSessionApplicationData())
@@ -212,6 +226,18 @@ private Event.Application populateEventApplicationData(
.build();
}
+ private Event.Application populateEventApplicationData(
+ int orientation, CrashlyticsReport.ApplicationExitInfo applicationExitInfo) {
+ boolean isBackground =
+ applicationExitInfo.getImportance() != RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
+
+ return Event.Application.builder()
+ .setBackground(isBackground)
+ .setUiOrientation(orientation)
+ .setExecution(populateExecutionData(applicationExitInfo))
+ .build();
+ }
+
private Event.Device populateEventDeviceData(int orientation) {
final BatteryState battery = BatteryState.get(context);
final Float batteryLevel = battery.getBatteryLevel();
@@ -250,6 +276,15 @@ private Execution populateExecutionData(
.build();
}
+ private Execution populateExecutionData(
+ CrashlyticsReport.ApplicationExitInfo applicationExitInfo) {
+ return Execution.builder()
+ .setAppExitInfo(applicationExitInfo)
+ .setSignal(populateSignalData())
+ .setBinaries(populateBinaryImagesList())
+ .build();
+ }
+
private ImmutableList populateThreadsList(
TrimmedThrowableData trimmedEvent,
Thread eventThread,
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 e60c565b1fc..2d65a89760b 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
@@ -14,9 +14,13 @@
package com.google.firebase.crashlytics.internal.common;
+import android.app.ApplicationExitInfo;
import android.content.Context;
+import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.crashlytics.internal.Logger;
@@ -30,7 +34,14 @@
import com.google.firebase.crashlytics.internal.send.DataTransportCrashlyticsReportSender;
import com.google.firebase.crashlytics.internal.settings.SettingsDataProvider;
import com.google.firebase.crashlytics.internal.stacktrace.StackTraceTrimmingStrategy;
+import java.io.BufferedReader;
import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -88,8 +99,9 @@ public static SessionReportingCoordinator create(
}
@Override
- public void onBeginSession(@NonNull String sessionId, long timestamp) {
- final CrashlyticsReport capturedReport = dataCapture.captureReportData(sessionId, timestamp);
+ public void onBeginSession(@NonNull String sessionId, long timestampSeconds) {
+ final CrashlyticsReport capturedReport =
+ dataCapture.captureReportData(sessionId, timestampSeconds);
reportPersistence.persistReport(capturedReport);
}
@@ -121,6 +133,34 @@ public void persistNonFatalEvent(
persistEvent(event, thread, sessionId, EVENT_TYPE_LOGGED, timestamp, false);
}
+ @RequiresApi(api = Build.VERSION_CODES.R)
+ public void persistAppExitInfoEvent(
+ String sessionId,
+ ApplicationExitInfo applicationExitInfo,
+ LogFileManager logFileManagerForSession,
+ UserMetadata userMetadataForSession) {
+ long sessionStartTime = reportPersistence.getStartTimestampMillis(sessionId);
+ // ApplicationExitInfo did not occur during the session.
+ if (applicationExitInfo.getTimestamp() < sessionStartTime) {
+ return;
+ }
+
+ // Currently we only persist these if it is an ANR.
+ if (applicationExitInfo.getReason() != ApplicationExitInfo.REASON_ANR) {
+ return;
+ }
+
+ final CrashlyticsReport.Session.Event capturedEvent =
+ dataCapture.captureAnrEventData(convertApplicationExitInfo(applicationExitInfo));
+
+ Logger.getLogger().d("Persisting anr for session " + sessionId);
+ reportPersistence.persistEvent(
+ addLogsAndCustomKeysToEvent(
+ capturedEvent, logFileManagerForSession, userMetadataForSession),
+ sessionId,
+ true);
+ }
+
public void finalizeSessionWithNativeEvent(
@NonNull String sessionId, @NonNull List nativeSessionFiles) {
ArrayList nativeFiles = new ArrayList<>();
@@ -184,28 +224,16 @@ public Task sendReports(@NonNull Executor reportSendCompleteExecutor) {
return Tasks.whenAll(sendTasks);
}
- private void persistEvent(
- @NonNull Throwable event,
- @NonNull Thread thread,
- @NonNull String sessionId,
- @NonNull String eventType,
- long timestamp,
- boolean includeAllThreads) {
-
- final boolean isHighPriority = eventType.equals(EVENT_TYPE_CRASH);
-
- final CrashlyticsReport.Session.Event capturedEvent =
- dataCapture.captureEventData(
- event,
- thread,
- eventType,
- timestamp,
- EVENT_THREAD_IMPORTANCE,
- MAX_CHAINED_EXCEPTION_DEPTH,
- includeAllThreads);
+ private CrashlyticsReport.Session.Event addLogsAndCustomKeysToEvent(
+ CrashlyticsReport.Session.Event capturedEvent) {
+ return addLogsAndCustomKeysToEvent(capturedEvent, logFileManager, reportMetadata);
+ }
+ private CrashlyticsReport.Session.Event addLogsAndCustomKeysToEvent(
+ CrashlyticsReport.Session.Event capturedEvent,
+ LogFileManager logFileManager,
+ UserMetadata reportMetadata) {
final CrashlyticsReport.Session.Event.Builder eventBuilder = capturedEvent.toBuilder();
-
final String content = logFileManager.getLogString();
if (content != null) {
@@ -231,7 +259,31 @@ private void persistEvent(
.build());
}
- reportPersistence.persistEvent(eventBuilder.build(), sessionId, isHighPriority);
+ return eventBuilder.build();
+ }
+
+ private void persistEvent(
+ @NonNull Throwable event,
+ @NonNull Thread thread,
+ @NonNull String sessionId,
+ @NonNull String eventType,
+ long timestamp,
+ boolean includeAllThreads) {
+
+ final boolean isHighPriority = eventType.equals(EVENT_TYPE_CRASH);
+
+ final CrashlyticsReport.Session.Event capturedEvent =
+ dataCapture.captureEventData(
+ event,
+ thread,
+ eventType,
+ timestamp,
+ EVENT_THREAD_IMPORTANCE,
+ MAX_CHAINED_EXCEPTION_DEPTH,
+ includeAllThreads);
+
+ reportPersistence.persistEvent(
+ addLogsAndCustomKeysToEvent(capturedEvent), sessionId, isHighPriority);
}
private boolean onReportSendComplete(@NonNull Task task) {
@@ -264,4 +316,48 @@ private static List getSortedCustomAttributes(
return attributesList;
}
+
+ @RequiresApi(api = Build.VERSION_CODES.R)
+ private static CrashlyticsReport.ApplicationExitInfo convertApplicationExitInfo(
+ ApplicationExitInfo applicationExitInfo) {
+ String traceFile = null;
+ try {
+ traceFile = convertInputStreamToString(applicationExitInfo.getTraceInputStream());
+ } catch (IOException | NullPointerException e) {
+ Logger.getLogger()
+ .w(
+ "Could not get input trace in application exit info: "
+ + applicationExitInfo.toString()
+ + " Error: "
+ + e);
+ }
+
+ return CrashlyticsReport.ApplicationExitInfo.builder()
+ .setImportance(applicationExitInfo.getImportance())
+ .setProcessName(applicationExitInfo.getProcessName())
+ .setReasonCode(applicationExitInfo.getReason())
+ .setTimestamp(applicationExitInfo.getTimestamp())
+ .setPid(applicationExitInfo.getPid())
+ .setPss(applicationExitInfo.getPss())
+ .setRss(applicationExitInfo.getRss())
+ .setTraceFile(traceFile)
+ .build();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+ @VisibleForTesting
+ public static String convertInputStreamToString(@Nullable InputStream inputStream)
+ throws IOException, NullPointerException {
+ StringBuilder stringBuilder = new StringBuilder();
+ try (Reader reader =
+ new BufferedReader(
+ new InputStreamReader(inputStream, Charset.forName(StandardCharsets.UTF_8.name())))) {
+ int c = 0;
+ while ((c = reader.read()) != -1) {
+ stringBuilder.append((char) c);
+ }
+
+ return stringBuilder.toString();
+ }
+ }
}
diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/model/CrashlyticsReport.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/model/CrashlyticsReport.java
index 9f8b4e53a1c..9019611def6 100644
--- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/model/CrashlyticsReport.java
+++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/model/CrashlyticsReport.java
@@ -656,12 +656,15 @@ public static Builder builder() {
return new AutoValue_CrashlyticsReport_Session_Event_Application_Execution.Builder();
}
- @NonNull
+ @Nullable
public abstract ImmutableList getThreads();
- @NonNull
+ @Nullable
public abstract Exception getException();
+ @Nullable
+ public abstract ApplicationExitInfo getAppExitInfo();
+
@NonNull
public abstract Signal getSignal();
@@ -895,6 +898,9 @@ public abstract static class Builder {
@NonNull
public abstract Builder setException(@NonNull Exception value);
+ @NonNull
+ public abstract Builder setAppExitInfo(@NonNull ApplicationExitInfo value);
+
@NonNull
public abstract Builder setSignal(@NonNull Signal value);
@@ -1027,6 +1033,71 @@ public abstract static class Builder {
}
}
+ @AutoValue
+ public abstract static class ApplicationExitInfo {
+
+ @NonNull
+ public static ApplicationExitInfo.Builder builder() {
+ return new AutoValue_CrashlyticsReport_ApplicationExitInfo.Builder();
+ }
+
+ @NonNull
+ public abstract int getPid();
+
+ @NonNull
+ public abstract String getProcessName();
+
+ @NonNull
+ public abstract int getReasonCode();
+
+ @NonNull
+ public abstract int getImportance();
+
+ @NonNull
+ public abstract long getPss();
+
+ @NonNull
+ public abstract long getRss();
+
+ @NonNull
+ public abstract long getTimestamp();
+
+ @Nullable
+ // Not all ApplicationExitInfos have a trace file.
+ public abstract String getTraceFile();
+
+ /** Builder for {@link ApplicationExitInfo}. */
+ @AutoValue.Builder
+ public abstract static class Builder {
+ @NonNull
+ public abstract ApplicationExitInfo.Builder setPid(@NonNull int value);
+
+ @NonNull
+ public abstract ApplicationExitInfo.Builder setProcessName(@NonNull String value);
+
+ @NonNull
+ public abstract ApplicationExitInfo.Builder setReasonCode(@NonNull int value);
+
+ @NonNull
+ public abstract ApplicationExitInfo.Builder setImportance(@NonNull int value);
+
+ @NonNull
+ public abstract ApplicationExitInfo.Builder setPss(@NonNull long value);
+
+ @NonNull
+ public abstract ApplicationExitInfo.Builder setRss(@NonNull long value);
+
+ @NonNull
+ public abstract ApplicationExitInfo.Builder setTimestamp(@NonNull long value);
+
+ @NonNull
+ public abstract ApplicationExitInfo.Builder setTraceFile(@Nullable String value);
+
+ @NonNull
+ public abstract ApplicationExitInfo build();
+ }
+ }
+
@AutoValue.Builder
public abstract static class Builder {
diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/model/serialization/CrashlyticsReportJsonTransform.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/model/serialization/CrashlyticsReportJsonTransform.java
index a7e9178b78a..7a222b445c8 100644
--- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/model/serialization/CrashlyticsReportJsonTransform.java
+++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/model/serialization/CrashlyticsReportJsonTransform.java
@@ -47,6 +47,12 @@ public String eventToJson(@NonNull CrashlyticsReport.Session.Event event) {
return CRASHLYTICS_REPORT_JSON_ENCODER.encode(event);
}
+ @NonNull
+ public String applicationExitInfoToJson(
+ @NonNull CrashlyticsReport.ApplicationExitInfo applicationExitInfo) {
+ return CRASHLYTICS_REPORT_JSON_ENCODER.encode(applicationExitInfo);
+ }
+
@NonNull
public CrashlyticsReport reportFromJson(@NonNull String json) throws IOException {
try (JsonReader jsonReader = new JsonReader(new StringReader(json))) {
@@ -65,6 +71,16 @@ public CrashlyticsReport.Session.Event eventFromJson(@NonNull String json) throw
}
}
+ @NonNull
+ public CrashlyticsReport.ApplicationExitInfo applicationExitInfoFromJson(@NonNull String json)
+ throws IOException {
+ try (JsonReader jsonReader = new JsonReader(new StringReader(json))) {
+ return parseAppExitInfo(jsonReader);
+ } catch (IllegalStateException e) {
+ throw new IOException(e);
+ }
+ }
+
@NonNull
private static CrashlyticsReport parseReport(@NonNull JsonReader jsonReader) throws IOException {
final CrashlyticsReport.Builder builder = CrashlyticsReport.builder();
@@ -184,6 +200,49 @@ private static CrashlyticsReport.FilesPayload parseNdkPayload(@NonNull JsonReade
return builder.build();
}
+ @NonNull
+ private static CrashlyticsReport.ApplicationExitInfo parseAppExitInfo(
+ @NonNull JsonReader jsonReader) throws IOException {
+ final CrashlyticsReport.ApplicationExitInfo.Builder builder =
+ CrashlyticsReport.ApplicationExitInfo.builder();
+
+ jsonReader.beginObject();
+ while (jsonReader.hasNext()) {
+ String name = jsonReader.nextName();
+ switch (name) {
+ case "pid":
+ builder.setPid(jsonReader.nextInt());
+ break;
+ case "processName":
+ builder.setProcessName(jsonReader.nextString());
+ break;
+ case "reasonCode":
+ builder.setReasonCode(jsonReader.nextInt());
+ break;
+ case "importance":
+ builder.setImportance(jsonReader.nextInt());
+ break;
+ case "pss":
+ builder.setPss(jsonReader.nextLong());
+ break;
+ case "rss":
+ builder.setRss(jsonReader.nextLong());
+ break;
+ case "timestamp":
+ builder.setTimestamp(jsonReader.nextLong());
+ break;
+ case "traceFile":
+ builder.setTraceFile(jsonReader.nextString());
+ break;
+ default:
+ jsonReader.skipValue();
+ break;
+ }
+ }
+ jsonReader.endObject();
+ return builder.build();
+ }
+
@NonNull
private static CrashlyticsReport.FilesPayload.File parseFile(@NonNull JsonReader jsonReader)
throws IOException {
@@ -439,6 +498,9 @@ private static Event.Application.Execution parseEventExecution(@NonNull JsonRead
builder.setBinaries(
parseArray(jsonReader, CrashlyticsReportJsonTransform::parseEventBinaryImage));
break;
+ case "appExitInfo":
+ builder.setAppExitInfo(parseAppExitInfo(jsonReader));
+ break;
default:
jsonReader.skipValue();
break;
diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/persistence/CrashlyticsReportPersistence.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/persistence/CrashlyticsReportPersistence.java
index 7dc67afa3bf..fad50d5f230 100644
--- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/persistence/CrashlyticsReportPersistence.java
+++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/persistence/CrashlyticsReportPersistence.java
@@ -59,6 +59,11 @@ public class CrashlyticsReportPersistence {
private static final String REPORT_FILE_NAME = "report";
private static final String USER_FILE_NAME = "user";
+ // A single session should have only a single AppExitInfo.
+ private static final String APP_EXIT_INFO_FILE_NAME = "app-exit-info";
+ // We use the lastModified timestamp of this file to quickly store and access the startTime in ms
+ // of a session.
+ private static final String SESSION_START_TIMESTAMP_FILE_NAME = "start-time";
private static final String EVENT_FILE_NAME_PREFIX = "event";
private static final int EVENT_COUNTER_WIDTH = 10; // String width of maximum positive int value
private static final String EVENT_COUNTER_FORMAT = "%0" + EVENT_COUNTER_WIDTH + "d";
@@ -66,6 +71,7 @@ public class CrashlyticsReportPersistence {
EVENT_FILE_NAME_PREFIX.length() + EVENT_COUNTER_WIDTH;
private static final String PRIORITY_EVENT_SUFFIX = "_";
private static final String NORMAL_EVENT_SUFFIX = "";
+ private static final String EVENT_TYPE_ANR = "anr";
private static final CrashlyticsReportJsonTransform TRANSFORM =
new CrashlyticsReportJsonTransform();
@@ -110,6 +116,10 @@ public void persistReport(@NonNull CrashlyticsReport report) {
final File sessionDirectory = prepareDirectory(getSessionDirectoryById(sessionId));
final String json = TRANSFORM.reportToJson(report);
writeTextFile(new File(sessionDirectory, REPORT_FILE_NAME), json);
+ writeTextFile(
+ new File(sessionDirectory, SESSION_START_TIMESTAMP_FILE_NAME),
+ "",
+ session.getStartedAt());
} catch (IOException e) {
Logger.getLogger().d("Could not persist report for session " + sessionId, e);
}
@@ -177,6 +187,19 @@ public List listSortedOpenSessionIds() {
return openSessionIds;
}
+ /**
+ * Gets the startTimestampMs of the given sessionId.
+ *
+ * @param sessionId
+ * @return startTimestampMs
+ */
+ public long getStartTimestampMillis(String sessionId) {
+ final File sessionDirectory = getSessionDirectoryById(sessionId);
+ final File sessionStartTimestampFile =
+ new File(sessionDirectory, SESSION_START_TIMESTAMP_FILE_NAME);
+ return sessionStartTimestampFile.lastModified();
+ }
+
public boolean hasFinalizedReports() {
return !getAllFinalizedReportFiles().isEmpty();
}
@@ -314,7 +337,8 @@ private void synthesizeReport(@NonNull File sessionDirectory, long sessionEndTim
for (File eventFile : eventFiles) {
try {
- events.add(TRANSFORM.eventFromJson(readTextFile(eventFile)));
+ Event event = TRANSFORM.eventFromJson(readTextFile(eventFile));
+ events.add(event);
isHighPriorityReport = isHighPriorityReport || isHighPriorityEventFile(eventFile.getName());
} catch (IOException e) {
Logger.getLogger().w("Could not add event to report for " + eventFile, e);
@@ -373,7 +397,6 @@ private static void synthesizeReportFile(
.reportFromJson(readTextFile(reportFile))
.withSessionEndFields(sessionEndTime, isCrashed, userId)
.withEvents(ImmutableList.from(events));
-
final Session session = report.getSession();
if (session == null) {
@@ -489,6 +512,14 @@ private static void writeTextFile(File file, String text) throws IOException {
}
}
+ private static void writeTextFile(File file, String text, long lastModifiedTimestampSeconds)
+ throws IOException {
+ try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file), UTF_8)) {
+ writer.write(text);
+ file.setLastModified(convertTimestampFromSecondsToMs(lastModifiedTimestampSeconds));
+ }
+ }
+
@NonNull
private static String readTextFile(@NonNull File file) throws IOException {
final byte[] readBuffer = new byte[8192];
@@ -532,4 +563,8 @@ private static void recursiveDelete(@Nullable File file) {
}
file.delete();
}
+
+ private static long convertTimestampFromSecondsToMs(long timestampSeconds) {
+ return timestampSeconds * 1000;
+ }
}
diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/DefaultSettingsJsonTransform.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/DefaultSettingsJsonTransform.java
index e8bb6ce6a31..d5d3870774a 100644
--- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/DefaultSettingsJsonTransform.java
+++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/DefaultSettingsJsonTransform.java
@@ -97,7 +97,12 @@ private static FeaturesSettingsData buildFeaturesSessionDataFrom(JSONObject json
SettingsJsonConstants.FEATURES_COLLECT_REPORTS_KEY,
SettingsJsonConstants.FEATURES_COLLECT_REPORTS_DEFAULT);
- return new FeaturesSettingsData(collectReports);
+ final boolean collectAnrs =
+ json.optBoolean(
+ SettingsJsonConstants.FEATURES_COLLECT_ANRS_KEY,
+ SettingsJsonConstants.FEATURES_COLLECT_ANRS_DEFAULT);
+
+ return new FeaturesSettingsData(collectReports, collectAnrs);
}
private static SessionSettingsData buildSessionDataFrom(JSONObject json) {
diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/SettingsJsonConstants.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/SettingsJsonConstants.java
index c5965059ac5..2d7bfcd7393 100644
--- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/SettingsJsonConstants.java
+++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/SettingsJsonConstants.java
@@ -29,9 +29,11 @@ class SettingsJsonConstants {
// Feature Switch Keys
static final String FEATURES_COLLECT_REPORTS_KEY = "collect_reports";
+ static final String FEATURES_COLLECT_ANRS_KEY = "collect_anrs";
// Feature Switch Defaults
static final boolean FEATURES_COLLECT_REPORTS_DEFAULT = true;
+ static final boolean FEATURES_COLLECT_ANRS_DEFAULT = false;
// Fabric JSON Keys
static final String FABRIC_BUNDLE_ID = "bundle_id";
diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/SettingsV3JsonTransform.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/SettingsV3JsonTransform.java
index 8f3a2daf32a..5422c4512c5 100644
--- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/SettingsV3JsonTransform.java
+++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/SettingsV3JsonTransform.java
@@ -119,9 +119,14 @@ private static FeaturesSettingsData buildFeaturesSessionDataFrom(JSONObject json
SettingsJsonConstants.FEATURES_COLLECT_REPORTS_KEY,
SettingsJsonConstants.FEATURES_COLLECT_REPORTS_DEFAULT);
+ final boolean collectAnrs =
+ json.optBoolean(
+ SettingsJsonConstants.FEATURES_COLLECT_ANRS_KEY,
+ SettingsJsonConstants.FEATURES_COLLECT_ANRS_DEFAULT);
+
// TODO: Build support back for "collect logged exceptions"
- return new FeaturesSettingsData(collectReports);
+ return new FeaturesSettingsData(collectReports, collectAnrs);
}
private static SessionSettingsData defaultSessionData() {
diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/model/FeaturesSettingsData.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/model/FeaturesSettingsData.java
index 14a6e8cfb59..ab7ead444d8 100644
--- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/model/FeaturesSettingsData.java
+++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/model/FeaturesSettingsData.java
@@ -17,8 +17,10 @@
/** Immutable value object for storing feature switch settings */
public class FeaturesSettingsData {
public final boolean collectReports;
+ public final boolean collectAnrs;
- public FeaturesSettingsData(boolean collectReports) {
+ public FeaturesSettingsData(boolean collectReports, boolean collectAnrs) {
this.collectReports = collectReports;
+ this.collectAnrs = collectAnrs;
}
}
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
new file mode 100644
index 00000000000..0624f2d89c7
--- /dev/null
+++ b/firebase-crashlytics/src/test/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerRobolectricTest.java
@@ -0,0 +1,162 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.firebase.crashlytics.internal.common;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.ActivityManager;
+import android.app.ApplicationExitInfo;
+import android.content.Context;
+import androidx.test.core.app.ApplicationProvider;
+import com.google.firebase.crashlytics.internal.ProviderProxyNativeComponent;
+import com.google.firebase.crashlytics.internal.analytics.AnalyticsEventLogger;
+import com.google.firebase.crashlytics.internal.log.LogFileManager;
+import com.google.firebase.crashlytics.internal.persistence.FileStore;
+import com.google.firebase.crashlytics.internal.settings.SettingsDataProvider;
+import com.google.firebase.crashlytics.internal.settings.model.FeaturesSettingsData;
+import com.google.firebase.crashlytics.internal.settings.model.Settings;
+import com.google.firebase.crashlytics.internal.unity.UnityVersionProvider;
+import java.io.File;
+import java.util.Collections;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class CrashlyticsControllerRobolectricTest {
+ private static final String GOOGLE_APP_ID = "google:app:id";
+
+ private Context testContext;
+ @Mock private IdManager idManager;
+ @Mock private SettingsDataProvider mockSettingsDataProvider;
+ @Mock private FileStore mockFileStore;
+ @Mock private File mockFilesDirectory;
+ @Mock private SessionReportingCoordinator mockSessionReportingCoordinator;
+ @Mock private DataCollectionArbiter mockDataCollectionArbiter;
+ @Mock private LogFileManager.DirectoryProvider mockLogFileDirecotryProvider;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ testContext = getApplicationContext();
+ }
+
+ @Test
+ public void testDoCloseSession_enabledAnrs_doesNotPersistsAppExitInfoIfItDoesntExist() {
+ final String sessionId = "sessionId";
+ final CrashlyticsController controller = createController();
+
+ when(mockSessionReportingCoordinator.listSortedOpenSessionIds())
+ .thenReturn(Collections.singletonList(sessionId));
+ mockSettingsData(true);
+ controller.doCloseSessions(mockSettingsDataProvider);
+ // Since we haven't added any app exit info to the shadow activity manager, there won't exist a
+ // single app exit info, and so this method won't be called.
+ verify(mockSessionReportingCoordinator, never())
+ .persistAppExitInfoEvent(
+ eq(sessionId), any(), any(LogFileManager.class), any(UserMetadata.class));
+ }
+
+ @Test
+ public void testDoCloseSession_enabledAnrs_persistsAppExitInfoIfItExists() {
+ final String sessionId = "sessionId";
+ final CrashlyticsController controller = createController();
+ ApplicationExitInfo testApplicationExitInfo = addAppExitInfo(ApplicationExitInfo.REASON_ANR);
+
+ when(mockSessionReportingCoordinator.listSortedOpenSessionIds())
+ .thenReturn(Collections.singletonList(sessionId));
+ mockSettingsData(true);
+ controller.doCloseSessions(mockSettingsDataProvider);
+ verify(mockSessionReportingCoordinator)
+ .persistAppExitInfoEvent(
+ eq(sessionId),
+ eq(testApplicationExitInfo),
+ any(LogFileManager.class),
+ any(UserMetadata.class));
+ }
+
+ @Test
+ public void testDoCloseSession_disabledAnrs_doesNotPersistsAppExitInfo() {
+ final String sessionId = "sessionId";
+ final CrashlyticsController controller = createController();
+
+ when(mockSessionReportingCoordinator.listSortedOpenSessionIds())
+ .thenReturn(Collections.singletonList(sessionId));
+ mockSettingsData(false);
+ controller.doCloseSessions(mockSettingsDataProvider);
+ verify(mockSessionReportingCoordinator, never())
+ .persistAppExitInfoEvent(
+ eq(sessionId), any(), any(LogFileManager.class), any(UserMetadata.class));
+ }
+
+ private void mockSettingsData(boolean collectAnrs) {
+ Settings mockSettings = mock(Settings.class);
+ when(mockSettingsDataProvider.getSettings()).thenReturn(mockSettings);
+ when(mockSettings.getFeaturesData()).thenReturn(new FeaturesSettingsData(true, collectAnrs));
+ }
+
+ /** Creates a new CrashlyticsController with default options and opens a session. */
+ private CrashlyticsController createController() {
+ AppData appData =
+ new AppData(
+ GOOGLE_APP_ID,
+ "buildId",
+ "installerPackageName",
+ "packageName",
+ "versionCode",
+ "versionName",
+ mock(UnityVersionProvider.class));
+
+ final CrashlyticsController controller =
+ new CrashlyticsController(
+ testContext,
+ new CrashlyticsBackgroundWorker(Runnable::run),
+ idManager,
+ mockDataCollectionArbiter,
+ mockFileStore,
+ new CrashlyticsFileMarker(CrashlyticsCore.CRASH_MARKER_FILE_NAME, mockFileStore),
+ appData,
+ null,
+ null,
+ mockLogFileDirecotryProvider,
+ mockSessionReportingCoordinator,
+ new ProviderProxyNativeComponent(() -> null),
+ mock(AnalyticsEventLogger.class));
+ controller.openSession();
+ return controller;
+ }
+
+ private ApplicationExitInfo addAppExitInfo(int reason) {
+ ActivityManager activityManager =
+ (ActivityManager)
+ ApplicationProvider.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
+ ActivityManager.RunningAppProcessInfo runningAppProcessInfo =
+ activityManager.getRunningAppProcesses().get(0);
+ shadowOf(activityManager)
+ .addApplicationExitInfo(
+ runningAppProcessInfo.processName, runningAppProcessInfo.pid, reason, 1);
+ return activityManager.getHistoricalProcessExitReasons(null, 0, 0).get(0);
+ }
+}
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
new file mode 100644
index 00000000000..8aa09151308
--- /dev/null
+++ b/firebase-crashlytics/src/test/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinatorRobolectricTest.java
@@ -0,0 +1,177 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.firebase.crashlytics.internal.common;
+
+import static org.junit.Assert.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.ActivityManager;
+import android.app.ApplicationExitInfo;
+import android.content.Context;
+import android.os.Build;
+import androidx.annotation.RequiresApi;
+import androidx.test.core.app.ApplicationProvider;
+import com.google.firebase.crashlytics.internal.log.LogFileManager;
+import com.google.firebase.crashlytics.internal.model.CrashlyticsReport;
+import com.google.firebase.crashlytics.internal.persistence.CrashlyticsReportPersistence;
+import com.google.firebase.crashlytics.internal.send.DataTransportCrashlyticsReportSender;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class SessionReportingCoordinatorRobolectricTest {
+ @Mock private CrashlyticsReportDataCapture dataCapture;
+ @Mock private CrashlyticsReportPersistence reportPersistence;
+ @Mock private DataTransportCrashlyticsReportSender reportSender;
+ @Mock private LogFileManager logFileManager;
+ @Mock private UserMetadata reportMetadata;
+ @Mock private CrashlyticsReport.Session.Event mockEvent;
+ @Mock private CrashlyticsReport.Session.Event.Builder mockEventBuilder;
+ @Mock private CrashlyticsReport.Session.Event.Application mockEventApp;
+ @Mock private CrashlyticsReport.Session.Event.Application.Builder mockEventAppBuilder;
+ @Mock private LogFileManager mockLogFileManager;
+ @Mock UserMetadata mockUserMetadata;
+
+ private SessionReportingCoordinator reportingCoordinator;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ reportingCoordinator =
+ new SessionReportingCoordinator(
+ dataCapture, reportPersistence, reportSender, logFileManager, reportMetadata);
+ mockEventInteractions();
+ }
+
+ @Test
+ public void testAppExitInfoEvent_persistIfAnrWithinSession() {
+ // The timestamp of the applicationExitInfo is 0, and so in this case, we want the session
+ // timestamp to be less than or equal to 0.
+ final long sessionStartTimestamp = 0;
+ final String sessionId = "testSessionId";
+ when(reportPersistence.getStartTimestampMillis(sessionId)).thenReturn(sessionStartTimestamp);
+
+ mockEventInteractions();
+ ApplicationExitInfo testApplicationExitInfo = addAppExitInfo(ApplicationExitInfo.REASON_ANR);
+
+ reportingCoordinator.onBeginSession(sessionId, sessionStartTimestamp);
+ reportingCoordinator.persistAppExitInfoEvent(
+ sessionId, testApplicationExitInfo, mockLogFileManager, mockUserMetadata);
+
+ verify(dataCapture).captureAnrEventData(convertApplicationExitInfo(testApplicationExitInfo));
+ verify(reportPersistence).persistEvent(any(), eq(sessionId), eq(true));
+ }
+
+ @Test
+ public void testAppExitInfoEvent_notPersistIfAnrBeforeSession() {
+ // The timestamp of the applicationExitInfo is 0, and so in this case, we want the session
+ // timestamp to be greater than 0.
+ final long sessionStartTimestamp = 10;
+ final String sessionId = "testSessionId";
+ when(reportPersistence.getStartTimestampMillis(sessionId)).thenReturn(sessionStartTimestamp);
+
+ ApplicationExitInfo testApplicationExitInfo = addAppExitInfo(ApplicationExitInfo.REASON_ANR);
+
+ reportingCoordinator.onBeginSession(sessionId, sessionStartTimestamp);
+ reportingCoordinator.persistAppExitInfoEvent(
+ sessionId, testApplicationExitInfo, mockLogFileManager, mockUserMetadata);
+
+ verify(dataCapture, never())
+ .captureAnrEventData(convertApplicationExitInfo(testApplicationExitInfo));
+ verify(reportPersistence, never()).persistEvent(any(), eq(sessionId), eq(true));
+ }
+
+ @Test
+ public void testAppExitInfoEvent_notPersistIfAppExitInfoNotAnrButWithinSession() {
+ // The timestamp of the applicationExitInfo is 0, and so in this case, we want the session
+ // timestamp to be less than or equal to 0.
+ final long sessionStartTimestamp = 0;
+ final String sessionId = "testSessionId";
+ when(reportPersistence.getStartTimestampMillis(sessionId)).thenReturn(sessionStartTimestamp);
+
+ ApplicationExitInfo testApplicationExitInfo =
+ addAppExitInfo(ApplicationExitInfo.REASON_DEPENDENCY_DIED);
+
+ reportingCoordinator.onBeginSession(sessionId, sessionStartTimestamp);
+ reportingCoordinator.persistAppExitInfoEvent(
+ sessionId, testApplicationExitInfo, mockLogFileManager, mockUserMetadata);
+
+ verify(dataCapture, never())
+ .captureAnrEventData(convertApplicationExitInfo(testApplicationExitInfo));
+ verify(reportPersistence, never()).persistEvent(any(), eq(sessionId), eq(true));
+ }
+
+ @Test
+ public void testconvertInputStreamToString_worksSuccessfully() throws IOException {
+ String stackTrace = "-----stacktrace---------";
+ InputStream inputStream = new ByteArrayInputStream(stackTrace.getBytes());
+ assertEquals(stackTrace, SessionReportingCoordinator.convertInputStreamToString(inputStream));
+ }
+
+ private void mockEventInteractions() {
+ when(mockEvent.toBuilder()).thenReturn(mockEventBuilder);
+ when(mockEventBuilder.build()).thenReturn(mockEvent);
+ when(mockEvent.getApp()).thenReturn(mockEventApp);
+ when(mockEventApp.toBuilder()).thenReturn(mockEventAppBuilder);
+ when(mockEventAppBuilder.setCustomAttributes(any())).thenReturn(mockEventAppBuilder);
+ when(mockEventAppBuilder.build()).thenReturn(mockEventApp);
+ when(dataCapture.captureAnrEventData(any(CrashlyticsReport.ApplicationExitInfo.class)))
+ .thenReturn(mockEvent);
+ }
+
+ private ApplicationExitInfo addAppExitInfo(int reason) {
+ ActivityManager activityManager =
+ (ActivityManager)
+ ApplicationProvider.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
+ ActivityManager.RunningAppProcessInfo runningAppProcessInfo =
+ activityManager.getRunningAppProcesses().get(0);
+ shadowOf(activityManager)
+ .addApplicationExitInfo(
+ runningAppProcessInfo.processName, runningAppProcessInfo.pid, reason, 1);
+ return activityManager.getHistoricalProcessExitReasons(null, 0, 0).get(0);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.R)
+ private static CrashlyticsReport.ApplicationExitInfo convertApplicationExitInfo(
+ ApplicationExitInfo applicationExitInfo) {
+ // The ApplicationExitInfo inserted by ShadowApplicationManager does not contain an input trace,
+ // and so it will be null.
+ // Thus, these tests verify that an ApplicationExitInfo is successfully converted even when the
+ // input trace fails to be parsed as a string.
+ return CrashlyticsReport.ApplicationExitInfo.builder()
+ .setImportance(applicationExitInfo.getImportance())
+ .setProcessName(applicationExitInfo.getProcessName())
+ .setReasonCode(applicationExitInfo.getReason())
+ .setTimestamp(applicationExitInfo.getTimestamp())
+ .setPid(applicationExitInfo.getPid())
+ .setPss(applicationExitInfo.getPss())
+ .setRss(applicationExitInfo.getRss())
+ .setTraceFile(null)
+ .build();
+ }
+}
diff --git a/firebase-crashlytics/src/test/java/com/google/firebase/crashlytics/internal/model/CrashlyticsReportTest.java b/firebase-crashlytics/src/test/java/com/google/firebase/crashlytics/internal/model/CrashlyticsReportTest.java
index 42222560d3c..81e2240c31c 100644
--- a/firebase-crashlytics/src/test/java/com/google/firebase/crashlytics/internal/model/CrashlyticsReportTest.java
+++ b/firebase-crashlytics/src/test/java/com/google/firebase/crashlytics/internal/model/CrashlyticsReportTest.java
@@ -43,6 +43,27 @@ public void testWithEvents_returnsNewReportWithEvents() {
assertEquals(2, withEventsReport.getSession().getEvents().size());
}
+ @Test
+ public void testWithEvents_returnsNewReportWithAnr() {
+ final CrashlyticsReport testReport = makeTestReport();
+
+ assertNull(testReport.getSession().getEvents());
+ final CrashlyticsReport withAnrEventsReport =
+ testReport.withEvents(ImmutableList.from(makeAnrEvent()));
+
+ assertNotEquals(testReport, withAnrEventsReport);
+ assertNotNull(withAnrEventsReport.getSession().getEvents());
+ assertEquals(1, withAnrEventsReport.getSession().getEvents().size());
+ assertNotNull(
+ withAnrEventsReport
+ .getSession()
+ .getEvents()
+ .get(0)
+ .getApp()
+ .getExecution()
+ .getAppExitInfo());
+ }
+
@Test
public void testWithOrganizationId_returnsNewReportWithOrganizationId() {
final CrashlyticsReport testReport = makeTestReport();
@@ -208,9 +229,43 @@ private static ImmutableList makeTestEvents(int numEvents) {
return ImmutableList.from(events);
}
+ private static Event makeAnrEvent() {
+ return Event.builder()
+ .setType("anr")
+ .setTimestamp(1000)
+ .setApp(
+ Session.Event.Application.builder()
+ .setBackground(false)
+ .setExecution(
+ Execution.builder()
+ .setBinaries(
+ ImmutableList.from(
+ Execution.BinaryImage.builder()
+ .setBaseAddress(0)
+ .setName("name")
+ .setSize(100000)
+ .setUuid("uuid")
+ .build()))
+ .setSignal(Signal.builder().setCode("0").setName("0").setAddress(0).build())
+ .setAppExitInfo(makeAppExitInfo())
+ .build())
+ .setUiOrientation(1)
+ .build())
+ .setDevice(
+ Session.Event.Device.builder()
+ .setBatteryLevel(0.5)
+ .setBatteryVelocity(3)
+ .setDiskUsed(10000000)
+ .setOrientation(1)
+ .setProximityOn(true)
+ .setRamUsed(10000000)
+ .build())
+ .build();
+ }
+
private static Event makeTestEvent() {
return Event.builder()
- .setType("type")
+ .setType("test")
.setTimestamp(1000)
.setApp(
Session.Event.Application.builder()
@@ -255,6 +310,19 @@ private static Event makeTestEvent() {
.build();
}
+ private static CrashlyticsReport.ApplicationExitInfo makeAppExitInfo() {
+ return CrashlyticsReport.ApplicationExitInfo.builder()
+ .setTraceFile("trace")
+ .setTimestamp(1L)
+ .setImportance(1)
+ .setReasonCode(1)
+ .setProcessName("test")
+ .setPid(1)
+ .setPss(1)
+ .setRss(1)
+ .build();
+ }
+
private static ImmutableList makeTestFrames() {
return ImmutableList.from(
Frame.builder()