My favorites | English | Sign in

Watch Google I/O LIVE on May 10th and 11th, 2011

Google Desktop APIs (Labs)

Offline Detection and Handling

James Yum, Google Desktop Team
November 2007

Introduction

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.

Offline Detection

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.

Online Detection

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.

Offline Handling

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:

  1. The gadget shouldn't break.
  2. The gadget should disable user actions that can't be fulfilled.
  3. The gadget should inform the user that it thinks the machine is offline or connection is down.
  4. The gadget should be able to return to normal when the connection returns.

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.

An Example

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):

  1. The network connection is not online.
  2. There was an error in request.send.
  3. The request timed out.

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:

  • Grey-out the link
  • Disable the Refresh command in the menu
  • Reattempt the request after a minute has passed
Online Offline
Online UI Offline UI

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.

Conclusion

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!

Resources

Author Bio

James

James enjoys long drives in the middle of the night.