Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e8c9652
Add Remote Config support design spec
angelix Oct 7, 2026
b097227
Add Remote Config implementation plan
angelix Oct 7, 2026
078e587
Add file storage APIs to Context backed by FirebasePlatform.getFilesDir
angelix Oct 7, 2026
eafa96b
Persist Remote Config and Installations preferences through FirebaseP…
angelix Oct 7, 2026
e11bf62
Port android.util.JsonReader from AOSP for Firebase Installations
angelix Oct 7, 2026
4d4af3d
Register Remote Config and port firebase-kotlin-sdk Remote Config tests
angelix Oct 7, 2026
f3fd6f6
Add shims for Remote Config fetch through Firebase Installations
angelix Oct 7, 2026
be05659
Add DateUtils.formatElapsedTime shim for Remote Config throttling mes…
angelix Oct 7, 2026
44a91b3
Add heartbeat storage and log capture to Remote Config spec and plan
angelix Oct 7, 2026
35f42fd
Persist Firebase heartbeat preferences through FirebasePlatform
angelix Oct 7, 2026
d7f9857
Capture and assert expected Remote Config warnings in tests
angelix Oct 7, 2026
30153e6
Document Remote Config and Installations support
angelix Oct 7, 2026
f45bf82
Check Remote Config test logs after every test
angelix Oct 7, 2026
8644834
Document FirebasePlatform thread-safety and key format requirements
angelix Oct 7, 2026
ddd7d93
Remove Remote Config design notes from the branch
angelix Oct 7, 2026
7261541
Add preferences storage design spec
angelix Oct 7, 2026
1ee8b75
Add preferences storage implementation plan
angelix Oct 7, 2026
bc488d2
Store preferences as JSON files in getFilesDir
angelix Oct 7, 2026
51162f8
Document file-based preferences, label AOSP ports, and await the Stor…
angelix Oct 7, 2026
2fc8afb
Correct storage documentation and warn about the temp-folder default
angelix Oct 7, 2026
2bce8f6
Remove preferences storage design notes from the branch
angelix Oct 7, 2026
df5520d
Keep new preference values in memory when a write fails, as Android does
angelix Oct 7, 2026
25cffac
Sync preference files to disk and skip writes for commits that change…
angelix Oct 7, 2026
5f0b044
Skip the failed-write test where folders stay writable, and await the…
angelix Oct 7, 2026
a44711e
Create the database folder on demand, pin the default storage folders…
angelix Oct 7, 2026
53ab563
Give a plain Context an Application as its application context, so Fi…
angelix Oct 8, 2026
55329bc
Add createDeviceProtectedStorageContext so the data collection settin…
angelix Oct 8, 2026
4382b8a
Write preferences again after a failed or lost write, and test write …
angelix Oct 8, 2026
cb4bd1d
Give each test a data folder that outlives Firebase's background work…
angelix Oct 8, 2026
3f91e80
Check the Storage child reference's path instead of requesting its do…
angelix Oct 8, 2026
a6c8e45
Test that a written preferences file is read back with its types
angelix Oct 8, 2026
354e259
Check the stored heartbeat on disk instead of the heartbeat header
angelix Oct 8, 2026
29a54c9
Fail the preferences and Context file tests on unexpected logs
angelix Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,16 @@ You can skip the above if you are using the SDK via the <a href="https://github.
### Initializing the SDK

Before you can use the SDK you need to call the `FirebasePlatform.initializeFirebasePlatform` function to provide an implementation for
logging, and persistent storage for simple key value pairs. This is used by the various Firebase products, for example, to persist the
signed-in user in Firebase Auth.
logging, and persistent storage for simple key value pairs. This is used by Firebase Auth to persist the signed-in user.

Firebase Auth calls these methods from background threads, so your implementation must be thread-safe. The other Firebase
products store their data in files under `getDatabasePath()` and `getFilesDir()`, described below.

Here's a simple example implementation in Kotlin that only persists in-memory:

```kotlin
FirebasePlatform.initializeFirebasePlatform(object : FirebasePlatform() {
val storage = mutableMapOf<String, String>()
val storage = ConcurrentHashMap<String, String>()
override fun store(key: String, value: String) = storage.set(key, value)
override fun retrieve(key: String) = storage[key]
override fun clear(key: String) { storage.remove(key) }
Expand All @@ -69,6 +71,21 @@ The `FirebasePlatform` interface also includes a `getDatabasePath` method for yo

This is used by Firestore to support [offline data persistence](https://firebase.google.com/docs/firestore/manage-data/enable-offline).

#### Customizing file storage location

The `FirebasePlatform` interface also includes a `getFilesDir` method for you to override if the following default implementation is not suitable:

```kotlin
open fun getFilesDir(): File = File("${System.getProperty("java.io.tmpdir")}${File.separatorChar}firebase-files")
```

This is used by Remote Config and Installations to persist fetched configs and the installation ID, and by all Firebase
products to store their preferences, as JSON files in a `shared_prefs` subfolder.

The default is in the OS temp folder, which the OS may clean up one file at a time. These files only make sense together: if
Remote Config's fetched configs are removed while its preferences remain, it can serve default values until the template next
changes. Production apps should override `getFilesDir()` with a persistent folder of their own.

#### Initialize the Firebase application

It is also up to you to initialize the Firebase application object manually (unlike the Android SDK which is normally initialized via
Expand Down Expand Up @@ -111,11 +128,11 @@ The following libraries are available for the various Firebase products.
| [Cloud Firestore](https://firebase.google.com/docs/firestore) | `24.10.0` |
| [Realtime Database](https://firebase.google.com/docs/database) | `20.3.0` |
| [Cloud Functions](https://firebase.google.com/docs/functions) | `20.4.0` |
| ~[Remote Config](https://firebase.google.com/docs/remote-config)~ | `21.6.0`[^2] |
| ~[Installations](https://firebase.google.com/docs/projects/manage-installations)~ | `17.2.0`[^2] |
| [Remote Config](https://firebase.google.com/docs/remote-config) | `21.6.0`[^2] |
| [Installations](https://firebase.google.com/docs/projects/manage-installations) | `17.2.0` |

[^1]: Google has not open-sourced the Firebase Auth implementation for Android so a basic implementation using the Rest API is provided.
[^2]: Although the libraries are inlcuded they are currently not funtional
[^2]: Realtime updates (`addOnConfigUpdateListener`) and defaults from XML resources are not supported.

Is the Firebase library or API you need missing? [Create an issue](https://github.com/GitLiveApp/firebase-java-sdk/issues/new?labels=API+coverage&template=increase-api-coverage.md&title=Add+%5Bclass+name%5D.%5Bfunction+name%5D+to+%5Blibrary+name) to request additional API coverage or be awesome and [submit a PR](https://github.com/GitLiveApp/firebase-java-sdk/fork).

Expand Down
5 changes: 5 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ tasks {
javadoc {
exclude("android/**", "libcore/util/**")
}
test {
// Tests keep Firebase's data in per-test folders here until the next run
val testData = layout.buildDirectory.dir("test-data")
doFirst { delete(testData) }
}
}

val jar by tasks.getting(Jar::class) {
Expand Down
107 changes: 36 additions & 71 deletions src/main/java/android/content/Context.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package android.content

import android.content.SharedPreferences.Editor
import android.app.Application
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.res.Resources
Expand All @@ -10,11 +10,17 @@ import android.os.PowerManager
import android.os.UserManager
import com.google.firebase.FirebasePlatform
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.net.URLEncoder

open class Context {

// Firebase casts the application context to Application, so a plain Context provides one
val applicationContext: Context
get() = this
get() = this as? Application ?: application

private val application: Application by lazy { Application() }

val mainLooper: Looper
get() = Looper.getMainLooper()
Expand All @@ -31,6 +37,9 @@ open class Context {
val isDeviceProtectedStorage: Boolean
get() = false

// The JVM has no separate device-protected storage, so this context already is it
fun createDeviceProtectedStorageContext(): Context = this

val noBackupFilesDir: File
get() = File(System.getProperty("java.io.tmpdir"))

Expand All @@ -42,74 +51,8 @@ open class Context {

val applicationInfo: ApplicationInfo = ApplicationInfo()

fun getSharedPreferences(name: String, mode: Int): SharedPreferences {
return object : SharedPreferences {
override fun contains(key: String): Boolean {
when (key) {
"firebase_data_collection_default_enabled" -> return false
"auto_init" -> return false
"firebase_messaging_auto_init_enabled" -> return false
"fire-fst" -> return false
"fire-global" -> return !FirebasePlatform.firebasePlatform.retrieve(key).isNullOrEmpty()
}
throw IllegalArgumentException(key)
}

override fun getString(key: String, defaultValue: String?): String? {
return when {
key == "last-used-date" -> FirebasePlatform.firebasePlatform.retrieve(key) ?: defaultValue
key.contains("|T|") -> null
key.startsWith("com.google.firebase.auth.FIREBASE_USER") ->
FirebasePlatform.firebasePlatform.retrieve(key) ?: defaultValue
else -> throw IllegalArgumentException(key)
}
}

override fun getLong(key: String?, defaultValue: Long): Long {
when (key) {
"fire-global" -> return FirebasePlatform.firebasePlatform.retrieve(key)?.toLong() ?: defaultValue
}
throw IllegalArgumentException(key)
}

override fun getAll(): Map<String, String> {
return emptyMap()
}

override fun edit(): Editor {
return object : Editor {
override fun putLong(key: String?, value: Long): Editor {
when (key) {
"fire-global" -> FirebasePlatform.firebasePlatform.store(key, value.toString())
else -> throw IllegalArgumentException(key)
}
return this
}

override fun putString(key: String?, value: String?): Editor {
when (key) {
"last-used-date" -> FirebasePlatform.firebasePlatform.store(key, value.toString())
else -> if (key?.startsWith("com.google.firebase.auth.FIREBASE_USER") == true) {
FirebasePlatform.firebasePlatform.store(key, value.toString())
} else {
throw IllegalArgumentException(key)
}
}
return this
}

override fun commit(): Boolean {
// Don't need to commit as changes are committed in the put method
return true
}

override fun apply() {
// Don't need to apply as changes are applied in the put method
}
}
}
}
}
fun getSharedPreferences(name: String, mode: Int): SharedPreferences =
PreferencesFile.at(File(File(filesDir, "shared_prefs"), "${encodeFileName(name)}.json"))

fun getSystemService(name: String): Any {
when (name) {
Expand All @@ -130,10 +73,32 @@ open class Context {
return File(System.getProperty("java.io.tmpdir"))
}

fun getDatabasePath(name: String): File = FirebasePlatform.firebasePlatform.getDatabasePath(name)
// Creates the folder, because the SQLite driver cannot create a database in a missing folder
fun getDatabasePath(name: String): File =
FirebasePlatform.firebasePlatform.getDatabasePath(name).apply { parentFile?.mkdirs() }

val filesDir: File
get() = FirebasePlatform.firebasePlatform.getFilesDir().apply { mkdirs() }

fun openFileInput(name: String): FileInputStream = FileInputStream(fileStreamPath(name))

fun openFileOutput(name: String, mode: Int): FileOutputStream =
FileOutputStream(fileStreamPath(name), mode and MODE_APPEND != 0)

fun deleteFile(name: String): Boolean = fileStreamPath(name).delete()

private fun fileStreamPath(name: String): File {
require('/' !in name && File.separatorChar !in name) { "File $name contains a path separator" }
return File(filesDir, encodeFileName(name))
}

// URL-encodes the name so characters such as ':' in Firebase app IDs are valid on every OS
private fun encodeFileName(name: String): String = URLEncoder.encode(name, Charsets.UTF_8)

companion object {
@JvmStatic
val CONNECTIVITY_SERVICE = "connectivity"

const val MODE_APPEND = 0x8000
}
}
Loading