I have an application that offers some offers for some stuff. The idea is the offer is added by me on another application I created and then be shown on users devices all at the same time.
I need only one user to be able to take the offer so when the first user clicks I do call removeValue() on the offer ref. The offer is correctly deleted from the database and from the other users recyclerview.
The problem is when 2 clicks happens in the same time the offer is deleted but onChildRemoved() doesn't have the time to be called so both users now have the same offer!
Is there any other idea how to make this operation more precise and time aware?
UPDATE as suggested from svi.data i tried this piece of code on user click but still the same problem occur.
offerUnAnsweredRef.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot dataSnapshot) {
boolean stillThere = false;
for (DataSnapshot offerSnap : dataSnapshot.getChildren()) {
if (offerSnap.getKey().equals(requestedOffer.getCurrentNodeKey())) {
stillThere = true;
}
}
if (stillThere) {
Timber.d("We have it " + requestedOffer.getEmployeeKey());
Toast.makeText(getContext(), "Welcome Dear ", Toast.LENGTH_SHORT).show();
offerUnAnsweredRef.child(requestedOffer.getCurrentNodeKey()).removeValue();
} else {
Toast.makeText(getContext(), "Go Away Bear", Toast.LENGTH_SHORT).show();
}
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError) {
}
});
UPDATE 2
Actually the solution is built on top of svi.data answer with some modifications so i wanted to share the working code to help if any one ever come cross similar situation
offerUnAnsweredRef.child(requestedOffer.getCurrentNodeKey()).runTransaction(new Transaction.Handler() {
#NonNull
#Override
public Transaction.Result doTransaction(#NonNull MutableData mutableData) {
RequestedOffer o = mutableData.getValue(RequestedOffer.class);
if (o == null) {
return Transaction.abort();
}
if (o.getEmployeeKey() == null) {
o.setEmployeeKey(employee.getUid());
mutableData.setValue(o);
return Transaction.success(mutableData);
} else {
return Transaction.success(mutableData);
}
}
#Override
public void onComplete(DatabaseError databaseError, boolean b,
DataSnapshot dataSnapshot) {
// check if the transaction completed successfully
// or if it failed
RequestedOffer o = dataSnapshot.getValue(RequestedOffer.class);
if (o.getEmployeeKey() == employee.getUid()) {
getActivity().runOnUiThread(() -> Toast.makeText(getActivity(), "Hello", Toast.LENGTH_SHORT).show());
DatabaseReference databaseReference = FirebaseFactory.getDatabase()
} else {
getActivity().runOnUiThread(() -> Toast.makeText(getActivity(), "NO", Toast.LENGTH_SHORT).show());
}
}
because as documentation says
public abstract Transaction.Result doTransaction (MutableData currentData)
This method will be called, possibly multiple times, with the current data at this location. It is responsible for inspecting that data and returning a Transaction.Result specifying either the desired new data at the location or that the transaction should be aborted.
So i added the code to check in onComplete to ensure that it called only once.
From what I understand:
1) You have a specific app for adding the offers (by you).
2) You have another app for reading the offers (by users).
3) If this is the case then both apps use the same project.
4) When a user clicks an offer, he/she will get the offer, then you will delete the offer from the database.
5) Now when 2 users click the same offer there is no time for the offer to be removed from the other user's list, so they end up with the same offer.
Now it seems that you don't want users to get same offers, and the problem really is a timing issue.
Possible solution:
1) When ever a user clicks an offer, you run a ValueEventListener() to the offers node in the database and check if the offer exist.
2) If the offer exists give him/her the offer and delete it.
3) Now when 2 users click the same offer, the ValueEventListener that I talked about will provide you with some time before reacting.
4) So users shouldn't end up with same offers.
Hope it solves your problem.
UPDATE:
As this is is a race condition between users, its time to talk about transactions. Firebase provides a nice way to directly read and write concurrent updates to the same node (which is your case).
I want your database to be like this:
Offers
|
|------offer_id_1
|
|-----taken:false
|-----......
|-----......
|
|-------offer_id_2
|
|------taken:false
|------......
|------......
Let me explain the above structure, each offer you post from the other application will have a flag by default called taken and it should have by default a value of false.
Now as you see above offer_id_1 and offer_id_2 are the push id or the random id given for the offer (when a user click on an offer you must get a reference of this key....I assume you know how to do this).
Before we start ofcourse you should have a model class for your posts we will call it Offer its just a class:
public class Offer{
public boolean taken;
......
......
}
The below function is what you will call after someone clicked an offer (we will use a transaction):
public void RunTransactionFor(String offer_id){
//first refer to your offers
DatabaseReference offers_ref = FirebaseDatabase.getInstance().getReference().child("offers").child(offer_id);
//run a transaction (a transaction is fast it reads and writes directly)
offer_ref.runTransaction(new Transaction.Handler() {
#Override
public Transaction.Result doTransaction(MutableData mutableData) {
//this is a ref to the offer class
Offer offer = mutableData.getValue(Offer.class);
if (offer == null) {
return Transaction.success(mutableData);
}
if(offer.taken == false){
//take the offer
offer.taken = true;
//show a message
Toast.makeText(context, "you took the offer",...).show();
//now you can remove the offer
offers_ref.setValue(null);//or delete it your way
}else{
//too late the offer is taken
Toast.makeText(context, "too late the offer is gone",...).show();
//do nothing
}
// Set value and report transaction success
mutableData.setValue(offer);
return Transaction.success(mutableData);
}
#Override
public void onComplete(DatabaseError databaseError, boolean b,
DataSnapshot dataSnapshot) {
// Transaction completed
}
});
}
Now when a user clicks an offer in the list store the id of the offer and pass it to the above function like this
//after user clicks
String offer_id = .......
//run transaction
RunTransactionFor(offer_id);
Note: Transactions only work online they can't work offline.
Related
I have an activity that registers active time data to my Firebase Realtime Database, this is input manually using an EditText which then displays it to a TextView in the UI. This is all working fine, however, if I was to put more data in, it would simply replace the value in the database and TextView.
I have previously done something similar to this in another activity (Adding (Sum) multiple Firebase child values together to give a total value?), however, this activity had additional nodes with dates, etc so the same approach would not work in my current activity. Ideally, I would have this node organized with dateStrings etc, however, for demonstration purposes, it's not necessary. Therefore I would just like to take whatever value is already existing in the database and add to it the input value, then restore that to the database.
It's probably quite simple but I've been staring at it so long I've confused myself. This is nowhere near right but just a bit confused about my next steps.. I've seen perhaps something along these lines how to sum money value in firebase realtime android studio but wondering if this is the easiest way?
Register Activity method:
registerActivity.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
String activityTime = activeMinutes.getText().toString().trim();
databaseReference.child("active time").setValue(activityTime);
addActivity.setVisibility(View.VISIBLE);
registerActivity.setVisibility(View.GONE);
activeTimeTI.setVisibility(View.GONE);
}
});
Method to display data (shortened):
databaseReference.addValueEventListener(new ValueEventListener() {
#RequiresApi(api = Build.VERSION_CODES.O)
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
int total = 0;
String activityTime = dataSnapshot.child("active time").getValue(String.class);
if (activityTime == null) {
activeTimeTV.setText("0");
} else {
total += Integer.parseInt(activityTime);
activeTimeTV.setText(String.valueOf(total) + " minutes");
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
Log.w("TAG", "onCancelled", databaseError.toException());
}
});
Firebase Hierarchy:
According to your last comment:
Active Time is at 500 (mins) just now, if I did another 90 mins of activity, I would like to add 90 to that value and then update that to show 590.
To increment the value of "active time" by 90, first of all, you need the change the type of the field to be number. That being said, your field should look like this:
ndkdn
|
--- active time: 500
See, there are no quotation marks. If your "ndkdn" node is a direct child of your Firebase Realtime Database root, to increase the value by 90, simply use the following lines of code:
DatabaseReference rootRef = FirebaseDatabase.getInstance().getReference();
DatabaseReference activeTimeRef = rootRef.child("ndkdn").child("active time");
activeTimeRef.setValue(ServerValue.increment(90));
This kind of operation is atomic. If you are not allowed to change the type of your field, to also have consistent data, then you should read the value first and increment it right after that using a transaction:
activeTimeRef.runTransaction(new Transaction.Handler() {
#Override
public Transaction.Result doTransaction(MutableData mutableData) {
Integer activeTime = Integer.parseInt(mutableData.getValue(String.class));
if (score == null) {
return Transaction.success(mutableData);
}
mutableData.setValue((activeTime + 90) + "");
return Transaction.success(mutableData);
}
#Override
public void onComplete(DatabaseError error, boolean b, DataSnapshot snapshot) {
Log.d("TAG", error.getMessage()); //Don't ignore potential errors!
}
});
I'm developing a social app, and I'm at the Feed part, which will display recent interactions made by friends. These friends are sorted by the amount of interaction we both have in the app - I call it RankFactor. The higher this value is, the higher my friend's interaction will appear.
First I read the User ID from my list of friends, ordered by The Rank Factor then I get each recent interaction.
I want the list of interactions displayed is created dynamically. It's supposed to be an infinite scroll, where per each scroll there will appear around 10 interactions.
So, ideally, I'd like that I'd keep reading friends ID's, and their interactions until I have 10 on the list. However, I'll have these interactions saved in a member List, which will be inside the ValueEventListener scope.
Even if I use orderByChild("rankFactor"), and then I limitToLast(10), nothing guarantees that these highest ranked Friends would have interacted recently. Therefore I believe I would need to have some kind of loop, focusing on the size of the interactions List.
How could I achieve my goal, knowing that "logically", what I want to achieve, would be something like the following:
while(mListOfInteractions.size() <= 10){
Query friendQuery = mDatabase.getReference().child(DB_REFERENCE).child(uId).child(FRIENDS_DB)
.orderByChild("rankFactor").endAt(mLastRankFactor - 0.0000001).limitToLast(1);
friendQuery.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot dataSnapshot) {
if(dataSnapshot.exists()){
FriendRank friendRank = dataSnapshot.getValue(FriendRank.class);
mLastRankFactor = friendRank.getRankFactor();
final String userId = friendRank.getUserId();
Query interactionsQuery = mDatabase.getReference().child(DB_REFERENCE).child(userId).child(INTERACTIONS_GIVEN).orderByChild("date")
.startAt(System.currentTimeMillis() - THRESHOLD_DAYS_IN_MILLIS).limitToLast(3);
interactionsQuery.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot dataSnapshot) {
if(dataSnapshot.hasChildren()){
for(DataSnapshot interactionData: dataSnapshot.getChildren()){
String interactionKey = interactionData.getKey();
Interaction interaction = interactionData.getValue(Interaction.class);
if(!DataUtils.doesListContainItem(mSavedInteractionsKeys, interactionsKey)){
mListOfInteractions.add(interactions);
mSavedInteractionsKeys.add(interactionKey);
}
}
}
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError) {
}
});
}
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError) {
}
});
}
So, as you see, I'd like that my list would keep on building up until I had gathered the amount of 10 Interactions (or a little more). The thing is, as many of you know, truly I can only access mListOfInteractions real value within the 2nd ValueEventListener.
How can I make the mListOfInteractions value accessible to my While loop?
Was I able to explain my challenge and goal?
Thank you in advance!
onPurchasesUpdated unable to consumeAsync and billingResult result error code 6.
I have tried moving consumeAsync to queryPurchases and queryPurchaseHistoryAsync, and the purchase will only be consume in queryPurchaseHistoryAsync and not queryPurchases
billingClient = BillingClient.newBuilder(this).setListener(new PurchasesUpdatedListener() {
#Override
public void onPurchasesUpdated(BillingResult billingResult, #Nullable List<Purchase> purchases) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
&& purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
//The following consumeAsync not working, return error code 6
ConsumeParams consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.setDeveloperPayload(purchase.getDeveloperPayload())
.build();
billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
#Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
//billingResult return code 6 here.
Log.i("TAG", String.valueOf(billingResult.getResponseCode()));
}
});
}
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
} else {
// Handle any other error codes.
}
}
}).enablePendingPurchases().build();
consumeAsync not working under queryPurchases():
Purchase.PurchasesResult purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
for(Purchase purchase : purchasesResult.getPurchasesList()) {
handlePurchase(purchase);
ConsumeParams consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.setDeveloperPayload(purchase.getDeveloperPayload())
.build();
billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
#Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
Log.i("TAG", String.valueOf(billingResult.getResponseCode()));
}
});
}
consumeAsync only works under the following code (I understand that the method is wrong and the item will be consumed over and over again and return code 8, but it is the only way that the item will be consumed):
billingClient.queryPurchaseHistoryAsync(BillingClient.SkuType.INAPP, new PurchaseHistoryResponseListener() {
#Override
public void onPurchaseHistoryResponse(BillingResult billingResult, List<PurchaseHistoryRecord> purchaseHistoryRecordList) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
&& purchaseHistoryRecordList != null) {
for (PurchaseHistoryRecord purchase : purchaseHistoryRecordList) {
handlePurchaseHistoryRecord(purchase);
ConsumeParams consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.setDeveloperPayload(purchase.getDeveloperPayload())
.build();
billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
#Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
Log.i("TAG", "item consumed");
}
});
}
}
}
});
Your code will consume every purchase as soon as it is made. In general, you would only want to consume a managed product if you want the buyer to be able to repurchase that product again. This might be the case if your item is expected to be consumable, such as additional coins in a game. Once the coins run out, you would consume the purchase, then the buyer could purchase more coins.
Another scenario where you might consume a purchase would be for testing, where you might want to purchase a product multiple times. You could set up a temporary button or other method to consume the purchase so that you can continue repurchasing while working out any bugs in your code.
A third scenario might be after you have issued a buyer a refund. After the refund, you might want to consume the purchase to give the buyer an option to purchase the product again.
These are the only three scenarios I can think of off hand where you would want to consume a managed product.
Edit
To YKW's comment:
YKW made a good point about purchasing additional coins that didn't initially occur to me... but either way, you can't consume the product a second time until you get a valid consumption response back from the first purchase consumption and then the product is purchased again. In this case, at a minimum you should first check to make sure the purchase is not PurchaseState.Pending. Then consume the product, not allowing the product to be purchased again until you get the onConsume response back. You will also want to check each time your app and/or activity starts and consume any purchased products as google confirms that occasionally a consumption will fail.
End Edit
UPDATE
Another possibility for receiving response code 6 is the developer payload string is empty. You can remove the .setDeveloperPayload(purchase.getDeveloperPayload()) line as specifying a developer payload is optional for ConsumeParams.
If you want to send a developer payload, make sure that purchase.getDeveloperPayload() is not returning an empty string.
END UPDATE
There is a good example of a method for consuming a purchase in the trivial drive app.
Trivial Drive V2 BillingManager
Here is a method I use for debugging based on the one found in the trivial drive V2 app.
void consumeAsync(final ConsumeParams consumeParams) {
// If we've already scheduled to consume this token - no action is needed (this could happen
// if you received the token when querying purchases inside onReceive() and later from
// onActivityResult()
if (paramsSetToConsume == null) {
paramsSetToConsume = new HashSet<>();
} else if (paramsSetToConsume.contains(consumeParams)) {
Log.i(TAG, "Params was already scheduled to be consumed - skipping...");
return;
}
paramsSetToConsume.add(consumeParams);
// Generating Consume Response listener
final ConsumeResponseListener onConsumeListener = new ConsumeResponseListener() {
#Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
// If billing service was disconnected, we try to reconnect 1 time
// (feel free to introduce your retry policy here).
Log.d(TAG, "ConsumeResponseListener fired. sending to onConsumedFinished listener");
mBillingUpdatesListener.onConsumeFinished(consumeParams.getPurchaseToken(), billingResult.getResponseCode());
}
};
// Creating a runnable from the request to use it inside our connection retry policy below
Runnable consumeRequest = new Runnable() {
#Override
public void run() {
// Consume the purchase async
Log.d(TAG, "consumeParams being sent to billingClient to consume");
billingClient.consumeAsync(consumeParams, onConsumeListener);
}
};
executeServiceRequest(consumeRequest);
}
Then I set up a temporary button, create my ConsumeParams and call this method in the onClick listener.
paramsSetToConsume is a member variable
private Set<ConsumeParams> paramsSetToConsume;
This question already has answers here:
How to save users score in firebase and retrieve it in real-time in Android studio
(3 answers)
Closed 4 years ago.
I'm having a problem when I try to star a movie which id doesn't exist in my likes firebase node. Here's my code:
private void onStarClicked(long releaseId, final String uid) {
final DatabaseReference postRef = ((GamePageActivity)getActivity()).mDatabaseRef.child("likes").child(mRegion).child(String.valueOf(releaseId));
postRef.runTransaction(new Transaction.Handler() {
#Override
public Transaction.Result doTransaction(MutableData mutableData) {
_Post p = mutableData.getValue(_Post.class);
if (p == null) {
Log.d(TAG, "Transaction success");
return Transaction.success(mutableData);
}
if (p.stars.containsKey(uid)) {
// Unstar the post and remove self from stars
p.starCount = p.starCount - 1;
p.stars.remove(uid);
} else {
// Star the post and add self to stars
p.starCount = p.starCount + 1;
p.stars.put(uid, true);
}
// 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 problem is if my id "releaseId" doesn't exist in the database, the star won't get added in, I thought my code was supposed to first add the "releaseId" if it doesn't exist?
It seems a bit complicated to try to add a movie inside your likes path during a ‘like’ transaction. Ideally, it would already be there. So it will be easier to separate adding the movie to the likes path, and after that use a Cloud Function onCreate or onUpdate.
I assume you have your movie data somewhere else in your database. Let’s call it the ‘movies’ path for now. One idea to try is to have a Cloud Function that watches your ‘movies’ path, and when a movie is added, the function will add that movie data to your ‘likes’ path. That way, once a user tries to like a movie, its information is already in the correct spot in the database. Fixing the problem of the non-existent 'releaseId'.
Firebase Realtime Database Triggers
If you don’t want to use a Cloud Function, you can consider writing to both the ‘movies’ path and ‘likes’ path directly from the client when the movie is added, to accomplish the same thing.
This question already has answers here:
Checking if a particular value exists in the Firebase database
(6 answers)
Closed 4 years ago.
I want to check if the specific user is present in database or not and if it is present then I don't want to override the data in the database.
I am using unique key generation at every node provided by firebase as User Id's
So I don't want to duplicate the user if he is already present.
How can I check if this user is present in database without listening to any events because my program execution is dependent on this values
When I am doing this the execution goes in a infinite loop
USER_REFERENCE.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
Iterable<DataSnapshot> children = dataSnapshot.getChildren();
for (DataSnapshot d : children) {
if (d.child("email").exists()) {
if (d.child("email").getValue().equals(user.getEmail())) {
user.setUserId(d.getKey());
mPresenter.saveUserIntoSession(user);
} else {
String userId = addUser(user);
user.setUserId(userId);
mPresenter.saveUserIntoSession(user);
}
}
}
}
Try this to check:
DatabaseReference ref=FirebaseDatabase.getInstance().getReference().child("Users");
ref.orderByChild("username").equalTo(name).addListenerForSingleValueEvent(new ValueEventListener(){
#Override
public void onDataChange(DataSnapshot dataSnapshot){
if(dataSnapshot.exist() {
//username exist
}
}
The above will read data once and it will check if a particular username exists in the database
Use addListenerForSingleValueEvent to read data once.
Restating the answer in simpler terms:
Given:
#Override
public boolean onLongClick(View view) {
return true; // or false
}
return true means that the event is consumed. It is handled. No other click events will be notified.
return false means the event is not consumed. Any other click events will continue to receive notifications.
So if you don't want onClick to also be triggered after an onLongClick, then you should return true from the onLongClick event.