From 3e15e86bc299f4f12c62b72702b7862ca2e091ef Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Mon, 3 Feb 2025 16:46:10 -0500
Subject: [PATCH 001/146] Upload a prototype of lazy AQS mapping
---
.../src/third_party/crashpad | 2 +-
firebase-crashlytics-ndk/src/third_party/lss | 2 +-
.../src/third_party/mini_chromium | 2 +-
.../firebase/perf/FirebasePerfEarly.java | 5 +-
.../firebase/perf/FirebasePerfRegistrar.java | 8 +++
.../firebase/perf/FirebasePerformance.java | 6 ++
.../firebase/perf/metrics/AppStartTrace.java | 1 +
.../firebase/perf/session/PerfSession.java | 30 ++++++---
.../firebase/perf/session/SessionManager.java | 62 ++++++++++---------
.../firebase/perf/session/SessionManagerKt.kt | 32 ++++++++++
.../perf/FirebasePerformanceTestBase.java | 2 +-
.../perf/metrics/AppStartTraceTest.java | 2 +-
.../NetworkRequestMetricBuilderTest.java | 4 +-
.../firebase/perf/metrics/TraceTest.java | 4 +-
.../perf/session/PerfSessionTest.java | 16 ++---
.../perf/session/SessionManagerTest.java | 22 +++----
.../api/FirebaseSessionsDependencies.kt | 13 ----
17 files changed, 131 insertions(+), 82 deletions(-)
create mode 100644 firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt
diff --git a/firebase-crashlytics-ndk/src/third_party/crashpad b/firebase-crashlytics-ndk/src/third_party/crashpad
index c902f6b1c9e..b8937c6cb4b 160000
--- a/firebase-crashlytics-ndk/src/third_party/crashpad
+++ b/firebase-crashlytics-ndk/src/third_party/crashpad
@@ -1 +1 @@
-Subproject commit c902f6b1c9e43224181969110b83e0053b2ddd3c
+Subproject commit b8937c6cb4b38c1ca06b46791c84b31632895f1f
diff --git a/firebase-crashlytics-ndk/src/third_party/lss b/firebase-crashlytics-ndk/src/third_party/lss
index 9719c1e1e67..ed31caa60f2 160000
--- a/firebase-crashlytics-ndk/src/third_party/lss
+++ b/firebase-crashlytics-ndk/src/third_party/lss
@@ -1 +1 @@
-Subproject commit 9719c1e1e676814c456b55f5f070eabad6709d31
+Subproject commit ed31caa60f20a4f6569883b2d752ef7522de51e0
diff --git a/firebase-crashlytics-ndk/src/third_party/mini_chromium b/firebase-crashlytics-ndk/src/third_party/mini_chromium
index 4332ddb6963..c081fd005b0 160000
--- a/firebase-crashlytics-ndk/src/third_party/mini_chromium
+++ b/firebase-crashlytics-ndk/src/third_party/mini_chromium
@@ -1 +1 @@
-Subproject commit 4332ddb6963750e1106efdcece6d6e2de6dc6430
+Subproject commit c081fd005b09a59a505b09a4b506f8ba45f70859
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java
index 5b89deaad82..91fcf4a0c49 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java
@@ -23,6 +23,8 @@
import com.google.firebase.perf.metrics.AppStartTrace;
import com.google.firebase.perf.session.SessionManager;
import java.util.concurrent.Executor;
+import com.google.firebase.perf.logging.AndroidLogger;
+import com.google.firebase.perf.session.SessionManagerKt;
/**
* The Firebase Performance early initialization.
@@ -51,12 +53,11 @@ public FirebasePerfEarly(
uiExecutor.execute(new AppStartTrace.StartFromBackgroundRunnable(appStartTrace));
}
- // TODO: Bring back Firebase Sessions dependency to watch for updates to sessions.
-
// In the case of cold start, we create a session and start collecting gauges as early as
// possible.
// There is code in SessionManager that prevents us from resetting the session twice in case
// of app cold start.
+ AndroidLogger.getInstance().debug("Initializing Gauge Collection");
SessionManager.getInstance().initializeGaugeCollection();
}
}
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java
index c01f035af1f..4f3a1d242ea 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java
@@ -30,6 +30,10 @@
import com.google.firebase.perf.injection.modules.FirebasePerformanceModule;
import com.google.firebase.platforminfo.LibraryVersionComponent;
import com.google.firebase.remoteconfig.RemoteConfigComponent;
+import com.google.firebase.sessions.FirebaseSessions;
+import com.google.firebase.sessions.api.FirebaseSessionsDependencies;
+import com.google.firebase.sessions.api.SessionSubscriber;
+
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
@@ -47,6 +51,10 @@ public class FirebasePerfRegistrar implements ComponentRegistrar {
private static final String LIBRARY_NAME = "fire-perf";
private static final String EARLY_LIBRARY_NAME = "fire-perf-early";
+ static {
+ FirebaseSessionsDependencies.addDependency(SessionSubscriber.Name.PERFORMANCE);
+ }
+
@Override
@Keep
public List> getComponents() {
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
index 40468566225..419f42246ce 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
@@ -37,11 +37,14 @@
import com.google.firebase.perf.metrics.HttpMetric;
import com.google.firebase.perf.metrics.Trace;
import com.google.firebase.perf.session.SessionManager;
+import com.google.firebase.perf.session.SessionManagerKt;
import com.google.firebase.perf.transport.TransportManager;
import com.google.firebase.perf.util.Constants;
import com.google.firebase.perf.util.ImmutableBundle;
import com.google.firebase.perf.util.Timer;
import com.google.firebase.remoteconfig.RemoteConfigComponent;
+import com.google.firebase.sessions.api.FirebaseSessionsDependencies;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.URL;
@@ -199,6 +202,9 @@ public static FirebasePerformance getInstance() {
ConsoleUrlGenerator.generateDashboardUrl(
firebaseApp.getOptions().getProjectId(), appContext.getPackageName())));
}
+
+ SessionManagerKt sessionSubscriber = new SessionManagerKt(isPerformanceCollectionEnabled());
+ FirebaseSessionsDependencies.register(sessionSubscriber);
}
/**
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java b/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java
index 7574f989d92..7d76684dc01 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java
@@ -370,6 +370,7 @@ public synchronized void onActivityResumed(Activity activity) {
appStartActivity = new WeakReference(activity);
onResumeTime = clock.getTime();
+ // TODO(b/394127311): Defer this to SessionManagerKt
this.startSession = SessionManager.getInstance().perfSession();
AndroidLogger.getInstance()
.debug(
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
index 160a4507560..f206e919a30 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
@@ -24,12 +24,14 @@
import com.google.firebase.perf.util.Timer;
import com.google.firebase.perf.v1.SessionVerbosity;
import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
import java.util.concurrent.TimeUnit;
/** Details of a session including a unique Id and related information. */
public class PerfSession implements Parcelable {
- private final String sessionId;
+ private final String internalSessionId;
private final Timer creationTime;
private boolean isGaugeAndEventCollectionEnabled = false;
@@ -37,31 +39,41 @@ public class PerfSession implements Parcelable {
/*
* Creates a PerfSession object and decides what metrics to collect.
*/
- public static PerfSession createWithId(@NonNull String sessionId) {
- String prunedSessionId = sessionId.replace("-", "");
+ public static PerfSession createNewSession() {
+ String prunedSessionId = UUID.randomUUID().toString().replace("-", "");
PerfSession session = new PerfSession(prunedSessionId, new Clock());
session.setGaugeAndEventCollectionEnabled(shouldCollectGaugesAndEvents());
+ // Every time a PerfSession is created, it sets the AQS to null. Once an AQS is received,
+ // SessionManagerKt verifies if this is an active session, and sets the AQS session ID.
+ // The assumption is that new PerfSessions *should* be limited to either App Start, or through AQS.
+ SessionManagerKt.Companion.getPerfSessionToAqs().put(prunedSessionId, null);
+
return session;
}
/** Creates a PerfSession with the provided {@code sessionId} and {@code clock}. */
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
- public PerfSession(String sessionId, Clock clock) {
- this.sessionId = sessionId;
+ public PerfSession(String internalSessionId, Clock clock) {
+ this.internalSessionId = internalSessionId;
creationTime = clock.getTime();
}
private PerfSession(@NonNull Parcel in) {
super();
- sessionId = in.readString();
+ internalSessionId = in.readString();
isGaugeAndEventCollectionEnabled = in.readByte() != 0;
creationTime = in.readParcelable(Timer.class.getClassLoader());
}
/** Returns the sessionId of the object. */
public String sessionId() {
- return sessionId;
+ // TODO(b/394127311): Verify edge cases.
+ return Objects.requireNonNull(SessionManagerKt.Companion.getPerfSessionToAqs().get(internalSessionId)).getSessionId();
+ }
+
+ protected String getInternalSessionId() {
+ return internalSessionId;
}
/**
@@ -114,7 +126,7 @@ public boolean isSessionRunningTooLong() {
/** Creates and returns the proto object for PerfSession object. */
public com.google.firebase.perf.v1.PerfSession build() {
com.google.firebase.perf.v1.PerfSession.Builder sessionMetric =
- com.google.firebase.perf.v1.PerfSession.newBuilder().setSessionId(sessionId);
+ com.google.firebase.perf.v1.PerfSession.newBuilder().setSessionId(internalSessionId);
// If gauge collection is enabled, enable gauge collection verbosity.
if (isGaugeAndEventCollectionEnabled) {
@@ -189,7 +201,7 @@ public int describeContents() {
* @param flags Additional flags about how the object should be written.
*/
public void writeToParcel(@NonNull Parcel out, int flags) {
- out.writeString(sessionId);
+ out.writeString(internalSessionId);
out.writeByte((byte) (isGaugeAndEventCollectionEnabled ? 1 : 0));
out.writeParcelable(creationTime, 0);
}
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
index 79d034b9b0b..869ef1ef055 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
@@ -27,8 +27,8 @@
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.Objects;
import java.util.Set;
-import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@@ -40,6 +40,8 @@ public class SessionManager extends AppStateUpdateHandler {
@SuppressLint("StaticFieldLeak")
private static final SessionManager instance = new SessionManager();
+ private static final String COLD_START_GAUGE_NAME = "coldstart";
+
private final GaugeManager gaugeManager;
private final AppStateMonitor appStateMonitor;
private final Set> clients = new HashSet<>();
@@ -61,7 +63,7 @@ private SessionManager() {
// Generate a new sessionID for every cold start.
this(
GaugeManager.getInstance(),
- PerfSession.createWithId(UUID.randomUUID().toString()),
+ PerfSession.createNewSession(),
AppStateMonitor.getInstance());
}
@@ -96,33 +98,33 @@ public void setApplicationContext(final Context appContext) {
});
}
- @Override
- public void onUpdateAppState(ApplicationProcessState newAppState) {
- super.onUpdateAppState(newAppState);
-
- if (appStateMonitor.isColdStart()) {
- // We want the Session to remain unchanged if this is a cold start of the app since we already
- // update the PerfSession in FirebasePerfProvider#onAttachInfo().
- return;
- }
-
- if (newAppState == ApplicationProcessState.FOREGROUND) {
- // A new foregrounding of app will force a new sessionID generation.
- PerfSession session = PerfSession.createWithId(UUID.randomUUID().toString());
- updatePerfSession(session);
- } else {
- // If the session is running for too long, generate a new session and collect gauges as
- // necessary.
- if (perfSession.isSessionRunningTooLong()) {
- PerfSession session = PerfSession.createWithId(UUID.randomUUID().toString());
- updatePerfSession(session);
- } else {
- // For any other state change of the application, modify gauge collection state as
- // necessary.
- startOrStopCollectingGauges(newAppState);
- }
- }
- }
+// @Override
+// public void onUpdateAppState(ApplicationProcessState newAppState) {
+// super.onUpdateAppState(newAppState);
+//
+// if (appStateMonitor.isColdStart()) {
+// // We want the Session to remain unchanged if this is a cold start of the app since we already
+// // update the PerfSession in FirebasePerfProvider#onAttachInfo().
+// return;
+// }
+//
+// if (newAppState == ApplicationProcessState.FOREGROUND) {
+// // A new foregrounding of app will force a new sessionID generation.
+// PerfSession session = PerfSession.createWithId(UUID.randomUUID().toString());
+// updatePerfSession(session);
+// } else {
+// // If the session is running for too long, generate a new session and collect gauges as
+// // necessary.
+// if (perfSession.isSessionRunningTooLong()) {
+// PerfSession session = PerfSession.createWithId(UUID.randomUUID().toString());
+// updatePerfSession(session);
+// } else {
+// // For any other state change of the application, modify gauge collection state as
+// // necessary.
+// startOrStopCollectingGauges(newAppState);
+// }
+// }
+// }
/**
* Checks if the current {@link PerfSession} is expired/timed out. If so, stop collecting gauges.
@@ -145,7 +147,7 @@ public void stopGaugeCollectionIfSessionRunningTooLong() {
*/
public void updatePerfSession(PerfSession perfSession) {
// Do not update the perf session if it is the exact same sessionId.
- if (perfSession.sessionId() == this.perfSession.sessionId()) {
+ if (Objects.equals(perfSession.getInternalSessionId(), this.perfSession.getInternalSessionId())) {
return;
}
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt
new file mode 100644
index 00000000000..e2c4f9cab51
--- /dev/null
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt
@@ -0,0 +1,32 @@
+package com.google.firebase.perf.session
+
+import com.google.firebase.sessions.api.SessionSubscriber
+import com.google.firebase.perf.logging.AndroidLogger
+
+
+class SessionManagerKt(val dataCollectionEnabled: Boolean): SessionSubscriber {
+ override val isDataCollectionEnabled: Boolean
+ get() = dataCollectionEnabled
+
+ override val sessionSubscriberName: SessionSubscriber.Name
+ get() = SessionSubscriber.Name.PERFORMANCE
+
+ override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) {
+ AndroidLogger.getInstance().debug("Session Changed: $sessionDetails")
+ val currentInternalSessionId = SessionManager.getInstance().perfSession().internalSessionId
+
+ // There can be situations where a new [PerfSession] was created, but an AQS wasn't
+ // available (during cold start).
+ if (perfSessionToAqs[currentInternalSessionId] == null) {
+ perfSessionToAqs[currentInternalSessionId] = sessionDetails
+ } else {
+ val newSession = PerfSession.createNewSession()
+ SessionManager.getInstance().updatePerfSession(newSession)
+ perfSessionToAqs[newSession.internalSessionId] = sessionDetails
+ }
+ }
+
+ companion object {
+ val perfSessionToAqs: MutableMap by lazy { mutableMapOf() }
+ }
+}
\ No newline at end of file
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
index b31696d963b..5bd2c2a4c47 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
@@ -98,6 +98,6 @@ private static void forceVerboseSessionWithSamplingPercentage(long samplingPerce
bundle.putFloat("sessions_sampling_percentage", samplingPercentage);
ConfigResolver.getInstance().setMetadataBundle(new ImmutableBundle(bundle));
- SessionManager.getInstance().setPerfSession(PerfSession.createWithId("sessionId"));
+ SessionManager.getInstance().setPerfSession(PerfSession.createNewSession());
}
}
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/metrics/AppStartTraceTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/metrics/AppStartTraceTest.java
index 36ae3d10116..47569cb4a2d 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/metrics/AppStartTraceTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/metrics/AppStartTraceTest.java
@@ -96,7 +96,7 @@ public Timer answer(InvocationOnMock invocationOnMock) throws Throwable {
@After
public void reset() {
- SessionManager.getInstance().updatePerfSession(PerfSession.createWithId("randomSessionId"));
+ SessionManager.getInstance().updatePerfSession(PerfSession.createNewSession());
}
/** Test activity sequentially goes through onCreate()->onStart()->onResume() state change. */
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/metrics/NetworkRequestMetricBuilderTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/metrics/NetworkRequestMetricBuilderTest.java
index 61b3823741d..3f6a3f929e2 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/metrics/NetworkRequestMetricBuilderTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/metrics/NetworkRequestMetricBuilderTest.java
@@ -225,7 +225,7 @@ public void testSessionIdAdditionInNetworkRequestMetric() {
assertThat(this.networkMetricBuilder.getSessions()).isEmpty();
int numberOfSessionIds = metricBuilder.getSessions().size();
- PerfSession perfSession = PerfSession.createWithId("testSessionId");
+ PerfSession perfSession = PerfSession.createNewSession();
SessionManager.getInstance().updatePerfSession(perfSession);
assertThat(metricBuilder.getSessions().size()).isEqualTo(numberOfSessionIds + 1);
@@ -328,7 +328,7 @@ public void testUpdateSessionWithValidSessionIsAdded() {
networkMetricBuilder.setRequestStartTimeMicros(/* time= */ 2000);
assertThat(networkMetricBuilder.getSessions()).hasSize(1);
- networkMetricBuilder.updateSession(PerfSession.createWithId("testSessionId"));
+ networkMetricBuilder.updateSession(PerfSession.createNewSession());
assertThat(networkMetricBuilder.getSessions()).hasSize(2);
}
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/metrics/TraceTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/metrics/TraceTest.java
index 0be443031f2..020bf433560 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/metrics/TraceTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/metrics/TraceTest.java
@@ -1016,7 +1016,7 @@ public void testSessionIdAdditionInTrace() {
int numberOfSessionIds = trace.getSessions().size();
- PerfSession perfSession = PerfSession.createWithId("test_session_id");
+ PerfSession perfSession = PerfSession.createNewSession();
SessionManager.getInstance().updatePerfSession(perfSession);
assertThat(trace.getSessions()).hasSize(numberOfSessionIds + 1);
@@ -1071,7 +1071,7 @@ public void testUpdateSessionWithValidSessionIsAdded() {
trace.start();
assertThat(trace.getSessions()).hasSize(1);
- trace.updateSession(PerfSession.createWithId("test_session_id"));
+ trace.updateSession(PerfSession.createNewSession());
assertThat(trace.getSessions()).hasSize(2);
trace.stop();
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/PerfSessionTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/PerfSessionTest.java
index 43257987b0f..9551f1d11ae 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/PerfSessionTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/PerfSessionTest.java
@@ -160,21 +160,21 @@ public void testPerfSessionConversionWithoutVerbosity() {
@Test
public void testPerfSessionsCreateDisabledGaugeCollectionWhenVerboseSessionForceDisabled() {
forceNonVerboseSession();
- PerfSession testPerfSession = PerfSession.createWithId("sessionId");
+ PerfSession testPerfSession = PerfSession.createNewSession();
assertThat(testPerfSession.isGaugeAndEventCollectionEnabled()).isFalse();
}
@Test
public void testPerfSessionsCreateDisabledGaugeCollectionWhenSessionsFeatureDisabled() {
forceSessionsFeatureDisabled();
- PerfSession testPerfSession = PerfSession.createWithId("sessionId");
+ PerfSession testPerfSession = PerfSession.createNewSession();
assertThat(testPerfSession.isGaugeAndEventCollectionEnabled()).isFalse();
}
@Test
public void testPerfSessionsCreateEnablesGaugeCollectionWhenVerboseSessionForceEnabled() {
forceVerboseSession();
- PerfSession testPerfSession = PerfSession.createWithId("sessionId");
+ PerfSession testPerfSession = PerfSession.createNewSession();
assertThat(testPerfSession.isGaugeAndEventCollectionEnabled()).isTrue();
}
@@ -185,16 +185,16 @@ public void testBuildAndSortMovesTheVerboseSessionToTop() {
// Next, create 3 non-verbose sessions
List sessions = new ArrayList<>();
- sessions.add(PerfSession.createWithId("sessionId1"));
- sessions.add(PerfSession.createWithId("sessionId2"));
- sessions.add(PerfSession.createWithId("sessionId3"));
+ sessions.add(PerfSession.createNewSession());
+ sessions.add(PerfSession.createNewSession());
+ sessions.add(PerfSession.createNewSession());
// Force all the sessions from now onwards to be verbose
forceVerboseSession();
// Next, create 2 verbose sessions
- sessions.add(PerfSession.createWithId("sessionId4"));
- sessions.add(PerfSession.createWithId("sessionId5"));
+ sessions.add(PerfSession.createNewSession());
+ sessions.add(PerfSession.createNewSession());
// Verify that the first session in the list of sessions was not verbose
assertThat(sessions.get(0).isVerbose()).isFalse();
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
index f3e3795f3f8..0a8f1a98b40 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
@@ -215,7 +215,7 @@ public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesIfSessionIs
SessionManager testSessionManager =
new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId"));
+ testSessionManager.updatePerfSession(PerfSession.createNewSession());
verify(mockGaugeManager).stopCollectingGauges();
}
@@ -226,8 +226,8 @@ public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesWhenSession
SessionManager testSessionManager =
new SessionManager(
- mockGaugeManager, PerfSession.createWithId("testSessionId"), mockAppStateMonitor);
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2"));
+ mockGaugeManager, PerfSession.createNewSession(), mockAppStateMonitor);
+ testSessionManager.updatePerfSession(PerfSession.createNewSession());
verify(mockGaugeManager).stopCollectingGauges();
}
@@ -240,7 +240,7 @@ public void testGaugeMetadataIsFlushedOnlyWhenNewVerboseSessionIsCreated() {
forceNonVerboseSession();
SessionManager testSessionManager =
new SessionManager(
- mockGaugeManager, PerfSession.createWithId("testSessionId1"), mockAppStateMonitor);
+ mockGaugeManager, PerfSession.createNewSession(), mockAppStateMonitor);
verify(mockGaugeManager, times(0))
.logGaugeMetadata(
@@ -249,12 +249,12 @@ public void testGaugeMetadataIsFlushedOnlyWhenNewVerboseSessionIsCreated() {
// Forcing a verbose session will enable Gauge collection
forceVerboseSession();
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2"));
+ testSessionManager.updatePerfSession(PerfSession.createNewSession());
verify(mockGaugeManager, times(1)).logGaugeMetadata(eq("testSessionId2"), any());
// Force a non-verbose session and verify if we are not logging metadata
forceVerboseSession();
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId3"));
+ testSessionManager.updatePerfSession(PerfSession.createNewSession());
verify(mockGaugeManager, times(1)).logGaugeMetadata(eq("testSessionId3"), any());
}
@@ -303,7 +303,7 @@ public void testPerfSession_sessionAwareObjects_doesntNotifyIfNotRegistered() {
FakeSessionAwareObject spySessionAwareObjectOne = spy(new FakeSessionAwareObject());
FakeSessionAwareObject spySessionAwareObjectTwo = spy(new FakeSessionAwareObject());
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1"));
+ testSessionManager.updatePerfSession(PerfSession.createNewSession());
verify(spySessionAwareObjectOne, never())
.updateSession(ArgumentMatchers.nullable(PerfSession.class));
@@ -322,8 +322,8 @@ public void testPerfSession_sessionAwareObjects_NotifiesIfRegistered() {
testSessionManager.registerForSessionUpdates(new WeakReference<>(spySessionAwareObjectOne));
testSessionManager.registerForSessionUpdates(new WeakReference<>(spySessionAwareObjectTwo));
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1"));
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2"));
+ testSessionManager.updatePerfSession(PerfSession.createNewSession());
+ testSessionManager.updatePerfSession(PerfSession.createNewSession());
verify(spySessionAwareObjectOne, times(2))
.updateSession(ArgumentMatchers.nullable(PerfSession.class));
@@ -347,11 +347,11 @@ public void testPerfSession_sessionAwareObjects_DoesNotNotifyIfUnregistered() {
testSessionManager.registerForSessionUpdates(weakSpySessionAwareObjectOne);
testSessionManager.registerForSessionUpdates(weakSpySessionAwareObjectTwo);
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1"));
+ testSessionManager.updatePerfSession(PerfSession.createNewSession());
testSessionManager.unregisterForSessionUpdates(weakSpySessionAwareObjectOne);
testSessionManager.unregisterForSessionUpdates(weakSpySessionAwareObjectTwo);
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2"));
+ testSessionManager.updatePerfSession(PerfSession.createNewSession());
verify(spySessionAwareObjectOne, times(1))
.updateSession(ArgumentMatchers.nullable(PerfSession.class));
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependencies.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependencies.kt
index 8d3548c8f4b..4b636a155e0 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependencies.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependencies.kt
@@ -40,19 +40,6 @@ object FirebaseSessionsDependencies {
*/
@JvmStatic
fun addDependency(subscriberName: SessionSubscriber.Name) {
- if (subscriberName == SessionSubscriber.Name.PERFORMANCE) {
- throw IllegalArgumentException(
- """
- Incompatible versions of Firebase Perf and Firebase Sessions.
- A safe combination would be:
- firebase-sessions:1.1.0
- firebase-crashlytics:18.5.0
- firebase-perf:20.5.0
- For more information contact Firebase Support.
- """
- .trimIndent()
- )
- }
if (dependencies.containsKey(subscriberName)) {
Log.d(TAG, "Dependency $subscriberName already added.")
return
From ad7217a2560ee77ef5b94f2544548cf1b20a3fd0 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Mon, 3 Feb 2025 16:52:05 -0500
Subject: [PATCH 002/146] Improve logging
---
.../java/com/google/firebase/perf/session/SessionManager.java | 3 +++
.../java/com/google/firebase/perf/session/SessionManagerKt.kt | 2 +-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
index 869ef1ef055..4c44ed4aa20 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
@@ -20,6 +20,7 @@
import androidx.annotation.VisibleForTesting;
import com.google.firebase.perf.application.AppStateMonitor;
import com.google.firebase.perf.application.AppStateUpdateHandler;
+import com.google.firebase.perf.logging.AndroidLogger;
import com.google.firebase.perf.session.gauges.GaugeManager;
import com.google.firebase.perf.v1.ApplicationProcessState;
import com.google.firebase.perf.v1.GaugeMetadata;
@@ -151,6 +152,8 @@ public void updatePerfSession(PerfSession perfSession) {
return;
}
+ AndroidLogger.getInstance().debug("Perf Session Changed: " + perfSession);
+
this.perfSession = perfSession;
synchronized (clients) {
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt
index e2c4f9cab51..458bd2ce82f 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt
@@ -12,7 +12,7 @@ class SessionManagerKt(val dataCollectionEnabled: Boolean): SessionSubscriber {
get() = SessionSubscriber.Name.PERFORMANCE
override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) {
- AndroidLogger.getInstance().debug("Session Changed: $sessionDetails")
+ AndroidLogger.getInstance().debug("AQS Session Changed: $sessionDetails")
val currentInternalSessionId = SessionManager.getInstance().perfSession().internalSessionId
// There can be situations where a new [PerfSession] was created, but an AQS wasn't
From aa0da1f6fd0857583eb1f8818d574da7aeea171a Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Mon, 3 Feb 2025 17:09:35 -0500
Subject: [PATCH 003/146] Better logging
---
.../firebase/perf/session/PerfSession.java | 8 ++++-
.../firebase/perf/session/SessionManager.java | 31 +------------------
2 files changed, 8 insertions(+), 31 deletions(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
index f206e919a30..5f206e2038e 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
@@ -20,9 +20,13 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.firebase.perf.config.ConfigResolver;
+import com.google.firebase.perf.logging.AndroidLogger;
import com.google.firebase.perf.util.Clock;
import com.google.firebase.perf.util.Timer;
import com.google.firebase.perf.v1.SessionVerbosity;
+import com.google.firebase.sessions.SessionDetails;
+import com.google.firebase.sessions.api.SessionSubscriber;
+
import java.util.List;
import java.util.Objects;
import java.util.UUID;
@@ -69,7 +73,9 @@ private PerfSession(@NonNull Parcel in) {
/** Returns the sessionId of the object. */
public String sessionId() {
// TODO(b/394127311): Verify edge cases.
- return Objects.requireNonNull(SessionManagerKt.Companion.getPerfSessionToAqs().get(internalSessionId)).getSessionId();
+ SessionSubscriber.SessionDetails sessionDetails = SessionManagerKt.Companion.getPerfSessionToAqs().get(internalSessionId);
+ AndroidLogger.getInstance().debug("AQS for " + this.internalSessionId + " is " + sessionDetails);
+ return Objects.requireNonNull(sessionDetails).getSessionId();
}
protected String getInternalSessionId() {
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
index 4c44ed4aa20..13dfde95b91 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
@@ -36,7 +36,7 @@
/** Session manager to generate sessionIDs and broadcast to the application. */
@Keep // Needed because of b/117526359.
-public class SessionManager extends AppStateUpdateHandler {
+public class SessionManager {
@SuppressLint("StaticFieldLeak")
private static final SessionManager instance = new SessionManager();
@@ -74,7 +74,6 @@ public SessionManager(
this.gaugeManager = gaugeManager;
this.perfSession = perfSession;
this.appStateMonitor = appStateMonitor;
- registerForAppState();
}
/**
@@ -99,34 +98,6 @@ public void setApplicationContext(final Context appContext) {
});
}
-// @Override
-// public void onUpdateAppState(ApplicationProcessState newAppState) {
-// super.onUpdateAppState(newAppState);
-//
-// if (appStateMonitor.isColdStart()) {
-// // We want the Session to remain unchanged if this is a cold start of the app since we already
-// // update the PerfSession in FirebasePerfProvider#onAttachInfo().
-// return;
-// }
-//
-// if (newAppState == ApplicationProcessState.FOREGROUND) {
-// // A new foregrounding of app will force a new sessionID generation.
-// PerfSession session = PerfSession.createWithId(UUID.randomUUID().toString());
-// updatePerfSession(session);
-// } else {
-// // If the session is running for too long, generate a new session and collect gauges as
-// // necessary.
-// if (perfSession.isSessionRunningTooLong()) {
-// PerfSession session = PerfSession.createWithId(UUID.randomUUID().toString());
-// updatePerfSession(session);
-// } else {
-// // For any other state change of the application, modify gauge collection state as
-// // necessary.
-// startOrStopCollectingGauges(newAppState);
-// }
-// }
-// }
-
/**
* Checks if the current {@link PerfSession} is expired/timed out. If so, stop collecting gauges.
*
From 9a91b1b106129666071d4014345d1f68532a11e2 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Mon, 3 Feb 2025 17:17:52 -0500
Subject: [PATCH 004/146] Switch the proto value to the AQS session
---
.../main/java/com/google/firebase/perf/session/PerfSession.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
index 5f206e2038e..2e13360103c 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
@@ -132,7 +132,7 @@ public boolean isSessionRunningTooLong() {
/** Creates and returns the proto object for PerfSession object. */
public com.google.firebase.perf.v1.PerfSession build() {
com.google.firebase.perf.v1.PerfSession.Builder sessionMetric =
- com.google.firebase.perf.v1.PerfSession.newBuilder().setSessionId(internalSessionId);
+ com.google.firebase.perf.v1.PerfSession.newBuilder().setSessionId(sessionId());
// If gauge collection is enabled, enable gauge collection verbosity.
if (isGaugeAndEventCollectionEnabled) {
From ed9f56c3008bd48c83794cb4a72da3dc72034d8a Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Mon, 3 Feb 2025 17:21:09 -0500
Subject: [PATCH 005/146] Remove TODO in AppStartTrace
---
.../java/com/google/firebase/perf/metrics/AppStartTrace.java | 1 -
1 file changed, 1 deletion(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java b/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java
index 7d76684dc01..7574f989d92 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java
@@ -370,7 +370,6 @@ public synchronized void onActivityResumed(Activity activity) {
appStartActivity = new WeakReference(activity);
onResumeTime = clock.getTime();
- // TODO(b/394127311): Defer this to SessionManagerKt
this.startSession = SessionManager.getInstance().perfSession();
AndroidLogger.getInstance()
.debug(
From ed765f54c12f15f61325d89834cd31450f6e1317 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Mon, 3 Feb 2025 17:25:27 -0500
Subject: [PATCH 006/146] style
---
.../firebase/perf/FirebasePerfEarly.java | 3 +-
.../firebase/perf/FirebasePerfRegistrar.java | 2 -
.../firebase/perf/FirebasePerformance.java | 1 -
.../firebase/perf/session/PerfSession.java | 11 ++---
.../firebase/perf/session/SessionManager.java | 9 ++--
.../firebase/perf/session/SessionManagerKt.kt | 45 ++++++++++---------
.../perf/session/SessionManagerTest.java | 6 +--
7 files changed, 35 insertions(+), 42 deletions(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java
index 91fcf4a0c49..969031a773f 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java
@@ -20,11 +20,10 @@
import com.google.firebase.StartupTime;
import com.google.firebase.perf.application.AppStateMonitor;
import com.google.firebase.perf.config.ConfigResolver;
+import com.google.firebase.perf.logging.AndroidLogger;
import com.google.firebase.perf.metrics.AppStartTrace;
import com.google.firebase.perf.session.SessionManager;
import java.util.concurrent.Executor;
-import com.google.firebase.perf.logging.AndroidLogger;
-import com.google.firebase.perf.session.SessionManagerKt;
/**
* The Firebase Performance early initialization.
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java
index 4f3a1d242ea..0754eddcd51 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java
@@ -30,10 +30,8 @@
import com.google.firebase.perf.injection.modules.FirebasePerformanceModule;
import com.google.firebase.platforminfo.LibraryVersionComponent;
import com.google.firebase.remoteconfig.RemoteConfigComponent;
-import com.google.firebase.sessions.FirebaseSessions;
import com.google.firebase.sessions.api.FirebaseSessionsDependencies;
import com.google.firebase.sessions.api.SessionSubscriber;
-
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
index 419f42246ce..cbfa6613d10 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
@@ -44,7 +44,6 @@
import com.google.firebase.perf.util.Timer;
import com.google.firebase.remoteconfig.RemoteConfigComponent;
import com.google.firebase.sessions.api.FirebaseSessionsDependencies;
-
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.URL;
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
index 2e13360103c..b36a2a683ed 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
@@ -24,9 +24,7 @@
import com.google.firebase.perf.util.Clock;
import com.google.firebase.perf.util.Timer;
import com.google.firebase.perf.v1.SessionVerbosity;
-import com.google.firebase.sessions.SessionDetails;
import com.google.firebase.sessions.api.SessionSubscriber;
-
import java.util.List;
import java.util.Objects;
import java.util.UUID;
@@ -50,7 +48,8 @@ public static PerfSession createNewSession() {
// Every time a PerfSession is created, it sets the AQS to null. Once an AQS is received,
// SessionManagerKt verifies if this is an active session, and sets the AQS session ID.
- // The assumption is that new PerfSessions *should* be limited to either App Start, or through AQS.
+ // The assumption is that new PerfSessions *should* be limited to either App Start, or through
+ // AQS.
SessionManagerKt.Companion.getPerfSessionToAqs().put(prunedSessionId, null);
return session;
@@ -73,8 +72,10 @@ private PerfSession(@NonNull Parcel in) {
/** Returns the sessionId of the object. */
public String sessionId() {
// TODO(b/394127311): Verify edge cases.
- SessionSubscriber.SessionDetails sessionDetails = SessionManagerKt.Companion.getPerfSessionToAqs().get(internalSessionId);
- AndroidLogger.getInstance().debug("AQS for " + this.internalSessionId + " is " + sessionDetails);
+ SessionSubscriber.SessionDetails sessionDetails =
+ SessionManagerKt.Companion.getPerfSessionToAqs().get(internalSessionId);
+ AndroidLogger.getInstance()
+ .debug("AQS for " + this.internalSessionId + " is " + sessionDetails);
return Objects.requireNonNull(sessionDetails).getSessionId();
}
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
index 13dfde95b91..973bccbef6f 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
@@ -19,7 +19,6 @@
import androidx.annotation.Keep;
import androidx.annotation.VisibleForTesting;
import com.google.firebase.perf.application.AppStateMonitor;
-import com.google.firebase.perf.application.AppStateUpdateHandler;
import com.google.firebase.perf.logging.AndroidLogger;
import com.google.firebase.perf.session.gauges.GaugeManager;
import com.google.firebase.perf.v1.ApplicationProcessState;
@@ -62,10 +61,7 @@ public final PerfSession perfSession() {
private SessionManager() {
// Generate a new sessionID for every cold start.
- this(
- GaugeManager.getInstance(),
- PerfSession.createNewSession(),
- AppStateMonitor.getInstance());
+ this(GaugeManager.getInstance(), PerfSession.createNewSession(), AppStateMonitor.getInstance());
}
@VisibleForTesting
@@ -119,7 +115,8 @@ public void stopGaugeCollectionIfSessionRunningTooLong() {
*/
public void updatePerfSession(PerfSession perfSession) {
// Do not update the perf session if it is the exact same sessionId.
- if (Objects.equals(perfSession.getInternalSessionId(), this.perfSession.getInternalSessionId())) {
+ if (Objects.equals(
+ perfSession.getInternalSessionId(), this.perfSession.getInternalSessionId())) {
return;
}
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt
index 458bd2ce82f..cf27cad3175 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt
@@ -1,32 +1,33 @@
package com.google.firebase.perf.session
-import com.google.firebase.sessions.api.SessionSubscriber
import com.google.firebase.perf.logging.AndroidLogger
+import com.google.firebase.sessions.api.SessionSubscriber
+class SessionManagerKt(val dataCollectionEnabled: Boolean) : SessionSubscriber {
+ override val isDataCollectionEnabled: Boolean
+ get() = dataCollectionEnabled
-class SessionManagerKt(val dataCollectionEnabled: Boolean): SessionSubscriber {
- override val isDataCollectionEnabled: Boolean
- get() = dataCollectionEnabled
-
- override val sessionSubscriberName: SessionSubscriber.Name
- get() = SessionSubscriber.Name.PERFORMANCE
+ override val sessionSubscriberName: SessionSubscriber.Name
+ get() = SessionSubscriber.Name.PERFORMANCE
- override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) {
- AndroidLogger.getInstance().debug("AQS Session Changed: $sessionDetails")
- val currentInternalSessionId = SessionManager.getInstance().perfSession().internalSessionId
+ override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) {
+ AndroidLogger.getInstance().debug("AQS Session Changed: $sessionDetails")
+ val currentInternalSessionId = SessionManager.getInstance().perfSession().internalSessionId
- // There can be situations where a new [PerfSession] was created, but an AQS wasn't
- // available (during cold start).
- if (perfSessionToAqs[currentInternalSessionId] == null) {
- perfSessionToAqs[currentInternalSessionId] = sessionDetails
- } else {
- val newSession = PerfSession.createNewSession()
- SessionManager.getInstance().updatePerfSession(newSession)
- perfSessionToAqs[newSession.internalSessionId] = sessionDetails
- }
+ // There can be situations where a new [PerfSession] was created, but an AQS wasn't
+ // available (during cold start).
+ if (perfSessionToAqs[currentInternalSessionId] == null) {
+ perfSessionToAqs[currentInternalSessionId] = sessionDetails
+ } else {
+ val newSession = PerfSession.createNewSession()
+ SessionManager.getInstance().updatePerfSession(newSession)
+ perfSessionToAqs[newSession.internalSessionId] = sessionDetails
}
+ }
- companion object {
- val perfSessionToAqs: MutableMap by lazy { mutableMapOf() }
+ companion object {
+ val perfSessionToAqs: MutableMap by lazy {
+ mutableMapOf()
}
-}
\ No newline at end of file
+ }
+}
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
index 0a8f1a98b40..264028b8139 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
@@ -225,8 +225,7 @@ public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesWhenSession
forceSessionsFeatureDisabled();
SessionManager testSessionManager =
- new SessionManager(
- mockGaugeManager, PerfSession.createNewSession(), mockAppStateMonitor);
+ new SessionManager(mockGaugeManager, PerfSession.createNewSession(), mockAppStateMonitor);
testSessionManager.updatePerfSession(PerfSession.createNewSession());
verify(mockGaugeManager).stopCollectingGauges();
@@ -239,8 +238,7 @@ public void testGaugeMetadataIsFlushedOnlyWhenNewVerboseSessionIsCreated() {
// Start with a non verbose session
forceNonVerboseSession();
SessionManager testSessionManager =
- new SessionManager(
- mockGaugeManager, PerfSession.createNewSession(), mockAppStateMonitor);
+ new SessionManager(mockGaugeManager, PerfSession.createNewSession(), mockAppStateMonitor);
verify(mockGaugeManager, times(0))
.logGaugeMetadata(
From d355af2935a0022686b5534f5a971112d97a44f9 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Mon, 3 Feb 2025 17:39:03 -0500
Subject: [PATCH 007/146] remove cold start gauge name
---
.../java/com/google/firebase/perf/session/SessionManager.java | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
index 973bccbef6f..6ff17e859c1 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
@@ -39,9 +39,7 @@ public class SessionManager {
@SuppressLint("StaticFieldLeak")
private static final SessionManager instance = new SessionManager();
-
- private static final String COLD_START_GAUGE_NAME = "coldstart";
-
+
private final GaugeManager gaugeManager;
private final AppStateMonitor appStateMonitor;
private final Set> clients = new HashSet<>();
From 4096b0a1149fc1ee5579f0546b92415779bb5122 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Mon, 3 Feb 2025 17:41:24 -0500
Subject: [PATCH 008/146] More changes
---
.../java/com/google/firebase/perf/session/PerfSession.java | 3 ++-
.../java/com/google/firebase/perf/session/SessionManager.java | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
index b36a2a683ed..91b63df9041 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
@@ -79,7 +79,8 @@ public String sessionId() {
return Objects.requireNonNull(sessionDetails).getSessionId();
}
- protected String getInternalSessionId() {
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ public String getInternalSessionId() {
return internalSessionId;
}
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
index 6ff17e859c1..54784d7a274 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
@@ -39,7 +39,7 @@ public class SessionManager {
@SuppressLint("StaticFieldLeak")
private static final SessionManager instance = new SessionManager();
-
+
private final GaugeManager gaugeManager;
private final AppStateMonitor appStateMonitor;
private final Set> clients = new HashSet<>();
From ff75fd91f9aac24a883dfd56a1fdef4b9a0e47dc Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Mon, 3 Feb 2025 17:49:01 -0500
Subject: [PATCH 009/146] Update comments
---
.../main/java/com/google/firebase/perf/FirebasePerfEarly.java | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java
index 969031a773f..45dad3c78f0 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java
@@ -54,9 +54,7 @@ public FirebasePerfEarly(
// In the case of cold start, we create a session and start collecting gauges as early as
// possible.
- // There is code in SessionManager that prevents us from resetting the session twice in case
- // of app cold start.
- AndroidLogger.getInstance().debug("Initializing Gauge Collection");
+ // The session is mapped to an AQS once AQS is initialized.
SessionManager.getInstance().initializeGaugeCollection();
}
}
From 6bdf12c77f4e5890e083106527c3ce518ce052dd Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Tue, 4 Feb 2025 12:06:18 -0500
Subject: [PATCH 010/146] Style
---
.../main/java/com/google/firebase/perf/FirebasePerfEarly.java | 1 -
1 file changed, 1 deletion(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java
index 45dad3c78f0..f2652ca1a9a 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java
@@ -20,7 +20,6 @@
import com.google.firebase.StartupTime;
import com.google.firebase.perf.application.AppStateMonitor;
import com.google.firebase.perf.config.ConfigResolver;
-import com.google.firebase.perf.logging.AndroidLogger;
import com.google.firebase.perf.metrics.AppStartTrace;
import com.google.firebase.perf.session.SessionManager;
import java.util.concurrent.Executor;
From 4e75c31fcb0aca3d3ab01bc3325a10d022cce3f1 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Tue, 4 Feb 2025 13:02:50 -0500
Subject: [PATCH 011/146] Change sessions dependency to HEAD
---
firebase-perf/firebase-perf.gradle | 2 +-
.../perf/session/SessionManagerTest.java | 20 +++++++++----------
2 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/firebase-perf/firebase-perf.gradle b/firebase-perf/firebase-perf.gradle
index b6028e75b61..3dc2fd5a38d 100644
--- a/firebase-perf/firebase-perf.gradle
+++ b/firebase-perf/firebase-perf.gradle
@@ -118,7 +118,7 @@ dependencies {
api("com.google.firebase:firebase-components:18.0.0")
api("com.google.firebase:firebase-config:21.5.0")
api("com.google.firebase:firebase-installations:17.2.0")
- api("com.google.firebase:firebase-sessions:2.0.7") {
+ api(project(":firebase-sessions")) {
exclude group: 'com.google.firebase', module: 'firebase-common'
exclude group: 'com.google.firebase', module: 'firebase-common-ktx'
exclude group: 'com.google.firebase', module: 'firebase-components'
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
index 264028b8139..c2a4098f972 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
@@ -33,7 +33,6 @@
import com.google.firebase.perf.session.gauges.GaugeManager;
import com.google.firebase.perf.util.Clock;
import com.google.firebase.perf.util.Timer;
-import com.google.firebase.perf.v1.ApplicationProcessState;
import java.lang.ref.WeakReference;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@@ -90,13 +89,14 @@ public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsIn
@Test
public void testOnUpdateAppStateDoesNothingDuringAppStart() {
String oldSessionId = SessionManager.getInstance().perfSession().sessionId();
+ assertThat(oldSessionId).isNotNull();
assertThat(oldSessionId).isNotNull();
assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
AppStateMonitor.getInstance().setIsColdStart(true);
- SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND);
+ // SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND);
assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
}
@@ -107,7 +107,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnForegroundState() {
assertThat(oldSessionId).isNotNull();
assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
- SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND);
+ // SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND);
assertThat(oldSessionId).isNotEqualTo(SessionManager.getInstance().perfSession().sessionId());
}
@@ -118,7 +118,7 @@ public void testOnUpdateAppStateDoesntGenerateNewSessionIdOnBackgroundState() {
assertThat(oldSessionId).isNotNull();
assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
- SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.BACKGROUND);
+ // SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.BACKGROUND);
assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
}
@@ -132,7 +132,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess
assertThat(oldSessionId).isNotNull();
assertThat(oldSessionId).isEqualTo(testSessionManager.perfSession().sessionId());
- testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
+ // testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
assertThat(oldSessionId).isNotEqualTo(testSessionManager.perfSession().sessionId());
}
@@ -143,7 +143,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess
SessionManager testSessionManager =
new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
+ // testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
verify(mockGaugeManager)
.logGaugeMetadata(
@@ -157,7 +157,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess
SessionManager testSessionManager =
new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
+ // testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
verify(mockGaugeManager, never())
.logGaugeMetadata(
@@ -171,7 +171,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess
SessionManager testSessionManager =
new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
+ // testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
verify(mockGaugeManager, never())
.logGaugeMetadata(
@@ -186,7 +186,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess
SessionManager testSessionManager =
new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
+ // testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
verify(mockGaugeManager)
.logGaugeMetadata(
@@ -199,7 +199,7 @@ public void testOnUpdateAppStateMakesGaugeManagerStartCollectingGaugesIfSessionI
SessionManager testSessionManager =
new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
+ // testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
verify(mockGaugeManager)
.startCollectingGauges(AdditionalMatchers.not(eq(mockPerfSession)), any());
From 407df2a8620d18983a24edde285590afb8a39645 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Tue, 4 Feb 2025 15:19:22 -0500
Subject: [PATCH 012/146] Switch to a singleton Fireperf session subscriber
---
.../firebase/perf/FirebasePerformance.java | 3 +--
.../firebase/perf/session/PerfSession.java | 13 +++---------
.../firebase/perf/session/SessionManagerKt.kt | 20 ++++++++++++++++---
.../perf/FirebasePerformanceTestBase.java | 8 +++++---
4 files changed, 26 insertions(+), 18 deletions(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
index cbfa6613d10..ec63b035d3f 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
@@ -202,8 +202,7 @@ public static FirebasePerformance getInstance() {
firebaseApp.getOptions().getProjectId(), appContext.getPackageName())));
}
- SessionManagerKt sessionSubscriber = new SessionManagerKt(isPerformanceCollectionEnabled());
- FirebaseSessionsDependencies.register(sessionSubscriber);
+ FirebaseSessionsDependencies.register(SessionManagerKt.Companion.getInstance());
}
/**
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
index 91b63df9041..75114cab15f 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
@@ -20,13 +20,10 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.firebase.perf.config.ConfigResolver;
-import com.google.firebase.perf.logging.AndroidLogger;
import com.google.firebase.perf.util.Clock;
import com.google.firebase.perf.util.Timer;
import com.google.firebase.perf.v1.SessionVerbosity;
-import com.google.firebase.sessions.api.SessionSubscriber;
import java.util.List;
-import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@@ -50,7 +47,7 @@ public static PerfSession createNewSession() {
// SessionManagerKt verifies if this is an active session, and sets the AQS session ID.
// The assumption is that new PerfSessions *should* be limited to either App Start, or through
// AQS.
- SessionManagerKt.Companion.getPerfSessionToAqs().put(prunedSessionId, null);
+ SessionManagerKt.Companion.getInstance().reportPerfSession(prunedSessionId);
return session;
}
@@ -71,12 +68,8 @@ private PerfSession(@NonNull Parcel in) {
/** Returns the sessionId of the object. */
public String sessionId() {
- // TODO(b/394127311): Verify edge cases.
- SessionSubscriber.SessionDetails sessionDetails =
- SessionManagerKt.Companion.getPerfSessionToAqs().get(internalSessionId);
- AndroidLogger.getInstance()
- .debug("AQS for " + this.internalSessionId + " is " + sessionDetails);
- return Objects.requireNonNull(sessionDetails).getSessionId();
+ return SessionManagerKt.Companion.getInstance()
+ .getAqsMappedToPerfSession(this.internalSessionId);
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt
index cf27cad3175..9913a923168 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt
@@ -1,9 +1,13 @@
package com.google.firebase.perf.session
+import com.google.firebase.perf.config.ConfigResolver
import com.google.firebase.perf.logging.AndroidLogger
import com.google.firebase.sessions.api.SessionSubscriber
-class SessionManagerKt(val dataCollectionEnabled: Boolean) : SessionSubscriber {
+class SessionManagerKt(private val dataCollectionEnabled: Boolean) : SessionSubscriber {
+ private val perfSessionToAqs: MutableMap =
+ mutableMapOf()
+
override val isDataCollectionEnabled: Boolean
get() = dataCollectionEnabled
@@ -25,9 +29,19 @@ class SessionManagerKt(val dataCollectionEnabled: Boolean) : SessionSubscriber {
}
}
+ fun reportPerfSession(perfSessionId: String) {
+ perfSessionToAqs[perfSessionId] = null
+ }
+
+ fun getAqsMappedToPerfSession(perfSessionId: String): String {
+ AndroidLogger.getInstance()
+ .debug("AQS for perf session $perfSessionId is ${perfSessionToAqs[perfSessionId]?.sessionId}")
+ return perfSessionToAqs[perfSessionId]?.sessionId ?: perfSessionId
+ }
+
companion object {
- val perfSessionToAqs: MutableMap by lazy {
- mutableMapOf()
+ val instance: SessionManagerKt by lazy {
+ SessionManagerKt(ConfigResolver.getInstance().isPerformanceMonitoringEnabled)
}
}
}
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
index 5bd2c2a4c47..344c9c81721 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
@@ -23,9 +23,9 @@
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.perf.config.ConfigResolver;
-import com.google.firebase.perf.session.PerfSession;
-import com.google.firebase.perf.session.SessionManager;
+import com.google.firebase.perf.session.SessionManagerKt;
import com.google.firebase.perf.util.ImmutableBundle;
+import com.google.firebase.sessions.api.SessionSubscriber;
import org.junit.After;
import org.junit.Before;
import org.robolectric.shadows.ShadowPackageManager;
@@ -72,6 +72,7 @@ public void setUpFirebaseApp() {
.setProjectId(FAKE_FIREBASE_PROJECT_ID)
.build();
FirebaseApp.initializeApp(appContext, options);
+ FirebasePerformance.getInstance();
}
@After
@@ -98,6 +99,7 @@ private static void forceVerboseSessionWithSamplingPercentage(long samplingPerce
bundle.putFloat("sessions_sampling_percentage", samplingPercentage);
ConfigResolver.getInstance().setMetadataBundle(new ImmutableBundle(bundle));
- SessionManager.getInstance().setPerfSession(PerfSession.createNewSession());
+ SessionManagerKt.Companion.getInstance()
+ .onSessionChanged(new SessionSubscriber.SessionDetails("sessionId"));
}
}
From eb341c0fadcf7c4bbbb29a96f1cece7379837daf Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Tue, 4 Feb 2025 15:43:16 -0500
Subject: [PATCH 013/146] Revert session manager test
---
.../perf/session/SessionManagerTest.java | 44 ++++++++++---------
1 file changed, 23 insertions(+), 21 deletions(-)
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
index c2a4098f972..f3e3795f3f8 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
@@ -33,6 +33,7 @@
import com.google.firebase.perf.session.gauges.GaugeManager;
import com.google.firebase.perf.util.Clock;
import com.google.firebase.perf.util.Timer;
+import com.google.firebase.perf.v1.ApplicationProcessState;
import java.lang.ref.WeakReference;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@@ -89,14 +90,13 @@ public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsIn
@Test
public void testOnUpdateAppStateDoesNothingDuringAppStart() {
String oldSessionId = SessionManager.getInstance().perfSession().sessionId();
- assertThat(oldSessionId).isNotNull();
assertThat(oldSessionId).isNotNull();
assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
AppStateMonitor.getInstance().setIsColdStart(true);
- // SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND);
+ SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND);
assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
}
@@ -107,7 +107,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnForegroundState() {
assertThat(oldSessionId).isNotNull();
assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
- // SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND);
+ SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND);
assertThat(oldSessionId).isNotEqualTo(SessionManager.getInstance().perfSession().sessionId());
}
@@ -118,7 +118,7 @@ public void testOnUpdateAppStateDoesntGenerateNewSessionIdOnBackgroundState() {
assertThat(oldSessionId).isNotNull();
assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
- // SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.BACKGROUND);
+ SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.BACKGROUND);
assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
}
@@ -132,7 +132,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess
assertThat(oldSessionId).isNotNull();
assertThat(oldSessionId).isEqualTo(testSessionManager.perfSession().sessionId());
- // testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
+ testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
assertThat(oldSessionId).isNotEqualTo(testSessionManager.perfSession().sessionId());
}
@@ -143,7 +143,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess
SessionManager testSessionManager =
new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- // testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
+ testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
verify(mockGaugeManager)
.logGaugeMetadata(
@@ -157,7 +157,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess
SessionManager testSessionManager =
new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- // testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
+ testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
verify(mockGaugeManager, never())
.logGaugeMetadata(
@@ -171,7 +171,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess
SessionManager testSessionManager =
new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- // testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
+ testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
verify(mockGaugeManager, never())
.logGaugeMetadata(
@@ -186,7 +186,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess
SessionManager testSessionManager =
new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- // testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
+ testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
verify(mockGaugeManager)
.logGaugeMetadata(
@@ -199,7 +199,7 @@ public void testOnUpdateAppStateMakesGaugeManagerStartCollectingGaugesIfSessionI
SessionManager testSessionManager =
new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- // testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
+ testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
verify(mockGaugeManager)
.startCollectingGauges(AdditionalMatchers.not(eq(mockPerfSession)), any());
@@ -215,7 +215,7 @@ public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesIfSessionIs
SessionManager testSessionManager =
new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.updatePerfSession(PerfSession.createNewSession());
+ testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId"));
verify(mockGaugeManager).stopCollectingGauges();
}
@@ -225,8 +225,9 @@ public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesWhenSession
forceSessionsFeatureDisabled();
SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, PerfSession.createNewSession(), mockAppStateMonitor);
- testSessionManager.updatePerfSession(PerfSession.createNewSession());
+ new SessionManager(
+ mockGaugeManager, PerfSession.createWithId("testSessionId"), mockAppStateMonitor);
+ testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2"));
verify(mockGaugeManager).stopCollectingGauges();
}
@@ -238,7 +239,8 @@ public void testGaugeMetadataIsFlushedOnlyWhenNewVerboseSessionIsCreated() {
// Start with a non verbose session
forceNonVerboseSession();
SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, PerfSession.createNewSession(), mockAppStateMonitor);
+ new SessionManager(
+ mockGaugeManager, PerfSession.createWithId("testSessionId1"), mockAppStateMonitor);
verify(mockGaugeManager, times(0))
.logGaugeMetadata(
@@ -247,12 +249,12 @@ public void testGaugeMetadataIsFlushedOnlyWhenNewVerboseSessionIsCreated() {
// Forcing a verbose session will enable Gauge collection
forceVerboseSession();
- testSessionManager.updatePerfSession(PerfSession.createNewSession());
+ testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2"));
verify(mockGaugeManager, times(1)).logGaugeMetadata(eq("testSessionId2"), any());
// Force a non-verbose session and verify if we are not logging metadata
forceVerboseSession();
- testSessionManager.updatePerfSession(PerfSession.createNewSession());
+ testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId3"));
verify(mockGaugeManager, times(1)).logGaugeMetadata(eq("testSessionId3"), any());
}
@@ -301,7 +303,7 @@ public void testPerfSession_sessionAwareObjects_doesntNotifyIfNotRegistered() {
FakeSessionAwareObject spySessionAwareObjectOne = spy(new FakeSessionAwareObject());
FakeSessionAwareObject spySessionAwareObjectTwo = spy(new FakeSessionAwareObject());
- testSessionManager.updatePerfSession(PerfSession.createNewSession());
+ testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1"));
verify(spySessionAwareObjectOne, never())
.updateSession(ArgumentMatchers.nullable(PerfSession.class));
@@ -320,8 +322,8 @@ public void testPerfSession_sessionAwareObjects_NotifiesIfRegistered() {
testSessionManager.registerForSessionUpdates(new WeakReference<>(spySessionAwareObjectOne));
testSessionManager.registerForSessionUpdates(new WeakReference<>(spySessionAwareObjectTwo));
- testSessionManager.updatePerfSession(PerfSession.createNewSession());
- testSessionManager.updatePerfSession(PerfSession.createNewSession());
+ testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1"));
+ testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2"));
verify(spySessionAwareObjectOne, times(2))
.updateSession(ArgumentMatchers.nullable(PerfSession.class));
@@ -345,11 +347,11 @@ public void testPerfSession_sessionAwareObjects_DoesNotNotifyIfUnregistered() {
testSessionManager.registerForSessionUpdates(weakSpySessionAwareObjectOne);
testSessionManager.registerForSessionUpdates(weakSpySessionAwareObjectTwo);
- testSessionManager.updatePerfSession(PerfSession.createNewSession());
+ testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1"));
testSessionManager.unregisterForSessionUpdates(weakSpySessionAwareObjectOne);
testSessionManager.unregisterForSessionUpdates(weakSpySessionAwareObjectTwo);
- testSessionManager.updatePerfSession(PerfSession.createNewSession());
+ testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2"));
verify(spySessionAwareObjectOne, times(1))
.updateSession(ArgumentMatchers.nullable(PerfSession.class));
From 042ee43b2de39be1f20165b7b2fb0b1c470e6849 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Tue, 4 Feb 2025 17:55:10 -0500
Subject: [PATCH 014/146] Update SessionManagerTest
---
.../perf/FirebasePerformanceTestBase.java | 13 +-
.../perf/session/SessionManagerTest.java | 239 +-----------------
2 files changed, 21 insertions(+), 231 deletions(-)
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
index 344c9c81721..9a787161017 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
@@ -26,6 +26,7 @@
import com.google.firebase.perf.session.SessionManagerKt;
import com.google.firebase.perf.util.ImmutableBundle;
import com.google.firebase.sessions.api.SessionSubscriber;
+import java.util.UUID;
import org.junit.After;
import org.junit.Before;
import org.robolectric.shadows.ShadowPackageManager;
@@ -52,6 +53,8 @@ public class FirebasePerformanceTestBase {
protected static final String FAKE_FIREBASE_DB_URL = "https://fir-perftestapp.firebaseio.com";
protected static final String FAKE_FIREBASE_PROJECT_ID = "fir-perftestapp";
+ protected static final String FAKE_AQS_SESSION_PREFIX = "AIzaSyBcE";
+
protected Context appContext;
@Before
@@ -94,12 +97,16 @@ protected static void forceNonVerboseSession() {
forceVerboseSessionWithSamplingPercentage(0);
}
+ protected static void triggerAqsSession() {
+ SessionManagerKt.Companion.getInstance()
+ .onSessionChanged(
+ new SessionSubscriber.SessionDetails(FAKE_AQS_SESSION_PREFIX + UUID.randomUUID()));
+ }
+
private static void forceVerboseSessionWithSamplingPercentage(long samplingPercentage) {
Bundle bundle = new Bundle();
bundle.putFloat("sessions_sampling_percentage", samplingPercentage);
ConfigResolver.getInstance().setMetadataBundle(new ImmutableBundle(bundle));
-
- SessionManagerKt.Companion.getInstance()
- .onSessionChanged(new SessionSubscriber.SessionDetails("sessionId"));
+ triggerAqsSession();
}
}
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
index f3e3795f3f8..bd8e69a74f4 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
@@ -16,10 +16,6 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -32,15 +28,11 @@
import com.google.firebase.perf.application.AppStateMonitor;
import com.google.firebase.perf.session.gauges.GaugeManager;
import com.google.firebase.perf.util.Clock;
-import com.google.firebase.perf.util.Timer;
-import com.google.firebase.perf.v1.ApplicationProcessState;
import java.lang.ref.WeakReference;
import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.AdditionalMatchers;
import org.mockito.ArgumentMatchers;
import org.mockito.InOrder;
import org.mockito.Mock;
@@ -64,13 +56,16 @@ public void setUp() {
when(mockPerfSession.sessionId()).thenReturn("sessionId");
when(mockAppStateMonitor.isColdStart()).thenReturn(false);
AppStateMonitor.getInstance().setIsColdStart(false);
+ // We assume that an AQS session has been created in all tests.
+ triggerAqsSession();
}
@Test
public void testInstanceCreation() {
assertThat(SessionManager.getInstance()).isNotNull();
assertThat(SessionManager.getInstance()).isEqualTo(SessionManager.getInstance());
- assertThat(SessionManager.getInstance().perfSession().sessionId()).isNotNull();
+ assertThat(SessionManager.getInstance().perfSession().sessionId())
+ .contains(FAKE_AQS_SESSION_PREFIX);
}
@Test
@@ -87,214 +82,6 @@ public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsIn
inOrder.verify(mockGaugeManager).logGaugeMetadata(any(), any());
}
- @Test
- public void testOnUpdateAppStateDoesNothingDuringAppStart() {
- String oldSessionId = SessionManager.getInstance().perfSession().sessionId();
-
- assertThat(oldSessionId).isNotNull();
- assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
-
- AppStateMonitor.getInstance().setIsColdStart(true);
-
- SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND);
- assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
- }
-
- @Test
- public void testOnUpdateAppStateGeneratesNewSessionIdOnForegroundState() {
- String oldSessionId = SessionManager.getInstance().perfSession().sessionId();
-
- assertThat(oldSessionId).isNotNull();
- assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
-
- SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND);
- assertThat(oldSessionId).isNotEqualTo(SessionManager.getInstance().perfSession().sessionId());
- }
-
- @Test
- public void testOnUpdateAppStateDoesntGenerateNewSessionIdOnBackgroundState() {
- String oldSessionId = SessionManager.getInstance().perfSession().sessionId();
-
- assertThat(oldSessionId).isNotNull();
- assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
-
- SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.BACKGROUND);
- assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
- }
-
- @Test
- public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSessionExpires() {
- when(mockPerfSession.isSessionRunningTooLong()).thenReturn(true);
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- String oldSessionId = testSessionManager.perfSession().sessionId();
-
- assertThat(oldSessionId).isNotNull();
- assertThat(oldSessionId).isEqualTo(testSessionManager.perfSession().sessionId());
-
- testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
- assertThat(oldSessionId).isNotEqualTo(testSessionManager.perfSession().sessionId());
- }
-
- @Test
- public void
- testOnUpdateAppStateMakesGaugeManagerLogGaugeMetadataOnForegroundStateIfSessionIsVerbose() {
- forceVerboseSession();
-
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
-
- verify(mockGaugeManager)
- .logGaugeMetadata(
- anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class));
- }
-
- @Test
- public void
- testOnUpdateAppStateDoesntMakeGaugeManagerLogGaugeMetadataOnForegroundStateIfSessionIsNonVerbose() {
- forceNonVerboseSession();
-
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
-
- verify(mockGaugeManager, never())
- .logGaugeMetadata(
- anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class));
- }
-
- @Test
- public void
- testOnUpdateAppStateDoesntMakeGaugeManagerLogGaugeMetadataOnBackgroundStateEvenIfSessionIsVerbose() {
- forceVerboseSession();
-
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
-
- verify(mockGaugeManager, never())
- .logGaugeMetadata(
- anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class));
- }
-
- @Test
- public void
- testOnUpdateAppStateMakesGaugeManagerLogGaugeMetadataOnBackgroundAppStateIfSessionIsVerboseAndTimedOut() {
- when(mockPerfSession.isSessionRunningTooLong()).thenReturn(true);
- forceVerboseSession();
-
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
-
- verify(mockGaugeManager)
- .logGaugeMetadata(
- anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class));
- }
-
- @Test
- public void testOnUpdateAppStateMakesGaugeManagerStartCollectingGaugesIfSessionIsVerbose() {
- forceVerboseSession();
-
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
-
- verify(mockGaugeManager)
- .startCollectingGauges(AdditionalMatchers.not(eq(mockPerfSession)), any());
- }
-
- // LogGaugeData on new perf session when Verbose
- // NotLogGaugeData on new perf session when not Verbose
- // Mark Session as expired after time limit.
-
- @Test
- public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesIfSessionIsNonVerbose() {
- forceNonVerboseSession();
-
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId"));
-
- verify(mockGaugeManager).stopCollectingGauges();
- }
-
- @Test
- public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesWhenSessionsDisabled() {
- forceSessionsFeatureDisabled();
-
- SessionManager testSessionManager =
- new SessionManager(
- mockGaugeManager, PerfSession.createWithId("testSessionId"), mockAppStateMonitor);
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2"));
-
- verify(mockGaugeManager).stopCollectingGauges();
- }
-
- @Test
- public void testGaugeMetadataIsFlushedOnlyWhenNewVerboseSessionIsCreated() {
- when(mockPerfSession.isSessionRunningTooLong()).thenReturn(false);
-
- // Start with a non verbose session
- forceNonVerboseSession();
- SessionManager testSessionManager =
- new SessionManager(
- mockGaugeManager, PerfSession.createWithId("testSessionId1"), mockAppStateMonitor);
-
- verify(mockGaugeManager, times(0))
- .logGaugeMetadata(
- eq("testSessionId1"),
- eq(com.google.firebase.perf.v1.ApplicationProcessState.FOREGROUND));
-
- // Forcing a verbose session will enable Gauge collection
- forceVerboseSession();
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2"));
- verify(mockGaugeManager, times(1)).logGaugeMetadata(eq("testSessionId2"), any());
-
- // Force a non-verbose session and verify if we are not logging metadata
- forceVerboseSession();
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId3"));
- verify(mockGaugeManager, times(1)).logGaugeMetadata(eq("testSessionId3"), any());
- }
-
- @Test
- public void testSessionIdDoesNotUpdateIfPerfSessionRunsTooLong() {
- Timer mockTimer = mock(Timer.class);
- when(mockClock.getTime()).thenReturn(mockTimer);
-
- PerfSession session = new PerfSession("sessionId", mockClock);
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, session, mockAppStateMonitor);
-
- assertThat(session.isSessionRunningTooLong()).isFalse();
-
- when(mockTimer.getDurationMicros())
- .thenReturn(TimeUnit.HOURS.toMicros(5)); // Default Max Session Length is 4 hours
- assertThat(session.isSessionRunningTooLong()).isTrue();
-
- assertThat(testSessionManager.perfSession().sessionId()).isEqualTo("sessionId");
- }
-
- @Test
- public void testPerfSessionExpiredMakesGaugeManagerStopsCollectingGaugesIfSessionIsVerbose() {
- forceVerboseSession();
- Timer mockTimer = mock(Timer.class);
- when(mockClock.getTime()).thenReturn(mockTimer);
-
- PerfSession session = new PerfSession("sessionId", mockClock);
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, session, mockAppStateMonitor);
-
- assertThat(session.isSessionRunningTooLong()).isFalse();
-
- when(mockTimer.getDurationMicros())
- .thenReturn(TimeUnit.HOURS.toMicros(5)); // Default Max Session Length is 4 hours
-
- assertThat(session.isSessionRunningTooLong()).isTrue();
- verify(mockGaugeManager, times(0)).logGaugeMetadata(any(), any());
- }
-
@Test
public void testPerfSession_sessionAwareObjects_doesntNotifyIfNotRegistered() {
SessionManager testSessionManager =
@@ -303,7 +90,7 @@ public void testPerfSession_sessionAwareObjects_doesntNotifyIfNotRegistered() {
FakeSessionAwareObject spySessionAwareObjectOne = spy(new FakeSessionAwareObject());
FakeSessionAwareObject spySessionAwareObjectTwo = spy(new FakeSessionAwareObject());
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1"));
+ testSessionManager.updatePerfSession(PerfSession.createNewSession());
verify(spySessionAwareObjectOne, never())
.updateSession(ArgumentMatchers.nullable(PerfSession.class));
@@ -313,17 +100,15 @@ public void testPerfSession_sessionAwareObjects_doesntNotifyIfNotRegistered() {
@Test
public void testPerfSession_sessionAwareObjects_NotifiesIfRegistered() {
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
-
+ SessionManager testSessionManager = SessionManager.getInstance();
FakeSessionAwareObject spySessionAwareObjectOne = spy(new FakeSessionAwareObject());
FakeSessionAwareObject spySessionAwareObjectTwo = spy(new FakeSessionAwareObject());
testSessionManager.registerForSessionUpdates(new WeakReference<>(spySessionAwareObjectOne));
testSessionManager.registerForSessionUpdates(new WeakReference<>(spySessionAwareObjectTwo));
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1"));
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2"));
+ triggerAqsSession();
+ triggerAqsSession();
verify(spySessionAwareObjectOne, times(2))
.updateSession(ArgumentMatchers.nullable(PerfSession.class));
@@ -333,9 +118,7 @@ public void testPerfSession_sessionAwareObjects_NotifiesIfRegistered() {
@Test
public void testPerfSession_sessionAwareObjects_DoesNotNotifyIfUnregistered() {
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
-
+ SessionManager testSessionManager = SessionManager.getInstance();
FakeSessionAwareObject spySessionAwareObjectOne = spy(new FakeSessionAwareObject());
FakeSessionAwareObject spySessionAwareObjectTwo = spy(new FakeSessionAwareObject());
@@ -347,11 +130,11 @@ public void testPerfSession_sessionAwareObjects_DoesNotNotifyIfUnregistered() {
testSessionManager.registerForSessionUpdates(weakSpySessionAwareObjectOne);
testSessionManager.registerForSessionUpdates(weakSpySessionAwareObjectTwo);
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1"));
+ triggerAqsSession();
testSessionManager.unregisterForSessionUpdates(weakSpySessionAwareObjectOne);
testSessionManager.unregisterForSessionUpdates(weakSpySessionAwareObjectTwo);
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2"));
+ triggerAqsSession();
verify(spySessionAwareObjectOne, times(1))
.updateSession(ArgumentMatchers.nullable(PerfSession.class));
From bacc81f322f120898174ac6dc0bdcdcea0df5e7a Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Tue, 4 Feb 2025 18:07:34 -0500
Subject: [PATCH 015/146] Update SessionManagerTest
---
.../com/google/firebase/perf/session/SessionManagerTest.java | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
index bd8e69a74f4..53ad5ea607f 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
@@ -84,8 +84,7 @@ public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsIn
@Test
public void testPerfSession_sessionAwareObjects_doesntNotifyIfNotRegistered() {
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
+ SessionManager testSessionManager = SessionManager.getInstance();
FakeSessionAwareObject spySessionAwareObjectOne = spy(new FakeSessionAwareObject());
FakeSessionAwareObject spySessionAwareObjectTwo = spy(new FakeSessionAwareObject());
From f2b30fe4db30142dc602a8491a8a47c78da3c11c Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Tue, 4 Feb 2025 18:13:27 -0500
Subject: [PATCH 016/146] Remove calling defaultInstance as it's already called
in FirebasePerformanceInitializer
---
.../com/google/firebase/perf/FirebasePerformanceTestBase.java | 1 -
1 file changed, 1 deletion(-)
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
index 9a787161017..471b8c59df7 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
@@ -75,7 +75,6 @@ public void setUpFirebaseApp() {
.setProjectId(FAKE_FIREBASE_PROJECT_ID)
.build();
FirebaseApp.initializeApp(appContext, options);
- FirebasePerformance.getInstance();
}
@After
From db14ff5d6c42c515e7ecc7a62e53efa5a4751e89 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Wed, 5 Feb 2025 09:50:13 -0500
Subject: [PATCH 017/146] More test changes
---
.../java/com/google/firebase/perf/session/SessionManagerKt.kt | 4 ++++
.../com/google/firebase/perf/FirebasePerformanceTestBase.java | 2 ++
.../com/google/firebase/perf/session/SessionManagerTest.java | 4 ++--
3 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt
index 9913a923168..855a890a4d6 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt
@@ -39,6 +39,10 @@ class SessionManagerKt(private val dataCollectionEnabled: Boolean) : SessionSubs
return perfSessionToAqs[perfSessionId]?.sessionId ?: perfSessionId
}
+ fun clearSessionForTest() {
+ perfSessionToAqs.clear()
+ }
+
companion object {
val instance: SessionManagerKt by lazy {
SessionManagerKt(ConfigResolver.getInstance().isPerformanceMonitoringEnabled)
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
index 471b8c59df7..4a811be9f32 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
@@ -75,11 +75,13 @@ public void setUpFirebaseApp() {
.setProjectId(FAKE_FIREBASE_PROJECT_ID)
.build();
FirebaseApp.initializeApp(appContext, options);
+ triggerAqsSession();
}
@After
public void tearDownFirebaseApp() {
FirebaseApp.clearInstancesForTest();
+ SessionManagerKt.Companion.getInstance().clearSessionForTest();
}
protected static void forceSessionsFeatureDisabled() {
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
index 53ad5ea607f..59c2ec21d2a 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
@@ -30,6 +30,8 @@
import com.google.firebase.perf.util.Clock;
import java.lang.ref.WeakReference;
import java.util.concurrent.ExecutionException;
+
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -56,8 +58,6 @@ public void setUp() {
when(mockPerfSession.sessionId()).thenReturn("sessionId");
when(mockAppStateMonitor.isColdStart()).thenReturn(false);
AppStateMonitor.getInstance().setIsColdStart(false);
- // We assume that an AQS session has been created in all tests.
- triggerAqsSession();
}
@Test
From 0b1be56a74eaf01dfde4f2f29b603015b6bd5f90 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Wed, 5 Feb 2025 09:50:34 -0500
Subject: [PATCH 018/146] style
---
.../com/google/firebase/perf/session/SessionManagerTest.java | 2 --
1 file changed, 2 deletions(-)
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
index 59c2ec21d2a..4dc97bda641 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
@@ -30,8 +30,6 @@
import com.google.firebase.perf.util.Clock;
import java.lang.ref.WeakReference;
import java.util.concurrent.ExecutionException;
-
-import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
From dfbc5215c6cf1e843f51bd0d1978890e48b0666d Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Wed, 5 Feb 2025 13:10:49 -0500
Subject: [PATCH 019/146] Change the location of registering subscriber
---
.../java/com/google/firebase/perf/FirebasePerformance.java | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
index ec63b035d3f..48e123ff2d1 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
@@ -180,6 +180,9 @@ public static FirebasePerformance getInstance() {
return;
}
+ // Prioritize registering the FirebaseSession dependency to have the session
+ // `setApplicationContext`.
+ FirebaseSessionsDependencies.register(SessionManagerKt.Companion.getInstance());
TransportManager.getInstance()
.initialize(firebaseApp, firebaseInstallationsApi, transportFactoryProvider);
@@ -201,8 +204,6 @@ public static FirebasePerformance getInstance() {
ConsoleUrlGenerator.generateDashboardUrl(
firebaseApp.getOptions().getProjectId(), appContext.getPackageName())));
}
-
- FirebaseSessionsDependencies.register(SessionManagerKt.Companion.getInstance());
}
/**
From 21d47785cd7ddbbbb31c4d1f72a7ac86801d4f9c Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Wed, 5 Feb 2025 14:28:17 -0500
Subject: [PATCH 020/146] Update subscriber
---
firebase-crashlytics-ndk/src/third_party/crashpad | 2 +-
firebase-crashlytics-ndk/src/third_party/lss | 2 +-
firebase-crashlytics-ndk/src/third_party/mini_chromium | 2 +-
.../com/google/firebase/perf/FirebasePerformance.java | 5 +++--
...agerKt.kt => FirebasePerformanceSessionSubscriber.kt} | 9 ++++++---
.../com/google/firebase/perf/session/PerfSession.java | 4 ++--
.../firebase/perf/FirebasePerformanceTestBase.java | 6 +++---
7 files changed, 17 insertions(+), 13 deletions(-)
rename firebase-perf/src/main/java/com/google/firebase/perf/session/{SessionManagerKt.kt => FirebasePerformanceSessionSubscriber.kt} (85%)
diff --git a/firebase-crashlytics-ndk/src/third_party/crashpad b/firebase-crashlytics-ndk/src/third_party/crashpad
index b8937c6cb4b..c902f6b1c9e 160000
--- a/firebase-crashlytics-ndk/src/third_party/crashpad
+++ b/firebase-crashlytics-ndk/src/third_party/crashpad
@@ -1 +1 @@
-Subproject commit b8937c6cb4b38c1ca06b46791c84b31632895f1f
+Subproject commit c902f6b1c9e43224181969110b83e0053b2ddd3c
diff --git a/firebase-crashlytics-ndk/src/third_party/lss b/firebase-crashlytics-ndk/src/third_party/lss
index ed31caa60f2..9719c1e1e67 160000
--- a/firebase-crashlytics-ndk/src/third_party/lss
+++ b/firebase-crashlytics-ndk/src/third_party/lss
@@ -1 +1 @@
-Subproject commit ed31caa60f20a4f6569883b2d752ef7522de51e0
+Subproject commit 9719c1e1e676814c456b55f5f070eabad6709d31
diff --git a/firebase-crashlytics-ndk/src/third_party/mini_chromium b/firebase-crashlytics-ndk/src/third_party/mini_chromium
index c081fd005b0..4332ddb6963 160000
--- a/firebase-crashlytics-ndk/src/third_party/mini_chromium
+++ b/firebase-crashlytics-ndk/src/third_party/mini_chromium
@@ -1 +1 @@
-Subproject commit c081fd005b09a59a505b09a4b506f8ba45f70859
+Subproject commit 4332ddb6963750e1106efdcece6d6e2de6dc6430
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
index 48e123ff2d1..17165e8e341 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
@@ -36,8 +36,8 @@
import com.google.firebase.perf.logging.ConsoleUrlGenerator;
import com.google.firebase.perf.metrics.HttpMetric;
import com.google.firebase.perf.metrics.Trace;
+import com.google.firebase.perf.session.FirebasePerformanceSessionSubscriber;
import com.google.firebase.perf.session.SessionManager;
-import com.google.firebase.perf.session.SessionManagerKt;
import com.google.firebase.perf.transport.TransportManager;
import com.google.firebase.perf.util.Constants;
import com.google.firebase.perf.util.ImmutableBundle;
@@ -182,7 +182,8 @@ public static FirebasePerformance getInstance() {
// Prioritize registering the FirebaseSession dependency to have the session
// `setApplicationContext`.
- FirebaseSessionsDependencies.register(SessionManagerKt.Companion.getInstance());
+ FirebaseSessionsDependencies.register(
+ FirebasePerformanceSessionSubscriber.Companion.getInstance());
TransportManager.getInstance()
.initialize(firebaseApp, firebaseInstallationsApi, transportFactoryProvider);
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt
similarity index 85%
rename from firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt
rename to firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt
index 855a890a4d6..44de7cc1f36 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt
@@ -4,7 +4,8 @@ import com.google.firebase.perf.config.ConfigResolver
import com.google.firebase.perf.logging.AndroidLogger
import com.google.firebase.sessions.api.SessionSubscriber
-class SessionManagerKt(private val dataCollectionEnabled: Boolean) : SessionSubscriber {
+class FirebasePerformanceSessionSubscriber(private val dataCollectionEnabled: Boolean) :
+ SessionSubscriber {
private val perfSessionToAqs: MutableMap =
mutableMapOf()
@@ -44,8 +45,10 @@ class SessionManagerKt(private val dataCollectionEnabled: Boolean) : SessionSubs
}
companion object {
- val instance: SessionManagerKt by lazy {
- SessionManagerKt(ConfigResolver.getInstance().isPerformanceMonitoringEnabled)
+ val instance: FirebasePerformanceSessionSubscriber by lazy {
+ FirebasePerformanceSessionSubscriber(
+ ConfigResolver.getInstance().isPerformanceMonitoringEnabled
+ )
}
}
}
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
index 75114cab15f..e23c0d5f0b3 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
@@ -47,7 +47,7 @@ public static PerfSession createNewSession() {
// SessionManagerKt verifies if this is an active session, and sets the AQS session ID.
// The assumption is that new PerfSessions *should* be limited to either App Start, or through
// AQS.
- SessionManagerKt.Companion.getInstance().reportPerfSession(prunedSessionId);
+ FirebasePerformanceSessionSubscriber.Companion.getInstance().reportPerfSession(prunedSessionId);
return session;
}
@@ -68,7 +68,7 @@ private PerfSession(@NonNull Parcel in) {
/** Returns the sessionId of the object. */
public String sessionId() {
- return SessionManagerKt.Companion.getInstance()
+ return FirebasePerformanceSessionSubscriber.Companion.getInstance()
.getAqsMappedToPerfSession(this.internalSessionId);
}
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
index 4a811be9f32..1a045b3f1b9 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
@@ -23,7 +23,7 @@
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.perf.config.ConfigResolver;
-import com.google.firebase.perf.session.SessionManagerKt;
+import com.google.firebase.perf.session.FirebasePerformanceSessionSubscriber;
import com.google.firebase.perf.util.ImmutableBundle;
import com.google.firebase.sessions.api.SessionSubscriber;
import java.util.UUID;
@@ -81,7 +81,7 @@ public void setUpFirebaseApp() {
@After
public void tearDownFirebaseApp() {
FirebaseApp.clearInstancesForTest();
- SessionManagerKt.Companion.getInstance().clearSessionForTest();
+ FirebasePerformanceSessionSubscriber.Companion.getInstance().clearSessionForTest();
}
protected static void forceSessionsFeatureDisabled() {
@@ -99,7 +99,7 @@ protected static void forceNonVerboseSession() {
}
protected static void triggerAqsSession() {
- SessionManagerKt.Companion.getInstance()
+ FirebasePerformanceSessionSubscriber.Companion.getInstance()
.onSessionChanged(
new SessionSubscriber.SessionDetails(FAKE_AQS_SESSION_PREFIX + UUID.randomUUID()));
}
From 9731146e234528656b42a02052dcce1fe647ae8f Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Wed, 5 Feb 2025 14:38:28 -0500
Subject: [PATCH 021/146] Revert internal perf session id
---
.../FirebasePerformanceSessionSubscriber.kt | 8 ++++----
.../firebase/perf/session/PerfSession.java | 18 ++++++------------
.../firebase/perf/session/SessionManager.java | 2 +-
3 files changed, 11 insertions(+), 17 deletions(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt
index 44de7cc1f36..66a6323ade1 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt
@@ -17,16 +17,16 @@ class FirebasePerformanceSessionSubscriber(private val dataCollectionEnabled: Bo
override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) {
AndroidLogger.getInstance().debug("AQS Session Changed: $sessionDetails")
- val currentInternalSessionId = SessionManager.getInstance().perfSession().internalSessionId
+ val perfSessionId = SessionManager.getInstance().perfSession().sessionId()
// There can be situations where a new [PerfSession] was created, but an AQS wasn't
// available (during cold start).
- if (perfSessionToAqs[currentInternalSessionId] == null) {
- perfSessionToAqs[currentInternalSessionId] = sessionDetails
+ if (perfSessionToAqs[perfSessionId] == null) {
+ perfSessionToAqs[perfSessionId] = sessionDetails
} else {
val newSession = PerfSession.createNewSession()
SessionManager.getInstance().updatePerfSession(newSession)
- perfSessionToAqs[newSession.internalSessionId] = sessionDetails
+ perfSessionToAqs[newSession.sessionId()] = sessionDetails
}
}
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
index e23c0d5f0b3..f18e97e094a 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
@@ -30,7 +30,7 @@
/** Details of a session including a unique Id and related information. */
public class PerfSession implements Parcelable {
- private final String internalSessionId;
+ private final String sessionId;
private final Timer creationTime;
private boolean isGaugeAndEventCollectionEnabled = false;
@@ -54,27 +54,21 @@ public static PerfSession createNewSession() {
/** Creates a PerfSession with the provided {@code sessionId} and {@code clock}. */
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
- public PerfSession(String internalSessionId, Clock clock) {
- this.internalSessionId = internalSessionId;
+ public PerfSession(String sessionId, Clock clock) {
+ this.sessionId = sessionId;
creationTime = clock.getTime();
}
private PerfSession(@NonNull Parcel in) {
super();
- internalSessionId = in.readString();
+ sessionId = in.readString();
isGaugeAndEventCollectionEnabled = in.readByte() != 0;
creationTime = in.readParcelable(Timer.class.getClassLoader());
}
/** Returns the sessionId of the object. */
public String sessionId() {
- return FirebasePerformanceSessionSubscriber.Companion.getInstance()
- .getAqsMappedToPerfSession(this.internalSessionId);
- }
-
- @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
- public String getInternalSessionId() {
- return internalSessionId;
+ return this.sessionId;
}
/**
@@ -202,7 +196,7 @@ public int describeContents() {
* @param flags Additional flags about how the object should be written.
*/
public void writeToParcel(@NonNull Parcel out, int flags) {
- out.writeString(internalSessionId);
+ out.writeString(sessionId);
out.writeByte((byte) (isGaugeAndEventCollectionEnabled ? 1 : 0));
out.writeParcelable(creationTime, 0);
}
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
index 54784d7a274..c077b98efb1 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
@@ -114,7 +114,7 @@ public void stopGaugeCollectionIfSessionRunningTooLong() {
public void updatePerfSession(PerfSession perfSession) {
// Do not update the perf session if it is the exact same sessionId.
if (Objects.equals(
- perfSession.getInternalSessionId(), this.perfSession.getInternalSessionId())) {
+ perfSession.sessionId(), this.perfSession.sessionId())) {
return;
}
From b942120f6e136cfe0826f26390cd4abce3a68a9b Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Wed, 5 Feb 2025 14:53:36 -0500
Subject: [PATCH 022/146] Attempt to simplify AQS session usage
---
.../firebase/perf/session/PerfSession.java | 22 +++++++++++--------
.../firebase/perf/session/SessionManager.java | 3 +--
.../perf/session/gauges/GaugeManager.java | 11 ++++++++--
3 files changed, 23 insertions(+), 13 deletions(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
index f18e97e094a..ac0cd6f9d71 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
@@ -30,6 +30,7 @@
/** Details of a session including a unique Id and related information. */
public class PerfSession implements Parcelable {
+ private static final String SESSION_ID_PREFIX = "fireperf-session";
private final String sessionId;
private final Timer creationTime;
@@ -39,16 +40,9 @@ public class PerfSession implements Parcelable {
* Creates a PerfSession object and decides what metrics to collect.
*/
public static PerfSession createNewSession() {
- String prunedSessionId = UUID.randomUUID().toString().replace("-", "");
+ String prunedSessionId = SESSION_ID_PREFIX + UUID.randomUUID().toString().replace("-", "");
PerfSession session = new PerfSession(prunedSessionId, new Clock());
session.setGaugeAndEventCollectionEnabled(shouldCollectGaugesAndEvents());
-
- // Every time a PerfSession is created, it sets the AQS to null. Once an AQS is received,
- // SessionManagerKt verifies if this is an active session, and sets the AQS session ID.
- // The assumption is that new PerfSessions *should* be limited to either App Start, or through
- // AQS.
- FirebasePerformanceSessionSubscriber.Companion.getInstance().reportPerfSession(prunedSessionId);
-
return session;
}
@@ -57,6 +51,11 @@ public static PerfSession createNewSession() {
public PerfSession(String sessionId, Clock clock) {
this.sessionId = sessionId;
creationTime = clock.getTime();
+ // Every time a PerfSession is created, it sets the AQS to null. Once an AQS is received,
+ // SessionManagerKt verifies if this is an active session, and sets the AQS session ID.
+ // The assumption is that new PerfSessions *should* be limited to either App Start, or through
+ // AQS.
+ FirebasePerformanceSessionSubscriber.Companion.getInstance().reportPerfSession(sessionId);
}
private PerfSession(@NonNull Parcel in) {
@@ -71,6 +70,11 @@ public String sessionId() {
return this.sessionId;
}
+ private String aqsSessionId() {
+ return FirebasePerformanceSessionSubscriber.Companion.getInstance()
+ .getAqsMappedToPerfSession(this.sessionId);
+ }
+
/**
* Returns a timer object that has been seeded with the system time at which the session began.
*/
@@ -121,7 +125,7 @@ public boolean isSessionRunningTooLong() {
/** Creates and returns the proto object for PerfSession object. */
public com.google.firebase.perf.v1.PerfSession build() {
com.google.firebase.perf.v1.PerfSession.Builder sessionMetric =
- com.google.firebase.perf.v1.PerfSession.newBuilder().setSessionId(sessionId());
+ com.google.firebase.perf.v1.PerfSession.newBuilder().setSessionId(aqsSessionId());
// If gauge collection is enabled, enable gauge collection verbosity.
if (isGaugeAndEventCollectionEnabled) {
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
index c077b98efb1..a6e95878a31 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
@@ -113,8 +113,7 @@ public void stopGaugeCollectionIfSessionRunningTooLong() {
*/
public void updatePerfSession(PerfSession perfSession) {
// Do not update the perf session if it is the exact same sessionId.
- if (Objects.equals(
- perfSession.sessionId(), this.perfSession.sessionId())) {
+ if (Objects.equals(perfSession.sessionId(), this.perfSession.sessionId())) {
return;
}
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
index 7f6182a9c15..46d67540f0b 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
@@ -22,6 +22,7 @@
import com.google.firebase.components.Lazy;
import com.google.firebase.perf.config.ConfigResolver;
import com.google.firebase.perf.logging.AndroidLogger;
+import com.google.firebase.perf.session.FirebasePerformanceSessionSubscriber;
import com.google.firebase.perf.session.PerfSession;
import com.google.firebase.perf.transport.TransportManager;
import com.google.firebase.perf.util.Timer;
@@ -242,7 +243,10 @@ private void syncFlush(String sessionId, ApplicationProcessState appState) {
}
// Adding Session ID info.
- gaugeMetricBuilder.setSessionId(sessionId);
+ String aqsSessionId =
+ FirebasePerformanceSessionSubscriber.Companion.getInstance()
+ .getAqsMappedToPerfSession(sessionId);
+ gaugeMetricBuilder.setSessionId(aqsSessionId);
transportManager.log(gaugeMetricBuilder.build(), appState);
}
@@ -256,10 +260,13 @@ private void syncFlush(String sessionId, ApplicationProcessState appState) {
* @return true if GaugeMetadata was logged, false otherwise.
*/
public boolean logGaugeMetadata(String sessionId, ApplicationProcessState appState) {
+ String aqsSessionId =
+ FirebasePerformanceSessionSubscriber.Companion.getInstance()
+ .getAqsMappedToPerfSession(sessionId);
if (gaugeMetadataManager != null) {
GaugeMetric gaugeMetric =
GaugeMetric.newBuilder()
- .setSessionId(sessionId)
+ .setSessionId(aqsSessionId)
.setGaugeMetadata(getGaugeMetadata())
.build();
transportManager.log(gaugeMetric, appState);
From 6899fde4ba688ee100546985248ae1cac4672303 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Wed, 5 Feb 2025 15:00:38 -0500
Subject: [PATCH 023/146] Update tests
---
.../com/google/firebase/perf/FirebasePerformanceTestBase.java | 2 +-
.../com/google/firebase/perf/session/SessionManagerTest.java | 3 +--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
index 1a045b3f1b9..f18bce3855b 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java
@@ -53,7 +53,7 @@ public class FirebasePerformanceTestBase {
protected static final String FAKE_FIREBASE_DB_URL = "https://fir-perftestapp.firebaseio.com";
protected static final String FAKE_FIREBASE_PROJECT_ID = "fir-perftestapp";
- protected static final String FAKE_AQS_SESSION_PREFIX = "AIzaSyBcE";
+ protected static final String FAKE_AQS_SESSION_PREFIX = "AQS";
protected Context appContext;
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
index 4dc97bda641..63edf3167e4 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
@@ -62,8 +62,7 @@ public void setUp() {
public void testInstanceCreation() {
assertThat(SessionManager.getInstance()).isNotNull();
assertThat(SessionManager.getInstance()).isEqualTo(SessionManager.getInstance());
- assertThat(SessionManager.getInstance().perfSession().sessionId())
- .contains(FAKE_AQS_SESSION_PREFIX);
+ assertThat(SessionManager.getInstance().perfSession().sessionId()).isNotNull();
}
@Test
From 5676892c077402104d021c2f89af24d3baa6a30c Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Wed, 5 Feb 2025 15:01:20 -0500
Subject: [PATCH 024/146] Update prefix
---
.../main/java/com/google/firebase/perf/session/PerfSession.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
index ac0cd6f9d71..7d49374bd1c 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
@@ -30,7 +30,7 @@
/** Details of a session including a unique Id and related information. */
public class PerfSession implements Parcelable {
- private static final String SESSION_ID_PREFIX = "fireperf-session";
+ private static final String SESSION_ID_PREFIX = "FPRS";
private final String sessionId;
private final Timer creationTime;
From e4b7114d497697ac4db71ef0091d51e34bf3729d Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Wed, 5 Feb 2025 15:01:35 -0500
Subject: [PATCH 025/146] Update prefix
---
.../main/java/com/google/firebase/perf/session/PerfSession.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
index 7d49374bd1c..2d742f3d825 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
@@ -30,7 +30,7 @@
/** Details of a session including a unique Id and related information. */
public class PerfSession implements Parcelable {
- private static final String SESSION_ID_PREFIX = "FPRS";
+ private static final String SESSION_ID_PREFIX = "FPR";
private final String sessionId;
private final Timer creationTime;
From 79aac4c91cd119f84283d92f6fec42de932af096 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Thu, 6 Feb 2025 12:45:09 -0500
Subject: [PATCH 026/146] Improve logging and adda TODO
---
.../perf/session/FirebasePerformanceSessionSubscriber.kt | 3 ++-
.../com/google/firebase/perf/session/PerfSession.java | 8 +++++---
.../com/google/firebase/perf/session/SessionManager.java | 3 +--
.../google/firebase/perf/session/gauges/GaugeManager.java | 5 +++++
4 files changed, 13 insertions(+), 6 deletions(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt
index 66a6323ade1..3c8933bc02a 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt
@@ -16,8 +16,9 @@ class FirebasePerformanceSessionSubscriber(private val dataCollectionEnabled: Bo
get() = SessionSubscriber.Name.PERFORMANCE
override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) {
- AndroidLogger.getInstance().debug("AQS Session Changed: $sessionDetails")
val perfSessionId = SessionManager.getInstance().perfSession().sessionId()
+ AndroidLogger.getInstance()
+ .debug("CFPRS AQS Session Changed: $sessionDetails, PerfSession: $perfSessionId")
// There can be situations where a new [PerfSession] was created, but an AQS wasn't
// available (during cold start).
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
index 2d742f3d825..6a485e11458 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
@@ -20,6 +20,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.firebase.perf.config.ConfigResolver;
+import com.google.firebase.perf.logging.AndroidLogger;
import com.google.firebase.perf.util.Clock;
import com.google.firebase.perf.util.Timer;
import com.google.firebase.perf.v1.SessionVerbosity;
@@ -55,6 +56,7 @@ public PerfSession(String sessionId, Clock clock) {
// SessionManagerKt verifies if this is an active session, and sets the AQS session ID.
// The assumption is that new PerfSessions *should* be limited to either App Start, or through
// AQS.
+ AndroidLogger.getInstance().debug("CFPRS PerfSession(): " + sessionId);
FirebasePerformanceSessionSubscriber.Companion.getInstance().reportPerfSession(sessionId);
}
@@ -176,9 +178,9 @@ public static com.google.firebase.perf.v1.PerfSession[] buildAndSort(
/** If true, Session Gauge collection is enabled. */
public static boolean shouldCollectGaugesAndEvents() {
ConfigResolver configResolver = ConfigResolver.getInstance();
-
- return configResolver.isPerformanceMonitoringEnabled()
- && Math.random() < configResolver.getSessionsSamplingRate();
+ return true;
+ // return configResolver.isPerformanceMonitoringEnabled()
+ // && Math.random() < configResolver.getSessionsSamplingRate();
}
/**
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
index a6e95878a31..afc88b15e49 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
@@ -68,6 +68,7 @@ public SessionManager(
this.gaugeManager = gaugeManager;
this.perfSession = perfSession;
this.appStateMonitor = appStateMonitor;
+ AndroidLogger.getInstance().debug("CFPRS: SessionManager()");
}
/**
@@ -117,8 +118,6 @@ public void updatePerfSession(PerfSession perfSession) {
return;
}
- AndroidLogger.getInstance().debug("Perf Session Changed: " + perfSession);
-
this.perfSession = perfSession;
synchronized (clients) {
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
index 46d67540f0b..fc115079d89 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
@@ -247,6 +247,7 @@ private void syncFlush(String sessionId, ApplicationProcessState appState) {
FirebasePerformanceSessionSubscriber.Companion.getInstance()
.getAqsMappedToPerfSession(sessionId);
gaugeMetricBuilder.setSessionId(aqsSessionId);
+ AndroidLogger.getInstance().debug("CFPR syncFlush: " + sessionId + " AQS: " + aqsSessionId);
transportManager.log(gaugeMetricBuilder.build(), appState);
}
@@ -260,9 +261,13 @@ private void syncFlush(String sessionId, ApplicationProcessState appState) {
* @return true if GaugeMetadata was logged, false otherwise.
*/
public boolean logGaugeMetadata(String sessionId, ApplicationProcessState appState) {
+ // TODO(b/394127311): Based on logs, AQS session ID isn't available any time
+ // this is called. Adding a TODO to identify potential changes.
String aqsSessionId =
FirebasePerformanceSessionSubscriber.Companion.getInstance()
.getAqsMappedToPerfSession(sessionId);
+ AndroidLogger.getInstance()
+ .debug("CFPR logGaugeMetadata: " + sessionId + " AQS: " + aqsSessionId);
if (gaugeMetadataManager != null) {
GaugeMetric gaugeMetric =
GaugeMetric.newBuilder()
From 5093a85c0d87aacdc2ec7b10ddc313743729bb94 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Thu, 6 Feb 2025 12:59:23 -0500
Subject: [PATCH 027/146] Additional TODO
---
.../com/google/firebase/perf/session/gauges/GaugeManager.java | 1 +
1 file changed, 1 insertion(+)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
index fc115079d89..acabf5ee827 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
@@ -274,6 +274,7 @@ public boolean logGaugeMetadata(String sessionId, ApplicationProcessState appSta
.setSessionId(aqsSessionId)
.setGaugeMetadata(getGaugeMetadata())
.build();
+ // TODO(b/394127311): Explore maintaining this metadata until AQS is available.
transportManager.log(gaugeMetric, appState);
return true;
}
From 8a8e8edec1d47099a7cc1236ab9d150ace012fd1 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Thu, 6 Feb 2025 15:26:30 -0500
Subject: [PATCH 028/146] Remove the logging of GaugeMetadata based on AppStart
---
.../firebase/perf/FirebasePerformance.java | 1 -
.../firebase/perf/session/SessionManager.java | 17 -----------------
.../perf/session/gauges/CpuGaugeCollector.java | 13 +++----------
.../perf/session/gauges/GaugeManager.java | 8 ++++----
.../session/gauges/GaugeMetadataManager.java | 10 +---------
.../session/gauges/MemoryGaugeCollector.java | 6 +++---
6 files changed, 11 insertions(+), 44 deletions(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
index 40468566225..3cc49896ce0 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
@@ -182,7 +182,6 @@ public static FirebasePerformance getInstance() {
.initialize(firebaseApp, firebaseInstallationsApi, transportFactoryProvider);
Context appContext = firebaseApp.getApplicationContext();
- // TODO(b/110178816): Explore moving off of main thread.
mMetadataBundle = extractMetadata(appContext);
remoteConfigManager.setFirebaseRemoteConfigProvider(firebaseRemoteConfigProvider);
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
index 79d034b9b0b..29ffb988ba0 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
@@ -79,9 +79,6 @@ public SessionManager(
* (currently that is before onResume finishes) to ensure gauge collection starts on time.
*/
public void setApplicationContext(final Context appContext) {
- // Get PerfSession in main thread first, because it is possible that app changes fg/bg state
- // which creates a new perfSession, before the following is executed in background thread
- final PerfSession appStartSession = perfSession;
// TODO(b/258263016): Migrate to go/firebase-android-executors
@SuppressLint("ThreadPoolCreation")
ExecutorService executorService = Executors.newSingleThreadExecutor();
@@ -89,10 +86,6 @@ public void setApplicationContext(final Context appContext) {
executorService.submit(
() -> {
gaugeManager.initializeGaugeMetadataManager(appContext);
- if (appStartSession.isGaugeAndEventCollectionEnabled()) {
- gaugeManager.logGaugeMetadata(
- appStartSession.sessionId(), ApplicationProcessState.FOREGROUND);
- }
});
}
@@ -164,9 +157,6 @@ public void updatePerfSession(PerfSession perfSession) {
}
}
- // Log the gauge metadata event if data collection is enabled.
- logGaugeMetadataIfCollectionEnabled(appStateMonitor.getAppState());
-
// Start of stop the gauge data collection.
startOrStopCollectingGauges(appStateMonitor.getAppState());
}
@@ -178,7 +168,6 @@ public void updatePerfSession(PerfSession perfSession) {
* this does not reset the perfSession.
*/
public void initializeGaugeCollection() {
- logGaugeMetadataIfCollectionEnabled(ApplicationProcessState.FOREGROUND);
startOrStopCollectingGauges(ApplicationProcessState.FOREGROUND);
}
@@ -206,12 +195,6 @@ public void unregisterForSessionUpdates(WeakReference client
}
}
- private void logGaugeMetadataIfCollectionEnabled(ApplicationProcessState appState) {
- if (perfSession.isGaugeAndEventCollectionEnabled()) {
- gaugeManager.logGaugeMetadata(perfSession.sessionId(), appState);
- }
- }
-
private void startOrStopCollectingGauges(ApplicationProcessState appState) {
if (perfSession.isGaugeAndEventCollectionEnabled()) {
gaugeManager.startCollectingGauges(perfSession, appState);
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/CpuGaugeCollector.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/CpuGaugeCollector.java
index e33d363c0aa..ceb636d56b3 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/CpuGaugeCollector.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/CpuGaugeCollector.java
@@ -17,8 +17,6 @@
import static android.system.Os.sysconf;
import android.annotation.SuppressLint;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
import android.system.OsConstants;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@@ -163,7 +161,7 @@ private synchronized void scheduleCpuMetricCollectionWithRate(
this.cpuMetricCollectionRateMs = cpuMetricCollectionRate;
try {
cpuMetricCollectorJob =
- cpuMetricCollectorExecutor.scheduleAtFixedRate(
+ cpuMetricCollectorExecutor.scheduleWithFixedDelay(
() -> {
CpuMetricReading currCpuReading = syncCollectCpuMetric(referenceTime);
if (currCpuReading != null) {
@@ -181,7 +179,7 @@ private synchronized void scheduleCpuMetricCollectionWithRate(
private synchronized void scheduleCpuMetricCollectionOnce(Timer referenceTime) {
try {
@SuppressWarnings("FutureReturnValueIgnored")
- ScheduledFuture unusedFuture =
+ ScheduledFuture> unusedFuture =
cpuMetricCollectorExecutor.schedule(
() -> {
CpuMetricReading currCpuReading = syncCollectCpuMetric(referenceTime);
@@ -227,12 +225,7 @@ private CpuMetricReading syncCollectCpuMetric(Timer referenceTime) {
}
private long getClockTicksPerSecond() {
- if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
- return sysconf(OsConstants._SC_CLK_TCK);
- } else {
- // TODO(b/110779408): Figure out how to collect this info for Android API 20 and below.
- return INVALID_SC_PER_CPU_CLOCK_TICK;
- }
+ return sysconf(OsConstants._SC_CLK_TCK);
}
private long convertClockTicksToMicroseconds(long clockTicks) {
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
index 7f6182a9c15..f8b623d5f89 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
@@ -72,8 +72,8 @@ private GaugeManager() {
TransportManager.getInstance(),
ConfigResolver.getInstance(),
null,
- new Lazy<>(() -> new CpuGaugeCollector()),
- new Lazy<>(() -> new MemoryGaugeCollector()));
+ new Lazy<>(CpuGaugeCollector::new),
+ new Lazy<>(MemoryGaugeCollector::new));
}
@VisibleForTesting
@@ -81,7 +81,7 @@ private GaugeManager() {
Lazy gaugeManagerExecutor,
TransportManager transportManager,
ConfigResolver configResolver,
- GaugeMetadataManager gaugeMetadataManager,
+ @Nullable GaugeMetadataManager gaugeMetadataManager,
Lazy cpuGaugeCollector,
Lazy memoryGaugeCollector) {
@@ -140,7 +140,7 @@ public void startCollectingGauges(
gaugeManagerDataCollectionJob =
gaugeManagerExecutor
.get()
- .scheduleAtFixedRate(
+ .scheduleWithFixedDelay(
() -> {
syncFlush(sessionIdForScheduledTask, applicationProcessStateForScheduledTask);
},
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeMetadataManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeMetadataManager.java
index 6b4466dfc35..d7ad10590b6 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeMetadataManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeMetadataManager.java
@@ -17,8 +17,6 @@
import android.app.ActivityManager;
import android.app.ActivityManager.MemoryInfo;
import android.content.Context;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
import androidx.annotation.VisibleForTesting;
import com.google.firebase.perf.logging.AndroidLogger;
import com.google.firebase.perf.util.StorageUnit;
@@ -41,7 +39,6 @@ class GaugeMetadataManager {
private final Runtime runtime;
private final ActivityManager activityManager;
private final MemoryInfo memoryInfo;
- private final Context appContext;
GaugeMetadataManager(Context appContext) {
this(Runtime.getRuntime(), appContext);
@@ -50,7 +47,6 @@ class GaugeMetadataManager {
@VisibleForTesting
GaugeMetadataManager(Runtime runtime, Context appContext) {
this.runtime = runtime;
- this.appContext = appContext;
this.activityManager = (ActivityManager) appContext.getSystemService(Context.ACTIVITY_SERVICE);
memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);
@@ -75,11 +71,7 @@ public int getMaxEncouragedAppJavaHeapMemoryKb() {
/** Returns the total memory (in kilobytes) accessible by the kernel (called the RAM size). */
public int getDeviceRamSizeKb() {
- if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
- return Utils.saturatedIntCast(StorageUnit.BYTES.toKilobytes(memoryInfo.totalMem));
- }
-
- return readTotalRAM(/* procFileName= */ "/proc/meminfo");
+ return Utils.saturatedIntCast(StorageUnit.BYTES.toKilobytes(memoryInfo.totalMem));
}
/** Returns the total ram size of the device (in kilobytes) by reading the "proc/meminfo" file. */
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollector.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollector.java
index eeaf4eb7c80..a7b4b40002a 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollector.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollector.java
@@ -50,7 +50,7 @@ public class MemoryGaugeCollector {
public final ConcurrentLinkedQueue memoryMetricReadings;
private final Runtime runtime;
- @Nullable private ScheduledFuture memoryMetricCollectorJob = null;
+ @Nullable private ScheduledFuture> memoryMetricCollectorJob = null;
private long memoryMetricCollectionRateMs = UNSET_MEMORY_METRIC_COLLECTION_RATE;
// TODO(b/258263016): Migrate to go/firebase-android-executors
@@ -124,7 +124,7 @@ private synchronized void scheduleMemoryMetricCollectionWithRate(
try {
memoryMetricCollectorJob =
- memoryMetricCollectorExecutor.scheduleAtFixedRate(
+ memoryMetricCollectorExecutor.scheduleWithFixedDelay(
() -> {
AndroidMemoryReading memoryReading = syncCollectMemoryMetric(referenceTime);
if (memoryReading != null) {
@@ -142,7 +142,7 @@ private synchronized void scheduleMemoryMetricCollectionWithRate(
private synchronized void scheduleMemoryMetricCollectionOnce(Timer referenceTime) {
try {
@SuppressWarnings("FutureReturnValueIgnored")
- ScheduledFuture unusedFuture =
+ ScheduledFuture> unusedFuture =
memoryMetricCollectorExecutor.schedule(
() -> {
AndroidMemoryReading memoryReading = syncCollectMemoryMetric(referenceTime);
From c606723099fb0af281152f038c555bc60b33295c Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Thu, 6 Feb 2025 16:00:10 -0500
Subject: [PATCH 029/146] Remove file based memory reading test
---
.../session/gauges/GaugeMetadataManager.java | 24 -------
.../gauges/GaugeMetadataManagerTest.java | 66 +------------------
2 files changed, 1 insertion(+), 89 deletions(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeMetadataManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeMetadataManager.java
index d7ad10590b6..ed38dd8f38d 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeMetadataManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeMetadataManager.java
@@ -22,11 +22,6 @@
import com.google.firebase.perf.util.StorageUnit;
import com.google.firebase.perf.util.Utils;
import com.google.firebase.perf.v1.GaugeMetadata;
-import java.io.BufferedReader;
-import java.io.FileReader;
-import java.io.IOException;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
/**
* The {@code GaugeMetadataManager} class is responsible for collecting {@link GaugeMetadata}
@@ -73,23 +68,4 @@ public int getMaxEncouragedAppJavaHeapMemoryKb() {
public int getDeviceRamSizeKb() {
return Utils.saturatedIntCast(StorageUnit.BYTES.toKilobytes(memoryInfo.totalMem));
}
-
- /** Returns the total ram size of the device (in kilobytes) by reading the "proc/meminfo" file. */
- @VisibleForTesting
- int readTotalRAM(String procFileName) {
- try (BufferedReader br = new BufferedReader(new FileReader(procFileName))) {
- for (String s = br.readLine(); s != null; s = br.readLine()) {
- if (s.startsWith("MemTotal")) {
- Matcher m = Pattern.compile("\\d+").matcher(s);
- return m.find() ? Integer.parseInt(m.group()) : 0;
- }
- }
- } catch (IOException ioe) {
- logger.warn("Unable to read '" + procFileName + "' file: " + ioe.getMessage());
- } catch (NumberFormatException nfe) {
- logger.warn("Unable to parse '" + procFileName + "' file: " + nfe.getMessage());
- }
-
- return 0;
- }
}
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeMetadataManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeMetadataManagerTest.java
index 292747121dd..1592f77e5ad 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeMetadataManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeMetadataManagerTest.java
@@ -15,26 +15,20 @@
package com.google.firebase.perf.session.gauges;
import static com.google.common.truth.Truth.assertThat;
-import static java.nio.charset.StandardCharsets.UTF_8;
import static org.mockito.MockitoAnnotations.initMocks;
import static org.robolectric.Shadows.shadowOf;
import android.app.ActivityManager;
import android.content.Context;
-import android.os.Environment;
import androidx.test.core.app.ApplicationProvider;
import com.google.firebase.perf.FirebasePerformanceTestBase;
import com.google.firebase.perf.util.StorageUnit;
-import java.io.File;
import java.io.IOException;
-import java.io.Writer;
-import java.nio.file.Files;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.robolectric.RobolectricTestRunner;
-import org.robolectric.shadows.ShadowEnvironment;
/** Unit tests for {@link com.google.firebase.perf.session.gauges.GaugeMetadataManager} */
@RunWith(RobolectricTestRunner.class)
@@ -49,12 +43,11 @@ public class GaugeMetadataManagerTest extends FirebasePerformanceTestBase {
@Mock private Runtime runtime;
private ActivityManager activityManager;
- private Context appContext;
@Before
public void setUp() {
initMocks(this);
- appContext = ApplicationProvider.getApplicationContext();
+ Context appContext = ApplicationProvider.getApplicationContext();
activityManager = (ActivityManager) appContext.getSystemService(Context.ACTIVITY_SERVICE);
mockMemory();
@@ -90,62 +83,5 @@ public void testGetDeviceRamSize_returnsExpectedValue() throws IOException {
int ramSize = testGaugeMetadataManager.getDeviceRamSizeKb();
assertThat(ramSize).isEqualTo(StorageUnit.BYTES.toKilobytes(DEVICE_RAM_SIZE_BYTES));
- assertThat(ramSize).isEqualTo(testGaugeMetadataManager.readTotalRAM(createFakeMemInfoFile()));
}
-
- /** @return The file path of this fake file which can be used to read the file. */
- private String createFakeMemInfoFile() throws IOException {
- // Due to file permission issues on forge, it's easiest to just write this file to the emulated
- // robolectric external storage.
- ShadowEnvironment.setExternalStorageState(Environment.MEDIA_MOUNTED);
-
- File file = new File(Environment.getExternalStorageDirectory(), "FakeProcMemInfoFile");
- Writer fileWriter;
-
- fileWriter = Files.newBufferedWriter(file.toPath(), UTF_8);
- fileWriter.write(MEM_INFO_CONTENTS);
- fileWriter.close();
-
- return file.getAbsolutePath();
- }
-
- private static final String MEM_INFO_CONTENTS =
- "MemTotal: "
- + DEVICE_RAM_SIZE_KB
- + " kB\n"
- + "MemFree: 542404 kB\n"
- + "MemAvailable: 1392324 kB\n"
- + "Buffers: 64292 kB\n"
- + "Cached: 826180 kB\n"
- + "SwapCached: 4196 kB\n"
- + "Active: 934768 kB\n"
- + "Inactive: 743812 kB\n"
- + "Active(anon): 582132 kB\n"
- + "Inactive(anon): 241500 kB\n"
- + "Active(file): 352636 kB\n"
- + "Inactive(file): 502312 kB\n"
- + "Unevictable: 5148 kB\n"
- + "Mlocked: 256 kB\n"
- + "SwapTotal: 524284 kB\n"
- + "SwapFree: 484800 kB\n"
- + "Dirty: 4 kB\n"
- + "Writeback: 0 kB\n"
- + "AnonPages: 789404 kB\n"
- + "Mapped: 241928 kB\n"
- + "Shmem: 30632 kB\n"
- + "Slab: 122320 kB\n"
- + "SReclaimable: 42552 kB\n"
- + "SUnreclaim: 79768 kB\n"
- + "KernelStack: 22816 kB\n"
- + "PageTables: 35344 kB\n"
- + "NFS_Unstable: 0 kB\n"
- + "Bounce: 0 kB\n"
- + "WritebackTmp: 0 kB\n"
- + "CommitLimit: 2042280 kB\n"
- + "Committed_AS: 76623352 kB\n"
- + "VmallocTotal: 251658176 kB\n"
- + "VmallocUsed: 232060 kB\n"
- + "VmallocChunk: 251347444 kB\n"
- + "NvMapMemFree: 48640 kB\n"
- + "NvMapMemUsed: 471460 kB\n";
}
From 148b068e0844796fd8be6c4c2b0050bb50030b19 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Thu, 6 Feb 2025 16:03:58 -0500
Subject: [PATCH 030/146] Add TODO to re-introduce metadata logging
---
.../com/google/firebase/perf/session/gauges/GaugeManager.java | 1 +
1 file changed, 1 insertion(+)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
index f8b623d5f89..30da2f0160f 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
@@ -256,6 +256,7 @@ private void syncFlush(String sessionId, ApplicationProcessState appState) {
* @return true if GaugeMetadata was logged, false otherwise.
*/
public boolean logGaugeMetadata(String sessionId, ApplicationProcessState appState) {
+ // TODO(b/394127311): Re-introduce logging of metadata for AQS.
if (gaugeMetadataManager != null) {
GaugeMetric gaugeMetric =
GaugeMetric.newBuilder()
From 4c637aa9e1a2717b9aa86e3da1a2a9a1bb995601 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Thu, 6 Feb 2025 16:36:00 -0500
Subject: [PATCH 031/146] Re-introduce gauge metadata collection
---
.../FirebasePerformanceSessionSubscriber.kt | 4 +++
.../firebase/perf/session/SessionManager.java | 11 +-----
.../perf/session/gauges/GaugeManager.java | 34 ++++++++-----------
.../perf/session/SessionManagerTest.java | 4 +--
.../perf/session/gauges/GaugeManagerTest.java | 31 +----------------
5 files changed, 22 insertions(+), 62 deletions(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt
index 3c8933bc02a..58a34bb5e98 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt
@@ -2,6 +2,7 @@ package com.google.firebase.perf.session
import com.google.firebase.perf.config.ConfigResolver
import com.google.firebase.perf.logging.AndroidLogger
+import com.google.firebase.perf.session.gauges.GaugeManager
import com.google.firebase.sessions.api.SessionSubscriber
class FirebasePerformanceSessionSubscriber(private val dataCollectionEnabled: Boolean) :
@@ -29,6 +30,9 @@ class FirebasePerformanceSessionSubscriber(private val dataCollectionEnabled: Bo
SessionManager.getInstance().updatePerfSession(newSession)
perfSessionToAqs[newSession.sessionId()] = sessionDetails
}
+
+ // Always log GaugeMetadata when a session changes.
+ GaugeManager.getInstance().logGaugeMetadata(sessionDetails.sessionId)
}
fun reportPerfSession(perfSessionId: String) {
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
index b46dff750ae..0d5493b9599 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
@@ -29,8 +29,6 @@
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/** Session manager to generate sessionIDs and broadcast to the application. */
@@ -76,14 +74,7 @@ public SessionManager(
* (currently that is before onResume finishes) to ensure gauge collection starts on time.
*/
public void setApplicationContext(final Context appContext) {
- // TODO(b/258263016): Migrate to go/firebase-android-executors
- @SuppressLint("ThreadPoolCreation")
- ExecutorService executorService = Executors.newSingleThreadExecutor();
- syncInitFuture =
- executorService.submit(
- () -> {
- gaugeManager.initializeGaugeMetadataManager(appContext);
- });
+ gaugeManager.initializeGaugeMetadataManager(appContext, ApplicationProcessState.FOREGROUND);
}
/**
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
index c4b828970d9..65e55e49c11 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
@@ -95,8 +95,10 @@ private GaugeManager() {
}
/** Initializes GaugeMetadataManager which requires application context. */
- public void initializeGaugeMetadataManager(Context appContext) {
+ public void initializeGaugeMetadataManager(
+ Context appContext, ApplicationProcessState applicationProcessState) {
this.gaugeMetadataManager = new GaugeMetadataManager(appContext);
+ this.applicationProcessState = applicationProcessState;
}
/** Returns the singleton instance of this class. */
@@ -255,28 +257,20 @@ private void syncFlush(String sessionId, ApplicationProcessState appState) {
/**
* Log the Gauge Metadata information to the transport.
*
- * @param sessionId The {@link PerfSession#sessionId()} to which the collected Gauge Metrics
+ * @param aqsSessionId The {@link FirebasePerformanceSessionSubscriber#getAqsMappedToPerfSession(String)} to which the collected Gauge Metrics
* should be associated with.
- * @param appState The {@link ApplicationProcessState} for which these gauges are collected.
* @return true if GaugeMetadata was logged, false otherwise.
*/
- public boolean logGaugeMetadata(String sessionId, ApplicationProcessState appState) {
- // TODO(b/394127311): Re-introduce logging of metadata for AQS.
- String aqsSessionId =
- FirebasePerformanceSessionSubscriber.Companion.getInstance()
- .getAqsMappedToPerfSession(sessionId);
- AndroidLogger.getInstance()
- .debug("CFPR logGaugeMetadata: " + sessionId + " AQS: " + aqsSessionId);
- if (gaugeMetadataManager != null) {
- GaugeMetric gaugeMetric =
- GaugeMetric.newBuilder()
- .setSessionId(aqsSessionId)
- .setGaugeMetadata(getGaugeMetadata())
- .build();
- transportManager.log(gaugeMetric, appState);
- return true;
- }
- return false;
+ public void logGaugeMetadata(String aqsSessionId) {
+ // TODO(b/394127311): This can now throw an NPE. Explore if there's anything that should be
+ // verified.
+ AndroidLogger.getInstance().debug("CFPR logGaugeMetadata: " + aqsSessionId);
+ GaugeMetric gaugeMetric =
+ GaugeMetric.newBuilder()
+ .setSessionId(aqsSessionId)
+ .setGaugeMetadata(getGaugeMetadata())
+ .build();
+ transportManager.log(gaugeMetric, this.applicationProcessState);
}
private GaugeMetadata getGaugeMetadata() {
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
index 63edf3167e4..13173838905 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
@@ -75,8 +75,8 @@ public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsIn
testSessionManager.setApplicationContext(mockApplicationContext);
testSessionManager.getSyncInitFuture().get();
- inOrder.verify(mockGaugeManager).initializeGaugeMetadataManager(any());
- inOrder.verify(mockGaugeManager).logGaugeMetadata(any(), any());
+ inOrder.verify(mockGaugeManager).initializeGaugeMetadataManager(any(), any());
+ inOrder.verify(mockGaugeManager).logGaugeMetadata(any());
}
@Test
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeManagerTest.java
index 5090d66c8b9..696b90ed466 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeManagerTest.java
@@ -641,7 +641,7 @@ public void testLogGaugeMetadataSendDataToTransport() {
when(fakeGaugeMetadataManager.getMaxAppJavaHeapMemoryKb()).thenReturn(1000);
when(fakeGaugeMetadataManager.getMaxEncouragedAppJavaHeapMemoryKb()).thenReturn(800);
- testGaugeManager.logGaugeMetadata("sessionId", ApplicationProcessState.FOREGROUND);
+ testGaugeManager.logGaugeMetadata("sessionId");
GaugeMetric recordedGaugeMetric =
getLastRecordedGaugeMetric(ApplicationProcessState.FOREGROUND, 1);
@@ -668,35 +668,6 @@ public void testLogGaugeMetadataDoesntLogWhenGaugeMetadataManagerNotAvailable()
/* gaugeMetadataManager= */ null,
new Lazy<>(() -> fakeCpuGaugeCollector),
new Lazy<>(() -> fakeMemoryGaugeCollector));
-
- assertThat(testGaugeManager.logGaugeMetadata("sessionId", ApplicationProcessState.FOREGROUND))
- .isFalse();
- }
-
- @Test
- public void testLogGaugeMetadataLogsAfterApplicationContextIsSet() {
-
- testGaugeManager =
- new GaugeManager(
- new Lazy<>(() -> fakeScheduledExecutorService),
- mockTransportManager,
- mockConfigResolver,
- /* gaugeMetadataManager= */ null,
- new Lazy<>(() -> fakeCpuGaugeCollector),
- new Lazy<>(() -> fakeMemoryGaugeCollector));
-
- assertThat(testGaugeManager.logGaugeMetadata("sessionId", ApplicationProcessState.FOREGROUND))
- .isFalse();
-
- testGaugeManager.initializeGaugeMetadataManager(ApplicationProvider.getApplicationContext());
- assertThat(testGaugeManager.logGaugeMetadata("sessionId", ApplicationProcessState.FOREGROUND))
- .isTrue();
-
- GaugeMetric recordedGaugeMetric =
- getLastRecordedGaugeMetric(ApplicationProcessState.FOREGROUND, 1);
- GaugeMetadata recordedGaugeMetadata = recordedGaugeMetric.getGaugeMetadata();
-
- assertThat(recordedGaugeMetric.getSessionId()).isEqualTo("sessionId");
}
@Test
From 705ceea73a4000713614f337911e45a5abb0d81e Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Thu, 6 Feb 2025 16:53:13 -0500
Subject: [PATCH 032/146] Delete gaugeMetadata tests that correctly fail
---
.../perf/session/SessionManagerTest.java | 55 -------------------
1 file changed, 55 deletions(-)
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
index f3e3795f3f8..d105594f4ce 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
@@ -136,20 +136,6 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess
assertThat(oldSessionId).isNotEqualTo(testSessionManager.perfSession().sessionId());
}
- @Test
- public void
- testOnUpdateAppStateMakesGaugeManagerLogGaugeMetadataOnForegroundStateIfSessionIsVerbose() {
- forceVerboseSession();
-
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
-
- verify(mockGaugeManager)
- .logGaugeMetadata(
- anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class));
- }
-
@Test
public void
testOnUpdateAppStateDoesntMakeGaugeManagerLogGaugeMetadataOnForegroundStateIfSessionIsNonVerbose() {
@@ -178,21 +164,6 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess
anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class));
}
- @Test
- public void
- testOnUpdateAppStateMakesGaugeManagerLogGaugeMetadataOnBackgroundAppStateIfSessionIsVerboseAndTimedOut() {
- when(mockPerfSession.isSessionRunningTooLong()).thenReturn(true);
- forceVerboseSession();
-
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
-
- verify(mockGaugeManager)
- .logGaugeMetadata(
- anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class));
- }
-
@Test
public void testOnUpdateAppStateMakesGaugeManagerStartCollectingGaugesIfSessionIsVerbose() {
forceVerboseSession();
@@ -232,32 +203,6 @@ public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesWhenSession
verify(mockGaugeManager).stopCollectingGauges();
}
- @Test
- public void testGaugeMetadataIsFlushedOnlyWhenNewVerboseSessionIsCreated() {
- when(mockPerfSession.isSessionRunningTooLong()).thenReturn(false);
-
- // Start with a non verbose session
- forceNonVerboseSession();
- SessionManager testSessionManager =
- new SessionManager(
- mockGaugeManager, PerfSession.createWithId("testSessionId1"), mockAppStateMonitor);
-
- verify(mockGaugeManager, times(0))
- .logGaugeMetadata(
- eq("testSessionId1"),
- eq(com.google.firebase.perf.v1.ApplicationProcessState.FOREGROUND));
-
- // Forcing a verbose session will enable Gauge collection
- forceVerboseSession();
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2"));
- verify(mockGaugeManager, times(1)).logGaugeMetadata(eq("testSessionId2"), any());
-
- // Force a non-verbose session and verify if we are not logging metadata
- forceVerboseSession();
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId3"));
- verify(mockGaugeManager, times(1)).logGaugeMetadata(eq("testSessionId3"), any());
- }
-
@Test
public void testSessionIdDoesNotUpdateIfPerfSessionRunsTooLong() {
Timer mockTimer = mock(Timer.class);
From c2e355e50fbb0b39d90eeb8e345686f0e718792d Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Thu, 6 Feb 2025 17:04:37 -0500
Subject: [PATCH 033/146] Fix unit test
---
.../com/google/firebase/perf/session/SessionManagerTest.java | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
index d105594f4ce..37b9ff7215b 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
@@ -74,7 +74,7 @@ public void testInstanceCreation() {
}
@Test
- public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsInitialized()
+ public void setApplicationContext_initializeGaugeMetadataManager()
throws ExecutionException, InterruptedException {
when(mockPerfSession.isGaugeAndEventCollectionEnabled()).thenReturn(true);
InOrder inOrder = Mockito.inOrder(mockGaugeManager);
@@ -84,7 +84,6 @@ public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsIn
testSessionManager.getSyncInitFuture().get();
inOrder.verify(mockGaugeManager).initializeGaugeMetadataManager(any());
- inOrder.verify(mockGaugeManager).logGaugeMetadata(any(), any());
}
@Test
From d803be6b2a0d78caa42496ee4cf37a9bbedafc36 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Thu, 6 Feb 2025 17:15:36 -0500
Subject: [PATCH 034/146] Remove LinkedHashMap TODO.
---
.../perf/session/SessionManagerTest.java | 130 +-----------------
1 file changed, 7 insertions(+), 123 deletions(-)
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
index 37b9ff7215b..16a2cf307c7 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
@@ -83,123 +83,7 @@ public void setApplicationContext_initializeGaugeMetadataManager()
testSessionManager.setApplicationContext(mockApplicationContext);
testSessionManager.getSyncInitFuture().get();
- inOrder.verify(mockGaugeManager).initializeGaugeMetadataManager(any());
- }
-
- @Test
- public void testOnUpdateAppStateDoesNothingDuringAppStart() {
- String oldSessionId = SessionManager.getInstance().perfSession().sessionId();
-
- assertThat(oldSessionId).isNotNull();
- assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
-
- AppStateMonitor.getInstance().setIsColdStart(true);
-
- SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND);
- assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
- }
-
- @Test
- public void testOnUpdateAppStateGeneratesNewSessionIdOnForegroundState() {
- String oldSessionId = SessionManager.getInstance().perfSession().sessionId();
-
- assertThat(oldSessionId).isNotNull();
- assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
-
- SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND);
- assertThat(oldSessionId).isNotEqualTo(SessionManager.getInstance().perfSession().sessionId());
- }
-
- @Test
- public void testOnUpdateAppStateDoesntGenerateNewSessionIdOnBackgroundState() {
- String oldSessionId = SessionManager.getInstance().perfSession().sessionId();
-
- assertThat(oldSessionId).isNotNull();
- assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
-
- SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.BACKGROUND);
- assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
- }
-
- @Test
- public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSessionExpires() {
- when(mockPerfSession.isSessionRunningTooLong()).thenReturn(true);
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- String oldSessionId = testSessionManager.perfSession().sessionId();
-
- assertThat(oldSessionId).isNotNull();
- assertThat(oldSessionId).isEqualTo(testSessionManager.perfSession().sessionId());
-
- testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
- assertThat(oldSessionId).isNotEqualTo(testSessionManager.perfSession().sessionId());
- }
-
- @Test
- public void
- testOnUpdateAppStateDoesntMakeGaugeManagerLogGaugeMetadataOnForegroundStateIfSessionIsNonVerbose() {
- forceNonVerboseSession();
-
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
-
- verify(mockGaugeManager, never())
- .logGaugeMetadata(
- anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class));
- }
-
- @Test
- public void
- testOnUpdateAppStateDoesntMakeGaugeManagerLogGaugeMetadataOnBackgroundStateEvenIfSessionIsVerbose() {
- forceVerboseSession();
-
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
-
- verify(mockGaugeManager, never())
- .logGaugeMetadata(
- anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class));
- }
-
- @Test
- public void testOnUpdateAppStateMakesGaugeManagerStartCollectingGaugesIfSessionIsVerbose() {
- forceVerboseSession();
-
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
-
- verify(mockGaugeManager)
- .startCollectingGauges(AdditionalMatchers.not(eq(mockPerfSession)), any());
- }
-
- // LogGaugeData on new perf session when Verbose
- // NotLogGaugeData on new perf session when not Verbose
- // Mark Session as expired after time limit.
-
- @Test
- public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesIfSessionIsNonVerbose() {
- forceNonVerboseSession();
-
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId"));
-
- verify(mockGaugeManager).stopCollectingGauges();
- }
-
- @Test
- public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesWhenSessionsDisabled() {
- forceSessionsFeatureDisabled();
-
- SessionManager testSessionManager =
- new SessionManager(
- mockGaugeManager, PerfSession.createWithId("testSessionId"), mockAppStateMonitor);
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2"));
-
- verify(mockGaugeManager).stopCollectingGauges();
+ inOrder.verify(mockGaugeManager).initializeGaugeMetadataManager(any(), ApplicationProcessState.FOREGROUND);
}
@Test
@@ -236,7 +120,7 @@ public void testPerfSessionExpiredMakesGaugeManagerStopsCollectingGaugesIfSessio
.thenReturn(TimeUnit.HOURS.toMicros(5)); // Default Max Session Length is 4 hours
assertThat(session.isSessionRunningTooLong()).isTrue();
- verify(mockGaugeManager, times(0)).logGaugeMetadata(any(), any());
+ verify(mockGaugeManager, times(0)).logGaugeMetadata(any());
}
@Test
@@ -247,7 +131,7 @@ public void testPerfSession_sessionAwareObjects_doesntNotifyIfNotRegistered() {
FakeSessionAwareObject spySessionAwareObjectOne = spy(new FakeSessionAwareObject());
FakeSessionAwareObject spySessionAwareObjectTwo = spy(new FakeSessionAwareObject());
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1"));
+ testSessionManager.updatePerfSession(PerfSession.createNewSession());
verify(spySessionAwareObjectOne, never())
.updateSession(ArgumentMatchers.nullable(PerfSession.class));
@@ -266,8 +150,8 @@ public void testPerfSession_sessionAwareObjects_NotifiesIfRegistered() {
testSessionManager.registerForSessionUpdates(new WeakReference<>(spySessionAwareObjectOne));
testSessionManager.registerForSessionUpdates(new WeakReference<>(spySessionAwareObjectTwo));
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1"));
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2"));
+ testSessionManager.updatePerfSession(PerfSession.createNewSession());
+ testSessionManager.updatePerfSession(PerfSession.createNewSession());
verify(spySessionAwareObjectOne, times(2))
.updateSession(ArgumentMatchers.nullable(PerfSession.class));
@@ -291,11 +175,11 @@ public void testPerfSession_sessionAwareObjects_DoesNotNotifyIfUnregistered() {
testSessionManager.registerForSessionUpdates(weakSpySessionAwareObjectOne);
testSessionManager.registerForSessionUpdates(weakSpySessionAwareObjectTwo);
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1"));
+ testSessionManager.updatePerfSession(PerfSession.createNewSession());
testSessionManager.unregisterForSessionUpdates(weakSpySessionAwareObjectOne);
testSessionManager.unregisterForSessionUpdates(weakSpySessionAwareObjectTwo);
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2"));
+ testSessionManager.updatePerfSession(PerfSession.createNewSession());
verify(spySessionAwareObjectOne, times(1))
.updateSession(ArgumentMatchers.nullable(PerfSession.class));
From 4dd97ac472e3aeaadff5f3c18455d3e5d06eb398 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Fri, 7 Feb 2025 11:21:35 -0500
Subject: [PATCH 035/146] Additional changes
---
.../google/firebase/perf/config/ConfigResolver.java | 6 +++---
.../session/FirebasePerformanceSessionSubscriber.kt | 11 ++++-------
.../firebase/perf/session/SessionManagerTest.java | 8 +++-----
3 files changed, 10 insertions(+), 15 deletions(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/config/ConfigResolver.java b/firebase-perf/src/main/java/com/google/firebase/perf/config/ConfigResolver.java
index 1ee9d395e03..7e321515141 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/config/ConfigResolver.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/config/ConfigResolver.java
@@ -116,7 +116,7 @@ public void setMetadataBundle(ImmutableBundle bundle) {
/** Default API to call for whether performance monitoring is currently silent. */
public boolean isPerformanceMonitoringEnabled() {
Boolean isPerformanceCollectionEnabled = getIsPerformanceCollectionEnabled();
- return (isPerformanceCollectionEnabled == null || isPerformanceCollectionEnabled == true)
+ return (isPerformanceCollectionEnabled == null || isPerformanceCollectionEnabled)
&& getIsServiceCollectionEnabled();
}
@@ -131,7 +131,7 @@ public Boolean getIsPerformanceCollectionEnabled() {
// return developer config.
// 4. Else, return null. Because Firebase Performance will read highlevel Firebase flag in this
// case.
- if (getIsPerformanceCollectionDeactivated()) {
+ if (Boolean.TRUE.equals(getIsPerformanceCollectionDeactivated())) {
// 1. If developer has deactivated Firebase Performance in Manifest, return false.
return false;
}
@@ -186,7 +186,7 @@ public void setIsPerformanceCollectionEnabled(Boolean isEnabled) {
// 2. Otherwise, save this configuration in device cache.
// If collection is deactivated, skip the action to save user configuration.
- if (getIsPerformanceCollectionDeactivated()) {
+ if (Boolean.TRUE.equals(getIsPerformanceCollectionDeactivated())) {
return;
}
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt
index 58a34bb5e98..f9f6e257c73 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt
@@ -1,17 +1,16 @@
package com.google.firebase.perf.session
-import com.google.firebase.perf.config.ConfigResolver
import com.google.firebase.perf.logging.AndroidLogger
import com.google.firebase.perf.session.gauges.GaugeManager
import com.google.firebase.sessions.api.SessionSubscriber
-class FirebasePerformanceSessionSubscriber(private val dataCollectionEnabled: Boolean) :
- SessionSubscriber {
+class FirebasePerformanceSessionSubscriber() : SessionSubscriber {
private val perfSessionToAqs: MutableMap =
mutableMapOf()
+ // TODO(b/394127311): Identify a way to ste this value after ConfigResolver has relevant metadata.
override val isDataCollectionEnabled: Boolean
- get() = dataCollectionEnabled
+ get() = true
override val sessionSubscriberName: SessionSubscriber.Name
get() = SessionSubscriber.Name.PERFORMANCE
@@ -51,9 +50,7 @@ class FirebasePerformanceSessionSubscriber(private val dataCollectionEnabled: Bo
companion object {
val instance: FirebasePerformanceSessionSubscriber by lazy {
- FirebasePerformanceSessionSubscriber(
- ConfigResolver.getInstance().isPerformanceMonitoringEnabled
- )
+ FirebasePerformanceSessionSubscriber()
}
}
}
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
index 16a2cf307c7..645f5482777 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
@@ -16,9 +16,6 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
@@ -40,7 +37,6 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.AdditionalMatchers;
import org.mockito.ArgumentMatchers;
import org.mockito.InOrder;
import org.mockito.Mock;
@@ -83,7 +79,9 @@ public void setApplicationContext_initializeGaugeMetadataManager()
testSessionManager.setApplicationContext(mockApplicationContext);
testSessionManager.getSyncInitFuture().get();
- inOrder.verify(mockGaugeManager).initializeGaugeMetadataManager(any(), ApplicationProcessState.FOREGROUND);
+ inOrder
+ .verify(mockGaugeManager)
+ .initializeGaugeMetadataManager(any(), ApplicationProcessState.FOREGROUND);
}
@Test
From 49f00082c40c115178ead5fc47c412011c9ca6e0 Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Fri, 7 Feb 2025 14:58:37 -0500
Subject: [PATCH 036/146] Remove the logging of GaugeMetadata to allow using
AQS (#6678)
Based on the behaviour of AQS w/ Fireperf, an AQS session isn't
available when (currently) logging gauge metadata.
Changes:
- Remove the current logging of gauge metadata - will be re-introduced
in a future PR.
- Switch Gauge collection from `scheduleAtFixedRate` to
`scheduleWithFixedDelay`. As
[documented](https://stackoverflow.com/a/78405653), this *should*
prevent a potentially large amounts of gauge collection if a process is
cached, and then restored during a verbose session - which *should* make
it work better w/ AQS.
- Remove API restricted behaviour which is no longer relevant.
---
.../firebase/perf/FirebasePerformance.java | 1 -
.../firebase/perf/session/SessionManager.java | 17 -----
.../session/gauges/CpuGaugeCollector.java | 13 +---
.../perf/session/gauges/GaugeManager.java | 9 +--
.../session/gauges/GaugeMetadataManager.java | 34 +---------
.../session/gauges/MemoryGaugeCollector.java | 6 +-
.../perf/session/SessionManagerTest.java | 58 +---------------
.../gauges/GaugeMetadataManagerTest.java | 66 +------------------
8 files changed, 14 insertions(+), 190 deletions(-)
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
index 40468566225..3cc49896ce0 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
@@ -182,7 +182,6 @@ public static FirebasePerformance getInstance() {
.initialize(firebaseApp, firebaseInstallationsApi, transportFactoryProvider);
Context appContext = firebaseApp.getApplicationContext();
- // TODO(b/110178816): Explore moving off of main thread.
mMetadataBundle = extractMetadata(appContext);
remoteConfigManager.setFirebaseRemoteConfigProvider(firebaseRemoteConfigProvider);
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
index 79d034b9b0b..29ffb988ba0 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
@@ -79,9 +79,6 @@ public SessionManager(
* (currently that is before onResume finishes) to ensure gauge collection starts on time.
*/
public void setApplicationContext(final Context appContext) {
- // Get PerfSession in main thread first, because it is possible that app changes fg/bg state
- // which creates a new perfSession, before the following is executed in background thread
- final PerfSession appStartSession = perfSession;
// TODO(b/258263016): Migrate to go/firebase-android-executors
@SuppressLint("ThreadPoolCreation")
ExecutorService executorService = Executors.newSingleThreadExecutor();
@@ -89,10 +86,6 @@ public void setApplicationContext(final Context appContext) {
executorService.submit(
() -> {
gaugeManager.initializeGaugeMetadataManager(appContext);
- if (appStartSession.isGaugeAndEventCollectionEnabled()) {
- gaugeManager.logGaugeMetadata(
- appStartSession.sessionId(), ApplicationProcessState.FOREGROUND);
- }
});
}
@@ -164,9 +157,6 @@ public void updatePerfSession(PerfSession perfSession) {
}
}
- // Log the gauge metadata event if data collection is enabled.
- logGaugeMetadataIfCollectionEnabled(appStateMonitor.getAppState());
-
// Start of stop the gauge data collection.
startOrStopCollectingGauges(appStateMonitor.getAppState());
}
@@ -178,7 +168,6 @@ public void updatePerfSession(PerfSession perfSession) {
* this does not reset the perfSession.
*/
public void initializeGaugeCollection() {
- logGaugeMetadataIfCollectionEnabled(ApplicationProcessState.FOREGROUND);
startOrStopCollectingGauges(ApplicationProcessState.FOREGROUND);
}
@@ -206,12 +195,6 @@ public void unregisterForSessionUpdates(WeakReference client
}
}
- private void logGaugeMetadataIfCollectionEnabled(ApplicationProcessState appState) {
- if (perfSession.isGaugeAndEventCollectionEnabled()) {
- gaugeManager.logGaugeMetadata(perfSession.sessionId(), appState);
- }
- }
-
private void startOrStopCollectingGauges(ApplicationProcessState appState) {
if (perfSession.isGaugeAndEventCollectionEnabled()) {
gaugeManager.startCollectingGauges(perfSession, appState);
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/CpuGaugeCollector.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/CpuGaugeCollector.java
index e33d363c0aa..ceb636d56b3 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/CpuGaugeCollector.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/CpuGaugeCollector.java
@@ -17,8 +17,6 @@
import static android.system.Os.sysconf;
import android.annotation.SuppressLint;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
import android.system.OsConstants;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@@ -163,7 +161,7 @@ private synchronized void scheduleCpuMetricCollectionWithRate(
this.cpuMetricCollectionRateMs = cpuMetricCollectionRate;
try {
cpuMetricCollectorJob =
- cpuMetricCollectorExecutor.scheduleAtFixedRate(
+ cpuMetricCollectorExecutor.scheduleWithFixedDelay(
() -> {
CpuMetricReading currCpuReading = syncCollectCpuMetric(referenceTime);
if (currCpuReading != null) {
@@ -181,7 +179,7 @@ private synchronized void scheduleCpuMetricCollectionWithRate(
private synchronized void scheduleCpuMetricCollectionOnce(Timer referenceTime) {
try {
@SuppressWarnings("FutureReturnValueIgnored")
- ScheduledFuture unusedFuture =
+ ScheduledFuture> unusedFuture =
cpuMetricCollectorExecutor.schedule(
() -> {
CpuMetricReading currCpuReading = syncCollectCpuMetric(referenceTime);
@@ -227,12 +225,7 @@ private CpuMetricReading syncCollectCpuMetric(Timer referenceTime) {
}
private long getClockTicksPerSecond() {
- if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
- return sysconf(OsConstants._SC_CLK_TCK);
- } else {
- // TODO(b/110779408): Figure out how to collect this info for Android API 20 and below.
- return INVALID_SC_PER_CPU_CLOCK_TICK;
- }
+ return sysconf(OsConstants._SC_CLK_TCK);
}
private long convertClockTicksToMicroseconds(long clockTicks) {
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
index 7f6182a9c15..30da2f0160f 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
@@ -72,8 +72,8 @@ private GaugeManager() {
TransportManager.getInstance(),
ConfigResolver.getInstance(),
null,
- new Lazy<>(() -> new CpuGaugeCollector()),
- new Lazy<>(() -> new MemoryGaugeCollector()));
+ new Lazy<>(CpuGaugeCollector::new),
+ new Lazy<>(MemoryGaugeCollector::new));
}
@VisibleForTesting
@@ -81,7 +81,7 @@ private GaugeManager() {
Lazy gaugeManagerExecutor,
TransportManager transportManager,
ConfigResolver configResolver,
- GaugeMetadataManager gaugeMetadataManager,
+ @Nullable GaugeMetadataManager gaugeMetadataManager,
Lazy cpuGaugeCollector,
Lazy memoryGaugeCollector) {
@@ -140,7 +140,7 @@ public void startCollectingGauges(
gaugeManagerDataCollectionJob =
gaugeManagerExecutor
.get()
- .scheduleAtFixedRate(
+ .scheduleWithFixedDelay(
() -> {
syncFlush(sessionIdForScheduledTask, applicationProcessStateForScheduledTask);
},
@@ -256,6 +256,7 @@ private void syncFlush(String sessionId, ApplicationProcessState appState) {
* @return true if GaugeMetadata was logged, false otherwise.
*/
public boolean logGaugeMetadata(String sessionId, ApplicationProcessState appState) {
+ // TODO(b/394127311): Re-introduce logging of metadata for AQS.
if (gaugeMetadataManager != null) {
GaugeMetric gaugeMetric =
GaugeMetric.newBuilder()
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeMetadataManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeMetadataManager.java
index 6b4466dfc35..ed38dd8f38d 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeMetadataManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeMetadataManager.java
@@ -17,18 +17,11 @@
import android.app.ActivityManager;
import android.app.ActivityManager.MemoryInfo;
import android.content.Context;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
import androidx.annotation.VisibleForTesting;
import com.google.firebase.perf.logging.AndroidLogger;
import com.google.firebase.perf.util.StorageUnit;
import com.google.firebase.perf.util.Utils;
import com.google.firebase.perf.v1.GaugeMetadata;
-import java.io.BufferedReader;
-import java.io.FileReader;
-import java.io.IOException;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
/**
* The {@code GaugeMetadataManager} class is responsible for collecting {@link GaugeMetadata}
@@ -41,7 +34,6 @@ class GaugeMetadataManager {
private final Runtime runtime;
private final ActivityManager activityManager;
private final MemoryInfo memoryInfo;
- private final Context appContext;
GaugeMetadataManager(Context appContext) {
this(Runtime.getRuntime(), appContext);
@@ -50,7 +42,6 @@ class GaugeMetadataManager {
@VisibleForTesting
GaugeMetadataManager(Runtime runtime, Context appContext) {
this.runtime = runtime;
- this.appContext = appContext;
this.activityManager = (ActivityManager) appContext.getSystemService(Context.ACTIVITY_SERVICE);
memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);
@@ -75,29 +66,6 @@ public int getMaxEncouragedAppJavaHeapMemoryKb() {
/** Returns the total memory (in kilobytes) accessible by the kernel (called the RAM size). */
public int getDeviceRamSizeKb() {
- if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
- return Utils.saturatedIntCast(StorageUnit.BYTES.toKilobytes(memoryInfo.totalMem));
- }
-
- return readTotalRAM(/* procFileName= */ "/proc/meminfo");
- }
-
- /** Returns the total ram size of the device (in kilobytes) by reading the "proc/meminfo" file. */
- @VisibleForTesting
- int readTotalRAM(String procFileName) {
- try (BufferedReader br = new BufferedReader(new FileReader(procFileName))) {
- for (String s = br.readLine(); s != null; s = br.readLine()) {
- if (s.startsWith("MemTotal")) {
- Matcher m = Pattern.compile("\\d+").matcher(s);
- return m.find() ? Integer.parseInt(m.group()) : 0;
- }
- }
- } catch (IOException ioe) {
- logger.warn("Unable to read '" + procFileName + "' file: " + ioe.getMessage());
- } catch (NumberFormatException nfe) {
- logger.warn("Unable to parse '" + procFileName + "' file: " + nfe.getMessage());
- }
-
- return 0;
+ return Utils.saturatedIntCast(StorageUnit.BYTES.toKilobytes(memoryInfo.totalMem));
}
}
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollector.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollector.java
index eeaf4eb7c80..a7b4b40002a 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollector.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollector.java
@@ -50,7 +50,7 @@ public class MemoryGaugeCollector {
public final ConcurrentLinkedQueue memoryMetricReadings;
private final Runtime runtime;
- @Nullable private ScheduledFuture memoryMetricCollectorJob = null;
+ @Nullable private ScheduledFuture> memoryMetricCollectorJob = null;
private long memoryMetricCollectionRateMs = UNSET_MEMORY_METRIC_COLLECTION_RATE;
// TODO(b/258263016): Migrate to go/firebase-android-executors
@@ -124,7 +124,7 @@ private synchronized void scheduleMemoryMetricCollectionWithRate(
try {
memoryMetricCollectorJob =
- memoryMetricCollectorExecutor.scheduleAtFixedRate(
+ memoryMetricCollectorExecutor.scheduleWithFixedDelay(
() -> {
AndroidMemoryReading memoryReading = syncCollectMemoryMetric(referenceTime);
if (memoryReading != null) {
@@ -142,7 +142,7 @@ private synchronized void scheduleMemoryMetricCollectionWithRate(
private synchronized void scheduleMemoryMetricCollectionOnce(Timer referenceTime) {
try {
@SuppressWarnings("FutureReturnValueIgnored")
- ScheduledFuture unusedFuture =
+ ScheduledFuture> unusedFuture =
memoryMetricCollectorExecutor.schedule(
() -> {
AndroidMemoryReading memoryReading = syncCollectMemoryMetric(referenceTime);
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
index f3e3795f3f8..37b9ff7215b 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
@@ -74,7 +74,7 @@ public void testInstanceCreation() {
}
@Test
- public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsInitialized()
+ public void setApplicationContext_initializeGaugeMetadataManager()
throws ExecutionException, InterruptedException {
when(mockPerfSession.isGaugeAndEventCollectionEnabled()).thenReturn(true);
InOrder inOrder = Mockito.inOrder(mockGaugeManager);
@@ -84,7 +84,6 @@ public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsIn
testSessionManager.getSyncInitFuture().get();
inOrder.verify(mockGaugeManager).initializeGaugeMetadataManager(any());
- inOrder.verify(mockGaugeManager).logGaugeMetadata(any(), any());
}
@Test
@@ -136,20 +135,6 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess
assertThat(oldSessionId).isNotEqualTo(testSessionManager.perfSession().sessionId());
}
- @Test
- public void
- testOnUpdateAppStateMakesGaugeManagerLogGaugeMetadataOnForegroundStateIfSessionIsVerbose() {
- forceVerboseSession();
-
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
-
- verify(mockGaugeManager)
- .logGaugeMetadata(
- anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class));
- }
-
@Test
public void
testOnUpdateAppStateDoesntMakeGaugeManagerLogGaugeMetadataOnForegroundStateIfSessionIsNonVerbose() {
@@ -178,21 +163,6 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess
anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class));
}
- @Test
- public void
- testOnUpdateAppStateMakesGaugeManagerLogGaugeMetadataOnBackgroundAppStateIfSessionIsVerboseAndTimedOut() {
- when(mockPerfSession.isSessionRunningTooLong()).thenReturn(true);
- forceVerboseSession();
-
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
-
- verify(mockGaugeManager)
- .logGaugeMetadata(
- anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class));
- }
-
@Test
public void testOnUpdateAppStateMakesGaugeManagerStartCollectingGaugesIfSessionIsVerbose() {
forceVerboseSession();
@@ -232,32 +202,6 @@ public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesWhenSession
verify(mockGaugeManager).stopCollectingGauges();
}
- @Test
- public void testGaugeMetadataIsFlushedOnlyWhenNewVerboseSessionIsCreated() {
- when(mockPerfSession.isSessionRunningTooLong()).thenReturn(false);
-
- // Start with a non verbose session
- forceNonVerboseSession();
- SessionManager testSessionManager =
- new SessionManager(
- mockGaugeManager, PerfSession.createWithId("testSessionId1"), mockAppStateMonitor);
-
- verify(mockGaugeManager, times(0))
- .logGaugeMetadata(
- eq("testSessionId1"),
- eq(com.google.firebase.perf.v1.ApplicationProcessState.FOREGROUND));
-
- // Forcing a verbose session will enable Gauge collection
- forceVerboseSession();
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2"));
- verify(mockGaugeManager, times(1)).logGaugeMetadata(eq("testSessionId2"), any());
-
- // Force a non-verbose session and verify if we are not logging metadata
- forceVerboseSession();
- testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId3"));
- verify(mockGaugeManager, times(1)).logGaugeMetadata(eq("testSessionId3"), any());
- }
-
@Test
public void testSessionIdDoesNotUpdateIfPerfSessionRunsTooLong() {
Timer mockTimer = mock(Timer.class);
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeMetadataManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeMetadataManagerTest.java
index 292747121dd..1592f77e5ad 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeMetadataManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeMetadataManagerTest.java
@@ -15,26 +15,20 @@
package com.google.firebase.perf.session.gauges;
import static com.google.common.truth.Truth.assertThat;
-import static java.nio.charset.StandardCharsets.UTF_8;
import static org.mockito.MockitoAnnotations.initMocks;
import static org.robolectric.Shadows.shadowOf;
import android.app.ActivityManager;
import android.content.Context;
-import android.os.Environment;
import androidx.test.core.app.ApplicationProvider;
import com.google.firebase.perf.FirebasePerformanceTestBase;
import com.google.firebase.perf.util.StorageUnit;
-import java.io.File;
import java.io.IOException;
-import java.io.Writer;
-import java.nio.file.Files;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.robolectric.RobolectricTestRunner;
-import org.robolectric.shadows.ShadowEnvironment;
/** Unit tests for {@link com.google.firebase.perf.session.gauges.GaugeMetadataManager} */
@RunWith(RobolectricTestRunner.class)
@@ -49,12 +43,11 @@ public class GaugeMetadataManagerTest extends FirebasePerformanceTestBase {
@Mock private Runtime runtime;
private ActivityManager activityManager;
- private Context appContext;
@Before
public void setUp() {
initMocks(this);
- appContext = ApplicationProvider.getApplicationContext();
+ Context appContext = ApplicationProvider.getApplicationContext();
activityManager = (ActivityManager) appContext.getSystemService(Context.ACTIVITY_SERVICE);
mockMemory();
@@ -90,62 +83,5 @@ public void testGetDeviceRamSize_returnsExpectedValue() throws IOException {
int ramSize = testGaugeMetadataManager.getDeviceRamSizeKb();
assertThat(ramSize).isEqualTo(StorageUnit.BYTES.toKilobytes(DEVICE_RAM_SIZE_BYTES));
- assertThat(ramSize).isEqualTo(testGaugeMetadataManager.readTotalRAM(createFakeMemInfoFile()));
}
-
- /** @return The file path of this fake file which can be used to read the file. */
- private String createFakeMemInfoFile() throws IOException {
- // Due to file permission issues on forge, it's easiest to just write this file to the emulated
- // robolectric external storage.
- ShadowEnvironment.setExternalStorageState(Environment.MEDIA_MOUNTED);
-
- File file = new File(Environment.getExternalStorageDirectory(), "FakeProcMemInfoFile");
- Writer fileWriter;
-
- fileWriter = Files.newBufferedWriter(file.toPath(), UTF_8);
- fileWriter.write(MEM_INFO_CONTENTS);
- fileWriter.close();
-
- return file.getAbsolutePath();
- }
-
- private static final String MEM_INFO_CONTENTS =
- "MemTotal: "
- + DEVICE_RAM_SIZE_KB
- + " kB\n"
- + "MemFree: 542404 kB\n"
- + "MemAvailable: 1392324 kB\n"
- + "Buffers: 64292 kB\n"
- + "Cached: 826180 kB\n"
- + "SwapCached: 4196 kB\n"
- + "Active: 934768 kB\n"
- + "Inactive: 743812 kB\n"
- + "Active(anon): 582132 kB\n"
- + "Inactive(anon): 241500 kB\n"
- + "Active(file): 352636 kB\n"
- + "Inactive(file): 502312 kB\n"
- + "Unevictable: 5148 kB\n"
- + "Mlocked: 256 kB\n"
- + "SwapTotal: 524284 kB\n"
- + "SwapFree: 484800 kB\n"
- + "Dirty: 4 kB\n"
- + "Writeback: 0 kB\n"
- + "AnonPages: 789404 kB\n"
- + "Mapped: 241928 kB\n"
- + "Shmem: 30632 kB\n"
- + "Slab: 122320 kB\n"
- + "SReclaimable: 42552 kB\n"
- + "SUnreclaim: 79768 kB\n"
- + "KernelStack: 22816 kB\n"
- + "PageTables: 35344 kB\n"
- + "NFS_Unstable: 0 kB\n"
- + "Bounce: 0 kB\n"
- + "WritebackTmp: 0 kB\n"
- + "CommitLimit: 2042280 kB\n"
- + "Committed_AS: 76623352 kB\n"
- + "VmallocTotal: 251658176 kB\n"
- + "VmallocUsed: 232060 kB\n"
- + "VmallocChunk: 251347444 kB\n"
- + "NvMapMemFree: 48640 kB\n"
- + "NvMapMemUsed: 471460 kB\n";
}
From c5e9b9b6ffb91245ac7d25846813bbcc1428fb3e Mon Sep 17 00:00:00 2001
From: Tejas Deshpande
Date: Tue, 11 Feb 2025 13:06:38 -0500
Subject: [PATCH 037/146] Implement a SessionSubscriber for Firebase
Performance (#6683)
This PR doesn't change the use of session ID to AQS - except in
GaugeMetadata. I've added TODOs to identify the missing locations.
---
firebase-perf/firebase-perf.gradle | 2 +-
.../firebase/perf/FirebasePerfRegistrar.java | 7 +
.../firebase/perf/FirebasePerformance.java | 17 +--
.../FirebasePerformanceSessionSubscriber.kt | 46 +++++++
.../firebase/perf/session/PerfSession.java | 18 ++-
.../firebase/perf/session/SessionManager.java | 53 +-------
.../perf/session/gauges/GaugeManager.java | 10 +-
.../perf/transport/TransportManager.java | 1 +
.../perf/session/SessionManagerTest.java | 121 +++---------------
.../api/FirebaseSessionsDependencies.kt | 13 --
10 files changed, 103 insertions(+), 185 deletions(-)
create mode 100644 firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt
diff --git a/firebase-perf/firebase-perf.gradle b/firebase-perf/firebase-perf.gradle
index c0fd6df6056..49c921edeb0 100644
--- a/firebase-perf/firebase-perf.gradle
+++ b/firebase-perf/firebase-perf.gradle
@@ -118,7 +118,7 @@ dependencies {
api("com.google.firebase:firebase-components:18.0.0")
api("com.google.firebase:firebase-config:21.5.0")
api("com.google.firebase:firebase-installations:17.2.0")
- api("com.google.firebase:firebase-sessions:2.0.7") {
+ api(project(":firebase-sessions")) {
exclude group: 'com.google.firebase', module: 'firebase-common'
exclude group: 'com.google.firebase', module: 'firebase-common-ktx'
exclude group: 'com.google.firebase', module: 'firebase-components'
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java
index c01f035af1f..daffc2de81a 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java
@@ -30,6 +30,8 @@
import com.google.firebase.perf.injection.modules.FirebasePerformanceModule;
import com.google.firebase.platforminfo.LibraryVersionComponent;
import com.google.firebase.remoteconfig.RemoteConfigComponent;
+import com.google.firebase.sessions.api.FirebaseSessionsDependencies;
+import com.google.firebase.sessions.api.SessionSubscriber;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
@@ -47,6 +49,11 @@ public class FirebasePerfRegistrar implements ComponentRegistrar {
private static final String LIBRARY_NAME = "fire-perf";
private static final String EARLY_LIBRARY_NAME = "fire-perf-early";
+ static {
+ // Add Firebase Performance as a dependency of Sessions when this class is loaded into memory.
+ FirebaseSessionsDependencies.addDependency(SessionSubscriber.Name.PERFORMANCE);
+ }
+
@Override
@Keep
public List> getComponents() {
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
index 3cc49896ce0..e4ddfcd600c 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java
@@ -36,12 +36,14 @@
import com.google.firebase.perf.logging.ConsoleUrlGenerator;
import com.google.firebase.perf.metrics.HttpMetric;
import com.google.firebase.perf.metrics.Trace;
+import com.google.firebase.perf.session.FirebasePerformanceSessionSubscriber;
import com.google.firebase.perf.session.SessionManager;
import com.google.firebase.perf.transport.TransportManager;
import com.google.firebase.perf.util.Constants;
import com.google.firebase.perf.util.ImmutableBundle;
import com.google.firebase.perf.util.Timer;
import com.google.firebase.remoteconfig.RemoteConfigComponent;
+import com.google.firebase.sessions.api.FirebaseSessionsDependencies;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.URL;
@@ -136,11 +138,6 @@ public static FirebasePerformance getInstance() {
// to false if it's been force disabled or it is set to null if neither.
@Nullable private Boolean mPerformanceCollectionForceEnabledState = null;
- private final FirebaseApp firebaseApp;
- private final Provider firebaseRemoteConfigProvider;
- private final FirebaseInstallationsApi firebaseInstallationsApi;
- private final Provider transportFactoryProvider;
-
/**
* Constructs the FirebasePerformance class and allows injecting dependencies.
*
@@ -166,11 +163,6 @@ public static FirebasePerformance getInstance() {
ConfigResolver configResolver,
SessionManager sessionManager) {
- this.firebaseApp = firebaseApp;
- this.firebaseRemoteConfigProvider = firebaseRemoteConfigProvider;
- this.firebaseInstallationsApi = firebaseInstallationsApi;
- this.transportFactoryProvider = transportFactoryProvider;
-
if (firebaseApp == null) {
this.mPerformanceCollectionForceEnabledState = false;
this.configResolver = configResolver;
@@ -191,6 +183,9 @@ public static FirebasePerformance getInstance() {
sessionManager.setApplicationContext(appContext);
mPerformanceCollectionForceEnabledState = configResolver.getIsPerformanceCollectionEnabled();
+ FirebaseSessionsDependencies.register(
+ new FirebasePerformanceSessionSubscriber(isPerformanceCollectionEnabled()));
+
if (logger.isLogcatEnabled() && isPerformanceCollectionEnabled()) {
logger.info(
String.format(
@@ -281,7 +276,7 @@ public synchronized void setPerformanceCollectionEnabled(@Nullable Boolean enabl
return;
}
- if (configResolver.getIsPerformanceCollectionDeactivated()) {
+ if (Boolean.TRUE.equals(configResolver.getIsPerformanceCollectionDeactivated())) {
logger.info("Firebase Performance is permanently disabled");
return;
}
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt
new file mode 100644
index 00000000000..b6a3d30c139
--- /dev/null
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2025 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.perf.session
+
+import com.google.firebase.perf.session.gauges.GaugeManager
+import com.google.firebase.perf.v1.ApplicationProcessState
+import com.google.firebase.sessions.api.SessionSubscriber
+import java.util.UUID
+
+class FirebasePerformanceSessionSubscriber(override val isDataCollectionEnabled: Boolean) :
+ SessionSubscriber {
+
+ override val sessionSubscriberName: SessionSubscriber.Name = SessionSubscriber.Name.PERFORMANCE
+
+ override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) {
+ val currentPerfSession = SessionManager.getInstance().perfSession()
+
+ // A [PerfSession] was created before a session was started.
+ if (currentPerfSession.aqsSessionId() == null) {
+ currentPerfSession.setAQSId(sessionDetails)
+ GaugeManager.getInstance()
+ .logGaugeMetadata(currentPerfSession.aqsSessionId(), ApplicationProcessState.FOREGROUND)
+ return
+ }
+
+ val updatedSession = PerfSession.createWithId(UUID.randomUUID().toString())
+ updatedSession.setAQSId(sessionDetails)
+ SessionManager.getInstance().updatePerfSession(updatedSession)
+ GaugeManager.getInstance()
+ .logGaugeMetadata(updatedSession.aqsSessionId(), ApplicationProcessState.FOREGROUND)
+ }
+}
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
index 160a4507560..075848ab747 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java
@@ -23,6 +23,7 @@
import com.google.firebase.perf.util.Clock;
import com.google.firebase.perf.util.Timer;
import com.google.firebase.perf.v1.SessionVerbosity;
+import com.google.firebase.sessions.api.SessionSubscriber;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -31,6 +32,7 @@ public class PerfSession implements Parcelable {
private final String sessionId;
private final Timer creationTime;
+ @Nullable private String aqsSessionId;
private boolean isGaugeAndEventCollectionEnabled = false;
@@ -59,11 +61,24 @@ private PerfSession(@NonNull Parcel in) {
creationTime = in.readParcelable(Timer.class.getClassLoader());
}
- /** Returns the sessionId of the object. */
+ /** Returns the sessionId of the session. */
public String sessionId() {
return sessionId;
}
+ /** Returns the AQS sessionId for the given session. */
+ @Nullable
+ public String aqsSessionId() {
+ return aqsSessionId;
+ }
+
+ /** Sets the AQS sessionId for the given session. */
+ public void setAQSId(SessionSubscriber.SessionDetails aqs) {
+ if (aqsSessionId == null) {
+ aqsSessionId = aqs.getSessionId();
+ }
+ }
+
/**
* Returns a timer object that has been seeded with the system time at which the session began.
*/
@@ -113,6 +128,7 @@ public boolean isSessionRunningTooLong() {
/** Creates and returns the proto object for PerfSession object. */
public com.google.firebase.perf.v1.PerfSession build() {
+ // TODO(b/394127311): Switch to using AQS.
com.google.firebase.perf.v1.PerfSession.Builder sessionMetric =
com.google.firebase.perf.v1.PerfSession.newBuilder().setSessionId(sessionId);
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
index 29ffb988ba0..cf99c1e52ea 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java
@@ -19,7 +19,6 @@
import androidx.annotation.Keep;
import androidx.annotation.VisibleForTesting;
import com.google.firebase.perf.application.AppStateMonitor;
-import com.google.firebase.perf.application.AppStateUpdateHandler;
import com.google.firebase.perf.session.gauges.GaugeManager;
import com.google.firebase.perf.v1.ApplicationProcessState;
import com.google.firebase.perf.v1.GaugeMetadata;
@@ -27,15 +26,13 @@
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.Objects;
import java.util.Set;
import java.util.UUID;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
/** Session manager to generate sessionIDs and broadcast to the application. */
@Keep // Needed because of b/117526359.
-public class SessionManager extends AppStateUpdateHandler {
+public class SessionManager {
@SuppressLint("StaticFieldLeak")
private static final SessionManager instance = new SessionManager();
@@ -45,7 +42,6 @@ public class SessionManager extends AppStateUpdateHandler {
private final Set> clients = new HashSet<>();
private PerfSession perfSession;
- private Future syncInitFuture;
/** Returns the singleton instance of SessionManager. */
public static SessionManager getInstance() {
@@ -71,7 +67,6 @@ public SessionManager(
this.gaugeManager = gaugeManager;
this.perfSession = perfSession;
this.appStateMonitor = appStateMonitor;
- registerForAppState();
}
/**
@@ -79,42 +74,7 @@ public SessionManager(
* (currently that is before onResume finishes) to ensure gauge collection starts on time.
*/
public void setApplicationContext(final Context appContext) {
- // TODO(b/258263016): Migrate to go/firebase-android-executors
- @SuppressLint("ThreadPoolCreation")
- ExecutorService executorService = Executors.newSingleThreadExecutor();
- syncInitFuture =
- executorService.submit(
- () -> {
- gaugeManager.initializeGaugeMetadataManager(appContext);
- });
- }
-
- @Override
- public void onUpdateAppState(ApplicationProcessState newAppState) {
- super.onUpdateAppState(newAppState);
-
- if (appStateMonitor.isColdStart()) {
- // We want the Session to remain unchanged if this is a cold start of the app since we already
- // update the PerfSession in FirebasePerfProvider#onAttachInfo().
- return;
- }
-
- if (newAppState == ApplicationProcessState.FOREGROUND) {
- // A new foregrounding of app will force a new sessionID generation.
- PerfSession session = PerfSession.createWithId(UUID.randomUUID().toString());
- updatePerfSession(session);
- } else {
- // If the session is running for too long, generate a new session and collect gauges as
- // necessary.
- if (perfSession.isSessionRunningTooLong()) {
- PerfSession session = PerfSession.createWithId(UUID.randomUUID().toString());
- updatePerfSession(session);
- } else {
- // For any other state change of the application, modify gauge collection state as
- // necessary.
- startOrStopCollectingGauges(newAppState);
- }
- }
+ gaugeManager.initializeGaugeMetadataManager(appContext);
}
/**
@@ -138,7 +98,7 @@ public void stopGaugeCollectionIfSessionRunningTooLong() {
*/
public void updatePerfSession(PerfSession perfSession) {
// Do not update the perf session if it is the exact same sessionId.
- if (perfSession.sessionId() == this.perfSession.sessionId()) {
+ if (Objects.equals(perfSession.sessionId(), this.perfSession.sessionId())) {
return;
}
@@ -207,9 +167,4 @@ private void startOrStopCollectingGauges(ApplicationProcessState appState) {
public void setPerfSession(PerfSession perfSession) {
this.perfSession = perfSession;
}
-
- @VisibleForTesting
- public Future getSyncInitFuture() {
- return this.syncInitFuture;
- }
}
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
index 30da2f0160f..1c06ceac9dd 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java
@@ -136,6 +136,7 @@ public void startCollectingGauges(
final String sessionIdForScheduledTask = sessionId;
final ApplicationProcessState applicationProcessStateForScheduledTask = applicationProcessState;
+ // TODO(b/394127311): Switch to using AQS.
try {
gaugeManagerDataCollectionJob =
gaugeManagerExecutor
@@ -204,6 +205,7 @@ public void stopCollectingGauges() {
gaugeManagerDataCollectionJob.cancel(false);
}
+ // TODO(b/394127311): Switch to using AQS.
// Flush any data that was collected for this session one last time.
@SuppressWarnings("FutureReturnValueIgnored")
ScheduledFuture unusedFuture =
@@ -242,6 +244,7 @@ private void syncFlush(String sessionId, ApplicationProcessState appState) {
}
// Adding Session ID info.
+ // TODO(b/394127311): Switch to using AQS.
gaugeMetricBuilder.setSessionId(sessionId);
transportManager.log(gaugeMetricBuilder.build(), appState);
@@ -250,17 +253,16 @@ private void syncFlush(String sessionId, ApplicationProcessState appState) {
/**
* Log the Gauge Metadata information to the transport.
*
- * @param sessionId The {@link PerfSession#sessionId()} to which the collected Gauge Metrics
+ * @param aqsSessionId The {@link PerfSession#aqsSessionId()} ()} to which the collected Gauge Metrics
* should be associated with.
* @param appState The {@link ApplicationProcessState} for which these gauges are collected.
* @return true if GaugeMetadata was logged, false otherwise.
*/
- public boolean logGaugeMetadata(String sessionId, ApplicationProcessState appState) {
- // TODO(b/394127311): Re-introduce logging of metadata for AQS.
+ public boolean logGaugeMetadata(String aqsSessionId, ApplicationProcessState appState) {
if (gaugeMetadataManager != null) {
GaugeMetric gaugeMetric =
GaugeMetric.newBuilder()
- .setSessionId(sessionId)
+ .setSessionId(aqsSessionId)
.setGaugeMetadata(getGaugeMetadata())
.build();
transportManager.log(gaugeMetric, appState);
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/transport/TransportManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/transport/TransportManager.java
index 9600b099a6d..159af53d3d3 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/transport/TransportManager.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/transport/TransportManager.java
@@ -354,6 +354,7 @@ public void log(final GaugeMetric gaugeMetric) {
* {@link #isAllowedToDispatch(PerfMetric)}).
*/
public void log(final GaugeMetric gaugeMetric, final ApplicationProcessState appState) {
+ // TODO(b/394127311): This *might* potentially be the right place to get AQS.
executorService.execute(
() -> syncLog(PerfMetric.newBuilder().setGaugeMetric(gaugeMetric), appState));
}
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
index 37b9ff7215b..954b0ae88d3 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java
@@ -16,9 +16,6 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
@@ -40,7 +37,6 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.AdditionalMatchers;
import org.mockito.ArgumentMatchers;
import org.mockito.InOrder;
import org.mockito.Mock;
@@ -82,105 +78,15 @@ public void setApplicationContext_initializeGaugeMetadataManager()
new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
testSessionManager.setApplicationContext(mockApplicationContext);
- testSessionManager.getSyncInitFuture().get();
inOrder.verify(mockGaugeManager).initializeGaugeMetadataManager(any());
}
- @Test
- public void testOnUpdateAppStateDoesNothingDuringAppStart() {
- String oldSessionId = SessionManager.getInstance().perfSession().sessionId();
-
- assertThat(oldSessionId).isNotNull();
- assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
-
- AppStateMonitor.getInstance().setIsColdStart(true);
-
- SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND);
- assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
- }
-
- @Test
- public void testOnUpdateAppStateGeneratesNewSessionIdOnForegroundState() {
- String oldSessionId = SessionManager.getInstance().perfSession().sessionId();
-
- assertThat(oldSessionId).isNotNull();
- assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
-
- SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND);
- assertThat(oldSessionId).isNotEqualTo(SessionManager.getInstance().perfSession().sessionId());
- }
-
- @Test
- public void testOnUpdateAppStateDoesntGenerateNewSessionIdOnBackgroundState() {
- String oldSessionId = SessionManager.getInstance().perfSession().sessionId();
-
- assertThat(oldSessionId).isNotNull();
- assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
-
- SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.BACKGROUND);
- assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId());
- }
-
- @Test
- public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSessionExpires() {
- when(mockPerfSession.isSessionRunningTooLong()).thenReturn(true);
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- String oldSessionId = testSessionManager.perfSession().sessionId();
-
- assertThat(oldSessionId).isNotNull();
- assertThat(oldSessionId).isEqualTo(testSessionManager.perfSession().sessionId());
-
- testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
- assertThat(oldSessionId).isNotEqualTo(testSessionManager.perfSession().sessionId());
- }
-
- @Test
- public void
- testOnUpdateAppStateDoesntMakeGaugeManagerLogGaugeMetadataOnForegroundStateIfSessionIsNonVerbose() {
- forceNonVerboseSession();
-
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
-
- verify(mockGaugeManager, never())
- .logGaugeMetadata(
- anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class));
- }
-
- @Test
- public void
- testOnUpdateAppStateDoesntMakeGaugeManagerLogGaugeMetadataOnBackgroundStateEvenIfSessionIsVerbose() {
- forceVerboseSession();
-
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND);
-
- verify(mockGaugeManager, never())
- .logGaugeMetadata(
- anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class));
- }
-
- @Test
- public void testOnUpdateAppStateMakesGaugeManagerStartCollectingGaugesIfSessionIsVerbose() {
- forceVerboseSession();
-
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor);
- testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND);
-
- verify(mockGaugeManager)
- .startCollectingGauges(AdditionalMatchers.not(eq(mockPerfSession)), any());
- }
-
// LogGaugeData on new perf session when Verbose
// NotLogGaugeData on new perf session when not Verbose
// Mark Session as expired after time limit.
@Test
- public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesIfSessionIsNonVerbose() {
+ public void testUpdatePerfSessionMakesGaugeManagerStopCollectingGaugesIfSessionIsNonVerbose() {
forceNonVerboseSession();
SessionManager testSessionManager =
@@ -191,7 +97,7 @@ public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesIfSessionIs
}
@Test
- public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesWhenSessionsDisabled() {
+ public void testUpdatePerfSessionMakesGaugeManagerStopCollectingGaugesWhenSessionsDisabled() {
forceSessionsFeatureDisabled();
SessionManager testSessionManager =
@@ -221,22 +127,25 @@ public void testSessionIdDoesNotUpdateIfPerfSessionRunsTooLong() {
}
@Test
- public void testPerfSessionExpiredMakesGaugeManagerStopsCollectingGaugesIfSessionIsVerbose() {
- forceVerboseSession();
+ public void testUpdatePerfSessionStartsCollectingGaugesIfSessionIsVerbose() {
Timer mockTimer = mock(Timer.class);
when(mockClock.getTime()).thenReturn(mockTimer);
+ when(mockAppStateMonitor.getAppState()).thenReturn(ApplicationProcessState.FOREGROUND);
- PerfSession session = new PerfSession("sessionId", mockClock);
- SessionManager testSessionManager =
- new SessionManager(mockGaugeManager, session, mockAppStateMonitor);
+ PerfSession previousSession = new PerfSession("previousSession", mockClock);
+ previousSession.setGaugeAndEventCollectionEnabled(false);
- assertThat(session.isSessionRunningTooLong()).isFalse();
+ PerfSession newSession = new PerfSession("newSession", mockClock);
+ newSession.setGaugeAndEventCollectionEnabled(true);
- when(mockTimer.getDurationMicros())
- .thenReturn(TimeUnit.HOURS.toMicros(5)); // Default Max Session Length is 4 hours
+ SessionManager testSessionManager =
+ new SessionManager(mockGaugeManager, previousSession, mockAppStateMonitor);
+ testSessionManager.updatePerfSession(newSession);
+ testSessionManager.setApplicationContext(mockApplicationContext);
- assertThat(session.isSessionRunningTooLong()).isTrue();
- verify(mockGaugeManager, times(0)).logGaugeMetadata(any(), any());
+ verify(mockGaugeManager, times(1)).initializeGaugeMetadataManager(mockApplicationContext);
+ verify(mockGaugeManager, times(1))
+ .startCollectingGauges(newSession, ApplicationProcessState.FOREGROUND);
}
@Test
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependencies.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependencies.kt
index 8d3548c8f4b..4b636a155e0 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependencies.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependencies.kt
@@ -40,19 +40,6 @@ object FirebaseSessionsDependencies {
*/
@JvmStatic
fun addDependency(subscriberName: SessionSubscriber.Name) {
- if (subscriberName == SessionSubscriber.Name.PERFORMANCE) {
- throw IllegalArgumentException(
- """
- Incompatible versions of Firebase Perf and Firebase Sessions.
- A safe combination would be:
- firebase-sessions:1.1.0
- firebase-crashlytics:18.5.0
- firebase-perf:20.5.0
- For more information contact Firebase Support.
- """
- .trimIndent()
- )
- }
if (dependencies.containsKey(subscriberName)) {
Log.d(TAG, "Dependency $subscriberName already added.")
return
From c71fd96f4adfaf6e099a4ea08654227c9c9e08d6 Mon Sep 17 00:00:00 2001
From: Matthew Robertson
Date: Thu, 20 Mar 2025 10:28:52 -0600
Subject: [PATCH 038/146] Use multi-process DataStore instead of Preferences
DataStore (#6781)
Use multi-process DataStore instead of Preferences DataStore.
This change allows multiple processes to share the same datastore file
safely. This reduces settings fetch to one per app run, not one per
process.
Also updated the TimeProvider to provide an object with explicit time
units. This will make time less error prone. Removed all instances of
`System.currentTimeMillis()` from tests, making them deterministic.
---
firebase-sessions/CHANGELOG.md | 14 +-
.../firebase-sessions.gradle.kts | 4 +-
.../sessions/FirebaseSessionsComponent.kt | 60 +++---
.../sessions/FirebaseSessionsRegistrar.kt | 4 +-
.../sessions/SessionDataStoreConfigs.kt | 40 ----
.../firebase/sessions/SessionDatastore.kt | 65 +++---
.../firebase/sessions/SessionGenerator.kt | 2 +-
.../google/firebase/sessions/TimeProvider.kt | 15 +-
.../sessions/settings/RemoteSettings.kt | 52 ++---
.../settings/RemoteSettingsFetcher.kt | 4 +-
.../sessions/settings/SessionConfigs.kt | 58 ++++++
.../sessions/settings/SettingsCache.kt | 123 ++++-------
.../firebase/sessions/SessionDatastoreTest.kt | 59 ++++++
.../firebase/sessions/SessionGeneratorTest.kt | 13 +-
.../sessions/settings/RemoteSettingsTest.kt | 196 ++++++++----------
.../sessions/settings/SessionsSettingsTest.kt | 48 ++---
.../sessions/settings/SettingsCacheTest.kt | 169 ++++++++-------
.../sessions/testing/FakeSettingsCache.kt | 52 +++++
.../sessions/testing/FakeTimeProvider.kt | 10 +-
.../sessions/testing/TestDataStores.kt | 50 +++++
.../sessions/testing/TestSessionEventData.kt | 16 +-
gradle/libs.versions.toml | 1 +
22 files changed, 575 insertions(+), 480 deletions(-)
delete mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDataStoreConfigs.kt
create mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionConfigs.kt
create mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt
create mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSettingsCache.kt
create mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestDataStores.kt
diff --git a/firebase-sessions/CHANGELOG.md b/firebase-sessions/CHANGELOG.md
index 8353fbf9029..f73a860ee60 100644
--- a/firebase-sessions/CHANGELOG.md
+++ b/firebase-sessions/CHANGELOG.md
@@ -1,5 +1,5 @@
# Unreleased
-
+* [changed] Use multi-process DataStore instead of Preferences DataStore
# 2.1.0
* [changed] Add warning for known issue b/328687152
@@ -7,21 +7,9 @@
* [changed] Updated datastore dependency to v1.1.3 to
fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8).
-
-## Kotlin
-The Kotlin extensions library transitively includes the updated
-`firebase-sessions` library. The Kotlin extensions library has no additional
-updates.
-
# 2.0.9
* [fixed] Make AQS resilient to background init in multi-process apps.
-
-## Kotlin
-The Kotlin extensions library transitively includes the updated
-`firebase-sessions` library. The Kotlin extensions library has no additional
-updates.
-
# 2.0.7
* [fixed] Removed extraneous logs that risk leaking internal identifiers.
diff --git a/firebase-sessions/firebase-sessions.gradle.kts b/firebase-sessions/firebase-sessions.gradle.kts
index b136a281660..23edc952d5e 100644
--- a/firebase-sessions/firebase-sessions.gradle.kts
+++ b/firebase-sessions/firebase-sessions.gradle.kts
@@ -21,6 +21,7 @@ plugins {
id("firebase-vendor")
id("kotlin-android")
id("kotlin-kapt")
+ id("kotlinx-serialization")
}
firebaseLibrary {
@@ -76,7 +77,8 @@ dependencies {
implementation("com.google.android.datatransport:transport-api:3.2.0")
implementation(libs.javax.inject)
implementation(libs.androidx.annotation)
- implementation(libs.androidx.datastore.preferences)
+ implementation(libs.androidx.datastore)
+ implementation(libs.kotlinx.serialization.json)
vendor(libs.dagger.dagger) { exclude(group = "javax.inject", module = "javax.inject") }
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt
index 5680c9cc0ec..99de9e4a3fc 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt
@@ -19,23 +19,24 @@ package com.google.firebase.sessions
import android.content.Context
import android.util.Log
import androidx.datastore.core.DataStore
+import androidx.datastore.core.MultiProcessDataStoreFactory
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
-import androidx.datastore.preferences.core.PreferenceDataStoreFactory
-import androidx.datastore.preferences.core.Preferences
-import androidx.datastore.preferences.core.emptyPreferences
-import androidx.datastore.preferences.preferencesDataStoreFile
+import androidx.datastore.dataStoreFile
import com.google.android.datatransport.TransportFactory
import com.google.firebase.FirebaseApp
import com.google.firebase.annotations.concurrent.Background
import com.google.firebase.annotations.concurrent.Blocking
import com.google.firebase.inject.Provider
import com.google.firebase.installations.FirebaseInstallationsApi
-import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName
import com.google.firebase.sessions.settings.CrashlyticsSettingsFetcher
import com.google.firebase.sessions.settings.LocalOverrideSettings
import com.google.firebase.sessions.settings.RemoteSettings
import com.google.firebase.sessions.settings.RemoteSettingsFetcher
+import com.google.firebase.sessions.settings.SessionConfigs
+import com.google.firebase.sessions.settings.SessionConfigsSerializer
import com.google.firebase.sessions.settings.SessionsSettings
+import com.google.firebase.sessions.settings.SettingsCache
+import com.google.firebase.sessions.settings.SettingsCacheImpl
import com.google.firebase.sessions.settings.SettingsProvider
import dagger.Binds
import dagger.BindsInstance
@@ -45,10 +46,7 @@ import dagger.Provides
import javax.inject.Qualifier
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext
-
-@Qualifier internal annotation class SessionConfigsDataStore
-
-@Qualifier internal annotation class SessionDetailsDataStore
+import kotlinx.coroutines.CoroutineScope
@Qualifier internal annotation class LocalOverrideSettingsProvider
@@ -119,6 +117,8 @@ internal interface FirebaseSessionsComponent {
@RemoteSettingsProvider
fun remoteSettings(impl: RemoteSettings): SettingsProvider
+ @Binds @Singleton fun settingsCache(impl: SettingsCacheImpl): SettingsCache
+
companion object {
private const val TAG = "FirebaseSessions"
@@ -133,31 +133,37 @@ internal interface FirebaseSessionsComponent {
@Provides
@Singleton
- @SessionConfigsDataStore
- fun sessionConfigsDataStore(appContext: Context): DataStore =
- PreferenceDataStoreFactory.create(
+ fun sessionConfigsDataStore(
+ appContext: Context,
+ @Blocking blockingDispatcher: CoroutineContext,
+ ): DataStore =
+ MultiProcessDataStoreFactory.create(
+ serializer = SessionConfigsSerializer,
corruptionHandler =
ReplaceFileCorruptionHandler { ex ->
- Log.w(TAG, "CorruptionException in settings DataStore in ${getProcessName()}.", ex)
- emptyPreferences()
- }
- ) {
- appContext.preferencesDataStoreFile(SessionDataStoreConfigs.SETTINGS_CONFIG_NAME)
- }
+ Log.w(TAG, "CorruptionException in session configs DataStore", ex)
+ SessionConfigsSerializer.defaultValue
+ },
+ scope = CoroutineScope(blockingDispatcher),
+ produceFile = { appContext.dataStoreFile("aqs/sessionConfigsDataStore.data") },
+ )
@Provides
@Singleton
- @SessionDetailsDataStore
- fun sessionDetailsDataStore(appContext: Context): DataStore =
- PreferenceDataStoreFactory.create(
+ fun sessionDataStore(
+ appContext: Context,
+ @Blocking blockingDispatcher: CoroutineContext,
+ ): DataStore =
+ MultiProcessDataStoreFactory.create(
+ serializer = SessionDataSerializer,
corruptionHandler =
ReplaceFileCorruptionHandler { ex ->
- Log.w(TAG, "CorruptionException in sessions DataStore in ${getProcessName()}.", ex)
- emptyPreferences()
- }
- ) {
- appContext.preferencesDataStoreFile(SessionDataStoreConfigs.SESSIONS_CONFIG_NAME)
- }
+ Log.w(TAG, "CorruptionException in session data DataStore", ex)
+ SessionDataSerializer.defaultValue
+ },
+ scope = CoroutineScope(blockingDispatcher),
+ produceFile = { appContext.dataStoreFile("aqs/sessionDataStore.data") },
+ )
}
}
}
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt
index 5cb8de7a182..76c0c6330f4 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt
@@ -19,7 +19,7 @@ package com.google.firebase.sessions
import android.content.Context
import android.util.Log
import androidx.annotation.Keep
-import androidx.datastore.preferences.preferencesDataStore
+import androidx.datastore.core.MultiProcessDataStoreFactory
import com.google.android.datatransport.TransportFactory
import com.google.firebase.FirebaseApp
import com.google.firebase.annotations.concurrent.Background
@@ -84,7 +84,7 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar {
init {
try {
- ::preferencesDataStore.javaClass
+ MultiProcessDataStoreFactory.javaClass
} catch (ex: NoClassDefFoundError) {
Log.w(
TAG,
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDataStoreConfigs.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDataStoreConfigs.kt
deleted file mode 100644
index 109e980e666..00000000000
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDataStoreConfigs.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 2023 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.sessions
-
-import android.util.Base64
-import com.google.firebase.sessions.settings.SessionsSettings
-
-/**
- * Util object for handling DataStore configs in multi-process apps safely.
- *
- * This can be removed when datastore-preferences:1.1.0 becomes stable.
- */
-internal object SessionDataStoreConfigs {
- /** Sanitized process name to use in config filenames. */
- private val PROCESS_NAME =
- Base64.encodeToString(
- ProcessDetailsProvider.getProcessName().encodeToByteArray(),
- Base64.NO_WRAP or Base64.URL_SAFE, // URL safe is also filename safe.
- )
-
- /** Config name for [SessionDatastore] */
- val SESSIONS_CONFIG_NAME = "firebase_session_${PROCESS_NAME}_data"
-
- /** Config name for [SessionsSettings] */
- val SETTINGS_CONFIG_NAME = "firebase_session_${PROCESS_NAME}_settings"
-}
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt
index 2c4f243f942..b3b72b4d4d7 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt
@@ -17,15 +17,15 @@
package com.google.firebase.sessions
import android.util.Log
+import androidx.datastore.core.CorruptionException
import androidx.datastore.core.DataStore
-import androidx.datastore.preferences.core.Preferences
-import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.core.emptyPreferences
-import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.core.Serializer
import com.google.firebase.Firebase
import com.google.firebase.annotations.concurrent.Background
import com.google.firebase.app
import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
import javax.inject.Singleton
@@ -33,11 +33,29 @@ import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
-/** Datastore for sessions information */
-internal data class FirebaseSessionsData(val sessionId: String?)
+/** Data for sessions information */
+@Serializable internal data class SessionData(val sessionId: String?)
+
+/** DataStore json [Serializer] for [SessionData]. */
+internal object SessionDataSerializer : Serializer {
+ override val defaultValue = SessionData(sessionId = null)
+
+ override suspend fun readFrom(input: InputStream): SessionData =
+ try {
+ Json.decodeFromString(input.readBytes().decodeToString())
+ } catch (ex: Exception) {
+ throw CorruptionException("Cannot parse session data", ex)
+ }
+
+ override suspend fun writeTo(t: SessionData, output: OutputStream) {
+ @Suppress("BlockingMethodInNonBlockingContext") // blockingDispatcher is safe for blocking calls
+ output.write(Json.encodeToString(SessionData.serializer(), t).encodeToByteArray())
+ }
+}
/** Handles reading to and writing from the [DataStore]. */
internal interface SessionDatastore {
@@ -61,23 +79,17 @@ internal class SessionDatastoreImpl
@Inject
constructor(
@Background private val backgroundDispatcher: CoroutineContext,
- @SessionDetailsDataStore private val dataStore: DataStore,
+ private val sessionDataStore: DataStore,
) : SessionDatastore {
/** Most recent session from datastore is updated asynchronously whenever it changes */
- private val currentSessionFromDatastore = AtomicReference()
+ private val currentSessionFromDatastore = AtomicReference()
- private object FirebaseSessionDataKeys {
- val SESSION_ID = stringPreferencesKey("session_id")
- }
-
- private val firebaseSessionDataFlow: Flow =
- dataStore.data
- .catch { exception ->
- Log.e(TAG, "Error reading stored session data.", exception)
- emit(emptyPreferences())
- }
- .map { preferences -> mapSessionsData(preferences) }
+ private val firebaseSessionDataFlow: Flow =
+ sessionDataStore.data.catch { ex ->
+ Log.e(TAG, "Error reading stored session data.", ex)
+ emit(SessionDataSerializer.defaultValue)
+ }
init {
CoroutineScope(backgroundDispatcher).launch {
@@ -88,19 +100,14 @@ constructor(
override fun updateSessionId(sessionId: String) {
CoroutineScope(backgroundDispatcher).launch {
try {
- dataStore.edit { preferences ->
- preferences[FirebaseSessionDataKeys.SESSION_ID] = sessionId
- }
- } catch (e: IOException) {
- Log.w(TAG, "Failed to update session Id: $e")
+ sessionDataStore.updateData { SessionData(sessionId) }
+ } catch (ex: IOException) {
+ Log.w(TAG, "Failed to update session Id", ex)
}
}
}
- override fun getCurrentSessionId() = currentSessionFromDatastore.get()?.sessionId
-
- private fun mapSessionsData(preferences: Preferences): FirebaseSessionsData =
- FirebaseSessionsData(preferences[FirebaseSessionDataKeys.SESSION_ID])
+ override fun getCurrentSessionId(): String? = currentSessionFromDatastore.get()?.sessionId
private companion object {
private const val TAG = "FirebaseSessionsRepo"
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt
index 4c4775e8b24..409f9989348 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt
@@ -60,7 +60,7 @@ constructor(private val timeProvider: TimeProvider, private val uuidGenerator: U
sessionId = if (sessionIndex == 0) firstSessionId else generateSessionId(),
firstSessionId,
sessionIndex,
- sessionStartTimestampUs = timeProvider.currentTimeUs(),
+ sessionStartTimestampUs = timeProvider.currentTime().us,
)
return currentSession
}
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt
index b66b09af19f..869b64b2ff2 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt
@@ -20,11 +20,17 @@ import android.os.SystemClock
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
+/** Time with accessors for microseconds, milliseconds, and seconds. */
+internal data class Time(val ms: Long) {
+ val us = ms * 1_000
+ val seconds = ms / 1_000
+}
+
/** Time provider interface, for testing purposes. */
internal interface TimeProvider {
fun elapsedRealtime(): Duration
- fun currentTimeUs(): Long
+ fun currentTime(): Time
}
/** "Wall clock" time provider implementation. */
@@ -38,14 +44,11 @@ internal object TimeProviderImpl : TimeProvider {
override fun elapsedRealtime(): Duration = SystemClock.elapsedRealtime().milliseconds
/**
- * Gets the current "wall clock" time in microseconds.
+ * Gets the current "wall clock" time.
*
* This clock can be set by the user or the phone network, so the time may jump backwards or
* forwards unpredictably. This clock should only be used when correspondence with real-world
* dates and times is important, such as in a calendar or alarm clock application.
*/
- override fun currentTimeUs(): Long = System.currentTimeMillis() * US_PER_MILLIS
-
- /** Microseconds per millisecond. */
- private const val US_PER_MILLIS = 1000L
+ override fun currentTime(): Time = Time(ms = System.currentTimeMillis())
}
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt
index 67a48bc7924..b715cd9f79c 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt
@@ -19,18 +19,17 @@ package com.google.firebase.sessions.settings
import android.os.Build
import android.util.Log
import androidx.annotation.VisibleForTesting
-import com.google.firebase.annotations.concurrent.Background
import com.google.firebase.installations.FirebaseInstallationsApi
import com.google.firebase.sessions.ApplicationInfo
import com.google.firebase.sessions.InstallationId
+import com.google.firebase.sessions.TimeProvider
import dagger.Lazy
import javax.inject.Inject
import javax.inject.Singleton
-import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration
+import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.json.JSONException
@@ -40,7 +39,7 @@ import org.json.JSONObject
internal class RemoteSettings
@Inject
constructor(
- @Background private val backgroundDispatcher: CoroutineContext,
+ private val timeProvider: TimeProvider,
private val firebaseInstallationsApi: FirebaseInstallationsApi,
private val appInfo: ApplicationInfo,
private val configsFetcher: CrashlyticsSettingsFetcher,
@@ -90,10 +89,9 @@ constructor(
val options =
mapOf(
"X-Crashlytics-Installation-ID" to installationId,
- "X-Crashlytics-Device-Model" to
- removeForwardSlashesIn(String.format("%s/%s", Build.MANUFACTURER, Build.MODEL)),
- "X-Crashlytics-OS-Build-Version" to removeForwardSlashesIn(Build.VERSION.INCREMENTAL),
- "X-Crashlytics-OS-Display-Version" to removeForwardSlashesIn(Build.VERSION.RELEASE),
+ "X-Crashlytics-Device-Model" to sanitize("${Build.MANUFACTURER}${Build.MODEL}"),
+ "X-Crashlytics-OS-Build-Version" to sanitize(Build.VERSION.INCREMENTAL),
+ "X-Crashlytics-OS-Display-Version" to sanitize(Build.VERSION.RELEASE),
"X-Crashlytics-API-Client-Version" to appInfo.sessionSdkVersion,
)
@@ -129,22 +127,19 @@ constructor(
}
}
- sessionsEnabled?.let { settingsCache.updateSettingsEnabled(sessionsEnabled) }
-
- sessionTimeoutSeconds?.let {
- settingsCache.updateSessionRestartTimeout(sessionTimeoutSeconds)
- }
-
- sessionSamplingRate?.let { settingsCache.updateSamplingRate(sessionSamplingRate) }
-
- cacheDuration?.let { settingsCache.updateSessionCacheDuration(cacheDuration) }
- ?: let { settingsCache.updateSessionCacheDuration(86400) }
-
- settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis())
+ settingsCache.updateConfigs(
+ SessionConfigs(
+ sessionsEnabled = sessionsEnabled,
+ sessionTimeoutSeconds = sessionTimeoutSeconds,
+ sessionSamplingRate = sessionSamplingRate,
+ cacheDurationSeconds = cacheDuration ?: defaultCacheDuration,
+ cacheUpdatedTimeMs = timeProvider.currentTime().ms,
+ )
+ )
},
onFailure = { msg ->
// Network request failed here.
- Log.e(TAG, "Error failing to fetch the remote configs: $msg")
+ Log.e(TAG, "Error failed to fetch the remote configs: $msg")
},
)
}
@@ -153,18 +148,17 @@ constructor(
override fun isSettingsStale(): Boolean = settingsCache.hasCacheExpired()
@VisibleForTesting
- internal fun clearCachedSettings() {
- val scope = CoroutineScope(backgroundDispatcher)
- scope.launch { settingsCache.removeConfigs() }
+ internal fun clearCachedSettings() = runBlocking {
+ settingsCache.updateConfigs(SessionConfigsSerializer.defaultValue)
}
- private fun removeForwardSlashesIn(s: String): String {
- return s.replace(FORWARD_SLASH_STRING.toRegex(), "")
- }
+ private fun sanitize(s: String) = s.replace(sanitizeRegex, "")
private companion object {
const val TAG = "SessionConfigFetcher"
- const val FORWARD_SLASH_STRING: String = "/"
+ val defaultCacheDuration = 24.hours.inWholeSeconds.toInt()
+
+ val sanitizeRegex = "/".toRegex()
}
}
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt
index 92d530f2fa1..bd45ec8fb24 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt
@@ -17,7 +17,7 @@
package com.google.firebase.sessions.settings
import android.net.Uri
-import com.google.firebase.annotations.concurrent.Background
+import com.google.firebase.annotations.concurrent.Blocking
import com.google.firebase.sessions.ApplicationInfo
import java.io.BufferedReader
import java.io.InputStreamReader
@@ -42,7 +42,7 @@ internal class RemoteSettingsFetcher
@Inject
constructor(
private val appInfo: ApplicationInfo,
- @Background private val blockingDispatcher: CoroutineContext,
+ @Blocking private val blockingDispatcher: CoroutineContext,
) : CrashlyticsSettingsFetcher {
@Suppress("BlockingMethodInNonBlockingContext") // blockingDispatcher is safe for blocking calls.
override suspend fun doConfigFetch(
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionConfigs.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionConfigs.kt
new file mode 100644
index 00000000000..8d7e2484675
--- /dev/null
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionConfigs.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2025 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.sessions.settings
+
+import androidx.datastore.core.CorruptionException
+import androidx.datastore.core.Serializer
+import java.io.InputStream
+import java.io.OutputStream
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+
+/** Session configs data for caching. */
+@Serializable
+internal data class SessionConfigs(
+ val sessionsEnabled: Boolean?,
+ val sessionSamplingRate: Double?,
+ val sessionTimeoutSeconds: Int?,
+ val cacheDurationSeconds: Int?,
+ val cacheUpdatedTimeMs: Long?,
+)
+
+/** DataStore json [Serializer] for [SessionConfigs]. */
+internal object SessionConfigsSerializer : Serializer {
+ override val defaultValue =
+ SessionConfigs(
+ sessionsEnabled = null,
+ sessionSamplingRate = null,
+ sessionTimeoutSeconds = null,
+ cacheDurationSeconds = null,
+ cacheUpdatedTimeMs = null,
+ )
+
+ override suspend fun readFrom(input: InputStream): SessionConfigs =
+ try {
+ Json.decodeFromString(input.readBytes().decodeToString())
+ } catch (ex: Exception) {
+ throw CorruptionException("Cannot parse session configs", ex)
+ }
+
+ override suspend fun writeTo(t: SessionConfigs, output: OutputStream) {
+ @Suppress("BlockingMethodInNonBlockingContext") // blockingDispatcher is safe for blocking calls
+ output.write(Json.encodeToString(SessionConfigs.serializer(), t).encodeToByteArray())
+ }
+}
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt
index 2e60e51650a..468bbad6b7a 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt
@@ -17,128 +17,77 @@
package com.google.firebase.sessions.settings
import android.util.Log
-import androidx.annotation.VisibleForTesting
import androidx.datastore.core.DataStore
-import androidx.datastore.preferences.core.Preferences
-import androidx.datastore.preferences.core.booleanPreferencesKey
-import androidx.datastore.preferences.core.doublePreferencesKey
-import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.core.intPreferencesKey
-import androidx.datastore.preferences.core.longPreferencesKey
-import com.google.firebase.sessions.SessionConfigsDataStore
+import com.google.firebase.sessions.TimeProvider
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
-internal data class SessionConfigs(
- val sessionEnabled: Boolean?,
- val sessionSamplingRate: Double?,
- val sessionRestartTimeout: Int?,
- val cacheDuration: Int?,
- val cacheUpdatedTime: Long?,
-)
+internal interface SettingsCache {
+ fun hasCacheExpired(): Boolean
+
+ fun sessionsEnabled(): Boolean?
+
+ fun sessionSamplingRate(): Double?
+
+ fun sessionRestartTimeout(): Int?
+
+ suspend fun updateConfigs(sessionConfigs: SessionConfigs)
+}
@Singleton
-internal class SettingsCache
+internal class SettingsCacheImpl
@Inject
-constructor(@SessionConfigsDataStore private val dataStore: DataStore) {
- private lateinit var sessionConfigs: SessionConfigs
+constructor(
+ private val timeProvider: TimeProvider,
+ private val sessionConfigsDataStore: DataStore,
+) : SettingsCache {
+ private var sessionConfigs: SessionConfigs
init {
// Block until the cache is loaded from disk to ensure cache
// values are valid and readable from the main thread on init.
- runBlocking { updateSessionConfigs(dataStore.data.first().toPreferences()) }
+ runBlocking { sessionConfigs = sessionConfigsDataStore.data.first() }
}
- /** Update session configs from the given [preferences]. */
- private fun updateSessionConfigs(preferences: Preferences) {
- sessionConfigs =
- SessionConfigs(
- sessionEnabled = preferences[SESSIONS_ENABLED],
- sessionSamplingRate = preferences[SAMPLING_RATE],
- sessionRestartTimeout = preferences[RESTART_TIMEOUT_SECONDS],
- cacheDuration = preferences[CACHE_DURATION_SECONDS],
- cacheUpdatedTime = preferences[CACHE_UPDATED_TIME],
- )
- }
+ override fun hasCacheExpired(): Boolean {
+ val cacheUpdatedTimeMs = sessionConfigs.cacheUpdatedTimeMs
+ val cacheDurationSeconds = sessionConfigs.cacheDurationSeconds
- internal fun hasCacheExpired(): Boolean {
- val cacheUpdatedTime = sessionConfigs.cacheUpdatedTime
- val cacheDuration = sessionConfigs.cacheDuration
-
- if (cacheUpdatedTime != null && cacheDuration != null) {
- val timeDifferenceSeconds = (System.currentTimeMillis() - cacheUpdatedTime) / 1000
- if (timeDifferenceSeconds < cacheDuration) {
+ if (cacheUpdatedTimeMs != null && cacheDurationSeconds != null) {
+ val timeDifferenceSeconds = (timeProvider.currentTime().ms - cacheUpdatedTimeMs) / 1000
+ if (timeDifferenceSeconds < cacheDurationSeconds) {
return false
}
}
return true
}
- fun sessionsEnabled(): Boolean? = sessionConfigs.sessionEnabled
-
- fun sessionSamplingRate(): Double? = sessionConfigs.sessionSamplingRate
-
- fun sessionRestartTimeout(): Int? = sessionConfigs.sessionRestartTimeout
-
- suspend fun updateSettingsEnabled(enabled: Boolean?) {
- updateConfigValue(SESSIONS_ENABLED, enabled)
- }
-
- suspend fun updateSamplingRate(rate: Double?) {
- updateConfigValue(SAMPLING_RATE, rate)
- }
-
- suspend fun updateSessionRestartTimeout(timeoutInSeconds: Int?) {
- updateConfigValue(RESTART_TIMEOUT_SECONDS, timeoutInSeconds)
- }
+ override fun sessionsEnabled(): Boolean? = sessionConfigs.sessionsEnabled
- suspend fun updateSessionCacheDuration(cacheDurationInSeconds: Int?) {
- updateConfigValue(CACHE_DURATION_SECONDS, cacheDurationInSeconds)
- }
+ override fun sessionSamplingRate(): Double? = sessionConfigs.sessionSamplingRate
- suspend fun updateSessionCacheUpdatedTime(cacheUpdatedTime: Long?) {
- updateConfigValue(CACHE_UPDATED_TIME, cacheUpdatedTime)
- }
+ override fun sessionRestartTimeout(): Int? = sessionConfigs.sessionTimeoutSeconds
- @VisibleForTesting
- internal suspend fun removeConfigs() {
+ override suspend fun updateConfigs(sessionConfigs: SessionConfigs) {
try {
- dataStore.edit { preferences ->
- preferences.clear()
- updateSessionConfigs(preferences)
- }
- } catch (e: IOException) {
- Log.w(TAG, "Failed to remove config values: $e")
+ sessionConfigsDataStore.updateData { sessionConfigs }
+ this.sessionConfigs = sessionConfigs
+ } catch (ex: IOException) {
+ Log.w(TAG, "Failed to update config values: $ex")
}
}
- /** Updated the config value, or remove the key if the value is null. */
- private suspend fun updateConfigValue(key: Preferences.Key, value: T?) {
- // TODO(mrober): Refactor these to update all the values in one transaction.
+ internal suspend fun removeConfigs() =
try {
- dataStore.edit { preferences ->
- if (value != null) {
- preferences[key] = value
- } else {
- preferences.remove(key)
- }
- updateSessionConfigs(preferences)
- }
+ sessionConfigsDataStore.updateData { SessionConfigsSerializer.defaultValue }
} catch (ex: IOException) {
- Log.w(TAG, "Failed to update cache config value: $ex")
+ Log.w(TAG, "Failed to remove config values: $ex")
}
- }
private companion object {
const val TAG = "SettingsCache"
-
- val SESSIONS_ENABLED = booleanPreferencesKey("firebase_sessions_enabled")
- val SAMPLING_RATE = doublePreferencesKey("firebase_sessions_sampling_rate")
- val RESTART_TIMEOUT_SECONDS = intPreferencesKey("firebase_sessions_restart_timeout")
- val CACHE_DURATION_SECONDS = intPreferencesKey("firebase_sessions_cache_duration")
- val CACHE_UPDATED_TIME = longPreferencesKey("firebase_sessions_cache_updated_time")
}
}
diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt
new file mode 100644
index 00000000000..7e94eb3113e
--- /dev/null
+++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2025 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.sessions
+
+import android.content.Context
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.dataStoreFile
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class SessionDatastoreTest {
+ private val appContext: Context = ApplicationProvider.getApplicationContext()
+
+ @Test
+ fun getCurrentSessionId_returnsLatest() = runTest {
+ val sessionDatastore =
+ SessionDatastoreImpl(
+ backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"),
+ sessionDataStore =
+ DataStoreFactory.create(
+ serializer = SessionDataSerializer,
+ scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")),
+ produceFile = { appContext.dataStoreFile("sessionDataTestDataStore.data") },
+ ),
+ )
+
+ sessionDatastore.updateSessionId("sessionId1")
+ sessionDatastore.updateSessionId("sessionId2")
+ sessionDatastore.updateSessionId("sessionId3")
+
+ runCurrent()
+
+ assertThat(sessionDatastore.getCurrentSessionId()).isEqualTo("sessionId3")
+ }
+}
diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt
index 7126bae4dbf..bf260e73a4f 100644
--- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt
+++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt
@@ -16,12 +16,15 @@
package com.google.firebase.sessions
+import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.google.firebase.sessions.testing.FakeTimeProvider
import com.google.firebase.sessions.testing.FakeUuidGenerator
-import com.google.firebase.sessions.testing.TestSessionEventData.TEST_SESSION_TIMESTAMP_US
+import com.google.firebase.sessions.testing.TestSessionEventData.TEST_SESSION_TIMESTAMP
import org.junit.Test
+import org.junit.runner.RunWith
+@RunWith(AndroidJUnit4::class)
class SessionGeneratorTest {
private fun isValidSessionId(sessionId: String): Boolean {
if (sessionId.length != 32) {
@@ -96,7 +99,7 @@ class SessionGeneratorTest {
sessionId = SESSION_ID_1,
firstSessionId = SESSION_ID_1,
sessionIndex = 0,
- sessionStartTimestampUs = TEST_SESSION_TIMESTAMP_US,
+ sessionStartTimestampUs = TEST_SESSION_TIMESTAMP.us,
)
)
}
@@ -119,7 +122,7 @@ class SessionGeneratorTest {
sessionId = SESSION_ID_1,
firstSessionId = SESSION_ID_1,
sessionIndex = 0,
- sessionStartTimestampUs = TEST_SESSION_TIMESTAMP_US,
+ sessionStartTimestampUs = TEST_SESSION_TIMESTAMP.us,
)
)
@@ -135,7 +138,7 @@ class SessionGeneratorTest {
sessionId = SESSION_ID_2,
firstSessionId = SESSION_ID_1,
sessionIndex = 1,
- sessionStartTimestampUs = TEST_SESSION_TIMESTAMP_US,
+ sessionStartTimestampUs = TEST_SESSION_TIMESTAMP.us,
)
)
@@ -151,7 +154,7 @@ class SessionGeneratorTest {
sessionId = SESSION_ID_3,
firstSessionId = SESSION_ID_1,
sessionIndex = 2,
- sessionStartTimestampUs = TEST_SESSION_TIMESTAMP_US,
+ sessionStartTimestampUs = TEST_SESSION_TIMESTAMP.us,
)
)
}
diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt
index e4fb0b00148..ccaf4f8954d 100644
--- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt
+++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt
@@ -16,24 +16,22 @@
package com.google.firebase.sessions.settings
-import androidx.datastore.preferences.core.PreferenceDataStoreFactory
-import androidx.datastore.preferences.preferencesDataStoreFile
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.google.firebase.FirebaseApp
-import com.google.firebase.concurrent.TestOnlyExecutors
import com.google.firebase.installations.FirebaseInstallationsApi
import com.google.firebase.sessions.ApplicationInfo
import com.google.firebase.sessions.SessionEvents
+import com.google.firebase.sessions.TimeProvider
import com.google.firebase.sessions.testing.FakeFirebaseApp
import com.google.firebase.sessions.testing.FakeFirebaseInstallations
import com.google.firebase.sessions.testing.FakeRemoteConfigFetcher
-import kotlin.coroutines.CoroutineContext
+import com.google.firebase.sessions.testing.FakeSettingsCache
+import com.google.firebase.sessions.testing.FakeTimeProvider
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -53,22 +51,16 @@ class RemoteSettingsTest {
fun remoteSettings_successfulFetchCachesValues() =
runTest(UnconfinedTestDispatcher()) {
val firebaseApp = FakeFirebaseApp().firebaseApp
- val context = firebaseApp.applicationContext
val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
val fakeFetcher = FakeRemoteConfigFetcher()
val remoteSettings =
buildRemoteSettings(
- TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext,
+ FakeTimeProvider(),
firebaseInstallations,
SessionEvents.getApplicationInfo(firebaseApp),
fakeFetcher,
- SettingsCache(
- PreferenceDataStoreFactory.create(
- scope = this,
- produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) },
- )
- ),
+ FakeSettingsCache(),
)
runCurrent()
@@ -90,120 +82,100 @@ class RemoteSettingsTest {
}
@Test
- fun remoteSettings_successfulFetchWithLessConfigsCachesOnlyReceivedValues() =
- runTest(UnconfinedTestDispatcher()) {
- val firebaseApp = FakeFirebaseApp().firebaseApp
- val context = firebaseApp.applicationContext
- val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
- val fakeFetcher = FakeRemoteConfigFetcher()
-
- val remoteSettings =
- buildRemoteSettings(
- TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext,
- firebaseInstallations,
- SessionEvents.getApplicationInfo(firebaseApp),
- fakeFetcher,
- SettingsCache(
- PreferenceDataStoreFactory.create(
- scope = this,
- produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) },
- )
- ),
- )
-
- runCurrent()
-
- assertThat(remoteSettings.sessionEnabled).isNull()
- assertThat(remoteSettings.samplingRate).isNull()
- assertThat(remoteSettings.sessionRestartTimeout).isNull()
-
- val fetchedResponse = JSONObject(VALID_RESPONSE)
- fetchedResponse.getJSONObject("app_quality").remove("sessions_enabled")
- fakeFetcher.responseJSONObject = fetchedResponse
- remoteSettings.updateSettings()
-
- runCurrent()
-
- assertThat(remoteSettings.sessionEnabled).isNull()
- assertThat(remoteSettings.samplingRate).isEqualTo(0.75)
- assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes)
-
- remoteSettings.clearCachedSettings()
- }
+ fun remoteSettings_successfulFetchWithLessConfigsCachesOnlyReceivedValues() = runTest {
+ val firebaseApp = FakeFirebaseApp().firebaseApp
+ val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
+ val fakeFetcher = FakeRemoteConfigFetcher()
+
+ val remoteSettings =
+ buildRemoteSettings(
+ FakeTimeProvider(),
+ firebaseInstallations,
+ SessionEvents.getApplicationInfo(firebaseApp),
+ fakeFetcher,
+ FakeSettingsCache(),
+ )
+
+ runCurrent()
+
+ assertThat(remoteSettings.sessionEnabled).isNull()
+ assertThat(remoteSettings.samplingRate).isNull()
+ assertThat(remoteSettings.sessionRestartTimeout).isNull()
+
+ val fetchedResponse = JSONObject(VALID_RESPONSE)
+ fetchedResponse.getJSONObject("app_quality").remove("sessions_enabled")
+ fakeFetcher.responseJSONObject = fetchedResponse
+ remoteSettings.updateSettings()
+
+ runCurrent()
+
+ assertThat(remoteSettings.sessionEnabled).isNull()
+ assertThat(remoteSettings.samplingRate).isEqualTo(0.75)
+ assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes)
+
+ remoteSettings.clearCachedSettings()
+ }
@Test
- fun remoteSettings_successfulReFetchUpdatesCache() =
- runTest(UnconfinedTestDispatcher()) {
- val firebaseApp = FakeFirebaseApp().firebaseApp
- val context = firebaseApp.applicationContext
- val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
- val fakeFetcher = FakeRemoteConfigFetcher()
+ fun remoteSettings_successfulReFetchUpdatesCache() = runTest {
+ val firebaseApp = FakeFirebaseApp().firebaseApp
+ val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
+ val fakeFetcher = FakeRemoteConfigFetcher()
+ val fakeTimeProvider = FakeTimeProvider()
- val remoteSettings =
- buildRemoteSettings(
- TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext,
- firebaseInstallations,
- SessionEvents.getApplicationInfo(firebaseApp),
- fakeFetcher,
- SettingsCache(
- PreferenceDataStoreFactory.create(
- scope = this,
- produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) },
- )
- ),
- )
+ val remoteSettings =
+ buildRemoteSettings(
+ fakeTimeProvider,
+ firebaseInstallations,
+ SessionEvents.getApplicationInfo(firebaseApp),
+ fakeFetcher,
+ FakeSettingsCache(fakeTimeProvider),
+ )
- val fetchedResponse = JSONObject(VALID_RESPONSE)
- fetchedResponse.getJSONObject("app_quality").put("cache_duration", 1)
- fakeFetcher.responseJSONObject = fetchedResponse
- remoteSettings.updateSettings()
+ val fetchedResponse = JSONObject(VALID_RESPONSE)
+ fetchedResponse.getJSONObject("app_quality").put("cache_duration", 1)
+ fakeFetcher.responseJSONObject = fetchedResponse
+ remoteSettings.updateSettings()
- runCurrent()
+ runCurrent()
- assertThat(remoteSettings.sessionEnabled).isFalse()
- assertThat(remoteSettings.samplingRate).isEqualTo(0.75)
- assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes)
+ assertThat(remoteSettings.sessionEnabled).isFalse()
+ assertThat(remoteSettings.samplingRate).isEqualTo(0.75)
+ assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes)
- fetchedResponse.getJSONObject("app_quality").put("sessions_enabled", true)
- fetchedResponse.getJSONObject("app_quality").put("sampling_rate", 0.25)
- fetchedResponse.getJSONObject("app_quality").put("session_timeout_seconds", 1200)
+ fetchedResponse.getJSONObject("app_quality").put("sessions_enabled", true)
+ fetchedResponse.getJSONObject("app_quality").put("sampling_rate", 0.25)
+ fetchedResponse.getJSONObject("app_quality").put("session_timeout_seconds", 1200)
- // TODO(mrober): Fix these so we don't need to sleep. Maybe use FakeTime?
- // Sleep for a second before updating configs
- Thread.sleep(2000)
+ fakeTimeProvider.addInterval(31.minutes)
- fakeFetcher.responseJSONObject = fetchedResponse
- remoteSettings.updateSettings()
+ fakeFetcher.responseJSONObject = fetchedResponse
+ remoteSettings.updateSettings()
- runCurrent()
+ runCurrent()
- assertThat(remoteSettings.sessionEnabled).isTrue()
- assertThat(remoteSettings.samplingRate).isEqualTo(0.25)
- assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(20.minutes)
+ assertThat(remoteSettings.sessionEnabled).isTrue()
+ assertThat(remoteSettings.samplingRate).isEqualTo(0.25)
+ assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(20.minutes)
- remoteSettings.clearCachedSettings()
- }
+ remoteSettings.clearCachedSettings()
+ }
@Test
fun remoteSettings_successfulFetchWithEmptyConfigRetainsOldConfigs() =
runTest(UnconfinedTestDispatcher()) {
val firebaseApp = FakeFirebaseApp().firebaseApp
- val context = firebaseApp.applicationContext
val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
val fakeFetcher = FakeRemoteConfigFetcher()
+ val fakeTimeProvider = FakeTimeProvider()
val remoteSettings =
buildRemoteSettings(
- TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext,
+ fakeTimeProvider,
firebaseInstallations,
SessionEvents.getApplicationInfo(firebaseApp),
fakeFetcher,
- SettingsCache(
- PreferenceDataStoreFactory.create(
- scope = this,
- produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) },
- )
- ),
+ FakeSettingsCache(),
)
val fetchedResponse = JSONObject(VALID_RESPONSE)
@@ -212,6 +184,7 @@ class RemoteSettingsTest {
remoteSettings.updateSettings()
runCurrent()
+ fakeTimeProvider.addInterval(31.seconds)
assertThat(remoteSettings.sessionEnabled).isFalse()
assertThat(remoteSettings.samplingRate).isEqualTo(0.75)
@@ -226,6 +199,7 @@ class RemoteSettingsTest {
remoteSettings.updateSettings()
runCurrent()
+ Thread.sleep(30)
assertThat(remoteSettings.sessionEnabled).isFalse()
assertThat(remoteSettings.samplingRate).isEqualTo(0.75)
@@ -249,7 +223,6 @@ class RemoteSettingsTest {
// - Third fetch should exit even earlier, never having gone into the mutex.
val firebaseApp = FakeFirebaseApp().firebaseApp
- val context = firebaseApp.applicationContext
val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
val fakeFetcherWithDelay =
FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE), networkDelay = 3.seconds)
@@ -260,16 +233,11 @@ class RemoteSettingsTest {
val remoteSettingsWithDelay =
buildRemoteSettings(
- TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext,
+ FakeTimeProvider(),
firebaseInstallations,
SessionEvents.getApplicationInfo(firebaseApp),
- fakeFetcherWithDelay,
- SettingsCache(
- PreferenceDataStoreFactory.create(
- scope = this,
- produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) },
- )
- ),
+ configsFetcher = fakeFetcherWithDelay,
+ FakeSettingsCache(),
)
// Do the first fetch. This one should fetched the configsFetcher.
@@ -298,8 +266,6 @@ class RemoteSettingsTest {
}
internal companion object {
- const val SESSION_TEST_CONFIGS_NAME = "firebase_session_settings_test"
-
const val VALID_RESPONSE =
"""
{
@@ -329,14 +295,14 @@ class RemoteSettingsTest {
* the test code.
*/
fun buildRemoteSettings(
- backgroundDispatcher: CoroutineContext,
+ timeProvider: TimeProvider,
firebaseInstallationsApi: FirebaseInstallationsApi,
appInfo: ApplicationInfo,
configsFetcher: CrashlyticsSettingsFetcher,
settingsCache: SettingsCache,
): RemoteSettings =
RemoteSettings_Factory.create(
- { backgroundDispatcher },
+ { timeProvider },
{ firebaseInstallationsApi },
{ appInfo },
{ configsFetcher },
diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt
index 12f40e7cca8..f87d773b970 100644
--- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt
+++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt
@@ -17,20 +17,18 @@
package com.google.firebase.sessions.settings
import android.os.Bundle
-import androidx.datastore.preferences.core.PreferenceDataStoreFactory
-import androidx.datastore.preferences.preferencesDataStoreFile
import com.google.common.truth.Truth.assertThat
import com.google.firebase.FirebaseApp
-import com.google.firebase.concurrent.TestOnlyExecutors
-import com.google.firebase.sessions.SessionDataStoreConfigs
import com.google.firebase.sessions.SessionEvents
+import com.google.firebase.sessions.settings.RemoteSettingsTest.Companion.buildRemoteSettings
import com.google.firebase.sessions.testing.FakeFirebaseApp
import com.google.firebase.sessions.testing.FakeFirebaseInstallations
import com.google.firebase.sessions.testing.FakeRemoteConfigFetcher
+import com.google.firebase.sessions.testing.FakeSettingsCache
import com.google.firebase.sessions.testing.FakeSettingsProvider
+import com.google.firebase.sessions.testing.FakeTimeProvider
import kotlin.time.Duration.Companion.minutes
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -107,17 +105,12 @@ class SessionsSettingsTest {
val fakeFetcher = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE))
val remoteSettings =
- RemoteSettingsTest.buildRemoteSettings(
- TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext,
+ buildRemoteSettings(
+ FakeTimeProvider(),
firebaseInstallations,
SessionEvents.getApplicationInfo(firebaseApp),
fakeFetcher,
- SettingsCache(
- PreferenceDataStoreFactory.create(
- scope = this,
- produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) },
- )
- ),
+ FakeSettingsCache(),
)
val sessionsSettings =
@@ -150,17 +143,12 @@ class SessionsSettingsTest {
val fakeFetcher = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE))
val remoteSettings =
- RemoteSettingsTest.buildRemoteSettings(
- TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext,
+ buildRemoteSettings(
+ FakeTimeProvider(),
firebaseInstallations,
SessionEvents.getApplicationInfo(firebaseApp),
fakeFetcher,
- SettingsCache(
- PreferenceDataStoreFactory.create(
- scope = this,
- produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) },
- )
- ),
+ FakeSettingsCache(),
)
val sessionsSettings =
@@ -199,17 +187,12 @@ class SessionsSettingsTest {
fakeFetcher.responseJSONObject = JSONObject(invalidResponse)
val remoteSettings =
- RemoteSettingsTest.buildRemoteSettings(
- TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext,
+ buildRemoteSettings(
+ FakeTimeProvider(),
firebaseInstallations,
SessionEvents.getApplicationInfo(firebaseApp),
fakeFetcher,
- SettingsCache(
- PreferenceDataStoreFactory.create(
- scope = this,
- produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) },
- )
- ),
+ FakeSettingsCache(),
)
val sessionsSettings =
@@ -229,19 +212,12 @@ class SessionsSettingsTest {
remoteSettings.clearCachedSettings()
}
- @Test
- fun sessionSettings_dataStorePreferencesNameIsFilenameSafe() {
- assertThat(SessionDataStoreConfigs.SESSIONS_CONFIG_NAME).matches("^[a-zA-Z0-9_=]+\$")
- }
-
@After
fun cleanUp() {
FirebaseApp.clearInstancesForTest()
}
private companion object {
- const val SESSION_TEST_CONFIGS_NAME = "firebase_session_settings_test"
-
const val VALID_RESPONSE =
"""
{
diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SettingsCacheTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SettingsCacheTest.kt
index c4d35c86456..729208c33ca 100644
--- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SettingsCacheTest.kt
+++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SettingsCacheTest.kt
@@ -16,30 +16,23 @@
package com.google.firebase.sessions.settings
-import android.content.Context
-import androidx.datastore.core.DataStore
-import androidx.datastore.preferences.core.Preferences
-import androidx.datastore.preferences.preferencesDataStore
import com.google.common.truth.Truth.assertThat
import com.google.firebase.FirebaseApp
-import com.google.firebase.sessions.testing.FakeFirebaseApp
-import kotlinx.coroutines.ExperimentalCoroutinesApi
+import com.google.firebase.sessions.testing.FakeTimeProvider
+import com.google.firebase.sessions.testing.TestDataStores
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
-@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
class SettingsCacheTest {
- private val Context.dataStore: DataStore by
- preferencesDataStore(name = SESSION_TEST_CONFIGS_NAME)
@Test
fun sessionCache_returnsEmptyCache() = runTest {
- val context = FakeFirebaseApp().firebaseApp.applicationContext
- val settingsCache = SettingsCache(context.dataStore)
+ val fakeTimeProvider = FakeTimeProvider()
+ val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore)
assertThat(settingsCache.sessionSamplingRate()).isNull()
assertThat(settingsCache.sessionsEnabled()).isNull()
@@ -49,14 +42,18 @@ class SettingsCacheTest {
@Test
fun settingConfigsReturnsCachedValue() = runTest {
- val context = FakeFirebaseApp().firebaseApp.applicationContext
- val settingsCache = SettingsCache(context.dataStore)
-
- settingsCache.updateSettingsEnabled(false)
- settingsCache.updateSamplingRate(0.25)
- settingsCache.updateSessionRestartTimeout(600)
- settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis())
- settingsCache.updateSessionCacheDuration(1000)
+ val fakeTimeProvider = FakeTimeProvider()
+ val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore)
+
+ settingsCache.updateConfigs(
+ SessionConfigs(
+ sessionsEnabled = false,
+ sessionSamplingRate = 0.25,
+ sessionTimeoutSeconds = 600,
+ cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms,
+ cacheDurationSeconds = 1000,
+ )
+ )
assertThat(settingsCache.sessionsEnabled()).isFalse()
assertThat(settingsCache.sessionSamplingRate()).isEqualTo(0.25)
@@ -69,17 +66,22 @@ class SettingsCacheTest {
@Test
fun settingConfigsReturnsPreviouslyStoredValue() = runTest {
- val context = FakeFirebaseApp().firebaseApp.applicationContext
- val settingsCache = SettingsCache(context.dataStore)
-
- settingsCache.updateSettingsEnabled(false)
- settingsCache.updateSamplingRate(0.25)
- settingsCache.updateSessionRestartTimeout(600)
- settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis())
- settingsCache.updateSessionCacheDuration(1000)
+ val fakeTimeProvider = FakeTimeProvider()
+ val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore)
+
+ settingsCache.updateConfigs(
+ SessionConfigs(
+ sessionsEnabled = false,
+ sessionSamplingRate = 0.25,
+ sessionTimeoutSeconds = 600,
+ cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms,
+ cacheDurationSeconds = 1000,
+ )
+ )
// Create a new instance to imitate a second app launch.
- val newSettingsCache = SettingsCache(context.dataStore)
+ val newSettingsCache =
+ SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore)
assertThat(newSettingsCache.sessionsEnabled()).isFalse()
assertThat(newSettingsCache.sessionSamplingRate()).isEqualTo(0.25)
@@ -93,14 +95,18 @@ class SettingsCacheTest {
@Test
fun settingConfigsReturnsCacheExpiredWithShortCacheDuration() = runTest {
- val context = FakeFirebaseApp().firebaseApp.applicationContext
- val settingsCache = SettingsCache(context.dataStore)
-
- settingsCache.updateSettingsEnabled(false)
- settingsCache.updateSamplingRate(0.25)
- settingsCache.updateSessionRestartTimeout(600)
- settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis())
- settingsCache.updateSessionCacheDuration(0)
+ val fakeTimeProvider = FakeTimeProvider()
+ val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore)
+
+ settingsCache.updateConfigs(
+ SessionConfigs(
+ sessionsEnabled = false,
+ sessionSamplingRate = 0.25,
+ sessionTimeoutSeconds = 600,
+ cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms,
+ cacheDurationSeconds = 0,
+ )
+ )
assertThat(settingsCache.sessionSamplingRate()).isEqualTo(0.25)
assertThat(settingsCache.sessionsEnabled()).isFalse()
@@ -112,13 +118,18 @@ class SettingsCacheTest {
@Test
fun settingConfigsReturnsCachedValueWithPartialConfigs() = runTest {
- val context = FakeFirebaseApp().firebaseApp.applicationContext
- val settingsCache = SettingsCache(context.dataStore)
-
- settingsCache.updateSettingsEnabled(false)
- settingsCache.updateSamplingRate(0.25)
- settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis())
- settingsCache.updateSessionCacheDuration(1000)
+ val fakeTimeProvider = FakeTimeProvider()
+ val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore)
+
+ settingsCache.updateConfigs(
+ SessionConfigs(
+ sessionsEnabled = false,
+ sessionSamplingRate = 0.25,
+ sessionTimeoutSeconds = null,
+ cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms,
+ cacheDurationSeconds = 1000,
+ )
+ )
assertThat(settingsCache.sessionSamplingRate()).isEqualTo(0.25)
assertThat(settingsCache.sessionsEnabled()).isFalse()
@@ -130,25 +141,33 @@ class SettingsCacheTest {
@Test
fun settingConfigsAllowsUpdateConfigsAndCachesValues() = runTest {
- val context = FakeFirebaseApp().firebaseApp.applicationContext
- val settingsCache = SettingsCache(context.dataStore)
-
- settingsCache.updateSettingsEnabled(false)
- settingsCache.updateSamplingRate(0.25)
- settingsCache.updateSessionRestartTimeout(600)
- settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis())
- settingsCache.updateSessionCacheDuration(1000)
+ val fakeTimeProvider = FakeTimeProvider()
+ val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore)
+
+ settingsCache.updateConfigs(
+ SessionConfigs(
+ sessionsEnabled = false,
+ sessionSamplingRate = 0.25,
+ sessionTimeoutSeconds = 600,
+ cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms,
+ cacheDurationSeconds = 1000,
+ )
+ )
assertThat(settingsCache.sessionSamplingRate()).isEqualTo(0.25)
assertThat(settingsCache.sessionsEnabled()).isFalse()
assertThat(settingsCache.sessionRestartTimeout()).isEqualTo(600)
assertThat(settingsCache.hasCacheExpired()).isFalse()
- settingsCache.updateSettingsEnabled(true)
- settingsCache.updateSamplingRate(0.33)
- settingsCache.updateSessionRestartTimeout(100)
- settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis())
- settingsCache.updateSessionCacheDuration(0)
+ settingsCache.updateConfigs(
+ SessionConfigs(
+ sessionsEnabled = true,
+ sessionSamplingRate = 0.33,
+ sessionTimeoutSeconds = 100,
+ cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms,
+ cacheDurationSeconds = 0,
+ )
+ )
assertThat(settingsCache.sessionSamplingRate()).isEqualTo(0.33)
assertThat(settingsCache.sessionsEnabled()).isTrue()
@@ -160,25 +179,33 @@ class SettingsCacheTest {
@Test
fun settingConfigsCleansCacheForNullValues() = runTest {
- val context = FakeFirebaseApp().firebaseApp.applicationContext
- val settingsCache = SettingsCache(context.dataStore)
-
- settingsCache.updateSettingsEnabled(false)
- settingsCache.updateSamplingRate(0.25)
- settingsCache.updateSessionRestartTimeout(600)
- settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis())
- settingsCache.updateSessionCacheDuration(1000)
+ val fakeTimeProvider = FakeTimeProvider()
+ val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore)
+
+ settingsCache.updateConfigs(
+ SessionConfigs(
+ sessionsEnabled = false,
+ sessionSamplingRate = 0.25,
+ sessionTimeoutSeconds = 600,
+ cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms,
+ cacheDurationSeconds = 1000,
+ )
+ )
assertThat(settingsCache.sessionSamplingRate()).isEqualTo(0.25)
assertThat(settingsCache.sessionsEnabled()).isFalse()
assertThat(settingsCache.sessionRestartTimeout()).isEqualTo(600)
assertThat(settingsCache.hasCacheExpired()).isFalse()
- settingsCache.updateSettingsEnabled(null)
- settingsCache.updateSamplingRate(0.33)
- settingsCache.updateSessionRestartTimeout(null)
- settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis())
- settingsCache.updateSessionCacheDuration(1000)
+ settingsCache.updateConfigs(
+ SessionConfigs(
+ sessionsEnabled = null,
+ sessionSamplingRate = 0.33,
+ sessionTimeoutSeconds = null,
+ cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms,
+ cacheDurationSeconds = 1000,
+ )
+ )
assertThat(settingsCache.sessionSamplingRate()).isEqualTo(0.33)
assertThat(settingsCache.sessionsEnabled()).isNull()
@@ -192,8 +219,4 @@ class SettingsCacheTest {
fun cleanUp() {
FirebaseApp.clearInstancesForTest()
}
-
- private companion object {
- const val SESSION_TEST_CONFIGS_NAME = "firebase_test_session_settings"
- }
}
diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSettingsCache.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSettingsCache.kt
new file mode 100644
index 00000000000..2a3e28c00b9
--- /dev/null
+++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSettingsCache.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2025 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.sessions.testing
+
+import com.google.firebase.sessions.TimeProvider
+import com.google.firebase.sessions.settings.SessionConfigs
+import com.google.firebase.sessions.settings.SessionConfigsSerializer
+import com.google.firebase.sessions.settings.SettingsCache
+
+/** Fake implementation of [SettingsCache]. */
+internal class FakeSettingsCache(
+ private val timeProvider: TimeProvider = FakeTimeProvider(),
+ private var sessionConfigs: SessionConfigs = SessionConfigsSerializer.defaultValue,
+) : SettingsCache {
+ override fun hasCacheExpired(): Boolean {
+ val cacheUpdatedTimeMs = sessionConfigs.cacheUpdatedTimeMs
+ val cacheDurationSeconds = sessionConfigs.cacheDurationSeconds
+
+ if (cacheUpdatedTimeMs != null && cacheDurationSeconds != null) {
+ val timeDifferenceSeconds = (timeProvider.currentTime().ms - cacheUpdatedTimeMs) / 1000
+ if (timeDifferenceSeconds < cacheDurationSeconds) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ override fun sessionsEnabled(): Boolean? = sessionConfigs.sessionsEnabled
+
+ override fun sessionSamplingRate(): Double? = sessionConfigs.sessionSamplingRate
+
+ override fun sessionRestartTimeout(): Int? = sessionConfigs.sessionTimeoutSeconds
+
+ override suspend fun updateConfigs(sessionConfigs: SessionConfigs) {
+ this.sessionConfigs = sessionConfigs
+ }
+}
diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeTimeProvider.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeTimeProvider.kt
index 35010de415a..295600cf48e 100644
--- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeTimeProvider.kt
+++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeTimeProvider.kt
@@ -16,17 +16,19 @@
package com.google.firebase.sessions.testing
+import com.google.firebase.sessions.Time
import com.google.firebase.sessions.TimeProvider
-import com.google.firebase.sessions.testing.TestSessionEventData.TEST_SESSION_TIMESTAMP_US
+import com.google.firebase.sessions.testing.TestSessionEventData.TEST_SESSION_TIMESTAMP
import kotlin.time.Duration
-import kotlin.time.DurationUnit
+import kotlin.time.DurationUnit.MILLISECONDS
/**
* Fake [TimeProvider] that allows programmatically elapsing time forward.
*
* Default [elapsedRealtime] is [Duration.ZERO] until the time is moved using [addInterval].
*/
-class FakeTimeProvider(private val initialTimeUs: Long = TEST_SESSION_TIMESTAMP_US) : TimeProvider {
+internal class FakeTimeProvider(private val initialTime: Time = TEST_SESSION_TIMESTAMP) :
+ TimeProvider {
private var elapsed = Duration.ZERO
fun addInterval(interval: Duration) {
@@ -38,5 +40,5 @@ class FakeTimeProvider(private val initialTimeUs: Long = TEST_SESSION_TIMESTAMP_
override fun elapsedRealtime(): Duration = elapsed
- override fun currentTimeUs(): Long = initialTimeUs + elapsed.toLong(DurationUnit.MICROSECONDS)
+ override fun currentTime(): Time = Time(ms = initialTime.ms + elapsed.toLong(MILLISECONDS))
}
diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestDataStores.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestDataStores.kt
new file mode 100644
index 00000000000..d7cc3a7f67d
--- /dev/null
+++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestDataStores.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2025 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.sessions.testing
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.dataStoreFile
+import androidx.test.core.app.ApplicationProvider
+import com.google.firebase.sessions.SessionData
+import com.google.firebase.sessions.SessionDataSerializer
+import com.google.firebase.sessions.settings.SessionConfigs
+import com.google.firebase.sessions.settings.SessionConfigsSerializer
+
+/**
+ * Container of instances of [DataStore] for testing.
+ *
+ * Note these do not pass the test scheduler to the instances, so won't work with `runCurrent`.
+ */
+internal object TestDataStores {
+ private val appContext: Context = ApplicationProvider.getApplicationContext()
+
+ val sessionConfigsDataStore: DataStore by lazy {
+ DataStoreFactory.create(
+ serializer = SessionConfigsSerializer,
+ produceFile = { appContext.dataStoreFile("sessionConfigsTestDataStore.data") },
+ )
+ }
+
+ val sessionDataStore: DataStore by lazy {
+ DataStoreFactory.create(
+ serializer = SessionDataSerializer,
+ produceFile = { appContext.dataStoreFile("sessionDataTestDataStore.data") },
+ )
+ }
+}
diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestSessionEventData.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestSessionEventData.kt
index 7619bc12588..105950a37f4 100644
--- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestSessionEventData.kt
+++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestSessionEventData.kt
@@ -30,23 +30,24 @@ import com.google.firebase.sessions.ProcessDetails
import com.google.firebase.sessions.SessionDetails
import com.google.firebase.sessions.SessionEvent
import com.google.firebase.sessions.SessionInfo
+import com.google.firebase.sessions.Time
internal object TestSessionEventData {
- const val TEST_SESSION_TIMESTAMP_US: Long = 12340000
+ val TEST_SESSION_TIMESTAMP: Time = Time(ms = 12340)
val TEST_SESSION_DETAILS =
SessionDetails(
sessionId = "a1b2c3",
firstSessionId = "a1a1a1",
sessionIndex = 3,
- sessionStartTimestampUs = TEST_SESSION_TIMESTAMP_US
+ sessionStartTimestampUs = TEST_SESSION_TIMESTAMP.us,
)
val TEST_DATA_COLLECTION_STATUS =
DataCollectionStatus(
performance = DataCollectionState.COLLECTION_SDK_NOT_INSTALLED,
crashlytics = DataCollectionState.COLLECTION_SDK_NOT_INSTALLED,
- sessionSamplingRate = 1.0
+ sessionSamplingRate = 1.0,
)
val TEST_SESSION_DATA =
@@ -54,19 +55,14 @@ internal object TestSessionEventData {
sessionId = "a1b2c3",
firstSessionId = "a1a1a1",
sessionIndex = 3,
- eventTimestampUs = TEST_SESSION_TIMESTAMP_US,
+ eventTimestampUs = TEST_SESSION_TIMESTAMP.us,
dataCollectionStatus = TEST_DATA_COLLECTION_STATUS,
firebaseInstallationId = "",
firebaseAuthenticationToken = "",
)
val TEST_PROCESS_DETAILS =
- ProcessDetails(
- processName = "com.google.firebase.sessions.test",
- 0,
- 100,
- false,
- )
+ ProcessDetails(processName = "com.google.firebase.sessions.test", 0, 100, false)
val TEST_APP_PROCESS_DETAILS = listOf(TEST_PROCESS_DETAILS)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 44079b349e8..5fa68f6cc03 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -91,6 +91,7 @@ androidx-cardview = { module = "androidx.cardview:cardview", version.ref = "card
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
androidx-core = { module = "androidx.core:core", version = "1.13.1" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
+androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" }
androidx-espresso-idling-resource = { module = "androidx.test.espresso:espresso-idling-resource", version.ref = "espressoCore" }
From fcd270c1f5717590a3764efa81f34302cfeecf7f Mon Sep 17 00:00:00 2001
From: Matthew Robertson
Date: Mon, 24 Mar 2025 07:11:04 -0600
Subject: [PATCH 039/146] Share settings cache between running processes
(#6788)
With the multi-process data store change, all processes will read the
settings cache from the same file safely. This means if a second process
started, it would read the cache the first process persisted.
But if 2 processes were already running, and one fetched and cached new
settings, it wouldn't automatically share it with the other running
process.
This change fixes that by having all processes watch the file.
Also cleaned up settings a bit, and made everything in seconds to avoid
converting units. Cleaned up unit tests. Also removed the need to lazy
load the cache by doing a double check in the getter instead.
There is more potential to clean up, but let's do it later.
---
.../sessions/FirebaseSessionsTests.kt | 34 +--
.../sessions/settings/RemoteSettings.kt | 11 +-
.../sessions/settings/SessionConfigs.kt | 4 +-
.../sessions/settings/SettingsCache.kt | 37 ++-
.../firebase/sessions/SessionDatastoreTest.kt | 2 +-
.../sessions/settings/RemoteSettingsTest.kt | 268 +++++++-----------
.../sessions/settings/SessionsSettingsTest.kt | 218 +++++++-------
.../sessions/settings/SettingsCacheTest.kt | 123 ++++++--
.../sessions/testing/FakeSettingsCache.kt | 6 +-
.../sessions/testing/TestDataStores.kt | 50 ----
10 files changed, 365 insertions(+), 388 deletions(-)
delete mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestDataStores.kt
diff --git a/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/FirebaseSessionsTests.kt b/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/FirebaseSessionsTests.kt
index 1cf67e0c5e1..c74b6e4e329 100644
--- a/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/FirebaseSessionsTests.kt
+++ b/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/FirebaseSessionsTests.kt
@@ -20,12 +20,10 @@ import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.google.firebase.Firebase
-import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions
import com.google.firebase.initialize
import com.google.firebase.sessions.settings.SessionsSettings
-import org.junit.After
-import org.junit.Before
+import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
@@ -36,23 +34,6 @@ import org.junit.runner.RunWith
*/
@RunWith(AndroidJUnit4::class)
class FirebaseSessionsTests {
- @Before
- fun setUp() {
- Firebase.initialize(
- ApplicationProvider.getApplicationContext(),
- FirebaseOptions.Builder()
- .setApplicationId(APP_ID)
- .setApiKey(API_KEY)
- .setProjectId(PROJECT_ID)
- .build()
- )
- }
-
- @After
- fun cleanUp() {
- FirebaseApp.clearInstancesForTest()
- }
-
@Test
fun firebaseSessionsDoesInitialize() {
assertThat(FirebaseSessions.instance).isNotNull()
@@ -69,5 +50,18 @@ class FirebaseSessionsTests {
private const val APP_ID = "1:1:android:1a"
private const val API_KEY = "API-KEY-API-KEY-API-KEY-API-KEY-API-KEY"
private const val PROJECT_ID = "PROJECT-ID"
+
+ @BeforeClass
+ @JvmStatic
+ fun setUp() {
+ Firebase.initialize(
+ ApplicationProvider.getApplicationContext(),
+ FirebaseOptions.Builder()
+ .setApplicationId(APP_ID)
+ .setApiKey(API_KEY)
+ .setProjectId(PROJECT_ID)
+ .build(),
+ )
+ }
}
}
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt
index b715cd9f79c..1079577e03c 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt
@@ -23,13 +23,11 @@ import com.google.firebase.installations.FirebaseInstallationsApi
import com.google.firebase.sessions.ApplicationInfo
import com.google.firebase.sessions.InstallationId
import com.google.firebase.sessions.TimeProvider
-import dagger.Lazy
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
-import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.json.JSONException
@@ -43,11 +41,8 @@ constructor(
private val firebaseInstallationsApi: FirebaseInstallationsApi,
private val appInfo: ApplicationInfo,
private val configsFetcher: CrashlyticsSettingsFetcher,
- private val lazySettingsCache: Lazy,
+ private val settingsCache: SettingsCache,
) : SettingsProvider {
- private val settingsCache: SettingsCache
- get() = lazySettingsCache.get()
-
private val fetchInProgress = Mutex()
override val sessionEnabled: Boolean?
@@ -133,7 +128,7 @@ constructor(
sessionTimeoutSeconds = sessionTimeoutSeconds,
sessionSamplingRate = sessionSamplingRate,
cacheDurationSeconds = cacheDuration ?: defaultCacheDuration,
- cacheUpdatedTimeMs = timeProvider.currentTime().ms,
+ cacheUpdatedTimeSeconds = timeProvider.currentTime().seconds,
)
)
},
@@ -148,7 +143,7 @@ constructor(
override fun isSettingsStale(): Boolean = settingsCache.hasCacheExpired()
@VisibleForTesting
- internal fun clearCachedSettings() = runBlocking {
+ internal suspend fun clearCachedSettings() {
settingsCache.updateConfigs(SessionConfigsSerializer.defaultValue)
}
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionConfigs.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionConfigs.kt
index 8d7e2484675..ab310ebed8a 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionConfigs.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionConfigs.kt
@@ -30,7 +30,7 @@ internal data class SessionConfigs(
val sessionSamplingRate: Double?,
val sessionTimeoutSeconds: Int?,
val cacheDurationSeconds: Int?,
- val cacheUpdatedTimeMs: Long?,
+ val cacheUpdatedTimeSeconds: Long?,
)
/** DataStore json [Serializer] for [SessionConfigs]. */
@@ -41,7 +41,7 @@ internal object SessionConfigsSerializer : Serializer {
sessionSamplingRate = null,
sessionTimeoutSeconds = null,
cacheDurationSeconds = null,
- cacheUpdatedTimeMs = null,
+ cacheUpdatedTimeSeconds = null,
)
override suspend fun readFrom(input: InputStream): SessionConfigs =
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt
index 468bbad6b7a..1640a5c7b7a 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt
@@ -17,12 +17,18 @@
package com.google.firebase.sessions.settings
import android.util.Log
+import androidx.annotation.VisibleForTesting
import androidx.datastore.core.DataStore
+import com.google.firebase.annotations.concurrent.Background
import com.google.firebase.sessions.TimeProvider
import java.io.IOException
+import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
import javax.inject.Singleton
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
internal interface SettingsCache {
@@ -41,23 +47,38 @@ internal interface SettingsCache {
internal class SettingsCacheImpl
@Inject
constructor(
+ @Background private val backgroundDispatcher: CoroutineContext,
private val timeProvider: TimeProvider,
private val sessionConfigsDataStore: DataStore,
) : SettingsCache {
- private var sessionConfigs: SessionConfigs
+ private val sessionConfigsAtomicReference = AtomicReference()
+
+ private val sessionConfigs: SessionConfigs
+ get() {
+ // Ensure configs are loaded from disk before the first access
+ if (sessionConfigsAtomicReference.get() == null) {
+ // Double check to avoid the `runBlocking` unless necessary
+ sessionConfigsAtomicReference.compareAndSet(
+ null,
+ runBlocking { sessionConfigsDataStore.data.first() },
+ )
+ }
+
+ return sessionConfigsAtomicReference.get()
+ }
init {
- // Block until the cache is loaded from disk to ensure cache
- // values are valid and readable from the main thread on init.
- runBlocking { sessionConfigs = sessionConfigsDataStore.data.first() }
+ CoroutineScope(backgroundDispatcher).launch {
+ sessionConfigsDataStore.data.collect(sessionConfigsAtomicReference::set)
+ }
}
override fun hasCacheExpired(): Boolean {
- val cacheUpdatedTimeMs = sessionConfigs.cacheUpdatedTimeMs
+ val cacheUpdatedTimeSeconds = sessionConfigs.cacheUpdatedTimeSeconds
val cacheDurationSeconds = sessionConfigs.cacheDurationSeconds
- if (cacheUpdatedTimeMs != null && cacheDurationSeconds != null) {
- val timeDifferenceSeconds = (timeProvider.currentTime().ms - cacheUpdatedTimeMs) / 1000
+ if (cacheUpdatedTimeSeconds != null && cacheDurationSeconds != null) {
+ val timeDifferenceSeconds = timeProvider.currentTime().seconds - cacheUpdatedTimeSeconds
if (timeDifferenceSeconds < cacheDurationSeconds) {
return false
}
@@ -74,12 +95,12 @@ constructor(
override suspend fun updateConfigs(sessionConfigs: SessionConfigs) {
try {
sessionConfigsDataStore.updateData { sessionConfigs }
- this.sessionConfigs = sessionConfigs
} catch (ex: IOException) {
Log.w(TAG, "Failed to update config values: $ex")
}
}
+ @VisibleForTesting
internal suspend fun removeConfigs() =
try {
sessionConfigsDataStore.updateData { SessionConfigsSerializer.defaultValue }
diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt
index 7e94eb3113e..efe7bb27a97 100644
--- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt
+++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt
@@ -44,7 +44,7 @@ class SessionDatastoreTest {
DataStoreFactory.create(
serializer = SessionDataSerializer,
scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")),
- produceFile = { appContext.dataStoreFile("sessionDataTestDataStore.data") },
+ produceFile = { appContext.dataStoreFile("sessionDataStore.data") },
),
)
diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt
index ccaf4f8954d..74df328ae57 100644
--- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt
+++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt
@@ -19,10 +19,7 @@ package com.google.firebase.sessions.settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.google.firebase.FirebaseApp
-import com.google.firebase.installations.FirebaseInstallationsApi
-import com.google.firebase.sessions.ApplicationInfo
import com.google.firebase.sessions.SessionEvents
-import com.google.firebase.sessions.TimeProvider
import com.google.firebase.sessions.testing.FakeFirebaseApp
import com.google.firebase.sessions.testing.FakeFirebaseInstallations
import com.google.firebase.sessions.testing.FakeRemoteConfigFetcher
@@ -31,11 +28,8 @@ import com.google.firebase.sessions.testing.FakeTimeProvider
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeout
import org.json.JSONObject
@@ -43,43 +37,37 @@ import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
-@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class RemoteSettingsTest {
@Test
- fun remoteSettings_successfulFetchCachesValues() =
- runTest(UnconfinedTestDispatcher()) {
- val firebaseApp = FakeFirebaseApp().firebaseApp
- val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
- val fakeFetcher = FakeRemoteConfigFetcher()
-
- val remoteSettings =
- buildRemoteSettings(
- FakeTimeProvider(),
- firebaseInstallations,
- SessionEvents.getApplicationInfo(firebaseApp),
- fakeFetcher,
- FakeSettingsCache(),
- )
-
- runCurrent()
+ fun remoteSettings_successfulFetchCachesValues() = runTest {
+ val firebaseApp = FakeFirebaseApp().firebaseApp
+ val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
+ val fakeFetcher = FakeRemoteConfigFetcher()
- assertThat(remoteSettings.sessionEnabled).isNull()
- assertThat(remoteSettings.samplingRate).isNull()
- assertThat(remoteSettings.sessionRestartTimeout).isNull()
+ val remoteSettings =
+ RemoteSettings(
+ FakeTimeProvider(),
+ firebaseInstallations,
+ SessionEvents.getApplicationInfo(firebaseApp),
+ fakeFetcher,
+ FakeSettingsCache(),
+ )
- fakeFetcher.responseJSONObject = JSONObject(VALID_RESPONSE)
- remoteSettings.updateSettings()
+ assertThat(remoteSettings.sessionEnabled).isNull()
+ assertThat(remoteSettings.samplingRate).isNull()
+ assertThat(remoteSettings.sessionRestartTimeout).isNull()
- runCurrent()
+ fakeFetcher.responseJSONObject = JSONObject(VALID_RESPONSE)
+ remoteSettings.updateSettings()
- assertThat(remoteSettings.sessionEnabled).isFalse()
- assertThat(remoteSettings.samplingRate).isEqualTo(0.75)
- assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes)
+ assertThat(remoteSettings.sessionEnabled).isFalse()
+ assertThat(remoteSettings.samplingRate).isEqualTo(0.75)
+ assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes)
- remoteSettings.clearCachedSettings()
- }
+ remoteSettings.clearCachedSettings()
+ }
@Test
fun remoteSettings_successfulFetchWithLessConfigsCachesOnlyReceivedValues() = runTest {
@@ -88,7 +76,7 @@ class RemoteSettingsTest {
val fakeFetcher = FakeRemoteConfigFetcher()
val remoteSettings =
- buildRemoteSettings(
+ RemoteSettings(
FakeTimeProvider(),
firebaseInstallations,
SessionEvents.getApplicationInfo(firebaseApp),
@@ -96,8 +84,6 @@ class RemoteSettingsTest {
FakeSettingsCache(),
)
- runCurrent()
-
assertThat(remoteSettings.sessionEnabled).isNull()
assertThat(remoteSettings.samplingRate).isNull()
assertThat(remoteSettings.sessionRestartTimeout).isNull()
@@ -107,8 +93,6 @@ class RemoteSettingsTest {
fakeFetcher.responseJSONObject = fetchedResponse
remoteSettings.updateSettings()
- runCurrent()
-
assertThat(remoteSettings.sessionEnabled).isNull()
assertThat(remoteSettings.samplingRate).isEqualTo(0.75)
assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes)
@@ -124,7 +108,7 @@ class RemoteSettingsTest {
val fakeTimeProvider = FakeTimeProvider()
val remoteSettings =
- buildRemoteSettings(
+ RemoteSettings(
fakeTimeProvider,
firebaseInstallations,
SessionEvents.getApplicationInfo(firebaseApp),
@@ -137,8 +121,6 @@ class RemoteSettingsTest {
fakeFetcher.responseJSONObject = fetchedResponse
remoteSettings.updateSettings()
- runCurrent()
-
assertThat(remoteSettings.sessionEnabled).isFalse()
assertThat(remoteSettings.samplingRate).isEqualTo(0.75)
assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes)
@@ -152,8 +134,6 @@ class RemoteSettingsTest {
fakeFetcher.responseJSONObject = fetchedResponse
remoteSettings.updateSettings()
- runCurrent()
-
assertThat(remoteSettings.sessionEnabled).isTrue()
assertThat(remoteSettings.samplingRate).isEqualTo(0.25)
assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(20.minutes)
@@ -162,110 +142,99 @@ class RemoteSettingsTest {
}
@Test
- fun remoteSettings_successfulFetchWithEmptyConfigRetainsOldConfigs() =
- runTest(UnconfinedTestDispatcher()) {
- val firebaseApp = FakeFirebaseApp().firebaseApp
- val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
- val fakeFetcher = FakeRemoteConfigFetcher()
- val fakeTimeProvider = FakeTimeProvider()
-
- val remoteSettings =
- buildRemoteSettings(
- fakeTimeProvider,
- firebaseInstallations,
- SessionEvents.getApplicationInfo(firebaseApp),
- fakeFetcher,
- FakeSettingsCache(),
- )
-
- val fetchedResponse = JSONObject(VALID_RESPONSE)
- fetchedResponse.getJSONObject("app_quality").put("cache_duration", 1)
- fakeFetcher.responseJSONObject = fetchedResponse
- remoteSettings.updateSettings()
-
- runCurrent()
- fakeTimeProvider.addInterval(31.seconds)
-
- assertThat(remoteSettings.sessionEnabled).isFalse()
- assertThat(remoteSettings.samplingRate).isEqualTo(0.75)
- assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes)
-
- fetchedResponse.remove("app_quality")
-
- // Sleep for a second before updating configs
- Thread.sleep(2000)
-
- fakeFetcher.responseJSONObject = fetchedResponse
- remoteSettings.updateSettings()
-
- runCurrent()
- Thread.sleep(30)
-
- assertThat(remoteSettings.sessionEnabled).isFalse()
- assertThat(remoteSettings.samplingRate).isEqualTo(0.75)
- assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes)
-
- remoteSettings.clearCachedSettings()
- }
+ fun remoteSettings_successfulFetchWithEmptyConfigRetainsOldConfigs() = runTest {
+ val firebaseApp = FakeFirebaseApp().firebaseApp
+ val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
+ val fakeFetcher = FakeRemoteConfigFetcher()
+ val fakeTimeProvider = FakeTimeProvider()
+
+ val remoteSettings =
+ RemoteSettings(
+ fakeTimeProvider,
+ firebaseInstallations,
+ SessionEvents.getApplicationInfo(firebaseApp),
+ fakeFetcher,
+ FakeSettingsCache(),
+ )
+
+ val fetchedResponse = JSONObject(VALID_RESPONSE)
+ fetchedResponse.getJSONObject("app_quality").put("cache_duration", 1)
+ fakeFetcher.responseJSONObject = fetchedResponse
+ remoteSettings.updateSettings()
+
+ fakeTimeProvider.addInterval(31.seconds)
+
+ assertThat(remoteSettings.sessionEnabled).isFalse()
+ assertThat(remoteSettings.samplingRate).isEqualTo(0.75)
+ assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes)
+
+ fetchedResponse.remove("app_quality")
+
+ fakeFetcher.responseJSONObject = fetchedResponse
+ remoteSettings.updateSettings()
+
+ assertThat(remoteSettings.sessionEnabled).isFalse()
+ assertThat(remoteSettings.samplingRate).isEqualTo(0.75)
+ assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes)
+
+ remoteSettings.clearCachedSettings()
+ }
@Test
- fun remoteSettings_fetchWhileFetchInProgress() =
- runTest(UnconfinedTestDispatcher()) {
- // This test does:
- // 1. Do a fetch with a fake fetcher that will block for 3 seconds.
- // 2. While that is happening, do a second fetch.
- // - First fetch is still fetching, so second fetch should fall through to the mutex.
- // - Second fetch will be blocked until first completes.
- // - First fetch returns, should unblock the second fetch.
- // - Second fetch should go into mutex, sees cache is valid in "double check," exist early.
- // 3. After a fetch completes, do a third fetch.
- // - First fetch should have have updated the cache.
- // - Third fetch should exit even earlier, never having gone into the mutex.
-
- val firebaseApp = FakeFirebaseApp().firebaseApp
- val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
- val fakeFetcherWithDelay =
- FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE), networkDelay = 3.seconds)
-
- fakeFetcherWithDelay.responseJSONObject
- .getJSONObject("app_quality")
- .put("sampling_rate", 0.125)
-
- val remoteSettingsWithDelay =
- buildRemoteSettings(
- FakeTimeProvider(),
- firebaseInstallations,
- SessionEvents.getApplicationInfo(firebaseApp),
- configsFetcher = fakeFetcherWithDelay,
- FakeSettingsCache(),
- )
-
- // Do the first fetch. This one should fetched the configsFetcher.
- val firstFetch = launch(Dispatchers.Default) { remoteSettingsWithDelay.updateSettings() }
-
- // Wait a second, and then do the second fetch while first is still running.
- // This one should block until the first fetch completes, but then exit early.
- launch(Dispatchers.Default) {
- delay(1.seconds)
- remoteSettingsWithDelay.updateSettings()
- }
+ fun remoteSettings_fetchWhileFetchInProgress() = runTest {
+ // This test does:
+ // 1. Do a fetch with a fake fetcher that will block for 3 seconds.
+ // 2. While that is happening, do a second fetch.
+ // - First fetch is still fetching, so second fetch should fall through to the mutex.
+ // - Second fetch will be blocked until first completes.
+ // - First fetch returns, should unblock the second fetch.
+ // - Second fetch should go into mutex, sees cache is valid in "double check," exist early.
+ // 3. After a fetch completes, do a third fetch.
+ // - First fetch should have have updated the cache.
+ // - Third fetch should exit even earlier, never having gone into the mutex.
+
+ val firebaseApp = FakeFirebaseApp().firebaseApp
+ val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
+ val fakeFetcherWithDelay =
+ FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE), networkDelay = 3.seconds)
+
+ fakeFetcherWithDelay.responseJSONObject.getJSONObject("app_quality").put("sampling_rate", 0.125)
+
+ val remoteSettingsWithDelay =
+ RemoteSettings(
+ FakeTimeProvider(),
+ firebaseInstallations,
+ SessionEvents.getApplicationInfo(firebaseApp),
+ configsFetcher = fakeFetcherWithDelay,
+ FakeSettingsCache(),
+ )
- // Wait until the first fetch is done, then do a third fetch.
- // This one should not even block, and exit early.
- firstFetch.join()
- withTimeout(1.seconds) { remoteSettingsWithDelay.updateSettings() }
+ // Do the first fetch. This one should fetched the configsFetcher.
+ val firstFetch = launch(Dispatchers.Default) { remoteSettingsWithDelay.updateSettings() }
- // Assert that the configsFetcher was fetched exactly once.
- assertThat(fakeFetcherWithDelay.timesCalled).isEqualTo(1)
- assertThat(remoteSettingsWithDelay.samplingRate).isEqualTo(0.125)
+ // Wait a second, and then do the second fetch while first is still running.
+ // This one should block until the first fetch completes, but then exit early.
+ launch(Dispatchers.Default) {
+ delay(1.seconds)
+ remoteSettingsWithDelay.updateSettings()
}
+ // Wait until the first fetch is done, then do a third fetch.
+ // This one should not even block, and exit early.
+ firstFetch.join()
+ withTimeout(1.seconds) { remoteSettingsWithDelay.updateSettings() }
+
+ // Assert that the configsFetcher was fetched exactly once.
+ assertThat(fakeFetcherWithDelay.timesCalled).isEqualTo(1)
+ assertThat(remoteSettingsWithDelay.samplingRate).isEqualTo(0.125)
+ }
+
@After
fun cleanUp() {
FirebaseApp.clearInstancesForTest()
}
- internal companion object {
+ private companion object {
const val VALID_RESPONSE =
"""
{
@@ -284,30 +253,5 @@ class RemoteSettingsTest {
}
}
"""
-
- /**
- * Build an instance of [RemoteSettings] using the Dagger factory.
- *
- * This is needed because the SDK vendors Dagger to a difference namespace, but it does not for
- * these unit tests. The [RemoteSettings.lazySettingsCache] has type [dagger.Lazy] in these
- * tests, but type `com.google.firebase.sessions.dagger.Lazy` in the SDK. This method to build
- * the instance is the easiest I could find that does not need any reference to [dagger.Lazy] in
- * the test code.
- */
- fun buildRemoteSettings(
- timeProvider: TimeProvider,
- firebaseInstallationsApi: FirebaseInstallationsApi,
- appInfo: ApplicationInfo,
- configsFetcher: CrashlyticsSettingsFetcher,
- settingsCache: SettingsCache,
- ): RemoteSettings =
- RemoteSettings_Factory.create(
- { timeProvider },
- { firebaseInstallationsApi },
- { appInfo },
- { configsFetcher },
- { settingsCache },
- )
- .get()
}
}
diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt
index f87d773b970..146857ae7f4 100644
--- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt
+++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt
@@ -20,7 +20,6 @@ import android.os.Bundle
import com.google.common.truth.Truth.assertThat
import com.google.firebase.FirebaseApp
import com.google.firebase.sessions.SessionEvents
-import com.google.firebase.sessions.settings.RemoteSettingsTest.Companion.buildRemoteSettings
import com.google.firebase.sessions.testing.FakeFirebaseApp
import com.google.firebase.sessions.testing.FakeFirebaseInstallations
import com.google.firebase.sessions.testing.FakeRemoteConfigFetcher
@@ -28,9 +27,6 @@ import com.google.firebase.sessions.testing.FakeSettingsCache
import com.google.firebase.sessions.testing.FakeSettingsProvider
import com.google.firebase.sessions.testing.FakeTimeProvider
import kotlin.time.Duration.Companion.minutes
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.json.JSONObject
import org.junit.After
@@ -38,7 +34,6 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
-@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
class SessionsSettingsTest {
@@ -89,128 +84,117 @@ class SessionsSettingsTest {
remoteSettings = FakeSettingsProvider(),
)
- runCurrent()
-
assertThat(sessionsSettings.sessionsEnabled).isFalse()
assertThat(sessionsSettings.samplingRate).isEqualTo(0.5)
assertThat(sessionsSettings.sessionRestartTimeout).isEqualTo(30.minutes)
}
@Test
- fun sessionSettings_remoteSettingsOverrideDefaultsWhenPresent() =
- runTest(UnconfinedTestDispatcher()) {
- val firebaseApp = FakeFirebaseApp().firebaseApp
- val context = firebaseApp.applicationContext
- val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
- val fakeFetcher = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE))
-
- val remoteSettings =
- buildRemoteSettings(
- FakeTimeProvider(),
- firebaseInstallations,
- SessionEvents.getApplicationInfo(firebaseApp),
- fakeFetcher,
- FakeSettingsCache(),
- )
-
- val sessionsSettings =
- SessionsSettings(
- localOverrideSettings = LocalOverrideSettings(context),
- remoteSettings = remoteSettings,
- )
-
- sessionsSettings.updateSettings()
-
- runCurrent()
-
- assertThat(sessionsSettings.sessionsEnabled).isFalse()
- assertThat(sessionsSettings.samplingRate).isEqualTo(0.75)
- assertThat(sessionsSettings.sessionRestartTimeout).isEqualTo(40.minutes)
-
- remoteSettings.clearCachedSettings()
- }
+ fun sessionSettings_remoteSettingsOverrideDefaultsWhenPresent() = runTest {
+ val firebaseApp = FakeFirebaseApp().firebaseApp
+ val context = firebaseApp.applicationContext
+ val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
+ val fakeFetcher = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE))
+
+ val remoteSettings =
+ RemoteSettings(
+ FakeTimeProvider(),
+ firebaseInstallations,
+ SessionEvents.getApplicationInfo(firebaseApp),
+ fakeFetcher,
+ FakeSettingsCache(),
+ )
+
+ val sessionsSettings =
+ SessionsSettings(
+ localOverrideSettings = LocalOverrideSettings(context),
+ remoteSettings = remoteSettings,
+ )
+
+ sessionsSettings.updateSettings()
+
+ assertThat(sessionsSettings.sessionsEnabled).isFalse()
+ assertThat(sessionsSettings.samplingRate).isEqualTo(0.75)
+ assertThat(sessionsSettings.sessionRestartTimeout).isEqualTo(40.minutes)
+
+ remoteSettings.clearCachedSettings()
+ }
@Test
- fun sessionSettings_manifestOverridesRemoteSettingsAndDefaultsWhenPresent() =
- runTest(UnconfinedTestDispatcher()) {
- val metadata = Bundle()
- metadata.putBoolean("firebase_sessions_enabled", true)
- metadata.putDouble("firebase_sessions_sampling_rate", 0.5)
- metadata.putInt("firebase_sessions_sessions_restart_timeout", 180)
- val firebaseApp = FakeFirebaseApp(metadata).firebaseApp
- val context = firebaseApp.applicationContext
- val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
- val fakeFetcher = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE))
-
- val remoteSettings =
- buildRemoteSettings(
- FakeTimeProvider(),
- firebaseInstallations,
- SessionEvents.getApplicationInfo(firebaseApp),
- fakeFetcher,
- FakeSettingsCache(),
- )
-
- val sessionsSettings =
- SessionsSettings(
- localOverrideSettings = LocalOverrideSettings(context),
- remoteSettings = remoteSettings,
- )
-
- sessionsSettings.updateSettings()
-
- runCurrent()
-
- assertThat(sessionsSettings.sessionsEnabled).isTrue()
- assertThat(sessionsSettings.samplingRate).isEqualTo(0.5)
- assertThat(sessionsSettings.sessionRestartTimeout).isEqualTo(3.minutes)
-
- remoteSettings.clearCachedSettings()
- }
+ fun sessionSettings_manifestOverridesRemoteSettingsAndDefaultsWhenPresent() = runTest {
+ val metadata = Bundle()
+ metadata.putBoolean("firebase_sessions_enabled", true)
+ metadata.putDouble("firebase_sessions_sampling_rate", 0.5)
+ metadata.putInt("firebase_sessions_sessions_restart_timeout", 180)
+ val firebaseApp = FakeFirebaseApp(metadata).firebaseApp
+ val context = firebaseApp.applicationContext
+ val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
+ val fakeFetcher = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE))
+
+ val remoteSettings =
+ RemoteSettings(
+ FakeTimeProvider(),
+ firebaseInstallations,
+ SessionEvents.getApplicationInfo(firebaseApp),
+ fakeFetcher,
+ FakeSettingsCache(),
+ )
+
+ val sessionsSettings =
+ SessionsSettings(
+ localOverrideSettings = LocalOverrideSettings(context),
+ remoteSettings = remoteSettings,
+ )
+
+ sessionsSettings.updateSettings()
+
+ assertThat(sessionsSettings.sessionsEnabled).isTrue()
+ assertThat(sessionsSettings.samplingRate).isEqualTo(0.5)
+ assertThat(sessionsSettings.sessionRestartTimeout).isEqualTo(3.minutes)
+
+ remoteSettings.clearCachedSettings()
+ }
@Test
- fun sessionSettings_invalidManifestConfigsDoNotOverride() =
- runTest(UnconfinedTestDispatcher()) {
- val metadata = Bundle()
- metadata.putBoolean("firebase_sessions_enabled", false)
- metadata.putDouble("firebase_sessions_sampling_rate", -0.2) // Invalid
- metadata.putInt("firebase_sessions_sessions_restart_timeout", -2) // Invalid
- val firebaseApp = FakeFirebaseApp(metadata).firebaseApp
- val context = firebaseApp.applicationContext
- val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
- val fakeFetcher = FakeRemoteConfigFetcher()
- val invalidResponse =
- VALID_RESPONSE.replace(
- "\"sampling_rate\":0.75,",
- "\"sampling_rate\":1.2,", // Invalid
- )
- fakeFetcher.responseJSONObject = JSONObject(invalidResponse)
-
- val remoteSettings =
- buildRemoteSettings(
- FakeTimeProvider(),
- firebaseInstallations,
- SessionEvents.getApplicationInfo(firebaseApp),
- fakeFetcher,
- FakeSettingsCache(),
- )
-
- val sessionsSettings =
- SessionsSettings(
- localOverrideSettings = LocalOverrideSettings(context),
- remoteSettings = remoteSettings,
- )
-
- sessionsSettings.updateSettings()
-
- runCurrent()
-
- assertThat(sessionsSettings.sessionsEnabled).isFalse() // Manifest
- assertThat(sessionsSettings.samplingRate).isEqualTo(1.0) // SDK default
- assertThat(sessionsSettings.sessionRestartTimeout).isEqualTo(40.minutes) // Remote
-
- remoteSettings.clearCachedSettings()
- }
+ fun sessionSettings_invalidManifestConfigsDoNotOverride() = runTest {
+ val metadata = Bundle()
+ metadata.putBoolean("firebase_sessions_enabled", false)
+ metadata.putDouble("firebase_sessions_sampling_rate", -0.2) // Invalid
+ metadata.putInt("firebase_sessions_sessions_restart_timeout", -2) // Invalid
+ val firebaseApp = FakeFirebaseApp(metadata).firebaseApp
+ val context = firebaseApp.applicationContext
+ val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
+ val fakeFetcher = FakeRemoteConfigFetcher()
+ val invalidResponse =
+ VALID_RESPONSE.replace(
+ "\"sampling_rate\":0.75,",
+ "\"sampling_rate\":1.2,", // Invalid
+ )
+ fakeFetcher.responseJSONObject = JSONObject(invalidResponse)
+
+ val remoteSettings =
+ RemoteSettings(
+ FakeTimeProvider(),
+ firebaseInstallations,
+ SessionEvents.getApplicationInfo(firebaseApp),
+ fakeFetcher,
+ FakeSettingsCache(),
+ )
+
+ val sessionsSettings =
+ SessionsSettings(
+ localOverrideSettings = LocalOverrideSettings(context),
+ remoteSettings = remoteSettings,
+ )
+
+ sessionsSettings.updateSettings()
+
+ assertThat(sessionsSettings.sessionsEnabled).isFalse() // Manifest
+ assertThat(sessionsSettings.samplingRate).isEqualTo(1.0) // SDK default
+ assertThat(sessionsSettings.sessionRestartTimeout).isEqualTo(40.minutes) // Remote
+
+ remoteSettings.clearCachedSettings()
+ }
@After
fun cleanUp() {
diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SettingsCacheTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SettingsCacheTest.kt
index 729208c33ca..a8d8429b5a8 100644
--- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SettingsCacheTest.kt
+++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SettingsCacheTest.kt
@@ -16,23 +16,44 @@
package com.google.firebase.sessions.settings
+import android.content.Context
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.dataStoreFile
+import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import com.google.firebase.FirebaseApp
import com.google.firebase.sessions.testing.FakeTimeProvider
-import com.google.firebase.sessions.testing.TestDataStores
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
+@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
class SettingsCacheTest {
+ private val appContext: Context = ApplicationProvider.getApplicationContext()
@Test
fun sessionCache_returnsEmptyCache() = runTest {
val fakeTimeProvider = FakeTimeProvider()
- val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore)
+ val settingsCache =
+ SettingsCacheImpl(
+ backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"),
+ timeProvider = fakeTimeProvider,
+ sessionConfigsDataStore =
+ DataStoreFactory.create(
+ serializer = SessionConfigsSerializer,
+ scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")),
+ produceFile = { appContext.dataStoreFile("sessionConfigsDataStore.data") },
+ ),
+ )
+
+ runCurrent()
assertThat(settingsCache.sessionSamplingRate()).isNull()
assertThat(settingsCache.sessionsEnabled()).isNull()
@@ -43,14 +64,24 @@ class SettingsCacheTest {
@Test
fun settingConfigsReturnsCachedValue() = runTest {
val fakeTimeProvider = FakeTimeProvider()
- val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore)
+ val settingsCache =
+ SettingsCacheImpl(
+ backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"),
+ timeProvider = fakeTimeProvider,
+ sessionConfigsDataStore =
+ DataStoreFactory.create(
+ serializer = SessionConfigsSerializer,
+ scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")),
+ produceFile = { appContext.dataStoreFile("sessionConfigsDataStore.data") },
+ ),
+ )
settingsCache.updateConfigs(
SessionConfigs(
sessionsEnabled = false,
sessionSamplingRate = 0.25,
sessionTimeoutSeconds = 600,
- cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms,
+ cacheUpdatedTimeSeconds = fakeTimeProvider.currentTime().seconds,
cacheDurationSeconds = 1000,
)
)
@@ -66,22 +97,40 @@ class SettingsCacheTest {
@Test
fun settingConfigsReturnsPreviouslyStoredValue() = runTest {
+ val sessionConfigsDataStore =
+ DataStoreFactory.create(
+ serializer = SessionConfigsSerializer,
+ scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")),
+ produceFile = { appContext.dataStoreFile("sessionConfigsDataStore.data") },
+ )
+
val fakeTimeProvider = FakeTimeProvider()
- val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore)
+ val settingsCache =
+ SettingsCacheImpl(
+ backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"),
+ timeProvider = fakeTimeProvider,
+ sessionConfigsDataStore = sessionConfigsDataStore,
+ )
settingsCache.updateConfigs(
SessionConfigs(
sessionsEnabled = false,
sessionSamplingRate = 0.25,
sessionTimeoutSeconds = 600,
- cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms,
+ cacheUpdatedTimeSeconds = fakeTimeProvider.currentTime().seconds,
cacheDurationSeconds = 1000,
)
)
// Create a new instance to imitate a second app launch.
val newSettingsCache =
- SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore)
+ SettingsCacheImpl(
+ backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"),
+ timeProvider = fakeTimeProvider,
+ sessionConfigsDataStore = sessionConfigsDataStore,
+ )
+
+ runCurrent()
assertThat(newSettingsCache.sessionsEnabled()).isFalse()
assertThat(newSettingsCache.sessionSamplingRate()).isEqualTo(0.25)
@@ -96,14 +145,24 @@ class SettingsCacheTest {
@Test
fun settingConfigsReturnsCacheExpiredWithShortCacheDuration() = runTest {
val fakeTimeProvider = FakeTimeProvider()
- val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore)
+ val settingsCache =
+ SettingsCacheImpl(
+ backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"),
+ timeProvider = fakeTimeProvider,
+ sessionConfigsDataStore =
+ DataStoreFactory.create(
+ serializer = SessionConfigsSerializer,
+ scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")),
+ produceFile = { appContext.dataStoreFile("sessionConfigsDataStore.data") },
+ ),
+ )
settingsCache.updateConfigs(
SessionConfigs(
sessionsEnabled = false,
sessionSamplingRate = 0.25,
sessionTimeoutSeconds = 600,
- cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms,
+ cacheUpdatedTimeSeconds = fakeTimeProvider.currentTime().seconds,
cacheDurationSeconds = 0,
)
)
@@ -119,14 +178,24 @@ class SettingsCacheTest {
@Test
fun settingConfigsReturnsCachedValueWithPartialConfigs() = runTest {
val fakeTimeProvider = FakeTimeProvider()
- val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore)
+ val settingsCache =
+ SettingsCacheImpl(
+ backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"),
+ timeProvider = fakeTimeProvider,
+ sessionConfigsDataStore =
+ DataStoreFactory.create(
+ serializer = SessionConfigsSerializer,
+ scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")),
+ produceFile = { appContext.dataStoreFile("sessionConfigsDataStore.data") },
+ ),
+ )
settingsCache.updateConfigs(
SessionConfigs(
sessionsEnabled = false,
sessionSamplingRate = 0.25,
sessionTimeoutSeconds = null,
- cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms,
+ cacheUpdatedTimeSeconds = fakeTimeProvider.currentTime().seconds,
cacheDurationSeconds = 1000,
)
)
@@ -142,14 +211,24 @@ class SettingsCacheTest {
@Test
fun settingConfigsAllowsUpdateConfigsAndCachesValues() = runTest {
val fakeTimeProvider = FakeTimeProvider()
- val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore)
+ val settingsCache =
+ SettingsCacheImpl(
+ backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"),
+ timeProvider = fakeTimeProvider,
+ sessionConfigsDataStore =
+ DataStoreFactory.create(
+ serializer = SessionConfigsSerializer,
+ scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")),
+ produceFile = { appContext.dataStoreFile("sessionConfigsDataStore.data") },
+ ),
+ )
settingsCache.updateConfigs(
SessionConfigs(
sessionsEnabled = false,
sessionSamplingRate = 0.25,
sessionTimeoutSeconds = 600,
- cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms,
+ cacheUpdatedTimeSeconds = fakeTimeProvider.currentTime().seconds,
cacheDurationSeconds = 1000,
)
)
@@ -164,7 +243,7 @@ class SettingsCacheTest {
sessionsEnabled = true,
sessionSamplingRate = 0.33,
sessionTimeoutSeconds = 100,
- cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms,
+ cacheUpdatedTimeSeconds = fakeTimeProvider.currentTime().seconds,
cacheDurationSeconds = 0,
)
)
@@ -180,14 +259,24 @@ class SettingsCacheTest {
@Test
fun settingConfigsCleansCacheForNullValues() = runTest {
val fakeTimeProvider = FakeTimeProvider()
- val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore)
+ val settingsCache =
+ SettingsCacheImpl(
+ backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"),
+ timeProvider = fakeTimeProvider,
+ sessionConfigsDataStore =
+ DataStoreFactory.create(
+ serializer = SessionConfigsSerializer,
+ scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")),
+ produceFile = { appContext.dataStoreFile("sessionConfigsDataStore.data") },
+ ),
+ )
settingsCache.updateConfigs(
SessionConfigs(
sessionsEnabled = false,
sessionSamplingRate = 0.25,
sessionTimeoutSeconds = 600,
- cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms,
+ cacheUpdatedTimeSeconds = fakeTimeProvider.currentTime().seconds,
cacheDurationSeconds = 1000,
)
)
@@ -202,7 +291,7 @@ class SettingsCacheTest {
sessionsEnabled = null,
sessionSamplingRate = 0.33,
sessionTimeoutSeconds = null,
- cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms,
+ cacheUpdatedTimeSeconds = fakeTimeProvider.currentTime().seconds,
cacheDurationSeconds = 1000,
)
)
diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSettingsCache.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSettingsCache.kt
index 2a3e28c00b9..2c58ef22d7d 100644
--- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSettingsCache.kt
+++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSettingsCache.kt
@@ -27,11 +27,11 @@ internal class FakeSettingsCache(
private var sessionConfigs: SessionConfigs = SessionConfigsSerializer.defaultValue,
) : SettingsCache {
override fun hasCacheExpired(): Boolean {
- val cacheUpdatedTimeMs = sessionConfigs.cacheUpdatedTimeMs
+ val cacheUpdatedTimeSeconds = sessionConfigs.cacheUpdatedTimeSeconds
val cacheDurationSeconds = sessionConfigs.cacheDurationSeconds
- if (cacheUpdatedTimeMs != null && cacheDurationSeconds != null) {
- val timeDifferenceSeconds = (timeProvider.currentTime().ms - cacheUpdatedTimeMs) / 1000
+ if (cacheUpdatedTimeSeconds != null && cacheDurationSeconds != null) {
+ val timeDifferenceSeconds = timeProvider.currentTime().seconds - cacheUpdatedTimeSeconds
if (timeDifferenceSeconds < cacheDurationSeconds) {
return false
}
diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestDataStores.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestDataStores.kt
deleted file mode 100644
index d7cc3a7f67d..00000000000
--- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestDataStores.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2025 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.sessions.testing
-
-import android.content.Context
-import androidx.datastore.core.DataStore
-import androidx.datastore.core.DataStoreFactory
-import androidx.datastore.dataStoreFile
-import androidx.test.core.app.ApplicationProvider
-import com.google.firebase.sessions.SessionData
-import com.google.firebase.sessions.SessionDataSerializer
-import com.google.firebase.sessions.settings.SessionConfigs
-import com.google.firebase.sessions.settings.SessionConfigsSerializer
-
-/**
- * Container of instances of [DataStore] for testing.
- *
- * Note these do not pass the test scheduler to the instances, so won't work with `runCurrent`.
- */
-internal object TestDataStores {
- private val appContext: Context = ApplicationProvider.getApplicationContext()
-
- val sessionConfigsDataStore: DataStore by lazy {
- DataStoreFactory.create(
- serializer = SessionConfigsSerializer,
- produceFile = { appContext.dataStoreFile("sessionConfigsTestDataStore.data") },
- )
- }
-
- val sessionDataStore: DataStore by lazy {
- DataStoreFactory.create(
- serializer = SessionDataSerializer,
- produceFile = { appContext.dataStoreFile("sessionDataTestDataStore.data") },
- )
- }
-}
From c726a1a0c63f47f99be0d51f23157668d4817280 Mon Sep 17 00:00:00 2001
From: Daymon <17409137+daymxn@users.noreply.github.com>
Date: Thu, 6 Feb 2025 17:06:40 -0600
Subject: [PATCH 040/146] Fix ModuleVersion bumping (#6679)
Per [b/394908865](https://b.corp.google.com/issues/394908865),
This fixes an issue where `ModuleVersion.bump()` was not properly
resetting the smaller version types. Additionally, this fixes some other
minor issues with bom generation.
Namely, this PR also fixes:
- [b/394908773](https://b.corp.google.com/issues/394908773) -> Fix bom
release note ordering
- [b/394909103](https://b.corp.google.com/issues/394909103) -> Separate
published bom artifacts
---
.github/workflows/make-bom.yml | 36 ++++++----
.../GenerateBomReleaseNotesTask.kt | 7 +-
.../gradle/bomgenerator/GenerateBomTask.kt | 4 +-
.../gradle/plugins/KotlinExtensions.kt | 37 ++++++++++
.../firebase/gradle/plugins/ModuleVersion.kt | 7 +-
.../gradle/plugins/PublishingPlugin.kt | 16 ++++-
.../gradle/plugins/datamodels/PomElement.kt | 24 +++++--
.../plugins/GenerateBomReleaseNotesTests.kt | 69 +++++++++++++++++--
.../gradle/plugins/GenerateBomTests.kt | 4 +-
.../gradle/plugins/ModuleVersionTests.kt | 10 +++
10 files changed, 176 insertions(+), 38 deletions(-)
diff --git a/.github/workflows/make-bom.yml b/.github/workflows/make-bom.yml
index 0ad2ecf4add..0e7d63f5c96 100644
--- a/.github/workflows/make-bom.yml
+++ b/.github/workflows/make-bom.yml
@@ -11,7 +11,9 @@ jobs:
uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3
with:
python-version: '3.10'
+
- uses: actions/checkout@v4.1.1
+
- name: Set up JDK 17
uses: actions/setup-java@v4.1.0
with:
@@ -21,19 +23,25 @@ jobs:
- name: Build
run: |
- ./ci/run.sh \
- --artifact-target-dir=./logs/artifacts \
- --artifact-patterns=bom.zip \
- --artifact-patterns=bomReleaseNotes.md \
- --artifact-patterns=recipeVersionUpdate.txt \
- gradle \
- -- \
- --build-cache \
- buildBomZip
-
- - name: Upload generated artifacts
+ ./gradlew buildBomBundleZip
+
+ - name: Upload bom
+ uses: actions/upload-artifact@v4.3.3
+ with:
+ name: bom
+ path: build/bom/
+ retention-days: 15
+
+ - name: Upload release notes
+ uses: actions/upload-artifact@v4.3.3
+ with:
+ name: bom_release_notes
+ path: build/bomReleaseNotes.md
+ retention-days: 15
+
+ - name: Upload recipe version update
uses: actions/upload-artifact@v4.3.3
with:
- name: artifacts
- path: ./logs/artifacts/
- retention-days: 5
+ name: recipe_version
+ path: build/recipeVersionUpdate.txt
+ retention-days: 15
diff --git a/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateBomReleaseNotesTask.kt b/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateBomReleaseNotesTask.kt
index 72fe51a8017..75bb991dd60 100644
--- a/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateBomReleaseNotesTask.kt
+++ b/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateBomReleaseNotesTask.kt
@@ -51,7 +51,7 @@ abstract class GenerateBomReleaseNotesTask : DefaultTask() {
val previousDeps = previousBom.get().dependencyManagement?.dependencies.orEmpty()
previousBomVersions.set(previousDeps.associate { it.fullArtifactName to it.version })
- val sortedDependencies = currentDeps.sortedBy { it.version }
+ val sortedDependencies = currentDeps.sortedBy { it.toString() }
val headingId = "{: #bom_v${bom.version.replace(".", "-")}}"
@@ -71,8 +71,9 @@ abstract class GenerateBomReleaseNotesTask : DefaultTask() {
| Firebase Android SDKs mapped to this {{bom}} version
|
|
- | Libraries that were versioned with this release are in highlighted rows.
- | Refer to a library's release notes (on this page) for details about its changes.
+ | Libraries that were versioned with this release are in highlighted rows.
+ |
Refer to a library's release notes (on this page) for details about its
+ | changes.
|
|
|
diff --git a/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateBomTask.kt b/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateBomTask.kt
index 8e5599a530a..9fb7fe35130 100644
--- a/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateBomTask.kt
+++ b/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateBomTask.kt
@@ -25,8 +25,8 @@ import com.google.firebase.gradle.plugins.datamodels.LicenseElement
import com.google.firebase.gradle.plugins.datamodels.PomElement
import com.google.firebase.gradle.plugins.datamodels.fullArtifactName
import com.google.firebase.gradle.plugins.datamodels.moduleVersion
-import com.google.firebase.gradle.plugins.diff
import com.google.firebase.gradle.plugins.orEmpty
+import com.google.firebase.gradle.plugins.pairBy
import com.google.firebase.gradle.plugins.partitionNotNull
import com.google.firebase.gradle.plugins.services.GMavenService
import org.gradle.api.DefaultTask
@@ -144,7 +144,7 @@ abstract class GenerateBomTask : DefaultTask() {
val oldBomVersion = ModuleVersion.fromString(oldBom.artifactId, oldBom.version)
val oldBomDependencies = oldBom.dependencyManagement?.dependencies.orEmpty()
- val changedDependencies = oldBomDependencies.diff(releasingDependencies)
+ val changedDependencies = oldBomDependencies.pairBy(releasingDependencies) { it.artifactId }
val versionBumps =
changedDependencies.mapNotNull { (old, new) ->
diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/KotlinExtensions.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/KotlinExtensions.kt
index 8b7634f4500..8f058603e3b 100644
--- a/plugins/src/main/java/com/google/firebase/gradle/plugins/KotlinExtensions.kt
+++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/KotlinExtensions.kt
@@ -296,6 +296,43 @@ infix fun List.diff(other: List): List> {
return firstList.zip(secondList).filter { it.first != it.second }
}
+/**
+ * Creates a list of pairs between two lists, matching according to the provided [mapper].
+ *
+ * ```kotlin
+ * data class Person(name: String, age: Int)
+ *
+ * val firstList = listOf(
+ * Person("Mike", 5),
+ * Person("Rachel", 6)
+ * )
+ *
+ * val secondList = listOf(
+ * Person("Michael", 4),
+ * Person("Mike", 1)
+ * )
+ *
+ * val diffList = firstList.pairBy(secondList) {
+ * it.name
+ * }
+ *
+ * diffList shouldBeEqualTo listOf(
+ * Person("Mike", 5) to Person("Mike", 1)
+ * Person("Rachel", 6) to null
+ * null to Person("Mike", 1)
+ * )
+ * ```
+ */
+inline fun List.pairBy(other: List, mapper: (T) -> R): List> {
+ val firstMap = associateBy { mapper(it) }
+ val secondMap = other.associateBy { mapper(it) }
+
+ val changedOrRemoved = firstMap.map { it.value to secondMap[it.key] }
+ val added = secondMap.filterKeys { it !in firstMap }.map { null to it.value }
+
+ return changedOrRemoved + added
+}
+
/**
* Creates a list that is forced to certain size.
*
diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/ModuleVersion.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/ModuleVersion.kt
index c07b382aac6..3479c536699 100644
--- a/plugins/src/main/java/com/google/firebase/gradle/plugins/ModuleVersion.kt
+++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/ModuleVersion.kt
@@ -269,9 +269,10 @@ data class ModuleVersion(
.let { it ?: if (pre != null) VersionType.PRE else VersionType.PATCH }
.let {
when (it) {
- VersionType.MAJOR -> copy(major = major + 1)
- VersionType.MINOR -> copy(minor = minor + 1)
- VersionType.PATCH -> copy(patch = patch + 1)
+ VersionType.MAJOR ->
+ copy(major = major + 1, minor = 0, patch = 0, pre = pre?.copy(build = 1))
+ VersionType.MINOR -> copy(minor = minor + 1, patch = 0, pre = pre?.copy(build = 1))
+ VersionType.PATCH -> copy(patch = patch + 1, pre = pre?.copy(build = 1))
VersionType.PRE -> copy(pre = pre?.bump())
}
}
diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt
index b698dc08ad8..82a2aaae858 100644
--- a/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt
+++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt
@@ -61,6 +61,8 @@ import org.gradle.kotlin.dsl.register
* outside of the standard [FIREBASE_PUBLISH_TASK] workflow (possibly at a later time in the release
* cycle):
* - [BUILD_BOM_ZIP_TASK] -> Creates a zip file of the contents of [GENERATE_BOM_TASK]
+ * [registerGenerateBomTask]
+ * - [BUILD_BOM_BUNDLE_ZIP_TASK] -> Creates a zip file of the contents of [BUILD_BOM_ZIP_TASK]
* [registerGenerateBomTask],
* [GENERATE_BOM_RELEASE_NOTES_TASK][registerGenerateBomReleaseNotesTask] and
* [GENERATE_TUTORIAL_BUNDLE_TASK][registerGenerateTutorialBundleTask]
@@ -140,9 +142,16 @@ abstract class PublishingPlugin : Plugin {
destinationDirectory.set(project.layout.buildDirectory)
}
- project.tasks.register(BUILD_BOM_ZIP_TASK) {
- from(generateBom, generateBomReleaseNotes, generateTutorialBundle)
- archiveFileName.set("bom.zip")
+ val buildBomZip =
+ project.tasks.register(BUILD_BOM_ZIP_TASK) {
+ from(generateBom)
+ archiveFileName.set("bom.zip")
+ destinationDirectory.set(project.layout.buildDirectory)
+ }
+
+ project.tasks.register(BUILD_BOM_BUNDLE_ZIP_TASK) {
+ from(buildBomZip, generateBomReleaseNotes, generateTutorialBundle)
+ archiveFileName.set("bomBundle.zip")
destinationDirectory.set(project.layout.projectDirectory)
}
@@ -757,6 +766,7 @@ abstract class PublishingPlugin : Plugin {
const val BUILD_KOTLINDOC_ZIP_TASK = "buildKotlindocZip"
const val BUILD_RELEASE_NOTES_ZIP_TASK = "buildReleaseNotesZip"
const val BUILD_BOM_ZIP_TASK = "buildBomZip"
+ const val BUILD_BOM_BUNDLE_ZIP_TASK = "buildBomBundleZip"
const val FIREBASE_PUBLISH_TASK = "firebasePublish"
const val PUBLISH_ALL_TO_BUILD_TASK = "publishAllToBuildDir"
diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/datamodels/PomElement.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/datamodels/PomElement.kt
index e3fccfb66e9..dda8db2896d 100644
--- a/plugins/src/main/java/com/google/firebase/gradle/plugins/datamodels/PomElement.kt
+++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/datamodels/PomElement.kt
@@ -158,7 +158,7 @@ data class PomElement(
@XmlElement val artifactId: String,
@XmlElement val version: String,
@XmlElement val packaging: String? = null,
- @XmlChildrenName("licenses") val licenses: List? = null,
+ @XmlChildrenName("license") val licenses: List? = null,
@XmlElement val scm: SourceControlManagement? = null,
@XmlElement val dependencyManagement: DependencyManagementElement? = null,
@XmlChildrenName("dependency") val dependencies: List? = null,
@@ -171,15 +171,25 @@ data class PomElement(
* @see fromFile
*/
fun toFile(file: File): File {
- val xmlWriter = XML {
- indent = 2
- xmlDeclMode = XmlDeclMode.None
- }
- file.writeText(xmlWriter.encodeToString(this))
+ file.writeText(toString())
return file
}
+ /**
+ * Serializes this pom element into a valid XML element.
+ *
+ * @see toFile
+ */
+ override fun toString(): String {
+ return xml.encodeToString(this)
+ }
+
companion object {
+ private val xml = XML {
+ indent = 2
+ xmlDeclMode = XmlDeclMode.None
+ }
+
/**
* Deserializes a [PomElement] from a `pom.xml` file.
*
@@ -201,6 +211,6 @@ data class PomElement(
* @see fromFile
*/
fun fromElement(element: Element): PomElement =
- XML.decodeFromReader(xmlStreaming.newReader(element))
+ xml.decodeFromReader(xmlStreaming.newReader(element))
}
}
diff --git a/plugins/src/test/kotlin/com/google/firebase/gradle/plugins/GenerateBomReleaseNotesTests.kt b/plugins/src/test/kotlin/com/google/firebase/gradle/plugins/GenerateBomReleaseNotesTests.kt
index 3d914df29d8..182d7d0ffb2 100644
--- a/plugins/src/test/kotlin/com/google/firebase/gradle/plugins/GenerateBomReleaseNotesTests.kt
+++ b/plugins/src/test/kotlin/com/google/firebase/gradle/plugins/GenerateBomReleaseNotesTests.kt
@@ -65,8 +65,68 @@ class GenerateBomReleaseNotesTests : FunSpec() {
Firebase Android SDKs mapped to this {{bom}} version
- Libraries that were versioned with this release are in highlighted rows.
- Refer to a library's release notes (on this page) for details about its changes.
+ Libraries that were versioned with this release are in highlighted rows.
+
Refer to a library's release notes (on this page) for details about its
+ changes.
+
+
+
+ Artifact name |
+ Version mapped to previous {{bom}} v1.0.0 |
+ Version mapped to this {{bom}} v1.0.0 |
+
+
+
+ com.google.firebase:firebase-auth |
+ 10.0.0 |
+ 10.0.0 |
+
+
+ com.google.firebase:firebase-firestore |
+ 10.0.0 |
+ 10.0.0 |
+
+
+
+
+ """
+ .trimIndent()
+ }
+
+ @Test
+ fun `sorts the entries alphabetically`() {
+ val dependencies =
+ listOf(
+ ArtifactDependency(
+ groupId = "com.google.firebase",
+ artifactId = "firebase-firestore",
+ version = "10.0.0",
+ ),
+ ArtifactDependency(
+ groupId = "com.google.firebase",
+ artifactId = "firebase-auth",
+ version = "10.0.0",
+ ),
+ )
+ val bom = makeBom("1.0.0", dependencies)
+ val file = makeReleaseNotes(bom, bom)
+
+ file.readText().trim() shouldBeText
+ """
+ ### {{firebase_bom_long}} ({{bill_of_materials}}) version 1.0.0 {: #bom_v1-0-0}
+ {% comment %}
+ These library versions must be flat-typed, do not use variables.
+ The release note for this BoM version is a library-version snapshot.
+ {% endcomment %}
+
+
+
+ Firebase Android SDKs mapped to this {{bom}} version
+
+
+ Libraries that were versioned with this release are in highlighted rows.
+
Refer to a library's release notes (on this page) for details about its
+ changes.
@@ -147,8 +207,9 @@ class GenerateBomReleaseNotesTests : FunSpec() {
Firebase Android SDKs mapped to this {{bom}} version
- Libraries that were versioned with this release are in highlighted rows.
- Refer to a library's release notes (on this page) for details about its changes.
+ Libraries that were versioned with this release are in highlighted rows.
+
Refer to a library's release notes (on this page) for details about its
+ changes.
diff --git a/plugins/src/test/kotlin/com/google/firebase/gradle/plugins/GenerateBomTests.kt b/plugins/src/test/kotlin/com/google/firebase/gradle/plugins/GenerateBomTests.kt
index 0298a4584f3..a1bd5fffbf1 100644
--- a/plugins/src/test/kotlin/com/google/firebase/gradle/plugins/GenerateBomTests.kt
+++ b/plugins/src/test/kotlin/com/google/firebase/gradle/plugins/GenerateBomTests.kt
@@ -243,11 +243,11 @@ class GenerateBomTests : FunSpec() {
1.0.1
pom
-
+
The Apache Software License, Version 2.0
http://www.apache.org/licenses/LICENSE-2.0.txt
repo
-
+
diff --git a/plugins/src/test/kotlin/com/google/firebase/gradle/plugins/ModuleVersionTests.kt b/plugins/src/test/kotlin/com/google/firebase/gradle/plugins/ModuleVersionTests.kt
index d5027399a0f..c6eb626825d 100644
--- a/plugins/src/test/kotlin/com/google/firebase/gradle/plugins/ModuleVersionTests.kt
+++ b/plugins/src/test/kotlin/com/google/firebase/gradle/plugins/ModuleVersionTests.kt
@@ -95,6 +95,16 @@ class ModuleVersionTests : FunSpec() {
ModuleVersion(1, 1, 1).apply { bump(PRE) shouldBe this }
}
+ @Test
+ fun `Bump resets the smaller version types`() {
+ val version = ModuleVersion(1, 1, 1, PreReleaseVersion(ALPHA, 2))
+
+ version.bump(PRE) shouldBe ModuleVersion(1, 1, 1, PreReleaseVersion(ALPHA, 3))
+ version.bump(PATCH) shouldBe ModuleVersion(1, 1, 2, PreReleaseVersion(ALPHA, 1))
+ version.bump(MINOR) shouldBe ModuleVersion(1, 2, 0, PreReleaseVersion(ALPHA, 1))
+ version.bump(MAJOR) shouldBe ModuleVersion(2, 0, 0, PreReleaseVersion(ALPHA, 1))
+ }
+
@Test
fun `Bump correctly chooses the smallest by default`() {
ModuleVersion(1, 1, 1).bump().patch shouldBe 2
From 7a13c36247d6fffb8d31e9f5dad67f764e7daf0d Mon Sep 17 00:00:00 2001
From: Google Open Source Bot
Date: Fri, 7 Feb 2025 10:16:22 -0800
Subject: [PATCH 041/146] m159 mergeback (#6680)
Auto-generated PR for cleaning up release m159
NO_RELEASE_CHANGE
---------
Co-authored-by: daymxn
Co-authored-by: Daymon
---
firebase-firestore/CHANGELOG.md | 11 ++++++++++-
firebase-firestore/gradle.properties | 4 ++--
firebase-vertexai/CHANGELOG.md | 4 +++-
firebase-vertexai/gradle.properties | 4 ++--
4 files changed, 17 insertions(+), 6 deletions(-)
diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md
index 6915eeaf788..66fce5b35ce 100644
--- a/firebase-firestore/CHANGELOG.md
+++ b/firebase-firestore/CHANGELOG.md
@@ -1,5 +1,14 @@
# Unreleased
-* [fixed] Fixed a server and SDK mismatch in unicode string sorting. [#6615](//github.com/firebase/firebase-android-sdk/pull/6615)
+
+
+# 25.1.2
+* [fixed] Fixed a server and sdk mismatch in unicode string sorting. [#6615](//github.com/firebase/firebase-android-sdk/pull/6615)
+
+
+## Kotlin
+The Kotlin extensions library transitively includes the updated
+`firebase-firestore` library. The Kotlin extensions library has no additional
+updates.
# 25.1.1
* [changed] Update Firestore proto definitions. [#6369](//github.com/firebase/firebase-android-sdk/pull/6369)
diff --git a/firebase-firestore/gradle.properties b/firebase-firestore/gradle.properties
index e5f0deed10c..baa5399b1dc 100644
--- a/firebase-firestore/gradle.properties
+++ b/firebase-firestore/gradle.properties
@@ -1,2 +1,2 @@
-version=25.1.2
-latestReleasedVersion=25.1.1
+version=25.1.3
+latestReleasedVersion=25.1.2
diff --git a/firebase-vertexai/CHANGELOG.md b/firebase-vertexai/CHANGELOG.md
index 5e8c2981970..6f16cb1fcc4 100644
--- a/firebase-vertexai/CHANGELOG.md
+++ b/firebase-vertexai/CHANGELOG.md
@@ -1,7 +1,9 @@
# Unreleased
-* [changed] Internal improvements to correctly handle empty responses from models.
+# 16.1.0
+* [changed] Internal improvements to correctly handle empty model responses.
+
# 16.0.2
* [fixed] Improved error message when using an invalid location. (#6428)
* [fixed] Fixed issue where Firebase App Check error tokens were unintentionally missing from the requests. (#6409)
diff --git a/firebase-vertexai/gradle.properties b/firebase-vertexai/gradle.properties
index 66585dec24c..e6719a371ef 100644
--- a/firebase-vertexai/gradle.properties
+++ b/firebase-vertexai/gradle.properties
@@ -12,5 +12,5 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-version=16.1.0
-latestReleasedVersion=16.0.2
+version=16.1.1
+latestReleasedVersion=16.1.0
From 0c02c4c0fb481c758653f51446511b22439f65d2 Mon Sep 17 00:00:00 2001
From: Denver Coneybeare
Date: Fri, 7 Feb 2025 23:08:27 +0000
Subject: [PATCH 042/146] dataconnect: DataConnectExecutableVersions.json
updated with versions 1.7.6 and 1.7.7 (#6684)
---
.../plugin/DataConnectExecutableVersions.json | 38 ++++++++++++++++++-
1 file changed, 37 insertions(+), 1 deletion(-)
diff --git a/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json b/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json
index c6d27b9d878..1854796df5e 100644
--- a/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json
+++ b/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json
@@ -1,5 +1,5 @@
{
- "defaultVersion": "1.7.5",
+ "defaultVersion": "1.7.7",
"versions": [
{
"version": "1.3.4",
@@ -378,6 +378,42 @@
"os": "linux",
"size": 25190552,
"sha512DigestHex": "14ec28595c1ebb2a7870e09c00c6401bebbe8b3ae56cdcc63e34cb86c1b0ec7ab359a16fb7d93f19b552343b0bd004f0990b847abee0b5feb19a720d48548432"
+ },
+ {
+ "version": "1.7.6",
+ "os": "windows",
+ "size": 25752064,
+ "sha512DigestHex": "c0541157251660c4fede1b021042f54ce15d885c4e2689316aaebf7903e6783e531b0c632f20dbe43b996e6a63dfa7d49c7b03d5c932248fcb95f3767a9ab4ac"
+ },
+ {
+ "version": "1.7.6",
+ "os": "macos",
+ "size": 25322240,
+ "sha512DigestHex": "5e9142bbcc4475a905c4b2e999998d7c450ec0852049012d8eefca7f264a1c880e9872d6fbb59fe770dd9388268b637a2011641173028bbbd4ddad71e8028d62"
+ },
+ {
+ "version": "1.7.6",
+ "os": "linux",
+ "size": 25235608,
+ "sha512DigestHex": "f55a259fdeaba503ff0f5202fbb2062619c14bc140c4c220d10c395bf8a587bc4352064d5f0800d52cb085d951460ebffdad26cfeb893894e655d0d74056e998"
+ },
+ {
+ "version": "1.7.7",
+ "os": "windows",
+ "size": 25788416,
+ "sha512DigestHex": "be114a86491cf0317e25437777febce6b2057ea4c1a4b1d26939d188091c3b1da19aeed7fcaa5085c687497842ee8330e1b623686899e0c7615340faa2b6eeff"
+ },
+ {
+ "version": "1.7.7",
+ "os": "macos",
+ "size": 25359104,
+ "sha512DigestHex": "009ade041a6c152b9657add2ad03f5e12058224679eef39cabeee0579c907a9be22e9ad212515e52491fbacd0aa477e6afd1684c0cddea037a76ba143ddc1b32"
+ },
+ {
+ "version": "1.7.7",
+ "os": "linux",
+ "size": 25268376,
+ "sha512DigestHex": "f55feb1ce670b4728bb30be138ab427545f77f63f9e11ee458096091c075699c647d5b768c642a1ef6b3569a2db87dbbed6f2fdaf64febd1154d1a730fda4a9c"
}
]
}
\ No newline at end of file
From d1cba61678235831d555e108d3ff14b5fa4c94ca Mon Sep 17 00:00:00 2001
From: Denver Coneybeare
Date: Fri, 7 Feb 2025 23:16:41 +0000
Subject: [PATCH 043/146] dataconnect.yaml: improve readability of the output
of the "tool versions" step (#6669)
---
.github/workflows/dataconnect.yml | 25 +++++++++++++---------
.github/workflows/dataconnect_demo_app.yml | 23 ++++++++++++--------
2 files changed, 29 insertions(+), 19 deletions(-)
diff --git a/.github/workflows/dataconnect.yml b/.github/workflows/dataconnect.yml
index 8671f5b0235..aa2a58fccc2 100644
--- a/.github/workflows/dataconnect.yml
+++ b/.github/workflows/dataconnect.yml
@@ -84,16 +84,21 @@ jobs:
- name: tool versions
continue-on-error: true
run: |
- set +e -v
- uname -a
- which java
- java -version
- which javac
- javac -version
- which node
- node --version
- ${{ env.FDC_FIREBASE_COMMAND }} --version
- ./gradlew --version
+ function run_cmd {
+ echo "==============================================================================="
+ echo "Running Command: $*"
+ ("$@" 2>&1) || echo "WARNING: command failed with non-zero exit code $?: $*"
+ }
+
+ run_cmd uname -a
+ run_cmd which java
+ run_cmd java -version
+ run_cmd which javac
+ run_cmd javac -version
+ run_cmd which node
+ run_cmd node --version
+ run_cmd ${{ env.FDC_FIREBASE_COMMAND }} --version
+ run_cmd ./gradlew --version
- name: Gradle assembleDebugAndroidTest
run: |
diff --git a/.github/workflows/dataconnect_demo_app.yml b/.github/workflows/dataconnect_demo_app.yml
index 7ce51814b4d..c401f296b71 100644
--- a/.github/workflows/dataconnect_demo_app.yml
+++ b/.github/workflows/dataconnect_demo_app.yml
@@ -89,15 +89,20 @@ jobs:
- name: tool versions
continue-on-error: true
run: |
- set +e -v
- which java
- java -version
- which javac
- javac -version
- which node
- node --version
- ${{ env.FDC_FIREBASE_COMMAND }} --version
- ./gradlew --version
+ function run_cmd {
+ echo "==============================================================================="
+ echo "Running Command: $*"
+ ("$@" 2>&1) || echo "WARNING: command failed with non-zero exit code $?: $*"
+ }
+
+ run_cmd which java
+ run_cmd java -version
+ run_cmd which javac
+ run_cmd javac -version
+ run_cmd which node
+ run_cmd node --version
+ run_cmd ${{ env.FDC_FIREBASE_COMMAND }} --version
+ run_cmd ./gradlew --version
- name: ./gradlew assemble test
run: |
From 944cdcac11897d909e58686b1a505b25c0cbea13 Mon Sep 17 00:00:00 2001
From: Denver Coneybeare
Date: Fri, 7 Feb 2025 23:36:48 +0000
Subject: [PATCH 044/146] dataconnect: fix cache key conflict on nighty
integration tests runs (#6667)
---
.github/workflows/dataconnect.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/dataconnect.yml b/.github/workflows/dataconnect.yml
index aa2a58fccc2..b452a0056aa 100644
--- a/.github/workflows/dataconnect.yml
+++ b/.github/workflows/dataconnect.yml
@@ -121,7 +121,7 @@ jobs:
path: |
~/.gradle/caches
~/.gradle/wrapper
- key: gradle-cache-jqnvfzw6w7
+ key: gradle-cache-jqnvfzw6w7-${{ github.run_id }}
- name: Enable KVM group permissions for Android Emulator
run: |
@@ -160,7 +160,7 @@ jobs:
path: |
~/.android/avd/*
~/.android/adb*
- key: avd-cache-zhdsn586je-api${{ env.FDC_ANDROID_EMULATOR_API_LEVEL }}
+ key: avd-cache-zhdsn586je-api${{ env.FDC_ANDROID_EMULATOR_API_LEVEL }}-${{ github.run_id }}
- name: Data Connect Emulator
run: |
From 582107a994fcef65232c2b723985e09c199d9b7c Mon Sep 17 00:00:00 2001
From: Rodrigo Lazo
Date: Fri, 7 Feb 2025 22:35:23 -0500
Subject: [PATCH 045/146] Add support for token-based usage metrics (#6658)
Token measurement is broken down by modaliy, with separate counters for
image, audio, etc.
Tests are in version 6.*, so this change also includes bumping
update_responses.sh
---
firebase-vertexai/CHANGELOG.md | 3 +-
firebase-vertexai/api.txt | 35 +++++++++-
firebase-vertexai/gradle.properties | 2 +-
.../firebase/vertexai/type/ContentModality.kt | 68 +++++++++++++++++++
.../vertexai/type/CountTokensResponse.kt | 20 ++++--
.../vertexai/type/ModalityTokenCount.kt | 41 +++++++++++
.../firebase/vertexai/type/UsageMetadata.kt | 18 ++++-
.../firebase/vertexai/UnarySnapshotTests.kt | 36 ++++++++--
firebase-vertexai/update_responses.sh | 2 +-
9 files changed, 209 insertions(+), 16 deletions(-)
create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ContentModality.kt
create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ModalityTokenCount.kt
diff --git a/firebase-vertexai/CHANGELOG.md b/firebase-vertexai/CHANGELOG.md
index 6f16cb1fcc4..029813fe9be 100644
--- a/firebase-vertexai/CHANGELOG.md
+++ b/firebase-vertexai/CHANGELOG.md
@@ -1,5 +1,5 @@
# Unreleased
-
+* [changed] Added support for modality-based token count. (#6658)
# 16.1.0
* [changed] Internal improvements to correctly handle empty model responses.
@@ -64,4 +64,3 @@
* [feature] Added support for `responseMimeType` in `GenerationConfig`.
* [changed] Renamed `GoogleGenerativeAIException` to `FirebaseVertexAIException`.
* [changed] Updated the KDocs for various classes and functions.
-
diff --git a/firebase-vertexai/api.txt b/firebase-vertexai/api.txt
index fe3003d880b..5bf1e3667bf 100644
--- a/firebase-vertexai/api.txt
+++ b/firebase-vertexai/api.txt
@@ -165,12 +165,30 @@ package com.google.firebase.vertexai.type {
method public static com.google.firebase.vertexai.type.Content content(String? role = "user", kotlin.jvm.functions.Function1 super com.google.firebase.vertexai.type.Content.Builder,kotlin.Unit> init);
}
+ public final class ContentModality {
+ method public int getOrdinal();
+ property public final int ordinal;
+ field public static final com.google.firebase.vertexai.type.ContentModality AUDIO;
+ field public static final com.google.firebase.vertexai.type.ContentModality.Companion Companion;
+ field public static final com.google.firebase.vertexai.type.ContentModality DOCUMENT;
+ field public static final com.google.firebase.vertexai.type.ContentModality IMAGE;
+ field public static final com.google.firebase.vertexai.type.ContentModality TEXT;
+ field public static final com.google.firebase.vertexai.type.ContentModality UNSPECIFIED;
+ field public static final com.google.firebase.vertexai.type.ContentModality VIDEO;
+ }
+
+ public static final class ContentModality.Companion {
+ }
+
public final class CountTokensResponse {
- ctor public CountTokensResponse(int totalTokens, Integer? totalBillableCharacters = null);
+ ctor public CountTokensResponse(int totalTokens, Integer? totalBillableCharacters = null, java.util.List? promptTokensDetails = null);
method public operator int component1();
method public operator Integer? component2();
+ method public operator java.util.List? component3();
+ method public java.util.List? getPromptTokensDetails();
method public Integer? getTotalBillableCharacters();
method public int getTotalTokens();
+ property public final java.util.List? promptTokensDetails;
property public final Integer? totalBillableCharacters;
property public final int totalTokens;
}
@@ -369,6 +387,15 @@ package com.google.firebase.vertexai.type {
public final class InvalidStateException extends com.google.firebase.vertexai.type.FirebaseVertexAIException {
}
+ public final class ModalityTokenCount {
+ method public operator com.google.firebase.vertexai.type.ContentModality component1();
+ method public operator int component2();
+ method public com.google.firebase.vertexai.type.ContentModality getModality();
+ method public int getTokenCount();
+ property public final com.google.firebase.vertexai.type.ContentModality modality;
+ property public final int tokenCount;
+ }
+
public interface Part {
}
@@ -549,12 +576,16 @@ package com.google.firebase.vertexai.type {
}
public final class UsageMetadata {
- ctor public UsageMetadata(int promptTokenCount, Integer? candidatesTokenCount, int totalTokenCount);
+ ctor public UsageMetadata(int promptTokenCount, Integer? candidatesTokenCount, int totalTokenCount, java.util.List? promptTokensDetails, java.util.List? candidatesTokensDetails);
method public Integer? getCandidatesTokenCount();
+ method public java.util.List? getCandidatesTokensDetails();
method public int getPromptTokenCount();
+ method public java.util.List? getPromptTokensDetails();
method public int getTotalTokenCount();
property public final Integer? candidatesTokenCount;
+ property public final java.util.List? candidatesTokensDetails;
property public final int promptTokenCount;
+ property public final java.util.List? promptTokensDetails;
property public final int totalTokenCount;
}
diff --git a/firebase-vertexai/gradle.properties b/firebase-vertexai/gradle.properties
index e6719a371ef..b686fdcb9db 100644
--- a/firebase-vertexai/gradle.properties
+++ b/firebase-vertexai/gradle.properties
@@ -12,5 +12,5 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-version=16.1.1
+version=16.2.0
latestReleasedVersion=16.1.0
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ContentModality.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ContentModality.kt
new file mode 100644
index 00000000000..dd928f92273
--- /dev/null
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ContentModality.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2025 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.vertexai.type
+
+import com.google.firebase.vertexai.common.util.FirstOrdinalSerializer
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/** Content part modality. */
+public class ContentModality private constructor(public val ordinal: Int) {
+
+ @Serializable(Internal.Serializer::class)
+ internal enum class Internal {
+ @SerialName("MODALITY_UNSPECIFIED") UNSPECIFIED,
+ TEXT,
+ IMAGE,
+ VIDEO,
+ AUDIO,
+ DOCUMENT;
+
+ internal object Serializer : KSerializer by FirstOrdinalSerializer(Internal::class)
+
+ internal fun toPublic() =
+ when (this) {
+ TEXT -> ContentModality.TEXT
+ IMAGE -> ContentModality.IMAGE
+ VIDEO -> ContentModality.VIDEO
+ AUDIO -> ContentModality.AUDIO
+ DOCUMENT -> ContentModality.DOCUMENT
+ else -> ContentModality.UNSPECIFIED
+ }
+ }
+
+ public companion object {
+ /** Unspecified modality. */
+ @JvmField public val UNSPECIFIED: ContentModality = ContentModality(0)
+
+ /** Plain text. */
+ @JvmField public val TEXT: ContentModality = ContentModality(1)
+
+ /** Image. */
+ @JvmField public val IMAGE: ContentModality = ContentModality(2)
+
+ /** Video. */
+ @JvmField public val VIDEO: ContentModality = ContentModality(3)
+
+ /** Audio. */
+ @JvmField public val AUDIO: ContentModality = ContentModality(4)
+
+ /** Document, e.g. PDF. */
+ @JvmField public val DOCUMENT: ContentModality = ContentModality(5)
+ }
+}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/CountTokensResponse.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/CountTokensResponse.kt
index 4c05521ad65..a6fe492862b 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/CountTokensResponse.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/CountTokensResponse.kt
@@ -30,21 +30,33 @@ import kotlinx.serialization.Serializable
* to the model as a prompt. **Important:** this property does not include billable image, video or
* other non-text input. See
* [Vertex AI pricing](https://cloud.google.com/vertex-ai/generative-ai/pricing) for details.
+ * @property promptTokensDetails The breakdown, by modality, of how many tokens are consumed by the
+ * prompt.
*/
public class CountTokensResponse(
public val totalTokens: Int,
- public val totalBillableCharacters: Int? = null
+ public val totalBillableCharacters: Int? = null,
+ public val promptTokensDetails: List? = null,
) {
public operator fun component1(): Int = totalTokens
public operator fun component2(): Int? = totalBillableCharacters
+ public operator fun component3(): List? = promptTokensDetails
+
@Serializable
- internal data class Internal(val totalTokens: Int, val totalBillableCharacters: Int? = null) :
- Response {
+ internal data class Internal(
+ val totalTokens: Int,
+ val totalBillableCharacters: Int? = null,
+ val promptTokensDetails: List? = null
+ ) : Response {
internal fun toPublic(): CountTokensResponse {
- return CountTokensResponse(totalTokens, totalBillableCharacters ?: 0)
+ return CountTokensResponse(
+ totalTokens,
+ totalBillableCharacters ?: 0,
+ promptTokensDetails?.map { it.toPublic() }
+ )
}
}
}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ModalityTokenCount.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ModalityTokenCount.kt
new file mode 100644
index 00000000000..16b7b1e4207
--- /dev/null
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ModalityTokenCount.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2025 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.vertexai.type
+
+import kotlinx.serialization.Serializable
+
+/**
+ * Represents token counting info for a single modality.
+ *
+ * @property modality The modality associated with this token count.
+ * @property tokenCount The number of tokens counted.
+ */
+public class ModalityTokenCount
+private constructor(public val modality: ContentModality, public val tokenCount: Int) {
+
+ public operator fun component1(): ContentModality = modality
+
+ public operator fun component2(): Int = tokenCount
+
+ @Serializable
+ internal data class Internal(
+ val modality: ContentModality.Internal,
+ val tokenCount: Int? = null
+ ) {
+ internal fun toPublic() = ModalityTokenCount(modality.toPublic(), tokenCount ?: 0)
+ }
+}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/UsageMetadata.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/UsageMetadata.kt
index 54f5cbd89b7..5ebbc3639d9 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/UsageMetadata.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/UsageMetadata.kt
@@ -24,11 +24,17 @@ import kotlinx.serialization.Serializable
* @param promptTokenCount Number of tokens in the request.
* @param candidatesTokenCount Number of tokens in the response(s).
* @param totalTokenCount Total number of tokens.
+ * @param promptTokensDetails The breakdown, by modality, of how many tokens are consumed by the
+ * prompt.
+ * @param candidatesTokensDetails The breakdown, by modality, of how many tokens are consumed by the
+ * candidates.
*/
public class UsageMetadata(
public val promptTokenCount: Int,
public val candidatesTokenCount: Int?,
- public val totalTokenCount: Int
+ public val totalTokenCount: Int,
+ public val promptTokensDetails: List?,
+ public val candidatesTokensDetails: List?,
) {
@Serializable
@@ -36,9 +42,17 @@ public class UsageMetadata(
val promptTokenCount: Int? = null,
val candidatesTokenCount: Int? = null,
val totalTokenCount: Int? = null,
+ val promptTokensDetails: List? = null,
+ val candidatesTokensDetails: List? = null,
) {
internal fun toPublic(): UsageMetadata =
- UsageMetadata(promptTokenCount ?: 0, candidatesTokenCount ?: 0, totalTokenCount ?: 0)
+ UsageMetadata(
+ promptTokenCount ?: 0,
+ candidatesTokenCount ?: 0,
+ totalTokenCount ?: 0,
+ promptTokensDetails = promptTokensDetails?.map { it.toPublic() },
+ candidatesTokensDetails = candidatesTokensDetails?.map { it.toPublic() }
+ )
}
}
diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt
index d538abae76e..e176fd8f7eb 100644
--- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt
+++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt
@@ -17,6 +17,7 @@
package com.google.firebase.vertexai
import com.google.firebase.vertexai.type.BlockReason
+import com.google.firebase.vertexai.type.ContentModality
import com.google.firebase.vertexai.type.FinishReason
import com.google.firebase.vertexai.type.FunctionCallPart
import com.google.firebase.vertexai.type.HarmCategory
@@ -34,7 +35,6 @@ import com.google.firebase.vertexai.util.goldenUnaryFile
import com.google.firebase.vertexai.util.shouldNotBeNullOrEmpty
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.inspectors.forAtLeastOne
-import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.collections.shouldNotBeEmpty
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.should
@@ -70,15 +70,27 @@ internal class UnarySnapshotTests {
}
@Test
- fun `long reply`() =
- goldenUnaryFile("unary-success-basic-reply-long.json") {
+ fun `response with detailed token-based usageMetadata`() =
+ goldenUnaryFile("unary-success-basic-response-long-usage-metadata.json") {
withTimeout(testTimeout) {
val response = model.generateContent("prompt")
response.candidates.isEmpty() shouldBe false
response.candidates.first().finishReason shouldBe FinishReason.STOP
response.candidates.first().content.parts.isEmpty() shouldBe false
- response.candidates.first().safetyRatings.isEmpty() shouldBe false
+ response.usageMetadata shouldNotBe null
+ response.usageMetadata?.apply {
+ totalTokenCount shouldBe 1913
+ candidatesTokenCount shouldBe 76
+ promptTokensDetails?.forAtLeastOne {
+ it.modality shouldBe ContentModality.IMAGE
+ it.tokenCount shouldBe 1806
+ }
+ candidatesTokensDetails?.forAtLeastOne {
+ it.modality shouldBe ContentModality.TEXT
+ it.tokenCount shouldBe 76
+ }
+ }
}
}
@@ -469,6 +481,22 @@ internal class UnarySnapshotTests {
}
}
+ @Test
+ fun `countTokens with modality fields returned`() =
+ goldenUnaryFile("unary-success-detailed-token-response.json") {
+ withTimeout(testTimeout) {
+ val response = model.countTokens("prompt")
+
+ response.totalTokens shouldBe 1837
+ response.totalBillableCharacters shouldBe 117
+ response.promptTokensDetails shouldNotBe null
+ response.promptTokensDetails?.forAtLeastOne {
+ it.modality shouldBe ContentModality.IMAGE
+ it.tokenCount shouldBe 1806
+ }
+ }
+ }
+
@Test
fun `countTokens succeeds with no billable characters`() =
goldenUnaryFile("unary-success-no-billable-characters.json") {
diff --git a/firebase-vertexai/update_responses.sh b/firebase-vertexai/update_responses.sh
index cb01e1a2c40..70e438090bd 100755
--- a/firebase-vertexai/update_responses.sh
+++ b/firebase-vertexai/update_responses.sh
@@ -17,7 +17,7 @@
# This script replaces mock response files for Vertex AI unit tests with a fresh
# clone of the shared repository of Vertex AI test data.
-RESPONSES_VERSION='v5.*' # The major version of mock responses to use
+RESPONSES_VERSION='v6.*' # The major version of mock responses to use
REPO_NAME="vertexai-sdk-test-data"
REPO_LINK="https://github.com/FirebaseExtended/$REPO_NAME.git"
From 57160069dd35fd093c48ce1511a68a53fe4aa0cc Mon Sep 17 00:00:00 2001
From: David Motsonashvili
Date: Mon, 10 Feb 2025 20:49:06 +0000
Subject: [PATCH 046/146] Add support for new FinishReason and BlockReason
values (#6685)
Co-authored-by: David Motsonashvili
Co-authored-by: Rodrigo Lazo
---
firebase-vertexai/CHANGELOG.md | 1 +
firebase-vertexai/api.txt | 6 +++++
.../firebase/vertexai/type/Candidate.kt | 26 ++++++++++++++++++-
.../firebase/vertexai/type/PromptFeedback.kt | 12 ++++++++-
4 files changed, 43 insertions(+), 2 deletions(-)
diff --git a/firebase-vertexai/CHANGELOG.md b/firebase-vertexai/CHANGELOG.md
index 029813fe9be..557a2f10589 100644
--- a/firebase-vertexai/CHANGELOG.md
+++ b/firebase-vertexai/CHANGELOG.md
@@ -1,4 +1,5 @@
# Unreleased
+* [fixed] Added support for new values sent by the server for `FinishReason` and `BlockReason`.
* [changed] Added support for modality-based token count. (#6658)
# 16.1.0
diff --git a/firebase-vertexai/api.txt b/firebase-vertexai/api.txt
index 5bf1e3667bf..7bb2f629c51 100644
--- a/firebase-vertexai/api.txt
+++ b/firebase-vertexai/api.txt
@@ -95,8 +95,10 @@ package com.google.firebase.vertexai.type {
method public int getOrdinal();
property public final String name;
property public final int ordinal;
+ field public static final com.google.firebase.vertexai.type.BlockReason BLOCKLIST;
field public static final com.google.firebase.vertexai.type.BlockReason.Companion Companion;
field public static final com.google.firebase.vertexai.type.BlockReason OTHER;
+ field public static final com.google.firebase.vertexai.type.BlockReason PROHIBITED_CONTENT;
field public static final com.google.firebase.vertexai.type.BlockReason SAFETY;
field public static final com.google.firebase.vertexai.type.BlockReason UNKNOWN;
}
@@ -206,11 +208,15 @@ package com.google.firebase.vertexai.type {
method public int getOrdinal();
property public final String name;
property public final int ordinal;
+ field public static final com.google.firebase.vertexai.type.FinishReason BLOCKLIST;
field public static final com.google.firebase.vertexai.type.FinishReason.Companion Companion;
+ field public static final com.google.firebase.vertexai.type.FinishReason MALFORMED_FUNCTION_CALL;
field public static final com.google.firebase.vertexai.type.FinishReason MAX_TOKENS;
field public static final com.google.firebase.vertexai.type.FinishReason OTHER;
+ field public static final com.google.firebase.vertexai.type.FinishReason PROHIBITED_CONTENT;
field public static final com.google.firebase.vertexai.type.FinishReason RECITATION;
field public static final com.google.firebase.vertexai.type.FinishReason SAFETY;
+ field public static final com.google.firebase.vertexai.type.FinishReason SPII;
field public static final com.google.firebase.vertexai.type.FinishReason STOP;
field public static final com.google.firebase.vertexai.type.FinishReason UNKNOWN;
}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Candidate.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Candidate.kt
index 54cca4a80b4..5d236c8ecc9 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Candidate.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Candidate.kt
@@ -247,7 +247,11 @@ public class FinishReason private constructor(public val name: String, public va
MAX_TOKENS,
SAFETY,
RECITATION,
- OTHER;
+ OTHER,
+ BLOCKLIST,
+ PROHIBITED_CONTENT,
+ SPII,
+ MALFORMED_FUNCTION_CALL;
internal object Serializer : KSerializer by FirstOrdinalSerializer(Internal::class)
@@ -258,6 +262,10 @@ public class FinishReason private constructor(public val name: String, public va
SAFETY -> FinishReason.SAFETY
STOP -> FinishReason.STOP
OTHER -> FinishReason.OTHER
+ BLOCKLIST -> FinishReason.BLOCKLIST
+ PROHIBITED_CONTENT -> FinishReason.PROHIBITED_CONTENT
+ SPII -> FinishReason.SPII
+ MALFORMED_FUNCTION_CALL -> FinishReason.MALFORMED_FUNCTION_CALL
else -> FinishReason.UNKNOWN
}
}
@@ -281,5 +289,21 @@ public class FinishReason private constructor(public val name: String, public va
/** Model stopped for another reason. */
@JvmField public val OTHER: FinishReason = FinishReason("OTHER", 5)
+
+ /** Token generation stopped because the content contains forbidden terms. */
+ @JvmField public val BLOCKLIST: FinishReason = FinishReason("BLOCKLIST", 6)
+
+ /** Token generation stopped for potentially containing prohibited content. */
+ @JvmField public val PROHIBITED_CONTENT: FinishReason = FinishReason("PROHIBITED_CONTENT", 7)
+
+ /**
+ * Token generation stopped because the content potentially contains Sensitive Personally
+ * Identifiable Information (SPII).
+ */
+ @JvmField public val SPII: FinishReason = FinishReason("SPII", 8)
+
+ /** The function call generated by the model is invalid. */
+ @JvmField
+ public val MALFORMED_FUNCTION_CALL: FinishReason = FinishReason("MALFORMED_FUNCTION_CALL", 9)
}
}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/PromptFeedback.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/PromptFeedback.kt
index 5f0e0edd017..f7e1ad0948a 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/PromptFeedback.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/PromptFeedback.kt
@@ -56,7 +56,9 @@ public class BlockReason private constructor(public val name: String, public val
UNKNOWN,
@SerialName("BLOCKED_REASON_UNSPECIFIED") UNSPECIFIED,
SAFETY,
- OTHER;
+ OTHER,
+ BLOCKLIST,
+ PROHIBITED_CONTENT;
internal object Serializer : KSerializer by FirstOrdinalSerializer(Internal::class)
@@ -64,6 +66,8 @@ public class BlockReason private constructor(public val name: String, public val
when (this) {
SAFETY -> BlockReason.SAFETY
OTHER -> BlockReason.OTHER
+ BLOCKLIST -> BlockReason.BLOCKLIST
+ PROHIBITED_CONTENT -> BlockReason.PROHIBITED_CONTENT
else -> BlockReason.UNKNOWN
}
}
@@ -76,5 +80,11 @@ public class BlockReason private constructor(public val name: String, public val
/** Content was blocked for another reason. */
@JvmField public val OTHER: BlockReason = BlockReason("OTHER", 2)
+
+ /** Content was blocked for another reason. */
+ @JvmField public val BLOCKLIST: BlockReason = BlockReason("BLOCKLIST", 3)
+
+ /** Candidates blocked due to the terms which are included from the terminology blocklist. */
+ @JvmField public val PROHIBITED_CONTENT: BlockReason = BlockReason("PROHIBITED_CONTENT", 4)
}
}
From 363a863c3f911c91e816028e37b5e7bf86c0f6a1 Mon Sep 17 00:00:00 2001
From: Matthew Robertson
Date: Mon, 10 Feb 2025 16:48:25 -0500
Subject: [PATCH 047/146] Fix deprecated message in KeyValueBuilder to render
properly (#6691)
Fix deprecated message in KeyValueBuilder to render properly
---
.../java/com/google/firebase/crashlytics/KeyValueBuilder.kt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/KeyValueBuilder.kt b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/KeyValueBuilder.kt
index 74d3793e215..636b975ab1d 100644
--- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/KeyValueBuilder.kt
+++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/KeyValueBuilder.kt
@@ -23,7 +23,7 @@ private constructor(
private val builder: CustomKeysAndValues.Builder,
) {
@Deprecated(
- "Do not construct this directly. Use `setCustomKeys` instead. To be removed in the next major release."
+ "Do not construct this directly. Use [setCustomKeys] instead. To be removed in the next major release."
)
constructor(crashlytics: FirebaseCrashlytics) : this(crashlytics, CustomKeysAndValues.Builder())
From d6551c3f19771cdabcd0d6744db822aa6122b660 Mon Sep 17 00:00:00 2001
From: emilypgoogle <110422458+emilypgoogle@users.noreply.github.com>
Date: Tue, 11 Feb 2025 12:12:59 -0600
Subject: [PATCH 048/146] Add functions changelog (#6694)
Changelog entry should be picked up for the next release
---
firebase-functions/CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/firebase-functions/CHANGELOG.md b/firebase-functions/CHANGELOG.md
index c26be0a15b6..e9fe66c897d 100644
--- a/firebase-functions/CHANGELOG.md
+++ b/firebase-functions/CHANGELOG.md
@@ -1,4 +1,6 @@
# Unreleased
+* [fixed] Resolve Kotlin migration visibility issues
+ ([#6522](//github.com/firebase/firebase-android-sdk/pull/6522))
# 21.1.0
From 809b794240ab906b56a43a2781be3b658934a24b Mon Sep 17 00:00:00 2001
From: Rodrigo Lazo
Date: Tue, 11 Feb 2025 15:13:35 -0500
Subject: [PATCH 049/146] Make token count details fields non-nullable (#6695)
If missing, they'll default to empty.
---
firebase-vertexai/api.txt | 16 ++++++++--------
.../vertexai/type/CountTokensResponse.kt | 4 ++--
.../firebase/vertexai/type/UsageMetadata.kt | 8 ++++----
.../firebase/vertexai/UnarySnapshotTests.kt | 2 ++
4 files changed, 16 insertions(+), 14 deletions(-)
diff --git a/firebase-vertexai/api.txt b/firebase-vertexai/api.txt
index 7bb2f629c51..02faa3674a4 100644
--- a/firebase-vertexai/api.txt
+++ b/firebase-vertexai/api.txt
@@ -183,14 +183,14 @@ package com.google.firebase.vertexai.type {
}
public final class CountTokensResponse {
- ctor public CountTokensResponse(int totalTokens, Integer? totalBillableCharacters = null, java.util.List? promptTokensDetails = null);
+ ctor public CountTokensResponse(int totalTokens, Integer? totalBillableCharacters = null, java.util.List promptTokensDetails = emptyList());
method public operator int component1();
method public operator Integer? component2();
method public operator java.util.List? component3();
- method public java.util.List? getPromptTokensDetails();
+ method public java.util.List getPromptTokensDetails();
method public Integer? getTotalBillableCharacters();
method public int getTotalTokens();
- property public final java.util.List? promptTokensDetails;
+ property public final java.util.List promptTokensDetails;
property public final Integer? totalBillableCharacters;
property public final int totalTokens;
}
@@ -582,16 +582,16 @@ package com.google.firebase.vertexai.type {
}
public final class UsageMetadata {
- ctor public UsageMetadata(int promptTokenCount, Integer? candidatesTokenCount, int totalTokenCount, java.util.List? promptTokensDetails, java.util.List? candidatesTokensDetails);
+ ctor public UsageMetadata(int promptTokenCount, Integer? candidatesTokenCount, int totalTokenCount, java.util.List promptTokensDetails, java.util.List candidatesTokensDetails);
method public Integer? getCandidatesTokenCount();
- method public java.util.List? getCandidatesTokensDetails();
+ method public java.util.List getCandidatesTokensDetails();
method public int getPromptTokenCount();
- method public java.util.List? getPromptTokensDetails();
+ method public java.util.List getPromptTokensDetails();
method public int getTotalTokenCount();
property public final Integer? candidatesTokenCount;
- property public final java.util.List? candidatesTokensDetails;
+ property public final java.util.List candidatesTokensDetails;
property public final int promptTokenCount;
- property public final java.util.List? promptTokensDetails;
+ property public final java.util.List promptTokensDetails;
property public final int totalTokenCount;
}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/CountTokensResponse.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/CountTokensResponse.kt
index a6fe492862b..49f6b0433e0 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/CountTokensResponse.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/CountTokensResponse.kt
@@ -36,7 +36,7 @@ import kotlinx.serialization.Serializable
public class CountTokensResponse(
public val totalTokens: Int,
public val totalBillableCharacters: Int? = null,
- public val promptTokensDetails: List? = null,
+ public val promptTokensDetails: List = emptyList(),
) {
public operator fun component1(): Int = totalTokens
@@ -55,7 +55,7 @@ public class CountTokensResponse(
return CountTokensResponse(
totalTokens,
totalBillableCharacters ?: 0,
- promptTokensDetails?.map { it.toPublic() }
+ promptTokensDetails?.map { it.toPublic() } ?: emptyList()
)
}
}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/UsageMetadata.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/UsageMetadata.kt
index 5ebbc3639d9..16200792f9c 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/UsageMetadata.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/UsageMetadata.kt
@@ -33,8 +33,8 @@ public class UsageMetadata(
public val promptTokenCount: Int,
public val candidatesTokenCount: Int?,
public val totalTokenCount: Int,
- public val promptTokensDetails: List?,
- public val candidatesTokensDetails: List?,
+ public val promptTokensDetails: List,
+ public val candidatesTokensDetails: List,
) {
@Serializable
@@ -51,8 +51,8 @@ public class UsageMetadata(
promptTokenCount ?: 0,
candidatesTokenCount ?: 0,
totalTokenCount ?: 0,
- promptTokensDetails = promptTokensDetails?.map { it.toPublic() },
- candidatesTokensDetails = candidatesTokensDetails?.map { it.toPublic() }
+ promptTokensDetails = promptTokensDetails?.map { it.toPublic() } ?: emptyList(),
+ candidatesTokensDetails = candidatesTokensDetails?.map { it.toPublic() } ?: emptyList()
)
}
}
diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt
index e176fd8f7eb..11d5a0df052 100644
--- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt
+++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt
@@ -289,6 +289,7 @@ internal class UnarySnapshotTests {
response.candidates.first().finishReason shouldBe FinishReason.STOP
response.usageMetadata shouldNotBe null
response.usageMetadata?.totalTokenCount shouldBe 363
+ response.usageMetadata?.promptTokensDetails?.isEmpty() shouldBe true
}
}
@@ -478,6 +479,7 @@ internal class UnarySnapshotTests {
response.totalTokens shouldBe 6
response.totalBillableCharacters shouldBe 16
+ response.promptTokensDetails.isEmpty() shouldBe true
}
}
From a889148699d4ae91763ae15f508a1d3bfb448d41 Mon Sep 17 00:00:00 2001
From: Denver Coneybeare
Date: Wed, 12 Feb 2025 02:01:03 +0000
Subject: [PATCH 050/146] dataconnect: fix generation of invalid timestamp
values in internal test utilities (#6689)
---
.../testutil/property/arbitrary/javatime.kt | 200 +++++++++++++++---
1 file changed, 174 insertions(+), 26 deletions(-)
diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/javatime.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/javatime.kt
index 12f780d2883..a826f1d9038 100644
--- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/javatime.kt
+++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/javatime.kt
@@ -24,6 +24,7 @@ import com.google.firebase.dataconnect.testutil.property.arbitrary.JavaTimeEdgeC
import com.google.firebase.dataconnect.testutil.property.arbitrary.JavaTimeEdgeCases.MIN_NANO
import com.google.firebase.dataconnect.testutil.property.arbitrary.JavaTimeEdgeCases.MIN_YEAR
import com.google.firebase.dataconnect.testutil.toTimestamp
+import io.kotest.common.mapError
import io.kotest.property.Arb
import io.kotest.property.arbitrary.arbitrary
import io.kotest.property.arbitrary.choice
@@ -31,7 +32,6 @@ import io.kotest.property.arbitrary.enum
import io.kotest.property.arbitrary.int
import io.kotest.property.arbitrary.of
import io.kotest.property.arbitrary.orNull
-import kotlin.random.nextInt
import org.threeten.bp.Instant
import org.threeten.bp.OffsetDateTime
import org.threeten.bp.ZoneOffset
@@ -153,7 +153,11 @@ private fun Instant.toFdcFieldRegex(): Regex {
return Regex(pattern)
}
-data class Nanoseconds(val nanoseconds: Int, val string: String)
+data class Nanoseconds(
+ val nanoseconds: Int,
+ val string: String,
+ val digitCounts: JavaTimeArbs.NanosecondComponents
+)
sealed interface TimeOffset {
@@ -177,8 +181,12 @@ sealed interface TimeOffset {
data class HhMm(val hours: Int, val minutes: Int, val sign: Sign) : TimeOffset {
init {
- require(hours in 0..18) { "invalid hours: $hours (must be in the closed range 0..23)" }
- require(minutes in 0..59) { "invalid minutes: $minutes (must be in the closed range 0..59)" }
+ require(hours in validHours) {
+ "invalid hours: $hours (must be in the closed range $validHours)"
+ }
+ require(minutes in validMinutes) {
+ "invalid minutes: $minutes (must be in the closed range $validMinutes)"
+ }
require(hours != 18 || minutes == 0) { "invalid minutes: $minutes (must be 0 when hours=18)" }
}
@@ -192,15 +200,44 @@ sealed interface TimeOffset {
append("$minutes".padStart(2, '0'))
}
+ fun toSeconds(): Int {
+ val absValue = (hours * SECONDS_PER_HOUR) + (minutes * SECONDS_PER_MINUTE)
+ return when (sign) {
+ Sign.Positive -> absValue
+ Sign.Negative -> -absValue
+ }
+ }
+
override fun toString() =
"HhMm(hours=$hours, minutes=$minutes, sign=$sign, " +
"zoneOffset=$zoneOffset, rfc3339String=$rfc3339String)"
+ operator fun compareTo(other: HhMm): Int = toSeconds() - other.toSeconds()
+
@Suppress("unused")
enum class Sign(val char: Char, val multiplier: Int) {
Positive('+', 1),
Negative('-', -1),
}
+
+ companion object {
+ private const val SECONDS_PER_MINUTE: Int = 60
+ private const val SECONDS_PER_HOUR: Int = 60 * SECONDS_PER_MINUTE
+
+ val validHours = 0..18
+ val validMinutes = 0..59
+
+ val maxSeconds: Int = 18 * SECONDS_PER_HOUR
+
+ fun forSeconds(seconds: Int, sign: Sign): HhMm {
+ require(seconds in 0..maxSeconds) {
+ "invalid seconds: $seconds (must be between 0 and $maxSeconds, inclusive)"
+ }
+ val hours = seconds / SECONDS_PER_HOUR
+ val minutes = (seconds - (hours * SECONDS_PER_HOUR)) / SECONDS_PER_MINUTE
+ return HhMm(hours = hours, minutes = minutes, sign = sign)
+ }
+ }
}
}
@@ -215,7 +252,6 @@ object JavaTimeArbs {
val minuteArb = minute()
val secondArb = second()
val nanosecondArb = nanosecond().orNull(nullProbability = 0.15)
- val timeOffsetArb = timeOffset()
return arbitrary(JavaTimeInstantEdgeCases.all) {
val year = yearArb.bind()
@@ -226,7 +262,55 @@ object JavaTimeArbs {
val minute = minuteArb.bind()
val second = secondArb.bind()
val nanosecond = nanosecondArb.bind()
- val timeOffset = timeOffsetArb.bind()
+
+ val instantUtc =
+ OffsetDateTime.of(
+ year,
+ month,
+ day,
+ hour,
+ minute,
+ second,
+ nanosecond?.nanoseconds ?: 0,
+ ZoneOffset.UTC,
+ )
+ .toInstant()
+
+ // The valid range below was copied from:
+ // com.google.firebase.Timestamp.Timestamp.validateRange() 253_402_300_800
+ val validEpochSecondRange = -62_135_596_800..253_402_300_800
+
+ val numSecondsBelowMaxEpochSecond = validEpochSecondRange.last - instantUtc.epochSecond
+ require(numSecondsBelowMaxEpochSecond > 0) {
+ "internal error gh98nqedss: " +
+ "invalid numSecondsBelowMaxEpochSecond: $numSecondsBelowMaxEpochSecond"
+ }
+ val minTimeZoneOffset =
+ if (numSecondsBelowMaxEpochSecond >= TimeOffset.HhMm.maxSeconds) {
+ null
+ } else {
+ TimeOffset.HhMm.forSeconds(
+ numSecondsBelowMaxEpochSecond.toInt(),
+ TimeOffset.HhMm.Sign.Negative
+ )
+ }
+
+ val numSecondsAboveMinEpochSecond = instantUtc.epochSecond - validEpochSecondRange.first
+ require(numSecondsAboveMinEpochSecond > 0) {
+ "internal error mje6a4mrbm: " +
+ "invalid numSecondsAboveMinEpochSecond: $numSecondsAboveMinEpochSecond"
+ }
+ val maxTimeZoneOffset =
+ if (numSecondsAboveMinEpochSecond >= TimeOffset.HhMm.maxSeconds) {
+ null
+ } else {
+ TimeOffset.HhMm.forSeconds(
+ numSecondsAboveMinEpochSecond.toInt(),
+ TimeOffset.HhMm.Sign.Positive
+ )
+ }
+
+ val timeOffset = timeOffset(min = minTimeZoneOffset, max = maxTimeZoneOffset).bind()
val instant =
OffsetDateTime.of(
@@ -241,6 +325,27 @@ object JavaTimeArbs {
)
.toInstant()
+ require(instant.epochSecond >= validEpochSecondRange.first) {
+ "internal error weppxzqj2y: " +
+ "instant.epochSecond out of range by " +
+ "${validEpochSecondRange.first - instant.epochSecond}: ${instant.epochSecond} (" +
+ "validEpochSecondRange.first=${validEpochSecondRange.first}, " +
+ "year=$year, month=$month, day=$day, " +
+ "hour=$hour, minute=$minute, second=$second, " +
+ "nanosecond=$nanosecond timeOffset=$timeOffset, " +
+ "minTimeZoneOffset=$minTimeZoneOffset, maxTimeZoneOffset=$maxTimeZoneOffset)"
+ }
+ require(instant.epochSecond <= validEpochSecondRange.last) {
+ "internal error yxga5xy9bm: " +
+ "instant.epochSecond out of range by " +
+ "${instant.epochSecond - validEpochSecondRange.last}: ${instant.epochSecond} (" +
+ "validEpochSecondRange.last=${validEpochSecondRange.last}, " +
+ "year=$year, month=$month, day=$day, " +
+ "hour=$hour, minute=$minute, second=$second, " +
+ "nanosecond=$nanosecond timeOffset=$timeOffset, " +
+ "minTimeZoneOffset=$minTimeZoneOffset, maxTimeZoneOffset=$maxTimeZoneOffset)"
+ }
+
val string = buildString {
append(year)
append('-')
@@ -268,7 +373,10 @@ object JavaTimeArbs {
}
}
- fun timeOffset(): Arb = Arb.choice(timeOffsetUtc(), timeOffsetHhMm())
+ fun timeOffset(
+ min: TimeOffset.HhMm?,
+ max: TimeOffset.HhMm?,
+ ): Arb = Arb.choice(timeOffsetUtc(), timeOffsetHhMm(min = min, max = max))
fun timeOffsetUtc(
case: Arb = Arb.enum(),
@@ -278,20 +386,45 @@ object JavaTimeArbs {
sign: Arb = Arb.enum(),
hour: Arb = Arb.positiveIntWithUniformNumDigitsProbability(0..18),
minute: Arb = minute(),
- ): Arb =
- arbitrary(
+ min: TimeOffset.HhMm?,
+ max: TimeOffset.HhMm?,
+ ): Arb {
+ require(min === null || max === null || min.toSeconds() < max.toSeconds()) {
+ "min must be strictly less than max, but got: " +
+ "min=$min (${min!!.toSeconds()} seconds), " +
+ "max=$max (${max!!.toSeconds()} seconds), " +
+ "a difference of ${min.toSeconds() - max.toSeconds()} seconds"
+ }
+
+ fun isBetweenMinAndMax(other: TimeOffset.HhMm): Boolean =
+ (min === null || other >= min) && (max === null || other <= max)
+
+ return arbitrary(
edgecases =
listOf(
- TimeOffset.HhMm(hours = 0, minutes = 0, sign = TimeOffset.HhMm.Sign.Positive),
- TimeOffset.HhMm(hours = 0, minutes = 0, sign = TimeOffset.HhMm.Sign.Negative),
- TimeOffset.HhMm(hours = 17, minutes = 59, sign = TimeOffset.HhMm.Sign.Positive),
- TimeOffset.HhMm(hours = 17, minutes = 59, sign = TimeOffset.HhMm.Sign.Negative),
- TimeOffset.HhMm(hours = 18, minutes = 0, sign = TimeOffset.HhMm.Sign.Positive),
- TimeOffset.HhMm(hours = 18, minutes = 0, sign = TimeOffset.HhMm.Sign.Negative),
- )
+ TimeOffset.HhMm(hours = 0, minutes = 0, sign = TimeOffset.HhMm.Sign.Positive),
+ TimeOffset.HhMm(hours = 0, minutes = 0, sign = TimeOffset.HhMm.Sign.Negative),
+ TimeOffset.HhMm(hours = 17, minutes = 59, sign = TimeOffset.HhMm.Sign.Positive),
+ TimeOffset.HhMm(hours = 17, minutes = 59, sign = TimeOffset.HhMm.Sign.Negative),
+ TimeOffset.HhMm(hours = 18, minutes = 0, sign = TimeOffset.HhMm.Sign.Positive),
+ TimeOffset.HhMm(hours = 18, minutes = 0, sign = TimeOffset.HhMm.Sign.Negative),
+ )
+ .filter(::isBetweenMinAndMax)
) {
- TimeOffset.HhMm(hours = hour.bind(), minutes = minute.bind(), sign = sign.bind())
+ var count = 0
+ var hhmm: TimeOffset.HhMm
+ while (true) {
+ count++
+ hhmm = TimeOffset.HhMm(hours = hour.bind(), minutes = minute.bind(), sign = sign.bind())
+ if (isBetweenMinAndMax(hhmm)) {
+ break
+ } else if (count > 1000) {
+ throw Exception("internal error j878fp4gmr: exhausted attempts to generate HhMm")
+ }
+ }
+ hhmm
}
+ }
fun year(): Arb = Arb.int(MIN_YEAR..MAX_YEAR)
@@ -316,8 +449,12 @@ object JavaTimeArbs {
repeat(digitCounts.leadingZeroes) { append('0') }
if (digitCounts.proper > 0) {
append(nonZeroDigits.bind())
- repeat(digitCounts.proper - 2) { append(digits.bind()) }
- append(nonZeroDigits.bind())
+ if (digitCounts.proper > 1) {
+ if (digitCounts.proper > 2) {
+ repeat(digitCounts.proper - 2) { append(digits.bind()) }
+ }
+ append(nonZeroDigits.bind())
+ }
}
repeat(digitCounts.trailingZeroes) { append('0') }
}
@@ -327,18 +464,29 @@ object JavaTimeArbs {
if (nanosecondsStringTrimmed.isEmpty()) {
0
} else {
- nanosecondsStringTrimmed.toInt()
+ val toIntResult = nanosecondsStringTrimmed.runCatching { toInt() }
+ toIntResult.mapError { exception ->
+ Exception(
+ "internal error qbdgapmye2: " +
+ "failed to parse nanosecondsStringTrimmed as an int: " +
+ "\"$nanosecondsStringTrimmed\" (digitCounts=$digitCounts)",
+ exception
+ )
+ }
+ toIntResult.getOrThrow()
}
- Nanoseconds(nanosecondsInt, nanosecondsString)
+ check(nanosecondsInt in 0..999_999_999) {
+ "internal error c7j2myw6bd: " +
+ "nanosecondsStringTrimmed parsed to a value outside the valid range: " +
+ "$nanosecondsInt (digitCounts=$digitCounts)"
+ }
+
+ Nanoseconds(nanosecondsInt, nanosecondsString, digitCounts)
}
}
- private data class NanosecondComponents(
- val leadingZeroes: Int,
- val proper: Int,
- val trailingZeroes: Int
- )
+ data class NanosecondComponents(val leadingZeroes: Int, val proper: Int, val trailingZeroes: Int)
private fun nanosecondComponents(): Arb =
arbitrary(
From c6a138dcbb4311c09fb0fc8eb93602fe982d0dda Mon Sep 17 00:00:00 2001
From: Denver Coneybeare
Date: Wed, 12 Feb 2025 19:48:49 +0000
Subject: [PATCH 051/146] dataconnect: improve cache restoration in github
actions workflow (#6697)
---
.github/workflows/dataconnect.yml | 21 ++++++++++++---------
1 file changed, 12 insertions(+), 9 deletions(-)
diff --git a/.github/workflows/dataconnect.yml b/.github/workflows/dataconnect.yml
index b452a0056aa..3a0b9aa4b93 100644
--- a/.github/workflows/dataconnect.yml
+++ b/.github/workflows/dataconnect.yml
@@ -73,13 +73,16 @@ jobs:
npm install --fund=false --audit=false --save --save-exact firebase-tools@${{ env.FDC_FIREBASE_TOOLS_VERSION }}
- name: Restore Gradle cache
+ id: restore-gradle-cache
uses: actions/cache/restore@v4
if: github.event_name != 'schedule'
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
- key: gradle-cache-jqnvfzw6w7
+ key: gradle-cache-jqnvfzw6w7-${{ github.run_id }}
+ restore-keys: |
+ gradle-cache-jqnvfzw6w7-
- name: tool versions
continue-on-error: true
@@ -121,7 +124,7 @@ jobs:
path: |
~/.gradle/caches
~/.gradle/wrapper
- key: gradle-cache-jqnvfzw6w7-${{ github.run_id }}
+ key: ${{ steps.restore-gradle-cache.outputs.cache-primary-key }}
- name: Enable KVM group permissions for Android Emulator
run: |
@@ -133,24 +136,24 @@ jobs:
- name: Restore AVD cache
uses: actions/cache/restore@v4
if: github.event_name != 'schedule'
- id: avd-cache
+ id: restore-avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
- key: avd-cache-zhdsn586je-api${{ env.FDC_ANDROID_EMULATOR_API_LEVEL }}
-
- - run: echo "github.event_name == '${{ github.event_name }}' steps.avd-cache.outputs.cache-hit == '${{ steps.avd-cache.outputs.cache-hit }}'"
+ key: avd-cache-zhdsn586je-api${{ env.FDC_ANDROID_EMULATOR_API_LEVEL }}-${{ github.run_id }}
+ restore-keys: |
+ avd-cache-zhdsn586je-api${{ env.FDC_ANDROID_EMULATOR_API_LEVEL }}-
- name: Create AVD
- if: github.event_name == 'schedule' || steps.avd-cache.outputs.cache-hit != 'true'
+ if: github.event_name == 'schedule' || steps.restore-avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.FDC_ANDROID_EMULATOR_API_LEVEL }}
arch: x86_64
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
- disable-animations: false
+ disable-animations: true
script: echo "Generated AVD snapshot for caching."
- name: Save AVD cache
@@ -160,7 +163,7 @@ jobs:
path: |
~/.android/avd/*
~/.android/adb*
- key: avd-cache-zhdsn586je-api${{ env.FDC_ANDROID_EMULATOR_API_LEVEL }}-${{ github.run_id }}
+ key: ${{ steps.restore-avd-cache.outputs.cache-primary-key }}
- name: Data Connect Emulator
run: |
From 7befca5a4bfcabc2cddabcb8a72450236c90fe20 Mon Sep 17 00:00:00 2001
From: Matthew Robertson
Date: Thu, 13 Feb 2025 08:46:46 -0500
Subject: [PATCH 052/146] Make AQS resilient to background init in
multi-process apps (#6699)
---
firebase-sessions/CHANGELOG.md | 1 +
.../sessions/SessionLifecycleService.kt | 26 +++++++++++++------
2 files changed, 19 insertions(+), 8 deletions(-)
diff --git a/firebase-sessions/CHANGELOG.md b/firebase-sessions/CHANGELOG.md
index 7147a6bf504..48987a62df5 100644
--- a/firebase-sessions/CHANGELOG.md
+++ b/firebase-sessions/CHANGELOG.md
@@ -1,5 +1,6 @@
# Unreleased
+* [fixed] Make AQS resilient to background init in multi-process apps.
# 2.0.7
* [fixed] Removed extraneous logs that risk leaking internal identifiers.
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt
index a900fcd95c1..e0f3720b8d7 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt
@@ -127,10 +127,16 @@ internal class SessionLifecycleService : Service() {
/** Generates a new session id and sends it everywhere it's needed */
private fun newSession() {
- SessionGenerator.instance.generateNewSession()
- Log.d(TAG, "Generated new session.")
- broadcastSession()
- SessionDatastore.instance.updateSessionId(SessionGenerator.instance.currentSession.sessionId)
+ try {
+ SessionGenerator.instance.generateNewSession()
+ Log.d(TAG, "Generated new session.")
+ broadcastSession()
+ SessionDatastore.instance.updateSessionId(
+ SessionGenerator.instance.currentSession.sessionId
+ )
+ } catch (ex: IllegalStateException) {
+ Log.w(TAG, "Failed to generate new session.", ex)
+ }
}
/**
@@ -149,10 +155,14 @@ internal class SessionLifecycleService : Service() {
if (hasForegrounded) {
sendSessionToClient(client, SessionGenerator.instance.currentSession.sessionId)
} else {
- // Send the value from the datastore before the first foregrounding it exists
- val storedSession = SessionDatastore.instance.getCurrentSessionId()
- Log.d(TAG, "App has not yet foregrounded. Using previously stored session.")
- storedSession?.let { sendSessionToClient(client, it) }
+ try {
+ // Send the value from the datastore before the first foregrounding it exists
+ val storedSession = SessionDatastore.instance.getCurrentSessionId()
+ Log.d(TAG, "App has not yet foregrounded. Using previously stored session.")
+ storedSession?.let { sendSessionToClient(client, it) }
+ } catch (ex: IllegalStateException) {
+ Log.w(TAG, "Failed to send session to client.", ex)
+ }
}
}
From 4fd7c2df7c52531cbe943dccf8d1328c55dadf27 Mon Sep 17 00:00:00 2001
From: Matthew Robertson
Date: Thu, 13 Feb 2025 10:34:00 -0500
Subject: [PATCH 053/146] Followup to #6699 (#6701)
---
.../firebase/sessions/SessionLifecycleService.kt | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt
index e0f3720b8d7..bde6d138fbe 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt
@@ -128,6 +128,7 @@ internal class SessionLifecycleService : Service() {
/** Generates a new session id and sends it everywhere it's needed */
private fun newSession() {
try {
+ // TODO(mrober): Consider migrating to Dagger, or update [FirebaseSessionsRegistrar].
SessionGenerator.instance.generateNewSession()
Log.d(TAG, "Generated new session.")
broadcastSession()
@@ -152,17 +153,17 @@ internal class SessionLifecycleService : Service() {
}
private fun maybeSendSessionToClient(client: Messenger) {
- if (hasForegrounded) {
- sendSessionToClient(client, SessionGenerator.instance.currentSession.sessionId)
- } else {
- try {
+ try {
+ if (hasForegrounded) {
+ sendSessionToClient(client, SessionGenerator.instance.currentSession.sessionId)
+ } else {
// Send the value from the datastore before the first foregrounding it exists
val storedSession = SessionDatastore.instance.getCurrentSessionId()
Log.d(TAG, "App has not yet foregrounded. Using previously stored session.")
storedSession?.let { sendSessionToClient(client, it) }
- } catch (ex: IllegalStateException) {
- Log.w(TAG, "Failed to send session to client.", ex)
}
+ } catch (ex: IllegalStateException) {
+ Log.w(TAG, "Failed to send session to client.", ex)
}
}
From d9f4fdb3e0cc333f67ff7a98422c4ef12bdc5b5b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ros=C3=A1rio=20P=2E=20Fernandes?=
Date: Mon, 17 Feb 2025 13:57:31 +0000
Subject: [PATCH 054/146] docs: update ImagePart refdocs (#6681)
The current refdocs are somewhat confusing and may lead to assume that
the conversion happens server-side, when it's actually client side.
---
.../src/main/kotlin/com/google/firebase/vertexai/type/Part.kt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Part.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Part.kt
index cfffe6885b0..a0a47cf79ee 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Part.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Part.kt
@@ -40,8 +40,8 @@ public class TextPart(public val text: String) : Part {
}
/**
- * Represents image data sent to and received from requests. When this is sent to the server it is
- * converted to jpeg encoding at 80% quality.
+ * Represents image data sent to and received from requests. The image is converted client-side to
+ * JPEG encoding at 80% quality before being sent to the server.
*
* @param image [Bitmap] to convert into a [Part]
*/
From f14ef94b98d7d5c837721889bc487da760831c89 Mon Sep 17 00:00:00 2001
From: David Motsonashvili
Date: Wed, 19 Feb 2025 16:11:29 +0000
Subject: [PATCH 055/146] Initial implementation to API spec (#6607)
Added support for querying imagen models to generate images both in gcs
and inline. Documentation incoming in a separate PR for readability
---------
Co-authored-by: David Motsonashvili
Co-authored-by: rachelsaunders <52258509+rachelsaunders@users.noreply.github.com>
Co-authored-by: Daymon <17409137+daymxn@users.noreply.github.com>
---
firebase-vertexai/CHANGELOG.md | 2 +
firebase-vertexai/api.txt | 127 +++++++++++++++++-
.../firebase/vertexai/FirebaseVertexAI.kt | 34 +++++
.../firebase/vertexai/GenerativeModel.kt | 50 +------
.../google/firebase/vertexai/ImagenModel.kt | 119 ++++++++++++++++
.../firebase/vertexai/common/APIController.kt | 23 ++++
.../vertexai/common/AppCheckHeaderProvider.kt | 64 +++++++++
.../firebase/vertexai/common/Exceptions.kt | 17 ++-
.../firebase/vertexai/common/Request.kt | 26 +++-
.../firebase/vertexai/internal/util/kotlin.kt | 41 ------
.../vertexai/java/ImagenModelFutures.kt | 59 ++++++++
.../firebase/vertexai/type/Exceptions.kt | 21 ++-
.../vertexai/type/ImagenAspectRatio.kt | 34 +++++
.../firebase/vertexai/type/ImagenGCSImage.kt | 27 ++++
.../vertexai/type/ImagenGenerationConfig.kt | 99 ++++++++++++++
.../vertexai/type/ImagenGenerationResponse.kt | 59 ++++++++
.../vertexai/type/ImagenImageFormat.kt | 53 ++++++++
.../vertexai/type/ImagenInlineImage.kt | 40 ++++++
.../vertexai/type/ImagenPersonFilterLevel.kt | 31 +++++
.../vertexai/type/ImagenSafetyFilterLevel.kt | 40 ++++++
.../vertexai/type/ImagenSafetySettings.kt | 29 ++++
.../vertexai/type/PublicPreviewAPI.kt | 26 ++++
.../vertexai/StreamingSnapshotTests.kt | 6 +-
.../firebase/vertexai/UnarySnapshotTests.kt | 32 ++++-
.../vertexai/common/StreamingSnapshotTests.kt | 2 +-
.../vertexai/common/UnarySnapshotTests.kt | 2 +-
.../google/firebase/vertexai/util/tests.kt | 13 +-
27 files changed, 965 insertions(+), 111 deletions(-)
create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/ImagenModel.kt
create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/AppCheckHeaderProvider.kt
delete mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/internal/util/kotlin.kt
create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/java/ImagenModelFutures.kt
create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenAspectRatio.kt
create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGCSImage.kt
create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt
create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationResponse.kt
create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenImageFormat.kt
create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenInlineImage.kt
create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenPersonFilterLevel.kt
create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenSafetyFilterLevel.kt
create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenSafetySettings.kt
create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/PublicPreviewAPI.kt
diff --git a/firebase-vertexai/CHANGELOG.md b/firebase-vertexai/CHANGELOG.md
index 557a2f10589..c0bfdfb214e 100644
--- a/firebase-vertexai/CHANGELOG.md
+++ b/firebase-vertexai/CHANGELOG.md
@@ -1,6 +1,7 @@
# Unreleased
* [fixed] Added support for new values sent by the server for `FinishReason` and `BlockReason`.
* [changed] Added support for modality-based token count. (#6658)
+* [feature] Added support for generating images with Imagen models.
# 16.1.0
* [changed] Internal improvements to correctly handle empty model responses.
@@ -65,3 +66,4 @@
* [feature] Added support for `responseMimeType` in `GenerationConfig`.
* [changed] Renamed `GoogleGenerativeAIException` to `FirebaseVertexAIException`.
* [changed] Updated the KDocs for various classes and functions.
+
diff --git a/firebase-vertexai/api.txt b/firebase-vertexai/api.txt
index 02faa3674a4..abfbf6572d4 100644
--- a/firebase-vertexai/api.txt
+++ b/firebase-vertexai/api.txt
@@ -25,6 +25,10 @@ package com.google.firebase.vertexai {
method public static com.google.firebase.vertexai.FirebaseVertexAI getInstance(com.google.firebase.FirebaseApp app);
method public static com.google.firebase.vertexai.FirebaseVertexAI getInstance(com.google.firebase.FirebaseApp app = Firebase.app, String location);
method public static com.google.firebase.vertexai.FirebaseVertexAI getInstance(String location);
+ method @com.google.firebase.vertexai.type.PublicPreviewAPI public com.google.firebase.vertexai.ImagenModel imagenModel(String modelName);
+ method @com.google.firebase.vertexai.type.PublicPreviewAPI public com.google.firebase.vertexai.ImagenModel imagenModel(String modelName, com.google.firebase.vertexai.type.ImagenGenerationConfig? generationConfig = null);
+ method @com.google.firebase.vertexai.type.PublicPreviewAPI public com.google.firebase.vertexai.ImagenModel imagenModel(String modelName, com.google.firebase.vertexai.type.ImagenGenerationConfig? generationConfig = null, com.google.firebase.vertexai.type.ImagenSafetySettings? safetySettings = null);
+ method @com.google.firebase.vertexai.type.PublicPreviewAPI public com.google.firebase.vertexai.ImagenModel imagenModel(String modelName, com.google.firebase.vertexai.type.ImagenGenerationConfig? generationConfig = null, com.google.firebase.vertexai.type.ImagenSafetySettings? safetySettings = null, com.google.firebase.vertexai.type.RequestOptions requestOptions = com.google.firebase.vertexai.type.RequestOptions());
property public static final com.google.firebase.vertexai.FirebaseVertexAI instance;
field public static final com.google.firebase.vertexai.FirebaseVertexAI.Companion Companion;
}
@@ -55,6 +59,10 @@ package com.google.firebase.vertexai {
method public com.google.firebase.vertexai.Chat startChat(java.util.List history = emptyList());
}
+ @com.google.firebase.vertexai.type.PublicPreviewAPI public final class ImagenModel {
+ method public suspend Object? generateImages(String prompt, kotlin.coroutines.Continuation super com.google.firebase.vertexai.type.ImagenGenerationResponse>);
+ }
+
}
package com.google.firebase.vertexai.java {
@@ -86,6 +94,17 @@ package com.google.firebase.vertexai.java {
method public com.google.firebase.vertexai.java.GenerativeModelFutures from(com.google.firebase.vertexai.GenerativeModel model);
}
+ @com.google.firebase.vertexai.type.PublicPreviewAPI public abstract class ImagenModelFutures {
+ method public static final com.google.firebase.vertexai.java.ImagenModelFutures from(com.google.firebase.vertexai.ImagenModel model);
+ method public abstract com.google.common.util.concurrent.ListenableFuture> generateImages(String prompt);
+ method public abstract com.google.firebase.vertexai.ImagenModel getImageModel();
+ field public static final com.google.firebase.vertexai.java.ImagenModelFutures.Companion Companion;
+ }
+
+ public static final class ImagenModelFutures.Companion {
+ method public com.google.firebase.vertexai.java.ImagenModelFutures from(com.google.firebase.vertexai.ImagenModel model);
+ }
+
}
package com.google.firebase.vertexai.type {
@@ -163,6 +182,9 @@ package com.google.firebase.vertexai.type {
property public final String? role;
}
+ public final class ContentBlockedException extends com.google.firebase.vertexai.type.FirebaseVertexAIException {
+ }
+
public final class ContentKt {
method public static com.google.firebase.vertexai.type.Content content(String? role = "user", kotlin.jvm.functions.Function1 super com.google.firebase.vertexai.type.Content.Builder,kotlin.Unit> init);
}
@@ -376,6 +398,104 @@ package com.google.firebase.vertexai.type {
property public final android.graphics.Bitmap image;
}
+ @com.google.firebase.vertexai.type.PublicPreviewAPI public final class ImagenAspectRatio {
+ field public static final com.google.firebase.vertexai.type.ImagenAspectRatio.Companion Companion;
+ field public static final com.google.firebase.vertexai.type.ImagenAspectRatio LANDSCAPE_16x9;
+ field public static final com.google.firebase.vertexai.type.ImagenAspectRatio LANDSCAPE_4x3;
+ field public static final com.google.firebase.vertexai.type.ImagenAspectRatio PORTRAIT_3x4;
+ field public static final com.google.firebase.vertexai.type.ImagenAspectRatio PORTRAIT_9x16;
+ field public static final com.google.firebase.vertexai.type.ImagenAspectRatio SQUARE_1x1;
+ }
+
+ public static final class ImagenAspectRatio.Companion {
+ }
+
+ @com.google.firebase.vertexai.type.PublicPreviewAPI public final class ImagenGenerationConfig {
+ ctor public ImagenGenerationConfig(String? negativePrompt = null, Integer? numberOfImages = 1, com.google.firebase.vertexai.type.ImagenAspectRatio? aspectRatio = null, com.google.firebase.vertexai.type.ImagenImageFormat? imageFormat = null, Boolean? addWatermark = null);
+ method public Boolean? getAddWatermark();
+ method public com.google.firebase.vertexai.type.ImagenAspectRatio? getAspectRatio();
+ method public com.google.firebase.vertexai.type.ImagenImageFormat? getImageFormat();
+ method public String? getNegativePrompt();
+ method public Integer? getNumberOfImages();
+ property public final Boolean? addWatermark;
+ property public final com.google.firebase.vertexai.type.ImagenAspectRatio? aspectRatio;
+ property public final com.google.firebase.vertexai.type.ImagenImageFormat? imageFormat;
+ property public final String? negativePrompt;
+ property public final Integer? numberOfImages;
+ field public static final com.google.firebase.vertexai.type.ImagenGenerationConfig.Companion Companion;
+ }
+
+ public static final class ImagenGenerationConfig.Builder {
+ ctor public ImagenGenerationConfig.Builder();
+ method public com.google.firebase.vertexai.type.ImagenGenerationConfig build();
+ field public Boolean? addWatermark;
+ field public com.google.firebase.vertexai.type.ImagenAspectRatio? aspectRatio;
+ field public com.google.firebase.vertexai.type.ImagenImageFormat? imageFormat;
+ field public String? negativePrompt;
+ field public Integer? numberOfImages;
+ }
+
+ public static final class ImagenGenerationConfig.Companion {
+ method public com.google.firebase.vertexai.type.ImagenGenerationConfig.Builder builder();
+ }
+
+ public final class ImagenGenerationConfigKt {
+ method @com.google.firebase.vertexai.type.PublicPreviewAPI public static com.google.firebase.vertexai.type.ImagenGenerationConfig imagenGenerationConfig(kotlin.jvm.functions.Function1 super com.google.firebase.vertexai.type.ImagenGenerationConfig.Builder,kotlin.Unit> init);
+ }
+
+ @com.google.firebase.vertexai.type.PublicPreviewAPI public final class ImagenGenerationResponse {
+ method public String? getFilteredReason();
+ method public java.util.List getImages();
+ property public final String? filteredReason;
+ property public final java.util.List images;
+ }
+
+ @com.google.firebase.vertexai.type.PublicPreviewAPI public final class ImagenImageFormat {
+ method public Integer? getCompressionQuality();
+ method public String getMimeType();
+ property public final Integer? compressionQuality;
+ property public final String mimeType;
+ field public static final com.google.firebase.vertexai.type.ImagenImageFormat.Companion Companion;
+ }
+
+ public static final class ImagenImageFormat.Companion {
+ method public com.google.firebase.vertexai.type.ImagenImageFormat jpeg(Integer? compressionQuality = null);
+ method public com.google.firebase.vertexai.type.ImagenImageFormat png();
+ }
+
+ @com.google.firebase.vertexai.type.PublicPreviewAPI public final class ImagenInlineImage {
+ method public android.graphics.Bitmap asBitmap();
+ method public byte[] getData();
+ method public String getMimeType();
+ property public final byte[] data;
+ property public final String mimeType;
+ }
+
+ @com.google.firebase.vertexai.type.PublicPreviewAPI public final class ImagenPersonFilterLevel {
+ field public static final com.google.firebase.vertexai.type.ImagenPersonFilterLevel ALLOW_ADULT;
+ field public static final com.google.firebase.vertexai.type.ImagenPersonFilterLevel ALLOW_ALL;
+ field public static final com.google.firebase.vertexai.type.ImagenPersonFilterLevel BLOCK_ALL;
+ field public static final com.google.firebase.vertexai.type.ImagenPersonFilterLevel.Companion Companion;
+ }
+
+ public static final class ImagenPersonFilterLevel.Companion {
+ }
+
+ @com.google.firebase.vertexai.type.PublicPreviewAPI public final class ImagenSafetyFilterLevel {
+ field public static final com.google.firebase.vertexai.type.ImagenSafetyFilterLevel BLOCK_LOW_AND_ABOVE;
+ field public static final com.google.firebase.vertexai.type.ImagenSafetyFilterLevel BLOCK_MEDIUM_AND_ABOVE;
+ field public static final com.google.firebase.vertexai.type.ImagenSafetyFilterLevel BLOCK_NONE;
+ field public static final com.google.firebase.vertexai.type.ImagenSafetyFilterLevel BLOCK_ONLY_HIGH;
+ field public static final com.google.firebase.vertexai.type.ImagenSafetyFilterLevel.Companion Companion;
+ }
+
+ public static final class ImagenSafetyFilterLevel.Companion {
+ }
+
+ @com.google.firebase.vertexai.type.PublicPreviewAPI public final class ImagenSafetySettings {
+ ctor public ImagenSafetySettings(com.google.firebase.vertexai.type.ImagenSafetyFilterLevel safetyFilterLevel, com.google.firebase.vertexai.type.ImagenPersonFilterLevel personFilterLevel);
+ }
+
public final class InlineDataPart implements com.google.firebase.vertexai.type.Part {
ctor public InlineDataPart(byte[] inlineData, String mimeType);
method public byte[] getInlineData();
@@ -413,8 +533,8 @@ package com.google.firebase.vertexai.type {
}
public final class PromptBlockedException extends com.google.firebase.vertexai.type.FirebaseVertexAIException {
- method public com.google.firebase.vertexai.type.GenerateContentResponse getResponse();
- property public final com.google.firebase.vertexai.type.GenerateContentResponse response;
+ method public com.google.firebase.vertexai.type.GenerateContentResponse? getResponse();
+ property public final com.google.firebase.vertexai.type.GenerateContentResponse? response;
}
public final class PromptFeedback {
@@ -427,6 +547,9 @@ package com.google.firebase.vertexai.type {
property public final java.util.List safetyRatings;
}
+ @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This API is part of an experimental public preview and may change in " + "backwards-incompatible ways without notice.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface PublicPreviewAPI {
+ }
+
public final class RequestOptions {
ctor public RequestOptions();
ctor public RequestOptions(long timeoutInMillis = 180.seconds.inWholeMilliseconds);
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAI.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAI.kt
index ff256482112..b89e5671992 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAI.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAI.kt
@@ -24,7 +24,10 @@ import com.google.firebase.auth.internal.InternalAuthProvider
import com.google.firebase.inject.Provider
import com.google.firebase.vertexai.type.Content
import com.google.firebase.vertexai.type.GenerationConfig
+import com.google.firebase.vertexai.type.ImagenGenerationConfig
+import com.google.firebase.vertexai.type.ImagenSafetySettings
import com.google.firebase.vertexai.type.InvalidLocationException
+import com.google.firebase.vertexai.type.PublicPreviewAPI
import com.google.firebase.vertexai.type.RequestOptions
import com.google.firebase.vertexai.type.SafetySetting
import com.google.firebase.vertexai.type.Tool
@@ -79,6 +82,37 @@ internal constructor(
)
}
+ /**
+ * Instantiates a new [ImagenModel] given the provided parameters.
+ *
+ * @param modelName The name of the model to use, for example `"imagen-3.0-generate-001"`.
+ * @param generationConfig The configuration parameters to use for image generation.
+ * @param safetySettings The safety bounds the model will abide by during image generation.
+ * @param requestOptions Configuration options for sending requests to the backend.
+ * @return The initialized [ImagenModel] instance.
+ */
+ @JvmOverloads
+ @PublicPreviewAPI
+ public fun imagenModel(
+ modelName: String,
+ generationConfig: ImagenGenerationConfig? = null,
+ safetySettings: ImagenSafetySettings? = null,
+ requestOptions: RequestOptions = RequestOptions(),
+ ): ImagenModel {
+ if (location.trim().isEmpty() || location.contains("/")) {
+ throw InvalidLocationException(location)
+ }
+ return ImagenModel(
+ "projects/${firebaseApp.options.projectId}/locations/${location}/publishers/google/models/${modelName}",
+ firebaseApp.options.apiKey,
+ generationConfig,
+ safetySettings,
+ requestOptions,
+ appCheckProvider.get(),
+ internalAuthProvider.get(),
+ )
+ }
+
public companion object {
/** The [FirebaseVertexAI] instance for the default [FirebaseApp] */
@JvmStatic
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/GenerativeModel.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/GenerativeModel.kt
index c2b9fcfd2f9..a49d4c279a8 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/GenerativeModel.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/GenerativeModel.kt
@@ -17,13 +17,12 @@
package com.google.firebase.vertexai
import android.graphics.Bitmap
-import android.util.Log
import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider
import com.google.firebase.auth.internal.InternalAuthProvider
import com.google.firebase.vertexai.common.APIController
+import com.google.firebase.vertexai.common.AppCheckHeaderProvider
import com.google.firebase.vertexai.common.CountTokensRequest
import com.google.firebase.vertexai.common.GenerateContentRequest
-import com.google.firebase.vertexai.common.HeaderProvider
import com.google.firebase.vertexai.type.Content
import com.google.firebase.vertexai.type.CountTokensResponse
import com.google.firebase.vertexai.type.FinishReason
@@ -38,12 +37,9 @@ import com.google.firebase.vertexai.type.SerializationException
import com.google.firebase.vertexai.type.Tool
import com.google.firebase.vertexai.type.ToolConfig
import com.google.firebase.vertexai.type.content
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.tasks.await
/**
* Represents a multimodal model (like Gemini), capable of generating content based on various input
@@ -57,10 +53,8 @@ internal constructor(
private val tools: List? = null,
private val toolConfig: ToolConfig? = null,
private val systemInstruction: Content? = null,
- private val controller: APIController
+ private val controller: APIController,
) {
-
- @JvmOverloads
internal constructor(
modelName: String,
apiKey: String,
@@ -84,42 +78,8 @@ internal constructor(
modelName,
requestOptions,
"gl-kotlin/${KotlinVersion.CURRENT} fire/${BuildConfig.VERSION_NAME}",
- object : HeaderProvider {
- override val timeout: Duration
- get() = 10.seconds
-
- override suspend fun generateHeaders(): Map {
- val headers = mutableMapOf()
- if (appCheckTokenProvider == null) {
- Log.w(TAG, "AppCheck not registered, skipping")
- } else {
- val token = appCheckTokenProvider.getToken(false).await()
-
- if (token.error != null) {
- Log.w(TAG, "Error obtaining AppCheck token", token.error)
- }
- // The Firebase App Check backend can differentiate between apps without App Check, and
- // wrongly configured apps by verifying the value of the token, so it always needs to be
- // included.
- headers["X-Firebase-AppCheck"] = token.token
- }
-
- if (internalAuthProvider == null) {
- Log.w(TAG, "Auth not registered, skipping")
- } else {
- try {
- val token = internalAuthProvider.getAccessToken(false).await()
-
- headers["Authorization"] = "Firebase ${token.token!!}"
- } catch (e: Exception) {
- Log.w(TAG, "Error getting Auth token ", e)
- }
- }
-
- return headers
- }
- }
- )
+ AppCheckHeaderProvider(TAG, appCheckTokenProvider, internalAuthProvider),
+ ),
)
/**
@@ -247,7 +207,7 @@ internal constructor(
generationConfig?.toInternal(),
tools?.map { it.toInternal() },
toolConfig?.toInternal(),
- systemInstruction?.copy(role = "system")?.toInternal()
+ systemInstruction?.copy(role = "system")?.toInternal(),
)
private fun constructCountTokensRequest(vararg prompt: Content) =
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/ImagenModel.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/ImagenModel.kt
new file mode 100644
index 00000000000..583ef24bcc4
--- /dev/null
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/ImagenModel.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2025 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.vertexai
+
+import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider
+import com.google.firebase.auth.internal.InternalAuthProvider
+import com.google.firebase.vertexai.common.APIController
+import com.google.firebase.vertexai.common.AppCheckHeaderProvider
+import com.google.firebase.vertexai.common.ContentBlockedException
+import com.google.firebase.vertexai.common.GenerateImageRequest
+import com.google.firebase.vertexai.type.FirebaseVertexAIException
+import com.google.firebase.vertexai.type.ImagenGenerationConfig
+import com.google.firebase.vertexai.type.ImagenGenerationResponse
+import com.google.firebase.vertexai.type.ImagenInlineImage
+import com.google.firebase.vertexai.type.ImagenSafetySettings
+import com.google.firebase.vertexai.type.PublicPreviewAPI
+import com.google.firebase.vertexai.type.RequestOptions
+
+/**
+ * Represents a generative model (like Imagen), capable of generating images based on various input
+ * types.
+ */
+@PublicPreviewAPI
+public class ImagenModel
+internal constructor(
+ private val modelName: String,
+ private val generationConfig: ImagenGenerationConfig? = null,
+ private val safetySettings: ImagenSafetySettings? = null,
+ private val controller: APIController,
+) {
+ @JvmOverloads
+ internal constructor(
+ modelName: String,
+ apiKey: String,
+ generationConfig: ImagenGenerationConfig? = null,
+ safetySettings: ImagenSafetySettings? = null,
+ requestOptions: RequestOptions = RequestOptions(),
+ appCheckTokenProvider: InteropAppCheckTokenProvider? = null,
+ internalAuthProvider: InternalAuthProvider? = null,
+ ) : this(
+ modelName,
+ generationConfig,
+ safetySettings,
+ APIController(
+ apiKey,
+ modelName,
+ requestOptions,
+ "gl-kotlin/${KotlinVersion.CURRENT} fire/${BuildConfig.VERSION_NAME}",
+ AppCheckHeaderProvider(TAG, appCheckTokenProvider, internalAuthProvider),
+ ),
+ )
+
+ /**
+ * Generates an image, returning the result directly to the caller.
+ *
+ * @param prompt The input(s) given to the model as a prompt.
+ */
+ public suspend fun generateImages(prompt: String): ImagenGenerationResponse =
+ try {
+ controller
+ .generateImage(constructRequest(prompt, null, generationConfig))
+ .validate()
+ .toPublicInline()
+ } catch (e: Throwable) {
+ throw FirebaseVertexAIException.from(e)
+ }
+
+ private fun constructRequest(
+ prompt: String,
+ gcsUri: String?,
+ config: ImagenGenerationConfig?,
+ ): GenerateImageRequest {
+ return GenerateImageRequest(
+ listOf(GenerateImageRequest.ImagenPrompt(prompt)),
+ GenerateImageRequest.ImagenParameters(
+ sampleCount = config?.numberOfImages ?: 1,
+ includeRaiReason = true,
+ addWatermark = generationConfig?.addWatermark,
+ personGeneration = safetySettings?.personFilterLevel?.internalVal,
+ negativePrompt = config?.negativePrompt,
+ safetySetting = safetySettings?.safetyFilterLevel?.internalVal,
+ storageUri = gcsUri,
+ aspectRatio = config?.aspectRatio?.internalVal,
+ imageOutputOptions = generationConfig?.imageFormat?.toInternal(),
+ ),
+ )
+ }
+
+ internal companion object {
+ private val TAG = ImagenModel::class.java.simpleName
+ internal const val DEFAULT_FILTERED_ERROR =
+ "Unable to show generated images. All images were filtered out because they violated Vertex AI's usage guidelines. You will not be charged for blocked images. Try rephrasing the prompt. If you think this was an error, send feedback."
+ }
+}
+
+@OptIn(PublicPreviewAPI::class)
+private fun ImagenGenerationResponse.Internal.validate(): ImagenGenerationResponse.Internal {
+ if (predictions.none { it.mimeType != null }) {
+ throw ContentBlockedException(
+ message = predictions.first { it.raiFilteredReason != null }.raiFilteredReason
+ ?: ImagenModel.DEFAULT_FILTERED_ERROR
+ )
+ }
+ return this
+}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/APIController.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/APIController.kt
index 286b8829241..f8bfe0bc24f 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/APIController.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/APIController.kt
@@ -25,6 +25,8 @@ import com.google.firebase.vertexai.type.CountTokensResponse
import com.google.firebase.vertexai.type.FinishReason
import com.google.firebase.vertexai.type.GRpcErrorResponse
import com.google.firebase.vertexai.type.GenerateContentResponse
+import com.google.firebase.vertexai.type.ImagenGenerationResponse
+import com.google.firebase.vertexai.type.PublicPreviewAPI
import com.google.firebase.vertexai.type.RequestOptions
import com.google.firebase.vertexai.type.Response
import io.ktor.client.HttpClient
@@ -58,12 +60,15 @@ import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
+import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
+@OptIn(ExperimentalSerializationApi::class)
internal val JSON = Json {
ignoreUnknownKeys = true
prettyPrint = false
isLenient = true
+ explicitNulls = false
}
/**
@@ -78,6 +83,7 @@ internal val JSON = Json {
* @property apiClient The value to pass in the `x-goog-api-client` header.
* @property headerProvider A provider that generates extra headers to include in all HTTP requests.
*/
+@OptIn(PublicPreviewAPI::class)
internal class APIController
internal constructor(
private val key: String,
@@ -122,6 +128,19 @@ internal constructor(
throw FirebaseCommonAIException.from(e)
}
+ suspend fun generateImage(request: GenerateImageRequest): ImagenGenerationResponse.Internal =
+ try {
+ client
+ .post("${requestOptions.endpoint}/${requestOptions.apiVersion}/$model:predict") {
+ applyCommonConfiguration(request)
+ applyHeaderProvider()
+ }
+ .also { validateResponse(it) }
+ .body()
+ } catch (e: Throwable) {
+ throw FirebaseCommonAIException.from(e)
+ }
+
fun generateContentStream(
request: GenerateContentRequest
): Flow =
@@ -151,6 +170,7 @@ internal constructor(
when (request) {
is GenerateContentRequest -> setBody(request)
is CountTokensRequest -> setBody(request)
+ is GenerateImageRequest -> setBody(request)
}
contentType(ContentType.Application.Json)
header("x-goog-api-key", key)
@@ -258,6 +278,9 @@ private suspend fun validateResponse(response: HttpResponse) {
if (message.contains("quota")) {
throw QuotaExceededException(message)
}
+ if (message.contains("The prompt could not be submitted")) {
+ throw PromptBlockedException(message)
+ }
getServiceDisabledErrorDetailsOrNull(error)?.let {
val errorMessage =
if (it.metadata?.get("service") == "firebasevertexai.googleapis.com") {
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/AppCheckHeaderProvider.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/AppCheckHeaderProvider.kt
new file mode 100644
index 00000000000..fb3b52ad46f
--- /dev/null
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/AppCheckHeaderProvider.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2025 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.vertexai.common
+
+import android.util.Log
+import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider
+import com.google.firebase.auth.internal.InternalAuthProvider
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.tasks.await
+
+internal class AppCheckHeaderProvider(
+ private val logTag: String,
+ private val appCheckTokenProvider: InteropAppCheckTokenProvider? = null,
+ private val internalAuthProvider: InternalAuthProvider? = null,
+) : HeaderProvider {
+ override val timeout: Duration
+ get() = 10.seconds
+
+ override suspend fun generateHeaders(): Map {
+ val headers = mutableMapOf()
+ if (appCheckTokenProvider == null) {
+ Log.w(logTag, "AppCheck not registered, skipping")
+ } else {
+ val token = appCheckTokenProvider.getToken(false).await()
+
+ if (token.error != null) {
+ Log.w(logTag, "Error obtaining AppCheck token", token.error)
+ }
+ // The Firebase App Check backend can differentiate between apps without App Check, and
+ // wrongly configured apps by verifying the value of the token, so it always needs to be
+ // included.
+ headers["X-Firebase-AppCheck"] = token.token
+ }
+
+ if (internalAuthProvider == null) {
+ Log.w(logTag, "Auth not registered, skipping")
+ } else {
+ try {
+ val token = internalAuthProvider.getAccessToken(false).await()
+
+ headers["Authorization"] = "Firebase ${token.token!!}"
+ } catch (e: Exception) {
+ Log.w(logTag, "Error getting Auth token ", e)
+ }
+ }
+
+ return headers
+ }
+}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Exceptions.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Exceptions.kt
index 7567c384618..ad982f2daff 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Exceptions.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Exceptions.kt
@@ -66,14 +66,18 @@ internal class InvalidAPIKeyException(message: String, cause: Throwable? = null)
*
* @property response the full server response for the request.
*/
-internal class PromptBlockedException(
- val response: GenerateContentResponse.Internal,
- cause: Throwable? = null
+internal class PromptBlockedException
+internal constructor(
+ val response: GenerateContentResponse.Internal?,
+ cause: Throwable? = null,
+ message: String? = null,
) :
FirebaseCommonAIException(
- "Prompt was blocked: ${response.promptFeedback?.blockReason?.name}",
+ "Prompt was blocked: ${response?.promptFeedback?.blockReason?.name?: message}",
cause,
- )
+ ) {
+ internal constructor(message: String, cause: Throwable? = null) : this(null, cause, message)
+}
/**
* The user's location (region) is not supported by the API.
@@ -127,6 +131,9 @@ internal class ServiceDisabledException(message: String, cause: Throwable? = nul
internal class UnknownException(message: String, cause: Throwable? = null) :
FirebaseCommonAIException(message, cause)
+internal class ContentBlockedException(message: String, cause: Throwable? = null) :
+ FirebaseCommonAIException(message, cause)
+
internal fun makeMissingCaseException(
source: String,
ordinal: Int
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Request.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Request.kt
index 040a38e0a0b..8696a090fc2 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Request.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Request.kt
@@ -19,13 +19,15 @@ package com.google.firebase.vertexai.common
import com.google.firebase.vertexai.common.util.fullModelName
import com.google.firebase.vertexai.type.Content
import com.google.firebase.vertexai.type.GenerationConfig
+import com.google.firebase.vertexai.type.ImagenImageFormat
+import com.google.firebase.vertexai.type.PublicPreviewAPI
import com.google.firebase.vertexai.type.SafetySetting
import com.google.firebase.vertexai.type.Tool
import com.google.firebase.vertexai.type.ToolConfig
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
-internal sealed interface Request
+internal interface Request
@Serializable
internal data class GenerateContentRequest(
@@ -65,3 +67,25 @@ internal data class CountTokensRequest(
)
}
}
+
+@Serializable
+internal data class GenerateImageRequest(
+ val instances: List,
+ val parameters: ImagenParameters,
+) : Request {
+ @Serializable internal data class ImagenPrompt(val prompt: String)
+
+ @OptIn(PublicPreviewAPI::class)
+ @Serializable
+ internal data class ImagenParameters(
+ val sampleCount: Int,
+ val includeRaiReason: Boolean,
+ val storageUri: String?,
+ val negativePrompt: String?,
+ val aspectRatio: String?,
+ val safetySetting: String?,
+ val personGeneration: String?,
+ val addWatermark: Boolean?,
+ val imageOutputOptions: ImagenImageFormat.Internal?,
+ )
+}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/internal/util/kotlin.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/internal/util/kotlin.kt
deleted file mode 100644
index b4d68d2f14c..00000000000
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/internal/util/kotlin.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2023 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.vertexai.internal.util
-
-import java.lang.reflect.Field
-
-/**
- * Removes the last character from the [StringBuilder].
- *
- * If the StringBuilder is empty, calling this function will throw an [IndexOutOfBoundsException].
- *
- * @return The [StringBuilder] used to make the call, for optional chaining.
- * @throws IndexOutOfBoundsException if the StringBuilder is empty.
- */
-internal fun StringBuilder.removeLast(): StringBuilder =
- if (isEmpty()) throw IndexOutOfBoundsException("StringBuilder is empty.")
- else deleteCharAt(length - 1)
-
-/**
- * A variant of [getAnnotation][Field.getAnnotation] that provides implicit Kotlin support.
- *
- * Syntax sugar for:
- * ```
- * getAnnotation(T::class.java)
- * ```
- */
-internal inline fun Field.getAnnotation() = getAnnotation(T::class.java)
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/java/ImagenModelFutures.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/java/ImagenModelFutures.kt
new file mode 100644
index 00000000000..97b043312c4
--- /dev/null
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/java/ImagenModelFutures.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2025 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.vertexai.java
+
+import androidx.concurrent.futures.SuspendToFutureAdapter
+import com.google.common.util.concurrent.ListenableFuture
+import com.google.firebase.vertexai.ImagenModel
+import com.google.firebase.vertexai.type.ImagenGenerationResponse
+import com.google.firebase.vertexai.type.ImagenInlineImage
+import com.google.firebase.vertexai.type.PublicPreviewAPI
+
+/**
+ * Wrapper class providing Java compatible methods for [ImagenModel].
+ *
+ * @see [ImagenModel]
+ */
+@PublicPreviewAPI
+public abstract class ImagenModelFutures internal constructor() {
+ /**
+ * Generates an image, returning the result directly to the caller.
+ *
+ * @param prompt The main text prompt from which the image is generated.
+ */
+ public abstract fun generateImages(
+ prompt: String,
+ ): ListenableFuture>
+
+ /** Returns the [ImagenModel] object wrapped by this object. */
+ public abstract fun getImageModel(): ImagenModel
+
+ private class FuturesImpl(private val model: ImagenModel) : ImagenModelFutures() {
+ override fun generateImages(
+ prompt: String,
+ ): ListenableFuture> =
+ SuspendToFutureAdapter.launchFuture { model.generateImages(prompt) }
+
+ override fun getImageModel(): ImagenModel = model
+ }
+
+ public companion object {
+
+ /** @return a [ImagenModelFutures] created around the provided [ImagenModel] */
+ @JvmStatic public fun from(model: ImagenModel): ImagenModelFutures = FuturesImpl(model)
+ }
+}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Exceptions.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Exceptions.kt
index 4f4ca954f36..4890cd7ada3 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Exceptions.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Exceptions.kt
@@ -44,7 +44,7 @@ internal constructor(message: String, cause: Throwable? = null) : RuntimeExcepti
is com.google.firebase.vertexai.common.InvalidAPIKeyException ->
InvalidAPIKeyException(cause.message ?: "")
is com.google.firebase.vertexai.common.PromptBlockedException ->
- PromptBlockedException(cause.response.toPublic(), cause.cause)
+ PromptBlockedException(cause.response?.toPublic(), cause.cause)
is com.google.firebase.vertexai.common.UnsupportedUserLocationException ->
UnsupportedUserLocationException(cause.cause)
is com.google.firebase.vertexai.common.InvalidStateException ->
@@ -57,6 +57,8 @@ internal constructor(message: String, cause: Throwable? = null) : RuntimeExcepti
ServiceDisabledException(cause.message ?: "", cause.cause)
is com.google.firebase.vertexai.common.UnknownException ->
UnknownException(cause.message ?: "", cause.cause)
+ is com.google.firebase.vertexai.common.ContentBlockedException ->
+ ContentBlockedException(cause.message ?: "", cause.cause)
else -> UnknownException(cause.message ?: "", cause)
}
is TimeoutCancellationException ->
@@ -87,13 +89,22 @@ internal constructor(message: String, cause: Throwable? = null) :
*
* @property response The full server response.
*/
-// TODO(rlazo): Add secondary constructor to pass through the message?
public class PromptBlockedException
-internal constructor(public val response: GenerateContentResponse, cause: Throwable? = null) :
+internal constructor(
+ public val response: GenerateContentResponse?,
+ cause: Throwable? = null,
+ message: String? = null,
+) :
FirebaseVertexAIException(
- "Prompt was blocked: ${response.promptFeedback?.blockReason?.name}",
+ "Prompt was blocked: ${response?.promptFeedback?.blockReason?.name?: message}",
cause,
- )
+ ) {
+ internal constructor(message: String, cause: Throwable? = null) : this(null, cause, message)
+}
+
+public class ContentBlockedException
+internal constructor(message: String, cause: Throwable? = null) :
+ FirebaseVertexAIException(message, cause)
/**
* The user's location (region) is not supported by the API.
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenAspectRatio.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenAspectRatio.kt
new file mode 100644
index 00000000000..e605a6e987e
--- /dev/null
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenAspectRatio.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2025 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.vertexai.type
+
+/** Represents the aspect ratio that the generated image should conform to. */
+@PublicPreviewAPI
+public class ImagenAspectRatio private constructor(internal val internalVal: String) {
+ public companion object {
+ /** A square image, useful for icons, profile pictures, etc. */
+ @JvmField public val SQUARE_1x1: ImagenAspectRatio = ImagenAspectRatio("1:1")
+ /** A portrait image in 3:4, the aspect ratio of older TVs. */
+ @JvmField public val PORTRAIT_3x4: ImagenAspectRatio = ImagenAspectRatio("3:4")
+ /** A landscape image in 4:3, the aspect ratio of older TVs. */
+ @JvmField public val LANDSCAPE_4x3: ImagenAspectRatio = ImagenAspectRatio("4:3")
+ /** A portrait image in 9:16, the aspect ratio of modern monitors and phone screens. */
+ @JvmField public val PORTRAIT_9x16: ImagenAspectRatio = ImagenAspectRatio("9:16")
+ /** A landscape image in 16:9, the aspect ratio of modern monitors and phone screens. */
+ @JvmField public val LANDSCAPE_16x9: ImagenAspectRatio = ImagenAspectRatio("16:9")
+ }
+}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGCSImage.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGCSImage.kt
new file mode 100644
index 00000000000..380bfa3c30b
--- /dev/null
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGCSImage.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2025 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.vertexai.type
+
+/**
+ * Represents an Imagen-generated image that is contained in Google Cloud Storage.
+ *
+ * @param gcsUri Contains the `gs://` URI for the image.
+ * @param mimeType Contains the MIME type of the image (for example, `"image/png"`).
+ */
+@PublicPreviewAPI
+internal class ImagenGCSImage
+internal constructor(public val gcsUri: String, public val mimeType: String) {}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt
new file mode 100644
index 00000000000..bbf795f4d40
--- /dev/null
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2025 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.vertexai.type
+
+/**
+ * Contains extra settings to configure image generation.
+ *
+ * @param negativePrompt This string contains things that should be explicitly excluded from
+ * generated images.
+ * @param numberOfImages How many images should be generated.
+ * @param aspectRatio The aspect ratio of the generated images.
+ * @param imageFormat The file format/compression of the generated images.
+ * @param addWatermark Adds an invisible watermark to mark the image as AI generated.
+ */
+@PublicPreviewAPI
+public class ImagenGenerationConfig(
+ public val negativePrompt: String? = null,
+ public val numberOfImages: Int? = 1,
+ public val aspectRatio: ImagenAspectRatio? = null,
+ public val imageFormat: ImagenImageFormat? = null,
+ public val addWatermark: Boolean? = null,
+) {
+ /**
+ * Builder for creating a [ImagenGenerationConfig].
+ *
+ * This is mainly intended for Java interop. For Kotlin, use [imagenGenerationConfig] for a more
+ * idiomatic experience.
+ *
+ * @property negativePrompt See [ImagenGenerationConfig.negativePrompt].
+ * @property numberOfImages See [ImagenGenerationConfig.numberOfImages].
+ * @property aspectRatio See [ImagenGenerationConfig.aspectRatio].
+ * @property imageFormat See [ImagenGenerationConfig.imageFormat]
+ * @property addWatermark See [ImagenGenerationConfig.addWatermark]
+ * @see [imagenGenerationConfig]
+ */
+ public class Builder {
+ @JvmField public var negativePrompt: String? = null
+ @JvmField public var numberOfImages: Int? = 1
+ @JvmField public var aspectRatio: ImagenAspectRatio? = null
+ @JvmField public var imageFormat: ImagenImageFormat? = null
+ @JvmField public var addWatermark: Boolean? = null
+
+ /**
+ * Alternative casing for [ImagenGenerationConfig.Builder]:
+ * ```
+ * val config = GenerationConfig.builder()
+ * ```
+ */
+ public fun build(): ImagenGenerationConfig =
+ ImagenGenerationConfig(
+ negativePrompt = negativePrompt,
+ numberOfImages = numberOfImages,
+ aspectRatio = aspectRatio,
+ imageFormat = imageFormat,
+ addWatermark = addWatermark,
+ )
+ }
+
+ public companion object {
+ public fun builder(): Builder = Builder()
+ }
+}
+
+/**
+ * Helper method to construct a [ImagenGenerationConfig] in a DSL-like manner.
+ *
+ * Example Usage:
+ * ```
+ * imagenGenerationConfig {
+ * negativePrompt = "People, black and white, painting"
+ * numberOfImages = 1
+ * aspectRatio = ImagenAspecRatio.SQUARE_1x1
+ * imageFormat = ImagenImageFormat.png()
+ * addWatermark = false
+ * }
+ * ```
+ */
+@PublicPreviewAPI
+public fun imagenGenerationConfig(
+ init: ImagenGenerationConfig.Builder.() -> Unit
+): ImagenGenerationConfig {
+ val builder = ImagenGenerationConfig.builder()
+ builder.init()
+ return builder.build()
+}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationResponse.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationResponse.kt
new file mode 100644
index 00000000000..a1a80360848
--- /dev/null
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationResponse.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2025 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.vertexai.type
+
+import kotlinx.serialization.Serializable
+
+/**
+ * Represents a response from a call to [ImagenModel#generateImages]
+ *
+ * @param images contains the generated images
+ * @param filteredReason if fewer images were generated than were requested, this field will contain
+ * the reason they were filtered out.
+ */
+@PublicPreviewAPI
+public class ImagenGenerationResponse
+internal constructor(public val images: List, public val filteredReason: String?) {
+
+ @Serializable
+ internal data class Internal(val predictions: List) {
+ internal fun toPublicGCS() =
+ ImagenGenerationResponse(
+ images = predictions.filter { it.mimeType != null }.map { it.toPublicGCS() },
+ null,
+ )
+
+ internal fun toPublicInline() =
+ ImagenGenerationResponse(
+ images = predictions.filter { it.mimeType != null }.map { it.toPublicInline() },
+ null,
+ )
+ }
+
+ @Serializable
+ internal data class ImagenImageResponse(
+ val bytesBase64Encoded: String? = null,
+ val gcsUri: String? = null,
+ val mimeType: String? = null,
+ val raiFilteredReason: String? = null,
+ ) {
+ internal fun toPublicInline() =
+ ImagenInlineImage(bytesBase64Encoded!!.toByteArray(), mimeType!!)
+
+ internal fun toPublicGCS() = ImagenGCSImage(gcsUri!!, mimeType!!)
+ }
+}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenImageFormat.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenImageFormat.kt
new file mode 100644
index 00000000000..41c85e98a7a
--- /dev/null
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenImageFormat.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2025 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.vertexai.type
+
+import kotlinx.serialization.Serializable
+
+/**
+ * Represents the format an image should be returned in.
+ *
+ * @param mimeType A string (like `"image/jpeg"`) specifying the encoding MIME type of the image.
+ * @param compressionQuality an int (1-100) representing the quality of the image; a lower number
+ * means the image is permitted to be lower quality to reduce size. This parameter is not relevant
+ * for every MIME type.
+ */
+@PublicPreviewAPI
+public class ImagenImageFormat
+private constructor(public val mimeType: String, public val compressionQuality: Int?) {
+
+ internal fun toInternal() = Internal(mimeType, compressionQuality)
+
+ @Serializable internal data class Internal(val mimeType: String, val compressionQuality: Int?)
+
+ public companion object {
+ /**
+ * An [ImagenImageFormat] representing a JPEG image.
+ *
+ * @param compressionQuality an int (1-100) representing the quality of the image; a lower
+ * number means the image is permitted to be lower quality to reduce size.
+ */
+ public fun jpeg(compressionQuality: Int? = null): ImagenImageFormat {
+ return ImagenImageFormat("image/jpeg", compressionQuality)
+ }
+
+ /** An [ImagenImageFormat] representing a PNG image */
+ public fun png(): ImagenImageFormat {
+ return ImagenImageFormat("image/png", null)
+ }
+ }
+}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenInlineImage.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenInlineImage.kt
new file mode 100644
index 00000000000..03e93abf8e7
--- /dev/null
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenInlineImage.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2025 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.vertexai.type
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.util.Base64
+
+/**
+ * Represents an Imagen-generated image that is contained inline
+ *
+ * @param data Contains the raw bytes of the image
+ * @param mimeType Contains the MIME type of the image (for example, `"image/png"`)
+ */
+@PublicPreviewAPI
+public class ImagenInlineImage
+internal constructor(public val data: ByteArray, public val mimeType: String) {
+
+ /**
+ * Returns the image as an Android OS native [Bitmap] so that it can be saved or sent to the UI.
+ */
+ public fun asBitmap(): Bitmap {
+ val data = Base64.decode(data, Base64.NO_WRAP)
+ return BitmapFactory.decodeByteArray(data, 0, data.size)
+ }
+}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenPersonFilterLevel.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenPersonFilterLevel.kt
new file mode 100644
index 00000000000..14031c86766
--- /dev/null
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenPersonFilterLevel.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 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.vertexai.type
+
+/** A filter used to prevent images from containing depictions of children or people. */
+@PublicPreviewAPI
+public class ImagenPersonFilterLevel private constructor(internal val internalVal: String) {
+ public companion object {
+ /** No filters applied. */
+ @JvmField public val ALLOW_ALL: ImagenPersonFilterLevel = ImagenPersonFilterLevel("allow_all")
+ /** Filters out any images containing depictions of children. */
+ @JvmField
+ public val ALLOW_ADULT: ImagenPersonFilterLevel = ImagenPersonFilterLevel("allow_adult")
+ /** Filters out any images containing depictions of people. */
+ @JvmField public val BLOCK_ALL: ImagenPersonFilterLevel = ImagenPersonFilterLevel("dont_allow")
+ }
+}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenSafetyFilterLevel.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenSafetyFilterLevel.kt
new file mode 100644
index 00000000000..205538ebc0a
--- /dev/null
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenSafetyFilterLevel.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2025 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.vertexai.type
+
+/** Used for safety filtering. */
+@PublicPreviewAPI
+public class ImagenSafetyFilterLevel private constructor(internal val internalVal: String) {
+ public companion object {
+ /** Strongest filtering level, most strict blocking. */
+ @JvmField
+ public val BLOCK_LOW_AND_ABOVE: ImagenSafetyFilterLevel =
+ ImagenSafetyFilterLevel("block_low_and_above")
+ /** Block some problematic prompts and responses. */
+ @JvmField
+ public val BLOCK_MEDIUM_AND_ABOVE: ImagenSafetyFilterLevel =
+ ImagenSafetyFilterLevel("block_medium_and_above")
+ /**
+ * Reduces the number of requests blocked due to safety filters. May increase objectionable
+ * content generated by the Imagen model.
+ */
+ @JvmField
+ public val BLOCK_ONLY_HIGH: ImagenSafetyFilterLevel = ImagenSafetyFilterLevel("block_only_high")
+ /** Turns off all optional safety filters. */
+ @JvmField public val BLOCK_NONE: ImagenSafetyFilterLevel = ImagenSafetyFilterLevel("block_none")
+ }
+}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenSafetySettings.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenSafetySettings.kt
new file mode 100644
index 00000000000..d5a00b557bd
--- /dev/null
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenSafetySettings.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2025 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.vertexai.type
+
+/**
+ * A configuration for filtering unsafe content or images containing people.
+ *
+ * @param safetyFilterLevel Used to filter unsafe content.
+ * @param personFilterLevel Used to filter images containing people.
+ */
+@PublicPreviewAPI
+public class ImagenSafetySettings(
+ internal val safetyFilterLevel: ImagenSafetyFilterLevel,
+ internal val personFilterLevel: ImagenPersonFilterLevel,
+) {}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/PublicPreviewAPI.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/PublicPreviewAPI.kt
new file mode 100644
index 00000000000..50f9880f3be
--- /dev/null
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/PublicPreviewAPI.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2025 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.vertexai.type
+
+@Retention(AnnotationRetention.BINARY)
+@RequiresOptIn(
+ level = RequiresOptIn.Level.ERROR,
+ message =
+ "This API is part of an experimental public preview and may change in " +
+ "backwards-incompatible ways without notice.",
+)
+public annotation class PublicPreviewAPI()
diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/StreamingSnapshotTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/StreamingSnapshotTests.kt
index 9a6beb057a1..ce53bcf9e33 100644
--- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/StreamingSnapshotTests.kt
+++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/StreamingSnapshotTests.kt
@@ -119,7 +119,7 @@ internal class StreamingSnapshotTests {
withTimeout(testTimeout) {
val exception = shouldThrow { responses.collect() }
- exception.response.promptFeedback?.blockReason shouldBe BlockReason.SAFETY
+ exception.response?.promptFeedback?.blockReason shouldBe BlockReason.SAFETY
}
}
@@ -130,8 +130,8 @@ internal class StreamingSnapshotTests {
withTimeout(testTimeout) {
val exception = shouldThrow { responses.collect() }
- exception.response.promptFeedback?.blockReason shouldBe BlockReason.SAFETY
- exception.response.promptFeedback?.blockReasonMessage shouldBe "Reasons"
+ exception.response?.promptFeedback?.blockReason shouldBe BlockReason.SAFETY
+ exception.response?.promptFeedback?.blockReasonMessage shouldBe "Reasons"
}
}
diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt
index 11d5a0df052..1724b3788cb 100644
--- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt
+++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt
@@ -17,6 +17,7 @@
package com.google.firebase.vertexai
import com.google.firebase.vertexai.type.BlockReason
+import com.google.firebase.vertexai.type.ContentBlockedException
import com.google.firebase.vertexai.type.ContentModality
import com.google.firebase.vertexai.type.FinishReason
import com.google.firebase.vertexai.type.FunctionCallPart
@@ -25,6 +26,7 @@ import com.google.firebase.vertexai.type.HarmProbability
import com.google.firebase.vertexai.type.HarmSeverity
import com.google.firebase.vertexai.type.InvalidAPIKeyException
import com.google.firebase.vertexai.type.PromptBlockedException
+import com.google.firebase.vertexai.type.PublicPreviewAPI
import com.google.firebase.vertexai.type.ResponseStoppedException
import com.google.firebase.vertexai.type.SerializationException
import com.google.firebase.vertexai.type.ServerException
@@ -53,6 +55,7 @@ import kotlinx.serialization.json.jsonPrimitive
import org.json.JSONArray
import org.junit.Test
+@OptIn(PublicPreviewAPI::class)
internal class UnarySnapshotTests {
private val testTimeout = 5.seconds
@@ -125,7 +128,7 @@ internal class UnarySnapshotTests {
withTimeout(testTimeout) {
shouldThrow { model.generateContent("prompt") } should
{
- it.response.promptFeedback?.blockReason shouldBe BlockReason.UNKNOWN
+ it.response?.promptFeedback?.blockReason shouldBe BlockReason.UNKNOWN
}
}
}
@@ -180,7 +183,7 @@ internal class UnarySnapshotTests {
withTimeout(testTimeout) {
shouldThrow { model.generateContent("prompt") } should
{
- it.response.promptFeedback?.blockReason shouldBe BlockReason.SAFETY
+ it.response?.promptFeedback?.blockReason shouldBe BlockReason.SAFETY
}
}
}
@@ -191,8 +194,8 @@ internal class UnarySnapshotTests {
withTimeout(testTimeout) {
shouldThrow { model.generateContent("prompt") } should
{
- it.response.promptFeedback?.blockReason shouldBe BlockReason.SAFETY
- it.response.promptFeedback?.blockReasonMessage shouldContain "Reasons"
+ it.response?.promptFeedback?.blockReason shouldBe BlockReason.SAFETY
+ it.response?.promptFeedback?.blockReasonMessage shouldContain "Reasons"
}
}
}
@@ -215,7 +218,7 @@ internal class UnarySnapshotTests {
fun `user location error`() =
goldenUnaryFile(
"unary-failure-unsupported-user-location.json",
- HttpStatusCode.PreconditionFailed
+ HttpStatusCode.PreconditionFailed,
) {
withTimeout(testTimeout) {
shouldThrow { model.generateContent("prompt") }
@@ -515,4 +518,23 @@ internal class UnarySnapshotTests {
goldenUnaryFile("unary-failure-model-not-found.json", HttpStatusCode.NotFound) {
withTimeout(testTimeout) { shouldThrow { model.countTokens("prompt") } }
}
+
+ @Test
+ fun `generateImages should throw when all images filtered`() =
+ goldenUnaryFile("unary-failure-generate-images-all-filtered.json") {
+ withTimeout(testTimeout) {
+ shouldThrow { imagenModel.generateImages("prompt") }
+ }
+ }
+
+ @Test
+ fun `generateImages should throw when prompt blocked`() =
+ goldenUnaryFile(
+ "unary-failure-generate-images-prompt-blocked.json",
+ HttpStatusCode.BadRequest,
+ ) {
+ withTimeout(testTimeout) {
+ shouldThrow { imagenModel.generateImages("prompt") }
+ }
+ }
}
diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt
index 2d29ad38ba7..6d14025b28c 100644
--- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt
+++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt
@@ -105,7 +105,7 @@ internal class StreamingSnapshotTests {
withTimeout(testTimeout) {
val exception = shouldThrow { responses.collect() }
- exception.response.promptFeedback?.blockReason shouldBe BlockReason.Internal.SAFETY
+ exception.response?.promptFeedback?.blockReason shouldBe BlockReason.Internal.SAFETY
}
}
diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/UnarySnapshotTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/UnarySnapshotTests.kt
index 33ebdda5322..c316a9ece81 100644
--- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/UnarySnapshotTests.kt
+++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/UnarySnapshotTests.kt
@@ -111,7 +111,7 @@ internal class UnarySnapshotTests {
withTimeout(testTimeout) {
shouldThrow {
apiController.generateContent(textGenerateContentRequest("prompt"))
- } should { it.response.promptFeedback?.blockReason shouldBe BlockReason.Internal.SAFETY }
+ } should { it.response?.promptFeedback?.blockReason shouldBe BlockReason.Internal.SAFETY }
}
}
diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/util/tests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/util/tests.kt
index 29b7923e35b..9428aea67ef 100644
--- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/util/tests.kt
+++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/util/tests.kt
@@ -14,10 +14,14 @@
* limitations under the License.
*/
+@file:OptIn(PublicPreviewAPI::class)
+
package com.google.firebase.vertexai.util
import com.google.firebase.vertexai.GenerativeModel
+import com.google.firebase.vertexai.ImagenModel
import com.google.firebase.vertexai.common.APIController
+import com.google.firebase.vertexai.type.PublicPreviewAPI
import com.google.firebase.vertexai.type.RequestOptions
import io.kotest.matchers.collections.shouldNotBeEmpty
import io.kotest.matchers.nulls.shouldNotBeNull
@@ -57,7 +61,11 @@ internal suspend fun ByteChannel.send(bytes: ByteArray) {
* @see commonTest
* @see send
*/
-internal data class CommonTestScope(val channel: ByteChannel, val model: GenerativeModel)
+internal data class CommonTestScope(
+ val channel: ByteChannel,
+ val model: GenerativeModel,
+ val imagenModel: ImagenModel,
+)
/** A test that runs under a [CommonTestScope]. */
internal typealias CommonTest = suspend CommonTestScope.() -> Unit
@@ -104,7 +112,8 @@ internal fun commonTest(
null,
)
val model = GenerativeModel("cool-model-name", controller = apiController)
- CommonTestScope(channel, model).block()
+ val imagenModel = ImagenModel("cooler-model-name", controller = apiController)
+ CommonTestScope(channel, model, imagenModel).block()
}
/**
From 65ff901f2f97dc9cd275960945eed21f5287f487 Mon Sep 17 00:00:00 2001
From: David Motsonashvili
Date: Fri, 21 Feb 2025 19:32:46 +0000
Subject: [PATCH 056/146] add new recipe entries to BoM generator (#6702)
Co-authored-by: David Motsonashvili
---
.../gradle/bomgenerator/GenerateTutorialBundleTask.kt | 6 ++++++
.../google/firebase/gradle/plugins/PublishingPlugin.kt | 8 +++++++-
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateTutorialBundleTask.kt b/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateTutorialBundleTask.kt
index b315e7188aa..e99565d47f7 100644
--- a/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateTutorialBundleTask.kt
+++ b/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateTutorialBundleTask.kt
@@ -257,6 +257,12 @@ abstract class GenerateTutorialBundleTask : DefaultTask() {
ArtifactTutorialMapping("FIAM Display", "fiamd-dependency"),
"com.google.firebase:firebase-ml-vision" to
ArtifactTutorialMapping("Firebase MLKit Vision", "ml-vision-dependency"),
+ "androidx.credentials:credentials" to
+ ArtifactTutorialMapping("Auth Google Sign In", "auth-google-signin-first-dependency"),
+ "androidx.credentials:credentials-play-services-auth" to
+ ArtifactTutorialMapping("Auth Google Sign In", "auth-google-signin-second-dependency"),
+ "com.google.android.libraries.identity.googleid" to
+ ArtifactTutorialMapping("Auth Google Sign In", "auth-google-signin-third-dependency"),
"com.google.firebase:firebase-appdistribution-gradle" to
ArtifactTutorialMapping(
"App Distribution",
diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt
index 82a2aaae858..e8384b4579f 100644
--- a/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt
+++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt
@@ -820,7 +820,13 @@ abstract class PublishingPlugin : Plugin {
/** Artifacts that we use in the tutorial bundle, but _not_ in the bom. */
val EXTRA_TUTORIAL_ARTIFACTS =
- listOf("com.google.android.gms:play-services-ads", "com.google.firebase:firebase-ml-vision")
+ listOf(
+ "com.google.android.gms:play-services-ads",
+ "com.google.firebase:firebase-ml-vision",
+ "androidx.credentials:credentials",
+ "androidx.credentials:credentials-play-services-auth",
+ "com.google.android.libraries.identity.googleid:googleid",
+ )
}
}
From 465a7294990a9e7a7ddbe9b57b5db44da5439cf2 Mon Sep 17 00:00:00 2001
From: Rodrigo Lazo
Date: Mon, 24 Feb 2025 10:51:39 -0500
Subject: [PATCH 057/146] Improve imagen Java API (#6712)
- Make the `ImagenGenerationConfig.Builder` follow the builder pattern
- Mark companion object `ImagenImageFormat` methods as @JvmStatic for
easier access
---
firebase-vertexai/api.txt | 8 ++++
.../vertexai/type/ImagenGenerationConfig.kt | 45 ++++++++++++++++---
.../vertexai/type/ImagenImageFormat.kt | 2 +
3 files changed, 48 insertions(+), 7 deletions(-)
diff --git a/firebase-vertexai/api.txt b/firebase-vertexai/api.txt
index abfbf6572d4..01a7204ac73 100644
--- a/firebase-vertexai/api.txt
+++ b/firebase-vertexai/api.txt
@@ -428,6 +428,11 @@ package com.google.firebase.vertexai.type {
public static final class ImagenGenerationConfig.Builder {
ctor public ImagenGenerationConfig.Builder();
method public com.google.firebase.vertexai.type.ImagenGenerationConfig build();
+ method public com.google.firebase.vertexai.type.ImagenGenerationConfig.Builder setAddWatermark(boolean addWatermark);
+ method public com.google.firebase.vertexai.type.ImagenGenerationConfig.Builder setAspectRatio(com.google.firebase.vertexai.type.ImagenAspectRatio aspectRatio);
+ method public com.google.firebase.vertexai.type.ImagenGenerationConfig.Builder setImageFormat(com.google.firebase.vertexai.type.ImagenImageFormat imageFormat);
+ method public com.google.firebase.vertexai.type.ImagenGenerationConfig.Builder setNegativePrompt(String negativePrompt);
+ method public com.google.firebase.vertexai.type.ImagenGenerationConfig.Builder setNumberOfImages(int numberOfImages);
field public Boolean? addWatermark;
field public com.google.firebase.vertexai.type.ImagenAspectRatio? aspectRatio;
field public com.google.firebase.vertexai.type.ImagenImageFormat? imageFormat;
@@ -441,6 +446,7 @@ package com.google.firebase.vertexai.type {
public final class ImagenGenerationConfigKt {
method @com.google.firebase.vertexai.type.PublicPreviewAPI public static com.google.firebase.vertexai.type.ImagenGenerationConfig imagenGenerationConfig(kotlin.jvm.functions.Function1 super com.google.firebase.vertexai.type.ImagenGenerationConfig.Builder,kotlin.Unit> init);
+ method public static void xx();
}
@com.google.firebase.vertexai.type.PublicPreviewAPI public final class ImagenGenerationResponse {
@@ -453,6 +459,8 @@ package com.google.firebase.vertexai.type {
@com.google.firebase.vertexai.type.PublicPreviewAPI public final class ImagenImageFormat {
method public Integer? getCompressionQuality();
method public String getMimeType();
+ method public static com.google.firebase.vertexai.type.ImagenImageFormat jpeg(Integer? compressionQuality = null);
+ method public static com.google.firebase.vertexai.type.ImagenImageFormat png();
property public final Integer? compressionQuality;
property public final String mimeType;
field public static final com.google.firebase.vertexai.type.ImagenImageFormat.Companion Companion;
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt
index bbf795f4d40..7f5b935bd1f 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt
@@ -26,6 +26,8 @@ package com.google.firebase.vertexai.type
* @param imageFormat The file format/compression of the generated images.
* @param addWatermark Adds an invisible watermark to mark the image as AI generated.
*/
+import kotlin.jvm.JvmField
+
@PublicPreviewAPI
public class ImagenGenerationConfig(
public val negativePrompt: String? = null,
@@ -39,13 +41,6 @@ public class ImagenGenerationConfig(
*
* This is mainly intended for Java interop. For Kotlin, use [imagenGenerationConfig] for a more
* idiomatic experience.
- *
- * @property negativePrompt See [ImagenGenerationConfig.negativePrompt].
- * @property numberOfImages See [ImagenGenerationConfig.numberOfImages].
- * @property aspectRatio See [ImagenGenerationConfig.aspectRatio].
- * @property imageFormat See [ImagenGenerationConfig.imageFormat]
- * @property addWatermark See [ImagenGenerationConfig.addWatermark]
- * @see [imagenGenerationConfig]
*/
public class Builder {
@JvmField public var negativePrompt: String? = null
@@ -54,6 +49,31 @@ public class ImagenGenerationConfig(
@JvmField public var imageFormat: ImagenImageFormat? = null
@JvmField public var addWatermark: Boolean? = null
+ /** See [ImagenGenerationConfig.negativePrompt]. */
+ public fun setNegativePrompt(negativePrompt: String): Builder = apply {
+ this.negativePrompt = negativePrompt
+ }
+
+ /** See [ImagenGenerationConfig.numberOfImages]. */
+ public fun setNumberOfImages(numberOfImages: Int): Builder = apply {
+ this.numberOfImages = numberOfImages
+ }
+
+ /** See [ImagenGenerationConfig.aspectRatio]. */
+ public fun setAspectRatio(aspectRatio: ImagenAspectRatio): Builder = apply {
+ this.aspectRatio = aspectRatio
+ }
+
+ /** See [ImagenGenerationConfig.imageFormat]. */
+ public fun setImageFormat(imageFormat: ImagenImageFormat): Builder = apply {
+ this.imageFormat = imageFormat
+ }
+
+ /** See [ImagenGenerationConfig.addWatermark]. */
+ public fun setAddWatermark(addWatermark: Boolean): Builder = apply {
+ this.addWatermark = addWatermark
+ }
+
/**
* Alternative casing for [ImagenGenerationConfig.Builder]:
* ```
@@ -97,3 +117,14 @@ public fun imagenGenerationConfig(
builder.init()
return builder.build()
}
+
+@OptIn(PublicPreviewAPI::class)
+public fun xx() {
+ imagenGenerationConfig {
+ negativePrompt = "People, black and white, painting"
+ numberOfImages = 1
+ aspectRatio = ImagenAspectRatio.SQUARE_1x1
+ imageFormat = ImagenImageFormat.png()
+ addWatermark = false
+ }
+}
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenImageFormat.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenImageFormat.kt
index 41c85e98a7a..5a44ddc3964 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenImageFormat.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenImageFormat.kt
@@ -41,11 +41,13 @@ private constructor(public val mimeType: String, public val compressionQuality:
* @param compressionQuality an int (1-100) representing the quality of the image; a lower
* number means the image is permitted to be lower quality to reduce size.
*/
+ @JvmStatic
public fun jpeg(compressionQuality: Int? = null): ImagenImageFormat {
return ImagenImageFormat("image/jpeg", compressionQuality)
}
/** An [ImagenImageFormat] representing a PNG image */
+ @JvmStatic
public fun png(): ImagenImageFormat {
return ImagenImageFormat("image/png", null)
}
From 7f2271d6d83fbfef34fcdfbc917682059e27283a Mon Sep 17 00:00:00 2001
From: Rodrigo Lazo
Date: Mon, 24 Feb 2025 12:46:02 -0500
Subject: [PATCH 058/146] Remove test code left by mistake (#6717)
Should be more careful with those changes...
---
firebase-vertexai/api.txt | 1 -
.../firebase/vertexai/type/ImagenGenerationConfig.kt | 11 -----------
2 files changed, 12 deletions(-)
diff --git a/firebase-vertexai/api.txt b/firebase-vertexai/api.txt
index 01a7204ac73..ecf5ab8eefc 100644
--- a/firebase-vertexai/api.txt
+++ b/firebase-vertexai/api.txt
@@ -446,7 +446,6 @@ package com.google.firebase.vertexai.type {
public final class ImagenGenerationConfigKt {
method @com.google.firebase.vertexai.type.PublicPreviewAPI public static com.google.firebase.vertexai.type.ImagenGenerationConfig imagenGenerationConfig(kotlin.jvm.functions.Function1 super com.google.firebase.vertexai.type.ImagenGenerationConfig.Builder,kotlin.Unit> init);
- method public static void xx();
}
@com.google.firebase.vertexai.type.PublicPreviewAPI public final class ImagenGenerationResponse {
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt
index 7f5b935bd1f..d05840d9cfc 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt
@@ -117,14 +117,3 @@ public fun imagenGenerationConfig(
builder.init()
return builder.build()
}
-
-@OptIn(PublicPreviewAPI::class)
-public fun xx() {
- imagenGenerationConfig {
- negativePrompt = "People, black and white, painting"
- numberOfImages = 1
- aspectRatio = ImagenAspectRatio.SQUARE_1x1
- imageFormat = ImagenImageFormat.png()
- addWatermark = false
- }
-}
From c600ab1fa32e681dc078b09bcf0eebb3065fea12 Mon Sep 17 00:00:00 2001
From: Rodrigo Lazo
Date: Mon, 24 Feb 2025 13:30:27 -0500
Subject: [PATCH 059/146] Add missing optIn declarations to reduce compilation
noise (#6713)
Part of the serialization API we use requires optIn, and without the
correct declarations we get warnings printed when compiling the code.
---
.../com/google/firebase/vertexai/GenerativeModel.kt | 2 ++
.../com/google/firebase/vertexai/common/Request.kt | 10 ++--------
.../com/google/firebase/vertexai/type/Candidate.kt | 2 ++
.../com/google/firebase/vertexai/type/Content.kt | 1 +
.../google/firebase/vertexai/GenerativeModelTesting.kt | 2 ++
.../firebase/vertexai/common/APIControllerTests.kt | 2 ++
.../firebase/vertexai/common/StreamingSnapshotTests.kt | 2 ++
7 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/GenerativeModel.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/GenerativeModel.kt
index a49d4c279a8..12d89ab5b59 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/GenerativeModel.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/GenerativeModel.kt
@@ -40,6 +40,7 @@ import com.google.firebase.vertexai.type.content
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
+import kotlinx.serialization.ExperimentalSerializationApi
/**
* Represents a multimodal model (like Gemini), capable of generating content based on various input
@@ -199,6 +200,7 @@ internal constructor(
return countTokens(content { image(prompt) })
}
+ @OptIn(ExperimentalSerializationApi::class)
private fun constructRequest(vararg prompt: Content) =
GenerateContentRequest(
modelName,
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Request.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Request.kt
index 8696a090fc2..7f84e053147 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Request.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Request.kt
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+@file:OptIn(ExperimentalSerializationApi::class)
package com.google.firebase.vertexai.common
@@ -24,6 +25,7 @@ import com.google.firebase.vertexai.type.PublicPreviewAPI
import com.google.firebase.vertexai.type.SafetySetting
import com.google.firebase.vertexai.type.Tool
import com.google.firebase.vertexai.type.ToolConfig
+import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -49,14 +51,6 @@ internal data class CountTokensRequest(
@SerialName("system_instruction") val systemInstruction: Content.Internal? = null,
) : Request {
companion object {
- fun forGenAI(generateContentRequest: GenerateContentRequest) =
- CountTokensRequest(
- generateContentRequest =
- generateContentRequest.model?.let {
- generateContentRequest.copy(model = fullModelName(it))
- }
- ?: generateContentRequest
- )
fun forVertexAI(generateContentRequest: GenerateContentRequest) =
CountTokensRequest(
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Candidate.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Candidate.kt
index 5d236c8ecc9..b84bd6929f4 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Candidate.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Candidate.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalSerializationApi::class)
+
package com.google.firebase.vertexai.type
import com.google.firebase.vertexai.common.util.FirstOrdinalSerializer
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Content.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Content.kt
index 241d0becfe6..9364f9cad3c 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Content.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Content.kt
@@ -80,6 +80,7 @@ constructor(public val role: String? = "user", public val parts: List) {
public fun build(): Content = Content(role, parts)
}
+ @OptIn(ExperimentalSerializationApi::class)
internal fun toInternal() = Internal(this.role ?: "user", this.parts.map { it.toInternal() })
@ExperimentalSerializationApi
diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/GenerativeModelTesting.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/GenerativeModelTesting.kt
index 67d41c9b5d6..d4c2ad37926 100644
--- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/GenerativeModelTesting.kt
+++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/GenerativeModelTesting.kt
@@ -40,6 +40,7 @@ import io.ktor.http.content.TextContent
import io.ktor.http.headersOf
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.withTimeout
+import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.encodeToString
import org.junit.Test
@@ -127,6 +128,7 @@ internal class GenerativeModelTesting {
exception.message shouldContain "location"
}
+ @OptIn(ExperimentalSerializationApi::class)
private fun generateContentResponseAsJsonString(text: String): String {
return JSON.encodeToString(
GenerateContentResponse.Internal(
diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/APIControllerTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/APIControllerTests.kt
index 463dbe773f7..29b52b81d1b 100644
--- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/APIControllerTests.kt
+++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/APIControllerTests.kt
@@ -46,6 +46,7 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeout
+import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.JsonObject
import org.junit.Test
@@ -84,6 +85,7 @@ internal class APIControllerTests {
}
}
+@OptIn(ExperimentalSerializationApi::class)
internal class RequestFormatTests {
@Test
fun `using default endpoint`() = doBlocking {
diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt
index 6d14025b28c..4abf386765a 100644
--- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt
+++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt
@@ -30,8 +30,10 @@ import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.withTimeout
+import kotlinx.serialization.ExperimentalSerializationApi
import org.junit.Test
+@OptIn(ExperimentalSerializationApi::class)
internal class StreamingSnapshotTests {
private val testTimeout = 5.seconds
From ad9f6e22c59897de64e46249ff05273704b56694 Mon Sep 17 00:00:00 2001
From: Konstantin Svist
Date: Mon, 24 Feb 2025 12:00:27 -0800
Subject: [PATCH 060/146] Support custom tabs in more browsers (#6705)
fixes #6692
---
firebase-appdistribution/src/main/AndroidManifest.xml | 6 ++++++
.../appdistribution/impl/TesterSignInManager.java | 11 ++++-------
2 files changed, 10 insertions(+), 7 deletions(-)
diff --git a/firebase-appdistribution/src/main/AndroidManifest.xml b/firebase-appdistribution/src/main/AndroidManifest.xml
index ef91581edc3..452fe856e6c 100644
--- a/firebase-appdistribution/src/main/AndroidManifest.xml
+++ b/firebase-appdistribution/src/main/AndroidManifest.xml
@@ -21,6 +21,12 @@
+
+
+
+
+
+
resolveInfos =
- context.getPackageManager().queryIntentServices(customTabIntent, 0);
- return resolveInfos != null && !resolveInfos.isEmpty();
+ String packageName = CustomTabsClient.getPackageName(context, Collections.emptyList());
+ return packageName != null;
}
}
From feeeed6e2c61c71fb3d107e7b8d1fe0634c60b22 Mon Sep 17 00:00:00 2001
From: Matthew Robertson
Date: Mon, 24 Feb 2025 15:32:20 -0500
Subject: [PATCH 061/146] Update CHANGELOG for Crashlytics and NDK (#6719)
---
firebase-crashlytics-ndk/CHANGELOG.md | 2 +-
firebase-crashlytics/CHANGELOG.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/firebase-crashlytics-ndk/CHANGELOG.md b/firebase-crashlytics-ndk/CHANGELOG.md
index b5b8f7868d4..ab8dd8dfdf3 100644
--- a/firebase-crashlytics-ndk/CHANGELOG.md
+++ b/firebase-crashlytics-ndk/CHANGELOG.md
@@ -1,5 +1,5 @@
# Unreleased
-
+* [changed] Updated `firebase-crashlytics` dependency to v19.4.1
# 19.3.0
* [changed] Updated `firebase-crashlytics` dependency to v19.3.0
diff --git a/firebase-crashlytics/CHANGELOG.md b/firebase-crashlytics/CHANGELOG.md
index 7086b0b0c9d..f0bbb91128b 100644
--- a/firebase-crashlytics/CHANGELOG.md
+++ b/firebase-crashlytics/CHANGELOG.md
@@ -1,5 +1,5 @@
# Unreleased
-
+* [changed] Updated `firebase-sessions` dependency to v2.0.9
# 19.4.0
* [feature] Added an overload for `recordException` that allows logging additional custom
From dcb3cb0984934b0c1331c437b15be5b6ee5136fb Mon Sep 17 00:00:00 2001
From: Daymon <17409137+daymxn@users.noreply.github.com>
Date: Mon, 24 Feb 2025 14:37:26 -0600
Subject: [PATCH 062/146] Bump well known types (#6716)
Per [b/398840288](https://b.corp.google.com/issues/398840288),
This bumps `protolite-well-known-types` to properly utilize `3.25.5`. It
seems as though this was an oversight in #6343, but since gradle uses
the highest version when resolving dependency conflicts (and all the
existing libraries already use `3.25.5`), this isn't a major issue. This
is only really an issue if someone is using `protolite-well-known-types`
in isolation (which isn't really a use-case we're shipping for). But the
main reason for fixing this is that it causes a bit of confusion when
trying to track dependency issues (see issue #6674 for an example of
this).
Fixes #6674
---
firebase-firestore/CHANGELOG.md | 1 +
firebase-inappmessaging-display/CHANGELOG.md | 1 +
firebase-inappmessaging/CHANGELOG.md | 1 +
firebase-perf/CHANGELOG.md | 1 +
firebase-perf/firebase-perf.gradle | 2 +-
protolite-well-known-types/CHANGELOG.md | 2 +-
protolite-well-known-types/README.md | 2 +-
protolite-well-known-types/gradle.properties | 2 +-
.../protolite-well-known-types.gradle | 7 ++++---
9 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md
index 66fce5b35ce..f14e653e79f 100644
--- a/firebase-firestore/CHANGELOG.md
+++ b/firebase-firestore/CHANGELOG.md
@@ -1,4 +1,5 @@
# Unreleased
+* [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716]
# 25.1.2
diff --git a/firebase-inappmessaging-display/CHANGELOG.md b/firebase-inappmessaging-display/CHANGELOG.md
index 15bd2abe75a..a9b37cf7f10 100644
--- a/firebase-inappmessaging-display/CHANGELOG.md
+++ b/firebase-inappmessaging-display/CHANGELOG.md
@@ -1,4 +1,5 @@
# Unreleased
+* [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716]
# 21.0.1
diff --git a/firebase-inappmessaging/CHANGELOG.md b/firebase-inappmessaging/CHANGELOG.md
index 90b3e93ccae..1252d73f787 100644
--- a/firebase-inappmessaging/CHANGELOG.md
+++ b/firebase-inappmessaging/CHANGELOG.md
@@ -1,4 +1,5 @@
# Unreleased
+* [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716]
# 21.0.1
diff --git a/firebase-perf/CHANGELOG.md b/firebase-perf/CHANGELOG.md
index 2112244a524..58bdd0f1d29 100644
--- a/firebase-perf/CHANGELOG.md
+++ b/firebase-perf/CHANGELOG.md
@@ -1,4 +1,5 @@
# Unreleased
+* [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716]
# 21.0.4
diff --git a/firebase-perf/firebase-perf.gradle b/firebase-perf/firebase-perf.gradle
index 3dc2fd5a38d..49c921edeb0 100644
--- a/firebase-perf/firebase-perf.gradle
+++ b/firebase-perf/firebase-perf.gradle
@@ -111,7 +111,7 @@ dependencies {
implementation libs.dagger.dagger
api 'com.google.firebase:firebase-annotations:16.2.0'
api 'com.google.firebase:firebase-installations-interop:17.1.0'
- api 'com.google.firebase:protolite-well-known-types:18.0.0'
+ api project(":protolite-well-known-types")
implementation libs.okhttp
api("com.google.firebase:firebase-common:21.0.0")
api("com.google.firebase:firebase-common-ktx:21.0.0")
diff --git a/protolite-well-known-types/CHANGELOG.md b/protolite-well-known-types/CHANGELOG.md
index f514bbb890e..9947693ea08 100644
--- a/protolite-well-known-types/CHANGELOG.md
+++ b/protolite-well-known-types/CHANGELOG.md
@@ -1,3 +1,3 @@
# Unreleased
-
+* [changed] Updated protobuf dependency to `3.25.5` to fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8).
diff --git a/protolite-well-known-types/README.md b/protolite-well-known-types/README.md
index 30926d080d8..c2691086078 100644
--- a/protolite-well-known-types/README.md
+++ b/protolite-well-known-types/README.md
@@ -73,7 +73,7 @@ android {
}
dependencies {
- implementation 'com.google.firebase:protolite-well-known-types:18.0.0'
+ implementation 'com.google.firebase:protolite-well-known-types:18.0.1'
implementation "io.grpc:grpc-stub:$grpcVersion"
// optionally override grpc's protobuf-lite runtime
diff --git a/protolite-well-known-types/gradle.properties b/protolite-well-known-types/gradle.properties
index a60ca35eca9..d7239d0c4fe 100644
--- a/protolite-well-known-types/gradle.properties
+++ b/protolite-well-known-types/gradle.properties
@@ -1,5 +1,5 @@
# IMPORTANT (b/285892320) Keep version and latestReleasedVersion in sync
# unless you are releasing a new version of the library to prevent issues
# with transitive dependencies.
-version=18.0.0
+version=18.0.1
latestReleasedVersion=18.0.0
diff --git a/protolite-well-known-types/protolite-well-known-types.gradle b/protolite-well-known-types/protolite-well-known-types.gradle
index f5e5bdd8ff2..fe73979660b 100644
--- a/protolite-well-known-types/protolite-well-known-types.gradle
+++ b/protolite-well-known-types/protolite-well-known-types.gradle
@@ -26,7 +26,7 @@ firebaseLibrary {
protobuf {
protoc {
- artifact = "com.google.protobuf:protoc:3.21.11"
+ artifact = libs.protoc.get().toString()
}
generateProtoTasks {
all().each { task ->
@@ -41,6 +41,7 @@ protobuf {
}
}
}
+
android {
namespace "firebase.com.protolitewrapper"
compileSdkVersion project.compileSdkVersion
@@ -64,9 +65,9 @@ android {
dependencies {
- protobuf("com.google.api.grpc:proto-google-common-protos:1.18.0"){
+ protobuf(libs.proto.google.common.protos){
exclude group: "com.google.protobuf", module: "protobuf-java"
}
- implementation "com.google.protobuf:protobuf-javalite:3.21.11"
+ implementation libs.protobuf.java.lite
}
From befade758ee96c79b6d3417b2ef9b6368361fbf5 Mon Sep 17 00:00:00 2001
From: Lee Kellogg
Date: Mon, 24 Feb 2025 15:39:07 -0500
Subject: [PATCH 063/146] Update CHANGELOG.md (#6718)
For this fix: https://github.com/firebase/firebase-android-sdk/pull/6705
---
firebase-appdistribution/CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/firebase-appdistribution/CHANGELOG.md b/firebase-appdistribution/CHANGELOG.md
index 6aa5cbfad70..3bfc9628fd5 100644
--- a/firebase-appdistribution/CHANGELOG.md
+++ b/firebase-appdistribution/CHANGELOG.md
@@ -1,5 +1,5 @@
# Unreleased
-
+* [fixed] Added custom tab support for more browsers [#6692]
# 16.0.0-beta14
* [changed] Internal improvements to testing on Android 14
From 7ea169ace8b0a53c0aa26bfd378c275efcffdcff Mon Sep 17 00:00:00 2001
From: Tushar Khandelwal <64364243+tusharkhandelwal8@users.noreply.github.com>
Date: Wed, 26 Feb 2025 20:21:19 +0530
Subject: [PATCH 064/146] Add custom signal limits link and fix Javadoc List
Formatting (#6722)
Add link to documentation about custom signal limits
([b/385028620](https://buganizer.corp.google.com/issues/385028620)) and
Update setCustomSignals Javadoc List Formatting
([b/390054823](https://buganizer.corp.google.com/issues/390054823))
---
.../firebase/remoteconfig/FirebaseRemoteConfig.java | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java
index 808892e7521..abd09dd0330 100644
--- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java
+++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java
@@ -656,16 +656,17 @@ private Task setDefaultsWithStringsMapAsync(Map defaultsSt
* Asynchronously changes the custom signals for this {@link FirebaseRemoteConfig} instance.
*
* Custom signals are subject to limits on the size of key/value pairs and the total
- * number of signals. Any calls that exceed these limits will be discarded.
+ * number of signals. Any calls that exceed these limits will be discarded. See Custom
+ * Signal Limits.
*
* @param customSignals The custom signals to set for this instance.
- *
+ *
* - New keys will add new key-value pairs in the custom signals.
*
- Existing keys with new values will update the corresponding signals.
*
- Setting a key's value to {@code null} will remove the associated signal.
- *
+ *
*/
- // TODO(b/385028620): Add link to documentation about custom signal limits.
@NonNull
public Task setCustomSignals(@NonNull CustomSignals customSignals) {
return Tasks.call(
From 4228d93ec1da4bd45e239db136528373fec43bd2 Mon Sep 17 00:00:00 2001
From: David Motsonashvili
Date: Wed, 26 Feb 2025 19:20:10 +0000
Subject: [PATCH 065/146] Fix documentation for ImagenGenerationResponse
(#6728)
Co-authored-by: David Motsonashvili
---
.../google/firebase/vertexai/type/ImagenGenerationResponse.kt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationResponse.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationResponse.kt
index a1a80360848..dfc011b58f9 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationResponse.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationResponse.kt
@@ -16,10 +16,11 @@
package com.google.firebase.vertexai.type
+import com.google.firebase.vertexai.ImagenModel
import kotlinx.serialization.Serializable
/**
- * Represents a response from a call to [ImagenModel#generateImages]
+ * Represents a response from a call to [ImagenModel.generateImages]
*
* @param images contains the generated images
* @param filteredReason if fewer images were generated than were requested, this field will contain
From c372b1d3e1f650df6665611047ca773ce592dac6 Mon Sep 17 00:00:00 2001
From: emilypgoogle <110422458+emilypgoogle@users.noreply.github.com>
Date: Wed, 26 Feb 2025 13:42:31 -0600
Subject: [PATCH 066/146] Add copyApiTxtFile task (#6724)
Prerequisite of Metalava based SemVer
---
.../firebase/gradle/plugins/CopyApiTask.kt | 33 +++++++++++++++++++
.../plugins/FirebaseAndroidLibraryPlugin.kt | 5 +++
.../plugins/FirebaseJavaLibraryPlugin.kt | 5 +++
.../plugins/FirebaseLibraryExtension.kt | 3 ++
4 files changed, 46 insertions(+)
create mode 100644 plugins/src/main/java/com/google/firebase/gradle/plugins/CopyApiTask.kt
diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/CopyApiTask.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/CopyApiTask.kt
new file mode 100644
index 00000000000..2de8c01cd22
--- /dev/null
+++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/CopyApiTask.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2025 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.gradle.plugins
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+
+abstract class CopyApiTask : DefaultTask() {
+ @get:InputFile abstract val apiTxtFile: RegularFileProperty
+ @get:OutputFile abstract val output: RegularFileProperty
+
+ @TaskAction
+ fun run() {
+ output.get().asFile.writeText(apiTxtFile.get().asFile.readText())
+ }
+}
diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseAndroidLibraryPlugin.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseAndroidLibraryPlugin.kt
index e0732174ae4..5ec8dcb22cf 100644
--- a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseAndroidLibraryPlugin.kt
+++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseAndroidLibraryPlugin.kt
@@ -160,6 +160,11 @@ class FirebaseAndroidLibraryPlugin : BaseFirebaseLibraryPlugin() {
.getLatestReleasedVersion()
)
}
+
+ project.tasks.register("copyApiTxtFile") {
+ apiTxtFile.set(project.file("api.txt"))
+ output.set(project.file("previous_api.txt"))
+ }
}
private fun setupApiInformationAnalysis(project: Project, android: LibraryExtension) {
diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.kt
index 0c7b2028f1c..0068f76e677 100644
--- a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.kt
+++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.kt
@@ -103,6 +103,11 @@ class FirebaseJavaLibraryPlugin : BaseFirebaseLibraryPlugin() {
dependsOn("copyPreviousArtifacts")
}
+
+ project.tasks.register("copyApiTxtFile") {
+ apiTxtFile.set(project.file("api.txt"))
+ output.set(project.file("previous_api.txt"))
+ }
}
private fun setupApiInformationAnalysis(project: Project) {
diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryExtension.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryExtension.kt
index 8da2d33fb5c..ecc69e0e5a4 100644
--- a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryExtension.kt
+++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryExtension.kt
@@ -241,6 +241,9 @@ constructor(val project: Project, val type: LibraryType) {
val version: String
get() = project.version.toString()
+ val previousVersion: String
+ get() = project.properties["latestReleasedVersion"].toString()
+
val path: String = project.path
val runtimeClasspath: String =
From 1e9c2905e690e13abe7134f07db73b804c6788d2 Mon Sep 17 00:00:00 2001
From: Denver Coneybeare
Date: Wed, 26 Feb 2025 20:26:15 +0000
Subject: [PATCH 067/146] dataconnect: minor cosmetic changes to the github
actions workflow (#6727)
---
.github/workflows/dataconnect.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/dataconnect.yml b/.github/workflows/dataconnect.yml
index 3a0b9aa4b93..797f112fd61 100644
--- a/.github/workflows/dataconnect.yml
+++ b/.github/workflows/dataconnect.yml
@@ -217,10 +217,10 @@ jobs:
if-no-files-found: warn
compression-level: 9
- - name: Check test result
+ - name: Verify "Gradle connectedCheck" step was successful
if: steps.connectedCheck.outcome != 'success'
run: |
- echo "Failing the job since the connectedCheck step failed"
+ echo 'Failing because the outcome of the "Gradle connectedCheck" step ("${{ steps.connectedCheck.outcome }}") was not successful'
exit 1
# Check this yml file with "actionlint": https://github.com/rhysd/actionlint
From d7b14a06eeae4127042132c5b4c7936909f3f794 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 27 Feb 2025 11:03:29 -0500
Subject: [PATCH 068/146] Bump truth from 1.4.2 to 1.4.4 (#6723)
Bumps `truth` from 1.4.2 to 1.4.4.
Updates `com.google.truth:truth` from 1.4.2 to 1.4.4
Release notes
Sourced from com.google.truth:truth's
releases.
v1.4.4
- Annotated the rest of the main package for nullness, and moved the
@NullMarked
annotation from individual classes up to the
package to avoid a warning
under --release 8
. (e107aeadc)
- Improved the failure message for
matches
to
conditionally suggest using containsMatch
. (7e9fc7aec)
1.4.3
Known Issue for at least some builds targeting Java 8, fixed
in 1.4.4:
"unknown enum constant ElementType.MODULE": google/truth#1320.
As far as we know, this is only a warning, so it should cause practical
problems only if you use -Werror
or you perform reflection
on @NullMarked
under a Java 8 runtime.
- Added more nullness information to our APIs (in the form of JSpecify annotations). This could lead
to additional warnings (or even errors) for users of Kotlin and other
nullness checkers. Please report any
problems. (ee680cbaf)
- Deprecated
Subject.Factory
methods for Java 8 types. We
won't remove them, but you can simplify your code by migrating off them:
Just replace assertAbout(foos()).that(foo)
with
assertThat(foo)
(or about(foos()).that(foo)
with that(foo)
). (59e7a5065)
Commits
ddeaa0c
Set version number for truth-parent to 1.4.4.
e107aea
Annotate the rest of the main package (basically just the Java 8
subjects) fo...
8ac91a6
Document that truth-java8-extension
is obsolete.
99af8be
Bump org.codehaus.mojo:animal-sniffer-maven-plugin from 1.23 to 1.24 in
the d...
54e548c
Bump the dependencies group with 2 updates
2183a14
Migrate from legacy com.google.gwt to org.gwtproject.
7e9fc7a
Make StringSubject.matches suggest using containsMatch if matches(x)
fails bu...
af140d6
Fix grammar in Javadoc comments.
afda443
Annotate formattingDiffsUsing
methods as supporting
nullable element/value ...
ee680cb
Use JSpecify annotations in the public release.
- Additional commits viewable in compare
view
Updates `com.google.truth.extensions:truth-liteproto-extension` from
1.4.2 to 1.4.4
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rodrigo Lazo
---
gradle/libs.versions.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 21442a483d3..46d4995cbd8 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -71,7 +71,7 @@ serialization-plugin = "1.8.22"
slf4jNop = "2.0.9"
spotless = "7.0.0.BETA3"
testServices = "1.2.0"
-truth = "1.4.2"
+truth = "1.4.4"
truthProtoExtension = "1.0"
wiremockStandalone = "2.26.3"
From d2365b2c98a33051b0750a2d0001bccb8ef56b52 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 27 Feb 2025 11:46:16 -0500
Subject: [PATCH 069/146] Bump semver from 7.5.0 to 7.5.4 in
/smoke-tests/src/androidTest/backend/functions/functions (#5164)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [semver](https://github.com/npm/node-semver) from 7.5.0 to 7.5.4.
Release notes
Sourced from semver's
releases.
v7.5.4
7.5.4
(2023-07-07)
Bug Fixes
v7.5.3
7.5.3
(2023-06-22)
Bug Fixes
Documentation
v7.5.2
7.5.2
(2023-06-15)
Bug Fixes
v7.5.1
7.5.1
(2023-05-12)
Bug Fixes
Changelog
Sourced from semver's
changelog.
7.5.4
(2023-07-07)
Bug Fixes
7.5.3
(2023-06-22)
Bug Fixes
Documentation
7.5.2
(2023-06-15)
Bug Fixes
7.5.1
(2023-05-12)
Bug Fixes
Commits
36cd334
chore: release 7.5.4
8456d87
chore: postinstall for dependabot template-oss PR
dde1f00
chore: postinstall for dependabot template-oss PR
dffcd1b
chore: bump @npmcli/template-oss
from 4.16.0 to
4.17.0
d619f66
chore: postinstall for dependabot template-oss PR
3bc4247
chore: bump @npmcli/template-oss
from 4.15.1 to
4.16.0
cc6fde2
fix: trim each range set before parsing
99d8287
fix: correctly parse long build ids as valid (#583)
4f0f6b1
chore: fix arguments in whitespace test (#574)
6bd1a37
chore: remove duplicate test in semver class (#575)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
You can trigger a rebase of this PR by commenting `@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/firebase/firebase-android-sdk/network/alerts).
> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rodrigo Lazo
---
.../backend/functions/functions/package-lock.json | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/smoke-tests/src/androidTest/backend/functions/functions/package-lock.json b/smoke-tests/src/androidTest/backend/functions/functions/package-lock.json
index ca5804fab55..9c9d1d98854 100644
--- a/smoke-tests/src/androidTest/backend/functions/functions/package-lock.json
+++ b/smoke-tests/src/androidTest/backend/functions/functions/package-lock.json
@@ -2711,9 +2711,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/semver": {
- "version": "7.5.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz",
- "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==",
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -5415,9 +5415,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"semver": {
- "version": "7.5.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz",
- "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==",
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"requires": {
"lru-cache": "^6.0.0"
}
From 020946383a209976102900eb6e161706a03b5c6a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 27 Feb 2025 17:32:38 +0000
Subject: [PATCH 070/146] build(deps): bump
com.fasterxml.jackson.core:jackson-databind from 2.13.1 to 2.18.2 (#6591)
Bumps
[com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson)
from 2.13.1 to 2.18.2.
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
You can trigger a rebase of this PR by commenting `@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rodrigo Lazo
---
gradle/libs.versions.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 46d4995cbd8..b234d7bbd0f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -38,7 +38,7 @@ hamcrestLibrary = "2.2"
httpclientAndroid = "4.3.5.1"
integrity = "1.2.0"
jacksonCore = "2.13.1"
-jacksonDatabind = "2.13.1"
+jacksonDatabind = "2.18.2"
javalite = "3.25.5"
jsonassert = "1.5.0"
kotest = "5.9.0" # Do not use 5.9.1 because it reverts the fix for https://github.com/kotest/kotest/issues/3981
From f4195aea16d9e6fe28d0f171c2ffdc60048bfda9 Mon Sep 17 00:00:00 2001
From: Denver Coneybeare
Date: Thu, 27 Feb 2025 20:09:03 +0000
Subject: [PATCH 071/146] dataconnect: DataConnectExecutableVersions.json
updated with versions 1.8.0, 1.8.1, 1.8.2, and 1.8.3 (#6732)
---
.../plugin/DataConnectExecutableVersions.json | 74 ++++++++++++++++++-
1 file changed, 73 insertions(+), 1 deletion(-)
diff --git a/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json b/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json
index 1854796df5e..bc2038801fe 100644
--- a/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json
+++ b/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json
@@ -1,5 +1,5 @@
{
- "defaultVersion": "1.7.7",
+ "defaultVersion": "1.8.3",
"versions": [
{
"version": "1.3.4",
@@ -414,6 +414,78 @@
"os": "linux",
"size": 25268376,
"sha512DigestHex": "f55feb1ce670b4728bb30be138ab427545f77f63f9e11ee458096091c075699c647d5b768c642a1ef6b3569a2db87dbbed6f2fdaf64febd1154d1a730fda4a9c"
+ },
+ {
+ "version": "1.8.0",
+ "os": "windows",
+ "size": 25903616,
+ "sha512DigestHex": "753a5e4be35c544317bcdbaaa860f079a9c9d8a24ca3db17fed601d30b64f083a9203fbb76718d23f3ad77f1556adfb5a4226751ec48c202bd227479c57d1ae9"
+ },
+ {
+ "version": "1.8.0",
+ "os": "macos",
+ "size": 25469696,
+ "sha512DigestHex": "23c1e405b196799a7c84b9783ca110459bba3aa86405d2fc03d83f90530642d590b02cd06588a8428e0e7bb7d1c59e6d03113bbc5c41e12cff7a7c46674fc430"
+ },
+ {
+ "version": "1.8.0",
+ "os": "linux",
+ "size": 25383064,
+ "sha512DigestHex": "9546bb62d54b67086847d3e129397f4cfceb5b715d64f0a1cc0a053b5dfe918e8372142b7e9bacd11dede77ddd17840058efb8ed6a7073e99fd5a684fdc57bea"
+ },
+ {
+ "version": "1.8.1",
+ "os": "windows",
+ "size": 25904128,
+ "sha512DigestHex": "26dc987e38d5d07a910da647920cc2fe990f1da0db56206def71a9833f8eeb66272d8f32ba091b0d4d6e065a3d5cd950cd835a891895c6a55d735a6f240bf4b7"
+ },
+ {
+ "version": "1.8.1",
+ "os": "macos",
+ "size": 25469696,
+ "sha512DigestHex": "d7bcb01912b1949a003fd0a7ebbc1bb42e79e97b7fd880ba9164b62e05d1ffb634662d97fd4664343e28780e69953aadecd5fb799a8f51229a4c0fbf552936ac"
+ },
+ {
+ "version": "1.8.1",
+ "os": "linux",
+ "size": 25383064,
+ "sha512DigestHex": "2a28ba7947f84ede9062b5f5efa145b29862be0a8724ac6b6a4210f6823024d33363bd3379a6474965fbd60376baae9103ce7e4509db9d52c2b13886bca5df92"
+ },
+ {
+ "version": "1.8.2",
+ "os": "windows",
+ "size": 25936384,
+ "sha512DigestHex": "f2aed75baaeed388d8fcd8a3d18e629f9ed012f60de0401bc365227094688f130ce7aa02db565002fe7b06a339b1cb133a7c87da365d480fb10cdb47d55c7dfa"
+ },
+ {
+ "version": "1.8.2",
+ "os": "macos",
+ "size": 25506560,
+ "sha512DigestHex": "d4ac9e15f5a42fed28fd2f3cb2c80bc3f4def60f76517661323c502fa7a4b085bda3d26eb62cdcb630a13999e2fb0428ee45d335e20641229a9439cc60a9e798"
+ },
+ {
+ "version": "1.8.2",
+ "os": "linux",
+ "size": 25415832,
+ "sha512DigestHex": "fec0fb97fb3ad30bdd9d0e3b65095e2dfdcfccd15e7c6ae9fe827ec1c3b5b9b592c80c59cadb3540e387d4adcf3560922094399c5ca3d162288a33403308104d"
+ },
+ {
+ "version": "1.8.3",
+ "os": "windows",
+ "size": 25965568,
+ "sha512DigestHex": "9b6ded9ddac61d5f137ac65944409003906d621bb3a03ba6bf037b1aeddabf23f9410de6fbc05b8ea0c9afa2a8328bb02a57ed225f8ebaa3c8d6921755ad715c"
+ },
+ {
+ "version": "1.8.3",
+ "os": "macos",
+ "size": 25535232,
+ "sha512DigestHex": "0c88a14ae64308e68957f5e79f9e20b4b946977187132dcc24193370c81b9117487fb0ee1c5be4e8f2368945add7ed37d6d97b015c3ea8232e09664458c8e802"
+ },
+ {
+ "version": "1.8.3",
+ "os": "linux",
+ "size": 25448600,
+ "sha512DigestHex": "6734188ed2dc41fdf9922e152848d46a4bd6a30083c918ac0de5197e1f998f8dc2b4e190c47b02c176f68b93591132c29be142b4b61a36c81aec2358a81864c6"
}
]
}
\ No newline at end of file
From 190d096bf6c1426aba9c73bbd800e1519b495ba4 Mon Sep 17 00:00:00 2001
From: Denver Coneybeare
Date: Thu, 27 Feb 2025 21:42:33 +0000
Subject: [PATCH 072/146] dataconnect: change grpc api version from "v1beta" to
"v1" (#6729)
---
firebase-dataconnect/CHANGELOG.md | 3 ++-
.../demo/firebase/dataconnect/dataconnect.yaml | 2 +-
firebase-dataconnect/emulator/dataconnect/dataconnect.yaml | 2 +-
.../google/firebase/dataconnect/proto/connector_service.proto | 2 +-
.../google/firebase/dataconnect/proto/graphql_error.proto | 2 +-
5 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md
index fa16a7ed32d..b21ac994950 100644
--- a/firebase-dataconnect/CHANGELOG.md
+++ b/firebase-dataconnect/CHANGELOG.md
@@ -1,5 +1,6 @@
# Unreleased
-
+* [changed] Changed gRPC proto package to v1 (was v1beta).
+ ([#6729](https://github.com/firebase/firebase-android-sdk/pull/6729))
# 16.0.0-beta04
* [changed] `FirebaseDataConnect.logLevel` type changed from `LogLevel` to
diff --git a/firebase-dataconnect/demo/firebase/dataconnect/dataconnect.yaml b/firebase-dataconnect/demo/firebase/dataconnect/dataconnect.yaml
index 341a3fc587a..3a718496328 100644
--- a/firebase-dataconnect/demo/firebase/dataconnect/dataconnect.yaml
+++ b/firebase-dataconnect/demo/firebase/dataconnect/dataconnect.yaml
@@ -1,4 +1,4 @@
-specVersion: v1beta
+specVersion: v1
serviceId: srv3ar8skbsza
location: us-central1
schema:
diff --git a/firebase-dataconnect/emulator/dataconnect/dataconnect.yaml b/firebase-dataconnect/emulator/dataconnect/dataconnect.yaml
index a17c5213bc0..e66af00a793 100644
--- a/firebase-dataconnect/emulator/dataconnect/dataconnect.yaml
+++ b/firebase-dataconnect/emulator/dataconnect/dataconnect.yaml
@@ -1,4 +1,4 @@
-specVersion: "v1beta"
+specVersion: "v1"
serviceId: "sid2ehn9ct8te"
location: "us-central1"
schema:
diff --git a/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/connector_service.proto b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/connector_service.proto
index 918227ef686..bb4bc986769 100644
--- a/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/connector_service.proto
+++ b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/connector_service.proto
@@ -18,7 +18,7 @@
syntax = "proto3";
-package google.firebase.dataconnect.v1beta;
+package google.firebase.dataconnect.v1;
import "google/firebase/dataconnect/proto/graphql_error.proto";
import "google/protobuf/struct.proto";
diff --git a/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/graphql_error.proto b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/graphql_error.proto
index f2ca45e9f66..be19dcbfa35 100644
--- a/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/graphql_error.proto
+++ b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/graphql_error.proto
@@ -18,7 +18,7 @@
syntax = "proto3";
-package google.firebase.dataconnect.v1beta;
+package google.firebase.dataconnect.v1;
import "google/protobuf/struct.proto";
From 2ad9b2c3b1ba997602ce8f3586c4c8b294139cb5 Mon Sep 17 00:00:00 2001
From: emilypgoogle <110422458+emilypgoogle@users.noreply.github.com>
Date: Fri, 28 Feb 2025 11:52:30 -0600
Subject: [PATCH 073/146] Add Metalava SemVer Task (#6725)
Needs #6724 in main before the task will be able to run
---------
Co-authored-by: Rodrigo Lazo
---
.github/workflows/metalava-semver-check.yml | 34 ++++++
.../plugins/FirebaseAndroidLibraryPlugin.kt | 8 ++
.../plugins/FirebaseJavaLibraryPlugin.kt | 8 ++
.../firebase/gradle/plugins/Metalava.kt | 1 -
.../firebase/gradle/plugins/SemVerTask.kt | 104 ++++++++++++++++++
5 files changed, 154 insertions(+), 1 deletion(-)
create mode 100644 .github/workflows/metalava-semver-check.yml
create mode 100644 plugins/src/main/java/com/google/firebase/gradle/plugins/SemVerTask.kt
diff --git a/.github/workflows/metalava-semver-check.yml b/.github/workflows/metalava-semver-check.yml
new file mode 100644
index 00000000000..df68a691234
--- /dev/null
+++ b/.github/workflows/metalava-semver-check.yml
@@ -0,0 +1,34 @@
+name: Metalava SemVer Check
+
+on:
+ pull_request:
+
+jobs:
+ semver-check:
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ steps:
+ - name: Checkout main
+ uses: actions/checkout@v4.1.1
+ with:
+ ref: ${{ github.base_ref }}
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4.1.0
+ with:
+ java-version: 17
+ distribution: temurin
+ cache: gradle
+
+ - name: Copy previous api.txt files
+ run: ./gradlew copyApiTxtFile
+
+ - name: Checkout PR
+ uses: actions/checkout@v4.1.1
+ with:
+ ref: ${{ github.head_ref }}
+ clean: false
+
+ - name: Run Metalava SemVer check
+ run: ./gradlew metalavaSemver
diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseAndroidLibraryPlugin.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseAndroidLibraryPlugin.kt
index 5ec8dcb22cf..de30763e7b4 100644
--- a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseAndroidLibraryPlugin.kt
+++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseAndroidLibraryPlugin.kt
@@ -165,6 +165,14 @@ class FirebaseAndroidLibraryPlugin : BaseFirebaseLibraryPlugin() {
apiTxtFile.set(project.file("api.txt"))
output.set(project.file("previous_api.txt"))
}
+
+ project.tasks.register("metalavaSemver") {
+ apiTxtFile.set(project.file("api.txt"))
+ otherApiFile.set(project.file("previous_api.txt"))
+ outputApiFile.set(project.file("opi.txt"))
+ currentVersionString.value(firebaseLibrary.version)
+ previousVersionString.value(firebaseLibrary.previousVersion)
+ }
}
private fun setupApiInformationAnalysis(project: Project, android: LibraryExtension) {
diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.kt
index 0068f76e677..acc72d7f0f7 100644
--- a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.kt
+++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.kt
@@ -108,6 +108,14 @@ class FirebaseJavaLibraryPlugin : BaseFirebaseLibraryPlugin() {
apiTxtFile.set(project.file("api.txt"))
output.set(project.file("previous_api.txt"))
}
+
+ project.tasks.register("metalavaSemver") {
+ apiTxtFile.set(project.file("api.txt"))
+ otherApiFile.set(project.file("previous_api.txt"))
+ outputApiFile.set(project.file("opi.txt"))
+ currentVersionString.value(firebaseLibrary.version)
+ previousVersionString.value(firebaseLibrary.previousVersion)
+ }
}
private fun setupApiInformationAnalysis(project: Project) {
diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/Metalava.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/Metalava.kt
index 4d04701b7f7..233be31a302 100644
--- a/plugins/src/main/java/com/google/firebase/gradle/plugins/Metalava.kt
+++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/Metalava.kt
@@ -55,7 +55,6 @@ fun Project.runMetalavaWithArgs(
) {
val allArgs =
listOf(
- "--no-banner",
"--hide",
"HiddenSuperclass", // We allow having a hidden parent class
"--hide",
diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/SemVerTask.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/SemVerTask.kt
new file mode 100644
index 00000000000..bafad14816b
--- /dev/null
+++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/SemVerTask.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2025 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.gradle.plugins
+
+import com.google.firebase.gradle.plugins.semver.VersionDelta
+import java.io.ByteArrayOutputStream
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+
+abstract class SemVerTask : DefaultTask() {
+ @get:InputFile abstract val apiTxtFile: RegularFileProperty
+ @get:InputFile abstract val otherApiFile: RegularFileProperty
+ @get:Input abstract val currentVersionString: Property
+ @get:Input abstract val previousVersionString: Property
+
+ @get:OutputFile abstract val outputApiFile: RegularFileProperty
+
+ @TaskAction
+ fun run() {
+ val previous = ModuleVersion.fromStringOrNull(previousVersionString.get()) ?: return
+ val current = ModuleVersion.fromStringOrNull(currentVersionString.get()) ?: return
+
+ val bump =
+ when {
+ previous.major != current.major -> VersionDelta.MAJOR
+ previous.minor != current.minor -> VersionDelta.MINOR
+ else -> VersionDelta.PATCH
+ }
+ val stream = ByteArrayOutputStream()
+ project.runMetalavaWithArgs(
+ listOf(
+ "--source-files",
+ apiTxtFile.get().asFile.absolutePath,
+ "--check-compatibility:api:released",
+ otherApiFile.get().asFile.absolutePath,
+ ) +
+ MAJOR.flatMap { m -> listOf("--error", m) } +
+ MINOR.flatMap { m -> listOf("--error", m) } +
+ IGNORED.flatMap { m -> listOf("--hide", m) } +
+ listOf("--format=v3", "--no-color"),
+ ignoreFailure = true,
+ stdOut = stream,
+ )
+
+ val string = String(stream.toByteArray())
+ val reg = Regex("(.*)\\s+error:\\s+(.*\\s+\\[(.*)\\])")
+ val minorChanges = mutableListOf()
+ val majorChanges = mutableListOf()
+ for (match in reg.findAll(string)) {
+ val loc = match.groups[1]!!.value
+ val message = match.groups[2]!!.value
+ val type = match.groups[3]!!.value
+ if (IGNORED.contains(type)) {
+ continue // Shouldn't be possible
+ } else if (MINOR.contains(type)) {
+ minorChanges.add(message)
+ } else {
+ majorChanges.add(message)
+ }
+ }
+ val allChanges =
+ (majorChanges.joinToString(separator = "") { m -> " MAJOR: $m\n" }) +
+ minorChanges.joinToString(separator = "") { m -> " MINOR: $m\n" }
+ if (majorChanges.isNotEmpty()) {
+ if (bump != VersionDelta.MAJOR) {
+ throw GradleException(
+ "API has non-bumped breaking MAJOR changes\nCurrent version bump is ${bump}, update the gradle.properties or fix the changes\n$allChanges"
+ )
+ }
+ } else if (minorChanges.isNotEmpty()) {
+ if (bump != VersionDelta.MAJOR && bump != VersionDelta.MINOR) {
+ throw GradleException(
+ "API has non-bumped MINOR changes\nCurrent version bump is ${bump}, update the gradle.properties or fix the changes\n$allChanges"
+ )
+ }
+ }
+ }
+
+ companion object {
+ private val MAJOR = setOf("AddedFinal")
+ private val MINOR = setOf("AddedClass", "AddedMethod", "AddedField", "ChangedDeprecated")
+ private val IGNORED = setOf("ReferencesDeprecated")
+ }
+}
From 53f3238fe196f6cef2acff70e554de69623a8915 Mon Sep 17 00:00:00 2001
From: Mila <107142260+milaGGL@users.noreply.github.com>
Date: Mon, 3 Mar 2025 11:38:48 -0500
Subject: [PATCH 074/146] Use lazy encoding in utf-8 encoded string comparison
(#6706)
---
firebase-firestore/CHANGELOG.md | 1 +
.../firebase/firestore/FirestoreTest.java | 209 ++++++++++++++----
.../google/firebase/firestore/util/Util.java | 41 +++-
.../firebase/firestore/util/UtilTest.java | 182 +++++++++++++++
4 files changed, 390 insertions(+), 43 deletions(-)
diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md
index f14e653e79f..939f70c93bb 100644
--- a/firebase-firestore/CHANGELOG.md
+++ b/firebase-firestore/CHANGELOG.md
@@ -1,4 +1,5 @@
# Unreleased
+* [fixed] Use lazy encoding in UTF-8 encoded byte comparison for strings to solve performance issues. [#6706](//github.com/firebase/firebase-android-sdk/pull/6706)
* [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716]
diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java
index 796632e192e..95dcd2863fe 100644
--- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java
+++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java
@@ -1658,17 +1658,33 @@ public void sdkOrdersQueryByDocumentIdTheSameWayOnlineAndOffline() {
public void snapshotListenerSortsUnicodeStringsAsServer() {
Map> testDocs =
map(
- "a", map("value", "Łukasiewicz"),
- "b", map("value", "Sierpiński"),
- "c", map("value", "岩澤"),
- "d", map("value", "🄟"),
- "e", map("value", "P"),
- "f", map("value", "︒"),
- "g", map("value", "🐵"));
+ "a",
+ map("value", "Łukasiewicz"),
+ "b",
+ map("value", "Sierpiński"),
+ "c",
+ map("value", "岩澤"),
+ "d",
+ map("value", "🄟"),
+ "e",
+ map("value", "P"),
+ "f",
+ map("value", "︒"),
+ "g",
+ map("value", "🐵"),
+ "h",
+ map("value", "你好"),
+ "i",
+ map("value", "你顥"),
+ "j",
+ map("value", "😁"),
+ "k",
+ map("value", "😀"));
CollectionReference colRef = testCollectionWithDocs(testDocs);
Query orderedQuery = colRef.orderBy("value");
- List expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g");
+ List expectedDocIds =
+ Arrays.asList("b", "a", "h", "i", "c", "f", "e", "d", "g", "k", "j");
QuerySnapshot getSnapshot = waitFor(orderedQuery.get());
List getSnapshotDocIds =
@@ -1699,17 +1715,33 @@ public void snapshotListenerSortsUnicodeStringsAsServer() {
public void snapshotListenerSortsUnicodeStringsInArrayAsServer() {
Map> testDocs =
map(
- "a", map("value", Arrays.asList("Łukasiewicz")),
- "b", map("value", Arrays.asList("Sierpiński")),
- "c", map("value", Arrays.asList("岩澤")),
- "d", map("value", Arrays.asList("🄟")),
- "e", map("value", Arrays.asList("P")),
- "f", map("value", Arrays.asList("︒")),
- "g", map("value", Arrays.asList("🐵")));
+ "a",
+ map("value", Arrays.asList("Łukasiewicz")),
+ "b",
+ map("value", Arrays.asList("Sierpiński")),
+ "c",
+ map("value", Arrays.asList("岩澤")),
+ "d",
+ map("value", Arrays.asList("🄟")),
+ "e",
+ map("value", Arrays.asList("P")),
+ "f",
+ map("value", Arrays.asList("︒")),
+ "g",
+ map("value", Arrays.asList("🐵")),
+ "h",
+ map("value", Arrays.asList("你好")),
+ "i",
+ map("value", Arrays.asList("你顥")),
+ "j",
+ map("value", Arrays.asList("😁")),
+ "k",
+ map("value", Arrays.asList("😀")));
CollectionReference colRef = testCollectionWithDocs(testDocs);
Query orderedQuery = colRef.orderBy("value");
- List expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g");
+ List expectedDocIds =
+ Arrays.asList("b", "a", "h", "i", "c", "f", "e", "d", "g", "k", "j");
QuerySnapshot getSnapshot = waitFor(orderedQuery.get());
List getSnapshotDocIds =
@@ -1740,17 +1772,33 @@ public void snapshotListenerSortsUnicodeStringsInArrayAsServer() {
public void snapshotListenerSortsUnicodeStringsInMapAsServer() {
Map> testDocs =
map(
- "a", map("value", map("foo", "Łukasiewicz")),
- "b", map("value", map("foo", "Sierpiński")),
- "c", map("value", map("foo", "岩澤")),
- "d", map("value", map("foo", "🄟")),
- "e", map("value", map("foo", "P")),
- "f", map("value", map("foo", "︒")),
- "g", map("value", map("foo", "🐵")));
+ "a",
+ map("value", map("foo", "Łukasiewicz")),
+ "b",
+ map("value", map("foo", "Sierpiński")),
+ "c",
+ map("value", map("foo", "岩澤")),
+ "d",
+ map("value", map("foo", "🄟")),
+ "e",
+ map("value", map("foo", "P")),
+ "f",
+ map("value", map("foo", "︒")),
+ "g",
+ map("value", map("foo", "🐵")),
+ "h",
+ map("value", map("foo", "你好")),
+ "i",
+ map("value", map("foo", "你顥")),
+ "j",
+ map("value", map("foo", "😁")),
+ "k",
+ map("value", map("foo", "😀")));
CollectionReference colRef = testCollectionWithDocs(testDocs);
Query orderedQuery = colRef.orderBy("value");
- List expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g");
+ List expectedDocIds =
+ Arrays.asList("b", "a", "h", "i", "c", "f", "e", "d", "g", "k", "j");
QuerySnapshot getSnapshot = waitFor(orderedQuery.get());
List getSnapshotDocIds =
@@ -1781,17 +1829,33 @@ public void snapshotListenerSortsUnicodeStringsInMapAsServer() {
public void snapshotListenerSortsUnicodeStringsInMapKeyAsServer() {
Map> testDocs =
map(
- "a", map("value", map("Łukasiewicz", "foo")),
- "b", map("value", map("Sierpiński", "foo")),
- "c", map("value", map("岩澤", "foo")),
- "d", map("value", map("🄟", "foo")),
- "e", map("value", map("P", "foo")),
- "f", map("value", map("︒", "foo")),
- "g", map("value", map("🐵", "foo")));
+ "a",
+ map("value", map("Łukasiewicz", "foo")),
+ "b",
+ map("value", map("Sierpiński", "foo")),
+ "c",
+ map("value", map("岩澤", "foo")),
+ "d",
+ map("value", map("🄟", "foo")),
+ "e",
+ map("value", map("P", "foo")),
+ "f",
+ map("value", map("︒", "foo")),
+ "g",
+ map("value", map("🐵", "foo")),
+ "h",
+ map("value", map("你好", "foo")),
+ "i",
+ map("value", map("你顥", "foo")),
+ "j",
+ map("value", map("😁", "foo")),
+ "k",
+ map("value", map("😀", "foo")));
CollectionReference colRef = testCollectionWithDocs(testDocs);
Query orderedQuery = colRef.orderBy("value");
- List expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g");
+ List expectedDocIds =
+ Arrays.asList("b", "a", "h", "i", "c", "f", "e", "d", "g", "k", "j");
QuerySnapshot getSnapshot = waitFor(orderedQuery.get());
List getSnapshotDocIds =
@@ -1822,18 +1886,83 @@ public void snapshotListenerSortsUnicodeStringsInMapKeyAsServer() {
public void snapshotListenerSortsUnicodeStringsInDocumentKeyAsServer() {
Map> testDocs =
map(
- "Łukasiewicz", map("value", "foo"),
- "Sierpiński", map("value", "foo"),
- "岩澤", map("value", "foo"),
- "🄟", map("value", "foo"),
- "P", map("value", "foo"),
- "︒", map("value", "foo"),
- "🐵", map("value", "foo"));
+ "Łukasiewicz",
+ map("value", "foo"),
+ "Sierpiński",
+ map("value", "foo"),
+ "岩澤",
+ map("value", "foo"),
+ "🄟",
+ map("value", "foo"),
+ "P",
+ map("value", "foo"),
+ "︒",
+ map("value", "foo"),
+ "🐵",
+ map("value", "foo"),
+ "你好",
+ map("value", "foo"),
+ "你顥",
+ map("value", "foo"),
+ "😁",
+ map("value", "foo"),
+ "😀",
+ map("value", "foo"));
CollectionReference colRef = testCollectionWithDocs(testDocs);
Query orderedQuery = colRef.orderBy(FieldPath.documentId());
List expectedDocIds =
- Arrays.asList("Sierpiński", "Łukasiewicz", "岩澤", "︒", "P", "🄟", "🐵");
+ Arrays.asList(
+ "Sierpiński", "Łukasiewicz", "你好", "你顥", "岩澤", "︒", "P", "🄟", "🐵", "😀", "😁");
+
+ QuerySnapshot getSnapshot = waitFor(orderedQuery.get());
+ List getSnapshotDocIds =
+ getSnapshot.getDocuments().stream().map(ds -> ds.getId()).collect(Collectors.toList());
+
+ EventAccumulator eventAccumulator = new EventAccumulator();
+ ListenerRegistration registration =
+ orderedQuery.addSnapshotListener(eventAccumulator.listener());
+
+ List watchSnapshotDocIds = new ArrayList<>();
+ try {
+ QuerySnapshot watchSnapshot = eventAccumulator.await();
+ watchSnapshotDocIds =
+ watchSnapshot.getDocuments().stream()
+ .map(documentSnapshot -> documentSnapshot.getId())
+ .collect(Collectors.toList());
+ } finally {
+ registration.remove();
+ }
+
+ assertTrue(getSnapshotDocIds.equals(expectedDocIds));
+ assertTrue(watchSnapshotDocIds.equals(expectedDocIds));
+
+ checkOnlineAndOfflineResultsMatch(orderedQuery, expectedDocIds.toArray(new String[0]));
+ }
+
+ @Test
+ public void snapshotListenerSortsInvalidUnicodeStringsAsServer() {
+ // Note: Protocol Buffer converts any invalid surrogates to "?".
+ Map> testDocs =
+ map(
+ "a",
+ map("value", "Z"),
+ "b",
+ map("value", "你好"),
+ "c",
+ map("value", "😀"),
+ "d",
+ map("value", "ab\uD800"), // Lone high surrogate
+ "e",
+ map("value", "ab\uDC00"), // Lone low surrogate
+ "f",
+ map("value", "ab\uD800\uD800"), // Unpaired high surrogate
+ "g",
+ map("value", "ab\uDC00\uDC00")); // Unpaired low surrogate
+
+ CollectionReference colRef = testCollectionWithDocs(testDocs);
+ Query orderedQuery = colRef.orderBy("value");
+ List expectedDocIds = Arrays.asList("a", "d", "e", "f", "g", "b", "c");
QuerySnapshot getSnapshot = waitFor(orderedQuery.get());
List getSnapshotDocIds =
diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java
index 543da11e7d3..2cc39337002 100644
--- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java
+++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java
@@ -87,9 +87,44 @@ public static int compareIntegers(int i1, int i2) {
/** Compare strings in UTF-8 encoded byte order */
public static int compareUtf8Strings(String left, String right) {
- ByteString leftBytes = ByteString.copyFromUtf8(left);
- ByteString rightBytes = ByteString.copyFromUtf8(right);
- return compareByteStrings(leftBytes, rightBytes);
+ int i = 0;
+ while (i < left.length() && i < right.length()) {
+ int leftCodePoint = left.codePointAt(i);
+ int rightCodePoint = right.codePointAt(i);
+
+ if (leftCodePoint != rightCodePoint) {
+ if (leftCodePoint < 128 && rightCodePoint < 128) {
+ // ASCII comparison
+ return Integer.compare(leftCodePoint, rightCodePoint);
+ } else {
+ // substring and do UTF-8 encoded byte comparison
+ ByteString leftBytes = ByteString.copyFromUtf8(getUtf8SafeBytes(left, i));
+ ByteString rightBytes = ByteString.copyFromUtf8(getUtf8SafeBytes(right, i));
+ int comp = compareByteStrings(leftBytes, rightBytes);
+ if (comp != 0) {
+ return comp;
+ } else {
+ // EXTREMELY RARE CASE: Code points differ, but their UTF-8 byte representations are
+ // identical. This can happen with malformed input (invalid surrogate pairs), where
+ // Java's encoding leads to unexpected byte sequences. Meanwhile, any invalid surrogate
+ // inputs get converted to "?" by protocol buffer while round tripping, so we almost
+ // never receive invalid strings from backend.
+ // Fallback to code point comparison for graceful handling.
+ return Integer.compare(leftCodePoint, rightCodePoint);
+ }
+ }
+ }
+ // Increment by 2 for surrogate pairs, 1 otherwise.
+ i += Character.charCount(leftCodePoint);
+ }
+
+ // Compare lengths if all characters are equal
+ return Integer.compare(left.length(), right.length());
+ }
+
+ private static String getUtf8SafeBytes(String str, int index) {
+ int firstCodePoint = str.codePointAt(index);
+ return str.substring(index, index + Character.charCount(firstCodePoint));
}
/**
diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java
index 6ff424ef994..ccd88854ba7 100644
--- a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java
+++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java
@@ -17,6 +17,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.firebase.firestore.util.Util.firstNEntries;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
import com.google.firebase.firestore.testutil.TestUtil;
import com.google.protobuf.ByteString;
@@ -26,6 +27,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Random;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -87,4 +89,184 @@ private void validateDiffCollection(List before, List after) {
Util.diffCollections(before, after, String::compareTo, result::add, result::remove);
assertThat(result).containsExactlyElementsIn(after);
}
+
+ @Test
+ public void compareUtf8StringsShouldReturnCorrectValue() {
+ ArrayList errors = new ArrayList<>();
+ int seed = new Random().nextInt(Integer.MAX_VALUE);
+ int passCount = 0;
+ StringGenerator stringGenerator = new StringGenerator(29750468);
+ StringPairGenerator stringPairGenerator = new StringPairGenerator(stringGenerator);
+ for (int i = 0; i < 1_000_000 && errors.size() < 10; i++) {
+ StringPairGenerator.StringPair stringPair = stringPairGenerator.next();
+ final String s1 = stringPair.s1;
+ final String s2 = stringPair.s2;
+
+ int actual = Util.compareUtf8Strings(s1, s2);
+
+ ByteString b1 = ByteString.copyFromUtf8(s1);
+ ByteString b2 = ByteString.copyFromUtf8(s2);
+ int expected = Util.compareByteStrings(b1, b2);
+
+ if (actual == expected) {
+ passCount++;
+ } else {
+ errors.add(
+ "compareUtf8Strings(s1=\""
+ + s1
+ + "\", s2=\""
+ + s2
+ + "\") returned "
+ + actual
+ + ", but expected "
+ + expected
+ + " (i="
+ + i
+ + ", s1.length="
+ + s1.length()
+ + ", s2.length="
+ + s2.length()
+ + ")");
+ }
+ }
+
+ if (!errors.isEmpty()) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(errors.size()).append(" test cases failed, ");
+ sb.append(passCount).append(" test cases passed, ");
+ sb.append("seed=").append(seed).append(";");
+ for (int i = 0; i < errors.size(); i++) {
+ sb.append("\nerrors[").append(i).append("]: ").append(errors.get(i));
+ }
+ fail(sb.toString());
+ }
+ }
+
+ private static class StringPairGenerator {
+
+ private final StringGenerator stringGenerator;
+
+ public StringPairGenerator(StringGenerator stringGenerator) {
+ this.stringGenerator = stringGenerator;
+ }
+
+ public StringPair next() {
+ String prefix = stringGenerator.next();
+ String s1 = prefix + stringGenerator.next();
+ String s2 = prefix + stringGenerator.next();
+ return new StringPair(s1, s2);
+ }
+
+ public static class StringPair {
+ public final String s1, s2;
+
+ public StringPair(String s1, String s2) {
+ this.s1 = s1;
+ this.s2 = s2;
+ }
+ }
+ }
+
+ private static class StringGenerator {
+
+ private static final float DEFAULT_SURROGATE_PAIR_PROBABILITY = 0.33f;
+ private static final int DEFAULT_MAX_LENGTH = 20;
+
+ private static final int MIN_HIGH_SURROGATE = 0xD800;
+ private static final int MAX_HIGH_SURROGATE = 0xDBFF;
+ private static final int MIN_LOW_SURROGATE = 0xDC00;
+ private static final int MAX_LOW_SURROGATE = 0xDFFF;
+
+ private final Random rnd;
+ private final float surrogatePairProbability;
+ private final int maxLength;
+
+ public StringGenerator(int seed) {
+ this(new Random(seed), DEFAULT_SURROGATE_PAIR_PROBABILITY, DEFAULT_MAX_LENGTH);
+ }
+
+ public StringGenerator(Random rnd, float surrogatePairProbability, int maxLength) {
+ this.rnd = rnd;
+ this.surrogatePairProbability = validateProbability(surrogatePairProbability);
+ this.maxLength = validateLength(maxLength);
+ }
+
+ private static float validateProbability(float probability) {
+ if (!Float.isFinite(probability)) {
+ throw new IllegalArgumentException(
+ "invalid surrogate pair probability: "
+ + probability
+ + " (must be between 0.0 and 1.0, inclusive)");
+ } else if (probability < 0.0f) {
+ throw new IllegalArgumentException(
+ "invalid surrogate pair probability: "
+ + probability
+ + " (must be greater than or equal to zero)");
+ } else if (probability > 1.0f) {
+ throw new IllegalArgumentException(
+ "invalid surrogate pair probability: "
+ + probability
+ + " (must be less than or equal to 1)");
+ }
+ return probability;
+ }
+
+ private static int validateLength(int length) {
+ if (length < 0) {
+ throw new IllegalArgumentException(
+ "invalid maximum string length: "
+ + length
+ + " (must be greater than or equal to zero)");
+ }
+ return length;
+ }
+
+ public String next() {
+ final int length = rnd.nextInt(maxLength + 1);
+ final StringBuilder sb = new StringBuilder();
+ while (sb.length() < length) {
+ int codePoint = nextCodePoint();
+ sb.appendCodePoint(codePoint);
+ }
+ return sb.toString();
+ }
+
+ private boolean isNextSurrogatePair() {
+ return nextBoolean(rnd, surrogatePairProbability);
+ }
+
+ private static boolean nextBoolean(Random rnd, float probability) {
+ if (probability == 0.0f) {
+ return false;
+ } else if (probability == 1.0f) {
+ return true;
+ } else {
+ return rnd.nextFloat() < probability;
+ }
+ }
+
+ private int nextCodePoint() {
+ if (isNextSurrogatePair()) {
+ return nextSurrogateCodePoint();
+ } else {
+ return nextNonSurrogateCodePoint();
+ }
+ }
+
+ private int nextSurrogateCodePoint() {
+ int highSurrogate =
+ rnd.nextInt(MAX_HIGH_SURROGATE - MIN_HIGH_SURROGATE + 1) + MIN_HIGH_SURROGATE;
+ int lowSurrogate = rnd.nextInt(MAX_LOW_SURROGATE - MIN_LOW_SURROGATE + 1) + MIN_LOW_SURROGATE;
+ return Character.toCodePoint((char) highSurrogate, (char) lowSurrogate);
+ }
+
+ private int nextNonSurrogateCodePoint() {
+ int codePoint;
+ do {
+ codePoint = rnd.nextInt(0x10000); // BMP range
+ } while (codePoint >= MIN_HIGH_SURROGATE
+ && codePoint <= MAX_LOW_SURROGATE); // Exclude surrogate range
+ return codePoint;
+ }
+ }
}
From 3df1a401d67f075015be589c17d55e3b2159960a Mon Sep 17 00:00:00 2001
From: Matthew Robertson
Date: Mon, 3 Mar 2025 13:46:33 -0700
Subject: [PATCH 075/146] Avoid Process.myProcessName() on Android 13 (#6720)
Avoid calling `Process.myProcessName()` on Android 13 because it appears
to be missing from some OEM-specific Android 13 builds. It is fine to
just let the method fall through to the next, older, method to get the
process name. See
https://github.com/firebase/firebase-unity-sdk/issues/1059
I have not been able to reproduce this issue locally, but this change is
very safe.
We should consider refactoring Crashlytics to consume the Sessions
`ProcessDetails` data class, instead of the current `@AutoValue` holder.
---
firebase-crashlytics/CHANGELOG.md | 13 +++++++++++--
.../crashlytics/internal/ProcessDetailsProvider.kt | 6 ++++--
.../firebase/sessions/ProcessDetailsProvider.kt | 2 +-
3 files changed, 16 insertions(+), 5 deletions(-)
diff --git a/firebase-crashlytics/CHANGELOG.md b/firebase-crashlytics/CHANGELOG.md
index f0bbb91128b..a6e5e087b60 100644
--- a/firebase-crashlytics/CHANGELOG.md
+++ b/firebase-crashlytics/CHANGELOG.md
@@ -1,6 +1,15 @@
# Unreleased
+* [fixed] Fixed NoSuchMethodError when getting process info on Android 13 [#6720]
+
+# 19.4.1
* [changed] Updated `firebase-sessions` dependency to v2.0.9
+
+## Kotlin
+The Kotlin extensions library transitively includes the updated
+`firebase-crashlytics` library. The Kotlin extensions library has no additional
+updates.
+
# 19.4.0
* [feature] Added an overload for `recordException` that allows logging additional custom
keys to the non fatal event [#3551]
@@ -324,10 +333,10 @@ updates.
# 18.2.10
* [fixed] Fixed a bug that could prevent unhandled exceptions from being
- propogated to the default handler when the network is unavailable.
+ propagated to the default handler when the network is unavailable.
* [changed] Internal changes to support on-demand fatal crash reporting for
Flutter apps.
-* [fixed] Fixed a bug that prevented [crashlytics] from initalizing on some
+* [fixed] Fixed a bug that prevented [crashlytics] from initializing on some
devices in some cases. (#3269)
diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/ProcessDetailsProvider.kt b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/ProcessDetailsProvider.kt
index 49fd2fafd18..172ebaaf477 100644
--- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/ProcessDetailsProvider.kt
+++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/ProcessDetailsProvider.kt
@@ -29,6 +29,8 @@ import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.Session.
* @hide
*/
internal object ProcessDetailsProvider {
+ // TODO(mrober): Merge this with [com.google.firebase.sessions.ProcessDetailsProvider].
+
/** Gets the details for all of this app's running processes. */
fun getAppProcessDetails(context: Context): List {
val appUid = context.applicationInfo.uid
@@ -70,7 +72,7 @@ internal object ProcessDetailsProvider {
processName: String,
pid: Int = 0,
importance: Int = 0,
- isDefaultProcess: Boolean = false
+ isDefaultProcess: Boolean = false,
) =
ProcessDetails.builder()
.setProcessName(processName)
@@ -81,7 +83,7 @@ internal object ProcessDetailsProvider {
/** Gets the app's current process name. If the API is not available, returns an empty string. */
private fun getProcessName(): String =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) {
Process.myProcessName()
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Application.getProcessName() ?: ""
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDetailsProvider.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDetailsProvider.kt
index 72e80469880..65d1dfbbc60 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDetailsProvider.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDetailsProvider.kt
@@ -74,7 +74,7 @@ internal object ProcessDetailsProvider {
/** Gets the app's current process name. If it could not be found, returns an empty string. */
internal fun getProcessName(): String {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) {
return Process.myProcessName()
}
From a0fee48d3e0563ee8a44f5d45813754ebe0f1f08 Mon Sep 17 00:00:00 2001
From: Matthew Robertson
Date: Mon, 3 Mar 2025 13:46:42 -0700
Subject: [PATCH 076/146] Fix bug that let responsePayloadBytes get set to -1
(#6721)
Fix a bug in `InstrHttpInputStream` that let
`NetworkRequestMetric.responsePayloadBytes` get set to -1 in some
conditions.
While investigating [b/398063523](http://b/398063523), I found that
`inputStream.read(...)` can return 0 in some cases, for example, when
the byte buffer length is 0. When this happens, it was possible to set
`responsePayloadBytes` to -1 because `-1 + 0 = -1`. I didn't just have
`bytesRead` initialize to 0 because there is a difference between 0
bytes read, and no read happened. Tested manually by hacking a test app
to force this to happen, and by unit tests.
---
firebase-perf/CHANGELOG.md | 1 +
.../perf/network/InstrHttpInputStream.java | 37 +++++++++++--------
.../network/InstrHttpInputStreamTest.java | 34 ++++++++++++++---
3 files changed, 52 insertions(+), 20 deletions(-)
diff --git a/firebase-perf/CHANGELOG.md b/firebase-perf/CHANGELOG.md
index 58bdd0f1d29..9cfa4e6537a 100644
--- a/firebase-perf/CHANGELOG.md
+++ b/firebase-perf/CHANGELOG.md
@@ -1,5 +1,6 @@
# Unreleased
* [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716]
+* [fixed] Fixed a bug that allowed invalid payload bytes value in network request metrics.
# 21.0.4
diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/network/InstrHttpInputStream.java b/firebase-perf/src/main/java/com/google/firebase/perf/network/InstrHttpInputStream.java
index fc660c70426..5ffff6c0d2f 100644
--- a/firebase-perf/src/main/java/com/google/firebase/perf/network/InstrHttpInputStream.java
+++ b/firebase-perf/src/main/java/com/google/firebase/perf/network/InstrHttpInputStream.java
@@ -30,13 +30,7 @@ public final class InstrHttpInputStream extends InputStream {
private long timeToResponseInitiated;
private long timeToResponseLastRead = -1;
- /**
- * Instrumented inputStream object
- *
- * @param inputStream
- * @param builder
- * @param timer
- */
+ /** Instrumented inputStream object */
public InstrHttpInputStream(
final InputStream inputStream, final NetworkRequestMetricBuilder builder, Timer timer) {
this.timer = timer;
@@ -99,12 +93,13 @@ public int read() throws IOException {
if (timeToResponseInitiated == -1) {
timeToResponseInitiated = tempTime;
}
- if (bytesRead == -1 && timeToResponseLastRead == -1) {
+ boolean endOfStream = bytesRead == -1;
+ if (endOfStream && timeToResponseLastRead == -1) {
timeToResponseLastRead = tempTime;
networkMetricBuilder.setTimeToResponseCompletedMicros(timeToResponseLastRead);
networkMetricBuilder.build();
} else {
- this.bytesRead++;
+ incrementBytesRead(1);
networkMetricBuilder.setResponsePayloadBytes(this.bytesRead);
}
return bytesRead;
@@ -124,12 +119,13 @@ public int read(final byte[] buffer, final int byteOffset, final int byteCount)
if (timeToResponseInitiated == -1) {
timeToResponseInitiated = tempTime;
}
- if (bytesRead == -1 && timeToResponseLastRead == -1) {
+ boolean endOfStream = bytesRead == -1;
+ if (endOfStream && timeToResponseLastRead == -1) {
timeToResponseLastRead = tempTime;
networkMetricBuilder.setTimeToResponseCompletedMicros(timeToResponseLastRead);
networkMetricBuilder.build();
} else {
- this.bytesRead += bytesRead;
+ incrementBytesRead(bytesRead);
networkMetricBuilder.setResponsePayloadBytes(this.bytesRead);
}
return bytesRead;
@@ -148,12 +144,13 @@ public int read(final byte[] buffer) throws IOException {
if (timeToResponseInitiated == -1) {
timeToResponseInitiated = tempTime;
}
- if (bytesRead == -1 && timeToResponseLastRead == -1) {
+ boolean endOfStream = bytesRead == -1;
+ if (endOfStream && timeToResponseLastRead == -1) {
timeToResponseLastRead = tempTime;
networkMetricBuilder.setTimeToResponseCompletedMicros(timeToResponseLastRead);
networkMetricBuilder.build();
} else {
- this.bytesRead += bytesRead;
+ incrementBytesRead(bytesRead);
networkMetricBuilder.setResponsePayloadBytes(this.bytesRead);
}
return bytesRead;
@@ -183,11 +180,13 @@ public long skip(final long byteCount) throws IOException {
if (timeToResponseInitiated == -1) {
timeToResponseInitiated = tempTime;
}
- if (skipped == -1 && timeToResponseLastRead == -1) {
+ // InputStream.skip will return 0 for both end of stream and for 0 bytes skipped.
+ boolean endOfStream = (skipped == 0 && byteCount != 0);
+ if (endOfStream && timeToResponseLastRead == -1) {
timeToResponseLastRead = tempTime;
networkMetricBuilder.setTimeToResponseCompletedMicros(timeToResponseLastRead);
} else {
- bytesRead += skipped;
+ incrementBytesRead(skipped);
networkMetricBuilder.setResponsePayloadBytes(bytesRead);
}
return skipped;
@@ -197,4 +196,12 @@ public long skip(final long byteCount) throws IOException {
throw e;
}
}
+
+ private void incrementBytesRead(long bytesRead) {
+ if (this.bytesRead == -1) {
+ this.bytesRead = bytesRead;
+ } else {
+ this.bytesRead += bytesRead;
+ }
+ }
}
diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/network/InstrHttpInputStreamTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/network/InstrHttpInputStreamTest.java
index 8a7ecb2131b..e1f45c45329 100644
--- a/firebase-perf/src/test/java/com/google/firebase/perf/network/InstrHttpInputStreamTest.java
+++ b/firebase-perf/src/test/java/com/google/firebase/perf/network/InstrHttpInputStreamTest.java
@@ -30,6 +30,7 @@
import com.google.firebase.perf.v1.NetworkRequestMetric.NetworkClientErrorReason;
import java.io.IOException;
import java.io.InputStream;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -40,10 +41,14 @@
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
-/** Unit tests for {@link com.google.firebase.perf.network.InstrHttpInputStream}. */
+/**
+ * Unit tests for {@link com.google.firebase.perf.network.InstrHttpInputStream}.
+ *
+ * @noinspection ResultOfMethodCallIgnored
+ */
@RunWith(RobolectricTestRunner.class)
public class InstrHttpInputStreamTest extends FirebasePerformanceTestBase {
-
+ private AutoCloseable closeable;
@Mock InputStream mInputStream;
@Mock TransportManager transportManager;
@Mock Timer timer;
@@ -53,12 +58,17 @@ public class InstrHttpInputStreamTest extends FirebasePerformanceTestBase {
@Before
public void setUp() {
- MockitoAnnotations.initMocks(this);
+ closeable = MockitoAnnotations.openMocks(this);
when(timer.getMicros()).thenReturn((long) 1000);
when(timer.getDurationMicros()).thenReturn((long) 2000);
networkMetricBuilder = NetworkRequestMetricBuilder.builder(transportManager);
}
+ @After
+ public void releaseMocks() throws Exception {
+ closeable.close();
+ }
+
@Test
public void testAvailable() throws IOException {
int availableVal = 7;
@@ -80,7 +90,7 @@ public void testClose() throws IOException {
}
@Test
- public void testMark() throws IOException {
+ public void testMark() {
int markInput = 256;
new InstrHttpInputStream(mInputStream, networkMetricBuilder, timer).mark(markInput);
@@ -89,7 +99,7 @@ public void testMark() throws IOException {
}
@Test
- public void testMarkSupported() throws IOException {
+ public void testMarkSupported() {
when(mInputStream.markSupported()).thenReturn(true);
boolean ret =
new InstrHttpInputStream(mInputStream, networkMetricBuilder, timer).markSupported();
@@ -108,6 +118,20 @@ public void testRead() throws IOException {
verify(mInputStream).read();
}
+ @Test
+ public void testReadBufferOffsetZero() throws IOException {
+ byte[] b = new byte[0];
+ int off = 0;
+ int len = 0;
+ when(mInputStream.read(b, off, len)).thenReturn(len);
+ int ret = new InstrHttpInputStream(mInputStream, networkMetricBuilder, timer).read(b, off, len);
+
+ NetworkRequestMetric metric = networkMetricBuilder.build();
+ assertThat(ret).isEqualTo(0);
+ assertThat(metric.getResponsePayloadBytes()).isEqualTo(0);
+ verify(mInputStream).read(b, off, len);
+ }
+
@Test
public void testReadBufferOffsetCount() throws IOException {
byte[] buffer = new byte[] {(byte) 0xe0};
From df70f99839bae002ad5a0d38cd5acc4917ecc93e Mon Sep 17 00:00:00 2001
From: Matthew Robertson
Date: Tue, 4 Mar 2025 06:47:40 -0700
Subject: [PATCH 077/146] Update datastore dependency to 1.1.3 (#6688)
Update datastore dependency to `1.1.3` to address
[CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8) in
AQS.
We had landed #6343, but it missed the datastore dependency because
version 1.0.0 "shaded" the vulnerable protobuf dependency, see #6534. I
verified this was happening by extracting the jar from
https://maven.google.com/web/index.html?q=datastore-pre#androidx.datastore:datastore-preferences-core:1.0.0
and seeing
`com.google.protobufprotobuf-parent3.10.0`
nested in a maven dir. I also verified datastore 1.1.3 has upgraded the
protobuf version to 4.28.2, a safe version. See
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-datastore-release:gradle/libs.versions.toml;l=59.
This datastore update also includes the stable
`MultiProcessDataStoreFactory` which we can utilize in a future change
to optimize things like the settings fetch for multi-process apps.
---
firebase-sessions/CHANGELOG.md | 4 ++++
firebase-sessions/firebase-sessions.gradle.kts | 2 +-
gradle/libs.versions.toml | 2 ++
smoke-tests/build.gradle | 2 ++
4 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/firebase-sessions/CHANGELOG.md b/firebase-sessions/CHANGELOG.md
index 48987a62df5..2473b64a1cf 100644
--- a/firebase-sessions/CHANGELOG.md
+++ b/firebase-sessions/CHANGELOG.md
@@ -1,5 +1,9 @@
# Unreleased
+* [changed] Updated datastore dependency to `1.1.3` to
+ fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8).
+
+# 2.0.9
* [fixed] Make AQS resilient to background init in multi-process apps.
# 2.0.7
diff --git a/firebase-sessions/firebase-sessions.gradle.kts b/firebase-sessions/firebase-sessions.gradle.kts
index 15d22381e31..0a09740bd77 100644
--- a/firebase-sessions/firebase-sessions.gradle.kts
+++ b/firebase-sessions/firebase-sessions.gradle.kts
@@ -67,12 +67,12 @@ dependencies {
exclude(group = "com.google.firebase", module = "firebase-common")
exclude(group = "com.google.firebase", module = "firebase-components")
}
- implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("com.google.android.datatransport:transport-api:3.2.0")
api("com.google.firebase:firebase-annotations:16.2.0")
api("com.google.firebase:firebase-encoders:17.0.0")
api("com.google.firebase:firebase-encoders-json:18.0.1")
implementation(libs.androidx.annotation)
+ implementation(libs.androidx.datastore.preferences)
compileOnly(libs.errorprone.annotations)
runtimeOnly("com.google.firebase:firebase-installations:18.0.0") {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b234d7bbd0f..4881c9d7d40 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -18,6 +18,7 @@ constraintlayout = "2.1.4"
coreKtx = "1.12.0"
coroutines = "1.7.3"
dagger = "2.43.2"
+datastore = "1.1.3"
dexmaker = "2.28.1"
dexmakerVersion = "1.2"
espressoCore = "3.6.1"
@@ -91,6 +92,7 @@ androidx-cardview = { module = "androidx.cardview:cardview", version.ref = "card
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
androidx-core = { module = "androidx.core:core", version = "1.2.0" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
+androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" }
androidx-espresso-idling-resource = { module = "androidx.test.espresso:espresso-idling-resource", version.ref = "espressoCore" }
androidx-espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "espressoCore" }
diff --git a/smoke-tests/build.gradle b/smoke-tests/build.gradle
index 346bad8698f..89df856dd06 100644
--- a/smoke-tests/build.gradle
+++ b/smoke-tests/build.gradle
@@ -24,12 +24,14 @@ buildscript {
dependencies {
classpath "com.android.tools.build:gradle:8.3.2"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0"
classpath "com.google.gms:google-services:4.3.14"
classpath "com.google.firebase:firebase-crashlytics-gradle:2.8.1"
}
}
apply plugin: "com.android.application"
+apply plugin: "org.jetbrains.kotlin.android"
android {
compileSdkVersion 34
From 948e98ccb53d6b282573a72fccb1d4a1dbd8d2b9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 4 Mar 2025 17:29:44 -0500
Subject: [PATCH 078/146] build(deps): bump org.eclipse.jgit:org.eclipse.jgit
from 6.3.0.202209071007-r to 7.1.0.202411261347-r (#6733)
Bumps
[org.eclipse.jgit:org.eclipse.jgit](https://github.com/eclipse-jgit/jgit)
from 6.3.0.202209071007-r to 7.1.0.202411261347-r.
Commits
4d1d885
JGit v7.1.0.202411261347-r
856c1c3
Merge branch 'master' into stable-7.1
683d444
Merge branch 'stable-7.0'
e3eabe5
Merge branch 'stable-6.10' into stable-7.0
f27ea51
Merge "Pack.java: Recover more often in Pack.copyAsIs2()" into
stable-6.10
f026c19
PackDirectory: Filter out tmp GC pack files
6fa28d7
Add pack-refs command to the CLI
079dbe8
Test advertised capabilities with protocol V0 and allow*Sha1InWant
5b1513a
Align request policies with CGit
f295477
Merge "GitTimeParser: Fix multiple errorprone and style
comments"
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rodrigo Lazo
---
plugins/build.gradle.kts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins/build.gradle.kts b/plugins/build.gradle.kts
index 85bad8507be..f429250bbc4 100644
--- a/plugins/build.gradle.kts
+++ b/plugins/build.gradle.kts
@@ -61,7 +61,7 @@ dependencies {
implementation("com.google.guava:guava:31.1-jre")
implementation("org.ow2.asm:asm-tree:9.5")
- implementation("org.eclipse.jgit:org.eclipse.jgit:6.3.0.202209071007-r")
+ implementation("org.eclipse.jgit:org.eclipse.jgit:7.1.0.202411261347-r")
implementation(libs.kotlinx.serialization.json)
implementation("com.google.code.gson:gson:2.8.9")
implementation(libs.android.gradlePlugin.gradle)
From 636e4748d27e759a845ab852a959fedf30faaab3 Mon Sep 17 00:00:00 2001
From: Rodrigo Lazo
Date: Wed, 5 Mar 2025 16:16:09 -0500
Subject: [PATCH 079/146] build(deps): bump kotestAssertionsCore from 5.5.5 to
5.8.1 (#6736)
Last version depending on kotlin stdlib 1.8.x
---
gradle/libs.versions.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4881c9d7d40..489b504e75c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -43,7 +43,7 @@ jacksonDatabind = "2.18.2"
javalite = "3.25.5"
jsonassert = "1.5.0"
kotest = "5.9.0" # Do not use 5.9.1 because it reverts the fix for https://github.com/kotest/kotest/issues/3981
-kotestAssertionsCore = "5.5.5"
+kotestAssertionsCore = "5.8.1"
kotlin = "1.8.22"
ktorVersion = "2.3.2"
legacySupportV4 = "1.0.0"
From be00f2cbd40efe6fe987d55129f408b348d667a7 Mon Sep 17 00:00:00 2001
From: Rodrigo Lazo
Date: Thu, 6 Mar 2025 19:17:14 -0500
Subject: [PATCH 080/146] Remove unnecessary steps in CI Testing for vertex
(#6743)
Additionally, make the update_responses.sh more verbose to ease debugging
---
.github/workflows/ci_tests.yml | 11 +----------
firebase-vertexai/update_responses.sh | 2 ++
2 files changed, 3 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml
index c706aa614bd..603719e8142 100644
--- a/.github/workflows/ci_tests.yml
+++ b/.github/workflows/ci_tests.yml
@@ -56,18 +56,9 @@ jobs:
distribution: temurin
cache: gradle
- - name: Pull genai-common
- if: matrix.module == ':firebase-vertexai'
- run: |
- git clone https://github.com/google-gemini/generative-ai-android.git
- cd generative-ai-android
- ./gradlew :common:updateVersion common:publishToMavenLocal
- cd ..
-
- name: Clone mock responses
if: matrix.module == ':firebase-vertexai'
- run: |
- firebase-vertexai/update_responses.sh
+ run: firebase-vertexai/update_responses.sh
- name: Add google-services.json
env:
diff --git a/firebase-vertexai/update_responses.sh b/firebase-vertexai/update_responses.sh
index 70e438090bd..3feec3b861b 100755
--- a/firebase-vertexai/update_responses.sh
+++ b/firebase-vertexai/update_responses.sh
@@ -21,6 +21,8 @@ RESPONSES_VERSION='v6.*' # The major version of mock responses to use
REPO_NAME="vertexai-sdk-test-data"
REPO_LINK="https://github.com/FirebaseExtended/$REPO_NAME.git"
+set -x
+
cd "$(dirname "$0")/src/test/resources" || exit
rm -rf "$REPO_NAME"
git clone "$REPO_LINK" --quiet || exit
From d5801dc471f0f9cbe1e1d302de8ccbaa0463f765 Mon Sep 17 00:00:00 2001
From: Rodrigo Lazo
Date: Mon, 10 Mar 2025 12:31:34 -0400
Subject: [PATCH 081/146] Update functions changelog (#6751)
The mergeback PR from last release didn't include the update to
function's CHANGELOG.md file
---
firebase-functions/CHANGELOG.md | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/firebase-functions/CHANGELOG.md b/firebase-functions/CHANGELOG.md
index e9fe66c897d..26bb8de7306 100644
--- a/firebase-functions/CHANGELOG.md
+++ b/firebase-functions/CHANGELOG.md
@@ -1,8 +1,15 @@
# Unreleased
+
+# 21.1.1
* [fixed] Resolve Kotlin migration visibility issues
([#6522](//github.com/firebase/firebase-android-sdk/pull/6522))
+## Kotlin
+The Kotlin extensions library transitively includes the updated
+`firebase-functions` library. The Kotlin extensions library has no additional
+updates.
+
# 21.1.0
* [changed] Migrated to Kotlin
From d32c474c3c67705c90077c1bf6090e1ca8a6f67d Mon Sep 17 00:00:00 2001
From: Rodrigo Lazo
Date: Mon, 10 Mar 2025 13:33:40 -0400
Subject: [PATCH 082/146] [VertexAI] Add initial support to export covered API
(#6749)
The server produces a discovery document with the details of the API
surface.
https://aiplatform.googleapis.com/$discovery/rest?version=v1beta1
This change introduces code that can generate similar a description of
the API covered by the SDK. This will enable us to track difference
between both.
In a follow up PR we can implement the logic to fully export the
surface.
---------
Co-authored-by: Daymon <17409137+daymxn@users.noreply.github.com>
---
.../vertexai/common/util/serialization.kt | 8 +-
.../firebase/vertexai/SerializationTests.kt | 215 ++++++++++++++++++
.../vertexai/common/util/descriptorToJson.kt | 167 ++++++++++++++
3 files changed, 389 insertions(+), 1 deletion(-)
create mode 100644 firebase-vertexai/src/test/java/com/google/firebase/vertexai/SerializationTests.kt
create mode 100644 firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/descriptorToJson.kt
diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/util/serialization.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/util/serialization.kt
index e64bb89afc3..4a2570b82d6 100644
--- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/util/serialization.kt
+++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/util/serialization.kt
@@ -23,6 +23,7 @@ import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.descriptors.element
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
@@ -36,7 +37,12 @@ import kotlinx.serialization.encoding.Encoder
*/
internal class FirstOrdinalSerializer>(private val enumClass: KClass) :
KSerializer {
- override val descriptor: SerialDescriptor = buildClassSerialDescriptor("FirstOrdinalSerializer")
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("FirstOrdinalSerializer") {
+ for (enumValue in enumClass.enumValues()) {
+ element(enumValue.toString())
+ }
+ }
override fun deserialize(decoder: Decoder): T {
val name = decoder.decodeString()
diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/SerializationTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/SerializationTests.kt
new file mode 100644
index 00000000000..cf6a40680e5
--- /dev/null
+++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/SerializationTests.kt
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2025 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.vertexai
+
+import com.google.firebase.vertexai.common.util.descriptorToJson
+import com.google.firebase.vertexai.type.Candidate
+import com.google.firebase.vertexai.type.CountTokensResponse
+import com.google.firebase.vertexai.type.GenerateContentResponse
+import com.google.firebase.vertexai.type.ModalityTokenCount
+import com.google.firebase.vertexai.type.Schema
+import io.kotest.assertions.json.shouldEqualJson
+import org.junit.Test
+
+internal class SerializationTests {
+ @Test
+ fun `test countTokensResponse serialization as Json`() {
+ val expectedJsonAsString =
+ """
+ {
+ "id": "CountTokensResponse",
+ "type": "object",
+ "properties": {
+ "totalTokens": {
+ "type": "integer"
+ },
+ "totalBillableCharacters": {
+ "type": "integer"
+ },
+ "promptTokensDetails": {
+ "type": "array",
+ "items": {
+ "${'$'}ref": "ModalityTokenCount"
+ }
+ }
+ }
+ }
+ """
+ .trimIndent()
+ val actualJson = descriptorToJson(CountTokensResponse.Internal.serializer().descriptor)
+ expectedJsonAsString shouldEqualJson actualJson.toString()
+ }
+
+ @Test
+ fun `test modalityTokenCount serialization as Json`() {
+ val expectedJsonAsString =
+ """
+ {
+ "id": "ModalityTokenCount",
+ "type": "object",
+ "properties": {
+ "modality": {
+ "type": "string",
+ "enum": [
+ "UNSPECIFIED",
+ "TEXT",
+ "IMAGE",
+ "VIDEO",
+ "AUDIO",
+ "DOCUMENT"
+ ]
+ },
+ "tokenCount": {
+ "type": "integer"
+ }
+ }
+ }
+ """
+ .trimIndent()
+ val actualJson = descriptorToJson(ModalityTokenCount.Internal.serializer().descriptor)
+ expectedJsonAsString shouldEqualJson actualJson.toString()
+ }
+
+ @Test
+ fun `test GenerateContentResponse serialization as Json`() {
+ val expectedJsonAsString =
+ """
+ {
+ "id": "GenerateContentResponse",
+ "type": "object",
+ "properties": {
+ "candidates": {
+ "type": "array",
+ "items": {
+ "${'$'}ref": "Candidate"
+ }
+ },
+ "promptFeedback": {
+ "${'$'}ref": "PromptFeedback"
+ },
+ "usageMetadata": {
+ "${'$'}ref": "UsageMetadata"
+ }
+ }
+ }
+ """
+ .trimIndent()
+ val actualJson = descriptorToJson(GenerateContentResponse.Internal.serializer().descriptor)
+ expectedJsonAsString shouldEqualJson actualJson.toString()
+ }
+
+ @Test
+ fun `test Candidate serialization as Json`() {
+ val expectedJsonAsString =
+ """
+ {
+ "id": "Candidate",
+ "type": "object",
+ "properties": {
+ "content": {
+ "${'$'}ref": "Content"
+ },
+ "finishReason": {
+ "type": "string",
+ "enum": [
+ "UNKNOWN",
+ "UNSPECIFIED",
+ "STOP",
+ "MAX_TOKENS",
+ "SAFETY",
+ "RECITATION",
+ "OTHER",
+ "BLOCKLIST",
+ "PROHIBITED_CONTENT",
+ "SPII",
+ "MALFORMED_FUNCTION_CALL"
+ ]
+ },
+ "safetyRatings": {
+ "type": "array",
+ "items": {
+ "${'$'}ref": "SafetyRating"
+ }
+ },
+ "citationMetadata": {
+ "${'$'}ref": "CitationMetadata"
+ },
+ "groundingMetadata": {
+ "${'$'}ref": "GroundingMetadata"
+ }
+ }
+ }
+ """
+ .trimIndent()
+ val actualJson = descriptorToJson(Candidate.Internal.serializer().descriptor)
+ expectedJsonAsString shouldEqualJson actualJson.toString()
+ }
+
+ @Test
+ fun `test Schema serialization as Json`() {
+ /**
+ * Unlike the actual schema in the background, we don't represent "type" as an enum, but rather
+ * as a string. This is because we restrict what values can be used (using helper methods,
+ * rather than type).
+ */
+ val expectedJsonAsString =
+ """
+ {
+ "id": "Schema",
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string"
+ },
+ "format": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "nullable": {
+ "type": "boolean"
+ },
+ "items": {
+ "${'$'}ref": "Schema"
+ },
+ "enum": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "properties": {
+ "type": "object",
+ "additionalProperties": {
+ "${'$'}ref": "Schema"
+ }
+ },
+ "required": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ """
+ .trimIndent()
+ val actualJson = descriptorToJson(Schema.Internal.serializer().descriptor)
+ expectedJsonAsString shouldEqualJson actualJson.toString()
+ }
+}
diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/descriptorToJson.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/descriptorToJson.kt
new file mode 100644
index 00000000000..31d9156bc75
--- /dev/null
+++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/descriptorToJson.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2025 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.vertexai.common.util
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.SerialKind
+import kotlinx.serialization.descriptors.StructureKind
+import kotlinx.serialization.descriptors.elementDescriptors
+import kotlinx.serialization.descriptors.elementNames
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonObjectBuilder
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.put
+import kotlinx.serialization.json.putJsonObject
+
+/**
+ * Returns a [JsonObject] representing the classes in the hierarchy of a serialization [descriptor].
+ *
+ * The format of the JSON object is similar to that of a Discovery Document, but restricted to these
+ * fields:
+ * - id
+ * - type
+ * - properties
+ * - items
+ * - $ref
+ *
+ * @param descriptor The [SerialDescriptor] to process.
+ */
+@OptIn(ExperimentalSerializationApi::class)
+internal fun descriptorToJson(descriptor: SerialDescriptor): JsonObject {
+ return buildJsonObject {
+ put("id", simpleNameFromSerialName(descriptor.serialName))
+ put("type", typeNameFromKind(descriptor.kind))
+ if (descriptor.kind != StructureKind.CLASS) {
+ throw UnsupportedOperationException("Only classes can be serialized to JSON for now.")
+ }
+ // For top-level enums, add them directly.
+ if (descriptor.serialName == "FirstOrdinalSerializer") {
+ addEnumDescription(descriptor)
+ } else {
+ addObjectProperties(descriptor)
+ }
+ }
+}
+
+@OptIn(ExperimentalSerializationApi::class)
+internal fun JsonObjectBuilder.addListDescription(descriptor: SerialDescriptor) =
+ putJsonObject("items") {
+ val itemDescriptor = descriptor.elementDescriptors.first()
+ val nestedIsPrimitive = (descriptor.elementsCount == 1 && itemDescriptor.kind is PrimitiveKind)
+ if (nestedIsPrimitive) {
+ put("type", typeNameFromKind(itemDescriptor.kind))
+ } else {
+ put("\$ref", simpleNameFromSerialName(itemDescriptor.serialName))
+ }
+ }
+
+@OptIn(ExperimentalSerializationApi::class)
+internal fun JsonObjectBuilder.addEnumDescription(descriptor: SerialDescriptor): JsonElement? {
+ put("type", typeNameFromKind(SerialKind.ENUM))
+ return put("enum", JsonArray(descriptor.elementNames.map { JsonPrimitive(it) }))
+}
+
+@OptIn(ExperimentalSerializationApi::class)
+internal fun JsonObjectBuilder.addObjectProperties(descriptor: SerialDescriptor): JsonElement? {
+ return putJsonObject("properties") {
+ for (i in 0 until descriptor.elementsCount) {
+ val elementDescriptor = descriptor.getElementDescriptor(i)
+ val elementName = descriptor.getElementName(i)
+ putJsonObject(elementName) {
+ when (elementDescriptor.kind) {
+ StructureKind.LIST -> {
+ put("type", typeNameFromKind(elementDescriptor.kind))
+ addListDescription(elementDescriptor)
+ }
+ StructureKind.CLASS -> {
+ if (elementDescriptor.serialName.startsWith("FirstOrdinalSerializer")) {
+ addEnumDescription(elementDescriptor)
+ } else {
+ put("\$ref", simpleNameFromSerialName(elementDescriptor.serialName))
+ }
+ }
+ StructureKind.MAP -> {
+ put("type", typeNameFromKind(elementDescriptor.kind))
+ putJsonObject("additionalProperties") {
+ put(
+ "\$ref",
+ simpleNameFromSerialName(elementDescriptor.getElementDescriptor(1).serialName)
+ )
+ }
+ }
+ else -> {
+ put("type", typeNameFromKind(elementDescriptor.kind))
+ }
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalSerializationApi::class)
+internal fun typeNameFromKind(kind: SerialKind): String {
+ return when (kind) {
+ PrimitiveKind.BOOLEAN -> "boolean"
+ PrimitiveKind.BYTE -> "integer"
+ PrimitiveKind.CHAR -> "string"
+ PrimitiveKind.DOUBLE -> "number"
+ PrimitiveKind.FLOAT -> "number"
+ PrimitiveKind.INT -> "integer"
+ PrimitiveKind.LONG -> "integer"
+ PrimitiveKind.SHORT -> "integer"
+ PrimitiveKind.STRING -> "string"
+ StructureKind.CLASS -> "object"
+ StructureKind.LIST -> "array"
+ SerialKind.ENUM -> "string"
+ StructureKind.MAP -> "object"
+ /* Only add new cases if they show up in actual test scenarios. */
+ else -> TODO()
+ }
+}
+
+/**
+ * Extracts the name expected for a class from its serial name.
+ *
+ * Our serialization classes are nested within the public-facing classes, and that's the name we
+ * want in the json output. There are two class names
+ *
+ * - `com.google.firebase.vertexai.type.Content.Internal` for regular scenarios
+ * - `com.google.firebase.vertexai.type.Content.Internal.SomeClass` for nested classes in the
+ * serializer.
+ *
+ * For the later time we need the second to last component, for the former we need the last
+ * component.
+ *
+ * Additionally, given that types can be nullable, we need to strip the `?` from the end of the
+ * name.
+ */
+internal fun simpleNameFromSerialName(serialName: String): String =
+ serialName
+ .split(".")
+ .let {
+ if (it.last().startsWith("Internal")) {
+ it[it.size - 2]
+ } else {
+ it.last()
+ }
+ }
+ .replace("?", "")
From 606a4bb685f00658486158553aa37ac5874f00af Mon Sep 17 00:00:00 2001
From: Matthew Robertson
Date: Mon, 10 Mar 2025 11:48:17 -0600
Subject: [PATCH 083/146] Remove code style from deprecated message (#6753)
---
.../java/com/google/firebase/crashlytics/KeyValueBuilder.kt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/KeyValueBuilder.kt b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/KeyValueBuilder.kt
index 636b975ab1d..74d3793e215 100644
--- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/KeyValueBuilder.kt
+++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/KeyValueBuilder.kt
@@ -23,7 +23,7 @@ private constructor(
private val builder: CustomKeysAndValues.Builder,
) {
@Deprecated(
- "Do not construct this directly. Use [setCustomKeys] instead. To be removed in the next major release."
+ "Do not construct this directly. Use `setCustomKeys` instead. To be removed in the next major release."
)
constructor(crashlytics: FirebaseCrashlytics) : this(crashlytics, CustomKeysAndValues.Builder())
From 3440cc177d085a56922a15614a99da991f4a7024 Mon Sep 17 00:00:00 2001
From: welishr <65972773+welishr@users.noreply.github.com>
Date: Mon, 10 Mar 2025 15:56:32 -0400
Subject: [PATCH 084/146] Store registered context for sync task
unregistrations (#6752)
For issue #6558, this is an attempt at fixing the
IllegalArgumentException by ensuring that the context we use for
registering the SyncTask is the same context we use to unregister the
task. Race conditions dont seem like a culprit here since unregister is
only triggered by the Receiver itself, which should be only executed
synchronously on the main thread.
---
firebase-messaging/CHANGELOG.md | 2 ++
.../java/com/google/firebase/messaging/SyncTask.java | 10 ++++++++--
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/firebase-messaging/CHANGELOG.md b/firebase-messaging/CHANGELOG.md
index 4a7e28a5766..6b0a8bdadd9 100644
--- a/firebase-messaging/CHANGELOG.md
+++ b/firebase-messaging/CHANGELOG.md
@@ -1,4 +1,6 @@
# Unreleased
+* [changed] Bug fix in SyncTask to always unregister the receiver on the same
+ context on which it was registered.
# 24.1.0
diff --git a/firebase-messaging/src/main/java/com/google/firebase/messaging/SyncTask.java b/firebase-messaging/src/main/java/com/google/firebase/messaging/SyncTask.java
index c0c4074c11e..cd821f0e1f3 100644
--- a/firebase-messaging/src/main/java/com/google/firebase/messaging/SyncTask.java
+++ b/firebase-messaging/src/main/java/com/google/firebase/messaging/SyncTask.java
@@ -161,6 +161,7 @@ boolean isDeviceConnected() {
static class ConnectivityChangeReceiver extends BroadcastReceiver {
@Nullable private SyncTask task; // task is set to null after it has been fired.
+ @Nullable private Context receiverContext;
public ConnectivityChangeReceiver(SyncTask task) {
this.task = task;
@@ -171,7 +172,10 @@ public void registerReceiver() {
Log.d(TAG, "Connectivity change received registered");
}
IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
- task.getContext().registerReceiver(this, intentFilter);
+ if (task != null) {
+ receiverContext = task.getContext();
+ receiverContext.registerReceiver(this, intentFilter);
+ }
}
@Override
@@ -191,7 +195,9 @@ public void onReceive(Context context, Intent intent) {
Log.d(TAG, "Connectivity changed. Starting background sync.");
}
task.firebaseMessaging.enqueueTaskWithDelaySeconds(task, 0);
- task.getContext().unregisterReceiver(this);
+ if (receiverContext != null) {
+ receiverContext.unregisterReceiver(this);
+ }
task = null;
}
}
From 1c5923685165919d69f999054445f5e31a70b939 Mon Sep 17 00:00:00 2001
From: mustafa jadid
Date: Mon, 10 Mar 2025 13:31:17 -0700
Subject: [PATCH 085/146] Extend Firebase SDK with new APIs to consume
streaming callable function response (#6602)
Extend Firebase SDK with new APIs to consume streaming callable function
response.
- Handling the server-sent event (SSE) parsing internally
- Providing proper error handling and connection management
- Maintaining memory efficiency for long-running streams
---------
Co-authored-by: Rodrigo Lazo
---
firebase-functions/api.txt | 17 +
.../firebase-functions.gradle.kts | 3 +
.../androidTest/backend/functions/index.js | 107 ++++++
.../google/firebase/functions/StreamTests.kt | 218 ++++++++++++
.../firebase/functions/FirebaseFunctions.kt | 16 +
.../functions/HttpsCallableReference.kt | 56 ++-
.../firebase/functions/PublisherStream.kt | 328 ++++++++++++++++++
.../firebase/functions/StreamResponse.kt | 57 +++
8 files changed, 798 insertions(+), 4 deletions(-)
create mode 100644 firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt
create mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt
create mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/StreamResponse.kt
diff --git a/firebase-functions/api.txt b/firebase-functions/api.txt
index a9a05c703a8..1a12a250b35 100644
--- a/firebase-functions/api.txt
+++ b/firebase-functions/api.txt
@@ -84,6 +84,8 @@ package com.google.firebase.functions {
method public com.google.android.gms.tasks.Task call(Object? data);
method public long getTimeout();
method public void setTimeout(long timeout, java.util.concurrent.TimeUnit units);
+ method public org.reactivestreams.Publisher stream();
+ method public org.reactivestreams.Publisher stream(Object? data = null);
method public com.google.firebase.functions.HttpsCallableReference withTimeout(long timeout, java.util.concurrent.TimeUnit units);
property public final long timeout;
}
@@ -93,6 +95,21 @@ package com.google.firebase.functions {
field public final Object? data;
}
+ public abstract class StreamResponse {
+ }
+
+ public static final class StreamResponse.Message extends com.google.firebase.functions.StreamResponse {
+ ctor public StreamResponse.Message(com.google.firebase.functions.HttpsCallableResult message);
+ method public com.google.firebase.functions.HttpsCallableResult getMessage();
+ property public final com.google.firebase.functions.HttpsCallableResult message;
+ }
+
+ public static final class StreamResponse.Result extends com.google.firebase.functions.StreamResponse {
+ ctor public StreamResponse.Result(com.google.firebase.functions.HttpsCallableResult result);
+ method public com.google.firebase.functions.HttpsCallableResult getResult();
+ property public final com.google.firebase.functions.HttpsCallableResult result;
+ }
+
}
package com.google.firebase.functions.ktx {
diff --git a/firebase-functions/firebase-functions.gradle.kts b/firebase-functions/firebase-functions.gradle.kts
index 7ec958bdd79..08a797112b9 100644
--- a/firebase-functions/firebase-functions.gradle.kts
+++ b/firebase-functions/firebase-functions.gradle.kts
@@ -112,6 +112,8 @@ dependencies {
implementation(libs.okhttp)
implementation(libs.playservices.base)
implementation(libs.playservices.basement)
+ implementation(libs.reactive.streams)
+
api(libs.playservices.tasks)
kapt(libs.autovalue)
@@ -131,6 +133,7 @@ dependencies {
androidTestImplementation(libs.truth)
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.test.junit)
+ androidTestImplementation(libs.kotlinx.coroutines.reactive)
androidTestImplementation(libs.mockito.core)
androidTestImplementation(libs.mockito.dexmaker)
kapt("com.google.dagger:dagger-android-processor:2.43.2")
diff --git a/firebase-functions/src/androidTest/backend/functions/index.js b/firebase-functions/src/androidTest/backend/functions/index.js
index fed5a371b89..f26d6615d68 100644
--- a/firebase-functions/src/androidTest/backend/functions/index.js
+++ b/firebase-functions/src/androidTest/backend/functions/index.js
@@ -14,6 +14,16 @@
const assert = require('assert');
const functions = require('firebase-functions');
+const functionsV2 = require('firebase-functions/v2');
+
+/**
+ * Pauses the execution for a specified amount of time.
+ * @param {number} ms - The number of milliseconds to sleep.
+ * @return {Promise} A promise that resolves after the specified time.
+ */
+function sleep(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
exports.dataTest = functions.https.onRequest((request, response) => {
assert.deepEqual(request.body, {
@@ -122,3 +132,100 @@ exports.timeoutTest = functions.https.onRequest((request, response) => {
// Wait for longer than 500ms.
setTimeout(() => response.send({data: true}), 500);
});
+
+const streamData = ['hello', 'world', 'this', 'is', 'cool'];
+
+/**
+ * Generates chunks of text asynchronously, yielding one chunk at a time.
+ * @async
+ * @generator
+ * @yields {string} A chunk of text from the data array.
+ */
+async function* generateText() {
+ for (const chunk of streamData) {
+ yield chunk;
+ await sleep(100);
+ }
+}
+
+exports.genStream = functionsV2.https.onCall(async (request, response) => {
+ if (request.acceptsStreaming) {
+ for await (const chunk of generateText()) {
+ response.sendChunk(chunk);
+ }
+ }
+ return streamData.join(' ');
+});
+
+exports.genStreamError = functionsV2.https.onCall(
+ async (request, response) => {
+ // Note: The functions backend does not pass the error message to the
+ // client at this time.
+ throw Error("BOOM")
+ });
+
+const weatherForecasts = {
+ Toronto: { conditions: 'snowy', temperature: 25 },
+ London: { conditions: 'rainy', temperature: 50 },
+ Dubai: { conditions: 'sunny', temperature: 75 }
+};
+
+/**
+ * Generates weather forecasts asynchronously for the given locations.
+ * @async
+ * @generator
+ * @param {Array<{name: string}>} locations - An array of location objects.
+ */
+async function* generateForecast(locations) {
+ for (const location of locations) {
+ yield { 'location': location, ...weatherForecasts[location.name] };
+ await sleep(100);
+ }
+};
+
+exports.genStreamWeather = functionsV2.https.onCall(
+ async (request, response) => {
+ const locations = request.data && request.data.data?
+ request.data.data: [];
+ const forecasts = [];
+ if (request.acceptsStreaming) {
+ for await (const chunk of generateForecast(locations)) {
+ forecasts.push(chunk);
+ response.sendChunk(chunk);
+ }
+ }
+ return {forecasts};
+ });
+
+exports.genStreamEmpty = functionsV2.https.onCall(
+ async (request, response) => {
+ if (request.acceptsStreaming) {
+ // Send no chunks
+ }
+ // Implicitly return null.
+ }
+);
+
+exports.genStreamResultOnly = functionsV2.https.onCall(
+ async (request, response) => {
+ if (request.acceptsStreaming) {
+ // Do not send any chunks.
+ }
+ return "Only a result";
+ }
+);
+
+exports.genStreamLargeData = functionsV2.https.onCall(
+ async (request, response) => {
+ if (request.acceptsStreaming) {
+ const largeString = 'A'.repeat(10000);
+ const chunkSize = 1024;
+ for (let i = 0; i < largeString.length; i += chunkSize) {
+ const chunk = largeString.substring(i, i + chunkSize);
+ response.sendChunk(chunk);
+ await sleep(100);
+ }
+ }
+ return "Stream Completed";
+ }
+);
diff --git a/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt b/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt
new file mode 100644
index 00000000000..e0de5cc2262
--- /dev/null
+++ b/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2025 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.functions
+
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import com.google.firebase.Firebase
+import com.google.firebase.initialize
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.reactive.asFlow
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.reactivestreams.Subscriber
+import org.reactivestreams.Subscription
+
+@RunWith(AndroidJUnit4::class)
+class StreamTests {
+
+ private lateinit var functions: FirebaseFunctions
+
+ @Before
+ fun setup() {
+ Firebase.initialize(ApplicationProvider.getApplicationContext())
+ functions = Firebase.functions
+ }
+
+ internal class StreamSubscriber : Subscriber {
+ internal val messages = mutableListOf()
+ internal var result: StreamResponse.Result? = null
+ internal var throwable: Throwable? = null
+ internal var isComplete = false
+ internal lateinit var subscription: Subscription
+
+ override fun onSubscribe(subscription: Subscription) {
+ this.subscription = subscription
+ subscription.request(Long.MAX_VALUE)
+ }
+
+ override fun onNext(streamResponse: StreamResponse) {
+ if (streamResponse is StreamResponse.Message) {
+ messages.add(streamResponse)
+ } else {
+ result = streamResponse as StreamResponse.Result
+ }
+ }
+
+ override fun onError(t: Throwable?) {
+ throwable = t
+ }
+
+ override fun onComplete() {
+ isComplete = true
+ }
+ }
+
+ @Test
+ fun genStream_withPublisher_receivesMessagesAndFinalResult() = runBlocking {
+ val input = mapOf("data" to "Why is the sky blue")
+ val function = functions.getHttpsCallable("genStream")
+ val subscriber = StreamSubscriber()
+
+ function.stream(input).subscribe(subscriber)
+
+ while (!subscriber.isComplete) {
+ delay(100)
+ }
+ assertThat(subscriber.messages.map { it.message.data.toString() })
+ .containsExactly("hello", "world", "this", "is", "cool")
+ assertThat(subscriber.result).isNotNull()
+ assertThat(subscriber.result!!.result.data.toString()).isEqualTo("hello world this is cool")
+ assertThat(subscriber.throwable).isNull()
+ assertThat(subscriber.isComplete).isTrue()
+ }
+
+ @Test
+ fun genStream_withFlow_receivesMessagesAndFinalResult() = runBlocking {
+ val input = mapOf("data" to "Why is the sky blue")
+ val function = functions.getHttpsCallable("genStream")
+ var isComplete = false
+ var throwable: Throwable? = null
+ val messages = mutableListOf()
+ var result: StreamResponse.Result? = null
+
+ val flow = function.stream(input).asFlow()
+ try {
+ withTimeout(1000) {
+ flow.collect { response ->
+ if (response is StreamResponse.Message) {
+ messages.add(response)
+ } else {
+ result = response as StreamResponse.Result
+ }
+ }
+ }
+ isComplete = true
+ } catch (e: Throwable) {
+ throwable = e
+ }
+
+ assertThat(messages.map { it.message.data.toString() })
+ .containsExactly("hello", "world", "this", "is", "cool")
+ assertThat(result).isNotNull()
+ assertThat(result!!.result.data.toString()).isEqualTo("hello world this is cool")
+ assertThat(throwable).isNull()
+ assertThat(isComplete).isTrue()
+ }
+
+ @Test
+ fun genStreamError_receivesError() = runBlocking {
+ val input = mapOf("data" to "test error")
+ val function =
+ functions.getHttpsCallable("genStreamError").withTimeout(2000, TimeUnit.MILLISECONDS)
+ val subscriber = StreamSubscriber()
+
+ function.stream(input).subscribe(subscriber)
+
+ withTimeout(2000) {
+ while (subscriber.throwable == null) {
+ delay(100)
+ }
+ }
+
+ assertThat(subscriber.throwable).isNotNull()
+ assertThat(subscriber.throwable).isInstanceOf(FirebaseFunctionsException::class.java)
+ }
+
+ @Test
+ fun genStreamWeather_receivesWeatherForecasts() = runBlocking {
+ val inputData = listOf(mapOf("name" to "Toronto"), mapOf("name" to "London"))
+ val input = mapOf("data" to inputData)
+
+ val function = functions.getHttpsCallable("genStreamWeather")
+ val subscriber = StreamSubscriber()
+
+ function.stream(input).subscribe(subscriber)
+
+ while (!subscriber.isComplete) {
+ delay(100)
+ }
+
+ assertThat(subscriber.messages.map { it.message.data.toString() })
+ .containsExactly(
+ "{temperature=25, location={name=Toronto}, conditions=snowy}",
+ "{temperature=50, location={name=London}, conditions=rainy}"
+ )
+ assertThat(subscriber.result).isNotNull()
+ assertThat(subscriber.result!!.result.data.toString()).contains("forecasts")
+ assertThat(subscriber.throwable).isNull()
+ assertThat(subscriber.isComplete).isTrue()
+ }
+
+ @Test
+ fun genStreamEmpty_receivesNoMessages() = runBlocking {
+ val function = functions.getHttpsCallable("genStreamEmpty")
+ val subscriber = StreamSubscriber()
+
+ function.stream(mapOf("data" to "test")).subscribe(subscriber)
+
+ withTimeout(2000) { delay(500) }
+ assertThat(subscriber.messages).isEmpty()
+ assertThat(subscriber.result).isNull()
+ }
+
+ @Test
+ fun genStreamResultOnly_receivesOnlyResult() = runBlocking {
+ val function = functions.getHttpsCallable("genStreamResultOnly")
+ val subscriber = StreamSubscriber()
+
+ function.stream(mapOf("data" to "test")).subscribe(subscriber)
+
+ while (!subscriber.isComplete) {
+ delay(100)
+ }
+ assertThat(subscriber.messages).isEmpty()
+ assertThat(subscriber.result).isNotNull()
+ assertThat(subscriber.result!!.result.data.toString()).isEqualTo("Only a result")
+ }
+
+ @Test
+ fun genStreamLargeData_receivesMultipleChunks() = runBlocking {
+ val function = functions.getHttpsCallable("genStreamLargeData")
+ val subscriber = StreamSubscriber()
+
+ function.stream(mapOf("data" to "test large data")).subscribe(subscriber)
+
+ while (!subscriber.isComplete) {
+ delay(100)
+ }
+ assertThat(subscriber.messages).isNotEmpty()
+ assertThat(subscriber.messages.size).isEqualTo(10)
+ val receivedString =
+ subscriber.messages.joinToString(separator = "") { it.message.data.toString() }
+ val expectedString = "A".repeat(10000)
+ assertThat(receivedString.length).isEqualTo(10000)
+ assertThat(receivedString).isEqualTo(expectedString)
+ assertThat(subscriber.result).isNotNull()
+ assertThat(subscriber.result!!.result.data.toString()).isEqualTo("Stream Completed")
+ }
+}
diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt
index 824670c4346..8839763c4a3 100644
--- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt
+++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt
@@ -45,6 +45,7 @@ import okhttp3.RequestBody
import okhttp3.Response
import org.json.JSONException
import org.json.JSONObject
+import org.reactivestreams.Publisher
/** FirebaseFunctions lets you call Cloud Functions for Firebase. */
public class FirebaseFunctions
@@ -311,6 +312,21 @@ internal constructor(
return tcs.task
}
+ internal fun stream(
+ name: String,
+ data: Any?,
+ options: HttpsCallOptions
+ ): Publisher = stream(getURL(name), data, options)
+
+ internal fun stream(url: URL, data: Any?, options: HttpsCallOptions): Publisher {
+ val task =
+ providerInstalled.task.continueWithTask(executor) {
+ contextProvider.getContext(options.limitedUseAppCheckTokens)
+ }
+
+ return PublisherStream(url, data, options, client, this.serializer, task, executor)
+ }
+
public companion object {
/** A task that will be resolved once ProviderInstaller has installed what it needs to. */
private val providerInstalled = TaskCompletionSource()
diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt
index 88db9db4ee4..215722584ba 100644
--- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt
+++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt
@@ -17,6 +17,7 @@ import androidx.annotation.VisibleForTesting
import com.google.android.gms.tasks.Task
import java.net.URL
import java.util.concurrent.TimeUnit
+import org.reactivestreams.Publisher
/** A reference to a particular Callable HTTPS trigger in Cloud Functions. */
public class HttpsCallableReference {
@@ -61,10 +62,8 @@ public class HttpsCallableReference {
*
* * Any primitive type, including null, int, long, float, and boolean.
* * [String]
- * * [List<?>][java.util.List], where the contained objects are also one of these
- * types.
- * * [Map<String, ?>>][java.util.Map], where the values are also one of these
- * types.
+ * * [List>][java.util.List], where the contained objects are also one of these types.
+ * * [Map][java.util.Map], where the values are also one of these types.
* * [org.json.JSONArray]
* * [org.json.JSONObject]
* * [org.json.JSONObject.NULL]
@@ -125,6 +124,55 @@ public class HttpsCallableReference {
}
}
+ /**
+ * Streams data to the specified HTTPS endpoint.
+ *
+ * The data passed into the trigger can be any of the following types:
+ *
+ * * Any primitive type, including null, int, long, float, and boolean.
+ * * [String]
+ * * [List>][java.util.List], where the contained objects are also one of these types.
+ * * [Map][java.util.Map], where the values are also one of these types.
+ * * [org.json.JSONArray]
+ * * [org.json.JSONObject]
+ * * [org.json.JSONObject.NULL]
+ *
+ * If the returned streamResponse fails, the exception will be one of the following types:
+ *
+ * * [java.io.IOException]
+ * - if the HTTPS request failed to connect.
+ * * [FirebaseFunctionsException]
+ * - if the request connected, but the function returned an error.
+ *
+ * The request to the Cloud Functions backend made by this method automatically includes a
+ * Firebase Instance ID token to identify the app instance. If a user is logged in with Firebase
+ * Auth, an auth token for the user will also be automatically included.
+ *
+ * Firebase Instance ID sends data to the Firebase backend periodically to collect information
+ * regarding the app instance. To stop this, see
+ * [com.google.firebase.iid.FirebaseInstanceId.deleteInstanceId]. It will resume with a new
+ * Instance ID the next time you call this method.
+ *
+ * @param data Parameters to pass to the endpoint. Defaults to `null` if not provided.
+ * @return [Publisher] that will emit intermediate data, and the final result, as it is generated
+ * by the function.
+ * @see org.json.JSONArray
+ *
+ * @see org.json.JSONObject
+ *
+ * @see java.io.IOException
+ *
+ * @see FirebaseFunctionsException
+ */
+ @JvmOverloads
+ public fun stream(data: Any? = null): Publisher {
+ return if (name != null) {
+ functionsClient.stream(name, data, options)
+ } else {
+ functionsClient.stream(requireNotNull(url), data, options)
+ }
+ }
+
/**
* Changes the timeout for calls from this instance of Functions. The default is 60 seconds.
*
diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt b/firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt
new file mode 100644
index 00000000000..6fc6a9d657c
--- /dev/null
+++ b/firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt
@@ -0,0 +1,328 @@
+/*
+ * Copyright 2025 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.functions
+
+import com.google.android.gms.tasks.Task
+import java.io.BufferedReader
+import java.io.IOException
+import java.io.InputStream
+import java.io.InputStreamReader
+import java.io.InterruptedIOException
+import java.net.URL
+import java.util.concurrent.ConcurrentLinkedQueue
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicLong
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.MediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody
+import okhttp3.Response
+import org.json.JSONObject
+import org.reactivestreams.Publisher
+import org.reactivestreams.Subscriber
+import org.reactivestreams.Subscription
+
+internal class PublisherStream(
+ private val url: URL,
+ private val data: Any?,
+ private val options: HttpsCallOptions,
+ private val client: OkHttpClient,
+ private val serializer: Serializer,
+ private val contextTask: Task,
+ private val executor: Executor
+) : Publisher {
+
+ private val subscribers = ConcurrentLinkedQueue, AtomicLong>>()
+ private var activeCall: Call? = null
+ @Volatile private var isStreamingStarted = false
+ @Volatile private var isCompleted = false
+ private val messageQueue = ConcurrentLinkedQueue()
+
+ override fun subscribe(subscriber: Subscriber) {
+ synchronized(this) {
+ if (isCompleted) {
+ subscriber.onError(
+ FirebaseFunctionsException(
+ "Cannot subscribe: Streaming has already completed.",
+ FirebaseFunctionsException.Code.CANCELLED,
+ null
+ )
+ )
+ return
+ }
+ subscribers.add(subscriber to AtomicLong(0))
+ }
+
+ subscriber.onSubscribe(
+ object : Subscription {
+ override fun request(n: Long) {
+ if (n <= 0) {
+ subscriber.onError(IllegalArgumentException("Requested messages must be positive."))
+ return
+ }
+
+ synchronized(this@PublisherStream) {
+ if (isCompleted) return
+
+ val subscriberEntry = subscribers.find { it.first == subscriber }
+ subscriberEntry?.second?.addAndGet(n)
+ dispatchMessages()
+ if (!isStreamingStarted) {
+ isStreamingStarted = true
+ startStreaming()
+ }
+ }
+ }
+
+ override fun cancel() {
+ synchronized(this@PublisherStream) {
+ notifyError(
+ FirebaseFunctionsException(
+ "Stream was canceled",
+ FirebaseFunctionsException.Code.CANCELLED,
+ null
+ )
+ )
+ val iterator = subscribers.iterator()
+ while (iterator.hasNext()) {
+ val pair = iterator.next()
+ if (pair.first == subscriber) {
+ iterator.remove()
+ }
+ }
+ if (subscribers.isEmpty()) {
+ cancelStream()
+ }
+ }
+ }
+ }
+ )
+ }
+
+ private fun startStreaming() {
+ contextTask.addOnCompleteListener(executor) { contextTask ->
+ if (!contextTask.isSuccessful) {
+ notifyError(
+ FirebaseFunctionsException(
+ "Error retrieving context",
+ FirebaseFunctionsException.Code.INTERNAL,
+ null,
+ contextTask.exception
+ )
+ )
+ return@addOnCompleteListener
+ }
+
+ val context = contextTask.result
+ val configuredClient = options.apply(client)
+ val requestBody =
+ RequestBody.create(
+ MediaType.parse("application/json"),
+ JSONObject(mapOf("data" to serializer.encode(data))).toString()
+ )
+ val requestBuilder =
+ Request.Builder().url(url).post(requestBody).header("Accept", "text/event-stream")
+ context?.authToken?.let { requestBuilder.header("Authorization", "Bearer $it") }
+ context?.instanceIdToken?.let { requestBuilder.header("Firebase-Instance-ID-Token", it) }
+ context?.appCheckToken?.let { requestBuilder.header("X-Firebase-AppCheck", it) }
+ val request = requestBuilder.build()
+ val call = configuredClient.newCall(request)
+ activeCall = call
+
+ call.enqueue(
+ object : Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ val code: FirebaseFunctionsException.Code =
+ if (e is InterruptedIOException) {
+ FirebaseFunctionsException.Code.DEADLINE_EXCEEDED
+ } else {
+ FirebaseFunctionsException.Code.INTERNAL
+ }
+ notifyError(FirebaseFunctionsException(code.name, code, null, e))
+ }
+
+ override fun onResponse(call: Call, response: Response) {
+ validateResponse(response)
+ val bodyStream = response.body()?.byteStream()
+ if (bodyStream != null) {
+ processSSEStream(bodyStream)
+ } else {
+ notifyError(
+ FirebaseFunctionsException(
+ "Response body is null",
+ FirebaseFunctionsException.Code.INTERNAL,
+ null
+ )
+ )
+ }
+ }
+ }
+ )
+ }
+ }
+
+ private fun cancelStream() {
+ activeCall?.cancel()
+ notifyError(
+ FirebaseFunctionsException(
+ "Stream was canceled",
+ FirebaseFunctionsException.Code.CANCELLED,
+ null
+ )
+ )
+ }
+
+ private fun processSSEStream(inputStream: InputStream) {
+ BufferedReader(InputStreamReader(inputStream)).use { reader ->
+ try {
+ val eventBuffer = StringBuilder()
+ reader.lineSequence().forEach { line ->
+ if (line.isBlank()) {
+ processEvent(eventBuffer.toString())
+ eventBuffer.clear()
+ } else {
+ val dataChunk =
+ when {
+ line.startsWith("data:") -> line.removePrefix("data:")
+ line.startsWith("result:") -> line.removePrefix("result:")
+ else -> return@forEach
+ }
+ eventBuffer.append(dataChunk.trim()).append("\n")
+ }
+ }
+ } catch (e: Exception) {
+ notifyError(
+ FirebaseFunctionsException(
+ e.message ?: "Error reading stream",
+ FirebaseFunctionsException.Code.INTERNAL,
+ e
+ )
+ )
+ }
+ }
+ }
+
+ private fun processEvent(dataChunk: String) {
+ try {
+ val json = JSONObject(dataChunk)
+ when {
+ json.has("message") -> {
+ serializer.decode(json.opt("message"))?.let {
+ messageQueue.add(StreamResponse.Message(message = HttpsCallableResult(it)))
+ }
+ dispatchMessages()
+ }
+ json.has("error") -> {
+ serializer.decode(json.opt("error"))?.let {
+ notifyError(
+ FirebaseFunctionsException(
+ it.toString(),
+ FirebaseFunctionsException.Code.INTERNAL,
+ it
+ )
+ )
+ }
+ }
+ json.has("result") -> {
+ serializer.decode(json.opt("result"))?.let {
+ messageQueue.add(StreamResponse.Result(result = HttpsCallableResult(it)))
+ dispatchMessages()
+ notifyComplete()
+ }
+ }
+ }
+ } catch (e: Throwable) {
+ notifyError(
+ FirebaseFunctionsException(
+ "Invalid JSON: $dataChunk",
+ FirebaseFunctionsException.Code.INTERNAL,
+ e
+ )
+ )
+ }
+ }
+
+ private fun dispatchMessages() {
+ synchronized(this) {
+ val iterator = subscribers.iterator()
+ while (iterator.hasNext()) {
+ val (subscriber, requestedCount) = iterator.next()
+ while (requestedCount.get() > 0 && messageQueue.isNotEmpty()) {
+ subscriber.onNext(messageQueue.poll())
+ requestedCount.decrementAndGet()
+ }
+ }
+ }
+ }
+
+ private fun notifyError(e: Throwable) {
+ if (!isCompleted) {
+ isCompleted = true
+ subscribers.forEach { (subscriber, _) ->
+ try {
+ subscriber.onError(e)
+ } catch (ignored: Exception) {}
+ }
+ subscribers.clear()
+ messageQueue.clear()
+ }
+ }
+
+ private fun notifyComplete() {
+ if (!isCompleted) {
+ isCompleted = true
+ subscribers.forEach { (subscriber, _) -> subscriber.onComplete() }
+ subscribers.clear()
+ messageQueue.clear()
+ }
+ }
+
+ private fun validateResponse(response: Response) {
+ if (response.isSuccessful) return
+
+ val htmlContentType = "text/html; charset=utf-8"
+ val errorMessage: String
+ if (response.code() == 404 && response.header("Content-Type") == htmlContentType) {
+ errorMessage = """URL not found. Raw response: ${response.body()?.string()}""".trimMargin()
+ throw FirebaseFunctionsException(
+ errorMessage,
+ FirebaseFunctionsException.Code.fromHttpStatus(response.code()),
+ null
+ )
+ }
+
+ val text = response.body()?.string() ?: ""
+ val error: Any?
+ try {
+ val json = JSONObject(text)
+ error = serializer.decode(json.opt("error"))
+ } catch (e: Throwable) {
+ throw FirebaseFunctionsException(
+ "${e.message} Unexpected Response:\n$text ",
+ FirebaseFunctionsException.Code.INTERNAL,
+ e
+ )
+ }
+ throw FirebaseFunctionsException(
+ error.toString(),
+ FirebaseFunctionsException.Code.INTERNAL,
+ error
+ )
+ }
+}
diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/StreamResponse.kt b/firebase-functions/src/main/java/com/google/firebase/functions/StreamResponse.kt
new file mode 100644
index 00000000000..123f804614d
--- /dev/null
+++ b/firebase-functions/src/main/java/com/google/firebase/functions/StreamResponse.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2025 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.functions
+
+/**
+ * Represents a response from a Server-Sent Event (SSE) stream.
+ *
+ * The SSE stream consists of two types of responses:
+ * - [Message]: Represents an intermediate event pushed from the server.
+ * - [Result]: Represents the final response that signifies the stream has ended.
+ */
+public abstract class StreamResponse private constructor() {
+
+ /**
+ * An event message received during the stream.
+ *
+ * Messages are intermediate data chunks sent by the server while processing a request.
+ *
+ * Example SSE format:
+ * ```json
+ * data: { "message": { "chunk": "foo" } }
+ * ```
+ *
+ * @property message the intermediate data received from the server.
+ */
+ public class Message(public val message: HttpsCallableResult) : StreamResponse()
+
+ /**
+ * The final result of the computation, marking the end of the stream.
+ *
+ * Unlike [Message], which represents intermediate data chunks, [Result] contains the complete
+ * computation output. If clients only care about the final result, they can process this type
+ * alone and ignore intermediate messages.
+ *
+ * Example SSE format:
+ * ```json
+ * data: { "result": { "text": "foo bar" } }
+ * ```
+ *
+ * @property result the final computed result received from the server.
+ */
+ public class Result(public val result: HttpsCallableResult) : StreamResponse()
+}
From c0fba25e96b757db10f3e0c1c2a3d3aee9660091 Mon Sep 17 00:00:00 2001
From: Rodrigo Lazo
Date: Mon, 10 Mar 2025 17:32:51 -0400
Subject: [PATCH 086/146] [Functions] Send the placeholder appcheck token in
case of error (#6750)
tracking b/399116207
---------
Co-authored-by: Daymon <17409137+daymxn@users.noreply.github.com>
---
firebase-functions/CHANGELOG.md | 1 +
.../functions/FirebaseContextProviderTest.java | 10 ++++++----
.../firebase/functions/FirebaseContextProvider.kt | 4 +---
3 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/firebase-functions/CHANGELOG.md b/firebase-functions/CHANGELOG.md
index 26bb8de7306..863de38db50 100644
--- a/firebase-functions/CHANGELOG.md
+++ b/firebase-functions/CHANGELOG.md
@@ -1,4 +1,5 @@
# Unreleased
+* [fixed] Fixed an issue that prevented the App Check token from being handled correctly in case of error.
# 21.1.1
* [fixed] Resolve Kotlin migration visibility issues
diff --git a/firebase-functions/src/androidTest/java/com/google/firebase/functions/FirebaseContextProviderTest.java b/firebase-functions/src/androidTest/java/com/google/firebase/functions/FirebaseContextProviderTest.java
index 1126ae55fbb..384230867d9 100644
--- a/firebase-functions/src/androidTest/java/com/google/firebase/functions/FirebaseContextProviderTest.java
+++ b/firebase-functions/src/androidTest/java/com/google/firebase/functions/FirebaseContextProviderTest.java
@@ -117,7 +117,7 @@ public void getContext_whenOnlyAuthIsAvailableAndNotSignedIn_shouldContainOnlyIi
}
@Test
- public void getContext_whenOnlyAppCheckIsAvailableAndHasError_shouldContainOnlyIid()
+ public void getContext_whenOnlyAppCheckIsAvailableAndHasError()
throws ExecutionException, InterruptedException {
FirebaseContextProvider contextProvider =
new FirebaseContextProvider(
@@ -129,11 +129,12 @@ public void getContext_whenOnlyAppCheckIsAvailableAndHasError_shouldContainOnlyI
HttpsCallableContext context = Tasks.await(contextProvider.getContext(false));
assertThat(context.getAuthToken()).isNull();
assertThat(context.getInstanceIdToken()).isEqualTo(IID_TOKEN);
- assertThat(context.getAppCheckToken()).isNull();
+ // AppCheck token needs to be send in all circumstances.
+ assertThat(context.getAppCheckToken()).isEqualTo(APP_CHECK_TOKEN);
}
@Test
- public void getContext_facLimitedUse_whenOnlyAppCheckIsAvailableAndHasError_shouldContainOnlyIid()
+ public void getContext_facLimitedUse_whenOnlyAppCheckIsAvailableAndHasError()
throws ExecutionException, InterruptedException {
FirebaseContextProvider contextProvider =
new FirebaseContextProvider(
@@ -145,7 +146,8 @@ public void getContext_facLimitedUse_whenOnlyAppCheckIsAvailableAndHasError_shou
HttpsCallableContext context = Tasks.await(contextProvider.getContext(true));
assertThat(context.getAuthToken()).isNull();
assertThat(context.getInstanceIdToken()).isEqualTo(IID_TOKEN);
- assertThat(context.getAppCheckToken()).isNull();
+ // AppCheck token needs to be sent in all circumstances.
+ assertThat(context.getAppCheckToken()).isEqualTo(APP_CHECK_LIMITED_USE_TOKEN);
}
@Test
diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt
index 7ab1f74bf5d..96f18eb2c05 100644
--- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt
+++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt
@@ -88,11 +88,9 @@ constructor(
if (getLimitedUseAppCheckToken) appCheck.limitedUseToken else appCheck.getToken(false)
return tokenTask.onSuccessTask(executor) { result: AppCheckTokenResult ->
if (result.error != null) {
- // If there was an error getting the App Check token, do NOT send the placeholder
- // token. Only valid App Check tokens should be sent to the functions backend.
Log.w(TAG, "Error getting App Check token. Error: " + result.error)
- return@onSuccessTask Tasks.forResult(null)
}
+ // Send valid token (success) or placeholder (failure).
Tasks.forResult(result.token)
}
}
From 04b1e21558d0ebc4bae7320c3df52f2f477a2d69 Mon Sep 17 00:00:00 2001
From: Matthew Robertson
Date: Tue, 11 Mar 2025 07:16:08 -0600
Subject: [PATCH 087/146] Read version control info from Android resource
(#6754)
---
firebase-crashlytics/CHANGELOG.md | 1 +
.../internal/common/CommonUtils.java | 11 +++++++
.../common/CrashlyticsController.java | 31 ++++++++++++-------
3 files changed, 31 insertions(+), 12 deletions(-)
diff --git a/firebase-crashlytics/CHANGELOG.md b/firebase-crashlytics/CHANGELOG.md
index a6e5e087b60..c0e6ddce86b 100644
--- a/firebase-crashlytics/CHANGELOG.md
+++ b/firebase-crashlytics/CHANGELOG.md
@@ -1,4 +1,5 @@
# Unreleased
+* [changed] Internal changes to read version control info more efficiently [6754]
* [fixed] Fixed NoSuchMethodError when getting process info on Android 13 [#6720]
# 19.4.1
diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java
index b29863f66c5..a116cf55542 100644
--- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java
+++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java
@@ -69,6 +69,8 @@ public class CommonUtils {
"com.google.firebase.crashlytics.build_ids_arch";
static final String BUILD_IDS_BUILD_ID_RESOURCE_NAME =
"com.google.firebase.crashlytics.build_ids_build_id";
+ static final String VERSION_CONTROL_INFO_RESOURCE_NAME =
+ "com.google.firebase.crashlytics.version_control_info";
// TODO: Maybe move this method into a more appropriate class.
public static SharedPreferences getSharedPrefs(Context context) {
@@ -525,6 +527,15 @@ public static List getBuildIdInfo(Context context) {
return buildIdInfoList;
}
+ @Nullable
+ public static String getVersionControlInfo(Context context) {
+ int id = getResourcesIdentifier(context, VERSION_CONTROL_INFO_RESOURCE_NAME, "string");
+ if (id == 0) {
+ return null;
+ }
+ return context.getResources().getString(id);
+ }
+
public static void closeQuietly(Closeable closeable) {
if (closeable != null) {
try {
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 b55a26678d4..da28e8708db 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
@@ -48,6 +48,7 @@
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@@ -77,6 +78,8 @@ class CrashlyticsController {
private static final String VERSION_CONTROL_INFO_FILE = "version-control-info.textproto";
private static final String META_INF_FOLDER = "META-INF/";
+ private static final Charset UTF_8 = Charset.forName("UTF-8");
+
private final Context context;
private final DataCollectionArbiter dataCollectionArbiter;
private final CrashlyticsFileMarker crashMarker;
@@ -628,13 +631,23 @@ void saveVersionControlInfo() {
}
String getVersionControlInfo() throws IOException {
- InputStream is = getResourceAsStream(META_INF_FOLDER + VERSION_CONTROL_INFO_FILE);
- if (is == null) {
- return null;
+ // Attempt to read from an Android string resource
+ String versionControlInfo = CommonUtils.getVersionControlInfo(context);
+ if (versionControlInfo != null) {
+ Logger.getLogger().d("Read version control info from string resource");
+ return Base64.encodeToString(versionControlInfo.getBytes(UTF_8), 0);
+ }
+
+ // Fallback to reading the file
+ try (InputStream is = getResourceAsStream(META_INF_FOLDER + VERSION_CONTROL_INFO_FILE)) {
+ if (is != null) {
+ Logger.getLogger().d("Read version control info from file");
+ return Base64.encodeToString(readResource(is), 0);
+ }
}
- Logger.getLogger().d("Read version control info");
- return Base64.encodeToString(readResource(is), 0);
+ Logger.getLogger().i("No version control information found");
+ return null;
}
private InputStream getResourceAsStream(String resource) {
@@ -644,13 +657,7 @@ private InputStream getResourceAsStream(String resource) {
return null;
}
- InputStream is = classLoader.getResourceAsStream(resource);
- if (is == null) {
- Logger.getLogger().i("No version control information found");
- return null;
- }
-
- return is;
+ return classLoader.getResourceAsStream(resource);
}
private static byte[] readResource(InputStream is) throws IOException {
From c1ffa6f3b51e7545c9955d7e3dc335abbb848b1e Mon Sep 17 00:00:00 2001
From: Matthew Robertson
Date: Tue, 11 Mar 2025 07:19:43 -0600
Subject: [PATCH 088/146] Use Dagger for dependency injection in Sessions
(#6745)
---
firebase-sessions/CHANGELOG.md | 1 +
.../firebase-sessions.gradle.kts | 10 +-
.../firebase/sessions/EventGDTLogger.kt | 14 +--
.../firebase/sessions/FirebaseSessions.kt | 10 +-
.../sessions/FirebaseSessionsComponent.kt | 86 +++++++++++++++++
.../sessions/FirebaseSessionsRegistrar.kt | 94 +++++--------------
.../firebase/sessions/SessionDatastore.kt | 27 +++---
.../sessions/SessionFirelogPublisher.kt | 12 ++-
.../firebase/sessions/SessionGenerator.kt | 8 +-
.../sessions/SessionLifecycleService.kt | 1 -
.../sessions/SessionLifecycleServiceBinder.kt | 10 +-
.../sessions/settings/SessionsSettings.kt | 13 ++-
.../testing/FirebaseSessionsFakeComponent.kt | 46 +++++++++
.../testing/FirebaseSessionsFakeRegistrar.kt | 30 +++---
14 files changed, 238 insertions(+), 124 deletions(-)
create mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt
create mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt
diff --git a/firebase-sessions/CHANGELOG.md b/firebase-sessions/CHANGELOG.md
index 2473b64a1cf..d5293913dc9 100644
--- a/firebase-sessions/CHANGELOG.md
+++ b/firebase-sessions/CHANGELOG.md
@@ -1,5 +1,6 @@
# Unreleased
+* [changed] Use Dagger for dependency injection
* [changed] Updated datastore dependency to `1.1.3` to
fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8).
diff --git a/firebase-sessions/firebase-sessions.gradle.kts b/firebase-sessions/firebase-sessions.gradle.kts
index 0a09740bd77..b136a281660 100644
--- a/firebase-sessions/firebase-sessions.gradle.kts
+++ b/firebase-sessions/firebase-sessions.gradle.kts
@@ -18,6 +18,7 @@
plugins {
id("firebase-library")
+ id("firebase-vendor")
id("kotlin-android")
id("kotlin-kapt")
}
@@ -67,12 +68,18 @@ dependencies {
exclude(group = "com.google.firebase", module = "firebase-common")
exclude(group = "com.google.firebase", module = "firebase-components")
}
- implementation("com.google.android.datatransport:transport-api:3.2.0")
+
api("com.google.firebase:firebase-annotations:16.2.0")
api("com.google.firebase:firebase-encoders:17.0.0")
api("com.google.firebase:firebase-encoders-json:18.0.1")
+
+ implementation("com.google.android.datatransport:transport-api:3.2.0")
+ implementation(libs.javax.inject)
implementation(libs.androidx.annotation)
implementation(libs.androidx.datastore.preferences)
+
+ vendor(libs.dagger.dagger) { exclude(group = "javax.inject", module = "javax.inject") }
+
compileOnly(libs.errorprone.annotations)
runtimeOnly("com.google.firebase:firebase-installations:18.0.0") {
@@ -85,6 +92,7 @@ dependencies {
}
kapt(project(":encoders:firebase-encoders-processor"))
+ kapt(libs.dagger.compiler)
testImplementation(project(":integ-testing")) {
exclude(group = "com.google.firebase", module = "firebase-common")
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/EventGDTLogger.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/EventGDTLogger.kt
index a11b20a7d5c..496cc70d36d 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/EventGDTLogger.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/EventGDTLogger.kt
@@ -21,6 +21,8 @@ import com.google.android.datatransport.Encoding
import com.google.android.datatransport.Event
import com.google.android.datatransport.TransportFactory
import com.google.firebase.inject.Provider
+import javax.inject.Inject
+import javax.inject.Singleton
/**
* The [EventGDTLoggerInterface] is for testing purposes so that we can mock EventGDTLogger in other
@@ -38,19 +40,17 @@ internal fun interface EventGDTLoggerInterface {
*
* @hide
*/
-internal class EventGDTLogger(private val transportFactoryProvider: Provider) :
+@Singleton
+internal class EventGDTLogger
+@Inject
+constructor(private val transportFactoryProvider: Provider) :
EventGDTLoggerInterface {
// Logs a [SessionEvent] to FireLog
override fun log(sessionEvent: SessionEvent) {
transportFactoryProvider
.get()
- .getTransport(
- AQS_LOG_SOURCE,
- SessionEvent::class.java,
- Encoding.of("json"),
- this::encode,
- )
+ .getTransport(AQS_LOG_SOURCE, SessionEvent::class.java, Encoding.of("json"), this::encode)
.send(Event.ofData(sessionEvent))
}
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt
index 0dec3b98150..18b9961724b 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt
@@ -20,18 +20,24 @@ import android.app.Application
import android.util.Log
import com.google.firebase.Firebase
import com.google.firebase.FirebaseApp
+import com.google.firebase.annotations.concurrent.Background
import com.google.firebase.app
import com.google.firebase.sessions.api.FirebaseSessionsDependencies
import com.google.firebase.sessions.settings.SessionsSettings
+import javax.inject.Inject
+import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
/** Responsible for initializing AQS */
-internal class FirebaseSessions(
+@Singleton
+internal class FirebaseSessions
+@Inject
+constructor(
private val firebaseApp: FirebaseApp,
private val settings: SessionsSettings,
- backgroundDispatcher: CoroutineContext,
+ @Background backgroundDispatcher: CoroutineContext,
lifecycleServiceBinder: SessionLifecycleServiceBinder,
) {
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt
new file mode 100644
index 00000000000..aa60f3f41df
--- /dev/null
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2025 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.sessions
+
+import android.content.Context
+import com.google.android.datatransport.TransportFactory
+import com.google.firebase.FirebaseApp
+import com.google.firebase.annotations.concurrent.Background
+import com.google.firebase.annotations.concurrent.Blocking
+import com.google.firebase.inject.Provider
+import com.google.firebase.installations.FirebaseInstallationsApi
+import com.google.firebase.sessions.settings.SessionsSettings
+import dagger.Binds
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+import kotlin.coroutines.CoroutineContext
+
+/** Dagger component to provide [FirebaseSessions] and its dependencies. */
+@Singleton
+@Component(modules = [FirebaseSessionsComponent.MainModule::class])
+internal interface FirebaseSessionsComponent {
+ val firebaseSessions: FirebaseSessions
+
+ val sessionDatastore: SessionDatastore
+ val sessionFirelogPublisher: SessionFirelogPublisher
+ val sessionGenerator: SessionGenerator
+ val sessionsSettings: SessionsSettings
+
+ @Component.Builder
+ interface Builder {
+ @BindsInstance fun appContext(appContext: Context): Builder
+
+ @BindsInstance
+ fun backgroundDispatcher(@Background backgroundDispatcher: CoroutineContext): Builder
+
+ @BindsInstance fun blockingDispatcher(@Blocking blockingDispatcher: CoroutineContext): Builder
+
+ @BindsInstance fun firebaseApp(firebaseApp: FirebaseApp): Builder
+
+ @BindsInstance
+ fun firebaseInstallationsApi(firebaseInstallationsApi: FirebaseInstallationsApi): Builder
+
+ @BindsInstance
+ fun transportFactoryProvider(transportFactoryProvider: Provider): Builder
+
+ fun build(): FirebaseSessionsComponent
+ }
+
+ @Module
+ interface MainModule {
+ @Binds @Singleton fun eventGDTLoggerInterface(impl: EventGDTLogger): EventGDTLoggerInterface
+
+ @Binds @Singleton fun sessionDatastore(impl: SessionDatastoreImpl): SessionDatastore
+
+ @Binds
+ @Singleton
+ fun sessionFirelogPublisher(impl: SessionFirelogPublisherImpl): SessionFirelogPublisher
+
+ @Binds
+ @Singleton
+ fun sessionLifecycleServiceBinder(
+ impl: SessionLifecycleServiceBinderImpl
+ ): SessionLifecycleServiceBinder
+
+ companion object {
+ @Provides @Singleton fun sessionGenerator() = SessionGenerator(timeProvider = WallClock)
+ }
+ }
+}
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt
index caad2de6ff8..1043ad74800 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt
@@ -16,6 +16,7 @@
package com.google.firebase.sessions
+import android.content.Context
import androidx.annotation.Keep
import com.google.android.datatransport.TransportFactory
import com.google.firebase.FirebaseApp
@@ -28,7 +29,6 @@ import com.google.firebase.components.Qualified.qualified
import com.google.firebase.components.Qualified.unqualified
import com.google.firebase.installations.FirebaseInstallationsApi
import com.google.firebase.platforminfo.LibraryVersionComponent
-import com.google.firebase.sessions.settings.SessionsSettings
import kotlinx.coroutines.CoroutineDispatcher
/**
@@ -42,87 +42,41 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar {
listOf(
Component.builder(FirebaseSessions::class.java)
.name(LIBRARY_NAME)
- .add(Dependency.required(firebaseApp))
- .add(Dependency.required(sessionsSettings))
- .add(Dependency.required(backgroundDispatcher))
- .add(Dependency.required(sessionLifecycleServiceBinder))
- .factory { container ->
- FirebaseSessions(
- container[firebaseApp],
- container[sessionsSettings],
- container[backgroundDispatcher],
- container[sessionLifecycleServiceBinder],
- )
- }
+ .add(Dependency.required(firebaseSessionsComponent))
+ .factory { container -> container[firebaseSessionsComponent].firebaseSessions }
.eagerInDefaultApp()
.build(),
- Component.builder(SessionGenerator::class.java)
- .name("session-generator")
- .factory { SessionGenerator(timeProvider = WallClock) }
- .build(),
- Component.builder(SessionFirelogPublisher::class.java)
- .name("session-publisher")
- .add(Dependency.required(firebaseApp))
- .add(Dependency.required(firebaseInstallationsApi))
- .add(Dependency.required(sessionsSettings))
- .add(Dependency.requiredProvider(transportFactory))
+ Component.builder(FirebaseSessionsComponent::class.java)
+ .name("fire-sessions-component")
+ .add(Dependency.required(appContext))
.add(Dependency.required(backgroundDispatcher))
- .factory { container ->
- SessionFirelogPublisherImpl(
- container[firebaseApp],
- container[firebaseInstallationsApi],
- container[sessionsSettings],
- EventGDTLogger(container.getProvider(transportFactory)),
- container[backgroundDispatcher],
- )
- }
- .build(),
- Component.builder(SessionsSettings::class.java)
- .name("sessions-settings")
- .add(Dependency.required(firebaseApp))
.add(Dependency.required(blockingDispatcher))
- .add(Dependency.required(backgroundDispatcher))
- .add(Dependency.required(firebaseInstallationsApi))
- .factory { container ->
- SessionsSettings(
- container[firebaseApp],
- container[blockingDispatcher],
- container[backgroundDispatcher],
- container[firebaseInstallationsApi],
- )
- }
- .build(),
- Component.builder(SessionDatastore::class.java)
- .name("sessions-datastore")
.add(Dependency.required(firebaseApp))
- .add(Dependency.required(backgroundDispatcher))
+ .add(Dependency.required(firebaseInstallationsApi))
+ .add(Dependency.requiredProvider(transportFactory))
.factory { container ->
- SessionDatastoreImpl(
- container[firebaseApp].applicationContext,
- container[backgroundDispatcher],
- )
+ DaggerFirebaseSessionsComponent.builder()
+ .appContext(container[appContext])
+ .backgroundDispatcher(container[backgroundDispatcher])
+ .blockingDispatcher(container[blockingDispatcher])
+ .firebaseApp(container[firebaseApp])
+ .firebaseInstallationsApi(container[firebaseInstallationsApi])
+ .transportFactoryProvider(container.getProvider(transportFactory))
+ .build()
}
.build(),
- Component.builder(SessionLifecycleServiceBinder::class.java)
- .name("sessions-service-binder")
- .add(Dependency.required(firebaseApp))
- .factory { container -> SessionLifecycleServiceBinderImpl(container[firebaseApp]) }
- .build(),
LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME),
)
private companion object {
- private const val LIBRARY_NAME = "fire-sessions"
+ const val LIBRARY_NAME = "fire-sessions"
- private val firebaseApp = unqualified(FirebaseApp::class.java)
- private val firebaseInstallationsApi = unqualified(FirebaseInstallationsApi::class.java)
- private val backgroundDispatcher =
- qualified(Background::class.java, CoroutineDispatcher::class.java)
- private val blockingDispatcher =
- qualified(Blocking::class.java, CoroutineDispatcher::class.java)
- private val transportFactory = unqualified(TransportFactory::class.java)
- private val sessionsSettings = unqualified(SessionsSettings::class.java)
- private val sessionLifecycleServiceBinder =
- unqualified(SessionLifecycleServiceBinder::class.java)
+ val appContext = unqualified(Context::class.java)
+ val firebaseApp = unqualified(FirebaseApp::class.java)
+ val firebaseInstallationsApi = unqualified(FirebaseInstallationsApi::class.java)
+ val backgroundDispatcher = qualified(Background::class.java, CoroutineDispatcher::class.java)
+ val blockingDispatcher = qualified(Blocking::class.java, CoroutineDispatcher::class.java)
+ val transportFactory = unqualified(TransportFactory::class.java)
+ val firebaseSessionsComponent = unqualified(FirebaseSessionsComponent::class.java)
}
}
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt
index 736761617fd..a2d46a48891 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt
@@ -26,10 +26,13 @@ import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.google.firebase.Firebase
+import com.google.firebase.annotations.concurrent.Background
import com.google.firebase.app
import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName
import java.io.IOException
import java.util.concurrent.atomic.AtomicReference
+import javax.inject.Inject
+import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -53,13 +56,16 @@ internal interface SessionDatastore {
companion object {
val instance: SessionDatastore
- get() = Firebase.app[SessionDatastore::class.java]
+ get() = Firebase.app[FirebaseSessionsComponent::class.java].sessionDatastore
}
}
-internal class SessionDatastoreImpl(
- private val context: Context,
- private val backgroundDispatcher: CoroutineContext,
+@Singleton
+internal class SessionDatastoreImpl
+@Inject
+constructor(
+ private val appContext: Context,
+ @Background private val backgroundDispatcher: CoroutineContext,
) : SessionDatastore {
/** Most recent session from datastore is updated asynchronously whenever it changes */
@@ -70,7 +76,7 @@ internal class SessionDatastoreImpl(
}
private val firebaseSessionDataFlow: Flow =
- context.dataStore.data
+ appContext.dataStore.data
.catch { exception ->
Log.e(TAG, "Error reading stored session data.", exception)
emit(emptyPreferences())
@@ -86,14 +92,11 @@ internal class SessionDatastoreImpl(
override fun updateSessionId(sessionId: String) {
CoroutineScope(backgroundDispatcher).launch {
try {
- context.dataStore.edit { preferences ->
+ appContext.dataStore.edit { preferences ->
preferences[FirebaseSessionDataKeys.SESSION_ID] = sessionId
}
} catch (e: IOException) {
- Log.w(
- TAG,
- "Failed to update session Id: $e",
- )
+ Log.w(TAG, "Failed to update session Id: $e")
}
}
}
@@ -101,9 +104,7 @@ internal class SessionDatastoreImpl(
override fun getCurrentSessionId() = currentSessionFromDatastore.get()?.sessionId
private fun mapSessionsData(preferences: Preferences): FirebaseSessionsData =
- FirebaseSessionsData(
- preferences[FirebaseSessionDataKeys.SESSION_ID],
- )
+ FirebaseSessionsData(preferences[FirebaseSessionDataKeys.SESSION_ID])
private companion object {
private const val TAG = "FirebaseSessionsRepo"
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt
index d63d49e3fe5..6e4b6153f8d 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt
@@ -19,10 +19,13 @@ package com.google.firebase.sessions
import android.util.Log
import com.google.firebase.Firebase
import com.google.firebase.FirebaseApp
+import com.google.firebase.annotations.concurrent.Background
import com.google.firebase.app
import com.google.firebase.installations.FirebaseInstallationsApi
import com.google.firebase.sessions.api.FirebaseSessionsDependencies
import com.google.firebase.sessions.settings.SessionsSettings
+import javax.inject.Inject
+import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -35,7 +38,7 @@ internal fun interface SessionFirelogPublisher {
companion object {
val instance: SessionFirelogPublisher
- get() = Firebase.app[SessionFirelogPublisher::class.java]
+ get() = Firebase.app[FirebaseSessionsComponent::class.java].sessionFirelogPublisher
}
}
@@ -44,12 +47,15 @@ internal fun interface SessionFirelogPublisher {
*
* @hide
*/
-internal class SessionFirelogPublisherImpl(
+@Singleton
+internal class SessionFirelogPublisherImpl
+@Inject
+constructor(
private val firebaseApp: FirebaseApp,
private val firebaseInstallations: FirebaseInstallationsApi,
private val sessionSettings: SessionsSettings,
private val eventGDTLogger: EventGDTLoggerInterface,
- private val backgroundDispatcher: CoroutineContext,
+ @Background private val backgroundDispatcher: CoroutineContext,
) : SessionFirelogPublisher {
/**
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt
index 3b4c3124c98..41aeb442cfb 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt
@@ -20,6 +20,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue
import com.google.firebase.Firebase
import com.google.firebase.app
import java.util.UUID
+import javax.inject.Singleton
/**
* [SessionDetails] is a data class responsible for storing information about the current Session.
@@ -35,9 +36,10 @@ internal data class SessionDetails(
* The [SessionGenerator] is responsible for generating the Session ID, and keeping the
* [SessionDetails] up to date with the latest values.
*/
+@Singleton
internal class SessionGenerator(
private val timeProvider: TimeProvider,
- private val uuidGenerator: () -> UUID = UUID::randomUUID
+ private val uuidGenerator: () -> UUID = UUID::randomUUID,
) {
private val firstSessionId = generateSessionId()
private var sessionIndex = -1
@@ -59,7 +61,7 @@ internal class SessionGenerator(
sessionId = if (sessionIndex == 0) firstSessionId else generateSessionId(),
firstSessionId,
sessionIndex,
- sessionStartTimestampUs = timeProvider.currentTimeUs()
+ sessionStartTimestampUs = timeProvider.currentTimeUs(),
)
return currentSession
}
@@ -68,6 +70,6 @@ internal class SessionGenerator(
internal companion object {
val instance: SessionGenerator
- get() = Firebase.app[SessionGenerator::class.java]
+ get() = Firebase.app[FirebaseSessionsComponent::class.java].sessionGenerator
}
}
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt
index bde6d138fbe..6807d8bec69 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt
@@ -128,7 +128,6 @@ internal class SessionLifecycleService : Service() {
/** Generates a new session id and sends it everywhere it's needed */
private fun newSession() {
try {
- // TODO(mrober): Consider migrating to Dagger, or update [FirebaseSessionsRegistrar].
SessionGenerator.instance.generateNewSession()
Log.d(TAG, "Generated new session.")
broadcastSession()
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt
index 97a7d6b73ae..094a76ee51c 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt
@@ -21,7 +21,8 @@ import android.content.Intent
import android.content.ServiceConnection
import android.os.Messenger
import android.util.Log
-import com.google.firebase.FirebaseApp
+import javax.inject.Inject
+import javax.inject.Singleton
/** Interface for binding with the [SessionLifecycleService]. */
internal fun interface SessionLifecycleServiceBinder {
@@ -32,11 +33,12 @@ internal fun interface SessionLifecycleServiceBinder {
fun bindToService(callback: Messenger, serviceConnection: ServiceConnection)
}
-internal class SessionLifecycleServiceBinderImpl(private val firebaseApp: FirebaseApp) :
- SessionLifecycleServiceBinder {
+@Singleton
+internal class SessionLifecycleServiceBinderImpl
+@Inject
+constructor(private val appContext: Context) : SessionLifecycleServiceBinder {
override fun bindToService(callback: Messenger, serviceConnection: ServiceConnection) {
- val appContext: Context = firebaseApp.applicationContext.applicationContext
Intent(appContext, SessionLifecycleService::class.java).also { intent ->
Log.d(TAG, "Binding service to application.")
// This is necessary for the onBind() to be called by each process
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt
index fd2ee5dbddd..41b73f14a4e 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt
@@ -25,17 +25,23 @@ import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.preferencesDataStore
import com.google.firebase.Firebase
import com.google.firebase.FirebaseApp
+import com.google.firebase.annotations.concurrent.Background
+import com.google.firebase.annotations.concurrent.Blocking
import com.google.firebase.app
import com.google.firebase.installations.FirebaseInstallationsApi
import com.google.firebase.sessions.ApplicationInfo
+import com.google.firebase.sessions.FirebaseSessionsComponent
import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName
import com.google.firebase.sessions.SessionDataStoreConfigs
import com.google.firebase.sessions.SessionEvents
+import javax.inject.Inject
+import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
/** [SessionsSettings] manages all the configs that are relevant to the sessions library. */
+@Singleton
internal class SessionsSettings(
private val localOverrideSettings: SettingsProvider,
private val remoteSettings: SettingsProvider,
@@ -62,10 +68,11 @@ internal class SessionsSettings(
),
)
+ @Inject
constructor(
firebaseApp: FirebaseApp,
- blockingDispatcher: CoroutineContext,
- backgroundDispatcher: CoroutineContext,
+ @Blocking blockingDispatcher: CoroutineContext,
+ @Background backgroundDispatcher: CoroutineContext,
firebaseInstallationsApi: FirebaseInstallationsApi,
) : this(
firebaseApp.applicationContext,
@@ -143,7 +150,7 @@ internal class SessionsSettings(
private const val TAG = "SessionsSettings"
val instance: SessionsSettings
- get() = Firebase.app[SessionsSettings::class.java]
+ get() = Firebase.app[FirebaseSessionsComponent::class.java].sessionsSettings
private val Context.dataStore: DataStore by
preferencesDataStore(
diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt
new file mode 100644
index 00000000000..eda16d8f0b4
--- /dev/null
+++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2025 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.sessions.testing
+
+import com.google.firebase.Firebase
+import com.google.firebase.app
+import com.google.firebase.sessions.FirebaseSessions
+import com.google.firebase.sessions.FirebaseSessionsComponent
+import com.google.firebase.sessions.SessionDatastore
+import com.google.firebase.sessions.SessionFirelogPublisher
+import com.google.firebase.sessions.SessionGenerator
+import com.google.firebase.sessions.settings.SessionsSettings
+
+/** Bridge between FirebaseSessionsComponent and FirebaseSessionsFakeRegistrar. */
+internal class FirebaseSessionsFakeComponent : FirebaseSessionsComponent {
+ // TODO(mrober): Move tests to use Dagger for DI.
+
+ override val firebaseSessions: FirebaseSessions
+ get() = Firebase.app[FirebaseSessions::class.java]
+
+ override val sessionDatastore: SessionDatastore
+ get() = Firebase.app[SessionDatastore::class.java]
+
+ override val sessionFirelogPublisher: SessionFirelogPublisher
+ get() = Firebase.app[SessionFirelogPublisher::class.java]
+
+ override val sessionGenerator: SessionGenerator
+ get() = Firebase.app[SessionGenerator::class.java]
+
+ override val sessionsSettings: SessionsSettings
+ get() = Firebase.app[SessionsSettings::class.java]
+}
diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt
index 9755a5e12d0..58855f622f3 100644
--- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt
+++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt
@@ -17,7 +17,6 @@
package com.google.firebase.sessions.testing
import androidx.annotation.Keep
-import com.google.android.datatransport.TransportFactory
import com.google.firebase.FirebaseApp
import com.google.firebase.annotations.concurrent.Background
import com.google.firebase.annotations.concurrent.Blocking
@@ -26,10 +25,10 @@ import com.google.firebase.components.ComponentRegistrar
import com.google.firebase.components.Dependency
import com.google.firebase.components.Qualified.qualified
import com.google.firebase.components.Qualified.unqualified
-import com.google.firebase.installations.FirebaseInstallationsApi
import com.google.firebase.platforminfo.LibraryVersionComponent
import com.google.firebase.sessions.BuildConfig
import com.google.firebase.sessions.FirebaseSessions
+import com.google.firebase.sessions.FirebaseSessionsComponent
import com.google.firebase.sessions.SessionDatastore
import com.google.firebase.sessions.SessionFirelogPublisher
import com.google.firebase.sessions.SessionGenerator
@@ -75,6 +74,10 @@ internal class FirebaseSessionsFakeRegistrar : ComponentRegistrar {
)
}
.build(),
+ Component.builder(FirebaseSessionsComponent::class.java)
+ .name("fake-fire-sessions-component")
+ .factory { FirebaseSessionsFakeComponent() }
+ .build(),
Component.builder(FakeSessionDatastore::class.java)
.name("fake-sessions-datastore")
.factory { FakeSessionDatastore() }
@@ -97,21 +100,14 @@ internal class FirebaseSessionsFakeRegistrar : ComponentRegistrar {
)
private companion object {
- private const val LIBRARY_NAME = "fire-sessions"
-
- private val firebaseApp = unqualified(FirebaseApp::class.java)
- private val firebaseInstallationsApi = unqualified(FirebaseInstallationsApi::class.java)
- private val backgroundDispatcher =
- qualified(Background::class.java, CoroutineDispatcher::class.java)
- private val blockingDispatcher =
- qualified(Blocking::class.java, CoroutineDispatcher::class.java)
- private val transportFactory = unqualified(TransportFactory::class.java)
- private val fakeFirelogPublisher = unqualified(FakeFirelogPublisher::class.java)
- private val fakeDatastore = unqualified(FakeSessionDatastore::class.java)
- private val fakeServiceBinder = unqualified(FakeSessionLifecycleServiceBinder::class.java)
- private val sessionGenerator = unqualified(SessionGenerator::class.java)
- private val sessionsSettings = unqualified(SessionsSettings::class.java)
+ const val LIBRARY_NAME = "fire-sessions"
- private val fakeFirebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
+ val firebaseApp = unqualified(FirebaseApp::class.java)
+ val backgroundDispatcher = qualified(Background::class.java, CoroutineDispatcher::class.java)
+ val blockingDispatcher = qualified(Blocking::class.java, CoroutineDispatcher::class.java)
+ val fakeFirelogPublisher = unqualified(FakeFirelogPublisher::class.java)
+ val fakeDatastore = unqualified(FakeSessionDatastore::class.java)
+ val fakeServiceBinder = unqualified(FakeSessionLifecycleServiceBinder::class.java)
+ val fakeFirebaseInstallations = FakeFirebaseInstallations("FaKeFiD")
}
}
From a59649eda3cbedeaa56f4f49fe5de1210845d76d Mon Sep 17 00:00:00 2001
From: Matthew Robertson
Date: Tue, 11 Mar 2025 09:14:56 -0600
Subject: [PATCH 089/146] Add warning for known issue b/328687152 (#6755)
---
firebase-sessions/CHANGELOG.md | 1 +
.../sessions/FirebaseSessionsRegistrar.kt | 27 +++++++++++++++++++
2 files changed, 28 insertions(+)
diff --git a/firebase-sessions/CHANGELOG.md b/firebase-sessions/CHANGELOG.md
index d5293913dc9..7285ff94e27 100644
--- a/firebase-sessions/CHANGELOG.md
+++ b/firebase-sessions/CHANGELOG.md
@@ -1,5 +1,6 @@
# Unreleased
+* [changed] Add warning for known issue b/328687152
* [changed] Use Dagger for dependency injection
* [changed] Updated datastore dependency to `1.1.3` to
fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8).
diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt
index 1043ad74800..5cb8de7a182 100644
--- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt
+++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt
@@ -17,7 +17,9 @@
package com.google.firebase.sessions
import android.content.Context
+import android.util.Log
import androidx.annotation.Keep
+import androidx.datastore.preferences.preferencesDataStore
import com.google.android.datatransport.TransportFactory
import com.google.firebase.FirebaseApp
import com.google.firebase.annotations.concurrent.Background
@@ -69,6 +71,7 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar {
)
private companion object {
+ const val TAG = "FirebaseSessions"
const val LIBRARY_NAME = "fire-sessions"
val appContext = unqualified(Context::class.java)
@@ -78,5 +81,29 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar {
val blockingDispatcher = qualified(Blocking::class.java, CoroutineDispatcher::class.java)
val transportFactory = unqualified(TransportFactory::class.java)
val firebaseSessionsComponent = unqualified(FirebaseSessionsComponent::class.java)
+
+ init {
+ try {
+ ::preferencesDataStore.javaClass
+ } catch (ex: NoClassDefFoundError) {
+ Log.w(
+ TAG,
+ """
+ Your app is experiencing a known issue in the Android Gradle plugin, see https://issuetracker.google.com/328687152
+
+ It affects Java-only apps using AGP version 8.3.2 and under. To avoid the issue, either:
+
+ 1. Upgrade Android Gradle plugin to 8.4.0+
+ Follow the guide at https://developer.android.com/build/agp-upgrade-assistant
+
+ 2. Or, add the Kotlin plugin to your app
+ Follow the guide at https://developer.android.com/kotlin/add-kotlin
+
+ 3. Or, do the technical workaround described in https://issuetracker.google.com/issues/328687152#comment3
+ """
+ .trimIndent(),
+ )
+ }
+ }
}
}
From 6e7b7a1788cce882fed44077907c28b9d51b4c2d Mon Sep 17 00:00:00 2001
From: Google Open Source Bot
Date: Tue, 11 Mar 2025 11:53:19 -0700
Subject: [PATCH 090/146] m160 mergeback (#6757)
Auto-generated PR for cleaning up release m160
NO_RELEASE_CHANGE
---------
Co-authored-by: VinayGuthal
Co-authored-by: VinayGuthal
---
firebase-crashlytics-ndk/CHANGELOG.md | 3 +++
firebase-crashlytics-ndk/gradle.properties | 4 ++--
firebase-crashlytics/CHANGELOG.md | 1 +
firebase-crashlytics/gradle.properties | 4 ++--
firebase-functions/CHANGELOG.md | 2 ++
firebase-functions/gradle.properties | 4 ++--
firebase-sessions/CHANGELOG.md | 8 +++++++-
firebase-sessions/gradle.properties | 4 ++--
firebase-vertexai/CHANGELOG.md | 3 +++
firebase-vertexai/gradle.properties | 4 ++--
10 files changed, 26 insertions(+), 11 deletions(-)
diff --git a/firebase-crashlytics-ndk/CHANGELOG.md b/firebase-crashlytics-ndk/CHANGELOG.md
index ab8dd8dfdf3..0cf17d1d025 100644
--- a/firebase-crashlytics-ndk/CHANGELOG.md
+++ b/firebase-crashlytics-ndk/CHANGELOG.md
@@ -1,4 +1,7 @@
# Unreleased
+
+
+# 19.4.1
* [changed] Updated `firebase-crashlytics` dependency to v19.4.1
# 19.3.0
diff --git a/firebase-crashlytics-ndk/gradle.properties b/firebase-crashlytics-ndk/gradle.properties
index 5ab96e1d760..a7ea562fe0f 100644
--- a/firebase-crashlytics-ndk/gradle.properties
+++ b/firebase-crashlytics-ndk/gradle.properties
@@ -1,2 +1,2 @@
-version=19.4.1
-latestReleasedVersion=19.4.0
+version=19.4.2
+latestReleasedVersion=19.4.1
diff --git a/firebase-crashlytics/CHANGELOG.md b/firebase-crashlytics/CHANGELOG.md
index c0e6ddce86b..723e4a2e7d1 100644
--- a/firebase-crashlytics/CHANGELOG.md
+++ b/firebase-crashlytics/CHANGELOG.md
@@ -2,6 +2,7 @@
* [changed] Internal changes to read version control info more efficiently [6754]
* [fixed] Fixed NoSuchMethodError when getting process info on Android 13 [#6720]
+
# 19.4.1
* [changed] Updated `firebase-sessions` dependency to v2.0.9
diff --git a/firebase-crashlytics/gradle.properties b/firebase-crashlytics/gradle.properties
index 5ab96e1d760..a7ea562fe0f 100644
--- a/firebase-crashlytics/gradle.properties
+++ b/firebase-crashlytics/gradle.properties
@@ -1,2 +1,2 @@
-version=19.4.1
-latestReleasedVersion=19.4.0
+version=19.4.2
+latestReleasedVersion=19.4.1
diff --git a/firebase-functions/CHANGELOG.md b/firebase-functions/CHANGELOG.md
index 863de38db50..785f0f9966a 100644
--- a/firebase-functions/CHANGELOG.md
+++ b/firebase-functions/CHANGELOG.md
@@ -1,6 +1,7 @@
# Unreleased
* [fixed] Fixed an issue that prevented the App Check token from being handled correctly in case of error.
+
# 21.1.1
* [fixed] Resolve Kotlin migration visibility issues
([#6522](//github.com/firebase/firebase-android-sdk/pull/6522))
@@ -225,3 +226,4 @@ updates.
optional region to override the default "us-central1".
* [feature] New `useFunctionsEmulator` method allows testing against a local
instance of the [Cloud Functions Emulator](https://firebase.google.com/docs/functions/local-emulator).
+
diff --git a/firebase-functions/gradle.properties b/firebase-functions/gradle.properties
index ff0fa6afed0..6fe83923849 100644
--- a/firebase-functions/gradle.properties
+++ b/firebase-functions/gradle.properties
@@ -1,3 +1,3 @@
-version=21.1.1
-latestReleasedVersion=21.1.0
+version=21.1.2
+latestReleasedVersion=21.1.1
android.enableUnitTestBinaryResources=true
diff --git a/firebase-sessions/CHANGELOG.md b/firebase-sessions/CHANGELOG.md
index 7285ff94e27..eb5a6b85596 100644
--- a/firebase-sessions/CHANGELOG.md
+++ b/firebase-sessions/CHANGELOG.md
@@ -1,13 +1,19 @@
# Unreleased
-
* [changed] Add warning for known issue b/328687152
* [changed] Use Dagger for dependency injection
* [changed] Updated datastore dependency to `1.1.3` to
fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8).
+
# 2.0.9
* [fixed] Make AQS resilient to background init in multi-process apps.
+
+## Kotlin
+The Kotlin extensions library transitively includes the updated
+`firebase-sessions` library. The Kotlin extensions library has no additional
+updates.
+
# 2.0.7
* [fixed] Removed extraneous logs that risk leaking internal identifiers.
diff --git a/firebase-sessions/gradle.properties b/firebase-sessions/gradle.properties
index c9bd869d4cd..6a74cb4445b 100644
--- a/firebase-sessions/gradle.properties
+++ b/firebase-sessions/gradle.properties
@@ -12,5 +12,5 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-version=2.0.9
-latestReleasedVersion=2.0.8
+version=2.0.10
+latestReleasedVersion=2.0.9
diff --git a/firebase-vertexai/CHANGELOG.md b/firebase-vertexai/CHANGELOG.md
index c0bfdfb214e..e28c285822e 100644
--- a/firebase-vertexai/CHANGELOG.md
+++ b/firebase-vertexai/CHANGELOG.md
@@ -1,4 +1,7 @@
# Unreleased
+
+
+# 16.2.0
* [fixed] Added support for new values sent by the server for `FinishReason` and `BlockReason`.
* [changed] Added support for modality-based token count. (#6658)
* [feature] Added support for generating images with Imagen models.
diff --git a/firebase-vertexai/gradle.properties b/firebase-vertexai/gradle.properties
index b686fdcb9db..546c015493e 100644
--- a/firebase-vertexai/gradle.properties
+++ b/firebase-vertexai/gradle.properties
@@ -12,5 +12,5 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-version=16.2.0
-latestReleasedVersion=16.1.0
+version=16.2.1
+latestReleasedVersion=16.2.0
From 3c839f3712771897512e16ba68731da0ee2f29d5 Mon Sep 17 00:00:00 2001
From: Andrew Heard
Date: Tue, 11 Mar 2025 14:59:20 -0400
Subject: [PATCH 091/146] [Vertex AI] Remove `golden-files` directory (#6740)
Remove the `firebase-vertexai/src/test/resources/golden-files`
directory. This was carried over from the
[`generative-ai-android`](https://github.com/google-gemini/generative-ai-android)
repository. We are now using
https://github.com/FirebaseExtended/vertexai-sdk-test-data/tree/main/mock-responses
instead.
#no-changelog
---------
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: Rodrigo Lazo
Co-authored-by: Daymon <17409137+daymxn@users.noreply.github.com>
Co-authored-by: Matthew Robertson
Co-authored-by: Rodrigo Lazo Paz
---
.../vertexai/common/StreamingSnapshotTests.kt | 16 +----
.../vertexai/common/UnarySnapshotTests.kt | 15 +---
.../firebase/vertexai/common/util/tests.kt | 7 +-
.../streaming/failure-api-key.txt | 21 ------
.../streaming/failure-empty-content.txt | 1 -
.../failure-finish-reason-safety.txt | 2 -
.../streaming/failure-http-error.txt | 13 ----
.../streaming/failure-image-rejected.txt | 7 --
.../failure-prompt-blocked-safety.txt | 2 -
.../failure-recitation-no-content.txt | 6 --
.../streaming/failure-unknown-model.txt | 13 ----
.../streaming/success-basic-reply-long.txt | 12 ----
.../streaming/success-basic-reply-short.txt | 2 -
.../streaming/success-citations-altname.txt | 12 ----
.../streaming/success-citations.txt | 12 ----
.../streaming/success-quotes-escaped.txt | 7 --
.../streaming/success-unknown-enum.txt | 11 ---
.../golden-files/unary/failure-api-key.json | 21 ------
.../unary/failure-empty-content.json | 28 --------
.../unary/failure-finish-reason-safety.json | 54 --------------
.../unary/failure-http-error.json | 13 ----
.../unary/failure-image-rejected.json | 13 ----
.../unary/failure-invalid-response.json | 14 ----
.../unary/failure-malformed-content.json | 30 --------
.../unary/failure-prompt-blocked-safety.json | 23 ------
.../unary/failure-quota-exceeded.json | 31 --------
.../unary/failure-service-disabled.json | 27 -------
.../unary/failure-unknown-model.json | 13 ----
.../failure-unsupported-user-location.json | 13 ----
.../unary/success-basic-reply-long.json | 54 --------------
.../unary/success-basic-reply-short.json | 54 --------------
.../unary/success-citations-altname.json | 70 -------------------
.../unary/success-citations-nolicense.json | 58 ---------------
.../golden-files/unary/success-citations.json | 70 -------------------
.../unary/success-code-execution.json | 48 -------------
.../success-constraint-decoding-json.json | 34 ---------
...success-function-call-empty-arguments.json | 18 -----
.../success-function-call-json-literal.json | 45 ------------
.../unary/success-function-call-null.json | 45 ------------
.../unary/success-including-severity.json | 50 -------------
.../unary/success-partial-usage-metadata.json | 57 ---------------
.../unary/success-quote-reply.json | 54 --------------
.../unary/success-unknown-enum.json | 52 --------------
.../unary/success-usage-metadata.json | 59 ----------------
44 files changed, 7 insertions(+), 1200 deletions(-)
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-api-key.txt
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-empty-content.txt
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-finish-reason-safety.txt
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-http-error.txt
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-image-rejected.txt
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-prompt-blocked-safety.txt
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-recitation-no-content.txt
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-unknown-model.txt
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/success-basic-reply-long.txt
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/success-basic-reply-short.txt
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/success-citations-altname.txt
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/success-citations.txt
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/success-quotes-escaped.txt
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/success-unknown-enum.txt
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-api-key.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-empty-content.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-finish-reason-safety.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-http-error.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-image-rejected.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-invalid-response.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-malformed-content.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-prompt-blocked-safety.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-quota-exceeded.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-service-disabled.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-unknown-model.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-unsupported-user-location.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-basic-reply-long.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-basic-reply-short.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-citations-altname.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-citations-nolicense.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-citations.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-code-execution.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-constraint-decoding-json.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-function-call-empty-arguments.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-function-call-json-literal.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-function-call-null.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-including-severity.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-partial-usage-metadata.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-quote-reply.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-unknown-enum.json
delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-usage-metadata.json
diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt
index 4abf386765a..8b421edfa50 100644
--- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt
+++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt
@@ -69,7 +69,7 @@ internal class StreamingSnapshotTests {
@Test
fun `unknown enum`() =
- goldenStreamingFile("success-unknown-enum.txt") {
+ goldenStreamingFile("success-unknown-safety-enum.txt") {
val responses = apiController.generateContentStream(textGenerateContentRequest("prompt"))
withTimeout(testTimeout) {
@@ -152,20 +152,6 @@ internal class StreamingSnapshotTests {
}
}
- @Test
- fun `citation returns correctly when using alternative name`() =
- goldenStreamingFile("success-citations-altname.txt") {
- val responses = apiController.generateContentStream(textGenerateContentRequest("prompt"))
-
- withTimeout(testTimeout) {
- val responseList = responses.toList()
- responseList.any {
- it.candidates?.any { it.citationMetadata?.citationSources?.isNotEmpty() ?: false }
- ?: false
- } shouldBe true
- }
- }
-
@Test
fun `stopped for recitation`() =
goldenStreamingFile("failure-recitation-no-content.txt") {
diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/UnarySnapshotTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/UnarySnapshotTests.kt
index c316a9ece81..49a24201c3f 100644
--- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/UnarySnapshotTests.kt
+++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/UnarySnapshotTests.kt
@@ -75,7 +75,7 @@ internal class UnarySnapshotTests {
@Test
fun `unknown enum`() =
- goldenUnaryFile("success-unknown-enum.json") {
+ goldenUnaryFile("success-unknown-enum-safety-ratings.json") {
withTimeout(testTimeout) {
val response = apiController.generateContent(textGenerateContentRequest("prompt"))
@@ -211,17 +211,6 @@ internal class UnarySnapshotTests {
}
}
- @Test
- fun `citation returns correctly when using alternative name`() =
- goldenUnaryFile("success-citations-altname.json") {
- withTimeout(testTimeout) {
- val response = apiController.generateContent(textGenerateContentRequest("prompt"))
-
- response.candidates?.isEmpty() shouldBe false
- response.candidates?.first()?.citationMetadata?.citationSources?.isNotEmpty() shouldBe true
- }
- }
-
@OptIn(ExperimentalSerializationApi::class)
@Test
fun `properly translates json text`() =
@@ -306,7 +295,7 @@ internal class UnarySnapshotTests {
@Test
fun `service disabled`() =
- goldenUnaryFile("failure-service-disabled.json", HttpStatusCode.Forbidden) {
+ goldenUnaryFile("failure-firebaseml-api-not-enabled.json", HttpStatusCode.Forbidden) {
withTimeout(testTimeout) {
shouldThrow