One of my iOS applications seems to have the symptoms of a classic Heisenbug. The application tracks a user's home location so certain events happen when the user enters and exits their home location.
While I'm testing the application, it works great. I walk in and out of a CLCircularRegion and it works every which way I try it. It works with the application in the background. It works with the application closed. It works with the application in the foreground. It works with green eggs and ham.
Unfortunately, users are reporting issues where it will be delayed by 15 minutes or so. The users will enter their homes, but the event will not occur until later. In some cases, the event does not occur at all. The pattern seems to be that when the user first starts using the application, it works great. After a day or so, the application doesn't seem to work as well. The events are delayed.
I'll be the first to admit that I'm no expert on the inner workings of CLLocationManager and CLCircularRegion. I believe I have everything set up properly though and I'm having a really hard time trying to figure out how I can debug something like this.
At any rate, I'll show some of my code here. Keep in mind this is developed with Xamarin so it's in C#.
AppDelegate.cs
public static AppDelegate self;
private CLLocationManager locationManager;
private CLCircularRegion[] locationFences;
private void initializeLocationManager()
{
this.locationManager = new CLLocationManager();
// iOS 8 additional permissions requirements
if (UIDevice.CurrentDevice.CheckSystemVersion(8, 0))
{
locationManager.RequestAlwaysAuthorization();
}
locationManager.AuthorizationChanged += (sender, e) =>
{
var status = e.Status;
// Location services was turned off or turned off for this specific application.
if (status == CLAuthorizationStatus.Denied)
{
stopLocationUpdates();
}
else if (status == CLAuthorizationStatus.AuthorizedAlways &&
iOSMethods.getKeyChainBool(OptionsViewController.GENERIC, OptionsViewController.SERVICE_GEOLOCATION_ENABLED))
{
startLocationUpdates();
}
};
if (CLLocationManager.IsMonitoringAvailable(typeof(CLCircularRegion)))
{
locationManager.RegionEntered += (sender, e) =>
{
setRegionStatus(e, "Inside");
};
locationManager.RegionLeft += (sender, e) =>
{
setRegionStatus(e, "Outside");
};
locationManager.DidDetermineState += (sender, e) =>
{
setRegionStatus(e);
};
}
else
{
// cant do it with this device
}
init();
}
public void init()
{
var data = SQL.query<SQLTables.RoomLocationData>("SELECT * FROM RoomLocationData").ToArray();
int dLen = data.Length;
if (dLen > 0)
{
locationFences = new CLCircularRegion[dLen];
for (int x = 0; x < dLen; x++)
{
var d = data[x];
CLCircularRegion locationFence = new CLCircularRegion(new CLLocationCoordinate2D(d.Latitude, d.Longitude), d.Radius, d.SomeID.ToString() + ":" + d.AnotherID.ToString());
locationFence.NotifyOnEntry = true;
locationFence.NotifyOnExit = true;
locationFences[x] = locationFence;
}
}
}
private void setRegionStatus(CLRegionEventArgs e, string status, bool calledFromDidDetermineState = false)
{
string identifier = e.Region.Identifier;
string lastStatus = iOSMethods.getKeyChainItem(OptionsViewController.GENERIC, OptionsViewController.SERVICE_LAST_GEO_STATUS);
if (lastStatus == status + ":" + identifier)
{
return;
}
iOSMethods.setKeychainItem(OptionsViewController.GENERIC, OptionsViewController.SERVICE_LAST_GEO_STATUS, status + ":" + identifier);
string[] split = identifier.Split(new string[] { ":" }, StringSplitOptions.RemoveEmptyEntries);
if (split.Length == 2)
{
try
{
int someID = Convert.ToInt32(split[0]);
int anotherID = Convert.ToInt32(split[1]);
// Notifies our API of a change.
updateGeofenceStatus(someID, anotherID, status);
if (iOSMethods.getKeyChainBool(OptionsViewController.GENERIC, OptionsViewController.SERVICE_GEOLOCATION_NOTIFICATIONS) &&
(status == "Inside" || status == "Outside" || status == "Unknown"))
{
var rm = SQL.query<SQLTables.KeyRoomPropertyData>("SELECT * FROM KeyRoomPropertyData WHERE SomeID ID = ? AND AnotherID = ?",
new object[] { someID, anotherID }).ToArray();
if (rm.Length > 0)
{
if (status == "Unknown")
{
return;
}
var rmD = rm[0];
UILocalNotification notification = new UILocalNotification();
notification.AlertAction = "Geolocation Event";
notification.AlertBody = status == "Inside" ? "Entered " + rmD.SomeName + ": " + rmD.AnotherName :
status == "Outside" ? "Exited " + rmD.SomeName + ": " + rmD.AnotherName :
"Geolocation update failed. If you would like to continue to use Geolocation, please make sure location services are enabled and are allowed for this application.";
notification.SoundName = UILocalNotification.DefaultSoundName;
notification.FireDate = NSDate.Now;
UIApplication.SharedApplication.ScheduleLocalNotification(notification);
}
}
}
catch (Exception er)
{
// conversion failed. we don't have ints for some reason.
}
}
}
private void setRegionStatus(CLRegionStateDeterminedEventArgs e)
{
string state = "";
if (e.State == CLRegionState.Inside)
{
state = "Inside";
}
else if (e.State == CLRegionState.Outside)
{
state = "Outside";
}
else
{
state = "Unknown";
}
CLRegionEventArgs ee = new CLRegionEventArgs(e.Region);
setRegionStatus(ee, state, true);
}
public void startLocationUpdates()
{
if (CLLocationManager.LocationServicesEnabled)
{
init();
if (locationFences != null)
{
foreach (CLCircularRegion location in locationFences)
{
locationManager.StartMonitoring(location);
Timer t = new Timer(new TimerCallback(delegate(object o) { locationManager.RequestState(location); }), null, TimeSpan.FromMilliseconds(500), TimeSpan.FromMilliseconds(-1));
}
}
}
}
public void stopLocationUpdates(bool isRestarting = false)
{
if (locationFences != null)
{
foreach (CLCircularRegion location in locationFences)
{
locationManager.StopMonitoring(location);
}
}
if (!isRestarting)
{
var rooms = SQL.query<SQLTables.KeyRoomPropertyData>("SELECT * FROM KeyRoomPropertyData").ToArray();
foreach (SQLTables.KeyRoomPropertyData room in rooms)
{
// notifies our API of a change
updateGeofenceStatus(room.SomeID, room.AnotherID, "Unknown");
}
}
}
I know it's a lot of code for anyone to sift through, but I really have no good theory at this point as to what is causing this bug or if it is even possible to fix with the limitations of iOS.
A few theories that I have are if the CLLocationManager.PausesLocationUpdatesAutomatically property may have something to do with it, or some other property of CLLocationManager such as ActivityType, DesiredAccuracy, or DistanceFilter. I've left all of these at their defaults which I would assume would be fine, but I'm not really sure.
Another theory is that there is an uncaught exception being thrown some time after the "service" has been running in the background for some time. If that is the case, is there anything iOS does that would give me a stack trace or something? In all of my tests, I never ran across any exceptions being thrown from this code so I kind of doubt that's the issue. At this point though, I'm willing to entertain any ideas or suggestions.
Also, please keep in mind that in order for this application to work the way it was intended, the location update events MUST occur as soon as the user enters or exist the CLCircularRegion (within a minute or so at least). Obviously I have to leave it to the user to keep their location services enabled and allow the app to have the appropriate permissions.
You are most likely right on target with your diagnosis - it is classic observer effect.
When you test the app, when users play with a new app, the iphone is being actively used. It is not given a chance to fall asleep. What happens on a next day, when users return home - their phones most likely are not in use for extended time right before reaching home location: normally we do not use phones during "last mile" walk after leaving public transportation, or while driving back home. iOS notices this extended inactivity period and adjusts its own behavior to optimize battery life.
The easiest way to observe this is to put together a simple breadcrumbs app - set geofence at your location and keep doing that every time you get exit event. Depending on the way you use (or not use) your phone results will be very different while walking the same route.
And when you get home, the phone is usually the last thing you reach for as well.
You may want to ask users to give more details on how exactly they used phones last 15 minutes before and after entering home, what other apps they use, if they drive do they keep turn by turn navigation app running etc. You will spot the pattern.
re. Also, please keep in mind that in order for this application to
work the way it was intended, the location update events MUST occur as
soon as the user enters or exist the CLCircularRegion (within a minute
or so at least).
You can't do this with geofencing only, especially taking into account different arrival/departure patterns - walking vs driving, "descend" paths (e.g. arrivals with U-turns). You have to anticipate both delays longer than 1 minute and "premature" triggering. I am afraid there is no workaround.
Some things to check:
What are some typical values for radius? You may want to consider reducing that.
iOS Location Services will provide a quicker response if the device has WiFi enabled even if the user is not connected to a network. Check if the problem users have wifi disabled and if you haven't done so already maybe test your device w/o wifi.
Is there a delay in the notification? That is, does the region event occur correctly but for some reason there is a delay in the notification?
How many RoomLocationData entries are there? iOS limits each app to 20 regions max.
Presuming the users are driving to/from their house, you may want to try the following settings (code is Swift):
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.desiredAccuracy = kCLLocationAccuracyBest // or kCLLocationAccuracyBestForNavigation
locationManager.pausesLocationUpdatesAutomatically = true // try false if nothing else works
locationManager.allowsBackgroundLocationUpdates = true
locationManager.activityType = CLActivityType.AutomotiveNavigation
Related
I'm new to C# .Net and Visual Studio 2022 - What I'm trying to achieve is to have a timer running every second to check that a website url is valid/is up. If the url IS reachable and the current WebView2 is not showing that website, then it should navigate to it. If it's already showing that website, it should do nothing else. If it was showing that website, but now it's no longer valid, the WebView should navigate to my custom error page. If whilst on the custom error page the website becomes available again, it should (re)load the website.
In my particular scenario I'm making a webView load localhost (127.0.0.1) for now. I want to continuously check the website is ip, and if it goes down, show custom error, if it comes back, show the website.
Not sure I'm explaining that very well. From the research I have done, I believe I need Task and also await using async method.
Here's my current timer and checkurl code as well as navigtionstarted and navigationcompeted:
private void webView_NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e)
{
timerCheckRSLCDURL.Enabled = false;
}
private void webView_NavigationCompleted(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationCompletedEventArgs e)
{
if (e.IsSuccess)
{
Debug.WriteLine("JT:IsSuccess");
((Microsoft.Web.WebView2.WinForms.WebView2) sender).ExecuteScriptAsync("document.querySelector('body').style.overflow='hidden'");
}
else if (!e.IsSuccess)
{
Debug.WriteLine("JT:IsNOTSuccess");
webView.DefaultBackgroundColor = Color.Blue;
//webView.CoreWebView2.NavigateToString(Program.htmlString);
}
timerCheckRSLCDURL.Enabled = true;
}
private void timerCheckRSLCDURL_Tick(object sender, EventArgs e)
{
Debug.WriteLine("Timer Fired! Timer.Enabled = " + timerCheckRSLCDURL.Enabled);
CheckURL(Properties.Settings.Default.URL, Properties.Settings.Default.Port);
}
private async void CheckURL(string url, decimal port)
{
timerCheckRSLCDURL = false;
Program.isWebSiteUp = false;
string webViewURL = BuildURL();
Debug.WriteLine("Checking URL: " + webViewURL);
try
{
var request = WebRequest.Create(webViewURL);
request.Method = "HEAD";
var response = (HttpWebResponse) await Task.Factory.FromAsync < WebResponse > (request.BeginGetResponse, request.EndGetResponse, null);
if (response.StatusCode == HttpStatusCode.OK)
{
Program.isWebSiteUp = true;
}
}
catch (System.Net.WebException exception)
{
Debug.WriteLine("WebException: " + exception.Message);
if (exception.Message.Contains("(401) Unauthorized"))
{
Program.isWebSiteUp = false;
}
else
{
Program.isWebSiteUp = false;
} // This little block is unfinished atm as it doesn't really affect me right now
}
catch (Exception exception)
{
Debug.WriteLine("Exception: " + exception.Message);
Program.isWebSiteUp = false;
}
if (Program.isWebSiteUp == true && webView.Source.ToString().Equals("about:blank"))
{
Debug.WriteLine("JT:1");
Debug.WriteLine("isWebSiteUp = true, webView.Source = about:blank");
webView.CoreWebView2.Navigate(webViewURL);
}
else if (Program.isWebSiteUp == true && !webView.Source.ToString().Equals(webViewURL))
{
Debug.WriteLine("JT:2");
Debug.WriteLine("isWebSiteUp = true\nwebView.Source = " + webView.Source.ToString() + "\nwebViewURL = " + webViewURL + "\nWebView Source == webViewURL: " + webView.Source.ToString().Equals(webViewURL) + "\n");
webView.CoreWebView2.Navigate(webViewURL);
}
else if (Program.isWebSiteUp == false && !webView.Source.ToString().Equals("about:blank"))
{
Debug.WriteLine("JT:3");
Debug.WriteLine("This SHOULD be reloading the BSOD page!");
webView.CoreWebView2.NavigateToString(Program.htmlString);
}
}
private string BuildURL()
{
string webViewURL;
string stringURL = Properties.Settings.Default.URL;
string stringPort = Properties.Settings.Default.Port.ToString();
string stringURLPORT = $ "{stringURL}:{stringPort}";
if (stringPort.Equals("80"))
{
webViewURL = stringURL;
}
else
{
webViewURL = stringURLPORT;
}
if (!webViewURL.EndsWith("/"))
{
webViewURL += "/";
}
//For now, the URL will always be at root, so don't need to worry about accidentally
//making an invalid url like http://example.com/subfolder/:port
//although potentially will need to address this at a later stage
Debug.WriteLine("BuildURL returns: " + webViewURL);
return webViewURL;
}
So the timer is fired every 1000ms (1 second) because I need to actively check the URL is still alive. I think the way I'm controlling the timer is wrong - and I imagine there's a better way of doing it, but what I want to do is this...
Check website URL every 1 second
To avoid repeating the same async task, I'm trying to disable the timer so it does not fire a second time whilst the async checkurl is running
Once the async/await task of checking the url has finished, the timer should be re-enabled to continue monitoring is the website url is still up
If the website is down, it should show my custom error page (referred to as BSOD) which is some super basic html loaded from resources and 'stored' in Program.htmlString
if the the website is down, and the webview is already showing the BSOD, the webview should do nothing. The timer should continue to monitor the URL.
if the website is up and the webview is showing the BSOD, then it should navigate to the checked url that is up. If the website is up, and the webview is already showing the website, then the webview should do nothing. The timer should continue to monitor the URL.
From other research, I'm aware I shouldn't be using private async void - eg shouldn't be using it as a void. But I've not yet figured out / understood the correct way to do this
In the Immediate Window, it appears that webView_NavigationCompleted is being fired twice (or sometimes even a few times) instantly as the immediate window output will show JT:IsSuccess or JT:IsNOTSuccess a few times repeated in quick succession. Is that normal? I'm assuming something isn't correct there.
The main problem appears to be due to the timer being only 1 second. If I change the timer to fire every 30 seconds for example, it seems to work ok, but when it's every second (I may even need it less than that at some point) it's not really working as expected. Sometimes the BSOD doesn't load at all for example, as well as the webView_NavigationCompleted being fire multiple times in quick succession etc.
Could someone pretty please help me make this code better and correct.
I've searched countless websites etc and whilst there is some good info, some of it seems overwhelming / too technical so to speak. I had to lookup what "antecedent" meant earlier as it's a completely new word to me! :facepalm:
Many thanks inadvance
This answer will focus on the Task timer loop to answer the specific part of your question "check a url is valid every second". There are lots of answers about how to perform the actual Ping (like How do you check if a website is online in C#) and here's the Microsoft documentation for Ping if you choose to go that route.
Since it's not uncommon to set a timeout value of 120 seconds for a ping request, it calls into question whether it would have any value to do this on a steady tick of one second. My suggestion is that it would make more sense to:
Make a background thread
Perform a synchronous ping (wait for the result) on the background thread.
Marshal the ping result onto the UI thread to perform the other tasks you have laid out.
Synchronously wait a Task.Delay on the background thread before performing the next ping.
Here is how I personally go about doing that in my own production code:
void execPing()
{
Task.Run(() =>
{
while (!DisposePing.IsCancellationRequested)
{
var pingSender = new Ping();
var pingOptions = new PingOptions
{
DontFragment = true,
};
// https://learn.microsoft.com/en-us/dotnet/api/system.net.networkinformation.ping?view=net-6.0#examples
// Create a buffer of 32 bytes of data to be transmitted.
string data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
byte[] buffer = Encoding.ASCII.GetBytes(data);
int timeout = 120;
try
{
// https://stackoverflow.com/a/25654227/5438626
if (Uri.TryCreate(textBoxUri.Text, UriKind.Absolute, out Uri? uri)
&& (uri.Scheme == Uri.UriSchemeHttp ||
uri.Scheme == Uri.UriSchemeHttps))
{
PingReply reply = pingSender.Send(
uri.Host,
timeout, buffer,
pingOptions);
switch (reply.Status)
{
case IPStatus.Success:
Invoke(() => onPingSuccess());
break;
default:
Invoke(() => onPingFailed(reply.Status));
break;
}
}
else
{
Invoke(() => labelStatus.Text =
$"{DateTime.Now}: Invalid URI: try 'http://");
}
}
catch (Exception ex)
{
// https://stackoverflow.com/a/60827505/5438626
if (ex.InnerException == null)
{
Invoke(() => labelStatus.Text = ex.Message);
}
else
{
Invoke(() => labelStatus.Text = ex.InnerException.Message);
}
}
Task.Delay(1000).Wait();
}
});
}
What works for me is initializing it when the main window handle is created:
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
if (!(DesignMode || _isHandleInitialized))
{
_isHandleInitialized = true;
execPing();
}
}
bool _isHandleInitialized = false;
Where:
private void onPingSuccess()
{
labelStatus.Text = $"{DateTime.Now}: {IPStatus.Success}";
// Up to you what you do here
}
private void onPingFailed(IPStatus status)
{
labelStatus.Text = $"{DateTime.Now}: {status}";
// Up to you what you do here
}
public CancellationTokenSource DisposePing { get; } = new CancellationTokenSource();
Example 404:
I have a Unity project with Firebase for google Authentication. I am storing user information in a realtime database. They are stored as:
UnityProject
1->
Name: "Something"
Email: "Something"
2->
Name: "some other thing"
Email: "Something else"
Now, these 1,2 are to be given by me (to be used as a primary key). I would start by giving the first user ID 1 and the second used ID 2 and so on.
But I need to get back from the firebase the last ID which was stored. For example,
idToInsert = GetLastUsedID() + 1;
I have used this code but it doesn't work. The screen just freezes until I force close Unity.
public int GetLastUsedID()
{
int currentID = 1;
bool continueSearch = true;
while (continueSearch)
{
FirebaseREST.DatabaseReference reference = FirebaseREST.FirebaseDatabase.Instance.GetReference(""+currentID);
string value = "";
currentID++;
reference.GetValueAsync(10, (res) =>
{
if (res.success)
{
value = res.data.GetRawJsonValue();
Debug.Log("Success fetched data : " + value);
if(value == "")
{
continueSearch = false;
Debug.Log(currentID);
}
}
else
{
Debug.Log("Fetch data failed : " + res.message);
continueSearch = false;
}
});
}
return currentID;
}
Basically I am just trying to iterate from 1 till whenever I get the empty string. The empty string means no data exists under that ID.
I'm not familiar with FirebaseREST (I would recommend using the official Firebase plugin if you're able, it does much more than just call REST endpoints), but I think I can see your issue.
GetValueAsync likely runs asynchronously in the background. So the logic inside the block (the (res)=> part) is firing off many times (basically infinitely with that while loop). Then, depending on how this is implemented, either continueSearch never goes to false because it isn't marked volatile or the callback logic never gets a chance to run (say if FirebaseREST tries to dispatch to the main thread, which is locked in an infinite while loop).
If GetValueAsync returns a Task, you can use some of the tips I cover in this article. My recommendation would be to try to use async/await, so your logic would look more like:
async public int GetLastUsedID()
{
volatile int currentID = 1;
volatile bool continueSearch = true;
while (continueSearch)
{
FirebaseREST.DatabaseReference reference = FirebaseREST.FirebaseDatabase.Instance.GetReference(""+currentID);
string value = "";
currentID++;
await reference.GetValueAsync(10, (res) =>
{
if (res.success)
{
value = res.data.GetRawJsonValue();
Debug.Log("Success fetched data : " + value);
if(value == "")
{
continueSearch = false;
Debug.Log(currentID);
}
}
else
{
Debug.Log("Fetch data failed : " + res.message);
continueSearch = false;
}
});
}
return currentID;
}
This is probably not the case, in which case you'll probably want to turn this into a recursive call. You'd want some function like:
public void GetLastUsedID(currentId)
{
FirebaseREST.DatabaseReference reference = FirebaseREST.FirebaseDatabase.Instance.GetReference(""+currentID);
reference.GetValueAsync(10, (res) =>
{
if (res.success)
{
value = res.data.GetRawJsonValue();
Debug.Log("Success fetched data : " + value);
if(value == "")
{
continueSearch = false;
Debug.Log(currentID);
}
else
{
// recurse
GetLastID(currentId+1);
}
}
else
{
Debug.Log("Fetch data failed : " + res.message);
continueSearch = false;
}
});
}
You will have to figure out how to pass in your own callback to bubble a success or failure like this (exercise to the reader).
Finally, I would be a little cautious with what you're doing entirely. If you're authenticating users, I'd recommend using Firebase Authentication which ties directly into Realtime Database. At most you'd only store user id's in there, and use security rules to ensure that only that user can write into it (for instance). By using Firebase Authentication to manage user data and tying that to RTDB rules, you avoid the same mistakes this author made.
I have a Xamarin.Android application that is supposed to sync my data when the device it is running on connects to a WiFi network. To save data and battery I only let this happen whenever it detects a connections.
The only thing is, the application would randomly run the sync service multiple time throughout the day, despite the WiFi connection remaining constant. Now, I can only imagine that the Android system is sending out the broadcast multiple times to remind applications of the connection state or something like that.
I've used a bit of a quick fix, the static bool FirstTime there, but I'm hoping to find a bit more of an elegant solution. Any suggestions?
This is the code I'm using to do it:
[BroadcastReceiver]
[IntentFilter(new string[] { "android.net.conn.CONNECTIVITY_CHANGE" })]
class ConnectivityReceiver : BroadcastReceiver
{
public static bool FirstTime = true;
public override void OnReceive(Context context, Intent intent)
{
if ( intent.Action != null && intent.Action == "android.net.conn.CONNECTIVITY_CHANGE")
{
ConnectivityManager cm = (ConnectivityManager)context.GetSystemService(Context.ConnectivityService);
NetworkInfo info = cm.ActiveNetworkInfo;
if (FirstTime && info != null)
{
FirstTime = false;
Intent background = new Intent(context, typeof(BackgroundDataSyncService));
context.StartService(background);
}else
{
FirstTime = true;
Intent background = new Intent(context, typeof(BackgroundDataSyncService));
context.StopService(background);
}
}
}
}
You have a plugin called Connectivity that can pulled from nuget.
This plugin is compatible with almost every mobile platform and it can be used only in android it you want.
The plugin does all that code for you and you only have to subscribe to connectivity events like:
CrossConnectivity.Current.ConnectivityChanged += (sender, args) =>
{
page.DisplayAlert("Connectivity Changed", "IsConnected: " + args.IsConnected.ToString(), "OK");
};
The plugin has another nice features please take a look at the documentation.
Check the information in your Intent Bundle. I had the same issue.
Whenever the system changes from WiFi to Cellular, or anything else weird happens in the system, the BroadCastReceiver will send out a message.
At first I thought this was a random event.
I also thought I had to use the ConnectivityManager to get the network information.
If you read the Intent Bundle that comes with the OnReceive(Context context, Intent intent), you'll get all kinds of information.
In my tests, I was working with Visual Studio's emulator. I used the built in tools to switch from WiFi to Cell then kill off the signal, as well as change the signal strength.
I found that the BroadcastReceiver reports at least twice; once for the disconnect, and once for the reconnect.
Here is how I read the Bundle.
Bundle extras = intent.Extras;
if (extras != null)
{
foreach (String key in extras.KeySet())
{
Object value = extras.Get(key);
Log.Info("myApp", "intent.extras " + key + " " + value.ToString());
}
}
You'll also find that one part of the Bundle contains the NetworkInfo too.
object netObj = extras.Get("networkInfo");
NetworkInfo netInf = null;
if (netObj != null)
{
netInf = (NetworkInfo)netObj;
}
With this, you can check the status of different network functions.
Log.Info("myApp", "netInf.IsAvailable: " + netInf.IsAvailable.ToString());
Log.Info("myApp", "netInf.IsConnected: " + netInf.IsConnected.ToString());
I truly hope this helps someone.
I've been chasing this for days.
Use Xamarin.Essentials.Connectivity.ConnectivityChanged event. Subscribe to this event.
How can I retrieve the call forwarding rules (routing) of a Lync client in a Managed SIP Application (serverside technologies like MSPL or UCMA)? The only thing I found is an Article on how you can do it clientside with the Lync SDK.
Also this Answer and this MSDN Article and this Question seem to indicate that it does work but I need this setting at a specific moment (if the User is online or not) and not as soon as he logs into his Lync account and publishes his presence info, as seen in link #1. Also it is necessary to get this for any client without creating an UserEndpoint first. So it would be best if this is possible with an ApplicationEndpoint (or another method).
As far as I found out, it should be possible to retrieve the forwarding settings from the presence metadata, but I do not get this information.
var categories = new string[] {
"state",
//"routing" // ?
};
var asyncresult = presenceSvc.BeginPresenceQuery(sips, categories, null, null, null);
var result = presenceSvc.EndPresenceQuery(asyncresult).ToList();
You cannot do it with an ApplicationEndpoint. You must have an UserEndpoint.
However, you can create a UserEndpoint who just need the CollaborationPlateform and the SipUser and not any passwords.
For my application, I decompiled SEFAUtil.exe via ILSpy to understand how they did in their programs. I advise you to take a look at it.
This is my technique to make it works:
1/ Creation of the UserEndPoint
When creating the user endpoint you have to subscribe for this presence to get the information even if is not connected
userEndpoint.LocalOwnerPresence.BeginSubscribe(null, null);
2/ Subscribe to PresenceNotificationReceived Event
userEndpoint.LocalOwnerPresence.PresenceNotificationReceived += OnCategoryNotificationReceived;
private static void OnCategoryNotificationReceived(object sender, LocalPresentityNotificationEventArgs e)
{
// Here you get the PresenceCategory and all the data of the user
foreach (PresenceCategoryWithMetaData current in e.AllCategories)
{
if (current.Name == "routing" && current.ContainerId == 0 && current.InstanceId == 0L)
// Creation of your Routing, I stock it in a Property
_routingCategory = new Routing(current);
}
// I set my event to continue my main thread
_routingCategoryUpdated.Set();
}
3/ Display the information you want
// Wait until you get the information of the user
if (!_routingCategoryUpdated.WaitOne(10000))
{
Console.WriteLine("TimeOut Getting Informations");
return;
}
// Just display all the data you can need
else
{
Console.WriteLine($"User Aor: {userEndPointTarget.OwnerUri}");
Console.WriteLine($"Display Name: {userEndPointTarget.OwnerDisplayName}");
Console.WriteLine($"UM Enabled: {userEndPointTarget.UmEnabled}");
Console.WriteLine($"Simulring enabled: {_routingCategory.SimultaneousRingEnabled}");
if (_routingCategory.SimultaneousRingEnabled && _routingCategory.SimultaneousRing != null)
{
foreach (string time in _routingCategory.SimultaneousRing)
{
Console.WriteLine($"Simul_Ringing to: {time}");
}
}
if (_routingCategory.DelegateRingEnabled)
{
if (_routingCategory.SkipPrimaryEnabled)
{
Console.Out.Write("Forwarding calls to Delegates: ");
}
else if (_routingCategory.UserWaitTimebeforeTeamOrDelegates.TotalSeconds > 0.0)
{
Console.Out.Write($"Delay Ringing Delegates (delay:{ _routingCategory.UserWaitTimebeforeTeamOrDelegates.TotalSeconds} seconds): ");
}
else
{
Console.Out.Write("Simultaneously Ringing Delegates: ");
}
foreach (string delegateCurrent in _routingCategory.Delegates)
{
Console.Out.Write($"{delegateCurrent} ");
}
Console.Out.WriteLine();
}
else if (_routingCategory.TeamRingEnabled)
{
if (_routingCategory.UserWaitTimebeforeTeamOrDelegates.TotalSeconds > 0.0)
{
Console.Out.Write($"Delay Ringing Team (delay:{_routingCategory.UserWaitTimebeforeTeamOrDelegates.TotalSeconds} seconds). Team: ");
}
else
{
Console.Out.Write("Team ringing enabled. Team: ");
}
foreach (string currentTeam in _routingCategory.Team)
{
Console.Out.Write($"{currentTeam} ");
}
Console.Out.WriteLine();
}
else if (_routingCategory.CallForwardToTargetsEnabled)
{
if (_routingCategory.CallForwardingEnabled)
{
Console.Out.WriteLine($"Forward immediate to: {_routingCategory.CallForwardTo}");
}
else
{
Console.Out.WriteLine($"User Ring time: {_routingCategory.UserOnlyWaitTime}");
Console.Out.WriteLine($"Call Forward No Answer to: {_routingCategory.CallForwardTo[0]}");
}
}
else if (userEndPointTarget.UmEnabled)
{
Console.Out.WriteLine($"User Ring time: {_routingCategory.UserOnlyWaitTime}");
Console.Out.WriteLine("Call Forward No Answer to: voicemail");
}
else
{
Console.Out.WriteLine("CallForwarding Enabled: false");
}
I'm having a problem with my Windows Phone 8 weather app's background agent.
Whenever the background agent is run, a new http weather request is made when certain conditions (that are not relevant to the problem I'm having) are met. When these conditions are unmet, cached weather data is used instead.
Furthermore, if you have set your live tile's location to your "Current Location", the background agent will use reverse geocoding to determine the name of the area for the location you're currently at. This is done whether new or cached data is used i.e. every time my app's background agent is run.
The problem I'm having is that whenever cached data is used, the live tile is not updating. But it doesn't appear to cause an exception to occur because the app's background agent never gets blocked, even if there's been more than two times where the live tile fails to update.
This is the relevant excerpt from the background agent's view model's "public async Task getWeatherForTileLocation()" method that's called from the scheduled agent:
Scheduled agent excerpt:
protected async override void OnInvoke(ScheduledTask task)
{
LiveTileViewModel viewModel = new LiveTileViewModel();
await viewModel.getWeatherForTileLocation();
// Etc.
}
getWeatherForTileLocation() excerpt:
// If the default location is 'Current Location', then update its coordinates.
if ((int)IsolatedStorageSettings.ApplicationSettings["LocationDefaultId"] == 1)
{
try
{
// Get new coordinates for current location.
await this.setCoordinates();;
}
catch (Exception e)
{
}
}
// Depending on the time now, since last update (and many other factors),
// must decide whether to use cached data or fresh data
if (this.useCachedData(timeNow, timeLastUpdated))
{
this.ExtractCachedData(); // This method works absolutely fine, trust me. But the live tile never updates when it's run outside debugging.
// Not because of what it does, but because of how fast it executes.
}
else
{
// a httpClient.GetAsync() call is made here that also works fine.
}
The setCoordinates method, as well the reverse geocoding related methods that are called from it:
public async Task<string> setCoordinates()
{
// Need to initialise the tracking mechanism.
Geolocator geolocator = new Geolocator();
// Location services are off.
// Get out - don't do anything.
if (geolocator.LocationStatus == PositionStatus.Disabled)
{
return "gps off";
}
// Location services are on.
// Proceed with obtaining longitude + latitude.
else
{
// Setup the desired accuracy in meters for data returned from the location service.
geolocator.DesiredAccuracyInMeters = 50;
try
{
// Taken from: http://bernhardelbl.wordpress.com/2013/11/26/geolocator-getgeopositionasync-with-correct-timeout/
// Because sometimes GetGeopositionAsync does not return. So you need to add a timeout procedure by your self.
// get the async task
var asyncResult = geolocator.GetGeopositionAsync();
var task = asyncResult.AsTask();
// add a race condition - task vs timeout task
var readyTask = await Task.WhenAny(task, Task.Delay(10000));
if (readyTask != task) // timeout wins
{
return "error";
}
// position found within timeout
Geoposition geoposition = await task;
// Retrieve latitude and longitude.
this._currentLocationLatitude = Convert.ToDouble(geoposition.Coordinate.Latitude.ToString("0.0000000000000"));
this._currentLocationLongitude = Convert.ToDouble(geoposition.Coordinate.Longitude.ToString("0.0000000000000"));
// Reverse geocoding to get your current location's name.
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
this.setCurrentLocationName();
});
return "success";
}
// If there's an error, may be because the ID_CAP_LOCATION in the app manifest wasn't include.
// Alternatively, may be because the user hasn't turned on the Location Services.
catch (Exception ex)
{
if ((uint)ex.HResult == 0x80004004)
{
return "gps off";
}
else
{
// Something else happened during the acquisition of the location.
// Return generic error message.
return "error";
}
}
}
}
/**
* Gets the name of the current location through reverse geocoding.
**/
public void setCurrentLocationName()
{
// Must perform reverse geocoding i.e. get location from latitude/longitude.
ReverseGeocodeQuery query = new ReverseGeocodeQuery()
{
GeoCoordinate = new GeoCoordinate(this._currentLocationLatitude, this._currentLocationLongitude)
};
query.QueryCompleted += query_QueryCompleted;
query.QueryAsync();
}
/**
* Event called when the reverse geocode call returns a location result.
**/
void query_QueryCompleted(object sender, QueryCompletedEventArgs<IList<MapLocation>> e)
{
foreach (var item in e.Result)
{
if (!item.Information.Address.District.Equals(""))
this._currentLocation = item.Information.Address.District;
else
this._currentLocation = item.Information.Address.City;
try
{
IsolatedStorageSettings.ApplicationSettings["LiveTileLocation"] = this._currentLocation;
IsolatedStorageSettings.ApplicationSettings.Save();
break;
}
catch (Exception ee)
{
//Console.WriteLine(ee);
}
}
}
I've debugged the code many times, and have found no problems when I have. The http request when called is good, cached data extraction is good, reverse geocoding does always return a location (eventually).
But I did notice that when I'm using cached data, the name of the current location is retrieved AFTER the scheduled task has created the updated live tile but before the scheduled task has finished.
That is, the name of the location is retrieved after this code in the scheduled agent is run:
extendedData.WideVisualElement = new LiveTileWideFront_Alternative()
{
Icon = viewModel.Location.Hourly.Data[0].Icon,
Temperature = viewModel.Location.Hourly.Data[0].Temperature,
Time = viewModel.Location.Hourly.Data[0].TimeFull.ToUpper(),
Summary = viewModel.Location.Hourly.Data[0].Summary + ". Feels like " + viewModel.Location.Hourly.Data[0].ApparentTemperature + ".",
Location = IsolatedStorageSettings.ApplicationSettings["LiveTileLocation"].ToString().ToUpper(),
PrecipProbability = viewModel.Location.Hourly.Data[0].PrecipProbabilityInt
};
But before:
foreach (ShellTile tile in ShellTile.ActiveTiles)
{
LiveTileHelper.UpdateTile(tile, extendedData);
break;
}
NotifyComplete();
Obviously due to memory constraints I can't create an updated visual element at this point.
For comparison, when I'm not using cached data, the reverse geocoding query always manages to return a location before the http request code has finished.
So as the view model's getWeatherForTileLocation() method is using "await" in the scheduled agent, I decided to make sure that the method doesn't return anything until the current location's name has been retrieved. I added a simple while loop to the method's footer that would only terminate after the _currentLocation field has received a value i.e. the reverse geocoding has completed:
// Keep looping until the reverse geocoding has given your current location a name.
while( this._currentLocation == null )
{
}
// You can exit the method now, as you can create an updated live tile with your current location's name now.
return true;
When I debugged, I think this loop ran around 3 million iterations (a very big number anyway). But this hack (I don't know how else to describe it) seemed to work when I'm debugging. That is, when the target of my build was my Lumia 1020, and when I created a live tile fresh from it, which calls:
ScheduledActionService.Add(periodicTask);
ScheduledActionService.LaunchForTest(periodicTaskName, TimeSpan.FromSeconds(1));
To ensure I don't have to wait for the first scheduled task. When I debugged this first scheduled task, everything works fine: 1) a reverse geocoding request is made, 2) cached data extracted correctly, 3) hacky while loop keeps iterating, 4) stops when the reverse geocoding has returned a location name, 5) tile gets updated successfully.
But subsequent background agent calls that do use cached data don't appear to update the tile. It's only when non-cached data is used that the live tile updates. I should remind you at this point the reverse geocoding query always manages to return a location before the http request code has finished i.e. the hacky loop iterates only once.
Any ideas on what I need to do in order to ensure that the live tile updates correctly when cached data is used (read: when the processing of data, after the reverse geocoding query is made, is much faster than a http request)? Also, is there a more elegant way to stop the getWeatherForTileLocation() from exiting than my while loop? I'm sure there is!
Sorry for the long post but wanted to be as thorough as possible!
This has been giving me sleepless nights (literally) for the last 72 hours, so your help and guidance would be most appreciated.
Many thanks.
Bardi
You have done a great job of providing lots of detail, but it is very disconnected so it is a litle hard to follow. I think the root of your problem is the following:
// Reverse geocoding to get your current location's name.
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
this.setCurrentLocationName();
});
You are attempting to get the location name, but your setCoordinates method will have already completed by the time the setCurrentLocationName method gets around to executing.
Now because you need to be in a UI thread to do any tile updating anyways, I would suggest just dispatching from the begining:
protected async override void OnInvoke(ScheduledTask task)
{
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
LiveTileViewModel viewModel = new LiveTileViewModel();
await viewModel.getWeatherForTileLocation();
}
}
This would remove the need to do any other dispatching in the future.
Two more things:
Generally weather data includes the name of the location you are getting data for. If this is the case, just use that data rather than doing the reverse geocode. This will save you some memory and save time.
If you do need to get the location, I might suggest pulling out a "LocationService" that can get data for you. In this class you can use a TaskCompltionSource to await the event rather than having code follow many different paths.
public class LocationService
{
public static Task<Location> ReverseGeocode(double lat, double lon)
{
TaskCompletionSource<Location> completionSource = new TaskCompletionSource<Location>();
var geocodeQuery = new ReverseGeocodeQuery();
geocodeQuery.GeoCoordinate = new GeoCoordinate(lat, lon);
EventHandler<QueryCompletedEventArgs<IList<MapLocation>>> query = null;
query = (sender, args) =>
{
geocodeQuery.QueryCompleted -= query;
MapLocation mapLocation = args.Result.FirstOrDefault();
var location = Location.FromMapLocation(mapLocation);
completionSource.SetResult(location);
};
geocodeQuery.QueryCompleted += query;
geocodeQuery.QueryAsync();
}
return completionSource.Task;
}
Using a TaskCometionSource allows you to await the method rather than using the event.
var location = await locationService.ReverseGeocode(lat, lon);
This example uses another Location class that I created do just hold things like City and State.
The key thing with background agents is to ensure that code always flows "synchronously". This doesn't mean code cannot be asynchronous, but does mean that code needs to be called one after the other. So if you have something that has events, you could continue all other code after the event.
Hope that helps!
I don't see your deferral call. When you use async you have to tell the task that you're deferring completion till later. Can't remember the method off the top of my head but it's either on the base class of your background task or on the parameter you get.
The reason it probably works with cache data is that it probably isn't actually an async operation.
I think this is sorted now! Thanks so much Shawn for the help. The setLocationName() method call is now awaited, and it looks like this now:
public Task<string> setLocationName()
{
var reverseGeocode = new ReverseGeocodeQuery();
reverseGeocode.GeoCoordinate = new System.Device.Location.GeoCoordinate(this._currentLocationLatitude, this._currentLocationLongitude );
var tcs = new TaskCompletionSource<string>();
EventHandler<QueryCompletedEventArgs<System.Collections.Generic.IList<MapLocation>>> handler = null;
handler = (sender, args) =>
{
MapLocation mapLocation = args.Result.FirstOrDefault();
string l;
if (!mapLocation.Information.Address.District.Equals(""))
l = mapLocation.Information.Address.District;
else
l = mapLocation.Information.Address.City;
try
{
System.DateTime t = System.DateTime.UtcNow.AddHours(1.0);
if (t.Minute < 10)
IsolatedStorageSettings.ApplicationSettings["LiveTileLocation"] = l + " " + t.Hour + ":0" + t.Minute;
else
IsolatedStorageSettings.ApplicationSettings["LiveTileLocation"] = l + " " + t.Hour + ":" + t.Minute;
IsolatedStorageSettings.ApplicationSettings.Save();
this._currentLocationName = IsolatedStorageSettings.ApplicationSettings["LiveTileLocation"].ToString();
}
catch (Exception ee)
{
//Console.WriteLine(ee);
}
reverseGeocode.QueryCompleted -= handler;
tcs.SetResult(l);
};
reverseGeocode.QueryCompleted += handler;
reverseGeocode.QueryAsync();
return tcs.Task;
}