Difference between revisions of "Development Team/Low-level Activity API"
m (moved Development Team/Low-level Activity API to Walter is a wanker 12/Low-level Activity API: Walter is a wanker) |
(deletions and) |
||
(28 intermediate revisions by 3 users not shown) | |||
Line 1: | Line 1: | ||
− | <noinclude>{{Developers | + | <noinclude>{{Developers}}</noinclude>__TOC__ |
Sugar activities are usually written in Python using the [http://api.sugarlabs.org/ Python Activity API]. This page documents the underlying mechanism that all activities need to conform to. Activities can be written in any language, as long as it can connect to D-Bus and provide an X11 interface. The discussion below tries to be language-agnostic. | Sugar activities are usually written in Python using the [http://api.sugarlabs.org/ Python Activity API]. This page documents the underlying mechanism that all activities need to conform to. Activities can be written in any language, as long as it can connect to D-Bus and provide an X11 interface. The discussion below tries to be language-agnostic. | ||
Line 22: | Line 22: | ||
# It creates an X11 window with special properties so the Sugar shell can associate an activity with its window. | # It creates an X11 window with special properties so the Sugar shell can associate an activity with its window. | ||
# If an object id was passed on the command line, the activity loads that object from the Datastore. Otherwise, it creates a new Datastore object. | # If an object id was passed on the command line, the activity loads that object from the Datastore. Otherwise, it creates a new Datastore object. | ||
− | # | + | # It needs to set up collaboration by connecting to the shared activity instance (if any). |
'''Operation''' | '''Operation''' | ||
− | # The activity continuously handles X11 user input, as well as D-Bus messages from the Sugar shell, or signals from other sources like the | + | # The activity continuously handles X11 user input, as well as D-Bus messages from the Sugar shell, or signals from other sources like Telepathy (the collaboration framework). |
# Whenever the state of the activity was altered significantly, it should update its Datastore object to prevent data loss on an unexpected shutdown. | # Whenever the state of the activity was altered significantly, it should update its Datastore object to prevent data loss on an unexpected shutdown. | ||
− | # If the user indicates wanting to share the activity, it | + | # If the user indicates wanting to share the activity, it needs to create a shared activity instance. |
# If the view-source key is pressed, some meta-action about the activity should be invoked, like showing its source code. | # If the view-source key is pressed, some meta-action about the activity should be invoked, like showing its source code. | ||
Line 185: | Line 185: | ||
Activities may query the datastore: | Activities may query the datastore: | ||
− | (results,count) = datastore.find(query) | + | (results,count) = datastore.find(query, properties) |
− | It returns the | + | It returns the matching object's metadata as array of dictionaries, and a count of matching items (the array may have fewer items if the query was limited). In addition to the usual metadata items, each dictionary will include the object id at key 'uid', <strike>the mountpoint of the item at key 'mountpoint',</strike> and possibly a 'filename' if requested. |
− | The | + | The '''properties''' argument is an array of strings. It defines which keys to include in the results. You should only specify the keys you actually need. If you pass an empty array here, all the metadata would be returned, which for a Journal with hundreds of entries and previews could amount to multiple megabytes of data. |
− | + | The '''query''' argument is a dictionary. If empty, all the datastore's entries will be returned, but again, you should avoid that. The key-value pairs specify the value (or array of values, or dictionary specifying range) for a specific property, e.g.: | |
− | + | : 'title' = 'First Project' <i>(but see note below)</i> | |
− | + | : 'mime_type' = ['image/png', 'image/jpeg'] | |
− | + | : 'mtime' = {'start' = '2007-07-01T00:00:00', 'end' = '2007-08-01T00:00:00'} | |
− | + | A few specific keys adjust the query: | |
− | + | : 'query': fulltext search term (supports some [http://xapian.org/docs/queryparser.html special operators]) | |
− | + | : 'order_by': sort key (or array of keys) to order results by, to reverse order use '-key' <i>(but see note below)</i> | |
− | + | : 'limit', 'offset': return only limit results starting at offset | |
− | + | : <strike>'mountpoints': array of [[#Mount Points|mountpoint ids]] to search (or all if not specified) </strike> | |
− | + | : 'include_files': if true, generate files as if get_filename() had been called for each item. In results, a property 'filename' will be added. | |
− | |||
− | : | ||
− | |||
− | <i>NOTE: Since Sugar 0.84 only very few keys are supported ('uid', 'activity', 'activity_id', 'mime_type', and 'keep', see [http://git.sugarlabs.org/projects/sugar-datastore/repos/mainline/blobs/master/src/carquinyol/indexstore.py#line32 here]). Other keys are ignored so you may get back more results than expected. Results are ordered by timestamp by default. Sort keys are limited, too (none in 0.84, in 0.86 'timestamp' and 'title', see [http://git.sugarlabs.org/projects/sugar-datastore/repos/mainline/blobs/master/src/carquinyol/indexstore.py#line266 here]).</i> | + | <i>NOTE: In Sugar 0.82 you cannot search for 'uid'. Since Sugar 0.84 only very few keys are supported in the query ('uid', 'activity', 'activity_id', 'mime_type', and 'keep', see [http://git.sugarlabs.org/projects/sugar-datastore/repos/mainline/blobs/master/src/carquinyol/indexstore.py#line32 here]). Other keys are ignored so you may get back more results than expected. Results are ordered by timestamp by default. Sort keys are limited, too (none in 0.84, in 0.86 'timestamp' and 'title', see [http://git.sugarlabs.org/projects/sugar-datastore/repos/mainline/blobs/master/src/carquinyol/indexstore.py#line266 here]).</i> |
You can also retrieve an array of unique values for a field: | You can also retrieve an array of unique values for a field: | ||
Line 211: | Line 208: | ||
values = datastore.get_uniquevaluesfor(property, query) | values = datastore.get_uniquevaluesfor(property, query) | ||
− | + | ''NOTE: currently (2007-07-25) the query is ignored in this call, it looks for all values in all entries. And in more recent Sugar versions, the only property allowed is 'activity'.'' | |
+ | |||
+ | ==Watching== | ||
+ | |||
+ | To keep cached information in your activity up-to-date, you can subscribe to these signals: | ||
+ | |||
+ | datastore.Created(object_id) | ||
+ | datastore.Updated(object_id) | ||
+ | datastore.Deleted(object_id) | ||
+ | |||
+ | E.g. if you display a document title in your activity, and the user changes that entry in the Journal while your activity is running, you can use the Updated signal to reflect that change. Otherwise, when you save the document, it will overwrite the title set by the user. | ||
+ | |||
+ | Similarly, if you display a list of Journal entries in your activity, it would have to be updated in response to the Created and Deleted signals. | ||
==Progress Display== | ==Progress Display== | ||
Line 237: | Line 246: | ||
===Choosing Objects=== | ===Choosing Objects=== | ||
Call this method to bring up the Chooser dialog (which looks like a small journal overlayed on your activity): | Call this method to bring up the Chooser dialog (which looks like a small journal overlayed on your activity): | ||
− | chooser_id = org.laptop.Journal.ChooseObject(xid, what_filter) | + | |
− | The xid should be your activity's X window handle, or <tt>0</tt>. The filter says what type of Journal entries is preselected in the drop-down menu (''this parameter was added in Sugar 0.83''). It's a string containing either a bundle id (e.g., <tt>'my.organization.MyActivity'</tt>), or one of the generic data types (<tt>'Text', 'Image', 'Video', 'Audio', 'Link'</tt> [http://git.sugarlabs.org/projects/sugar-base/repos/mainline/blobs/master/src/sugar/mime.py#line32 definition]), or an empty string for no filter. The call returns immediately with a string chooser_id. You need to watch | + | chooser_id = org.laptop.Journal.ChooseObject(xid:s, what_filter:s) # in Sugar 0.82 this had only one argument |
− | ObjectChooserResponse(chooser_id, | + | |
− | ObjectChooserCancelled(chooser_id) | + | The xid should be your activity's X window handle, or <tt>0</tt>. The filter says what type of Journal entries is preselected in the drop-down menu (''this parameter was added in Sugar 0.83''). It's a string containing either a bundle id (e.g., <tt>'my.organization.MyActivity'</tt>), or one of the generic data types (<tt>'Text', 'Image', 'Video', 'Audio', 'Link'</tt> [http://git.sugarlabs.org/projects/sugar-base/repos/mainline/blobs/master/src/sugar/mime.py#line32 definition]), or an empty string for no filter. The call returns immediately with a string chooser_id. You need to watch the following signals which get emitted when an item is chosen or the dialog is cancelled: |
− | The | + | |
+ | ObjectChooserResponse(chooser_id:s, object_id_or_path:s) | ||
+ | ObjectChooserCancelled(chooser_id:s) | ||
+ | |||
+ | The chooser_id can be tested to see if this signal was in response to our own request. This is important if the user opens two file choosers at the same time (possibly in two different activities). If the user closes the chooser without selecting anything, the ObjectChooserCancelled signal is sent. Otherwise, the ObjectChooserResponse signal will be sent, and its object_id_or_path string argument is either an object id or a file path. To distinguish between the two cases, check if the first character is a slash (<tt>'/'</tt>). If it indeed begins with a slash, then this is an absolute path to a file, which can be opened directly. Otherwise, if the first character is not a slash, then object_id_or_path is an object id, which can be used to open the corresponding [[#Keeping and Resuming|datastore object]]. | ||
===Focusing Objects=== | ===Focusing Objects=== | ||
Line 248: | Line 261: | ||
org.laptop.Journal.ShowObject(object_id) | org.laptop.Journal.ShowObject(object_id) | ||
− | Switches to the Journal activity and shows the selected object. | + | Switches to the Journal activity and shows the "detail view" of the selected object. |
+ | |||
+ | This can be used to, e.g., open a URL (create a URL object in the journal then call this) or to view source code (store the source code as text file, then call ShowObject() on it). | ||
− | + | Note: since Sugar 0.84, you also can give the full path to an existing file instead of an object id. If the user resumes that file, it will be copied to the Journal first and then opened using the chosen activity. <i>This seems to be of limited usefulness and is documented here just for completeness</i> | |
<s>To focus on multiple objects: <code>org.laptop.Journal.FocusSearch(query)</code></s> ''Removed in Sugar 0.83'' | <s>To focus on multiple objects: <code>org.laptop.Journal.FocusSearch(query)</code></s> ''Removed in Sugar 0.83'' | ||
==Mount Points== | ==Mount Points== | ||
+ | <strike> | ||
Devices are represented as mount points in the datastore. If no mountpoint is explicitly specified, the main datastore (Journal) is used. | Devices are represented as mount points in the datastore. If no mountpoint is explicitly specified, the main datastore (Journal) is used. | ||
Line 261: | Line 277: | ||
Returns an array of mount point descriptors where each descriptor is a dictionary containing at least the following keys: | Returns an array of mount point descriptors where each descriptor is a dictionary containing at least the following keys: | ||
:'id': the id used to refer explicitly to the mount point | :'id': the id used to refer explicitly to the mount point | ||
− | :'title': Human readable identifier for the mountpoint | + | :'title': Human readable identifier for the mountpoint ''(in Sugar 0.82, just the uri)'' |
:'uri': The uri which triggered the mount | :'uri': The uri which triggered the mount | ||
Line 267: | Line 283: | ||
Large files to be stored on an external device should be placed at the uri of the mount point (see [[#External Media|external media]]). | Large files to be stored on an external device should be placed at the uri of the mount point (see [[#External Media|external media]]). | ||
+ | </strike> | ||
+ | |||
+ | '''Note:''' ''This only works as described in Sugar up to 0.82. The functions do exist in later Sugar versions, but do not return useful data. Use the [[#External Media|external media API]] instead.'' | ||
=Security= | =Security= | ||
Line 293: | Line 312: | ||
;$SUGAR_ACTIVITY_ROOT/data/: This directory is used similar to a traditional home directory, for persistent activity data such as configuration files. Make sure files in there are group readable and writable (see [[#Users and Groups|users and groups]]). The directory itself is group-writable. Files stored here will survive reboots and OS upgrades. | ;$SUGAR_ACTIVITY_ROOT/data/: This directory is used similar to a traditional home directory, for persistent activity data such as configuration files. Make sure files in there are group readable and writable (see [[#Users and Groups|users and groups]]). The directory itself is group-writable. Files stored here will survive reboots and OS upgrades. | ||
− | ;$SUGAR_ACTIVITY_ROOT/tmp/: This directory is used similar to a /tmp directory, being backed by RAM. It may be as small as 1 MB. | + | ;$SUGAR_ACTIVITY_ROOT/tmp/: This directory is used similar to a /tmp directory, being backed by RAM if [[olpc:Rainbow|Rainbow]] is present. It may be as small as 1 MB. With [[olpc:Rainbow|Rainbow]], this directory is deleted when the activity exits (specifically, as soon as all children of the activity's first process die). This directory is ''only'' accessible to the activity and its children; not even to Sugar. |
− | ;$SUGAR_ACTIVITY_ROOT/instance/: This directory is used similar to a /var/tmp directory, being backed by flash rather than by RAM. It is unique per instance. It is used for transfer to and from the datastore (see [[#Keeping and Resuming|keeping and resuming]]). | + | ;$SUGAR_ACTIVITY_ROOT/instance/: This directory is used similar to a /var/tmp directory, being backed by flash rather than by RAM. It is unique per instance. It is used for transfer to and from the datastore (see [[#Keeping and Resuming|keeping and resuming]]). With [[olpc:Rainbow|Rainbow]], this directory is deleted when the activity exits (specifically, as soon as all children of the activity's first process die). |
As of version 8.2, all the activity root directories are created in /home/olpc/isolation and can be examined there with the Terminal activity. However, activities MUST use the $SUGAR_ACTIVITY_ROOT variable because the isolation directory layout is expected to change. | As of version 8.2, all the activity root directories are created in /home/olpc/isolation and can be examined there with the Terminal activity. However, activities MUST use the $SUGAR_ACTIVITY_ROOT variable because the isolation directory layout is expected to change. | ||
Line 305: | Line 324: | ||
=== External Media === | === External Media === | ||
− | External media (USB drives, SD cards) are auto-mounted by the Journal | + | External media (USB drives, SD cards) are auto-mounted by the Journal. No access restrictions are applied currently. If activities use these external volumes directly, they need to take care of ensuring data integrity since the user may (and will) remove the medium at any time. |
+ | |||
+ | Depending on the Linux distro version, the volumes appear as /mnt/*, /media/*, /run/media/$USER/*, or in other places. Sugar up to version 0.82 provided an API to access their [[#Mount Points|mount points]]. Nowadays you should use general Linux API to find the volumes. The recommended way is using the [http://developer.gnome.org/gio/stable/GVolumeMonitor.html GNOME Volume Monitor] with a fallback to [http://www.freedesktop.org/wiki/Software/hal HAL] (there is an example in the [http://git.sugarlabs.org/backup/mainline/blobs/master/backup.py#line671 Backup activity]). | ||
== Signing == | == Signing == | ||
Line 323: | Line 344: | ||
Collaboration plays a large role in Sugar. Still, the presence and sharing APIs are still somewhat rough. There are attempts to explain sharing, see [[olpc:Activity sharing|Activity sharing]]. The following are the bare essentials. | Collaboration plays a large role in Sugar. Still, the presence and sharing APIs are still somewhat rough. There are attempts to explain sharing, see [[olpc:Activity sharing|Activity sharing]]. The following are the bare essentials. | ||
+ | ''Note: The Presence Service has been deprecated. Activities need to talk to [[Telepathy]] directly instead. See [[#Collaboration|below]] for details'' | ||
+ | |||
+ | <strike> | ||
==General== | ==General== | ||
Line 377: | Line 401: | ||
BuddyJoined (o: buddy) | BuddyJoined (o: buddy) | ||
BuddyLeft (o: buddy) | BuddyLeft (o: buddy) | ||
+ | |||
+ | </strike> | ||
==Tubes== | ==Tubes== | ||
Line 428: | Line 454: | ||
===D-Bus Tubes=== | ===D-Bus Tubes=== | ||
− | '' | + | ''see [[olpc:Presence Service D-Bus API]] and [http://telepathy.freedesktop.org/spec-0.16.html#org.freedesktop.Telepathy.Channel.Type.Tubes Tubes]'' |
+ | |||
+ | = Collaboration = | ||
+ | |||
+ | Sharing and collaboration are core principles in Sugar. They are implemented using [[Telepathy]]. | ||
+ | |||
+ | '''Note:''' ''The [[#Presence|Presence Service]] (which was a wrapper around Telepathy in earlier Sugar releases) has been deprecated. Activities need to use Telepathy directly.'' | ||
+ | |||
+ | == Telepathy == | ||
+ | |||
+ | For details, please refer to the [http://telepathy.freedesktop.org/spec/ Telepathy Specification] and the [http://telepathy.freedesktop.org/doc/book/ Telepathy Manual]. The code below is pseudo code, you need to adapt it for your specific programming language. | ||
+ | |||
+ | The entry point to Telepathy is the AccountManager: | ||
+ | |||
+ | Service: org.freedesktop.Telepathy.AccountManager | ||
+ | Interface: org.freedesktop.Telepathy.AccountManager | ||
+ | Object Path: /org/freedesktop/Telepathy/AccountManager | ||
+ | |||
+ | It has a list of valid Accounts. Each account has a Connection property, which is valid if its D-Bus path is not '/', and connected if the account's ConnectionStatus property is 0 (1 means connecting, 2 disconnected). | ||
+ | |||
+ | accounts = accountManager.Get('ValidAccounts') | ||
+ | for account in accounts: | ||
+ | connection = account.Get('Connection') | ||
+ | if connection.path != '/' && account.Get('ConnectionStatus') == 0: | ||
+ | connections.add(connection) | ||
+ | |||
+ | This gathers all the usable connections, there may be more than one. Since connections can become available later, you need to either track changes using D-Bus signals, or do the above every time a new connection is needed. | ||
+ | |||
+ | == Startup == | ||
+ | |||
+ | On startup, an activity needs to check all the currently available connections if there is already a "room" for it. The GetActivity method checks for the room given an activity_id: | ||
+ | |||
+ | for connection in connections: | ||
+ | room_handle = connection.GetActivity(activityId) # may fail | ||
+ | |||
+ | It is provided by the non-standard ActivityProperties interface: | ||
+ | |||
+ | INTERFACE org.laptop.Telepathy.ActivityProperties | ||
+ | METHOD GetActivity(activity_id:s) => (room:u) | ||
+ | METHOD GetProperties(room:u) => (properties:a{sv}) | ||
+ | METHOD SetProperties(room:u, properties:a{sv}) => () | ||
+ | SIGNAL ActivityPropertiesChanged(room:u, properties:a{sv}) | ||
+ | |||
+ | If a room is found, the activity needs to join it immediately (see [[#Joining|below]]). Otherwise, a normal "unshared" activity startup should commence. | ||
+ | |||
+ | == Joining == | ||
+ | |||
+ | If the activity finds itself to be shared already on startup, or the user chooses to enable sharing later, it needs to "join" the room used for communication: | ||
+ | |||
+ | * create text channel | ||
+ | * create tubes channel | ||
+ | * add self to group | ||
+ | |||
+ | ''to be documented'' | ||
+ | |||
+ | == Leaving == | ||
+ | |||
+ | ''to be documented'' | ||
+ | |||
+ | == Inviting == | ||
+ | |||
+ | ''to be documented'' | ||
+ | |||
+ | == Communicating == | ||
+ | |||
+ | This depends on the needs of the activity, but in general it would | ||
+ | |||
+ | * create a tube in tubes channel | ||
+ | * use the tube for communication | ||
+ | |||
+ | ''to be documented'' | ||
[[Category:API]] | [[Category:API]] | ||
[[Category:Collaboration]] | [[Category:Collaboration]] |
Latest revision as of 21:17, 19 January 2016
Sugar activities are usually written in Python using the Python Activity API. This page documents the underlying mechanism that all activities need to conform to. Activities can be written in any language, as long as it can connect to D-Bus and provide an X11 interface. The discussion below tries to be language-agnostic.
This documentation effort was started by Bert on the OLPC wiki while implementing the Squeak-based Etoys activity. Please fill in missing pieces and correct mistakes!
See olpc:Activity Development Alternatives for an overview of various ways to develop activities.
As Sugar evolves to become compatible with ordinary X11 programs, and as services like collaboration migrate into generic X11 desktops, this document will become less and less relevant. All the special rules for "Sugar Activities" will fall by the wayside, because any X11 program will be usable with Sugar. This will greatly simplify both the job of Activity authors, and the usefulness of Sugar-based computers (which will have access to thousands of X "applications" that have never heard of Sugar and never will).
Overview
An Activity is basically a regular X11 program which communicates with the special Sugar services via D-Bus.
The Activity bundle specifies an executable. For each activity instance, that executable is run with arguments specifying the bundle id (taken from the bundle) and activity id (generated by Sugar). The instance opens an X window, putting these ids into window properties. It also needs to provide a D-Bus service to receive messages from Sugar. An activity must retrieve and store its state in the datastore, implement sharing on the mesh network, and be security compliant.
Activity Life Cycle
Please see Activity Basics for the user's point of view. The programmer's point of view is outlined here, and detailed in the following sections:
Start Up
- The activity is executed.
- It creates a D-Bus service to receive method calls from the Sugar shell.
- It creates an X11 window with special properties so the Sugar shell can associate an activity with its window.
- If an object id was passed on the command line, the activity loads that object from the Datastore. Otherwise, it creates a new Datastore object.
- It needs to set up collaboration by connecting to the shared activity instance (if any).
Operation
- The activity continuously handles X11 user input, as well as D-Bus messages from the Sugar shell, or signals from other sources like Telepathy (the collaboration framework).
- Whenever the state of the activity was altered significantly, it should update its Datastore object to prevent data loss on an unexpected shutdown.
- If the user indicates wanting to share the activity, it needs to create a shared activity instance.
- If the view-source key is pressed, some meta-action about the activity should be invoked, like showing its source code.
Shut Down
- When the activity window is closed, it updates its Datastore object with its current state.
- It leaves the shared activity (if shared),
- and then quits.
Activity Instance
When the activity instance is executed, the current working directory will be set to the bundle directory (e.g., ~/Activities/MyActivity.activity) so resource files can be accessed using relative paths. Also, its "bin" subdirectory is added to the PATH.
Command Line Arguments
The following arguments are passed to the executable:
- -b, --bundle-id
- Identifier of the activity bundle. Must be made available as window property.
- -a, --activity-id
- Unique identifier of the activity instance. Must be made available as window property, and is used to create the D-Bus service.
- -o, --object-id
- (optional) Identity of the journal object associated with the activity instance. When you resume an activity from the journal the object id will be passed in (see datastore).
- -u, --uri
- (optional) URI associated with the activity. Used when opening an external file or resource in the activity, rather than a journal object (downloads stored on the file system for example or web pages).
Environment Variables
Some environment variables are setup before the activity is launched:
SUGAR_ACTIVITY_ROOT
Writable space for the activity, see security. Activities are prohibited from writing anywhere else in the file system.
SUGAR_BUNDLE_PATH
Path to the current activity bundle (e.g., /usr/share/activities/MyActivity.activity or ~/Activities/MyActivity.activity). This is also the current working directory when the activity is started, so relative paths can be used to access files inside the bundle, rather than constructing absolute paths using this variable.
X Properties
The activity instance needs to set some properties on its top-level window, before the window is shown on the screen (see #5271):
_SUGAR_BUNDLE_ID
The bundle id (e.g., my.organization.MyActivity) of type STRING as passed on the command line.
_SUGAR_ACTIVITY_ID
The activity id (e.g., 6f7f3acacca87886332f50bdd522d805f0abbf1f) of type STRING as passed on the command line.
- The above properties need to be on the window before it pops up. This is easy when programming with raw libX11, but often difficult with high-level toolkits. E.g., in GTK you can use the "realize" event. The toolkit is likely to create and pop up the window in one operation, so you don't get a chance to set the properties. A workable solution is to piggyback on a function within the toolkit. For example, you can implement XChangeProperty in your activity. Using dlsym() with the RTLD_NEXT flag, you can obtain a function pointer to the normal XChangeProperty function in libX11. Your implementation normally just calls that. The first time your implementation is called though, it also sets up the sugar-specific properties. Essentially you are supplying a callback function to a toolkit that was never intended to call one. Once #5271 is settled, this hack is not necessary anymore
Also, some Window Manager hints need to be set:
_NET_WM_NAME
should be set to the current activity title. It usually corresponds to the title which is displayed in the journal and advertised on the network for shared activities. See Freedesktop specification.
_NET_WM_PID
must be set to the activity's process id so the shell can associate memory usage with an activity. See Freedesktop specification.
D-Bus Methods
An activity instance needs to create a D-Bus service:
Service name: org.laptop.Activity6f7f3acacca87886332f50bdd522d805f0abbf1f # no dot after Activity! Object path: /org/laptop/Activity/6f7f3acacca87886332f50bdd522d805f0abbf1f Interface: org.laptop.Activity
(where 6f7f3acacca87886332f50bdd522d805f0abbf1f is the activity id as passed on the cmd line)
It must support the following methods:
org.laptop.Activity.SetActive(b: active)
Activate or passivate an activity. This is sent when switching activities, there is only one active activity at a time, all others are passive. A passive activity must immediately release resources like sound, camera etc. Also it should prepare for being killed without warning at any time in the future (see OOM) by auto-saving to the datastore.
org.laptop.Activity.Invite(s: buddy_key)
If not yet shared, share this activity privately because the user chose "invite" from the mesh view. Then, invite the buddy (see below).
The following methods are optional:
org.laptop.Activity.HandleViewSource()
The user wants to view the source. If this method does not answer an error, the activity will handle viewing the source code itself, otherwise Sugar will show the source.
org.laptop.Activity.GetDocumentPath():s
Answer the path of a document source file to view, in addition to the activity bundle itself. Sugar will delete that document when the view-source interface is closed. This method is only called when Sugar handles the source viewing.
Session
To communicate to an activity when it needs to save data and quit, we use the X Session management protocol. The part of the protocol which deals with application restarting is not used and we don't plan to implement it.
In the Glucose 0.84 release cycle we are planning do add support for a D-Bus based protocol which is currently being developed for the GNOME desktop. You can read about it on this bugzilla report.
Datastore
An Activity instance must store its complete state in the central datastore so it appears in the Journal and can be resumed later. It needs to connect to the datastore service:
Service name: org.laptop.sugar.DataStore Object path: /org/laptop/sugar/DataStore Interface: org.laptop.sugar.DataStore
Meta Data
An item in the datastore is referenced by an object_id, it has a dictionary of properties, and possibly a file. The properties have String keys but Variant values. Here are a few properties:
'activity': 'my.organization.MyActivity' # bundle id (determines icon and default activity) 'activity_id': '6f7f3acacca87886332f50bdd522d805f0abbf1f' 'title': 'My new project' # as shown in journal 'title_set_by_user': '0' # '1' if not default title 'keep': '0' # '1' if marked as "favorite" (star) 'ctime': '1972-05-12T18:41:08' # created (local time) 'mtime': '2007-06-16T03:42:33' # modified (local time), deprecated but still used internally so must be present 'timestamp': 1192715145 # modified (UTC), in seconds since the UNIX epoch, must be present 'preview': ByteArray(png file data, 300x225 px) 'icon-color': '#ff0000,#ffff00' # owner buddy or shared activity color 'mime_type': 'application/x-my-activity' 'share-scope': # if shared 'buddies': '{}' # buddies in a shared activity as JSON 'description': 'some longer text' # description editable in journal detail view 'tags': 'one two' # tags editable in journal detail view 'something:text': 'text I want to be indexed' # properties with key ending in ":text" will be searched in fulltext search 'checksum': # md5 hash of file, created by DS, do not set or modify 'my.organization.MyActivity.myProperty' # private property, prefixed with activity name to avoid name clashes
Due to bug #4662 only some known properties are retained! The list is at the bottom of datastore/model.py (fixed in Sugar 0.83)
And custom properties must have String values for now (bug #5134).
And since Sugar 0.83 all former String values are now stored and returned as Arrays of Bytes. You can still set them as Strings.
Keeping and Resuming
To create an item in the datastore, call create():
object_id = datastore.create(properties, filename, transfer_ownership)
The metadata properties are a dictionary (type "a{sv}") containing (at least) the entries mentioned above. If filename is not empty, the file will be moved or copied to the datastore, depending on the transfer_ownership flag. The activity should delete the file once the call completes (if transfer_ownership was false). Otherwise (if transfer_ownership was true) the datastore will remove the file. For this to work under rainbow you must place the file in the "instance" directory. The returned id will be a string like '4543af91-7be9-404e-b2f1-3e27cb15a15d'.
To update an item use update():
datastore.update(object_id, properties, filename, transfer_ownership)
Again, if a filename was given and transfer_ownership is false, the activity is responsible for deleting the file after the call returns.
To retrieve an object's properties and file:
properties = datastore.get_properties(object_id) filename = datastore.get_filename(object_id)
The returned temp file should be deleted by the activity as soon as possible, latest when the activity quits.
The metadata properties need to be preserved and stored again when updating an entry. An activity should also track updates to the properties made in the Journal while editing the datastore object. For this it should subscribe to the Updated signal:
datastore.Updated(object_id)
- An efficient way to do this is registering a DBus match like path='/org/laptop/sugar/DataStore', member='Updated', interface='org.laptop.sugar.DataStore', type='signal', arg0='objectId' --Bert
Querying
Activities may query the datastore:
(results,count) = datastore.find(query, properties)
It returns the matching object's metadata as array of dictionaries, and a count of matching items (the array may have fewer items if the query was limited). In addition to the usual metadata items, each dictionary will include the object id at key 'uid', the mountpoint of the item at key 'mountpoint', and possibly a 'filename' if requested.
The properties argument is an array of strings. It defines which keys to include in the results. You should only specify the keys you actually need. If you pass an empty array here, all the metadata would be returned, which for a Journal with hundreds of entries and previews could amount to multiple megabytes of data.
The query argument is a dictionary. If empty, all the datastore's entries will be returned, but again, you should avoid that. The key-value pairs specify the value (or array of values, or dictionary specifying range) for a specific property, e.g.:
- 'title' = 'First Project' (but see note below)
- 'mime_type' = ['image/png', 'image/jpeg']
- 'mtime' = {'start' = '2007-07-01T00:00:00', 'end' = '2007-08-01T00:00:00'}
A few specific keys adjust the query:
- 'query': fulltext search term (supports some special operators)
- 'order_by': sort key (or array of keys) to order results by, to reverse order use '-key' (but see note below)
- 'limit', 'offset': return only limit results starting at offset
'mountpoints': array of mountpoint ids to search (or all if not specified)- 'include_files': if true, generate files as if get_filename() had been called for each item. In results, a property 'filename' will be added.
NOTE: In Sugar 0.82 you cannot search for 'uid'. Since Sugar 0.84 only very few keys are supported in the query ('uid', 'activity', 'activity_id', 'mime_type', and 'keep', see here). Other keys are ignored so you may get back more results than expected. Results are ordered by timestamp by default. Sort keys are limited, too (none in 0.84, in 0.86 'timestamp' and 'title', see here).
You can also retrieve an array of unique values for a field:
values = datastore.get_uniquevaluesfor(property, query)
NOTE: currently (2007-07-25) the query is ignored in this call, it looks for all values in all entries. And in more recent Sugar versions, the only property allowed is 'activity'.
Watching
To keep cached information in your activity up-to-date, you can subscribe to these signals:
datastore.Created(object_id) datastore.Updated(object_id) datastore.Deleted(object_id)
E.g. if you display a document title in your activity, and the user changes that entry in the Journal while your activity is running, you can use the Updated signal to reflect that change. Otherwise, when you save the document, it will overwrite the title set by the user.
Similarly, if you display a list of Journal entries in your activity, it would have to be updated in response to the Created and Deleted signals.
Progress Display
To add a progress bar to a Journal entry (like the Browse activity does while downloading files), create an entry with a "progress" property in the meta data. The value is the percentage done (0 to 100). The file cannot actually be stored incrementally in the datastore, it needs to be saved to a temp file first. But the entry metadata can be updated continuously to inform the user of progress while creating the temp file (by not giving a filename yet in the update() call). Once the temp file is complete, it can be checked-in as usual. The user can cancel by clicking the x icon next to the progress bar. This deletes the entry, so you need to watch for the datastore's "Deleted(id)" signal.
meta = ... # regular metadata meta["progress"] = 0 id = datastore.create(meta, "") # create with progress bar while (done() < 100) { if (got_signal(datastore, "Deleted", id)) return user_cancelled(); write_to(tmpfile) meta['progress'] = done() datastore.update(id, meta, "") # update progress bar } meta.deleteKey("progress") datastore.update(id, meta, tmpfile.name) # check-in file, remove progress bar
Journal UI
The Journal activity provides a D-Bus service to allow activities to bring up an object chooser dialog, focus searches etc.:
Service name: org.laptop.Journal Object path: /org/laptop/Journal Interface: org.laptop.Journal
Choosing Objects
Call this method to bring up the Chooser dialog (which looks like a small journal overlayed on your activity):
chooser_id = org.laptop.Journal.ChooseObject(xid:s, what_filter:s) # in Sugar 0.82 this had only one argument
The xid should be your activity's X window handle, or 0. The filter says what type of Journal entries is preselected in the drop-down menu (this parameter was added in Sugar 0.83). It's a string containing either a bundle id (e.g., 'my.organization.MyActivity'), or one of the generic data types ('Text', 'Image', 'Video', 'Audio', 'Link' definition), or an empty string for no filter. The call returns immediately with a string chooser_id. You need to watch the following signals which get emitted when an item is chosen or the dialog is cancelled:
ObjectChooserResponse(chooser_id:s, object_id_or_path:s) ObjectChooserCancelled(chooser_id:s)
The chooser_id can be tested to see if this signal was in response to our own request. This is important if the user opens two file choosers at the same time (possibly in two different activities). If the user closes the chooser without selecting anything, the ObjectChooserCancelled signal is sent. Otherwise, the ObjectChooserResponse signal will be sent, and its object_id_or_path string argument is either an object id or a file path. To distinguish between the two cases, check if the first character is a slash ('/'). If it indeed begins with a slash, then this is an absolute path to a file, which can be opened directly. Otherwise, if the first character is not a slash, then object_id_or_path is an object id, which can be used to open the corresponding datastore object.
Focusing Objects
The Journal activity allows an activitiy to "focus" an object, so the user can immediately open it (this is currently the only way to have one activity "launch" a different activity).
org.laptop.Journal.ShowObject(object_id)
Switches to the Journal activity and shows the "detail view" of the selected object.
This can be used to, e.g., open a URL (create a URL object in the journal then call this) or to view source code (store the source code as text file, then call ShowObject() on it).
Note: since Sugar 0.84, you also can give the full path to an existing file instead of an object id. If the user resumes that file, it will be copied to the Journal first and then opened using the chosen activity. This seems to be of limited usefulness and is documented here just for completeness
To focus on multiple objects: Removed in Sugar 0.83
org.laptop.Journal.FocusSearch(query)
Mount Points
Devices are represented as mount points in the datastore. If no mountpoint is explicitly specified, the main datastore (Journal) is used.
mounts = datastore.mounts()
Returns an array of mount point descriptors where each descriptor is a dictionary containing at least the following keys:
- 'id': the id used to refer explicitly to the mount point
- 'title': Human readable identifier for the mountpoint (in Sugar 0.82, just the uri)
- 'uri': The uri which triggered the mount
Mount points can be specified when creating an object (using a 'mountpoint' key and id value in the properties), and when querying the datastore (by adding a 'mountpoints' query option).
Large files to be stored on an external device should be placed at the uri of the mount point (see external media).
Note: This only works as described in Sugar up to 0.82. The functions do exist in later Sugar versions, but do not return useful data. Use the external media API instead.
Security
Activities are isolated from each other and from the "olpc" user. They do not have the same permissions as you would expect in a non-restricted Linux environment (see Bitfrost and Rainbow). In particular, they can not write in the /home/olpc directory!
Users and Groups
While Sugar runs as user "olpc", activities do not (the Terminal activity as a maintenance tool is an exception).
Instead, each activity instance is run with a unique user id. That is, a new anonymous user is created when the user clicks an activity icon, and the Rainbow demon runs the activity under that user.
All instance of the same activity get the same unique group id. That is, a new anonymous group is created when the activity is run for the first time, each subsequent activity launch will use the same group id. This means files to be shared for all instances of an activity must be made group-accessible.
File Access
Home Directory
Since each activity is run as a different user, it gets a different home directory on each invocation. In release 8.2, the home directory for an activity equals the $SUGAR_ACTIVITY_ROOT/instance/ directory (see below). For data such as config files to survive and be accessible by all future activity invocations, they must not be stored in $HOME but rather $SUGAR_ACTIVITY_ROOT/data/ should be used.
- Hint: A trick to help porting legacy software which expects its config files to be in the home directory is to export HOME=$SUGAR_ACTIVITY_ROOT/data in an activity's launch script. --Bert
Writable Directories
All writing to the file system is restricted to subdirectories of the path given in the SUGAR_ACTIVITY_ROOT environment variable. This directory has three subdirectories with different policies:
- $SUGAR_ACTIVITY_ROOT/data/
- This directory is used similar to a traditional home directory, for persistent activity data such as configuration files. Make sure files in there are group readable and writable (see users and groups). The directory itself is group-writable. Files stored here will survive reboots and OS upgrades.
- $SUGAR_ACTIVITY_ROOT/tmp/
- This directory is used similar to a /tmp directory, being backed by RAM if Rainbow is present. It may be as small as 1 MB. With Rainbow, this directory is deleted when the activity exits (specifically, as soon as all children of the activity's first process die). This directory is only accessible to the activity and its children; not even to Sugar.
- $SUGAR_ACTIVITY_ROOT/instance/
- This directory is used similar to a /var/tmp directory, being backed by flash rather than by RAM. It is unique per instance. It is used for transfer to and from the datastore (see keeping and resuming). With Rainbow, this directory is deleted when the activity exits (specifically, as soon as all children of the activity's first process die).
As of version 8.2, all the activity root directories are created in /home/olpc/isolation and can be examined there with the Terminal activity. However, activities MUST use the $SUGAR_ACTIVITY_ROOT variable because the isolation directory layout is expected to change.
Concurrency
Multiple instances of an activity may communicate with one another through their shared 'data' directory; however, since each instance runs as a different user, some care must be taken (#5476) when sending messages to other activities through this shared medium.
External Media
External media (USB drives, SD cards) are auto-mounted by the Journal. No access restrictions are applied currently. If activities use these external volumes directly, they need to take care of ensuring data integrity since the user may (and will) remove the medium at any time.
Depending on the Linux distro version, the volumes appear as /mnt/*, /media/*, /run/media/$USER/*, or in other places. Sugar up to version 0.82 provided an API to access their mount points. Nowadays you should use general Linux API to find the volumes. The recommended way is using the GNOME Volume Monitor with a fallback to HAL (there is an example in the Backup activity).
Signing
An activity will have to be cryptographically signed to allow secure activity upgrades once they are on the machines. Tools for this will be provided soon. See discussion of contents.sig.
to be detailed
Permissions Declarations
Permission declarations will enumerate which special permissions (camera access? microphone access? non-Tubes network access? etc.) your activity may need for its normal operation. See permissions.info and generally Bitfrost.
to be detailed
Presence
Collaboration plays a large role in Sugar. Still, the presence and sharing APIs are still somewhat rough. There are attempts to explain sharing, see Activity sharing. The following are the bare essentials.
Note: The Presence Service has been deprecated. Activities need to talk to Telepathy directly instead. See below for details
General
Activities must support sharing using the Presence Service (PS). It is accessible on the D-Bus:
Service: org.laptop.Sugar.Presence Interface: org.laptop.Sugar.Presence Object Path: /org/laptop/Sugar/Presence
Sharing
If the activity was not yet shared but the user clicked the Share button, sharing is initiated by calling ShareActivity():
activity = PS.ShareActivity(activity_id, bundle_id, name, properties)
The bundle id is used for the icon and to launch the same activity when someone joins it. The name will be shown in the mesh view and should generally be the same as the title of the datastore object (see above). The properties argument is not used currently (but see #4660) and should be an empty dictionary.
Note that sharing will be private (invitation-only) by default, that is, the icon will not be visible in the mesh. To share publicly, set the 'private' property to False:
activity.SetProperties({'private': False})
Inviting
Another way of starting a shared session is by inviting a buddy from the mesh view. The Invite() method of the activity is called (see above). Then the activity should be shared privately, and the buddy must be invited using the key that was passed to Invite():
buddy = PS.GetBuddyByPublicKey(buddy_key) activity.Invite(buddy, message)
Joining
When launching, the PS must be consulted to see if this instance was shared by someone else, meaning it was launched by the user is trying to join it:
activity = PS.GetActivityById(activity_id)
This yields an error if this instance (identified by its activity id) was not shared, in which case a regular non-shared startup should be performed. Otherwise, the activity object held by the PS is returned, and this activity instance needs to join:
activity.Join()
It should continue by establishing a communication channel with the originating instance (see below)
Leaving
To leave a shared activity (e.g. because it is closing) you need to inform the PS:
activity.Leave()
Buddies
The activity object created by either sharing the current activity or joining an existing activity is used to establish means of communication between these instances. The joined XOs can be accessed to start communicating with them:
buddies = activity.GetJoinedBuddies()
To get notified of buddies joining or leaving, listen to these signals:
BuddyJoined (o: buddy) BuddyLeft (o: buddy)
Tubes
"Tubes" are the transport medium of choice on the XO, provided by the Telepathy framework. There are "D-Bus Tubes" allowing remote D-Bus calls, and "Stream Tubes" which forward sockets (similar to ssh forwarding). Tubes are collected in a "Channel", and channels are associated with a "room", one per shared activity instance.
First, get the Telepathy connection from the shared activity object:
(tp_service, tp_connection, channels) = activity.GetChannels()
where tp_service and tp_connection is the D-Bus service name and object path for the Telepathy connection. An array of channels pre-created in the activity room is returned, too. There is at least a text chat and a tubes channel at
Service: (tp_service) Object path: (channels[i]) Interface: 'org.freedesktop.Telepathy.Channel'
Use GetChannelType() to tell the channels apart:
if (channel.GetChannelType() == 'org.freedesktop.Telepathy.Channel.Type.Tubes') ... elseif (channel.GetChannelType() == 'org.freedesktop.Telepathy.Channel.Type.Text') ...
Stream Tubes
A stream tube forwards a socket to a remote host (similar to ssh forwarding, but not encrypted). The activity can set up a listening socket by whatever means and then create a tube to forward it:
tube_id = channel.OfferStreamTube( # sa{sv}uvuv -> u 'service-name', # well-known TCP service name conforming to RFC 2782, see [1] {}, # dict of params 2, # socket type (0=Unix, 2=IPv4, 3=IPv6) ('127.0.0.1', 12345), # socket address (depends on type) 0, 0) # access control & params
You can forward Unix, IPv4, or IPv6 sockets. Access control is restricted to localhost by default.
New tubes are announced by a signal:
NewTube ( u: tube_id, u: initiator, u: type, s: service, a{sv}: parameters, u: state )
and you can list all available tubes:
tubes = channel.ListTubes() # -> a(uuusa{sv}u), same as in NewTube signal
To connect to a tube:
address = channel.AcceptStreamTube( # uuuv -> v tube_id, 2, # socket type to return (0=Unix, 2=IPv4, 3=IPv6) 0, 0) # access control & params
which returns an address struct of the specified type, e.g., ('127.0.0.1', 45679). Then just connect a socket to that address and you are ready to share data.
D-Bus Tubes
see olpc:Presence Service D-Bus API and Tubes
Collaboration
Sharing and collaboration are core principles in Sugar. They are implemented using Telepathy.
Note: The Presence Service (which was a wrapper around Telepathy in earlier Sugar releases) has been deprecated. Activities need to use Telepathy directly.
Telepathy
For details, please refer to the Telepathy Specification and the Telepathy Manual. The code below is pseudo code, you need to adapt it for your specific programming language.
The entry point to Telepathy is the AccountManager:
Service: org.freedesktop.Telepathy.AccountManager Interface: org.freedesktop.Telepathy.AccountManager Object Path: /org/freedesktop/Telepathy/AccountManager
It has a list of valid Accounts. Each account has a Connection property, which is valid if its D-Bus path is not '/', and connected if the account's ConnectionStatus property is 0 (1 means connecting, 2 disconnected).
accounts = accountManager.Get('ValidAccounts') for account in accounts: connection = account.Get('Connection') if connection.path != '/' && account.Get('ConnectionStatus') == 0: connections.add(connection)
This gathers all the usable connections, there may be more than one. Since connections can become available later, you need to either track changes using D-Bus signals, or do the above every time a new connection is needed.
Startup
On startup, an activity needs to check all the currently available connections if there is already a "room" for it. The GetActivity method checks for the room given an activity_id:
for connection in connections: room_handle = connection.GetActivity(activityId) # may fail
It is provided by the non-standard ActivityProperties interface:
INTERFACE org.laptop.Telepathy.ActivityProperties METHOD GetActivity(activity_id:s) => (room:u) METHOD GetProperties(room:u) => (properties:a{sv}) METHOD SetProperties(room:u, properties:a{sv}) => () SIGNAL ActivityPropertiesChanged(room:u, properties:a{sv})
If a room is found, the activity needs to join it immediately (see below). Otherwise, a normal "unshared" activity startup should commence.
Joining
If the activity finds itself to be shared already on startup, or the user chooses to enable sharing later, it needs to "join" the room used for communication:
- create text channel
- create tubes channel
- add self to group
to be documented
Leaving
to be documented
Inviting
to be documented
Communicating
This depends on the needs of the activity, but in general it would
- create a tube in tubes channel
- use the tube for communication
to be documented