diff --git a/src/sugar/activity/activity.py b/src/sugar/activity/activity.py index 20c9c7dc..0c1a2060 100644 --- a/src/sugar/activity/activity.py +++ b/src/sugar/activity/activity.py @@ -268,7 +268,6 @@ class Activity(Window, gtk.Container): self._active = False self._activity_id = handle.activity_id self.shared_activity = None - self._share_id = None self._join_id = None self._updating_jobject = False self._closing = False @@ -639,6 +638,7 @@ class Activity(Window, gtk.Container): self._jobject.object_id = None def __privacy_changed_cb(self, shared_activity, param_spec): + logging.debug('__privacy_changed_cb %r', shared_activity.props.private) if shared_activity.props.private: self._jobject.metadata['share-scope'] = SCOPE_INVITE_ONLY else: @@ -670,8 +670,6 @@ class Activity(Window, gtk.Container): return self.shared_activity.props.joined def __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) @@ -734,9 +732,9 @@ class Activity(Window, gtk.Container): 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.__share_cb) - self._pservice.share_activity(self, private=private) + pservice = presenceservice.get_instance() + pservice.connect('activity-shared', self.__share_cb) + pservice.share_activity(self, private=private) def _show_keep_failed_dialog(self): alert = Alert() diff --git a/src/sugar/presence/activity.py b/src/sugar/presence/activity.py index 617409f0..2c52eeaf 100644 --- a/src/sugar/presence/activity.py +++ b/src/sugar/presence/activity.py @@ -21,6 +21,7 @@ STABLE. """ import logging +from functools import partial import dbus import gobject @@ -33,6 +34,7 @@ from telepathy.interfaces import CHANNEL, \ from telepathy.constants import HANDLE_TYPE_ROOM CONN_INTERFACE_ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties' +CONN_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo' _logger = logging.getLogger('sugar.presence.activity') @@ -71,7 +73,13 @@ class Activity(gobject.GObject): 'joined': (bool, None, None, False, gobject.PARAM_READABLE), } - def __init__(self, connection, room_handle): + def __init__(self, connection, room_handle=None, properties=None): + if room_handle is None and properties is None: + raise ValueError('Need to pass one of room_handle or properties') + + if properties is None: + properties = {} + gobject.GObject.__init__(self) self.telepathy_conn = connection @@ -79,18 +87,23 @@ class Activity(gobject.GObject): self.telepathy_tubes_chan = None self._room_handle = room_handle - self._id = None - self._color = None - self._name = None - self._type = None - self._tags = None - self._private = True - self._joined = False + self._id = properties.get('id', None) + self._color = properties.get('color', None) + self._name = properties.get('name', None) + self._type = properties.get('type', None) + self._tags = properties.get('tags', None) + self._private = properties.get('private', True) + self._joined = properties.get('joined', False) + self._get_properties_call = None + if not self._room_handle is None: + self._start_tracking_properties() + + def _start_tracking_properties(self): bus = dbus.SessionBus() self._get_properties_call = bus.call_async( - connection.requested_bus_name, - connection.object_path, + self.telepathy_conn.requested_bus_name, + self.telepathy_conn.object_path, CONN_INTERFACE_ACTIVITY_PROPERTIES, 'GetProperties', 'u', @@ -99,17 +112,24 @@ class Activity(gobject.GObject): error_handler=self._error_handler_cb, utf8_strings=True) + # As only one Activity instance is needed per activity process, + # we can afford listening to ActivityPropertiesChanged like this. + self.telepathy_conn.connect_to_signal( + 'ActivityPropertiesChanged', + self.__activity_properties_changed_cb, + dbus_interface=CONN_INTERFACE_ACTIVITY_PROPERTIES) + + def __activity_properties_changed_cb(self, room_handle, properties): + _logger.debug('%r: Activity properties changed to %r', self, properties) + self._update_properties(properties) + def _got_properties_cb(self, properties): - _logger.debug('_got_properties_cb', properties) + _logger.debug('_got_properties_cb %r', properties) self._get_properties_call = None self._update_properties(properties) def _error_handler_cb(self, error): - _logger.debug('_error_handler_cb', error) - - def _properties_changed_cb(self, new_props): - _logger.debug('%r: Activity properties changed to %r', self, new_props) - self._update_properties(new_props) + _logger.debug('_error_handler_cb %r', error) def _update_properties(self, new_props): val = new_props.get('name', self._name) @@ -169,20 +189,22 @@ class Activity(gobject.GObject): """Set a particular property in our property dictionary""" # FIXME: need an asynchronous API to set these properties, # particularly 'private' + if pspec.name == "name": - self._activity.SetProperties({'name': val}) self._name = val elif pspec.name == "color": - self._activity.SetProperties({'color': val}) self._color = val elif pspec.name == "tags": - self._activity.SetProperties({'tags': val}) self._tags = val elif pspec.name == "private": - self._activity.SetProperties({'private': val}) self._private = val + else: + raise ValueError('Unknown property "%s"', pspec.name) + + self._publish_properties() def set_private(self, val, reply_handler, error_handler): + _logger.debug('set_private %r', val) self._activity.SetProperties({'private': bool(val)}, reply_handler=reply_handler, error_handler=error_handler) @@ -263,6 +285,9 @@ class Activity(gobject.GObject): def set_up_tubes(self, reply_handler, error_handler): + if self._room_handle is None: + raise ValueError("Don't have a handle for the room yet") + chans = [] def tubes_ready(): @@ -327,7 +352,71 @@ class Activity(gobject.GObject): _logger.debug('%r: joining', self) self.set_up_tubes(reply_handler=self._join_cb, - error_handler=self._join_error_cb) + error_handler=self._join_error_cb) + + def share(self, share_activity_cb, share_activity_error_cb): + if not self._room_handle is None: + raise ValueError('Already have a room handle') + + """ TODO: Check we don't need this + # We shouldn't have to do this, but Gabble sometimes finds the IRC + # transport and goes "that has chatrooms, that'll do nicely". Work + # around it til Gabble gets better at finding the MUC service. + return '%s@%s' % (activity_id, + self._account['fallback-conference-server']) + """ + + self.telepathy_conn.RequestHandles( + HANDLE_TYPE_ROOM, + [self._id], + reply_handler=partial(self.__got_handles_cb, share_activity_cb, share_activity_error_cb), + error_handler=partial(self.__share_error_cb, share_activity_error_cb), + dbus_interface=CONNECTION) + + def __got_handles_cb(self, share_activity_cb, share_activity_error_cb, handles): + logging.debug('__got_handles_cb %r', handles) + self._room_handle = handles[0] + self._joined = True + + self.set_up_tubes( + partial(self.__tubes_set_up_cb, share_activity_cb, share_activity_error_cb), + share_activity_error_cb) + + def __tubes_set_up_cb(self, share_activity_cb, share_activity_error_cb): + self.telepathy_conn.AddActivity( + self._id, + self._room_handle, + reply_handler=partial(self.__added_activity_cb, share_activity_cb), + error_handler=partial(self.__share_error_cb, share_activity_error_cb), + dbus_interface=CONN_INTERFACE_BUDDY_INFO) + + def __added_activity_cb(self, share_activity_cb): + self._publish_properties() + self._start_tracking_properties() + share_activity_cb(self) + + def _publish_properties(self): + properties = {} + + if self._color is not None: + properties['color'] = self._color + if self._name is not None: + properties['name'] = self._name + if self._type is not None: + properties['type'] = self._type + if self._tags is not None: + properties['tags'] = self._tags + properties['private'] = self._private + + logging.debug('_publish_properties calling SetProperties') + self.telepathy_conn.SetProperties( + self._room_handle, + properties, + dbus_interface=CONN_INTERFACE_ACTIVITY_PROPERTIES) + + def __share_error_cb(self, share_activity_error_cb, error): + logging.debug('%r: Share failed because: %s', self, error) + share_activity_error_cb(self, error) # GetChannels() wrapper diff --git a/src/sugar/presence/presenceservice.py b/src/sugar/presence/presenceservice.py index 0b949249..b36f4698 100644 --- a/src/sugar/presence/presenceservice.py +++ b/src/sugar/presence/presenceservice.py @@ -68,6 +68,7 @@ class PresenceService(gobject.GObject): """ gobject.GObject.__init__(self) + self._activity_cache = None self._buddy_cache = {} def _new_object(self, object_path): @@ -249,12 +250,20 @@ class PresenceService(gobject.GObject): returns single Activity object or None if the activity is not found using GetActivityById on the service """ - for connection in get_connection_manager().connections: - try: - room_handle = connection.GetActivity(activity_id) - return Activity(connection, room_handle) - except: - pass + if self._activity_cache is not None: + if self._activity_cache.props.id != activity_id: + raise RuntimeError('Activities can only access their own shared' + 'instance') + return self._activity_cache + else: + for connection in get_connection_manager().connections: + try: + room_handle = connection.GetActivity(activity_id) + activity = Activity(connection, room_handle) + self._activity_cache = activity + return activity + except: + pass return None @@ -351,64 +360,56 @@ class PresenceService(gobject.GObject): """Retrieves the laptop Buddy object.""" return Owner() - def _share_activity_cb(self, activity, op): + def __share_activity_cb(self, activity): """Finish sharing the activity """ - # FIXME find a better way to shutup pylint - psact = self._new_object(op) - psact._joined = True - _logger.debug('%r: Just shared, setting up tubes', activity) - psact.set_up_tubes(reply_handler=lambda: - self.emit("activity-shared", True, psact, None), - error_handler=lambda e: - self._share_activity_error_cb(activity, e)) + self.emit("activity-shared", True, activity, None) - def _share_activity_error_cb(self, activity, err): - """Notify with GObject event of unsuccessful sharing of activity""" - _logger.debug('Error sharing activity %s: %s', activity.get_id(), - err) - self.emit("activity-shared", False, None, err) + def __share_activity_error_cb(self, activity, error): + """Notify with GObject event of unsuccessful sharing of activity + """ + self.emit("activity-shared", False, activity, error) def share_activity(self, activity, properties=None, private=True): - """Ask presence service to ask the activity to share itself publicly. - - Uses the AdvertiseActivity method on the service to ask for the - sharing of the given activity. Arranges to emit activity-shared - event with: - - (success, Activity, err) - - on success/failure. - - returns None - """ - actid = activity.get_id() - if properties is None: properties = {} - # Ensure the activity is not already shared/joined - for obj in self._objcache.values(): - if not isinstance(object, Activity): - continue - if obj.props.id == actid or obj.props.joined: - raise RuntimeError("Activity %s is already shared." % - actid) + if 'id' not in properties: + properties['id'] = activity.get_id() - atype = activity.get_bundle_id() - name = activity.props.title - properties['private'] = bool(private) - self._ps.ShareActivity(actid, atype, name, properties, - reply_handler=lambda op: \ - self._share_activity_cb(activity, op), - error_handler=lambda e: \ - self._share_activity_error_cb(activity, e)) + if 'type' not in properties: + properties['type'] = activity.get_bundle_id() + + if 'name' not in properties: + properties['name'] = activity.metadata.get('title', None) + + if 'color' not in properties: + properties['color'] = activity.metadata.get('icon-color', None) + + properties['private'] = private + + if self._activity_cache is not None: + raise ValueError('Activity %s is already tracked', activity.get_id()) + + connection = get_connection_manager().get_preferred_connection() + shared_activity = Activity(connection, properties=properties) + self._activity_cache = shared_activity + + """ + if shared_activity.props.joined: + raise RuntimeError('Activity %s is already shared.' % + activity.get_id()) + """ + + shared_activity.share(self.__share_activity_cb, + self.__share_activity_error_cb) def get_preferred_connection(self): """Gets the preferred telepathy connection object that an activity should use when talking directly to telepathy - returns the bus name and the object path of the Telepathy connection""" + returns the bus name and the object path of the Telepathy connection + """ connection = get_connection_manager().get_preferred_connection() if connection is None: return None