Windows Phone 7 Live Tile Schedules – How to execute instant Live Tile updates

comments

If you’ve worked with Windows Phone 7 Live Tiles, you may have noticed a bit of a hole in the platform SDK’s functionality – the inability to programmatically update the current running applications tile without the push coming from a remote webserver. The purpose of this post is to show you that this is not the end of the world, and there is a way around this. 

What you usually want to do with a live tile and it’s schedule is something along the lines of:

  1. Update the live tile with a web Url “right now”
  2. Set the phone’s operating system to fetch new versions of this Url on a schedule

The problem is, this is currently not possible. It is easy to set a schedule, but debugging this is hell as you have to wait an hour to see the result.

The current state of affairs for live tile “schedules”

image

 

The way Windows Phone 7 Live Tile schedule functionality is basically the following:

  1. The Windows Phone 7 application starts a “schedule” with the operating system to go and fetch a static URL and replace your live tile with it.
  2. Whenever this scheduled time is met, the Windows Phone 7 operating system goes and fetches a new tile from the URL. It must complete this request in under 15 seconds (i.e. your website/web service must reply in a timely manner).

The major problem with the above is two fold. Firstly the tile will not update until the first time the schedule time is hit – and secondly because this minimum time is 1 hour. What this means is that when your user turns on a live tile functionality in your app, nothing happens – its that simple. Nothing will happen until this first hour has rolled by. This is not good application behaviour from your users’ point of view, and you may even get some bad ratings on the marketplace because your users have complained that your live tile functionality doesn’t work! Don’t even get me started on how you are meant to debug this when your application functionality isn’t working properly.

The current state of affairs for live tile “toast” notifications

image

There is a second way to update a Windows Phone 7 device’s live tile and it’s by using a “Toast” notification.

This allows for the following functionality:

  1. The Windows Phone 7 application opens a “toast” channel with Microsoft’s Windows Live service. This gives the phone a Windows Live web service Url that is the equivalent of the device’s phone number.
  2. The Windows Phone 7 application can then upload this “Phone Home” Url to a web service that the developer of the application has written. This requires a web server and a custom web service that can store this Url for later use.
  3. Whenever your web service wants to update a user’s Live Tile, it can send a request containing a bunch of xml data to the users phone using this Windows Live web service Url telling it to update and where to grab the live tile image from.

The problem with this is that there are quite a few moving parts and this involves going to a lot of effort just to update the live tile right this very second as it requires you to setup a web service that stores and then bounces back an update to your phone.

What if we could just remove the web service altogether?

image

The way to get around this problem, we have to a take a new approach. An approach that is a bit of a hybrid solution containing both methods from above, except we fake a few things on the user’s device.

We take the best parts from both, and create a toast channel, send a live tile update from the phone itself, and then start a schedule for later updates.

The new flow for this operation becomes:

  1. The Windows Phone 7 application opens a “toast” channel with the Microsoft Windows Live web service.
  2. The Phone sends an Http toast update notification to itself containing the Url that the application wants to be downloaded from, in turn making the user’s phone update the live tile “right now”.
  3. Close the toast channel and start a schedule for the same Url that the application sent itself so that on the schedule the users phone will update the tile again.

The code

Below is a class that handles both of these situations, and allows you to simply update the tile, or update it and start a new schedule.

public class InstantLiveTileUpdateHelper
{
    public InstantLiveTileUpdateHelper(ShellTileSchedule schedule)
    {
        tileSchedule = schedule;
    }

    public InstantLiveTileUpdateHelper()
    {
    }

    private ShellTileSchedule tileSchedule;

    public void UpdateLiveTileAndStartSchedule(string liveTileTitle, int? liveTileCount)
    {
        if (tileSchedule == null)
            throw new ArgumentException("You cannot call this method unless you have initialised", "tileSchedule");

        UpdateLiveTile(tileSchedule.RemoteImageUri, liveTileTitle, liveTileCount, tileSchedule.Start);
    }

    public void UpdateLiveTile(Uri liveTileUri, string liveTileTitle, int? liveTileCount, Action onComplete)
    {
        HttpNotificationChannel toastChannel = HttpNotificationChannel.Find("liveTileChannel");
        if (toastChannel != null)
        {
            toastChannel.Close();
        }
        
        toastChannel = new HttpNotificationChannel("liveTileChannel");
        

        toastChannel.ChannelUriUpdated +=
            (s, e) =>
            {
                Debug.WriteLine(String.Format("Is image an absolute Uri: {0}", tileSchedule.RemoteImageUri.IsAbsoluteUri));
                if (liveTileUri.IsAbsoluteUri)
                {
                    toastChannel.BindToShellTile(new Collection<Uri> { liveTileUri });
                }
                else
                {
                    toastChannel.BindToShellTile();
                }

                SendTileToPhone(e.ChannelUri, liveTileUri.ToString(), liveTileCount, liveTileTitle,
                            () =>
                            {
                                //Give it some time to let the update propagate
                                Thread.Sleep(TimeSpan.FromSeconds(10));

                                toastChannel.UnbindToShellTile();
                                toastChannel.Close();

                                //Call the "complete" delegate
                                if (onComplete != null)
                                    onComplete();
                            }
                    );
            };
        toastChannel.Open();
    }

    private void SendTileToPhone(Uri notificationUrl, string liveTileUri, int? count, string liveTileTitle, Action onComplete)
    {
        var stream = new MemoryStream();
        var settings = new XmlWriterSettings
                        {
                               Indent = true, 
                            Encoding = Encoding.UTF8
                        };

        XmlWriter w = XmlWriter.Create(stream, settings);
        w.WriteStartDocument();
        w.WriteStartElement("wp", "Notification", "WPNotification");
        w.WriteStartElement("wp", "Tile", "WPNotification");
        if (!string.IsNullOrEmpty(liveTileUri))
        {
            Debug.WriteLine(String.Format("Tile Uri in xml: {0}", liveTileUri));
            w.WriteStartElement("wp", "BackgroundImage", "WPNotification");
            w.WriteValue(liveTileUri);
            w.WriteEndElement();
        }
        if (count.HasValue)
        {
            w.WriteStartElement("wp", "Count", "WPNotification");
            w.WriteValue(count.ToString());
            w.WriteEndElement();
        }
        w.WriteStartElement("wp", "Title", "WPNotification");
        w.WriteString(liveTileTitle ?? String.Empty);
        w.WriteEndElement();

        w.WriteEndElement();
        w.Close();

        byte[] payload = stream.ToArray();

        Debug.WriteLine("Sending tile request update payload");
        //Check the length of the payload and reject it if too long));
        if (payload.Length > 1024)
        {
            throw new ArgumentOutOfRangeException(
                string.Format("Payload is too long. Maximum payload size shouldn't exceed {0} bytes",
                                1024));
        }

        var httpRequest = (HttpWebRequest)WebRequest.Create(notificationUrl);
        httpRequest.Method = "POST";
        httpRequest.ContentType = "text/xml;";
        httpRequest.Headers["X-MessageID"] = Guid.NewGuid().ToString();
        httpRequest.Headers["X-NotificationClass"] = 1.ToString();
        httpRequest.Headers["X-WindowsPhone-Target"] = "token";

        httpRequest.BeginGetRequestStream(
            ar =>
            {
                Stream requestStream = httpRequest.EndGetRequestStream(ar);

                Debug.WriteLine("Sending payload in uploaded request stream...");
                requestStream.BeginWrite(
                    payload, 0, payload.Length,
                    iar =>
                    {
                        requestStream.EndWrite(iar);
                        requestStream.Close();

                        httpRequest.BeginGetResponse(
                            iarr =>
                            {
                                if (onComplete != null)
                                    onComplete();
                            },
                            null);
                    },
                    null);
            },
            null);
    }
}

How to use it

The code can be used one of two ways. One to simply do an update using an image Url that you supply, the other to update the tile AND then start a schedule – but this time using the scheduled tile Uri for the first fetch.

To update the tile and start a schedule:

ShellTileSchedule tileSchedule = new ShellTileSchedule()
                                {
                                    Interval = UpdateInterval.EveryHour,
                                    RemoteImageUri = new Uri("http://mysite.com/new-live-tile.jpg"),
                                    StartTime = DateTime.Now
                                };
InstantLiveTileUpdateHelper helper = new InstantLiveTileUpdateHelper(tileSchedule);
//update the tile title and set the tile count overlay to 10
helper.UpdateLiveTileAndStartSchedule("My New Title", 10);

To just update the tile “right now” without a schedule:

InstantLiveTileUpdateHelper helper = new InstantLiveTileUpdateHelper();
//update the tile title and set the tile count overlay to 10
int newTileCountOverlay = 10;
helper.UpdateLiveTile(
    new Uri("http://mysite.com/new-live-tile.jpg"), 
    "New tile title", 
    newTileCountOverlay, 
    null);