# Copyright (c) 2010 AG Projects. See LICENSE for details.
#

__all__ = ['ClientConference', 'ConferenceDialog', 'AudioSessionModel', 'AudioSessionListView', 'ChatSessionModel', 'ChatSessionListView', 'SessionManager']

import bisect
import cPickle as pickle
import os
import re
import string

from collections import defaultdict, deque
from datetime import datetime, timedelta
from functools import partial
from itertools import chain, izip, repeat
from operator import attrgetter

from PyQt4 import uic
from PyQt4.QtCore import Qt, QAbstractListModel, QByteArray, QEasingCurve, QEvent, QMimeData, QModelIndex, QObject, QPointF, QPropertyAnimation, QRect, QSize, QTimer, pyqtSignal
from PyQt4.QtGui  import QApplication, QBrush, QColor, QDrag, QIcon, QLabel, QLinearGradient, QListView, QMenu, QPainter, QPalette, QPen, QPixmap, QPolygonF, QShortcut
from PyQt4.QtGui  import QStyle, QStyledItemDelegate, QStyleOption

from application.notification import IObserver, NotificationCenter, NotificationData, ObserverWeakrefProxy
from application.python import Null, limit
from application.python.types import MarkerType, Singleton
from application.python.weakref import weakobjectmap
from zope.interface import implements

from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.application import SIPApplication
from sipsimple.audio import AudioConference, WavePlayer
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.core import SIPCoreError, SIPURI, ToHeader
from sipsimple.lookup import DNSLookup
from sipsimple.session import Session
from sipsimple.streams import MediaStreamRegistry

from blink.resources import Resources
from blink.util import call_later, run_in_gui_thread
from blink.widgets.buttons import LeftSegment, MiddleSegment, RightSegment
from blink.widgets.labels import Status
from blink.widgets.color import ColorHelperMixin
from blink.widgets.util import ContextMenuActions, QtDynamicProperty


class RTPStreamInfo(object):
    dataset_size = 5000
    average_interval = 10

    def __init__(self):
        self.ice_status = None
        self.encryption = None
        self.codec_name = None
        self.sample_rate = None
        self.local_address = None
        self.remote_address = None
        self.local_rtp_candidate = None
        self.remote_rtp_candidate = None
        self.latency = deque(maxlen=self.dataset_size)
        self.packet_loss = deque(maxlen=self.dataset_size)
        self.jitter = deque(maxlen=self.dataset_size)
        self.incoming_traffic = deque(maxlen=self.dataset_size)
        self.outgoing_traffic = deque(maxlen=self.dataset_size)
        self.bytes_sent = 0
        self.bytes_received = 0
        self._total_packets = 0
        self._total_packets_lost = 0
        self._total_packets_discarded = 0
        self._average_loss_queue = deque(maxlen=self.average_interval)

    @property
    def codec(self):
        return '%s %dkHz' % (self.codec_name, self.sample_rate/1000) if self.codec_name else None

    def _update(self, stream):
        if stream is not None:
            self.codec_name = stream.codec
            self.sample_rate = stream.sample_rate
            self.local_address = stream.local_rtp_address
            self.remote_address = stream.remote_rtp_address
            self.encryption = 'SRTP' if stream.srtp_active else None
            if stream.session and not stream.session.account.nat_traversal.use_ice:
                self.ice_status = 'disabled'

    def _update_statistics(self, statistics):
        if statistics:
            packets = statistics['rx']['packets'] - self._total_packets
            packets_lost = statistics['rx']['packets_lost'] - self._total_packets_lost
            packets_discarded = statistics['rx']['packets_discarded'] - self._total_packets_discarded
            self._average_loss_queue.append(100.0 * packets_lost / (packets + packets_lost - packets_discarded) if packets_lost else 0)
            self._total_packets += packets
            self._total_packets_lost += packets_lost
            self._total_packets_discarded += packets_discarded
            self.latency.append(statistics['rtt']['last'] / 1000 / 2)
            self.jitter.append(statistics['rx']['jitter']['last'] / 1000)
            self.incoming_traffic.append(float(statistics['rx']['bytes'] - self.bytes_received)) # bytes/second
            self.outgoing_traffic.append(float(statistics['tx']['bytes'] - self.bytes_sent))     # bytes/second
            self.bytes_sent = statistics['tx']['bytes']
            self.bytes_received = statistics['rx']['bytes']
            self.packet_loss.append(sum(self._average_loss_queue) / self.average_interval)

    def _reset(self):
        self.__init__()


class MSRPStreamInfo(object):
    def __init__(self):
        self.local_address = None
        self.remote_address = None
        self.transport = None
        self.full_local_path = []
        self.full_remote_path = []

    def _update(self, stream):
        if stream is not None:
            if stream.msrp:
                self.transport = stream.transport
                self.local_address = stream.msrp.local_uri.host
                self.remote_address = stream.msrp.next_host().host
                self.full_local_path = stream.msrp.full_local_path
                self.full_remote_path = stream.msrp.full_remote_path
            elif stream.session:
                self.transport = stream.transport
                self.local_address = stream.local_uri.host

    def _reset(self):
        self.__init__()


class StreamsInfo(object):
    __slots__ = 'audio', 'video', 'chat'

    def __init__(self):
        self.audio = RTPStreamInfo()
        self.video = RTPStreamInfo()
        self.chat = MSRPStreamInfo()

    def __getitem__(self, key):
        try:
            return getattr(self, key)
        except AttributeError:
            raise KeyError(key)

    def _update(self, streams):
        self.audio._update(streams.get('audio'))
        self.video._update(streams.get('video'))
        self.chat._update(streams.get('chat'))


class SessionInfo(object):
    def __init__(self):
        self.duration = timedelta(0)
        self.local_address = None
        self.remote_address = None
        self.transport = None
        self.remote_user_agent = None
        self.streams = StreamsInfo()

    def _update(self, session):
        sip_session = session.sip_session
        if sip_session is not None:
            self.transport = sip_session.transport
            self.local_address = session.account.contact[self.transport].host
            self.remote_address = sip_session.peer_address # consider reading from sip_session.route if peer_address is None (route can also be None) -Dan
            self.remote_user_agent = sip_session.remote_user_agent
        self.streams._update(session.streams)


class StreamDescription(object):
    def __init__(self, type, **kw):
        self.type = type
        self.attributes = kw

    def create_stream(self):
        registry = MediaStreamRegistry()
        cls = registry.get(self.type)
        return cls(**self.attributes)

    def __repr__(self):
        if self.attributes:
            return "%s(%r, %s)" % (self.__class__.__name__, self.type, ', '.join("%s=%r" % pair for pair in self.attributes.iteritems()))
        else:
            return "%s(%r)" % (self.__class__.__name__, self.type)


class StreamSet(object):
    def __init__(self, streams):
        self._stream_map = {stream.type: stream for stream in streams}

    def __getitem__(self, key):
        return self._stream_map[key]

    def __contains__(self, key):
        return key in self._stream_map or key in self._stream_map.values()

    def __iter__(self):
        return iter(sorted(self._stream_map.values(), key=attrgetter('type')))

    def __reversed__(self):
        return iter(sorted(self._stream_map.values(), key=attrgetter('type'), reverse=True))

    __hash__ = None

    def __len__(self):
        return len(self._stream_map)

    @property
    def types(self):
        return set(self._stream_map)

    def get(self, key, default=None):
        return self._stream_map.get(key, default)


class StreamContainer(object):
    def __init__(self, session, stream_map):
        self._session = session
        self._stream_map = stream_map

    def __getitem__(self, key):
        return self._stream_map[key]

    def __contains__(self, key):
        return key in self._stream_map or key in self._stream_map.values()

    def __iter__(self):
        return iter(sorted(self._stream_map.values(), key=attrgetter('type')))

    def __reversed__(self):
        return iter(sorted(self._stream_map.values(), key=attrgetter('type'), reverse=True))

    __hash__ = None

    def __len__(self):
        return len(self._stream_map)

    @property
    def types(self):
        return set(self._stream_map)

    def get(self, key, default=None):
        return self._stream_map.get(key, default)

    def add(self, stream):
        notification_center = NotificationCenter()
        old_stream = self._stream_map.get(stream.type, None)
        if old_stream is not None:
            notification_center.remove_observer(self._session, sender=old_stream)
        stream.blink_session = self._session
        self._stream_map[stream.type] = stream
        notification_center.add_observer(self._session, sender=stream)

    def remove(self, stream):
        # is it a good choice to silently ignore removing a stream that is not in the container? -Dan
        if stream in self:
            self._stream_map.pop(stream.type)
            notification_center = NotificationCenter()
            notification_center.remove_observer(self._session, sender=stream)

    def extend(self, iterable):
        for item in iterable:
            self.add(item)

    def clear(self):
        for stream in self._stream_map.values():
            self.remove(stream)


class defaultweakobjectmap(weakobjectmap):
    def __init__(self, factory, *args, **kw):
        self.default_factory = factory
        super(defaultweakobjectmap, self).__init__(*args, **kw)
    def __missing__(self, key):
        return self.setdefault(key.object, self.default_factory())


class StreamListDescriptor(object):
    def __init__(self):
        self.values = defaultweakobjectmap(dict)

    def __get__(self, obj, objtype):
        if obj is None:
            return self
        return StreamContainer(obj, self.values[obj])

    def __set__(self, obj, value):
        raise AttributeError("Attribute cannot be set")

    def __delete__(self, obj):
        raise AttributeError("Attribute cannot be deleted")


class BlinkSessionState(str):
    state    = property(lambda self: str(self.partition('/')[0]) or None)
    substate = property(lambda self: str(self.partition('/')[2]) or None)

    def __eq__(self, other):
        if isinstance(other, BlinkSessionState):
            return self.state == other.state and self.substate == other.substate
        elif isinstance(other, basestring):
            state    = other.partition('/')[0] or None
            substate = other.partition('/')[2] or None
            if state == '*':
                return substate in ('*', None) or self.substate == substate
            elif substate == '*':
                return self.state == state
            else:
                return self.state == state and self.substate == substate
        return NotImplemented

    def __ne__(self, other):
        return not (self == other)


class SessionItemsDescriptor(object):
    class SessionItems(object):
        def __getattr__(self, name):
            return None

    def __init__(self):
        self.values = defaultweakobjectmap(self.SessionItems)

    def __get__(self, obj, objtype):
        return self.values[obj] if obj is not None else None

    def __set__(self, obj, value):
        raise AttributeError("Attribute cannot be set")

    def __delete__(self, obj):
        raise AttributeError("Attribute cannot be deleted")


class BlinkSession(QObject):
    implements(IObserver)

    # check what should be a signal and what a notification -Dan
    clientConferenceChanged = pyqtSignal(object, object) # old_conference, new_conference

    streams = StreamListDescriptor()
    items = SessionItemsDescriptor()

    def __init__(self):
        super(BlinkSession, self).__init__()
        self._initialize()

    def _initialize(self, reinitialize=False):
        if not reinitialize:
            self.state = None

            self.account = None
            self.contact = None
            self.contact_uri = None
            self.uri = None
            self.server_conference = ServerConference(self)

            self._delete_when_done = False
            self._delete_requested = False

            self.timer = QTimer()
            self.timer.setInterval(1000)
            self.timer.timeout.connect(self._SH_TimerFired)
        else:
            self.timer.stop()

        self.direction = None
        self.__dict__['active'] = False

        self.lookup = None
        self.client_conference = None
        self.sip_session = None
        self.stream_descriptions = None
        self.streams.clear()

        self.local_hold = False
        self.remote_hold = False
        self.recording = False

        self.info = SessionInfo()

        self._sibling = None

    def _get_state(self):
        return self.__dict__['state']

    def _set_state(self, value):
        if value is not None and not isinstance(value, BlinkSessionState):
            value = BlinkSessionState(value)
        old_state = self.__dict__.get('state', None)
        new_state = self.__dict__['state'] = value
        if new_state != old_state:
            NotificationCenter().post_notification('BlinkSessionDidChangeState', sender=self, data=NotificationData(old_state=old_state, new_state=new_state))

    state = property(_get_state, _set_state)
    del _get_state, _set_state

    def _get_contact(self):
        return self.__dict__['contact']

    def _set_contact(self, value):
        old_contact = self.__dict__.get('contact', None)
        new_contact = self.__dict__['contact'] = value
        if new_contact != old_contact:
            notification_center = NotificationCenter()
            if old_contact is not None:
                notification_center.remove_observer(self, sender=old_contact)
            if new_contact is not None:
                notification_center.add_observer(self, sender=new_contact)

    contact = property(_get_contact, _set_contact)
    del _get_contact, _set_contact

    def _get_sip_session(self):
        return self.__dict__['sip_session']

    def _set_sip_session(self, value):
        old_session = self.__dict__.get('sip_session', None)
        new_session = self.__dict__['sip_session'] = value
        if new_session != old_session:
            notification_center = NotificationCenter()
            if old_session is not None:
                notification_center.remove_observer(self, sender=old_session)
            if new_session is not None:
                notification_center.add_observer(self, sender=new_session)

    sip_session = property(_get_sip_session, _set_sip_session)
    del _get_sip_session, _set_sip_session

    def _get_active(self):
        return self.__dict__['active']

    def _set_active(self, value):
        value = bool(value)
        if self.__dict__.get('active', None) == value:
            return
        self.__dict__['active'] = value
        if self.state in ('connecting/*', 'connected/*') and self.streams.types.intersection({'audio', 'video'}):
            entity = self.client_conference or self
            if value:
                entity.unhold()
            else:
                entity.hold()

    active = property(_get_active, _set_active)
    del _get_active, _set_active

    def _get_account(self):
        account_manager = AccountManager()
        account_id = self.__dict__.get('account', None)
        if account_id is not None:
            try:
                account = account_manager.get_account(account_id)
                if account.enabled:
                    return account
            except KeyError:
                pass
        account = account_manager.default_account
        self.__dict__['account'] = account.id
        return account

    def _set_account(self, account):
        self.__dict__['account'] = account.id if account is not None else None

    account = property(_get_account, _set_account)
    del _get_account, _set_account

    def _get_client_conference(self):
        return self.__dict__['client_conference']

    def _set_client_conference(self, value):
        old_conference = self.__dict__.get('client_conference', None)
        new_conference = self.__dict__['client_conference'] = value
        if old_conference is new_conference:
            return
        if old_conference is not None:
            old_conference.remove_session(self)
        if new_conference is not None:
            new_conference.add_session(self)
            self.unhold()
        elif not self.active:
            self.hold()
        self.clientConferenceChanged.emit(old_conference, new_conference)

    client_conference = property(_get_client_conference, _set_client_conference)
    del _get_client_conference, _set_client_conference

    @property
    def persistent(self):
        return not self._delete_when_done and not self._delete_requested

    @property
    def reusable(self):
        return self.persistent and self.state in (None, 'initialized', 'ended')

    @property
    def duration(self):
        return self.info.duration

    @property
    def transport(self):
        return self.sip_session.transport if self.sip_session is not None else None

    @property
    def on_hold(self):
        return self.local_hold or self.remote_hold

    @property
    def remote_focus(self):
        return self.sip_session is not None and self.sip_session.remote_focus

    def init_incoming(self, sip_session, streams, contact, contact_uri, reinitialize=False):
        assert self.state in (None, 'initialized', 'ended')
        assert self.contact is None or contact.settings is self.contact.settings
        notification_center = NotificationCenter()
        if reinitialize:
            notification_center.post_notification('BlinkSessionWillReinitialize', sender=self)
            self._initialize(reinitialize=True)
        else:
            self._delete_when_done = len(streams)==1 and streams[0].type=='audio'
        self.direction = 'incoming'
        self.sip_session = sip_session
        self.account = sip_session.account
        self.contact = contact
        self.contact_uri = contact_uri
        self.uri = self._parse_uri(contact_uri.uri)
        self.streams.extend(streams)
        self.info._update(self)
        self.state = 'connecting'
        if reinitialize:
            notification_center.post_notification('BlinkSessionDidReinitializeForIncoming', sender=self)
        else:
            notification_center.post_notification('BlinkSessionNewIncoming', sender=self)
        notification_center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'session', 'media', 'statistics'}))
        notification_center.post_notification('BlinkSessionConnectionProgress', sender=self, data=NotificationData(stage='connecting'))
        self.sip_session.accept(streams)

    def init_outgoing(self, account, contact, contact_uri, stream_descriptions, sibling=None, reinitialize=False):
        assert self.state in (None, 'initialized', 'ended')
        assert self.contact is None or contact.settings is self.contact.settings
        notification_center = NotificationCenter()
        if reinitialize:
            notification_center.post_notification('BlinkSessionWillReinitialize', sender=self)
            self._initialize(reinitialize=True)
        else:
            self._delete_when_done = len(stream_descriptions)==1 and stream_descriptions[0].type=='audio'
        self.direction = 'outgoing'
        self.account = account
        self.contact = contact
        self.contact_uri = contact_uri
        self.uri = self._normalize_uri(contact_uri.uri)
        # reevaluate later, after we add the .active/.proposed attributes to streams, if creating the sip session and the streams at this point is desirable -Dan
        # note: creating the sip session early also need the test in hold/unhold/end to change from sip_session is (not) None to sip_session.state is (not) None -Dan
        self.stream_descriptions = StreamSet(stream_descriptions)
        self._sibling = sibling
        self.state = 'initialized'
        self.info._update(self)
        if reinitialize:
            notification_center.post_notification('BlinkSessionDidReinitializeForOutgoing', sender=self)
        else:
            notification_center.post_notification('BlinkSessionNewOutgoing', sender=self)
        notification_center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'session', 'media', 'statistics'}))

    def connect(self):
        assert self.direction == 'outgoing' and self.state == 'initialized'
        notification_center = NotificationCenter()
        self.streams.extend(stream_description.create_stream() for stream_description in self.stream_descriptions)
        self.state = 'connecting/dns_lookup'
        notification_center.post_notification('BlinkSessionWillConnect', sender=self, data=NotificationData(sibling=self._sibling))
        self.stream_descriptions = None
        self._sibling = None
        notification_center.post_notification('BlinkSessionConnectionProgress', sender=self, data=NotificationData(stage='dns_lookup'))
        account = self.account
        settings = SIPSimpleSettings()
        if isinstance(account, Account):
            if account.sip.outbound_proxy is not None:
                proxy = account.sip.outbound_proxy
                uri = SIPURI(host=proxy.host, port=proxy.port, parameters={'transport': proxy.transport})
            elif account.sip.always_use_my_proxy:
                uri = SIPURI(host=account.id.domain)
            else:
                uri = self.uri
        else:
            uri = self.uri
        self.lookup = DNSLookup()
        notification_center.add_observer(self, sender=self.lookup)
        self.lookup.lookup_sip_proxy(uri, settings.sip.transport_list)

    def add_stream(self, stream_description):
        assert self.state == 'connected'
        if stream_description.type in self.streams:
            raise RuntimeError('session already has a stream of type %s' % stream_description.type)
        self.info.streams[stream_description.type]._reset()
        stream = stream_description.create_stream()
        self.sip_session.add_stream(stream)
        self.streams.add(stream)
        notification_center = NotificationCenter()
        notification_center.post_notification('BlinkSessionWillAddStream', sender=self, data=NotificationData(stream=stream))
        notification_center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'media', 'statistics'}))

    def remove_stream(self, stream):
        assert self.state == 'connected'
        if stream not in self.streams:
            raise RuntimeError('stream is not part of the current session')
        self.sip_session.remove_stream(stream)
        notification_center = NotificationCenter()
        notification_center.post_notification('BlinkSessionWillRemoveStream', sender=self, data=NotificationData(stream=stream))

    def accept_proposal(self, streams):
        assert self.state == 'connected/received_proposal'
        duplicate_types = sorted(stream.type for stream in streams if stream.type in self.streams)
        if duplicate_types:
            raise RuntimeError('accepting proposal would result in duplicated streams for: %s' % ', '.join(duplicate_types))
        self.sip_session.accept_proposal(streams)
        notification_center = NotificationCenter()
        for stream in streams:
            self.info.streams[stream.type]._reset()
            self.streams.add(stream)
            notification_center.post_notification('BlinkSessionWillAddStream', sender=self, data=NotificationData(stream=stream))
        notification_center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'media', 'statistics'}))

    def hold(self):
        if self.sip_session is not None and not self.local_hold:
            self.local_hold = True
            self.sip_session.hold()
            NotificationCenter().post_notification('BlinkSessionDidChangeHoldState', sender=self, data=NotificationData(local_hold=self.local_hold, remote_hold=self.remote_hold))

    def unhold(self):
        if self.sip_session is not None and self.local_hold:
            self.local_hold = False
            self.sip_session.unhold()
            NotificationCenter().post_notification('BlinkSessionDidChangeHoldState', sender=self, data=NotificationData(local_hold=self.local_hold, remote_hold=self.remote_hold))

    def send_dtmf(self, digit):
        audio_stream = self.streams.get('audio')
        if audio_stream is None:
            return
        try:
            audio_stream.send_dtmf(digit)
        except RuntimeError:
            pass
        else:
            digit_map = {'*': 'star'}
            filename = 'sounds/dtmf_%s_tone.wav' % digit_map.get(digit, digit)
            player = WavePlayer(SIPApplication.voice_audio_bridge.mixer, Resources.get(filename))
            if self.account.rtp.inband_dtmf:
                audio_stream.bridge.add(player)
            SIPApplication.voice_audio_bridge.add(player)
            player.start()

    def start_recording(self):
        # I'd like to start recording before the call starts -Dan
        audio_stream = self.streams.get('audio')
        if audio_stream is not None and not self.recording:
            settings = SIPSimpleSettings()
            direction = self.sip_session.direction
            remote = "%s@%s" % (self.sip_session.remote_identity.uri.user, self.sip_session.remote_identity.uri.host)
            filename = "%s-%s-%s.wav" % (datetime.now().strftime("%Y%m%d-%H%M%S"), remote, direction)
            path = os.path.join(settings.audio.recordings_directory.normalized, self.account.id)
            try:
                audio_stream.start_recording(os.path.join(path, filename))
            except (SIPCoreError, IOError, OSError), e:
                print 'Failed to record: %s' % e

    def stop_recording(self):
        audio_stream = self.streams.get('audio')
        if audio_stream is not None:
            audio_stream.stop_recording()

    def end(self, delete=False):
        if self.state == 'ending':
            self._delete_requested = delete
        elif self.state == 'ended':
            self._delete_requested = delete
            if delete:
                self._delete()
        elif self.state in ('initialized', 'connecting/*', 'connected/*'):
            self._delete_requested = delete
            self.state = 'ending'
            notification_center = NotificationCenter()
            notification_center.post_notification('BlinkSessionWillEnd', sender=self)
            if self.sip_session is None:
                self._terminate(reason='Call cancelled', error=True)
            else:
                self.sip_session.end()

    def _delete(self):
        if self.state != 'ended':
            return
        self.state = 'deleted'

        notification_center = NotificationCenter()
        notification_center.post_notification('BlinkSessionWasDeleted', sender=self)

        self.account = None
        self.contact = None
        self.contact_uri = None

    def _terminate(self, reason, error=False):
        notification_center = NotificationCenter()

        if self.state != 'ending':
            self.state = 'ending'
            notification_center.post_notification('BlinkSessionWillEnd', sender=self)

        self.timer.stop()
        self.streams.clear()

        self.lookup = None
        self.sip_session = None
        self.stream_descriptions = None
        self._sibling = None

        self.local_hold = False
        self.remote_hold = False
        self.recording = False

        state = BlinkSessionState('ended')
        state.reason = reason
        state.error = error

        self.state = state
        notification_center.post_notification('BlinkSessionDidEnd', sender=self, data=NotificationData(reason=reason, error=error))

        if not self.persistent:
            self._delete()

    def _parse_uri(self, uri):
        if '@' not in uri:
            uri += '@' + self.account.id.domain
        if not uri.startswith(('sip:', 'sips:')):
            uri = 'sip:' + uri
        return SIPURI.parse(str(uri))

    def _normalize_uri(self, uri):
        from blink.contacts import URIUtils

        if '@' not in uri:
            uri += '@' + self.account.id.domain
        if not uri.startswith(('sip:', 'sips:')):
            uri = 'sip:' + uri
        uri = SIPURI.parse(str(uri).translate(None, ' \t'))
        if URIUtils.is_number(uri.user):
            uri.user = URIUtils.trim_number(uri.user)
            if isinstance(self.account, Account):
                if self.account.pstn.idd_prefix is not None:
                    uri.user = re.sub(r'^\+', self.account.pstn.idd_prefix, uri.user)
                if self.account.pstn.prefix is not None:
                    uri.user = self.account.pstn.prefix + uri.user
        return uri

    def _SH_TimerFired(self):
        self.info.duration += timedelta(seconds=1)
        self.info.streams.audio._update_statistics(self.streams.get('audio', Null).statistics)
        self.info.streams.video._update_statistics(self.streams.get('video', Null).statistics)
        notification_center = NotificationCenter()
        notification_center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'statistics'}))

    @run_in_gui_thread
    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_DNSLookupDidSucceed(self, notification):
        notification.center.remove_observer(self, sender=notification.sender)
        if notification.sender is self.lookup:
            routes = notification.data.result
            if routes:
                self.sip_session = Session(self.account)
                self.sip_session.connect(ToHeader(self.uri), routes, list(self.streams))
            else:
                self._terminate(reason='Destination not found', error=True)

    def _NH_DNSLookupDidFail(self, notification):
        notification.center.remove_observer(self, sender=notification.sender)
        if notification.sender is self.lookup:
            self._terminate(reason='Destination not found', error=True)

    def _NH_SIPSessionNewOutgoing(self, notification):
        self.state = 'connecting'
        self.info._update(self)
        notification.center.post_notification('BlinkSessionConnectionProgress', sender=self, data=NotificationData(stage='connecting'))
        notification.center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'session'}))

    def _NH_SIPSessionGotProvisionalResponse(self, notification):
        if notification.data.code == 180:
            self.state = 'connecting/ringing'
            notification.center.post_notification('BlinkSessionConnectionProgress', sender=self, data=NotificationData(stage='ringing'))
        elif notification.data.code == 183:
            self.state = 'connecting/early_media'
            notification.center.post_notification('BlinkSessionConnectionProgress', sender=self, data=NotificationData(stage='early_media'))
        self.info._update(self)
        notification.center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'session', 'media'}))

    def _NH_SIPSessionWillStart(self, notification):
        self.state = 'connecting/starting'
        self.info._update(self)
        notification.center.post_notification('BlinkSessionConnectionProgress', sender=self, data=NotificationData(stage='starting'))
        notification.center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'session', 'media'}))

    def _NH_SIPSessionDidStart(self, notification):
        for stream in set(self.streams).difference(notification.data.streams):
            self.streams.remove(stream)
        if self.state not in ('ending', 'ended', 'deleted'):
            self.state = 'connected'
            self.timer.start()
            self.info._update(self)
            notification.center.post_notification('BlinkSessionDidConnect', sender=self)
            notification.center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'session', 'media'}))

    def _NH_SIPSessionDidFail(self, notification):
        if notification.data.failure_reason == 'user request':
            if notification.data.code == 487:
                reason = 'Call cancelled'
            else:
                reason = notification.data.reason
        else:
            reason = notification.data.failure_reason
        self._terminate(reason=reason, error=True)

    def _NH_SIPSessionDidEnd(self, notification):
        self._terminate('Call ended' if notification.data.originator=='local' else 'Call ended by remote')

    def _NH_SIPSessionDidChangeHoldState(self, notification):
        if notification.data.originator == 'remote':
            self.remote_hold = notification.data.on_hold
        notification.center.post_notification('BlinkSessionDidChangeHoldState', sender=self, data=NotificationData(local_hold=self.local_hold, remote_hold=self.remote_hold))

    def _NH_SIPSessionNewProposal(self, notification):
        if self.state not in ('ending', 'ended', 'deleted'):
            if notification.data.originator == 'local':
                self.state = 'connected/sent_proposal'
            else:
                self.state = 'connected/received_proposal'

    def _NH_SIPSessionProposalAccepted(self, notification):
        accepted_streams = notification.data.accepted_streams
        proposed_streams = notification.data.proposed_streams
        if self.state not in ('ending', 'ended', 'deleted'):
            self.state = 'connected'
        for stream in proposed_streams:
            if stream in accepted_streams:
                notification.center.post_notification('BlinkSessionDidAddStream', sender=self, data=NotificationData(stream=stream))
            else:
                self.streams.remove(stream)
                notification.center.post_notification('BlinkSessionDidNotAddStream', sender=self, data=NotificationData(stream=stream))
        if accepted_streams:
            self.info.streams._update(self.streams)
            notification.center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'media'}))

    def _NH_SIPSessionProposalRejected(self, notification):
        for stream in set(notification.data.proposed_streams).intersection(self.streams):
            self.streams.remove(stream)
            notification.center.post_notification('BlinkSessionDidNotAddStream', sender=self, data=NotificationData(stream=stream))
        if self.state not in ('ending', 'ended', 'deleted'):
            self.state = 'connected'

    def _NH_SIPSessionHadProposalFailure(self, notification):
        for stream in set(notification.data.proposed_streams).intersection(self.streams):
            self.streams.remove(stream)
            notification.center.post_notification('BlinkSessionDidNotAddStream', sender=self, data=NotificationData(stream=stream))
        if self.state not in ('ending', 'ended', 'deleted'):
            self.state = 'connected'

    def _NH_SIPSessionDidRenegotiateStreams(self, notification):
        if notification.data.added_streams:
            self._delete_when_done = False
        for stream in set(notification.data.removed_streams).intersection(self.streams):
            self.streams.remove(stream)
            notification.center.post_notification('BlinkSessionDidRemoveStream', sender=self, data=NotificationData(stream=stream))
        if not self.streams:
            self.end()
        elif self.streams.types.isdisjoint({'audio', 'video'}):
            self.unhold()

    def _NH_AudioStreamICENegotiationStateDidChange(self, notification):
        state = notification.data.state
        if state == 'GATHERING':
            self.info.streams.audio.ice_status = 'gathering'
            notification.center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'media'}))
        if state == 'GATHERING_COMPLETE':
            self.info.streams.audio.ice_status = 'gathering_complete'
            notification.center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'media'}))
        elif state == 'NEGOTIATING':
            self.info.streams.audio.ice_status = 'negotiating'
            notification.center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'media'}))

    def _NH_AudioStreamICENegotiationDidSucceed(self, notification):
        self.info.streams.audio.ice_status = 'succeeded'
        self.info.streams.audio.local_rtp_candidate = notification.sender.local_rtp_candidate
        self.info.streams.audio.remote_rtp_candidate = notification.sender.remote_rtp_candidate
        notification.center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'media'}))

    def _NH_AudioStreamICENegotiationDidFail(self, notification):
        self.info.streams.audio.ice_status = 'failed'
        notification.center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'media'}))

    def _NH_AudioStreamGotDTMF(self, notification):
        digit_map = {'*': 'star'}
        filename = 'sounds/dtmf_%s_tone.wav' % digit_map.get(notification.data.digit, notification.data.digit)
        player = WavePlayer(SIPApplication.voice_audio_bridge.mixer, Resources.get(filename))
        SIPApplication.voice_audio_bridge.add(player)
        player.start()

    def _NH_AudioStreamDidStartRecordingAudio(self, notification):
        self.recording = True
        notification.center.post_notification('BlinkSessionDidChangeRecordingState', sender=self, data=NotificationData(recording=self.recording))

    def _NH_AudioStreamWillStopRecordingAudio(self, notification):
        self.recording = False
        notification.center.post_notification('BlinkSessionDidChangeRecordingState', sender=self, data=NotificationData(recording=self.recording))

    def _NH_BlinkContactDidChange(self, notification):
        notification.center.post_notification('BlinkSessionContactDidChange', sender=self)


class ClientConference(object):
    def __init__(self):
        self.sessions = []
        self.stream_map = {}
        self.audio_conference = AudioConference()
        self.audio_conference.hold()

    def add_session(self, session):
        audio_stream = session.streams.get('audio')
        self.sessions.append(session)
        self.stream_map[session] = audio_stream
        if audio_stream is not None:
            self.audio_conference.add(audio_stream)

    def remove_session(self, session):
        self.sessions.remove(session)
        audio_stream = self.stream_map.pop(session)
        if audio_stream is not None:
            self.audio_conference.remove(audio_stream)

    def hold(self):
        self.audio_conference.hold()

    def unhold(self):
        self.audio_conference.unhold()


class ConferenceParticipant(object):
    implements(IObserver)

    def __init__(self, contact, contact_uri):
        self.contact = contact
        self.contact_uri = contact_uri
        self.uri = contact_uri.uri

        self.active_media = set()
        self.display_name = None
        self.on_hold = False
        self.is_composing = False    # TODO: set this from the chat stream -Saul
        self.request_status = None

        notification_center = NotificationCenter()
        notification_center.add_observer(ObserverWeakrefProxy(self), sender=contact)

    def __repr__(self):
        return '%s(%r, %r)' % (self.__class__.__name__, self.contact, self.contact_uri)

    @property
    def pending_request(self):
        return self.request_status is not None

    def _get_is_composing(self):
        return self.__dict__['is_composing']

    def _set_is_composing(self, value):
        old_value = self.__dict__.get('is_composing', False)
        self.__dict__['is_composing'] = value
        if old_value != value:
            NotificationCenter().post_notification('ConferenceParticipantDidChange', sender=self)

    is_composing = property(_get_is_composing, _set_is_composing)
    del _get_is_composing, _set_is_composing

    def _get_request_status(self):
        return self.__dict__['request_status']

    def _set_request_status(self, value):
        old_value = self.__dict__.get('request_status', None)
        self.__dict__['request_status'] = value
        if old_value != value:
            NotificationCenter().post_notification('ConferenceParticipantDidChange', sender=self)

    request_status = property(_get_request_status, _set_request_status)
    del _get_request_status, _set_request_status

    def _update(self, data):
        old_values = dict(active_media=self.active_media.copy(), display_name=self.display_name, on_hold=self.on_hold)
        self.display_name = data.display_text.value if data.display_text else None
        self.active_media.clear()
        for media in chain(*data):
            if media.media_type.value == 'message':
                self.active_media.add('chat')
            else:
                self.active_media.add(media.media_type.value)
        audio_endpoints = [endpt for endpt in data if any(media.media_type=='audio' for media in endpt)]
        self.on_hold = all(endpt.status=='on-hold' for endpt in audio_endpoints) if audio_endpoints else False
        for attr, value in old_values.iteritems():
            if value != getattr(self, attr):
                NotificationCenter().post_notification('ConferenceParticipantDidChange', sender=self)
                break

    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_BlinkContactDidChange(self, notification):
        notification.center.post_notification('ConferenceParticipantDidChange', sender=self)


class ServerConference(object):
    implements(IObserver)

    sip_prefix_re = re.compile('^sips?:')

    def __init__(self, session):
        self.session = session
        self.sip_session = None

        self.participants = {}
        self.pending_additions = set()
        self.pending_removals = set()

        notification_center = NotificationCenter()
        notification_center.add_observer(self, sender=session)

    def add_participant(self, contact, contact_uri):
        if contact_uri.uri in self.participants:
            return
        participant = ConferenceParticipant(contact, contact_uri)
        participant.request_status = 'Joining'
        self.session.sip_session.conference.add_participant(participant.uri)
        self.participants[participant.uri] = participant
        self.pending_additions.add(participant)
        notification_center = NotificationCenter()
        notification_center.post_notification('BlinkSessionWillAddParticipant', sender=self.session, data=NotificationData(participant=participant))

    def remove_participant(self, participant):
        if participant.uri not in self.participants:
            return
        if participant in self.pending_removals:
            return
        participant.request_status = 'Leaving'
        self.session.sip_session.conference.remove_participant(participant.uri)
        self.pending_removals.add(participant)
        notification_center = NotificationCenter()
        notification_center.post_notification('BlinkSessionWillRemoveParticipant', sender=self.session, data=NotificationData(participant=participant))

    @run_in_gui_thread
    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_BlinkSessionDidConnect(self, notification):
        self.sip_session = notification.sender.sip_session
        notification.center.add_observer(self, sender=self.sip_session)

    def _NH_BlinkSessionDidEnd(self, notification):
        if self.sip_session is not None:
            notification.center.remove_observer(self, sender=self.sip_session)
        self.sip_session = None
        self.participants.clear()

    def _NH_BlinkSessionWasDeleted(self, notification):
        notification.center.remove_observer(self, sender=notification.sender)
        self.session = None

    def _NH_SIPSessionGotConferenceInfo(self, notification):
        from blink.contacts import URIUtils
        users = dict((self.sip_prefix_re.sub('', str(user.entity)), user) for user in notification.data.conference_info.users)

        removed_participants = [participant for participant in self.participants.itervalues() if participant.uri not in users and participant not in self.pending_additions]
        confirmed_participants = [participant for participant in self.participants.itervalues() if participant in self.pending_additions and participant.uri in users]
        updated_participants = [self.participants[uri] for uri in users if uri in self.participants]
        added_users = set(users.keys()).difference(self.participants.keys())

        for participant in removed_participants:
            self.participants.pop(participant.uri)
            if participant in self.pending_removals:
                self.pending_removals.remove(participant)
            else:
                notification.center.post_notification('BlinkSessionWillRemoveParticipant', sender=self.session, data=NotificationData(participant=participant))
            notification.center.post_notification('BlinkSessionDidRemoveParticipant', sender=self.session, data=NotificationData(participant=participant))
            participant.request_status = None

        for participant in confirmed_participants:
            participant.request_status = None
            participant._update(users[participant.uri])
            self.pending_additions.remove(participant)
            notification.center.post_notification('BlinkSessionDidAddParticipant', sender=self.session, data=NotificationData(participant=participant))

        for participant in updated_participants:
            participant._update(users[participant.uri])

        for uri in added_users:
            contact, contact_uri = URIUtils.find_contact(uri)
            participant = ConferenceParticipant(contact, contact_uri)
            participant._update(users[participant.uri])
            self.participants[participant.uri] = participant
            notification.center.post_notification('BlinkSessionWillAddParticipant', sender=self.session, data=NotificationData(participant=participant))
            notification.center.post_notification('BlinkSessionDidAddParticipant', sender=self.session, data=NotificationData(participant=participant))

    def _NH_SIPConferenceDidNotAddParticipant(self, notification):
        uri = self.sip_prefix_re.sub('', str(notification.data.participant))
        try:
            participant = self.participants[uri]
        except KeyError:
            return
        if participant not in self.pending_additions:
            return
        participant.request_status = None
        del self.participants[uri]
        self.pending_additions.remove(participant)
        notification.center.post_notification('BlinkSessionDidNotAddParticipant', sender=self.session, data=NotificationData(participant=participant, reason=notification.data.reason))

    def _NH_SIPConferenceDidNotRemoveParticipant(self, notification):
        uri = self.sip_prefix_re.sub('', str(notification.data.participant))
        try:
            participant = self.participants[uri]
        except KeyError:
            return
        if participant not in self.pending_removals:
            return
        participant.request_status = None
        self.pending_removals.remove(participant)
        notification.center.post_notification('BlinkSessionDidNotRemoveParticipant', sender=self.session, data=NotificationData(participant=participant, reason=notification.data.reason))

    def _NH_SIPConferenceGotAddParticipantProgress(self, notification):
        uri = self.sip_prefix_re.sub('', str(notification.data.participant))
        try:
            participant = self.participants[uri]
        except KeyError:
            return
        if participant not in self.pending_additions:
            return
        participant.request_status = notification.data.reason

    def _NH_SIPConferenceGotRemoveParticipantProgress(self, notification):
        uri = self.sip_prefix_re.sub('', str(notification.data.participant))
        try:
            participant = self.participants[uri]
        except KeyError:
            return
        if participant not in self.pending_removals:
            return
        participant.request_status = notification.data.reason


# Audio sessions
#

# positions for sessions in a client conference.
class Top(object): pass
class Middle(object): pass
class Bottom(object): pass


ui_class, base_class = uic.loadUiType(Resources.get('audio_session.ui'))

class AudioSessionWidget(base_class, ui_class):
    def __init__(self, session, parent=None):
        super(AudioSessionWidget, self).__init__(parent)
        with Resources.directory:
            self.setupUi(self)
        # add a left margin for the colored band
        self.address_layout.setContentsMargins(8, -1, -1, -1)
        self.stream_layout.setContentsMargins(8, -1, -1, -1)
        self.bottom_layout.setContentsMargins(8, -1, -1, -1)
        font = self.latency_label.font()
        font.setPointSizeF(self.status_label.fontInfo().pointSizeF() - 1)
        self.latency_label.setFont(font)
        font = self.packet_loss_label.font()
        font.setPointSizeF(self.status_label.fontInfo().pointSizeF() - 1)
        self.packet_loss_label.setFont(font)
        self.mute_button.type = LeftSegment
        self.hold_button.type = MiddleSegment
        self.record_button.type = MiddleSegment
        self.hangup_button.type = RightSegment
        self.selected = False
        self.drop_indicator = False
        self.position_in_conference = None
        self._disable_dnd = False
        self.mute_button.hidden.connect(self._SH_MuteButtonHidden)
        self.mute_button.shown.connect(self._SH_MuteButtonShown)
        self.mute_button.pressed.connect(self._SH_ToolButtonPressed)
        self.hold_button.pressed.connect(self._SH_ToolButtonPressed)
        self.record_button.pressed.connect(self._SH_ToolButtonPressed)
        self.hangup_button.pressed.connect(self._SH_ToolButtonPressed)
        self.mute_button.hide()
        self.mute_button.setEnabled(False)
        self.hold_button.setEnabled(False)
        self.record_button.setEnabled(False)
        self.address_label.setText(session.name)
        self.stream_info_label.session_type = session.type
        self.stream_info_label.codec_info = session.codec_info
        self.duration_label.value = session.duration
        self.latency_label.value = session.latency
        self.packet_loss_label.value = session.packet_loss
        self.status_label.value = session.status
        self.tls_label.setVisible(bool(session.tls))
        self.srtp_label.setVisible(bool(session.srtp))

    def _get_selected(self):
        return self.__dict__['selected']

    def _set_selected(self, value):
        if self.__dict__.get('selected', None) == value:
            return
        self.__dict__['selected'] = value
        self.update()

    selected = property(_get_selected, _set_selected)
    del _get_selected, _set_selected

    def _get_drop_indicator(self):
        return self.__dict__['drop_indicator']

    def _set_drop_indicator(self, value):
        if self.__dict__.get('drop_indicator', None) == value:
            return
        self.__dict__['drop_indicator'] = value
        self.update()

    drop_indicator = property(_get_drop_indicator, _set_drop_indicator)
    del _get_drop_indicator, _set_drop_indicator

    def _get_position_in_conference(self):
        return self.__dict__['position_in_conference']

    def _set_position_in_conference(self, value):
        if self.__dict__.get('position_in_conference', Null) == value:
            return
        self.__dict__['position_in_conference'] = value
        self.update()

    position_in_conference = property(_get_position_in_conference, _set_position_in_conference)
    del _get_position_in_conference, _set_position_in_conference

    def _SH_MuteButtonHidden(self):
        self.hold_button.type = LeftSegment

    def _SH_MuteButtonShown(self):
        self.hold_button.type = MiddleSegment

    def _SH_ToolButtonPressed(self):
        self._disable_dnd = True

    def mousePressEvent(self, event):
        self._disable_dnd = False
        super(AudioSessionWidget, self).mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if self._disable_dnd:
            return
        super(AudioSessionWidget, self).mouseMoveEvent(event)

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing, True)

        rect = self.rect()

        # draw inner rect and border
        #
        if self.selected:
            background = QLinearGradient(0, 0, 10, 0)
            background.setColorAt(0.00, QColor('#75c0ff'))
            background.setColorAt(0.99, QColor('#75c0ff'))
            background.setColorAt(1.00, QColor('#ffffff'))
            painter.setBrush(QBrush(background))
            painter.setPen(QPen(QBrush(QColor('#606060' if self.position_in_conference is None else '#b0b0b0')), 2.0))
        elif self.position_in_conference is not None:
            background = QLinearGradient(0, 0, 10, 0)
            background.setColorAt(0.00, QColor('#95ff95'))
            background.setColorAt(0.99, QColor('#95ff95'))
            background.setColorAt(1.00, QColor('#ffffff'))
            painter.setBrush(QBrush(background))
            painter.setPen(QPen(QBrush(QColor('#b0b0b0')), 2.0))
        else:
            background = QLinearGradient(0, 0, 10, 0)
            background.setColorAt(0.00, QColor('#d0d0d0'))
            background.setColorAt(0.99, QColor('#d0d0d0'))
            background.setColorAt(1.00, QColor('#ffffff'))
            painter.setBrush(QBrush(background))
            painter.setPen(QPen(QBrush(QColor('#b0b0b0')), 2.0))
        painter.drawRoundedRect(rect.adjusted(2, 2, -2, -2), 3, 3)

        # for conferences extend the left marker over the whole conference
        #
        if self.position_in_conference is not None:
            painter.setPen(Qt.NoPen)
            left_rect = rect.adjusted(0, 0, 10-rect.width(), 0)
            if self.position_in_conference is Top:
                painter.drawRect(left_rect.adjusted(2, 5, 0, 5))
            elif self.position_in_conference is Middle:
                painter.drawRect(left_rect.adjusted(2, -5, 0, 5))
            elif self.position_in_conference is Bottom:
                painter.drawRect(left_rect.adjusted(2, -5, 0, -5))

        # draw outer border
        #
        if self.selected or self.drop_indicator:
            painter.setBrush(Qt.NoBrush)
            if self.drop_indicator:
                painter.setPen(QPen(QBrush(QColor('#dc3169')), 2.0))
            elif self.selected:
                painter.setPen(QPen(QBrush(QColor('#3075c0')), 2.0)) # or #2070c0 (next best look) or gray: #606060

            if self.position_in_conference is Top:
                painter.drawRoundedRect(rect.adjusted(2, 2, -2, 5), 3, 3)
                painter.drawRoundedRect(rect.adjusted(1, 1, -1, 5), 3, 3)
            elif self.position_in_conference is Middle:
                painter.drawRoundedRect(rect.adjusted(2, -5, -2, 5), 3, 3)
                painter.drawRoundedRect(rect.adjusted(1, -5, -1, 5), 3, 3)
            elif self.position_in_conference is Bottom:
                painter.drawRoundedRect(rect.adjusted(2, -5, -2, -2), 3, 3)
                painter.drawRoundedRect(rect.adjusted(1, -5, -1, -1), 3, 3)
            else:
                painter.drawRoundedRect(rect.adjusted(2, 2, -2, -2), 3, 3)
                painter.drawRoundedRect(rect.adjusted(1, 1, -1, -1), 3, 3)
        elif self.position_in_conference is not None:
            painter.setBrush(Qt.NoBrush)
            painter.setPen(QPen(QBrush(QColor('#309030')), 2.0)) # or 237523, #2b8f2b
            if self.position_in_conference is Top:
                painter.drawRoundedRect(rect.adjusted(2, 2, -2, 5), 3, 3)
            elif self.position_in_conference is Middle:
                painter.drawRoundedRect(rect.adjusted(2, -5, -2, 5), 3, 3)
            elif self.position_in_conference is Bottom:
                painter.drawRoundedRect(rect.adjusted(2, -5, -2, -2), 3, 3)
            else:
                painter.drawRoundedRect(rect.adjusted(2, 2, -2, -2), 3, 3)

        painter.end()
        super(AudioSessionWidget, self).paintEvent(event)


class DraggedAudioSessionWidget(base_class, ui_class):
    """Used to draw a dragged session item"""
    def __init__(self, session_widget, parent=None):
        super(DraggedAudioSessionWidget, self).__init__(parent)
        with Resources.directory:
            self.setupUi(self)
        # add a left margin for the colored band
        self.address_layout.setContentsMargins(8, -1, -1, -1)
        self.stream_layout.setContentsMargins(8, -1, -1, -1)
        self.bottom_layout.setContentsMargins(8, -1, -1, -1)
        self.mute_button.hide()
        self.hold_button.hide()
        self.record_button.hide()
        self.hangup_button.hide()
        self.tls_label.hide()
        self.srtp_label.hide()
        self.latency_label.hide()
        self.packet_loss_label.hide()
        self.duration_label.hide()
        self.stream_info_label.setText(u'')
        self.address_label.setText(session_widget.address_label.text())
        self.selected = session_widget.selected
        self.in_conference = session_widget.position_in_conference is not None
        if self.in_conference:
            self.status_label.setText(u'Drop outside the conference to detach')
        else:
            self.status_label.setText(u'Drop over a session to conference them')
        self.status_label.show()

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing, True)
        if self.in_conference:
            background = QLinearGradient(0, 0, 10, 0)
            background.setColorAt(0.00, QColor('#95ff95'))
            background.setColorAt(0.99, QColor('#95ff95'))
            background.setColorAt(1.00, QColor('#f8f8f8'))
            painter.setBrush(QBrush(background))
            painter.setPen(QPen(QBrush(QColor('#309030')), 2.0))
        elif self.selected:
            background = QLinearGradient(0, 0, 10, 0)
            background.setColorAt(0.00, QColor('#75c0ff'))
            background.setColorAt(0.99, QColor('#75c0ff'))
            background.setColorAt(1.00, QColor('#f8f8f8'))
            painter.setBrush(QBrush(background))
            painter.setPen(QPen(QBrush(QColor('#3075c0')), 2.0))
        else:
            background = QLinearGradient(0, 0, 10, 0)
            background.setColorAt(0.00, QColor('#d0d0d0'))
            background.setColorAt(0.99, QColor('#d0d0d0'))
            background.setColorAt(1.00, QColor('#f8f8f8'))
            painter.setBrush(QBrush(background))
            painter.setPen(QPen(QBrush(QColor('#808080')), 2.0))
        painter.drawRoundedRect(self.rect().adjusted(1, 1, -1, -1), 3, 3)
        painter.end()
        super(DraggedAudioSessionWidget, self).paintEvent(event)

del ui_class, base_class


class AudioSessionItem(object):
    implements(IObserver)

    def __init__(self, session):
        assert session.items.audio is None
        self.name = session.contact.name
        self.uri = session.uri
        self.blink_session = session
        self.blink_session.items.audio = self

        self.widget = Null
        self.status = None
        self.type = 'Audio'
        self.codec_info = ''
        self.tls = False
        self.srtp = False
        self.latency = 0
        self.packet_loss = 0
        self.pending_removal = False

        self.__deleted__ = False

        notification_center = NotificationCenter()
        notification_center.add_observer(self, sender=self.blink_session)

    @property
    def audio_stream(self):
        return self.blink_session.streams.get('audio')

    def _get_active(self):
        return self.blink_session.active

    def _set_active(self, value):
        self.blink_session.active = bool(value)

    active = property(_get_active, _set_active)
    del _get_active, _set_active

    def _get_client_conference(self):
        return self.blink_session.client_conference

    def _set_client_conference(self, value):
        self.blink_session.client_conference = value

    client_conference = property(_get_client_conference, _set_client_conference)
    del _get_client_conference, _set_client_conference

    def _get_latency(self):
        return self.__dict__['latency']

    def _set_latency(self, value):
        if self.__dict__.get('latency', None) == value:
            return
        self.__dict__['latency'] = value
        self.widget.latency_label.value = value

    latency = property(_get_latency, _set_latency)
    del _get_latency, _set_latency

    def _get_packet_loss(self):
        return self.__dict__['packet_loss']

    def _set_packet_loss(self, value):
        if self.__dict__.get('packet_loss', None) == value:
            return
        self.__dict__['packet_loss'] = value
        self.widget.packet_loss_label.value = value

    packet_loss = property(_get_packet_loss, _set_packet_loss)
    del _get_packet_loss, _set_packet_loss

    def _get_status(self):
        return self.__dict__['status']

    def _set_status(self, value):
        if self.__dict__.get('status', Null) == value:
            return
        self.__dict__['status'] = value
        self.widget.status_label.value = value

    status = property(_get_status, _set_status)
    del _get_status, _set_status

    def _get_type(self):
        return self.__dict__['type']

    def _set_type(self, value):
        if self.__dict__.get('type', Null) == value:
            return
        self.__dict__['type'] = value
        self.widget.stream_info_label.session_type = value

    type = property(_get_type, _set_type)
    del _get_type, _set_type

    def _get_codec_info(self):
        return self.__dict__['codec_info']

    def _set_codec_info(self, value):
        if self.__dict__.get('codec_info', None) == value:
            return
        self.__dict__['codec_info'] = value
        self.widget.stream_info_label.codec_info = value

    codec_info = property(_get_codec_info, _set_codec_info)
    del _get_codec_info, _set_codec_info

    def _get_srtp(self):
        return self.__dict__['srtp']

    def _set_srtp(self, value):
        if self.__dict__.get('srtp', None) == value:
            return
        self.__dict__['srtp'] = value
        self.widget.srtp_label.setVisible(bool(value))

    srtp = property(_get_srtp, _set_srtp)
    del _get_srtp, _set_srtp

    def _get_tls(self):
        return self.__dict__['tls']

    def _set_tls(self, value):
        if self.__dict__.get('tls', None) == value:
            return
        self.__dict__['tls'] = value
        self.widget.tls_label.setVisible(bool(value))

    tls = property(_get_tls, _set_tls)
    del _get_tls, _set_tls

    def _get_widget(self):
        return self.__dict__['widget']

    def _set_widget(self, widget):
        old_widget = self.__dict__.get('widget', Null)
        self.__dict__['widget'] = widget
        if old_widget is not Null:
            old_widget.mute_button.clicked.disconnect(self._SH_MuteButtonClicked)
            old_widget.hold_button.clicked.disconnect(self._SH_HoldButtonClicked)
            old_widget.record_button.clicked.disconnect(self._SH_RecordButtonClicked)
            old_widget.hangup_button.clicked.disconnect(self._SH_HangupButtonClicked)
            widget.mute_button.setEnabled(old_widget.mute_button.isEnabled())
            widget.mute_button.setChecked(old_widget.mute_button.isChecked())
            widget.hold_button.setEnabled(old_widget.hold_button.isEnabled())
            widget.hold_button.setChecked(old_widget.hold_button.isChecked())
            widget.record_button.setEnabled(old_widget.record_button.isEnabled())
            widget.record_button.setChecked(old_widget.record_button.isChecked())
            widget.hangup_button.setEnabled(old_widget.hangup_button.isEnabled())
        widget.mute_button.clicked.connect(self._SH_MuteButtonClicked)
        widget.hold_button.clicked.connect(self._SH_HoldButtonClicked)
        widget.record_button.clicked.connect(self._SH_RecordButtonClicked)
        widget.hangup_button.clicked.connect(self._SH_HangupButtonClicked)

    widget = property(_get_widget, _set_widget)
    del _get_widget, _set_widget

    @property
    def duration(self):
        return self.blink_session.info.duration

    def end(self):
        # this needs to consider the case where the audio stream is being added. in that case we need to cancel the proposal -Dan
        # however that information is not yet available (need the proposed flag on the streams) -Dan
        if len(self.blink_session.streams) > 1 and self.blink_session.state == 'connected':
            self.blink_session.remove_stream(self.audio_stream)
        else:
            self.blink_session.end()

    def delete(self):
        notification_center = NotificationCenter()
        notification_center.remove_observer(self, sender=self.blink_session)
        self.blink_session.items.audio = None
        self.blink_session = None
        self.widget = Null

    def send_dtmf(self, digit):
        self.blink_session.send_dtmf(digit)

    def _cleanup(self):
        if self.__deleted__:
            return
        self.__deleted__ = True
        self.widget.mute_button.setEnabled(False)
        self.widget.hold_button.setEnabled(False)
        self.widget.record_button.setEnabled(False)
        self.widget.hangup_button.setEnabled(False)

    def _reset_status(self):
        if not self.blink_session.on_hold:
            self.status = None

    def _SH_HangupButtonClicked(self):
        self.end()

    def _SH_HoldButtonClicked(self, checked):
        if checked:
            self.blink_session.hold()
        else:
            self.blink_session.unhold()

    def _SH_MuteButtonClicked(self, checked):
        if self.audio_stream is not None:
            self.audio_stream.muted = checked

    def _SH_RecordButtonClicked(self, checked):
        if checked:
            self.blink_session.start_recording()
        else:
            self.blink_session.stop_recording()

    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_BlinkSessionConnectionProgress(self, notification):
        stage = notification.data.stage
        if stage == 'dns_lookup':
            self.status = Status('Looking up destination...')
        elif stage == 'connecting':
            self.tls = self.blink_session.transport=='tls'
            self.status = Status('Connecting...')
        elif stage == 'ringing':
            self.status = Status('Ringing...')
        elif stage == 'starting':
            self.status = Status('Starting media...')
        else:
            self.status = None

    def _NH_BlinkSessionInfoUpdated(self, notification):
        if 'media' in notification.data.elements:
            audio_info = self.blink_session.info.streams.audio
            self.type = 'HD Audio' if audio_info.sample_rate >= 16000 else 'Audio'
            self.codec_info = audio_info.codec
            self.srtp = audio_info.encryption == 'SRTP'
        if 'statistics' in notification.data.elements:
            self.widget.duration_label.value = self.blink_session.info.duration
            # TODO: compute packet loss and latency statistics -Saul

    def _NH_BlinkSessionDidChangeHoldState(self, notification):
        self.widget.hold_button.setChecked(notification.data.local_hold)
        if self.blink_session.state == 'connected':
            if notification.data.local_hold:
                self.status = Status('On hold', color='#000090')
            elif notification.data.remote_hold:
                self.status = Status('Hold by remote', color='#000090')
            else:
                self.status = None

    def _NH_BlinkSessionDidChangeRecordingState(self, notification):
        self.widget.record_button.setChecked(notification.data.recording)

    def _NH_BlinkSessionDidConnect(self, notification):
        session = notification.sender
        self.tls = session.transport=='tls'
        if 'audio' in session.streams:
            self.widget.mute_button.setEnabled(True)
            self.widget.hold_button.setEnabled(True)
            self.widget.record_button.setEnabled(True)
            self.widget.hangup_button.setEnabled(True)
            self.status = Status('Connected')
            call_later(3, self._reset_status)
        else:
            self.status = Status('Audio refused', color='#900000')
            self._cleanup()

    def _NH_BlinkSessionDidAddStream(self, notification):
        if notification.data.stream.type == 'audio':
            self.widget.mute_button.setEnabled(True)
            self.widget.hold_button.setEnabled(True)
            self.widget.record_button.setEnabled(True)
            self.widget.hangup_button.setEnabled(True)
            self.status = Status('Connected')
            call_later(3, self._reset_status)

    def _NH_BlinkSessionDidNotAddStream(self, notification):
        if notification.data.stream.type == 'audio':
            self.status = Status('Audio refused', color='#900000') # where can we get the reason from? (rejected, cancelled, failed, ...) -Dan
            self._cleanup()

    def _NH_BlinkSessionWillRemoveStream(self, notification):
        if notification.data.stream.type == 'audio':
            self.widget.mute_button.setEnabled(False)
            self.widget.hold_button.setEnabled(False)
            self.widget.record_button.setEnabled(False)
            self.widget.hangup_button.setEnabled(False)
            self.status = Status('Ending...')

    def _NH_BlinkSessionDidRemoveStream(self, notification):
        if notification.data.stream.type == 'audio':
            self.status = Status('Call ended')
            self._cleanup()

    def _NH_BlinkSessionWillEnd(self, notification):
        self.widget.mute_button.setEnabled(False)
        self.widget.hold_button.setEnabled(False)
        self.widget.record_button.setEnabled(False)
        self.widget.hangup_button.setEnabled(False)
        self.status = Status('Ending...')

    def _NH_BlinkSessionDidEnd(self, notification):
        if not self.__deleted__: # may have been removed by BlinkSessionDidRemoveStream less than 5 seconds before the session ended.
            if notification.data.error:
                self.status = Status(notification.data.reason, color='#900000')
            else:
                self.status = Status(notification.data.reason)
            self._cleanup()


class AudioSessionDelegate(QStyledItemDelegate):
    size_hint = QSize(200, 62)

    def __init__(self, parent=None):
        super(AudioSessionDelegate, self).__init__(parent)

    def createEditor(self, parent, options, index):
        session = index.data(Qt.UserRole)
        session.widget = AudioSessionWidget(session, parent)
        session.widget.hold_button.clicked.connect(partial(self._SH_HoldButtonClicked, session)) # this partial still creates a memory cycle -Dan
        return session.widget

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)

    def paint(self, painter, option, index):
        session = index.data(Qt.UserRole)
        if session.widget.size() != option.rect.size():
            # For some reason updateEditorGeometry only receives the peak value
            # of the size that the widget ever had, so it will never shrink it.
            session.widget.resize(option.rect.size())

    def sizeHint(self, option, index):
        return self.size_hint

    def _SH_HoldButtonClicked(self, session, checked):
        if session.client_conference is None and not session.active and not checked:
            session_list = self.parent()
            model = session_list.model()
            selection_model = session_list.selectionModel()
            selection_model.select(model.index(model.sessions.index(session)), selection_model.ClearAndSelect)


class AudioSessionModel(QAbstractListModel):
    implements(IObserver)

    sessionAboutToBeAdded = pyqtSignal(AudioSessionItem)
    sessionAboutToBeRemoved = pyqtSignal(AudioSessionItem)
    sessionAdded = pyqtSignal(AudioSessionItem)
    sessionRemoved = pyqtSignal(AudioSessionItem)
    structureChanged = pyqtSignal()

    # The MIME types we accept in drop operations, in the order they should be handled
    accepted_mime_types = ['application/x-blink-session-list', 'application/x-blink-contact-list', 'application/x-blink-contact-uri-list']

    def __init__(self, parent=None):
        super(AudioSessionModel, self).__init__(parent)
        self.sessions = []
        self.session_list = parent.session_list

        notification_center = NotificationCenter()
        notification_center.add_observer(self, name='BlinkSessionNewIncoming')
        notification_center.add_observer(self, name='BlinkSessionWillReinitialize')
        notification_center.add_observer(self, name='BlinkSessionDidReinitializeForIncoming')
        notification_center.add_observer(self, name='BlinkSessionWillConnect')
        notification_center.add_observer(self, name='BlinkSessionDidConnect')
        notification_center.add_observer(self, name='BlinkSessionWillAddStream')
        notification_center.add_observer(self, name='BlinkSessionDidNotAddStream')
        notification_center.add_observer(self, name='BlinkSessionDidRemoveStream')
        notification_center.add_observer(self, name='BlinkSessionDidEnd')

    @property
    def active_sessions(self):
        return [session for session in self.sessions if not session.pending_removal]

    def flags(self, index):
        if index.isValid():
            return QAbstractListModel.flags(self, index) | Qt.ItemIsDropEnabled | Qt.ItemIsDragEnabled | Qt.ItemIsEditable
        else:
            return QAbstractListModel.flags(self, index)

    def rowCount(self, parent=QModelIndex()):
        return len(self.sessions)

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None
        item = self.sessions[index.row()]
        if role == Qt.UserRole:
            return item
        elif role == Qt.DisplayRole:
            return unicode(item)
        return None

    def supportedDropActions(self):
        return Qt.CopyAction | Qt.MoveAction

    def mimeTypes(self):
        return ['application/x-blink-session-list']

    def mimeData(self, indexes):
        mime_data = QMimeData()
        sessions = [self.sessions[index.row()] for index in indexes if index.isValid()]
        if sessions:
            # TODO: pass a session id which can then be fetched from the SessionManager -Saul
            mime_data.setData('application/x-blink-session-list', QByteArray())
        return mime_data

    def dropMimeData(self, mime_data, action, row, column, parent_index):
        # this is here just to keep the default Qt DnD API happy
        # the custom handler is in handleDroppedData
        return False

    def handleDroppedData(self, mime_data, action, index):
        if action == Qt.IgnoreAction:
            return True

        for mime_type in self.accepted_mime_types:
            if mime_data.hasFormat(mime_type):
                name = mime_type.replace('/', ' ').replace('-', ' ').title().replace(' ', '')
                handler = getattr(self, '_DH_%s' % name)
                return handler(mime_data, action, index)
        else:
            return False

    def _DH_ApplicationXBlinkSessionList(self, mime_data, action, index):
        session_list = self.session_list
        selection_model = session_list.selectionModel()
        source = session_list.dragged_session
        target = self.sessions[index.row()] if index.isValid() else None
        if source.client_conference is None:  # the dragged session is not in a conference yet
            if target.client_conference is not None:
                source_row = self.sessions.index(source)
                target_row = self.sessions.index(target.client_conference.sessions[-1].items.audio) + 1
                if self.beginMoveRows(QModelIndex(), source_row, source_row, QModelIndex(), target_row):
                    insert_point = target_row if source_row >= target_row else target_row-1
                    self.sessions.remove(source)
                    self.sessions.insert(insert_point, source)
                    self.endMoveRows()
                source.client_conference = target.client_conference
                session_list.scrollTo(self.index(self.sessions.index(source)), session_list.EnsureVisible) # is this even needed? -Dan
            else:
                target_row = self.sessions.index(target)
                if self.beginMoveRows(QModelIndex(), target_row, target_row, QModelIndex(), 0):
                    self.sessions.remove(target)
                    self.sessions.insert(0, target)
                    self.endMoveRows()
                source_row = self.sessions.index(source)
                if self.beginMoveRows(QModelIndex(), source_row, source_row, QModelIndex(), 1):
                    self.sessions.remove(source)
                    self.sessions.insert(1, source)
                    self.endMoveRows()
                conference = ClientConference()
                target.client_conference = conference # must add them to the conference in the same order they are in the list (target is first, source is last)
                source.client_conference = conference
                session_list.scrollToTop()
            for session in source.client_conference.sessions:
                session.items.audio.widget.selected = source.widget.selected or target.widget.selected
                session.active = source.active or target.active
            if source.active:
                source.client_conference.unhold()
        else:  # the dragged session is in a conference
            dragged = source
            sibling = next(session.items.audio for session in dragged.client_conference.sessions if session.items.audio is not dragged)
            if selection_model.isSelected(self.index(self.sessions.index(dragged))):
                selection_model.select(self.index(self.sessions.index(sibling)), selection_model.ClearAndSelect)
            if len(dragged.client_conference.sessions) == 2:
                dragged.client_conference = None
                sibling.client_conference = None
                ## eventually only move past the last conference to minimize movement. see how this feels during usage. (or sort them alphabetically with conferences at the top) -Dan
                #for position, session in enumerate(self.sessions):
                #    if session not in (dragged, sibling) and session.client_conference is None:
                #        move_point = position
                #        break
                #else:
                #    move_point = len(self.sessions)
                move_point = len(self.sessions)
                dragged_row = self.sessions.index(dragged)
                if self.beginMoveRows(QModelIndex(), dragged_row, dragged_row, QModelIndex(), move_point):
                    self.sessions.remove(dragged)
                    self.sessions.insert(move_point-1, dragged)
                    self.endMoveRows()
                move_point -= 1
                sibling_row = self.sessions.index(sibling)
                if self.beginMoveRows(QModelIndex(), sibling_row, sibling_row, QModelIndex(), move_point):
                    self.sessions.remove(sibling)
                    self.sessions.insert(move_point-1, sibling)
                    self.endMoveRows()
                session_list.scrollToBottom()
            else:
                dragged.client_conference = None
                move_point = len(self.sessions)
                dragged_row = self.sessions.index(dragged)
                if self.beginMoveRows(QModelIndex(), dragged_row, dragged_row, QModelIndex(), move_point):
                    self.sessions.remove(dragged)
                    self.sessions.append(dragged)
                    self.endMoveRows()
                session_list.scrollTo(self.index(self.sessions.index(sibling)), session_list.PositionAtCenter)
            dragged.widget.selected = False
            dragged.active = False
        self.structureChanged.emit()
        return True

    def _DH_ApplicationXBlinkContactList(self, mime_data, action, index):
        if not index.isValid():
            return
        try:
            contacts = pickle.loads(str(mime_data.data('application/x-blink-contact-list')))
        except Exception:
            return False
        session = self.sessions[index.row()]
        session_manager = SessionManager()
        for contact in contacts:
            session_manager.create_session(contact, contact.uri, [StreamDescription('audio')], sibling=session.blink_session)
        return True

    def _DH_ApplicationXBlinkContactUriList(self, mime_data, action, index):
        if not index.isValid():
            return
        try:
            contact, contact_uris = pickle.loads(str(mime_data.data('application/x-blink-contact-uri-list')))
        except Exception:
            return False
        session = self.sessions[index.row()]
        session_manager = SessionManager()
        for contact_uri in contact_uris:
            session_manager.create_session(contact, contact_uri.uri, [StreamDescription('audio')], sibling=session.blink_session)
        return True

    def _add_session(self, session):
        position = len(self.sessions)
        self.beginInsertRows(QModelIndex(), position, position)
        self.sessions.append(session)
        self.endInsertRows()
        self.session_list.openPersistentEditor(self.index(position))

    def _remove_session(self, session):
        position = self.sessions.index(session)
        self.beginRemoveRows(QModelIndex(), position, position)
        del self.sessions[position]
        self.endRemoveRows()

    def addSession(self, session):
        if session in self.sessions:
            return
        session.blink_session.clientConferenceChanged.connect(self._SH_BlinkSessionClientConferenceChanged)
        self.sessionAboutToBeAdded.emit(session)
        self._add_session(session)
        # not the right place to do this. the list should do it (else the model needs a backreference to the list), however in addSessionAndConference we can't avoid doing it -Dan
        selection_model = self.session_list.selectionModel()
        selection_model.select(self.index(self.rowCount()-1), selection_model.ClearAndSelect)
        self.sessionAdded.emit(session)
        self.structureChanged.emit()

    def addSessionAndConference(self, session, sibling):
        if session in self.sessions:
            return
        if sibling not in self.sessions:
            raise ValueError('sibling %r not in sessions list' % sibling)
        session.blink_session.clientConferenceChanged.connect(self._SH_BlinkSessionClientConferenceChanged)
        self.sessionAboutToBeAdded.emit(session)
        session_list = self.session_list
        if sibling.client_conference is not None:
            position = self.sessions.index(sibling.client_conference.sessions[-1].items.audio) + 1
            self.beginInsertRows(QModelIndex(), position, position)
            self.sessions.insert(position, session)
            self.endInsertRows()
            session_list.openPersistentEditor(self.index(position))
            session.client_conference = sibling.client_conference
            session_list.scrollTo(self.index(position), session_list.EnsureVisible) # or PositionAtBottom (is this even needed? -Dan)
        else:
            sibling_row = self.sessions.index(sibling)
            if self.beginMoveRows(QModelIndex(), sibling_row, sibling_row, QModelIndex(), 0):
                self.sessions.remove(sibling)
                self.sessions.insert(0, sibling)
                self.endMoveRows()
            self.beginInsertRows(QModelIndex(), 1, 1)
            self.sessions.insert(1, session)
            self.endInsertRows()
            session_list.openPersistentEditor(self.index(1))
            conference = ClientConference()
            sibling.client_conference = conference # must add them to the conference in the same order they are in the list (sibling first, new session last)
            session.client_conference = conference
            if sibling.active:
                conference.unhold()
            session_list.scrollToTop()
        session.widget.selected = sibling.widget.selected
        session.active = sibling.active
        self.sessionAdded.emit(session)
        self.structureChanged.emit()

    def removeSession(self, session):
        if session not in self.sessions:
            return
        self.sessionAboutToBeRemoved.emit(session)
        session_list = self.session_list
        selection_mode = session_list.selectionMode()
        session_list.setSelectionMode(session_list.NoSelection)
        if session.client_conference is not None:
            sibling = next(s.items.audio for s in session.client_conference.sessions if s.items.audio is not session)
            session_index = self.index(self.sessions.index(session))
            sibling_index = self.index(self.sessions.index(sibling))
            selection_model = session_list.selectionModel()
            if selection_model.isSelected(session_index):
                selection_model.select(sibling_index, selection_model.ClearAndSelect)
        self._remove_session(session)
        session_list.setSelectionMode(selection_mode)
        if session.client_conference is not None:
            if len(session.client_conference.sessions) == 2:
                first, last = session.client_conference.sessions
                first.client_conference = None
                last.client_conference = None
            else:
                session.client_conference = None

        session.blink_session.clientConferenceChanged.disconnect(self._SH_BlinkSessionClientConferenceChanged)
        session.delete()

        self.sessionRemoved.emit(session)
        self.structureChanged.emit()

    def conferenceSessions(self, sessions):
        session_list = self.session_list
        selected = any(session.widget.selected for session in sessions)
        active = any(session.active for session in sessions)
        conference = ClientConference()
        for position, session in enumerate(sessions):
            session_row = self.sessions.index(session)
            if self.beginMoveRows(QModelIndex(), session_row, session_row, QModelIndex(), position):
                self.sessions.remove(session)
                self.sessions.insert(position, session)
                self.endMoveRows()
            session.client_conference = conference
            session.widget.selected = selected
            session.active = active
        if active:
            conference.unhold()
        session_list.scrollToTop()
        self.structureChanged.emit()

    def breakConference(self, conference): # replace this by an endConference (or termninate/hangupConference) functionality -Dan
        sessions = [blink_session.items.audio for blink_session in conference.sessions]
        session_list = self.session_list
        selection_model = session_list.selectionModel()
        selection = selection_model.selection()
        selected_session = selection[0].topLeft().data(Qt.UserRole) if selection else None
        move_point = len(self.sessions)
        for index, session in enumerate(reversed(sessions)):
            session_row = self.sessions.index(session)
            if self.beginMoveRows(QModelIndex(), session_row, session_row, QModelIndex(), move_point-index):
                self.sessions.remove(session)
                self.sessions.insert(move_point-index-1, session)
                self.endMoveRows()
            session.client_conference = None
            session.widget.selected = session is selected_session
            session.active = session is selected_session
        session_list.scrollToBottom()
        self.structureChanged.emit()

    def _SH_BlinkSessionClientConferenceChanged(self, old_conference, new_conference): # would this better be handled by the audio session item itself? (apparently not) -Dan
        blink_session = self.sender()
        session = blink_session.items.audio

        if not new_conference:
            session.widget.position_in_conference = None
            session.widget.mute_button.hide()
        if session.widget.mute_button.isChecked():
            session.widget.mute_button.click()

        for conference in (conference for conference in (old_conference, new_conference) if conference):
            session_count = len(conference.sessions)
            if session_count == 1:
                blink_session = conference.sessions[0]
                session = blink_session.items.audio
                session.widget.position_in_conference = None
                session.widget.mute_button.hide()
            elif session_count > 1:
                for blink_session in conference.sessions:
                    session = blink_session.items.audio
                    session.widget.position_in_conference = Top if blink_session is conference.sessions[0] else Bottom if blink_session is conference.sessions[-1] else Middle
                    session.widget.mute_button.show()

    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_BlinkSessionNewIncoming(self, notification):
        session = notification.sender
        if 'audio' in session.streams:
            session_item = AudioSessionItem(session)
            self.addSession(session_item)

    def _NH_BlinkSessionDidReinitializeForIncoming(self, notification):
        session = notification.sender
        if 'audio' in session.streams:
            session_item = AudioSessionItem(session)
            self.addSession(session_item)

    def _NH_BlinkSessionWillConnect(self, notification):
        session = notification.sender
        if 'audio' in session.streams:
            session_item = AudioSessionItem(session)
            if notification.data.sibling is not None:
                self.addSessionAndConference(session_item, notification.data.sibling.items.audio)
            else:
                self.addSession(session_item)

    def _NH_BlinkSessionDidConnect(self, notification):
        session = notification.sender
        session_item = session.items.audio
        if session_item is not None and 'audio' not in session.streams:
            session_item.pending_removal = True
            call_later(5, self.removeSession, session_item)
            self.structureChanged.emit()

    def _NH_BlinkSessionWillAddStream(self, notification):
        if notification.data.stream.type == 'audio':
            if notification.sender.items.audio is not None:
                self.removeSession(notification.sender.items.audio)
            session_item = AudioSessionItem(notification.sender)
            self.addSession(session_item)

    def _NH_BlinkSessionDidNotAddStream(self, notification):
        if notification.data.stream.type == 'audio':
            session_item = notification.sender.items.audio
            session_item.pending_removal = True
            call_later(5, self.removeSession, session_item)
            self.structureChanged.emit()

    def _NH_BlinkSessionDidRemoveStream(self, notification):
        if notification.data.stream.type == 'audio':
            session_item = notification.sender.items.audio
            session_item.pending_removal = True
            call_later(5, self.removeSession, session_item)
            self.structureChanged.emit()

    def _NH_BlinkSessionDidEnd(self, notification):
        session_item = notification.sender.items.audio
        if session_item is not None and not session_item.pending_removal:
            session_item.pending_removal = True
            call_later(5, self.removeSession, session_item)
            self.structureChanged.emit()

    def _NH_BlinkSessionWillReinitialize(self, notification):
        session_item = notification.sender.items.audio
        if session_item is not None:
            self.removeSession(session_item)


# workaround class because passing context to the QShortcut constructor segfaults (fixed upstreams on 09-Apr-2013) -Dan
class QShortcut(QShortcut):
    def __init__(self, key, parent, member=None, ambiguousMember=None, context=Qt.WindowShortcut):
        super(QShortcut, self).__init__(key, parent, member, ambiguousMember)
        self.setContext(context)


class AudioSessionListView(QListView):
    implements(IObserver)

    def __init__(self, parent=None):
        super(AudioSessionListView, self).__init__(parent)
        self.setItemDelegate(AudioSessionDelegate(self))
        self.setDropIndicatorShown(False)
        self.context_menu = QMenu(self)
        self.actions = ContextMenuActions()
        self.dragged_session = None
        self.ignore_selection_changes = False
        self._pressed_position = None
        self._pressed_index = None
        self._hangup_shortcuts = []
        self._hangup_shortcuts.append(QShortcut('Ctrl+Esc', self, member=self._SH_HangupShortcutActivated, context=Qt.ApplicationShortcut))
        self._hangup_shortcuts.append(QShortcut('Ctrl+Delete', self, member=self._SH_HangupShortcutActivated, context=Qt.ApplicationShortcut))
        self._hangup_shortcuts.append(QShortcut('Ctrl+Backspace', self, member=self._SH_HangupShortcutActivated, context=Qt.ApplicationShortcut))
        self._hold_shortcut = QShortcut('Ctrl+Space', self, member=self._SH_HoldShortcutActivated, context=Qt.ApplicationShortcut)
        notification_center = NotificationCenter()
        notification_center.add_observer(self, name='BlinkActiveSessionDidChange')

    def contextMenuEvent(self, event):
        pass

    def hideEvent(self, event):
        self.context_menu.hide()

    def keyPressEvent(self, event):
        digit = chr(event.key()) if event.key() < 256 else None
        if digit is not None and digit in string.digits+string.uppercase+'#*':
            letter_map = {'2': 'ABC', '3': 'DEF', '4': 'GHI', '5': 'JKL', '6': 'MNO', '7': 'PQRS', '8': 'TUV', '9': 'WXYZ'}
            letter_map = dict(chain(*(izip(letters, repeat(digit)) for digit, letters in letter_map.iteritems())))
            for session in (s for s in self.model().sessions if s.active):
                session.send_dtmf(letter_map.get(digit, digit))
        elif event.key() in (Qt.Key_Up, Qt.Key_Down):
            selection_model = self.selectionModel()
            current_index = selection_model.currentIndex()
            if current_index.isValid():
                step = 1 if event.key() == Qt.Key_Down else -1
                conference = current_index.data(Qt.UserRole).client_conference
                new_index = current_index.sibling(current_index.row()+step, current_index.column())
                while conference is not None and new_index.isValid() and new_index.data(Qt.UserRole).client_conference is conference:
                    new_index = new_index.sibling(new_index.row()+step, new_index.column())
                if new_index.isValid():
                    selection_model.select(new_index, selection_model.ClearAndSelect)
        else:
            super(AudioSessionListView, self).keyPressEvent(event)

    def mousePressEvent(self, event):
        self._pressed_position = event.pos()
        self._pressed_index = self.indexAt(self._pressed_position)
        super(AudioSessionListView, self).mousePressEvent(event)
        selection_model = self.selectionModel()
        selected_indexes = selection_model.selectedIndexes()
        if selected_indexes:
            selection_model.setCurrentIndex(selected_indexes[0], selection_model.Select)
        else:
            selection_model.setCurrentIndex(self.model().index(-1), selection_model.Select)

    def mouseReleaseEvent(self, event):
        self._pressed_position = None
        self._pressed_index = None
        super(AudioSessionListView, self).mouseReleaseEvent(event)

    def selectionCommand(self, index, event=None):
        selection_model = self.selectionModel()
        if self.selectionMode() == self.NoSelection:
            return selection_model.NoUpdate
        elif not index.isValid() or event is None:
            return selection_model.NoUpdate
        elif event.type() == QEvent.MouseButtonPress and not selection_model.selectedIndexes():
            return selection_model.ClearAndSelect
        elif event.type() in (QEvent.MouseButtonPress, QEvent.MouseMove):
            return selection_model.NoUpdate
        elif event.type() == QEvent.MouseButtonRelease:
            return selection_model.ClearAndSelect
        else:
            return super(AudioSessionListView, self).selectionCommand(index, event)

    def selectionChanged(self, selected, deselected):
        super(AudioSessionListView, self).selectionChanged(selected, deselected)
        selected_indexes = selected.indexes()
        deselected_indexes = deselected.indexes()
        for session in (index.data(Qt.UserRole) for index in deselected_indexes):
            if session.client_conference is not None:
                for sibling in session.client_conference.sessions:
                    sibling.items.audio.widget.selected = False
            else:
                session.widget.selected = False
        for session in (index.data(Qt.UserRole) for index in selected_indexes):
            if session.client_conference is not None:
                for sibling in session.client_conference.sessions:
                    sibling.items.audio.widget.selected = True
            else:
                session.widget.selected = True
        if selected_indexes:
            self.setCurrentIndex(selected_indexes[0])
        else:
            self.setCurrentIndex(self.model().index(-1))
        self.context_menu.hide()
        #print "-- audio selection changed %s -> %s (ignore=%s)" % ([x.row() for x in deselected.indexes()], [x.row() for x in selected.indexes()], self.ignore_selection_changes)
        if self.ignore_selection_changes:
            return
        notification_center = NotificationCenter()
        selected_blink_session = selected[0].topLeft().data(Qt.UserRole).blink_session if selected else None
        deselected_blink_session = deselected[0].topLeft().data(Qt.UserRole).blink_session if deselected else None
        notification_data = NotificationData(selected_session=selected_blink_session, deselected_session=deselected_blink_session)
        notification_center.post_notification('BlinkSessionListSelectionChanged', sender=self, data=notification_data)

    def startDrag(self, supported_actions):
        if self._pressed_index is not None and self._pressed_index.isValid():
            self.dragged_session = self._pressed_index.data(Qt.UserRole)
            rect = self.visualRect(self._pressed_index)
            rect.adjust(1, 1, -1, -1)
            pixmap = QPixmap(rect.size())
            pixmap.fill(Qt.transparent)
            widget = DraggedAudioSessionWidget(self.dragged_session.widget, None)
            widget.resize(rect.size())
            widget.render(pixmap)
            drag = QDrag(self)
            drag.setPixmap(pixmap)
            drag.setMimeData(self.model().mimeData([self._pressed_index]))
            drag.setHotSpot(self._pressed_position - rect.topLeft())
            drag.exec_(supported_actions, Qt.CopyAction)
            self.dragged_session = None
            self._pressed_position = None
            self._pressed_index = None

    def dragEnterEvent(self, event):
        event_source = event.source()
        accepted_mime_types = set(self.model().accepted_mime_types)
        provided_mime_types = set(event.mimeData().formats())
        acceptable_mime_types = accepted_mime_types & provided_mime_types
        if not acceptable_mime_types:
            event.ignore()
        elif event_source is not self and 'application/x-blink-session-list' in provided_mime_types:
            event.ignore() # we don't handle drops for blink sessions from other sources
        else:
            if event_source is self:
                event.setDropAction(Qt.MoveAction)
            event.accept()
            self.setState(self.DraggingState)

    def dragLeaveEvent(self, event):
        super(AudioSessionListView, self).dragLeaveEvent(event)
        for session in self.model().sessions:
            session.widget.drop_indicator = False

    def dragMoveEvent(self, event):
        super(AudioSessionListView, self).dragMoveEvent(event)
        if event.source() is self:
            event.setDropAction(Qt.MoveAction)

        model = self.model()

        for session in model.sessions:
            session.widget.drop_indicator = False

        for mime_type in model.accepted_mime_types:
            if event.provides(mime_type):
                index = self.indexAt(event.pos())
                rect = self.visualRect(index)
                session = index.data(Qt.UserRole)
                name = mime_type.replace('/', ' ').replace('-', ' ').title().replace(' ', '')
                handler = getattr(self, '_DH_%s' % name)
                handler(event, index, rect, session)
                break
        else:
            event.ignore()

    def dropEvent(self, event):
        model = self.model()
        if event.source() is self:
            event.setDropAction(Qt.MoveAction)
        for session in self.model().sessions:
            session.widget.drop_indicator = False
        if model.handleDroppedData(event.mimeData(), event.dropAction(), self.indexAt(event.pos())):
            event.accept()
        super(AudioSessionListView, self).dropEvent(event)

    def _DH_ApplicationXBlinkSessionList(self, event, index, rect, session):
        dragged_session = self.dragged_session
        if not index.isValid():
            model = self.model()
            rect = self.viewport().rect()
            rect.setTop(self.visualRect(model.index(len(model.sessions)-1)).bottom())
            if dragged_session.client_conference is not None:
                event.accept(rect)
            else:
                event.ignore(rect)
        else:
            conference = dragged_session.client_conference or Null
            if dragged_session is session or session.blink_session in conference.sessions:
                event.ignore(rect)
            else:
                if dragged_session.client_conference is None:
                    if session.client_conference is not None:
                        for sibling in session.client_conference.sessions:
                            sibling.items.audio.widget.drop_indicator = True
                    else:
                        session.widget.drop_indicator = True
                event.accept(rect)

    def _DH_ApplicationXBlinkContactList(self, event, index, rect, session):
        model = self.model()
        if not index.isValid():
            rect = self.viewport().rect()
            rect.setTop(self.visualRect(model.index(len(model.sessions)-1)).bottom())
            event.ignore(rect)
        else:
            event.accept(rect)
            if session.client_conference is not None:
                for sibling in session.client_conference.sessions:
                    sibling.items.audio.widget.drop_indicator = True
            else:
                session.widget.drop_indicator = True

    def _DH_ApplicationXBlinkContactUriList(self, event, index, rect, session):
        model = self.model()
        if not index.isValid():
            rect = self.viewport().rect()
            rect.setTop(self.visualRect(model.index(len(model.sessions)-1)).bottom())
            event.ignore(rect)
        else:
            event.accept(rect)
            if session.client_conference is not None:
                for sibling in session.client_conference.sessions:
                    sibling.items.audio.widget.drop_indicator = True
            else:
                session.widget.drop_indicator = True

    def _SH_HangupShortcutActivated(self):
        session = self.selectedIndexes()[0].data(Qt.UserRole)
        if session.client_conference is None:
            session.widget.hangup_button.click()

    def _SH_HoldShortcutActivated(self):
        session = self.selectedIndexes()[0].data(Qt.UserRole)
        if session.client_conference is None:
            session.widget.hold_button.click()

    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_BlinkActiveSessionDidChange(self, notification):
        self.ignore_selection_changes = True
        selection_model = self.selectionModel()
        if notification.data.active_session is None:
            selection = selection_model.selection()
            # check the code in this if branch if it's needed -Dan
            #selected_blink_session = selection[0].topLeft().data(Qt.UserRole).blink_session if selection else None
            #if notification.data.previous_active_session is selected_blink_session:
            #    print "-- audio session list updating selection to None None"
            #    selection_model.clearSelection()
        else:
            model = self.model()
            position = model.sessions.index(notification.data.active_session.items.audio)
            #print "-- audio session list updating selection to", position, notification.data.active_session
            selection_model.select(model.index(position), selection_model.ClearAndSelect)
        self.ignore_selection_changes = False


# Chat sessions
#

class Container(object): pass
class Palettes(Container): pass
class PixmapContainer(Container): pass


class ChatSessionIconLabel(QLabel):
    icon = QtDynamicProperty('icon', type=QIcon)
    selectedCompositionColor = QtDynamicProperty('selectedCompositionColor', type=QColor)

    def __init__(self, parent=None):
        super(ChatSessionIconLabel, self).__init__(parent)
        self.pixmaps = PixmapContainer()
        self.icon_size = 12
        self.selectedCompositionColor = Qt.transparent
        self.icon = None

    def event(self, event):
        if event.type() == QEvent.DynamicPropertyChange and event.propertyName() in ('icon', 'selectedCompositionColor') and getattr(self, 'icon', None) is not None:
            self.pixmaps.standard = self.icon.pixmap(self.icon_size)
            self.pixmaps.selected = QPixmap(self.pixmaps.standard)
            painter = QPainter(self.pixmaps.selected)
            painter.setRenderHint(QPainter.Antialiasing, True)
            painter.setCompositionMode(QPainter.CompositionMode_SourceAtop)
            painter.fillRect(self.pixmaps.selected.rect(), self.selectedCompositionColor)
            painter.end()
        return super(ChatSessionIconLabel, self).event(event)

    def paintEvent(self, event):
        if self.icon is None or self.icon.isNull():
            return
        session_widget = self.parent().parent()
        style = self.style()
        painter = QPainter(self)
        margin = self.margin()
        rect = self.contentsRect().adjusted(margin, margin, -margin, -margin)
        if not self.isEnabled():
            option = QStyleOption()
            option.initFrom(self)
            pixmap = style.generatedIconPixmap(QIcon.Disabled, self.pixmaps.standard, option)
        elif session_widget.display_mode is session_widget.SelectedDisplayMode:
            pixmap = self.pixmaps.selected
        else:
            pixmap = self.pixmaps.standard
        align = style.visualAlignment(self.layoutDirection(), self.alignment())
        style.drawItemPixmap(painter, rect, align, pixmap)


ui_class, base_class = uic.loadUiType(Resources.get('chat_session.ui'))

class ChatSessionWidget(base_class, ui_class):
    class StandardDisplayMode:  __metaclass__ = MarkerType
    class AlternateDisplayMode: __metaclass__ = MarkerType
    class SelectedDisplayMode:  __metaclass__ = MarkerType

    def __init__(self, parent=None):
        super(ChatSessionWidget, self).__init__(parent)
        with Resources.directory:
            self.setupUi(self)
        self.palettes = Palettes()
        self.palettes.standard = self.palette()
        self.palettes.alternate = self.palette()
        self.palettes.selected = self.palette()
        self.palettes.standard.setColor(QPalette.Window,  self.palettes.standard.color(QPalette.Base))          # We modify the palettes because only the Oxygen theme honors the BackgroundRole if set
        self.palettes.alternate.setColor(QPalette.Window, self.palettes.standard.color(QPalette.AlternateBase)) # AlternateBase set to #f0f4ff or #e0e9ff by designer
        self.palettes.selected.setColor(QPalette.Window,  self.palettes.standard.color(QPalette.Highlight))     # #0066cc #0066d5 #0066dd #0066aa (0, 102, 170) '#256182' (37, 97, 130), #2960a8 (41, 96, 168), '#2d6bbc' (45, 107, 188), '#245897' (36, 88, 151) #0044aa #0055d4
        self.setBackgroundRole(QPalette.Window)
        self.display_mode = self.StandardDisplayMode
        self.hold_icon.installEventFilter(self)
        self.composing_icon.installEventFilter(self)
        self.audio_icon.installEventFilter(self)
        self.chat_icon.installEventFilter(self)
        self.video_icon.installEventFilter(self)
        self.screen_sharing_icon.installEventFilter(self)
        self.widget_layout.invalidate()
        self.widget_layout.activate()
        #self.setAttribute(103) # Qt.WA_DontShowOnScreen == 103 and is missing from pyqt, but is present in qt and pyside -Dan
        #self.show()

    def _get_display_mode(self):
        return self.__dict__['display_mode']

    def _set_display_mode(self, value):
        if value not in (self.StandardDisplayMode, self.AlternateDisplayMode, self.SelectedDisplayMode):
            raise ValueError("invalid display_mode: %r" % value)
        old_mode = self.__dict__.get('display_mode', None)
        new_mode = self.__dict__['display_mode'] = value
        if new_mode == old_mode:
            return
        if new_mode is self.StandardDisplayMode:
            self.setPalette(self.palettes.standard)
            self.setForegroundRole(QPalette.WindowText)
            self.name_label.setForegroundRole(QPalette.WindowText)
            self.info_label.setForegroundRole(QPalette.Dark)
        elif new_mode is self.AlternateDisplayMode:
            self.setPalette(self.palettes.alternate)
            self.setForegroundRole(QPalette.WindowText)
            self.name_label.setForegroundRole(QPalette.WindowText)
            self.info_label.setForegroundRole(QPalette.Dark)
        elif new_mode is self.SelectedDisplayMode:
            self.setPalette(self.palettes.selected)
            self.setForegroundRole(QPalette.HighlightedText)
            self.name_label.setForegroundRole(QPalette.HighlightedText)
            self.info_label.setForegroundRole(QPalette.HighlightedText)

    display_mode = property(_get_display_mode, _set_display_mode)
    del _get_display_mode, _set_display_mode

    def eventFilter(self, watched, event):
        if event.type() in (QEvent.ShowToParent, QEvent.HideToParent):
            self.widget_layout.invalidate()
            self.widget_layout.activate()
        return False

    def paintEvent(self, event):
        super(ChatSessionWidget, self).paintEvent(event)
        if self.display_mode == self.SelectedDisplayMode and self.state_label.state is not None:
            rect = self.state_label.geometry()
            rect.setWidth(self.width() - rect.x())
            gradient = QLinearGradient(0, 0, 1, 0)
            gradient.setCoordinateMode(QLinearGradient.ObjectBoundingMode)
            gradient.setColorAt(0.0, Qt.transparent)
            gradient.setColorAt(1.0, Qt.white)
            painter = QPainter(self)
            painter.setRenderHint(QPainter.Antialiasing, True)
            painter.fillRect(rect, QBrush(gradient))
            painter.end()

    def update_content(self, session):
        self.name_label.setText(session.name)
        self.info_label.setText(session.info)
        self.icon_label.setPixmap(session.pixmap)
        self.state_label.state = session.state
        self.hold_icon.setVisible(session.blink_session.on_hold)
        self.composing_icon.setVisible(session.remote_composing)
        self.chat_icon.setVisible('chat' in session.blink_session.streams)
        self.video_icon.setVisible('video' in session.blink_session.streams)
        self.screen_sharing_icon.setVisible('screen-sharing' in session.blink_session.streams)
        self.audio_icon.setVisible(session.blink_session.streams.types.intersection(('audio', 'video', 'screen-sharing')) == {'audio'})

del ui_class, base_class


class ChatSessionItem(object):
    implements(IObserver)

    size_hint = QSize(200, 36)

    def __init__(self, blink_session):
        self.blink_session = blink_session
        self.blink_session.items.chat = self
        self.remote_composing = False
        self.remote_composing_timer = QTimer()
        self.remote_composing_timer.timeout.connect(self._SH_RemoteComposingTimerTimeout)
        self.participants_model = ConferenceParticipantModel(blink_session)
        self.widget = ChatSessionWidget(None)
        self.widget.update_content(self)
        notification_center = NotificationCenter()
        notification_center.add_observer(self, sender=blink_session)

    def __repr__(self):
        return '%s(%r)' % (self.__class__.__name__, self.blink_session)

    @property
    def name(self):
        return self.blink_session.contact.name

    @property
    def info(self):
        return self.blink_session.contact.note or self.blink_session.contact_uri.uri

    @property
    def state(self):
        return self.blink_session.contact.state

    @property
    def icon(self):
        return self.blink_session.contact.icon

    @property
    def pixmap(self):
        return self.blink_session.contact.pixmap

    @property
    def chat_stream(self):
        return self.blink_session.streams.get('chat')

    def _get_remote_composing(self):
        return self.__dict__['remote_composing']

    def _set_remote_composing(self, value):
        old_value = self.__dict__.get('remote_composing', False)
        self.__dict__['remote_composing'] = value
        if value != old_value and self.widget is not None:
            self.widget.composing_icon.setVisible(value)
            notification_center = NotificationCenter()
            notification_center.post_notification('ChatSessionItemDidChange', sender=self)

    remote_composing = property(_get_remote_composing, _set_remote_composing)
    del _get_remote_composing, _set_remote_composing

    def end(self, delete=False):
        self.blink_session.end(delete=delete)

    def delete(self):
        notification_center = NotificationCenter()
        notification_center.remove_observer(self, sender=self.blink_session)
        self.participants_model = None
        self.blink_session.items.chat = None
        self.blink_session = None
        self.widget = None

    def update_composing_indication(self, data):
        if data.state == 'active':
            self.remote_composing = True
            refresh_rate = data.refresh if data.refresh else 120
            self.remote_composing_timer.start(refresh_rate*1000)
        elif data.state == 'idle':
            self.remote_composing = False
            self.remote_composing_timer.stop()

    def _SH_RemoteComposingTimerTimeout(self):
        self.remote_composing_timer.stop()
        self.remote_composing = False

    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_BlinkSessionContactDidChange(self, notification):
        self.widget.update_content(self)
        notification.center.post_notification('ChatSessionItemDidChange', sender=self)

    def _NH_BlinkSessionDidReinitializeForIncoming(self, notification):
        self.widget.update_content(self)
        notification.center.post_notification('ChatSessionItemDidChange', sender=self)

    def _NH_BlinkSessionDidReinitializeForOutgoing(self, notification):
        self.widget.update_content(self)
        notification.center.post_notification('ChatSessionItemDidChange', sender=self)

    def _NH_BlinkSessionWillConnect(self, notification):
        self.widget.chat_icon.setEnabled(False)
        self.widget.audio_icon.setEnabled(False)
        self.widget.video_icon.setEnabled(False)
        self.widget.screen_sharing_icon.setEnabled(False)
        self.widget.update_content(self)
        notification.center.post_notification('ChatSessionItemDidChange', sender=self)

    def _NH_BlinkSessionDidConnect(self, notification):
        self.widget.chat_icon.setEnabled(True)
        self.widget.audio_icon.setEnabled(True)
        self.widget.video_icon.setEnabled(True)
        self.widget.screen_sharing_icon.setEnabled(True)
        self.widget.update_content(self)
        notification.center.post_notification('ChatSessionItemDidChange', sender=self)

    def _NH_BlinkSessionWillAddStream(self, notification):
        icon_label = getattr(self.widget, "%s_icon" % notification.data.stream.type.replace('-', '_'))
        icon_label.setEnabled(False)
        self.widget.update_content(self)
        notification.center.post_notification('ChatSessionItemDidChange', sender=self)

    def _NH_BlinkSessionDidAddStream(self, notification):
        icon_label = getattr(self.widget, "%s_icon" % notification.data.stream.type.replace('-', '_'))
        icon_label.setEnabled(True)
        self.widget.update_content(self)
        notification.center.post_notification('ChatSessionItemDidChange', sender=self)

    def _NH_BlinkSessionDidNotAddStream(self, notification):
        icon_label = getattr(self.widget, "%s_icon" % notification.data.stream.type.replace('-', '_'))
        icon_label.setEnabled(True)
        self.widget.update_content(self)
        notification.center.post_notification('ChatSessionItemDidChange', sender=self)

    def _NH_BlinkSessionDidRemoveStream(self, notification):
        if notification.data.stream.type == 'chat':
            self.remote_composing = False
        self.widget.update_content(self)
        notification.center.post_notification('ChatSessionItemDidChange', sender=self)

    def _NH_BlinkSessionDidEnd(self, notification):
        self.remote_composing = False
        self.widget.update_content(self)
        notification.center.post_notification('ChatSessionItemDidChange', sender=self)

    def _NH_BlinkSessionDidChangeHoldState(self, notification):
        self.widget.hold_icon.setVisible(self.blink_session.on_hold)
        notification.center.post_notification('ChatSessionItemDidChange', sender=self)

    def _NH_BlinkSessionDidChangeRecordingState(self, notification):
        notification.center.post_notification('ChatSessionItemDidChange', sender=self)


class ChatSessionDelegate(QStyledItemDelegate, ColorHelperMixin):
    def __init__(self, parent=None):
        super(ChatSessionDelegate, self).__init__(parent)

    def editorEvent(self, event, model, option, index):
        if event.type()==QEvent.MouseButtonRelease and event.button()==Qt.LeftButton and event.modifiers()==Qt.NoModifier:
            arrow_rect = option.rect.adjusted(option.rect.width()-14, option.rect.height()/2, 0, 0)  # bottom half of the rightmost 14 pixels
            cross_rect = option.rect.adjusted(option.rect.width()-14, 0, 0, -option.rect.height()/2) # top half of the rightmost 14 pixels
            if arrow_rect.contains(event.pos()):
                session_list = self.parent()
                session_list.animation.setDirection(QPropertyAnimation.Backward)
                session_list.animation.start()
                return True
            elif cross_rect.contains(event.pos()):
                session = index.data(Qt.UserRole)
                session.end(delete=True)
                return True
        return super(ChatSessionDelegate, self).editorEvent(event, model, option, index)

    def paint(self, painter, option, index):
        session = index.data(Qt.UserRole)
        if option.state & QStyle.State_Selected:
            session.widget.display_mode = session.widget.SelectedDisplayMode
        elif index.row() % 2 == 0:
            session.widget.display_mode = session.widget.StandardDisplayMode
        else:
            session.widget.display_mode = session.widget.AlternateDisplayMode
        session.widget.setFixedSize(option.rect.size())

        painter.save()
        painter.drawPixmap(option.rect, QPixmap.grabWidget(session.widget))
        if option.state & QStyle.State_MouseOver:
            self.drawSessionIndicators(session, option, painter, session.widget)
        if 0 and (option.state & QStyle.State_MouseOver):
            painter.setRenderHint(QPainter.Antialiasing, True)
            if option.state & QStyle.State_Selected:
                painter.fillRect(option.rect, QColor(240, 244, 255, 40))
            else:
                painter.setCompositionMode(QPainter.CompositionMode_DestinationIn)
                painter.fillRect(option.rect, QColor(240, 244, 255, 230))
        painter.restore()

    def drawSessionIndicators(self, session, option, painter, widget):
        pen_thickness = 1.6

        if widget.state_label.state is not None:
            foreground_color = option.palette.color(QPalette.Normal, QPalette.WindowText)
            background_color = widget.state_label.state_colors[widget.state_label.state]
            base_contrast_color = self.calc_light_color(background_color)
            gradient = QLinearGradient(0, 0, 1, 0)
            gradient.setCoordinateMode(QLinearGradient.ObjectBoundingMode)
            gradient.setColorAt(0.0, self.color_with_alpha(base_contrast_color, 0.3*255))
            gradient.setColorAt(1.0, self.color_with_alpha(base_contrast_color, 0.8*255))
            contrast_color = QBrush(gradient)
        else:
            #foreground_color = option.palette.color(QPalette.Normal, QPalette.WindowText)
            #background_color = option.palette.color(QPalette.Window)
            foreground_color = widget.palette().color(QPalette.Normal, widget.foregroundRole())
            background_color = widget.palette().color(widget.backgroundRole())
            contrast_color = self.calc_light_color(background_color)
        line_color = self.deco_color(background_color, foreground_color)

        pen = QPen(line_color, pen_thickness, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
        contrast_pen = QPen(contrast_color, pen_thickness, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)

        # draw the expansion indicator at the bottom (works best with a state_label of width 14)
        arrow_rect = QRect(0, 0, 14, 14)
        arrow_rect.moveBottomRight(widget.state_label.geometry().bottomRight())
        arrow_rect.translate(option.rect.topLeft())

        arrow = QPolygonF([QPointF(3, 1.5), QPointF(-0.5, -2.5), QPointF(-4, 1.5)])
        arrow.translate(2, 1)

        painter.save()
        painter.setRenderHint(QPainter.Antialiasing, True)
        painter.setCompositionMode(QPainter.CompositionMode_SourceOver)
        painter.translate(arrow_rect.center())
        painter.translate(0, +1)
        painter.setPen(contrast_pen)
        painter.drawPolyline(arrow)
        painter.translate(0, -1)
        painter.setPen(pen)
        painter.drawPolyline(arrow)
        painter.restore()

        # draw the close indicator at the top (works best with a state_label of width 14)
        cross_rect = QRect(0, 0, 14, 14)
        cross_rect.moveTopRight(widget.state_label.geometry().topRight())
        cross_rect.translate(option.rect.topLeft())

        painter.save()
        painter.setRenderHint(QPainter.Antialiasing, True)
        painter.setCompositionMode(QPainter.CompositionMode_SourceOver)
        painter.translate(cross_rect.center())
        painter.translate(+1.5, +1)
        painter.translate(0, +1)
        painter.setPen(contrast_pen)
        painter.drawLine(-3.5, -3.5, 3.5, 3.5)
        painter.drawLine(-3.5, 3.5, 3.5, -3.5)
        painter.translate(0, -1)
        painter.setPen(pen)
        painter.drawLine(-3.5, -3.5, 3.5, 3.5)
        painter.drawLine(-3.5, 3.5, 3.5, -3.5)
        painter.restore()

    def sizeHint(self, option, index):
        return index.data(Qt.SizeHintRole)


class ChatSessionModel(QAbstractListModel):
    implements(IObserver)

    sessionAboutToBeAdded = pyqtSignal(ChatSessionItem)
    sessionAboutToBeRemoved = pyqtSignal(ChatSessionItem)
    sessionAdded = pyqtSignal(ChatSessionItem)
    sessionRemoved = pyqtSignal(ChatSessionItem)

    # The MIME types we accept in drop operations, in the order they should be handled
    accepted_mime_types = ['application/x-blink-contact-list', 'text/uri-list']

    def __init__(self, parent=None):
        super(ChatSessionModel, self).__init__(parent)
        self.sessions = []

        notification_center = NotificationCenter()
        notification_center.add_observer(self, name='BlinkSessionNewIncoming')
        notification_center.add_observer(self, name='BlinkSessionNewOutgoing')
        notification_center.add_observer(self, name='BlinkSessionWasDeleted')
        notification_center.add_observer(self, name='ChatSessionItemDidChange')

    def flags(self, index):
        if index.isValid():
            return QAbstractListModel.flags(self, index) | Qt.ItemIsDropEnabled
        else:
            return QAbstractListModel.flags(self, index) | Qt.ItemIsDropEnabled

    def rowCount(self, parent=QModelIndex()):
        return len(self.sessions)

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None
        item = self.sessions[index.row()]
        if role == Qt.UserRole:
            return item
        elif role == Qt.SizeHintRole:
            return item.size_hint
        elif role == Qt.DisplayRole:
            return unicode(item)
        return None

    def supportedDropActions(self):
        return Qt.CopyAction# | Qt.MoveAction

    def dropMimeData(self, mime_data, action, row, column, parent_index):
        # this is here just to keep the default Qt DnD API happy
        # the custom handler is in handleDroppedData
        return False

    def handleDroppedData(self, mime_data, action, index):
        if action == Qt.IgnoreAction:
            return True

        for mime_type in self.accepted_mime_types:
            if mime_data.hasFormat(mime_type):
                name = mime_type.replace('/', ' ').replace('-', ' ').title().replace(' ', '')
                handler = getattr(self, '_DH_%s' % name)
                return handler(mime_data, action, index)
        else:
            return False

    def _DH_ApplicationXBlinkContactList(self, mime_data, action, index):
        return True

    def _DH_TextUriList(self, mime_data, action, index):
        return False

    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_BlinkSessionNewIncoming(self, notification):
        self.addSession(ChatSessionItem(notification.sender))

    def _NH_BlinkSessionNewOutgoing(self, notification):
        self.addSession(ChatSessionItem(notification.sender))

    def _NH_BlinkSessionWasDeleted(self, notification):
        self.removeSession(notification.sender.items.chat)

    def _NH_ChatSessionItemDidChange(self, notification):
        index = self.index(self.sessions.index(notification.sender))
        self.dataChanged.emit(index, index)

    def _find_insertion_point(self, session):
        for position, item in enumerate(self.sessions):
            if item.name > session.name:
                break
        else:
            position = len(self.sessions)
        return position

    def _add_session(self, session):
        position = self._find_insertion_point(session)
        self.beginInsertRows(QModelIndex(), position, position)
        self.sessions.insert(position, session)
        self.endInsertRows()

    def _pop_session(self, session):
        position = self.sessions.index(session)
        self.beginRemoveRows(QModelIndex(), position, position)
        del self.sessions[position]
        self.endRemoveRows()
        return session

    def addSession(self, session):
        if session in self.sessions:
            return
        self.sessionAboutToBeAdded.emit(session)
        self._add_session(session)
        self.sessionAdded.emit(session)

    def removeSession(self, session):
        if session not in self.sessions:
            return
        self.sessionAboutToBeRemoved.emit(session)
        self._pop_session(session).delete()
        self.sessionRemoved.emit(session)


class ChatSessionListView(QListView):
    implements(IObserver)

    def __init__(self, chat_window):
        super(ChatSessionListView, self).__init__(chat_window.session_panel)
        self.chat_window = chat_window
        self.setItemDelegate(ChatSessionDelegate(self))

        self.setMouseTracking(True)
        self.setAlternatingRowColors(True)
        self.setAutoFillBackground(True)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        #self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) # default
        self.setDragEnabled(False) # default
        #self.setDropIndicatorShown(True)
        self.setDragDropMode(QListView.DropOnly)
        self.setSelectionMode(QListView.SingleSelection) # default

        self.setStyleSheet("""QListView { border: 1px inset palette(dark); border-radius: 3px; }""")
        self.animation = QPropertyAnimation(self, 'geometry')
        self.animation.setDuration(250)
        self.animation.setEasingCurve(QEasingCurve.Linear)
        self.animation.finished.connect(self._SH_AnimationFinished)
        self.context_menu = QMenu(self)
        self.actions = ContextMenuActions()
        self.drop_indicator_index = QModelIndex()
        self.ignore_selection_changes = False
        self.doubleClicked.connect(self._SH_DoubleClicked) # activated is emitted on single click
        chat_window.session_panel.installEventFilter(self)

        notification_center = NotificationCenter()
        notification_center.add_observer(self, name='BlinkActiveSessionDidChange')

    def selectionChanged(self, selected, deselected):
        super(ChatSessionListView, self).selectionChanged(selected, deselected)
        selection_model = self.selectionModel()
        selection = selection_model.selection()
        if selection_model.currentIndex() not in selection:
            index = selection.indexes()[0] if not selection.isEmpty() else self.model().index(-1)
            selection_model.setCurrentIndex(index, selection_model.Select)
        self.context_menu.hide()
        if self.ignore_selection_changes:
            return
        notification_center = NotificationCenter()
        selected_blink_session = selected[0].topLeft().data(Qt.UserRole).blink_session if selected else None
        deselected_blink_session = deselected[0].topLeft().data(Qt.UserRole).blink_session if deselected else None
        notification_data = NotificationData(selected_session=selected_blink_session, deselected_session=deselected_blink_session)
        notification_center.post_notification('BlinkSessionListSelectionChanged', sender=self, data=notification_data)

    def eventFilter(self, watched, event):
        if event.type() == QEvent.Resize:
            new_size = event.size()
            geometry = self.animation.endValue()
            if geometry is not None:
                old_size = geometry.size()
                geometry.setSize(new_size)
                self.animation.setEndValue(geometry)
                geometry = self.animation.startValue()
                geometry.setWidth(geometry.width() + new_size.width() - old_size.width())
                self.animation.setStartValue(geometry)
            self.resize(new_size)
        return False

    def contextMenuEvent(self, event):
        pass

    def hideEvent(self, event):
        self.context_menu.hide()

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Escape and self.selectionModel().selection():
            self.animation.setDirection(QPropertyAnimation.Backward)
            self.animation.start()
        else:
            super(ChatSessionListView, self).keyPressEvent(event)

    def paintEvent(self, event):
        super(ChatSessionListView, self).paintEvent(event)
        if self.drop_indicator_index.isValid():
            rect = self.visualRect(self.drop_indicator_index)
            painter = QPainter(self.viewport())
            painter.setRenderHint(QPainter.Antialiasing, True)
            painter.setBrush(Qt.NoBrush)
            painter.setPen(QPen(QBrush(QColor('#dc3169')), 2.0))
            painter.drawRoundedRect(rect.adjusted(1, 1, -1, -1), 3, 3)
            painter.end()

    def dragEnterEvent(self, event):
        model = self.model()
        accepted_mime_types = set(model.accepted_mime_types)
        provided_mime_types = set(event.mimeData().formats())
        acceptable_mime_types = accepted_mime_types & provided_mime_types
        if not acceptable_mime_types:
            event.ignore()
        else:
            event.accept()
            self.setState(self.DraggingState)

    def dragLeaveEvent(self, event):
        super(ChatSessionListView, self).dragLeaveEvent(event)
        self.viewport().update(self.visualRect(self.drop_indicator_index))
        self.drop_indicator_index = QModelIndex()

    def dragMoveEvent(self, event):
        super(ChatSessionListView, self).dragMoveEvent(event)
        model = self.model()
        for mime_type in model.accepted_mime_types:
            if event.provides(mime_type):
                self.viewport().update(self.visualRect(self.drop_indicator_index))
                self.drop_indicator_index = QModelIndex()
                index = self.indexAt(event.pos())
                rect = self.visualRect(index)
                item = index.data(Qt.UserRole)
                name = mime_type.replace('/', ' ').replace('-', ' ').title().replace(' ', '')
                handler = getattr(self, '_DH_%s' % name)
                handler(event, index, rect, item)
                self.viewport().update(self.visualRect(self.drop_indicator_index))
                break
        else:
            event.ignore()

    def dropEvent(self, event):
        model = self.model()
        if event.source() is self:
            event.setDropAction(Qt.MoveAction)
        if model.handleDroppedData(event.mimeData(), event.dropAction(), self.indexAt(event.pos())):
            event.accept()
        super(ChatSessionListView, self).dropEvent(event)
        self.viewport().update(self.visualRect(self.drop_indicator_index))
        self.drop_indicator_index = QModelIndex()

    def _DH_ApplicationXBlinkContactList(self, event, index, rect, item):
        event.accept(rect)

    def _DH_TextUriList(self, event, index, rect, item):
        model = self.model()
        if not index.isValid():
            rect = self.viewport().rect()
            rect.setTop(self.visualRect(model.index(len(model.sessions)-1)).bottom())
        event.accept(rect)
        self.drop_indicator_index = index

    def _SH_AnimationFinished(self):
        if self.animation.direction() == QPropertyAnimation.Forward:
            self.setFocus(Qt.OtherFocusReason)
        else:
            self.hide()
            current_tab = self.chat_window.tab_widget.currentWidget()
            current_tab.chat_input.setFocus(Qt.OtherFocusReason)

    def _SH_DoubleClicked(self, index):
        self.animation.setDirection(QPropertyAnimation.Backward)
        self.animation.start()

    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_BlinkActiveSessionDidChange(self, notification):
        self.ignore_selection_changes = True
        selection_model = self.selectionModel()
        if notification.data.active_session is None:
            selection = selection_model.selection()
            # check the code in this if branch if it's needed -Dan (if not also remove previous_active_session maybe)
            #selected_blink_session = selection[0].topLeft().data(Qt.UserRole).blink_session if selection else None
            #if notification.data.previous_active_session is selected_blink_session:
            #    print "-- chat session list updating selection to None None"
            #    selection_model.clearSelection()
        else:
            model = self.model()
            position = model.sessions.index(notification.data.active_session.items.chat)
            #print "-- chat session list updating selection to", position, notification.data.active_session
            selection_model.select(model.index(position), selection_model.ClearAndSelect)
        self.ignore_selection_changes = False


# Conference participants
#

class ConferenceParticipantWidget(ChatSessionWidget):
    def update_content(self, participant):
        self.setDisabled(participant.pending_request)
        self.name_label.setText(participant.name)
        self.info_label.setText(participant.info)
        self.icon_label.setPixmap(participant.pixmap)
        self.state_label.state = participant.state
        self.hold_icon.setVisible(participant.on_hold)
        self.composing_icon.setVisible(participant.is_composing)
        self.chat_icon.setVisible('chat' in participant.active_media)
        self.video_icon.setVisible('video' in participant.active_media)
        self.screen_sharing_icon.setVisible('screen-sharing' in participant.active_media)
        self.audio_icon.setVisible(participant.active_media.intersection(('audio', 'video', 'screen-sharing')) == {'audio'})


class ConferenceParticipantItem(object):
    implements(IObserver)

    size_hint = QSize(200, 36)

    def __init__(self, participant):
        self.participant = participant
        self.widget = ConferenceParticipantWidget(None)
        self.widget.update_content(self)
        notification_center = NotificationCenter()
        notification_center.add_observer(ObserverWeakrefProxy(self), sender=participant)

    def __repr__(self):
        return '%s(%r)' % (self.__class__.__name__, self.participant)

    @property
    def pending_request(self):
        return self.participant.pending_request

    @property
    def name(self):
        if self.participant.contact.type == 'dummy':
            return self.participant.display_name or self.participant.contact.name
        else:
            return self.participant.contact.name

    @property
    def info(self):
        return self.participant.request_status or self.participant.contact.note or self.participant.uri

    @property
    def state(self):
        return self.participant.contact.state

    @property
    def on_hold(self):
        return self.participant.on_hold

    @property
    def is_composing(self):
        return self.participant.is_composing

    @property
    def active_media(self):
        return self.participant.active_media

    @property
    def icon(self):
        return self.participant.contact.icon

    @property
    def pixmap(self):
        return self.participant.contact.pixmap

    @run_in_gui_thread
    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_ConferenceParticipantDidChange(self, notification):
        self.widget.update_content(self)
        notification.center.post_notification('ConferenceParticipantItemDidChange', sender=self)


class ConferenceParticipantDelegate(QStyledItemDelegate, ColorHelperMixin):
    def __init__(self, parent=None):
        super(ConferenceParticipantDelegate, self).__init__(parent)

    def editorEvent(self, event, model, option, index):
        if event.type()==QEvent.MouseButtonRelease and event.button()==Qt.LeftButton and event.modifiers()==Qt.NoModifier:
            cross_rect = option.rect.adjusted(option.rect.width()-14, 0, 0, -option.rect.height()/2) # top half of the rightmost 14 pixels
            if cross_rect.contains(event.pos()):
                item = index.data(Qt.UserRole)
                model.session.server_conference.remove_participant(item.participant)
                return True
        return super(ConferenceParticipantDelegate, self).editorEvent(event, model, option, index)

    def paint(self, painter, option, index):
        participant = index.data(Qt.UserRole)
        if option.state & QStyle.State_Selected:
            participant.widget.display_mode = participant.widget.SelectedDisplayMode
        elif index.row() % 2 == 0:
            participant.widget.display_mode = participant.widget.StandardDisplayMode
        else:
            participant.widget.display_mode = participant.widget.AlternateDisplayMode
        participant.widget.setFixedSize(option.rect.size())

        painter.save()
        painter.drawPixmap(option.rect, QPixmap.grabWidget(participant.widget))
        if (option.state & QStyle.State_MouseOver) and participant.widget.isEnabled():
            self.drawRemoveIndicator(participant, option, painter, participant.widget)
        if 0 and (option.state & QStyle.State_MouseOver):
            painter.setRenderHint(QPainter.Antialiasing, True)
            if option.state & QStyle.State_Selected:
                painter.fillRect(option.rect, QColor(240, 244, 255, 40))
            else:
                painter.setCompositionMode(QPainter.CompositionMode_DestinationIn)
                painter.fillRect(option.rect, QColor(240, 244, 255, 230))
        painter.restore()

    def drawRemoveIndicator(self, participant, option, painter, widget):
        pen_thickness = 1.6

        if widget.state_label.state is not None:
            foreground_color = option.palette.color(QPalette.Normal, QPalette.WindowText)
            background_color = widget.state_label.state_colors[widget.state_label.state]
            base_contrast_color = self.calc_light_color(background_color)
            gradient = QLinearGradient(0, 0, 1, 0)
            gradient.setCoordinateMode(QLinearGradient.ObjectBoundingMode)
            gradient.setColorAt(0.0, self.color_with_alpha(base_contrast_color, 0.3*255))
            gradient.setColorAt(1.0, self.color_with_alpha(base_contrast_color, 0.8*255))
            contrast_color = QBrush(gradient)
        else:
            foreground_color = widget.palette().color(QPalette.Normal, widget.foregroundRole())
            background_color = widget.palette().color(widget.backgroundRole())
            contrast_color = self.calc_light_color(background_color)
        line_color = self.deco_color(background_color, foreground_color)

        pen = QPen(line_color, pen_thickness, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
        contrast_pen = QPen(contrast_color, pen_thickness, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)

        # draw the remove indicator at the top (works best with a state_label of width 14)
        cross_rect = QRect(0, 0, 14, 14)
        cross_rect.moveTopRight(widget.state_label.geometry().topRight())
        cross_rect.translate(option.rect.topLeft())

        painter.save()
        painter.setRenderHint(QPainter.Antialiasing, True)
        painter.setCompositionMode(QPainter.CompositionMode_SourceOver)
        painter.translate(cross_rect.center())
        painter.translate(+1.5, +1)
        painter.translate(0, +1)
        painter.setPen(contrast_pen)
        painter.drawLine(-3.5, -3.5, 3.5, 3.5)
        painter.drawLine(-3.5, 3.5, 3.5, -3.5)
        painter.translate(0, -1)
        painter.setPen(pen)
        painter.drawLine(-3.5, -3.5, 3.5, 3.5)
        painter.drawLine(-3.5, 3.5, 3.5, -3.5)
        painter.restore()

    def sizeHint(self, option, index):
        return index.data(Qt.SizeHintRole)


class ConferenceParticipantModel(QAbstractListModel):
    implements(IObserver)

    participantAboutToBeAdded = pyqtSignal(ConferenceParticipantItem)
    participantAboutToBeRemoved = pyqtSignal(ConferenceParticipantItem)
    participantAdded = pyqtSignal(ConferenceParticipantItem)
    participantRemoved = pyqtSignal(ConferenceParticipantItem)

    # The MIME types we accept in drop operations, in the order they should be handled
    accepted_mime_types = ['application/x-blink-contact-list', 'application/x-blink-contact-uri-list', 'text/uri-list']

    def __init__(self, session, parent=None):
        super(ConferenceParticipantModel, self).__init__(parent)
        self.session = session
        self.participants = []

        notification_center = NotificationCenter()
        notification_center.add_observer(self, sender=session)

    def flags(self, index):
        if index.isValid():
            return QAbstractListModel.flags(self, index) | Qt.ItemIsDropEnabled
        else:
            return QAbstractListModel.flags(self, index) | Qt.ItemIsDropEnabled

    def rowCount(self, parent=QModelIndex()):
        return len(self.participants)

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None
        item = self.participants[index.row()]
        if role == Qt.UserRole:
            return item
        elif role == Qt.SizeHintRole:
            return item.size_hint
        elif role == Qt.DisplayRole:
            return unicode(item)
        return None

    def supportedDropActions(self):
        return Qt.CopyAction# | Qt.MoveAction

    def dropMimeData(self, mime_data, action, row, column, parent_index):
        # this is here just to keep the default Qt DnD API happy
        # the custom handler is in handleDroppedData
        return False

    def handleDroppedData(self, mime_data, action, index):
        if action == Qt.IgnoreAction:
            return True

        for mime_type in self.accepted_mime_types:
            if mime_data.hasFormat(mime_type):
                name = mime_type.replace('/', ' ').replace('-', ' ').title().replace(' ', '')
                handler = getattr(self, '_DH_%s' % name)
                return handler(mime_data, action, index)
        else:
            return False

    def _DH_ApplicationXBlinkContactList(self, mime_data, action, index):
        try:
            contacts = pickle.loads(str(mime_data.data('application/x-blink-contact-list')))
        except Exception:
            return False
        for contact in contacts:
            self.session.server_conference.add_participant(contact, contact.uri)
        return True

    def _DH_ApplicationXBlinkContactUriList(self, mime_data, action, index):
        try:
            contact, contact_uris = pickle.loads(str(mime_data.data('application/x-blink-contact-uri-list')))
        except Exception:
            return False
        for contact_uri in contact_uris:
            self.session.server_conference.add_participant(contact, contact_uri.uri)
        return True

    def _DH_TextUriList(self, mime_data, action, index):
        return False

    @run_in_gui_thread
    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_BlinkSessionDidEnd(self, notification):
        self.clear()

    def _NH_BlinkSessionWasDeleted(self, notification):
        notification.center.remove_observer(self, sender=self.session)
        self.session = None

    def _NH_BlinkSessionWillAddParticipant(self, notification):
        self.addParticipant(ConferenceParticipantItem(notification.data.participant))

    def _NH_BlinkSessionDidNotAddParticipant(self, notification):
        self.removeParticipant(notification.data.participant.participant_item)

    def _NH_BlinkSessionDidRemoveParticipant(self, notification):
        self.removeParticipant(notification.data.participant.participant_item)

    def _NH_ConferenceParticipantItemDidChange(self, notification):
        index = self.index(self.participants.index(notification.sender))
        self.dataChanged.emit(index, index)

    def _find_insertion_point(self, participant):
        for position, item in enumerate(self.participants):
            if item.name > participant.name:
                break
        else:
            position = len(self.participants)
        return position

    def _add_participant(self, participant):
        position = self._find_insertion_point(participant)
        self.beginInsertRows(QModelIndex(), position, position)
        self.participants.insert(position, participant)
        self.endInsertRows()

    def _pop_participant(self, participant):
        position = self.participants.index(participant)
        self.beginRemoveRows(QModelIndex(), position, position)
        del self.participants[position]
        self.endRemoveRows()
        return participant

    def addParticipant(self, participant):
        if participant in self.participants:
            return
        participant.participant.participant_item = participant # add a back reference to this item so we can find it later without iterating all participants
        self.participantAboutToBeAdded.emit(participant)
        self._add_participant(participant)
        self.participantAdded.emit(participant)
        notification_center = NotificationCenter()
        notification_center.add_observer(self, sender=participant)

    def removeParticipant(self, participant):
        if participant not in self.participants:
            return
        notification_center = NotificationCenter()
        notification_center.remove_observer(self, sender=participant)
        del participant.participant.participant_item
        self.participantAboutToBeRemoved.emit(participant)
        self._pop_participant(participant)
        self.participantRemoved.emit(participant)

    def clear(self):
        notification_center = NotificationCenter()
        self.beginResetModel()
        for participant in self.participants:
            del participant.participant.participant_item
            notification_center.remove_observer(self, sender=participant)
        self.participants = []
        self.endResetModel()


class ConferenceParticipantListView(QListView, ColorHelperMixin):
    def __init__(self, parent=None):
        super(ConferenceParticipantListView, self).__init__(parent)
        self.setItemDelegate(ConferenceParticipantDelegate(self))
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.context_menu = QMenu(self)
        self.actions = ContextMenuActions()
        self.paint_drop_indicator = False

    def setModel(self, model):
        selection_model = self.selectionModel()
        if selection_model is not None:
            selection_model.deleteLater()
        super(ConferenceParticipantListView, self).setModel(model)

    def contextMenuEvent(self, event):
        pass

    def hideEvent(self, event):
        self.context_menu.hide()

    def paintEvent(self, event):
        super(ConferenceParticipantListView, self).paintEvent(event)
        if self.paint_drop_indicator:
            rect = self.viewport().rect() # or should this be self.contentsRect() ? -Dan
            #color = QColor('#b91959')
            #color = QColor('#00aaff')
            #color = QColor('#55aaff')
            #color = QColor('#00aa00')
            #color = QColor('#aa007f')
            #color = QColor('#dd44aa')
            color = QColor('#aa007f')
            pen_color = self.color_with_alpha(color, 120)
            brush_color = self.color_with_alpha(color, 10)
            painter = QPainter(self.viewport())
            painter.setRenderHint(QPainter.Antialiasing, True)
            painter.setBrush(brush_color)
            painter.setPen(QPen(pen_color, 1.6))
            painter.drawRoundedRect(rect.adjusted(1, 1, -1, -1), 3, 3)
            painter.end()

    def dragEnterEvent(self, event):
        model = self.model()
        accepted_mime_types = set(model.accepted_mime_types)
        provided_mime_types = set(event.mimeData().formats())
        acceptable_mime_types = accepted_mime_types & provided_mime_types
        if not acceptable_mime_types:
            event.ignore()
        else:
            event.accept()
            self.setState(self.DraggingState)

    def dragLeaveEvent(self, event):
        super(ConferenceParticipantListView, self).dragLeaveEvent(event)
        self.paint_drop_indicator = False
        self.viewport().update()

    def dragMoveEvent(self, event):
        super(ConferenceParticipantListView, self).dragMoveEvent(event)
        model = self.model()
        for mime_type in model.accepted_mime_types:
            if event.provides(mime_type):
                handler = getattr(self, '_DH_%s' % mime_type.replace('/', ' ').replace('-', ' ').title().replace(' ', ''))
                handler(event)
                self.viewport().update()
                break
        else:
            event.ignore()

    def dropEvent(self, event):
        model = self.model()
        if event.source() is self:
            event.setDropAction(Qt.MoveAction)
        if model.handleDroppedData(event.mimeData(), event.dropAction(), self.indexAt(event.pos())):
            event.accept()
        super(ConferenceParticipantListView, self).dropEvent(event)
        self.paint_drop_indicator = False
        self.viewport().update()

    def _DH_ApplicationXBlinkContactList(self, event):
        event.accept(self.viewport().rect())
        self.paint_drop_indicator = True

    def _DH_ApplicationXBlinkContactUriList(self, event):
        event.accept(self.viewport().rect())
        self.paint_drop_indicator = True

    def _DH_TextUriList(self, event):
        event.ignore(self.viewport().rect())
        #event.accept(self.viewport().rect())
        #self.paint_drop_indicator = True


# Session management
#

ui_class, base_class = uic.loadUiType(Resources.get('incoming_dialog.ui'))

class IncomingDialog(base_class, ui_class):
    def __init__(self, parent=None):
        super(IncomingDialog, self).__init__(parent)
        self.setWindowFlags(Qt.WindowStaysOnTopHint)
        self.setAttribute(Qt.WA_DeleteOnClose)
        with Resources.directory:
            self.setupUi(self)
        font = self.username_label.font()
        font.setPointSizeF(self.uri_label.fontInfo().pointSizeF() + 3)
        font.setFamily("Sans Serif")
        self.username_label.setFont(font)
        font = self.note_label.font()
        font.setPointSizeF(self.uri_label.fontInfo().pointSizeF() - 1)
        self.note_label.setFont(font)
        self.reject_mode = 'ignore'
        self.busy_button.released.connect(self._set_busy_mode)
        self.reject_button.released.connect(self._set_reject_mode)
        for stream in self.streams:
            stream.toggled.connect(self._update_accept_button)
            stream.hidden.connect(self._update_streams_layout)
            stream.shown.connect(self._update_streams_layout)
        self.screensharing_stream.hidden.connect(self.screensharing_label.hide)
        self.screensharing_stream.shown.connect(self.screensharing_label.show)
        for stream in self.streams:
            stream.hide()
        self.position = None

    def show(self, activate=True, position=1):
        blink = QApplication.instance()
        screen_geometry = blink.desktop().screenGeometry(self)
        available_geometry = blink.desktop().availableGeometry(self)
        main_window_geometry = blink.main_window.geometry()
        main_window_framegeometry = blink.main_window.frameGeometry()

        horizontal_decorations = main_window_framegeometry.width() - main_window_geometry.width()
        vertical_decorations = main_window_framegeometry.height() - main_window_geometry.height()
        width = limit(self.sizeHint().width(), min=self.minimumSize().width(), max=min(self.maximumSize().width(), available_geometry.width()-horizontal_decorations))
        height = limit(self.sizeHint().height(), min=self.minimumSize().height(), max=min(self.maximumSize().height(), available_geometry.height()-vertical_decorations))
        total_width = width + horizontal_decorations
        total_height = height + vertical_decorations
        x = limit(screen_geometry.center().x() - total_width/2, min=available_geometry.left(), max=available_geometry.right()-total_width)
        if position is None:
            y = -1
        elif position % 2 == 0:
            y = screen_geometry.center().y() + (position-1)*total_height/2
        else:
            y = screen_geometry.center().y() - position*total_height/2
        if available_geometry.top() <= y <= available_geometry.bottom() - total_height:
            self.setGeometry(x, y, width, height)
        else:
            self.resize(width, height)

        self.position = position
        self.setAttribute(Qt.WA_ShowWithoutActivating, not activate)
        super(IncomingDialog, self).show()

    @property
    def streams(self):
        return (self.audio_stream, self.chat_stream, self.screensharing_stream, self.video_stream)

    @property
    def accepted_streams(self):
        return [stream for stream in self.streams if stream.in_use and stream.accepted]

    def _set_busy_mode(self):
        self.reject_mode = 'busy'

    def _set_reject_mode(self):
        self.reject_mode = 'reject'

    def _update_accept_button(self):
        was_enabled = self.accept_button.isEnabled()
        self.accept_button.setEnabled(len(self.accepted_streams) > 0)
        if self.accept_button.isEnabled() != was_enabled:
            self.accept_button.setFocus()

    def _update_streams_layout(self):
        if len([stream for stream in self.streams if stream.in_use]) > 1:
            self.audio_stream.active = True
            self.chat_stream.active = True
            self.screensharing_stream.active = True
            self.video_stream.active = True
            self.note_label.setText(u'To refuse a stream click its icon')
        else:
            self.audio_stream.active = False
            self.chat_stream.active = False
            self.screensharing_stream.active = False
            self.video_stream.active = False
            if self.audio_stream.in_use:
                self.note_label.setText(u'Audio call')
            elif self.chat_stream.in_use:
                self.note_label.setText(u'Chat session')
            elif self.video_stream.in_use:
                self.note_label.setText(u'Video call')
            elif self.screensharing_stream.in_use:
                self.note_label.setText(u'Screen sharing request')
            else:
                self.note_label.setText(u'')
        self._update_accept_button()

del ui_class, base_class


class IncomingRequest(QObject):
    accepted = pyqtSignal(object)
    rejected = pyqtSignal(object, str)

    def __init__(self, dialog, session, contact, contact_uri, proposal=False, audio_stream=None, video_stream=None, chat_stream=None, screensharing_stream=None):
        super(IncomingRequest, self).__init__()
        self.dialog = dialog
        self.session = session
        self.contact = contact
        self.contact_uri = contact_uri
        self.proposal = proposal
        self.audio_stream = audio_stream
        self.video_stream = video_stream
        self.chat_stream = chat_stream
        self.screensharing_stream = screensharing_stream

        if proposal:
            self.dialog.setWindowTitle(u'Incoming Session Update')
            self.dialog.setWindowIconText(u'Incoming Session Update')
            self.dialog.busy_button.hide()
        else:
            self.dialog.setWindowTitle(u'Incoming Session Request')
            self.dialog.setWindowIconText(u'Incoming Session Request')
        address = u'%s@%s' % (session.remote_identity.uri.user, session.remote_identity.uri.host)
        self.dialog.uri_label.setText(address)
        self.dialog.username_label.setText(contact.name or session.remote_identity.display_name or address)
        if contact.pixmap:
            self.dialog.user_icon.setPixmap(contact.pixmap)
        if self.audio_stream:
            self.dialog.audio_stream.show()
        if self.video_stream:
            self.dialog.video_stream.show()
        if self.chat_stream:
            self.dialog.chat_stream.show()
        if self.screensharing_stream:
            if self.screensharing_stream.handler.type == 'active':
                self.dialog.screensharing_label.setText(u'is offering to share his screen')
            else:
                self.dialog.screensharing_label.setText(u'is asking to share your screen')
            self.dialog.screensharing_stream.accepted = False # Remove when implemented later -Luci
            self.dialog.screensharing_stream.show()
        self.dialog.audio_device_label.setText(u'Selected audio device is: %s' % SIPApplication.voice_audio_bridge.mixer.real_output_device)

        self.dialog.accepted.connect(self._SH_DialogAccepted)
        self.dialog.rejected.connect(self._SH_DialogRejected)

    def __eq__(self, other):
        return self is other

    def __ne__(self, other):
        return self is not other

    def __lt__(self, other):
        return self.priority < other.priority

    def __le__(self, other):
        return self.priority <= other.priority

    def __gt__(self, other):
        return self.priority > other.priority

    def __ge__(self, other):
        return self.priority >= other.priority

    @property
    def accepted_streams(self):
        streams = []
        if self.audio_accepted:
            streams.append(self.audio_stream)
        if self.video_accepted:
            streams.append(self.video_stream)
        if self.chat_accepted:
            streams.append(self.chat_stream)
        if self.screensharing_accepted:
            streams.append(self.screensharing_stream)
        return streams

    @property
    def audio_accepted(self):
        return self.dialog.audio_stream.in_use and self.dialog.audio_stream.accepted

    @property
    def video_accepted(self):
        return self.dialog.video_stream.in_use and self.dialog.video_stream.accepted

    @property
    def chat_accepted(self):
        return self.dialog.chat_stream.in_use and self.dialog.chat_stream.accepted

    @property
    def screensharing_accepted(self):
        return self.dialog.screensharing_stream.in_use and self.dialog.screensharing_stream.accepted

    @property
    def priority(self):
        if self.audio_stream:
            return 0
        elif self.video_stream:
            return 1
        elif self.screensharing_stream:
            return 2
        elif self.chat_stream:
            return 3
        else:
            return 4

    def _SH_DialogAccepted(self):
        self.accepted.emit(self)

    def _SH_DialogRejected(self):
        self.rejected.emit(self, self.dialog.reject_mode)


ui_class, base_class = uic.loadUiType(Resources.get('conference_dialog.ui'))

class ConferenceDialog(base_class, ui_class):
    def __init__(self, parent=None):
        super(ConferenceDialog, self).__init__(parent)
        with Resources.directory:
            self.setupUi(self)
        self.audio_button.clicked.connect(self._SH_MediaButtonClicked)
        self.chat_button.clicked.connect(self._SH_MediaButtonClicked)
        self.room_button.editTextChanged.connect(self._SH_RoomButtonEditTextChanged)
        self.accepted.connect(self.join_conference)

    def _SH_MediaButtonClicked(self, checked):
        self.accept_button.setEnabled(self.room_button.currentText() != u'' and any(button.isChecked() for button in (self.audio_button, self.chat_button)))

    def _SH_RoomButtonEditTextChanged(self, text):
        self.accept_button.setEnabled(text != u'' and any(button.isChecked() for button in (self.audio_button, self.chat_button)))

    def show(self):
        self.room_button.setCurrentIndex(-1)
        self.audio_button.setChecked(True)
        self.chat_button.setChecked(True)
        self.accept_button.setEnabled(False)
        super(ConferenceDialog, self).show()

    def join_conference(self):
        from blink.contacts import URIUtils

        account_manager = AccountManager()
        session_manager = SessionManager()
        account = account_manager.default_account
        if account is not BonjourAccount():
            conference_uri = u'%s@%s' % (self.room_button.currentText(), account.server.conference_server or 'conference.sip2sip.info')
        else:
            conference_uri = u'%s@%s' % (self.room_button.currentText(), 'conference.sip2sip.info')
        contact, contact_uri = URIUtils.find_contact(conference_uri, display_name='Conference')
        streams = []
        if self.audio_button.isChecked():
            streams.append(StreamDescription('audio'))
        if self.chat_button.isChecked():
            streams.append(StreamDescription('chat'))
        session_manager.create_session(contact, contact_uri, streams, account=account)

del ui_class, base_class


class RingtoneDescriptor(object):
    def __init__(self):
        self.values = weakobjectmap()

    def __get__(self, obj, objtype):
        if obj is None:
            return self
        return self.values[obj]

    def __set__(self, obj, ringtone): # review this again -Dan
        old_ringtone = self.values.get(obj, Null)
        if ringtone is not Null and ringtone.type == old_ringtone.type:
            return
        old_ringtone.stop()
        old_ringtone.bridge.remove(old_ringtone)
        ringtone.bridge.add(ringtone)
        ringtone.start()
        self.values[obj] = ringtone

    def __delete__(self, obj):
        raise AttributeError("Attribute cannot be deleted")


class SessionManager(object):
    __metaclass__ = Singleton

    implements(IObserver)

    class PrimaryRingtone:   __metaclass__ = MarkerType
    class SecondaryRingtone: __metaclass__ = MarkerType

    inbound_ringtone  = RingtoneDescriptor()
    outbound_ringtone = RingtoneDescriptor()
    hold_tone         = RingtoneDescriptor()
    # have the hangup tone also a descriptor that is not reset to Null when it ends playing, but after the cooldown period -Dan

    def __init__(self):
        self.sessions = []
        self.incoming_requests = []
        self.dialog_positions = range(1, 100)
        self.last_dialed_uri = None
        self.active_session = None

        self.inbound_ringtone = Null
        self.outbound_ringtone = Null
        self.hold_tone = Null

        self._hangup_tone_timer = QTimer() # we should consider replacing this with a timestamp -Dan
        self._hangup_tone_timer.setInterval(1000)
        self._hangup_tone_timer.setSingleShot(True)

        notification_center = NotificationCenter()
        notification_center.add_observer(self, name='SIPSessionNewIncoming')
        notification_center.add_observer(self, name='SIPSessionDidFail')
        notification_center.add_observer(self, name='SIPSessionProposalRejected')
        notification_center.add_observer(self, name='SIPSessionHadProposalFailure')

        notification_center.add_observer(self, name='BlinkSessionNewIncoming')
        notification_center.add_observer(self, name='BlinkSessionDidReinitializeForIncoming')
        notification_center.add_observer(self, name='BlinkSessionDidEnd')
        notification_center.add_observer(self, name='BlinkSessionWasDeleted')
        notification_center.add_observer(self, name='BlinkSessionDidChangeState')
        notification_center.add_observer(self, name='BlinkSessionDidChangeHoldState')

        notification_center.add_observer(self, name='BlinkSessionListSelectionChanged')

    def create_session(self, contact, contact_uri, streams, account=None, connect=True, sibling=None):
        if account is None:
            if contact.type == 'bonjour':
                account = BonjourAccount()
            else:
                account = AccountManager().default_account

        assert account is not None

        try:
            session = next(session for session in self.sessions if session.reusable and session.contact.settings is contact.settings)
            reinitialize = True
        except StopIteration:
            session = BlinkSession()
            self.sessions.append(session)
            reinitialize = False

        session.init_outgoing(account, contact, contact_uri, streams, sibling=sibling, reinitialize=reinitialize)
        self.last_dialed_uri = session.uri
        if connect:
            session.connect()

        return session

    def update_ringtone(self):
        # Outgoing ringtone
        outgoing_sessions_or_proposals = [session for session in self.sessions if session.state=='connecting/ringing' and session.direction=='outgoing' or session.state=='connected/sent_proposal']
        if any(not session.on_hold for session in outgoing_sessions_or_proposals):
            settings = SIPSimpleSettings()
            outbound_ringtone = settings.sounds.outbound_ringtone
            if outbound_ringtone:
                if any('audio' in session.streams and not session.on_hold for session in outgoing_sessions_or_proposals):
                    ringtone_path = outbound_ringtone.path
                    ringtone_type = self.PrimaryRingtone
                else:
                    ringtone_path = Resources.get('sounds/beeping_ringtone.wav')
                    ringtone_type = self.SecondaryRingtone
                outbound_ringtone = WavePlayer(SIPApplication.voice_audio_mixer, ringtone_path, outbound_ringtone.volume, loop_count=0, pause_time=5)
                outbound_ringtone.bridge = SIPApplication.voice_audio_bridge
                outbound_ringtone.type = ringtone_type
            else:
                outbound_ringtone = Null
        else:
            outbound_ringtone = Null

        if outbound_ringtone.type is self.PrimaryRingtone and self.inbound_ringtone.type is self.PrimaryRingtone:
            self.inbound_ringtone = Null
        if outbound_ringtone is not Null and self.hold_tone is not Null:
            self.hold_tone = Null
        self.outbound_ringtone = outbound_ringtone

        # Incoming ringtone
        if self.incoming_requests:
            try:
                request = next(req for req in self.incoming_requests if req.audio_stream or req.video_stream)
                ringtone_type = self.PrimaryRingtone
            except StopIteration:
                request = self.incoming_requests[0]
                ringtone_type = self.SecondaryRingtone

            if self.active_session is not None and self.active_session.state in ('connecting/ringing', 'connected/*'):
                ringtone_type = self.SecondaryRingtone
                initial_delay = 1 # have a small delay to avoid sounds overlapping
            else:
                initial_delay = 0

            settings = SIPSimpleSettings()
            sound_file = request.session.account.sounds.inbound_ringtone or settings.sounds.inbound_ringtone
            if sound_file:
                if ringtone_type is self.PrimaryRingtone:
                    ringtone_path = sound_file.path
                else:
                    ringtone_path = Resources.get('sounds/beeping_ringtone.wav')
                inbound_ringtone = WavePlayer(SIPApplication.alert_audio_mixer, ringtone_path, volume=sound_file.volume, loop_count=0, pause_time=3, initial_delay=initial_delay)
                inbound_ringtone.bridge = SIPApplication.alert_audio_bridge
                inbound_ringtone.type = ringtone_type
            else:
                inbound_ringtone = Null
        else:
            inbound_ringtone = Null

        if inbound_ringtone is not Null and self.hold_tone is not Null:
            self.hold_tone = Null
        self.inbound_ringtone = inbound_ringtone

        # Hold tone
        # we need to beep every 15 seconds only when we put all calls on hold. If all are on hold but not all by local, we ring at 45 seconds -Dan
        connected_sessions = [session for session in self.sessions if session.state=='connected/*']
        connected_on_hold_sessions = [session for session in connected_sessions if session.on_hold]
        if self.outbound_ringtone is Null and self.inbound_ringtone is Null and connected_sessions:
            if len(connected_sessions) == len(connected_on_hold_sessions):
                hold_tone = WavePlayer(SIPApplication.alert_audio_mixer, Resources.get('sounds/hold_tone.wav'), loop_count=0, pause_time=15, volume=30, initial_delay=15)
                hold_tone.bridge = SIPApplication.alert_audio_bridge
                hold_tone.type = None
            elif len(connected_on_hold_sessions) > 0:
                hold_tone = WavePlayer(SIPApplication.voice_audio_mixer, Resources.get('sounds/hold_tone.wav'), loop_count=0, pause_time=45, volume=30, initial_delay=15)
                hold_tone.bridge = SIPApplication.voice_audio_bridge
                hold_tone.type = None
            else:
                hold_tone = Null
        else:
            hold_tone = Null
        self.hold_tone = hold_tone

    def _process_remote_proposal(self, blink_session):
        sip_session = blink_session.sip_session

        current_stream_types = set(stream.type for stream in sip_session.streams)
        stream_map = defaultdict(list)
        # TODO: we should fetch the proposed streams from the BlinkSession -Saul
        for stream in (stream for stream in sip_session.proposed_streams if stream.type not in current_stream_types):
            stream_map[stream.type].append(stream)
        proposed_stream_types = set(stream_map)

        audio_streams = stream_map['audio']
        video_streams = stream_map['video']
        chat_streams = stream_map['chat']
        screensharing_streams = stream_map['screen-sharing']

        if not proposed_stream_types or proposed_stream_types == {'file-transfer'}:
            sip_session.reject_proposal(488) # maybe add a reject_proposal on blink_session for symmetry? -Dan
            return

        if proposed_stream_types == {'chat'}:
            blink_session.accept_proposal([chat_streams[0]])
            return

        sip_session.send_ring_indication()

        contact = blink_session.contact
        contact_uri = blink_session.contact_uri

        audio_stream = audio_streams[0] if audio_streams else None
        video_stream = video_streams[0] if video_streams else None
        chat_stream = chat_streams[0] if chat_streams else None
        screensharing_stream = screensharing_streams[0] if screensharing_streams else None

        dialog = IncomingDialog() # The dialog is constructed without the main window as parent so that on Linux it is displayed on the current workspace rather than the one where the main window is.
        incoming_request = IncomingRequest(dialog, sip_session, contact, contact_uri, proposal=True, audio_stream=audio_stream, video_stream=video_stream, chat_stream=chat_stream, screensharing_stream=screensharing_stream)
        bisect.insort_right(self.incoming_requests, incoming_request)
        incoming_request.accepted.connect(self._SH_IncomingRequestAccepted)
        incoming_request.rejected.connect(self._SH_IncomingRequestRejected)
        try:
            position = self.dialog_positions.pop(0)
        except IndexError:
            position = None
        incoming_request.dialog.show(activate=QApplication.activeWindow() is not None and self.incoming_requests.index(incoming_request)==0, position=position)

    def _SH_IncomingRequestAccepted(self, incoming_request):
        if incoming_request.dialog.position is not None:
            bisect.insort_left(self.dialog_positions, incoming_request.dialog.position)
        self.incoming_requests.remove(incoming_request)
        self.update_ringtone()
        accepted_streams = incoming_request.accepted_streams
        if incoming_request.proposal:
            blink_session = next(session for session in self.sessions if session.sip_session is incoming_request.session)
            blink_session.accept_proposal(accepted_streams)
        else:
            try:
                blink_session = next(session for session in self.sessions if session.reusable and session.contact.settings is incoming_request.contact.settings)
                reinitialize = True
            except StopIteration:
                blink_session = BlinkSession()
                self.sessions.append(blink_session)
                reinitialize = False
            blink_session.init_incoming(incoming_request.session, accepted_streams, incoming_request.contact, incoming_request.contact_uri, reinitialize=reinitialize)

    def _SH_IncomingRequestRejected(self, incoming_request, mode):
        if incoming_request.dialog.position is not None:
            bisect.insort_left(self.dialog_positions, incoming_request.dialog.position)
        self.incoming_requests.remove(incoming_request)
        self.update_ringtone()
        if incoming_request.proposal:
            incoming_request.session.reject_proposal(488)
        elif mode == 'busy':
            incoming_request.session.reject(486)
        elif mode == 'reject':
            incoming_request.session.reject(603)

    @run_in_gui_thread
    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_SIPSessionNewIncoming(self, notification):
        from blink.contacts import URIUtils

        session = notification.sender

        stream_map = defaultdict(list)
        for stream in notification.data.streams:
            stream_map[stream.type].append(stream)

        audio_streams = stream_map['audio']
        video_streams = stream_map['video']
        chat_streams = stream_map['chat']
        screensharing_streams = stream_map['screen-sharing']
        filetransfer_streams = stream_map['file-transfer']

        if not audio_streams and not video_streams and not chat_streams and not screensharing_streams and not filetransfer_streams:
            session.reject(488)
            return
        if filetransfer_streams and not (audio_streams or video_streams or chat_streams or screensharing_streams):
            # TODO: add support for this with different type of session -Saul
            session.reject(488)
            return

        session.send_ring_indication()

        contact, contact_uri = URIUtils.find_contact(session.remote_identity.uri, display_name=session.remote_identity.display_name, exact=False)

        audio_stream = audio_streams[0] if audio_streams else None
        video_stream = video_streams[0] if video_streams else None
        chat_stream = chat_streams[0] if chat_streams else None
        screensharing_stream = screensharing_streams[0] if screensharing_streams else None

        dialog = IncomingDialog() # The dialog is constructed without the main window as parent so that on Linux it is displayed on the current workspace rather than the one where the main window is.
        incoming_request = IncomingRequest(dialog, session, contact, contact_uri, proposal=False, audio_stream=audio_stream, video_stream=video_stream, chat_stream=chat_stream, screensharing_stream=screensharing_stream)
        bisect.insort_right(self.incoming_requests, incoming_request)
        incoming_request.accepted.connect(self._SH_IncomingRequestAccepted)
        incoming_request.rejected.connect(self._SH_IncomingRequestRejected)
        try:
            position = self.dialog_positions.pop(0)
        except IndexError:
            position = None
        incoming_request.dialog.show(activate=QApplication.activeWindow() is not None and self.incoming_requests.index(incoming_request)==0, position=position)
        self.update_ringtone()

    def _NH_SIPSessionDidFail(self, notification):
        try:
            incoming_request = next(incoming_request for incoming_request in self.incoming_requests if incoming_request.session is notification.sender)
        except StopIteration:
            return
        if incoming_request.dialog.position is not None:
            bisect.insort_left(self.dialog_positions, incoming_request.dialog.position)
        incoming_request.dialog.hide()
        self.incoming_requests.remove(incoming_request)
        self.update_ringtone()

    def _NH_SIPSessionProposalRejected(self, notification):
        try:
            incoming_request = next(incoming_request for incoming_request in self.incoming_requests if incoming_request.session is notification.sender)
        except StopIteration:
            return
        if incoming_request.dialog.position is not None:
            bisect.insort_left(self.dialog_positions, incoming_request.dialog.position)
        incoming_request.dialog.hide()
        self.incoming_requests.remove(incoming_request)
        self.update_ringtone()

    def _NH_SIPSessionHadProposalFailure(self, notification):
        try:
            incoming_request = next(incoming_request for incoming_request in self.incoming_requests if incoming_request.session is notification.sender)
        except StopIteration:
            return
        if incoming_request.dialog.position is not None:
            bisect.insort_left(self.dialog_positions, incoming_request.dialog.position)
        incoming_request.dialog.hide()
        self.incoming_requests.remove(incoming_request)
        self.update_ringtone()

    def _NH_BlinkSessionDidChangeState(self, notification):
        new_state = notification.data.new_state
        if new_state == 'connected/received_proposal':
            self._process_remote_proposal(notification.sender)
        if new_state in ('connecting/ringing', 'connecting/early_media', 'connected/*'):
            self.update_ringtone()
        elif new_state == 'ending':
            notification.sender._play_hangup_tone = notification.data.old_state in ('connecting/*', 'connected/*')

    def _NH_BlinkSessionDidChangeHoldState(self, notification):
        if notification.data.remote_hold and not notification.data.local_hold: # check if this could be integrated in update_ringtone -Dan
            player = WavePlayer(SIPApplication.voice_audio_bridge.mixer, Resources.get('sounds/hold_tone.wav'), loop_count=1, volume=30)
            SIPApplication.voice_audio_bridge.add(player)
            player.start()
        self.update_ringtone()

    def _NH_BlinkSessionNewIncoming(self, notification):
        self.update_ringtone()

    def _NH_BlinkSessionDidReinitializeForIncoming(self, notification):
        self.update_ringtone()

    def _NH_BlinkSessionDidEnd(self, notification):
        self.update_ringtone()
        if notification.sender._play_hangup_tone and not self._hangup_tone_timer.isActive():
            self._hangup_tone_timer.start()
            player = WavePlayer(SIPApplication.voice_audio_bridge.mixer, Resources.get('sounds/hangup_tone.wav'), volume=60)
            SIPApplication.voice_audio_bridge.add(player)
            player.start()

    def _NH_BlinkSessionWasDeleted(self, notification):
        self.sessions.remove(notification.sender)

    def _NH_BlinkSessionListSelectionChanged(self, notification):
        selected_session = notification.data.selected_session
        deselected_session = notification.data.deselected_session
        old_active_session = self.active_session

        if selected_session is self.active_session: # both None or both the same session. nothing to do in either case.
            return
        elif selected_session is None and deselected_session is old_active_session is not None:
            self.active_session = None
            sessions = deselected_session.client_conference.sessions if deselected_session.client_conference is not None else [deselected_session]
            for session in sessions:
                session.active = False
            notification.center.post_notification('BlinkActiveSessionDidChange', sender=self, data=NotificationData(previous_active_session=old_active_session, active_session=None))
        elif selected_session is not None and selected_session.state in ('connecting/*', 'connected/*') and selected_session.streams.types.intersection({'audio', 'video'}):
            old_active_session = old_active_session or Null
            new_active_session = selected_session
            if old_active_session.client_conference is not None and old_active_session.client_conference is not new_active_session.client_conference:
                for session in old_active_session.client_conference.sessions:
                    session.active = False
            elif old_active_session.client_conference is None:
                old_active_session.active = False
            if new_active_session.client_conference is not None and new_active_session.client_conference is not old_active_session.client_conference:
                for session in new_active_session.client_conference.sessions:
                    session.active = True
            elif new_active_session.client_conference is None:
                new_active_session.active = True
            self.active_session = selected_session
            notification.center.post_notification('BlinkActiveSessionDidChange', sender=self, data=NotificationData(previous_active_session=old_active_session or None, active_session=selected_session))