diff --git a/src/sugar/activity/activity.py b/src/sugar/activity/activity.py index 7c797e45..c534d753 100644 --- a/src/sugar/activity/activity.py +++ b/src/sugar/activity/activity.py @@ -52,15 +52,25 @@ import logging import os import time from hashlib import sha1 -import gconf +from functools import partial +import gconf import gtk import gobject import dbus import dbus.service +from dbus import PROPERTIES_IFACE import cjson +from telepathy.server import DBusProperties +from telepathy.interfaces import CONNECTION, \ + CHANNEL, \ + CHANNEL_TYPE_TEXT, \ + CLIENT, \ + CLIENT_HANDLER +from telepathy.constants import CONNECTION_HANDLE_TYPE_CONTACT from sugar import util +from sugar import dispatch from sugar.presence import presenceservice from sugar.activity.activityservice import ActivityService from sugar.activity.namingalert import NamingAlert @@ -88,6 +98,7 @@ J_DBUS_SERVICE = 'org.laptop.Journal' J_DBUS_PATH = '/org/laptop/Journal' J_DBUS_INTERFACE = 'org.laptop.Journal' +CONN_INTERFACE_ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties' class _ActivitySession(gobject.GObject): @@ -300,10 +311,50 @@ class Activity(Window, gtk.Container): if self._jobject.metadata.has_key('share-scope'): share_scope = self._jobject.metadata['share-scope'] + self.shared_activity = None + self._join_id = None + + if handle.handle_invite: + wait_loop = gobject.MainLoop() + self._client_handler = _ClientHandler( + self.get_bundle_id(), + partial(self.__got_channel_cb, wait_loop)) + # The current API requires that self.shared_activity is set before + # exiting from __init__, so we wait until we have got the shared + # activity. + wait_loop.run() + else: + pservice = presenceservice.get_instance() + mesh_instance = pservice.get_activity(self._activity_id, + warn_if_none=False) + self._set_up_sharing(mesh_instance, share_scope) + + if handle.object_id is None and create_jobject: + logging.debug('Creating a jobject.') + self._jobject = datastore.create() + title = _('%s Activity') % get_bundle_name() + self._jobject.metadata['title'] = title + 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: + client = gconf.client_get_default() + icon_color = client.get_string('/desktop/sugar/user/color') + self._jobject.metadata['icon-color'] = icon_color + + self._jobject.file_path = '' + # Cannot call datastore.write async for creates: + # https://dev.laptop.org/ticket/3071 + datastore.write(self._jobject) + + def _set_up_sharing(self, mesh_instance, share_scope): # handle activity share/join - pservice = presenceservice.get_instance() - mesh_instance = pservice.get_activity(self._activity_id, - warn_if_none=False) logging.debug("*** Act %s, mesh instance %r, scope %s", self._activity_id, mesh_instance, share_scope) if mesh_instance is not None: @@ -331,29 +382,19 @@ class Activity(Window, gtk.Container): else: logging.debug('Unknown share scope %r', share_scope) - if handle.object_id is None and create_jobject: - logging.debug('Creating a jobject.') - self._jobject = datastore.create() - title = _('%s Activity') % get_bundle_name() - self._jobject.metadata['title'] = title - 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: - client = gconf.client_get_default() - icon_color = client.get_string('/desktop/sugar/user/color') - self._jobject.metadata['icon-color'] = icon_color + def __got_channel_cb(self, wait_loop, connection_path, channel_path): + logging.debug('Activity.__got_channel_cb') + connection_name = connection_path.replace('/', '.')[1:] - self._jobject.file_path = '' - # Cannot call datastore.write async for creates: - # https://dev.laptop.org/ticket/3071 - datastore.write(self._jobject) + bus = dbus.SessionBus() + channel = bus.get_object(connection_name, channel_path) + room_handle = channel.Get(CHANNEL, 'TargetHandle') + + pservice = presenceservice.get_instance() + mesh_instance = pservice.get_activity_by_handle(connection_path, + room_handle) + self._set_up_sharing(mesh_instance, SCOPE_PRIVATE) + wait_loop.quit() def get_active(self): return self._active @@ -646,6 +687,7 @@ class Activity(Window, gtk.Container): def __joined_cb(self, activity, success, err): """Callback when join has finished""" + logging.debug('Activity.__joined_cb %r', success) self.shared_activity.disconnect(self._join_id) self._join_id = None if not success: @@ -854,6 +896,50 @@ class Activity(Window, gtk.Container): # DEPRECATED _shared_activity = property(lambda self: self.shared_activity, None) +SUGAR_CLIENT_PATH = '/org/freedesktop/Telepathy/Client/Sugar' + +class _ClientHandler(dbus.service.Object, DBusProperties): + def __init__(self, bundle_id, got_channel_cb): + self._interfaces = set([CLIENT, CLIENT_HANDLER, PROPERTIES_IFACE]) + self._got_channel_cb = got_channel_cb + + bus = dbus.Bus() + name = CLIENT + '.' + bundle_id + bus_name = dbus.service.BusName(name, bus=bus) + + path = '/' + name.replace('.', '/') + dbus.service.Object.__init__(self, bus_name, path) + DBusProperties.__init__(self) + + self._implement_property_get(CLIENT, { + 'Interfaces': lambda: list(self._interfaces), + }) + self._implement_property_get(CLIENT_HANDLER, { + 'HandlerChannelFilter': self.__get_filters_cb, + }) + + def __get_filters_cb(self): + logging.debug('__get_filters_cb') + filters = { + CHANNEL + '.ChannelType' : CHANNEL_TYPE_TEXT, + CHANNEL + '.TargetHandleType': CONNECTION_HANDLE_TYPE_CONTACT, + } + filter_dict = dbus.Dictionary(filters, signature='sv') + logging.debug('__get_filters_cb %r', dbus.Array([filter_dict], signature='a{sv}')) + return dbus.Array([filter_dict], signature='a{sv}') + + @dbus.service.method(dbus_interface=CLIENT_HANDLER, + in_signature='ooa(oa{sv})aota{sv}', out_signature='') + def HandleChannels(self, account, connection, channels, requests_satisfied, + user_action_time, handler_info): + logging.debug('HandleChannels\n\t%r\n\t%r\n\t%r\n\t%r\n\t%r\n\t%r', + account, connection, channels, requests_satisfied, + user_action_time, handler_info) + try: + for channel in channels: + self._got_channel_cb(connection, channel[0]) + except Exception, e: + logging.exception(e) _session = None diff --git a/src/sugar/activity/activityfactory.py b/src/sugar/activity/activityfactory.py index 0dd37938..4789c332 100644 --- a/src/sugar/activity/activityfactory.py +++ b/src/sugar/activity/activityfactory.py @@ -121,7 +121,8 @@ def get_environment(activity): return environ -def get_command(activity, activity_id=None, object_id=None, uri=None): +def get_command(activity, activity_id=None, object_id=None, uri=None, + activity_invite=False): if not activity_id: activity_id = create_activity_id() @@ -133,6 +134,8 @@ def get_command(activity, activity_id=None, object_id=None, uri=None): command.extend(['-o', object_id]) if uri is not None: command.extend(['-u', uri]) + if activity_invite: + command.append('-i') # if the command is in $BUNDLE_ROOT/bin, execute the absolute path so there # is no need to mangle with the shell's PATH @@ -236,8 +239,8 @@ class ActivityCreationHandler(gobject.GObject): environ = get_environment(self._bundle) (log_path, log_file) = open_log_file(self._bundle) command = get_command(self._bundle, self._handle.activity_id, - self._handle.object_id, - self._handle.uri) + self._handle.object_id, self._handle.uri, + self._handle.handle_invite) dev_null = file('/dev/null', 'w') environment_dir = None diff --git a/src/sugar/activity/activityhandle.py b/src/sugar/activity/activityhandle.py index 4ceadb00..3297c106 100644 --- a/src/sugar/activity/activityhandle.py +++ b/src/sugar/activity/activityhandle.py @@ -23,7 +23,8 @@ STABLE. class ActivityHandle(object): """Data structure storing simple activity metadata""" - def __init__(self, activity_id=None, object_id=None, uri=None): + def __init__(self, activity_id=None, object_id=None, uri=None, + handle_invite=False): """Initialise the handle from activity_id activity_id -- unique id for the activity to be @@ -45,14 +46,18 @@ class ActivityHandle(object): activity, rather than a journal object (downloads stored on the file system for example or web pages) + handle_invite -- the activity is being launched for handling an invite + from the network """ self.activity_id = activity_id self.object_id = object_id self.uri = uri + self.handle_invite = handle_invite def get_dict(self): """Retrieve our settings as a dictionary""" - result = {'activity_id': self.activity_id} + result = {'activity_id': self.activity_id, + 'handle_invite': self.handle_invite} if self.object_id: result['object_id'] = self.object_id if self.uri: @@ -65,5 +70,6 @@ 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')) + uri = handle_dict.get('uri'), + handle_invite = handle_dict.get('handle_invite')) return result diff --git a/src/sugar/activity/main.py b/src/sugar/activity/main.py index 0647e810..b66b0e86 100644 --- a/src/sugar/activity/main.py +++ b/src/sugar/activity/main.py @@ -75,6 +75,10 @@ def main(): parser.add_option('-s', '--single-process', dest='single_process', action='store_true', help='start all the instances in the same process') + parser.add_option('-i', '--handle-invite', dest='handle_invite', + action='store_true', + help='the activity is being launched for handling an ' + 'invite from the network') (options, args) = parser.parse_args() logger.start() @@ -121,7 +125,8 @@ def main(): activity_constructor = getattr(module, class_name) activity_handle = activityhandle.ActivityHandle( activity_id=options.activity_id, - object_id=options.object_id, uri=options.uri) + object_id=options.object_id, uri=options.uri, + handle_invite=options.handle_invite) if options.single_process is True: sessionbus = dbus.SessionBus() diff --git a/src/sugar/presence/activity.py b/src/sugar/presence/activity.py index 6882d3b8..32ebbf7c 100644 --- a/src/sugar/presence/activity.py +++ b/src/sugar/presence/activity.py @@ -94,7 +94,7 @@ class Activity(gobject.GObject): self.telepathy_text_chan = None self.telepathy_tubes_chan = None - self._room_handle = room_handle + self.room_handle = room_handle self._join_command = None self._id = properties.get('id', None) self._color = properties.get('color', None) @@ -111,7 +111,7 @@ class Activity(gobject.GObject): self._handle_to_buddy = {} self._get_properties_call = None - if not self._room_handle is None: + if not self.room_handle is None: self._start_tracking_properties() def _start_tracking_properties(self): @@ -122,7 +122,7 @@ class Activity(gobject.GObject): CONN_INTERFACE_ACTIVITY_PROPERTIES, 'GetProperties', 'u', - (self._room_handle,), + (self.room_handle,), reply_handler=self.__got_properties_cb, error_handler=self.__error_handler_cb, utf8_strings=True) @@ -364,12 +364,12 @@ class Activity(gobject.GObject): _logger.debug('%r: joining', self) self._join_command = _JoinCommand(self.telepathy_conn, - self._room_handle) + self.room_handle) self._join_command.connect('finished', self.__joined_cb) self._join_command.run() def share(self, share_activity_cb, share_activity_error_cb): - if not self._room_handle is None: + if not self.room_handle is None: raise ValueError('Already have a room handle') self._share_command = _ShareCommand(self.telepathy_conn, self._id) @@ -384,7 +384,7 @@ class Activity(gobject.GObject): _logger.debug('%r: Share finished %r', self, error) if error is None: self._joined = True - self._room_handle = share_command.room_handle + self.room_handle = share_command.room_handle self.telepathy_text_chan = share_command.text_channel self.telepathy_tubes_chan = share_command.tubes_channel self._publish_properties() @@ -410,7 +410,7 @@ class Activity(gobject.GObject): logging.debug('_publish_properties calling SetProperties %r', properties) self.telepathy_conn.SetProperties( - self._room_handle, + self.room_handle, properties, dbus_interface=CONN_INTERFACE_ACTIVITY_PROPERTIES) @@ -528,7 +528,7 @@ class _JoinCommand(_BaseCommand): _BaseCommand.__init__(self) self._connection = connection - self._room_handle = room_handle + self.room_handle = room_handle self._finished = False self._text_channel_group_flags = None self.text_channel = None @@ -539,13 +539,13 @@ class _JoinCommand(_BaseCommand): raise RuntimeError('This command has already finished') self._connection.RequestChannel(CHANNEL_TYPE_TEXT, - HANDLE_TYPE_ROOM, self._room_handle, True, + HANDLE_TYPE_ROOM, self.room_handle, True, reply_handler=self.__create_text_channel_cb, error_handler=self.__error_handler_cb, dbus_interface=CONNECTION) self._connection.RequestChannel(CHANNEL_TYPE_TUBES, - HANDLE_TYPE_ROOM, self._room_handle, True, + HANDLE_TYPE_ROOM, self.room_handle, True, reply_handler=self.__create_tubes_channel_cb, error_handler=self.__error_handler_cb, dbus_interface=CONNECTION) @@ -636,7 +636,7 @@ class _JoinCommand(_BaseCommand): def __text_channel_members_changed_cb(self, message, added, removed, local_pending, remote_pending, actor, reason): - _logger.debug('__text_channel_members_changed_cb added %r removed %r local_pending %r remote_pending %r', added, removed, local_pending, remote_pending) + _logger.debug('__text_channel_members_changed_cb added %r removed %r local_pending %r remote_pending %r self_handle %r', added, removed, local_pending, remote_pending, self._self_handle) if self._self_handle in added: logging.info('KILL_PS Set the channel properties') self._finished = True @@ -645,9 +645,9 @@ class _JoinCommand(_BaseCommand): return #_logger.debug('Activity %r text channel %u currently has %r', - # self, self._room_handle, self._handle_to_buddy) + # self, self.room_handle, self._handle_to_buddy) _logger.debug('Text channel %u members changed: + %r, - %r, LP %r, ' - 'RP %r, message %r, actor %r, reason %r', self._room_handle, + 'RP %r, message %r, actor %r, reason %r', self.room_handle, added, removed, local_pending, remote_pending, message, actor, reason) # Note: D-Bus calls this with list arguments, but after GetMembers() diff --git a/src/sugar/presence/presenceservice.py b/src/sugar/presence/presenceservice.py index d0e20dcd..02f727c2 100644 --- a/src/sugar/presence/presenceservice.py +++ b/src/sugar/presence/presenceservice.py @@ -280,6 +280,24 @@ class PresenceService(gobject.GObject): return None + def get_activity_by_handle(self, connection_path, room_handle): + if self._activity_cache is not None: + if self._activity_cache.room_handle != room_handle: + raise RuntimeError('Activities can only access their own shared' + 'instance') + return self._activity_cache + else: + connection_manager = get_connection_manager() + account_path = connection_manager.get_account_for_connection(connection_path) + + connection_name = connection_path.replace('/', '.')[1:] + bus = dbus.SessionBus() + connection = bus.get_object(connection_name, connection_path) + activity = Activity(account_path, connection, + room_handle=room_handle) + self._activity_cache = activity + return activity + def get_buddies(self): """Retrieve set of all buddies from service diff --git a/src/sugar/presence/util.py b/src/sugar/presence/util.py index 30346c1a..361ee16d 100644 --- a/src/sugar/presence/util.py +++ b/src/sugar/presence/util.py @@ -50,6 +50,12 @@ class ConnectionManager(object): def get_connections_per_account(self): return self._connections_per_account + def get_account_for_connection(self, connection_path): + for account_path, connection in self._connections_per_account.items(): + if connection.object_path == connection_path: + return account_path + return None + _connection_manager = None def get_connection_manager():