I am trying to receive the JSON value from the Realtime Database of Firebase using Unity.
I do the following:
FirebaseDatabase.DefaultInstance
.GetReference("Leaders").OrderByChild("score").GetValueAsync().ContinueWith(task =>
{
if (task.IsFaulted)
{
Debug.LogError("error in reading LeaderBoard");
return;
}
else if (task.IsCompleted)
{
Debug.Log("Received values for Leaders.");
string JsonLeaderBaord = task.Result.GetRawJsonValue();
callback(JsonLeaderBaord);
}
}
});
Trying to Read the CallBack :
private string GetStoredHighScores()
{
private string JsonLeaderBoardResult;
DataBaseModel.Instance.RetriveLeaderBoard(result =>
{
JsonLeaderBoardResult = result; //gets the data
});
return JsonLeaderBoardResult; //returns Null since it doesn't wait for the result to come.
}
Question is how do i wait for the callback to return the value and afterwards return the value of the JsonLeaderBoardResult.
return JsonLeaderBoardResult; //returns Null since it doesn't wait
for the result to come.
The RetriveLeaderBoard function doesn't return immediately. You can either use coroutine to wait for it or return the JsonLeaderBoardResult result via Action. Using Action make more sense in your case.
Change the string return type to void then return the result through Action:
private void GetStoredHighScores(Action<string> callback)
{
string JsonLeaderBoardResult;
DataBaseModel.Instance.RetriveLeaderBoard(result =>
{
JsonLeaderBoardResult = result; //gets the data
if (callback != null)
callback(JsonLeaderBoardResult);
});
}
Usage:
GetStoredHighScores((result) =>
{
Debug.Log(result);
});
EDIT:
That is great, but still need to do some stuff after getting the
result in `GetStoredHighScores' outside the Action, otherwise i can
get an error like: get_transform can only be called from the main
thread.
You get this error because RetriveLeaderBoard is running from on another Thread. Grab UnityThread from this post then do the callback on the main Thread with UnityThread.executeInUpdate.
Your new code:
void Awake()
{
UnityThread.initUnityThread();
}
private void GetStoredHighScores(Action<string> callback)
{
string JsonLeaderBoardResult;
DataBaseModel.Instance.RetriveLeaderBoard(result =>
{
JsonLeaderBoardResult = result; //gets the data
UnityThread.executeInUpdate(() =>
{
if (callback != null)
callback(JsonLeaderBoardResult);
});
});
}
You're seeing a classical confusion with asynchronous APIs. Since loading data from Firebase may take some time, it happens asynchronously (on a separate thread). This allows your main code to continue, while the Firebase client is waiting for a response from the server. When the data is available, the Firebase client calls your callback method with that data, so that you can process it.
It's easiest to see what this does in your code by placing a few logging statements:
Debug.Log("Before starting to load data");
FirebaseDatabase.DefaultInstance
.GetReference("Leaders").OrderByChild("score").GetValueAsync().ContinueWith(task => {
Debug.Log("Got data");
}
});
Debug.Log("After starting to load data");
When you run this code, it prints:
Before starting to load data
After starting to load data
Got data
This is probably not the order in which you expected the output. Due to the asynchronous nature of the call to Firebase, the second log line gets printed last. That explains precisely why you're seeing an empty array when you return it: by that time the data hasn't been loaded from Firebase yet, and your ContinueWith hasn't executed yet.
The solution to this is always the same: since you can't return data that hasn't loaded yet, you have to implement a callback function. The code you shared does that twice already: once for the ContinueWith of Firebase itself and one in RetriveLeaderBoard.
Any code that needs to have the up to date leaderboard, will essentially have to call RetriveLeaderBoard and do any work with the leaderboard data in its callback. For example, if you want to print the leaderboard:
DataBaseModel.Instance.RetriveLeaderBoard(result => {
Debug.Log(result);
});
Related
I have looked all over for a solution to an issue. I have noticed that in my android app, every time I fire an event from <button Clicked="GPSToggle_Clicked">, for some reason it increments the number of times my methods get called. So after I compile and load this on my phone, I hit my "GPSToggle_Clicked" button, and then to stop hit that button again. On the first "stop", I'll get a single instance of the below output:
---------------------------------------------------------------Attempting string parsing
---------------------------------------------------------------Sending string to SubmitGeneratedGPX
---------------------------------------------------------------path: /storage/emulated/0/Download/GPX/2022-10-27-02-44-06.gpx
---------------------------------------------------------------GPX File creation success
---------------------------------------------------------------:RawBufferToJsonString: [{"Timestamp":"2022-10-27T18:43:52.849+00:00","Latitude":41.5263818,"Longitude":-81.6507923,"Altitude":153.29998779296875,"Accuracy":20.0,"VerticalAccuracy":1.7990270853042603,"Speed":null,"Course":null,"IsFromMockProvider":false,"AltitudeReferenceSystem":2},{"Timestamp":"2022-10-27T18:43:53.696+00:00","Latitude":41.5263819,"Longitude":-81.6507921,"Altitude":153.29998779296875,"Accuracy":20.0,"VerticalAccuracy":1.7697961330413818,"Speed":null,"Course":null,"IsFromMockProvider":false,"AltitudeReferenceSystem":2},{"Timestamp":"2022-10-27T18:43:54.526+00:00","Latitude":41.5263819,"Longitude":-81.6507921,"Altitude":153.29998779296875,"Accuracy":20.0,"VerticalAccuracy":1.7697961330413818,"Speed":null,"Course":null,"IsFromMockProvider":false,"AltitudeReferenceSystem":2},{"Timestamp":"2022-10-27T18:43:55.374+00:00","Latitude":41.5263819,"Longitude":-81.6507921,"Altitude":153.29998779296875,"Accuracy":20.0,"VerticalAccuracy":1.7697961330413818,"Speed":null,"Course":null,"IsFromMockProvider":false,"AltitudeReferenceSystem":2},{"Timestamp":"2022-10-27T18:43:56.21+00:00","Latitude":41.5263811,"Longitude":-81.650792,"Altitude":153.29998779296875,"Accuracy":20.0,"VerticalAccuracy":1.7160584926605225,"Speed":null,"Course":null,"IsFromMockProvider":false,"AltitudeReferenceSystem":2}]
Every subsequent time I hit start/stop on the app, I get the real-time data in the output multiplied by the number of times I've started/stopped since the last compiling.
the main app page button event thats fired:
private async void GPSToggle_Clicked(object sender, EventArgs e)
{
var LocationPermissionStatus = await Xamarin.Essentials.Permissions.RequestAsync<Xamarin.Essentials.Permissions.LocationAlways>();
var FileReadPermissionStatus = await Xamarin.Essentials.Permissions.RequestAsync<Xamarin.Essentials.Permissions.StorageRead>();
var FileWritePermissionStatus = await Xamarin.Essentials.Permissions.RequestAsync<Xamarin.Essentials.Permissions.StorageWrite>();
if(LocationPermissionStatus == Xamarin.Essentials.PermissionStatus.Denied)
{
// TODO
return;
}
// run if device is android
if(Device.RuntimePlatform == Device.Android)
{
if (!CrossGeolocator.Current.IsGeolocationAvailable || !CrossGeolocator.Current.IsGeolocationEnabled)
{
// gps is not enabled, throw alert
Console.WriteLine("---------------------------------------------------------------GPS is DISABLED");
await DisplayAlert("Error", "GPS is not enabled. You must enable GPS to use this feature", "Ok");
}
else
{
// set our IsTracking = true flag
if (!IsTracking)
{
// start background listening for GPS
await StartListening();
Console.WriteLine("---------------------------------------------------------------Listening: " + CrossGeolocator.Current.IsListening);
StartService();
Console.WriteLine("---------------------------------------------------------------Service initiated");
IsTracking = true;
Console.WriteLine("---------------------------------------------------------------Tracking initiated");
GPSToggle.Text = "Stop Tracking";
}
else
{
//
// verify that the submittal wasn't done in error, before stopping services and submitting data
bool DoneInError = await DisplayAlert("Finish?", "Are you sure you want to stop services and submit?", "No", "Yes");
if (!DoneInError)
{
await StopListening();
Console.WriteLine("---------------------------------------------------------------listening:" + CrossGeolocator.Current.IsListening);
IsTracking = false;
Console.WriteLine("---------------------------------------------------------------Tracking ceased");
// stop the gps service
StopService();
Console.WriteLine("---------------------------------------------------------------Service ceased");
// stop the background listening for gps
Console.WriteLine("---------------------------------------------------------------Attempt GPX parse from buffer obj");
GPSToggle.Text = "Start Tracking";
}
}
}
}
}
Specifically the line:
StartService();
Fires this method off within the same class, specifically the MessagingCenter.Send<>, which initiates my foreground service to handle logging the gps data into a buffer:
private void StartService()
{
var startServiceMessage = new StartServiceMessage();
MessagingCenter.Send(startServiceMessage, "ServiceStarted");
Preferences.Set("LocationServiceRunning", true);
StatusLabel.Text = "Location service has been started";
Console.WriteLine("---------------------------------------------------------------location service has been started. preferences saved");
}
and
StopService();
Fires this method off to stop the services and retrieve the gps buffer data from the foreground to the main thread:
private void StopService()
{
var stopServiceMessage = new StopServiceMessage();
MessagingCenter.Unsubscribe<App, List<Location>>(this, "GPXBufferData");
MessagingCenter.Subscribe<App, List<Location>>(this, "GPXBufferData", (sender, args) =>
{
RawGPXData = args;
Generate_CreateGPX_File(RawGPXData);
RawBufferToJsonString = GPXParse.GenerateJSON_GPXPoints(RawGPXData);
Console.WriteLine("---------------------------------------------------------------:RawBufferToJsonString: " + RawBufferToJsonString);
PromptForSubmission_GPXPoints_API();
});
Console.WriteLine("--------------------------------------------------------------------------");
MessagingCenter.Send(stopServiceMessage, "ServiceStopped");
Preferences.Set("LocationServiceRunning", false);
Console.WriteLine("---------------------------------------------------------------Location service stopped. preferences saved");
}
In the above snippet, this line is subscribed to in the GPSLocationService.cs file:
MessagingCenter.Send(stopServiceMessage, "ServiceStopped");
This is a portion of my GPSLocationService.cs file that is relevant to this:
public async Task Run(CancellationToken token)
{
int ObjCount = 0;
await Task.Run(async () => {
// if the task was stopped
// check the buffer for data, if data, send to GPXGenerator
MessagingCenter.Subscribe<StopServiceMessage>(this, "ServiceStopped",
message =>
{
if (GPSBufferObj != null)
{
Device.BeginInvokeOnMainThread(() =>
{
MessagingCenter.Unsubscribe<App, List<Location>>((App)Xamarin.Forms.Application.Current, "GPXBufferData");
MessagingCenter.Send<App, List<Location>>((App)Xamarin.Forms.Application.Current, "GPXBufferData", GPSBufferObj);
});
}
});
return;
}, token);
}
I believe I have tracked down where the issue is starting. In my StopService() method, I have the following line (just to keep track of where Im at in the buffer) and it is only sent to output once.
Console.WriteLine("--------------------------------------------------------------------------");
BUT if I place that same line within the pasted portion of my GPSLocationService.cs file, I will get the incremented output. I'm leaning towards the nested task being the issue, I wrote this based losely off of this example repro:
https://github.com/jfversluis/XFBackgroundLocationSample
You don't have MessagingCenter.Unsubscribe<StopServiceMessage> anywhere in your code. StopServiceMessage is what you are accumulating subscriptions to.
You need to make sure Unsubscribe is unsubscribing the instance that was previously subscribed to. It sounds to me like there are multiple instances of GPSLocationService. [In which case, this is no longer referring to the original instance. Unsubscribe won't do anything, unless you have the this that was originally Subscribed.]
If so, instead create an instance of GPSLocationService ONCE, and store it in a static variable. Re-use it. start/stop it, but don't discard it.
Alternatively, if you only want a message ONE TIME from each Subscribe, then Unsubscribe as soon as you receive each message:
MessagingCenter.Subscribe<StopServiceMessage>(this, "ServiceStopped",
message =>
{
MessagingCenter.Unsubscribe<StopServiceMessage>(this, "ServiceStopped");
... your code ...
});
Use this same pattern EVERYWHERE you have a Subscribe (unless you Subscribe ONLY ONE TIME at app start, as Jason suggested.)
I am using the following code to retrieve data from Firebase database on a user with Unity3D, in our case i am getting User Level:
FirebaseDatabase.DefaultInstance
.GetReference("users").Child(userID)
.GetValueAsync().ContinueWith(task =>
{
if (task.IsFaulted)
{
Debug.LogError("Error retriving user data: " + userID);
// Handle the error...
}
else if (task.IsCompleted)
{
DataSnapshot snapshot = task.Result;
int TempUserLevel = (int)snapshot.Child("Level").Value;
//this get's an error
PlayerPrefs.SetInt(_UserLevel, TempUserLevel);
}
}
Error:
TrySetInt can only be called from the main thread. Constructors and
field initializers will be executed from the loading thread when
loading a scene.
As I understand the TASK is a new thread and not Unity Main thread. Still
I can't seem save values locally on unity, or get the value out of the TASK.
It cannot be called because continue with is a delegate and it waits for response. What I did is just made a waituntil coroutine before calling this delegate using and set a bool for instance some bool check = false.
else if(task.IsCompleted)
{
// your operation
check=true;
}
////////
IEnumerator myRoutine()
{
yield return new WaitUntil ( () => check );
// set your playerprefs.
}
Actually you could just change "ContinueWith" to "ContinueWithOnMainThread".
You can change ContinueWith to ContinueWithOnMainThread, but you will need add "using Firebase.Extensions;"
using Firebase.Extensions;
FirebaseDatabase.DefaultInstance.GetReference("users").Child(userID)
.GetValueAsync().ContinueWithOnMainThread(task =>
{
if (task.IsFaulted)
{
// Handle the error...
}
else if (task.IsCompleted)
{
DataSnapshot snapshot = task.Result;
// Do something with snapshot...
}
});
See documentation for more examples: https://firebase.google.com/docs/database/unity/retrieve-data
Potential Note: If you are following documentation for Google Sign in with Firebase and downloaded the Firebase SDK for Unity 2020, you may get an error regarding a conflict with System.Threading.Tasks (this happened to me). If anyone else gets this error, it can be dealt with by deleting or renaming the Unity.Compat and Unity.Tasks files under Unity > Assets > Parse > Plugins, but do not change or delete the files in the dotNet45 folder.
Specifically I am doing C# development using Xamarin.Forms, however working on the native Android side writing a GPS wrapper class which will be usable in the Xamarin.Forms side via dependency injection. The calls should be the same between C# and Java in regards to Android for the most part.
Essentially, I have this method in my Geolocator object (which implements ILocationListener) on the Android side:
public async Task<Tuple<bool, string, GPSData>> GetGPSData() {
gpsData = null;
var success = false;
var error = string.Empty;
if (!manager.IsProviderEnabled(LocationManager.GpsProvider)) {
//request permission or location services enabling
//set error
} else {
manager.RequestSingleUpdate(LocationManager.GpsProvider, this, null);
success = true;
}
return new Tuple<bool, string, GPSData>(success, error, gpsData);
}
and
public void OnLocationChanged(Location location) {
gpsData = new GPSData(location.Latitude, location.Longitude);
}
I want to be able to call GetGPSData and have it return the tuple, which for now the only important thing about the Tuple is gpsData is filled in. I know it can take seconds to find a fix, so I want this method to be async and be awaitable in the Xamarin.Forms side once I actually need the value.
My issue is I can't figure out a way to have manager.RequestSingleUpdate work synchronously, or any work around. You call that method, and then eventually OnLocationChanged fires off. I tried throwing in a disgusting, barbaric
while (gpsData == null);
after the call to force it not to continue until OnLocationChanged was fired, however when I put that line in, OnLocationChanged never gets called. I'm assuming that is because OnLocationChanged is invoked on the same thread instead of being a background thread.
Is there any way for me to take this scenario and have GetGPSData not return until OnLocationChanged has fired off?
Thanks
EDIT: To add, this method will not be regularly called. It's spontaneous and rare, so I don't want to use RequestLocationUpdates, get regular updates and return the most recent one because that would require always having GPS on, while would rain batteries unnecessarily.
You can do what you want with a TaskCompletionSource. I had the same issue and this is how I solved it:
TaskCompletionSource<Tuple<bool, string, GPSData> tcs;
// No need for the method to be async, as nothing is await-ed inside it.
public Task<Tuple<bool, string, GPSData>> GetGPSData() {
tcs = new TaskCompletionSource<Tuple<bool, string, GPSData>>();
gpsData = null;
var success = false;
var error = string.Empty;
if (!manager.IsProviderEnabled(LocationManager.GpsProvider)) {
//request permission or location services enabling
//set error
tcs.TrySetException(new Exception("some error")); // This will throw on the await-ing caller of this method.
} else {
manager.RequestSingleUpdate(LocationManager.GpsProvider, this, null);
success = true;
}
//return new Tuple<bool, string, GPSData>(success, error, gpsData); <-- change this to:
return this.tcs.Task;
}
And:
public void OnLocationChanged(Location location) {
gpsData = new GPSData(location.Latitude, location.Longitude);
// Here you set the result of TaskCompletionSource. Your other method completes the task and returns the result to its caller.
tcs.TrySetResult(new Tuple<bool, string, GPSData>(false, "someString", gpsData));
}
I am using the following code to update data using the MongoDB C# Driver:
public async Task<bool> UpdateFirstName(string id, string firstName)
{
await Collection.UpdateOneAsync(Builders<User>.Filter.Eq(
"_id", new ObjectId(id)),
Builders<User>.Update.Set("firstName", firstName)
.CurrentDate("lastUpdatedDate"));
}
This method returns "bool", because I want to know if the data has been updated successfully. This would be the pseudocode for checking if the data has been updated successfully:
if (data updated successfully)
{
return true;
}
else
{
return false;
}
Does anyone know how to write the code for checking if the data updated successfully? Thanks!
If the method was executed so the update was done, otherwise the method will throw an exception - In case of async it's important to not forget the await (since using async method without await can't ensure your application stay around long enough to retreive the exception)
UpdateOneAsync has a return value of UpdateResult?, which gives you access to the ModifiedCount. As you you UpdateOne a single greater 0 check is enough.
var response = await Collection.UpdateOneAsync(Builders<User>.Filter.Eq(
"_id", new ObjectId(id)),
Builders<User>.Update.Set("firstName", firstName)
.CurrentDate("lastUpdatedDate"));
if response.ModifiedCount > 0 {
// success
}
// failed
This will throw an Exception if the Update is not acknowledged.
I have an HTTP server written in C# based off the HttpListenerContext class. The server is for processing binary log files and converting them to text, and can take quite a long time to do the conversion. I would like to indicate progress back to the user, but I am unsure on the best way to do this. On the server side, in handling my HTTP request, I essentially have this function:
public async Task HandleRequest()
{
try
{
await ProcessRequest();
}
catch (HttpListenerException)
{
// Something happened to the http connection, don't try to send anything
}
catch (Exception e)
{
SendFailureResponse(500);
}
}
Currently, ProcessRequest() sends the HTML response when finished, but I would like to essentially add the IProgress interface to the function and somehow indicate that progress back to the Web client. What is the best way to do this?
One way of doing it would be to store progress on server side and periodically pull the information from client.
However, if you want the server to notify the client ( push ), then you will need to implement some kind of bi-directional communication between the server and client (I am currently using ASP.NET Web API and SignalR to achieve this at work).
Here is what I got I'll try to explain and I hope you notice its not FULL FULL complete, you'll have to understand the logic behind this and accept or not as a plausible option.
The Method: Set a custom object to store progress of your ongoing operations, make a global static list containing this metadata. Notice how I track them with Ids: I don't store that on DB, the natural act of instantiating the class will auto_increment their Id.
Then, you can add a new controller to respond the progress of a particular ongoing process.
Now that you have a controller to respond the progress of an ongoing process by Id, you can create a javascript timer to call it and update the DOM.
When creating your process, dont hold the htmlrequest until its over, open a background operation instead and just respond with the newly created ProgressTracker.Id, through that class/list you can keep track of the progress and reply accordingly.
As said in another answer, when an operation finishes you can send a push notification and the clientside javascript will interrupt the timers and proceed to the next view/result/page, or you can increment the looping timer to detect when its done and call the results from another controller. (this way you can avoid using push if needed.)
Here is the partial code:
public class ProgressTracker {
private static GlobalIdProvider = 0;
public int _id = ++GlobalIdProvider;
public int Id { get { return _id; } }
bool IsInProgress = false;
bool IsComplete = false;
float Progress;
public YourProgressObject Data;
}
public class GlobalStatic {
public static List<ProgressTracker> FooOperations = new List<ProgressTracker>();
}
public class SomeWebApiController {
[HttpGet]
[Authorize]
public HttpResponseMessage GetProgress(int Id) {
var query = (from a in GlobalStatic.FooOperations where a.Id==Id select a);
if(!query.Any()) {
return Request.CreateResponse(HttpStatusCode.NotFound, "No operation with this Id found.");
} else {
return Request.CreateResponse(HttpStatusCode.Ok, query.First());
}
}
}
// this is javascript
// ... Your code until it starts the process.
// You'll have to get the ProgressTracker Id from the server somehow.
var InProgress = true;
window.setTimeout(function(e) {
var xmlhttp = new XMLHttpRequest();
var url = "<myHostSomething>/SomeWebApiController/GetProgress?Id="+theId;
xmlhttp.setRequestHeader("Authentication","bearer "+localStorage.getItem("access_token"));
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
var data = JSON.parse(xmlhttp.responseText);
updateProgressBar(data);
}
}
xmlhttp.open("GET", url, true);
xmlhttp.send();
function updateProgressBar(data) {
document.getElementById("myProgressText").innerHTML = data.Progress;
}
}, 3000);
Disclaimer: If my javascript is shitty, pardon me but I'm too used to using jQuery and all this fancy stuff x_x