Skip to content

Commit 18bd2ee

Browse files
authored
Merge 42824aa into 70c8e89
2 parents 70c8e89 + 42824aa commit 18bd2ee

File tree

6 files changed

+389
-135
lines changed

6 files changed

+389
-135
lines changed

firebase-common/firebase-common.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ dependencies {
5757

5858
api("com.google.firebase:firebase-components:18.0.0")
5959
api("com.google.firebase:firebase-annotations:16.2.0")
60+
implementation(libs.androidx.datastore.preferences)
6061
implementation(libs.androidx.annotation)
6162
implementation(libs.androidx.futures)
6263
implementation(libs.kotlin.stdlib)
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/*
2+
* Copyright 2025 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.datastore
18+
19+
import android.content.Context
20+
import androidx.datastore.core.DataStore
21+
import androidx.datastore.preferences.SharedPreferencesMigration
22+
import androidx.datastore.preferences.core.MutablePreferences
23+
import androidx.datastore.preferences.core.Preferences
24+
import androidx.datastore.preferences.core.edit
25+
import androidx.datastore.preferences.preferencesDataStore
26+
import kotlinx.coroutines.flow.firstOrNull
27+
import kotlinx.coroutines.runBlocking
28+
29+
/**
30+
* Wrapper around [DataStore] for easier migration from `SharedPreferences` in Java code.
31+
*
32+
* Automatically migrates data from any `SharedPreferences` that share the same context and name.
33+
*
34+
* There should only ever be _one_ instance of this class per context and name variant.
35+
*
36+
* > Do **NOT** use this _unless_ you're bridging Java code. If you're writing new code, or your
37+
* code is in Kotlin, then you should create your own singleton that uses [DataStore] directly.
38+
*
39+
* Example:
40+
* ```java
41+
* DataStorage heartBeatStorage = new DataStorage(applicationContext, "FirebaseHeartBeat");
42+
* ```
43+
*
44+
* @property context The [Context] that this data will be saved under.
45+
* @property name What the storage file should be named.
46+
*/
47+
class DataStorage(val context: Context, val name: String) {
48+
/**
49+
* Used to ensure that there's only ever one call to [editSync] per thread; as to avoid deadlocks.
50+
*/
51+
private val editLock = ThreadLocal<Boolean>()
52+
53+
private val Context.dataStore: DataStore<Preferences> by
54+
preferencesDataStore(
55+
name = name,
56+
produceMigrations = { listOf(SharedPreferencesMigration(it, name)) }
57+
)
58+
59+
private val dataStore = context.dataStore
60+
61+
/**
62+
* Get data from the datastore _synchronously_.
63+
*
64+
* Note that if the key is _not_ in the datastore, while the [defaultValue] will be returned
65+
* instead- it will **not** be saved to the datastore; you'll have to manually do that.
66+
*
67+
* Blocks on the currently running thread.
68+
*
69+
* Example:
70+
* ```java
71+
* Preferences.Key<Long> fireCountKey = PreferencesKeys.longKey("fire-count");
72+
* assert dataStore.get(fireCountKey, 0L) == 0L;
73+
*
74+
* dataStore.putSync(fireCountKey, 102L);
75+
* assert dataStore.get(fireCountKey, 0L) == 102L;
76+
* ```
77+
*
78+
* @param key The typed key of the entry to get data for.
79+
* @param defaultValue A value to default to, if the key isn't found.
80+
*
81+
* @see Preferences.getOrDefault
82+
*/
83+
fun <T> getSync(key: Preferences.Key<T>, defaultValue: T): T = runBlocking {
84+
dataStore.data.firstOrNull()?.get(key) ?: defaultValue
85+
}
86+
87+
/**
88+
* Checks if a key is present in the datastore _synchronously_.
89+
*
90+
* Blocks on the currently running thread.
91+
*
92+
* Example:
93+
* ```java
94+
* Preferences.Key<Long> fireCountKey = PreferencesKeys.longKey("fire-count");
95+
* assert !dataStore.contains(fireCountKey);
96+
*
97+
* dataStore.putSync(fireCountKey, 102L);
98+
* assert dataStore.contains(fireCountKey);
99+
* ```
100+
*
101+
* @param key The typed key of the entry to find.
102+
*/
103+
fun <T> contains(key: Preferences.Key<T>): Boolean = runBlocking {
104+
dataStore.data.firstOrNull()?.contains(key) ?: false
105+
}
106+
107+
/**
108+
* Sets and saves data in the datastore _synchronously_.
109+
*
110+
* Existing values will be overwritten.
111+
*
112+
* Blocks on the currently running thread.
113+
*
114+
* Example:
115+
* ```java
116+
* dataStore.putSync(PreferencesKeys.longKey("fire-count"), 102L);
117+
* ```
118+
*
119+
* @param key The typed key of the entry to save the data under.
120+
* @param value The data to save.
121+
*
122+
* @return The [Preferences] object that the data was saved under.
123+
*/
124+
fun <T> putSync(key: Preferences.Key<T>, value: T): Preferences = runBlocking {
125+
dataStore.edit { it[key] = value }
126+
}
127+
128+
/**
129+
* Gets all data in the datastore _synchronously_.
130+
*
131+
* Blocks on the currently running thread.
132+
*
133+
* Example:
134+
* ```java
135+
* ArrayList<String> allDates = new ArrayList<>();
136+
*
137+
* for (Map.Entry<Preferences.Key<?>, Object> entry : dataStore.getAllSync().entrySet()) {
138+
* if (entry.getValue() instanceof Set) {
139+
* Set<String> dates = new HashSet<>((Set<String>) entry.getValue());
140+
* if (!dates.isEmpty()) {
141+
* allDates.add(new ArrayList<>(dates));
142+
* }
143+
* }
144+
* }
145+
* ```
146+
*
147+
* @return An _immutable_ map of data currently present in the datastore.
148+
*/
149+
fun getAllSync(): Map<Preferences.Key<*>, Any> = runBlocking {
150+
dataStore.data.firstOrNull()?.asMap() ?: emptyMap()
151+
}
152+
153+
/**
154+
* Transactionally edit data in the datastore _synchronously_.
155+
*
156+
* Edits made within the [transform] callback will be saved (committed) all at once once the
157+
* [transform] block exits.
158+
*
159+
* Because of the blocking nature of this function, you should _never_ call [editSync] within an
160+
* already running [transform] block. Since this can cause a deadlock, [editSync] will instead
161+
* throw an exception if it's caught.
162+
*
163+
* Blocks on the currently running thread.
164+
*
165+
* Example:
166+
* ```java
167+
* dataStore.editSync((pref) -> {
168+
* Long heartBeatCount = pref.get(HEART_BEAT_COUNT_TAG);
169+
* if (heartBeatCount == null || heartBeatCount > 30) {
170+
* heartBeatCount = 0L;
171+
* }
172+
* pref.set(HEART_BEAT_COUNT_TAG, heartBeatCount);
173+
* pref.set(LAST_STORED_DATE, "1970-0-1");
174+
*
175+
* return null;
176+
* });
177+
* ```
178+
*
179+
* @param transform A callback to invoke with the [MutablePreferences] object.
180+
*
181+
* @return The [Preferences] object that the data was saved under.
182+
* @throws IllegalStateException If you attempt to call [editSync] within another [transform]
183+
* block.
184+
*
185+
* @see Preferences.getOrDefault
186+
*/
187+
fun editSync(transform: (MutablePreferences) -> Unit): Preferences = runBlocking {
188+
if (editLock.get() == true) {
189+
throw IllegalStateException(
190+
"""
191+
Don't call DataStorage.edit() from within an existing edit() callback.
192+
This causes deadlocks, and is generally indicative of a code smell.
193+
Instead, either pass around the initial `MutablePreferences` instance, or don't do everything in a single callback.
194+
"""
195+
.trimIndent()
196+
)
197+
}
198+
editLock.set(true)
199+
try {
200+
dataStore.edit { transform(it) }
201+
} finally {
202+
editLock.set(false)
203+
}
204+
}
205+
}
206+
207+
/**
208+
* Helper method for getting the value out of a [Preferences] object if it exists, else falling back
209+
* to the default value.
210+
*
211+
* This is primarily useful when working with an instance of [MutablePreferences]
212+
* - like when working within an [DataStorage.editSync] callback.
213+
*
214+
* Example:
215+
* ```java
216+
* dataStore.editSync((pref) -> {
217+
* long heartBeatCount = DataStoreKt.getOrDefault(pref, HEART_BEAT_COUNT_TAG, 0L);
218+
* heartBeatCount+=1;
219+
* pref.set(HEART_BEAT_COUNT_TAG, heartBeatCount);
220+
*
221+
* return null;
222+
* });
223+
* ```
224+
*
225+
* @param key The typed key of the entry to get data for.
226+
* @param defaultValue A value to default to, if the key isn't found.
227+
*/
228+
fun <T> Preferences.getOrDefault(key: Preferences.Key<T>, defaultValue: T) =
229+
get(key) ?: defaultValue

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.google.firebase.annotations.concurrent.Background;
2727
import com.google.firebase.components.Component;
2828
import com.google.firebase.components.Dependency;
29+
import com.google.firebase.components.Lazy;
2930
import com.google.firebase.components.Qualified;
3031
import com.google.firebase.inject.Provider;
3132
import com.google.firebase.platforminfo.UserAgentPublisher;
@@ -116,7 +117,7 @@ private DefaultHeartBeatController(
116117
Provider<UserAgentPublisher> userAgentProvider,
117118
Executor backgroundExecutor) {
118119
this(
119-
() -> new HeartBeatInfoStorage(context, persistenceKey),
120+
new Lazy<>(() -> new HeartBeatInfoStorage(context, persistenceKey)),
120121
consumers,
121122
backgroundExecutor,
122123
userAgentProvider,

0 commit comments

Comments
 (0)