Beyond Hello World

From OLPC
Jump to: navigation, search

Making An Activity That Uses The Journal

To make a real Activity for the XO you need to work with entries in the Journal. You may need to launch your Activity from the Journal, or save state to the Journal so your activity can be resumed later, or use the Journal entry as a data store for documents, etc. This is actually easy to do, once you know how.

Understanding the Journal

Files and directories are a major stumbling block for new computer users. If you've ever tried to teach your parents about files and directories you'll understand why the XO laptop uses the Journal concept instead. As a developer you know that beneath these Journal entries are files and directories, and once you know how to get at them you can use them in the normal way.

Every journal entry has two components:

  • A file
  • Metadata about that file.

There are two kinds of metadata: standard metadata that all journal entries have (name, description, screen capture image) and metadata that is unique to a given Activity. Examples of the second kind in the Read activity are the page number the user was on when he last used the Activity and the page zoom level he was using. Metadata can only be stored as character strings.

Journal entries are associated with Activities. There are two ways to accomplish this:

  • If an Activity creates a Journal entry, afterwards when the user resumes the Journal entry that Activity will be launched.
  • If a Journal entry represents a file that was not created by an Activity, or if the file is in a format that can be shared by multiple Activities, you can associate the journal entry to your Activity by specifying its MIME type in activity.info. For example, the application/pdf MIME is handled by Read, the text/plain activity is handled by Write, application/zip is handled by Etoys, etc. It is possible to have multiple Activities assigned to the same MIME type. In this case the user will select the Journal entry and be given a menu showing all of the Activities associated with that MIME type, and the user can then select the one she wishes to launch.

The last thing you need to know about the journal is that when you open a file from the journal you are not working on the file itself, but on a temporary copy of that file. In this way the journal can keep multiple versions of the same journal entry. This is true even for Journal entries on an SD card or a USB drive. Any file you work on will be a copy of the original. This will limit your ability to work with large files, like video files kept on a USB drive.

Using The Journal in Your Activity

All Activities extend a Python class named Activity. This class handles all the low level details of working with the Journal, and works on what is sometimes called "The Hollywood Principle", which is "Don't call us, we'll call you." In other words, in your Python code you will implement three methods which will be called by the Activity superclass, and it will be your Activity's job to read and write the file when asked to. The names of the three methods are:

__init__(self, handle)
Called when your Activity is constructed.
read_file(self, filename)
Called when there is a file to read, and only after __init__() is finished. filename will be the temporary copy of the file corresponding to the journal entry. When this method finishes running that file will be deleted, but if you haven't closed the file you'll still be able to read from the file in the rest of the Activity. This is also a good place to read any meta data left over from previous runs of the Activity.
write_file(self, filename)
Called when there is a request to write the journal entry. Even if you don't have a file to write you'll still need this method to save metadata. If you want to cancel the write (for example because you have nothing to write), you can raise NotImplementedError to cause the calling code to abort.

Sample Code: __init__()

All of the sample code is from an Activity that allows the user to page through Gutenberg etext files, either as ascii text files or a .zip files containing a single ascii text file (which is the easiest format to download using the Browse activity).

    def __init__(self, handle):
        "The entry point to the Activity"
        activity.Activity.__init__(self, handle)
        self.connect("key_press_event", self.keypress_cb)
        toolbox = activity.ActivityToolbox(self)
        self.set_toolbox(toolbox)
        
        self._read_toolbar = ReadToolbar()
        toolbox.add_toolbar(_('Read'), self._read_toolbar)
        self._read_toolbar.show()

        toolbox.show()
        scrolled = gtk.ScrolledWindow()
        scrolled.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
        scrolled.props.shadow_type = gtk.SHADOW_NONE
        self.label = gtk.Label()
        eb = gtk.EventBox()
        eb.add(self.label)
        eb.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("white"))
        scrolled.add_with_viewport(eb)
        self.label.show()
        eb.show()
        scrolled.show()
        self.set_canvas(scrolled)
        scrolled.show()
      
        self._read_toolbar.set_activity(self)

This method should do everything you can do to set up your Activity without being supplied with any information from the Journal. Even if your app is always launched from the Journal (like the Read activity) you'll still need to set up your canvas and your toolbar here. When you install a new Activity by resuming it from the Journal this is the only method of the three that will get called.

There is reason to believe that if you don't set up your canvas in __init__() the read_file() method will never be called.

In the code above we call the __init__() method in the superclass, register a listener for keypress events, create and install a toolbox, and install a ScrolledWindow as the canvas. The ScrolledWindow contains a Label, which is nested within an EventBox so the label can be given a white background. Finally we give the read toolbar a pointer to the Activity so that controls in the toolbar can invoke methods in the Activity.

Sample Code: read_file()

    def read_file(self, filename):
        "Read the Etext file"
        global PAGE_SIZE
        
        if filename.endswith(".zip"):
            self.zf = zipfile.ZipFile(filename, 'r')
            self.book_files = self.zf.namelist()
            self.save_extracted_file(self.zf, self.book_files[0])
            current_file_name = "/tmp/" + self.book_files[0]
        else:
            current_file_name = filename
            
        self.etext_file = open(current_file_name,"r")
        
        self.page_index = [ 0 ]
        pagecount = 0
        linecount = 0
        while self.etext_file:
            line = self.etext_file.readline()
            if not line:
                break
            linecount = linecount + 1
            if linecount >= PAGE_SIZE:
                position = self.etext_file.tell()
                self.page_index.append(position)
                linecount = 0
                pagecount = pagecount + 1
        self.page = int(self.metadata.get('current_page', '0'))
        self.show_page(self.page)
        self._read_toolbar.set_total_pages(pagecount + 1)
        self._read_toolbar.set_current_page(self.page)
        if filename.endswith(".zip"):
            os.remove(current_file_name)

This method first figures out what kind of file we're dealing with by looking at the file suffix. If it's a Zip file the contents are extracted to the /tmp directory and read, otherwise we read the filename passed to the method. If we create a temporary file we open it, read it, then delete it. Because of the way the Unix (GNU Linux) file system works we can still read the file after it has been deleted, because all we've done is delete a pointer to the file. The file doesn't actually go away until we close it. Since this is a Python program, the file should be closed automatically when the Activity closes down.

This line demonstrates how to read metadata:

   self.page = int(self.metadata.get('current_page', '0'))

Metadata is always stored as character strings, so we have to convert the bookmarked page number to an int before we can use it. metadata.get has a second parameter, which is what value to return if current_page is not defined.

Sample Code: write_file()

    def write_file(self, filename):
        "Save meta data for the file."
        self.metadata['current_page'] = str(self.page)

All write_file() does is save the current page as metadata, after converting it from an int to a string. If your application needed to save actual data you would open the filename for writing and write it out here.

The rest of the application is normal pygtk coding.

You can learn a lot more about writing Activities by studying the Pydoc for Sugar.

See also