Nowadays, we expect our favorite blogs and news websites to have RSS feeds. Also, many popular sites are offering APIs to their data and services. It's a good time to be a developer, as gadgets are practically begging to be written and can potentially tap into huge userbases.
In the Desktop Gadget world, we've seen cool gadgets that use the Communication API to implement multiplayer games and match-making programs.
"Web as the platform" has now become a reality. While these applications can do cool things through the Internet, what should a gadget do when the Internet is unavailable? This is a common occurrence for laptop users on the go. If you're like me and own an off-brand router that freezes every hour, it's an aggravating fact of life. Far too often, offline conditions result in nasty error messages or completely broken gadgets.
In this article, we'll look at ways to detect and recover from offline conditions in a Google Desktop gadget. I'll then step through an example gadget that demonstrates various techniques for offline handling.
We first would like to know when our machine goes offline.
Did you know the API has a framework.system.network.online
property
which tells us if the network connection is on?
function isNetworkOnline() { return framework.system.network.online; }
Wow! If it seems too easy, notice that it only tells you whether or not the network connection is online. Sometimes, you really need to know whether a host is reachable or a service is responding. Here's a slightly more involved offline detection technique that resembles the "ping" command-line utility:
function ping(url, timeout, onSuccess, onFailure) { var request = new XMLHttpRequest(); request.onreadystatechange = onReadyStateChange; // Called when an error occurs. function onRequestError() { request.abort(); onFailure(); } var timeoutTimer = view.setTimeout(onRequestError, timeout); try { request.open('GET', url, true); request.send(); } catch(e) { debug.warning('Could not send ping request: ' + e.message); view.clearTimeout(timeoutTimer); onRequestError(); return; } function onReadyStateChange() { if (request.readyState != 4) { return; } if (request.status == 200) { // Success, clear the timeout timer. view.clearTimeout(timeoutTimer); onSuccess(); } } }
The function "pings" an HTTP server and expects a response within the specified timeout
interval.
We create a timer that signals the timeout upon expiration.
If the request succeeds, the timer is cancelled, and onSuccess
is called.
On the other hand, if the request times out, the function executes the onFailure
callback.
NOTE: In this example, the request specifies the GET
method.
In most cases, it is recommended to use HEAD
, keeping in mind some servers disallow HEAD
requests.
It would also be useful to know when the connection has returned. One technique is to poll until the machine is back online:
function waitForOnline(callback) { var RETRY_DELAY_MS = 60 * 1000; if (isOnline()) { // We're back, call the callback. callback(); } else { // Try again later. view.setTimeout(function() { waitForOnline(callback); }, RETRY_DELAY_MS); } }
This function checks if the machine is online. If so, it executes the provided callback. If the machine is offline, we try again later after the defined interval. You shouldn't be too aggressive with the interval, as this will increase CPU load. However, if the interval is long, you won't be able detect the return of the connection quickly enough. Ideally, you'd want to resume regular operation immediately after the connection returns, but instead we settle on a retry interval of one minute.
NOTE: The example above assumes isOnline
is a synchronous function. If your online detector is asynchronous, well, let's say that's an exercise for the reader. :)
Also remember to be considerate of the web service. If you have millions of users all hitting the same server frequently, the server might go down and somebody will be mad at you. If there is a risk of this, be sure to implement exponential backoff or another similar safeguard.
We're now equipped with a few tools to determine the connection status. We should take this knowledge to "offline-proof" our gadgets. The remainder of this article focuses on handling offline conditions in a Desktop Gadget.
Here is what I think comprises offline handling:
Try running some of your gadgets offline. Do you see error messages? Then at the very least, prevent the errors from occurring or catch the exceptions. Next, the user should receive some indication that the connection is down. To reinforce this, the gadget should disable controls, e.g. "Refresh" or "Send", that no longer work. Also, the gadget should restore full functionality in a timely manner after it's back online.
The following is an example of a gadget with basic offline handling.
The gadget displays a link to the top viewed YouTube video of the day.
First, let's look at the XMLHttpRequest
related code that is responsible for detecting offline conditions.
function getTopVideo(onSuccess, onFailure) { var TOP_VIEWED_FEED = 'http://youtube.com/rss/global/top_viewed_today.rss'; // Request timeout in milliseconds. var TIMEOUT_INTERVAL_MS = 5000; // Check network connection. if (!framework.system.network.online) { debug.trace('Network connection is offline.'); onFailure(); return; } var request = new XMLHttpRequest(); request.onreadystatechange = onReadyStateChange; // Called when a timeout occurs. function onRequestTimeout() { request.abort(); onFailure(); } // Create timer that raises the timeout. var timeoutTimer = view.setTimeout(onRequestTimeout, TIMEOUT_INTERVAL_MS); try { request.open('GET', TOP_VIEWED_FEED, true); request.send(); } catch(e) { debug.warning('Could not send HTTP request: ' + e.message); // Clear the timeout timer. view.clearTimeout(timeoutTimer); onFailure(); return; } function onReadyStateChange() { if (request.readyState != 4) { return; } if (request.status == 200) { // Clear the timeout timer. view.clearTimeout(timeoutTimer); var doc = new DOMDocument(); doc.loadXML(request.responseText); var videoData = {}; try { var item = doc.getElementsByTagName('item')[0]; videoData['title'] = item.getElementsByTagName('title')[0].text; videoData['link'] = item.getElementsByTagName('link')[0].text; } catch(e) { debug.error('Could not parse feed: ' + e.message); return; } onSuccess(videoData); } } }
Notice there are three ways things can go wrong (see the bolded code):
request.send
.
We call onFailure
if something goes wrong or onSuccess
if the requests succeeds.
These callbacks will change the behavior and UI of the gadget as you'll see below.
Our example gadget doesn't have much as far as UI goes. It has a link to the current top video and a Refresh menu command to refresh the data. In our case offline handling should be easy. When the gadget detects it's offline, it should:
Online | Offline |
---|---|
![]() |
![]() |
Here are the functions to switch between online and offline mode:
// Global variable keeps track of online state. var g_isOfflineMode = true; // Offline retry interval. var RETRY_INTERVAL_MS = 60 * 1000; // Link color when offline. var OFFLINE_LINK_COLOR = '#CCCCCC'; // Regular link color. var REGULAR_LINK_COLOR = '#0000FF'; ... function onTopVideoReady(videoData) { // Request was successful. Set online mode. setOnlineMode(); // Set contents of the link. videoLink.innerText = videoData['title']; videoLink.href = videoData['link']; } function onTopVideoFail() { // Set offline mode. setOfflineMode(); // Retry again. debug.trace('Set retry timer.'); view.setTimeout(refresh, RETRY_INTERVAL_MS); } function setOnlineMode() { debug.trace('Switching to online mode.'); g_isOfflineMode = false; // Restore label color. videoLink.color = REGULAR_LINK_COLOR; } function setOfflineMode() { debug.trace('Switching to offline mode.'); g_isOfflineMode = true; // Grey out the label. videoLink.color = OFFLINE_LINK_COLOR; } function onAddCustomMenuItems(menu) { if (g_isOfflineMode) { // Grey-out the menu item and set its handler to null. menu.AddItem(strings.REFRESH, gddMenuItemFlagGrayed, null); } else { // Restore the menu item. menu.AddItem(strings.REFRESH, 0, refresh); } }
onTopVideoReady
and onTopVideoFail
are the callbacks passed to getTopVideo
.
Each function is responsible for updating the UI to reflect the current mode. onTopVideoFail
also creates a timer that will retry the request after a specified delay. This is an adaption of the "polling" technique discussed earlier.
We use the global variable g_isOfflineMode
to keep track of the current connection state. This variable is used in the menu handler onAddCustomMenuItems
to enable/disable the Refresh command.
If we wanted to eliminate the global, we could create separate menu handler functions and swap between when the connection state changes.
In this gadget, there is a run-forever refresh timer that causes the gadget to update periodically:
function onOpen() { // Set up menu handler. pluginHelper.onAddCustomMenuItems = onAddCustomMenuItems; // Run-forever refresh timer. view.setInterval(refresh, REFRESH_INTERVAL_MS); // Initial refresh. refresh(); }
Therefore during offline mode, two refresh timers are running simultaneously. In practice this is rarely a problem, but you could always disable the main timer and reactivate it when the connection comes back.
Offline handling is a mark of a quality gadget and should be present in any gadget that accesses information over a network. Remember, there are all kinds of ways to implement offline handling, and this is just one example to give you some ideas. Be sure to check out some of your desktop applications for inspiration and think about what works best for your gadget.
Thanks for reading!
James enjoys long drives in the middle of the night.