From 5d7ddf6235b839633e3dedc333ad34424689a690 Mon Sep 17 00:00:00 2001 From: Konstantin Mandrika Date: Fri, 12 Jun 2026 16:53:25 -0400 Subject: [PATCH 1/8] Capture ProfilingManager OOM and Anomaly triggers --- .../firebase-crashlytics.gradle | 2 +- .../common/CrashlyticsController.java | 81 +++++++++++++++++++ .../internal/common/CrashlyticsCore.java | 6 ++ .../common/SessionReportingCoordinator.java | 57 +++++++++++-- .../internal/model/CrashlyticsReport.java | 49 +++++++++++ .../CrashlyticsReportJsonTransform.java | 63 +++++++++++++++ .../CrashlyticsReportPersistence.java | 53 +++++++++++- gradle/libs.versions.toml | 2 +- 8 files changed, 304 insertions(+), 9 deletions(-) diff --git a/firebase-crashlytics/firebase-crashlytics.gradle b/firebase-crashlytics/firebase-crashlytics.gradle index 039fee4f687..d596826e497 100644 --- a/firebase-crashlytics/firebase-crashlytics.gradle +++ b/firebase-crashlytics/firebase-crashlytics.gradle @@ -37,7 +37,7 @@ android { timeOutInMs 60 * 1000 } namespace "com.google.firebase.crashlytics" - compileSdkVersion project.compileSdkVersion + compileSdkVersion 37 testOptions.unitTests.includeAndroidResources = true defaultConfig { minSdkVersion project.minSdkVersion 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..663d83998d9 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 @@ -19,17 +19,22 @@ import android.content.Context; import android.os.Build; import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.Environment; +import android.os.ProfilingManager; +import android.os.ProfilingTrigger; import android.os.StatFs; import android.util.Base64; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import com.google.android.gms.tasks.SuccessContinuation; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; +import com.google.firebase.annotations.concurrent.Background; import com.google.firebase.crashlytics.internal.CrashlyticsNativeComponent; import com.google.firebase.crashlytics.internal.Logger; import com.google.firebase.crashlytics.internal.NativeSessionFileProvider; @@ -57,10 +62,12 @@ import java.util.Map; import java.util.SortedSet; import java.util.concurrent.Callable; +import java.util.concurrent.Executor; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; class CrashlyticsController { @@ -79,6 +86,8 @@ class CrashlyticsController { private static final String VERSION_CONTROL_INFO_KEY = "com.crashlytics.version-control-info"; private static final String VERSION_CONTROL_INFO_FILE = "version-control-info.textproto"; private static final String META_INF_FOLDER = "META-INF/"; + private static final String TRIGGER_TYPE_ANOMALY_FILENAME = "trigger-type-anomaly"; + private static final String TRIGGER_TYPE_OOM_FILENAME = "trigger-type-oom"; private static final Charset UTF_8 = Charset.forName("UTF-8"); @@ -175,6 +184,12 @@ public void onUncaughtException( Thread.setDefaultUncaughtExceptionHandler(crashHandler); } + @RequiresApi(api = VERSION_CODES.CINNAMON_BUN) + void enableProfilingManagerListener(String sessionId) { + // Set up profiling manager here, use background executor because it's just file I/O + registerProfilingManagerListener(sessionId, crashlyticsWorkers.diskWrite.getExecutor()); + } + void handleUncaughtException( @NonNull SettingsProvider settingsProvider, @NonNull final Thread thread, @@ -603,6 +618,10 @@ private void doCloseSessions( finalizePreviousNativeSession(mostRecentSessionIdToClose); } + if (android.os.Build.VERSION.SDK_INT >= VERSION_CODES.CINNAMON_BUN) { + writeProfilingManagerInfo(mostRecentSessionIdToClose); + } + String currentSessionId = null; if (skipCurrentSession) { currentSessionId = sortedOpenSessions.get(0); @@ -944,4 +963,66 @@ private void writeApplicationExitInfoEventIfRelevant(String sessionId) { } } // endregion + + // region ProfilingManager + @RequiresApi(api = VERSION_CODES.CINNAMON_BUN) + private void writeProfilingManagerInfo(String sessionId) { + ActivityManager manager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + + // For anomaly triggers, there should be a file written to disk by the registered consumer. + // However, for OOMs, it is less guaranteed that the process had enough resources to write the + // corresponding file. This is ok because we can check ApplicationExitInfo to see if an OOM + // occurred. + List triggers = fileStore.getSessionFiles(sessionId, (dir, name) -> List.of( + TRIGGER_TYPE_ANOMALY_FILENAME, + TRIGGER_TYPE_OOM_FILENAME + ).contains(name)).stream() + .map(triggerFile -> { + switch (triggerFile.getName()) { + case TRIGGER_TYPE_ANOMALY_FILENAME: return ProfilingTrigger.TRIGGER_TYPE_ANOMALY; + case TRIGGER_TYPE_OOM_FILENAME: return ProfilingTrigger.TRIGGER_TYPE_OOM; + } + + return ProfilingTrigger.TRIGGER_TYPE_NONE; + }) + .filter(trigger -> trigger != ProfilingTrigger.TRIGGER_TYPE_NONE) + .collect(Collectors.toList()); + + List appExits = manager.getHistoricalProcessExitReasons(null, 0, 0); + + reportingCoordinator.persistProfilingManagerInfo(sessionId, triggers, appExits); + } + + @SuppressLint("WrongConstant") // TRIGGER_TYPE_OOM, TRIGGER_TYPE_ANOMALY + @RequiresApi(api = VERSION_CODES.CINNAMON_BUN) + private void registerProfilingManagerListener(String sessionId, @Background Executor backgroundExecutor) { + ProfilingManager profilingManager = context.getSystemService(ProfilingManager.class); + + profilingManager.addProfilingTriggers(List.of( + new ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_OOM).build(), + new ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_ANOMALY).build() + )); + profilingManager.registerForAllProfilingResults(backgroundExecutor, (result) -> { + writeTriggerTypeFile(sessionId, result.getTriggerType()); + }); + } + + @RequiresApi(api = VERSION_CODES.CINNAMON_BUN) + private void writeTriggerTypeFile(String sessionId, int triggerType) { + String triggerFilename = + triggerType == ProfilingTrigger.TRIGGER_TYPE_ANOMALY ? TRIGGER_TYPE_ANOMALY_FILENAME : + triggerType == ProfilingTrigger.TRIGGER_TYPE_OOM ? TRIGGER_TYPE_OOM_FILENAME : + "trigger-type-unknown"; + + try { + if (!fileStore.getSessionFile(sessionId, triggerFilename).createNewFile()) { + Logger.getLogger() + .d("Trigger file " + triggerFilename + " exists for session: " + sessionId); + } + } catch (IOException e) { + Logger.getLogger().e("Unable to touch trigger file " + triggerFilename); + } + } + // 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..6e8cdb4dd39 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 @@ -15,6 +15,8 @@ package com.google.firebase.crashlytics.internal.common; import android.content.Context; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; @@ -200,6 +202,10 @@ public boolean onPreExecute(AppData appData, SettingsProvider settingsProvider) controller.enableExceptionHandling( sessionIdentifier, Thread.getDefaultUncaughtExceptionHandler(), settingsProvider); + if (VERSION.SDK_INT >= VERSION_CODES.CINNAMON_BUN) { + controller.enableProfilingManagerListener(sessionIdentifier); + } + if (initializeSynchronously && CommonUtils.canTryConnection(context)) { Logger.getLogger() .d( 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..ffbda47736b 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 @@ -17,6 +17,9 @@ import android.app.ApplicationExitInfo; import android.content.Context; import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.os.ProfilingTrigger; +import android.system.OsConstants; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -31,6 +34,7 @@ import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.CustomAttribute; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.FilesPayload; +import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.ProfilingManagerInfo; import com.google.firebase.crashlytics.internal.persistence.CrashlyticsReportPersistence; import com.google.firebase.crashlytics.internal.persistence.FileStore; import com.google.firebase.crashlytics.internal.send.DataTransportCrashlyticsReportSender; @@ -46,8 +50,10 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.SortedSet; import java.util.concurrent.Executor; +import java.util.function.Predicate; /** * This class coordinates Crashlytics session data capture and persistence, as well as sending of @@ -144,7 +150,11 @@ public void persistRelevantAppExitInfoEvent( UserMetadata userMetadataForSession) { ApplicationExitInfo relevantApplicationExitInfo = - findRelevantApplicationExitInfo(sessionId, applicationExitInfoList); + findRelevantApplicationExitInfo(sessionId, applicationExitInfoList, aei -> { + // If the ApplicationExitInfo is not an ANR, but it was within the session, loop through + // all ApplicationExitInfos that fall within the session. + return aei.getReason() != ApplicationExitInfo.REASON_ANR; + }); if (relevantApplicationExitInfo == null) { Logger.getLogger().v("No relevant ApplicationExitInfo occurred during session: " + sessionId); @@ -164,6 +174,26 @@ public void persistRelevantAppExitInfoEvent( reportPersistence.persistEvent(eventWithRolloutsState, sessionId, true); } + @RequiresApi(api = VERSION_CODES.CINNAMON_BUN) + public void persistProfilingManagerInfo( + String sessionId, + List triggers, + List applicationExitInfoList + ) { + Optional trigger = triggers.stream() + .findFirst() + .or(() -> isOom(sessionId, applicationExitInfoList) + ? Optional.of(ProfilingTrigger.TRIGGER_TYPE_OOM) : Optional.empty()); + + trigger.ifPresent(t -> reportPersistence.persistProfilingManagerInfo( + ProfilingManagerInfo.builder() + .setProfilingTrigger(ProfilingManagerInfo.ProfilingTrigger.builder() + .setTrigger(t) + .build()) + .build(), sessionId + )); + } + public void finalizeSessionWithNativeEvent( @NonNull String sessionId, @NonNull List nativeSessionFiles, @@ -426,7 +456,6 @@ private static CrashlyticsReport.ApplicationExitInfo convertApplicationExitInfo( } @VisibleForTesting - @RequiresApi(api = Build.VERSION_CODES.KITKAT) public static String convertInputStreamToString(InputStream inputStream) throws IOException { try (BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { @@ -442,7 +471,7 @@ public static String convertInputStreamToString(InputStream inputStream) throws /** Finds the first ANR ApplicationExitInfo within the session. */ @RequiresApi(api = Build.VERSION_CODES.R) private @Nullable ApplicationExitInfo findRelevantApplicationExitInfo( - String sessionId, List applicationExitInfoList) { + String sessionId, List applicationExitInfoList, Predicate skip) { long sessionStartTime = reportPersistence.getStartTimestampMillis(sessionId); // The order of ApplicationExitInfos is latest first. @@ -453,9 +482,7 @@ public static String convertInputStreamToString(InputStream inputStream) throws return null; } - // If the ApplicationExitInfo is not an ANR, but it was within the session, loop through - // all ApplicationExitInfos that fall within the session. - if (applicationExitInfo.getReason() != ApplicationExitInfo.REASON_ANR) { + if (skip.test(applicationExitInfo)) { continue; } @@ -464,4 +491,22 @@ public static String convertInputStreamToString(InputStream inputStream) throws return null; } + + @RequiresApi(api = VERSION_CODES.CINNAMON_BUN) + private boolean isOom(String sessionId, List applicationExitInfoList) { + ApplicationExitInfo relevant = + findRelevantApplicationExitInfo(sessionId, applicationExitInfoList, aei -> { + // Most devices should support REASON_LOW_MEMORY + boolean viaLowMemory = aei.getReason() == ApplicationExitInfo.REASON_LOW_MEMORY + && "OOM".equals(aei.getDescription()); + // In cases where the above isn't supported, fall back to a more primitive check + boolean viaSignaled = aei.getReason() == ApplicationExitInfo.REASON_SIGNALED + && aei.getStatus() == OsConstants.SIGKILL; + + // Skip all that aren't related to OOMs + return !viaLowMemory && !viaSignaled; + }); + + return relevant != null; + } } 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 4b9e3a1ace1..9546cc078f1 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 @@ -728,12 +728,18 @@ public static Builder builder() { @Nullable public abstract ApplicationExitInfo getAppExitInfo(); + @Nullable + public abstract ProfilingManagerInfo getProfilingManagerInfo(); + @NonNull public abstract Signal getSignal(); @NonNull public abstract List getBinaries(); + @NonNull + public abstract Builder toBuilder(); + @AutoValue public abstract static class Thread { @@ -964,6 +970,9 @@ public abstract static class Builder { @NonNull public abstract Builder setAppExitInfo(@NonNull ApplicationExitInfo value); + @NonNull + public abstract Builder setProfilingManagerInfo(@NonNull ProfilingManagerInfo value); + @NonNull public abstract Builder setSignal(@NonNull Signal value); @@ -1341,6 +1350,46 @@ public abstract static class Builder { } } + @AutoValue + public abstract static class ProfilingManagerInfo { + @NonNull + public static ProfilingManagerInfo.Builder builder() { + return new AutoValue_CrashlyticsReport_ProfilingManagerInfo.Builder(); + } + + @NonNull + public abstract ProfilingTrigger getProfilingTrigger(); + + @AutoValue.Builder + public abstract static class Builder { + @NonNull + public abstract Builder setProfilingTrigger(@NonNull ProfilingTrigger trigger); + + @NonNull + public abstract ProfilingManagerInfo build(); + } + + @AutoValue + public abstract static class ProfilingTrigger { + @NonNull + public static ProfilingTrigger.Builder builder() { + return new AutoValue_CrashlyticsReport_ProfilingManagerInfo_ProfilingTrigger.Builder(); + } + + @NonNull + public abstract int getTrigger(); + + @AutoValue.Builder + public abstract static class Builder { + @NonNull + public abstract ProfilingTrigger.Builder setTrigger(@NonNull int value); + + @NonNull + public abstract ProfilingTrigger 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 a384f669cf4..24547eb7cf0 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 @@ -21,6 +21,7 @@ import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.ApplicationExitInfo.BuildIdMappingForArch; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.CustomAttribute; +import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.ProfilingManagerInfo.ProfilingTrigger; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.Session.Event; import com.google.firebase.encoders.DataEncoder; import com.google.firebase.encoders.json.JsonDataEncoderBuilder; @@ -54,6 +55,12 @@ public String applicationExitInfoToJson( return CRASHLYTICS_REPORT_JSON_ENCODER.encode(applicationExitInfo); } + @NonNull + public String profilingManagerInfoToJson( + @NonNull CrashlyticsReport.ProfilingManagerInfo profilingManagerInfo) { + return CRASHLYTICS_REPORT_JSON_ENCODER.encode(profilingManagerInfo); + } + @NonNull public CrashlyticsReport reportFromJson(@NonNull String json) throws IOException { try (JsonReader jsonReader = new JsonReader(new StringReader(json))) { @@ -82,6 +89,16 @@ public CrashlyticsReport.ApplicationExitInfo applicationExitInfoFromJson(@NonNul } } + @NonNull + public CrashlyticsReport.ProfilingManagerInfo profilingManagerInfoFromJson(@NonNull String json) + throws IOException { + try (JsonReader reader = new JsonReader(new StringReader(json))) { + return parseProfilingManagerInfo(reader); + } catch (IllegalStateException e) { + throw new IOException(e); + } + } + @NonNull private static CrashlyticsReport parseReport(@NonNull JsonReader jsonReader) throws IOException { final CrashlyticsReport.Builder builder = CrashlyticsReport.builder(); @@ -263,6 +280,28 @@ private static CrashlyticsReport.ApplicationExitInfo parseAppExitInfo( return builder.build(); } + @NonNull + private static CrashlyticsReport.ProfilingManagerInfo parseProfilingManagerInfo( + @NonNull JsonReader jsonReader) throws IOException { + CrashlyticsReport.ProfilingManagerInfo.Builder builder = + CrashlyticsReport.ProfilingManagerInfo.builder(); + + jsonReader.beginObject(); + while (jsonReader.hasNext()) { + String name = jsonReader.nextName(); + switch (name) { + case "profilingTrigger": + builder.setProfilingTrigger(parseProfilingTrigger(jsonReader)); + break; + default: + jsonReader.skipValue(); + } + } + jsonReader.endObject(); + + return builder.build(); + } + @NonNull private static CrashlyticsReport.FilesPayload.File parseFile(@NonNull JsonReader jsonReader) throws IOException { @@ -559,6 +598,9 @@ private static Event.Application.Execution parseEventExecution(@NonNull JsonRead case "appExitInfo": builder.setAppExitInfo(parseAppExitInfo(jsonReader)); break; + case "profilingManagerInfo": + builder.setProfilingManagerInfo(parseProfilingManagerInfo(jsonReader)); + break; default: jsonReader.skipValue(); break; @@ -905,6 +947,27 @@ private static BuildIdMappingForArch parseBuildIdMappingForArch(@NonNull JsonRea return builder.build(); } + @NonNull + private static ProfilingTrigger parseProfilingTrigger(@NonNull JsonReader jsonReader) + throws IOException { + ProfilingTrigger.Builder builder = ProfilingTrigger.builder(); + + jsonReader.beginObject(); + while (jsonReader.hasNext()) { + String name = jsonReader.nextName(); + switch (name) { + case "trigger": + builder.setTrigger(jsonReader.nextInt()); + break; + default: + jsonReader.skipValue(); + } + } + jsonReader.endObject(); + + return builder.build(); + } + @NonNull private static List parseArray( @NonNull JsonReader jsonReader, @NonNull ObjectParser objectParser) throws IOException { 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 d684fda7b31..33d592c13a6 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 @@ -14,13 +14,17 @@ package com.google.firebase.crashlytics.internal.persistence; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.firebase.crashlytics.internal.Logger; import com.google.firebase.crashlytics.internal.common.CrashlyticsAppQualitySessionsSubscriber; import com.google.firebase.crashlytics.internal.common.CrashlyticsReportWithSessionId; import com.google.firebase.crashlytics.internal.metadata.UserMetadata; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; +import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.ProfilingManagerInfo; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.Session; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.Session.Event; import com.google.firebase.crashlytics.internal.model.serialization.CrashlyticsReportJsonTransform; @@ -39,6 +43,7 @@ import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.Optional; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicInteger; @@ -58,6 +63,8 @@ public class CrashlyticsReportPersistence { // 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 PROFILING_MANAGER_INFO_FILE_NAME = "profiling-manager-info"; 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"; @@ -144,6 +151,21 @@ public void persistEvent( trimEvents(sessionId, maxEventsToKeep); } + /** + * Persists Profiling Manager information for a given session. + */ + @RequiresApi(api = VERSION_CODES.CINNAMON_BUN) + public void persistProfilingManagerInfo( + @NonNull ProfilingManagerInfo profilingManagerInfo, + @NonNull String sessionId) { + try { + String json = TRANSFORM.profilingManagerInfoToJson(profilingManagerInfo); + writeTextFile(fileStore.getSessionFile(sessionId, PROFILING_MANAGER_INFO_FILE_NAME), json); + } catch (IOException e) { + Logger.getLogger().w("Could not persist Profiling Manager info " + sessionId, e); + } + } + /** * @return all open session ids, sorted in ascending order of recency (such that the first element * is the most recently-opened session ID). @@ -302,7 +324,7 @@ private void synthesizeReport(String sessionId, long sessionEndTime) { for (File eventFile : eventFiles) { try { Event event = TRANSFORM.eventFromJson(readTextFile(eventFile)); - events.add(event); + events.add(decorateWithProfilingManagerInfoIfFatal(sessionId, event)); isHighPriorityReport = isHighPriorityReport || isHighPriorityEventFile(eventFile.getName()); } catch (IOException e) { Logger.getLogger().w("Could not add event to report for " + eventFile, e); @@ -376,6 +398,35 @@ private void synthesizeReportFile( } } + private Event decorateWithProfilingManagerInfoIfFatal(String sessionId, Event event) { + if (VERSION.SDK_INT >= VERSION_CODES.CINNAMON_BUN && isFatalEvent(event)) { + Optional profilingManagerInfo = + Optional + .of(fileStore.getSessionFile(sessionId, PROFILING_MANAGER_INFO_FILE_NAME)) + .filter(File::exists) + .flatMap(f -> { + try { + return Optional.of(TRANSFORM.profilingManagerInfoFromJson(readTextFile(f))); + } catch (IOException e) { + Logger.getLogger().w("Unable to read the Profiling Manager file ", e); + return Optional.empty(); + } + }); + + return profilingManagerInfo.map(info -> event.toBuilder().setApp( + event.getApp().toBuilder().setExecution( + event.getApp().getExecution().toBuilder().setProfilingManagerInfo(info).build() + ).build() + ).build()).orElse(event); + } + + return event; + } + + private static boolean isFatalEvent(Event event) { + return event.getType().equals("crash") || event.getType().equals("ndk-crash"); + } + private static boolean isHighPriorityEventFile(@NonNull String fileName) { return fileName.startsWith(EVENT_FILE_NAME_PREFIX) && fileName.endsWith(PRIORITY_EVENT_SUFFIX); } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a3e9dd6cec6..35d80a592cf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ # it needs to match the protobuf version which grpc has transitive dependency on, which # needs to match the version of grpc that grpc-kotlin has a transitive dependency on. android-lint = "31.3.2" -androidGradlePlugin = "8.6.1" +androidGradlePlugin = "8.10.1" androidx-core = "1.13.1" androidx-test-core = "1.5.0" androidx-test-junit = "1.1.5" From d5947f52ff01c3813502bf610899dec97b5d50a7 Mon Sep 17 00:00:00 2001 From: Konstantin Mandrika Date: Mon, 22 Jun 2026 15:56:39 -0400 Subject: [PATCH 2/8] Add tests --- .../common/CrashlyticsControllerTest.java | 60 +++++++++++++++ .../SessionReportingCoordinatorTest.java | 76 +++++++++++++++++++ .../CrashlyticsReportPersistenceTest.java | 28 +++++++ .../common/CrashlyticsController.java | 3 +- .../common/SessionReportingCoordinator.java | 5 +- 5 files changed, 169 insertions(+), 3 deletions(-) diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerTest.java index 9260a7f7950..6efe00b6ddd 100644 --- a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerTest.java +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerTest.java @@ -32,6 +32,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; +import android.os.ProfilingTrigger; import androidx.test.filters.SdkSuppress; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; @@ -62,6 +63,7 @@ import java.util.TreeSet; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -569,6 +571,64 @@ public void testUploadDisabledThenEnabled() throws Exception { verifyNoMoreInteractions(mockSessionReportingCoordinator); } + @SdkSuppress(minSdkVersion = 37) // ProfilingManager + @Test + public void testWritingProfilingManagerTriggerAnomaly() { + LogFileManager logFileManager = new LogFileManager(testFileStore); + CrashlyticsController controller = builder() + .setLogFileManager(logFileManager) + .build(); + + controller.writeTriggerTypeFile("sessionId", ProfilingTrigger.TRIGGER_TYPE_ANOMALY); + + List triggers = testFileStore.getSessionFiles("sessionId", (dir, name) -> List.of( + "trigger-type-anomaly", + "trigger-type-oom" + ).contains(name)).stream() + .map(triggerFile -> { + switch (triggerFile.getName()) { + case "trigger-type-anomaly": return ProfilingTrigger.TRIGGER_TYPE_ANOMALY; + case "trigger-type-oom": return ProfilingTrigger.TRIGGER_TYPE_OOM; + } + + return ProfilingTrigger.TRIGGER_TYPE_NONE; + }) + .filter(trigger -> trigger != ProfilingTrigger.TRIGGER_TYPE_NONE) + .collect(Collectors.toList()); + + assertFalse(triggers.isEmpty()); + assertEquals(triggers, List.of(ProfilingTrigger.TRIGGER_TYPE_ANOMALY)); + } + + @SdkSuppress(minSdkVersion = 37) // ProfilingTrigger + @Test + public void testWritingProfilingManagerTriggerOom() { + LogFileManager logFileManager = new LogFileManager(testFileStore); + CrashlyticsController controller = builder() + .setLogFileManager(logFileManager) + .build(); + + controller.writeTriggerTypeFile("sessionId", ProfilingTrigger.TRIGGER_TYPE_OOM); + + List triggers = testFileStore.getSessionFiles("sessionId", (dir, name) -> List.of( + "trigger-type-anomaly", + "trigger-type-oom" + ).contains(name)).stream() + .map(triggerFile -> { + switch (triggerFile.getName()) { + case "trigger-type-anomaly": return ProfilingTrigger.TRIGGER_TYPE_ANOMALY; + case "trigger-type-oom": return ProfilingTrigger.TRIGGER_TYPE_OOM; + } + + return ProfilingTrigger.TRIGGER_TYPE_NONE; + }) + .filter(trigger -> trigger != ProfilingTrigger.TRIGGER_TYPE_NONE) + .collect(Collectors.toList()); + + assertFalse(triggers.isEmpty()); + assertEquals(triggers, List.of(ProfilingTrigger.TRIGGER_TYPE_OOM)); + } + @SdkSuppress(minSdkVersion = 30) // ApplicationExitInfo @Test public void testFatalEvent_sendsAppExceptionEvent() throws Exception { diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinatorTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinatorTest.java index 0f5cf09233b..840330be56a 100644 --- a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinatorTest.java +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinatorTest.java @@ -31,6 +31,9 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.ApplicationExitInfo; +import android.os.Parcel; +import android.system.OsConstants; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; import com.google.firebase.concurrent.TestOnlyExecutors; @@ -612,6 +615,79 @@ public void testRemoveAllReports_deletesPersistedReports() { verify(reportPersistence).deleteAllReports(); } + @Test + public void testIsOom_skipsAllIrrelevantAppExitInfos() { + ApplicationExitInfo nonOom1 = makeApplicationExitInfo( + ApplicationExitInfo.REASON_ANR, 0, 0); + ApplicationExitInfo nonOom2 = makeApplicationExitInfo( + ApplicationExitInfo.REASON_CRASH, 0, 0); + ApplicationExitInfo nonOom3 = makeApplicationExitInfo( + ApplicationExitInfo.REASON_SIGNALED, 0, 0); + + boolean isOom = reportingCoordinator.isOom("sessionId", List.of( + nonOom1, + nonOom2, + nonOom3 + )); + + assertFalse(isOom); + } + + @Test + public void testIsOom_returnsTrueOnReasonSignaled() { + ApplicationExitInfo oom = makeApplicationExitInfo(ApplicationExitInfo.REASON_SIGNALED, 0, + OsConstants.SIGKILL); + ApplicationExitInfo nonOom = makeApplicationExitInfo( + ApplicationExitInfo.REASON_ANR, 0, 0); + + boolean isOom = reportingCoordinator.isOom("sessionId", List.of( + nonOom, + oom + )); + + assertTrue(isOom); + } + + @Test + public void testIsOom_returnTrueOnReasonLowMemory() { + ApplicationExitInfo oom = makeApplicationExitInfo( + ApplicationExitInfo.REASON_LOW_MEMORY, /*SUBREASON_OOM_KILL=*/30, 0); + ApplicationExitInfo nonOom = makeApplicationExitInfo( + ApplicationExitInfo.REASON_ANR, 0, 0); + + boolean isOom = reportingCoordinator.isOom("sessionId", List.of( + nonOom, + oom + )); + + assertTrue(isOom); + } + + private ApplicationExitInfo makeApplicationExitInfo( + int reason, int subreason, int status) { + Parcel dest = Parcel.obtain(); + + dest.writeInt(1); + dest.writeInt(1); + dest.writeInt(1); + dest.writeInt(1); + dest.writeString("process"); + dest.writeString("com.test"); + dest.writeInt(1); + dest.writeInt(reason); + dest.writeInt(subreason); + dest.writeInt(status); + dest.writeInt(1); + dest.writeLong(1L); + dest.writeLong(1L); + dest.writeLong(1L); + dest.writeString(""); + dest.writeByteArray(new byte[]{}); + dest.setDataPosition(0); + + return ApplicationExitInfo.CREATOR.createFromParcel(dest); + } + private void addCustomKeysToUserMetadata(Map customKeys) throws Exception { reportMetadata.setCustomKeys(customKeys); for (Map.Entry entry : customKeys.entrySet()) { diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/persistence/CrashlyticsReportPersistenceTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/persistence/CrashlyticsReportPersistenceTest.java index dbd3f8e3015..6e7eab249d9 100644 --- a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/persistence/CrashlyticsReportPersistenceTest.java +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/persistence/CrashlyticsReportPersistenceTest.java @@ -21,10 +21,13 @@ import static org.mockito.Mockito.when; import androidx.annotation.Nullable; +import androidx.test.filters.SdkSuppress; import com.google.firebase.crashlytics.internal.CrashlyticsTestCase; import com.google.firebase.crashlytics.internal.common.CrashlyticsAppQualitySessionsSubscriber; import com.google.firebase.crashlytics.internal.common.CrashlyticsReportWithSessionId; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; +import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.ProfilingManagerInfo; +import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.ProfilingManagerInfo.ProfilingTrigger; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.Session; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.Session.Application; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.Session.Event; @@ -178,6 +181,31 @@ public void testLoadFinalizedReports_reportThenEvent_returnsReportWithEvent() { finalizedReport); } + @SdkSuppress(minSdkVersion = 37) // ProfilingManager, ProfilingTrigger + @Test + public void testLoadFinalizedReports_reportWithProfilingManagerTrigger_containsTriggers() { + String sessionId = "sessionId"; + CrashlyticsReport report = makeTestReport(sessionId); + CrashlyticsReport.Session.Event event = makeTestEvent("crash", "reason"); + + ProfilingManagerInfo pmi = ProfilingManagerInfo.builder() + .setProfilingTrigger(ProfilingTrigger.builder() + .setTrigger(android.os.ProfilingTrigger.TRIGGER_TYPE_ANOMALY) + .build()) + .build(); + + reportPersistence.persistReport(report); + reportPersistence.persistEvent(event, sessionId); + reportPersistence.persistProfilingManagerInfo(pmi, sessionId); + reportPersistence.finalizeReports("skipSession", System.currentTimeMillis()); + + List reports = reportPersistence.loadFinalizedReports(); + + assertEquals(1, reports.size()); + assertEquals(1, reports.get(0).getReport().getSession().getEvents().size()); + assertEquals(pmi, reports.get(0).getReport().getSession().getEvents().get(0).getApp().getExecution().getProfilingManagerInfo()); + } + @Test public void testLoadFinalizedReports_reportThenMultipleEvents_returnsReportWithMultipleEvents() { final String sessionId = "testSession"; 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 663d83998d9..a60b8aa87b0 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 @@ -1009,7 +1009,8 @@ private void registerProfilingManagerListener(String sessionId, @Background Exec } @RequiresApi(api = VERSION_CODES.CINNAMON_BUN) - private void writeTriggerTypeFile(String sessionId, int triggerType) { + @VisibleForTesting + void writeTriggerTypeFile(String sessionId, int triggerType) { String triggerFilename = triggerType == ProfilingTrigger.TRIGGER_TYPE_ANOMALY ? TRIGGER_TYPE_ANOMALY_FILENAME : triggerType == ProfilingTrigger.TRIGGER_TYPE_OOM ? TRIGGER_TYPE_OOM_FILENAME : 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 ffbda47736b..c2cd3d8e6c5 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 @@ -493,12 +493,13 @@ public static String convertInputStreamToString(InputStream inputStream) throws } @RequiresApi(api = VERSION_CODES.CINNAMON_BUN) - private boolean isOom(String sessionId, List applicationExitInfoList) { + @VisibleForTesting + boolean isOom(String sessionId, List applicationExitInfoList) { ApplicationExitInfo relevant = findRelevantApplicationExitInfo(sessionId, applicationExitInfoList, aei -> { // Most devices should support REASON_LOW_MEMORY boolean viaLowMemory = aei.getReason() == ApplicationExitInfo.REASON_LOW_MEMORY - && "OOM".equals(aei.getDescription()); + && aei.getDescription() != null && aei.getDescription().contains("OOM"); // In cases where the above isn't supported, fall back to a more primitive check boolean viaSignaled = aei.getReason() == ApplicationExitInfo.REASON_SIGNALED && aei.getStatus() == OsConstants.SIGKILL; From 951ee8baee425d86ebfe04e5068bd6cb24f7e16b Mon Sep 17 00:00:00 2001 From: Konstantin Mandrika Date: Mon, 22 Jun 2026 16:08:09 -0400 Subject: [PATCH 3/8] Formatting --- .../common/CrashlyticsControllerTest.java | 74 +++++++++-------- .../SessionReportingCoordinatorTest.java | 48 ++++------- .../CrashlyticsReportPersistenceTest.java | 24 ++++-- .../common/CrashlyticsController.java | 66 ++++++++------- .../common/SessionReportingCoordinator.java | 80 +++++++++++-------- .../CrashlyticsReportJsonTransform.java | 2 +- .../CrashlyticsReportPersistence.java | 43 +++++----- 7 files changed, 187 insertions(+), 150 deletions(-) diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerTest.java index 6efe00b6ddd..ae67f55313e 100644 --- a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerTest.java +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerTest.java @@ -575,26 +575,29 @@ public void testUploadDisabledThenEnabled() throws Exception { @Test public void testWritingProfilingManagerTriggerAnomaly() { LogFileManager logFileManager = new LogFileManager(testFileStore); - CrashlyticsController controller = builder() - .setLogFileManager(logFileManager) - .build(); + CrashlyticsController controller = builder().setLogFileManager(logFileManager).build(); controller.writeTriggerTypeFile("sessionId", ProfilingTrigger.TRIGGER_TYPE_ANOMALY); - List triggers = testFileStore.getSessionFiles("sessionId", (dir, name) -> List.of( - "trigger-type-anomaly", - "trigger-type-oom" - ).contains(name)).stream() - .map(triggerFile -> { - switch (triggerFile.getName()) { - case "trigger-type-anomaly": return ProfilingTrigger.TRIGGER_TYPE_ANOMALY; - case "trigger-type-oom": return ProfilingTrigger.TRIGGER_TYPE_OOM; - } - - return ProfilingTrigger.TRIGGER_TYPE_NONE; - }) - .filter(trigger -> trigger != ProfilingTrigger.TRIGGER_TYPE_NONE) - .collect(Collectors.toList()); + List triggers = + testFileStore + .getSessionFiles( + "sessionId", + (dir, name) -> List.of("trigger-type-anomaly", "trigger-type-oom").contains(name)) + .stream() + .map( + triggerFile -> { + switch (triggerFile.getName()) { + case "trigger-type-anomaly": + return ProfilingTrigger.TRIGGER_TYPE_ANOMALY; + case "trigger-type-oom": + return ProfilingTrigger.TRIGGER_TYPE_OOM; + } + + return ProfilingTrigger.TRIGGER_TYPE_NONE; + }) + .filter(trigger -> trigger != ProfilingTrigger.TRIGGER_TYPE_NONE) + .collect(Collectors.toList()); assertFalse(triggers.isEmpty()); assertEquals(triggers, List.of(ProfilingTrigger.TRIGGER_TYPE_ANOMALY)); @@ -604,26 +607,29 @@ public void testWritingProfilingManagerTriggerAnomaly() { @Test public void testWritingProfilingManagerTriggerOom() { LogFileManager logFileManager = new LogFileManager(testFileStore); - CrashlyticsController controller = builder() - .setLogFileManager(logFileManager) - .build(); + CrashlyticsController controller = builder().setLogFileManager(logFileManager).build(); controller.writeTriggerTypeFile("sessionId", ProfilingTrigger.TRIGGER_TYPE_OOM); - List triggers = testFileStore.getSessionFiles("sessionId", (dir, name) -> List.of( - "trigger-type-anomaly", - "trigger-type-oom" - ).contains(name)).stream() - .map(triggerFile -> { - switch (triggerFile.getName()) { - case "trigger-type-anomaly": return ProfilingTrigger.TRIGGER_TYPE_ANOMALY; - case "trigger-type-oom": return ProfilingTrigger.TRIGGER_TYPE_OOM; - } - - return ProfilingTrigger.TRIGGER_TYPE_NONE; - }) - .filter(trigger -> trigger != ProfilingTrigger.TRIGGER_TYPE_NONE) - .collect(Collectors.toList()); + List triggers = + testFileStore + .getSessionFiles( + "sessionId", + (dir, name) -> List.of("trigger-type-anomaly", "trigger-type-oom").contains(name)) + .stream() + .map( + triggerFile -> { + switch (triggerFile.getName()) { + case "trigger-type-anomaly": + return ProfilingTrigger.TRIGGER_TYPE_ANOMALY; + case "trigger-type-oom": + return ProfilingTrigger.TRIGGER_TYPE_OOM; + } + + return ProfilingTrigger.TRIGGER_TYPE_NONE; + }) + .filter(trigger -> trigger != ProfilingTrigger.TRIGGER_TYPE_NONE) + .collect(Collectors.toList()); assertFalse(triggers.isEmpty()); assertEquals(triggers, List.of(ProfilingTrigger.TRIGGER_TYPE_OOM)); diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinatorTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinatorTest.java index 840330be56a..fa068b2266c 100644 --- a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinatorTest.java +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinatorTest.java @@ -617,54 +617,40 @@ public void testRemoveAllReports_deletesPersistedReports() { @Test public void testIsOom_skipsAllIrrelevantAppExitInfos() { - ApplicationExitInfo nonOom1 = makeApplicationExitInfo( - ApplicationExitInfo.REASON_ANR, 0, 0); - ApplicationExitInfo nonOom2 = makeApplicationExitInfo( - ApplicationExitInfo.REASON_CRASH, 0, 0); - ApplicationExitInfo nonOom3 = makeApplicationExitInfo( - ApplicationExitInfo.REASON_SIGNALED, 0, 0); - - boolean isOom = reportingCoordinator.isOom("sessionId", List.of( - nonOom1, - nonOom2, - nonOom3 - )); + ApplicationExitInfo nonOom1 = makeApplicationExitInfo(ApplicationExitInfo.REASON_ANR, 0, 0); + ApplicationExitInfo nonOom2 = makeApplicationExitInfo(ApplicationExitInfo.REASON_CRASH, 0, 0); + ApplicationExitInfo nonOom3 = + makeApplicationExitInfo(ApplicationExitInfo.REASON_SIGNALED, 0, 0); + + boolean isOom = reportingCoordinator.isOom("sessionId", List.of(nonOom1, nonOom2, nonOom3)); assertFalse(isOom); } @Test public void testIsOom_returnsTrueOnReasonSignaled() { - ApplicationExitInfo oom = makeApplicationExitInfo(ApplicationExitInfo.REASON_SIGNALED, 0, - OsConstants.SIGKILL); - ApplicationExitInfo nonOom = makeApplicationExitInfo( - ApplicationExitInfo.REASON_ANR, 0, 0); + ApplicationExitInfo oom = + makeApplicationExitInfo(ApplicationExitInfo.REASON_SIGNALED, 0, OsConstants.SIGKILL); + ApplicationExitInfo nonOom = makeApplicationExitInfo(ApplicationExitInfo.REASON_ANR, 0, 0); - boolean isOom = reportingCoordinator.isOom("sessionId", List.of( - nonOom, - oom - )); + boolean isOom = reportingCoordinator.isOom("sessionId", List.of(nonOom, oom)); assertTrue(isOom); } @Test public void testIsOom_returnTrueOnReasonLowMemory() { - ApplicationExitInfo oom = makeApplicationExitInfo( - ApplicationExitInfo.REASON_LOW_MEMORY, /*SUBREASON_OOM_KILL=*/30, 0); - ApplicationExitInfo nonOom = makeApplicationExitInfo( - ApplicationExitInfo.REASON_ANR, 0, 0); + ApplicationExitInfo oom = + makeApplicationExitInfo( + ApplicationExitInfo.REASON_LOW_MEMORY, /* SUBREASON_OOM_KILL= */ 30, 0); + ApplicationExitInfo nonOom = makeApplicationExitInfo(ApplicationExitInfo.REASON_ANR, 0, 0); - boolean isOom = reportingCoordinator.isOom("sessionId", List.of( - nonOom, - oom - )); + boolean isOom = reportingCoordinator.isOom("sessionId", List.of(nonOom, oom)); assertTrue(isOom); } - private ApplicationExitInfo makeApplicationExitInfo( - int reason, int subreason, int status) { + private ApplicationExitInfo makeApplicationExitInfo(int reason, int subreason, int status) { Parcel dest = Parcel.obtain(); dest.writeInt(1); @@ -682,7 +668,7 @@ private ApplicationExitInfo makeApplicationExitInfo( dest.writeLong(1L); dest.writeLong(1L); dest.writeString(""); - dest.writeByteArray(new byte[]{}); + dest.writeByteArray(new byte[] {}); dest.setDataPosition(0); return ApplicationExitInfo.CREATOR.createFromParcel(dest); diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/persistence/CrashlyticsReportPersistenceTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/persistence/CrashlyticsReportPersistenceTest.java index 6e7eab249d9..eb15a3eeaea 100644 --- a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/persistence/CrashlyticsReportPersistenceTest.java +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/persistence/CrashlyticsReportPersistenceTest.java @@ -188,11 +188,13 @@ public void testLoadFinalizedReports_reportWithProfilingManagerTrigger_containsT CrashlyticsReport report = makeTestReport(sessionId); CrashlyticsReport.Session.Event event = makeTestEvent("crash", "reason"); - ProfilingManagerInfo pmi = ProfilingManagerInfo.builder() - .setProfilingTrigger(ProfilingTrigger.builder() - .setTrigger(android.os.ProfilingTrigger.TRIGGER_TYPE_ANOMALY) - .build()) - .build(); + ProfilingManagerInfo pmi = + ProfilingManagerInfo.builder() + .setProfilingTrigger( + ProfilingTrigger.builder() + .setTrigger(android.os.ProfilingTrigger.TRIGGER_TYPE_ANOMALY) + .build()) + .build(); reportPersistence.persistReport(report); reportPersistence.persistEvent(event, sessionId); @@ -203,7 +205,17 @@ public void testLoadFinalizedReports_reportWithProfilingManagerTrigger_containsT assertEquals(1, reports.size()); assertEquals(1, reports.get(0).getReport().getSession().getEvents().size()); - assertEquals(pmi, reports.get(0).getReport().getSession().getEvents().get(0).getApp().getExecution().getProfilingManagerInfo()); + assertEquals( + pmi, + reports + .get(0) + .getReport() + .getSession() + .getEvents() + .get(0) + .getApp() + .getExecution() + .getProfilingManagerInfo()); } @Test 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 a60b8aa87b0..52473f623f9 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 @@ -962,59 +962,71 @@ private void writeApplicationExitInfoEventIfRelevant(String sessionId) { .v("ANR feature enabled, but device is API " + android.os.Build.VERSION.SDK_INT); } } + // endregion // region ProfilingManager @RequiresApi(api = VERSION_CODES.CINNAMON_BUN) private void writeProfilingManagerInfo(String sessionId) { - ActivityManager manager = - (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); // For anomaly triggers, there should be a file written to disk by the registered consumer. // However, for OOMs, it is less guaranteed that the process had enough resources to write the // corresponding file. This is ok because we can check ApplicationExitInfo to see if an OOM // occurred. - List triggers = fileStore.getSessionFiles(sessionId, (dir, name) -> List.of( - TRIGGER_TYPE_ANOMALY_FILENAME, - TRIGGER_TYPE_OOM_FILENAME - ).contains(name)).stream() - .map(triggerFile -> { - switch (triggerFile.getName()) { - case TRIGGER_TYPE_ANOMALY_FILENAME: return ProfilingTrigger.TRIGGER_TYPE_ANOMALY; - case TRIGGER_TYPE_OOM_FILENAME: return ProfilingTrigger.TRIGGER_TYPE_OOM; - } + List triggers = + fileStore + .getSessionFiles( + sessionId, + (dir, name) -> + List.of(TRIGGER_TYPE_ANOMALY_FILENAME, TRIGGER_TYPE_OOM_FILENAME) + .contains(name)) + .stream() + .map( + triggerFile -> { + switch (triggerFile.getName()) { + case TRIGGER_TYPE_ANOMALY_FILENAME: + return ProfilingTrigger.TRIGGER_TYPE_ANOMALY; + case TRIGGER_TYPE_OOM_FILENAME: + return ProfilingTrigger.TRIGGER_TYPE_OOM; + } - return ProfilingTrigger.TRIGGER_TYPE_NONE; - }) - .filter(trigger -> trigger != ProfilingTrigger.TRIGGER_TYPE_NONE) - .collect(Collectors.toList()); + return ProfilingTrigger.TRIGGER_TYPE_NONE; + }) + .filter(trigger -> trigger != ProfilingTrigger.TRIGGER_TYPE_NONE) + .collect(Collectors.toList()); List appExits = manager.getHistoricalProcessExitReasons(null, 0, 0); reportingCoordinator.persistProfilingManagerInfo(sessionId, triggers, appExits); } - + @SuppressLint("WrongConstant") // TRIGGER_TYPE_OOM, TRIGGER_TYPE_ANOMALY @RequiresApi(api = VERSION_CODES.CINNAMON_BUN) - private void registerProfilingManagerListener(String sessionId, @Background Executor backgroundExecutor) { + private void registerProfilingManagerListener( + String sessionId, @Background Executor backgroundExecutor) { ProfilingManager profilingManager = context.getSystemService(ProfilingManager.class); - profilingManager.addProfilingTriggers(List.of( - new ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_OOM).build(), - new ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_ANOMALY).build() - )); - profilingManager.registerForAllProfilingResults(backgroundExecutor, (result) -> { - writeTriggerTypeFile(sessionId, result.getTriggerType()); - }); + profilingManager.addProfilingTriggers( + List.of( + new ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_OOM).build(), + new ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_ANOMALY).build())); + profilingManager.registerForAllProfilingResults( + backgroundExecutor, + (result) -> { + writeTriggerTypeFile(sessionId, result.getTriggerType()); + }); } @RequiresApi(api = VERSION_CODES.CINNAMON_BUN) @VisibleForTesting void writeTriggerTypeFile(String sessionId, int triggerType) { String triggerFilename = - triggerType == ProfilingTrigger.TRIGGER_TYPE_ANOMALY ? TRIGGER_TYPE_ANOMALY_FILENAME : - triggerType == ProfilingTrigger.TRIGGER_TYPE_OOM ? TRIGGER_TYPE_OOM_FILENAME : - "trigger-type-unknown"; + triggerType == ProfilingTrigger.TRIGGER_TYPE_ANOMALY + ? TRIGGER_TYPE_ANOMALY_FILENAME + : triggerType == ProfilingTrigger.TRIGGER_TYPE_OOM + ? TRIGGER_TYPE_OOM_FILENAME + : "trigger-type-unknown"; try { if (!fileStore.getSessionFile(sessionId, triggerFilename).createNewFile()) { 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 c2cd3d8e6c5..09b972daeac 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 @@ -150,11 +150,15 @@ public void persistRelevantAppExitInfoEvent( UserMetadata userMetadataForSession) { ApplicationExitInfo relevantApplicationExitInfo = - findRelevantApplicationExitInfo(sessionId, applicationExitInfoList, aei -> { - // If the ApplicationExitInfo is not an ANR, but it was within the session, loop through - // all ApplicationExitInfos that fall within the session. - return aei.getReason() != ApplicationExitInfo.REASON_ANR; - }); + findRelevantApplicationExitInfo( + sessionId, + applicationExitInfoList, + aei -> { + // If the ApplicationExitInfo is not an ANR, but it was within the session, loop + // through + // all ApplicationExitInfos that fall within the session. + return aei.getReason() != ApplicationExitInfo.REASON_ANR; + }); if (relevantApplicationExitInfo == null) { Logger.getLogger().v("No relevant ApplicationExitInfo occurred during session: " + sessionId); @@ -176,22 +180,24 @@ public void persistRelevantAppExitInfoEvent( @RequiresApi(api = VERSION_CODES.CINNAMON_BUN) public void persistProfilingManagerInfo( - String sessionId, - List triggers, - List applicationExitInfoList - ) { - Optional trigger = triggers.stream() - .findFirst() - .or(() -> isOom(sessionId, applicationExitInfoList) - ? Optional.of(ProfilingTrigger.TRIGGER_TYPE_OOM) : Optional.empty()); - - trigger.ifPresent(t -> reportPersistence.persistProfilingManagerInfo( - ProfilingManagerInfo.builder() - .setProfilingTrigger(ProfilingManagerInfo.ProfilingTrigger.builder() - .setTrigger(t) - .build()) - .build(), sessionId - )); + String sessionId, List triggers, List applicationExitInfoList) { + Optional trigger = + triggers.stream() + .findFirst() + .or( + () -> + isOom(sessionId, applicationExitInfoList) + ? Optional.of(ProfilingTrigger.TRIGGER_TYPE_OOM) + : Optional.empty()); + + trigger.ifPresent( + t -> + reportPersistence.persistProfilingManagerInfo( + ProfilingManagerInfo.builder() + .setProfilingTrigger( + ProfilingManagerInfo.ProfilingTrigger.builder().setTrigger(t).build()) + .build(), + sessionId)); } public void finalizeSessionWithNativeEvent( @@ -471,7 +477,9 @@ public static String convertInputStreamToString(InputStream inputStream) throws /** Finds the first ANR ApplicationExitInfo within the session. */ @RequiresApi(api = Build.VERSION_CODES.R) private @Nullable ApplicationExitInfo findRelevantApplicationExitInfo( - String sessionId, List applicationExitInfoList, Predicate skip) { + String sessionId, + List applicationExitInfoList, + Predicate skip) { long sessionStartTime = reportPersistence.getStartTimestampMillis(sessionId); // The order of ApplicationExitInfos is latest first. @@ -496,17 +504,23 @@ public static String convertInputStreamToString(InputStream inputStream) throws @VisibleForTesting boolean isOom(String sessionId, List applicationExitInfoList) { ApplicationExitInfo relevant = - findRelevantApplicationExitInfo(sessionId, applicationExitInfoList, aei -> { - // Most devices should support REASON_LOW_MEMORY - boolean viaLowMemory = aei.getReason() == ApplicationExitInfo.REASON_LOW_MEMORY - && aei.getDescription() != null && aei.getDescription().contains("OOM"); - // In cases where the above isn't supported, fall back to a more primitive check - boolean viaSignaled = aei.getReason() == ApplicationExitInfo.REASON_SIGNALED - && aei.getStatus() == OsConstants.SIGKILL; - - // Skip all that aren't related to OOMs - return !viaLowMemory && !viaSignaled; - }); + findRelevantApplicationExitInfo( + sessionId, + applicationExitInfoList, + aei -> { + // Most devices should support REASON_LOW_MEMORY + boolean viaLowMemory = + aei.getReason() == ApplicationExitInfo.REASON_LOW_MEMORY + && aei.getDescription() != null + && aei.getDescription().contains("OOM"); + // In cases where the above isn't supported, fall back to a more primitive check + boolean viaSignaled = + aei.getReason() == ApplicationExitInfo.REASON_SIGNALED + && aei.getStatus() == OsConstants.SIGKILL; + + // Skip all that aren't related to OOMs + return !viaLowMemory && !viaSignaled; + }); return relevant != null; } 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 24547eb7cf0..a29a3078c74 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 @@ -91,7 +91,7 @@ public CrashlyticsReport.ApplicationExitInfo applicationExitInfoFromJson(@NonNul @NonNull public CrashlyticsReport.ProfilingManagerInfo profilingManagerInfoFromJson(@NonNull String json) - throws IOException { + throws IOException { try (JsonReader reader = new JsonReader(new StringReader(json))) { return parseProfilingManagerInfo(reader); } catch (IllegalStateException e) { 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 33d592c13a6..27353e354fd 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 @@ -156,8 +156,7 @@ public void persistEvent( */ @RequiresApi(api = VERSION_CODES.CINNAMON_BUN) public void persistProfilingManagerInfo( - @NonNull ProfilingManagerInfo profilingManagerInfo, - @NonNull String sessionId) { + @NonNull ProfilingManagerInfo profilingManagerInfo, @NonNull String sessionId) { try { String json = TRANSFORM.profilingManagerInfoToJson(profilingManagerInfo); writeTextFile(fileStore.getSessionFile(sessionId, PROFILING_MANAGER_INFO_FILE_NAME), json); @@ -401,23 +400,31 @@ private void synthesizeReportFile( private Event decorateWithProfilingManagerInfoIfFatal(String sessionId, Event event) { if (VERSION.SDK_INT >= VERSION_CODES.CINNAMON_BUN && isFatalEvent(event)) { Optional profilingManagerInfo = - Optional - .of(fileStore.getSessionFile(sessionId, PROFILING_MANAGER_INFO_FILE_NAME)) + Optional.of(fileStore.getSessionFile(sessionId, PROFILING_MANAGER_INFO_FILE_NAME)) .filter(File::exists) - .flatMap(f -> { - try { - return Optional.of(TRANSFORM.profilingManagerInfoFromJson(readTextFile(f))); - } catch (IOException e) { - Logger.getLogger().w("Unable to read the Profiling Manager file ", e); - return Optional.empty(); - } - }); - - return profilingManagerInfo.map(info -> event.toBuilder().setApp( - event.getApp().toBuilder().setExecution( - event.getApp().getExecution().toBuilder().setProfilingManagerInfo(info).build() - ).build() - ).build()).orElse(event); + .flatMap( + f -> { + try { + return Optional.of(TRANSFORM.profilingManagerInfoFromJson(readTextFile(f))); + } catch (IOException e) { + Logger.getLogger().w("Unable to read the Profiling Manager file ", e); + return Optional.empty(); + } + }); + + return profilingManagerInfo + .map( + info -> + event.toBuilder() + .setApp( + event.getApp().toBuilder() + .setExecution( + event.getApp().getExecution().toBuilder() + .setProfilingManagerInfo(info) + .build()) + .build()) + .build()) + .orElse(event); } return event; From d81d40a45e00eb954ec61c2c70a8e2e00a24fedf Mon Sep 17 00:00:00 2001 From: Konstantin Mandrika Date: Wed, 24 Jun 2026 09:10:48 -0400 Subject: [PATCH 4/8] Add changelog entry --- firebase-crashlytics/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/firebase-crashlytics/CHANGELOG.md b/firebase-crashlytics/CHANGELOG.md index 2c75af37754..1b8b1672460 100644 --- a/firebase-crashlytics/CHANGELOG.md +++ b/firebase-crashlytics/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- [feature] Added OOM and Anomaly trigger collection for the ProfilingManager API [#8343] + # 20.0.6 - [fixed] Fixed race condition that caused logs from background threads to not be attached to From 773fe33b3cf101161d8732bb06939274d5c0cceb Mon Sep 17 00:00:00 2001 From: Konstantin Mandrika Date: Wed, 24 Jun 2026 09:20:41 -0400 Subject: [PATCH 5/8] Fix android path logic to work for android 37 --- .../main/java/com/google/firebase/gradle/plugins/SdkUtil.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/SdkUtil.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/SdkUtil.kt index b5b283fe247..3f3ec25b2eb 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/plugins/SdkUtil.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/SdkUtil.kt @@ -47,7 +47,10 @@ val Project.sdkDir: File val Project.androidJar: File? get() { val android = project.extensions.findByType(LibraryExtension::class.java) ?: return null - return File(sdkDir, String.format("/platforms/%s/android.jar", android.compileSdkVersion)) + return android.bootClasspath.firstOrNull { it.name == "android.jar" } ?: File( + sdkDir, + String.format("/platforms/%s/android.jar", android.compileSdkVersion) + ) } /** From 8afd586503b00ede2242175bb7bcfdd9e50a5cc9 Mon Sep 17 00:00:00 2001 From: Konstantin Mandrika Date: Wed, 24 Jun 2026 09:48:43 -0400 Subject: [PATCH 6/8] Reverse the logic for findRelevant... --- .../common/SessionReportingCoordinator.java | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) 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 09b972daeac..e6128f509fe 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 @@ -154,10 +154,8 @@ public void persistRelevantAppExitInfoEvent( sessionId, applicationExitInfoList, aei -> { - // If the ApplicationExitInfo is not an ANR, but it was within the session, loop - // through - // all ApplicationExitInfos that fall within the session. - return aei.getReason() != ApplicationExitInfo.REASON_ANR; + // Get the first ANR ApplicationExitInfo that occurred within this session + return aei.getReason() == ApplicationExitInfo.REASON_ANR; }); if (relevantApplicationExitInfo == null) { @@ -479,22 +477,17 @@ public static String convertInputStreamToString(InputStream inputStream) throws private @Nullable ApplicationExitInfo findRelevantApplicationExitInfo( String sessionId, List applicationExitInfoList, - Predicate skip) { + Predicate predicate) { long sessionStartTime = reportPersistence.getStartTimestampMillis(sessionId); // The order of ApplicationExitInfos is latest first. // Java For-each preserves the order. for (ApplicationExitInfo applicationExitInfo : applicationExitInfoList) { // ApplicationExitInfo did not occur during the session. - if (applicationExitInfo.getTimestamp() < sessionStartTime) { - return null; + if (applicationExitInfo.getTimestamp() >= sessionStartTime + && predicate.test(applicationExitInfo)) { + return applicationExitInfo; } - - if (skip.test(applicationExitInfo)) { - continue; - } - - return applicationExitInfo; } return null; @@ -518,8 +511,8 @@ boolean isOom(String sessionId, List applicationExitInfoLis aei.getReason() == ApplicationExitInfo.REASON_SIGNALED && aei.getStatus() == OsConstants.SIGKILL; - // Skip all that aren't related to OOMs - return !viaLowMemory && !viaSignaled; + // Get the first instance that is an OOM + return viaLowMemory || viaSignaled; }); return relevant != null; From 33ef396c2952fc89b3b2e748b176724e6c0ab161 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Wed, 24 Jun 2026 10:42:32 -0400 Subject: [PATCH 7/8] Fix robolectric tests --- firebase-crashlytics/firebase-crashlytics.gradle | 2 +- .../common/CrashlyticsControllerRobolectricTest.java | 8 +++++++- .../common/DataCollectionArbiterRobolectricTest.java | 1 + .../SessionReportingCoordinatorRobolectricTest.java | 10 ++++++++-- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/firebase-crashlytics/firebase-crashlytics.gradle b/firebase-crashlytics/firebase-crashlytics.gradle index d596826e497..8081045143d 100644 --- a/firebase-crashlytics/firebase-crashlytics.gradle +++ b/firebase-crashlytics/firebase-crashlytics.gradle @@ -99,7 +99,7 @@ dependencies { testImplementation(libs.androidx.test.runner) testImplementation(libs.junit) testImplementation(libs.mockito.core) - testImplementation(libs.robolectric) + testImplementation("org.robolectric:robolectric:4.16.1") testImplementation(libs.truth) testImplementation(project(":integ-testing")) 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..469c920f13c 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 @@ -53,6 +53,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowActivityManager; @RunWith(RobolectricTestRunner.class) public class CrashlyticsControllerRobolectricTest { @@ -205,7 +206,12 @@ private void addAppExitInfo(int reason) { activityManager.getRunningAppProcesses().get(0); shadowOf(activityManager) .addApplicationExitInfo( - runningAppProcessInfo.processName, runningAppProcessInfo.pid, reason, 1); + ShadowActivityManager.ApplicationExitInfoBuilder.newBuilder() + .setRealUid(runningAppProcessInfo.pid) + .setProcessName(runningAppProcessInfo.processName) + .setReason(reason) + .setStatus(1) + .build()); } private List getApplicationExitInfoList() { diff --git a/firebase-crashlytics/src/test/java/com/google/firebase/crashlytics/internal/common/DataCollectionArbiterRobolectricTest.java b/firebase-crashlytics/src/test/java/com/google/firebase/crashlytics/internal/common/DataCollectionArbiterRobolectricTest.java index e19fee9753c..e431a71ea35 100644 --- a/firebase-crashlytics/src/test/java/com/google/firebase/crashlytics/internal/common/DataCollectionArbiterRobolectricTest.java +++ b/firebase-crashlytics/src/test/java/com/google/firebase/crashlytics/internal/common/DataCollectionArbiterRobolectricTest.java @@ -23,6 +23,7 @@ import android.content.Context; import android.os.Bundle; import com.google.firebase.FirebaseApp; +import java.util.Optional; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; 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..5949b81e5a3 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 @@ -45,6 +45,8 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowActivityManager; @RunWith(RobolectricTestRunner.class) public class SessionReportingCoordinatorRobolectricTest { @@ -190,8 +192,12 @@ private void addAppExitInfo(int reason) { activityManager.getRunningAppProcesses().get(0); shadowOf(activityManager) .addApplicationExitInfo( - runningAppProcessInfo.processName, runningAppProcessInfo.pid, reason, 1); - return; + ShadowActivityManager.ApplicationExitInfoBuilder.newBuilder() + .setRealUid(runningAppProcessInfo.pid) + .setProcessName(runningAppProcessInfo.processName) + .setReason(reason) + .setStatus(1) + .build()); } private List getAppExitInfoList() { From 9c0807a462dc97a55826e3e828b8931730fc05ad Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Wed, 24 Jun 2026 10:46:54 -0400 Subject: [PATCH 8/8] Format --- .../main/java/com/google/firebase/gradle/plugins/SdkUtil.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/SdkUtil.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/SdkUtil.kt index 3f3ec25b2eb..68c73d81134 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/plugins/SdkUtil.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/SdkUtil.kt @@ -47,10 +47,8 @@ val Project.sdkDir: File val Project.androidJar: File? get() { val android = project.extensions.findByType(LibraryExtension::class.java) ?: return null - return android.bootClasspath.firstOrNull { it.name == "android.jar" } ?: File( - sdkDir, - String.format("/platforms/%s/android.jar", android.compileSdkVersion) - ) + return android.bootClasspath.firstOrNull { it.name == "android.jar" } + ?: File(sdkDir, String.format("/platforms/%s/android.jar", android.compileSdkVersion)) } /**