Adding data to FirebaseDatabase when there is no Internet connection - android

If I try to send data to FirebaseDatabase in the absence of an Internet connection, then, as expected, nothing happens. If you turn on the Internet, then this data is added themselves, and even if I restarted the application. And they are added to the very end of the database. Even if you add data after this, the offline post will still be the last.
I think to solve the problem by checking the Internet before sending. Maybe there are any other solution methods?
I send data in a simple way:
final String phone_val = etPhone.getText().toString().trim();
final String comment_val = etComment.getText().toString().trim();
DatabaseReference newTrip = mDatabase.push();
newTrip.child("phone").setValue(phone_val);
newTrip.child("comment").setValue(comment_val);
newTrip.child("type").setValue(1);
startActivity(new Intent(AddDriverActivity.this, TripActivity.class));
finishAffinity();

Firstly, if you haven't already, read through the offline capabilities documentation so you have a general grasp of how Firebase behaves while offline.
Next, we'll clean up your write operations so that they are a single atomic operation rather than a few separate write operations.
HashMap<String, Object> tripData = new HashMap<>();
tripData.put("phone", phone_val);
tripData.put("comment", comment_val);
tripData.put("type", 1);
DatabaseReference newTrip = mDatabase.push();
newTrip.setValue(tripData);
As stated in the offline capabilities documentation, you can check whether your app is offline by checking the special database location /.info/connected which returns the current connection state. This value will be either true or false.
While you could check this value before posting your trips, the connection state may change while you are sending the data.
Even if you add data after this, the offline post will still be the last.
This is the trickier part to manage. I think the easiest way to deal with this is to have a "staging" section of your database and then move data as it is created at this location to the main storage location using a Cloud Function for Firebase.
Client Side
Let's say you are storing these trips in /trips/someTripId.
private DatabaseReference mDatabase = FirebaseDatabase.getInstance().getReference();
private DatabaseReference mAllTripsRef = mDatabase.child('trips');
To add a new trip, you would use:
HashMap<String, Object> tripData = new HashMap<>();
tripData.put("phone", phone_val);
tripData.put("comment", comment_val);
tripData.put("type", 1);
DatabaseReference mNewTripRef = mAllTripsRef.push();
mNewTripRef.setValue(tripData);
Because references created by push() are based on the estimated time of the Firebase servers, they will be ordered by when they were created rather than when they are received by the Firebase servers. But if you wanted to preserve that offline trips are always last, instead of writing new trips to /trips, you would instead write to /trips-staging.
private DatabaseReference mDatabase = FirebaseDatabase.getInstance().getReference();
// trips that have been posted to "online" database
private DatabaseReference mAllTripsRef = mDatabase.child('trips');
// trips yet to be posted online
private DatabaseReference mStagingTripsRef = mDatabase.child('trips-staging');
New data would be added using:
HashMap<String, Object> tripData = new HashMap<>();
tripData.put("phone", phone_val);
tripData.put("comment", comment_val);
tripData.put("type", 1);
DatabaseReference mNewTripRef = stagingTripsRef.push();
mNewTripRef.setValue(tripData);
Now that we have the reference to the trip waiting to be posted, mNewTripRef, we can add a listener to it to see when it has been posted.
In the cloud side below, we are going to make it so that if there is data at /trips-staging/someTripId and it is just a string, then the trip has been received and posted by the server to the location /trips/<string-value>.
ValueEventListener stagingTripListener = new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
// Get trip data
Object tripData = dataSnapshot.getValue();
if (tripData == null) {
// Data has been deleted!
// Disconnect this listener
mNewTripRef.removeEventListener(this);
// TODO: What now?
} else if (tripData instanceof String) {
// Data has been moved!
DatabaseReference postedTripRef = mAllTripsRef.child((String) tripData);
// Disconnect this listener
mNewTripRef.removeEventListener(this);
Log.i(TAG, "stagingTripListener:onDataChange", "New trip has been successfully posted as trip '" + mNewTripRef.getKey() + "'");
// TODO: do something with postedTripRef
} else {
// ignore - the trip hasn't been moved yet, continue waiting
// tripData is a Map<string, Object> with our original data
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
// Getting this trip failed, log a message
Log.w(TAG, "stagingTripListener:onCancelled", databaseError.toException());
}
};
mNewTripRef.addValueEventListener(stagingTripListener);
Cloud Side
Now, we need to move these new trips over to /trips once they are received on the server. For this we can use a Cloud Function for Firebase that listens to Realtime Database events. For our use case, we want to exclusively listen to data creation events that happen on our /trips-staging location.
When the Cloud Function is triggered, it should take the data at /trips-staging/someId and move it to /trips/someNewId. It is probably also a good idea to store where we moved the data to at the old location if it is ever needed but also so we can tell when the trip has been received by the server.
After following the Getting Started documentation up to Step 4, you can use the following code as your index.js or index.ts file and then deploy it using firebase deploy --only functions.
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp(); // use defaults
export const moveTripsFromStaging = functions.database.ref('/trips-staging/{stagingTripId}')
.onCreate((snapshot, context) => {
const stagingTripId = snapshot.key;
const tripData = snapshot.val();
// get reference to the root of the database
let dbRootRef = admin.database().ref();
let allTripsRef = dbRootRef.child('trips');
let newTripRef = allTripsRef.push();
return dbRootRef.update({
// move data to it's new home
['trips/' + newTripRef.key]: tripData,
// save where we moved data to as a simple string containing the new key in /trips
['trips-staging/' + stagingTripId]: newTripRef.key // or set to null to delete it
});
})
Once deployed, you should see new trips that are uploaded to /trips-staging be received by the server and then moved across to /trips in the order that server receives them.

Related

Firebase Real-Time Database what would happen to concurrent requests in this case?

I have 2 nodes in my database items and my_items.
I have this function that does a multi-path update, this function deletes an item from items and adds it to the user's my_items:
I secured the my_items node in a way that you can only write to it if the item exists in items, otherwise it fails:
private void getItem(String uid, String item_key) {
Map<String, Object> updates = new HashMap<>();
//delete item
updates.put("items/" + item_key, null);
//give the item to the user
updates.put("my_items/" + uid + "/" + item_key, itemObject);
mDatabaseReference.updateChildren(updates);
}
Question (In theory):
Knowing that Firebase Database handles requests one by one.
If users A,B,C,D called the getItem(..) function for the same item id together at the same time:
Does this makes sense:
lets say A's request reached the server first and Succeeds
(now the item was deleted from items and added to user A at my_items)
B: Fails
(because the item no longer exists in items, so security rule of my_items prevents this)
C: Fails
(same reason)
D: Fails
(same reason)
Is this what would happen ? Or I got this wrong ?
Thanks.
The Firebase Realtime Database processes (write) operations sequentially. So any previous write operation will have been completed before the next one is processed. That means that any data written by a previous user will indeed be present in the root variable in security rules evaluation for later write operations.
This Firebase Blog post discusses atomic operations. RTDB acts on requests as they arrive. RTDB processes events as they arrive. There is another question that might be relevant Does Firebase always guarantee added events in order?
You can ... do this in a single atomic update to both locations:
JavaScript
var ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com");
// Generate a new push ID for the new post
var newPostRef = ref.child("posts").push();
var newPostKey = newPostRef.key();
// Create the data we want to update
var updatedUserData = {};
updatedUserData["user/posts/" + newPostKey] = true;
updatedUserData["posts/" + newPostKey] = {
title: "New Post",
content: "Here is my new post!"
};
// Do a deep-path update
ref.update(updatedUserData, function(error) {
if (error) {
console.log("Error updating data:", error);
}
});
Java
Firebase ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com");
// Generate a new push ID for the new post
Firebase newPostRef = ref.child("posts").push();
String newPostKey = newPostRef.getKey();
// Create the data we want to update
Map newPost = new HashMap();
newPost.put("title", "New Post");
newPost.put("content", "Here is my new post!");
Map updatedUserData = new HashMap();
updatedUserData.put("users/posts/" + newPostKey, true);
updatedUserData.put("posts/" + newPostKey, newPost);
// Do a deep-path update
ref.updateChildren(updatedUserData, new Firebase.CompletionListener() {
#Override
public void onComplete(FirebaseError firebaseError, Firebase firebase) {
if (firebaseError != null) {
System.out.println("Error updating data: " + firebaseError.getMessage());
}
}
});
Objective-C
Firebase *ref = [[Firebase alloc] initWithUrl: #"https://<YOUR-FIREBASE-APP>.firebaseio.com"];
// Generate a new push ID for the new post
Firebase *newPostRef = [[ref childByAppendingPath:#"posts"] childByAutoId];
NSString *newPostKey = newPostRef.key;
NSString *updateUserPath = [NSString stringWithFormat:#"users/posts/%#", newPostKey];
NSString *updatePostPath = [NSString stringWithFormat:#"posts/%#", newPostKey];
// Create the data we want to update
NSMutableDictionary *updatedUserData = [[NSMutableDictionary alloc] init];
[updatedUserData setValue:[NSNumber numberWithBool:YES] forKey:updateUserPath];
[updatedUserData setValue:#{#"title": #"New Post", #"content": #"Here is my new post!"} forKey:updatePostPath];
// Do a deep-path update
[ref updateChildValues:updatedUserData withCompletionBlock:^(NSError *error, Firebase *ref) {
if (error) {
NSLog(#"Error updating data: %#", error.debugDescription);
}
}];
Swift
"https://<YOUR-FIREBASE-APP>.firebaseio.com") // Generate a new push
ID for the new post let newPostRef =
ref.childByAppendingPath("posts").childByAutoId() let newPostKey =
newPostRef.key // Create the data we want to update let
updatedUserData = ["users/posts/(newPostKey)": true,
"posts/(newPostKey)": > ["title": "New Post", "content": "Here is my
new post!"]] // Do a deep-path update
ref.updateChildValues(updatedUserData, withCompletionBlock: { (error,
ref) -> > Void in
if (error) {
print("Error updating data: (error.description)")
} })
Deep path updates let you write cleaner code and easily denormalize
data across multiple nodes in your Firebase database.

Mongodb- How to get data from a partition that was created by other user?

I am using Realm MongoDB for my android app, and I have a problem:
I have different users in my app, and each user has his "cards". The partition of each user's cards is:
"Card=userID".
So, I want to be able to send a card from one user to the other. I do it via a link that includes userID and specific cardID.
So my code looks something like:
Realm.init(this);
mainApp = new App(new AppConfiguration.Builder(APP_ID).defaultSyncErrorHandler((session, error) ->
Log.e("TAG()", "Sync error: ${error.errorMessage}")
).build());
//TEMP CODE
String partition = "Card=611d7n582w36796ce34af106"; //test partition of another user
if(mainApp.currentUser() != null) {
SyncConfiguration config = new SyncConfiguration.Builder(
mainApp.currentUser(),
partition)
.build();
Realm realmLinkCard = Realm.getInstance(config);
Log.d(TAG, "onCreate: cards found- " + realmLinkCard.where(Card.class).findAll().size());
}
The last log always shows 0. I know there are cards for sure because if the user that created the corresponding partition is signed in then it does find the cards.
permissions are set to true for both read and write for the whole sync.
What can the problem be?
You cannot access a Realm by a user who has a different partition.
Instead you can create a mongodb function and call it from your user.
Make your function here:
Check here on How to create a function
And call it by checking here on How to call a function from client
Quick example of a realm function:
exports = async function funcName(partition) {
const cluster = context.services.get('myclustername');
const mycollection = cluster.db('mydbname').collection('mycollectionname');
let result = [];
try {
result = mycollection.findOne({
_partition: partition,
});
} catch (e) {
result.push(e);
return result;
}
return result;
};
To call it, please see above for the documentation as I'm not an Android developper.

Xamarin Forms Android: map.Pins.Clear() issue

my development team and I have run into an issue on our Android distribution of our Xamarin project. The issue is as such: The application uses an observable collection of objects and represents these objects in the form of a list view and a map view with pins representing the objects. In the map view, our code is designed to subscribe to a messaging center call that periodically updates the observable collection of objects from our API (other part of project). The issue we are having is that when we call PlotPins method in the messaging center code block, the application should first retrieve the updated list and then access that list to plot pins on the map. Every time an update is received, the application will clear all pins from the map and then replot the pins based on the updated list (inefficient we know, but this is a temporary solution). However, the pins are never updated. Through the use of the debugger we have discovered that once map.Pins.Clear() within PlotPins() is called, the application jumps to the end of the RequestUpdatedListAsync method (which occurs periodically to retrieve the updated list and which triggers the Messaging Center) and then halts.
Our solution works for our GTK build, with the pins being cleared and redrawn on the map as intended, so this seems to be an Android specific issue.
Any help would be appreciated, thank you.
Relevant code located below:
MESSAGING CENTER:
MessagingCenter.Subscribe<object, ObservableCollection<MyObject>>(Application.Current, Constants.ListUpdateContract, (sender, newList) =>
{
//update list
listUpdater.UpdateList(newList);
//call method to plot pins again
PlotPins(map);
});
PLOTPINS:
private void PlotPins(Map map)
{
map.Pins.Clear();
foreach (MyObject in MyObjects)
{
var pin = new Pin
{
Label = MyObject.ID,
Address = "Latitude: " + MyObject.Latitude + " " + "Longitude: " + MyObject.Longitude,
Type = PinType.Place,
Position = new Position(Convert.ToDouble(MyObject.Latitude), Convert.ToDouble(MyObject.Longitude))
};
//event handler for when user clicks on pin's info window
pin.InfoWindowClicked += async (s, args) =>
{
//opens up detail page for pin associated with myObject
await Navigation.PushAsync(new DetailPage(MyObject));
};
map.Pins.Add(pin);
}
}
REQUEST UPDATED LIST ASYNC:
public static async System.Threading.Tasks.Task<bool> RequestUpdatedListAsync()
{
if (!_tokenIsGood)
return false;
var success = false;
var response = new HttpResponseMessage();
try
{
response = await _client.GetAsync(Constants. MyObjectDisplayUrl);
}
catch (Exception e)
{
System.Diagnostics.Debug.WriteLine("Error requesting updated list.");
System.Diagnostics.Debug.WriteLine(e.Message);
System.Diagnostics.Debug.WriteLine(e.StackTrace);
return success;
}
response.EnsureSuccessStatusCode();
success = true;
var responseBody = await response.Content.ReadAsStringAsync();
// Update list
MyObjects.Clear();
MyObjects = JsonConvert.DeserializeObject<ObservableCollection< MyObject >>(responseBody);
//Alert subscribed ViewModels to update list
MessagingCenter.Send<object, ObservableCollection< MyObject >>(Application.Current, Constants.ListUpdateContract, units);
return success;
}
Since maps.Pins is UI related it has to be run in main UI thread.
MessagingCenter doesnt always publish/subscribe in main threads .
So to fix this issue call the maps.Pins.Clear() in main thread.
Device.BeginInvokeOnMainThread(()=> maps.Pins.Clear());
Credits: #shanranm for mentioning limitation of MessagingCenter for using main threads.

Calling sendFollowerNotification Firebase Function from Android App

So I realize that, since version 12.0, you can call Firebase Functions directly from an Android app... this makes sense with the given example for sending messages:
private Task<String> addMessage(String text) {
// Create the arguments to the callable function.
Map<String, Object> data = new HashMap<>();
data.put("text", text);
data.put("push", true);
return mFunctions
.getHttpsCallable("addMessage")
.call(data)
.continueWith(new Continuation<HttpsCallableResult, String>() {
#Override
public String then(#NonNull Task<HttpsCallableResult> task) throws Exception {
// This continuation runs on either success or failure, but if the task
// has failed then getResult() will throw an Exception which will be
// propagated down.
String result = (String) task.getResult().getData();
return result;
}
});
}
...where you're sending text to a function.
exports.addMessage = functions.https.onCall((data, context) => {
// [START_EXCLUDE]
// [START readMessageData]
// Message text passed from the client.
const text = data.text;
// [END readMessageData]
// [START messageHttpsErrors]
// Checking attribute.
if (!(typeof text === 'string') || text.length === 0) {
// Throwing an HttpsError so that the client gets the error details.
throw new functions.https.HttpsError('invalid-argument', 'The function must be called with ' +
'one arguments "text" containing the message text to add.');
}
// Checking that the user is authenticated.
if (!context.auth) {
// Throwing an HttpsError so that the client gets the error details.
throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +
'while authenticated.');
}
But I'm not exactly sure what I should be sending for something like the sendFollowerNotification example:
https://github.com/firebase/functions-samples/tree/master/fcm-notifications
exports.sendFollowerNotification = functions.database.ref('/followers/{followedUid}/{followerUid}')
.onWrite((change, context) => {
const followerUid = context.params.followerUid;
const followedUid = context.params.followedUid;
// If un-follow we exit the function.
if (!change.after.val()) {
return console.log('User ', followerUid, 'un-followed user', followedUid);
}
I mean... assuming the users are logged-in and have firebase UIDs and are in the database (my app automatically creates a firebase user when someone logs in)... it would appear that sendFollowerNotification just gets everything from the realtime database.
So what do I put under?:
.call(data)
And how am I retrieving the UID for the user that I'm trying to follow? For one that's logged in and using the app... I obviously already have that user's UID, token, and everything else... but I'm unsure of how to retrieve that info for the user who's about to be followed... if that makes any sense.
I've googled all over the internet and have never found an example of this particular kind of function call being used from within an android app using the new post 12.0.0 method. So I'm curious to know what the proper syntax should be.
Ok! This one really enraged me trying to figure it out... It turns out you don't need to call "sendFollowerNotification" at all.. All it does is it listens for changes to the Firebase Realtime Database. If you make changes in the syntax where sendFollowerNotification is looking... it automatically sends out the notification.
There's no call at all in "sendFollwerNotification" for writing users to the Realtime Database. I actually handle this at login:
private DatabaseReference mDatabase; //up top
mDatabase = FirebaseDatabase.getInstance().getReference(); //somewhere in "onCreate"
final String userId = mAuth.getUid();
String refreshedToken = FirebaseInstanceId.getInstance().getToken();
mDatabase.child("users").child(userId).child("displayName").setValue(name);
mDatabase.child("users").child(userId).child("notificationTokens").child(refreshedToken).setValue(true);
mDatabase.child("users").child(userId).child("photoURL").setValue(avatar);
Then when one user follows another I just write that to the realtime database as well:
mDatabase.child("followers").child(user_Id).child(follower_id).setValue(true);
And that's it! The second a new follower is added to the RealTime Database... sendFollwerNotification will automatically send out a notification. You just need to setup a listener in your app for receiving messages and where it should redirect your users once they tap a message that's been received and you're done.

How can I set Firebase Database Security Rules for Android App

I'm making android app which will collect useful info from user. Here is the code
Firebase postRef = mRef.child("marks");
Map<String, String> marks = new HashMap<String, String>();
marks.put("Name", fname);
marks.put("Phone", fphone);
marks.put("Year", fyear);
marks.put("Email", femail);
String uid=postRef.getKey();
Toast.makeText(im.this,"Thanks!",Toast.LENGTH_LONG).show();
This will result in getting the data from every user of my Android App. How should I set specific rules by which only authenticated users can get access of their information. Not other users info.
I tried this in Firebase Rules:
{
"rules":{
"$uid":{
".read":"auth.uid==$uid",
".write":"auth.uid==$uid",
".validate":"newData.hasChildren(['uid','Email','Name'])"
}
}
}
But its not working. Kindly tell me specific solution.
If I read your question and comment correctly you are putting your data under the "marks" node. That means you also have to include that in your security rules like this:
{
"rules":{
"marks":{
"$uid":{
".read":"auth.uid==$uid",
".write":"auth.uid==$uid",
".validate":"newData.hasChildren(['uid','Email','Name'])"
}
}
}
}

Categories

Resources