1414
1515package com .google .firebase .crashlytics .ndk ;
1616
17+ import android .app .ActivityManager ;
18+ import android .app .ApplicationExitInfo ;
1719import android .content .Context ;
20+ import android .os .Build ;
1821import androidx .annotation .NonNull ;
1922import androidx .annotation .Nullable ;
23+ import androidx .annotation .RequiresApi ;
24+ import androidx .annotation .VisibleForTesting ;
2025import com .google .firebase .crashlytics .internal .Logger ;
2126import com .google .firebase .crashlytics .internal .common .CommonUtils ;
27+ import com .google .firebase .crashlytics .internal .model .CrashlyticsReport ;
2228import com .google .firebase .crashlytics .internal .model .StaticSessionData ;
2329import com .google .firebase .crashlytics .internal .persistence .FileStore ;
2430import java .io .BufferedWriter ;
31+ import java .io .ByteArrayOutputStream ;
2532import java .io .File ;
2633import java .io .FileOutputStream ;
2734import java .io .IOException ;
35+ import java .io .InputStream ;
2836import java .io .OutputStreamWriter ;
2937import java .nio .charset .Charset ;
38+ import java .util .ArrayList ;
39+ import java .util .Base64 ;
40+ import java .util .List ;
41+ import java .util .zip .GZIPOutputStream ;
3042
3143public class CrashpadController {
3244
3345 @ SuppressWarnings ("CharsetObjectCanBeUsed" ) // StandardCharsets requires API level 19.
3446 private static final Charset UTF_8 = Charset .forName ("UTF-8" );
3547
48+ private static final String SESSION_START_TIMESTAMP_FILE_NAME = "start-time" ;
49+
3650 private static final String SESSION_METADATA_FILE = "session.json" ;
3751 private static final String APP_METADATA_FILE = "app.json" ;
3852 private static final String DEVICE_METADATA_FILE = "device.json" ;
@@ -70,8 +84,8 @@ public boolean initialize(
7084 }
7185
7286 public boolean hasCrashDataForSession (String sessionId ) {
73- File crashFile = getFilesForSession (sessionId ). minidump ;
74- return crashFile != null && crashFile . exists ();
87+ SessionFiles files = getFilesForSession (sessionId );
88+ return files . nativeCore != null && files . nativeCore . hasCore ();
7589 }
7690
7791 @ NonNull
@@ -94,7 +108,7 @@ public SessionFiles getFilesForSession(String sessionId) {
94108 && sessionFileDirectory .exists ()
95109 && sessionFileDirectoryForMinidump .exists ()) {
96110 builder
97- .minidumpFile ( getSingleFileWithExtension ( sessionFileDirectoryForMinidump , ".dmp" ))
111+ .nativeCore ( getNativeCore ( sessionId , sessionFileDirectoryForMinidump ))
98112 .metadataFile (getSingleFileWithExtension (sessionFileDirectory , ".device_info" ))
99113 .sessionFile (new File (sessionFileDirectory , SESSION_METADATA_FILE ))
100114 .appFile (new File (sessionFileDirectory , APP_METADATA_FILE ))
@@ -104,6 +118,52 @@ public SessionFiles getFilesForSession(String sessionId) {
104118 return builder .build ();
105119 }
106120
121+ private SessionFiles .NativeCore getNativeCore (
122+ String sessionId , File sessionFileDirectoryForMinidump ) {
123+ File minidump = getSingleFileWithExtension (sessionFileDirectoryForMinidump , ".dmp" );
124+ CrashlyticsReport .ApplicationExitInfo applicationExitInfo = getApplicationExitInfo (sessionId );
125+ return new SessionFiles .NativeCore (minidump , applicationExitInfo );
126+ }
127+
128+ private CrashlyticsReport .ApplicationExitInfo getApplicationExitInfo (String sessionId ) {
129+ return android .os .Build .VERSION .SDK_INT >= Build .VERSION_CODES .S
130+ ? getNativeCrashApplicationExitInfo (sessionId )
131+ : null ;
132+ }
133+
134+ @ RequiresApi (api = Build .VERSION_CODES .S )
135+ private CrashlyticsReport .ApplicationExitInfo getNativeCrashApplicationExitInfo (
136+ String sessionId ) {
137+ ActivityManager activityManager =
138+ (ActivityManager ) context .getSystemService (Context .ACTIVITY_SERVICE );
139+ List <ApplicationExitInfo > applicationExitInfoList =
140+ activityManager .getHistoricalProcessExitReasons (null , 0 , 0 );
141+
142+ File sessionStartFile = fileStore .getSessionFile (sessionId , SESSION_START_TIMESTAMP_FILE_NAME );
143+ long sessionTime =
144+ sessionStartFile == null ? System .currentTimeMillis () : sessionStartFile .lastModified ();
145+
146+ return getRelevantApplicationExitInfo (sessionTime , applicationExitInfoList );
147+ }
148+
149+ @ RequiresApi (api = Build .VERSION_CODES .S )
150+ private CrashlyticsReport .ApplicationExitInfo getRelevantApplicationExitInfo (
151+ long sessionTime , List <ApplicationExitInfo > applicationExitInfoList ) {
152+ List <ApplicationExitInfo > filtered = new ArrayList <>();
153+ for (ApplicationExitInfo applicationExitInfo : applicationExitInfoList ) {
154+ if (applicationExitInfo .getReason () != ApplicationExitInfo .REASON_CRASH_NATIVE
155+ || applicationExitInfo .getTimestamp () < sessionTime ) {
156+ continue ;
157+ }
158+
159+ filtered .add (applicationExitInfo );
160+ }
161+
162+ // For GWP-ASan and MTE, there can only be one tombstone, even in the case of non-crashy
163+ // GWP-ASan.
164+ return filtered .isEmpty () ? null : convertApplicationExitInfoToModel (filtered .get (0 ));
165+ }
166+
107167 public void writeBeginSession (String sessionId , String generator , long startedAtSeconds ) {
108168 final String json =
109169 SessionMetadataJsonSerializer .serializeBeginSession (sessionId , generator , startedAtSeconds );
@@ -181,4 +241,59 @@ private static File getSingleFileWithExtension(File directory, String extension)
181241
182242 return null ;
183243 }
244+
245+ @ RequiresApi (api = Build .VERSION_CODES .S )
246+ private static CrashlyticsReport .ApplicationExitInfo convertApplicationExitInfoToModel (
247+ ApplicationExitInfo applicationExitInfo ) {
248+ return CrashlyticsReport .ApplicationExitInfo .builder ()
249+ .setImportance (applicationExitInfo .getImportance ())
250+ .setProcessName (applicationExitInfo .getProcessName ())
251+ .setReasonCode (applicationExitInfo .getReason ())
252+ .setTimestamp (applicationExitInfo .getTimestamp ())
253+ .setPid (applicationExitInfo .getPid ())
254+ .setPss (applicationExitInfo .getPss ())
255+ .setRss (applicationExitInfo .getRss ())
256+ .setTraceFile (getTraceFileFromApplicationExitInfo (applicationExitInfo ))
257+ .build ();
258+ }
259+
260+ @ RequiresApi (api = Build .VERSION_CODES .S )
261+ private static String getTraceFileFromApplicationExitInfo (
262+ ApplicationExitInfo applicationExitInfo ) {
263+ try {
264+ return convertInputStreamToString (applicationExitInfo .getTraceInputStream ());
265+ } catch (IOException e ) {
266+ Logger .getLogger ().w ("Failed to get input stream from ApplicationExitInfo" );
267+ }
268+
269+ return null ;
270+ }
271+
272+ @ VisibleForTesting
273+ @ RequiresApi (api = Build .VERSION_CODES .S )
274+ public static String convertInputStreamToString (InputStream inputStream ) throws IOException {
275+ if (inputStream == null ) {
276+ return null ;
277+ }
278+
279+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream ();
280+ byte [] bytes = new byte [8192 ];
281+ int length ;
282+ while ((length = inputStream .read (bytes )) != -1 ) {
283+ byteArrayOutputStream .write (bytes , 0 , length );
284+ }
285+
286+ return zipAndEncode (byteArrayOutputStream .toByteArray ());
287+ }
288+
289+ @ RequiresApi (api = Build .VERSION_CODES .S )
290+ private static String zipAndEncode (byte [] bytes ) throws IOException {
291+ try (ByteArrayOutputStream out = new ByteArrayOutputStream ();
292+ GZIPOutputStream gzip = new GZIPOutputStream (out )) {
293+ gzip .write (bytes );
294+ gzip .finish ();
295+
296+ return Base64 .getEncoder ().encodeToString (out .toByteArray ());
297+ }
298+ }
184299}
0 commit comments