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.