diff --git a/firebase-crashlytics/src/androidTest/assets/firebase_settings.json b/firebase-crashlytics/src/androidTest/assets/firebase_settings.json index d2f5454e32c..4ba1e7f73a7 100644 --- a/firebase-crashlytics/src/androidTest/assets/firebase_settings.json +++ b/firebase-crashlytics/src/androidTest/assets/firebase_settings.json @@ -8,6 +8,7 @@ "app": { "status": "activated", "report_upload_variant": 2, + "native_report_upload_variant": 2, "update_required": true }, "fabric": { diff --git a/firebase-crashlytics/src/androidTest/assets/firebase_settings_new.json b/firebase-crashlytics/src/androidTest/assets/firebase_settings_new.json index 588ca40d8df..22246a55e5d 100644 --- a/firebase-crashlytics/src/androidTest/assets/firebase_settings_new.json +++ b/firebase-crashlytics/src/androidTest/assets/firebase_settings_new.json @@ -8,6 +8,7 @@ "app": { "status": "new", "report_upload_variant": 2, + "native_report_upload_variant": 2, "update_required": true }, "fabric": { diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/BytesBackedNativeSessionFileTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/BytesBackedNativeSessionFileTest.java new file mode 100644 index 00000000000..936f9e308b1 --- /dev/null +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/BytesBackedNativeSessionFileTest.java @@ -0,0 +1,62 @@ +// Copyright 2020 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.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; +import java.io.IOException; +import org.junit.Test; + +public class BytesBackedNativeSessionFileTest { + byte[] testBytes = {0, 2, 20, 10}; + byte[] emptyBytes = {}; + + @Test + public void testAsStream_convertsToStream() throws IOException { + BytesBackedNativeSessionFile nativeSessionFile = + new BytesBackedNativeSessionFile("file_name", "file", testBytes); + byte[] readBytes = new byte[4]; + nativeSessionFile.getStream().read(readBytes); + assertArrayEquals(testBytes, readBytes); + } + + @Test + public void testAsStreamWhenEmpty_returnsNull() { + BytesBackedNativeSessionFile nativeSessionFile = + new BytesBackedNativeSessionFile("file_name", "file", emptyBytes); + assertNull(nativeSessionFile.getStream()); + } + + @Test + public void testAsFilePayload_convertsToFilePayload() { + BytesBackedNativeSessionFile nativeSessionFile = + new BytesBackedNativeSessionFile("file_name", "file", testBytes); + CrashlyticsReport.FilesPayload.File filesPayload = nativeSessionFile.asFilePayload(); + assertNotNull(filesPayload); + assertArrayEquals(testBytes, filesPayload.getContents()); + assertEquals("file_name", filesPayload.getFilename()); + } + + @Test + public void testAsFilePayloadWhenEmpty_convertsToNull() { + BytesBackedNativeSessionFile nativeSessionFile = + new BytesBackedNativeSessionFile("file_name", "file", emptyBytes); + assertNull(nativeSessionFile.asFilePayload()); + } +} 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 526a8a2763d..fc511601b7c 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 @@ -116,7 +116,10 @@ protected void setUp() throws Exception { when(mockFileStore.getFilesDirPath()).thenReturn(testFilesDirectory.getPath()); final SettingsData testSettingsData = - new TestSettingsData(3, CrashlyticsController.REPORT_UPLOAD_VARIANT_LEGACY); + new TestSettingsData( + 3, + DataTransportState.REPORT_UPLOAD_VARIANT_LEGACY, + DataTransportState.REPORT_UPLOAD_VARIANT_LEGACY); appSettingsData = testSettingsData.appData; sessionSettingsData = testSettingsData.sessionData; diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCoreTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCoreTest.java index 7551f205d8d..c2bbaa9b687 100644 --- a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCoreTest.java +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCoreTest.java @@ -610,7 +610,10 @@ private Task startCoreAsync(CrashlyticsCore crashlyticsCore) { SettingsController mockSettingsController = mock(SettingsController.class); final SettingsData settings = - new TestSettingsData(3, CrashlyticsController.REPORT_UPLOAD_VARIANT_LEGACY); + new TestSettingsData( + 3, + DataTransportState.REPORT_UPLOAD_VARIANT_LEGACY, + DataTransportState.REPORT_UPLOAD_VARIANT_LEGACY); when(mockSettingsController.getSettings()).thenReturn(settings); when(mockSettingsController.getAppSettings()).thenReturn(Tasks.forResult(settings.appData)); diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/FileBackedNativeSessionFileTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/FileBackedNativeSessionFileTest.java new file mode 100644 index 00000000000..68afb133413 --- /dev/null +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/FileBackedNativeSessionFileTest.java @@ -0,0 +1,108 @@ +// Copyright 2020 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.assertArrayEquals; + +import android.content.Context; +import com.google.firebase.crashlytics.internal.CrashlyticsTestCase; +import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import org.junit.Test; + +public class FileBackedNativeSessionFileTest extends CrashlyticsTestCase { + byte[] testContents = {0, 2, 20, 10}; + byte[] emptyContents = {}; + File testFile; + File emptyFile; + File missingFile; + + @Override + protected void setUp() throws Exception { + super.setUp(); + final Context context = getContext(); + testFile = new File(context.getFilesDir(), "testFile"); + try (FileOutputStream fout = new FileOutputStream(testFile); + ByteArrayOutputStream stream = new ByteArrayOutputStream()) { + stream.write(testContents); + stream.writeTo(fout); + } + emptyFile = new File(context.getFilesDir(), "emptyFile"); + emptyFile.createNewFile(); + missingFile = new File(context.getFilesDir(), "missingFile"); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + testFile.delete(); + emptyFile.delete(); + } + + @Test + public void testAsStream_convertsToStream() throws IOException { + FileBackedNativeSessionFile nativeSessionFile = + new FileBackedNativeSessionFile("file_name", "file", testFile); + byte[] readBytes = new byte[4]; + nativeSessionFile.getStream().read(readBytes); + assertArrayEquals(testContents, readBytes); + } + + @Test + public void testAsStreamWhenEmpty_returnsEmpty() throws IOException { + FileBackedNativeSessionFile nativeSessionFile = + new FileBackedNativeSessionFile("file_name", "file", emptyFile); + byte[] readBytes = new byte[0]; + nativeSessionFile.getStream().read(readBytes); + assertArrayEquals(emptyContents, readBytes); + } + + @Test + public void testAsStreamWhenMissing_returnsNull() { + FileBackedNativeSessionFile nativeSessionFile = + new FileBackedNativeSessionFile("file_name", "file", missingFile); + assertNull(nativeSessionFile.getStream()); + } + + @Test + public void testAsFilePayload_convertsToFilePayload() { + FileBackedNativeSessionFile nativeSessionFile = + new FileBackedNativeSessionFile("file_name", "file", testFile); + CrashlyticsReport.FilesPayload.File filesPayload = nativeSessionFile.asFilePayload(); + assertNotNull(filesPayload); + assertArrayEquals(testContents, filesPayload.getContents()); + assertEquals("file_name", filesPayload.getFilename()); + } + + @Test + public void testAsFilePayloadWhenEmpty_returnsEmptyPayload() { + FileBackedNativeSessionFile nativeSessionFile = + new FileBackedNativeSessionFile("file_name", "file", emptyFile); + CrashlyticsReport.FilesPayload.File filesPayload = nativeSessionFile.asFilePayload(); + assertNotNull(filesPayload); + assertArrayEquals(emptyContents, filesPayload.getContents()); + assertEquals("file_name", filesPayload.getFilename()); + } + + @Test + public void testAsFilePayloadWhenMissing_convertsToNull() { + FileBackedNativeSessionFile nativeSessionFile = + new FileBackedNativeSessionFile("file_name", "file", missingFile); + assertNull(nativeSessionFile.asFilePayload()); + } +} diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/NativeSessionFileGzipperTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/NativeSessionFileGzipperTest.java new file mode 100644 index 00000000000..a60c9510d8c --- /dev/null +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/NativeSessionFileGzipperTest.java @@ -0,0 +1,81 @@ +// Copyright 2020 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 android.content.Context; +import com.google.firebase.crashlytics.internal.CrashlyticsTestCase; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import org.junit.Test; +import org.mockito.internal.util.collections.Sets; + +public class NativeSessionFileGzipperTest extends CrashlyticsTestCase { + byte[] testContents = {0, 2, 20, 10}; + File testFile; + File missingFile; + File gzipDir; + + @Override + protected void setUp() throws Exception { + super.setUp(); + final Context context = getContext(); + testFile = new File(context.getFilesDir(), "testFile"); + try (FileOutputStream fout = new FileOutputStream(testFile); + ByteArrayOutputStream stream = new ByteArrayOutputStream()) { + stream.write(testContents); + stream.writeTo(fout); + } + File baseDirectory = context.getFilesDir(); + missingFile = new File(baseDirectory, "missingFile"); + gzipDir = new File(baseDirectory, "gzip"); + gzipDir.mkdirs(); + } + + @Test + public void testProcessNativeSessions_putsFilesInCorrectLocation() throws IOException { + String fileBackedSessionName = "file"; + String byteBackedSessionName = "byte"; + FileBackedNativeSessionFile fileSession = + new FileBackedNativeSessionFile("not_applicable", fileBackedSessionName, testFile); + BytesBackedNativeSessionFile byteSession = + new BytesBackedNativeSessionFile("not_applicable", byteBackedSessionName, testContents); + List files = Arrays.asList(fileSession, byteSession); + NativeSessionFileGzipper.processNativeSessions(gzipDir, files); + + assertEquals( + Sets.newSet( + new File(gzipDir, fileBackedSessionName), new File(gzipDir, byteBackedSessionName)), + Sets.newSet(gzipDir.listFiles())); + } + + @Test + public void testProcessNativeSessionsWhenDataIsNull_putsFilesInCorrectLocation() { + String fileBackedSessionName = "file"; + String byteBackedSessionName = "byte"; + FileBackedNativeSessionFile fileSession = + new FileBackedNativeSessionFile("not_applicable", fileBackedSessionName, missingFile); + BytesBackedNativeSessionFile byteSession = + new BytesBackedNativeSessionFile("not_applicable", byteBackedSessionName, testContents); + List files = Arrays.asList(fileSession, byteSession); + NativeSessionFileGzipper.processNativeSessions(gzipDir, files); + + assertEquals( + Sets.newSet(new File(gzipDir, byteBackedSessionName)), Sets.newSet(gzipDir.listFiles())); + } +} 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 0228be0869e..c1ec60aede0 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 @@ -14,11 +14,13 @@ package com.google.firebase.crashlytics.internal.common; +import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -27,7 +29,6 @@ import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; -import com.google.firebase.crashlytics.internal.common.SessionReportingCoordinator.SendReportPredicate; import com.google.firebase.crashlytics.internal.log.LogFileManager; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.CustomAttribute; @@ -35,12 +36,14 @@ import com.google.firebase.crashlytics.internal.persistence.CrashlyticsReportPersistence; import com.google.firebase.crashlytics.internal.send.DataTransportCrashlyticsReportSender; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -51,7 +54,6 @@ public class SessionReportingCoordinatorTest { @Mock private DataTransportCrashlyticsReportSender reportSender; @Mock private LogFileManager logFileManager; @Mock private UserMetadata reportMetadata; - @Mock private SendReportPredicate mockSendReportPredicate; @Mock private CrashlyticsReport mockReport; @Mock private CrashlyticsReport.Session.Event mockEvent; @Mock private CrashlyticsReport.Session.Event.Builder mockEventBuilder; @@ -324,6 +326,23 @@ public void onUserId_writesUserToReportMetadata() { verify(reportMetadata).setUserId(userId); } + @Test + public void testFinalizeSessionWithNativeEvent_createsCrashlyticsReportWithNativePayload() { + byte[] testBytes = {0, 2, 20, 10}; + String byteBackedSessionName = "byte"; + BytesBackedNativeSessionFile byteSession = + new BytesBackedNativeSessionFile(byteBackedSessionName, "not_applicable", testBytes); + reportManager.finalizeSessionWithNativeEvent("id", Arrays.asList(byteSession)); + + ArgumentCaptor filesPayload = + ArgumentCaptor.forClass(CrashlyticsReport.FilesPayload.class); + verify(reportPersistence).finalizeSessionWithNativeEvent(eq("id"), filesPayload.capture()); + CrashlyticsReport.FilesPayload ndkPayloadFinalized = filesPayload.getValue(); + assertEquals(1, ndkPayloadFinalized.getFiles().size()); + assertEquals(testBytes, ndkPayloadFinalized.getFiles().get(0).getContents()); + assertEquals(byteBackedSessionName, ndkPayloadFinalized.getFiles().get(0).getFilename()); + } + @Test public void onSessionsFinalize_finalizesReports() { final String sessionId = "testSessionId"; @@ -337,26 +356,26 @@ public void onSessionsFinalize_finalizesReports() { @Test @SuppressWarnings("unchecked") public void onReportSend_successfulReportsAreDeleted() { - when(mockSendReportPredicate.shouldSendViaDataTransport()).thenReturn(true); final String orgId = "testOrgId"; final String sessionId1 = "sessionId1"; final String sessionId2 = "sessionId2"; - final List finalizedReports = new ArrayList<>(); - final CrashlyticsReport mockReport1 = mockReport(sessionId1, orgId); - final CrashlyticsReport mockReport2 = mockReport(sessionId2, orgId); + final List finalizedReports = new ArrayList<>(); + final CrashlyticsReportWithSessionId mockReport1 = mockReportWithSessionId(sessionId1, orgId); + final CrashlyticsReportWithSessionId mockReport2 = mockReportWithSessionId(sessionId2, orgId); finalizedReports.add(mockReport1); finalizedReports.add(mockReport2); when(reportPersistence.loadFinalizedReports()).thenReturn(finalizedReports); - final Task successfulTask = Tasks.forResult(mockReport1); - final Task failedTask = Tasks.forException(new Exception("fail")); + final Task successfulTask = Tasks.forResult(mockReport1); + final Task failedTask = + Tasks.forException(new Exception("fail")); when(reportSender.sendReport(mockReport1)).thenReturn(successfulTask); when(reportSender.sendReport(mockReport2)).thenReturn(failedTask); - reportManager.sendReports(orgId, Runnable::run, mockSendReportPredicate); + reportManager.sendReports(orgId, Runnable::run, DataTransportState.ALL); verify(reportSender).sendReport(mockReport1); verify(reportSender).sendReport(mockReport2); @@ -366,9 +385,8 @@ public void onReportSend_successfulReportsAreDeleted() { } @Test - public void onReportSend_reportsAreDeletedWithoutBeingSent_whenSendPredicateIsFalse() { - when(mockSendReportPredicate.shouldSendViaDataTransport()).thenReturn(false); - reportManager.sendReports("testOrgId", Runnable::run, mockSendReportPredicate); + public void onReportSend_reportsAreDeletedWithoutBeingSent_whenDataTransportStateNone() { + reportManager.sendReports("testOrgId", Runnable::run, DataTransportState.NONE); verify(reportPersistence).deleteAllReports(); verify(reportPersistence, never()).loadFinalizedReports(); @@ -376,6 +394,37 @@ public void onReportSend_reportsAreDeletedWithoutBeingSent_whenSendPredicateIsFa verifyZeroInteractions(reportSender); } + @Test + public void + onReportSend_javaReportsAreSentNativeReportsDeletedWithoutBeingSent_whenDataTransportStateJavaOnly() { + final String orgId = "testOrgId"; + final String sessionIdJava = "sessionIdJava"; + final String sessionIdNative = "sessionIdNative"; + + final List finalizedReports = new ArrayList<>(); + final CrashlyticsReportWithSessionId mockReportJava = + mockReportWithSessionId(sessionIdJava, orgId); + final CrashlyticsReportWithSessionId mockReportNative = + mockReportWithSessionId(sessionIdNative, orgId); + finalizedReports.add(mockReportJava); + finalizedReports.add(mockReportNative); + + when(mockReportJava.getReport().getType()).thenReturn(CrashlyticsReport.Type.JAVA); + when(mockReportNative.getReport().getType()).thenReturn(CrashlyticsReport.Type.NATIVE); + + when(reportPersistence.loadFinalizedReports()).thenReturn(finalizedReports); + + when(reportSender.sendReport(mockReportJava)).thenReturn(Tasks.forResult(mockReportJava)); + + reportManager.sendReports(orgId, Runnable::run, DataTransportState.JAVA_ONLY); + + verify(reportSender).sendReport(mockReportJava); + verify(reportSender, never()).sendReport(mockReportNative); + + verify(reportPersistence).deleteFinalizedReport(sessionIdJava); + verify(reportPersistence).deleteFinalizedReport(sessionIdNative); + } + @Test public void testPersistUserIdForCurrentSession_persistsCurrentUserIdForCurrentSessionId() { final String currentSessionId = "currentSessionId"; @@ -397,6 +446,17 @@ public void testRemoveAllReports_deletesPersistedReports() { verify(reportPersistence).deleteAllReports(); } + private static CrashlyticsReport makeIncompleteReport() { + return CrashlyticsReport.builder() + .setSdkVersion("sdkVersion") + .setGmpAppId("gmpAppId") + .setPlatform(1) + .setInstallationUuid("installationId") + .setBuildVersion("1") + .setDisplayVersion("1.0.0") + .build(); + } + private void mockEventInteractions() { when(mockEvent.toBuilder()).thenReturn(mockEventBuilder); when(mockEventBuilder.build()).thenReturn(mockEvent); @@ -423,4 +483,9 @@ private static CrashlyticsReport mockReport(String sessionId, String orgId) { when(mockReport.withOrganizationId(orgId)).thenReturn(mockReport); return mockReport; } + + private static CrashlyticsReportWithSessionId mockReportWithSessionId( + String sessionId, String orgId) { + return CrashlyticsReportWithSessionId.create(mockReport(sessionId, orgId), sessionId); + } } 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 44d54456622..17d63daf43f 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 @@ -19,6 +19,7 @@ import static org.mockito.Mockito.when; import androidx.test.runner.AndroidJUnit4; +import com.google.firebase.crashlytics.internal.common.CrashlyticsReportWithSessionId; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.Session; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.Session.Application; @@ -34,12 +35,15 @@ import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; +import org.mockito.internal.util.collections.Sets; @RunWith(AndroidJUnit4.class) public class CrashlyticsReportPersistenceTest { @@ -94,9 +98,10 @@ public void testLoadFinalizedReports_reportThenEvent_returnsReportWithEvent() { reportPersistence.finalizeReports("skippedSession", endedAt); - final List finalizedReports = reportPersistence.loadFinalizedReports(); + final List finalizedReports = + reportPersistence.loadFinalizedReports(); assertEquals(1, finalizedReports.size()); - final CrashlyticsReport finalizedReport = finalizedReports.get(0); + final CrashlyticsReport finalizedReport = finalizedReports.get(0).getReport(); assertEquals( testReport .withSessionEndFields(endedAt, false, null) @@ -119,9 +124,10 @@ public void testLoadFinalizedReports_reportThenMultipleEvents_returnsReportWithM reportPersistence.finalizeReports("skippedSession", endedAt); - final List finalizedReports = reportPersistence.loadFinalizedReports(); + final List finalizedReports = + reportPersistence.loadFinalizedReports(); assertEquals(1, finalizedReports.size()); - final CrashlyticsReport finalizedReport = finalizedReports.get(0); + final CrashlyticsReport finalizedReport = finalizedReports.get(0).getReport(); assertEquals( testReport .withSessionEndFields(endedAt, false, null) @@ -148,15 +154,16 @@ public void testLoadFinalizedReports_reportThenMultipleEvents_returnsReportWithM reportPersistence.finalizeReports("skippedSession", endedAt); - final List finalizedReports = reportPersistence.loadFinalizedReports(); + final List finalizedReports = + reportPersistence.loadFinalizedReports(); assertEquals(2, finalizedReports.size()); - final CrashlyticsReport finalizedReport1 = finalizedReports.get(1); + final CrashlyticsReport finalizedReport1 = finalizedReports.get(1).getReport(); assertEquals( testReport1 .withSessionEndFields(endedAt, false, null) .withEvents(ImmutableList.from(testEvent1)), finalizedReport1); - final CrashlyticsReport finalizedReport2 = finalizedReports.get(0); + final CrashlyticsReport finalizedReport2 = finalizedReports.get(0).getReport(); assertEquals( testReport2 .withSessionEndFields(endedAt, false, null) @@ -174,7 +181,8 @@ public void testFinalizeReports_capsOpenSessions() throws IOException { reportPersistence.finalizeReports("skippedSession", endedAt); - final List finalizedReports = reportPersistence.loadFinalizedReports(); + final List finalizedReports = + reportPersistence.loadFinalizedReports(); assertEquals(8, finalizedReports.size()); } @@ -189,12 +197,13 @@ public void testFinalizeReports_capsOldestSessionsFirst() throws IOException { reportPersistence.finalizeReports("skippedSession", endedAt); - final List finalizedReports = reportPersistence.loadFinalizedReports(); + final List finalizedReports = + reportPersistence.loadFinalizedReports(); assertEquals(8, finalizedReports.size()); List reportIdentifiers = new ArrayList<>(); - for (CrashlyticsReport finalizedReport : finalizedReports) { - reportIdentifiers.add(finalizedReport.getSession().getIdentifier()); + for (CrashlyticsReportWithSessionId finalizedReport : finalizedReports) { + reportIdentifiers.add(finalizedReport.getSessionId()); } List expectedSessions = Arrays.asList("testSession12", "testSession13", "testSession14", "testSession15"); @@ -216,7 +225,8 @@ public void testFinalizeReports_skipsCappingCurrentSession() throws IOException final long endedAt = System.currentTimeMillis(); reportPersistence.finalizeReports("testSession5", endedAt); - List finalizedReports = reportPersistence.loadFinalizedReports(); + List finalizedReports = + reportPersistence.loadFinalizedReports(); assertEquals(8, finalizedReports.size()); persistReportWithEvent(reportPersistence, "testSession11", true); reportPersistence.finalizeReports("testSession11", endedAt); @@ -235,7 +245,8 @@ public void testFinalizeReports_capsReports() throws IOException { reportPersistence.finalizeReports("skippedSession", 0L); - final List finalizedReports = reportPersistence.loadFinalizedReports(); + final List finalizedReports = + reportPersistence.loadFinalizedReports(); assertEquals(4, finalizedReports.size()); } @@ -257,7 +268,8 @@ public void testFinalizeReports_whenSettingsChanges_capsReports() throws IOExcep } reportPersistence.finalizeReports("skippedSession", 0L); - List finalizedReports = reportPersistence.loadFinalizedReports(); + List finalizedReports = + reportPersistence.loadFinalizedReports(); assertEquals(4, finalizedReports.size()); when(settingsMock.getSessionData()).thenReturn(sessionSettingsDataMockDifferentValues); @@ -285,13 +297,40 @@ public void testFinalizeReports_removesLowPriorityReportsFirst() throws IOExcept reportPersistence.finalizeReports("skippedSession", 0L); - final List finalizedReports = reportPersistence.loadFinalizedReports(); + final List finalizedReports = + reportPersistence.loadFinalizedReports(); assertEquals(4, finalizedReports.size()); - for (CrashlyticsReport finalizedReport : finalizedReports) { - assertTrue(finalizedReport.getSession().getIdentifier().contains("high")); + for (CrashlyticsReportWithSessionId finalizedReport : finalizedReports) { + assertTrue(finalizedReport.getReport().getSession().getIdentifier().contains("high")); } } + @Test + public void testFinalizeReports_prioritizesNativeAndNonnativeFatals() throws IOException { + + CrashlyticsReport.FilesPayload filesPayload = makeFilePayload(); + reportPersistence = + new CrashlyticsReportPersistence( + folder.newFolder(), getSettingsMock(4, VERY_LARGE_UPPER_LIMIT)); + + persistReportWithEvent(reportPersistence, "testSession1", true); + reportPersistence.finalizeSessionWithNativeEvent("testSession1", filesPayload); + persistReportWithEvent(reportPersistence, "testSession2low", false); + persistReportWithEvent(reportPersistence, "testSession3low", false); + persistReportWithEvent(reportPersistence, "testSession4", true); + reportPersistence.finalizeSessionWithNativeEvent("testSession4", filesPayload); + reportPersistence.finalizeReports("skippedSession", 0L); + + List finalizedReports = + reportPersistence.loadFinalizedReports(); + Set reportNames = new HashSet<>(); + for (CrashlyticsReportWithSessionId finalizedReport : finalizedReports) { + reportNames.add(finalizedReport.getSessionId()); + } + assertEquals(Sets.newSet("testSession1", "testSession4"), reportNames); + assertEquals(4, finalizedReports.size()); + } + @Test public void testFinalizeReports_removesOldestReportsFirst() throws IOException { reportPersistence = @@ -304,11 +343,12 @@ public void testFinalizeReports_removesOldestReportsFirst() throws IOException { reportPersistence.finalizeReports("skippedSession", 0L); - final List finalizedReports = reportPersistence.loadFinalizedReports(); + final List finalizedReports = + reportPersistence.loadFinalizedReports(); assertEquals(4, finalizedReports.size()); List reportIdentifiers = new ArrayList<>(); - for (CrashlyticsReport finalizedReport : finalizedReports) { - reportIdentifiers.add(finalizedReport.getSession().getIdentifier()); + for (CrashlyticsReportWithSessionId finalizedReport : finalizedReports) { + reportIdentifiers.add(finalizedReport.getSessionId()); } List expectedSessions = @@ -335,9 +375,10 @@ public void testLoadFinalizedReports_reportWithUserId_returnsReportWithProperUse reportPersistence.finalizeReports("skippedSession", 0L); - final List finalizedReports = reportPersistence.loadFinalizedReports(); + final List finalizedReports = + reportPersistence.loadFinalizedReports(); assertEquals(1, finalizedReports.size()); - final CrashlyticsReport finalizedReport = finalizedReports.get(0); + final CrashlyticsReport finalizedReport = finalizedReports.get(0).getReport(); assertNotNull(finalizedReport.getSession().getUser()); assertEquals(userId, finalizedReport.getSession().getUser().getIdentifier()); } @@ -363,14 +404,32 @@ public void testLoadFinalizedReports_reportWithUserId_returnsReportWithProperUse reportPersistence.finalizeReports("skippedSession", 0L); - final List finalizedReports = reportPersistence.loadFinalizedReports(); + final List finalizedReports = + reportPersistence.loadFinalizedReports(); assertEquals(2, finalizedReports.size()); - final CrashlyticsReport finalizedReport1 = finalizedReports.get(1); - assertNotNull(finalizedReport1.getSession().getUser()); - assertEquals(userId1, finalizedReport1.getSession().getUser().getIdentifier()); - final CrashlyticsReport finalizedReport2 = finalizedReports.get(0); - assertNotNull(finalizedReport2.getSession().getUser()); - assertEquals(userId2, finalizedReport2.getSession().getUser().getIdentifier()); + final CrashlyticsReportWithSessionId finalizedReport1 = finalizedReports.get(1); + assertNotNull(finalizedReport1.getReport().getSession().getUser()); + assertEquals(userId1, finalizedReport1.getReport().getSession().getUser().getIdentifier()); + final CrashlyticsReportWithSessionId finalizedReport2 = finalizedReports.get(0); + assertNotNull(finalizedReport2.getReport().getSession().getUser()); + assertEquals(userId2, finalizedReport2.getReport().getSession().getUser().getIdentifier()); + } + + @Test + public void testFinalizeSessionWithNativeEvent_writesNativeSessions() { + final CrashlyticsReport testReport = makeTestReport("sessionId"); + reportPersistence.persistReport(testReport); + CrashlyticsReport.FilesPayload filesPayload = makeFilePayload(); + List finalizedReports = + reportPersistence.loadFinalizedReports(); + + assertEquals(0, finalizedReports.size()); + + reportPersistence.finalizeSessionWithNativeEvent("sessionId", filesPayload); + + finalizedReports = reportPersistence.loadFinalizedReports(); + assertEquals(1, finalizedReports.size()); + assertEquals(filesPayload, finalizedReports.get(0).getReport().getNdkPayload()); } @Test @@ -456,9 +515,10 @@ public void testPersistEvent_keepsAppropriateNumberOfMostRecentEvents() throws I reportPersistence.finalizeReports("skippedSession", endedAt); - final List finalizedReports = reportPersistence.loadFinalizedReports(); + final List finalizedReports = + reportPersistence.loadFinalizedReports(); assertEquals(1, finalizedReports.size()); - final CrashlyticsReport finalizedReport = finalizedReports.get(0); + final CrashlyticsReport finalizedReport = finalizedReports.get(0).getReport(); assertEquals(4, finalizedReport.getSession().getEvents().size()); assertEquals( testReport @@ -499,9 +559,10 @@ public void testPersistEvent_whenSettingsChanges_keepsAppropriateNumberOfMostRec reportPersistence.finalizeReports("skippedSession", endedAt); - final List finalizedReports = reportPersistence.loadFinalizedReports(); + final List finalizedReports = + reportPersistence.loadFinalizedReports(); assertEquals(1, finalizedReports.size()); - final CrashlyticsReport finalizedReport = finalizedReports.get(0); + final CrashlyticsReport finalizedReport = finalizedReports.get(0).getReport(); assertEquals(4, finalizedReport.getSession().getEvents().size()); assertEquals( testReport @@ -535,9 +596,10 @@ public void testPersistEvent_whenSettingsChanges_keepsAppropriateNumberOfMostRec reportPersistence.finalizeReports("skippedSession", endedAt); - final List finalizedReports2 = reportPersistence.loadFinalizedReports(); + final List finalizedReports2 = + reportPersistence.loadFinalizedReports(); assertEquals(2, finalizedReports2.size()); - final CrashlyticsReport finalizedReport2 = finalizedReports2.get(0); + final CrashlyticsReport finalizedReport2 = finalizedReports2.get(0).getReport(); assertEquals(8, finalizedReport2.getSession().getEvents().size()); assertEquals( testReport2 @@ -563,18 +625,49 @@ private static void persistReportWithEvent( reportPersistence.persistEvent(testEvent, sessionId, isHighPriority); } - private static CrashlyticsReport makeTestReport(String sessionId) { + private static CrashlyticsReport.Builder makeIncompleteReport() { return CrashlyticsReport.builder() .setSdkVersion("sdkVersion") .setGmpAppId("gmpAppId") .setPlatform(1) .setInstallationUuid("installationId") .setBuildVersion("1") - .setDisplayVersion("1.0.0") - .setSession(makeTestSession(sessionId)) + .setDisplayVersion("1.0.0"); + } + + private static CrashlyticsReport makeTestReport(String sessionId) { + return makeIncompleteReport().setSession(makeTestSession(sessionId)).build(); + } + + private static CrashlyticsReport.FilesPayload makeFilePayload() { + byte[] testContents = {0, 2, 20, 10}; + return CrashlyticsReport.FilesPayload.builder() + .setOrgId("orgId") + .setFiles( + ImmutableList.from( + CrashlyticsReport.FilesPayload.File.builder() + .setContents(testContents) + .setFilename("bytes") + .build())) .build(); } + private static CrashlyticsReport makeTestNativeReport() { + byte[] testContents = {0, 2, 20, 10}; + CrashlyticsReport.FilesPayload filesPayload = + CrashlyticsReport.FilesPayload.builder() + .setOrgId("orgId") + .setFiles( + ImmutableList.from( + CrashlyticsReport.FilesPayload.File.builder() + .setContents(testContents) + .setFilename("bytes") + .build())) + .build(); + + return makeIncompleteReport().setNdkPayload(filesPayload).build(); + } + private static CrashlyticsReport.Session makeTestSession(String sessionId) { return Session.builder() .setGenerator("generator") diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/report/ReportUploaderTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/report/ReportUploaderTest.java index 5e64f0dc041..bdc768a3877 100644 --- a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/report/ReportUploaderTest.java +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/report/ReportUploaderTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.when; import com.google.firebase.crashlytics.internal.CrashlyticsTestCase; +import com.google.firebase.crashlytics.internal.common.DataTransportState; import com.google.firebase.crashlytics.internal.common.TestNativeReportFilesProvider; import com.google.firebase.crashlytics.internal.common.TestReportFilesProvider; import com.google.firebase.crashlytics.internal.report.model.CreateReportRequest; @@ -57,7 +58,7 @@ protected void setUp() throws Exception { new ReportUploader( "testOrganizationId", "testGoogleAppId", - true, + DataTransportState.NONE, reportManager, mockCall, mockHandlingExceptionCheck); @@ -113,13 +114,12 @@ public void testRetry() throws Exception { verifyZeroInteractions(mockCall); } - public void testSendReport_deletesWithoutSendingWhenNotUsingReportsEndpoint() throws Exception { - final boolean isUsingReportsEndpoint = false; + public void testSendReport_deletesWithoutSendingWhenDataTransportAll() throws Exception { reportUploader = new ReportUploader( "testOrganizationId", "testGoogleAppId", - isUsingReportsEndpoint, + DataTransportState.ALL, reportManager, mockCall, mockHandlingExceptionCheck); @@ -133,17 +133,16 @@ public void testSendReport_deletesWithoutSendingWhenNotUsingReportsEndpoint() th verifyZeroInteractions(mockCall); } - public void testSendReport_sendsNativeCrashesRegardlessOfEndpointFlag() throws Exception { + public void testSendReport_sendsNativeCrashesWhenDataTransportJavaOnly() throws Exception { final TestNativeReportFilesProvider nativeReportFilesProvider = new TestNativeReportFilesProvider(getContext()); reportManager = new ReportManager(nativeReportFilesProvider); - final boolean isUsingReportsEndpoint = false; reportUploader = new ReportUploader( "testOrganizationId", "testGoogleAppId", - isUsingReportsEndpoint, + DataTransportState.JAVA_ONLY, reportManager, mockCall, mockHandlingExceptionCheck); diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/send/DataTransportCrashlyticsReportSenderTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/send/DataTransportCrashlyticsReportSenderTest.java index 379966d971a..822717b523a 100644 --- a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/send/DataTransportCrashlyticsReportSenderTest.java +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/send/DataTransportCrashlyticsReportSenderTest.java @@ -26,6 +26,7 @@ import com.google.android.datatransport.TransportScheduleCallback; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; +import com.google.firebase.crashlytics.internal.common.CrashlyticsReportWithSessionId; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; import java.util.concurrent.ExecutionException; import org.junit.Before; @@ -52,11 +53,11 @@ public void setUp() throws Exception { public void testSendReportsSuccessful() throws Exception { doAnswer(callbackAnswer(null)).when(mockTransport).schedule(any(), any()); - final CrashlyticsReport report1 = mock(CrashlyticsReport.class); - final CrashlyticsReport report2 = mock(CrashlyticsReport.class); + final CrashlyticsReportWithSessionId report1 = mockReportWithSessionId(); + final CrashlyticsReportWithSessionId report2 = mockReportWithSessionId(); - final Task send1 = reportSender.sendReport(report1); - final Task send2 = reportSender.sendReport(report2); + final Task send1 = reportSender.sendReport(report1); + final Task send2 = reportSender.sendReport(report2); try { Tasks.await(send1); @@ -76,11 +77,11 @@ public void testSendReportsFailure() throws Exception { final Exception ex = new Exception("fail"); doAnswer(callbackAnswer(ex)).when(mockTransport).schedule(any(), any()); - final CrashlyticsReport report1 = mock(CrashlyticsReport.class); - final CrashlyticsReport report2 = mock(CrashlyticsReport.class); + final CrashlyticsReportWithSessionId report1 = mockReportWithSessionId(); + final CrashlyticsReportWithSessionId report2 = mockReportWithSessionId(); - final Task send1 = reportSender.sendReport(report1); - final Task send2 = reportSender.sendReport(report2); + final Task send1 = reportSender.sendReport(report1); + final Task send2 = reportSender.sendReport(report2); try { Tasks.await(send1); @@ -103,11 +104,11 @@ public void testSendReports_oneSuccessOneFail() throws Exception { .when(mockTransport) .schedule(any(), any()); - final CrashlyticsReport report1 = mock(CrashlyticsReport.class); - final CrashlyticsReport report2 = mock(CrashlyticsReport.class); + final CrashlyticsReportWithSessionId report1 = mockReportWithSessionId(); + final CrashlyticsReportWithSessionId report2 = mockReportWithSessionId(); - final Task send1 = reportSender.sendReport(report1); - final Task send2 = reportSender.sendReport(report2); + final Task send1 = reportSender.sendReport(report1); + final Task send2 = reportSender.sendReport(report2); try { Tasks.await(send1); @@ -129,4 +130,8 @@ private static Answer callbackAnswer(Exception failure) { return null; }; } + + private static CrashlyticsReportWithSessionId mockReportWithSessionId() { + return CrashlyticsReportWithSessionId.create(mock(CrashlyticsReport.class), "sessionId"); + } } diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/settings/SettingsV3JsonTransformTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/settings/SettingsV3JsonTransformTest.java index 82e9afc8caa..1995b6e99d0 100644 --- a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/settings/SettingsV3JsonTransformTest.java +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/settings/SettingsV3JsonTransformTest.java @@ -86,6 +86,7 @@ private void assertAppData(AppSettingsData appData, boolean isAppNew) { appData.ndkReportsUrl); assertTrue(appData.updateRequired); assertEquals(2, appData.reportUploadVariant); + assertEquals(2, appData.nativeReportUploadVariant); } private void assertSettingsData(SessionSettingsData settingsData) { diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/settings/TestSettingsData.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/settings/TestSettingsData.java index 08e6e4a8729..ae8afa2deac 100644 --- a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/settings/TestSettingsData.java +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/settings/TestSettingsData.java @@ -22,13 +22,14 @@ public class TestSettingsData extends SettingsData { public TestSettingsData() { - this(2, 0); + this(2, 0, 0); } - public TestSettingsData(int settingsVersion, int reportUploadVariant) { + public TestSettingsData( + int settingsVersion, int reportUploadVariant, int nativeReportUploadVariant) { super( 5, - buildAppData(reportUploadVariant), + buildAppData(reportUploadVariant, nativeReportUploadVariant), buildSettingsData(), buildFeaturesData(), settingsVersion, @@ -43,7 +44,8 @@ private static SessionSettingsData buildSettingsData() { return new SessionSettingsData(64, 4); } - private static AppSettingsData buildAppData(int reportUploadVariant) { + private static AppSettingsData buildAppData( + int reportUploadVariant, int nativeReportUploadVariant) { return new AppSettingsData( AppSettingsData.STATUS_ACTIVATED, "http://localhost", @@ -52,6 +54,7 @@ private static AppSettingsData buildAppData(int reportUploadVariant) { "testBundleId", "testOrganizationId", false, - reportUploadVariant); + reportUploadVariant, + nativeReportUploadVariant); } } diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/BytesBackedNativeSessionFile.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/BytesBackedNativeSessionFile.java new file mode 100644 index 00000000000..b94a0eb84fa --- /dev/null +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/BytesBackedNativeSessionFile.java @@ -0,0 +1,64 @@ +// Copyright 2020 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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +/** A {@link NativeSessionFile} backed by a byte array. */ +class BytesBackedNativeSessionFile implements NativeSessionFile { + private final byte[] bytes; + private final String dataTransportFilename; + private final String reportsEndpointFilename; + + BytesBackedNativeSessionFile( + @NonNull String dataTransportFilename, + @NonNull String reportsEndpointFilename, + @Nullable byte[] bytes) { + this.dataTransportFilename = dataTransportFilename; + this.reportsEndpointFilename = reportsEndpointFilename; + this.bytes = bytes; + } + + @Override + @NonNull + public String getReportsEndpointFilename() { + return this.reportsEndpointFilename; + } + + @Override + @Nullable + public InputStream getStream() { + return isEmpty() ? null : new ByteArrayInputStream(bytes); + } + + @Override + @Nullable + public CrashlyticsReport.FilesPayload.File asFilePayload() { + return isEmpty() + ? null + : CrashlyticsReport.FilesPayload.File.builder() + .setContents(bytes) + .setFilename(dataTransportFilename) + .build(); + } + + private boolean isEmpty() { + return bytes == null || bytes.length == 0; + } +} 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 6ce96ce2fb4..f5cc24a0c6a 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 @@ -64,6 +64,7 @@ import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; @@ -85,7 +86,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.zip.GZIPOutputStream; @SuppressWarnings("PMD") class CrashlyticsController { @@ -104,11 +104,6 @@ class CrashlyticsController { static final String FIREBASE_APPLICATION_EXCEPTION = "_ae"; static final String FIREBASE_ANALYTICS_ORIGIN_CRASHLYTICS = "clx"; - // Used to determine whether to upload reports through the legacy reports endpoint - static final int REPORT_UPLOAD_VARIANT_LEGACY = 1; - // Used to determine whether to upload reports through the new DataTransport API. - static final int REPORT_UPLOAD_VARIANT_DATATRANSPORT = 2; - // region CLS File filters for retrieving specific sets of files. /** File filter that matches if a specified string is contained in the file name. */ @@ -433,7 +428,7 @@ public Task then(@Nullable AppSettingsData appSettingsData) reportingCoordinator.sendReports( appSettingsData.organizationId, executor, - shouldSendViaDataTransport(appSettingsData.reportUploadVariant)); + DataTransportState.getState(appSettingsData)); return recordFatalFirebaseEventTask; } }); @@ -587,8 +582,7 @@ public Task then(@Nullable AppSettingsData appSettingsData) reportingCoordinator.sendReports( appSettingsData.organizationId, executor, - shouldSendViaDataTransport( - appSettingsData.reportUploadVariant)); + DataTransportState.getState(appSettingsData)); unsentReportsHandled.trySetResult(null); return Tasks.forResult(null); @@ -607,13 +601,11 @@ public ReportUploader createReportUploader(AppSettingsData appSettingsData) { final String reportsUrl = appSettingsData.reportsUrl; final String ndkReportsUrl = appSettingsData.ndkReportsUrl; final String organizationId = appSettingsData.organizationId; - final boolean isUsingReportsEndpoint = - appSettingsData.reportUploadVariant != REPORT_UPLOAD_VARIANT_DATATRANSPORT; final CreateReportSpiCall call = getCreateReportSpiCall(reportsUrl, ndkReportsUrl); return new ReportUploader( organizationId, appData.googleAppId, - isUsingReportsEndpoint, + DataTransportState.getState(appSettingsData), reportManager, call, handlingExceptionCheck); @@ -1103,33 +1095,17 @@ public boolean accept(File dir, String filename) { // endregion - private void finalizePreviousNativeSession(String previousSessionId) throws IOException { + private void finalizePreviousNativeSession(String previousSessionId) { Logger.getLogger().d("Finalizing native report for session " + previousSessionId); NativeSessionFileProvider nativeSessionFileProvider = nativeComponent.getSessionFileProvider(previousSessionId); - - final File minidump = nativeSessionFileProvider.getMinidumpFile(); - final File binaryImages = nativeSessionFileProvider.getBinaryImagesFile(); - final File metadata = nativeSessionFileProvider.getMetadataFile(); - final File sessionFile = nativeSessionFileProvider.getSessionFile(); - final File sessionApp = nativeSessionFileProvider.getAppFile(); - final File sessionDevice = nativeSessionFileProvider.getDeviceFile(); - final File sessionOs = nativeSessionFileProvider.getOsFile(); - - if (minidump == null || !minidump.exists()) { + File minidumpFile = nativeSessionFileProvider.getMinidumpFile(); + if (minidumpFile == null || !minidumpFile.exists()) { Logger.getLogger().w("No minidump data found for session " + previousSessionId); return; } - - final File filesDir = getFilesDir(); - final MetaDataStore metaDataStore = new MetaDataStore(filesDir); - final File sessionUser = metaDataStore.getUserDataFileForSession(previousSessionId); - final File sessionKeys = metaDataStore.getKeysFileForSession(previousSessionId); - final LogFileManager previousSessionLogManager = - new LogFileManager(getContext(), logFileDirectoryProvider, previousSessionId); - final byte[] logs = previousSessionLogManager.getBytesForLog(); - + new LogFileManager(context, logFileDirectoryProvider, previousSessionId); final File nativeSessionDirectory = new File(getNativeSessionFilesDir(), previousSessionId); if (!nativeSessionDirectory.mkdirs()) { @@ -1137,65 +1113,19 @@ private void finalizePreviousNativeSession(String previousSessionId) throws IOEx return; } - gzipFile(minidump, new File(nativeSessionDirectory, "minidump")); - gzipIfNotEmpty( - NativeFileUtils.binaryImagesJsonFromMapsFile(binaryImages, context), - new File(nativeSessionDirectory, "binaryImages")); - gzipFile(metadata, new File(nativeSessionDirectory, "metadata")); - gzipFile(sessionFile, new File(nativeSessionDirectory, "session")); - gzipFile(sessionApp, new File(nativeSessionDirectory, "app")); - gzipFile(sessionDevice, new File(nativeSessionDirectory, "device")); - gzipFile(sessionOs, new File(nativeSessionDirectory, "os")); - gzipFile(sessionUser, new File(nativeSessionDirectory, "user")); - gzipFile(sessionKeys, new File(nativeSessionDirectory, "keys")); - gzipIfNotEmpty(logs, new File(nativeSessionDirectory, "logs")); - + List nativeSessionFiles = + getNativeSessionFiles( + nativeSessionFileProvider, + previousSessionId, + getContext(), + getFilesDir(), + previousSessionLogManager.getBytesForLog()); + NativeSessionFileGzipper.processNativeSessions(nativeSessionDirectory, nativeSessionFiles); + reportingCoordinator.finalizeSessionWithNativeEvent( + makeFirebaseSessionIdentifier(previousSessionId), nativeSessionFiles); previousSessionLogManager.clearLog(); } - // TODO: Maybe make this a separate collaborator/serializer - private static void gzipFile(@NonNull File input, @NonNull File output) throws IOException { - if (!input.exists() || !input.isFile()) { - return; - } - byte[] buffer = new byte[1024]; - FileInputStream fis = null; - GZIPOutputStream gos = null; - try { - fis = new FileInputStream(input); - gos = new GZIPOutputStream(new FileOutputStream(output)); - - int read; - - while ((read = fis.read(buffer)) > 0) { - gos.write(buffer, 0, read); - } - - gos.finish(); - } finally { - CommonUtils.closeQuietly(fis); - CommonUtils.closeQuietly(gos); - } - } - - private static void gzipIfNotEmpty(@Nullable byte[] content, @NonNull File path) - throws IOException { - if (content != null && content.length > 0) { - gzip(content, path); - } - } - - private static void gzip(@NonNull byte[] bytes, @NonNull File path) throws IOException { - GZIPOutputStream gos = null; - try { - gos = new GZIPOutputStream(new FileOutputStream(path)); - gos.write(bytes, 0, bytes.length); - gos.finish(); - } finally { - CommonUtils.closeQuietly(gos); - } - } - private static long getCurrentTimestampSeconds() { return getTimestampSeconds(new Date()); } @@ -1209,11 +1139,6 @@ private static String makeFirebaseSessionIdentifier(String sessionIdentifier) { return sessionIdentifier.replaceAll("-", ""); } - private static SessionReportingCoordinator.SendReportPredicate shouldSendViaDataTransport( - int reportUploadVariant) { - return () -> REPORT_UPLOAD_VARIANT_DATATRANSPORT == reportUploadVariant; - } - // region Serialization to protobuf /** @@ -1917,6 +1842,50 @@ public void run() { } } + static List getNativeSessionFiles( + NativeSessionFileProvider fileProvider, + String previousSessionId, + Context context, + File filesDir, + byte[] logBytes) { + + final MetaDataStore metaDataStore = new MetaDataStore(filesDir); + final File userFile = metaDataStore.getUserDataFileForSession(previousSessionId); + final File keysFile = metaDataStore.getKeysFileForSession(previousSessionId); + + byte[] binaryImageBytes = null; + try { + binaryImageBytes = + NativeFileUtils.binaryImagesJsonFromMapsFile(fileProvider.getBinaryImagesFile(), context); + } catch (IOException e) { + // Keep processing, we'll add an empty binaryImages object. + } + + List nativeSessionFiles = new ArrayList<>(); + nativeSessionFiles.add(new BytesBackedNativeSessionFile("logs_file", "logs", logBytes)); + nativeSessionFiles.add( + new BytesBackedNativeSessionFile("binary_images_file", "binaryImages", binaryImageBytes)); + nativeSessionFiles.add( + new FileBackedNativeSessionFile( + "crash_meta_file", "metadata", fileProvider.getMetadataFile())); + nativeSessionFiles.add( + new FileBackedNativeSessionFile( + "session_meta_file", "session", fileProvider.getSessionFile())); + nativeSessionFiles.add( + new FileBackedNativeSessionFile("app_meta_file", "app", fileProvider.getAppFile())); + nativeSessionFiles.add( + new FileBackedNativeSessionFile( + "device_meta_file", "device", fileProvider.getDeviceFile())); + nativeSessionFiles.add( + new FileBackedNativeSessionFile("os_meta_file", "os", fileProvider.getOsFile())); + nativeSessionFiles.add( + new FileBackedNativeSessionFile( + "minidump_file", "minidump", fileProvider.getMinidumpFile())); + nativeSessionFiles.add(new FileBackedNativeSessionFile("user_meta_file", "user", userFile)); + nativeSessionFiles.add(new FileBackedNativeSessionFile("keys_file", "keys", keysFile)); + return nativeSessionFiles; + } + private static final class LogFileDirectoryProvider implements LogFileManager.DirectoryProvider { private static final String LOG_FILES_DIR = "log-files"; 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 b943d7b4fb9..dba2abf4755 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 @@ -80,15 +80,11 @@ public CrashlyticsReportDataCapture( } public CrashlyticsReport captureReportData(String identifier, long timestamp) { - return CrashlyticsReport.builder() - .setSdkVersion(BuildConfig.VERSION_NAME) - .setGmpAppId(appData.googleAppId) - .setInstallationUuid(idManager.getCrashlyticsInstallId()) - .setBuildVersion(appData.versionCode) - .setDisplayVersion(appData.versionName) - .setPlatform(REPORT_ANDROID_PLATFORM) - .setSession(populateSessionData(identifier, timestamp)) - .build(); + return buildReportData().setSession(populateSessionData(identifier, timestamp)).build(); + } + + public CrashlyticsReport captureReportData() { + return buildReportData().build(); } public Event captureEventData( @@ -118,6 +114,16 @@ public Event captureEventData( .build(); } + private CrashlyticsReport.Builder buildReportData() { + return CrashlyticsReport.builder() + .setSdkVersion(BuildConfig.VERSION_NAME) + .setGmpAppId(appData.googleAppId) + .setInstallationUuid(idManager.getCrashlyticsInstallId()) + .setBuildVersion(appData.versionCode) + .setDisplayVersion(appData.versionName) + .setPlatform(REPORT_ANDROID_PLATFORM); + } + private CrashlyticsReport.Session populateSessionData(String identifier, long timestamp) { return CrashlyticsReport.Session.builder() .setStartedAt(timestamp) diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsReportWithSessionId.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsReportWithSessionId.java new file mode 100644 index 00000000000..6deec6545e3 --- /dev/null +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsReportWithSessionId.java @@ -0,0 +1,32 @@ +// Copyright 2020 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 androidx.annotation.NonNull; +import com.google.auto.value.AutoValue; +import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; + +@AutoValue +public abstract class CrashlyticsReportWithSessionId { + + public abstract CrashlyticsReport getReport(); + + public abstract String getSessionId(); + + @NonNull + public static CrashlyticsReportWithSessionId create(CrashlyticsReport report, String sessionId) { + return new AutoValue_CrashlyticsReportWithSessionId(report, sessionId); + } +} diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/DataTransportState.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/DataTransportState.java new file mode 100644 index 00000000000..5ff9ce916f8 --- /dev/null +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/DataTransportState.java @@ -0,0 +1,46 @@ +// Copyright 2020 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 com.google.firebase.crashlytics.internal.settings.model.AppSettingsData; + +public enum DataTransportState { + NONE, + JAVA_ONLY, + ALL; + + // Used to determine whether to upload reports through the legacy reports endpoint + static final int REPORT_UPLOAD_VARIANT_LEGACY = 1; + // Used to determine whether to upload reports through the new DataTransport API. + static final int REPORT_UPLOAD_VARIANT_DATATRANSPORT = 2; + + static DataTransportState getState(boolean dataTransportState, boolean dataTransportNativeState) { + if (!dataTransportState) { + return NONE; + } + if (!dataTransportNativeState) { + return JAVA_ONLY; + } + return ALL; + } + + static DataTransportState getState(AppSettingsData appSettingsData) { + final boolean dataTransportState = + appSettingsData.reportUploadVariant == REPORT_UPLOAD_VARIANT_DATATRANSPORT; + final boolean dataTransportNativeState = + appSettingsData.nativeReportUploadVariant == REPORT_UPLOAD_VARIANT_DATATRANSPORT; + return getState(dataTransportState, dataTransportNativeState); + } +} diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/FileBackedNativeSessionFile.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/FileBackedNativeSessionFile.java new file mode 100644 index 00000000000..0e97d9ed62c --- /dev/null +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/FileBackedNativeSessionFile.java @@ -0,0 +1,90 @@ +// Copyright 2020 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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +/** A {@link NativeSessionFile} backed by a {@link File} currently on disk. */ +class FileBackedNativeSessionFile implements NativeSessionFile { + + private final File file; + private final String dataTransportFilename; + private final String reportsEndpointFilename; + + FileBackedNativeSessionFile( + @NonNull String dataTransportFilename, + @NonNull String reportsEndpointFilename, + @NonNull File file) { + this.dataTransportFilename = dataTransportFilename; + this.reportsEndpointFilename = reportsEndpointFilename; + this.file = file; + } + + @Override + @NonNull + public String getReportsEndpointFilename() { + return this.reportsEndpointFilename; + } + + @Override + @Nullable + public InputStream getStream() { + if (!file.exists() || !file.isFile()) { + return null; + } + try { + return new FileInputStream(file); + } catch (FileNotFoundException f) { + return null; + } + } + + @Override + @Nullable + public CrashlyticsReport.FilesPayload.File asFilePayload() { + byte[] bytes = asBytes(); + return bytes != null + ? CrashlyticsReport.FilesPayload.File.builder() + .setContents(bytes) + .setFilename(dataTransportFilename) + .build() + : null; + } + + private byte[] asBytes() { + final byte[] readBuffer = new byte[8192]; + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (InputStream stream = getStream()) { + if (stream == null) { + return null; + } + int read; + while ((read = stream.read(readBuffer)) > 0) { + bos.write(readBuffer, 0, read); + } + return bos.toByteArray(); + } catch (IOException e) { + return null; + } + } +} diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/NativeSessionFile.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/NativeSessionFile.java new file mode 100644 index 00000000000..c866a535a81 --- /dev/null +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/NativeSessionFile.java @@ -0,0 +1,41 @@ +// Copyright 2020 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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; +import java.io.InputStream; + +/** + * An abstraction of a "File" resource sent to the Crashlytics backend when an NDK Crash has + * occurred. + */ +interface NativeSessionFile { + + /** Shortname of the file sent to the reports endpoint, e.g., "logs" */ + @NonNull + String getReportsEndpointFilename(); + + /** Representation of the NativeSessionFile as a stream */ + @Nullable + InputStream getStream(); + + /** + * Representation of the NativeSessionFile as {@link CrashlyticsReport.FilesPayload.File} object. + */ + @Nullable + CrashlyticsReport.FilesPayload.File asFilePayload(); +} diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/NativeSessionFileGzipper.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/NativeSessionFileGzipper.java new file mode 100644 index 00000000000..8e23052004c --- /dev/null +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/NativeSessionFileGzipper.java @@ -0,0 +1,69 @@ +// Copyright 2020 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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.zip.GZIPOutputStream; + +/** Copies {@link NativeSessionFile} into gzipped files within a provided path. */ +class NativeSessionFileGzipper { + + static void processNativeSessions(File nativeSessionDirectory, List streams) { + + for (NativeSessionFile stream : streams) { + InputStream inputStream = null; + try { + inputStream = stream.getStream(); + if (inputStream == null) { + continue; + } + gzipInputStream( + inputStream, new File(nativeSessionDirectory, stream.getReportsEndpointFilename())); + } catch (IOException e) { + // Skip invalid files, and we'll clean them up later. + } finally { + CommonUtils.closeQuietly(inputStream); + } + } + } + + private static void gzipInputStream(@Nullable InputStream input, @NonNull File output) + throws IOException { + if (input == null) { + return; + } + byte[] buffer = new byte[8192]; + GZIPOutputStream gos = null; + try { + gos = new GZIPOutputStream(new FileOutputStream(output)); + + int read; + + while ((read = input.read(buffer)) > 0) { + gos.write(buffer, 0, read); + } + + gos.finish(); + } finally { + CommonUtils.closeQuietly(gos); + } + } +} 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 ebb773001da..1bc967b4634 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 @@ -15,11 +15,13 @@ package com.google.firebase.crashlytics.internal.common; import android.content.Context; +import androidx.annotation.NonNull; import com.google.android.gms.tasks.Task; import com.google.firebase.crashlytics.internal.Logger; import com.google.firebase.crashlytics.internal.log.LogFileManager; 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.ImmutableList; import com.google.firebase.crashlytics.internal.persistence.CrashlyticsReportPersistence; import com.google.firebase.crashlytics.internal.persistence.FileStore; @@ -39,19 +41,11 @@ */ class SessionReportingCoordinator implements CrashlyticsLifecycleEvents { - public interface SendReportPredicate { - boolean shouldSendViaDataTransport(); - } - private static final String EVENT_TYPE_CRASH = "crash"; private static final String EVENT_TYPE_LOGGED = "error"; private static final int EVENT_THREAD_IMPORTANCE = 4; private static final int MAX_CHAINED_EXCEPTION_DEPTH = 8; - private static final int DEFAULT_MAX_EVENTS_TO_KEEP = 8; - private static final int DEFAULT_MAX_REPORTS_TO_KEEP = 4; - private static final int DEFAULT_MAX_SESSIONS_TO_KEEP = 8; - public static SessionReportingCoordinator create( Context context, IdManager idManager, @@ -130,6 +124,22 @@ public void persistNonFatalEvent(Throwable event, Thread thread, long timestamp) persistEvent(event, thread, EVENT_TYPE_LOGGED, timestamp, false); } + public void finalizeSessionWithNativeEvent( + String sessionId, @NonNull List nativeSessionFiles) { + FilesPayload.Builder filesPayloadBuilder = FilesPayload.builder(); + ArrayList nativeFiles = new ArrayList<>(); + for (NativeSessionFile nativeSessionFile : nativeSessionFiles) { + FilesPayload.File filePayload = nativeSessionFile.asFilePayload(); + if (filePayload != null) { + nativeFiles.add(filePayload); + } + } + + filesPayloadBuilder.setFiles(ImmutableList.from(nativeFiles)); + + reportPersistence.finalizeSessionWithNativeEvent(sessionId, filesPayloadBuilder.build()); + } + public void persistUserId() { reportPersistence.persistUserIdForSession(reportMetadata.getUserId(), currentSessionId); } @@ -149,22 +159,31 @@ public void removeAllReports() { * @param organizationId The organization ID this crash report should be associated with * @param reportSendCompleteExecutor executor on which to run report cleanup after each report is * sent. - * @param sendReportPredicate Predicate determining whether to send reports before cleaning them - * up + * @param dataTransportState used to determine whether to send the report before cleaning it up. */ public void sendReports( String organizationId, Executor reportSendCompleteExecutor, - SendReportPredicate sendReportPredicate) { - if (!sendReportPredicate.shouldSendViaDataTransport()) { - Logger.getLogger().d("Send via DataTransport disabled. Removing reports."); + DataTransportState dataTransportState) { + if (dataTransportState == DataTransportState.NONE) { + Logger.getLogger().d("Send via DataTransport disabled. Removing DataTransport reports."); reportPersistence.deleteAllReports(); return; } - final List reportsToSend = reportPersistence.loadFinalizedReports(); - for (CrashlyticsReport report : reportsToSend) { + final List reportsToSend = + reportPersistence.loadFinalizedReports(); + for (CrashlyticsReportWithSessionId report : reportsToSend) { + if (report.getReport().getType() == CrashlyticsReport.Type.NATIVE + && dataTransportState != DataTransportState.ALL) { + Logger.getLogger() + .d("Send native reports via DataTransport disabled. Removing DataTransport reports."); + reportPersistence.deleteFinalizedReport(report.getSessionId()); + continue; + } reportsSender - .sendReport(report.withOrganizationId(organizationId)) + .sendReport( + CrashlyticsReportWithSessionId.create( + report.getReport().withOrganizationId(organizationId), report.getSessionId())) .continueWith(reportSendCompleteExecutor, this::onReportSendComplete); } } @@ -213,13 +232,13 @@ private void persistEvent( reportPersistence.persistEvent(eventBuilder.build(), currentSessionId, isHighPriority); } - private boolean onReportSendComplete(Task task) { + private boolean onReportSendComplete(Task task) { if (task.isSuccessful()) { // TODO: if the report is fatal, send an analytics event. - final CrashlyticsReport report = task.getResult(); - final String reportId = report.getSession().getIdentifier(); - Logger.getLogger().i("Crashlytics report sent successfully: " + reportId); - reportPersistence.deleteFinalizedReport(reportId); + final CrashlyticsReportWithSessionId report = task.getResult(); + Logger.getLogger() + .i("Crashlytics report successfully enqueued to DataTransport: " + report.getSessionId()); + reportPersistence.deleteFinalizedReport(report.getSessionId()); return true; } // TODO: Something went wrong. Log? Throw? 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 669a93e5c12..2476af8f86b 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 @@ -57,6 +57,12 @@ public abstract class CrashlyticsReport { int UNKNOWN = 7; } + public enum Type { + INCOMPLETE, + JAVA, + NATIVE + } + private static final Charset UTF_8 = Charset.forName("UTF-8"); @NonNull @@ -64,6 +70,16 @@ public static Builder builder() { return new AutoValue_CrashlyticsReport.Builder(); } + @Ignore + public Type getType() { + if (getSession() != null) { + return Type.JAVA; + } else if (getNdkPayload() != null) { + return Type.NATIVE; + } + return Type.INCOMPLETE; + } + @NonNull public abstract String getSdkVersion(); @@ -81,12 +97,11 @@ public static Builder builder() { @NonNull public abstract String getDisplayVersion(); - @NonNull + @Nullable public abstract Session getSession(); - // TODO: Add back once NDK data is ready to be serialized - // @Nullable - // public abstract byte[] getNdkPayload(); + @Nullable + public abstract FilesPayload getNdkPayload(); @NonNull protected abstract Builder toBuilder(); @@ -98,6 +113,10 @@ public static Builder builder() { */ @NonNull public CrashlyticsReport withEvents(@NonNull ImmutableList events) { + if (getSession() == null) { + throw new IllegalStateException("Reports without sessions cannot have events added to them."); + } + return toBuilder().setSession(getSession().withEvents(events)).build(); } @@ -109,18 +128,29 @@ public CrashlyticsReport withEvents(@NonNull ImmutableList events) { */ @NonNull public CrashlyticsReport withOrganizationId(@NonNull String organizationId) { - return toBuilder().setSession(getSession().withOrganizationId(organizationId)).build(); + CrashlyticsReport.Builder builder = toBuilder(); + + FilesPayload ndkPayload = getNdkPayload(); + if (ndkPayload != null) { + builder.setNdkPayload(ndkPayload.toBuilder().setOrgId(organizationId).build()); + } + + Session session = getSession(); + if (session != null) { + builder.setSession(session.withOrganizationId(organizationId)); + } + + return builder.build(); } /** - * Augment an existing {@link CrashlyticsReport} with a given user ID. + * Augment an existing {@link CrashlyticsReport} with an NdkPayload * - * @return a new {@link CrashlyticsReport} with its Session.User object containing the given user - * ID. + * @return a new {@link CrashlyticsReport} with Ndk data inside of it. */ @NonNull - public CrashlyticsReport withUserId(@NonNull String userId) { - return toBuilder().setSession(getSession().withUserId(userId)).build(); + public CrashlyticsReport withNdkPayload(@NonNull FilesPayload filesPayload) { + return toBuilder().setSession(null).setNdkPayload(filesPayload).build(); } /** @@ -134,9 +164,65 @@ public CrashlyticsReport withUserId(@NonNull String userId) { @NonNull public CrashlyticsReport withSessionEndFields( long endedAt, boolean isCrashed, @Nullable String userId) { - return toBuilder() - .setSession(getSession().withSessionEndFields(endedAt, isCrashed, userId)) - .build(); + final Builder builder = toBuilder(); + if (getSession() != null) { + builder.setSession(getSession().withSessionEndFields(endedAt, isCrashed, userId)); + } + return builder.build(); + } + + @AutoValue + public abstract static class FilesPayload { + + @NonNull + public static Builder builder() { + return new AutoValue_CrashlyticsReport_FilesPayload.Builder(); + } + + @NonNull + public abstract ImmutableList getFiles(); + + @Nullable + public abstract String getOrgId(); + + abstract Builder toBuilder(); + + @AutoValue + public abstract static class File { + + @NonNull + public static Builder builder() { + return new AutoValue_CrashlyticsReport_FilesPayload_File.Builder(); + } + + @NonNull + public abstract String getFilename(); + + @NonNull + public abstract byte[] getContents(); + + /** Builder for {@link File}. */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setFilename(String value); + + public abstract Builder setContents(byte[] value); + + public abstract File build(); + } + } + + /** Builder for {@link FilesPayload}. */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setFiles(ImmutableList value); + + public abstract Builder setOrgId(String value); + + public abstract FilesPayload build(); + } } @AutoValue @@ -227,11 +313,6 @@ Session withOrganizationId(@NonNull String organizationId) { return toBuilder().setApp(app).build(); } - @NonNull - Session withUserId(@NonNull String userId) { - return toBuilder().setUser(User.builder().setIdentifier(userId).build()).build(); - } - @NonNull Session withSessionEndFields(long timestamp, boolean isCrashed, @Nullable String userId) { final Builder builder = toBuilder(); @@ -949,6 +1030,9 @@ public abstract static class Builder { @NonNull public abstract Builder setSession(@NonNull Session value); + @NonNull + public abstract Builder setNdkPayload(FilesPayload value); + @NonNull public abstract CrashlyticsReport build(); } 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 b47d9ef2fbf..e55db635fdd 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 @@ -97,6 +97,9 @@ private static CrashlyticsReport parseReport(JsonReader jsonReader) throws IOExc case "session": builder.setSession(parseSession(jsonReader)); break; + case "ndkPayload": + builder.setNdkPayload(parseNdkPayload(jsonReader)); + break; default: jsonReader.skipValue(); break; @@ -157,6 +160,55 @@ private static CrashlyticsReport.Session parseSession(JsonReader jsonReader) thr return builder.build(); } + private static CrashlyticsReport.FilesPayload parseNdkPayload(JsonReader jsonReader) + throws IOException { + final CrashlyticsReport.FilesPayload.Builder builder = CrashlyticsReport.FilesPayload.builder(); + + jsonReader.beginObject(); + while (jsonReader.hasNext()) { + String name = jsonReader.nextName(); + switch (name) { + case "files": + builder.setFiles(parseArray(jsonReader, CrashlyticsReportJsonTransform::parseFile)); + break; + case "orgId": + builder.setOrgId(jsonReader.nextString()); + break; + default: + jsonReader.skipValue(); + break; + } + } + jsonReader.endObject(); + + return builder.build(); + } + + private static CrashlyticsReport.FilesPayload.File parseFile(JsonReader jsonReader) + throws IOException { + final CrashlyticsReport.FilesPayload.File.Builder builder = + CrashlyticsReport.FilesPayload.File.builder(); + + jsonReader.beginObject(); + while (jsonReader.hasNext()) { + String name = jsonReader.nextName(); + switch (name) { + case "filename": + builder.setFilename(jsonReader.nextString()); + break; + case "contents": + builder.setContents(Base64.decode(jsonReader.nextString(), Base64.NO_WRAP)); + break; + default: + jsonReader.skipValue(); + break; + } + } + jsonReader.endObject(); + + return builder.build(); + } + private static CrashlyticsReport.Session.User parseUser(JsonReader jsonReader) throws IOException { final CrashlyticsReport.Session.User.Builder builder = CrashlyticsReport.Session.User.builder(); 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 c187c48dbcd..dd2f1ec452f 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 @@ -15,6 +15,7 @@ package com.google.firebase.crashlytics.internal.persistence; import androidx.annotation.NonNull; +import com.google.firebase.crashlytics.internal.common.CrashlyticsReportWithSessionId; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.Session.Event; import com.google.firebase.crashlytics.internal.model.ImmutableList; @@ -50,6 +51,7 @@ public class CrashlyticsReportPersistence { private static final String WORKING_DIRECTORY_NAME = "report-persistence"; private static final String OPEN_SESSIONS_DIRECTORY_NAME = "sessions"; private static final String PRIORITY_REPORTS_DIRECTORY = "priority-reports"; + private static final String NATIVE_REPORTS_DIRECTORY = "native-reports"; private static final String REPORTS_DIRECTORY = "reports"; private static final String REPORT_FILE_NAME = "report"; @@ -78,6 +80,9 @@ public class CrashlyticsReportPersistence { private final File priorityReportsDirectory; private final File reportsDirectory; + // Storage for ndk Reports + private final File nativeReportsDirectory; + private final SettingsDataProvider settingsDataProvider; public CrashlyticsReportPersistence( @@ -86,6 +91,7 @@ public CrashlyticsReportPersistence( openSessionsDirectory = new File(workingDirectory, OPEN_SESSIONS_DIRECTORY_NAME); priorityReportsDirectory = new File(workingDirectory, PRIORITY_REPORTS_DIRECTORY); reportsDirectory = new File(workingDirectory, REPORTS_DIRECTORY); + nativeReportsDirectory = new File(workingDirectory, NATIVE_REPORTS_DIRECTORY); this.settingsDataProvider = settingsDataProvider; } @@ -154,14 +160,44 @@ public void deleteAllReports() { public void deleteFinalizedReport(String sessionId) { final FilenameFilter filter = (d, f) -> f.startsWith(sessionId); List filteredReports = - sortAndCombineReportFiles( + combineReportFiles( getFilesInDirectory(priorityReportsDirectory, filter), + getFilesInDirectory(nativeReportsDirectory, filter), getFilesInDirectory(reportsDirectory, filter)); for (File reportFile : filteredReports) { reportFile.delete(); } } + public void finalizeSessionWithNativeEvent( + String sessionId, CrashlyticsReport.FilesPayload build) { + final File outputDirectory = prepareDirectory(nativeReportsDirectory); + + File sessionFile = new File(getSessionDirectoryById(sessionId), REPORT_FILE_NAME); + + if (!sessionFile.exists()) { + return; + } + + String textFile = readTextFile(sessionFile); + + if (textFile == null) { + return; + } + + CrashlyticsReport report = TRANSFORM.reportFromJson(textFile); + + // In the unlikely event the open non-native report has been cleaned up, + // we no longer can retrieve the relevant context about the session. + if (report == null) { + return; + } + + report = report.withNdkPayload(build); + + writeTextFile(new File(outputDirectory, sessionId), TRANSFORM.reportToJson(report)); + } + // TODO: Deal with potential runtime exceptions public void finalizeReports(String currentSessionId, long sessionEndTime) { // TODO: Need to implement procedure to skip finalizing the current session when this is @@ -180,12 +216,14 @@ public void finalizeReports(String currentSessionId, long sessionEndTime) { * @return finalized (no longer changing) Crashlytics Reports, sorted first from high to low * priority, secondarily sorted from most recent to least */ - public List loadFinalizedReports() { + public List loadFinalizedReports() { final List allReportFiles = getAllFinalizedReportFiles(); - final ArrayList allReports = new ArrayList<>(); + final ArrayList allReports = new ArrayList<>(); allReports.ensureCapacity(allReportFiles.size()); for (File reportFile : getAllFinalizedReportFiles()) { - allReports.add(TRANSFORM.reportFromJson(readTextFile(reportFile))); + // TODO: Handle null value in jsonReport in a way that eventually cleans up file. + CrashlyticsReport jsonReport = TRANSFORM.reportFromJson(readTextFile(reportFile)); + allReports.add(CrashlyticsReportWithSessionId.create(jsonReport, reportFile.getName())); } return allReports; } @@ -233,7 +271,10 @@ private void capFinalizedReports() { @NonNull private List getAllFinalizedReportFiles() { return sortAndCombineReportFiles( - getAllFilesInDirectory(priorityReportsDirectory), getAllFilesInDirectory(reportsDirectory)); + combineReportFiles( + getAllFilesInDirectory(priorityReportsDirectory), + getAllFilesInDirectory(nativeReportsDirectory)), + getAllFilesInDirectory(reportsDirectory)); } private File getSessionDirectoryById(String sessionId) { @@ -291,14 +332,24 @@ private void synthesizeReportFile(File sessionDirectory, long sessionEndTime) { } @NonNull - private static List sortAndCombineReportFiles( - List priorityReports, List reports) { - Collections.sort(priorityReports, LATEST_SESSION_ID_FIRST_COMPARATOR); - Collections.sort(reports, LATEST_SESSION_ID_FIRST_COMPARATOR); + private static List sortAndCombineReportFiles(List... reports) { + for (List reportList : reports) { + Collections.sort(reportList, LATEST_SESSION_ID_FIRST_COMPARATOR); + } + + return combineReportFiles(reports); + } + + private static List combineReportFiles(List... reports) { final ArrayList allReportsFiles = new ArrayList<>(); - allReportsFiles.ensureCapacity(priorityReports.size() + reports.size()); - allReportsFiles.addAll(priorityReports); - allReportsFiles.addAll(reports); + int totalReports = 0; + for (List reportList : reports) { + totalReports += reportList.size(); + } + allReportsFiles.ensureCapacity(totalReports); + for (List reportList : reports) { + allReportsFiles.addAll(reportList); + } return allReportsFiles; } diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/report/ReportUploader.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/report/ReportUploader.java index 42a30babe27..35d889e67d4 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/report/ReportUploader.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/report/ReportUploader.java @@ -16,6 +16,7 @@ import com.google.firebase.crashlytics.internal.Logger; import com.google.firebase.crashlytics.internal.common.BackgroundPriorityRunnable; +import com.google.firebase.crashlytics.internal.common.DataTransportState; import com.google.firebase.crashlytics.internal.report.model.CreateReportRequest; import com.google.firebase.crashlytics.internal.report.model.Report; import com.google.firebase.crashlytics.internal.report.network.CreateReportSpiCall; @@ -48,7 +49,7 @@ public interface ReportFilesProvider { private final CreateReportSpiCall createReportCall; private final String organizationId; private final String googleAppId; - private final boolean isUsingReportsEndpoint; + private final DataTransportState dataTransportState; private final ReportManager reportManager; private final HandlingExceptionCheck handlingExceptionCheck; private Thread uploadThread; @@ -56,7 +57,7 @@ public interface ReportFilesProvider { public ReportUploader( String organizationId, String googleAppId, - boolean isUsingReportsEndpoint, + DataTransportState dataTransportState, ReportManager reportManager, CreateReportSpiCall createReportCall, HandlingExceptionCheck handlingExceptionCheck) { @@ -66,7 +67,7 @@ public ReportUploader( this.createReportCall = createReportCall; this.organizationId = organizationId; this.googleAppId = googleAppId; - this.isUsingReportsEndpoint = isUsingReportsEndpoint; + this.dataTransportState = dataTransportState; this.reportManager = reportManager; this.handlingExceptionCheck = handlingExceptionCheck; } @@ -99,21 +100,24 @@ public boolean uploadReport(Report report, boolean dataCollectionToken) { final CreateReportRequest requestData = new CreateReportRequest(organizationId, googleAppId, report); - boolean shouldDeleteReport; - // For now, send native reports to reports endpoint regardless of the setting. - // TODO: Remove report type check once all reports can be sent through DataTransport - if (isUsingReportsEndpoint || report.getType() == Report.Type.NATIVE) { - final boolean sent = createReportCall.invoke(requestData, dataCollectionToken); + boolean shouldDeleteReport = true; + if (dataTransportState == DataTransportState.ALL) { + Logger.getLogger() + .d("Send to Reports Endpoint disabled. Removing Reports Endpoint report."); + } else if (dataTransportState == DataTransportState.JAVA_ONLY + && report.getType() == Report.Type.JAVA) { + Logger.getLogger() + .d( + "Send to Reports Endpoint for non-native reports disabled. Removing Reports Uploader report."); + } else { + final boolean sent = createReportCall.invoke(requestData, dataCollectionToken); Logger.getLogger() .i( - "Crashlytics report upload " + "Crashlytics Reports Endpoint upload " + (sent ? "complete: " : "FAILED: ") + report.getIdentifier()); shouldDeleteReport = sent; - } else { - Logger.getLogger().d("Send to reports endpoint disabled. Removing report."); - shouldDeleteReport = true; } if (shouldDeleteReport) { diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/send/DataTransportCrashlyticsReportSender.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/send/DataTransportCrashlyticsReportSender.java index 742ae6eb140..425f2667f22 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/send/DataTransportCrashlyticsReportSender.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/send/DataTransportCrashlyticsReportSender.java @@ -23,6 +23,7 @@ import com.google.android.datatransport.runtime.TransportRuntime; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.firebase.crashlytics.internal.common.CrashlyticsReportWithSessionId; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; import com.google.firebase.crashlytics.internal.model.serialization.CrashlyticsReportJsonTransform; import java.nio.charset.Charset; @@ -60,10 +61,11 @@ public static DataTransportCrashlyticsReportSender create(Context context) { this.transport = transport; } - public Task sendReport(@NonNull CrashlyticsReport report) { - TaskCompletionSource tcs = new TaskCompletionSource<>(); + public Task sendReport( + @NonNull CrashlyticsReportWithSessionId report) { + TaskCompletionSource tcs = new TaskCompletionSource<>(); transport.schedule( - Event.ofUrgent(report), + Event.ofUrgent(report.getReport()), error -> { if (error != null) { tcs.trySetException(error); 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 235cb1621dc..c5965059ac5 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 @@ -44,10 +44,12 @@ class SettingsJsonConstants { static final String APP_NDK_REPORTS_URL_KEY = "ndk_reports_url"; static final String APP_UPDATE_REQUIRED_KEY = "update_required"; static final String APP_REPORT_UPLOAD_VARIANT_KEY = "report_upload_variant"; + static final String APP_NATIVE_REPORT_UPLOAD_VARIANT_KEY = "native_report_upload_variant"; // App JSON Defaults static final boolean APP_UPDATE_REQUIRED_DEFAULT = false; static final int APP_REPORT_UPLOAD_VARIANT_DEFAULT = 0; + static final int APP_NATIVE_REPORT_UPLOAD_VARIANT_DEFAULT = 0; // Settings JSON Keys static final String SETTINGS_MAX_CUSTOM_EXCEPTION_EVENTS_KEY = "max_custom_exception_events"; 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 ad162c0ff01..8f3a2daf32a 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 @@ -96,6 +96,11 @@ private static AppSettingsData buildAppDataFrom(JSONObject fabricJson, JSONObjec SettingsJsonConstants.APP_REPORT_UPLOAD_VARIANT_KEY, SettingsJsonConstants.APP_REPORT_UPLOAD_VARIANT_DEFAULT); + final int nativeReportUploadVariant = + appJson.optInt( + SettingsJsonConstants.APP_NATIVE_REPORT_UPLOAD_VARIANT_KEY, + SettingsJsonConstants.APP_NATIVE_REPORT_UPLOAD_VARIANT_DEFAULT); + return new AppSettingsData( status, url, @@ -104,7 +109,8 @@ private static AppSettingsData buildAppDataFrom(JSONObject fabricJson, JSONObjec bundleId, organizationId, updateRequired, - reportUploadVariant); + reportUploadVariant, + nativeReportUploadVariant); } private static FeaturesSettingsData buildFeaturesSessionDataFrom(JSONObject json) { @@ -141,7 +147,10 @@ private JSONObject toAppJson(AppSettingsData appData) throws JSONException { new JSONObject() .put(SettingsJsonConstants.APP_STATUS_KEY, appData.status) .put(SettingsJsonConstants.APP_UPDATE_REQUIRED_KEY, appData.updateRequired) - .put(SettingsJsonConstants.APP_REPORT_UPLOAD_VARIANT_KEY, appData.reportUploadVariant); + .put(SettingsJsonConstants.APP_REPORT_UPLOAD_VARIANT_KEY, appData.reportUploadVariant) + .put( + SettingsJsonConstants.APP_NATIVE_REPORT_UPLOAD_VARIANT_KEY, + appData.nativeReportUploadVariant); return json; } diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/model/AppSettingsData.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/model/AppSettingsData.java index 03171f0913e..114e9d1a452 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/model/AppSettingsData.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/settings/model/AppSettingsData.java @@ -28,6 +28,7 @@ public class AppSettingsData { public final String organizationId; public final boolean updateRequired; public final int reportUploadVariant; + public final int nativeReportUploadVariant; public AppSettingsData( String status, @@ -37,7 +38,8 @@ public AppSettingsData( String bundleId, String organizationId, boolean updateRequired, - int reportUploadVariant) { + int reportUploadVariant, + int nativeReportUploadVariant) { this.status = status; this.url = url; this.reportsUrl = reportsUrl; @@ -46,10 +48,11 @@ public AppSettingsData( this.organizationId = organizationId; this.updateRequired = updateRequired; this.reportUploadVariant = reportUploadVariant; + this.nativeReportUploadVariant = nativeReportUploadVariant; } public AppSettingsData( String status, String url, String reportsUrl, String ndkReportsUrl, boolean updateRequired) { - this(status, url, reportsUrl, ndkReportsUrl, null, null, updateRequired, 0); + this(status, url, reportsUrl, ndkReportsUrl, null, null, updateRequired, 0, 0); } } 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 f0eec213a3e..93c1ec78f53 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 @@ -59,19 +59,6 @@ public void testWithOrganizationId_returnsNewReportWithOrganizationId() { withOrganizationIdReport.getSession().getApp().getOrganization().getClsId()); } - @Test - public void testWithUserId_returnsNewReportWithUserId() { - final CrashlyticsReport testReport = makeTestReport(); - - assertNull(testReport.getSession().getUser()); - - final CrashlyticsReport withUserIdReport = testReport.withUserId("userId"); - - assertNotEquals(testReport, withUserIdReport); - assertNotNull(withUserIdReport.getSession().getUser()); - assertEquals("userId", withUserIdReport.getSession().getUser().getIdentifier()); - } - @Test public void testWithSessionEndFields_returnsNewReportWithSessionEndFields() { final CrashlyticsReport testReport = makeTestReport();