Felix Rieseberg

Windows 8: Displaying a clock on Live Tiles

The application life cycle imposed upon developers by Windows 8 can be tricky. An app can only execute code while being in the visible foreground, with background tasks being the only exception. Those tasks have extremely limited resources, but are in addition to that not allowed to run at the developers discretion. A task has to be registered using one of the available triggers – for instance when Internet becomes available, an SMS message has been received or when a time zone change happens on the device used. A scenario that requires a background task to run in a timed and repeating manner is quite tricky: If the app is not put on the lock screen, a background task is only allowed to run every two hours. In addition, the only available timed trigger is the MaintenanceTrigger, which is only executed when the device is connected to AC power. Should the user put your app on the lock screen, the TimeTrigger can be used, which is probably exactly what you are looking for – could the trigger execute more frequently than every 15 minutes. Since it cannot, how can one do something as outlandish as putting a clock on a live tile?

While the solution to this problem might seem like a hack, it is merely a crafty best practice solution. While we have a quite rigid constraints for the frequency of our code execution, we can schedule notifications that are delivered whenever we deem it appropriate – and we can schedule a whopping 4096 of them.

The Code

The solution is thus quite simple. My app “Awesome World Time” first asks the user whether it can be added to the lock screen. This dialog is completely managed by Windows and simply has to be called. Windows will also make sure that the user is not asked too often should he choose to decline my request.

function manageBackgroundTile() {
	Windows.ApplicationModel.Background.BackgroundExecutionManager.requestAccessAsync().done(
			function (e) {
				if (e == 2) {
					globalSettings.backgroundTask = BackgroundTasks.RegisterBackgroundTask("TileUpdater");
				} else {
					console.log("Background Lock Screen access denied")
				}
			});
	}

If the user accepts, I ask my controller for background tasks to register a timed background task, set to run at every 45 minutes under the condition that a user is present. Of course, before registering this task, one should check whether a task has already been registered and act accordingly.

function RegisterBackgroundTask(name) {

	// Check for existing registrations of this background task.
	var background = Windows.ApplicationModel.Background;

	if (background.BackgroundTaskRegistration.allTasks.size > 0) {

		var iter = background.BackgroundTaskRegistration.allTasks.first();
		var hascur = iter.hasCurrent;

		while (hascur) {
			var cur = iter.current.value;
			if (cur.name === name) {
				cur.unregister(true);
				console.log("Task unregistered");
			};
			hascur = iter.moveNext();
		}
	}

	// Register the background task.
	var builder = new Windows.ApplicationModel.Background.BackgroundTaskBuilder();

	builder.name = name;
	builder.taskEntryPoint = "js\backgroundTileUpdater.js";

	var hourlyTrigger = new Windows.ApplicationModel.Background.TimeTrigger(45, false);
	var userCondition = new Windows.ApplicationModel.Background.SystemCondition(Windows.ApplicationModel.Background.SystemConditionType.userPresent);

	builder.setTrigger(hourlyTrigger);
	builder.addCondition(userCondition)

	var accessStatus = Windows.ApplicationModel.Background.BackgroundExecutionManager.getAccessStatus();
	var task = builder.register();
	console.log("Task registered");

	return task;

}

The task itself consists of a simple iteration: It asks for every secondary tile pinned to the start screen, deletes all already scheduled notifications and creates two hours of notifications in advance – with each notification being active for exactly one minute. To keep this code clean and not too specific to its task, I removed all code used to do time zone magic.

(function () {
    "use strict";

    var cancel = false,
        progress = 0,
        backgroundTaskInstance = Windows.UI.WebUI.WebUIBackgroundTaskInstance.current,
        notifications = Windows.UI.Notifications;

    console.log("Background " + backgroundTaskInstance.task.name + " Starting...");

    // Associate a cancellation handler with the background task.
    function onCanceled(cancelSender, cancelReason) {
        cancel = true;
    }
    backgroundTaskInstance.addEventListener("canceled", onCanceled);

    function displayTileNotification() {

        Windows.UI.StartScreen.SecondaryTile.findAllAsync().done(
            function (secondaryTiles) {

                console.log("Secondary Tiles fetched");

                if (secondaryTiles.size > 0) {
                    for (var n = 0; n < secondaryTiles.size; n++) {
                        var tileUpdater = Windows.UI.Notifications.TileUpdateManager.createTileUpdaterForSecondaryTile(secondaryTiles[n].tileId);
                        tileUpdater.clear();

                        var plannedUpdate = tileUpdater.getScheduledTileNotifications();
                        var Notifications = Windows.UI.Notifications;

                        for (var addMinutes = 1; addMinutes < 120; addMinutes++) {

                            var dueTime = new Date(currentTime.getTime() + 60000 * addMinutes);
                            var expTime = new Date(dueTime.getTime() + 60000)
                            var idNumber = Math.floor(Math.random() * 100000000);  // Generates a unique ID number for the notification.

                            // Setup tile
                            var template = notifications.TileTemplateType.tileSquareText02;
                            var tileXml = notifications.TileUpdateManager.getTemplateContent(template);

                            var timeToSet = {}
                            timeToSet.minutes = displayTime.getMinutes();
                            timeToSet.hours = displayTime.getHours();

                            var tileTextAttributes = tileXml.getElementsByTagName("text");
                            tileTextAttributes[0].appendChild(tileXml.createTextNode(timeToSet.hours() + ":" + timeToSet.minutes));
                            tileTextAttributes[1].appendChild(tileXml.createTextNode(secondaryTiles[n].displayName));

                            // Create the notification object.
                            var futureTile = new Notifications.ScheduledTileNotification(tileXml, dueTime);
                            futureTile.id = "Tile" + idNumber;
                            futureTile.expirationTime = expTime;

                            // Add to the schedule.
                            tileUpdater.addToSchedule(futureTile);
                        }

                    };
                }

            });
    }

    displayTileNotification();
})();