My favorites | English | Sign in

Google Desktop APIs (Labs)

Desktop gadgets and App Engine


Paneendra BA, Vishwajith Krishnamurthy, James Yum
Google Desktop Team

August 2008

Introduction

After Google App Engine was released, we folks over at Google Desktop started thinking about how great it would be to build collaborative, interactive applications using Desktop gadgets in tandem with App Engine. For those new to Google Desktop gadgets, they are graphically rich desktop gadgets (widgets) programmed using XML and JavaScript. Not only are they easy to develop, but your creations can reach the tens of millions of Google Desktop users out there, whether they are on Windows, Mac, or Linux.


Figure 1: Desktop gadgets

Desktop gadgets, as opposed to web-based gadgets and front ends, become a constant, sticky presence on the user's machine. What you do with that space is up to your imagination. You could build something fun, like a multiplayer trivia or word scramble game. Or let's say you have an avatar feature on your site. You could build a Desktop gadget that is used to design and animate the avatar, making use of the transparency, blending, and animation features in the platform.

If you have already built or are planning to build a web application with App Engine, you could consider opening up an API and writing a Desktop gadget as a client. A gadget can enhance the user experience by bringing recent activities or fresh content regularly to the user's desktop. For example, if you have an auction site, a gadget that tracks bids would be very valuable and promote use of the site.

This article shows you how easy it is to implement a desktop client with Desktop gadgets and a complementary server API with App Engine. We hope it inspires you to think of the next world-shattering application that amazes us all.

Example app: Q&A

Do you like questions? How about answers? If you answered yes to either, you'll love our example application. We're going to develop a question and answer server that accomplishes the following:

  • Serves up random questions
  • Tracks answers to questions
  • Allows you to submit new questions
  • Lists most answered questions

Here is what the accompanying Desktop gadget will look like:


Figure 2: Client for the Q&A server

This article assumes basic familiarity with App Engine. If you're new, please check out the excellent Getting Started guide. In any case, you should be able to follow the server code with some basic knowledge of web application development.

The server API

The Desktop gadget client needs to communicate with the server through an API. If you have a clear idea of what the server does, the API design should be straightforward.

Our application offers these API methods:

Method Parameters Description
/question Returns a random question. See Response formats.
/vote qid
vote
Records an answer. qid is the ID of the question. vote should be set to 1 for a yes and 0 for a no answer.
/top num Returns the list of most voted questions with at most num entries. See Response formats.
/submit question Adds a new question to the pool. question contains the question text.

For simplicity, all requests are HTTP GETs, and the responses are newline-separated plain text. Of course your much cooler API could be 100% RESTful and return XML, JSON, and so on.

Response formats

Here's a sample response for the /question method:

ahNkZXNrdG9wZ2FkZ2V0c2FtcGxlcg4LEghRdWVzdGlvbhgCDA
Is this a question?
2008-06-24 08:42:09.269583

The first line is the question ID, used to identify the question in other requests. The second line is the question text. The last line is the timestamp in ISO 8601 format.

And here's an example for the /top response:

Is today a good day?
35
26
Is this a question?
10
11
If a tree falls in the forest, does it make a sound?
0
0

Each question in the list takes up three lines of data. The first line is the question text. The next two lines are the yes and no answer count.

Data model

The server's main responsibility is to manage the question and answer data. App Engine uses data models in its Datastore API. Here is the definition for the Question data model:

class Question(db.Model):
  text = db.StringProperty(required=True)
  yesCount = db.IntegerProperty(default=0)
  totalCount = db.IntegerProperty(default=0)
  randomValue = db.FloatProperty()

Creating, updating, and deleting entities in the datastore is as simple as calling one of model's built-in methods. Here's an example of how to create and store a new Question:

question = Question(text=questionText)
question.put()

Queries are just as easy. Here is the code that selects a random question with the help of the randomValue property.

    randomSelection = random.random();
    # Select the question with the largest 'randomValue' but within
    # 'randomSelection'.
    try:
      question = Question.all().filter('randomValue <=', randomSelection).order('-randomValue').get()
      # If randomSelection is too small, we just pick the first question.
      # Warning: This strategy isn't fair on all questions.
      if not question:
        question = Question.all().get()
    except db.Error:
      self.response.out.write('DATABASE_ERROR')

Server code

The server weighs in at under 200 lines of code. You can get the complete code here or browse the Python script.

We've already discussed the data model. In addition, the script must also process incoming requests and output the responses.

Let's look at the handler for /submit:

class SubmitQuestionHandler(webapp.RequestHandler):
  def post(self):
    self.response.headers['Content-Type'] = 'text/plain'

    questionText = self.request.get('question')
    if not questionText:
      self.response.out.write('INVALID_REQUEST')

    question = Question(text=questionText)
    question.randomValue = random.random()
    question.put()
    # GAE doesn't support transactions across entity groups. Hence, one of the 2 writes might not happen.
    # Thus we write the question first so that in the worst-case, our count a bit lagging from the 'actual count'
    # If the count is slightly less, the last few questions will not be displayed instantaneously.
    # But that is okay.(better than 404s/retries that can be caused by incrementing the count first)
    Counter.increment()

The handler creates a new Question data object with the provided question text and saves it to the datastore. Now let's look at the implementation for /top.

# Handler for 'GET' requests for the top questions.
class TopQuestionsHandler(webapp.RequestHandler):
  def get(self):
    self.response.headers['Content-Type'] = 'text/plain'

    numQuestions = self.request.get('num')
    if not numQuestions:
      numQuestions = 10
    elif (numQuestions > 25):
      numQuestions = 25

    questions = Question.all().order("-totalCount").fetch(limit=numQuestions)

    for question in questions:
      noCount = question.totalCount - question.yesCount
      self.response.out.write("%s\n%ld\n%ld\n\n" % (question.text, question.yesCount, noCount))

The method contains logic to determine the number of questions to return. It then retrieves a list of questions, reverse ordered by the total votes. The other handlers are just as simple.

Again, the data model makes it fairly easy to implement a basic CRUD API for your web application.

The Desktop gadget

Now that we have an API, it's time to bring the application to life. Please download the Desktop gadget, try it out, and contribute a question or two (keep it clean!). For the remainder of this article, we look at a simplified version of the gadget.

The gadget contains three source files: main.xml, main.js, and gadget.gmanifest.

main.xml describes the UI:

<view height="150" width="400" onopen="view_onOpen();">
  <div enabled="true" height="100%" hitTest="htclient" width="100%" background="#000000">
    <div enabled="true" height="148" hitTest="htclient" width="398" x="1" y="1" background="#DDDDFF">
      <a height="25" name="yes" width="45" x="78" y="105" size="10" bold="true" onclick="answerQuestion(1);"> YES;</a>
      <a height="25" name="no" width="45" x="178" y="105" size="10" bold="true" onclick="answerQuestion(0);"> NO;</a>
      <a height="25" name="skip" width="45" x="278" y="105" size="10" bold="true" onclick="getNextQuestion();"> SKIP;</a>
      <label height="75" name="question" width="250" x="50" y="20" size="10" bold="true" multiline="true"
        wordwrap="true">LOADING_QUESTION;</label>
    </div>
  </div>
  <script src="main.js" />
</view>


Figure 3: Sample gadget UI

The UI description is quite similar to HTML. We use a label to output the status and current question and a elements (links) to trigger actions.

The application code is defined in main.js. Desktop gadgets use XMLHttpRequest, which should be familiar to most of us. We use it to interact with the server API.

Here is an example of getting a question:

// Fetches a new question from the server and displays it.
function getNextQuestion() {
  is_loading = true;
  question.innerText = 'Loading question...';
  var req = new XMLHttpRequest();

  // Append a random number to the url so that we do not get the question from
  // the cache.
  req.open('GET', 'http://desktopgadgetsample.appspot.com/question?' +
           Math.random(), true);
  req.onreadystatechange = function() {
    if (req.readyState == 4) {
      if (req.status == 200) {
        onQuestionData(req.responseText);
      } else {
        getNextQuestion();
      }
      req = null;
    }
  };
  req.send();
}

Here's the code that parses the data:

// Called when the question is received from the server.
function onQuestionData(response_text) {
  var data = response_text.split('\n');
  question_info = {};
  question_info.id = data[0];
  question_info.text = data[1];
  question_info.timestamp = data[2];

  question.innerText = question_info.text;
  is_loading = false;
}

The last file, gadget.gmanifest, contains metadata such as the gadget name's and description.

As you've seen, the Desktop gadget API uses XML to describe the UI with HTML-like tags. It also uses standard JavaScript to implement application logic, manipulate the UI, and request remote data.

To learn more about Desktop gadgets, please visit the Google Desktop Gadget API page. You'll find links to tutorials, docs, and other resources.

Next steps

As a programmer, you've probably noticed some flaws in the example Desktop gadget and server API. Well then, let me present "exercises for the reader":

  • Use App Engine's support for Google accounts to limit one user vote per question.
  • Don't display questions that have already been answered.
  • Add an Ignore button to skip questions and prevent them from appearing again.

Conclusion

Desktop gadgets are a convenient way to build and distribute a desktop client for your server application. App Engine makes it easy to expose a server API, especially since CRUD operations are nicely encapsulated in the data model. We encourage everybody to bring your sites to the desktop. Please visit us on our developer group if you wish to discuss ideas or need help.

Resources

Author bios

Head

Paneendra once killed a cat with his bare hands.


Arm

Vishwajith is an Indus River dolphin.


Leg

James has lost three cell phones and broken four within the past few years. Needless to say, he won't be buying an iPhone.