Following on from my first post on Band 2 development, I noticed that the Band 2 SDK had been updated.
The new capability that interested me is one that’s been added whereby a UWP app can receive tile events from a Band 2 even if the foreground app isn’t running.
To experiment, I took my project from that previous post and I updated the Microsoft.Band NuGet package to be at version 1.3.20217 and then I went off to read the SDK docs to see what had changed.
Section 9.3.2 of those docs say that there’s a new capability whereby a UWP app can expose an App Service which can then be invoked by the Health App in order that tile notifications for that specific app can be routed to its service. It’s an interesting architecture and one of the first places where I’ve been offered a true ‘App Service’ powered extensibility point so that’s nice to see.
So, largely just following those docs I changed the manifest for my project such that it included;
where the name com.microsoft.band.observer is pre-defined to link up with something that must be built into the Microsoft Health app.
From there, I changed the code that I’d previously written which created a square tile on my band and I added just the one line of code advised by the SDK docs which I’ve tried to highlight clearly below – this code lives behind 2 buttons on ‘a UI’ which are labelled ‘Create’ and ‘Remove’ respectively;
namespace App320 { using Microsoft.Band; using Microsoft.Band.Tiles; using System; using System.Linq; using Windows.Storage; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Media.Imaging; public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); } async void OnCreateTile(object sender, RoutedEventArgs args) { var bands = await BandClientManager.Instance.GetBandsAsync(); if (bands?.Count() > 0) { this.client = await BandClientManager.Instance.ConnectAsync(bands.First()); var tileSpace = await this.client.TileManager.GetRemainingTileCapacityAsync(); if (tileSpace > 0) { var iconFile = await StorageFile.GetFileFromApplicationUriAsync( new Uri("ms-appx:///Assets/tileicon.png")); var smallIconFile = await StorageFile.GetFileFromApplicationUriAsync( new Uri("ms-appx:///Assets/smalltileicon.png")); using (var stream = await iconFile.OpenReadAsync()) { using (var smallStream = await smallIconFile.OpenReadAsync()) { var largeBitmap = new WriteableBitmap(48, 48); largeBitmap.SetSource(stream); var largeIcon = largeBitmap.ToBandIcon(); var smallBitmap = new WriteableBitmap(24, 24); smallBitmap.SetSource(smallStream); var smallIcon = smallBitmap.ToBandIcon(); this.tileGuid = Guid.NewGuid(); var bandTile = new BandTile(this.tileGuid) { Name = "Test", TileIcon = largeIcon, SmallIcon = smallIcon }; var added = await this.client.TileManager.AddTileAsync(bandTile); // NEW NEW NEW. // This is new. // NEW NEW NEW. // Is that clearly labelled enough? 🙂 await this.client.SubscribeToBackgroundTileEventsAsync(this.tileGuid); } } } } } async void OnRemove(object sender, RoutedEventArgs e) { await this.client.TileManager.RemoveTileAsync(this.tileGuid); } IBandClient client; Guid tileGuid; } }
With that set up, it’s my responsibility to build a background task (i.e. implementation of IBackgroundTask) that offers up an App Service which the Microsoft Health app can invoke.
So, I made a new “Windows Runtime Component” project in Visual Studio, added it to my solution, referenced it from my original project so that it would be packaged and deployed with the rest of my bits. Here’s the first version of the code;
namespace TileBackgroundComponent { using System.Diagnostics; using Windows.ApplicationModel.AppService; using Windows.ApplicationModel.Background; public sealed class TheTask : IBackgroundTask { static readonly string BAND_OBSERVER_SERVICE_NAME = "com.microsoft.band.observer"; public async void Run(IBackgroundTaskInstance taskInstance) { this.deferral = taskInstance.GetDeferral(); taskInstance.Canceled += OnCancelled; var triggerDetails = taskInstance.TriggerDetails as AppServiceTriggerDetails; if (triggerDetails.Name == BAND_OBSERVER_SERVICE_NAME) { triggerDetails.AppServiceConnection.RequestReceived += this.OnRequestReceived; } } void OnRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args) { foreach (var key in args.Request.Message.Keys) { Debug.WriteLine($"{key} has value {args.Request.Message[key]}"); } } void OnCancelled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason) { this.deferral?.Complete(); this.deferral = null; } BackgroundTaskDeferral deferral; } }
and I then used the UI to;
- Create the tile on the band.
- Press the tile on the band to open it up.
- Remove the tile on the band.
and, interestingly, the debug output from this is;
Timestamp has value 03/03/2016 08:59:45 +00:00 Sequence# has value 4 TileId has value ed0ed23b-f431-4c38-84e2-3cba98f2c4bb Type has value TileOpenedEvent Timestamp has value 03/03/2016 09:00:05 +00:00 Sequence# has value 5 TileId has value ed0ed23b-f431-4c38-84e2-3cba98f2c4bb Type has value TileClosedEvent
and so, clearly, the application service call is sending over a ValueSet (dictionary) that has all the information within it to determine which tile is being referred to (I only have one) and what the event is – i.e. this one is a TileOpenedEvent.
So, it’s all there. I could, for instance, use the event here to pop up a toast notification on the phone. Here’s that example;
namespace TileBackgroundComponent { using NotificationsExtensions.ToastContent; using Windows.ApplicationModel.AppService; using Windows.ApplicationModel.Background; using Windows.UI.Notifications; public sealed class TheTask : IBackgroundTask { static readonly string BAND_OBSERVER_SERVICE_NAME = "com.microsoft.band.observer"; static readonly string EVENT_TYPE_KEY = "Type"; static readonly string SEQUENCE_NUMBER_KEY = "Sequence#"; const string EVENT_TYPE_OPENED = "TileOpenedEvent"; public async void Run(IBackgroundTaskInstance taskInstance) { this.deferral = taskInstance.GetDeferral(); taskInstance.Canceled += OnCancelled; var triggerDetails = taskInstance.TriggerDetails as AppServiceTriggerDetails; if (triggerDetails.Name == BAND_OBSERVER_SERVICE_NAME) { triggerDetails.AppServiceConnection.RequestReceived += this.OnRequestReceived; } } void OnRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args) { if (args.Request.Message.ContainsKey(EVENT_TYPE_KEY)) { string eventType = (string)args.Request.Message[EVENT_TYPE_KEY]; int sequenceNumber = (int)args.Request.Message[SEQUENCE_NUMBER_KEY]; switch (eventType) { // NB: there is also a tile closed, and a tile button pressed // event. case EVENT_TYPE_OPENED: this.PopToast(sequenceNumber); break; default: break; } } } void OnCancelled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason) { this.deferral?.Complete(); this.deferral = null; } void PopToast(int sequenceNumber) { var notifier = ToastNotificationManager.CreateToastNotifier("App"); var toast = ToastContentFactory.CreateToastText01(); toast.TextBodyWrap.Text = $"Sequence Number {sequenceNumber}"; notifier.Show(toast.CreateNotification()); } BackgroundTaskDeferral deferral; } }
and that works just fine but it’s a little bit reliant on some hard-coded strings and so on which I expect that the Band SDK team would rather you didn’t get involved in so it looks like they’ve been very kind and added a strongly typed helper to make it easier;
namespace TileBackgroundComponent { using Microsoft.Band; using Microsoft.Band.Tiles; using NotificationsExtensions.ToastContent; using Windows.ApplicationModel.AppService; using Windows.ApplicationModel.Background; using Windows.UI.Notifications; public sealed class TheTask : IBackgroundTask { static readonly string BAND_OBSERVER_SERVICE_NAME = "com.microsoft.band.observer"; public async void Run(IBackgroundTaskInstance taskInstance) { this.deferral = taskInstance.GetDeferral(); taskInstance.Canceled += OnCancelled; var triggerDetails = taskInstance.TriggerDetails as AppServiceTriggerDetails; if (triggerDetails.Name == BAND_OBSERVER_SERVICE_NAME) { triggerDetails.AppServiceConnection.RequestReceived += this.OnRequestReceived; } } void OnRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args) { if (!this.addedHandlers) { this.addedHandlers = true; BackgroundTileEventHandler.Instance.TileOpened += this.OnTileOpened; } // Ask this class to figure out the details of the message that's // coming in. BackgroundTileEventHandler.Instance.HandleTileEvent(args.Request.Message); } private void OnTileOpened(object sender, BandTileEventArgs<IBandTileOpenedEvent> e) { // most scenarios would need this, I don't. var tileId = e.TileEvent.TileId; // NB: sequence number isn't part of this data so I'll pop 0. this.PopToast(0); } void OnCancelled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason) { this.deferral?.Complete(); this.deferral = null; } void PopToast(int sequenceNumber) { var notifier = ToastNotificationManager.CreateToastNotifier("App"); var toast = ToastContentFactory.CreateToastText01(); toast.TextBodyWrap.Text = $"Sequence Number {sequenceNumber}"; notifier.Show(toast.CreateNotification()); } bool addedHandlers; BackgroundTaskDeferral deferral; } }
and that also works quite nicely. I’m not 100% sure when that event handler should be removed (or whether it should be removed).
I’m also not 100% sure about whether a response should be send back to the Microsoft Health app when a request is received – the code in the SDK documentation does send an (empty) response back to the sender so perhaps this code should be amended to do that.
But…the new bits in the SDK definitely work for me here – I can have a tile on my Band 2 talk to a background agent on my phone when the foreground app isn’t running and I think that’s a significant change in that it means that the Band 2 can truly initiate action on its buddy device without first expecting the user to run an app over there.
Updated to add a link to the VS project.