diff options
Diffstat (limited to 'lib/sugar/activity')
-rw-r--r-- | lib/sugar/activity/Makefile.am | 9 | ||||
-rw-r--r-- | lib/sugar/activity/__init__.py | 58 | ||||
-rw-r--r-- | lib/sugar/activity/__init__py | 0 | ||||
-rw-r--r-- | lib/sugar/activity/activity.py | 633 | ||||
-rw-r--r-- | lib/sugar/activity/activityfactory.py | 256 | ||||
-rw-r--r-- | lib/sugar/activity/activityhandle.py | 68 | ||||
-rw-r--r-- | lib/sugar/activity/activityservice.py | 66 | ||||
-rw-r--r-- | lib/sugar/activity/bundlebuilder.py | 403 | ||||
-rw-r--r-- | lib/sugar/activity/registry.py | 156 |
9 files changed, 1649 insertions, 0 deletions
diff --git a/lib/sugar/activity/Makefile.am b/lib/sugar/activity/Makefile.am new file mode 100644 index 0000000..9dfc8de --- /dev/null +++ b/lib/sugar/activity/Makefile.am @@ -0,0 +1,9 @@ +sugardir = $(pythondir)/sugar/activity +sugar_PYTHON = \ + __init__.py \ + activity.py \ + activityfactory.py \ + activityhandle.py \ + activityservice.py \ + bundlebuilder.py \ + registry.py diff --git a/lib/sugar/activity/__init__.py b/lib/sugar/activity/__init__.py new file mode 100644 index 0000000..8a984ad --- /dev/null +++ b/lib/sugar/activity/__init__.py @@ -0,0 +1,58 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +"""Activity implementation code for Sugar-based activities + +Each activity within the OLPC environment must provide two +dbus services. The first, patterned after the + + sugar.activity.activityfactory.ActivityFactory + +class is responsible for providing a "create" method which +takes a small dictionary with values corresponding to a + + sugar.activity.activityhandle.ActivityHandle + +describing an individual instance of the activity. + +Each activity so registered is described by a + + sugar.activity.bundle.Bundle + +instance, which parses a specially formatted activity.info +file (stored in the activity directory's ./activity +subdirectory). The + + sugar.activity.bundlebuilder + +module provides facilities for the standard setup.py module +which produces and registers bundles from activity source +directories. + +Once instantiated by the ActivityFactory's create method, +each activity must provide an introspection API patterned +after the + + sugar.activity.activityservice.ActivityService + +class. This class allows for querying the ID of the root +window, requesting sharing across the network, and basic +"what type of application are you" queries. +""" +from sugar.activity.registry import ActivityRegistry +from sugar.activity.registry import get_registry +from sugar.activity.registry import ActivityInfo diff --git a/lib/sugar/activity/__init__py b/lib/sugar/activity/__init__py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/sugar/activity/__init__py diff --git a/lib/sugar/activity/activity.py b/lib/sugar/activity/activity.py new file mode 100644 index 0000000..c581c15 --- /dev/null +++ b/lib/sugar/activity/activity.py @@ -0,0 +1,633 @@ +"""Base class for Python-coded activities + +This is currently the only reference for what an +activity must do to participate in the Sugar desktop. +""" +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +from gettext import gettext as _ +import logging +import os +import time +import tempfile +from hashlib import sha1 + +import gtk, gobject +import dbus +import json + +from sugar import util +from sugar.presence import presenceservice +from sugar.activity.activityservice import ActivityService +from sugar.graphics import style +from sugar.graphics.window import Window +from sugar.graphics.toolbox import Toolbox +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.toolcombobox import ToolComboBox +from sugar.datastore import datastore +from sugar import wm +from sugar import profile +from sugar import _sugarbaseext + +SCOPE_PRIVATE = "private" +SCOPE_INVITE_ONLY = "invite" # shouldn't be shown in UI, it's implicit when you invite somebody +SCOPE_NEIGHBORHOOD = "public" + +class ActivityToolbar(gtk.Toolbar): + def __init__(self, activity): + gtk.Toolbar.__init__(self) + + self._activity = activity + self._updating_share = False + + activity.connect('shared', self._activity_shared_cb) + activity.connect('joined', self._activity_shared_cb) + activity.connect('notify::max_participants', + self._max_participants_changed_cb) + + if activity.metadata: + self.title = gtk.Entry() + self.title.set_size_request(int(gtk.gdk.screen_width() / 6), -1) + self.title.set_text(activity.metadata['title']) + self.title.connect('changed', self._title_changed_cb) + self._add_widget(self.title) + + activity.metadata.connect('updated', self._jobject_updated_cb) + + separator = gtk.SeparatorToolItem() + separator.props.draw = False + separator.set_expand(True); + self.insert(separator, -1) + separator.show() + + self.share = ToolComboBox(label_text=_('Share with:')) + self.share.combo.connect('changed', self._share_changed_cb) + self.share.combo.append_item(SCOPE_PRIVATE, _('Private'), + 'zoom-home-mini') + self.share.combo.append_item(SCOPE_NEIGHBORHOOD, _('My Neighborhood'), + 'zoom-neighborhood-mini') + self.insert(self.share, -1) + self.share.show() + + self._update_share() + + self.keep = ToolButton('document-save') + self.keep.set_tooltip(_('Keep')) + self.keep.connect('clicked', self._keep_clicked_cb) + self.insert(self.keep, -1) + self.keep.show() + + self.stop = ToolButton('activity-stop') + self.stop.set_tooltip(_('Stop')) + self.stop.connect('clicked', self._stop_clicked_cb) + self.insert(self.stop, -1) + self.stop.show() + + self._update_title_sid = None + + def _update_share(self): + self._updating_share = True + + if self._activity.props.max_participants == 1: + self.share.hide() + + if self._activity.get_shared(): + self.share.set_sensitive(False) + self.share.combo.set_active(1) + else: + self.share.set_sensitive(True) + self.share.combo.set_active(0) + + self._updating_share = False + + def _share_changed_cb(self, combo): + if self._updating_share: + return + + model = self.share.combo.get_model() + it = self.share.combo.get_active_iter() + (scope, ) = model.get(it, 0) + if scope == SCOPE_NEIGHBORHOOD: + self._activity.share() + + def _keep_clicked_cb(self, button): + self._activity.copy() + + def _stop_clicked_cb(self, button): + self._activity.close() + + def _jobject_updated_cb(self, jobject): + self.title.set_text(jobject['title']) + + def _title_changed_cb(self, entry): + if not self._update_title_sid: + self._update_title_sid = gobject.timeout_add(1000, self._update_title_cb) + + def _update_title_cb(self): + title = self.title.get_text() + + self._activity.metadata['title'] = title + self._activity.metadata['title_set_by_user'] = '1' + self._activity.save() + + shared_activity = self._activity._shared_activity + if shared_activity: + shared_activity.props.name = title + + self._update_title_sid = None + return False + + def _add_widget(self, widget, expand=False): + tool_item = gtk.ToolItem() + tool_item.set_expand(expand) + + tool_item.add(widget) + widget.show() + + self.insert(tool_item, -1) + tool_item.show() + + def _activity_shared_cb(self, activity): + self._update_share() + + def _max_participants_changed_cb(self, activity, pspec): + self._update_share() + +class EditToolbar(gtk.Toolbar): + def __init__(self): + gtk.Toolbar.__init__(self) + + self.undo = ToolButton('edit-undo') + self.undo.set_tooltip(_('Undo')) + self.insert(self.undo, -1) + self.undo.show() + + self.redo = ToolButton('edit-redo') + self.redo.set_tooltip(_('Redo')) + self.insert(self.redo, -1) + self.redo.show() + + self.separator = gtk.SeparatorToolItem() + self.separator.set_draw(True) + self.insert(self.separator, -1) + self.separator.show() + + self.copy = ToolButton('edit-copy') + self.copy.set_tooltip(_('Copy')) + self.insert(self.copy, -1) + self.copy.show() + + self.paste = ToolButton('edit-paste') + self.paste.set_tooltip(_('Paste')) + self.insert(self.paste, -1) + self.paste.show() + +class ActivityToolbox(Toolbox): + def __init__(self, activity): + Toolbox.__init__(self) + + self._activity_toolbar = ActivityToolbar(activity) + self.add_toolbar('Activity', self._activity_toolbar) + self._activity_toolbar.show() + + def get_activity_toolbar(self): + return self._activity_toolbar + +class Activity(Window, gtk.Container): + """Base Activity class that all other Activities derive from.""" + __gtype_name__ = 'SugarActivity' + + __gsignals__ = { + 'shared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'joined': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])) + } + + __gproperties__ = { + 'active' : (bool, None, None, False, + gobject.PARAM_READWRITE), + 'max-participants': (int, None, None, 0, 1000, 0, + gobject.PARAM_READWRITE) + } + + def __init__(self, handle, create_jobject=True): + """Initialise the Activity + + handle -- sugar.activity.activityhandle.ActivityHandle + instance providing the activity id and access to the + presence service which *may* provide sharing for this + application + + create_jobject -- boolean + define if it should create a journal object if we are + not resuming + + Side effects: + + Sets the gdk screen DPI setting (resolution) to the + Sugar screen resolution. + + Connects our "destroy" message to our _destroy_cb + method. + + Creates a base gtk.Window within this window. + + Creates an ActivityService (self._bus) servicing + this application. + """ + Window.__init__(self) + + # process titles will only show 15 characters + # but they get truncated anyway so if more characters + # are supported in the future we will get a better view + # of the processes + proc_title = "%s <%s>" % (get_bundle_name(), handle.activity_id) + util.set_proc_title(proc_title) + + self.connect('realize', self._realize_cb) + self.connect('delete-event', self.__delete_event_cb) + + self._active = False + self._activity_id = handle.activity_id + self._pservice = presenceservice.get_instance() + self._shared_activity = None + self._share_id = None + self._join_id = None + self._preview = None + self._updating_jobject = False + self._closing = False + self._deleting = False + self._max_participants = 0 + self._invites_queue = [] + + self._bus = ActivityService(self) + self._owns_file = False + + share_scope = SCOPE_PRIVATE + + if handle.object_id: + self._jobject = datastore.get(handle.object_id) + # TODO: Don't create so many objects until we have versioning + # support in the datastore + #self._jobject.object_id = '' + #del self._jobject.metadata['ctime'] + del self._jobject.metadata['mtime'] + + self.set_title(self._jobject.metadata['title']) + + if self._jobject.metadata.has_key('share-scope'): + share_scope = self._jobject.metadata['share-scope'] + + elif create_jobject: + logging.debug('Creating a jobject.') + self._jobject = datastore.create() + self._jobject.metadata['title'] = _('%s Activity') % get_bundle_name() + self.set_title(self._jobject.metadata['title']) + self._jobject.metadata['title_set_by_user'] = '0' + self._jobject.metadata['activity'] = self.get_bundle_id() + self._jobject.metadata['activity_id'] = self.get_id() + self._jobject.metadata['keep'] = '0' + self._jobject.metadata['preview'] = '' + self._jobject.metadata['share-scope'] = SCOPE_PRIVATE + + if self._shared_activity is not None: + icon_color = self._shared_activity.props.color + else: + icon_color = profile.get_color().to_string() + + self._jobject.metadata['icon-color'] = icon_color + + self._jobject.file_path = '' + datastore.write(self._jobject, + reply_handler=self._internal_jobject_create_cb, + error_handler=self._internal_jobject_error_cb) + else: + self._jobject = None + + # handle activity share/join + mesh_instance = self._pservice.get_activity(self._activity_id) + logging.debug("*** Act %s, mesh instance %r, scope %s" % (self._activity_id, mesh_instance, share_scope)) + if mesh_instance: + # There's already an instance on the mesh, join it + logging.debug("*** Act %s joining existing mesh instance" % self._activity_id) + self._shared_activity = mesh_instance + self._shared_activity.connect('notify::private', + self._privacy_changed_cb) + self._join_id = self._shared_activity.connect("joined", self._internal_joined_cb) + if not self._shared_activity.props.joined: + self._shared_activity.join() + else: + self._internal_joined_cb(self._shared_activity, True, None) + elif share_scope != SCOPE_PRIVATE: + logging.debug("*** Act %s no existing mesh instance, but used to be shared, will share" % self._activity_id) + # no existing mesh instance, but activity used to be shared, so + # restart the share + if share_scope == SCOPE_INVITE_ONLY: + self.share(private=True) + elif share_scope == SCOPE_NEIGHBORHOOD: + self.share(private=False) + else: + logging.debug("Unknown share scope %r" % share_scope) + + def do_set_property(self, pspec, value): + if pspec.name == 'active': + if self._active != value: + self._active = value + if not self._active and self._jobject: + self.save() + elif pspec.name == 'max-participants': + self._max_participants = value + + def do_get_property(self, pspec): + if pspec.name == 'active': + return self._active + elif pspec.name == 'max-participants': + return self._max_participants + + def get_id(self): + return self._activity_id + + def get_bundle_id(self): + return _sugarbaseext.get_prgname() + + def set_canvas(self, canvas): + Window.set_canvas(self, canvas) + canvas.connect('map', self._canvas_map_cb) + + def _canvas_map_cb(self, canvas): + if self._jobject and self._jobject.file_path: + self.read_file(self._jobject.file_path) + + def _internal_jobject_create_cb(self): + pass + + def _internal_jobject_error_cb(self, err): + logging.debug("Error creating activity datastore object: %s" % err) + + def get_activity_root(self): + """ + Return the appropriate location in the fs where to store activity related + data that doesn't pertain to the current execution of the activity and + thus cannot go into the DataStore. + """ + if os.environ.has_key('SUGAR_ACTIVITY_ROOT') and \ + os.environ['SUGAR_ACTIVITY_ROOT']: + return os.environ['SUGAR_ACTIVITY_ROOT'] + else: + return '/' + + def read_file(self, file_path): + """ + Subclasses implement this method if they support resuming objects from + the journal. 'file_path' is the file to read from. + """ + raise NotImplementedError + + def write_file(self, file_path): + """ + Subclasses implement this method if they support saving data to objects + in the journal. 'file_path' is the file to write to. + """ + raise NotImplementedError + + def _internal_save_cb(self): + logging.debug('Activity._internal_save_cb') + self._updating_jobject = False + if self._closing: + self._cleanup_jobject() + self.destroy() + + def _internal_save_error_cb(self, err): + logging.debug('Activity._internal_save_error_cb') + self._updating_jobject = False + if self._closing: + self._cleanup_jobject() + self.destroy() + logging.debug("Error saving activity object to datastore: %s" % err) + + def _cleanup_jobject(self): + if self._jobject: + if self._owns_file and os.path.isfile(self._jobject.file_path): + logging.debug('_cleanup_jobject: removing %r' % self._jobject.file_path) + os.remove(self._jobject.file_path) + self._owns_file = False + self._jobject.destroy() + self._jobject = None + + def _get_preview(self): + preview_pixbuf = self.get_canvas_screenshot() + if preview_pixbuf is None: + return None + preview_pixbuf = preview_pixbuf.scale_simple(style.zoom(300), + style.zoom(225), + gtk.gdk.INTERP_BILINEAR) + + # TODO: Find a way of taking a png out of the pixbuf without saving to a temp file. + # Impementing gtk.gdk.Pixbuf.save_to_buffer in pygtk would solve this. + fd, file_path = tempfile.mkstemp('.png') + del fd + preview_pixbuf.save(file_path, 'png') + f = open(file_path) + try: + preview_data = f.read() + finally: + f.close() + os.remove(file_path) + + return preview_data + + def _get_buddies(self): + if self._shared_activity is not None: + buddies = {} + for buddy in self._shared_activity.get_joined_buddies(): + if not buddy.props.owner: + buddy_id = sha1(buddy.props.key).hexdigest() + buddies[buddy_id] = [buddy.props.nick, buddy.props.color] + return buddies + else: + return {} + + def save(self): + """Request that the activity is saved to the Journal.""" + + logging.debug('Activity.save: %r' % self._jobject.object_id) + + if self._updating_jobject: + logging.info('Activity.save: still processing a previous request.') + return + + buddies_dict = self._get_buddies() + if buddies_dict: + self.metadata['buddies_id'] = json.write(buddies_dict.keys()) + self.metadata['buddies'] = json.write(self._get_buddies()) + + if self._preview is None: + self.metadata['preview'] = '' + else: + self.metadata['preview'] = dbus.ByteArray(self._preview) + + try: + if self._jobject.file_path: + self.write_file(self._jobject.file_path) + else: + file_path = os.path.join(tempfile.gettempdir(), '%i' % time.time()) + self.write_file(file_path) + self._owns_file = True + self._jobject.file_path = file_path + except NotImplementedError: + pass + + # Cannot call datastore.write async for creates: https://dev.laptop.org/ticket/3071 + if self._jobject.object_id is None: + datastore.write(self._jobject, transfer_ownership=True) + else: + self._updating_jobject = True + datastore.write(self._jobject, + transfer_ownership=True, + reply_handler=self._internal_save_cb, + error_handler=self._internal_save_error_cb) + + def copy(self): + logging.debug('Activity.copy: %r' % self._jobject.object_id) + self._preview = self._get_preview() + self.save() + self._jobject.object_id = None + + def _privacy_changed_cb(self, shared_activity, param_spec): + if shared_activity.props.private: + self._jobject.metadata['share-scope'] = SCOPE_INVITE_ONLY + else: + self._jobject.metadata['share-scope'] = SCOPE_NEIGHBORHOOD + + def _internal_joined_cb(self, activity, success, err): + """Callback when join has finished""" + self._shared_activity.disconnect(self._join_id) + self._join_id = None + if not success: + logging.debug("Failed to join activity: %s" % err) + return + + self.present() + self.emit('joined') + self._privacy_changed_cb(self._shared_activity, None) + + def get_shared(self): + """Returns TRUE if the activity is shared on the mesh.""" + if not self._shared_activity: + return False + return self._shared_activity.props.joined + + def _internal_share_cb(self, ps, success, activity, err): + self._pservice.disconnect(self._share_id) + self._share_id = None + if not success: + logging.debug('Share of activity %s failed: %s.' % (self._activity_id, err)) + return + + logging.debug('Share of activity %s successful.' % self._activity_id) + + activity.props.name = self._jobject.metadata['title'] + + self._shared_activity = activity + self._shared_activity.connect('notify::private', + self._privacy_changed_cb) + self.emit('shared') + self._privacy_changed_cb(self._shared_activity, None) + + self._send_invites() + + def _invite_response_cb(self, error): + if error: + logging.error('Invite failed: %s' % error) + + def _send_invites(self): + while self._invites_queue: + buddy_key = self._invites_queue.pop() + buddy = self._pservice.get_buddy(buddy_key) + if buddy: + self._shared_activity.invite(buddy, '', self._invite_response_cb) + else: + logging.error('Cannot invite %s, no such buddy.' % buddy_key) + + def invite(self, buddy_key): + self._invites_queue.append(buddy_key) + + if (self._shared_activity is None + or not self._shared_activity.props.joined): + self.share(True) + else: + self._send_invites() + + def share(self, private=False): + """Request that the activity be shared on the network. + + private -- bool: True to share by invitation only, + False to advertise as shared to everyone. + + Once the activity is shared, its privacy can be changed by setting + its 'private' property. + """ + # FIXME: Make private=True to turn on the by-invitation-only scope + if self._shared_activity and self._shared_activity.props.joined: + raise RuntimeError("Activity %s already shared." % + self._activity_id) + verb = private and 'private' or 'public' + logging.debug('Requesting %s share of activity %s.' % + (verb, self._activity_id)) + self._share_id = self._pservice.connect("activity-shared", + self._internal_share_cb) + self._pservice.share_activity(self, private=private) + + def close(self): + self._preview = self._get_preview() + + self.save() + + if self._shared_activity: + self._shared_activity.leave() + + if self._updating_jobject: + self._closing = True + else: + self.destroy() + + def _realize_cb(self, window): + wm.set_bundle_id(window.window, self.get_bundle_id()) + wm.set_activity_id(window.window, self._activity_id) + + def __delete_event_cb(self, widget, event): + self.close() + return True + + def get_metadata(self): + if self._jobject: + return self._jobject.metadata + else: + return None + + metadata = property(get_metadata, None) + +def get_bundle_name(): + """Return the bundle name for the current process' bundle + """ + return _sugarbaseext.get_application_name() + +def get_bundle_path(): + """Return the bundle path for the current process' bundle + """ + return os.environ['SUGAR_BUNDLE_PATH'] + diff --git a/lib/sugar/activity/activityfactory.py b/lib/sugar/activity/activityfactory.py new file mode 100644 index 0000000..ae08ada --- /dev/null +++ b/lib/sugar/activity/activityfactory.py @@ -0,0 +1,256 @@ +"""Shell side object which manages request to start activity""" +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import logging +import subprocess + +import dbus +import gobject +import gtk + +from sugar.presence import presenceservice +from sugar.activity.activityhandle import ActivityHandle +from sugar.activity import registry +from sugar.datastore import datastore +from sugar import util +from sugar import env + +import os + +# #3903 - this constant can be removed and assumed to be 1 when dbus-python +# 0.82.3 is the only version used +if dbus.version >= (0, 82, 3): + DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND = 1 +else: + DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND = 1000 + +_SHELL_SERVICE = "org.laptop.Shell" +_SHELL_PATH = "/org/laptop/Shell" +_SHELL_IFACE = "org.laptop.Shell" + +_DS_SERVICE = "org.laptop.sugar.DataStore" +_DS_INTERFACE = "org.laptop.sugar.DataStore" +_DS_PATH = "/org/laptop/sugar/DataStore" + +_ACTIVITY_FACTORY_INTERFACE = "org.laptop.ActivityFactory" + +_RAINBOW_SERVICE_NAME = "org.laptop.security.Rainbow" +_RAINBOW_ACTIVITY_FACTORY_PATH = "/" +_RAINBOW_ACTIVITY_FACTORY_INTERFACE = "org.laptop.security.Rainbow" + +def create_activity_id(): + """Generate a new, unique ID for this activity""" + pservice = presenceservice.get_instance() + + # create a new unique activity ID + i = 0 + act_id = None + while i < 10: + act_id = util.unique_id() + i += 1 + + # check through network activities + found = False + activities = pservice.get_activities() + for act in activities: + if act_id == act.props.id: + found = True + break + if not found: + return act_id + raise RuntimeError("Cannot generate unique activity id.") + +def get_environment(activity): + environ = os.environ.copy() + + bin_path = os.path.join(activity.path, 'bin') + environ['SUGAR_BUNDLE_PATH'] = activity.path + environ['PATH'] = bin_path + ':' + environ['PATH'] + + return environ + +def get_command(activity, activity_id=None, object_id=None, uri=None): + if not activity_id: + activity_id = create_activity_id() + + command = activity.command + command += ' -b %s' % activity.bundle_id + command += ' -a %s' % activity_id + + if object_id is not None: + command += ' -o %s' % object_id + if uri is not None: + command += ' -u %s' % uri + + return command + +def open_log_file(activity, activity_id): + for i in range(1, 100): + path = env.get_logs_path('%s-%s.log' % (activity.bundle_id, i)) + if not os.path.exists(path): + return open(path, 'w') + +class ActivityCreationHandler(gobject.GObject): + """Sugar-side activity creation interface + + This object uses a dbus method on the ActivityFactory + service to create the new activity. It generates + GObject events in response to the success/failure of + activity startup using callbacks to the service's + create call. + """ + + def __init__(self, service_name, handle): + """Initialise the handler + + service_name -- the service name of the bundle factory + activity_handle -- stores the values which are to + be passed to the service to uniquely identify + the activity to be created and the sharing + service that may or may not be connected with it + + sugar.activity.activityhandle.ActivityHandle instance + + calls the "create" method on the service for this + particular activity type and registers the + _reply_handler and _error_handler methods on that + call's results. + + The specific service which creates new instances of this + particular type of activity is created during the activity + registration process in shell bundle registry which creates + service definition files for each registered bundle type. + + If the file '/etc/olpc-security' exists, then activity launching + will be delegated to the prototype 'Rainbow' security service. + """ + gobject.GObject.__init__(self) + self._service_name = service_name + self._handle = handle + + bus = dbus.SessionBus() + + bus_object = bus.get_object(_SHELL_SERVICE, _SHELL_PATH) + self._shell = dbus.Interface(bus_object, _SHELL_IFACE) + + if handle.activity_id is not None and \ + handle.object_id is None: + datastore = dbus.Interface( + bus.get_object(_DS_SERVICE, _DS_PATH), _DS_INTERFACE) + datastore.find({ 'activity_id': self._handle.activity_id }, [], + reply_handler=self._find_object_reply_handler, + error_handler=self._find_object_error_handler) + else: + self._launch_activity() + + def _launch_activity(self): + if self._handle.activity_id != None: + self._shell.ActivateActivity(self._handle.activity_id, + reply_handler=self._activate_reply_handler, + error_handler=self._activate_error_handler) + else: + self._create_activity() + + def _create_activity(self): + if self._handle.activity_id is None: + self._handle.activity_id = create_activity_id() + + self._shell.NotifyLaunch( + self._service_name, self._handle.activity_id, + reply_handler=self._no_reply_handler, + error_handler=self._notify_launch_error_handler) + + if not os.path.exists('/etc/olpc-security'): + activity_registry = registry.get_registry() + activity = activity_registry.get_activity(self._service_name) + if activity: + env = get_environment(activity) + log_file = open_log_file(activity, self._handle.activity_id) + command = get_command(activity, self._handle.activity_id, + self._handle.object_id, + self._handle.uri) + process = subprocess.Popen(command, env=env, shell=True, + cwd=activity.path, stdout=log_file, + stderr=log_file) + else: + system_bus = dbus.SystemBus() + factory = system_bus.get_object(_RAINBOW_SERVICE_NAME, + _RAINBOW_ACTIVITY_FACTORY_PATH) + stdio_paths = {'stdout': '/logs/stdout', 'stderr': '/logs/stderr'} + factory.CreateActivity( + self._service_name, + self._handle.get_dict(), + stdio_paths, + timeout=120 * DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND, + reply_handler=self._create_reply_handler, + error_handler=self._create_error_handler, + dbus_interface=_RAINBOW_ACTIVITY_FACTORY_INTERFACE) + + def _no_reply_handler(self, *args): + pass + + def _notify_launch_failure_error_handler(self, err): + logging.error('Notify launch failure failed %s' % err) + + def _notify_launch_error_handler(self, err): + logging.debug('Notify launch failed %s' % err) + + def _activate_reply_handler(self, activated): + if not activated: + self._create_activity() + + def _activate_error_handler(self, err): + logging.error("Activity activation request failed %s" % err) + + def _create_reply_handler(self, xid): + logging.debug("Activity created %s (%s)." % + (self._handle.activity_id, self._service_name)) + + def _create_error_handler(self, err): + logging.error("Couldn't create activity %s (%s): %s" % + (self._handle.activity_id, self._service_name, err)) + self._shell.NotifyLaunchFailure( + self._handle.activity_id, reply_handler=self._no_reply_handler, + error_handler=self._notify_launch_failure_error_handler) + + def _find_object_reply_handler(self, jobjects, count): + if count > 0: + if count > 1: + logging.debug("Multiple objects has the same activity_id.") + self._handle.object_id = jobjects[0]['uid'] + self._create_activity() + + def _find_object_error_handler(self, err): + logging.error("Datastore find failed %s" % err) + self._create_activity() + +def create(service_name, activity_handle=None): + """Create a new activity from its name.""" + if not activity_handle: + activity_handle = ActivityHandle() + return ActivityCreationHandler(service_name, activity_handle) + +def create_with_uri(service_name, uri): + """Create a new activity and pass the uri as handle.""" + activity_handle = ActivityHandle(uri=uri) + return ActivityCreationHandler(service_name, activity_handle) + +def create_with_object_id(service_name, object_id): + """Create a new activity and pass the object id as handle.""" + activity_handle = ActivityHandle(object_id=object_id) + return ActivityCreationHandler(service_name, activity_handle) diff --git a/lib/sugar/activity/activityhandle.py b/lib/sugar/activity/activityhandle.py new file mode 100644 index 0000000..f91651e --- /dev/null +++ b/lib/sugar/activity/activityhandle.py @@ -0,0 +1,68 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +from sugar.presence import presenceservice + +class ActivityHandle(object): + """Data structure storing simple activity metadata""" + def __init__( + self, activity_id=None, object_id=None, uri=None + ): + """Initialise the handle from activity_id + + activity_id -- unique id for the activity to be + created + object_id -- identity of the journal object + associated with the activity. It was used by + the journal prototype implementation, might + change when we do the real one. + + When you resume an activity from the journal + the object_id will be passed in. It's optional + since new activities does not have an + associated object (yet). + + XXX Not clear how this relates to the activity + id yet, i.e. not sure we really need both. TBF + uri -- 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) + """ + self.activity_id = activity_id + self.object_id = object_id + self.uri = uri + + def get_dict(self): + """Retrieve our settings as a dictionary""" + result = { 'activity_id' : self.activity_id } + if self.object_id: + result['object_id'] = self.object_id + if self.uri: + result['uri'] = self.uri + + return result + +def create_from_dict(handle_dict): + """Create a handle from a dictionary of parameters""" + result = ActivityHandle( + handle_dict['activity_id'], + object_id = handle_dict.get('object_id'), + uri = handle_dict.get('uri'), + ) + return result diff --git a/lib/sugar/activity/activityservice.py b/lib/sugar/activity/activityservice.py new file mode 100644 index 0000000..b2f7e15 --- /dev/null +++ b/lib/sugar/activity/activityservice.py @@ -0,0 +1,66 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import logging + +import dbus +import dbus.service + +_ACTIVITY_SERVICE_NAME = "org.laptop.Activity" +_ACTIVITY_SERVICE_PATH = "/org/laptop/Activity" +_ACTIVITY_INTERFACE = "org.laptop.Activity" + +class ActivityService(dbus.service.Object): + """Base dbus service object that each Activity uses to export dbus methods. + + The dbus service is separate from the actual Activity object so that we can + tightly control what stuff passes through the dbus python bindings.""" + + def __init__(self, activity): + """Initialise the service for the given activity + + activity -- sugar.activity.activity.Activity instance + + Creates dbus services that use the instance's activity_id + as discriminants among all active services + of this type. That is, the services are all available + as names/paths derived from the instance's activity_id. + + The various methods exposed on dbus are just forwarded + to the client Activity object's equally-named methods. + """ + activity.realize() + + activity_id = activity.get_id() + service_name = _ACTIVITY_SERVICE_NAME + activity_id + object_path = _ACTIVITY_SERVICE_PATH + "/" + activity_id + + bus = dbus.SessionBus() + bus_name = dbus.service.BusName(service_name, bus=bus) + dbus.service.Object.__init__(self, bus_name, object_path) + + self._activity = activity + + @dbus.service.method(_ACTIVITY_INTERFACE) + def SetActive(self, active): + logging.debug('ActivityService.set_active: %s.' % active) + self._activity.props.active = active + + @dbus.service.method(_ACTIVITY_INTERFACE) + def Invite(self, buddy_key): + self._activity.invite(buddy_key) + diff --git a/lib/sugar/activity/bundlebuilder.py b/lib/sugar/activity/bundlebuilder.py new file mode 100644 index 0000000..c2e3278 --- /dev/null +++ b/lib/sugar/activity/bundlebuilder.py @@ -0,0 +1,403 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import sys +import os +import zipfile +import shutil +import subprocess +import re +import gettext + +from sugar import env +from sugar.bundle.activitybundle import ActivityBundle + +class _SvnFileList(list): + def __init__(self): + f = os.popen('svn list -R') + for line in f.readlines(): + filename = line.strip() + if os.path.isfile(filename): + self.append(filename) + f.close() + +class _GitFileList(list): + def __init__(self): + f = os.popen('git-ls-files') + for line in f.readlines(): + filename = line.strip() + if not filename.startswith('.'): + self.append(filename) + f.close() + +class _DefaultFileList(list): + def __init__(self): + for name in os.listdir('activity'): + if name.endswith('.svg'): + self.append(os.path.join('activity', name)) + + self.append('activity/activity.info') + + if os.path.isfile(_get_source_path('NEWS')): + self.append('NEWS') + +class _ManifestFileList(_DefaultFileList): + def __init__(self, manifest): + _DefaultFileList.__init__(self) + self.append(manifest) + + f = open(manifest,'r') + for line in f.readlines(): + stripped_line = line.strip() + if stripped_line and not stripped_line in self: + self.append(stripped_line) + f.close() + +def _extract_bundle(source_file, dest_dir): + if not os.path.exists(dest_dir): + os.mkdir(dest_dir) + + zf = zipfile.ZipFile(source_file) + + for i, name in enumerate(zf.namelist()): + path = os.path.join(dest_dir, name) + + if not os.path.exists(os.path.dirname(path)): + os.makedirs(os.path.dirname(path)) + + outfile = open(path, 'wb') + outfile.write(zf.read(name)) + outfile.flush() + outfile.close() + +def _get_source_path(path=None): + if path: + return os.path.join(os.getcwd(), path) + else: + return os.getcwd() + +def _get_bundle_dir(): + bundle_name = os.path.basename(_get_source_path()) + return bundle_name + '.activity' + +def _get_install_dir(prefix): + return os.path.join(prefix, 'share/activities') + +def _get_package_name(bundle_name): + bundle = ActivityBundle(_get_source_path()) + zipname = '%s-%d.xo' % (bundle_name, bundle.get_activity_version()) + return zipname + +def _delete_backups(arg, dirname, names): + for name in names: + if name.endswith('~') or name.endswith('pyc'): + os.remove(os.path.join(dirname, name)) + +def _get_bundle_id(): + bundle = ActivityBundle(_get_source_path()) + return bundle.get_bundle_id() + +def cmd_help(): + print 'Usage: \n\ +setup.py dev - setup for development \n\ +setup.py dist - create a bundle package \n\ +setup.py install [dirname] - install the bundle \n\ +setup.py uninstall [dirname] - uninstall the bundle \n\ +setup.py genpot - generate the gettext pot file \n\ +setup.py genl10n - generate localization files \n\ +setup.py clean - clean the directory \n\ +setup.py release - do a new release of the bundle \n\ +setup.py help - print this message \n\ +' + +def cmd_dev(): + bundle_path = env.get_user_activities_path() + if not os.path.isdir(bundle_path): + os.mkdir(bundle_path) + bundle_path = os.path.join(bundle_path, _get_bundle_dir()) + try: + os.symlink(_get_source_path(), bundle_path) + except OSError: + if os.path.islink(bundle_path): + print 'ERROR - The bundle has been already setup for development.' + else: + print 'ERROR - A bundle with the same name is already installed.' + +def _get_file_list(manifest): + if os.path.isfile(manifest): + return _ManifestFileList(manifest) + elif os.path.isdir('.git'): + return _GitFileList() + elif os.path.isdir('.svn'): + return _SvnFileList() + else: + return _DefaultFileList() + +def _get_po_list(manifest): + file_list = {} + + po_regex = re.compile("po/(.*)\.po$") + for file_name in _get_file_list(manifest): + match = po_regex.match(file_name) + if match: + file_list[match.group(1)] = file_name + + return file_list + +def _get_l10n_list(manifest): + l10n_list = [] + + for lang in _get_po_list(manifest).keys(): + filename = _get_bundle_id() + '.mo' + l10n_list.append(os.path.join('locale', lang, 'LC_MESSAGES', filename)) + l10n_list.append(os.path.join('locale', lang, 'activity.linfo')) + + return l10n_list + +def _get_activity_name(): + info_path = os.path.join(_get_source_path(), 'activity', 'activity.info') + f = open(info_path,'r') + info = f.read() + f.close() + match = re.search('^name\s*=\s*(.*)$', info, flags = re.MULTILINE) + return match.group(1) + +def cmd_dist(bundle_name, manifest): + cmd_genl10n(bundle_name, manifest) + file_list = _get_file_list(manifest) + + zipname = _get_package_name(bundle_name) + bundle_zip = zipfile.ZipFile(zipname, 'w', zipfile.ZIP_DEFLATED) + base_dir = bundle_name + '.activity' + + for filename in file_list: + bundle_zip.write(filename, os.path.join(base_dir, filename)) + + for filename in _get_l10n_list(manifest): + bundle_zip.write(filename, os.path.join(base_dir, filename)) + + bundle_zip.close() + +def cmd_install(bundle_name, manifest, prefix): + cmd_dist(bundle_name, manifest) + cmd_uninstall(prefix) + + _extract_bundle(_get_package_name(bundle_name), + _get_install_dir(prefix)) + +def cmd_uninstall(prefix): + path = os.path.join(_get_install_dir(prefix), _get_bundle_dir()) + if os.path.isdir(path): + shutil.rmtree(path) + +def cmd_genpot(bundle_name, manifest): + po_path = os.path.join(_get_source_path(), 'po') + if not os.path.isdir(po_path): + os.mkdir(po_path) + + python_files = [] + file_list = _get_file_list(manifest) + for file_name in file_list: + if file_name.endswith('.py'): + python_files.append(file_name) + + # First write out a stub .pot file containing just the translated + # activity name, then have xgettext merge the rest of the + # translations into that. (We can't just append the activity name + # to the end of the .pot file afterwards, because that might + # create a duplicate msgid.) + pot_file = os.path.join('po', '%s.pot' % bundle_name) + activity_name = _get_activity_name() + escaped_name = re.sub('([\\\\"])', '\\\\\\1', activity_name) + f = open(pot_file, 'w') + f.write('#: activity/activity.info:2\n') + f.write('msgid "%s"\n' % escaped_name) + f.write('msgstr ""\n') + f.close() + + args = [ 'xgettext', '--join-existing', '--language=Python', + '--keyword=_', '--add-comments=TRANS:', '--output=%s' % pot_file ] + + args += python_files + retcode = subprocess.call(args) + if retcode: + print 'ERROR - xgettext failed with return code %i.' % retcode + + for file_name in _get_po_list(manifest).values(): + args = [ 'msgmerge', '-U', file_name, pot_file ] + retcode = subprocess.call(args) + if retcode: + print 'ERROR - msgmerge failed with return code %i.' % retcode + +def cmd_genl10n(bundle_name, manifest): + source_path = _get_source_path() + activity_name = _get_activity_name() + + po_list = _get_po_list(manifest) + for lang in po_list.keys(): + file_name = po_list[lang] + + localedir = os.path.join(source_path, 'locale', lang) + mo_path = os.path.join(localedir, 'LC_MESSAGES') + if not os.path.isdir(mo_path): + os.makedirs(mo_path) + + mo_file = os.path.join(mo_path, "%s.mo" % _get_bundle_id()) + args = ["msgfmt", "--output-file=%s" % mo_file, file_name] + retcode = subprocess.call(args) + if retcode: + print 'ERROR - msgfmt failed with return code %i.' % retcode + + cat = gettext.GNUTranslations(open(mo_file, 'r')) + translated_name = cat.gettext(activity_name) + linfo_file = os.path.join(localedir, 'activity.linfo') + f = open(linfo_file, 'w') + f.write('[Activity]\nname = %s\n' % translated_name) + f.close() + +def cmd_release(bundle_name, manifest): + if not os.path.isdir('.git'): + print 'ERROR - this command works only for git repositories' + + retcode = subprocess.call(['git', 'pull']) + if retcode: + print 'ERROR - cannot pull from git' + + print 'Bumping activity version...' + + info_path = os.path.join(_get_source_path(), 'activity', 'activity.info') + f = open(info_path,'r') + info = f.read() + f.close() + + exp = re.compile('activity_version\s?=\s?([0-9]*)') + match = re.search(exp, info) + version = int(match.group(1)) + 1 + info = re.sub(exp, 'activity_version = %d' % version, info) + + f = open(info_path, 'w') + f.write(info) + f.close() + + news_path = os.path.join(_get_source_path(), 'NEWS') + + if os.environ.has_key('SUGAR_NEWS'): + print 'Update NEWS.sugar...' + + sugar_news_path = os.environ['SUGAR_NEWS'] + if os.path.isfile(sugar_news_path): + f = open(sugar_news_path,'r') + sugar_news = f.read() + f.close() + else: + sugar_news = '' + + sugar_news += '%s - %d\n\n' % (bundle_name, version) + + f = open(news_path,'r') + for line in f.readlines(): + if len(line.strip()) > 0: + sugar_news += line + else: + break + f.close() + + sugar_news += '\n' + + f = open(sugar_news_path, 'w') + f.write(sugar_news) + f.close() + + print 'Update NEWS...' + + f = open(news_path,'r') + news = f.read() + f.close() + + news = '%d\n\n' % version + news + + f = open(news_path, 'w') + f.write(news) + f.close() + + print 'Committing to git...' + + changelog = 'Release version %d.' % version + retcode = subprocess.call(['git', 'commit', '-a', '-m % s' % changelog]) + if retcode: + print 'ERROR - cannot commit to git' + + retcode = subprocess.call(['git', 'push']) + if retcode: + print 'ERROR - cannot push to git' + + print 'Creating the bundle...' + cmd_dist(bundle_name, manifest) + + if os.environ.has_key('ACTIVITIES_REPOSITORY'): + print 'Uploading to the activities repository...' + repo = os.environ['ACTIVITIES_REPOSITORY'] + + server, path = repo.split(':') + retcode = subprocess.call(['ssh', server, 'rm', + '%s/%s*' % (path, bundle_name)]) + if retcode: + print 'ERROR - cannot remove old bundles from the repository.' + + bundle_path = os.path.join(_get_source_path(), + _get_package_name(bundle_name)) + retcode = subprocess.call(['scp', bundle_path, repo]) + if retcode: + print 'ERROR - cannot upload the bundle to the repository.' + + print 'Done.' + +def cmd_clean(): + os.path.walk('.', _delete_backups, None) + +def sanity_check(): + if not os.path.isfile(_get_source_path('NEWS')): + print 'WARNING: NEWS file is missing.' + +def start(bundle_name, manifest='MANIFEST'): + sanity_check() + + if len(sys.argv) < 2: + cmd_help() + elif sys.argv[1] == 'build': + pass + elif sys.argv[1] == 'dev': + cmd_dev() + elif sys.argv[1] == 'dist': + cmd_dist(bundle_name, manifest) + elif sys.argv[1] == 'install' and len(sys.argv) == 3: + cmd_install(bundle_name, manifest, sys.argv[2]) + elif sys.argv[1] == 'uninstall' and len(sys.argv) == 3: + cmd_uninstall(sys.argv[2]) + elif sys.argv[1] == 'genpot': + cmd_genpot(bundle_name, manifest) + elif sys.argv[1] == 'genl10n': + cmd_genl10n(bundle_name, manifest) + elif sys.argv[1] == 'clean': + cmd_clean() + elif sys.argv[1] == 'release': + cmd_release(bundle_name, manifest) + else: + cmd_help() + +if __name__ == '__main__': + start() diff --git a/lib/sugar/activity/registry.py b/lib/sugar/activity/registry.py new file mode 100644 index 0000000..a279aa9 --- /dev/null +++ b/lib/sugar/activity/registry.py @@ -0,0 +1,156 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2007 One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import logging + +import dbus +import gobject + +_ACTIVITY_REGISTRY_SERVICE_NAME = 'org.laptop.ActivityRegistry' +_ACTIVITY_REGISTRY_IFACE = 'org.laptop.ActivityRegistry' +_ACTIVITY_REGISTRY_PATH = '/org/laptop/ActivityRegistry' + +def _activity_info_from_dict(info_dict): + if not info_dict: + return None + return ActivityInfo(info_dict['name'], info_dict['icon'], + info_dict['bundle_id'], info_dict['path'], + info_dict['show_launcher'], info_dict['command']) + +class ActivityInfo(object): + def __init__(self, name, icon, bundle_id, + path, show_launcher, command): + self.name = name + self.icon = icon + self.bundle_id = bundle_id + self.path = path + self.command = command + self.show_launcher = show_launcher + +class ActivityRegistry(gobject.GObject): + __gsignals__ = { + 'activity-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'activity-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + def __init__(self): + gobject.GObject.__init__(self) + + bus = dbus.SessionBus() + + # NOTE: We need to follow_name_owner_changes here + # because we can not connect to a signal unless + # we follow the changes or we start the service + # before we connect. Starting the service here + # causes a major bottleneck during startup + bus_object = bus.get_object(_ACTIVITY_REGISTRY_SERVICE_NAME, + _ACTIVITY_REGISTRY_PATH, + follow_name_owner_changes = True) + self._registry = dbus.Interface(bus_object, _ACTIVITY_REGISTRY_IFACE) + self._registry.connect_to_signal('ActivityAdded', self._activity_added_cb) + self._registry.connect_to_signal('ActivityRemoved', self._activity_removed_cb) + + # Two caches fo saving some travel across dbus. + self._service_name_to_activity_info = {} + self._mime_type_to_activities = {} + + def _convert_info_list(self, info_list): + result = [] + + for info_dict in info_list: + result.append(_activity_info_from_dict(info_dict)) + + return result + + def get_activities(self): + info_list = self._registry.GetActivities() + return self._convert_info_list(info_list) + + def _get_activities_cb(self, reply_handler, info_list): + result = [] + i = 0 + for info_dict in info_list: + result.append(_activity_info_from_dict(info_dict)) + + reply_handler(result) + + def _get_activities_error_cb(self, error_handler, e): + if error_handler: + error_handler(e) + else: + logging.error('Error getting activities async: %s' % str(e)) + + def get_activities_async(self, reply_handler=None, error_handler=None): + if not reply_handler: + logging.error('Function get_activities_async called without a reply handler. Can not run.') + return + + self._registry.GetActivities( + reply_handler=lambda info_list:self._get_activities_cb(reply_handler, info_list), + error_handler=lambda e:self._get_activities_error_cb(error_handler, e)) + + def get_activity(self, service_name): + if self._service_name_to_activity_info.has_key(service_name): + return self._service_name_to_activity_info[service_name] + + info_dict = self._registry.GetActivity(service_name) + activity_info = _activity_info_from_dict(info_dict) + + self._service_name_to_activity_info[service_name] = activity_info + return activity_info + + def find_activity(self, name): + info_list = self._registry.FindActivity(name) + return self._convert_info_list(info_list) + + def get_activities_for_type(self, mime_type): + if self._mime_type_to_activities.has_key(mime_type): + return self._mime_type_to_activities[mime_type] + + info_list = self._registry.GetActivitiesForType(mime_type) + activities = self._convert_info_list(info_list) + + self._mime_type_to_activities[mime_type] = activities + return activities + + def add_bundle(self, bundle_path): + return self._registry.AddBundle(bundle_path) + + def _activity_added_cb(self, info_dict): + logging.debug('ActivityRegistry._activity_added_cb: flushing caches') + self._service_name_to_activity_info.clear() + self._mime_type_to_activities.clear() + self.emit('activity-added', _activity_info_from_dict(info_dict)) + + def remove_bundle(self, bundle_path): + return self._registry.RemoveBundle(bundle_path) + + def _activity_removed_cb(self, info_dict): + logging.debug('ActivityRegistry._activity_removed_cb: flushing caches') + self._service_name_to_activity_info.clear() + self._mime_type_to_activities.clear() + self.emit('activity-removed', _activity_info_from_dict(info_dict)) + +_registry = None + +def get_registry(): + global _registry + if not _registry: + _registry = ActivityRegistry() + return _registry |