There is an issue in my app that connect to firebase real time database ..
when i try to insert data to firebase during offline...
when internet available the data automaticaly send to firebase database..
when my app close on offline then the insert will not happen...
i want to prevent the data insert to firebase which the after effect of insert try during offline...
//------------------------------------------------
private void onStarClicked(final DatabaseReference postRef) {
postRef.runTransaction(new Transaction.Handler() {
#Override
public Transaction.Result doTransaction(MutableData mutableData) {
if (mutableData.hasChildren()) {
int inv = 0;
for (MutableData postData : mutableData.getChildren()) {
inv = postData.child("inv").getValue(Integer.class);
}
SalesModel salesModel = new SalesModel("samsung",inv+1,987.90);
String newKey = postRef.push().getKey();
// Set value and report transaction success
mutableData.child(newKey).setValue(salesModel);
}
// Set value and report transaction success
// mutableData.setValue(p);
return Transaction.success(mutableData);
}
#Override
public void onComplete(DatabaseError databaseError, boolean b,
DataSnapshot dataSnapshot) {
// Transaction completed
// Log.d(TAG, "postTransaction:onComplete:" + databaseError);
}
});
}
//------------------------------------------------------/
The Realtime Database SDK cannot be configured to ignore pending writes when the app process is killed. The SDK will always try to synchronize all writes. If you don't want this behavior, you'll have to find another way to buffer writes in memory and try to only send them when online. But I suspect you will have a lot of trouble with this, as you never have a guarantee that any write will succeed at the moment you try it.
Related
I have implemented the Firebase Real-Time Database presence system as shown in the official Firebase documentation. I would like to make the database secure so that logged-in users can only write to their own presence entries in the DB. So, on login, the user writes to the reference path /auth/{authId}/connections and at the same time sets up the onDisconnect to remove the value.
Here is the code from the Android app that is setting presence in rtdb:
getFirebaseDatabase().goOnline();
DatabaseReference.goOnline();
// Since I can connect from multiple devices, we store each connection instance separately
// any time that connectionsRef's value is null (i.e. has no children) I am offline
final FirebaseDatabase database = getFirebaseDatabase();
final DatabaseReference myConnectionsRef = database.getReference("/auth/" + getFirebaseAuth().getUid() + "/connections");
// Stores the timestamp of my last disconnect (the last time I was seen online)
final DatabaseReference lastOnlineRef = database.getReference("/auth/" + getFirebaseAuth().getUid() + "/lastOnline");
connectedRef = database.getReference(".info/connected");
presenceChangeListener = connectedRef.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot snapshot) {
boolean connected = snapshot.getValue(Boolean.class);
if (connected) {
DatabaseReference con = myConnectionsRef.push();
// When this device disconnects, remove it
con.onDisconnect().removeValue()
.addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
// Add this device to my connections list
// this value could contain info about the device or a timestamp too
con.setValue("ANDROID");
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Log.d(TAG, "### Failed to set onDisconnect ###");
e.printStackTrace();
}
});
// When I disconnect, update the last time I was seen online
lastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP);
}
}
#Override
public void onCancelled(DatabaseError error) {
Log.w(TAG, "Listener was cancelled at .info/connected");
}
});
The problem that I am having is that if the user logs out, the onDisconnect doesn't execute unless I first manually disconnect from rtdb. I'm assuming that the code running on the Real-Time DB gets a permission denied since the auth is no longer valid.
//If I don't go offline first the record in rtdb will not be removed.
DatabaseReference.goOffline();
AuthUI.getInstance().signOut(this)
.addOnCompleteListener(new OnCompleteListener<Void>() {
public void onComplete(#NonNull Task<Void> task) {
// user is now signed out
Log.d(TAG, "Logged out");
application.clearData();
DatabaseReference.goOffline(); //This doesn't cause a presence update here
finish();
}
});
Above is the work-around I'm using, first telling the database to goOffline then to logout. If the user ever gets logged out by another means (the web app is seeing if multiple tabs are using the app and one logs out) the user will be left with a connection not removed.
If I don't call the goOffline() prior to logout, the connection in rtdb will not be removed, even if I force close the application.
I have also verified that I can get everything working fine if I change my rtdb rules to be ".write": "true" <-which is no good. This tells me that there is a permission denied with the onDisconnect running when a user logs out of the auth.
I would like my real-time rules to be something like this.
{
"rules": {
"auth": {
"$uid": {
".read": "auth != null && auth.uid == $uid",
".write": "auth != null && auth.uid == $uid"
}
}
}
}
I would have hoped that the onDisconnect would still be able to execute with the auth of the user when the onDisconnect was setup.
When you attach a onDisconnect() handler, you're registering a delayed write on the Firebase servers. Whether that write is allowed is checked both when you attach the handler, and when the handler is triggered. And since your user is signed out when the write is triggered, it get rejected by your rules. There is no configuration option to change this behavior, so you'll have to come up with a different approach.
So, because 1.) the onDisconnect() execution is evaluated against the rules of the RTDB, 2.) the user who setup the onDisconnect() may lose authentication, and 3.) I would like to make the presence system secure for my auth'ed users... I came up with the following solution:
First, write the presence entries to the RTDB under a path that contains both the user's authId and a UUID to make the location "unguessable".
"/presence/" + {auth-uid} + "/connections/" + {UUID}
and setup a .onDisconnect() to remove this value stored at the unguessable location.
Then, setup the RTDB rules to do the following:
do not allow any reading of the presence data
allow users to add/modify data only under their auth directory
allow any user to delete records (they would need to know the unguessable path)
"presence": {
".read": "false",
".write": "false",
"$auth_id": {
"connections": {
"$uuid": {
".write": "(newData.exists() && $auth_id === auth.uid) || !newData.exists()"
}
}
}
}
Finally, setup a trigger function on the RTDB to read the .ref('/presence/{authid}') location and push the user's presence to another user accessible location (I'm pushing it to my Firestore DB). Also, if the user is changing from "online" to "offline" update a lastOnline timestamp to the current time.
This seems like the best solution given my requirements of having reliable and secure presence system. I hope this helps others.
It is an old question but it made me think about a possible solution and came with the following...
use onDisconnect.setValue/removeValue as long as you have control/awareness over the application
use onDisconnect.cancel and delete to data before logging out
I took #FrankvanPuffelen code and modified it but didn't tested it so....
//getFirebaseDatabase().goOnline();
//DatabaseReference.goOnline();
// Since I can connect from multiple devices, we store each connection instance separately
// any time that connectionsRef's value is null (i.e. has no children) I am offline
final FirebaseDatabase database = getFirebaseDatabase();
final DatabaseReference myConnectionsRef = database.getReference("/auth/" + getFirebaseAuth().getUid() + "/connections");
// Stores the timestamp of my last disconnect (the last time I was seen online)
final DatabaseReference lastOnlineRef = database.getReference("/auth/" + getFirebaseAuth().getUid() + "/lastOnline");
connectedRef = database.getReference(".info/connected");
presenceChangeListener = connectedRef.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot snapshot) {
boolean connected = snapshot.getValue(Boolean.class);
if (connected) {
// simple solution to reuse the old unique key-name otherwise current solution is like performing new registration of a new client over and over on the same client. we should use the old unique key-name until logout is performed
String keyName = SharedPrefUtil.INSTANCE.getFirebaseConnectionKeyName(context);
DatabaseReference con;
if (TextUtils.isEmpty(keyName)) {
con = myConnectionsRef.push();
SharedPrefUtil.INSTANCE.setFirebaseConnectionKeyName(context.getApplicationContext(), con.getKey());
}else{
con = myConnectionsRef.child(keyName);
}
// When this device disconnects, remove it
con.onDisconnect().removeValue()
.addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
// Add this device to my connections list
// this value could contain info about the device or a timestamp too
con.setValue("ANDROID");
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Log.d(TAG, "### Failed to set onDisconnect ###");
e.printStackTrace();
}
});
// When I disconnect, update the last time I was seen online
lastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP);
}
}
#Override
public void onCancelled(DatabaseError error) {
Log.w(TAG, "Listener was cancelled at .info/connected");
}
});
in the logout method we need to cancel the disconnect
String keyName = SharedPrefUtil.INSTANCE.getFirebaseConnectionKeyName(context);
if (!TextUtils.isEmpty(keyName)) {
final FirebaseDatabase database = getFirebaseDatabase();
final DatabaseReference myConnectionsRef = database.getReference("/auth/" + getFirebaseAuth().getUid() + "/connections/" + keyName);
// Stores the timestamp of my last disconnect (the last time I was seen online)
final DatabaseReference lastOnlineRef = database.getReference("/auth/" + getFirebaseAuth().getUid() + "/lastOnline");
// This client/user doesn't need the disconnect functionality
myConnectionsRef.onDisconnect().cancel();
// now we are on our own so we need to remove the key-name from the rmdb
myConnectionsRef.setValue(null);
// remove the key-name from the preferences so we will create a new one in the next login session
SharedPrefUtil.INSTANCE.removeFirebaseConnectionKeyName(context);
// we will not forget to disconnect last time updates
lastOnlineRef.onDisconnect().cancel()
}
AuthUI.getInstance().signOut(this)
I didn't tested it and it will not run as it is missing the SharedPrefUtil implementation
I'm learning Firebase in android and I'm trying to make a room system with their respective slots. As in this case several users may want to access the same slot at the same time I decided to use Firebase transactions.
So when the user tries to log in to the slot I do:
mySlotRef1.runTransaction(new Transaction.Handler() {
#Override
public Transaction.Result doTransaction(MutableData mutableData) {
Slot p = mutableData.getValue(Slot.class);
if (p == null) {
return Transaction.success(mutableData);
}
if (p.getState().equals("closed")) {
return Transaction.abort();
}
// Set value and report transaction success
Slot sl1 = new Slot("slot1", idRoom, auth.getCurrentUser().getDisplayName(), auth.getCurrentUser().getUid(), "closed", Profile.getCurrentProfile().getProfilePictureUri(200, 200).toString());
mutableData.setValue(sl1);
return Transaction.success(mutableData);
}
#Override
public void onComplete(DatabaseError databaseError, boolean b,
DataSnapshot dataSnapshot) {
// Transaction completed
Log.d(TAG, "postTransaction:onComplete:" + databaseError);
}
});
I think this works, i. e. when the slot is closed it only enters one user and rejects the others in the "Slot" ref. But the problem is that I also need to update two more values in the "User" directory and I can't find a way to do it "only when the user occupied the slot", that is, when the transaction was completed.
UPDATE:
This is the part where I check to see if the slot is open.
mySlotRef1.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
Slot data = dataSnapshot.getValue(Slot.class);
state = data.getState();
if (state.equals("open")) {
saveSlot(slot);
} else {
Toast.makeText(getApplicationContext(), "Slot not available", Toast.LENGTH_SHORT).show();
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
This not how things are working when it comes to concurent writes. None of your slots will be closed. When you are using transactions, it means that every write that will be made in the same time by different users will be made in different thread of execution. So using transactions, it doesn't matter if two or many other users will make a write operation in the same time, you'll have a correct result.
If you want to update otrher fields within another class, just put your logic inside onComplete() method. This method is triggered once the transaction is complete.
ATNRef= FirebaseDatabase.getInstance().getReference("AvailableTokenNumber");
ATNRef.runTransaction(new Transaction.Handler() {
#Override
public Transaction.Result doTransaction(MutableData mutableData) {
if (mutableData.getValue(int.class ) == 0){
mutableData.setValue(2);
data.tokenNo = 1;
return Transaction.success(mutableData);
}else {
data.tokenNo=mutableData.getValue(int.class);
mutableData.setValue(data.tokenNo + 1);
return Transaction.success(mutableData);
}
}
Whenever the code is run for first time, value of data.tokenNo is 0.
After that it is updating correctly as per the database value.
I am not able to figure it out what makes the value of data.token = 0 on every first run of the app/code.
You should be expecting that the initial snapshot of data available in your transaction could be null during its first execution. Note this from the documentation:
Note: Because doTransaction() is called multiple times, it must be
able to handle null data. Even if there is existing data in your
remote database, it may not be locally cached when the transaction
function is run, resulting in null for the initial value.
Also in the javadoc it says:
This method will be called, possibly multiple times, with the current
data at this location.
Expect that the first time your handler is run, it will be dealing with an empty database. Then, when the data there is known (if any), you should be prepared to handle that case also. If you don't want to do anything where there is no data (or unknown data) at the location, simply return a successful Transaction with no changes to mutableData.
Thank you for explaining, #Doug Stevenson.
I understand this situation from your answer.
Then I solved this problem with this.
This works for me at least.
FirebaseDatabase database = FirebaseDatabase.getInstance();
try {
database.setPersistenceEnabled(true);
} catch (Exception e) {
// ignore
}
...
DatabaseReference ref = database.getReference("somewhere");
// Read value from database to synchronize localDB with remoteDB
ref.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot dataSnapshot) {
// Then execute transaction.
ref.runTransaction(new Transaction.Handler() {
#NonNull
#Override
public Transaction.Result doTransaction(#NonNull MutableData mutableData) {
...
return Transaction.success(mutableData);
}
#Override
public void onComplete(#Nullable DatabaseError databaseError, boolean committed, #Nullable DataSnapshot dataSnapshot) {
...
}
});
}
});
I am trying to implement a two player game in android using firebase(for realtime pairing).
On firebase, i have set up a node representing active players. And on each client i have a childEventListener to listen to any changes on the players node.
Suppose
Initially there is only one player (Player A).
Then 3 more players(B,C and D) got added (At the SAME TIMESTAMP).
Then, on client side
1. Each of 4 players will get notified about the changes in the node through childEventListener.
Now, What i want to achieve is "UNIQUE PAIRING" i.e I should be able to generate 2 pairs from these 4 players. (Of course, one player can't be in both the pairs)
I have written code to pair two players in a transaction block so that no two players get paired with the same player.
private void attachActivePlayersEventListener() {
Log.i(TAG, "attachActivePlayersEventListener");
if (activePlayersEventListener == null) {
activePlayersEventListener = new ChildEventListener() {
#Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
searchPlayer(dataSnapshot);
Log.i("PLAYER_ADDED ", dataSnapshot.getValue(Player.class).getName());
}
#Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
searchPlayer(dataSnapshot);
Log.i("PLAYER_CHANGED ", dataSnapshot.getValue(Player.class).getName());
}
#Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
}
#Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
};
mActivePlayersDbRef.addChildEventListener(activePlayersEventListener);
}
}
private void searchPlayer(DataSnapshot dataSnapshot) {
if(mPlayer.getState().equals(PlayerState.ACTIVE)) { // if the current user is active
final String oppKey = dataSnapshot.getKey();
Player oppPlayer = dataSnapshot.getValue(Player.class);
if (oppPlayer.getState().equals(PlayerState.ACTIVE)
&& !oppKey.equals(pushId)) { // if the opponent chosen is not the current user
DatabaseReference oppRef = mActivePlayersDbRef.child(oppKey);
oppRef.runTransaction(new Transaction.Handler() {
#Override
public Transaction.Result doTransaction(MutableData mutableData) {
Player player2 = mutableData.getValue(Player.class);
if (player2 == null) {
return Transaction.success(mutableData);
}
mActivePlayersDbRef.child(pushId).child("state").setValue(PlayerState.PLAYING);
mActivePlayersDbRef.child(oppKey).child("state").setValue(PlayerState.PLAYING);
return Transaction.success(mutableData);
}
#Override
public void onComplete(DatabaseError databaseError, boolean b, DataSnapshot dataSnapshot) {
Log.d("PAIRING", "pairing:onComplete:" + databaseError);
}
});
}
}
}
My DOUBT is
Is this the correct way of pairing?
What happens when PlayerA tries to pair up with PlayerB, PlayerB tries to pair up with PlayerC and PlayerC tries to pair up with PlayerD and finally PlayerD tries to pair up with PlayerA(In a CYCLIC way). How to make sure this case doesn't happen?
Note that in Doubt2, I am not taking care of the condition where two players try to pair up with the same player. (As transaction block will ensure that it won't happen, I guess :/ ).
What you are trying to do is difficult to do correctly and safely. It's better to let a backend service do this matching so that the clients don't all have to figure out how to agree with each other somehow.
You can use Cloud Functions for Firebase to write a database trigger that responds to changes in your database. One strategy would be for clients to push data into a location in the database to indicate their intent to be matched. Then, when a function triggers on those writes, it can check to see if there are other suitable players to be matched, and write more data into the database to set up the game. The clients will also need a way to listen for the game starting up after they have been matched. This is still all very much non-trivial, but a lot easier than putting the logic in the clients.
-uniqueid1
status:0
userTokenId: uniqueid(which is null)
I am having a problem of race around condition where I don't know how firebase is implementing this case. The problem is as follows:
user checks status for whether the status is 0.
if status is 0 it adds its token to userTokenid field and status 1
if not then it does'nt add its tokenid.
Now the problem is when 2 or more user check status 0 and both enter the 2 point. This question might be pointless but I need to confirm. Do i have to worry about this case?
firebase.child('uniqueid1');
firebase.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
if(datasnapshot.child('status').getValue().equals(0)){
//update record for userTokenId and status
}
else{
//don't update
}
}
#Override
public void onCancelled(FirebaseError firebaseError) {
}
});
If both clients happen to update the same value around the same time with a setValue() or updateChildren() call, the last write wins.
If you don't want that, you should probably be using a transaction. From that documentation:
When working with complex data that could be corrupted by concurrent modifications, such as incremental counters, we provide a transaction operation. You give this operation two arguments: an update function and an optional completion callback. The update function takes the current state of the data as an argument and will return the new desired state you would like to write. For example, if we wanted to increment the number of upvotes on a specific blog post, we would write a transaction like the following:
Firebase upvotesRef = new Firebase("https://docs-examples.firebaseio.com/android/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes");
upvotesRef.runTransaction(new Transaction.Handler() {
#Override
public Transaction.Result doTransaction(MutableData currentData) {
if(currentData.getValue() == null) {
currentData.setValue(1);
} else {
currentData.setValue((Long) currentData.getValue() + 1);
}
return Transaction.success(currentData); //we can also abort by calling Transaction.abort()
}
#Override
public void onComplete(FirebaseError firebaseError, boolean committed, DataSnapshot currentData) {
//This method will be called once with the results of the transaction.
}
});
Read the documentation on transaction() for full details.
Firebase ref=firebase.child("order_details/"+orderId);
ref.runTransaction(new Transaction.Handler() {
#Override
public Transaction.Result doTransaction(MutableData currentData) {
Log.d("status",currentData.getValue());
int status= (int) currentData.child("status").getValue();
if(status==0){
HashMap<String,Object> map=new HashMap<String, Object>();
map.put("TokenId",sharedPreferences.getString("tokenId",""));
map.put("status",1);
currentData.setValue(map);
}
((MapLoadingActivity)getActivity()).setProgressVisibility(false);
return Transaction.success(currentData);
}
#Override
public void onComplete(FirebaseError firebaseError, boolean b, DataSnapshot dataSnapshot) {
}
});
I know its late to ask the question,but doTransaction() is pretty weird sometimes it calls sometimes it simply returns null.Therefore i switched my approach to php by calling firebase using php wrapper class.Please do suggest ,if my approach is wrong.