I’ve been trying to track the ability of a Windows 8 Store application to do work in the background since the first previews of Windows 8 shipped. The idea that a Windows Store app is more than the UI that you see on screen is a really interesting one and, for me, it’s important to understand that area reasonably well such that you can decide what a Store app can/can’t do and how to put in place an architecture that allows code to run in the right place at the right time.
This is explained up on the developer centre and I’ve posted about certain aspects of it in the past.
One of the new types of triggers that arrived in the Windows 8.1 Preview was a LocationTrigger – a way of triggering activity when the machine’s location changes or “geofencing” as it seems to be known.
Running background tasks in response is just one part of the new geofencing support in Windows 8.1. It’s worth having a look at the “Start to Finish” guide up on the developer centre to get a bigger picture view of geofencing and it’s also worth look at the BUILD video referenced below for more info.
For me, it’s perhaps more likely that I’d want this kind of functionality on a phone that on a Windows 8 tablet/laptop but it’s not hard to think up use cases for it and especially as slates running Windows 8.1 are getting smaller as illustrated by the new Dell Venue 8 Pro below in the video from CNET;
http://www.cnet.com/av/video/embed/player.swf
It’s not too hard to think up scenarios – e.g. it might be used in a line-of-business scenario to download some kind of work items when a device enters/leaves a particular location such as for a delivery messenger or a travelling salesperson or whatever.
I thought I’d try give the API a try for myself. Like a number of Windows 8 triggers, it’s only possible to use a Geo-fence trigger if an application is on the lock-screen so I figured I’d start a blank app and put it on the lock-screen and putting an app on the lock-screen requires both a badge logo;
and then I needed to specify a background task so I went and created a basic, empty background task project (i.e. a WinRT component in .NET) and listed it in my manifest and I also referenced it from my main project. I tend to do that for background tasks because the API for registering them wants a name of the type implementing the background task and I find that “easiest” to do if I can use reflection to ask the component what its name is;
From there, I hit a bit of a problem which surprised me. I find that if I try and build this code then;
- The build system errored if I tried to build my app without specifying that I am lock-screen capable because I had used a location trigger.
- The build system errored if I specified that I was lock-screen capable and had used a location trigger because it told me I needed to use a “control channel, system or push notification” trigger.
I wonder if I’m missing something or whether this is just “a preview thing” so I tried to fool the build system by claiming that I also supported a “Timer” task and that seemed to pacify the build system even if at runtime I’m not actually going to implement a task that is driven from a Timer trigger.
From there, I tried to add a couple of test buttons onto a UI just to run some sketched out code to see how things worked. Firstly, I wrote a little piece of code whereby my app could ask to be on the lock-screen;
if (BackgroundExecutionManager.GetAccessStatus() == BackgroundAccessStatus.Unspecified) { await BackgroundExecutionManager.RequestAccessAsync(); }
followed by a little piece of code which attempted to clear any registered background tasks from the system and then to register my background task using the new LocationTrigger type and requesting a Geofence trigger (I think that’s the only kind of LocationTriggerType right now);
foreach (var task in BackgroundTaskRegistration.AllTasks) { task.Value.Unregister(false); } BackgroundTaskBuilder builder = new BackgroundTaskBuilder(); builder.Name = "Location Task"; builder.TaskEntryPoint = typeof(MyLocTriggerLibrary.MyLocTrigger).FullName; builder.SetTrigger(new LocationTrigger(LocationTriggerType.Geofence)); builder.Register();
The next thing is to set up a Geofence around some region of interest and then ask the system to alert me (via my background task) when the user wanders in/out of that fenced area for a particular period of time. The most natural thing to do here is probably to put a map on the screen and so I thought I would make an attempt at that. I went out and got the Bing Maps SDK for Windows 8.1 Preview and dropped a simple map control onto the screen along with a few buttons that might let me try some things out;
I hacked some code together (no nice patterns, design, MVVM or even a nice set of classes) behind those buttons.
The “new fence” button changes the mouse cursor to a cross-hair, lets the user click on the map and drops a 1km geofenced area wherever the user clicked. Just so I could see what I was doing I made the system’s current location appear on the map as a Pushpin with the letter “C” (for centre) in it and I made each geofence have an incrementing id which is also displayed on the map. So, in the above screenshot the machine is centred in Seattle on 178th Avenue NE and there is a geofenced area in the golf course to the East.
In terms of setting up a geofence, I found it pretty simple – here’s the handler that does the work;
private void OnMapPointerReleased(object sender, PointerRoutedEventArgs e) { // If we are in "drop a new fence" mode. if (this.waitingForPointerReleased) { // Clear the mouse pointer that we changed. this.waitingForPointerReleased = false; Window.Current.CoreWindow.PointerCursor = this.defaultCursor; // Ask the map where the user clicked. PointerPoint mousePoint = e.GetCurrentPoint(this.map); Bing.Maps.Location mapLocation; if (this.map.TryPixelToLocation(mousePoint.Position, out mapLocation)) { // Omitting altitude right now. BasicGeoposition basicPosition = new BasicGeoposition(); basicPosition.Latitude = mapLocation.Latitude; basicPosition.Longitude = mapLocation.Longitude; // Ask for a 1km fence. Geocircle circle = new Geocircle(basicPosition, 1000d, AltitudeReferenceSystem.Surface); Geofence fence = new Geofence( this.fenceCount++.ToString(), circle, MonitoredGeofenceStates.Entered | MonitoredGeofenceStates.Exited, false, TimeSpan.FromSeconds(5)); // Add it to the list of monitored fences. GeofenceMonitor.Current.Geofences.Add(fence); } e.Handled = true; this.DrawFencesOnMap(); } }
and so the handler essentially gets the mouse click position from the map in lat/lon coordinates and then creates a BasicGeoposition, uses it to construct a Geocircle with a radius of 1km and uses that to create a Geofence which is monitored for the machine entering/leaving it (line 27), multiple times (line 28) and the machine has to be in/out of that area for 5 seconds minimum in order to trigger the geofence (line 29).
That Geofence is then added to the list of Geofences that the system is monitoring on my app’s behalf (this list can easily be retrieved later and interrogated, cleared, etc).
For the moment, I added some dummy code into my background task implementation and then set about trying to test this out. This proved hard.
Debugging/Testing Geofencing
I struggled with getting this to work for many hours. Firstly, the only way I know of testing this out (other than to drive around the block with a device) is to use the Simulator which has that nice dialog box for typing co-ordinates into;
It’d be nice if this was a draggable map and had some more of the features of the Windows Phone emulator version but it serves its purpose. I encountered a few things though;
- You can’t request lock-screen access from an application that’s running inside of the simulator so I think you first have to run the app outside of the simulator and get it on the lock-screen.
- I’m not 100% sure on this but, as far as I could tell geofences that I set up outside the simulator didn’t seem to show up inside the simulator so I found that I had to set up my fences inside the simulator.
- Periodically, I would get a catastrophic failure error back from the call which registers my background task. Once that happened, I found that I had to reboot to clear that error and get my task registered again.
But I came across a bit more of a blocker. In as much as I could tell, changing the simulated location in the simulator did not seem to ever trigger my background task which was waiting on its LocationTrigger.
I wrote bits of code that sync’d up to the GeofenceStateChanged event on the GeofenceMonitor class to try the scenario where an application wants notification of geofencing events while it is the foreground, running app and I would find that I could change the simulated location of the device such that these foreground events would fire when that location entered/exited a geo-fenced area so the bits seemed to be working.
However, I couldn’t get a background task to fire on a LocationTrigger. I went and found the official geolocation sample and that turned out to have geofencing added to it also with background tasks associated with a LocationTrigger but I couldn’t get that sample to work for me in the simulator either.
Getting a bit desperate, I did a bit of a web-search and found a bug report which suggested that things might not work.
I asked around a little and found that there is a limitation – simulating a geo-location in the simulator works fine if you’re handling notifications in the foreground but if you are using the new LocationTrigger and trying to trigger a background task based on entering/exiting a geofence in the background then the simulator isn’t going to help in that respect – those triggers do not fire based on changes to the simulated location in the simulator.
That was a bit of a blow – but not to be defeated, I tried another tack…
“Walk Down the Street” Testing
I thought I’d change my test code such that rather than setting up a 1km geo-fence area it allowed me to set up a 50m geo-fenced area and then I figured I’d install the app onto my Surface RT (also running Windows 8.1 Preview) and then set up a 50m geofence around my current location before walking up the road until the device “knew” that it was no longer within that 50m geofenced area and then I’d expect an “exiting” event after I left the area and an “entering” event when I came back into the area.
Naturally, this is a bit hit-and-miss with the Surface RT because it’s not a device that has GPS making it exactly aware of where it is at any time – you have to hope that it has some other way of figuring out that its location has changed and, in my case, that did seem to be happening and I can only assume that the device was doing this based on the WiFi networks it could/couldn’t see.
I experimented with this but, to date, I’ve had no success. I’ve not seen a background notification on my device actually fire. I can walk far enough (i.e. a few 100m) up the street such that the device clearly knows it is more than 50m away from the geofenced location (I can see this using both my own app and the built-in Maps app on Windows) but I never see my background task get invoked.
So, at the time of writing I’m still waiting to verify whether I can get this to work – perhaps it’s a “preview” thing or perhaps it’s something wrong with my hacked together test code. Speaking of which.
Code
Here’s the code I was trying out. I have a tiny piece of “UI”;
<Page x:Class="App21.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:App21" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:map="using:Bing.Maps" mc:Ignorable="d"> <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"> <Grid.ColumnDefinitions> <ColumnDefinition Width="auto" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <StackPanel> <StackPanel.Resources> <Style TargetType="Button"> <Setter Property="HorizontalAlignment" Value="Stretch" /> </Style> </StackPanel.Resources> <Button Content="Register Lock Screen" Click="OnRegisterLockScreen" /> <Button Content="Register Background Task" Click="OnRegisterLocationTriggerBackgroundTask" /> <Button Content="Register Foreground Handler" Click="OnRegisterLocationForegroundHandler" /> <Button Content="Clear All Fences" Click="OnClearFencesButtonHandler" /> <Button Content="New Fence" Click="OnNewFenceButtonHandler" /> </StackPanel> <map:Map x:Name="map" Grid.Column="1" ShowBreadcrumb="True" ShowNavigationBar="True" ShowScaleBar="True" PointerReleased="OnMapPointerReleased"/> </Grid> </Page>
and then I have a little bit of code sitting behind that;
using System; using System.Collections.Generic; using System.Diagnostics; using Bing.Maps; using Windows.ApplicationModel.Background; using Windows.Devices.Geolocation; using Windows.Devices.Geolocation.Geofencing; using Windows.Foundation; using Windows.UI.Core; using Windows.UI.Input; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Input; namespace App21 { public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); this.Loaded += OnLoaded; } void OnLoaded(object sender, RoutedEventArgs x) { this.fencePins = new List<Pushpin>(); this.locator = new Geolocator(); locator.PositionChanged += new TypedEventHandler<Geolocator, PositionChangedEventArgs>( this.OnCurrentLocationChanged); PointerEventHandler handler = new PointerEventHandler(this.OnMapPointerReleased); this.map.AddHandler(PointerReleasedEvent, handler, true); this.DrawFencePinsOnMap(); } void DrawCurrentLocationOnMap() { if (this.centerPin == null) { this.centerPin = new Pushpin(); this.centerPin.Text = "C"; this.map.Children.Add(centerPin); } MapLayer.SetPosition( centerPin, new Location( this.currentLocation.Latitude, this.currentLocation.Longitude)); } void OnGeofenceStateChanged(GeofenceMonitor sender, object args) { ToastLibrary.ToastHelper.ToastNotifyGeofenceReports(); } void OnCurrentLocationChanged(Geolocator sender, PositionChangedEventArgs args) { this.Dispatcher.RunAsync( CoreDispatcherPriority.Normal, () => { this.currentLocation.Latitude = args.Position.Coordinate.Point.Position.Latitude; this.currentLocation.Longitude = args.Position.Coordinate.Point.Position.Longitude; this.CenterMapOnCurrentLocation(); this.DrawCurrentLocationOnMap(); } ); } void CenterMapOnCurrentLocation() { this.map.SetView(new Bing.Maps.Location( this.currentLocation.Latitude, this.currentLocation.Longitude), 16); } async void OnRegisterLockScreen(object sender, RoutedEventArgs e) { if (BackgroundExecutionManager.GetAccessStatus() == BackgroundAccessStatus.Unspecified) { await BackgroundExecutionManager.RequestAccessAsync(); } } private void OnRegisterLocationTriggerBackgroundTask(object sender, RoutedEventArgs e) { if (BackgroundTaskRegistration.AllTasks.Count == 0) { BackgroundTaskBuilder builder = new BackgroundTaskBuilder(); builder.Name = "TestLocationTask"; builder.TaskEntryPoint = typeof(MyLocTriggerLibrary.MyLocTrigger).FullName; builder.SetTrigger(new LocationTrigger(LocationTriggerType.Geofence)); BackgroundTaskRegistration registration = builder.Register(); } } void OnRegisterLocationForegroundHandler(object sender, RoutedEventArgs args) { GeofenceMonitor.Current.GeofenceStateChanged += new TypedEventHandler<GeofenceMonitor, object>( this.OnGeofenceStateChanged); } void OnNewFenceButtonHandler(object sender, RoutedEventArgs e) { this.defaultCursor = Window.Current.CoreWindow.PointerCursor; Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.Cross, 1); this.waitingForNewFencePointerRelease = true; } private void OnClearFencesButtonHandler(object sender, RoutedEventArgs e) { GeofenceMonitor.Current.Geofences.Clear(); this.DrawFencePinsOnMap(); } private void OnMapPointerReleased(object sender, PointerRoutedEventArgs e) { // If we are in "drop a new fence" mode. if (this.waitingForNewFencePointerRelease) { // Clear the mouse pointer that we changed. this.waitingForNewFencePointerRelease = false; Window.Current.CoreWindow.PointerCursor = this.defaultCursor; // Ask the map where the user clicked. PointerPoint mousePoint = e.GetCurrentPoint(this.map); Bing.Maps.Location mapLocation; if (this.map.TryPixelToLocation(mousePoint.Position, out mapLocation)) { // Omitting altitude right now. BasicGeoposition basicPosition = new BasicGeoposition(); basicPosition.Latitude = mapLocation.Latitude; basicPosition.Longitude = mapLocation.Longitude; // Ask for a 1km fence. // Scrub that - for "in the street" testing, I want a 50m geo-fence. Geocircle circle = new Geocircle(basicPosition, 50d, AltitudeReferenceSystem.Surface); Geofence fence = new Geofence( (this.fencePins.Count + 1).ToString(), circle, MonitoredGeofenceStates.Entered | MonitoredGeofenceStates.Exited, false, TimeSpan.FromSeconds(5)); // Add it to the list of monitored fences. GeofenceMonitor.Current.Geofences.Add(fence); } e.Handled = true; this.DrawFencePinsOnMap(); } } void DrawFencePinsOnMap() { foreach (var pin in this.fencePins) { this.map.Children.Remove(pin); } this.fencePins.Clear(); foreach (var fence in GeofenceMonitor.Current.Geofences) { Pushpin pin = new Pushpin(); pin.Text = (this.fencePins.Count + 1).ToString(); Geocircle circle = (Geocircle)fence.Geoshape; MapLayer.SetPosition(pin, new Bing.Maps.Location(circle.Center.Latitude, circle.Center.Longitude)); this.map.Children.Add(pin); this.fencePins.Add(pin); } } CoreCursor defaultCursor; bool waitingForNewFencePointerRelease; BasicGeoposition currentLocation; Pushpin centerPin; List<Pushpin> fencePins; Geolocator locator; } }
and then a separate WinRT component project for my background task;
using Windows.ApplicationModel.Background; namespace MyLocTriggerLibrary { public sealed class MyLocTrigger : IBackgroundTask { public async void Run(IBackgroundTaskInstance taskInstance) { var deferral = taskInstance.GetDeferral(); try { ToastLibrary.ToastHelper.ToastNotifyGeofenceReports(); } finally { deferral.Complete(); } } } }
To Be Continued
So far, I’ve failed to get this LocationTrigger->Background Task scenario to work for me with setting up geofenced areas and then entering/leaving them.
I’ll return to it when I’ve moved to Windows 8.1 RTM and see if I have more success there but it’s worth being aware of this new capability and perhaps trying it out for yourself and see if you can get the SDK sample to do the right thing with respect to background tasks and geofenced areas.