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.                                   *
 *                                                                         *
 ***************************************************************************/

// General application classes
#include "applicationlist.h"
#include "mimeapplication.h"
#include "p2papplication.h"

// MIME application classes
#include "filetransfer.h"
#include "gnomemeeting.h"
#include "msnremotedesktop.h"
#include "voiceconversation.h"

// P2P application classes
#include "picturetransferp2p.h"
#include "filetransferp2p.h"
#include "webapplicationp2p.h"
#include "webcamtransferp2p.h"

#include "../mimemessage.h"
#include "../p2pmessage.h"
#include "../msnswitchboardconnection.h"
#include "../extra/directconnectionpool.h"
#include "../extra/msndirectconnection.h"
#include "../../currentaccount.h"
#include "../../kmessdebug.h"
#include "../../msnobject.h"
#include "../../chat/chatmessage.h"
#include "../../contact/contactbase.h"

#include <qregexp.h>

#include <klocale.h>

#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.
 */
00064 ApplicationList::ApplicationList(const QString &contactHandle)
  : QObject(0, "ApplicationList")
  , addingConnections_(false)
  , contactHandle_(contactHandle.lower())
  , directConnection_(0)
  , directConnectionPool_(0)
  , pauseApplications_(false)
  , pendingConnectionInvitation_(false)
{
  connect( &sbReadyWriteTester_, SIGNAL(timeout()), this, SLOT(slotSwitchboardTestReadyWrite()) );
}



/**
 * @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.
 */
00085 ApplicationList::~ApplicationList()
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList - entering destructor." << endl;
#endif

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

    // Remove all mime applications
    QPtrListIterator<MimeApplication> it1( mimeApplications_ );
    while( it1.current() != 0 )
    {
      delete it1.current();
      ++it1;
    }
  }

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

    // Remove all p2p applications
    QPtrListIterator<P2PApplication> it2( p2pApplications_ );
    while( it2.current() != 0 )
    {
      delete it2.current();
      ++it2;
    }
  }


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

#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "DESTROYED ApplicationList [handle=" << contactHandle_ << "]" << endl;
#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.
 */
00147 bool ApplicationList::abortApplications(const MsnSwitchboardConnection *abortingConnection, bool userInitiated)
{
  // Check if there is nothing to do
  if( mimeApplications_.isEmpty() && p2pApplications_.isEmpty() )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kdDebug() << "ApplicationList::abortApplications: No applications active." << endl;
#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 QPtrList<MsnSwitchboardConnection>   switchboards = contact->getSwitchboardConnections();
    QPtrListIterator<MsnSwitchboardConnection> switchboardIt(switchboards);;

    while( switchboardIt.current() != 0 )
    {
      MsnSwitchboardConnection *connection = switchboardIt.current();
      if( connection != abortingConnection )
      {
        if( connection->isExclusiveChatWithContact( contactHandle_ ) )
        {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
          kdDebug() << "ApplicationList::abortApplications: Contact is still in another private chat, not terminating applications." << endl;
#endif
          return (! abortingApplications_.isEmpty());  // in case the function was called before
        }
        else
        {
          hasMultiChats = true;
        }
      }

      ++switchboardIt;
    }
  }


  // Check if any multi chats are found.
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  if( ! hasMultiChats )
  {
    kdDebug() << "ApplicationList::abortApplications: Contact left all chats, aborting applications." << endl;
  }
  else
  {
    kdDebug() << "ApplicationList::abortApplications: Contact left all private chats, aborting applications that don't support multi-chats." << endl;
  }
#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.
  QPtrListIterator<MimeApplication> it(mimeApplications_);
  while( it.current() != 0 )
  {
    MimeApplication *app = it.current();
    if( ( abortingApplications_.count() > 0 && ! 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.
        }
      }
    }
    ++it;
  }

  // Then abort all p2p-based applications.
  QPtrListIterator<P2PApplication> it2(p2pApplications_);
  while( it2.current() != 0 )
  {
    P2PApplication *app = it2.current();
    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.
        }
      }
    }
    ++it2;
  }

  // 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
  kdDebug() << "ApplicationList::abortApplications: Got " << abortingApplications_.count() << " applications left to abort." << endl;
#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.
 */
00278 void ApplicationList::addApplication(MimeApplication *application)
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList::addApplication: connecting mime application signals." << endl;
#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&) ));
}



/**
 * @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.
 */
00305 void ApplicationList::addApplication(P2PApplication *application)
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList::addApplication: connecting p2p application signals." << endl;
#endif

  p2pApplications_.append( application );
  connectApplication( 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.
 */
00332 bool ApplicationList::addConnection(const QString &ipAddress, const int 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.
      kdWarning() << "ApplicationList: Refusing connection attempt, a connection has already been made "
                     "(contact=" << contactHandle_ << " action=return)." << endl;
      return 0;
    }
  }

#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList::addConnection: adding new connection attempt." << endl;
#endif

  // Avoid overwriting the current directConnection_ variable.
  if( directConnection_ != 0 )
  {
    kdWarning() << "ApplicationList: Refusing connection attempt, a connection has already been made "
                    "(contact=" << contactHandle_ << " action=return)." << endl;
    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.
 */
00393 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.
      kdWarning() << "ApplicationList: Refusing server connection attempt, a connection has already been made "
                      "(contact=" << contactHandle_ << " action=return)." << endl;
      return 0;
    }
  }

#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList::addServerConnection: adding new server socket." << endl;
#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.
 */
00443 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.
 */
00461 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.
 */
00477 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.
 */
00494 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:
    kdWarning() << "ApplicationList: MIME message can't be handled, "
                << "Application-GUID not found. (message dump follows)\n" << message.getFields() << endl;
    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
  kdDebug() << "ApplicationList: Connecting MimeApplication signals" << endl;
#endif

  // Auto-add application
  addApplication(app);

  // Inform listeners about the new application (notably ChatMaster)
  emit newApplication(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. 
 */
00545 P2PApplication * ApplicationList::createApplication(const P2PMessage &message)
{
#ifdef KMESSTEST
  ASSERT( message.getSessionID() == 0 );
  ASSERT( message.getDataSize()   > 0 );
#endif

#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList: Creating new P2PApplication instance to parse the P2P message." << endl;
#endif

  P2PApplication *app = 0;

  // Extract the SLP data message
  QString slpMessage = QString::fromUtf8( message.getData(), 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.find(":"), 20) );
    kdWarning() << "ApplicationList: 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.
                   ")." << endl;
    return 0;
  }


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

  // If no EUF-GUID is found:
  if( guidPos == -1 )
  {
    // Notify the user:
    kdWarning() << "ApplicationList: P2P message can't be handled, "
                << "EUF-GUID not found. (message dump follows)\n" << slpMessage << endl;
    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
  kdDebug() << "ApplicationList: Connecting P2PApplication signals" << endl;
#endif

  // Auto-add application
  addApplication(app);

  // Inform listeners about the new application (notably ChatMaster)
  emit newApplication(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.
 */
00623 MimeApplication * ApplicationList::createApplicationByGuid(const QString &appGuid)
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList: Creating MimeApplication for GUID " << appGuid << endl;
#endif

  if( appGuid == GnomeMeeting::getAppId() )
  {
    return new GnomeMeeting( contactHandle_ );
  }
  else if( appGuid == MsnRemoteDesktop::getAppId() )
  {
    return new MsnRemoteDesktop( contactHandle_ );
  }
  else if( appGuid == FileTransfer::getAppId() )
  {
    return new FileTransfer( contactHandle_ );
  }
  else if( appGuid == VoiceConversation::getAppId() )
  {
    // TODO: check if there is already an active audio conversation.
    // i18n("The contact is inviting you to start an audio conversation, but you can't have more then one audio conversation at the same time.")) );
    return new VoiceConversation( contactHandle_ );
  }
  else
  {
    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.
 */
00660 P2PApplication * ApplicationList::createApplicationByEufGuid(const QString &eufGuid)
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList: Creating P2PApplication for GUID " << eufGuid << endl;
#endif

  if( eufGuid == PictureTransferP2P::getAppId() )
  {
    return new PictureTransferP2P( this );
  }
  else if( eufGuid == FileTransferP2P::getAppId() )
  {
    return new FileTransferP2P( this );
  }
  else if( eufGuid == WebApplicationP2P::getAppId() )
  {
    return new WebApplicationP2P( this );
  }
  else if( eufGuid == WebcamTransferP2P::getPullAppId() )
  {
    return new WebcamTransferP2P( this );
  }
  else if( eufGuid == WebcamTransferP2P::getPushAppId() )
  {
    return new WebcamTransferP2P( this );
  }
  else
  {
    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.
 */
00699 P2PApplication * ApplicationList::getApplicationByAckData(const P2PMessage &ackMessage) const
{
  unsigned long ackUniqueID  = ackMessage.getAckUniqueID();
  QPtrListIterator<P2PApplication> p2pIterator(p2pApplications_);
  while( p2pIterator.current() != 0 )
  {
    // The application may have multiple messages unacked, see whether the ACK matches.
    if( p2pIterator.current()->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)
        kdWarning() << "P2PApplication: Got P2P message with matching ackSessionID and no ackUniqueID set "
                    << "(delivering anyway, contact=" << contactHandle_ << ")!" << endl;
      }

      return p2pIterator.current();
    }

    ++p2pIterator;
  }

  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.
 */
00733 P2PApplication * ApplicationList::getApplicationByCallId(const QString &callID) const
{
  QPtrListIterator<P2PApplication> p2pIterator(p2pApplications_);
  while( p2pIterator.current() != 0 )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kdDebug() << "ApplicationList: Demultiplexing P2P message"
              << "    (CallID " << p2pIterator.current()->getCallID() << " == " << callID  << " ?)" << endl;
#endif

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

    ++p2pIterator;
  }

  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.
 */
00766 Application * ApplicationList::getApplicationByCookie(const QString &cookie) const
{
  // First see if there is a p2p application with the cookie.
  QPtrListIterator<P2PApplication> p2pIterator(p2pApplications_);
  while ( p2pIterator.current() != 0 )
  {
    if ( p2pIterator.current()->getCookie() == cookie )
    {
      // Found it, return reference.
      return p2pIterator.current();
    }

    ++p2pIterator;
  }

  // Also look for a mime application with the cookie.
  QPtrListIterator<MimeApplication> mimeIterator(mimeApplications_);
  while ( mimeIterator.current() != 0 )
  {
    if ( mimeIterator.current()->getCookie() == cookie )
    {
      // Found it, return reference.
      return mimeIterator.current();
    }

    ++mimeIterator;
  }

  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.
 */
00804 P2PApplication * ApplicationList::getApplicationByLastMessage(unsigned long messageID) const
{
  QPtrListIterator<P2PApplication> p2pIterator(p2pApplications_);
  while( p2pIterator.current() != 0 )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    unsigned long debugMsgId = p2pIterator.current()->getLastContactMessageID();
    kdDebug() << "ApplicationList: Demultiplexing P2P message"
              << "    (fragment MessageID " << debugMsgId << " == " << messageID << " ?)" << endl;
#endif

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

      // Found it, save reference
      return p2pIterator.current();
    }

    ++p2pIterator;
  }

  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.
 */
00836 P2PApplication * ApplicationList::getApplicationByNonce(const QString &nonce) const
{
  QPtrListIterator<P2PApplication> p2pIterator(p2pApplications_);
  while( p2pIterator.current() != 0 )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kdDebug() << "ApplicationList: Demultiplexing P2P message"
              << "    (Nonce " << p2pIterator.current()->getNonce() << " == " << nonce << " ?)" << endl;
#endif

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

    ++p2pIterator;
  }

  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.
 */
00865 P2PApplication * ApplicationList::getApplicationBySessionId(unsigned long sessionID) const
{
  QPtrListIterator<P2PApplication> p2pIterator(p2pApplications_);
  while( p2pIterator.current() != 0 )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kdDebug() << "ApplicationList: Demultiplexing P2P message"
              << "    (SessionID " << p2pIterator.current()->getSessionID() << " == " << sessionID << " ?)" << endl;
#endif
    if( p2pIterator.current()->getSessionID() == sessionID )
    {
      // Found the session
      return p2pIterator.current();
    }

    ++p2pIterator;
  }

  return 0;
}



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



/**
 * @brief Return the direct connection, if available.
 * @returns The direct connection object.
 */
00903 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.
 */
00920 void ApplicationList::gotMessage(const MimeMessage &message)
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList: Parsing Mime appliation message." << endl;
  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 )
    {
      kdWarning() << "ApplicationList: Unable to handle MIME message, "
                  << "Invitation-Cookie not found (contact=" << contactHandle_ << ")." << endl;
      return;
    }
  }

  // Last check before casting the object.
  if( ! app->inherits("MimeApplication") )
  {
    kdWarning() << "ApplicationList::gotMessage: found application is not a mime application!" << endl;
    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.
 */
00976 void ApplicationList::gotMessage(const P2PMessage &message)
{
  // Parse the message
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList: 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() << endl;
#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 );

    // Notify the user if the message was invalid (send error-response later)
    if(p2pApplication == 0)
    {
      kdWarning() << "ApplicationList: Unable to handle P2P message, Session-ID " << sessionID << " not found." << endl;
    }
  }
  else if( message.getFlags() != 0 && 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() )
    {
      kdWarning() << "ApplicationList: Received a direct connection handshake message at the switchboard "
                     "(contact=" << contactHandle_ << " action=return)." << endl;
      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.
      kdWarning() << "ApplicationList: P2P message can't be handled,"
                     " no match for ackSessionID " << message.getAckSessionID() <<
                     " or ackUniqueID " << message.getAckUniqueID() << " found." << endl;
    }
  }
  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)
    kdWarning() << "SwitchBoard P2P message has no ACK fields set at all, assuming base identifier message!" << endl;
  }
  else if( message.getFlags() == 0
       &&  message.getDataSize() > 0)  // HACK: added size constraint for Bot2k3 4.1 (sends an empty message with a zero flag)
  {
    // 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(), message.getDataSize() );
      int callPos = callRE.search(slpMessage);

      if( callPos == -1 )
      {
        kdWarning() << "ApplicationList: Unable to handle P2P message, CallID field not found." << endl;
      }
      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
          kdDebug() << "ApplicationList: Found null Call-ID, using Session-ID to deliver the message." << endl;
#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.search( slpMessage );
          if( sessionPos == -1 )
          {
            kdWarning() << "ApplicationList: Unable to handle P2P message, found null-guid CallID but no SessionID instead." << endl;
          }
          else
          {
            unsigned long sessionID = sessionRE.cap(1).toLong();
            if( sessionID != 0 )
            {
              p2pApplication = getApplicationBySessionId( sessionID );
            }
            else
            {
              // Also observed in some cases.
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
              kdDebug() << "ApplicationList: Also found zero Session-ID, will create an empty application instance to handle the message." << endl;
#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 );
            }
          }
        }
        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
            kdDebug() << "ApplicationList: No existing P2P session found for the MSNSLP message, "
                      << "must be a new session invitation." << endl;
#endif
 
            p2pApplication = createApplication( message );
          }
        }
      }
    }
  }
  else
  {
    // Didn't reconize the flag type.
    kdWarning() << "ApplicationList: P2P message can't be handled, unknown flag type found." << endl;
    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.
    kdWarning() << "ApplicationList: 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_ << ")." << endl;

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

    // 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.getDataSize() == 0 )
    {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kdDebug() << "ApplicationList: Not sending a P2P Error response for error message." << endl;
#endif
    }
    else
    {
      kdDebug() << "ApplicationList: Creating temporary P2PApplication instance to send error response." << endl;

      // 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 );
      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.
 */
01189 bool ApplicationList::hasAuthorizedDirectConnection() const
{
  return ( directConnection_ != 0 && directConnection_->isAuthorized() && directConnection_->isConnected() );
}



/**
 * @brief Return whether there are applications which need to send data.
 */
01199 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.
 */
01219 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.
 */
01234 bool ApplicationList::hasMsnObjectTransfer( const MsnObject &msnObject ) const
{
  QPtrListIterator<P2PApplication> it( p2pApplications_ );
  P2PApplication *app;
  PictureTransferP2P *transferApp;
  while( it.current() != 0 )
  {
    app = it.current();
    if( app->isA("PictureTransferP2P") )
    {
      transferApp = static_cast<PictureTransferP2P*>( app );
      if( transferApp->getMsnObject().getDataHash() == msnObject.getDataHash() )
      {
        return true;
      }
    }

    ++it;
  }

  return false;
}



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



/**
 * @brief Return whether there are pending connection attempts.
 * @returns  Returns true when there are pending connection attempts.
 */
01274 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.
 */
01287 void ApplicationList::initializeDirectConnectionPool()
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList: Initializing direct connection pool." << endl;
#endif
#ifdef KMESSTEST
  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.
 */
01320 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.
 */
01335 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.
 */
01349 void ApplicationList::registerDataSendingApplication( P2PApplication *application )
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList::registerDataSendingApplication() - adding application to the list of active outgoing transfers." << endl;
#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
      kdDebug() << "ApplicationList::registerDataSendingApplication() - no direct connection available, starting switchboard write handler instead." << endl;
#endif

      // Start the write handler for the switchboard.
      sbReadyWriteTester_.start( 10, true );
    }
    else
    {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kdDebug() << "ApplicationList::registerDataSendingApplication() - no direct connection available, but switchboard write handler is already active." << endl;
#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
        kdDebug() << "ApplicationList::registerDataSendingApplication() - first application added, "
                  << "starting connection write handler." << endl;
#endif
      }
      else
      {
          kdWarning() << "ApplicationList::registerDataSendingApplication() - there are already applications, but no write handler connected yet." << endl;
      }

      // 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.
 */
01416 void ApplicationList::resumeApplications(bool /*isPrivateChat*/)
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList::resumeApplications() - resuming applications." << endl;
#endif
#ifdef KMESSTEST
  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
    kdDebug() << "ApplicationList::resumeApplications() - no action taken because no appliations need to transfer data." << endl;
#endif
    return;
  }

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

      // No direct connection (as expected), restart switchboard write handler.
      sbReadyWriteTester_.start( 10, true );
    }
  }
  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
      kdDebug() << "ApplicationList::resumeApplications() - direct connection is available with write handler, "
                << "no special action needed." << endl;
#endif
    }
    else
    {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kdDebug() << "ApplicationList::resumeApplications() - direct connection is available, "
                << "reconnecting write handler.." << endl;
#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.
 */
01488 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.
  ASSERT( messageBody[0].isLetter() );
  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.
 */
01528 bool ApplicationList::sendMessage(const P2PApplication *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
    kdDebug() << "ApplicationList::sendMessage() - there is a direct connection object, "
                 "test if the socket is still connected and not timed out." << endl;
#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.
    // Concatenate the message.
    uint  rawSize = (header.size() + messageData.size());   // header + body
    char *rawData = new char[rawSize];                      // create buffer
    memcpy(rawData, header.data(), header.size());          // copy header

    if( messageData.size() > 0 )
    {
      // Copy body
      memcpy(rawData + header.size(), messageData.data(), messageData.size());
    }

    // Wrap *rawData in a QByteArray, and sent over the direct connection
    QByteArray p2pMessage;
    p2pMessage.assign(rawData, rawSize);  // auto deletes *rawData
    bool success = directConnection_->sendMessage(p2pMessage);
    if( success )
    {
      return true;
    }
    else if( directConnection_->hasTemporaryWriteError() )
    {
      kdWarning() << "ApplicationList::sendMessage() - message could not be fully sent, "
                     "but the remaining part is in the send buffer." << endl;
      return true;
    }
    else
    {
#ifdef KMESSTEST
      ASSERT( directConnection_->hasLastWriteFailed() );
#endif

      kdWarning() << "ApplicationList::sendMessage() - P2P message could not be sent over the direct connection, "
                     "using switchboard instead (contact=" << contactHandle_ << ")." << endl;

      // 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.
 */
01632 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.
 */
01649 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.
 */
01671 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
    kdDebug() << "ApplicationList: Pending connection invitation was aborted by P2PApplication, "
              << "informing application no direct connection is possible." << endl;
#endif

    emit connectionFailed();
  }
}



/**
 * @brief The direct connection is authorized.
 *
 * At this point, the direct connection can be re-used by other P2P applications.
 */
01699 void ApplicationList::slotActiveConnectionAuthorized()
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList: Direct connection with '" << contactHandle_ << "' authorized." << endl;
#endif
#ifdef KMESSTEST
  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.
 */
01720 void ApplicationList::slotActiveConnectionClosed()
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList: The direct connection with '" << contactHandle_ << "' was closed." << endl;
#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
    kdDebug() << "ApplicationList: The current connection was closed prematurely, "
              << "signaling instead that the connection failed." << endl;
#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;
      QPtrListIterator<P2PApplication> it( dataSendingApplications_ );
      while( it.current() != 0 )
      {
        P2PApplication *app = it.current();
        if( ! activeSessions.isEmpty() )
        {
          activeSessions += ",";
        }
        activeSessions += QString::number( app->getSessionID() );
        ++it;
      }

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

#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kdDebug() << "ApplicationList::slotActiveConnectionClosed() - direct connection closed, "
                << "starting switchboard write handler to continue transfers." << endl;
#endif

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



/**
 * @brief All direct connection attempts failed.
 *
 * This resets the internal state and fires the connectionFailed() signal.
 */
01804 void ApplicationList::slotAllConnectionsFailed()
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList::slotAllConnectionsFailed() - The direct connection with '" << contactHandle_ << "' could not be made." << endl;
#endif
#ifdef KMESSTEST
  ASSERT( directConnectionPool_ != 0 );
  ASSERT( ! directConnectionPool_->hasActiveConnection()   );
  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
    kdDebug() << "ApplicationList::slotAllConnectionsFailed() - No connections left, but still adding connections. Ignoring event." << endl;
#endif
    return;
  }

#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  if( directConnection_ == 0 )
  {
    kdDebug() << "ApplicationList::slotAllConnectionsFailed() - No remaining direct connections left" << endl;
  }
  else
  {
    kdDebug() << "ApplicationList::slotAllConnectionsFailed() - The current connection was closed prematurely" << endl;
  }
#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.
 */
01861 void ApplicationList::slotConnectionEstablished()
{
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList: Direct connection with '" << contactHandle_ << "' established." << endl;
#endif
#ifdef KMESSTEST
  ASSERT( directConnection_     == 0 );
  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.
*/
01890 void ApplicationList::slotConnectionReadyWrite()
{
  if( KMESS_NULL(directConnection_) ) return;

#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList::slotConnectionReadyWrite() - The direct connection is ready to send more data, "
            << "notifying applications." << endl;
#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
    kdDebug() << "ApplicationList::slotConnectionReadyWrite() - no active outgoing transfers left, "
              << "disconnecting write handler." << endl;
#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;
  QPtrListIterator<P2PApplication> it( dataSendingApplications_ );
  while( it.current() != 0 )
  {
    // Tell the application to send the next file parts.
    P2PApplication *app = it.current();
    writeSuccess = app->sendNextDataParts( preferredFragments );

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

    ++it;
  }
}



/**
 * @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.
 */
01946 void ApplicationList::slotGotDirectConnectionMessage(const QByteArray &messageData)
{
#ifdef KMESSTEST
  ASSERT( directConnection_ != 0 );
#endif

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

  // Parse the message
  P2PMessage message(messageData);
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList: Received data message from the direct connection link"
            << " (flags=0x" << QString::number(message.getFlags(), 16) << ")." << endl;
#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
      kdWarning() << "P2PApplication: Contact sent invalid nonce or session was already terminated, "
                  << "closing direct connection." << endl;
#ifdef KMESSDEBUG_P2PAPPLICATION_GENERAL
      kdDebug() << "P2PApplication: Closing connection, moving transfer to switchboard." << endl;
#endif
#ifdef KMESSTEST
      ASSERT( sender() != 0 );
      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.
 */
02014 void ApplicationList::slotSwitchboardTestReadyWrite()
{
  // Test if the switchboard requested a pause,
  // meaning it's no longer ready.
  if( pauseApplications_ )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kdDebug() << "ApplicationList::slotSwitchboardTestReadyWrite() - switchboard requested pause, "
              << "postponing action." << endl;
#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
    kdDebug() << "ApplicationList::slotSwitchboardTestReadyWrite() - no active outgoing transfers left, "
              << "disconnecting write handler." << endl;
#endif
    return;
  }

#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList::slotSwitchboardTestReadyWrite() - a switchboard is ready to send more data, "
            << "notifying applications." << endl;
#endif

  // Start notifying.
  bool writeSuccess = true;
  QPtrListIterator<P2PApplication> it( dataSendingApplications_ );
  while( it.current() != 0 )
  {
    // Tell the application to send the next file parts.
    P2PApplication *app = it.current();
    writeSuccess = app->sendNextDataParts( 1 );

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

    ++it;
  }

  // Find out if the applications are done now.
  if( dataSendingApplications_.isEmpty() )
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kdDebug() << "ApplicationList::slotSwitchboardTestReadyWrite() - all applications are done." << endl;
#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() )
    {
      kdDebug() << "ApplicationList::slotSwitchboardTestReadyWrite() - A direct connection became available, "
                  "but had a error while writing. Will continue to use the switchboard." << endl;
    }
  }
#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
      kdDebug() << "ApplicationList::slotSwitchboardTestReadyWrite() - scheduling next call." << endl;
#endif

      // Instead of receiving a signal from the switchboard,
      // fake one right now. This is currently good enough.
      sbReadyWriteTester_.start( 200, true );
    }
    else
    {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kdDebug() << "ApplicationList::slotSwitchboardTestReadyWrite() - switchboard requested to pause the transfer, "
                << "postponing next action." << endl;
#endif
    }
  }
  else
  {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
    kdDebug() << "ApplicationList::slotSwitchboardTestReadyWrite() - switching to direct connection transfer." << endl;
#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
      kdDebug() << "ApplicationList::slotSwitchboardTestReadyWrite() - no write handler connected yet, "
                << "connecting write handler to direct connection." << endl;
#endif
      directConnection_->connectWriteHandler( this, SLOT(slotConnectionReadyWrite()) );
    }
  }
}



/**
 * @brief An application was finished and requested removal.
 * @param  application  The application object instance.
 */
02142 void ApplicationList::slotTerminateApplication(Application *application)
{
  // Update the internal lists.
  if( application->qt_cast("MimeApplication") != 0 )
  {
    // It's a mime application
    if( ! mimeApplications_.removeRef( static_cast<MimeApplication*>(application) ) )
    {
      kdWarning() << "ApplicationList::slotTerminateApplication: Could not remove MimeApplication"
                  << " (type=" << application->className() << " contact=" << contactHandle_ << ")!" << endl;
    }
  }
  else if( application->qt_cast("P2PApplication") != 0 )
  {
    // Remove from data sending apps too!
    // Use the same unregister function so the write handler will also be disconnected.
    P2PApplication *p2pApplication = static_cast<P2PApplication*>( application );
    if( dataSendingApplications_.containsRef( p2pApplication ) )
    {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
      kdDebug() << "ApplicationList::slotTerminateApplication() - application is terminating while sending data, unregistering as data sending application first." << endl;
#endif

      unregisterDataSendingApplication( p2pApplication );
    }

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


#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
  kdDebug() << "ApplicationList: Removing application." << endl;
#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_.removeRef(application) )
    {
      if( ! abortingApplications_.isEmpty() )
      {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
        kdDebug() << "ApplicationList::slotTerminateApplication: Still " << abortingApplications_.count() << " other applications left to abort..." << endl;
#endif
      }
      else
      {
#ifdef KMESSDEBUG_APPLICATIONLIST_GENERAL
        kdDebug() << "ApplicationList::slotTerminateApplication: Signalling that applications of '" << contactHandle_ << "' have been aborted." << endl;
#endif

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


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

  bool found = dataSendingApplications_.removeRef( application );
  if( ! found )
  {
    kdWarning() << "ApplicationList::unregisterDataSendingApplication() - application not found in list." << endl;
  }

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

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



#include "applicationlist.moc"

Generated by  Doxygen 1.6.0   Back to index