I have been working to solve this problem for like one & a half day and couldn't find any solution to it.
Working on below Android Lollipop and others.
Tried mostly all the solution.
Even FailureListener is not triggered.
Code:
DatabaseReference mFirebaseRef = FirebaseDatabase.getInstance().getReferenceFromUrl(constants.FIREBASE_URL+"rootssahaj/authGplus/users/teachers");
ProfileInformation pro= new ProfileInformation(email,personName,personPhotoUrll,personGooglePlusProfile,personPhotoUrllThumb,"true");
Log.e("SahajLOGwq", "CalledInside" +prefs.getBoolean("callProfileToFireBase",true)+" Email: "+EmailPref +pro+" yo > "+pro.getUserNAME());
mFirebaseRef.child(EmailPref).child("profile").setValue(pro, new DatabaseReference.CompletionListener() {
#Override
public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) {
if (databaseError != null) {
Log.e("SahajLOGwq", "Data could not be saved. " + databaseError.getMessage());
} else {
Log.e("SahajLOGwq", "Data saved successfully.");
}
}
});
Logcat:
08-07 01:18:32.866 30547-30547/com.rana.sahaj.myyu E/SahajLOGwq: NOTICE__android USER SIGNED IN
08-07 01:18:32.921 30547-30547/com.rana.sahaj.myyu E/SahajLOGwq: CalledOutsidetrue
08-07 01:18:32.922 30547-30547/com.rana.sahaj.myyu E/SahajLOGwq: CalledInsidetrue Email: narveshrana68 com.rana.sahaj.myyu.profile.ProfileInformation#e6ddcfd yo > Narvesh Rana
*Rules for now
{
"rules": {
".read": true,
".write": true
}
}
*Edit:
this is what I found after Debug LogLevel of Firebase
08-07 18:22:08.275 8863-9013/com.rana.sahaj.myyu D/RepoOperation: set: /testforwrite
08-07 18:22:08.275 8863-9013/com.rana.sahaj.myyu D/DataOperation: set: /testforwrite true
08-07 18:22:08.289 8863-9013/com.rana.sahaj.myyu D/RepoOperation: Aborting transactions for path: /testforwrite. Affected: /testforwrite
08-07 18:22:08.290 8863-9013/com.rana.sahaj.myyu D/RepoOperation: set: /rootssahaj/authGplus/users/teachers/narveshrana68/profile
08-07 18:22:08.291 8863-9013/com.rana.sahaj.myyu D/DataOperation: set: /rootssahaj/authGplus/users/teachers/narveshrana68/profile {
isTeacher=true
picurl=https://l400
picurl50DP=https://lh230
userEmail=na.om
userNAME=Narvesh Rana
}
08-07 18:22:08.296 8863-9013/com.rana.sahaj.myyu D/RepoOperation: Aborting transactions for path: /rootssahaj/
I am able to reproduce this and get the same D/RepoOperation: Aborting transactions for path: /testforwrite. Affected: /testforwrite on Lollipop when I am connected to wifi, disable the actual internet connectivity, and then re-enable internet connection (all with the device remaining connected to wifi network). After this, if I try to perform a setValue() write, about 18 minutes will pass before Firebase reconnects and the setValue() completes. I reached out to the Firebase team and was told this is expected behavior due to the Android OS basically neglecting to informing apps when the connection is restored.
The only way to resolve in my experience is either disabling and re-enabling WiFi, or restarting the app (restarting the activity won't help).
Firebase support got back to me and suggested calling goOffline() followed by goOnline() - I verified this immediately reconnects the Firebase database. What I do now is set a Handler with my own timeout for setValue(), since I can't seem to depend on DatabaseError being thrown with onComplete().
Related
My android application crashes when i call signout upon the fitebaseauth instance,
below is the logout call...
logout.setOnClickListener {
logoutAlertDialog.dismiss()
binding.msgLl.isVisible = true
binding.msgTv.isVisible = true
binding.msgTv.text = "Logging out"
binding.progressBar.isVisible = true
Handler(Looper.getMainLooper()).postDelayed({
FirebaseAuth.getInstance().signOut()
requireContext().startActivity(Intent(requireContext(),MainActivity::class.java))
requireActivity().finish()
},3000)
this is the code and
2022-07-13 13:17:50.446 5496-5496/com.aman.ache D/FirebaseAuth: Notifying id token listeners about a sign-out event.
2022-07-13 13:17:50.447 5496-5496/com.aman.ache D/FirebaseAuth: Notifying auth state listeners about a sign-out event.
2022-07-13 13:17:50.452 5496-5496/com.aman.ache E/Parcel: Reading a NULL string not supported here.
2022-07-13 13:17:50.502 5496-6482/com.aman.ache E/libEGL: Invalid file path for libcolorx-loader.so
2022-07-13 13:28:16.702 13149-13529/com.aman.ache W/Firestore: (23.0.3) [Firestore]: Listen for Query(target=Query(Users/GBVFmnu3zPTooz9nvmX6nBaQ6d02 order by name);limitType=LIMIT_TO_FIRST) failed: Status{code=PERMISSION_DENIED, description=Missing or insufficient permissions., cause=null}
highlighted part is the error in red.
From the error message it seems like you have an active listener on Firestore for which your security rules require that a user is signed in. When the user signs out that condition is no longer met, so the security rules remove the listener at that point, and raise an error to the SDK which you then log.
You didn't share the code of how you listen for data in Firestore, but you generally have two options:
Remove the listener in your application code before you sign the user out.
Change your code to ignore the error that you get.
The net result is the same, as the listener also is removed by the system automatically. I typically prefer the first approach though, as it's cleaner to have the application code clean up after itself.
The App performs a simple sign up (using FirebaseAuth, FirebaseUI & Google Sign In). When authenticated successfully, I take firebaseUser.userId and use it to fetch user profile from Realtime Database (example location /users/{userId}/someUserDataIsHere).
In case Realtime Database returns null object for that userId, it means the user with that userId does not exist in realtime DB, and is signing in for the first time (using Sign up with Google Account), hence a profile should be created (or in other words, user is about to register). In other case, if Firebase db returns user object, the app moves forward to the home screen.
The user profile contains some mandatory data like userId, email, and a name. But also contains some optional data like age, country etc, that could be empty.
The problem is that from time to time, when a user starts the app and the whole authentication process starts, after successful authentication, RealtimeDatabase tries to fetch user profile (for userId provided by FirebaseAuth), but error java.lang.Exception: Client is offline occurs, returns an empty object so the app "thinks" the user is new and must be inserted in the Realtime Database, and does that (even if it said "Client is offline" like 300ms before)
How it is offline when it authenticated user a few milliseconds before, failed to fetch data for that user from the realtime database (because it is offline??), and managed to write a new profile to the realtime database few ms after?
It does not make a huge problem, because it inserts data to the same userId key (it performs update technically), but remember that I have some optional fields, and those will be reset when this case happens. It is strange from the user's perspective because the user entered some optional fields (for example age) and it disappeared after some time.
I must point out the most usual use case for this:
User starts the app, sings in successfully and it is authenticated, provided with all the data for operating the app on a Home screen
Exit/kills the app
Starts the app after 2 hours
Authenticates successfully but fails to fetch user profile for that userId (which is valid and exists in the Realtime Database) due to Client is Offline error
Performs new user insertion successfully
Some of the dependencies that I use in the app ->
implementation platform('com.google.firebase:firebase-bom:26.4.0')
implementation 'com.google.firebase:firebase-analytics-ktx'
implementation 'com.google.firebase:firebase-auth-ktx'
implementation 'com.google.firebase:firebase-messaging-ktx'
implementation 'com.google.firebase:firebase-database-ktx'
implementation 'com.firebaseui:firebase-ui-auth:6.2.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.1.1'
implementation 'com.google.android.gms:play-services-auth:19.0.0'
Also, using this on app start:
FirebaseDatabase.getInstance().setPersistenceEnabled(false)
And this is the error I get (UPDATED with logs of some other GET request):
2021-01-30 16:12:12.210 9157-9599/com.fourexample.oab D/PersistentConnection: pc_0 - Connection interrupted for: connection_idle
2021-01-30 16:12:12.221 9157-9599/com.fourexample.oab D/Connection: conn_0 - closing realtime connection
2021-01-30 16:12:12.221 9157-9599/com.fourexample.oab D/WebSocket: ws_0 - websocket is being closed
2021-01-30 16:12:12.224 9157-9599/com.fourexample.oab D/PersistentConnection: pc_0 - Got on disconnect due to OTHER
2021-01-30 16:12:12.372 9157-9599/com.fourexample.oab D/WebSocket: ws_0 - closed
2021-01-30 16:13:07.094 9157-9166/com.fourexample.oab I/zygote64: Debugger is no longer active
2021-01-30 16:13:08.682 9157-9599/com.fourexample.oab D/Persistence: Starting transaction.
2021-01-30 16:13:08.687 9157-9599/com.fourexample.oab D/Persistence: Saved new tracked query in 3ms
2021-01-30 16:13:08.705 9157-9599/com.fourexample.oab D/Persistence: Transaction completed. Elapsed: 22ms
2021-01-30 16:13:11.708 9157-9599/com.fourexample.oab D/PersistentConnection: pc_0 - get 1 timed out waiting for connection
2021-01-30 16:13:11.713 9157-9157/com.fourexample.oab I/RepoOperation: get for query /requests/rs falling back to cache after error: Client is offline
2021-01-30 16:13:11.715 9157-9157/com.fourexample.oab D/Persistence: Starting transaction.
2021-01-30 16:13:11.718 9157-9157/com.fourexample.oab D/Persistence: Saved new tracked query in 2ms
2021-01-30 16:13:11.726 9157-9157/com.fourexample.oab D/Persistence: Transaction completed. Elapsed: 9ms
2021-01-30 16:13:11.741 9157-9157/com.fourexample.oab E/RequestService: java.lang.Exception: Client is offline
at com.google.firebase.database.connection.PersistentConnectionImpl$2.run(PersistentConnectionImpl.java:432)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:457)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
at java.lang.Thread.run(Thread.java:764)
For me, it ended up being because my google-services.json was out of date after I had updated some things in Firebase. Particularly it was because my code ended up referencing the default (US-East) region, when I had updated in Firebase to a different one. Try downloading the latest google-services.json, then making sure you clean and rebuild your app!
Okay, there is a bug in Firebase SDK.
Reported/opened issue on GitHub, and they are about to resolve it. Check more on this link
The main problem was usage of suspending functions with get().await() in the following query:
val dataSnapshot = firebaseRoutes.getRequestsReference(countryCode)
.orderByChild("isActive").equalTo(true)
.limitToFirst(20)
.get()
.await()
This would randomly close connection with Realtime Database.
I came up with a workaround using extensions until they solve it on their end.
So if you want to use queries and suspending functions, check this extension:
suspend inline fun <reified T> Query.awaitSingleValueEventList(): Flow<FlowDataState<List<T>>> =
callbackFlow {
val valueEventListener = object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
try {
val entityList = mutableListOf<T>()
snapshot.children.forEach { dataSnapshot ->
dataSnapshot.getValue<T>()?.let {
entityList.add(it)
}
}
offer(FlowDataState.Success(entityList))
} catch (e: DatabaseException) {
offer(FlowDataState.Error(e))
}
}
override fun onCancelled(error: DatabaseError) {
offer(FlowDataState.Error(error.toException()))
}
}
addListenerForSingleValueEvent(valueEventListener)
awaitClose { removeEventListener(valueEventListener) }
}
Usage:
suspend fun getActiveRequests(countryCode: String): Flow<FlowDataState<List<RequestEntity>>> {
return firebaseRoutes.getRequestsReference(countryCode)
.orderByChild("isActive").equalTo(true)
.limitToFirst(20)
.awaitSingleValueEventList()
}
FlowDataState is nothing but a wrapper that could be Data or Error
sealed class FlowDataState<out R> {
data class Success<out T>(val data: T) : FlowDataState<T>()
data class Error(val throwable: Throwable) : FlowDataState<Nothing>()
}
Calling this:
service.getActiveRequests(countryCode).collect {
when (it) {
is FlowDataState.Success -> {
// map from entity list to domain model list
// and emit to ViewModel
}
is FlowDataState.Error -> {
// emit error to viewModel
}
}
}
I was facing the same issue and replaced the get().addOnCompleteListener with addValueEventListener method. Below is completed code.
Code which throws client is offline issue:
databaseReference.child("pages").child("Help").get().addOnCompleteListener { task: Task<DataSnapshot> ->
if (!task.isSuccessful) {
Log.e("firebase", "Error getting data", task.exception)
} else {
Log.d("firebase",task.result.value.toString())
}
}
Code which solved the above issue:
databaseReference.child("pages").child("Help").addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
Log.e("firebase", "onDataChange ${snapshot.value.toString()}" )
}
override fun onCancelled(error: DatabaseError) {
Log.e("firebase", "onCancelled ${error.message}" )
}
})
What worked for me is re-downloading the google-services.json file, then cleaning and re-building my Android Studio project
I had an issue with my rules. The message sometime comes up as "Permission denied" and sometimes as "Client is offline" when there is an issue with rules. I fixed my rules and things started working.
Message is misleading at times.
It wasn't working for me because I didn't specify server Url when getting database Instance.
Besides this error I was getting this in console :
Firebase Database connection was forcefully killed by the server. Will
not attempt reconnect. Reason: Database lives in a different region.
Please change your database URL to
To fix it I just specified server like that:
FirebaseDatabase.getInstance("YOUR_URL_HERE")
"YOUR_URL_HERE" - you can find this in your firebase console, here:
screenshot
The error is still occurring randomly (no logical explanation as to why) , it seems when I build the app (mobile flutter) and use the firebase emulator on the first launch the error would occur .
The Solution :
I had to manually uninstall the app and then build it again
I've been running into this same issue when using a coroutine with the IO dispatcher:
private fun performFirstReadOfEntities() {
viewModelScope.launch(ioDispatcher) {
mLoadingState.value = true
try {
Log.d(logTag, "Performing first read of $firebaseDBPath")
dbEntityRef.get()
.addOnSuccessListener { snapshot ->
val snapMap = snapshot!!.value as Map<*, *>
val tempEntityList = mutableListOf<FirebaseEntity>()
for (entry in snapMap) {
Log.d(logTag, "Adding entry with \n\tkey: ${entry.key}\n\tval: ${entry.value.toString()}")
tempEntityList.add(
entityFactory.createFirebaseEntity(
uid = entry.key as String,
map = entry.value as Map<String,Any>))
}
mEntities.value = tempEntityList.sortedBy { it.uid } // So that list and expected list match for emulation testing
Log.d(logTag, "Got ${tempEntityList.size} entities on initial read of database")
dbEntityRef.addChildEventListener(childEventListener) // Add child event listener after
dbListenersInitialized = true
mLoadingState.value = false
clearErrorState()
}
.addOnFailureListener {
setErrorState("Failed to get response from firebase", it)
}
I'm not sure if this is considered "proper" use, but it was working for me, until I got:
error: client is offline
The solution for me was uninstalling the app launcher from the emulated android device, cleaning/rebuilding, and then re-running the test.
Uninstalling the application was what finally did it. Even force closing it didn't do the trick. Maybe there's a firebase issue persisting in the cache and clearing it would have also done the trick? In any case, I'm hoping this is of use to anyone facing the same issue.
The Firestore database set() does not call OnSuccessListener or OnFailureListener and don't writes the data to the database online!
As you see in the code first I get the instanceID via getInstanceId() and this is working because I see the log statements.
But the Firestore database is never executed. I don't see any data in my database and the both methods are not called either.
No error message in the log.
I don't get it what's the problem here without any error message :-(
FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(instanceIdResult -> {
if(instanceIdResult == null) {
Log.e(TAG, "Firebase Instance Id Result is null!");
HyperLog.e(TAG, "Firebase Instance Id Result is null!");
return;
}
String mToken = instanceIdResult.getToken();
HyperLog.e(TAG, mToken);
Log.e(TAG,mToken);
Log.e(TAG,"ID is" + instanceIdResult.getId());
db.collection(C_USERS).document(eMail).collection(C_DEVICES).document(instanceIdResult.getId())
.set(deviceInfo)
.addOnSuccessListener(aVoid -> {
Log.e(TAG, "Success!");
callBack.onAddUpdateTokenSuccess();
})
.addOnFailureListener(e -> {
//noinspection Convert2MethodRef
Log.e(TAG, "Failure!");
callBack.onAddUpdateTokenFailure(e);
});
I test this on a real device with debug build and this device has internet (Mobile and WiFi).
Seems the problem is a not allowed API in the Google Cloud Console.
After digging deeper into the logs I have seen that "FireStore" writes this line:
Requests to this API securetoken.googleapis.com method google.identity.securetoken.v1.SecureToken.GrantToken are blocked.
Now I have added Token Service API to the allowed API's and it is working.
Both callbacks are now called successfully and the document is also written.
Please check that your API is allowed or not in the Google CLoud Console
I am trying to add user details to Firestore db, but can't write data in it.
Actually none of the listeners are triggered neither onSuccess() nor onFailure().
here is my code.
Map<String,Object> userlol = new HashMap<>();
userlol.put("name",name);
userlol.put("email",email);
userlol.put("uid",currentUser.getUid());
Log.d(TAG, "we are here");
CollectionReference dc = db.collection("users");
DocumentReference dd = dc.document(currentUser.getUid());
dd.set(userlol)
.addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Toast.makeText(SignupActivity.this, "User Added" ,
Toast.LENGTH_SHORT).show();
Log.d(TAG,"User added to database");
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Toast.makeText(SignupActivity.this, "ERROR" +e.toString(),
Toast.LENGTH_SHORT).show();
Log.d(TAG, e.toString());
}
});
There is no toast nor the Log in my logcat.I can see
D/logging: we are here
This log and logs after this method.
There is no issue with rules as onFailure() is also not working.
I have searched every possible thing but nothing worked.
The only way I can get neither of the callbacks to trigger is when there is no network connection on my device. In that case the behavior makes sense, since the task is only considered completed when the data has been committed (or rejected) on the server.
To easily see if the Firestore client indeed can't connect to the server, enable debug logging:
FirebaseFirestore.setLoggingEnabled(true);
I see log entries like this after doing so:
11-12 07:56:21.366 10034-10066/com.firebase.firestorestackoverflow I/Firestore: (0.6.6-dev) [zzetk]: (b6322ac) Stream closed with status: zzcd{code=UNAVAILABLE, description=null, cause=java.net.UnknownHostException: Unable to resolve host "firestore.googleapis.com": No address associated with hostname
at java.net.InetAddress.lookupHostByName(InetAddress.java:470)
at java.net.InetAddress.getAllByNameImpl(InetAddress.java:252)
at java.net.InetAddress.getAllByName(InetAddress.java:215)
at io.grpc.internal.zzbj$zzb.zztu(Unknown Source)
at io.grpc.internal.zzbk.run(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
Caused by: android.system.GaiException: android_getaddrinfo failed: EAI_NODATA (No address associated with hostname)
at libcore.io.Posix.android_getaddrinfo(Native Method)
at libcore.io.ForwardingOs.android_getaddrinfo(ForwardingOs.java:55)
at java.net.InetAddress.lookupHostByName(InetAddress.java:451)
at java.net.InetAddress.getAllByNameImpl(InetAddress.java:252)
at java.net.InetAddress.getAllByName(InetAddress.java:215)
at io.grpc.internal.zzbj$zzb.zztu(Unknown Source)
at io.grpc.internal.zzbk.run(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
}.
well for anyone having that problem in the future just Uninstall your application and reinstall it, as savage as it seams it works(y)
I had the same problem.
First of all turning on the logs helped a lot as #Frank van Puffelen suggested.
In my case I got "The Cloud Firestore API is not available for Datastore Mode projects".
So I went to GCP and created manually a collection by changing the db to a native one. And then I had an option on the gui.
Now the error changed to "FirebaseFirestoreException: permission_denied: missing or insufficient permissions"
So I changed the permissions under the "rules" tab in firestore.
And that fixed it for me :)
I guess the problem was the permissions from the beginning, but I can't tell for sure now.
I had the same error. In my case, I was creating a user and automatically logging him/her out, for them to log manually.
I removed the sign out, as data appears not to be written if there is no user logged in.
Do you have a user signed in when the data is written? Creating a user signs him/her in automatically.
Hope it helps!
It appears that just like after adding new permissions in the manifest file, the app has to be reinstalled to register the changes. I tried reinstalling the app after adding the firestore connection and everything worked fine. voila!
I'm having issues with some code which was working fine until today. Basically, i'm doing the standard create user object after auth for extra data.
But, when i was just testing the code below it no longer enters the onComplete method. Instead, i get the error in the log below about the transaction being aborted.
A Google brings up nothing, what could it be?
Code:
final Firebase userLocation = new Firebase(Constants.USERS_URL).child(uid);
User newUser = new User(name, email);
userLocation.setValue(newUser, new Firebase.CompletionListener() {
#Override
public void onComplete(FirebaseError firebaseError, Firebase firebase) {
if(firebaseError == null) {
...
}
else {
...
}
}
});
Logs:
04-05 16:13:04.274 4855-4912/com.nicedistractions.bestbefore D/AuthenticationManager: Sending request to https://auth.firebase.com/v2/best-before/users with 2 query params
04-05 16:13:05.437 4855-4855/com.nicedistractions.bestbefore I/RegisterPresenter: Successfully created user account with uid: 4a97cb4b-4034-4f46-98c3-4104d1d649b7
04-05 16:13:05.437 4855-4855/com.nicedistractions.bestbefore D/RegisterPresenter: Creating user object
04-05 16:13:05.484 4855-4912/com.nicedistractions.bestbefore D/WebSocket: ws_1 - Reset keepAlive. Remaining: 0
04-05 16:13:05.502 4855-4912/com.nicedistractions.bestbefore D/RepoOperation: set: /users/4a97cb4b-4034-4f46-98c3-4104d1d649b7
04-05 16:13:05.503 4855-4912/com.nicedistractions.bestbefore D/DataOperation: set: /users/4a97cb4b-4034-4f46-98c3-4104d1d649b7 {
email=xxxxxxxxxx#gmail.com
name=xxxxx xxxxxxxx
timestampJoined={
timestamp={.sv=timestamp}
}
}
04-05 16:13:05.503 4855-4912/com.nicedistractions.bestbefore D/Persistence: Starting transaction.
04-05 16:13:05.524 4855-4912/com.nicedistractions.bestbefore D/Persistence: Persisted user overwrite in 20ms
04-05 16:13:05.536 4855-4912/com.nicedistractions.bestbefore D/Persistence: Transaction completed. Elapsed: 33ms
04-05 16:13:05.538 4855-4912/com.nicedistractions.bestbefore D/RepoOperation: Aborting transactions for path: /users/4a97cb4b-4034-4f46-98c3-4104d1d649b7. Affected: /users/4a97cb4b-4034-4f46-98c3-4104d1d649b7
Thanks for your help!
I am able to reproduce this on Lollipop when I am connected to wifi, disable the actual internet connectivity, and then re-enable internet connection (all with the device remaining connected to wifi network). After this, if I try to perform a setValue() write, about 18 minutes will pass before Firebase reconnects and the setValue() completes. I reached out to the Firebase team and was told this is expected behavior due to the Android OS basically neglecting to informing apps when the connection is restored.
The only way to resolve in my experience is either disabling and re-enabling WiFi, or restarting the app (restarting the activity won't help).
Firebase support got back to me and suggested calling goOffline() followed by goOnline() - I verified this immediately reconnects the Firebase database. What I do now is set a Handler with my own timeout for setValue(), since I can't seem to depend on DatabaseError being thrown with onComplete().