Logo Search packages:      
Sourcecode: kmess version File versions  Download package

applicationlist.cpp

/***************************************************************************
                          applicationlist.cpp -  description
                             -------------------
    begin                : Mon 01 16 2005
    copyright            : (C) 2005 by Diederik van der Boor
    email                : "vdboor" --at-- "codingdomain.com"
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#include "applicationlist.h"

#include "../../contact/contactbase.h"
#include "../../contact/msnobject.h"
#include "../../currentaccount.h"
#include "../../kmessdebug.h"
#include "../extra/directconnectionpool.h"
#include "../extra/msndirectconnection.h"
#include "../chatmessage.h"
#include "../mimemessage.h"
#include "../msnswitchboardconnection.h"
#include "../p2pmessage.h"
#include "filetransfer.h"
#include "filetransferp2p.h"
#include "inktransferp2p.h"
#include "mimeapplication.h"
#include "msnobjecttransferp2p.h"
#include "p2papplication.h"
#include "unknownapplication.h"

#include <QRegExp>


#ifdef KMESSDEBUG_APPLICATIONLIST
#define KMESSDEBUG_APPLICATIONLIST_GENERAL
#endif


#define CEIL( val ) ( (int) (val) + 1)



/**
 * @brief The constructor
 *
 * Initialize the application list for the given contact.
 * @param  contactHandle  The contact this application list is created for.
 */
00056 ApplicationList::ApplicationList(const QString &contactHandle)
  : QObject()
  , addingConnections_(false)
  , contactHandle_(contactHandle.toLower())
  , directConnection_(0)
  , directConnectionPool_(0)
  , pauseApplications_(false)
  , pendingConnectionInvitation_(false)
{
  setObjectName("ApplicationList[" + contactHandle + "]");

  // Configure timer
  connect( &sbReadyWriteTester_, SIGNAL(timeout()), this, SLOT(slotSwitchboardTestReadyWrite()) );
  sbReadyWriteTester_.setSingleShot( true );
}



/**
 * @brief The destructor.
 *
 * This method cleans up the direct connection and connection pool.
 * All MimeApplication and P2PApplication objects should be removed already.
 * If an application object still exists, it will be deleted directly.
 */
00081 ApplicationList::~ApplicationList()
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "Starting removal. [handle=" << contactHandle_ << "]";
#endif

  // See if there are remaining MIME applications
  if( ! mimeApplications_.isEmpty() )
  {
    kWarning() << "still have " << mimeApplications_.count() <<
                   " active MimeApplication objects for " << contactHandle_ << "!";

    // Remove all mime applications
    qDeleteAll( mimeApplications_ );
    mimeApplications_.clear();
  }

  // See if there are remaining P2P applications
  if( ! p2pApplications_.isEmpty() )
  {
    kWarning() << "still have " << p2pApplications_.count() <<
                   " active P2PApplication objects for " << contactHandle_ << "!";

    // Remove all p2p applications
    qDeleteAll( p2pApplications_ );
    p2pApplications_.clear();
  }

  // Delete the connections
  directConnection_ = 0;  // deleted by connection pool
  delete directConnectionPool_;
  directConnectionPool_ = 0;

#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "DESTROYED. [handle=" << contactHandle_ << "]";
#endif
}



/**
 * @brief Abort all applications, the connection is closing or closed.
 *
 * This is called from contactLeftChat() and contactLeavingChat() to abort the
 * applications that can't operate without a switchboard connection (sometimes thats's only needed usability wise).
 *
 * If applications need to be aborted, this method returns true.
 * The applicationsAborted() signal will be fired when the aborting is completed.
 *
 * @param  abortingConnection  Connection that is closing.
 * @param  userInitiated       Indicates whether this action was initiated by the user (closing the chat window),
 *                             or the contact left the chat first.
 * @returns Returns whether the object started to abort applications. Returns false if there is nothing to abort.
 */
00135 bool ApplicationList::abortApplications(const MsnSwitchboardConnection *abortingConnection, bool userInitiated)
{
  // Check if there is nothing to do
  if( mimeApplications_.isEmpty() && p2pApplications_.isEmpty() )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kDebug() << "No applications active.";
#endif
    return false;
  }

  // If the contact is still in another private chat, all applications can use that one.
  // If the contact is only in a multi-chat, all applications need to close that don't support this.
  // If the contact left all chats, all applications need to be closed.
  bool hasMultiChats = false;

  // Check how many other chat sessions the contact still participates in.
  const ContactBase *contact = CurrentAccount::instance()->getContactByHandle(contactHandle_);
  if( ! KMESS_NULL(contact) )
  {
    const QList<const MsnSwitchboardConnection*> switchboards = contact->getSwitchboardConnections();
    foreach( const MsnSwitchboardConnection *connection, switchboards )
    {
      if( connection != abortingConnection )
      {
        if( connection->isExclusiveChatWithContact( contactHandle_ ) )
        {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
          kDebug() << "Contact is still in another private chat, not terminating applications.";
#endif
          return (! abortingApplications_.isEmpty());  // in case the function was called before
        }
        else
        {
          hasMultiChats = true;
        }
      }
    }
  }


  // Check if any multi chats are found.
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  if( ! hasMultiChats )
  {
    kDebug() << "Contact left all chats, aborting applications.";
  }
  else
  {
    kDebug() << "Contact left all private chats, aborting applications that don't support multi-chats.";
  }
#endif

  // When aborting. verify whether the application is not being aborted here already.
  // Also check if the application was already started closing due a packet it received earlier.
  // This prevents nasty issues when a switchboard both reports the contact
  // will leave and actually left, or two switchboard report a contact has left.

  // First abort all mime-based applications.
  foreach( MimeApplication *app, mimeApplications_ )
  {
    if( ! abortingApplications_.isEmpty() && ! abortingApplications_.contains( static_cast<Application*>( app ) )
    &&  ( app->isPrivateChatRequired() || ! hasMultiChats ) )
    {
      abortingApplications_.append(app);
      if( ! app->isClosing() )
      {
        if( userInitiated )
        {
          app->userAborted();  // requests deletion after cancel message is delivered.
        }
        else
        {
          app->contactAborted();  // requests deletion.
        }
      }
    }
  }

  // Then abort all p2p-based applications.
  foreach( P2PApplication *app, p2pApplications_ )
  {
    if( ! abortingApplications_.contains(app)
    &&  ( app->isPrivateChatRequired() || ! hasMultiChats ) )
    {
      abortingApplications_.append(app);
      if( ! app->isClosing() )
      {
        if( userInitiated )
        {
          app->userAborted();  // requests deletion when bye message is delivered.
        }
        else
        {
          app->contactAborted();  // requests deletion.
        }
      }
    }
  }

  // The application may abort really quick and update the abortingApplications_ list directly from a signal.
  // In this case, the list is already emty, theirby also returning 'false'.
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "Got " << abortingApplications_.count() << " applications left to abort.";
#endif
  return (! abortingApplications_.isEmpty());
}




/**
 * @brief Add a new MIME application to the list.
 *
 * After the application is added, it can be started with it's <code>start()</code> operation.
 * The putMsg() signal of the application will be connected to the sendMessage() slot,
 * the deleteMe() signal to the slotTerminateApplication() slot.
 *
 * @param application  The configured application instance.
 */
00255 void ApplicationList::addApplication(MimeApplication *application)
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "connecting mime application signals.";
#endif

  mimeApplications_.append( application );
  connectApplication( application );

  // The new-style P2P application manually call sendMessage() or put the message at the direct connection link.
  // The old-style MIME applications are directly connected here, so they don't need to have a reference to this object.
  connect( application, SIGNAL(      putMsg(const MimeApplication*, const MimeMessage&) ),
           this,        SLOT  ( sendMessage(const MimeApplication*, const MimeMessage&) ));

  // Inform listeners about the new application (notably ChatMaster)
  emit newApplication( application );
}



/**
 * @brief Add a new P2P application to the list.
 *
 * After the application is added, it can be started with it's <code>start()</code> operation.
 * The deleteMe() signal will be connected to the slotTerminateApplication() slot.
 * When an application needs to send a message, the sendMessage() methods determines whether the
 * message can be sent over the direct connection, or a switchboard connection is needed (fireing a putMsg() signal).
 *
 * @param application  The configured application instance.
 */
00285 void ApplicationList::addApplication(P2PApplication *application)
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "connecting p2p application signals.";
#endif

  p2pApplications_.append( application );
  connectApplication( application );

  // Inform listeners about the new application (notably ChatMaster)
  emit newApplication( application );
}



/**
 * @brief Attempt to establish a direct connection at the given ipaddress/port.
 *
 * It uses the DirectConnectionPool class internally to handle multiple connection attempts.
 * When all connection attempts failed, a connectionFailed() signal will be sent.
 *
 * It's possible multiple P2PApplication instances require a direct connection.
 * Only the first application handles the connectionEstablished() signal.
 * The other applications wait until a connectionAuthorized() or connectionFailed() signal is fired.
 *
 * @param  ipAddress  The IP-Address to connect to.
 * @param  port       The port number to connect to.
 * @return Whether the socket could be created or not.
 *         Returns false if the socket could not be created.
 */
00315 bool ApplicationList::addConnection(const QString &ipAddress, const quint16 port)
{
  // Avoid overwriting the current directConnection_ variable.
  if( directConnection_ != 0 )
  {
    if( ! directConnection_->isConnected() )
    {
      // Still had an old connection which went idle and closed, cleanup.
      if( ! KMESS_NULL(directConnectionPool_) )
      {
        directConnectionPool_->verifyActiveConnection();
      }
      directConnection_ = 0;
    }
    else
    {
      // Already connected, don't allow second connection.
      kWarning() << "Refusing connection attempt, a connection has already been made "
                     "(contact=" << contactHandle_ << " action=return).";
      return 0;
    }
  }

#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "adding new connection attempt.";
#endif

  // Avoid overwriting the current directConnection_ variable.
  if( directConnection_ != 0 )
  {
    kWarning() << "Refusing connection attempt, a connection has already been made "
                    "(contact=" << contactHandle_ << " action=return).";
    return 0;
  }

  // Init direct connection pool
  if( directConnectionPool_ == 0 )
  {
    initializeDirectConnectionPool();
  }

  // Init connection
  MsnDirectConnection *connection = new MsnDirectConnection(contactHandle_);

  // Connect connection
  connect(connection, SIGNAL(                 messageReceived(const QByteArray&) ),
          this,         SLOT(  slotGotDirectConnectionMessage(const QByteArray&) ));

  // Add to pool, and start
  return directConnectionPool_->addConnection(connection, ipAddress, port);
}



/**
 * @brief Attempt to establish a direct connection by listening at a random port.
 *
 * This method operates similar like the addConnection() method.
 *
 * @return The port number the connection is listening at. Returns zero if listening failed.
 */
00376 int ApplicationList::addServerConnection()
{
  // Avoid overwriting the current directConnection_ variable.
  if( directConnection_ != 0 )
  {
    if( ! directConnection_->isConnected() )
    {
      // Still had an old connection which went idle and closed, cleanup.
      if( ! KMESS_NULL(directConnectionPool_) )
      {
        directConnectionPool_->verifyActiveConnection();
      }
      directConnection_ = 0;
    }
    else
    {
      // Already connected, don't allow second connection.
      kWarning() << "Refusing server connection attempt, a connection has already been made "
                      "(contact=" << contactHandle_ << " action=return).";
      return 0;
    }
  }

#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "adding new server socket.";
#endif

  // Init direct connection pool
  if( directConnectionPool_ == 0 )
  {
    initializeDirectConnectionPool();
  }

  // Init connection
  MsnDirectConnection *connection = new MsnDirectConnection(contactHandle_);

  // Connect connection
  connect(connection, SIGNAL(                 messageReceived(const QByteArray&) ),
          this,         SLOT(  slotGotDirectConnectionMessage(const QByteArray&) ));

  // Add to pool, and start.
  return directConnectionPool_->addServerConnection(connection);
}



/**
 * @brief Connect the application object signals.
 * @param app The application object instance.
 */
00426 void ApplicationList::connectApplication(Application *app)
{
  connect( app,  SIGNAL(                 deleteMe( Application* ) ),    // Request for removal
           this,   SLOT( slotTerminateApplication( Application* ) ) );
}



/**
 * @brief Abort all applications, the contact is leaving a chat.
 *
 * This method is called by the MsnSwitchboardConnection to inform the contact is about the leave the chat.
 *
 * @param connection     The connection the contact is leaving.
 * @param userInitiated  Indicates whether this action was initiated by the user (closing the chat window),
 *                       or the contact left earlier.
 * @returns Returns whether the object started to abort applications.
 */
00444 bool ApplicationList::contactLeavingChat(const MsnSwitchboardConnection *connection, bool userInitiated)
{
  return abortApplications(connection, userInitiated);
}



/**
 * @brief Abort all applications, the contact left a chat.
 *
 * This method is called by the MsnSwitchboardConnection to inform the contact left the chat.
 *
 * @param userInitiated  Indicates whether this action was initiated by the user (closing the chat window),
 *                       or the contact left earlier.
 * @returns Returns whether the object started to abort applications.
 */
00460 bool ApplicationList::contactLeftChat(bool userInitiated)
{
  return abortApplications(0, userInitiated);
}



/**
 * @brief Create the MIME application instance for the given invitation message.
 *
 * This extracts the Application-GUID field from the message and calls
 * createApplicationByGuid() to create the correct MimeApplication object.
 * When an invitation type is not recognized, it's rejected with sendMimeRejectMessage().
 *
 * @param  message  The MimeMessage sent by the contact.
 * @returns The new application instance.
 */
00477 MimeApplication * ApplicationList::createApplication(const MimeMessage &message)
{
  MimeApplication *app = 0;
  QString appGuid( message.getValue("Application-GUID") );

  // If no GUID is found:
  if( appGuid.isEmpty() )
  {
    // Notify the user:
    kWarning() << "MIME message can't be handled, "
                << "Application-GUID not found. (message dump follows)\n" << message.getFields();
    return 0;
  }

  // Determine what application to start based on the Application-GUID
  app = createApplicationByGuid( appGuid );
  if( app == 0 )
  {
    MimeMessage reject;
    // TODO: create reject message.
    sendMimeRejectMessage( message.getValue("Invitation-Cookie"), true);
    return 0;
  }


  // Connect up the application
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "Connecting MimeApplication signals";
#endif

  // Auto-add application
  addApplication(app);
  return app;
}



/**
 * @brief Create the P2P application instance for the given invitation message.
 *
 * This extracts the EUF-GUID field from the message and calls
 * createApplicationByEufGuid() to create the correct P2PApplication object.
 * When an invitation type is not recognized, a temporary
 * application will be initiated to reject and close the session properly.
 *
 * @param  message  The P2PMessage sent by the contact.
 * @returns The new application instance.
 */
00525 P2PApplication * ApplicationList::createApplication(const P2PMessage &message)
{
#ifdef KMESSTEST
  KMESS_ASSERT( message.getSessionID() == 0 );
  KMESS_ASSERT( message.getDataSize()   > 0 );
#endif

#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "Creating new P2PApplication instance to parse the P2P message.";
#endif

  P2PApplication *app = 0;

  // Extract the SLP data message
  QString slpMessage( QString::fromUtf8( message.getData(), (int) message.getDataSize() ) );

  if(! slpMessage.startsWith("INVITE"))
  {
    // Not an invite message
    // this belongs to another session, but gotMessage() couldn't find it.
    // Likely a P2PApplication object terminated prematurely.
    QString preamble( slpMessage.left( qMin(slpMessage.indexOf(":"), 20) ) );
    kWarning() << "P2P message can't be handled, detected SLP message but no INVITE. "
                   "P2PApplication already terminated? "
                   "(slp-preamble=" << preamble <<
                   " contact="      << contactHandle_ <<
                   // TODO: get session ID from SLP body if available, it's not set in the headers.
                   ").";
    return 0;
  }


  // Find the EUF-GUID in the data
  QRegExp guidRE("EUF-GUID: (.+)\r\n");
  guidRE.setMinimal(true);
  int guidPos = guidRE.indexIn(slpMessage);

  // If no EUF-GUID is found:
  if( guidPos == -1 )
  {
    // Notify the user:
    kWarning() << "P2P message can't be handled, "
                << "EUF-GUID not found. (message dump follows)\n" << slpMessage;
    return 0;
  }

  // Determine what application to start based on the EUF-GUID
  app = createApplicationByEufGuid( guidRE.cap(1) );
  if( app == 0 )
  {
    // Create an application instance which will reject the invitation correctly.
    app = new P2PApplication(this);

    // Application::contactStarted1 rejects correct invitatations automatically,
    // so don't set app->setMode(APP_MODE_ERROR_HANDLER) here..!
  }


  // Connect up the application
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "Connecting P2PApplication signals";
#endif

  // Auto-add application
  addApplication(app);
  return app;
}



/**
 * @brief Factory method to create a MIME application instance for the given Application-GUID.
 * @param  appGuid  The Application-Guid field of the invitation message, identifies the application type to start.
 * @return The application object instance, or null if the Application-GUID is not recognized.
 */
00600 MimeApplication * ApplicationList::createApplicationByGuid(const QString &appGuid)
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "Creating MimeApplication for GUID" << appGuid;
#endif

  if( appGuid == FileTransfer::getAppId() )
  {
    return new FileTransfer( contactHandle_ );
  }
  else
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kDebug() << "No MimeApplication exists for this GUID!";
#endif
    return 0;
  }
}



/**
 * @brief Factory method to create a P2P application instance for the given EUF-GUID.
 * @param  eufGuid  The EUF-Guid field of the invitation message, identifies the application type to start.
 * @return The application object instance, or null if the EUF-GUID is not recognized.
 */
00626 P2PApplication * ApplicationList::createApplicationByEufGuid(const QString &eufGuid)
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "Creating P2PApplication for GUID" << eufGuid;
#endif

  if( eufGuid == MsnObjectTransferP2P::getAppId() )
  {
    return new MsnObjectTransferP2P( this );
  }
  else if( eufGuid == FileTransferP2P::getAppId() )
  {
    return new FileTransferP2P( this );
  }
  else if( eufGuid == UnknownApplication::getGamesAppId()
       ||  eufGuid == UnknownApplication::getMeetingAppId()
       ||  eufGuid == UnknownApplication::getRemoteDesktopAppId()
       ||  eufGuid == UnknownApplication::getWebcamPullAppId()
       ||  eufGuid == UnknownApplication::getWebcamPushAppId()
       ||  eufGuid == UnknownApplication::getVoiceAppId() )
  {
    return new UnknownApplication( this, eufGuid );
  }
  else
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kDebug() << "No P2PApplication exists for this GUID!";
#endif
    return 0;
  }
}



/**
 * @brief Find the P2P application which should handle the ACK message.
 * @param    ackMessage  The P2P ACK message.
 * @returns  The application instance, or null if the object can't be found.
 */
00665 P2PApplication * ApplicationList::getApplicationByAckData(const P2PMessage &ackMessage) const
{
  unsigned long ackUniqueID  = ackMessage.getAckUniqueID();
  foreach( P2PApplication *app, p2pApplications_ )
  {
    // The application may have multiple messages unacked, see whether the ACK matches.
    if( app->hasUnAckedMessage(ackMessage) )
    {
      // Test whether the match was only partial (error displayed here because P2PApplication
      // calls hasUnAckedMessage() later again, causing the message to appear twice)
      if( ackUniqueID == 0 )
      {
        // HACK: added for GAIM 1.5 (does not return UniqueID)
        kWarning() << "Got P2P message with matching ackSessionID and no ackUniqueID set "
                    << "(delivering anyway, contact=" << contactHandle_ << ")!";
      }

      return app;
    }
  }

  return 0;
}



/**
 * @brief Find the P2P application which handles the session for the given Call-ID.
 * @param    callID  The CallID value of the application session.
 * @returns  The application instance, or null if the object can't be found.
 */
00696 P2PApplication * ApplicationList::getApplicationByCallId(const QString &callID) const
{
  foreach( P2PApplication *app, p2pApplications_ )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kDebug() << "Demultiplexing P2P message"
              << "    (CallID " << app->getCallID() << " == " << callID  << " ?)";
#endif

    if( app->getCallID() == callID )
    {
      // Found it, return reference
      return app;
    }
  }

  return 0;
}



/**
 * @brief Find the application  with uses the given cookie.
 *
 * This is used to deliver accept/cancel commands from the chat window.
 * If the cookie exists, the method either returns a MimeApplication or P2PApplication instance.
 *
 * @param   cookie  The cookie string of the application.
 * @returns The application instance, or null if the object can't be found.
 */
00726 Application * ApplicationList::getApplicationByCookie(const QString &cookie) const
{
  // First see if there is a p2p application with the cookie.
  foreach( P2PApplication *app, p2pApplications_ )
  {
    if( app->getCookie() == cookie )
    {
      // Found it, return reference.
      return app;
    }
  }

  // Also look for a mime application with the cookie.
  foreach( MimeApplication *app, mimeApplications_ )
  {
    if ( app->getCookie() == cookie )
    {
      // Found it, return reference.
      return app;
    }
  }

  return 0;
}



/**
 * @brief Find the P2P application which handled the previous message fragment from the contact.
 * @param    messageID  The identifier of the last received message.
 * @returns  The application instance, or null if the object can't be found.
 */
00758 P2PApplication * ApplicationList::getApplicationByLastMessage(unsigned long messageID) const
{
  foreach( P2PApplication *app, p2pApplications_ )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    unsigned long debugMsgId = app->getLastContactMessageID();
    kDebug() << "Demultiplexing P2P message"
              << "    (fragment MessageID " << debugMsgId << " == " << messageID << " ?)";
#endif

    if( app->getLastContactMessageID() == messageID )
    {
      // HACK: Not tested for "getLastContactAckMessageID() == ackMessageID" to support broken implementations.

      // Found it, save reference
      return app;
    }
  }

  return 0;
}



/**
 * @brief Find the P2P application which authorizes the transfer for the given nonce.
 * @param    nonce  The Nonce value of the application session.
 * @returns  The application instance, or null if the object can't be found.
 */
00787 P2PApplication * ApplicationList::getApplicationByNonce(const QString &nonce) const
{
  foreach( P2PApplication *app, p2pApplications_ )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kDebug() << "Demultiplexing P2P message"
              << "    (Nonce " << app->getNonce() << " == " << nonce << " ?)";
#endif

    if( app->getNonce() == nonce )
    {
      // Found it, return reference
      return app;
    }
  }

  return 0;
}



/**
 * @brief Find the P2P application identified with the given session ID.
 * @param    sessionID  The identifier of the application.
 * @returns  The application instance, or null if the object can't be found.
 */
00813 P2PApplication * ApplicationList::getApplicationBySessionId(unsigned long sessionID) const
{
  foreach( P2PApplication *app, p2pApplications_ )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kDebug() << "Demultiplexing P2P message"
              << "    (SessionID " << app->getSessionID() << " == " << sessionID << " ?)";
#endif
    if( app->getSessionID() == sessionID )
    {
      // Found the session
      return app;
    }
  }

  return 0;
}



/**
 * @brief Return the handle of the contact this class manages.
 * @returns The contact handle.
 */
00837 const QString & ApplicationList::getContactHandle() const
{
  return contactHandle_;
}



/**
 * @brief Return the direct connection, if available.
 * @returns The direct connection object.
 */
00848 MsnDirectConnection * ApplicationList::getDirectConnection() const
{
  return directConnection_;
}



/**
 * @brief Deliver a received MIME message to the correct application instance.
 *
 * This method is called when a message was received by the MsnSwitchboardConnection class.
 * It examines the various fields of the message to deliver the message to the correct MimeApplication instance.
 * If the message contains an invitation, a new application instance will be created with createApplication().
 * A warning will be displayed at the console when an application instance could not be found.
 *
 * @param  message  The received mime message.
 */
00865 void ApplicationList::gotMessage(const MimeMessage &message)
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "Parsing Mime appliation message.";
  message.print();
#endif
  Application *app = 0;

  // If this is an invite message, an application need to be created
  if( message.getValue("Invitation-Command") == "INVITE" )
  {
    // Create the application based on the type
    app = createApplication( message );

    // Check if Application could not be created for some reason
    if( app == 0 )
    {
      return;
    }
  }
  else
  {
    // Find the application based on the app cookie
    app = getApplicationByCookie( message.getValue("Invitation-Cookie") );
    if( app == 0 )
    {
      kWarning() << "Unable to handle MIME message, "
                  << "Invitation-Cookie not found (contact=" << contactHandle_ << ").";
      return;
    }
  }

  // Last check before casting the object.
  if( ! app->inherits("MimeApplication") )
  {
    kWarning() << "found application is not a mime application!";
    return;
  }

  // Deliver the message
  static_cast<MimeApplication*>(app)->gotMessage( message );
}



/**
 * @brief Deliver a received P2P message to the correct application instance.
 *
 * This method is called when a message was received by
 * the MsnSwitchboardConnection class (via the ChatMaster) or MsnDirectConnection class.
 * It examines the various fields of the message to deliver the message to the correct P2PApplication instance.
 * If the message contains an invitation, a new application instance will be created with createApplication().
 * A warning will be displayed at the console when an application instance could not be found.
 *
 * @param  message  The received p2p message.
 */
00921 void ApplicationList::gotMessage(const P2PMessage &message)
{
  // Parse the message
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "Parsing received P2P message:"
            << " sid="         << message.getSessionID()
            << " mid="         << message.getMessageID()
            << " ackSid="      << message.getAckSessionID()
            << " ackUid="      << message.getAckUniqueID()
            << " flags=0x"     << QString::number(message.getFlags(), 16)
            << " size="        << message.getDataSize()
            << " offset="      << message.getDataOffset()  // also useful for debugging!
            << " total="       << message.getTotalSize()
            << " no.sessions=" << p2pApplications_.count();
#endif

  P2PApplication *p2pApplication = 0;
  unsigned long sessionID = message.getSessionID();
  QString slpMessage;


  // Session ID is 0 if clients negotiate (for both INVITE and BYE messages!)
  // Otherwise it's set.
  if( sessionID != 0 )
  {
    // The message has a SessionID set
    p2pApplication = getApplicationBySessionId( sessionID );

    // If there is no session for this ID, there are still some special cases.
    if(p2pApplication == 0)
    {
      switch( sessionID )
      {
        case InkTransferP2P::SESSION_ID:
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
          kDebug() << "Found reserved session ID for Ink transfers, creating InkTransferP2P instance.";
#endif
          // Ink messages have a fixed session ID.
          p2pApplication = new InkTransferP2P( this );
          addApplication( p2pApplication );
          break;

        // MSN 6 also used this system for Winks.

        default:
          // Normal P2P session -> notify the user if the message was invalid. (send error-response later)
          kWarning() << "Unable to handle P2P message, Session-ID " << sessionID << " not found.";
      }
    }
  }
  else if( message.getFlags() != 0
        && message.getFlags() != P2PMessage::MSN_FLAG_OBJECT_DATA   // WLM2009 uses 0x1000000 instead of 0x00.
        && message.getDataSize() == 0 )
  {
    // This can also be a connection handshake, even though it shouldn't appear at the switchboard.
    // That occurs when the connection was closed before that packet could be sent.
    if( message.isConnectionHandshake() )
    {
      kWarning() << "Received a direct connection handshake message at the switchboard "
                     "(contact=" << contactHandle_ << " action=return).";
      return;
    }

    // It's some kind of low-level control message, typically without data/payload.
    // Both the ACK-Message-ID and ACK-Unique-ID fields
    // should resolve the orginal session it was sent from.
    p2pApplication = getApplicationByAckData(message);

    // No existing session found..?
    if(p2pApplication == 0)
    {
      // NOTE: if flags==64 (0x40), it it's possible this is actually a bug from MSN7.0.
      kWarning() << "P2P message can't be handled,"
                     " no match for ackSessionID " << message.getAckSessionID() <<
                     " or ackUniqueID " << message.getAckUniqueID() << " found.";
    }
  }
  else if( message.getFlags() == 0 && message.getDataSize() == 0 && message.getAckUniqueID() == 0 )
  {
    // HACK: added for bot Bot2k3 4.1 (sends first ack without UniqueID, flags or any other context)
    kWarning() << "SwitchBoard P2P message has no ACK fields set at all, assuming base identifier message!";
  }
  else if( message.isSlpData() )
  {
    // It's a negotiation/MSNSLP message
    if( message.getDataOffset() > 0 )
    {
      // This is another fragment of the SLP message.
      // Find the application which received the previous fragment too.
      p2pApplication = getApplicationByLastMessage( message.getMessageID() );
    }
    else
    {
      // No offset, start of SLP message
      // Initialize Call and session ID regexps
      QRegExp callRE("Call-ID: (.+)\r\n"); // match everything because MSN6 accepts anything.
      callRE.setMinimal(true);

      // Find the call ID
      // Note this assumes that a Call-ID will be found in the first fragment.
      slpMessage  = QString::fromUtf8( message.getData(), (int)message.getDataSize() );
      int callPos = callRE.indexIn( slpMessage );

      if( callPos == -1 )
      {
        kWarning() << "Unable to handle P2P message, CallID field not found.";
      }
      else
      {
        QString callID( callRE.cap(1) );

        // See if it's a null-guid.
        // This happens for the out-of-band SLP "application/x-msnmsgr-transdestaddrupdate" and "application/x-msnmsgr-transudpswitch" messages.
        if( callID == "{00000000-0000-0000-0000-000000000000}" )
        {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
          kDebug() << "Found null Call-ID, using Session-ID to deliver the message.";
#endif

          // HACK use the SessionID from the SLP body to route the message.
          // This is a bit like reading the "HTTP Host" field to route an IP packet, but MSNP2P leaves us with no other choice.
          QRegExp sessionRE("SessionID: ([0-9]+)\r\n");
          sessionRE.setMinimal( true );
          long sessionPos = sessionRE.indexIn( slpMessage );
          if( sessionPos == -1 )
          {
            kWarning() << "Unable to handle P2P message, found null-guid CallID but no SessionID instead.";
          }
          else
          {
            unsigned long sessionID = sessionRE.cap(1).toLong();
            if( sessionID != 0 )
            {
              p2pApplication = getApplicationBySessionId( sessionID );
            }
            else
            {
              // Also observed in some cases.
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
              kDebug() << "Also found zero Session-ID, will create an empty application instance to handle the message.";
#endif

              // Create it here so there won't be any message dumps.
              // We already know what those look like.
              p2pApplication = new P2PApplication(this);
              p2pApplication->setMode( Application::APP_MODE_ERROR_HANDLER );
              addApplication( p2pApplication );
            }
          }
        }
        else
        {
          // Normal call-id, normal operation.
          // first try to find an existing session searching by Call ID. (for 200 OK and BYE messages)
          p2pApplication = getApplicationByCallId( callRE.cap(1) );

          // No existing applications have been found
          // Initialize an application class to handle the invitation.
          if( p2pApplication == 0 )
          {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
            kDebug() << "No existing P2P session found for the MSNSLP message, "
                      << "must be a new session invitation.";
#endif

            p2pApplication = createApplication( message );
          }
        }
      }
    }
  }
  else
  {
    // Didn't reconize the flag type.
    kWarning() << "P2P message can't be handled, unknown flag type found.";
    if( message.getFlags() == 0 )
    {
      // HACK: added for broken clients who sent an empty message with a zero flag (Bot2K3 v4.1)
    }
  }


  // If the application was found, deliver the message
  if( p2pApplication != 0 )
  {
    // Deliver the message
    p2pApplication->gotMessage( message );
  }
  else
  {
    // Force logging all unknown messages, to help improving the protocol knowledge
    // This is not hidden, because this information is essential for debugging user problems.
    kWarning() << "A message could not be delivered to an existing P2P application "
                   "(see previous warnings for details,"
                   " sid="     << message.getSessionID() <<
                   " flags=0x" << QString::number(message.getFlags(), 16) <<
                   " size="    << message.getDataSize() <<
                   " contact=" << contactHandle_ << ").";

    // Also dump the SLP message if it's there.
    if( ! slpMessage.isEmpty() && message.getSessionID() == 0 && message.getDataSize() > 0 )
    {
      kDebug() << "Dumping SLP message payload:\n" << slpMessage << "\n";
    }

    // If we didn't find the application, the message had bad fields
    // or we don't support the invitation type:
    if( message.getFlags() != 0
     && message.getFlags() != P2PMessage::MSN_FLAG_OBJECT_DATA   // WLM2009 uses 0x1000000 instead of 0x00.
     && message.getDataSize() == 0 )
    {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kDebug() << "Not sending a P2P Error response for error message.";
#endif
    }
    else
    {
      kDebug() << "Creating temporary P2PApplication instance to send error response.";

      // Create a default application to send the correct error response (also for unknown invitation types)
      // Application auto-deletes itself.
      p2pApplication = new P2PApplication(this);
      p2pApplication->setMode( Application::APP_MODE_ERROR_HANDLER );
      addApplication( p2pApplication );
      p2pApplication->gotMessage( message );
    }
  }
}


/**
 * @brief Return whether the direct connection is connected and authorized.
 * If a direct connection is not authorized, it's not able to send the data transfer.
 */
01155 bool ApplicationList::hasAuthorizedDirectConnection() const
{
  return ( directConnection_ != 0 && directConnection_->isAuthorized() && directConnection_->isConnected() );
}



/**
 * @brief Return whether there are applications which need to send data.
 */
01165 bool ApplicationList::hasDataSendingApplications() const
{
  return ! dataSendingApplications_.isEmpty();
}



/**
 * @brief Return whether there is an active direct connection.
 *
 * If the direct connection is no longer connected, it returns false.
 * Methods like sendMessage() automatically clean up a disconnected instance.
 *
 * Note that the connection might not be authorized. Such connection
 * can only be used to send the direct connection handshake.
 * use hasAuthorizedDirectConnection() if the connection needs to
 * be able to transfer data too.
 *
 * @returns  The direct connection if it's available *and* connected.
 */
01185 bool ApplicationList::hasDirectConnection() const
{
  return ( directConnection_ != 0 && directConnection_->isConnected() );
}



/**
 * @brief Return whether an MSN object transfer for the given msn object is already running.
 *
 * This is called by ChatMaster to avoid multiple invitations for the same MsnObject (display picture, wink, emoticon).
 *
 * @param msnObject  The requested msn object.
 * @return Whether the msn object is being transferred already.
 */
01200 bool ApplicationList::hasMsnObjectTransfer( const MsnObject &msnObject ) const
{
  foreach( P2PApplication *app, p2pApplications_ )
  {
    MsnObjectTransferP2P *transferApp = qobject_cast<MsnObjectTransferP2P*>( app );
    if( transferApp != 0 )
    {
      if( transferApp->getMsnObject().getDataHash() == msnObject.getDataHash() )
      {
        return true;
      }
    }
  }

  return false;
}



/**
 * @brief Return whether the transfer invitation has been sent.
 * @return  Whether there is a pending connection invitation.
 */
01223 bool ApplicationList::hasPendingConnectionInvitation() const
{
  return pendingConnectionInvitation_;
}



/**
 * @brief Return whether there are pending connection attempts.
 * @returns  Returns true when there are pending connection attempts.
 */
01234 bool ApplicationList::hasPendingConnections() const
{
  return (directConnectionPool_ != 0 && directConnectionPool_->hasPendingConnections());
}



/**
 * @brief Create the direct connection pool.
 *
 * This pool handles all connection attempts and returns
 * the connection that could be established.
 */
01247 void ApplicationList::initializeDirectConnectionPool()
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "Initializing direct connection pool.";
#endif
#ifdef KMESSTEST
  KMESS_ASSERT( directConnectionPool_ == 0 );
#endif

  if( directConnectionPool_ == 0 )
  {
    // The direct connection pool handles all connection attempts,
    // making it appear as if we only need to handle one connection.
    directConnectionPool_ = new DirectConnectionPool();

    connect(directConnectionPool_, SIGNAL(      activeConnectionAuthorized()  ),    // The connection was authorized
            this,                    SLOT(  slotActiveConnectionAuthorized()  ));
    connect(directConnectionPool_, SIGNAL(          activeConnectionClosed()  ),    // The active connection was closed
            this,                    SLOT(      slotActiveConnectionClosed()  ));
    connect(directConnectionPool_, SIGNAL(           connectionEstablished()  ),    // The connection was established
            this,                    SLOT(       slotConnectionEstablished()  ));
    connect(directConnectionPool_, SIGNAL(            allConnectionsFailed()  ),    // All connection attempts failed.
            this,                    SLOT(        slotAllConnectionsFailed()  ));
  }
}



/**
 * @brief Return whether the application list does not have any applications.
 * Note that there could also be a direct connection.
 * @return  Whether there are no MimeApplication or P2PAppliation objects left.
 */
01280 bool ApplicationList::isEmpty() const
{
  return ( mimeApplications_.isEmpty() && p2pApplications_.isEmpty() );
}



/**
 * @brief Pause all applications, avoid flooding the switchboard message queue.
 *
 * This is called when the send queue of all switchboards is full.
 * Applications are paused to prevent flooding of the queue.
 * The resumeApplications() method is called when
 * a switchboard is ready to send more messages.
 */
01295 void ApplicationList::pauseApplications()
{
  // This only affects P2Papplications now, mime applications don't send many messages.
  pauseApplications_ = true;   // setting this causes slotSwitchboardTestReadyWrite() to halt.
}



/**
 * @brief Register an application which is sending a file.
 *
 * This causes the application to receive requests to send new parts,
 * so the file transfer speed remains optimal for low and high speed network links.
 */
01309 void ApplicationList::registerDataSendingApplication( P2PApplicationBase *application )
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "adding application to the list of active outgoing transfers.";
#endif

  // See if it's the first app.
  bool isFirstApp = ( dataSendingApplications_.isEmpty() );

  // Add to list.
  dataSendingApplications_.append( application );


  // Make sure the application is polled frequently for the next data packets.
  // For the switchboard:       locally, frequent calls are made to slotSwitchboardTestReadyWrite().
  // For the direct connection: each time the socket is ready, slotConnectionReadyWrite() is called.
  if( ! hasAuthorizedDirectConnection() )
  {
    if( ! sbReadyWriteTester_.isActive() )
    {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kDebug() << "no direct connection available, starting switchboard write handler instead.";
#endif

      // Start the write handler for the switchboard.
      sbReadyWriteTester_.start( 10 );
    }
    else
    {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kDebug() << "no direct connection available, but switchboard write handler is already active.";
#endif
    }
  }
  else
  {
    // Attach the the write handler of the direct connection.
    // This is only needed once for the first application.
    // Still check if the handler is connected already, avoid race conditions with slotSwitchboardTestReadyWrite()
    if( ! directConnection_->isWriteHandlerConnected() )
    {
      if( isFirstApp )
      {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
        kDebug() << "first application added, "
                  << "starting connection write handler.";
#endif
      }
      else
      {
          kWarning() << "there are already applications, but no write handler connected yet.";
      }

      // Connect the write handler.
      directConnection_->connectWriteHandler( this, SLOT(slotConnectionReadyWrite()) );
    }
  }
}


/**
 * @brief Resume all applications, allowing messages to be sent again.
 *
 * This method is called when a MsnSwitchboardConnection is ready to send more messages.
 * @param isPrivateChat  Whether the available switchboard is a private chat,
 *                       otherwise only multi-chat capable applications may resume.
 */
01376 void ApplicationList::resumeApplications(bool /*isPrivateChat*/)
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "resuming applications.";
#endif
#ifdef KMESSTEST
  KMESS_ASSERT( pauseApplications_ );
#endif

  // Allow slotSwitchboardTestReadyWrite() again.
  pauseApplications_ = false;

  // See if there are still applications that need to write data.
  if( dataSendingApplications_.isEmpty() )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kDebug() << "no action taken because no appliations need to transfer data.";
#endif
    return;
  }

  // Restart the handler
  if( ! hasAuthorizedDirectConnection() )
  {
    if( sbReadyWriteTester_.isActive() )
    {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kDebug() << "switchboard write handler is already active, "
                << "no special action needed.";
#endif
    }
    else
    {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kDebug() << "restarting switchboard write handler.";
#endif

      // No direct connection (as expected), restart switchboard write handler.
      sbReadyWriteTester_.start( 10 );
    }
  }
  else
  {
    // A direct connection is available.
    // See if the direct connection has a write handler which triggers slotConnectionReadyWrite()
    if( directConnection_->isWriteHandlerConnected() )
    {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kDebug() << "direct connection is available with write handler, "
                << "no special action needed.";
#endif
    }
    else
    {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kDebug() << "direct connection is available, "
                << "reconnecting write handler..";
#endif

      // Connect the write handler to resume!
      directConnection_->connectWriteHandler( this, SLOT(slotConnectionReadyWrite()) );
    }
  }
}



/**
 * @brief Send a message for a MIME application.
 * @param  source       The MimeApplication responsable for sending the message.
 * @param  messageData  The message body.
 */
01448 void ApplicationList::sendMessage(const MimeApplication *source, const MimeMessage &messageData)
{
  QString messageBody( messageData.getFields() );
#ifdef KMESSTEST
  // Check whether a QByteArray is passed. gcc is able to create the cast
  // because MimeMessage() has a constructor which accepts a QByteArray.
  KMESS_ASSERT( messageBody[0].isLetter() );
  KMESS_ASSERT( messageData.getBinaryBody().size() == 0 );
#endif

  // Deliver the message over the switchboard in a mime message
  MimeMessage messageWrapper;
  messageWrapper.addField("MIME-Version", "1.0");
  messageWrapper.addField("Content-Type", "text/x-msmsgsinvite; charset=UTF-8");
  messageWrapper.setBody( messageBody );

  emit putMsg( messageWrapper, contactHandle_, (source == 0 || source->isPrivateChatRequired()) );
}



/**
 * @brief Send a P2P message over one of the available connections/bridges.
 *
 * If a direct connection is available, it will be used to transfer the message.
 * It's possible this connection is congested, and would block. In that case
 * false is returned.
 *
 * If a direct connection is not available, the message will be delivered
 * to a MsnSwitchboardConnection instance by the ChatMaster (connected to the putMsg() signal).
 * Before the P2PMessage can be handled by a switchboard it's wrapped in a MimeMessage,
 * as the switchboard sends all messages in MIME messages.
 *
 * @param source       The P2PApplication responsable for sending the message.
 * @param header       The message headers (48 bytes).
 * @param messageData  The message body / payload. This field is optional.
 * @param footerCode   The footer code (4 bytes), appended to messages which are sent over the switchboard.
 * @return             Whether the message can be sent. If the socket would block, false is returned.
 *                     This only occurs for direct connections connections.
 */
01488 bool ApplicationList::sendMessage(const P2PApplicationBase *source, const QByteArray &header, const QByteArray &messageData, uint footerCode)
{
  // Verify whether a direct connection exists to send the message.
  // It needs to be connected and authorized.
  if( directConnection_ != 0 )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kDebug() << "there is a direct connection object, "
                 "test if the socket is still connected and not timed out.";
#endif

    if( ! KMESS_NULL(directConnectionPool_) )
    {
      // verify connection, deletes closed connection.
      if( ! directConnectionPool_->verifyActiveConnection() )
      {
        // not valid, also unset here.
        directConnection_ = 0;
      }
    }
  }

  // Deliver over a direct connection or switchboard session.
  // Now also test if the connection is authorized, before sending data over it.
  if( directConnection_ != 0 && directConnection_->isAuthorized() )
  {
    // Deliver the message over the direct connection.
    // The footer will not be appended to the message.
    QByteArray p2pMessage = header + messageData;
    bool success = directConnection_->sendMessage( p2pMessage );
    if( success )
    {
      return true;
    }
    else if( directConnection_->hasTemporaryWriteError() )
    {
      kWarning() << "message could not be fully sent, "
                     "but the remaining part is in the send buffer.";
      return true;
    }
    else
    {
#ifdef KMESSTEST
      KMESS_ASSERT( directConnection_->hasLastWriteFailed() );
#endif

      kWarning() << "P2P message could not be sent over the direct connection, "
                     "using switchboard instead (contact=" << contactHandle_ << ").";

      // NOTE: The direct connection will still appear as connected.
      // Next events are not processed yet. That's why there is a check for hasLastWriteFailed() below.
    }

    // NOTE: This method does not return false anymore on socket write errors.
    //       Instead, it relies on the DirectConnectionBase class to send the remaining data if needed.
  }

  // Sending message over switchboard. Encapsulate in a MimeMessage.

  // At the end of the message we need to add an footer of 4 bytes.
  // This is little endian format, so we can't use P2PMessage::insertBytes()
  //
  // Possible values:
  //  0x00 session
  //  0x01 for display/emoticon
  //  0x02 filetransfer, or start menu code

  char footer[4];
  footer[0] = (char) ((footerCode >> 24) & 0xFF);
  footer[1] = (char) ((footerCode >> 16) & 0xFF);
  footer[2] = (char) ((footerCode >>  8) & 0xFF);
  footer[3] = (char) ( footerCode        & 0xFF);

  // Deliver the P2P message over the switchboard in a mime message
  MimeMessage messageWrapper;
  messageWrapper.addField("MIME-Version", "1.0");
  messageWrapper.addField("Content-Type", "application/x-msnmsgrp2p");
  messageWrapper.addField("P2P-Dest",     getContactHandle());
  messageWrapper.setBinaryBody(header, messageData, footer);

  emit putMsg( messageWrapper, contactHandle_, (source == 0 || source->isPrivateChatRequired()) );
  return true;
}



/**
 * @brief Reject an invitation for a mime-application.
 * @param invitationCookie  The cookie used in the invitation message.
 * @param notInstalled      Whether the invitation was rejected because it is not supported by KMess.
 */
01579 void ApplicationList::sendMimeRejectMessage( const QString &invitationCookie, bool notInstalled )
{
  // Create mime message
  MimeMessage message;
  message.addField( "Invitation-Command", "CANCEL"         );
  message.addField( "Invitation-Cookie",  invitationCookie );
  message.addField( "Cancel-Code",        (notInstalled ? "REJECT_NOT_INSTALLED" : "CANCEL") );

  // Forward to send
  sendMessage(0, message);
}



/**
 * @brief Set an application is adding connections, don't send "all failed" signal.
 */
01596 void ApplicationList::setAddingConnections(bool state)
{
  addingConnections_ = state;
}



/**
 * @brief Set whether the transfer invitation has been sent.
 *
 * This property is set by the P2PApplication class, to prevent
 * other P2PApplication classes from sending the same invitation.
 * The other P2PApplication classes wait until the first class establishes
 * the connection, aborts it, or the connection attempts failed.
 *
 * The state is automatically reset when a connection is established
 * or all connection attempts failed. If a class sets the state back to false
 * while a connection was pending, a connectionFailed() signal will be
 * fired so other P2P applications also continue to transfer data over the switchboard.
 *
 * @param state  True if the transfer invitation has been sent, false when it was redrawn.
 */
01618 void ApplicationList::setPendingConnectionInvitation(bool state)
{
  // Detect first
  bool failed = ( ! state && pendingConnectionInvitation_ );

  // set state
  pendingConnectionInvitation_ = state;

  // Emulate the connectionFailed() signal. The connection could not be made,
  // but it wasn't even attempted.
  if( failed )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kDebug() << "Pending connection invitation was aborted by P2PApplication, "
              << "informing application no direct connection is possible.";
#endif

    emit connectionFailed();
  }
}



/**
 * @brief The direct connection is authorized.
 *
 * At this point, the direct connection can be re-used by other P2P applications.
 */
01646 void ApplicationList::slotActiveConnectionAuthorized()
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "Direct connection with '" << contactHandle_ << "' authorized.";
#endif
#ifdef KMESSTEST
  KMESS_ASSERT( directConnection_ != 0 );
#endif

  // Inform *all* awaiting applications they can start to use the connection.
  // Each application will start their data transfer now by calling registerDataSendingApplication()
  emit connectionAuthorized();
}



/**
 * @brief The direct connection was closed.
 *
 * It resets the internal state and fires a connectionFailed() or connectionClosed() signal to other classes.
 */
01667 void ApplicationList::slotActiveConnectionClosed()
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "The direct connection with '" << contactHandle_ << "' was closed.";
#endif

  // Reset state variable.
  pendingConnectionInvitation_ = false;

  // Detect whether the connection was already set here.
  bool hadConnectionBefore = ( directConnection_ != 0 );

  // Reset the pointers first.
  // No deletion needed, the connection will be removed by the DirectConnectionPool later
  directConnection_ = 0;

  // If the pool was still active, it can be closed now.
  if( directConnectionPool_ != 0 )
  {
    directConnectionPool_->deleteLater();
    directConnectionPool_ = 0;
  }

  // Notify the listeners
  if( ! hadConnectionBefore )
  {
    // Active connection closed, but no pointer known.
    // It must be reset in slotGotDirectConnectionMessage() when the nonce was invalid.
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kDebug() << "The current connection was closed prematurely, "
              << "signaling instead that the connection failed.";
#endif

    // Notify the connection failed.
    // Applications start their transfer over the switchboard.
    emit connectionFailed();
  }
  else
  {
    // Also forward to our listeners.
    emit connectionClosed();

    // If there are still data sending applications, resume transfer over the switchboard.
    if( ! dataSendingApplications_.isEmpty() )
    {
      // Build a list of sessions for debugging
      QString activeSessions;
      foreach( P2PApplicationBase *app, dataSendingApplications_ )
      {
        if( ! activeSessions.isEmpty() )
        {
          activeSessions += ",";
        }
        activeSessions += QString::number( app->getSessionID() );
      }

      // Give users some insight why transfer becomes slow.
      kWarning() << "The direct connection closed "
                     "while there are active file transfers over the connection. "
                     "Switching sessions to slow indirect transfer "
                     "(sessions=" << activeSessions <<
                     " contact=" << contactHandle_ << ").";

#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kDebug() << "direct connection closed, "
                << "starting switchboard write handler to continue transfers.";
#endif

      // No direct connection (as expected), restart switchboard write handler.
      sbReadyWriteTester_.start( 10 );
    }
  }
}



/**
 * @brief All direct connection attempts failed.
 *
 * This resets the internal state and fires the connectionFailed() signal.
 */
01748 void ApplicationList::slotAllConnectionsFailed()
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "The direct connection with '" << contactHandle_ << "' could not be made.";
#endif
#ifdef KMESSTEST
  KMESS_ASSERT( directConnectionPool_ != 0 );
  KMESS_ASSERT( ! directConnectionPool_->hasActiveConnection()   );
  KMESS_ASSERT( ! directConnectionPool_->hasPendingConnections() );
#endif

  // It's possible a connection fails directly before all addConnection() calls are made.
  // Don't notify applications to revert the transfer.
  // This should also avoid hangs at the "connect()" call.
  if( directConnection_ == 0 && addingConnections_ )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kDebug() << "No connections left, but still adding connections. Ignoring event.";
#endif
    return;
  }

#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  if( directConnection_ == 0 )
  {
    kDebug() << "No remaining direct connections left";
  }
  else
  {
    kDebug() << "The current connection was closed prematurely";
  }
#endif

  // Reset state variable.
  pendingConnectionInvitation_ = false;
  directConnection_ = 0;   // somehow it could be != 0 with timeout events.

  // If the pool was still active, it can be closed now.
  if( directConnectionPool_ != 0 )
  {
    directConnectionPool_->deleteLater();
    directConnectionPool_ = 0;
  }

  // Also forward to our listeners.
  // This will cause the application to initiate the transfer.
  // The messages will be sent over the switchboard instead as fallback.
  emit connectionFailed();
}



/**
 * @brief The direct connection is established.
 *
 * This sets the internal state and fires a connectionEstablished() signal.
 */
01805 void ApplicationList::slotConnectionEstablished()
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "Direct connection with '" << contactHandle_ << "' established.";
#endif
#ifdef KMESSTEST
  KMESS_ASSERT( directConnection_     == 0 );
  KMESS_ASSERT( directConnectionPool_ != 0 );
#endif

  // Reset state variable.
  pendingConnectionInvitation_ = false;

  // Get active connection.
  directConnection_ = static_cast<MsnDirectConnection*>( directConnectionPool_->getActiveConnection() );
  if(KMESS_NULL(directConnection_)) return;

  // Inform the requested application it can start to initialize the connection.
  // This application will authenticate the connection itself,
  // and slotActiveConnectionAuthorized() will be called afterwards.
  // If the authentication fails, slotAllConnectionsFailed() will be called instead.
  emit connectionEstablished();
}



/**
 * @brief The direct connection is ready to send more data.
*/
01834 void ApplicationList::slotConnectionReadyWrite()
{
  if( KMESS_NULL(directConnection_) ) return;

#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "The direct connection is ready to send more data, "
            << "notifying applications.";
#endif

  // Disconnect the event handler if there are no longer applications that need to transfer data.
  if( dataSendingApplications_.isEmpty() || p2pApplications_.isEmpty() )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kDebug() << "no active outgoing transfers left, "
              << "disconnecting write handler.";
#endif

    directConnection_->disconnectWriteHandler( this, SLOT(slotConnectionReadyWrite()) );
    return;
  }

  // Determine the number of preferred fragments to send.
  // By default 3 for a direct connection, unless there are more parallel transfers.
  int preferredFragments = CEIL( qMin( 3, 4 / dataSendingApplications_.count() ) );

  // Start notifying.
  bool writeSuccess = true;
  foreach( P2PApplicationBase *app, dataSendingApplications_ )
  {
    // Tell the application to send the next file parts.
    writeSuccess = app->sendNextDataParts( preferredFragments );

    // Avoid freezes, don't call the app again if will fail every time.
    if( ! writeSuccess )
    {
      unregisterDataSendingApplication( app );
    }
  }
}



/**
 * @brief A message was received from the direct connection.
 *
 * The message will be verified and forwarded to the correct P2PApplication instance (using gotMessage()).
 * If it's a handshake message containing a Nonce, it returns the message to the application
 * which generated the Nonce. If no application was found, the connection will be dropped
 * to indicate the Nonce was invalid.
 *
 * @param  messageData  The received message data.
 */
01886 void ApplicationList::slotGotDirectConnectionMessage(const QByteArray &messageData)
{
#ifdef KMESSTEST
  KMESS_ASSERT( directConnection_ != 0 );
#endif

  // Last check to know this is really a P2P message.
  if(messageData.size() < 48)
  {
    kWarning() << "Received an unknown message from the "
                << " direct-connection-link (message dump follows).\n" << messageData.data();
    return;
  }

  // Parse the message
  P2PMessage message(messageData);
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "Received data message from the direct connection link"
            << " (flags=0x" << QString::number(message.getFlags(), 16) << ").";
#endif


  // The nonce/handshake message doesn't have ack fields, so parse it here.
  if( message.isConnectionHandshake() )
  {
    P2PApplication *app = getApplicationByNonce( message.getNonce() );

    if( app == 0 )
    {
      // Invalid nonce! Drop connection
      kWarning() << "Contact sent invalid nonce or session was already terminated, "
                  << "closing direct connection.";
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
      kDebug() << "Closing connection, moving transfer to switchboard.";
#endif
#ifdef KMESSTEST
      KMESS_ASSERT( sender() != 0 );
      KMESS_ASSERT( static_cast<const MsnDirectConnection*>(sender()) == directConnectionPool_->getActiveConnection() );
#endif

      // Close the connection.
      directConnection_ = 0;   // used for slotConnectionClosed()
      directConnectionPool_->getActiveConnection()->closeConnection();  // DirectConnectionPool deletes it.
      directConnectionPool_->deleteLater();
      directConnectionPool_ = 0;
    }
    else
    {
      // Nonce is valid.
      app->gotMessage( message );
    }
  }
  else
  {
    // Normal message
    // Deliver the message to the correct p2p application.
    gotMessage(message);
  }
}



/**
 * @brief The switchboard is ready to send more data.
 *
 * This method works very much like slotConnectionReadyWrite(),
 * except the source of the signal differs.
 */
01954 void ApplicationList::slotSwitchboardTestReadyWrite()
{
  // Test if the switchboard requested a pause,
  // meaning it's no longer ready.
  if( pauseApplications_ )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kDebug() << "switchboard requested pause, "
              << "postponing action.";
#endif
    return;
  }

  // Disconnect the event handler if there are no longer applications that need to transfer data.
  if( dataSendingApplications_.isEmpty() ||  p2pApplications_.isEmpty() )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kDebug() << "no active outgoing transfers left, "
              << "disconnecting write handler.";
#endif
    return;
  }

#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "a switchboard is ready to send more data, "
            << "notifying applications.";
#endif

  // Start notifying.
  bool writeSuccess = true;
  foreach( P2PApplicationBase *app, dataSendingApplications_ )
  {
    // Tell the application to send the next file parts.
    writeSuccess = app->sendNextDataParts( 1 );

    // Avoid freezes, don't call the app again if will fail every time.
    if( ! writeSuccess )
    {
      unregisterDataSendingApplication( app );
    }
  }

  // Find out if the applications are done now.
  if( dataSendingApplications_.isEmpty() )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kDebug() << "all applications are done.";
#endif
    return;
  }

  // See if we can switch to a Direct connection instead.
  //
  // The "last write failed" check is required, because isConnected() still returns true,
  // even if writeBlock() failed with a "broken pipe" error, and used the SB instead. The stack becomes:
  // this->slotSwitchboardTestReadyWrite() -> p2papp->sendNextDataParts() -> this->sendMessage() -> dc->writeBlock()
  //
  // Also look out for direct connections which did have a temporary write error,
  // but have the remaining message buffered to send later.
  bool switchToDc = ( hasAuthorizedDirectConnection()
                      && ( ! directConnection_->hasLastWriteFailed() || directConnection_->hasTemporaryWriteError() ) );

#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  if( hasAuthorizedDirectConnection() )
  {
    if( directConnection_->hasLastWriteFailed() )
    {
      kDebug() << "A direct connection became available, "
                  "but had a error while writing. Will continue to use the switchboard.";
    }
  }
#endif

  // Applications are not done with the transfer yet.
  // Find out how to make sure sendNextDataParts() is called again.
  if( ! switchToDc )
  {
    // Continue with normal behavour
    // Currently this method triggers itself,
    // until the switchboards are busy and "pausedApplications_" is set.
    if( ! pauseApplications_ )
    {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kDebug() << "scheduling next call.";
#endif

      // Instead of receiving a signal from the switchboard,
      // fake one right now. This is currently good enough.
      sbReadyWriteTester_.start( 200 );
    }
    else
    {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kDebug() << "switchboard requested to pause the transfer, "
                << "postponing next action.";
#endif
    }
  }
  else
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kDebug() << "switching to direct connection transfer.";
#endif

    // There appears to be a direct connection!
    // It's no longer needed to transfer the data over the switchboard
    // so find out if the write handler is not connected yet.
    if( ! directConnection_->isWriteHandlerConnected() )
    {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kDebug() << "no write handler connected yet, "
                << "connecting write handler to direct connection.";
#endif
      directConnection_->connectWriteHandler( this, SLOT(slotConnectionReadyWrite()) );
    }
  }
}



/**
 * @brief An application was finished and requested removal.
 * @param  application  The application object instance.
 */
02078 void ApplicationList::slotTerminateApplication(Application *application)
{
  MimeApplication    *mimeApp = qobject_cast<MimeApplication*>( application );
  P2PApplicationBase *p2pApp  = qobject_cast<P2PApplicationBase*>( application );

  // Update the internal lists.
  if( mimeApp != 0 )
  {
    // It's a mime application
    if( mimeApplications_.removeAll( mimeApp ) == 0 )
    {
      kWarning() << "Could not remove MimeApplication"
                  << " (type=" << mimeApp->metaObject()->className() << " contact=" << contactHandle_ << ")!";
    }
  }
  else if( p2pApp != 0 )
  {
    // Remove from data sending apps too!
    // Use the same unregister function so the write handler will also be disconnected.
    if( dataSendingApplications_.contains( p2pApp ) )
    {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kDebug() << "application is terminating while sending data, unregistering as data sending application first.";
#endif

      unregisterDataSendingApplication( p2pApp );
    }

    // Remove from collection
    if( p2pApplications_.removeAll( static_cast<P2PApplication*>( p2pApp ) ) == 0 )
    {
      kWarning() << "Could not remove P2PApplication"
                    " (type=" << p2pApp->metaObject()->className() << " contact=" << contactHandle_ << ")!";
    }
  }
  else
  {
    // Warn when there is a problem deleting the object.
    kWarning() << "Can't determine type of application "
                  " (type=" << application->metaObject()->className() << " contact=" << contactHandle_ << ")!";
  }


#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "Removing application.";
#endif

  // Remove the application
  application->deleteLater();


  // Check whether this application was scheduelled to be aborted by contactLeftChat()
  // When all applications aborted, signal so that the switchboard connection can be terminated.
  // This is using a QPtrList so the process is not disturbed by newly added applications or apps left active.
  if( ! abortingApplications_.isEmpty() )
  {
    if( abortingApplications_.removeAll( application ) )
    {
      if( ! abortingApplications_.isEmpty() )
      {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
        kDebug() << "Still " << abortingApplications_.count() << " other applications left to abort...";
#endif
      }
      else
      {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
        kDebug() << "Signalling that applications of '" << contactHandle_ << "' have been aborted.";
#endif

        // This may cause listeners to delete this object..
        emit applicationsAborted(contactHandle_);
      }
    }
    else
    {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kDebug() << "Application was not part of the scheduelled applications to abort.";
#endif
    }
  }
}


/**
 * @brief Remove the registration of an application which sends a file.
 */
02165 void ApplicationList::unregisterDataSendingApplication( P2PApplicationBase *application )
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kDebug() << "removing application from the list of active outgoing transfers.";
#endif

  bool found = dataSendingApplications_.removeAll( application ) > 0;
  if( ! found )
  {
    kWarning() << "application not found in list.";
  }

  // Stop write handler is all applications stopped sending a file.
  if( dataSendingApplications_.isEmpty() && directConnection_ != 0 )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kDebug() << "no applications remaining, disabling connection write handler.";
#endif

    directConnection_->disconnectWriteHandler( this, SLOT(slotConnectionReadyWrite()) );
  }
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  else
  {
    // Debug why the write handler was not disabled yet.
    if( directConnection_ != 0 )
    {
      kDebug() << "connection write handler not disabled, there are still " << dataSendingApplications_.count() << " applications sending file data.";
    }
    else
    {
      kDebug() << "connection write handler not disabled, no direct connection active already.";
    }
  }
#endif
}



#include "applicationlist.moc"

Generated by  Doxygen 1.6.0   Back to index