diff --git a/firebase-common/firebase-common.gradle.kts b/firebase-common/firebase-common.gradle.kts index 88bfa58e27d..52114f82558 100644 --- a/firebase-common/firebase-common.gradle.kts +++ b/firebase-common/firebase-common.gradle.kts @@ -57,6 +57,7 @@ dependencies { api("com.google.firebase:firebase-components:18.0.0") api("com.google.firebase:firebase-annotations:16.2.0") + implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.annotation) implementation(libs.androidx.futures) implementation(libs.kotlin.stdlib) diff --git a/firebase-common/src/main/java/com/google/firebase/datastore/DataStore.kt b/firebase-common/src/main/java/com/google/firebase/datastore/DataStore.kt new file mode 100644 index 00000000000..831700f75a4 --- /dev/null +++ b/firebase-common/src/main/java/com/google/firebase/datastore/DataStore.kt @@ -0,0 +1,229 @@ +/* + * 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.datastore + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.SharedPreferencesMigration +import androidx.datastore.preferences.core.MutablePreferences +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.runBlocking + +/** + * Wrapper around [DataStore] for easier migration from `SharedPreferences` in Java code. + * + * Automatically migrates data from any `SharedPreferences` that share the same context and name. + * + * There should only ever be _one_ instance of this class per context and name variant. + * + * > Do **NOT** use this _unless_ you're bridging Java code. If you're writing new code, or your + * code is in Kotlin, then you should create your own singleton that uses [DataStore] directly. + * + * Example: + * ```java + * DataStorage heartBeatStorage = new DataStorage(applicationContext, "FirebaseHeartBeat"); + * ``` + * + * @property context The [Context] that this data will be saved under. + * @property name What the storage file should be named. + */ +class DataStorage(val context: Context, val name: String) { + /** + * Used to ensure that there's only ever one call to [editSync] per thread; as to avoid deadlocks. + */ + private val editLock = ThreadLocal() + + private val Context.dataStore: DataStore by + preferencesDataStore( + name = name, + produceMigrations = { listOf(SharedPreferencesMigration(it, name)) } + ) + + private val dataStore = context.dataStore + + /** + * Get data from the datastore _synchronously_. + * + * Note that if the key is _not_ in the datastore, while the [defaultValue] will be returned + * instead- it will **not** be saved to the datastore; you'll have to manually do that. + * + * Blocks on the currently running thread. + * + * Example: + * ```java + * Preferences.Key fireCountKey = PreferencesKeys.longKey("fire-count"); + * assert dataStore.get(fireCountKey, 0L) == 0L; + * + * dataStore.putSync(fireCountKey, 102L); + * assert dataStore.get(fireCountKey, 0L) == 102L; + * ``` + * + * @param key The typed key of the entry to get data for. + * @param defaultValue A value to default to, if the key isn't found. + * + * @see Preferences.getOrDefault + */ + fun getSync(key: Preferences.Key, defaultValue: T): T = runBlocking { + dataStore.data.firstOrNull()?.get(key) ?: defaultValue + } + + /** + * Checks if a key is present in the datastore _synchronously_. + * + * Blocks on the currently running thread. + * + * Example: + * ```java + * Preferences.Key fireCountKey = PreferencesKeys.longKey("fire-count"); + * assert !dataStore.contains(fireCountKey); + * + * dataStore.putSync(fireCountKey, 102L); + * assert dataStore.contains(fireCountKey); + * ``` + * + * @param key The typed key of the entry to find. + */ + fun contains(key: Preferences.Key): Boolean = runBlocking { + dataStore.data.firstOrNull()?.contains(key) ?: false + } + + /** + * Sets and saves data in the datastore _synchronously_. + * + * Existing values will be overwritten. + * + * Blocks on the currently running thread. + * + * Example: + * ```java + * dataStore.putSync(PreferencesKeys.longKey("fire-count"), 102L); + * ``` + * + * @param key The typed key of the entry to save the data under. + * @param value The data to save. + * + * @return The [Preferences] object that the data was saved under. + */ + fun putSync(key: Preferences.Key, value: T): Preferences = runBlocking { + dataStore.edit { it[key] = value } + } + + /** + * Gets all data in the datastore _synchronously_. + * + * Blocks on the currently running thread. + * + * Example: + * ```java + * ArrayList allDates = new ArrayList<>(); + * + * for (Map.Entry, Object> entry : dataStore.getAllSync().entrySet()) { + * if (entry.getValue() instanceof Set) { + * Set dates = new HashSet<>((Set) entry.getValue()); + * if (!dates.isEmpty()) { + * allDates.add(new ArrayList<>(dates)); + * } + * } + * } + * ``` + * + * @return An _immutable_ map of data currently present in the datastore. + */ + fun getAllSync(): Map, Any> = runBlocking { + dataStore.data.firstOrNull()?.asMap() ?: emptyMap() + } + + /** + * Transactionally edit data in the datastore _synchronously_. + * + * Edits made within the [transform] callback will be saved (committed) all at once once the + * [transform] block exits. + * + * Because of the blocking nature of this function, you should _never_ call [editSync] within an + * already running [transform] block. Since this can cause a deadlock, [editSync] will instead + * throw an exception if it's caught. + * + * Blocks on the currently running thread. + * + * Example: + * ```java + * dataStore.editSync((pref) -> { + * Long heartBeatCount = pref.get(HEART_BEAT_COUNT_TAG); + * if (heartBeatCount == null || heartBeatCount > 30) { + * heartBeatCount = 0L; + * } + * pref.set(HEART_BEAT_COUNT_TAG, heartBeatCount); + * pref.set(LAST_STORED_DATE, "1970-0-1"); + * + * return null; + * }); + * ``` + * + * @param transform A callback to invoke with the [MutablePreferences] object. + * + * @return The [Preferences] object that the data was saved under. + * @throws IllegalStateException If you attempt to call [editSync] within another [transform] + * block. + * + * @see Preferences.getOrDefault + */ + fun editSync(transform: (MutablePreferences) -> Unit): Preferences = runBlocking { + if (editLock.get() == true) { + throw IllegalStateException( + """ + Don't call DataStorage.edit() from within an existing edit() callback. + This causes deadlocks, and is generally indicative of a code smell. + Instead, either pass around the initial `MutablePreferences` instance, or don't do everything in a single callback. + """ + .trimIndent() + ) + } + editLock.set(true) + try { + dataStore.edit { transform(it) } + } finally { + editLock.set(false) + } + } +} + +/** + * Helper method for getting the value out of a [Preferences] object if it exists, else falling back + * to the default value. + * + * This is primarily useful when working with an instance of [MutablePreferences] + * - like when working within an [DataStorage.editSync] callback. + * + * Example: + * ```java + * dataStore.editSync((pref) -> { + * long heartBeatCount = DataStoreKt.getOrDefault(pref, HEART_BEAT_COUNT_TAG, 0L); + * heartBeatCount+=1; + * pref.set(HEART_BEAT_COUNT_TAG, heartBeatCount); + * + * return null; + * }); + * ``` + * + * @param key The typed key of the entry to get data for. + * @param defaultValue A value to default to, if the key isn't found. + */ +fun Preferences.getOrDefault(key: Preferences.Key, defaultValue: T) = + get(key) ?: defaultValue diff --git a/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatController.java b/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatController.java index ff9ba5123fc..7aa8cfb613d 100644 --- a/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatController.java +++ b/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatController.java @@ -26,6 +26,7 @@ import com.google.firebase.annotations.concurrent.Background; import com.google.firebase.components.Component; import com.google.firebase.components.Dependency; +import com.google.firebase.components.Lazy; import com.google.firebase.components.Qualified; import com.google.firebase.inject.Provider; import com.google.firebase.platforminfo.UserAgentPublisher; @@ -116,7 +117,7 @@ private DefaultHeartBeatController( Provider userAgentProvider, Executor backgroundExecutor) { this( - () -> new HeartBeatInfoStorage(context, persistenceKey), + new Lazy<>(() -> new HeartBeatInfoStorage(context, persistenceKey)), consumers, backgroundExecutor, userAgentProvider, diff --git a/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/HeartBeatInfoStorage.java b/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/HeartBeatInfoStorage.java index a8a9fee5104..0d59f0103c4 100644 --- a/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/HeartBeatInfoStorage.java +++ b/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/HeartBeatInfoStorage.java @@ -15,10 +15,13 @@ package com.google.firebase.heartbeatinfo; import android.content.Context; -import android.content.SharedPreferences; import android.os.Build; import androidx.annotation.RestrictTo; import androidx.annotation.VisibleForTesting; +import androidx.datastore.preferences.core.*; +import androidx.datastore.preferences.core.Preferences; +import com.google.firebase.datastore.DataStorage; +import com.google.firebase.datastore.DataStoreKt; import java.text.SimpleDateFormat; import java.time.Instant; import java.time.LocalDateTime; @@ -40,91 +43,98 @@ class HeartBeatInfoStorage { private static HeartBeatInfoStorage instance = null; - private static final String GLOBAL = "fire-global"; + private static final Preferences.Key GLOBAL = PreferencesKeys.longKey("fire-global"); private static final String PREFERENCES_NAME = "FirebaseAppHeartBeat"; private static final String HEARTBEAT_PREFERENCES_NAME = "FirebaseHeartBeat"; - private static final String HEART_BEAT_COUNT_TAG = "fire-count"; + private static final Preferences.Key HEART_BEAT_COUNT_TAG = + PreferencesKeys.longKey("fire-count"); - private static final String LAST_STORED_DATE = "last-used-date"; + private static final Preferences.Key LAST_STORED_DATE = + PreferencesKeys.stringKey("last-used-date"); // As soon as you hit the limit of heartbeats. The number of stored heartbeats is halved. private static final int HEART_BEAT_COUNT_LIMIT = 30; - private final SharedPreferences firebaseSharedPreferences; + private final DataStorage firebaseDataStore; public HeartBeatInfoStorage(Context applicationContext, String persistenceKey) { - this.firebaseSharedPreferences = - applicationContext.getSharedPreferences( - HEARTBEAT_PREFERENCES_NAME + persistenceKey, Context.MODE_PRIVATE); + this.firebaseDataStore = + new DataStorage(applicationContext, HEARTBEAT_PREFERENCES_NAME + persistenceKey); } @VisibleForTesting @RestrictTo(RestrictTo.Scope.TESTS) - HeartBeatInfoStorage(SharedPreferences firebaseSharedPreferences) { - this.firebaseSharedPreferences = firebaseSharedPreferences; + HeartBeatInfoStorage(DataStorage dataStorage) { + this.firebaseDataStore = dataStorage; } @VisibleForTesting @RestrictTo(RestrictTo.Scope.TESTS) int getHeartBeatCount() { - return (int) this.firebaseSharedPreferences.getLong(HEART_BEAT_COUNT_TAG, 0); + return this.firebaseDataStore.getSync(HEART_BEAT_COUNT_TAG, 0L).intValue(); } synchronized void deleteAllHeartBeats() { - SharedPreferences.Editor editor = firebaseSharedPreferences.edit(); - int counter = 0; - for (Map.Entry entry : this.firebaseSharedPreferences.getAll().entrySet()) { - if (entry.getValue() instanceof Set) { - // All other heartbeats other than the heartbeats stored today will be deleted. - Set dates = (Set) entry.getValue(); - String today = getFormattedDate(System.currentTimeMillis()); - String key = entry.getKey(); - if (dates.contains(today)) { - Set userAgentDateSet = new HashSet<>(); - userAgentDateSet.add(today); - counter += 1; - editor.putStringSet(key, userAgentDateSet); - } else { - editor.remove(key); - } - } - } - if (counter == 0) { - editor.remove(HEART_BEAT_COUNT_TAG); - } else { - editor.putLong(HEART_BEAT_COUNT_TAG, counter); - } + firebaseDataStore.editSync( + (pref) -> { + long counter = 0; + for (Map.Entry, Object> entry : pref.asMap().entrySet()) { + if (entry.getValue() instanceof Set) { + // All other heartbeats other than the heartbeats stored today will be deleted. + Preferences.Key> key = (Preferences.Key>) entry.getKey(); + Set dates = (Set) entry.getValue(); + String today = getFormattedDate(System.currentTimeMillis()); + + if (dates.contains(today)) { + pref.set(key, Set.of(today)); + counter += 1; + } else { + pref.remove(key); + } + } + } + if (counter == 0) { + pref.remove(HEART_BEAT_COUNT_TAG); + } else { + pref.set(HEART_BEAT_COUNT_TAG, counter); + } - editor.commit(); + return null; + }); } synchronized List getAllHeartBeats() { ArrayList heartBeatResults = new ArrayList<>(); - for (Map.Entry entry : this.firebaseSharedPreferences.getAll().entrySet()) { + String today = getFormattedDate(System.currentTimeMillis()); + + for (Map.Entry, Object> entry : + this.firebaseDataStore.getAllSync().entrySet()) { if (entry.getValue() instanceof Set) { Set dates = new HashSet<>((Set) entry.getValue()); - String today = getFormattedDate(System.currentTimeMillis()); dates.remove(today); if (!dates.isEmpty()) { heartBeatResults.add( - HeartBeatResult.create(entry.getKey(), new ArrayList(dates))); + HeartBeatResult.create(entry.getKey().getName(), new ArrayList<>(dates))); } } } + updateGlobalHeartBeat(System.currentTimeMillis()); + return heartBeatResults; } - private synchronized String getStoredUserAgentString(String dateString) { - for (Map.Entry entry : firebaseSharedPreferences.getAll().entrySet()) { + private synchronized Preferences.Key> getStoredUserAgentString( + MutablePreferences preferences, String dateString) { + for (Map.Entry, Object> entry : preferences.asMap().entrySet()) { if (entry.getValue() instanceof Set) { Set dateSet = (Set) entry.getValue(); for (String date : dateSet) { if (dateString.equals(date)) { - return entry.getKey(); + return PreferencesKeys.stringSetKey(entry.getKey().getName()); } } } @@ -132,36 +142,40 @@ private synchronized String getStoredUserAgentString(String dateString) { return null; } - private synchronized void updateStoredUserAgent(String userAgent, String dateString) { - removeStoredDate(dateString); + private synchronized void updateStoredUserAgent( + MutablePreferences preferences, Preferences.Key> userAgent, String dateString) { + removeStoredDate(preferences, dateString); Set userAgentDateSet = - new HashSet( - firebaseSharedPreferences.getStringSet(userAgent, new HashSet())); + new HashSet<>(DataStoreKt.getOrDefault(preferences, userAgent, new HashSet<>())); userAgentDateSet.add(dateString); - firebaseSharedPreferences.edit().putStringSet(userAgent, userAgentDateSet).commit(); + preferences.set(userAgent, userAgentDateSet); } - private synchronized void removeStoredDate(String dateString) { + private synchronized void removeStoredDate(MutablePreferences preferences, String dateString) { // Find stored heartbeat and clear it. - String userAgentString = getStoredUserAgentString(dateString); - if (userAgentString == null) { + Preferences.Key> userAgent = getStoredUserAgentString(preferences, dateString); + if (userAgent == null) { return; } Set userAgentDateSet = - new HashSet( - firebaseSharedPreferences.getStringSet(userAgentString, new HashSet())); + new HashSet<>(DataStoreKt.getOrDefault(preferences, userAgent, new HashSet<>())); userAgentDateSet.remove(dateString); if (userAgentDateSet.isEmpty()) { - firebaseSharedPreferences.edit().remove(userAgentString).commit(); + preferences.remove(userAgent); } else { - firebaseSharedPreferences.edit().putStringSet(userAgentString, userAgentDateSet).commit(); + preferences.set(userAgent, userAgentDateSet); } } synchronized void postHeartBeatCleanUp() { String dateString = getFormattedDate(System.currentTimeMillis()); - firebaseSharedPreferences.edit().putString(LAST_STORED_DATE, dateString).commit(); - removeStoredDate(dateString); + + firebaseDataStore.editSync( + (pref) -> { + pref.set(LAST_STORED_DATE, dateString); + removeStoredDate(pref, dateString); + return null; + }); } private synchronized String getFormattedDate(long millis) { @@ -176,71 +190,77 @@ private synchronized String getFormattedDate(long millis) { synchronized void storeHeartBeat(long millis, String userAgentString) { String dateString = getFormattedDate(millis); - String lastDateString = firebaseSharedPreferences.getString(LAST_STORED_DATE, ""); - if (lastDateString.equals(dateString)) { - String storedUserAgentString = getStoredUserAgentString(dateString); - if (storedUserAgentString == null) { - // Heartbeat already sent for today. - return; - } - if (storedUserAgentString.equals(userAgentString)) { - // UserAgent not updated. - return; - } else { - updateStoredUserAgent(userAgentString, dateString); - return; - } - } - long heartBeatCount = firebaseSharedPreferences.getLong(HEART_BEAT_COUNT_TAG, 0); - if (heartBeatCount + 1 == HEART_BEAT_COUNT_LIMIT) { - cleanUpStoredHeartBeats(); - heartBeatCount = firebaseSharedPreferences.getLong(HEART_BEAT_COUNT_TAG, 0); - } - Set userAgentDateSet = - new HashSet( - firebaseSharedPreferences.getStringSet(userAgentString, new HashSet())); - userAgentDateSet.add(dateString); - heartBeatCount += 1; - firebaseSharedPreferences - .edit() - .putStringSet(userAgentString, userAgentDateSet) - .putLong(HEART_BEAT_COUNT_TAG, heartBeatCount) - .putString(LAST_STORED_DATE, dateString) - .commit(); + Preferences.Key> userAgent = PreferencesKeys.stringSetKey(userAgentString); + firebaseDataStore.editSync( + (pref) -> { + String lastDateString = DataStoreKt.getOrDefault(pref, LAST_STORED_DATE, ""); + if (lastDateString.equals(dateString)) { + Preferences.Key> storedUserAgent = + getStoredUserAgentString(pref, dateString); + if (storedUserAgent == null) { + // Heartbeat already sent for today. + return null; + } else if (storedUserAgent.getName().equals(userAgentString)) { + // UserAgent not updated. + return null; + } else { + updateStoredUserAgent(pref, userAgent, dateString); + return null; + } + } + long heartBeatCount = DataStoreKt.getOrDefault(pref, HEART_BEAT_COUNT_TAG, 0L); + if (heartBeatCount + 1 == HEART_BEAT_COUNT_LIMIT) { + heartBeatCount = cleanUpStoredHeartBeats(pref); + } + Set userAgentDateSet = + new HashSet<>(DataStoreKt.getOrDefault(pref, userAgent, new HashSet<>())); + userAgentDateSet.add(dateString); + heartBeatCount += 1; + + pref.set(userAgent, userAgentDateSet); + pref.set(HEART_BEAT_COUNT_TAG, heartBeatCount); + pref.set(LAST_STORED_DATE, dateString); + + return null; + }); } - private synchronized void cleanUpStoredHeartBeats() { - long heartBeatCount = firebaseSharedPreferences.getLong(HEART_BEAT_COUNT_TAG, 0); + private synchronized long cleanUpStoredHeartBeats(MutablePreferences preferences) { + long heartBeatCount = DataStoreKt.getOrDefault(preferences, HEART_BEAT_COUNT_TAG, 0L); + String lowestDate = null; String userAgentString = ""; - for (Map.Entry entry : firebaseSharedPreferences.getAll().entrySet()) { + Set userAgentDateSet = new HashSet<>(); + for (Map.Entry, Object> entry : preferences.asMap().entrySet()) { if (entry.getValue() instanceof Set) { Set dateSet = (Set) entry.getValue(); for (String date : dateSet) { if (lowestDate == null || lowestDate.compareTo(date) > 0) { + userAgentDateSet = dateSet; lowestDate = date; - userAgentString = entry.getKey(); + userAgentString = entry.getKey().getName(); } } } } - Set userAgentDateSet = - new HashSet( - firebaseSharedPreferences.getStringSet(userAgentString, new HashSet())); + userAgentDateSet = new HashSet<>(userAgentDateSet); userAgentDateSet.remove(lowestDate); - firebaseSharedPreferences - .edit() - .putStringSet(userAgentString, userAgentDateSet) - .putLong(HEART_BEAT_COUNT_TAG, heartBeatCount - 1) - .commit(); + preferences.set(PreferencesKeys.stringSetKey(userAgentString), userAgentDateSet); + preferences.set(HEART_BEAT_COUNT_TAG, heartBeatCount - 1); + + return heartBeatCount - 1; } synchronized long getLastGlobalHeartBeat() { - return firebaseSharedPreferences.getLong(GLOBAL, -1); + return firebaseDataStore.getSync(GLOBAL, -1L); } synchronized void updateGlobalHeartBeat(long millis) { - firebaseSharedPreferences.edit().putLong(GLOBAL, millis).commit(); + firebaseDataStore.editSync( + (pref) -> { + pref.set(GLOBAL, millis); + return null; + }); } synchronized boolean isSameDateUtc(long base, long target) { @@ -252,15 +272,11 @@ synchronized boolean isSameDateUtc(long base, long target) { A sdk heartbeat is sent either when there is no heartbeat sent ever for the sdk or when the last heartbeat send for the sdk was later than a day before. */ - synchronized boolean shouldSendSdkHeartBeat(String heartBeatTag, long millis) { - if (firebaseSharedPreferences.contains(heartBeatTag)) { - if (!this.isSameDateUtc(firebaseSharedPreferences.getLong(heartBeatTag, -1), millis)) { - firebaseSharedPreferences.edit().putLong(heartBeatTag, millis).commit(); - return true; - } + synchronized boolean shouldSendSdkHeartBeat(Preferences.Key heartBeatTag, long millis) { + if (this.isSameDateUtc(firebaseDataStore.getSync(heartBeatTag, -1L), millis)) { return false; } else { - firebaseSharedPreferences.edit().putLong(heartBeatTag, millis).commit(); + firebaseDataStore.putSync(heartBeatTag, millis); return true; } } diff --git a/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatControllerTest.java b/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatControllerTest.java index fe564ca242e..a57c1768f66 100644 --- a/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatControllerTest.java +++ b/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatControllerTest.java @@ -25,10 +25,10 @@ import static org.mockito.Mockito.when; import android.content.Context; -import android.content.SharedPreferences; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableSet; +import com.google.firebase.datastore.DataStorage; import com.google.firebase.platforminfo.UserAgentPublisher; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -107,10 +107,8 @@ public void generateHeartBeat_oneHeartBeat() throws InterruptedException, Timeou public void firstNewThenOld_synchronizedCorrectly() throws InterruptedException, TimeoutException { Context context = ApplicationProvider.getApplicationContext(); - SharedPreferences heartBeatSharedPreferences = - context.getSharedPreferences("testHeartBeat", Context.MODE_PRIVATE); - HeartBeatInfoStorage heartBeatInfoStorage = - new HeartBeatInfoStorage(heartBeatSharedPreferences); + DataStorage heartBeatDataStore = new DataStorage(context, "testHeartBeat"); + HeartBeatInfoStorage heartBeatInfoStorage = new HeartBeatInfoStorage(heartBeatDataStore); DefaultHeartBeatController controller = new DefaultHeartBeatController( () -> heartBeatInfoStorage, logSources, executor, () -> publisher, context); @@ -130,10 +128,8 @@ public void firstNewThenOld_synchronizedCorrectly() public void firstOldThenNew_synchronizedCorrectly() throws InterruptedException, TimeoutException { Context context = ApplicationProvider.getApplicationContext(); - SharedPreferences heartBeatSharedPreferences = - context.getSharedPreferences("testHeartBeat", Context.MODE_PRIVATE); - HeartBeatInfoStorage heartBeatInfoStorage = - new HeartBeatInfoStorage(heartBeatSharedPreferences); + DataStorage heartBeatDataStore = new DataStorage(context, "testHeartBeat"); + HeartBeatInfoStorage heartBeatInfoStorage = new HeartBeatInfoStorage(heartBeatDataStore); DefaultHeartBeatController controller = new DefaultHeartBeatController( () -> heartBeatInfoStorage, logSources, executor, () -> publisher, context); diff --git a/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/HeartBeatInfoStorageTest.java b/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/HeartBeatInfoStorageTest.java index 81b191f117d..b83ba4d3523 100644 --- a/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/HeartBeatInfoStorageTest.java +++ b/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/HeartBeatInfoStorageTest.java @@ -17,12 +17,15 @@ import static com.google.common.truth.Truth.assertThat; import android.content.Context; -import android.content.SharedPreferences; +import androidx.datastore.preferences.core.Preferences; +import androidx.datastore.preferences.core.PreferencesKeys; import androidx.test.core.app.ApplicationProvider; import androidx.test.runner.AndroidJUnit4; +import com.google.firebase.datastore.DataStorage; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.Set; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -31,23 +34,30 @@ @RunWith(AndroidJUnit4.class) public class HeartBeatInfoStorageTest { - private final String testSdk = "testSdk"; - private final String GLOBAL = "fire-global"; + private final Preferences.Key testSdk = PreferencesKeys.longKey("testSdk"); + private final Preferences.Key GLOBAL = PreferencesKeys.longKey("fire-global"); private static final int HEART_BEAT_COUNT_LIMIT = 30; private static Context applicationContext = ApplicationProvider.getApplicationContext(); - private static SharedPreferences heartBeatSharedPreferences = - applicationContext.getSharedPreferences("testHeartBeat", Context.MODE_PRIVATE); - private HeartBeatInfoStorage heartBeatInfoStorage = - new HeartBeatInfoStorage(heartBeatSharedPreferences); + private static DataStorage heartBeatDataStore = + new DataStorage(applicationContext, "testHeartBeat"); + private HeartBeatInfoStorage heartBeatInfoStorage = new HeartBeatInfoStorage(heartBeatDataStore); @Before public void setUp() { - heartBeatSharedPreferences.edit().clear().apply(); + heartBeatDataStore.editSync( + (pref) -> { + pref.clear(); + return null; + }); } @After public void tearDown() { - heartBeatSharedPreferences.edit().clear().apply(); + heartBeatDataStore.editSync( + (pref) -> { + pref.clear(); + return null; + }); } @Config(sdk = 29) @@ -169,31 +179,32 @@ public void storeExcessHeartBeats_cleanUpProperly() { public void shouldSendSdkHeartBeat_answerIsYes() { long currentTime = System.currentTimeMillis(); assertThat(heartBeatInfoStorage.shouldSendSdkHeartBeat(testSdk, 1)).isTrue(); - assertThat(heartBeatSharedPreferences.getLong(testSdk, -1)).isEqualTo(1); + assertThat(heartBeatDataStore.getSync(testSdk, -1L)).isEqualTo(1); assertThat(heartBeatInfoStorage.shouldSendSdkHeartBeat(testSdk, currentTime)).isTrue(); - assertThat(heartBeatSharedPreferences.getLong(testSdk, -1)).isEqualTo(currentTime); + assertThat(heartBeatDataStore.getSync(testSdk, -1L)).isEqualTo(currentTime); } @Test public void shouldSendGlobalHeartBeat_answerIsNo() { - heartBeatSharedPreferences.edit().putLong(GLOBAL, 1).apply(); + heartBeatDataStore.putSync(GLOBAL, 1L); assertThat(heartBeatInfoStorage.shouldSendGlobalHeartBeat(1)).isFalse(); } @Test public void currentDayHeartbeatNotSent_updatesCorrectly() { long millis = System.currentTimeMillis(); + Preferences.Key> testAgent = PreferencesKeys.stringSetKey("test-agent"); + Preferences.Key> testAgent1 = PreferencesKeys.stringSetKey("test-agent-1"); assertThat(heartBeatInfoStorage.getHeartBeatCount()).isEqualTo(0); heartBeatInfoStorage.storeHeartBeat(millis, "test-agent"); assertThat(heartBeatInfoStorage.getHeartBeatCount()).isEqualTo(1); assertThat(heartBeatInfoStorage.getAllHeartBeats().size()).isEqualTo(0); heartBeatInfoStorage.deleteAllHeartBeats(); assertThat(heartBeatInfoStorage.getHeartBeatCount()).isEqualTo(1); - assertThat(heartBeatSharedPreferences.getStringSet("test-agent", new HashSet<>())).isNotEmpty(); + assertThat(heartBeatDataStore.getSync(testAgent, new HashSet<>())).isNotEmpty(); heartBeatInfoStorage.storeHeartBeat(millis, "test-agent-1"); - assertThat(heartBeatSharedPreferences.getStringSet("test-agent", new HashSet<>())).isEmpty(); - assertThat(heartBeatSharedPreferences.getStringSet("test-agent-1", new HashSet<>())) - .isNotEmpty(); + assertThat(heartBeatDataStore.getSync(testAgent, new HashSet<>())).isEmpty(); + assertThat(heartBeatDataStore.getSync(testAgent1, new HashSet<>())).isNotEmpty(); } @Test @@ -222,8 +233,8 @@ public void isSameDate_returnsCorrectly() { public void shouldSendGlobalHeartBeat_answerIsYes() { long currentTime = System.currentTimeMillis(); assertThat(heartBeatInfoStorage.shouldSendGlobalHeartBeat(1)).isTrue(); - assertThat(heartBeatSharedPreferences.getLong(GLOBAL, -1)).isEqualTo(1); + assertThat(heartBeatDataStore.getSync(GLOBAL, -1L)).isEqualTo(1); assertThat(heartBeatInfoStorage.shouldSendGlobalHeartBeat(currentTime)).isTrue(); - assertThat(heartBeatSharedPreferences.getLong(GLOBAL, -1)).isEqualTo(currentTime); + assertThat(heartBeatDataStore.getSync(GLOBAL, -1L)).isEqualTo(currentTime); } }