What is a proper, solid way of having getToken() return with a token id?
I have seen a couple of attempts using
while-loop, calling getToken() until it's not null
Local BroadcastReceiver
Timer
The while loop seem really risky in terms of ANRs and having a loop running indefinitely.
The local BroadcastReceiver isn't readily applicable to my application, because even though there is a main activity in my app, there is nothing forcing the user to interact with it. Users can accomplish tasks in the app without going through that main activity.
The timer seem fragile. How long do you have to wait for? Seconds, minutes, hours?
The Firebase quickstart sample code does not provide an example to handle the situation when getToken() is null, and I don't see what a proper and solid implementation would be to ensure there's a token being returned, without any of the nasty side effects of the above mentioned implementations would bring.
Having a callback method to hook onto would've made this a non-issue, but since that isn't available, I just have to run this through StackOverflow to get an idea of how people resolve this issue.
Note: I have implemented onTokenRefresh() and that works for the specific situations stated in the docs, but I can't use that alone, because the app is only upgraded on a lot of devices, and this method isn't called during an app upgrade. I have the null issues on (Nexus) hardware devices, I am not using the emulator at all and don't intend to use it.
As it seems there is noone adding other approaches, than the ones I mentioned before, I'm now going to answer my own question.
The "solution" I have implemented to having a token being generated so that you can handle it more consistently, and store it for later reference, is this;
The following class follows the quickstart almost to the letter. But instead of sending the token away to a server, at the exact time it is accessible, I instead store it for later use:
public class FirebaseIdService extends FirebaseInstanceIdService {
#Override
public void onTokenRefresh() {
// Get updated InstanceID token.
String refreshedToken = FirebaseInstanceId.getInstance().getToken();
if(refreshedToken != null) {
Log.d("TOKENTAG", "Refreshed token: " + refreshedToken);
SharedPreferences prefs = getApplicationContext().getSharedPreferences("pref_id", 0);
prefs.edit().putString("share_pref_token", refreshedToken).apply();
}
}
}
When I want/need to send the token I check if there's already one available in the SharedPreferences and if not, I simply delete the Firebase instance, and call getToken(), to have another one generated. What this essentially does is to tell Firebase that "I want to invalidate the current instance", and then, by calling getToken() afterwards, have it generate a new one, essentially making onTokenRefresh() to be called, whenever a token has been generated and is available:
SharedPreferences prefs = getApplicationContext().getSharedPreferences("pref_id", 0);
if(prefs.getString("share_pref_token", "").isEmpty()) {
Log.d("TOKENTAG", "token not available, so let's force one to be generated");
try {
FirebaseInstanceId.getInstance().deleteInstanceId();
} catch (IOException e) {
e.printStackTrace();
}
FirebaseInstanceId.getInstance().getToken();
}
I am pretty sure this is a sub optimal solution, and I'm welcoming suggestions to make this suggestion better, but at this point this is basically the only way that seem to cover the bases of combining onTokenRefresh() and getToken() to more consistently get a token without any of the afformentioned drawbacks.
Related
I'm having a little issue with FirebaseAuth.AuthStateListener while working with email verification on Firebase. I've verified my email by clicking the received verification link, and then I reloaded the current user by the lines of code below:
suspend fun reloadUserInfo() {
firebaseAuth.currentUser?.reload()?.await()
}
But AuthStateListener is not firing up even tho I reloaded the cached user. If I understood correctly AuthStateListener should trigger after reloading the current user. The reload() function's documentation says: Manually refreshes the data of the current user (for example, attached providers, display name, and so on). The isEmailVerified state changed the firebase user. Right?
val isEmailVerified: Flow<Boolean> = callbackFlow {
val authStateListener = AuthStateListener { auth ->
val isEmailVerified = auth.currentUser?.isEmailVerified == true
trySend(isEmailVerified)
}
firebaseAuth.addAuthStateListener(authStateListener)
awaitClose {
firebaseAuth.removeAuthStateListener(authStateListener)
}
}
This flow is not sending anything. But after restarting the application the callback gets fired. I don't want to restart the application to get the job done. It would not be a good user experience.
I did some research but nothing was found. If you take the time to help me, I appreciate it.
The email verification happens out of the band, so when you click the link in the email, there is nothing built-in that triggers Firebase Authentication clients to be updated. So there is no callback for that. Firebase only refreshes the ID token once per hour. This means that it may take up to an hour before the token is refreshed, a case in which the onIdTokenChanged() method fires. So onAuthStateChanged() nor onIdTokenChanged() fires when the link is clicked, which basically means that we have to check that in our application code, on demand. Since you're using Kotlin, the solution is quite simple:
return try {
auth.currentUser?.reload()?.await()
} catch (e: Exception) {
Log.e(TAG, "${e.message}")
}
However, do not attach a complete listener and call await() at the same time. It's one or the other. Do not combine them! Why? Because there is no guarantee that the listener will be called before or after await(). That means that there is a chance that the code in the complete listener won't be called until or after the suspend function returns. Besides that, one of the major reasons to use Kotlin Coroutines in the first place is to avoid using callbacks, which are not life-cycle aware.
If you want to see a concrete example, this resource will help. Here is the corresponding repo.
My firestore onSnapshot() function is being called twice.
let user = firebase.firestore().collection('users').doc(userID).onSnapshot
({
next: (documentSnapshot: firebase.firestore.DocumentSnapshot) =>
{
this.userArray.push(documentSnapshot as User);
console.log(documentSnapshot);
//here
},
error: (firestoreError: firebase.firestore.FirestoreError) =>
{
console.log(firestoreError);
//here
}
});
I have also tried subscribing like in https://firebase.google.com/docs/firestore/query-data/listen#detach_a_listener by including user() at the //here comment but to no avail.
How can I modify such that the function only executes one time, i.e. push only one user object per time instead of twice.
I don't know if this is related to your question. If one is using
firebase.firestore.FieldValue.serverTimestamp()
to give a document a timestamp, then onSnaphot will fire twice. This seem to be because when you add a new document to your database onSnapshot will fire, but the serverTimestamp has not run yet. After a few milliseconds serverTimestamp will run and update you document => onSnapshot will fire again.
I would like to add a small delay before onSnapshot fires (say 0,5s or so), but I couldn't find the way to do this.
You can also make a server side function for onCreate event, I believe that would solve your problem. Maybe your userArray.push-action would be more suitable to execute in server side.
Update: To learn more about the behavior of serverTimestamp() and why it triggers the listener twice read this article: The secrets of Firestore’s FieldValue.serverTimestamp() — REVEALED!. Also, the official documentation states:
When you perform a write, your listeners will be notified with the new data before the data is sent to the backend.
In the article there are a couple of suggested solutions, one of which is to use the metadata property of the snapshot to find whether the Boolean value of metadata.hasPendingWrites is true (which tells you that the snapshot you’re looking at hasn’t been written to the server yet) or false.
For example, in your case you can check whether hasPendingWrites is false and then push the object:
if ( !documentSnapshot.metadata.hasPendingWrites ){
// This code will only execute once the data has been written to the server
this.userArray.push(documentSnapshot as User);
console.log(documentSnapshot);
}
In a more generic example, the code will look like this:
firestore.collection("MyCollection")
.onSnapshot( snapshot => {
if ( snapshot.metadata.hasPendingWrites ){
// Local changes have not yet been written to the backend
} else {
// Changes have been written to the backend
}
});
Another useful approach, found in the documentation is the following:
If you just want to know when your write has completed, you can listen to the completion callback rather than using hasPendingWrites. In JavaScript, use the Promise returned from your write operation by attaching a .then() callback.
I hope these resources and the various approaches will help anyone trying to figure out a solution.
REFERENCES:
Events for local changes
The hasPendingWrites metadata property
Snapshot Listen Options
If you need a one time response, use the .get() method for a promise.
firebase.firestore().collection('users').doc(userID).get().then(snap => {
this.userArray = [...this.userArray, snap.doc);
});
However, I suggest using AngularFire (totally biased since I maintain the library). It makes handling common Angular + Firebase tasks much easier.
I have been using Firebase Database in my Android app for almost a year now and it works pretty nice. Unfortunately the data stops being synced to the could after some time. It is just never synced/stored to the cloud. Only local. So when user reinstalls the app, it only contains the data which was stored in the cloud. So to the user it looks like the data was removed, but actually is was never stored. I checked and the data is not visible in the firebase-console. Because it happens after a reinstall I guess it has something to do with the syncing. Users report losing data of about 2-3 months.
I'm using the following singleton helper class. Note I use the setPersistenceEnabled(true) and keepSynced(true).
public class FirebaseHelper{
protected FirebaseHelper(Context c) {
this.c = c.getApplicationContext();
FirebaseDatabase.getInstance().setPersistenceEnabled(true);
mAuth = FirebaseAuth.getInstance();
this.userRef = FirebaseDatabase.getInstance().getReference().child(((BuildConfig.DEBUG ? "debug" : "release"))).child("users").child(getUID());
this.userRef.keepSynced(true);
this.path1 = userRef.child("path1");
this.path2 = userRef.child("path2");
this.path3 = userRef.child("path3");
this.path4 = userRef.child("path4");
}
public static FirebaseHelper getInstance(Context c) {
if (instance == null) {
instance = new FirebaseHelper(c);
}
return instance;
}
public String insertObject(MyObject obj) {
DatabaseReference newItem = this.path1.push();
String pushID = newItem.getKey();
obj.id = pushID;
newItem.setValue(obj.getObject());
return pushID;
}
public void updateData(...){}
...other methods
}
What could possibly be the cause of this?
There are only three reasons for this to happen to the best of my knowledge.
1) The method getUID()
Somehow the getUID() method is returning a null or invalid value which leads the data to be stored to somewhere else or it is not getting stored at all.
You are using simply getUid() instead of FirebaseAuth.getInstance().getCurrentUser().getUid(). So it must be a user defined method.
I think your getUid() does something like this.
String getUid() {
try{
return FirebaseAuth.getInstance().getCurrentUser().getUid() ;
} catch (NullPointerException e) {
return null;
}
}
FirebaseAuth.getInstance().getCurrentUser() will return null if cache is cleared. It will lead your data to be lost.
2) Redundant data
Since Marshmallow, data is backed up to the cloud including shared preference. If you are checking shared preference to decide if user is logged in, user will be gone taken inside after reinstalling the app, skipping the login page. But actually he is not logged in which means FirebaseAuth.getInstance().getCurrentUser() returns null and any attempt to access the database will fail (depends on your database rules).
Solution: use FirebaseAuth.getInstance().getCurrentUser()==null to check if user is enabled.
You can alternatively set backup to false. But I prefer first method.
3) An internal bug in Firebase SDK
Unfortunately there is nothing much we can do with it. Try narrowing it down and find a scenario by which the issue can be reproduced and report it to Firebase.
By the way, child(((BuildConfig.DEBUG ? "debug" : "release"))) is really smart. I am going to adopt it.
Well, if your database has been syncing and you have not made any changes to the code whatsoever, it means that this is a firebase error, particularly related to the mobile phone the user is using.
Most developers who use firebase find problems querying the database when certain carriers are used. I have researched into this issue but i have not yet resolved it yet. If you happen to be using mobile data, actions like authenticating a user may not work.
Solution
Use a different internet source to test your code. Try using wifi instead of mobile data while debugging or testing your app.
if I find any helpful work around, I will file it on a firebase project experiencing the problem on open source Lucem
Throwing this out as a guess as well:
At some point you distributed an app where BuildConfig.DEBUG = true, so users that install an updated version "lose" their data. Doesn't explain why other users haven't reported shorter losses though...
The solution would be a data migration, checking which has newer data and then copying the data if DEBUG is newer.
Let me to start explain my problem. There is repository with some explanations, but there are no methods how to get collection or json file from Meteor server(only insert). Also author did not explain properly methods onDataChanged, onDataAdded etc.
public class Login extends Activity implements MeteorCallback{
public static Meteor mMeteor;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
mMeteor = new Meteor(this, "some_socket_it_doesn't_matter");
mMeteor.setCallback(this);
}
}
public class ListOfElements extends ListFragment implements MeteorCallback{
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
String subscriptionId = Login.mMeteor.subscribe("notifications");
Log.d("Log", subscriptionId);
}
}
I didn't understand how i have to use subscription or how to get collection from server. Why there are only insert methods in github repository and no get? I really have no idea how make the code to get collection, use subscribe and so on. There are no any understandable explanations in the network. Please, can you help me with this by explaining how to realize getting, subscribing in this code.
There are two special things about Meteor: It works asynchronously and it has been designed specifically for real-time applications. Thus it has a few different concepts for retrieving data and for some other tasks.
In a synchronous application, you would just call insert(...) and immediately get the method's return value, e.g. a boolean value for success/error or a numeric value for the number of rows that have been inserted.
You would call get(...) and immediately receive a collection of rows as the method's return value.
But in Meteor, everything is asynchronous. This means that you get the results not immediately, but a few (milli)seconds later, in a callback method.
When you call insert(...), this is not so important, as you have noticed. You just call this method and often forget about the result, i.e. you don't wait and check for the result because insertions are usually successful. But this method is still asynchronous and you could (and sometimes should) listen for the result which will arrive a few (milli)seconds later, again.
When you want to call get(...), this would be possible in theory, with the important point again being that it's asynchronous. So you would say "get me all chat messages from the last 5 minutes". There would be no result or return value, as usual, but the result would arrive a short time later, asynchronously, in a callback method that you define. This is what onDataAdded(...), onDataChanged(...) and onDataRemoved(...) are for.
Now it's not clear, yet, why you can't call get(...) and wait for data to arrive in those methods.
The answer to that question is Meteor being designed for real-time applications. This is why you can't say "get me all chat messages from the last 5 minutes". Instead, you have to say "I want to subscribe to all chat messages from the last 5 minutes and always be updated about changes".
So, in Meteor, you subscribe to data sets instead of requesting them via get(...).
All in all, this means the following:
If you want to get some messages, you subscribe to your data set that holds those messages.
When the initial rows are sent (!) and whenever new rows are added to the collection, you receive those in your onDataAdded(...) callback. When rows are modified, you receive those changes in your onDataChanged(...) callback. And, finally, when rows are deleted, you are informed about those deletions in your onDataRemoved(...) callback.
When you don't want to get updates for your data set anymore, you unsubscribe from that set. This is optional.
With the Android-DDP library in your Android application, it translates to the following:
final String subscriptionId = mMeteor.subscribe("chats");
public void onDataAdded(String collection, String docID, String json) { ... }
mMeteor.unsubscribe(subscriptionId);
As you can see, what you have to learn is really Meteor and not the library Android-DDP. Meteor has some new concepts that one has to understand. But when you know how Meteor works, translating those things to Android-DDP is really simple and only a matter of looking up the method names.
I want to clear the application's data when a user manually removes an account from the Accounts & sync section in the settings app.
I have my own implementation of AbstractAccountAuthenticator but there is no method to hook in the remove account process. Any hints?
I've been pondering on the same problem and here's the "solution" I decided upon. It's not what I'd call the "correct" solution but it's the best I believe you can manage with the current API.
In my implementation of the AbstractAccountAuthenticator class, I've overriden the getAccountRemovalAllowed function as follows:
#Override
public Bundle getAccountRemovalAllowed(
AccountAuthenticatorResponse response, Account account)
throws NetworkErrorException {
Bundle result = super.getAccountRemovalAllowed(response, account);
if (result != null && result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
&& !result.containsKey(AccountManager.KEY_INTENT)) {
final boolean removalAllowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
if (removalAllowed) {
// Do my removal stuff here
}
}
return result;
}
There is a tiny chance that removal could fail AFTER you return from getAccountRemovalAllowed but it's negligible (IMHO).
As MisterSquonk suggested there is an Intent that you could listen for (ACCOUNTS_CHANGED_INTENT) but, unfortunately, it's broadcast when an account changes, and not just when an account is deleted.
I don't understand why this isn't part of the SDK but maybe we've both missed something obvious! For now, I'm sticking with this approach as I need to delete some database tables of my own on account deletion.
I hope this helps.