Skip to content

Commit f56224c

Browse files
authored
Merge 5619a56 into f61a649
2 parents f61a649 + 5619a56 commit f56224c

File tree

8 files changed

+413
-135
lines changed

8 files changed

+413
-135
lines changed

firebase-common/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Unreleased
22
* [fixed] Correctly declare dependency on firebase-components, issue #5732
33
* [changed] Added extension method `Random.nextAlphanumericString()` (PR #5818)
4+
* [changed] Migrated internal `SharedPreferences` usages to `DataStore`. ([GitHub PR #6801](https://github.com/firebase/firebase-android-sdk/pull/6801){ .external})
45

56
# 20.4.0
67
* [changed] Added Kotlin extensions (KTX) APIs from `com.google.firebase:firebase-common-ktx`

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

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)