Skip to content

Commit 4d1ec13

Browse files
authored
Merge branch 'main' into rl.recaptcha.common
2 parents 16cc204 + de675d9 commit 4d1ec13

23 files changed

Lines changed: 2234 additions & 208 deletions

File tree

.github/workflows/app-distribution-gradle-compatibility-tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: App Distribution Gradle Compatibility Tests
22

33
on:
44
schedule:
5-
- cron: '0 6 * * *' # Run daily at 6 AM
5+
- cron: '0 6 * * *' # Run daily at 6:00 am UTC
66
pull_request:
77
paths:
88
- 'firebase-appdistribution/**'
@@ -55,11 +55,11 @@ jobs:
5555
path: firebase-appdistribution-gradle/build/reports/tests/
5656

5757
- name: Create GitHub Issue on Failure
58-
if: failure() && (github.event_name == 'schedule')
58+
if: failure()
5959
env:
6060
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
6161
run: |
6262
gh issue create \
6363
--title "[firebase-appdistribution] Gradle Compatibility Tests Failed" \
6464
--body "The daily compatibility tests failed on the main branch. Please check the workflow logs here to diagnose the failure: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
65-
--label "bug,ci-failure,api: appdistribution"
65+
--label "api: appdistribution"

ai-logic/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/AIModels.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ class AIModels {
2828
var app: FirebaseApp? = null
2929
lateinit var vertexAIFlashModel: GenerativeModel
3030
lateinit var vertexAIFlashLiteModel: GenerativeModel
31+
lateinit var vertexAI3_5FlashModel: GenerativeModel
3132
lateinit var googleAIFlashModel: GenerativeModel
3233
lateinit var googleAIFlashLiteModel: GenerativeModel
34+
lateinit var googleAI3_5FlashModel: GenerativeModel
3335
lateinit var vertexAITemplateModel: TemplateGenerativeModel
3436
lateinit var googleAITemplateModel: TemplateGenerativeModel
3537

@@ -41,8 +43,10 @@ class AIModels {
4143
return listOf(
4244
vertexAIFlashModel,
4345
vertexAIFlashLiteModel,
46+
vertexAI3_5FlashModel,
4447
googleAIFlashModel,
45-
googleAIFlashLiteModel
48+
googleAIFlashLiteModel,
49+
googleAI3_5FlashModel,
4650
)
4751
}
4852

@@ -66,6 +70,11 @@ class AIModels {
6670
.generativeModel(
6771
modelName = "gemini-2.5-flash-lite",
6872
)
73+
vertexAI3_5FlashModel =
74+
FirebaseAI.getInstance(app!!, GenerativeBackend.vertexAI("global"))
75+
.generativeModel(
76+
modelName = "gemini-3.5-flash",
77+
)
6978
googleAIFlashModel =
7079
FirebaseAI.getInstance(app!!, GenerativeBackend.googleAI())
7180
.generativeModel(
@@ -76,6 +85,11 @@ class AIModels {
7685
.generativeModel(
7786
modelName = "gemini-2.5-flash-lite",
7887
)
88+
googleAI3_5FlashModel =
89+
FirebaseAI.getInstance(app!!, GenerativeBackend.googleAI())
90+
.generativeModel(
91+
modelName = "gemini-3.5-flash",
92+
)
7993
vertexAITemplateModel =
8094
FirebaseAI.getInstance(app!!, GenerativeBackend.vertexAI()).templateGenerativeModel()
8195
googleAITemplateModel =

firebase-common/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- [feature] Added support for reading the `recaptcha_site_key` value from the `google-services.json`
44
file. (#8216)
5+
- [fixed] Resolved a thread deadlock in HeartBeatInfoStorage when using Jetpack DataStore background executors.(#8182)
56

67
# 22.0.1
78

firebase-common/src/main/java/com/google/firebase/heartbeatinfo/HeartBeatInfoStorage.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ synchronized List<HeartBeatResult> getAllHeartBeats() {
128128
return heartBeatResults;
129129
}
130130

131-
private synchronized Preferences.Key<Set<String>> getStoredUserAgentString(
131+
private Preferences.Key<Set<String>> getStoredUserAgentString(
132132
MutablePreferences preferences, String dateString) {
133133
for (Map.Entry<Preferences.Key<?>, Object> entry : preferences.asMap().entrySet()) {
134134
if (entry.getValue() instanceof Set) {
@@ -143,7 +143,7 @@ private synchronized Preferences.Key<Set<String>> getStoredUserAgentString(
143143
return null;
144144
}
145145

146-
private synchronized void updateStoredUserAgent(
146+
private void updateStoredUserAgent(
147147
MutablePreferences preferences, Preferences.Key<Set<String>> userAgent, String dateString) {
148148
removeStoredDate(preferences, dateString);
149149
Set<String> userAgentDateSet =
@@ -152,7 +152,7 @@ private synchronized void updateStoredUserAgent(
152152
preferences.set(userAgent, userAgentDateSet);
153153
}
154154

155-
private synchronized void removeStoredDate(MutablePreferences preferences, String dateString) {
155+
private void removeStoredDate(MutablePreferences preferences, String dateString) {
156156
// Find stored heartbeat and clear it.
157157
Preferences.Key<Set<String>> userAgent = getStoredUserAgentString(preferences, dateString);
158158
if (userAgent == null) {
@@ -179,7 +179,7 @@ synchronized void postHeartBeatCleanUp() {
179179
});
180180
}
181181

182-
private synchronized String getFormattedDate(long millis) {
182+
private String getFormattedDate(long millis) {
183183
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
184184
Instant instant = new Date(millis).toInstant();
185185
LocalDateTime ldt = instant.atOffset(ZoneOffset.UTC).toLocalDateTime();
@@ -226,7 +226,7 @@ synchronized void storeHeartBeat(long millis, String userAgentString) {
226226
});
227227
}
228228

229-
private synchronized long cleanUpStoredHeartBeats(MutablePreferences preferences) {
229+
private long cleanUpStoredHeartBeats(MutablePreferences preferences) {
230230
long heartBeatCount = JavaDataStorageKt.getOrDefault(preferences, HEART_BEAT_COUNT_TAG, 0L);
231231

232232
String lowestDate = null;
@@ -264,7 +264,7 @@ synchronized void updateGlobalHeartBeat(long millis) {
264264
});
265265
}
266266

267-
synchronized boolean isSameDateUtc(long base, long target) {
267+
boolean isSameDateUtc(long base, long target) {
268268
return getFormattedDate(base).equals(getFormattedDate(target));
269269
}
270270

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.heartbeatinfo
18+
19+
import androidx.datastore.preferences.core.MutablePreferences
20+
import androidx.datastore.preferences.core.Preferences
21+
import androidx.datastore.preferences.core.stringPreferencesKey
22+
import androidx.test.ext.junit.runners.AndroidJUnit4
23+
import com.google.common.truth.Truth.assertThat
24+
import com.google.firebase.datastorage.JavaDataStorage
25+
import java.util.Collections
26+
import java.util.concurrent.CompletableFuture
27+
import org.junit.Test
28+
import org.junit.runner.RunWith
29+
import org.mockito.ArgumentMatchers.any
30+
import org.mockito.Mockito.doAnswer
31+
import org.mockito.Mockito.mock
32+
import org.mockito.Mockito.`when`
33+
34+
@RunWith(AndroidJUnit4::class)
35+
class HeartBeatInfoStorageKotlinTest {
36+
37+
/**
38+
* Regression test for https://github.com/firebase/firebase-android-sdk/issues/8016
39+
*
40+
* <p>Root Cause: The synchronized storeHeartBeat() method locks the HeartBeatInfoStorage instance
41+
* while waiting for JavaDataStorage.editSync(...) to complete. However, editSync schedules its
42+
* preference updates on a different background thread. If helper methods called inside the
43+
* editSync transaction block (such as getStoredUserAgentString, updateStoredUserAgent, or
44+
* cleanUpStoredHeartBeats) are also marked as synchronized, the background thread blocks trying
45+
* to acquire the HeartBeatInfoStorage lock, which is held by the caller thread waiting for the
46+
* background thread to complete, causing a permanent deadlock.
47+
*
48+
* <p>Fix: Removed the synchronized keyword from helper methods (and isSameDateUtc) since they
49+
* operate exclusively on thread-local transaction parameters and do not access shared mutable
50+
* instance state.
51+
*/
52+
@Test
53+
fun storeHeartBeat_whenCalledOnSeparateThread_doesNotDeadlock() {
54+
val mockDataStore = mock(JavaDataStorage::class.java)
55+
val heartBeatStorageWithMock = HeartBeatInfoStorage(mockDataStore)
56+
57+
// Mock editSync to run the transform on a background thread and block the caller thread
58+
doAnswer { invocation ->
59+
@Suppress("UNCHECKED_CAST")
60+
val transform = invocation.getArgument<(MutablePreferences) -> Unit>(0)
61+
62+
val future =
63+
CompletableFuture.runAsync {
64+
val mockPrefs = mock(MutablePreferences::class.java)
65+
// Mock get(LAST_STORED_DATE) to return the target date to force entry into the if block
66+
`when`(mockPrefs.get(stringPreferencesKey("last-used-date"))).thenReturn("1970-01-01")
67+
// Mock asMap() to avoid NullPointerException
68+
`when`(mockPrefs.asMap()).thenReturn(Collections.emptyMap())
69+
70+
transform(mockPrefs)
71+
}
72+
73+
future.get() // Blocks the caller thread
74+
mock(Preferences::class.java)
75+
}
76+
.`when`(mockDataStore)
77+
.editSync(anyTransform())
78+
79+
// Spawn a thread to call storeHeartBeat, which would deadlock under the bug
80+
val thread = Thread {
81+
heartBeatStorageWithMock.storeHeartBeat(0L, "test-agent") // 1970-01-01
82+
}
83+
84+
thread.start()
85+
thread.join(3000) // Wait 3 seconds
86+
87+
try {
88+
// Since the bug is fixed, the thread should not be alive.
89+
assertThat(thread.isAlive).isFalse()
90+
} finally {
91+
thread.interrupt()
92+
}
93+
}
94+
95+
private fun anyTransform(): (MutablePreferences) -> Unit {
96+
any<kotlin.jvm.functions.Function1<MutablePreferences, kotlin.Unit>>()
97+
return { _: MutablePreferences -> }
98+
}
99+
}

firebase-dataconnect/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
as the random numbers were not used in a security-sensitive context,
1010
thus the performance costs of secure random number generation were unnecessary.
1111
([#8154](https://github.com/firebase/firebase-android-sdk/pull/8154))
12+
- [fixed] Queries executed with FetchPolicy.CACHE_ONLY now fail, as expected,
13+
if local caching is not enabled, instead of behaving like SERVER_ONLY.
14+
([#8214](https://github.com/firebase/firebase-android-sdk/pull/8214))
1215

1316
# 17.2.2
1417

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.dataconnect.core
18+
19+
import com.google.firebase.dataconnect.core.LoggerGlobals.Logger
20+
import com.google.firebase.dataconnect.core.LoggerGlobals.debug
21+
import com.google.firebase.dataconnect.sqlite.DataConnectCacheDatabase
22+
import com.google.firebase.dataconnect.util.ObjectLifecycleManager
23+
import java.io.File
24+
import kotlinx.coroutines.CoroutineDispatcher
25+
26+
internal class DataConnectCache(
27+
private val dbFile: File?,
28+
val maxAge: kotlin.time.Duration,
29+
private val cpuDispatcher: CoroutineDispatcher,
30+
private val logger: Logger,
31+
) : ObjectLifecycleManager<DataConnectCacheDatabase>(cpuDispatcher, logger) {
32+
33+
val maxAgeProto: com.google.protobuf.Duration =
34+
maxAge.toComponents { seconds, nanos ->
35+
com.google.protobuf.Duration.newBuilder().setSeconds(seconds).setNanos(nanos).build()
36+
}
37+
38+
override fun create() =
39+
DataConnectCacheDatabase(
40+
dbFile,
41+
cpuDispatcher,
42+
Logger("DataConnectCacheDatabase").apply { debug { "created by ${logger.nameWithId}" } }
43+
)
44+
45+
override suspend fun initialize(instance: DataConnectCacheDatabase) {
46+
instance.initialize()
47+
}
48+
49+
override suspend fun destroy(instance: DataConnectCacheDatabase) {
50+
instance.close()
51+
}
52+
53+
override fun toString() = "DataConnectCache(dbFile=$dbFile, maxAge=$maxAge)"
54+
}

0 commit comments

Comments
 (0)