From b53d4046e4ca322bd00e768e171c34f38bbe3d7a Mon Sep 17 00:00:00 2001 From: Ed Page Date: Sat, 6 Feb 2010 23:00:37 -0600 Subject: [PATCH] Added detection of missed calls. As part of this I moved some of the connections plumbing out to separate classes to make --- src/autogv.py | 186 +++++++++++++++++++++++++++++++++++++++++++ src/channel/debug_prompt.py | 2 +- src/connection.py | 128 ++++------------------------- src/util/tp_utils.py | 36 +++++++-- 4 files changed, 232 insertions(+), 120 deletions(-) create mode 100644 src/autogv.py diff --git a/src/autogv.py b/src/autogv.py new file mode 100644 index 0000000..4c93132 --- /dev/null +++ b/src/autogv.py @@ -0,0 +1,186 @@ +import logging + +import gobject +import telepathy + +try: + import conic as _conic + conic = _conic +except (ImportError, OSError): + conic = None + +import constants +import util.coroutines as coroutines +import util.go_utils as gobject_utils +import util.tp_utils as telepathy_utils +import gtk_toolbox + + +_moduleLogger = logging.getLogger("autogv") + + +class NewGVConversations(object): + + def __init__(self, connRef): + self._connRef = connRef + self.__callback = None + + def start(self): + self.__callback = coroutines.func_sink( + coroutines.expand_positional( + self._on_conversations_updated + ) + ) + self._connRef().session.voicemails.updateSignalHandler.register_sink( + self.__callback + ) + self._connRef().session.texts.updateSignalHandler.register_sink( + self.__callback + ) + + def stop(self): + self._connRef().session.voicemails.updateSignalHandler.unregister_sink( + self.__callback + ) + self._connRef().session.texts.updateSignalHandler.unregister_sink( + self.__callback + ) + self.__callback = None + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_conversations_updated(self, conv, conversationIds): + _moduleLogger.debug("Incoming messages from: %r" % (conversationIds, )) + for phoneNumber in conversationIds: + h = self._connRef().get_handle_by_name(telepathy.HANDLE_TYPE_CONTACT, phoneNumber) + # Just let the TextChannel decide whether it should be reported to the user or not + props = self._connRef().generate_props(telepathy.CHANNEL_TYPE_TEXT, h, False) + if self._channel_manager.channel_exists(props): + continue + + # Maemo 4.1's RTComm opens a window for a chat regardless if a + # message is received or not, so we need to do some filtering here + mergedConv = conv.get_conversation(phoneNumber) + unreadConvs = [ + conversation + for conversation in mergedConv.conversations + if not conversation.isRead and not conversation.isArchived + ] + if not unreadConvs: + continue + + chan = self._channel_manager.channel_for_props(props, signal=True) + + +class RefreshVoicemail(object): + + def __init__(self, connRef): + self._connRef = connRef + self._newChannelSignaller = telepathy_utils.NewChannelSignaller(self._on_new_channel) + self._outstandingRequests = [] + + def start(self): + self._newChannelSignaller.start() + + def stop(self): + _moduleLogger.info("Stopping voicemail refresh") + self._newChannelSignaller.stop() + + # I don't want to trust whether the cancel happens within the current + # callback or not which could be the deciding factor between invalid + # iterators or infinite loops + localRequests = [r for r in self._outstandingRequests] + for request in localRequests: + localRequests.cancel() + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_new_channel(self, bus, serviceName, connObjectPath, channelObjectPath, channelType): + if channelType != telepathy.interfaces.CHANNEL_TYPE_STREAMED_MEDIA: + return + + cmName = telepathy_utils.cm_from_path(connObjectPath) + if cmName == constants._telepathy_implementation_name_: + _moduleLogger.debug("Ignoring channels from self to prevent deadlock") + return + + conn = telepathy.client.Connection(serviceName, connObjectPath) + chan = telepathy.client.Channel(serviceName, channelObjectPath) + missDetection = telepathy_utils.WasMissedCall( + bus, conn, chan, self._on_missed_call, self._on_error_for_missed + ) + self._outstandingRequests.append(missDetection) + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_missed_call(self, missDetection): + _moduleLogger.info("Missed a call") + self._connRef().session.voicemailsStateMachine.reset_timers() + self._outstandingRequests.remove(missDetection) + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_error_for_missed(self, missDetection, reason): + _moduleLogger.debug("Error: %r claims %r" % (missDetection, reason)) + self._outstandingRequests.remove(missDetection) + + +class AutoDisconnect(object): + + def __init__(self, connRef): + self._connRef = connRef + if conic is not None: + self.__connection = conic.Connection() + else: + self.__connection = None + + self.__connectionEventId = None + self.__delayedDisconnectEventId = None + + def start(self): + if self.__connection is not None: + self.__connectionEventId = self.__connection.connect("connection-event", self._on_connection_change) + + def stop(self): + self._cancel_delayed_disconnect() + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_connection_change(self, connection, event): + """ + @note Maemo specific + """ + status = event.get_status() + error = event.get_error() + iap_id = event.get_iap_id() + bearer = event.get_bearer_type() + + if status == conic.STATUS_DISCONNECTED: + _moduleLogger.info("Disconnected from network, starting countdown to logoff") + self.__delayedDisconnectEventId = gobject_utils.timeout_add_seconds( + 5, self._on_delayed_disconnect + ) + elif status == conic.STATUS_CONNECTED: + _moduleLogger.info("Connected to network") + self._cancel_delayed_disconnect() + else: + _moduleLogger.info("Other status: %r" % (status, )) + + def _cancel_delayed_disconnect(self): + if self.__delayedDisconnectEventId is None: + return + _moduleLogger.info("Cancelling auto-log off") + gobject.source_reove(self.__delayedDisconnectEventId) + self.__delayedDisconnectEventId = None + + @gtk_toolbox.log_exception(_moduleLogger) + def _on_delayed_disconnect(self): + if not self.session.is_logged_in(): + _moduleLogger.info("Received connection change event when not logged in") + return + try: + self._connRef().disconnect() + except Exception: + _moduleLogger.exception("Error durring disconnect") + self._connRef().StatusChanged( + telepathy.CONNECTION_STATUS_DISCONNECTED, + telepathy.CONNECTION_STATUS_REASON_NETWORK_ERROR + ) + self.__delayedDisconnectEventId = None + return False + diff --git a/src/channel/debug_prompt.py b/src/channel/debug_prompt.py index 7c8df1c..6b7cebc 100644 --- a/src/channel/debug_prompt.py +++ b/src/channel/debug_prompt.py @@ -262,7 +262,7 @@ class DebugPromptChannel(tp.ChannelTypeText, cmd.Cmd): return try: - publishProps = self._conn._generate_props(telepathy.CHANNEL_TYPE_FILE_TRANSFER, self.__otherHandle, False) + publishProps = self._conn.generate_props(telepathy.CHANNEL_TYPE_FILE_TRANSFER, self.__otherHandle, False) self._conn._channel_manager.channel_for_props(publishProps, signal=True) except Exception, e: self._report_new_message(str(e)) diff --git a/src/connection.py b/src/connection.py index d3951bc..41ea912 100644 --- a/src/connection.py +++ b/src/connection.py @@ -2,19 +2,10 @@ import os import weakref import logging -import gobject import telepathy -try: - import conic as _conic - conic = _conic -except (ImportError, OSError): - conic = None - import constants import tp -import util.coroutines as coroutines -import util.go_utils as gobject_utils import util.misc as util_misc import gtk_toolbox @@ -28,6 +19,7 @@ import simple_presence import presence import capabilities +import autogv import channel_manager @@ -131,10 +123,6 @@ class TheOneRingConnection( self.__callbackNumberParameter = encodedCallback self.__channelManager = channel_manager.ChannelManager(self) - if conic is not None: - self.__connection = conic.Connection() - else: - self.__connection = None self.__cachePath = os.sep.join((constants._data_path_, "cache", self.username)) try: os.makedirs(self.__cachePath) @@ -143,10 +131,12 @@ class TheOneRingConnection( raise self.set_self_handle(handle.create_handle(self, 'connection')) + self._plumbing = [ + autogv.NewGVConversations(weakref.ref(self)), + autogv.RefreshVoicemail(weakref.ref(self)), + autogv.AutoDisconnect(weakref.ref(self)), + ] - self.__callback = None - self.__connectionEventId = None - self.__delayedDisconnectEventId = None _moduleLogger.info("Connection to the account %s created" % account) @property @@ -199,17 +189,8 @@ class TheOneRingConnection( try: self.__session.load(self.__cachePath) - self.__callback = coroutines.func_sink( - coroutines.expand_positional( - self._on_conversations_updated - ) - ) - self.session.voicemails.updateSignalHandler.register_sink( - self.__callback - ) - self.session.texts.updateSignalHandler.register_sink( - self.__callback - ) + for plumber in self._plumbing: + plumber.start() self.session.login(*self.__credentials) if not self.__callbackNumberParameter: callback = gvoice.backend.get_sane_callback( @@ -219,10 +200,10 @@ class TheOneRingConnection( self.session.backend.set_callback_number(self.__callbackNumberParameter) subscribeHandle = self.get_handle_by_name(telepathy.HANDLE_TYPE_LIST, "subscribe") - subscribeProps = self._generate_props(telepathy.CHANNEL_TYPE_CONTACT_LIST, subscribeHandle, False) + subscribeProps = self.generate_props(telepathy.CHANNEL_TYPE_CONTACT_LIST, subscribeHandle, False) self.__channelManager.channel_for_props(subscribeProps, signal=True) publishHandle = self.get_handle_by_name(telepathy.HANDLE_TYPE_LIST, "publish") - publishProps = self._generate_props(telepathy.CHANNEL_TYPE_CONTACT_LIST, publishHandle, False) + publishProps = self.generate_props(telepathy.CHANNEL_TYPE_CONTACT_LIST, publishHandle, False) self.__channelManager.channel_for_props(publishProps, signal=True) except gvoice.backend.NetworkError, e: _moduleLogger.exception("Connection Failed") @@ -244,8 +225,6 @@ class TheOneRingConnection( telepathy.CONNECTION_STATUS_CONNECTED, telepathy.CONNECTION_STATUS_REASON_REQUESTED ) - if self.__connection is not None: - self.__connectionEventId = self.__connection.connect("connection-event", self._on_connection_change) @gtk_toolbox.log_exception(_moduleLogger) def Disconnect(self): @@ -253,7 +232,7 @@ class TheOneRingConnection( For org.freedesktop.telepathy.Connection """ try: - self._disconnect() + self.disconnect() except Exception: _moduleLogger.exception("Error durring disconnect") self.StatusChanged( @@ -275,7 +254,7 @@ class TheOneRingConnection( self.check_handle(handleType, handleId) h = self.get_handle_by_id(handleType, handleId) if handleId != 0 else None - props = self._generate_props(type, h, suppressHandler) + props = self.generate_props(type, h, suppressHandler) self._validate_handle(props) chan = self.__channelManager.channel_for_props(props, signal=True) @@ -283,7 +262,7 @@ class TheOneRingConnection( _moduleLogger.info("RequestChannel Object Path (%s): %s" % (type.rsplit(".", 1)[-1], path)) return path - def _generate_props(self, channelType, handle, suppressHandler, initiatorHandle=None): + def generate_props(self, channelType, handle, suppressHandler, initiatorHandle=None): targetHandle = 0 if handle is None else handle.get_id() targetHandleType = telepathy.HANDLE_TYPE_NONE if handle is None else handle.get_type() props = { @@ -298,15 +277,10 @@ class TheOneRingConnection( return props - def _disconnect(self): + def disconnect(self): _moduleLogger.info("Disconnecting") - self.session.voicemails.updateSignalHandler.unregister_sink( - self.__callback - ) - self.session.texts.updateSignalHandler.unregister_sink( - self.__callback - ) - self.__callback = None + for plumber in self._plumbing: + plumber.stop() self.__channelManager.close() self.session.save(self.__cachePath) @@ -314,74 +288,4 @@ class TheOneRingConnection( self.session.close() self.manager.disconnected(self) - - self._cancel_delayed_disconnect() - self.__connection = None _moduleLogger.info("Disconnected") - - @gtk_toolbox.log_exception(_moduleLogger) - def _on_conversations_updated(self, conv, conversationIds): - _moduleLogger.debug("Incoming messages from: %r" % (conversationIds, )) - for phoneNumber in conversationIds: - h = self.get_handle_by_name(telepathy.HANDLE_TYPE_CONTACT, phoneNumber) - # Just let the TextChannel decide whether it should be reported to the user or not - props = self._generate_props(telepathy.CHANNEL_TYPE_TEXT, h, False) - if self.__channelManager.channel_exists(props): - continue - - # Maemo 4.1's RTComm opens a window for a chat regardless if a - # message is received or not, so we need to do some filtering here - mergedConv = conv.get_conversation(phoneNumber) - unreadConvs = [ - conversation - for conversation in mergedConv.conversations - if not conversation.isRead and not conversation.isArchived - ] - if not unreadConvs: - continue - - chan = self.__channelManager.channel_for_props(props, signal=True) - - @gtk_toolbox.log_exception(_moduleLogger) - def _on_connection_change(self, connection, event): - """ - @note Maemo specific - """ - status = event.get_status() - error = event.get_error() - iap_id = event.get_iap_id() - bearer = event.get_bearer_type() - - if status == conic.STATUS_DISCONNECTED: - _moduleLogger.info("Disconnected from network, starting countdown to logoff") - self.__delayedDisconnectEventId = gobject_utils.timeout_add_seconds( - 5, self._on_delayed_disconnect - ) - elif status == conic.STATUS_CONNECTED: - _moduleLogger.info("Connected to network") - self._cancel_delayed_disconnect() - else: - _moduleLogger.info("Other status: %r" % (status, )) - - def _cancel_delayed_disconnect(self): - if self.__delayedDisconnectEventId is None: - return - _moduleLogger.info("Cancelling auto-log off") - gobject.source_reove(self.__delayedDisconnectEventId) - self.__delayedDisconnectEventId = None - - @gtk_toolbox.log_exception(_moduleLogger) - def _on_delayed_disconnect(self): - if not self.session.is_logged_in(): - _moduleLogger.info("Received connection change event when not logged in") - return - try: - self._disconnect() - except Exception: - _moduleLogger.exception("Error durring disconnect") - self.StatusChanged( - telepathy.CONNECTION_STATUS_DISCONNECTED, - telepathy.CONNECTION_STATUS_REASON_NETWORK_ERROR - ) - self.__delayedDisconnectEventId = None - return False diff --git a/src/util/tp_utils.py b/src/util/tp_utils.py index ce8722e..dbf06ec 100644 --- a/src/util/tp_utils.py +++ b/src/util/tp_utils.py @@ -1,7 +1,6 @@ #!/usr/bin/env python import logging -import pprint import gobject import dbus @@ -84,7 +83,6 @@ class WasMissedCall(object): @gtk_toolbox.log_exception(_moduleLogger) def _on_members_changed(self, message, added, removed, lp, rp, actor, reason): - pprint.pprint((message, added, removed, lp, rp, actor, reason)) if added: self._didMembersChange = True self._report_missed_if_ready() @@ -132,8 +130,32 @@ class NewChannelSignaller(object): def _on_new_channel( self, channelObjectPath, channelType, handleType, handle, supressHandler ): - connObjectPath = channelObjectPath.rsplit("/", 1)[0] - serviceName = connObjectPath[1:].replace("/", ".") - conn = telepathy.client.Connection(serviceName, connObjectPath) - chan = telepathy.client.Channel(serviceName, channelObjectPath) - self._on_user_new_channel(self._sessionBus, conn, chan, channelType) + connObjectPath = channel_path_to_conn_path(channelObjectPath) + serviceName = path_to_service_name(channelObjectPath) + self._on_user_new_channel( + self._sessionBus, serviceName, connObjectPath, channelObjectPath, channelType + ) + + +def channel_path_to_conn_path(channelObjectPath): + """ + >>> channel_path_to_conn_path("/org/freedesktop/Telepathy/ConnectionManager/theonering/sip/USERNAME/Channel1") + '/org/freedesktop/Telepathy/ConnectionManager/theonering/sip/USERNAME' + """ + return channelObjectPath.rsplit("/", 1)[0] + + +def path_to_service_name(path): + """ + >>> path_to_service_name("/org/freedesktop/Telepathy/ConnectionManager/theonering/sip/USERNAME/Channel1") + 'org.freedesktop.Telepathy.ConnectionManager.theonering.sip.USERNAME' + """ + return ".".join(path[1:].split("/")[0:7]) + + +def cm_from_path(path): + """ + >>> cm_from_path("/org/freedesktop/Telepathy/ConnectionManager/theonering/sip/USERNAME/Channel1") + 'theonering' + """ + return path[1:].split("/")[4] -- 1.7.9.5