예제 #1
0
  /**
   * Indicates that the security is time-outed, is not supported by the other end.
   *
   * @param evt Details about the event that caused this message.
   */
  @Override
  public void securityTimeout(CallPeerSecurityTimeoutEvent evt) {
    timer.cancel();

    // fail peer, call
    if (evt.getSource() instanceof AbstractCallPeer) {
      try {
        CallPeer peer = (CallPeer) evt.getSource();
        OperationSetBasicTelephony<?> telephony =
            peer.getProtocolProvider().getOperationSet(OperationSetBasicTelephony.class);

        telephony.hangupCallPeer(
            peer,
            OperationSetBasicTelephony.HANGUP_REASON_ENCRYPTION_REQUIRED,
            "Encryption Required!");
      } catch (OperationFailedException ex) {
        Logger.getLogger(getClass()).error("Failed to hangup peer", ex);
      }
    }
  }
예제 #2
0
/**
 * The <tt>StatusSubMenu</tt> provides a menu which allow to select the status for each of the
 * protocol providers registered when the menu appears
 *
 * @author Nicolas Chamouard
 */
public class StatusSubMenu extends JMenu {
  /** A reference of <tt>Systray</tt> */
  private SystrayServiceJdicImpl parentSystray;

  /** Contains all accounts and corresponding menus. */
  private Hashtable accountSelectors = new Hashtable();

  private Logger logger = Logger.getLogger(StatusSubMenu.class);

  /**
   * Creates an instance of <tt>StatusSubMenu</tt>.
   *
   * @param tray a reference of the parent <tt>Systray</tt>
   */
  public StatusSubMenu(SystrayServiceJdicImpl tray) {

    parentSystray = tray;

    this.setText(Resources.getString("setStatus"));
    this.setIcon(Resources.getImage("statusMenuIcon"));

    /* makes the menu look better */
    this.setPreferredSize(new java.awt.Dimension(28, 24));

    this.init();
  }

  /**
   * Adds the account corresponding to the given protocol provider to this menu.
   *
   * @param protocolProvider the protocol provider corresponding to the account to add
   */
  private void addAccount(ProtocolProviderService protocolProvider) {
    OperationSetPresence presence =
        (OperationSetPresence) protocolProvider.getOperationSet(OperationSetPresence.class);

    if (presence == null) {
      StatusSimpleSelector simpleSelector =
          new StatusSimpleSelector(parentSystray, protocolProvider);

      this.accountSelectors.put(protocolProvider.getAccountID(), simpleSelector);
      this.add(simpleSelector);
    } else {
      StatusSelector statusSelector = new StatusSelector(parentSystray, protocolProvider, presence);

      this.accountSelectors.put(protocolProvider.getAccountID(), statusSelector);
      this.add(statusSelector);

      presence.addProviderPresenceStatusListener(new SystrayProviderPresenceStatusListener());
    }
  }

  /**
   * Removes the account corresponding to the given protocol provider from this menu.
   *
   * @param protocolProvider the protocol provider corresponding to the account to remove.
   */
  private void removeAccount(ProtocolProviderService protocolProvider) {
    Component c = (Component) this.accountSelectors.get(protocolProvider.getAccountID());

    this.remove(c);
  }

  /**
   * We fill the protocolProviderTable with all running protocol providers at the start of the
   * bundle.
   */
  private void init() {
    SystrayActivator.bundleContext.addServiceListener(new ProtocolProviderServiceListener());

    ServiceReference[] protocolProviderRefs = null;
    try {
      protocolProviderRefs =
          SystrayActivator.bundleContext.getServiceReferences(
              ProtocolProviderService.class.getName(), null);
    } catch (InvalidSyntaxException ex) {
      // this shouldn't happen since we're providing no parameter string
      // but let's log just in case.
      logger.error("Error while retrieving service refs", ex);
      return;
    }

    // in case we found any
    if (protocolProviderRefs != null) {

      for (int i = 0; i < protocolProviderRefs.length; i++) {
        ProtocolProviderService provider =
            (ProtocolProviderService)
                SystrayActivator.bundleContext.getService(protocolProviderRefs[i]);

        boolean isHidden =
            provider.getAccountID().getAccountProperties().get("HIDDEN_PROTOCOL") != null;

        if (!isHidden) this.addAccount(provider);
      }
    }
  }

  /**
   * Listens for <tt>ServiceEvent</tt>s indicating that a <tt>ProtocolProviderService</tt> has been
   * registered and completes the account status menu.
   */
  private class ProtocolProviderServiceListener implements ServiceListener {
    /**
     * When a service is registered or unregistered, we update the provider tables and add/remove
     * listeners (if it supports BasicInstantMessenging implementation)
     *
     * @param event ServiceEvent
     */
    public void serviceChanged(ServiceEvent event) {
      // if the event is caused by a bundle being stopped, we don't want to
      // know
      if (event.getServiceReference().getBundle().getState() == Bundle.STOPPING) {
        return;
      }

      Object service = SystrayActivator.bundleContext.getService(event.getServiceReference());

      if (!(service instanceof ProtocolProviderService)) return;

      ProtocolProviderService provider = (ProtocolProviderService) service;

      if (event.getType() == ServiceEvent.REGISTERED) addAccount(provider);

      if (event.getType() == ServiceEvent.UNREGISTERING) removeAccount(provider);
    }
  }

  /**
   * Listens for all providerStatusChanged and providerStatusMessageChanged events in order to
   * refresh the account status panel, when a status is changed.
   */
  private class SystrayProviderPresenceStatusListener implements ProviderPresenceStatusListener {
    /** Fired when an account has changed its status. We update the icon in the menu. */
    public void providerStatusChanged(ProviderPresenceStatusChangeEvent evt) {
      ProtocolProviderService pps = evt.getProvider();

      StatusSelector selectorBox = (StatusSelector) accountSelectors.get(pps.getAccountID());

      if (selectorBox == null) return;

      selectorBox.updateStatus(evt.getNewStatus());
    }

    public void providerStatusMessageChanged(PropertyChangeEvent evt) {}
  }
}
/**
 * Tests whether accaounts are uninstalled properly. It is important that tests from this class be
 * called last since they will install the accounts that have been used to test the implementations.
 * Apart from uninstallation tests the class also contains tests that remove and reinstall the
 * protocol provider bundle in order to verify that accounts are persistent.
 *
 * @author Emil Ivov
 */
public class TestAccountUninstallation extends TestCase {
  private static final Logger logger = Logger.getLogger(TestAccountUninstallation.class);

  private SipSlickFixture fixture = new SipSlickFixture();

  /**
   * Constructs a test instance
   *
   * @param name The name of the test.
   */
  public TestAccountUninstallation(String name) {
    super(name);
  }

  /**
   * JUnit setup method.
   *
   * @throws Exception in case anything goes wrong.
   */
  protected void setUp() throws Exception {
    super.setUp();
    fixture.setUp();
  }

  /**
   * JUnit teardown method.
   *
   * @throws Exception in case anything goes wrong.
   */
  protected void tearDown() throws Exception {
    fixture.tearDown();
    super.tearDown();
  }

  /**
   * Returns a suite containing tests in this class in the order that we'd like them executed.
   *
   * @return a Test suite containing tests in this class in the order that we'd like them executed.
   */
  public static Test suite() {
    TestSuite suite = new TestSuite();

    suite.addTest(new TestAccountUninstallation("testProviderUnregister"));
    suite.addTest(new TestAccountUninstallation("testInstallationPersistency"));
    suite.addTest(new TestAccountUninstallation("testUninstallAccount"));

    return suite;
  }

  /**
   * Unregisters both providers and verifies whether they have changed state accordingly.
   *
   * @throws OperationFailedException if unregister fails with an error.
   */
  public void testProviderUnregister() throws OperationFailedException {
    // make sure providers are still registered
    assertEquals(fixture.provider1.getRegistrationState(), RegistrationState.REGISTERED);
    assertEquals(fixture.provider2.getRegistrationState(), RegistrationState.REGISTERED);

    UnregistrationEventCollector collector1 = new UnregistrationEventCollector();
    UnregistrationEventCollector collector2 = new UnregistrationEventCollector();

    fixture.provider1.addRegistrationStateChangeListener(collector1);
    fixture.provider2.addRegistrationStateChangeListener(collector2);

    // unregister both providers
    fixture.provider1.unregister();
    fixture.provider2.unregister();

    collector1.waitForEvent(10000);
    collector2.waitForEvent(10000);

    assertTrue(
        "Provider did not distribute unregister events", 2 <= collector1.collectedNewStates.size());
    assertTrue(
        "Provider did not distribute unregister events", 2 <= collector2.collectedNewStates.size());

    // make sure both providers are now unregistered.
    assertEquals(
        "Provider state after calling unregister().",
        RegistrationState.UNREGISTERED,
        fixture.provider1.getRegistrationState());
    assertEquals(
        "Provider state after calling unregister().",
        RegistrationState.UNREGISTERED,
        fixture.provider2.getRegistrationState());
  }

  /**
   * Stops and removes the tested bundle, verifies that it has unregistered its provider, then
   * reloads and restarts the bundle and verifies that the protocol provider is reRegistered in the
   * bundle context.
   *
   * @throws java.lang.Exception if an exception occurs during testing.
   */
  public void testInstallationPersistency() throws Exception {
    Bundle providerBundle = fixture.findProtocolProviderBundle(fixture.provider1);

    // set the global providerBundle reference that we will be using
    // in the last series of tests (Account uninstallation persistency)
    SipSlickFixture.providerBundle = providerBundle;

    assertNotNull("Couldn't find a bundle for the tested provider", providerBundle);

    providerBundle.stop();

    assertTrue(
        "Couldn't stop the protocol provider bundle. State was " + providerBundle.getState(),
        Bundle.ACTIVE != providerBundle.getState() && Bundle.STOPPING != providerBundle.getState());

    providerBundle.uninstall();

    assertEquals(
        "Couldn't stop the protocol provider bundle.",
        Bundle.UNINSTALLED,
        providerBundle.getState());

    // verify that the provider is no longer available
    ServiceReference[] sipProviderRefs = null;
    try {
      sipProviderRefs =
          fixture.bc.getServiceReferences(
              ProtocolProviderService.class.getName(),
              "(&"
                  + "("
                  + ProtocolProviderFactory.PROTOCOL
                  + "="
                  + ProtocolNames.SIP
                  + ")"
                  + "("
                  + ProtocolProviderFactory.USER_ID
                  + "="
                  + fixture.userID1
                  + ")"
                  + ")");
    } catch (InvalidSyntaxException ex) {
      fail("We apparently got our filter wrong: " + ex.getMessage());
    }

    // make sure we didn't see a service
    assertTrue(
        "A Protocol Provider Service was still regged as an osgi service "
            + "for SIP URI:"
            + fixture.userID1
            + "After it was explicitly uninstalled",
        sipProviderRefs == null || sipProviderRefs.length == 0);

    // verify that the provider factory knows that we have uninstalled the
    // provider.
    assertTrue(
        "The SIP provider factory kept a reference to the provider we just "
            + "uninstalled (uri="
            + fixture.userID1
            + ")",
        fixture.providerFactory.getRegisteredAccounts().isEmpty()
            && fixture.providerFactory.getProviderForAccount(fixture.provider1.getAccountID())
                == null);

    // Now reinstall the bundle
    providerBundle = fixture.bc.installBundle(providerBundle.getLocation());

    // set the global providerBundle reference that we will be using
    // in the last series of tests (Account uninstallation persistency)
    SipSlickFixture.providerBundle = providerBundle;

    assertEquals(
        "Couldn't re-install protocol provider bundle.",
        Bundle.INSTALLED,
        providerBundle.getState());

    AccountManagerUtils.startBundleAndWaitStoredAccountsLoaded(
        fixture.bc, providerBundle, ProtocolNames.SIP);
    assertEquals(
        "Couldn't re-start protocol provider bundle.", Bundle.ACTIVE, providerBundle.getState());

    // Make sure that the provider is there again.
    // verify that the provider is no longer available
    try {
      sipProviderRefs =
          fixture.bc.getServiceReferences(
              ProtocolProviderService.class.getName(),
              "(&"
                  + "("
                  + ProtocolProviderFactory.PROTOCOL
                  + "="
                  + ProtocolNames.SIP
                  + ")"
                  + "("
                  + ProtocolProviderFactory.USER_ID
                  + "="
                  + fixture.userID1
                  + ")"
                  + ")");
    } catch (InvalidSyntaxException ex) {
      fail("We apparently got our filter wrong " + ex.getMessage());
    }

    // make sure we didn't see a service
    assertTrue(
        "A Protocol Provider Service was not restored after being"
            + "reinstalled. SIP URI:"
            + fixture.userID1,
        sipProviderRefs != null && sipProviderRefs.length > 0);

    ServiceReference[] sipFactoryRefs = null;
    try {
      sipFactoryRefs =
          fixture.bc.getServiceReferences(
              ProtocolProviderFactory.class.getName(),
              "(" + ProtocolProviderFactory.PROTOCOL + "=" + ProtocolNames.SIP + ")");
    } catch (InvalidSyntaxException ex) {
      fail("We apparently got our filter wrong " + ex.getMessage());
    }

    // we're the ones who've reinstalled the factory so it's our
    // responsibility to update the fixture.
    fixture.providerFactory = (ProtocolProviderFactory) fixture.bc.getService(sipFactoryRefs[0]);
    fixture.provider1 = (ProtocolProviderService) fixture.bc.getService(sipProviderRefs[0]);

    // verify that the provider is also restored in the provider factory
    // itself
    assertTrue(
        "The SIP provider did not restore its own reference to the provider "
            + "that we just reinstalled (URI="
            + fixture.userID1
            + ")",
        !fixture.providerFactory.getRegisteredAccounts().isEmpty()
            && fixture.providerFactory.getProviderForAccount(fixture.provider1.getAccountID())
                != null);
  }

  /** Uinstalls our test account and makes sure it really has been removed. */
  public void testUninstallAccount() {
    assertFalse(
        "No installed accounts found", fixture.providerFactory.getRegisteredAccounts().isEmpty());

    assertNotNull(
        "Found no provider corresponding to URI " + fixture.userID1,
        fixture.providerFactory.getProviderForAccount(fixture.provider1.getAccountID()));

    assertTrue(
        "Failed to remove a provider corresponding to URI " + fixture.userID1,
        fixture.providerFactory.uninstallAccount(fixture.provider1.getAccountID()));
    assertTrue(
        "Failed to remove a provider corresponding to URI " + fixture.userID1,
        fixture.providerFactory.uninstallAccount(fixture.provider2.getAccountID()));

    // make sure no providers have remained installed.
    ServiceReference[] sipProviderRefs = null;
    try {
      sipProviderRefs =
          fixture.bc.getServiceReferences(
              ProtocolProviderService.class.getName(),
              "(" + ProtocolProviderFactory.PROTOCOL + "=" + ProtocolNames.SIP + ")");
    } catch (InvalidSyntaxException ex) {
      fail("We apparently got our filter wrong " + ex.getMessage());
    }

    // make sure we didn't see a service
    assertTrue(
        "A Protocol Provider Service was still regged as an osgi "
            + "service for SIP URI:"
            + fixture.userID1
            + "After it was explicitly uninstalled",
        sipProviderRefs == null || sipProviderRefs.length == 0);

    // verify that the provider factory knows that we have uninstalled the
    // provider.
    assertTrue(
        "The SIP provider factory kept a reference to the provider we just "
            + "uninstalled (uri="
            + fixture.userID1
            + ")",
        fixture.providerFactory.getRegisteredAccounts().isEmpty()
            && fixture.providerFactory.getProviderForAccount(fixture.provider1.getAccountID())
                == null);
  }

  /**
   * A class that would plugin as a registration listener to a protocol provider and simply record
   * all events that it sees and notifyAll() if it sees an event that notifies us of a completed
   * registration.
   */
  public class UnregistrationEventCollector implements RegistrationStateChangeListener {
    public List<RegistrationState> collectedNewStates = new LinkedList<RegistrationState>();

    /**
     * The method would simply register all received events so that they could be available for
     * later inspection by the unit tests. In the case where a registraiton event notifying us of a
     * completed registration is seen, the method would call notifyAll().
     *
     * @param evt ProviderStatusChangeEvent the event describing the status change.
     */
    public void registrationStateChanged(RegistrationStateChangeEvent evt) {
      logger.debug("Received a RegistrationStateChangeEvent: " + evt);

      collectedNewStates.add(evt.getNewState());

      if (evt.getNewState().equals(RegistrationState.UNREGISTERED)) {
        logger.debug("We're registered and will notify those who wait");
        synchronized (this) {
          notifyAll();
        }
      }
    }

    /**
     * Blocks until an event notifying us of the awaited state change is received or until waitFor
     * miliseconds pass (whichever happens first).
     *
     * @param waitFor the number of miliseconds that we should be waiting for an event before simply
     *     bailing out.
     */
    public void waitForEvent(long waitFor) {
      logger.trace("Waiting for a RegistrationStateChangeEvent");

      synchronized (this) {
        if (collectedNewStates.contains(RegistrationState.UNREGISTERED)) {
          logger.trace("Event already received. " + collectedNewStates);
          return;
        }

        try {
          wait(waitFor);

          if (collectedNewStates.size() > 0)
            logger.trace("Received a RegistrationStateChangeEvent.");
          else logger.trace("No RegistrationStateChangeEvent received for " + waitFor + "ms.");

        } catch (InterruptedException ex) {
          logger.debug("Interrupted while waiting for a " + "RegistrationStateChangeEvent", ex);
        }
      }
    }
  }
}
예제 #4
0
/**
 * The contactlist panel not only contains the contact list but it has the role of a message
 * dispatcher. It process all sent and received messages as well as all typing notifications. Here
 * are managed all contact list mouse events.
 *
 * @author Yana Stamcheva
 * @author Hristo Terezov
 */
public class ContactListPane extends SIPCommScrollPane
    implements MessageListener,
        TypingNotificationsListener,
        FileTransferListener,
        ContactListListener,
        PluginComponentListener {
  /** Serial version UID. */
  private static final long serialVersionUID = 0L;

  private final MainFrame mainFrame;

  private TreeContactList contactList;

  private final TypingTimer typingTimer = new TypingTimer();

  private CommonRightButtonMenu commonRightButtonMenu;

  private final Logger logger = Logger.getLogger(ContactListPane.class);

  private final ChatWindowManager chatWindowManager;

  /**
   * Creates the contactlist scroll panel defining the parent frame.
   *
   * @param mainFrame The parent frame.
   */
  public ContactListPane(MainFrame mainFrame) {
    this.mainFrame = mainFrame;

    this.chatWindowManager = GuiActivator.getUIService().getChatWindowManager();

    this.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);

    this.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, Color.GRAY));

    this.initPluginComponents();
  }

  /**
   * Initializes the contact list.
   *
   * @param contactListService The MetaContactListService which will be used for a contact list data
   *     model.
   */
  public void initList(MetaContactListService contactListService) {
    this.contactList = new TreeContactList(mainFrame);
    // We should first set the contact list to the GuiActivator, so that
    // anybody could get it from there.
    GuiActivator.setContactList(contactList);

    // By default we set the current filter to be the presence filter.
    contactList.applyFilter(TreeContactList.presenceFilter);

    TransparentPanel transparentPanel = new TransparentPanel(new BorderLayout());

    transparentPanel.add(contactList, BorderLayout.NORTH);

    this.setViewportView(transparentPanel);

    transparentPanel.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
    this.contactList.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));

    this.contactList.addContactListListener(this);
    this.addMouseListener(
        new MouseAdapter() {
          @Override
          public void mousePressed(MouseEvent e) {
            if ((e.getModifiers() & InputEvent.BUTTON3_MASK) != 0) {
              commonRightButtonMenu = new CommonRightButtonMenu(mainFrame);

              commonRightButtonMenu.setInvoker(ContactListPane.this);

              commonRightButtonMenu.setLocation(
                  e.getX() + mainFrame.getX() + 5, e.getY() + mainFrame.getY() + 105);

              commonRightButtonMenu.setVisible(true);
            }
          }
        });
  }

  /**
   * Returns the contact list.
   *
   * @return the contact list
   */
  public TreeContactList getContactList() {
    return this.contactList;
  }

  /**
   * Implements the ContactListListener.contactSelected method.
   *
   * @param evt the <tt>ContactListEvent</tt> that notified us
   */
  public void contactClicked(ContactListEvent evt) {
    // We're interested only in two click events.
    if (evt.getClickCount() < 2) return;

    UIContact descriptor = evt.getSourceContact();

    // We're currently only interested in MetaContacts.
    if (descriptor.getDescriptor() instanceof MetaContact) {
      MetaContact metaContact = (MetaContact) descriptor.getDescriptor();

      // Searching for the right proto contact to use as default for the
      // chat conversation.
      Contact defaultContact =
          metaContact.getDefaultContact(OperationSetBasicInstantMessaging.class);

      // do nothing
      if (defaultContact == null) {
        defaultContact = metaContact.getDefaultContact(OperationSetSmsMessaging.class);

        if (defaultContact == null) return;
      }

      ProtocolProviderService defaultProvider = defaultContact.getProtocolProvider();

      OperationSetBasicInstantMessaging defaultIM =
          defaultProvider.getOperationSet(OperationSetBasicInstantMessaging.class);

      ProtocolProviderService protoContactProvider;
      OperationSetBasicInstantMessaging protoContactIM;

      boolean isOfflineMessagingSupported =
          defaultIM != null && !defaultIM.isOfflineMessagingSupported();

      if (defaultContact.getPresenceStatus().getStatus() < 1
          && (!isOfflineMessagingSupported || !defaultProvider.isRegistered())) {
        Iterator<Contact> protoContacts = metaContact.getContacts();

        while (protoContacts.hasNext()) {
          Contact contact = protoContacts.next();

          protoContactProvider = contact.getProtocolProvider();

          protoContactIM =
              protoContactProvider.getOperationSet(OperationSetBasicInstantMessaging.class);

          if (protoContactIM != null
              && protoContactIM.isOfflineMessagingSupported()
              && protoContactProvider.isRegistered()) {
            defaultContact = contact;
          }
        }
      }

      ContactEventHandler contactHandler =
          mainFrame.getContactHandler(defaultContact.getProtocolProvider());

      contactHandler.contactClicked(defaultContact, evt.getClickCount());
    } else if (descriptor.getDescriptor() instanceof SourceContact) {
      SourceContact contact = (SourceContact) descriptor.getDescriptor();

      List<ContactDetail> imDetails =
          contact.getContactDetails(OperationSetBasicInstantMessaging.class);
      List<ContactDetail> mucDetails = contact.getContactDetails(OperationSetMultiUserChat.class);

      if (imDetails != null && imDetails.size() > 0) {
        ProtocolProviderService pps =
            imDetails.get(0).getPreferredProtocolProvider(OperationSetBasicInstantMessaging.class);

        GuiActivator.getUIService()
            .getChatWindowManager()
            .startChat(contact.getContactAddress(), pps);
      } else if (mucDetails != null && mucDetails.size() > 0) {
        ChatRoomWrapper room =
            GuiActivator.getMUCService().findChatRoomWrapperFromSourceContact(contact);

        if (room == null) {
          // lets check by id
          ProtocolProviderService pps =
              mucDetails.get(0).getPreferredProtocolProvider(OperationSetMultiUserChat.class);

          room =
              GuiActivator.getMUCService()
                  .findChatRoomWrapperFromChatRoomID(contact.getContactAddress(), pps);

          if (room == null) {
            GuiActivator.getMUCService()
                .createChatRoom(
                    contact.getContactAddress(),
                    pps,
                    new ArrayList<String>(),
                    "",
                    false,
                    false,
                    false);
          }
        }

        if (room != null) GuiActivator.getMUCService().openChatRoom(room);
      } else {
        List<ContactDetail> smsDetails = contact.getContactDetails(OperationSetSmsMessaging.class);

        if (smsDetails != null && smsDetails.size() > 0) {
          GuiActivator.getUIService()
              .getChatWindowManager()
              .startChat(contact.getContactAddress(), true);
        }
      }
    }
  }

  /**
   * Implements the ContactListListener.groupSelected method.
   *
   * @param evt the <tt>ContactListEvent</tt> that notified us
   */
  public void groupClicked(ContactListEvent evt) {}

  /** We're not interested in group selection events here. */
  public void groupSelected(ContactListEvent evt) {}

  /** We're not interested in contact selection events here. */
  public void contactSelected(ContactListEvent evt) {}

  /**
   * When a message is received determines whether to open a new chat window or chat window tab, or
   * to indicate that a message is received from a contact which already has an open chat. When the
   * chat is found checks if in mode "Auto popup enabled" and if this is the case shows the message
   * in the appropriate chat panel.
   *
   * @param evt the event containing details on the received message
   */
  public void messageReceived(MessageReceivedEvent evt) {
    if (logger.isTraceEnabled())
      logger.trace("MESSAGE RECEIVED from contact: " + evt.getSourceContact().getAddress());

    Contact protocolContact = evt.getSourceContact();
    ContactResource contactResource = evt.getContactResource();
    Message message = evt.getSourceMessage();
    int eventType = evt.getEventType();
    MetaContact metaContact =
        GuiActivator.getContactListService().findMetaContactByContact(protocolContact);

    if (metaContact != null) {
      messageReceived(
          protocolContact,
          contactResource,
          metaContact,
          message,
          eventType,
          evt.getTimestamp(),
          evt.getCorrectedMessageUID(),
          evt.isPrivateMessaging(),
          evt.getPrivateMessagingContactRoom());
    } else {
      if (logger.isTraceEnabled())
        logger.trace("MetaContact not found for protocol contact: " + protocolContact + ".");
    }
  }

  /**
   * When a message is received determines whether to open a new chat window or chat window tab, or
   * to indicate that a message is received from a contact which already has an open chat. When the
   * chat is found checks if in mode "Auto popup enabled" and if this is the case shows the message
   * in the appropriate chat panel.
   *
   * @param protocolContact the source contact of the event
   * @param contactResource the resource from which the contact is writing
   * @param metaContact the metacontact containing <tt>protocolContact</tt>
   * @param message the message to deliver
   * @param eventType the event type
   * @param timestamp the timestamp of the event
   * @param correctedMessageUID the identifier of the corrected message
   * @param isPrivateMessaging if <tt>true</tt> the message is received from private messaging
   *     contact.
   * @param privateContactRoom the chat room associated with the private messaging contact.
   */
  private void messageReceived(
      final Contact protocolContact,
      final ContactResource contactResource,
      final MetaContact metaContact,
      final Message message,
      final int eventType,
      final Date timestamp,
      final String correctedMessageUID,
      final boolean isPrivateMessaging,
      final ChatRoom privateContactRoom) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              messageReceived(
                  protocolContact,
                  contactResource,
                  metaContact,
                  message,
                  eventType,
                  timestamp,
                  correctedMessageUID,
                  isPrivateMessaging,
                  privateContactRoom);
            }
          });
      return;
    }

    // Obtain the corresponding chat panel.
    final ChatPanel chatPanel =
        chatWindowManager.getContactChat(
            metaContact, protocolContact, contactResource, message.getMessageUID());

    // Show an envelope on the sender contact in the contact list and
    // in the systray.
    if (!chatPanel.isChatFocused()) contactList.setActiveContact(metaContact, true);

    // Distinguish the message type, depending on the type of event that
    // we have received.
    String messageType = null;

    if (eventType == MessageReceivedEvent.CONVERSATION_MESSAGE_RECEIVED) {
      messageType = Chat.INCOMING_MESSAGE;
    } else if (eventType == MessageReceivedEvent.SYSTEM_MESSAGE_RECEIVED) {
      messageType = Chat.SYSTEM_MESSAGE;
    } else if (eventType == MessageReceivedEvent.SMS_MESSAGE_RECEIVED) {
      messageType = Chat.SMS_MESSAGE;
    }

    String contactAddress =
        (contactResource != null)
            ? protocolContact.getAddress() + " (" + contactResource.getResourceName() + ")"
            : protocolContact.getAddress();

    chatPanel.addMessage(
        contactAddress,
        protocolContact.getDisplayName(),
        timestamp,
        messageType,
        message.getContent(),
        message.getContentType(),
        message.getMessageUID(),
        correctedMessageUID);

    String resourceName = (contactResource != null) ? contactResource.getResourceName() : null;

    if (isPrivateMessaging) {
      chatWindowManager.openPrivateChatForChatRoomMember(privateContactRoom, protocolContact);
    } else {
      chatWindowManager.openChat(chatPanel, false);
    }

    ChatTransport chatTransport =
        chatPanel.getChatSession().findChatTransportForDescriptor(protocolContact, resourceName);

    chatPanel.setSelectedChatTransport(chatTransport, true);
  }

  /**
   * When a sent message is delivered shows it in the chat conversation panel.
   *
   * @param evt the event containing details on the message delivery
   */
  public void messageDelivered(MessageDeliveredEvent evt) {
    Contact contact = evt.getDestinationContact();
    MetaContact metaContact =
        GuiActivator.getContactListService().findMetaContactByContact(contact);

    if (logger.isTraceEnabled())
      logger.trace("MESSAGE DELIVERED to contact: " + contact.getAddress());

    ChatPanel chatPanel = chatWindowManager.getContactChat(metaContact, false);

    if (chatPanel != null) {
      Message msg = evt.getSourceMessage();
      ProtocolProviderService protocolProvider = contact.getProtocolProvider();

      if (logger.isTraceEnabled())
        logger.trace(
            "MESSAGE DELIVERED: process message to chat for contact: "
                + contact.getAddress()
                + " MESSAGE: "
                + msg.getContent());

      chatPanel.addMessage(
          this.mainFrame.getAccountAddress(protocolProvider),
          this.mainFrame.getAccountDisplayName(protocolProvider),
          evt.getTimestamp(),
          Chat.OUTGOING_MESSAGE,
          msg.getContent(),
          msg.getContentType(),
          msg.getMessageUID(),
          evt.getCorrectedMessageUID());

      if (evt.isSmsMessage() && !ConfigurationUtils.isSmsNotifyTextDisabled()) {
        chatPanel.addMessage(
            contact.getDisplayName(),
            new Date(),
            Chat.ACTION_MESSAGE,
            GuiActivator.getResources().getI18NString("service.gui.SMS_SUCCESSFULLY_SENT"),
            "text");
      }
    }
  }

  /**
   * Shows a warning message to the user when message delivery has failed.
   *
   * @param evt the event containing details on the message delivery failure
   */
  public void messageDeliveryFailed(MessageDeliveryFailedEvent evt) {
    logger.error(evt.getReason());

    String errorMsg = null;

    Message sourceMessage = (Message) evt.getSource();

    Contact sourceContact = evt.getDestinationContact();

    MetaContact metaContact =
        GuiActivator.getContactListService().findMetaContactByContact(sourceContact);

    if (evt.getErrorCode() == MessageDeliveryFailedEvent.OFFLINE_MESSAGES_NOT_SUPPORTED) {
      errorMsg =
          GuiActivator.getResources()
              .getI18NString(
                  "service.gui.MSG_DELIVERY_NOT_SUPPORTED",
                  new String[] {sourceContact.getDisplayName()});
    } else if (evt.getErrorCode() == MessageDeliveryFailedEvent.NETWORK_FAILURE) {
      errorMsg = GuiActivator.getResources().getI18NString("service.gui.MSG_NOT_DELIVERED");
    } else if (evt.getErrorCode() == MessageDeliveryFailedEvent.PROVIDER_NOT_REGISTERED) {
      errorMsg =
          GuiActivator.getResources().getI18NString("service.gui.MSG_SEND_CONNECTION_PROBLEM");
    } else if (evt.getErrorCode() == MessageDeliveryFailedEvent.INTERNAL_ERROR) {
      errorMsg =
          GuiActivator.getResources().getI18NString("service.gui.MSG_DELIVERY_INTERNAL_ERROR");
    } else {
      errorMsg = GuiActivator.getResources().getI18NString("service.gui.MSG_DELIVERY_ERROR");
    }

    String reason = evt.getReason();
    if (reason != null)
      errorMsg +=
          " "
              + GuiActivator.getResources()
                  .getI18NString("service.gui.ERROR_WAS", new String[] {reason});

    ChatPanel chatPanel = chatWindowManager.getContactChat(metaContact, sourceContact);

    chatPanel.addMessage(
        sourceContact.getAddress(),
        metaContact.getDisplayName(),
        new Date(),
        Chat.OUTGOING_MESSAGE,
        sourceMessage.getContent(),
        sourceMessage.getContentType(),
        sourceMessage.getMessageUID(),
        evt.getCorrectedMessageUID());

    chatPanel.addErrorMessage(metaContact.getDisplayName(), errorMsg);

    chatWindowManager.openChat(chatPanel, false);
  }

  /**
   * Informs the user what is the typing state of his chat contacts.
   *
   * @param evt the event containing details on the typing notification
   */
  public void typingNotificationReceived(TypingNotificationEvent evt) {
    if (typingTimer.isRunning()) typingTimer.stop();

    String notificationMsg = "";

    MetaContact metaContact =
        GuiActivator.getContactListService().findMetaContactByContact(evt.getSourceContact());
    String contactName = metaContact.getDisplayName() + " ";

    if (contactName.equals("")) {
      contactName = GuiActivator.getResources().getI18NString("service.gui.UNKNOWN") + " ";
    }

    int typingState = evt.getTypingState();

    ChatPanel chatPanel = chatWindowManager.getContactChat(metaContact, false);

    if (typingState == OperationSetTypingNotifications.STATE_TYPING) {
      notificationMsg =
          GuiActivator.getResources()
              .getI18NString("service.gui.CONTACT_TYPING", new String[] {contactName});

      // Proactive typing notification
      if (!chatWindowManager.isChatOpenedFor(metaContact)) {
        return;
      }

      if (chatPanel != null) chatPanel.addTypingNotification(notificationMsg);

      typingTimer.setMetaContact(metaContact);
      typingTimer.start();
    } else if (typingState == OperationSetTypingNotifications.STATE_PAUSED) {
      notificationMsg =
          GuiActivator.getResources()
              .getI18NString("service.gui.CONTACT_PAUSED_TYPING", new String[] {contactName});

      if (chatPanel != null) chatPanel.addTypingNotification(notificationMsg);

      typingTimer.setMetaContact(metaContact);
      typingTimer.start();
    } else {
      if (chatPanel != null) chatPanel.removeTypingNotification();
    }
  }

  /**
   * Called to indicate that sending typing notification has failed.
   *
   * @param evt a <tt>TypingNotificationEvent</tt> containing the sender of the notification and its
   *     type.
   */
  public void typingNotificationDeliveryFailed(TypingNotificationEvent evt) {
    if (typingTimer.isRunning()) typingTimer.stop();

    String notificationMsg = "";

    MetaContact metaContact =
        GuiActivator.getContactListService().findMetaContactByContact(evt.getSourceContact());
    String contactName = metaContact.getDisplayName();

    if (contactName.equals("")) {
      contactName = GuiActivator.getResources().getI18NString("service.gui.UNKNOWN") + " ";
    }

    ChatPanel chatPanel = chatWindowManager.getContactChat(metaContact, false);

    notificationMsg =
        GuiActivator.getResources()
            .getI18NString("service.gui.CONTACT_TYPING_SEND_FAILED", new String[] {contactName});

    // Proactive typing notification
    if (!chatWindowManager.isChatOpenedFor(metaContact)) {
      return;
    }

    if (chatPanel != null) chatPanel.addErrorSendingTypingNotification(notificationMsg);

    typingTimer.setMetaContact(metaContact);
    typingTimer.start();
  }

  /**
   * When a request has been received we show it to the user through the chat session renderer.
   *
   * @param event <tt>FileTransferRequestEvent</tt>
   * @see FileTransferListener#fileTransferRequestReceived(FileTransferRequestEvent)
   */
  public void fileTransferRequestReceived(FileTransferRequestEvent event) {
    IncomingFileTransferRequest request = event.getRequest();

    Contact sourceContact = request.getSender();

    MetaContact metaContact =
        GuiActivator.getContactListService().findMetaContactByContact(sourceContact);

    final ChatPanel chatPanel = chatWindowManager.getContactChat(metaContact, sourceContact);

    chatPanel.addIncomingFileTransferRequest(
        event.getFileTransferOperationSet(), request, event.getTimestamp());

    ChatTransport chatTransport =
        chatPanel.getChatSession().findChatTransportForDescriptor(sourceContact, null);

    chatPanel.setSelectedChatTransport(chatTransport, true);

    // Opens the chat panel with the new message in the UI thread.
    chatWindowManager.openChat(chatPanel, false);
  }

  /**
   * Nothing to do here, because we already know when a file transfer is created.
   *
   * @param event the <tt>FileTransferCreatedEvent</tt> that notified us
   */
  public void fileTransferCreated(FileTransferCreatedEvent event) {}

  /**
   * Called when a new <tt>IncomingFileTransferRequest</tt> has been rejected. Nothing to do here,
   * because we are the one who rejects the request.
   *
   * @param event the <tt>FileTransferRequestEvent</tt> containing the received request which was
   *     rejected.
   */
  public void fileTransferRequestRejected(FileTransferRequestEvent event) {}

  /**
   * Called when an <tt>IncomingFileTransferRequest</tt> has been canceled from the contact who sent
   * it.
   *
   * @param event the <tt>FileTransferRequestEvent</tt> containing the request which was canceled.
   */
  public void fileTransferRequestCanceled(FileTransferRequestEvent event) {}

  /**
   * Returns the right button menu of the contact list.
   *
   * @return the right button menu of the contact list
   */
  public CommonRightButtonMenu getCommonRightButtonMenu() {
    return commonRightButtonMenu;
  }

  /**
   * The TypingTimer is started after a PAUSED typing notification is received. It waits 5 seconds
   * and if no other typing event occurs removes the PAUSED message from the chat status panel.
   */
  private class TypingTimer extends Timer {
    /** Serial version UID. */
    private static final long serialVersionUID = 0L;

    private MetaContact metaContact;

    public TypingTimer() {
      // Set delay
      super(5 * 1000, null);

      this.addActionListener(new TimerActionListener());
    }

    private class TimerActionListener implements ActionListener {
      public void actionPerformed(ActionEvent e) {
        ChatPanel chatPanel = chatWindowManager.getContactChat(metaContact, false);

        if (chatPanel != null) chatPanel.removeTypingNotification();
      }
    }

    private void setMetaContact(MetaContact metaContact) {
      this.metaContact = metaContact;
    }
  }

  private void initPluginComponents() {
    // Search for plugin components registered through the OSGI bundle
    // context.
    ServiceReference[] serRefs = null;

    String osgiFilter =
        "(" + Container.CONTAINER_ID + "=" + Container.CONTAINER_CONTACT_LIST.getID() + ")";

    try {
      serRefs =
          GuiActivator.bundleContext.getServiceReferences(
              PluginComponentFactory.class.getName(), osgiFilter);
    } catch (InvalidSyntaxException exc) {
      logger.error("Could not obtain plugin reference.", exc);
    }

    if (serRefs != null) {
      for (ServiceReference serRef : serRefs) {
        PluginComponentFactory factory =
            (PluginComponentFactory) GuiActivator.bundleContext.getService(serRef);
        PluginComponent component = factory.getPluginComponentInstance(this);

        Object selectedValue = getContactList().getSelectedValue();

        if (selectedValue instanceof MetaContact) {
          component.setCurrentContact((MetaContact) selectedValue);
        } else if (selectedValue instanceof MetaContactGroup) {
          component.setCurrentContactGroup((MetaContactGroup) selectedValue);
        }

        String pluginConstraints = factory.getConstraints();
        Object constraints;

        if (pluginConstraints != null)
          constraints = UIServiceImpl.getBorderLayoutConstraintsFromContainer(pluginConstraints);
        else constraints = BorderLayout.SOUTH;

        this.add((Component) component.getComponent(), constraints);

        this.repaint();
      }
    }

    GuiActivator.getUIService().addPluginComponentListener(this);
  }

  /**
   * Adds the plugin component given by <tt>event</tt> to this panel if it's its container.
   *
   * @param event the <tt>PluginComponentEvent</tt> that notified us
   */
  public void pluginComponentAdded(PluginComponentEvent event) {
    PluginComponentFactory factory = event.getPluginComponentFactory();

    // If the container id doesn't correspond to the id of the plugin
    // container we're not interested.
    if (!factory.getContainer().equals(Container.CONTAINER_CONTACT_LIST)) return;

    Object constraints =
        UIServiceImpl.getBorderLayoutConstraintsFromContainer(factory.getConstraints());

    if (constraints == null) constraints = BorderLayout.SOUTH;

    PluginComponent pluginComponent = factory.getPluginComponentInstance(this);
    this.add((Component) pluginComponent.getComponent(), constraints);

    Object selectedValue = getContactList().getSelectedValue();

    if (selectedValue instanceof MetaContact) {
      pluginComponent.setCurrentContact((MetaContact) selectedValue);
    } else if (selectedValue instanceof MetaContactGroup) {
      pluginComponent.setCurrentContactGroup((MetaContactGroup) selectedValue);
    }

    this.revalidate();
    this.repaint();
  }

  /**
   * Removes the plugin component given by <tt>event</tt> if previously added in this panel.
   *
   * @param event the <tt>PluginComponentEvent</tt> that notified us
   */
  public void pluginComponentRemoved(PluginComponentEvent event) {
    PluginComponentFactory factory = event.getPluginComponentFactory();

    // If the container id doesn't correspond to the id of the plugin
    // container we're not interested.
    if (!factory.getContainer().equals(Container.CONTAINER_CONTACT_LIST)) return;

    this.remove((Component) factory.getPluginComponentInstance(this).getComponent());
  }
}
예제 #5
0
/**
 * The <tt>OneToOneCallPeerPanel</tt> is the panel containing data for a call peer in a given call.
 * It contains information like call peer name, photo, call duration, etc.
 *
 * @author Yana Stamcheva
 * @author Lyubomir Marinov
 * @author Sebastien Vincent
 * @author Adam Netocny
 */
public class OneToOneCallPeerPanel extends TransparentPanel
    implements SwingCallPeerRenderer, PropertyChangeListener, Skinnable {
  /**
   * The <tt>Logger</tt> used by the <tt>OneToOneCallPeerPanel</tt> class and its instances for
   * logging output.
   */
  private static final Logger logger = Logger.getLogger(OneToOneCallPeerPanel.class);

  /** Serial version UID. */
  private static final long serialVersionUID = 0L;

  /** The <tt>CallPeer</tt>, which is rendered in this panel. */
  private final CallPeer callPeer;

  /** The <tt>Call</tt>, which is rendered in this panel. */
  private final Call call;

  /**
   * The <tt>CallPeerAdapter</tt> which implements common <tt>CallPeer</tt>-related listeners on
   * behalf of this instance.
   */
  private final CallPeerAdapter callPeerAdapter;

  /** The renderer of the call. */
  private final SwingCallRenderer callRenderer;

  /** The component showing the status of the underlying call peer. */
  private final JLabel callStatusLabel = new JLabel();

  /** The center component. */
  private final VideoContainer center;

  /**
   * The AWT <tt>Component</tt> which implements a button which allows closing/hiding the visual
   * <tt>Component</tt> which depicts the video streaming from the local peer/user to the remote
   * peer(s).
   */
  private Component closeLocalVisualComponentButton;

  /**
   * A listener to desktop sharing granted/revoked events and to mouse and keyboard interaction with
   * the remote video displaying the remote desktop.
   */
  private final DesktopSharingMouseAndKeyboardListener desktopSharingMouseAndKeyboardListener;

  /**
   * The indicator which determines whether {@link #dispose()} has already been invoked on this
   * instance. If <tt>true</tt>, this instance is considered non-functional and is to be left to the
   * garbage collector.
   */
  private boolean disposed = false;

  /** The DTMF label. */
  private final JLabel dtmfLabel = new JLabel();

  /** The component responsible for displaying an error message. */
  private JTextComponent errorMessageComponent;

  /** The label showing whether the call is on or off hold. */
  private final JLabel holdStatusLabel = new JLabel();

  /** Sound local level label. */
  private InputVolumeControlButton localLevel;

  /**
   * The <tt>Component</tt> which {@link #updateViewFromModelInEventDispatchThread()} last added to
   * {@link #center} as the visual <tt>Component</tt> displaying the video streaming from the local
   * peer/user to the remote peer(s).
   *
   * <p><b>Warning</b>: It is not to be used for any other purposes because it may not represent the
   * current state of the model of this view.
   */
  private Component localVideo;

  /** The label showing whether the voice has been set to mute. */
  private final JLabel muteStatusLabel = new JLabel();

  /** The <tt>Icon</tt> which represents the avatar of the associated call peer. */
  private ImageIcon peerImage;

  /** The name of the peer. */
  private String peerName;

  /** The label containing the user photo. */
  private final JLabel photoLabel;

  /** Sound remote level label. */
  private Component remoteLevel;

  /**
   * The <tt>Component</tt> which {@link #updateViewFromModelInEventDispatchThread()} last added to
   * {@link #center} as the visual <tt>Component</tt> displaying the video streaming from the remote
   * peer(s) to the local peer/user.
   *
   * <p><b>Warning</b>: It is not to be used for any other purposes because it may not represent the
   * current state of the model of this view.
   */
  private Component remoteVideo;

  /** Current id for security image. */
  private ImageID securityImageID = ImageLoader.SECURE_BUTTON_OFF;

  /** The panel containing security related components. */
  private SecurityPanel<?> securityPanel;

  /** The security status of the peer */
  private final SecurityStatusLabel securityStatusLabel = new SecurityStatusLabel();

  /** The status bar component. */
  private final Component statusBar;

  /** The facility which aids this instance in the dealing with the video-related information. */
  private final UIVideoHandler2 uiVideoHandler;

  /**
   * The <tt>Observer</tt> which listens to changes in the video-related information detected and
   * reported by {@link #uiVideoHandler}.
   */
  private final Observer uiVideoHandlerObserver =
      new Observer() {
        public void update(Observable o, Object arg) {
          updateViewFromModel();
        }
      };

  /**
   * The <tt>Runnable</tt> which is scheduled by {@link #updateViewFromModel()} for execution in the
   * AWT event dispatching thread in order to invoke {@link
   * #updateViewFromModelInEventDispatchThread()}.
   */
  private final Runnable updateViewFromModelInEventDispatchThread =
      new Runnable() {
        public void run() {
          /*
           * We receive events/notifications from various threads and we
           * respond to them in the AWT event dispatching thread. It is
           * possible to first schedule an event to be brought to the AWT
           * event dispatching thread, then to have #dispose() invoked on
           * this instance and, finally, to receive the scheduled event in
           * the AWT event dispatching thread. In such a case, this
           * disposed instance should not respond to the event.
           */
          if (!disposed) updateViewFromModelInEventDispatchThread();
        }
      };

  /**
   * Creates a <tt>CallPeerPanel</tt> for the given call peer.
   *
   * @param callRenderer the renderer of the call
   * @param callPeer the <tt>CallPeer</tt> represented in this panel
   * @param uiVideoHandler the facility which is to aid the new instance in the dealing with the
   *     video-related information
   */
  public OneToOneCallPeerPanel(
      SwingCallRenderer callRenderer, CallPeer callPeer, UIVideoHandler2 uiVideoHandler) {
    this.callRenderer = callRenderer;
    this.callPeer = callPeer;
    // we need to obtain call as soon as possible
    // cause if it fails too quickly we may fail to show it
    this.call = callPeer.getCall();
    this.uiVideoHandler = uiVideoHandler;

    peerName = CallManager.getPeerDisplayName(callPeer);
    securityPanel = SecurityPanel.create(this, callPeer, null);

    photoLabel = new JLabel(getPhotoLabelIcon());
    center = createCenter();
    statusBar = createStatusBar();

    setPeerImage(CallManager.getPeerImage(callPeer));

    /* Lay out the main Components of the UI. */
    setLayout(new GridBagLayout());

    GridBagConstraints cnstrnts = new GridBagConstraints();

    if (center != null) {
      cnstrnts.fill = GridBagConstraints.BOTH;
      cnstrnts.gridx = 0;
      cnstrnts.gridy = 1;
      cnstrnts.weightx = 1;
      cnstrnts.weighty = 1;
      add(center, cnstrnts);
    }
    if (statusBar != null) {
      cnstrnts.fill = GridBagConstraints.NONE;
      cnstrnts.gridx = 0;
      cnstrnts.gridy = 3;
      cnstrnts.weightx = 0;
      cnstrnts.weighty = 0;
      cnstrnts.insets = new Insets(5, 0, 0, 0);
      add(statusBar, cnstrnts);
    }

    createSoundLevelIndicators();
    initSecuritySettings();

    /*
     * Add the listeners which will be notified about changes in the model
     * and which will update this view.
     */
    callPeerAdapter = new CallPeerAdapter(callPeer, this);
    uiVideoHandler.addObserver(uiVideoHandlerObserver);

    /*
     * This view adapts to whether it is displayed in full-screen or
     * windowed mode.
     */
    if (callRenderer instanceof Component) {
      ((Component) callRenderer).addPropertyChangeListener(CallContainer.PROP_FULL_SCREEN, this);
    }

    OperationSetDesktopSharingClient desktopSharingClient =
        callPeer.getProtocolProvider().getOperationSet(OperationSetDesktopSharingClient.class);
    if (desktopSharingClient != null) {
      desktopSharingMouseAndKeyboardListener =
          new DesktopSharingMouseAndKeyboardListener(callPeer, desktopSharingClient);
    } else desktopSharingMouseAndKeyboardListener = null;

    updateViewFromModel();
  }

  /**
   * Creates the <tt>Component</tt> hierarchy of the central area of this <tt>CallPeerPanel</tt>
   * which displays the photo of the <tt>CallPeer</tt> or the video if any.
   */
  private VideoContainer createCenter() {
    photoLabel.setPreferredSize(new Dimension(90, 90));

    return createVideoContainer(photoLabel);
  }

  /** Creates sound level related components. */
  private void createSoundLevelIndicators() {
    TransparentPanel localLevelPanel = new TransparentPanel(new BorderLayout(5, 0));
    TransparentPanel remoteLevelPanel = new TransparentPanel(new BorderLayout(5, 0));

    localLevel =
        new InputVolumeControlButton(
            call, ImageLoader.MICROPHONE, ImageLoader.MUTE_BUTTON, false, false);
    remoteLevel =
        new OutputVolumeControlButton(call.getConference(), ImageLoader.HEADPHONE, false, false)
            .getComponent();

    final SoundLevelIndicator localLevelIndicator =
        new SoundLevelIndicator(SoundLevelChangeEvent.MIN_LEVEL, SoundLevelChangeEvent.MAX_LEVEL);
    final SoundLevelIndicator remoteLevelIndicator =
        new SoundLevelIndicator(SoundLevelChangeEvent.MIN_LEVEL, SoundLevelChangeEvent.MAX_LEVEL);

    localLevelPanel.add(localLevel, BorderLayout.WEST);
    localLevelPanel.add(localLevelIndicator, BorderLayout.CENTER);
    remoteLevelPanel.add(remoteLevel, BorderLayout.WEST);
    remoteLevelPanel.add(remoteLevelIndicator, BorderLayout.CENTER);

    GridBagConstraints constraints = new GridBagConstraints();
    constraints.fill = GridBagConstraints.NONE;
    constraints.gridx = 0;
    constraints.gridy = 5;
    constraints.weightx = 0;
    constraints.weighty = 0;
    constraints.insets = new Insets(10, 0, 0, 0);

    add(localLevelPanel, constraints);

    constraints.fill = GridBagConstraints.NONE;
    constraints.gridx = 0;
    constraints.gridy = 6;
    constraints.weightx = 0;
    constraints.weighty = 0;
    constraints.insets = new Insets(5, 0, 10, 0);

    add(remoteLevelPanel, constraints);

    if (!GuiActivator.getConfigurationService()
        .getBoolean(
            "net.java.sip.communicator.impl.gui.main.call." + "DISABLE_SOUND_LEVEL_INDICATORS",
            false)) {
      callPeer.addStreamSoundLevelListener(
          new SoundLevelListener() {
            public void soundLevelChanged(Object source, int level) {
              remoteLevelIndicator.updateSoundLevel(level);
            }
          });
      /*
       * By the time the UI gets to be initialized, the callPeer may have
       * been removed from its Call. As far as the UI is concerned, the
       * callPeer will never have a Call again and there will be no audio
       * levels to display anyway so there is no point in throwing a
       * NullPointerException here.
       */
      if (call != null) {
        call.addLocalUserSoundLevelListener(
            new SoundLevelListener() {
              public void soundLevelChanged(Object source, int level) {
                localLevelIndicator.updateSoundLevel(level);
              }
            });
      }
    }
  }

  /**
   * Creates the <tt>Component</tt> hierarchy of the area of status-related information such as
   * <tt>CallPeer</tt> display name, call duration, security status.
   *
   * @return the root of the <tt>Component</tt> hierarchy of the area of status-related information
   *     such as <tt>CallPeer</tt> display name, call duration, security status
   */
  private Component createStatusBar() {
    // stateLabel
    callStatusLabel.setForeground(Color.WHITE);
    dtmfLabel.setForeground(Color.WHITE);
    callStatusLabel.setText(callPeer.getState().getLocalizedStateString());

    PeerStatusPanel statusPanel = new PeerStatusPanel(new GridBagLayout());

    GridBagConstraints constraints = new GridBagConstraints();

    constraints.gridx = 0;
    constraints.gridy = 0;
    statusPanel.add(securityStatusLabel, constraints);
    initSecurityStatusLabel();

    constraints.gridx++;
    statusPanel.add(holdStatusLabel, constraints);

    constraints.gridx++;
    statusPanel.add(muteStatusLabel, constraints);

    constraints.gridx++;
    callStatusLabel.setBorder(BorderFactory.createEmptyBorder(2, 3, 2, 12));
    statusPanel.add(callStatusLabel, constraints);

    constraints.gridx++;
    constraints.weightx = 1f;
    statusPanel.add(dtmfLabel, constraints);

    return statusPanel;
  }

  /**
   * Creates a new AWT <tt>Container</tt> which can display a single <tt>Component</tt> at a time
   * (supposedly, one which represents video) and, in the absence of such a <tt>Component</tt>,
   * displays a predefined default <tt>Component</tt> (in accord with the previous supposition, one
   * which is the default when there is no video). The returned <tt>Container</tt> will track the
   * <tt>Components</tt>s added to and removed from it in order to make sure that
   * <tt>noVideoContainer</tt> is displayed as described.
   *
   * @param noVideoComponent the predefined default <tt>Component</tt> to be displayed in the
   *     returned <tt>Container</tt> when there is no other <tt>Component</tt> in it
   * @return a new <tt>Container</tt> which can display a single <tt>Component</tt> at a time and,
   *     in the absence of such a <tt>Component</tt>, displays <tt>noVideoComponent</tt>
   */
  private VideoContainer createVideoContainer(Component noVideoComponent) {
    Container oldParent = noVideoComponent.getParent();

    if (oldParent != null) oldParent.remove(noVideoComponent);

    return new VideoContainer(noVideoComponent, false);
  }

  /**
   * Releases the resources acquired by this instance which require explicit disposal (e.g. any
   * listeners added to the depicted <tt>CallPeer</tt>. Invoked by <tt>OneToOneCallPanel</tt> when
   * it determines that this <tt>OneToOneCallPeerPanel</tt> is no longer necessary.
   */
  public void dispose() {
    disposed = true;

    callPeerAdapter.dispose();
    uiVideoHandler.deleteObserver(uiVideoHandlerObserver);

    if (callRenderer instanceof Component) {
      ((Component) callRenderer).removePropertyChangeListener(CallContainer.PROP_FULL_SCREEN, this);
    }
  }

  /**
   * Returns the parent <tt>CallPanel</tt> containing this renderer.
   *
   * @return the parent <tt>CallPanel</tt> containing this renderer
   */
  public CallPanel getCallPanel() {
    return callRenderer.getCallContainer();
  }

  /**
   * Returns the parent call renderer.
   *
   * @return the parent call renderer
   */
  public CallRenderer getCallRenderer() {
    return callRenderer;
  }

  /**
   * Returns the component associated with this renderer.
   *
   * @return the component associated with this renderer
   */
  public Component getComponent() {
    return this;
  }

  /**
   * Returns the name of the peer, contained in this panel.
   *
   * @return the name of the peer, contained in this panel
   */
  public String getPeerName() {
    return peerName;
  }

  /**
   * Gets the <tt>Icon</tt> to be displayed in {@link #photoLabel}.
   *
   * @return the <tt>Icon</tt> to be displayed in {@link #photoLabel}
   */
  private ImageIcon getPhotoLabelIcon() {
    return (peerImage == null)
        ? new ImageIcon(ImageLoader.getImage(ImageLoader.DEFAULT_USER_PHOTO))
        : peerImage;
  }

  /** Initializes the security settings for this call peer. */
  private void initSecuritySettings() {
    CallPeerSecurityStatusEvent securityEvent = callPeer.getCurrentSecuritySettings();

    if (securityEvent instanceof CallPeerSecurityOnEvent)
      securityOn((CallPeerSecurityOnEvent) securityEvent);
  }

  /** Initializes the security status label, shown in the call status bar. */
  private void initSecurityStatusLabel() {
    securityStatusLabel.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 5));

    securityStatusLabel.addMouseListener(
        new MouseAdapter() {
          /** Invoked when a mouse button has been pressed on a component. */
          @Override
          public void mousePressed(MouseEvent e) {
            // Only show the security details if the security is on.
            SrtpControl ctrl = securityPanel.getSecurityControl();
            if (ctrl instanceof ZrtpControl && ctrl.getSecureCommunicationStatus()) {
              setSecurityPanelVisible(
                  !callRenderer
                      .getCallContainer()
                      .getCallWindow()
                      .getFrame()
                      .getGlassPane()
                      .isVisible());
            }
          }
        });
  }

  /**
   * Determines whether the visual <tt>Component</tt> depicting the video streaming from the local
   * peer/user to the remote peer(s) is currently visible.
   *
   * @return <tt>true</tt> if the visual <tt>Component</tt> depicting the video streaming from the
   *     local peer/user to the remote peer(s) is currently visible; otherwise, <tt>false</tt>
   */
  public boolean isLocalVideoVisible() {
    return uiVideoHandler.isLocalVideoVisible();
  }

  /** Reloads all icons. */
  public void loadSkin() {
    if (localLevel != null)
      localLevel.setIcon(new ImageIcon(ImageLoader.getImage(ImageLoader.MICROPHONE)));

    if (remoteLevel != null && remoteLevel instanceof Skinnable)
      ((Skinnable) remoteLevel).loadSkin();

    if (muteStatusLabel.getIcon() != null)
      muteStatusLabel.setIcon(new ImageIcon(ImageLoader.getImage(ImageLoader.MUTE_STATUS_ICON)));

    if (holdStatusLabel.getIcon() != null)
      holdStatusLabel.setIcon(new ImageIcon(ImageLoader.getImage(ImageLoader.HOLD_STATUS_ICON)));

    securityStatusLabel.setIcon(new ImageIcon(ImageLoader.getImage(securityImageID)));

    if (peerImage == null) {
      photoLabel.setIcon(new ImageIcon(ImageLoader.getImage(ImageLoader.DEFAULT_USER_PHOTO)));
    }
  }

  /**
   * Prints the given DTMG character through this <tt>CallPeerRenderer</tt>.
   *
   * @param dtmfChar the DTMF char to print
   */
  public void printDTMFTone(char dtmfChar) {
    dtmfLabel.setText(dtmfLabel.getText() + dtmfChar);
    if (dtmfLabel.getBorder() == null)
      dtmfLabel.setBorder(BorderFactory.createEmptyBorder(2, 1, 2, 5));
  }

  /**
   * Notifies this instance about a change in the value of a property of a source which of interest
   * to this instance. For example, <tt>OneToOneCallPeerPanel</tt> updates its user
   * interface-related properties upon changes in the value of the {@link
   * CallContainer#PROP_FULL_SCREEN} property of its associated {@link #callRenderer}.
   *
   * @param ev a <tt>PropertyChangeEvent</tt> which identifies the source, the name of the property
   *     and the old and new values
   */
  public void propertyChange(PropertyChangeEvent ev) {
    if (CallContainer.PROP_FULL_SCREEN.equals(ev.getPropertyName())) updateViewFromModel();
  }

  /**
   * Re-dispatches glass pane mouse events only in case they occur on the security panel.
   *
   * @param glassPane the glass pane
   * @param e the mouse event in question
   */
  private void redispatchMouseEvent(Component glassPane, MouseEvent e) {
    Point glassPanePoint = e.getPoint();

    Point securityPanelPoint =
        SwingUtilities.convertPoint(glassPane, glassPanePoint, securityPanel);

    Component component;
    Point componentPoint;

    if (securityPanelPoint.y > 0) {
      component = securityPanel;
      componentPoint = securityPanelPoint;
    } else {
      Container contentPane =
          callRenderer.getCallContainer().getCallWindow().getFrame().getContentPane();

      Point containerPoint = SwingUtilities.convertPoint(glassPane, glassPanePoint, contentPane);

      component =
          SwingUtilities.getDeepestComponentAt(contentPane, containerPoint.x, containerPoint.y);

      componentPoint = SwingUtilities.convertPoint(contentPane, glassPanePoint, component);
    }

    if (component != null)
      component.dispatchEvent(
          new MouseEvent(
              component,
              e.getID(),
              e.getWhen(),
              e.getModifiers(),
              componentPoint.x,
              componentPoint.y,
              e.getClickCount(),
              e.isPopupTrigger()));

    e.consume();
  }

  /**
   * The handler for the security event received. The security event for starting establish a secure
   * connection.
   *
   * @param evt the security started event received
   */
  public void securityNegotiationStarted(CallPeerSecurityNegotiationStartedEvent evt) {
    if (Boolean.parseBoolean(
        GuiActivator.getResources().getSettingsString("impl.gui.PARANOIA_UI"))) {
      SrtpControl srtpControl = null;
      if (callPeer instanceof MediaAwareCallPeer) srtpControl = evt.getSecurityController();

      securityPanel = new ParanoiaTimerSecurityPanel<SrtpControl>(srtpControl);

      setSecurityPanelVisible(true);
    }
  }

  /**
   * Indicates that the security has gone off.
   *
   * @param evt the <tt>CallPeerSecurityOffEvent</tt> that notified us
   */
  public void securityOff(final CallPeerSecurityOffEvent evt) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              securityOff(evt);
            }
          });
      return;
    }

    if (evt.getSessionType() == CallPeerSecurityOffEvent.AUDIO_SESSION) {
      securityStatusLabel.setText("");
      securityStatusLabel.setSecurityOff();
      if (securityStatusLabel.getBorder() == null)
        securityStatusLabel.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 3));
    }

    securityPanel.securityOff(evt);
  }

  /**
   * Indicates that the security is turned on.
   *
   * <p>Sets the secured status icon to the status panel and initializes/updates the corresponding
   * security details.
   *
   * @param evt Details about the event that caused this message.
   */
  public void securityOn(final CallPeerSecurityOnEvent evt) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              securityOn(evt);
            }
          });
      return;
    }

    // If the securityOn is called without a specific event, we'll just set
    // the security label status to on.
    if (evt == null) {
      securityStatusLabel.setSecurityOn();
      return;
    }

    SrtpControl srtpControl = evt.getSecurityController();

    if (!srtpControl.requiresSecureSignalingTransport()
        || callPeer.getProtocolProvider().isSignalingTransportSecure()) {
      if (srtpControl instanceof ZrtpControl) {
        securityStatusLabel.setText("zrtp");

        if (!((ZrtpControl) srtpControl).isSecurityVerified())
          securityStatusLabel.setSecurityPending();
        else securityStatusLabel.setSecurityOn();
      } else securityStatusLabel.setSecurityOn();
    }

    // if we have some other panel, using other control
    if (!srtpControl.getClass().isInstance(securityPanel.getSecurityControl())
        || (securityPanel instanceof ParanoiaTimerSecurityPanel)) {
      setSecurityPanelVisible(false);

      securityPanel = SecurityPanel.create(this, callPeer, srtpControl);

      if (srtpControl instanceof ZrtpControl)
        ((ZrtpSecurityPanel) securityPanel).setSecurityStatusLabel(securityStatusLabel);
    }

    securityPanel.securityOn(evt);

    boolean isSecurityLowPriority =
        Boolean.parseBoolean(
            GuiActivator.getResources()
                .getSettingsString("impl.gui.I_DONT_CARE_THAT_MUCH_ABOUT_SECURITY"));

    // Display ZRTP panel in case SAS was not verified or a AOR mismtach
    // was detected during creation of ZrtpSecurityPanel.
    // Don't show panel if user does not care about security at all.
    if (srtpControl instanceof ZrtpControl
        && !isSecurityLowPriority
        && (!((ZrtpControl) srtpControl).isSecurityVerified()
            || ((ZrtpSecurityPanel) securityPanel).isZidAorMismatch())) {
      setSecurityPanelVisible(true);
    }

    this.revalidate();
  }

  /** Indicates that the security status is pending confirmation. */
  public void securityPending() {
    securityStatusLabel.setSecurityPending();
  }

  /**
   * Indicates that the security is timeouted, is not supported by the other end.
   *
   * @param evt Details about the event that caused this message.
   */
  public void securityTimeout(final CallPeerSecurityTimeoutEvent evt) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              securityTimeout(evt);
            }
          });
      return;
    }

    if (securityPanel != null) securityPanel.securityTimeout(evt);
  }

  /**
   * Sets the reason of a call failure if one occurs. The renderer should display this reason to the
   * user.
   *
   * @param reason the reason to display
   */
  public void setErrorReason(final String reason) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              setErrorReason(reason);
            }
          });
      return;
    }

    if (errorMessageComponent == null) {
      errorMessageComponent = new JTextPane();

      JTextPane textPane = (JTextPane) errorMessageComponent;
      textPane.setEditable(false);
      textPane.setOpaque(false);

      StyledDocument doc = textPane.getStyledDocument();

      MutableAttributeSet standard = new SimpleAttributeSet();
      StyleConstants.setAlignment(standard, StyleConstants.ALIGN_CENTER);
      StyleConstants.setFontFamily(standard, textPane.getFont().getFamily());
      StyleConstants.setFontSize(standard, 12);
      doc.setParagraphAttributes(0, 0, standard, true);

      GridBagConstraints constraints = new GridBagConstraints();
      constraints.fill = GridBagConstraints.HORIZONTAL;
      constraints.gridx = 0;
      constraints.gridy = 4;
      constraints.weightx = 1;
      constraints.weighty = 0;
      constraints.insets = new Insets(5, 0, 0, 0);

      add(errorMessageComponent, constraints);
      this.revalidate();
    }

    errorMessageComponent.setText(reason);

    if (isVisible()) errorMessageComponent.repaint();
  }

  /**
   * Shows/hides the visual <tt>Component</tt> depicting the video streaming from the local
   * peer/user to the remote peer(s).
   *
   * @param visible <tt>true</tt> to show the visual <tt>Component</tt> depicting the video
   *     streaming from the local peer/user to the remote peer(s); <tt>false</tt>, otherwise
   */
  public void setLocalVideoVisible(boolean visible) {
    uiVideoHandler.setLocalVideoVisible(visible);
  }

  /**
   * Sets the mute status icon to the status panel.
   *
   * @param isMute indicates if the call with this peer is muted
   */
  public void setMute(final boolean isMute) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              setMute(isMute);
            }
          });
      return;
    }

    if (isMute) {
      muteStatusLabel.setIcon(new ImageIcon(ImageLoader.getImage(ImageLoader.MUTE_STATUS_ICON)));
      muteStatusLabel.setBorder(BorderFactory.createEmptyBorder(2, 3, 2, 3));
    } else {
      muteStatusLabel.setIcon(null);
      muteStatusLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
    }

    // Update input volume control button state to reflect the current
    // mute status.
    if (localLevel.isSelected() != isMute) localLevel.setSelected(isMute);

    this.revalidate();
    this.repaint();
  }

  /**
   * Sets the "on hold" property value.
   *
   * @param isOnHold indicates if the call with this peer is put on hold
   */
  public void setOnHold(final boolean isOnHold) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              setOnHold(isOnHold);
            }
          });
      return;
    }

    if (isOnHold) {
      holdStatusLabel.setIcon(new ImageIcon(ImageLoader.getImage(ImageLoader.HOLD_STATUS_ICON)));
      holdStatusLabel.setBorder(BorderFactory.createEmptyBorder(2, 3, 2, 3));
    } else {
      holdStatusLabel.setIcon(null);
      holdStatusLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
    }

    this.revalidate();
    this.repaint();
  }

  /**
   * Set the image of the peer
   *
   * @param image new image
   */
  public void setPeerImage(byte[] image) {
    // If the image is still null we try to obtain it from one of the
    // available contact sources.
    if (image == null || image.length <= 0) {
      GuiActivator.getContactList().setSourceContactImage(peerName, photoLabel, 100, 100);
    } else {
      peerImage = ImageUtils.getScaledRoundedIcon(image, 100, 100);
      if (peerImage == null) peerImage = getPhotoLabelIcon();

      if (!SwingUtilities.isEventDispatchThread()) {
        SwingUtilities.invokeLater(
            new Runnable() {
              public void run() {
                photoLabel.setIcon(peerImage);
                photoLabel.repaint();
              }
            });
      } else {
        photoLabel.setIcon(peerImage);
        photoLabel.repaint();
      }
    }
  }

  /**
   * Sets the name of the peer.
   *
   * @param name the name of the peer
   */
  public void setPeerName(final String name) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              setPeerName(name);
            }
          });
      return;
    }

    peerName = name;

    ((OneToOneCallPanel) callRenderer).setPeerName(name);
  }

  /**
   * Sets the state of the contained call peer by specifying the state name.
   *
   * @param oldState the previous state of the peer
   * @param newState the new state of the peer
   * @param stateString the state of the contained call peer
   */
  public void setPeerState(
      final CallPeerState oldState, final CallPeerState newState, final String stateString) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              setPeerState(oldState, newState, stateString);
            }
          });
      return;
    }

    this.callStatusLabel.setText(stateString);

    if (newState == CallPeerState.CONNECTED
        && !CallPeerState.isOnHold(oldState)
        && !securityStatusLabel.isSecurityStatusSet()) {
      securityStatusLabel.setSecurityOff();
    }
  }

  /**
   * Shows/hides the security panel.
   *
   * @param isVisible <tt>true</tt> to show the security panel, <tt>false</tt> to hide it
   */
  public void setSecurityPanelVisible(final boolean isVisible) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              setSecurityPanelVisible(isVisible);
            }
          });
      return;
    }

    final JFrame callFrame = callRenderer.getCallContainer().getCallWindow().getFrame();

    final JPanel glassPane = (JPanel) callFrame.getGlassPane();

    if (!isVisible) {
      // Need to hide the security panel explicitly in order to keep the
      // fade effect.
      securityPanel.setVisible(false);
      glassPane.setVisible(false);
      glassPane.removeAll();
    } else {
      glassPane.setLayout(null);
      glassPane.addMouseListener(
          new MouseListener() {
            public void mouseClicked(MouseEvent e) {
              redispatchMouseEvent(glassPane, e);
            }

            public void mouseEntered(MouseEvent e) {
              redispatchMouseEvent(glassPane, e);
            }

            public void mouseExited(MouseEvent e) {
              redispatchMouseEvent(glassPane, e);
            }

            public void mousePressed(MouseEvent e) {
              redispatchMouseEvent(glassPane, e);
            }

            public void mouseReleased(MouseEvent e) {
              redispatchMouseEvent(glassPane, e);
            }
          });

      Point securityLabelPoint = securityStatusLabel.getLocation();

      Point newPoint =
          SwingUtilities.convertPoint(
              securityStatusLabel.getParent(),
              securityLabelPoint.x,
              securityLabelPoint.y,
              callFrame);

      securityPanel.setBeginPoint(new Point((int) newPoint.getX() + 15, 0));
      securityPanel.setBounds(0, (int) newPoint.getY() - 5, this.getWidth(), 130);

      glassPane.add(securityPanel);
      // Need to show the security panel explicitly in order to keep the
      // fade effect.
      securityPanel.setVisible(true);
      glassPane.setVisible(true);

      glassPane.addComponentListener(
          new ComponentAdapter() {
            /** Invoked when the component's size changes. */
            @Override
            public void componentResized(ComponentEvent e) {
              if (glassPane.isVisible()) {
                glassPane.setVisible(false);
                callFrame.removeComponentListener(this);
              }
            }
          });
    }
  }

  /**
   * Updates this view i.e. <tt>OneToOneCallPeerPanel</tt> so that it depicts the current state of
   * its model i.e. <tt>callPeer</tt>.
   */
  private void updateViewFromModel() {
    /*
     * We receive events/notifications from various threads and we respond
     * to them in the AWT event dispatching thread. It is possible to first
     * schedule an event to be brought to the AWT event dispatching thread,
     * then to have #dispose() invoked on this instance and, finally, to
     * receive the scheduled event in the AWT event dispatching thread. In
     * such a case, this disposed instance should not respond to the event
     * because it may, for example, steal a visual Components depicting
     * video (which cannot belong to more than one parent at a time) from
     * another non-disposed OneToOneCallPeerPanel.
     */
    if (!disposed) {
      if (SwingUtilities.isEventDispatchThread()) updateViewFromModelInEventDispatchThread();
      else {
        SwingUtilities.invokeLater(updateViewFromModelInEventDispatchThread);
      }
    }
  }

  /**
   * Updates this view i.e. <tt>OneToOneCallPeerPanel</tt> so that it depicts the current state of
   * its model i.e. <tt>callPeer</tt>. The update is performed in the AWT event dispatching thread.
   */
  private void updateViewFromModelInEventDispatchThread() {
    /*
     * We receive events/notifications from various threads and we respond
     * to them in the AWT event dispatching thread. It is possible to first
     * schedule an event to be brought to the AWT event dispatching thread,
     * then to have #dispose() invoked on this instance and, finally, to
     * receive the scheduled event in the AWT event dispatching thread. In
     * such a case, this disposed instance should not respond to the event
     * because it may, for example, steal a visual Components depicting
     * video (which cannot belong to more than one parent at a time) from
     * another non-disposed OneToOneCallPeerPanel.
     */
    if (disposed) return;

    /*
     * Update the display of visual <tt>Component</tt>s depicting video
     * streaming between the local peer/user and the remote peer(s).
     */

    OperationSetVideoTelephony videoTelephony =
        callPeer.getProtocolProvider().getOperationSet(OperationSetVideoTelephony.class);
    Component remoteVideo = null;
    Component localVideo = null;

    if (videoTelephony != null) {
      List<Component> remoteVideos = videoTelephony.getVisualComponents(callPeer);

      if ((remoteVideos != null) && !remoteVideos.isEmpty()) {
        /*
         * TODO OneToOneCallPeerPanel displays a one-to-one conversation
         * between the local peer/user and a specific remote peer. If
         * the remote peer is the focus of a telephony conference of its
         * own, it may be sending multiple videos to the local peer.
         * Switching to a user interface which displays multiple videos
         * is the responsibility of whoever decided that this
         * OneToOneCallPeerPanel is to be used to depict the current
         * state of the CallConference associated with the CallPeer
         * depicted by this instance. If that switching decides that
         * this instance is to continue being the user interface, then
         * we should probably pick up the remote video which is
         * generated by the remote peer and not one of its
         * ConferenceMembers.
         */
        remoteVideo = remoteVideos.get(0);
      }

      if (uiVideoHandler.isLocalVideoVisible()) {
        try {
          localVideo = videoTelephony.getLocalVisualComponent(callPeer);
        } catch (OperationFailedException ofe) {
          /*
           * Well, we cannot do much about the exception. We'll just
           * not display the local video.
           */
          logger.warn("Failed to retrieve local video to be displayed.", ofe);
        }
      }

      /*
       * Determine whether there is actually a change in the local and
       * remote videos which requires an update.
       */
      boolean localVideoChanged =
          ((localVideo != this.localVideo)
              || ((localVideo != null) && !UIVideoHandler2.isAncestor(center, localVideo)));
      boolean remoteVideoChanged =
          ((remoteVideo != this.remoteVideo)
              || ((remoteVideo != null) && !UIVideoHandler2.isAncestor(center, remoteVideo)));

      // If the remote video has changed, maybe the CallPanel can display
      // the LO/SD/HD button.
      if (remoteVideoChanged) {
        // Updates video component which may listen the mouse and key
        // events.
        if (desktopSharingMouseAndKeyboardListener != null) {
          desktopSharingMouseAndKeyboardListener.setVideoComponent(remoteVideo);
        }

        CallPanel callPanel = callRenderer.getCallContainer();
        // The remote video has been added, then tries to display the
        // LO/SD/HD button.
        if (remoteVideo != null) {
          callPanel.addRemoteVideoSpecificComponents(callPeer);
        }
        // The remote video has been removed, then hide the LO/SD/HD
        // button if it is currently displayed.
        else {
          callPanel.removeRemoteVideoSpecificComponents();
        }
      }

      if (localVideoChanged || remoteVideoChanged) {
        /*
         * VideoContainer and JAWTRenderer cannot handle random
         * additions of Components. Removing the localVideo when the
         * user has requests its hiding though, should work without
         * removing all Components from the VideoCotainer and adding
         * them again.
         */
        if (localVideoChanged && !remoteVideoChanged && (localVideo == null)) {
          if (this.localVideo != null) {
            center.remove(this.localVideo);
            this.localVideo = null;

            if (closeLocalVisualComponentButton != null)
              center.remove(closeLocalVisualComponentButton);
          }
        } else {
          center.removeAll();
          this.localVideo = null;
          this.remoteVideo = null;

          /*
           * AWT does not make a guarantee about the Z order even
           * within an operating system i.e. the order of adding the
           * Components to their Container does not mean that they
           * will be determinedly painted in that or reverse order.
           * Anyway, there appears to be an expectation among the
           * developers less acquainted with AWT that AWT paints the
           * Components of a Container in an order that is the reverse
           * of the order of their adding. In order to satisfy that
           * expectation and thus give at least some idea to the
           * developers reading the code bellow, do add the Components
           * according to that expectation.
           */

          if (localVideo != null) {
            if (closeLocalVisualComponentButton == null) {
              closeLocalVisualComponentButton = new CloseLocalVisualComponentButton(uiVideoHandler);
            }
            center.add(closeLocalVisualComponentButton, VideoLayout.CLOSE_LOCAL_BUTTON, -1);

            center.add(localVideo, VideoLayout.LOCAL, -1);
            this.localVideo = localVideo;
          }

          if (remoteVideo != null) {
            center.add(remoteVideo, VideoLayout.CENTER_REMOTE, -1);
            this.remoteVideo = remoteVideo;
          }
        }
      }
    }
  }

  /** The <tt>TransparentPanel</tt> that will display the peer status. */
  private static class PeerStatusPanel extends TransparentPanel {
    /**
     * Silence the serial warning. Though there isn't a plan to serialize the instances of the
     * class, there're no fields so the default serialization routine will work.
     */
    private static final long serialVersionUID = 0L;

    /**
     * Constructs a new <tt>PeerStatusPanel</tt>.
     *
     * @param layout the <tt>LayoutManager</tt> to use
     */
    public PeerStatusPanel(LayoutManager layout) {
      super(layout);
    }

    /** @{inheritDoc} */
    @Override
    public void paintComponent(Graphics g) {
      super.paintComponent(g);

      g = g.create();

      try {
        AntialiasingManager.activateAntialiasing(g);

        g.setColor(Color.DARK_GRAY);
        g.fillRoundRect(0, 0, this.getWidth(), this.getHeight(), 10, 10);
      } finally {
        g.dispose();
      }
    }
  }
}
/**
 * Performs testing on protocol provider methods.
 *
 * @todo add more detailed docs once the tests are written.
 * @author Emil Ivov
 * @author Valentin Martinet
 */
public class TestProtocolProviderServiceJabberImpl extends TestCase {
  private static final Logger logger =
      Logger.getLogger(TestProtocolProviderServiceJabberImpl.class);

  private JabberSlickFixture fixture = new JabberSlickFixture();

  /** An event adapter that would collec registation state change events */
  public RegistrationEventCollector regEvtCollector1 = new RegistrationEventCollector();

  /** An event adapter that would collec registation state change events */
  public RegistrationEventCollector regEvtCollector2 = new RegistrationEventCollector();

  /** An event adapter that would collec registation state change events */
  public RegistrationEventCollector regEvtCollector3 = new RegistrationEventCollector();

  /**
   * Creates a test encapsulator for the method with the specified name.
   *
   * @param name the name of the method this test should run.
   */
  public TestProtocolProviderServiceJabberImpl(String name) {
    super(name);
  }

  /**
   * Initializes the fixture.
   *
   * @throws Exception if super.setUp() throws one.
   */
  @Override
  protected void setUp() throws Exception {
    super.setUp();
    fixture.setUp();
  }

  /**
   * Tears the fixture down.
   *
   * @throws Exception if fixture.tearDown() fails.
   */
  @Override
  protected void tearDown() throws Exception {
    fixture.tearDown();
    super.tearDown();
  }

  /**
   * Makes sure that the instance of the Jabber protocol provider that we're going to use for
   * testing is properly initialized and registered with a Jabber registrar. This MUST be called
   * before any other online testing of the Jabber provider so that we won't have to reregister for
   * every single test.
   *
   * <p>The method also verifies that a registration event is fired upon succesful registration and
   * collected by our event collector.
   *
   * @throws OperationFailedException if provider.register() fails.
   */
  public void testRegister() throws OperationFailedException {
    // add an event collector that will collect all events during the
    // registration and allow us to later inspect them and make sure
    // they were properly dispatched.
    fixture.provider1.addRegistrationStateChangeListener(regEvtCollector1);
    fixture.provider2.addRegistrationStateChangeListener(regEvtCollector2);
    fixture.provider3.addRegistrationStateChangeListener(regEvtCollector3);

    // register our three providers
    fixture.provider1.register(
        new SecurityAuthorityImpl(
            System.getProperty(
                    JabberProtocolProviderServiceLick.ACCOUNT_1_PREFIX
                        + ProtocolProviderFactory.PASSWORD)
                .toCharArray()));
    fixture.provider2.register(
        new SecurityAuthorityImpl(
            System.getProperty(
                    JabberProtocolProviderServiceLick.ACCOUNT_2_PREFIX
                        + ProtocolProviderFactory.PASSWORD)
                .toCharArray()));
    fixture.provider3.register(
        new SecurityAuthorityImpl(
            System.getProperty(
                    JabberProtocolProviderServiceLick.ACCOUNT_3_PREFIX
                        + ProtocolProviderFactory.PASSWORD)
                .toCharArray()));

    // give it enough time to register. We won't really have to wait all this
    // time since the registration event collector would notify us the moment
    // we get signed on.
    logger.debug("Waiting for registration to complete ...");

    regEvtCollector1.waitForEvent(15000);
    regEvtCollector2.waitForEvent(40000);
    regEvtCollector3.waitForEvent(60000);

    // make sure that the registration process trigerred the corresponding
    // events.
    assertTrue(
        "No events were dispatched during the registration process.",
        regEvtCollector1.collectedNewStates.size() > 0);

    assertTrue(
        "No registration event notifying of registration was dispatched. "
            + "All events were: "
            + regEvtCollector1.collectedNewStates,
        regEvtCollector1.collectedNewStates.contains(RegistrationState.REGISTERED));

    // now the same for provider 2
    assertTrue(
        "No events were dispatched during the registration process " + "of provider2.",
        regEvtCollector2.collectedNewStates.size() > 0);

    assertTrue(
        "No registration event notifying of registration was dispatched. "
            + "All events were: "
            + regEvtCollector2.collectedNewStates,
        regEvtCollector2.collectedNewStates.contains(RegistrationState.REGISTERED));

    // now the same for provider 3
    assertTrue(
        "No events were dispatched during the registration process " + "of provider3.",
        regEvtCollector3.collectedNewStates.size() > 0);

    assertTrue(
        "No registration event notifying of registration was dispatched. "
            + "All events were: "
            + regEvtCollector3.collectedNewStates,
        regEvtCollector3.collectedNewStates.contains(RegistrationState.REGISTERED));

    fixture.provider1.removeRegistrationStateChangeListener(regEvtCollector1);
    fixture.provider2.removeRegistrationStateChangeListener(regEvtCollector2);
    fixture.provider3.removeRegistrationStateChangeListener(regEvtCollector3);
  }

  /**
   * Verifies that all operation sets have the type they are declarded to have.
   *
   * @throws java.lang.Exception if a class indicated in one of the keys could not be forName()ed.
   */
  public void testOperationSetTypes() throws Exception {
    Map<String, OperationSet> supportedOperationSets =
        fixture.provider1.getSupportedOperationSets();

    // make sure that keys (which are supposed to be class names) correspond
    // what the class of the values recorded against them.
    for (Map.Entry<String, OperationSet> entry : supportedOperationSets.entrySet()) {
      String setName = entry.getKey();
      Object opSet = entry.getValue();

      assertTrue(
          opSet + " was not an instance of " + setName + " as declared",
          Class.forName(setName).isInstance(opSet));
    }
  }

  /**
   * A class that would plugin as a registration listener to a protocol provider and simply record
   * all events that it sees and notifyAll() if it sees an event that notifies us of a completed
   * registration.
   */
  public class RegistrationEventCollector implements RegistrationStateChangeListener {
    public List<RegistrationState> collectedNewStates = new LinkedList<RegistrationState>();

    /**
     * The method would simply register all received events so that they could be available for
     * later inspection by the unit tests. In the case where a registration event notifying us of a
     * completed registration is seen, the method would call notifyAll().
     *
     * @param evt ProviderStatusChangeEvent the event describing the status change.
     */
    public void registrationStateChanged(RegistrationStateChangeEvent evt) {
      logger.debug("Received a RegistrationStateChangeEvent: " + evt);

      collectedNewStates.add(evt.getNewState());

      if (evt.getNewState().equals(RegistrationState.REGISTERED)) {
        logger.debug("We're registered and will notify those who wait");
        synchronized (this) {
          notifyAll();
        }
      }
    }

    /**
     * Blocks until an event notifying us of the awaited state change is received or until waitFor
     * miliseconds pass (whichever happens first).
     *
     * @param waitFor the number of miliseconds that we should be waiting for an event before simply
     *     bailing out.
     */
    public void waitForEvent(long waitFor) {
      logger.trace("Waiting for a RegistrationStateChangeEvent ");

      synchronized (this) {
        if (collectedNewStates.contains(RegistrationState.REGISTERED)) {
          logger.trace("Event already received. " + collectedNewStates);
          return;
        }

        try {
          wait(waitFor);

          if (collectedNewStates.size() > 0)
            logger.trace("Received a RegistrationStateChangeEvent.");
          else logger.trace("No RegistrationStateChangeEvent received for " + waitFor + "ms.");

        } catch (InterruptedException ex) {
          logger.debug("Interrupted while waiting for a " + "RegistrationStateChangeEvent", ex);
        }
      }
    }
  }
}
예제 #7
0
/**
 * The <tt>CallManager</tt> is the one that handles calls. It contains also the "Call" and "Hangup"
 * buttons panel. Here are handles incoming and outgoing calls from and to the call operation set.
 *
 * @author Yana Stamcheva
 */
public class CallManager extends JPanel
    implements ActionListener, CallListener, ListSelectionListener, ChangeListener {
  private Logger logger = Logger.getLogger(CallManager.class.getName());

  private CallComboBox phoneNumberCombo;

  private JPanel comboPanel = new JPanel(new BorderLayout());

  private JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 0));

  private JLabel callViaLabel = new JLabel(Messages.getI18NString("callVia").getText() + " ");

  private JPanel callViaPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 4));

  private AccountSelectorBox accountSelectorBox;

  private SIPCommButton callButton =
      new SIPCommButton(
          ImageLoader.getImage(ImageLoader.CALL_BUTTON_BG),
          ImageLoader.getImage(ImageLoader.CALL_ROLLOVER_BUTTON_BG),
          null,
          ImageLoader.getImage(ImageLoader.CALL_BUTTON_PRESSED_BG));

  private SIPCommButton hangupButton =
      new SIPCommButton(
          ImageLoader.getImage(ImageLoader.HANGUP_BUTTON_BG),
          ImageLoader.getImage(ImageLoader.HANGUP_ROLLOVER_BUTTON_BG),
          null,
          ImageLoader.getImage(ImageLoader.HANGUP_BUTTON_PRESSED_BG));

  private SIPCommButton minimizeButton =
      new SIPCommButton(
          ImageLoader.getImage(ImageLoader.CALL_PANEL_MINIMIZE_BUTTON),
          ImageLoader.getImage(ImageLoader.CALL_PANEL_MINIMIZE_ROLLOVER_BUTTON));

  private SIPCommButton restoreButton =
      new SIPCommButton(
          ImageLoader.getImage(ImageLoader.CALL_PANEL_RESTORE_BUTTON),
          ImageLoader.getImage(ImageLoader.CALL_PANEL_RESTORE_ROLLOVER_BUTTON));

  private JPanel minimizeButtonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));

  private MainFrame mainFrame;

  private Hashtable activeCalls = new Hashtable();

  private boolean isCallMetaContact;

  private Hashtable removeCallTimers = new Hashtable();

  private ProtocolProviderService selectedCallProvider;

  /**
   * Creates an instance of <tt>CallManager</tt>.
   *
   * @param mainFrame The main application window.
   */
  public CallManager(MainFrame mainFrame) {
    super(new BorderLayout());

    this.mainFrame = mainFrame;

    this.phoneNumberCombo = new CallComboBox(this);

    this.accountSelectorBox = new AccountSelectorBox(this);

    this.buttonsPanel.setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 0));

    this.comboPanel.setBorder(BorderFactory.createEmptyBorder(10, 5, 0, 5));

    this.init();
  }

  /** Initializes and constructs this panel. */
  private void init() {
    this.phoneNumberCombo.setEditable(true);

    this.callViaPanel.add(callViaLabel);
    this.callViaPanel.add(accountSelectorBox);

    this.comboPanel.add(phoneNumberCombo, BorderLayout.CENTER);

    this.callButton.setName("call");
    this.hangupButton.setName("hangup");
    this.minimizeButton.setName("minimize");
    this.restoreButton.setName("restore");

    this.minimizeButton.setToolTipText(
        Messages.getI18NString("hideCallPanel").getText() + " Ctrl - H");
    this.restoreButton.setToolTipText(
        Messages.getI18NString("showCallPanel").getText() + " Ctrl - H");

    this.callButton.addActionListener(this);
    this.hangupButton.addActionListener(this);
    this.minimizeButton.addActionListener(this);
    this.restoreButton.addActionListener(this);

    this.buttonsPanel.add(callButton);
    this.buttonsPanel.add(hangupButton);

    this.callButton.setEnabled(false);

    this.hangupButton.setEnabled(false);

    this.add(minimizeButtonPanel, BorderLayout.SOUTH);

    this.setCallPanelVisible(ConfigurationManager.isCallPanelShown());
  }

  /**
   * Handles the <tt>ActionEvent</tt> generated when user presses one of the buttons in this panel.
   */
  public void actionPerformed(ActionEvent evt) {
    JButton button = (JButton) evt.getSource();
    String buttonName = button.getName();

    if (buttonName.equals("call")) {
      Component selectedPanel = mainFrame.getSelectedTab();

      // call button is pressed over an already open call panel
      if (selectedPanel != null
          && selectedPanel instanceof CallPanel
          && ((CallPanel) selectedPanel).getCall().getCallState()
              == CallState.CALL_INITIALIZATION) {

        NotificationManager.stopSound(NotificationManager.BUSY_CALL);
        NotificationManager.stopSound(NotificationManager.INCOMING_CALL);

        CallPanel callPanel = (CallPanel) selectedPanel;

        Iterator participantPanels = callPanel.getParticipantsPanels();

        while (participantPanels.hasNext()) {
          CallParticipantPanel panel = (CallParticipantPanel) participantPanels.next();

          panel.setState("Connecting");
        }

        Call call = callPanel.getCall();

        answerCall(call);
      }
      // call button is pressed over the call list
      else if (selectedPanel != null
          && selectedPanel instanceof CallListPanel
          && ((CallListPanel) selectedPanel).getCallList().getSelectedIndex() != -1) {

        CallListPanel callListPanel = (CallListPanel) selectedPanel;

        GuiCallParticipantRecord callRecord =
            (GuiCallParticipantRecord) callListPanel.getCallList().getSelectedValue();

        String stringContact = callRecord.getParticipantName();

        createCall(stringContact);
      }
      // call button is pressed over the contact list
      else if (selectedPanel != null && selectedPanel instanceof ContactListPanel) {
        // call button is pressed when a meta contact is selected
        if (isCallMetaContact) {
          Object[] selectedContacts =
              mainFrame.getContactListPanel().getContactList().getSelectedValues();

          Vector telephonyContacts = new Vector();

          for (int i = 0; i < selectedContacts.length; i++) {

            Object o = selectedContacts[i];

            if (o instanceof MetaContact) {

              Contact contact =
                  ((MetaContact) o).getDefaultContact(OperationSetBasicTelephony.class);

              if (contact != null) telephonyContacts.add(contact);
              else {
                new ErrorDialog(
                        this.mainFrame,
                        Messages.getI18NString("warning").getText(),
                        Messages.getI18NString(
                                "contactNotSupportingTelephony",
                                new String[] {((MetaContact) o).getDisplayName()})
                            .getText())
                    .showDialog();
              }
            }
          }

          if (telephonyContacts.size() > 0) createCall(telephonyContacts);

        } else if (!phoneNumberCombo.isComboFieldEmpty()) {

          // if no contact is selected checks if the user has chosen
          // or has
          // writen something in the phone combo box

          String stringContact = phoneNumberCombo.getEditor().getItem().toString();

          createCall(stringContact);
        }
      } else if (selectedPanel != null && selectedPanel instanceof DialPanel) {
        String stringContact = phoneNumberCombo.getEditor().getItem().toString();
        createCall(stringContact);
      }
    } else if (buttonName.equalsIgnoreCase("hangup")) {
      Component selectedPanel = this.mainFrame.getSelectedTab();

      if (selectedPanel != null && selectedPanel instanceof CallPanel) {

        NotificationManager.stopSound(NotificationManager.BUSY_CALL);
        NotificationManager.stopSound(NotificationManager.INCOMING_CALL);
        NotificationManager.stopSound(NotificationManager.OUTGOING_CALL);

        CallPanel callPanel = (CallPanel) selectedPanel;

        Call call = callPanel.getCall();

        if (removeCallTimers.containsKey(callPanel)) {
          ((Timer) removeCallTimers.get(callPanel)).stop();
          removeCallTimers.remove(callPanel);
        }

        removeCallPanel(callPanel);

        if (call != null) {
          ProtocolProviderService pps = call.getProtocolProvider();

          OperationSetBasicTelephony telephony = mainFrame.getTelephonyOpSet(pps);

          Iterator participants = call.getCallParticipants();

          while (participants.hasNext()) {
            try {
              // now we hang up the first call participant in the
              // call
              telephony.hangupCallParticipant((CallParticipant) participants.next());
            } catch (OperationFailedException e) {
              logger.error("Hang up was not successful: " + e);
            }
          }
        }
      }
    } else if (buttonName.equalsIgnoreCase("minimize")) {
      JCheckBoxMenuItem hideCallPanelItem =
          mainFrame.getMainMenu().getViewMenu().getHideCallPanelItem();

      if (!hideCallPanelItem.isSelected()) hideCallPanelItem.setSelected(true);

      this.setCallPanelVisible(false);
    } else if (buttonName.equalsIgnoreCase("restore")) {

      JCheckBoxMenuItem hideCallPanelItem =
          mainFrame.getMainMenu().getViewMenu().getHideCallPanelItem();

      if (hideCallPanelItem.isSelected()) hideCallPanelItem.setSelected(false);

      this.setCallPanelVisible(true);
    }
  }

  /** Hides the panel containing call and hangup buttons. */
  public void setCallPanelVisible(boolean isVisible) {
    if (isVisible) {
      this.add(comboPanel, BorderLayout.NORTH);
      this.add(buttonsPanel, BorderLayout.CENTER);

      this.minimizeButtonPanel.removeAll();
      this.minimizeButtonPanel.add(minimizeButton);
    } else {
      this.remove(comboPanel);
      this.remove(buttonsPanel);

      this.minimizeButtonPanel.removeAll();
      this.minimizeButtonPanel.add(restoreButton);

      if (mainFrame.isVisible())
        this.mainFrame.getContactListPanel().getContactList().requestFocus();
    }

    if (ConfigurationManager.isCallPanelShown() != isVisible)
      ConfigurationManager.setShowCallPanel(isVisible);

    this.mainFrame.validate();
  }

  /**
   * Adds the given call account to the list of call via accounts.
   *
   * @param pps the protocol provider service corresponding to the account
   */
  public void addCallAccount(ProtocolProviderService pps) {
    if (accountSelectorBox.getAccountsNumber() > 0) {
      this.comboPanel.add(callViaPanel, BorderLayout.SOUTH);
    }
    accountSelectorBox.addAccount(pps);
  }

  /**
   * Removes the account corresponding to the given protocol provider from the call via selector
   * box.
   *
   * @param pps the protocol provider service to remove
   */
  public void removeCallAccount(ProtocolProviderService pps) {
    this.accountSelectorBox.removeAccount(pps);

    if (accountSelectorBox.getAccountsNumber() < 2) {
      this.comboPanel.remove(callViaPanel);
    }
  }

  /**
   * Returns TRUE if the account corresponding to the given protocol provider is already contained
   * in the call via selector box, otherwise returns FALSE.
   *
   * @param pps the protocol provider service for the account
   * @return TRUE if the account corresponding to the given protocol provider is already contained
   *     in the call via selector box, otherwise returns FALSE
   */
  public boolean containsCallAccount(ProtocolProviderService pps) {
    return accountSelectorBox.containsAccount(pps);
  }

  /**
   * Updates the call via account status.
   *
   * @param pps the protocol provider service for the account
   */
  public void updateCallAccountStatus(ProtocolProviderService pps) {
    accountSelectorBox.updateAccountStatus(pps);
  }

  /**
   * Returns the account selector box.
   *
   * @return the account selector box.
   */
  public AccountSelectorBox getAccountSelectorBox() {
    return accountSelectorBox;
  }

  /**
   * Sets the protocol provider to use for a call.
   *
   * @param provider the protocol provider to use for a call.
   */
  public void setCallProvider(ProtocolProviderService provider) {
    this.selectedCallProvider = provider;
  }

  /**
   * Implements CallListener.incomingCallReceived. When a call is received creates a call panel and
   * adds it to the main tabbed pane and plays the ring phone sound to the user.
   */
  public void incomingCallReceived(CallEvent event) {
    Call sourceCall = event.getSourceCall();

    CallPanel callPanel = new CallPanel(this, sourceCall, GuiCallParticipantRecord.INCOMING_CALL);

    mainFrame.addCallPanel(callPanel);

    if (mainFrame.getState() == JFrame.ICONIFIED) mainFrame.setState(JFrame.NORMAL);

    if (!mainFrame.isVisible()) mainFrame.setVisible(true);

    mainFrame.toFront();

    this.callButton.setEnabled(true);
    this.hangupButton.setEnabled(true);

    NotificationManager.fireNotification(
        NotificationManager.INCOMING_CALL,
        null,
        "Incoming call recived from: " + sourceCall.getCallParticipants().next());

    activeCalls.put(sourceCall, callPanel);

    this.setCallPanelVisible(true);
  }

  /**
   * Implements CallListener.callEnded. Stops sounds that are playing at the moment if there're any.
   * Removes the call panel and disables the hangup button.
   */
  public void callEnded(CallEvent event) {
    Call sourceCall = event.getSourceCall();

    NotificationManager.stopSound(NotificationManager.BUSY_CALL);
    NotificationManager.stopSound(NotificationManager.INCOMING_CALL);
    NotificationManager.stopSound(NotificationManager.OUTGOING_CALL);

    if (activeCalls.get(sourceCall) != null) {
      CallPanel callPanel = (CallPanel) activeCalls.get(sourceCall);

      this.removeCallPanelWait(callPanel);
    }
  }

  public void outgoingCallCreated(CallEvent event) {}

  /**
   * Removes the given call panel tab.
   *
   * @param callPanel the CallPanel to remove
   */
  public void removeCallPanelWait(CallPanel callPanel) {
    Timer timer = new Timer(5000, new RemoveCallPanelListener(callPanel));

    this.removeCallTimers.put(callPanel, timer);

    timer.setRepeats(false);
    timer.start();
  }

  /**
   * Removes the given call panel tab.
   *
   * @param callPanel the CallPanel to remove
   */
  private void removeCallPanel(CallPanel callPanel) {
    if (callPanel.getCall() != null && activeCalls.contains(callPanel.getCall())) {
      this.activeCalls.remove(callPanel.getCall());
    }

    mainFrame.removeCallPanel(callPanel);
    updateButtonsStateAccordingToSelectedPanel();
  }

  /** Removes the given CallPanel from the main tabbed pane. */
  private class RemoveCallPanelListener implements ActionListener {
    private CallPanel callPanel;

    public RemoveCallPanelListener(CallPanel callPanel) {
      this.callPanel = callPanel;
    }

    public void actionPerformed(ActionEvent e) {
      removeCallPanel(callPanel);
    }
  }

  /**
   * Implements ListSelectionListener.valueChanged. Enables or disables call and hangup buttons
   * depending on the selection in the contactlist.
   */
  public void valueChanged(ListSelectionEvent e) {
    Object o = mainFrame.getContactListPanel().getContactList().getSelectedValue();

    if ((e.getFirstIndex() != -1 || e.getLastIndex() != -1) && (o instanceof MetaContact)) {
      setCallMetaContact(true);

      // Switch automatically to the appropriate pps in account selector
      // box and enable callButton if telephony is supported.
      Contact contact = ((MetaContact) o).getDefaultContact(OperationSetBasicTelephony.class);

      if (contact != null) {
        callButton.setEnabled(true);

        if (contact.getProtocolProvider().isRegistered())
          getAccountSelectorBox().setSelected(contact.getProtocolProvider());
      } else {
        callButton.setEnabled(false);
      }
    } else if (phoneNumberCombo.isComboFieldEmpty()) {
      callButton.setEnabled(false);
    }
  }

  /**
   * Implements ChangeListener.stateChanged. Enables the hangup button if ones selects a tab in the
   * main tabbed pane that contains a call panel.
   */
  public void stateChanged(ChangeEvent e) {
    this.updateButtonsStateAccordingToSelectedPanel();

    Component selectedPanel = mainFrame.getSelectedTab();
    if (selectedPanel == null || !(selectedPanel instanceof CallPanel)) {
      Iterator callPanels = activeCalls.values().iterator();

      while (callPanels.hasNext()) {
        CallPanel callPanel = (CallPanel) callPanels.next();

        callPanel.removeDialogs();
      }
    }
  }

  /** Updates call and hangup buttons' states aa */
  private void updateButtonsStateAccordingToSelectedPanel() {
    Component selectedPanel = mainFrame.getSelectedTab();
    if (selectedPanel != null && selectedPanel instanceof CallPanel) {
      this.hangupButton.setEnabled(true);
    } else {
      this.hangupButton.setEnabled(false);
    }
  }

  /**
   * Returns the call button.
   *
   * @return the call button
   */
  public SIPCommButton getCallButton() {
    return callButton;
  }

  /**
   * Returns the hangup button.
   *
   * @return the hangup button
   */
  public SIPCommButton getHangupButton() {
    return hangupButton;
  }

  /**
   * Returns the main application frame. Meant to be used from the contained components that do not
   * have direct access to the MainFrame.
   *
   * @return the main application frame
   */
  public MainFrame getMainFrame() {
    return mainFrame;
  }

  /**
   * Returns the combo box, where user enters the phone number to call to.
   *
   * @return the combo box, where user enters the phone number to call to.
   */
  public JComboBox getCallComboBox() {
    return phoneNumberCombo;
  }

  /**
   * Answers the given call.
   *
   * @param call the call to answer
   */
  public void answerCall(Call call) {
    new AnswerCallThread(call).start();
  }

  /**
   * Returns TRUE if this call is a call to an internal meta contact from the contact list,
   * otherwise returns FALSE.
   *
   * @return TRUE if this call is a call to an internal meta contact from the contact list,
   *     otherwise returns FALSE
   */
  public boolean isCallMetaContact() {
    return isCallMetaContact;
  }

  /**
   * Sets the isCallMetaContact variable to TRUE or FALSE. This defines if this call is a call to a
   * given meta contact selected from the contact list or a call to an external contact or phone
   * number.
   *
   * @param isCallMetaContact TRUE to define this call as a call to an internal meta contact and
   *     FALSE to define it as a call to an external contact or phone number.
   */
  public void setCallMetaContact(boolean isCallMetaContact) {
    this.isCallMetaContact = isCallMetaContact;
  }

  /**
   * Creates a call to the contact represented by the given string.
   *
   * @param contact the contact to call to
   */
  public void createCall(String contact) {
    CallPanel callPanel = new CallPanel(this, contact);

    mainFrame.addCallPanel(callPanel);

    new CreateCallThread(contact, callPanel).start();
  }

  /**
   * Creates a call to the given list of contacts.
   *
   * @param contacts the list of contacts to call to
   */
  public void createCall(Vector contacts) {
    CallPanel callPanel = new CallPanel(this, contacts);

    mainFrame.addCallPanel(callPanel);

    new CreateCallThread(contacts, callPanel).start();
  }

  /** Creates a call from a given Contact or a given String. */
  private class CreateCallThread extends Thread {
    Vector contacts;

    CallPanel callPanel;

    String stringContact;

    OperationSetBasicTelephony telephony;

    public CreateCallThread(String contact, CallPanel callPanel) {
      this.stringContact = contact;
      this.callPanel = callPanel;

      if (selectedCallProvider != null)
        telephony = mainFrame.getTelephonyOpSet(selectedCallProvider);
    }

    public CreateCallThread(Vector contacts, CallPanel callPanel) {
      this.contacts = contacts;
      this.callPanel = callPanel;

      if (selectedCallProvider != null)
        telephony = mainFrame.getTelephonyOpSet(selectedCallProvider);
    }

    public void run() {
      if (telephony == null) return;

      Call createdCall = null;

      if (contacts != null) {
        Contact contact = (Contact) contacts.get(0);

        // NOTE: The multi user call is not yet implemented!
        // We just get the first contact and create a call for him.
        try {
          createdCall = telephony.createCall(contact);
        } catch (OperationFailedException e) {
          logger.error("The call could not be created: " + e);

          callPanel.getParticipantPanel(contact.getDisplayName()).setState(e.getMessage());

          removeCallPanelWait(callPanel);
        }

        // If the call is successfully created we set the created
        // Call instance to the already existing CallPanel and we
        // add this call to the active calls.
        if (createdCall != null) {
          callPanel.setCall(createdCall, GuiCallParticipantRecord.OUTGOING_CALL);

          activeCalls.put(createdCall, callPanel);
        }
      } else {
        try {
          createdCall = telephony.createCall(stringContact);
        } catch (ParseException e) {
          logger.error("The call could not be created: " + e);

          callPanel.getParticipantPanel(stringContact).setState(e.getMessage());

          removeCallPanelWait(callPanel);
        } catch (OperationFailedException e) {
          logger.error("The call could not be created: " + e);

          callPanel.getParticipantPanel(stringContact).setState(e.getMessage());

          removeCallPanelWait(callPanel);
        }

        // If the call is successfully created we set the created
        // Call instance to the already existing CallPanel and we
        // add this call to the active calls.
        if (createdCall != null) {
          callPanel.setCall(createdCall, GuiCallParticipantRecord.OUTGOING_CALL);

          activeCalls.put(createdCall, callPanel);
        }
      }
    }
  }

  /** Answers all call participants in the given call. */
  private class AnswerCallThread extends Thread {
    private Call call;

    public AnswerCallThread(Call call) {
      this.call = call;
    }

    public void run() {
      ProtocolProviderService pps = call.getProtocolProvider();

      Iterator participants = call.getCallParticipants();

      while (participants.hasNext()) {
        CallParticipant participant = (CallParticipant) participants.next();

        OperationSetBasicTelephony telephony = mainFrame.getTelephonyOpSet(pps);

        try {
          telephony.answerCallParticipant(participant);
        } catch (OperationFailedException e) {
          logger.error(
              "Could not answer to : " + participant + " caused by the following exception: " + e);
        }
      }
    }
  }
}
예제 #8
0
/**
 * Handles OPTIONS requests by replying with an OK response containing methods that we support.
 *
 * @author Emil Ivov
 * @author Pawel Domas
 */
public class ClientCapabilities extends MethodProcessorAdapter {

  /**
   * The <tt>Logger</tt> used by the <tt>ClientCapabilities</tt> class and its instances for logging
   * output.
   */
  private static final Logger logger = Logger.getLogger(ClientCapabilities.class);

  /** The protocol provider that created us. */
  private final ProtocolProviderServiceSipImpl provider;

  /** Registration listener instance. */
  private final RegistrationListener registrationListener;

  /** The timer that runs the keep-alive task */
  private Timer keepAliveTimer = null;

  /** The next long to use as a cseq header value. */
  private long nextCSeqValue = 1;

  /**
   * Creates a new instance of this class using the specified parent <tt>protocolProvider</tt>.
   *
   * @param protocolProvider a reference to the <tt>ProtocolProviderServiceSipImpl</tt> instance
   *     that created us.
   */
  public ClientCapabilities(ProtocolProviderServiceSipImpl protocolProvider) {
    this.provider = protocolProvider;

    provider.registerMethodProcessor(Request.OPTIONS, this);

    registrationListener = new RegistrationListener();
    provider.addRegistrationStateChangeListener(registrationListener);
  }

  /**
   * Receives options requests and replies with an OK response containing methods that we support.
   *
   * @param requestEvent the incoming options request.
   * @return <tt>true</tt> if request has been successfully processed, <tt>false</tt> otherwise
   */
  @Override
  public boolean processRequest(RequestEvent requestEvent) {
    Response optionsOK = null;
    try {
      optionsOK =
          provider.getMessageFactory().createResponse(Response.OK, requestEvent.getRequest());

      // add to the allows header all methods that we support
      for (String method : provider.getSupportedMethods()) {
        // don't support REGISTERs
        if (!method.equals(Request.REGISTER))
          optionsOK.addHeader(provider.getHeaderFactory().createAllowHeader(method));
      }

      Iterable<String> knownEventsList = provider.getKnownEventsList();

      synchronized (knownEventsList) {
        for (String event : knownEventsList)
          optionsOK.addHeader(provider.getHeaderFactory().createAllowEventsHeader(event));
      }
    } catch (ParseException ex) {
      // What else could we do apart from logging?
      logger.warn("Failed to create an incoming OPTIONS request", ex);
      return false;
    }

    try {
      SipStackSharing.getOrCreateServerTransaction(requestEvent).sendResponse(optionsOK);
    } catch (TransactionUnavailableException ex) {
      // this means that we received an OPTIONS request outside the scope
      // of a transaction which could mean that someone is simply sending
      // us b****hit to keep a NAT connection alive, so let's not get too
      // excited.
      if (logger.isInfoEnabled())
        logger.info("Failed to respond to an incoming " + "transactionless OPTIONS request");
      if (logger.isTraceEnabled()) logger.trace("Exception was:", ex);
      return false;
    } catch (InvalidArgumentException ex) {
      // What else could we do apart from logging?
      logger.warn("Failed to send an incoming OPTIONS request", ex);
      return false;
    } catch (SipException ex) {
      // What else could we do apart from logging?
      logger.warn("Failed to send an incoming OPTIONS request", ex);
      return false;
    }

    return true;
  }

  /**
   * Returns the next long to use as a cseq header value.
   *
   * @return the next long to use as a cseq header value.
   */
  private long getNextCSeqValue() {
    return nextCSeqValue++;
  }

  /** Fire event that connection has failed and we had to unregister the protocol provider. */
  private void disconnect() {
    // don't alert the user if we're already off
    if (provider.getRegistrationState().equals(RegistrationState.UNREGISTERED)) {
      return;
    }

    provider
        .getRegistrarConnection()
        .setRegistrationState(
            RegistrationState.CONNECTION_FAILED,
            RegistrationStateChangeEvent.REASON_NOT_SPECIFIED,
            "A timeout occurred while trying to connect to the server.");
  }

  /** Frees allocated resources. */
  void shutdown() {
    provider.removeRegistrationStateChangeListener(registrationListener);
  }

  /** The task would continuously send OPTIONs request that we use as a keep alive method. */
  private class OptionsKeepAliveTask extends TimerTask {
    @Override
    public void run() {
      try {
        logger.logEntry();

        // From
        FromHeader fromHeader = null;
        try {
          // this keep alive task only makes sense in case we have
          // a registrar so we deliberately use our AOR and do not
          // use the getOurSipAddress() method.
          fromHeader =
              provider
                  .getHeaderFactory()
                  .createFromHeader(
                      provider.getRegistrarConnection().getAddressOfRecord(),
                      SipMessageFactory.generateLocalTag());
        } catch (ParseException ex) {
          // this should never happen so let's just log and bail.
          logger.error("Failed to generate a from header for " + "our register request.", ex);
          return;
        }

        // Call ID Header
        CallIdHeader callIdHeader = provider.getDefaultJainSipProvider().getNewCallId();

        // CSeq Header
        CSeqHeader cSeqHeader = null;
        try {
          cSeqHeader =
              provider.getHeaderFactory().createCSeqHeader(getNextCSeqValue(), Request.OPTIONS);
        } catch (ParseException ex) {
          // Should never happen
          logger.error("Corrupt Sip Stack", ex);
          return;
        } catch (InvalidArgumentException ex) {
          // Should never happen
          logger.error("The application is corrupt", ex);
          return;
        }

        // To Header
        ToHeader toHeader = null;
        try {
          // this request isn't really going anywhere so we put our
          // own address in the To Header.
          toHeader = provider.getHeaderFactory().createToHeader(fromHeader.getAddress(), null);
        } catch (ParseException ex) {
          logger.error("Could not create a To header for address:" + fromHeader.getAddress(), ex);
          return;
        }

        // MaxForwardsHeader
        MaxForwardsHeader maxForwardsHeader = provider.getMaxForwardsHeader();
        // Request
        Request request = null;
        try {
          // create a host-only uri for the request uri header.
          String domain = ((SipURI) toHeader.getAddress().getURI()).getHost();

          // request URI
          SipURI requestURI = provider.getAddressFactory().createSipURI(null, domain);

          // Via Headers
          ArrayList<ViaHeader> viaHeaders = provider.getLocalViaHeaders(requestURI);

          request =
              provider
                  .getMessageFactory()
                  .createRequest(
                      requestURI,
                      Request.OPTIONS,
                      callIdHeader,
                      cSeqHeader,
                      fromHeader,
                      toHeader,
                      viaHeaders,
                      maxForwardsHeader);

          if (logger.isDebugEnabled()) logger.debug("Created OPTIONS request " + request);
        } catch (ParseException ex) {
          logger.error("Could not create an OPTIONS request!", ex);
          return;
        }

        Iterator<String> supportedMethods = provider.getSupportedMethods().iterator();

        // add to the allows header all methods that we support
        while (supportedMethods.hasNext()) {
          String method = supportedMethods.next();

          // don't support REGISTERs
          if (method.equals(Request.REGISTER)) continue;

          request.addHeader(provider.getHeaderFactory().createAllowHeader(method));
        }

        Iterator<String> events = provider.getKnownEventsList().iterator();

        synchronized (provider.getKnownEventsList()) {
          while (events.hasNext()) {
            String event = events.next();

            request.addHeader(provider.getHeaderFactory().createAllowEventsHeader(event));
          }
        }

        // Transaction
        ClientTransaction optionsTrans = null;
        try {
          optionsTrans = provider.getDefaultJainSipProvider().getNewClientTransaction(request);
        } catch (TransactionUnavailableException ex) {
          logger.error("Could not create options transaction!\n", ex);
          return;
        }
        try {
          optionsTrans.sendRequest();
          if (logger.isDebugEnabled()) logger.debug("sent request= " + request);
        } catch (SipException ex) {
          logger.error("Could not send out the options request!", ex);

          if (ex.getCause() instanceof IOException) {
            // IOException problem with network
            disconnect();
          }

          return;
        }
      } catch (Exception ex) {
        logger.error("Cannot send OPTIONS keep alive", ex);
      }
    }
  }

  /** Class implements CRLF keep alive method. */
  private class CRLfKeepAliveTask extends TimerTask {

    @Override
    public void run() {
      ProxyConnection connection = provider.getConnection();
      if (connection == null) {
        logger.error("No connection found to send CRLF keep alive" + " with " + provider);
        return;
      }

      ListeningPoint lp = provider.getListeningPoint(connection.getTransport());

      if (!(lp instanceof ListeningPointExt)) {
        logger.error("ListeningPoint is not ListeningPointExt" + "(or is null)");
        return;
      }

      InetSocketAddress address = connection.getAddress();
      try {
        ((ListeningPointExt) lp)
            .sendHeartbeat(address.getAddress().getHostAddress(), address.getPort());
      } catch (IOException e) {
        logger.error("Error while sending a heartbeat", e);
      }
    }
  }

  private class RegistrationListener implements RegistrationStateChangeListener {
    /**
     * The method is called by a ProtocolProvider implementation whenever a change in the
     * registration state of the corresponding provider had occurred. The method is particularly
     * interested in events stating that the SIP provider has unregistered so that it would fire
     * status change events for all contacts in our buddy list.
     *
     * @param evt ProviderStatusChangeEvent the event describing the status change.
     */
    public void registrationStateChanged(RegistrationStateChangeEvent evt) {
      if (evt.getNewState() == RegistrationState.UNREGISTERING
          || evt.getNewState() == RegistrationState.UNREGISTERED
          || evt.getNewState() == RegistrationState.AUTHENTICATION_FAILED
          || evt.getNewState() == RegistrationState.CONNECTION_FAILED) {
        // stop any task associated with the timer
        if (keepAliveTimer != null) {
          keepAliveTimer.cancel();
          keepAliveTimer = null;
        }
      } else if (evt.getNewState().equals(RegistrationState.REGISTERED)) {
        String keepAliveMethod =
            provider
                .getAccountID()
                .getAccountPropertyString(ProtocolProviderFactory.KEEP_ALIVE_METHOD);

        if (logger.isTraceEnabled()) logger.trace("Keep alive method " + keepAliveMethod);
        // options is default keep-alive, if property is missing
        // then options is used
        if (keepAliveMethod != null
            && !(keepAliveMethod.equalsIgnoreCase("options")
                || keepAliveMethod.equalsIgnoreCase("crlf"))) return;

        int keepAliveInterval =
            provider
                .getAccountID()
                .getAccountPropertyInt(ProtocolProviderFactory.KEEP_ALIVE_INTERVAL, -1);

        if (logger.isTraceEnabled()) logger.trace("Keep alive interval is " + keepAliveInterval);
        if (keepAliveInterval > 0 && !provider.getRegistrarConnection().isRegistrarless()) {
          if (keepAliveTimer == null) keepAliveTimer = new Timer();

          TimerTask keepAliveTask;
          // CRLF is used by default on Android
          if ((OSUtils.IS_ANDROID && keepAliveMethod == null)
              || "crlf".equalsIgnoreCase(keepAliveMethod)) {
            keepAliveTask = new CRLfKeepAliveTask();
          } else {
            // OPTIONS
            keepAliveTask = new OptionsKeepAliveTask();
          }

          if (logger.isDebugEnabled()) logger.debug("Scheduling keep alives: " + keepAliveTask);

          keepAliveTimer.schedule(keepAliveTask, 0, keepAliveInterval * 1000);
        }
      }
    }
  }
}
/**
 * Performs testing of the basic instant messaging operation set. Tests include going over basic
 * functionality such as sending a message from the tested implementation and asserting reception by
 * the tester agent and vice versa.
 *
 * @author Emil Ivov
 */
public class TestOperationSetBasicInstantMessaging extends TestCase {
  private static final Logger logger =
      Logger.getLogger(TestOperationSetBasicInstantMessaging.class);

  private GibberishSlickFixture fixture = new GibberishSlickFixture();

  private OperationSetBasicInstantMessaging opSetBasicIM1 = null;
  private OperationSetBasicInstantMessaging opSetBasicIM2 = null;

  private OperationSetPresence opSetPresence1 = null;
  private OperationSetPresence opSetPresence2 = null;

  public TestOperationSetBasicInstantMessaging(String name) {
    super(name);
  }

  /**
   * Get a reference to the basic IM operation set.
   *
   * @throws Exception if this is not a good day.
   */
  protected void setUp() throws Exception {
    super.setUp();
    fixture.setUp();

    Map<String, OperationSet> supportedOperationSets1 =
        fixture.provider1.getSupportedOperationSets();

    if (supportedOperationSets1 == null || supportedOperationSets1.size() < 1)
      throw new NullPointerException(
          "No OperationSet implementations are supported by " + "this implementation. ");

    // get the operation set presence here.
    opSetBasicIM1 =
        (OperationSetBasicInstantMessaging)
            supportedOperationSets1.get(OperationSetBasicInstantMessaging.class.getName());

    if (opSetBasicIM1 == null) {
      throw new NullPointerException("No implementation for basic IM was found");
    }

    // we also need the presence op set in order to retrieve contacts.
    opSetPresence1 =
        (OperationSetPresence) supportedOperationSets1.get(OperationSetPresence.class.getName());

    // if the op set is null show that we're not happy.
    if (opSetPresence1 == null) {
      throw new NullPointerException(
          "An implementation of the service must provide an "
              + "implementation of at least one of the PresenceOperationSets");
    }

    Map<String, OperationSet> supportedOperationSets2 =
        fixture.provider2.getSupportedOperationSets();

    if (supportedOperationSets2 == null || supportedOperationSets2.size() < 1)
      throw new NullPointerException(
          "No OperationSet implementations are supported by " + "this implementation. ");

    // get the operation set presence here.
    opSetBasicIM2 =
        (OperationSetBasicInstantMessaging)
            supportedOperationSets2.get(OperationSetBasicInstantMessaging.class.getName());

    if (opSetBasicIM2 == null) {
      throw new NullPointerException("No implementation for basic IM was found");
    }

    opSetPresence2 =
        (OperationSetPresence) supportedOperationSets2.get(OperationSetPresence.class.getName());

    // if the op set is null show that we're not happy.
    if (opSetPresence2 == null) {
      throw new NullPointerException(
          "An implementation of the service must provide an "
              + "implementation of at least one of the PresenceOperationSets");
    }
  }

  protected void tearDown() throws Exception {
    super.tearDown();

    fixture.tearDown();
  }

  /**
   * Creates a test suite containing tests of this class in a specific order. We'll first execute
   * tests beginning with the "test" prefix and then go to ordered tests.We first execture tests for
   * receiving messagese, so that a volatile contact is created for the sender. we'll then be able
   * to retrieve this volatile contact and send them a message on our turn. We need to do things
   * this way as the contact corresponding to the tester agent has been removed in the previous test
   * and we no longer have it in our contact list.
   *
   * @return Test a testsuite containing all tests to execute.
   */
  public static Test suite() {
    TestSuite suite = new TestSuite();

    suite.addTest(new TestOperationSetBasicInstantMessaging("prepareContactList"));

    suite.addTestSuite(TestOperationSetBasicInstantMessaging.class);

    // the following 2 need to be run in the specified order.
    suite.addTest(new TestOperationSetBasicInstantMessaging("firstTestReceiveMessage"));
    suite.addTest(new TestOperationSetBasicInstantMessaging("thenTestSendMessage"));

    return suite;
  }

  /**
   * Create the list to be sure that contacts exchanging messages exists in each other lists
   *
   * @throws Exception
   */
  public void prepareContactList() throws Exception {
    fixture.clearProvidersLists();

    Object o = new Object();
    synchronized (o) {
      o.wait(2000);
    }

    try {
      opSetPresence1.subscribe(fixture.userID2);
    } catch (OperationFailedException ex) {
      // the contact already exist its OK
    }

    try {
      opSetPresence2.subscribe(fixture.userID1);
    } catch (OperationFailedException ex1) {
      // the contact already exist its OK
    }

    synchronized (o) {
      o.wait(2000);
    }
  }

  /**
   * Send an instant message from the tested operation set and assert reception by the tester agent.
   */
  public void firstTestReceiveMessage() {
    String body = "This is an IM coming from the tester agent" + " on " + new Date().toString();

    ImEventCollector evtCollector = new ImEventCollector();

    // add a msg listener and register to the op set and send an instant
    // msg from the tester agent.
    opSetBasicIM1.addMessageListener(evtCollector);

    Contact testerAgentContact = opSetPresence2.findContactByID(fixture.userID1);

    logger.debug("Will send message " + body + " to: " + testerAgentContact);

    opSetBasicIM2.sendInstantMessage(testerAgentContact, opSetBasicIM2.createMessage(body));

    evtCollector.waitForEvent(10000);

    opSetBasicIM1.removeMessageListener(evtCollector);

    // assert reception of a message event
    assertTrue(
        "No events delivered upon a received message", evtCollector.collectedEvents.size() > 0);

    // assert event instance of Message Received Evt
    assertTrue(
        "Received evt was not an instance of " + MessageReceivedEvent.class.getName(),
        evtCollector.collectedEvents.get(0) instanceof MessageReceivedEvent);

    // assert source contact == testAgent.uin
    MessageReceivedEvent evt = (MessageReceivedEvent) evtCollector.collectedEvents.get(0);
    assertEquals("message sender ", evt.getSourceContact().getAddress(), fixture.userID2);

    // assert messageBody == body
    assertEquals("message body", body, evt.getSourceMessage().getContent());
  }

  /**
   * Send an instant message from the tester agent and assert reception by the tested implementation
   */
  public void thenTestSendMessage() {
    logger.debug(
        "Printing Server Stored list to see if message fails are contacts in each other lists");
    ContactGroup rootGroup1 =
        ((OperationSetPersistentPresence) opSetPresence1).getServerStoredContactListRoot();

    logger.debug("=========== Server Stored Contact List 1 =================");

    logger.debug(
        "rootGroup="
            + rootGroup1.getGroupName()
            + " rootGroup.childContacts="
            + rootGroup1.countContacts()
            + "rootGroup.childGroups="
            + rootGroup1.countSubgroups()
            + "Printing rootGroupContents=\n"
            + rootGroup1.toString());

    ContactGroup rootGroup2 =
        ((OperationSetPersistentPresence) opSetPresence2).getServerStoredContactListRoot();

    logger.debug("=========== Server Stored Contact List 2 =================");

    logger.debug(
        "rootGroup="
            + rootGroup2.getGroupName()
            + " rootGroup.childContacts="
            + rootGroup2.countContacts()
            + "rootGroup.childGroups="
            + rootGroup2.countSubgroups()
            + "Printing rootGroupContents=\n"
            + rootGroup2.toString());

    String body =
        "This is an IM coming from the tested implementation" + " on " + new Date().toString();

    // create the message
    net.java.sip.communicator.service.protocol.Message msg = opSetBasicIM1.createMessage(body);

    // register a listener in the op set
    ImEventCollector imEvtCollector1 = new ImEventCollector();
    opSetBasicIM1.addMessageListener(imEvtCollector1);

    // register a listener in the tester agent
    ImEventCollector imEvtCollector2 = new ImEventCollector();
    opSetBasicIM2.addMessageListener(imEvtCollector2);

    Contact testerAgentContact = opSetPresence1.findContactByID(fixture.userID2);

    opSetBasicIM1.sendInstantMessage(testerAgentContact, msg);

    imEvtCollector1.waitForEvent(10000);
    imEvtCollector2.waitForEvent(10000);

    opSetBasicIM1.removeMessageListener(imEvtCollector1);
    opSetBasicIM2.removeMessageListener(imEvtCollector2);

    // verify that the message delivered event was dispatched
    assertTrue(
        "No events delivered when sending a message", imEvtCollector1.collectedEvents.size() > 0);

    assertTrue(
        "Received evt was not an instance of " + MessageDeliveredEvent.class.getName(),
        imEvtCollector1.collectedEvents.get(0) instanceof MessageDeliveredEvent);

    MessageDeliveredEvent evt = (MessageDeliveredEvent) imEvtCollector1.collectedEvents.get(0);
    assertEquals("message destination ", evt.getDestinationContact().getAddress(), fixture.userID2);

    assertSame("source message", msg, evt.getSourceMessage());

    // verify that the message has successfully arived at the destination
    assertTrue(
        "No messages received by the tester agent", imEvtCollector2.collectedEvents.size() > 0);

    assertFalse(
        "Message was unable to deliver !",
        imEvtCollector2.collectedEvents.get(0) instanceof MessageDeliveryFailedEvent);

    String receivedBody =
        ((MessageReceivedEvent) imEvtCollector2.collectedEvents.get(0))
            .getSourceMessage()
            .getContent();

    assertEquals("received message body", msg.getContent(), receivedBody);
  }

  /** Creates an Message through the simple createMessage() method and inspects its parameters. */
  public void testCreateMessage1() {
    String body =
        "This is an IM coming from the tested implementation" + " on " + new Date().toString();
    net.java.sip.communicator.service.protocol.Message msg = opSetBasicIM1.createMessage(body);

    assertEquals("message body", body, msg.getContent());
    assertTrue("message body bytes", Arrays.equals(body.getBytes(), msg.getRawData()));
    assertEquals("message length", body.length(), msg.getSize());
    assertEquals(
        "message content type",
        OperationSetBasicInstantMessaging.DEFAULT_MIME_TYPE,
        msg.getContentType());

    assertEquals(
        "message encoding",
        OperationSetBasicInstantMessaging.DEFAULT_MIME_ENCODING,
        msg.getEncoding());

    assertNotNull("message uid", msg.getMessageUID());

    // a further test on message uid.
    net.java.sip.communicator.service.protocol.Message msg2 = opSetBasicIM1.createMessage(body);
    assertFalse("message uid", msg.getMessageUID().equals(msg2.getMessageUID()));
  }

  /** Creates an Message through the advance createMessage() method and inspects its parameters. */
  public void testCreateMessage2() throws UnsupportedEncodingException {
    String body =
        "This is an IM coming from the tested implementation" + " on " + new Date().toString();
    String contentType = "text/html";
    String encoding = "UTF-16";
    String subject = "test message";
    net.java.sip.communicator.service.protocol.Message msg =
        opSetBasicIM1.createMessage(body, contentType, encoding, subject);
    byte[] bodyBytes = body.getBytes(encoding);

    assertEquals("message body", body, msg.getContent());
    assertTrue("message body bytes", Arrays.equals(bodyBytes, msg.getRawData()));
    assertEquals("message length", bodyBytes.length, msg.getSize());
    assertEquals("message content type", contentType, msg.getContentType());
    assertEquals("message encoding", encoding, msg.getEncoding());
    assertNotNull("message uid", msg.getMessageUID());

    // a further test on message uid.
    net.java.sip.communicator.service.protocol.Message msg2 = opSetBasicIM1.createMessage(body);
    assertFalse("message uid", msg.getMessageUID().equals(msg2.getMessageUID()));
  }

  /** Collects instant messaging events. */
  private class ImEventCollector implements MessageListener {
    private List<EventObject> collectedEvents = new LinkedList<EventObject>();
    /**
     * Called when a new incoming <tt>Message</tt> has been received.
     *
     * @param evt the <tt>MessageReceivedEvent</tt> containing the newly received message, its
     *     sender and other details.
     */
    public void messageReceived(MessageReceivedEvent evt) {
      logger.debug("Received a MessageReceivedEvent: " + evt);

      synchronized (this) {
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Called to indicated that delivery of a message sent earlier has failed. Reason code and
     * phrase are contained by the <tt>MessageFailedEvent</tt>
     *
     * @param evt the <tt>MessageFailedEvent</tt> containing the ID of the message whose delivery
     *     has failed.
     */
    public void messageDeliveryFailed(MessageDeliveryFailedEvent evt) {
      logger.debug("Received a MessageDeliveryFailedEvent: " + evt);

      synchronized (this) {
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Called when the underlying implementation has received an indication that a message, sent
     * earlier has been successfully received by the destination.
     *
     * @param evt the MessageDeliveredEvent containing the id of the message that has caused the
     *     event.
     */
    public void messageDelivered(MessageDeliveredEvent evt) {
      logger.debug("Received a MessageDeliveredEvent: " + evt);

      synchronized (this) {
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Blocks until at least one event is received or until waitFor miliseconds pass (whichever
     * happens first).
     *
     * @param waitFor the number of miliseconds that we should be waiting for an event before simply
     *     bailing out.
     */
    public void waitForEvent(long waitFor) {
      synchronized (this) {
        if (collectedEvents.size() > 0) return;

        try {
          wait(waitFor);
        } catch (InterruptedException ex) {
          logger.debug("Interrupted while waiting for a message evt", ex);
        }
      }
    }
  }
}
/**
 * The <tt>AndroidLoginRenderer</tt> is the Android renderer for login events.
 *
 * @author Yana Stamcheva
 * @author Pawel Domas
 */
public class AndroidLoginRenderer implements LoginRenderer {
  /** The logger */
  private static final Logger logger = Logger.getLogger(AndroidLoginRenderer.class);

  /** The <tt>CallListener</tt>. */
  private CallListener androidCallListener;

  /** The android implementation of the provider presence listener. */
  private final ProviderPresenceStatusListener androidPresenceListener =
      new UIProviderPresenceStatusListener();

  /** The security authority used by this login renderer. */
  private final SecurityAuthority securityAuthority;

  /** Authorization handler instance. */
  private final AuthorizationHandlerImpl authorizationHandler;

  /** Cached global status value */
  private PresenceStatus globalStatus;

  /** List of global status listeners. */
  private EventListenerList<PresenceStatus> globalStatusListeners =
      new EventListenerList<PresenceStatus>();

  /** Caches avatar image to track the changes */
  private byte[] localAvatarRaw;

  /** Local avatar drawable */
  private Drawable localAvatar;

  /** Caches local status to track the changes */
  private byte[] localStatusRaw;

  /** Local status drawable */
  private Drawable localStatusDrawable;

  /**
   * Creates an instance of <tt>AndroidLoginRenderer</tt> by specifying the current
   * <tt>Context</tt>.
   *
   * @param defaultSecurityAuthority the security authority that will be used by this login renderer
   */
  public AndroidLoginRenderer(SecurityAuthority defaultSecurityAuthority) {
    androidCallListener = new AndroidCallListener();

    securityAuthority = defaultSecurityAuthority;

    authorizationHandler = new AuthorizationHandlerImpl();
  }

  /**
   * Adds the user interface related to the given protocol provider.
   *
   * @param protocolProvider the protocol provider for which we add the user interface
   */
  public void addProtocolProviderUI(ProtocolProviderService protocolProvider) {
    OperationSetBasicTelephony<?> telOpSet =
        protocolProvider.getOperationSet(OperationSetBasicTelephony.class);

    if (telOpSet != null) {
      telOpSet.addCallListener(androidCallListener);
    }

    OperationSetPresence presenceOpSet =
        protocolProvider.getOperationSet(OperationSetPresence.class);

    if (presenceOpSet != null) {
      presenceOpSet.addProviderPresenceStatusListener(androidPresenceListener);
    }
  }

  /**
   * Removes the user interface related to the given protocol provider.
   *
   * @param protocolProvider the protocol provider to remove
   */
  public void removeProtocolProviderUI(ProtocolProviderService protocolProvider) {
    OperationSetBasicTelephony<?> telOpSet =
        protocolProvider.getOperationSet(OperationSetBasicTelephony.class);

    if (telOpSet != null) {
      telOpSet.removeCallListener(androidCallListener);
    }

    OperationSetPresence presenceOpSet =
        protocolProvider.getOperationSet(OperationSetPresence.class);

    if (presenceOpSet != null) {
      presenceOpSet.removeProviderPresenceStatusListener(androidPresenceListener);
    }

    // Removes all chat session for unregistered provider
    ChatSessionManager.removeAllChatsForProvider(protocolProvider);
  }

  /**
   * Starts the connecting user interface for the given protocol provider.
   *
   * @param protocolProvider the protocol provider for which we add the connecting user interface
   */
  public void startConnectingUI(ProtocolProviderService protocolProvider) {}

  /**
   * Stops the connecting user interface for the given protocol provider.
   *
   * @param protocolProvider the protocol provider for which we remove the connecting user interface
   */
  public void stopConnectingUI(ProtocolProviderService protocolProvider) {}

  /**
   * Indicates that the given protocol provider has been connected at the given time.
   *
   * @param protocolProvider the <tt>ProtocolProviderService</tt> corresponding to the connected
   *     account
   * @param date the date/time at which the account has connected
   */
  public void protocolProviderConnected(ProtocolProviderService protocolProvider, long date) {

    OperationSetPresence presence = AccountStatusUtils.getProtocolPresenceOpSet(protocolProvider);

    if (presence != null) {
      presence.setAuthorizationHandler(authorizationHandler);
    }

    updateGlobalStatus();
  }

  /**
   * Indicates that a protocol provider connection has failed.
   *
   * @param protocolProvider the <tt>ProtocolProviderService</tt>, which connection failed
   * @param loginManagerCallback the <tt>LoginManager</tt> implementation, which is managing the
   *     process
   */
  public void protocolProviderConnectionFailed(
      final ProtocolProviderService protocolProvider, final LoginManager loginManagerCallback) {
    AccountID accountID = protocolProvider.getAccountID();

    AndroidUtils.showAlertConfirmDialog(
        JitsiApplication.getGlobalContext(),
        JitsiApplication.getResString(R.string.service_gui_ERROR),
        JitsiApplication.getResString(
            R.string.service_gui_CONNECTION_FAILED_MSG,
            accountID.getUserID(),
            accountID.getService()),
        JitsiApplication.getResString(R.string.service_gui_RETRY),
        new DialogActivity.DialogListener() {
          public boolean onConfirmClicked(DialogActivity dialog) {
            loginManagerCallback.login(protocolProvider);
            return true;
          }

          public void onDialogCancelled(DialogActivity dialog) {}
        });
  }

  /**
   * Returns the <tt>SecurityAuthority</tt> implementation related to this login renderer.
   *
   * @param protocolProvider the specific <tt>ProtocolProviderService</tt>, for which we're
   *     obtaining a security authority
   * @return the <tt>SecurityAuthority</tt> implementation related to this login renderer
   */
  public SecurityAuthority getSecurityAuthorityImpl(ProtocolProviderService protocolProvider) {
    return securityAuthority;
  }

  /** Updates Jitsi icon notification to reflect current global status. */
  public void updateJitsiIconNotification() {
    String status;
    if (getGlobalStatus().isOnline()) {
      // At least one provider is online
      status = JitsiApplication.getResString(R.string.service_gui_ONLINE);
    } else {
      // There are no active providers so we consider to be in
      // the offline state
      status = JitsiApplication.getResString(R.string.service_gui_OFFLINE);
    }

    int notificationID = OSGiService.getGeneralNotificationId();
    if (notificationID == -1) {
      logger.debug(
          "Not displaying status notification because"
              + " there's no global notification icon available.");
      return;
    }

    AndroidUtils.updateGeneralNotification(
        JitsiApplication.getGlobalContext(),
        notificationID,
        JitsiApplication.getResString(R.string.app_name),
        status,
        System.currentTimeMillis());
  }

  /**
   * Adds global status listener.
   *
   * @param l the listener to be add.
   */
  public void addGlobalStatusListener(EventListener<PresenceStatus> l) {
    globalStatusListeners.addEventListener(l);
  }

  /**
   * Removes global status listener.
   *
   * @param l the listener to remove.
   */
  public void removeGlobalStatusListener(EventListener<PresenceStatus> l) {
    globalStatusListeners.removeEventListener(l);
  }

  /**
   * Returns current global status.
   *
   * @return current global status.
   */
  public PresenceStatus getGlobalStatus() {
    if (globalStatus == null) {
      GlobalStatusService gss = AndroidGUIActivator.getGlobalStatusService();

      globalStatus = gss != null ? gss.getGlobalPresenceStatus() : GlobalStatusEnum.OFFLINE;
    }
    return globalStatus;
  }

  /** AuthorizationHandler instance used by this login renderer. */
  public AuthorizationHandlerImpl getAuthorizationHandler() {
    return authorizationHandler;
  }

  /**
   * Listens for all providerStatusChanged and providerStatusMessageChanged events in order to
   * refresh the account status panel, when a status is changed.
   */
  private class UIProviderPresenceStatusListener implements ProviderPresenceStatusListener {
    public void providerStatusChanged(ProviderPresenceStatusChangeEvent evt) {
      updateGlobalStatus();
    }

    public void providerStatusMessageChanged(PropertyChangeEvent evt) {}
  }

  /**
   * Indicates if the given <tt>protocolProvider</tt> related user interface is already rendered.
   *
   * @param protocolProvider the <tt>ProtocolProviderService</tt>, which related user interface
   *     we're looking for
   * @return <tt>true</tt> if the given <tt>protocolProvider</tt> related user interface is already
   *     rendered
   */
  public boolean containsProtocolProviderUI(ProtocolProviderService protocolProvider) {
    return false;
  }

  /** Updates the global status by picking the most connected protocol provider status. */
  private void updateGlobalStatus() {
    // Only if the GUI is active (bundle context will be null on shutdown)
    if (AndroidGUIActivator.bundleContext != null) {
      // Invalidate local status image
      localStatusRaw = null;
      // Invalidate global status
      globalStatus = null;
      globalStatusListeners.notifyEventListeners(getGlobalStatus());
    }

    updateJitsiIconNotification();
  }

  /**
   * Returns the local user avatar drawable.
   *
   * @return the local user avatar drawable.
   */
  public Drawable getLocalAvatarDrawable() {
    GlobalDisplayDetailsService displayDetailsService =
        AndroidGUIActivator.getGlobalDisplayDetailsService();

    byte[] avatarImage = displayDetailsService.getGlobalDisplayAvatar();
    // Re-create drawable only if avatar has changed
    if (avatarImage != localAvatarRaw) {
      localAvatarRaw = avatarImage;
      localAvatar = AndroidImageUtil.roundedDrawableFromBytes(avatarImage);
    }
    return localAvatar;
  }

  /**
   * Returns the local user status drawable.
   *
   * @return the local user status drawable
   */
  public synchronized Drawable getLocalStatusDrawable() {
    byte[] statusImage = StatusUtil.getContactStatusIcon(getGlobalStatus());
    if (statusImage != localStatusRaw) {
      localStatusRaw = statusImage;
      localStatusDrawable =
          localStatusRaw != null ? AndroidImageUtil.drawableFromBytes(statusImage) : null;
    }
    return localStatusDrawable;
  }
}
/**
 * Represents a default implementation of {@link OperationSetServerStoredAccountInfo} in order to
 * make it easier for implementers to provide complete solutions while focusing on
 * implementation-specific details.
 *
 * @author Damian Minkov
 */
public abstract class AbstractOperationSetServerStoredAccountInfo
    implements OperationSetServerStoredAccountInfo {
  /**
   * The <tt>Logger</tt> used by the <tt>AbstractOperationSetPersistentPresence</tt> class and its
   * instances for logging output.
   */
  private static final Logger logger =
      Logger.getLogger(AbstractOperationSetServerStoredAccountInfo.class);

  /** A list of listeners registered for <tt>ServerStoredDetailsChangeListener</tt>s. */
  private final List<ServerStoredDetailsChangeListener> serverStoredDetailsListeners =
      new ArrayList<ServerStoredDetailsChangeListener>();

  /**
   * Registers a ServerStoredDetailsChangeListener with this operation set so that it gets
   * notifications of details change.
   *
   * @param listener the <tt>ServerStoredDetailsChangeListener</tt> to register.
   */
  public void addServerStoredDetailsChangeListener(ServerStoredDetailsChangeListener listener) {
    synchronized (serverStoredDetailsListeners) {
      if (!serverStoredDetailsListeners.contains(listener))
        serverStoredDetailsListeners.add(listener);
    }
  }

  /**
   * Unregisters <tt>listener</tt> so that it won't receive any further notifications upon details
   * change.
   *
   * @param listener the <tt>ServerStoredDetailsChangeListener</tt> to unregister.
   */
  public void removeServerStoredDetailsChangeListener(ServerStoredDetailsChangeListener listener) {
    synchronized (serverStoredDetailsListeners) {
      serverStoredDetailsListeners.remove(listener);
    }
  }

  /**
   * Notify all listeners of the corresponding account detail change event.
   *
   * @param source the protocol provider service source
   * @param eventID the int ID of the event to dispatch
   * @param oldValue the value that the changed property had before the change occurred.
   * @param newValue the value that the changed property currently has (after the change has
   *     occurred).
   */
  public void fireServerStoredDetailsChangeEvent(
      ProtocolProviderService source, int eventID, Object oldValue, Object newValue) {
    ServerStoredDetailsChangeEvent evt =
        new ServerStoredDetailsChangeEvent(source, eventID, oldValue, newValue);

    Collection<ServerStoredDetailsChangeListener> listeners;
    synchronized (serverStoredDetailsListeners) {
      listeners = new ArrayList<ServerStoredDetailsChangeListener>(serverStoredDetailsListeners);
    }

    if (logger.isDebugEnabled())
      logger.debug(
          "Dispatching a Contact Property Change Event to"
              + listeners.size()
              + " listeners. Evt="
              + evt);

    for (ServerStoredDetailsChangeListener listener : listeners)
      listener.serverStoredDetailsChanged(evt);
  }
}
예제 #12
0
/**
 * The single chat implementation of the <tt>ChatTransport</tt> interface that provides abstraction
 * to protocol provider access.
 *
 * @author Yana Stamcheva
 */
public class MetaContactChatTransport implements ChatTransport, ContactPresenceStatusListener {
  /** The logger. */
  private static final Logger logger = Logger.getLogger(MetaContactChatTransport.class);

  /** The parent <tt>ChatSession</tt>, where this transport is available. */
  private final MetaContactChatSession parentChatSession;

  /** The associated protocol <tt>Contact</tt>. */
  private final Contact contact;

  /** The resource associated with this contact. */
  private ContactResource contactResource;

  /** The protocol presence operation set associated with this transport. */
  private final OperationSetPresence presenceOpSet;

  /** The thumbnail default width. */
  private static final int THUMBNAIL_WIDTH = 64;

  /** The thumbnail default height. */
  private static final int THUMBNAIL_HEIGHT = 64;

  /** Indicates if only the resource name should be displayed. */
  private boolean isDisplayResourceOnly = false;

  /**
   * Creates an instance of <tt>MetaContactChatTransport</tt> by specifying the parent
   * <tt>chatSession</tt> and the <tt>contact</tt> associated with the transport.
   *
   * @param chatSession the parent <tt>ChatSession</tt>
   * @param contact the <tt>Contact</tt> associated with this transport
   */
  public MetaContactChatTransport(MetaContactChatSession chatSession, Contact contact) {
    this(chatSession, contact, null, false);
  }

  /**
   * Creates an instance of <tt>MetaContactChatTransport</tt> by specifying the parent
   * <tt>chatSession</tt> and the <tt>contact</tt> associated with the transport.
   *
   * @param chatSession the parent <tt>ChatSession</tt>
   * @param contact the <tt>Contact</tt> associated with this transport
   * @param contactResource the <tt>ContactResource</tt> associated with the contact
   * @param isDisplayResourceOnly indicates if only the resource name should be displayed
   */
  public MetaContactChatTransport(
      MetaContactChatSession chatSession,
      Contact contact,
      ContactResource contactResource,
      boolean isDisplayResourceOnly) {
    this.parentChatSession = chatSession;
    this.contact = contact;
    this.contactResource = contactResource;
    this.isDisplayResourceOnly = isDisplayResourceOnly;

    presenceOpSet = contact.getProtocolProvider().getOperationSet(OperationSetPresence.class);

    if (presenceOpSet != null) presenceOpSet.addContactPresenceStatusListener(this);

    // checking this can be slow so make
    // sure its out of our way
    new Thread(
            new Runnable() {
              public void run() {
                checkImCaps();
              }
            })
        .start();
  }

  /**
   * If sending im is supported check it for supporting html messages if a font is set. As it can be
   * slow make sure its not on our way
   */
  private void checkImCaps() {
    if (ConfigurationUtils.getChatDefaultFontFamily() != null
        && ConfigurationUtils.getChatDefaultFontSize() > 0) {
      OperationSetBasicInstantMessaging imOpSet =
          contact.getProtocolProvider().getOperationSet(OperationSetBasicInstantMessaging.class);

      if (imOpSet != null)
        imOpSet.isContentTypeSupported(OperationSetBasicInstantMessaging.HTML_MIME_TYPE, contact);
    }
  }

  /**
   * Returns the contact associated with this transport.
   *
   * @return the contact associated with this transport
   */
  public Contact getContact() {
    return contact;
  }

  /**
   * Returns the contact address corresponding to this chat transport.
   *
   * @return The contact address corresponding to this chat transport.
   */
  public String getName() {
    return contact.getAddress();
  }

  /**
   * Returns the display name corresponding to this chat transport.
   *
   * @return The display name corresponding to this chat transport.
   */
  public String getDisplayName() {
    return contact.getDisplayName();
  }

  /**
   * Returns the resource name of this chat transport. This is for example the name of the user
   * agent from which the contact is logged.
   *
   * @return The display name of this chat transport resource.
   */
  public String getResourceName() {
    if (contactResource != null) return contactResource.getResourceName();

    return null;
  }

  public boolean isDisplayResourceOnly() {
    return isDisplayResourceOnly;
  }

  /**
   * Returns the presence status of this transport.
   *
   * @return the presence status of this transport.
   */
  public PresenceStatus getStatus() {
    if (contactResource != null) return contactResource.getPresenceStatus();
    else return contact.getPresenceStatus();
  }

  /**
   * Returns the <tt>ProtocolProviderService</tt>, corresponding to this chat transport.
   *
   * @return the <tt>ProtocolProviderService</tt>, corresponding to this chat transport.
   */
  public ProtocolProviderService getProtocolProvider() {
    return contact.getProtocolProvider();
  }

  /**
   * Returns <code>true</code> if this chat transport supports instant messaging, otherwise returns
   * <code>false</code>.
   *
   * @return <code>true</code> if this chat transport supports instant messaging, otherwise returns
   *     <code>false</code>.
   */
  public boolean allowsInstantMessage() {
    // First try to ask the capabilities operation set if such is
    // available.
    OperationSetContactCapabilities capOpSet =
        getProtocolProvider().getOperationSet(OperationSetContactCapabilities.class);

    if (capOpSet != null) {
      if (capOpSet.getOperationSet(contact, OperationSetBasicInstantMessaging.class) != null) {
        return true;
      }
    } else if (contact
            .getProtocolProvider()
            .getOperationSet(OperationSetBasicInstantMessaging.class)
        != null) return true;

    return false;
  }

  /**
   * Returns <code>true</code> if this chat transport supports message corrections and false
   * otherwise.
   *
   * @return <code>true</code> if this chat transport supports message corrections and false
   *     otherwise.
   */
  public boolean allowsMessageCorrections() {
    OperationSetContactCapabilities capOpSet =
        getProtocolProvider().getOperationSet(OperationSetContactCapabilities.class);

    if (capOpSet != null) {
      return capOpSet.getOperationSet(contact, OperationSetMessageCorrection.class) != null;
    } else {
      return contact.getProtocolProvider().getOperationSet(OperationSetMessageCorrection.class)
          != null;
    }
  }

  /**
   * Returns <code>true</code> if this chat transport supports sms messaging, otherwise returns
   * <code>false</code>.
   *
   * @return <code>true</code> if this chat transport supports sms messaging, otherwise returns
   *     <code>false</code>.
   */
  public boolean allowsSmsMessage() {
    // First try to ask the capabilities operation set if such is
    // available.
    OperationSetContactCapabilities capOpSet =
        getProtocolProvider().getOperationSet(OperationSetContactCapabilities.class);

    if (capOpSet != null) {
      if (capOpSet.getOperationSet(contact, OperationSetSmsMessaging.class) != null) {
        return true;
      }
    } else if (contact.getProtocolProvider().getOperationSet(OperationSetSmsMessaging.class)
        != null) return true;

    return false;
  }

  /**
   * Returns <code>true</code> if this chat transport supports typing notifications, otherwise
   * returns <code>false</code>.
   *
   * @return <code>true</code> if this chat transport supports typing notifications, otherwise
   *     returns <code>false</code>.
   */
  public boolean allowsTypingNotifications() {
    Object tnOpSet =
        contact.getProtocolProvider().getOperationSet(OperationSetTypingNotifications.class);

    if (tnOpSet != null) return true;
    else return false;
  }

  /**
   * Returns <code>true</code> if this chat transport supports file transfer, otherwise returns
   * <code>false</code>.
   *
   * @return <code>true</code> if this chat transport supports file transfer, otherwise returns
   *     <code>false</code>.
   */
  public boolean allowsFileTransfer() {
    Object ftOpSet = contact.getProtocolProvider().getOperationSet(OperationSetFileTransfer.class);

    if (ftOpSet != null) return true;
    else return false;
  }

  /**
   * Sends the given instant message through this chat transport, by specifying the mime type (html
   * or plain text).
   *
   * @param message The message to send.
   * @param mimeType The mime type of the message to send: text/html or text/plain.
   * @throws Exception if the send operation is interrupted
   */
  public void sendInstantMessage(String message, String mimeType) throws Exception {
    // If this chat transport does not support instant messaging we do
    // nothing here.
    if (!allowsInstantMessage()) return;

    OperationSetBasicInstantMessaging imOpSet =
        contact.getProtocolProvider().getOperationSet(OperationSetBasicInstantMessaging.class);

    Message msg;
    if (mimeType.equals(OperationSetBasicInstantMessaging.HTML_MIME_TYPE)
        && imOpSet.isContentTypeSupported(OperationSetBasicInstantMessaging.HTML_MIME_TYPE)) {
      msg =
          imOpSet.createMessage(
              message, OperationSetBasicInstantMessaging.HTML_MIME_TYPE, "utf-8", "");
    } else {
      msg = imOpSet.createMessage(message);
    }

    if (contactResource != null) imOpSet.sendInstantMessage(contact, contactResource, msg);
    else imOpSet.sendInstantMessage(contact, ContactResource.BASE_RESOURCE, msg);
  }

  /**
   * Sends <tt>message</tt> as a message correction through this transport, specifying the mime type
   * (html or plain text) and the id of the message to replace.
   *
   * @param message The message to send.
   * @param mimeType The mime type of the message to send: text/html or text/plain.
   * @param correctedMessageUID The ID of the message being corrected by this message.
   */
  public void correctInstantMessage(String message, String mimeType, String correctedMessageUID) {
    if (!allowsMessageCorrections()) {
      return;
    }

    OperationSetMessageCorrection mcOpSet =
        contact.getProtocolProvider().getOperationSet(OperationSetMessageCorrection.class);

    Message msg;
    if (mimeType.equals(OperationSetBasicInstantMessaging.HTML_MIME_TYPE)
        && mcOpSet.isContentTypeSupported(OperationSetBasicInstantMessaging.HTML_MIME_TYPE)) {
      msg =
          mcOpSet.createMessage(
              message, OperationSetBasicInstantMessaging.HTML_MIME_TYPE, "utf-8", "");
    } else {
      msg = mcOpSet.createMessage(message);
    }

    mcOpSet.correctMessage(contact, contactResource, msg, correctedMessageUID);
  }

  /**
   * Determines whether this chat transport supports the supplied content type
   *
   * @param contentType the type we want to check
   * @return <tt>true</tt> if the chat transport supports it and <tt>false</tt> otherwise.
   */
  public boolean isContentTypeSupported(String contentType) {
    OperationSetBasicInstantMessaging imOpSet =
        contact.getProtocolProvider().getOperationSet(OperationSetBasicInstantMessaging.class);

    if (imOpSet != null) return imOpSet.isContentTypeSupported(contentType);
    else return false;
  }

  /**
   * Sends the given sms message trough this chat transport.
   *
   * @param phoneNumber phone number of the destination
   * @param messageText The message to send.
   * @throws Exception if the send operation is interrupted
   */
  public void sendSmsMessage(String phoneNumber, String messageText) throws Exception {
    // If this chat transport does not support sms messaging we do
    // nothing here.
    if (!allowsSmsMessage()) return;

    SMSManager.sendSMS(contact.getProtocolProvider(), phoneNumber, messageText);
  }

  /**
   * Whether a dialog need to be opened so the user can enter the destination number.
   *
   * @return <tt>true</tt> if dialog needs to be open.
   */
  public boolean askForSMSNumber() {
    // If this chat transport does not support sms messaging we do
    // nothing here.
    if (!allowsSmsMessage()) return false;

    OperationSetSmsMessaging smsOpSet =
        contact.getProtocolProvider().getOperationSet(OperationSetSmsMessaging.class);

    return smsOpSet.askForNumber(contact);
  }

  /**
   * Sends the given sms message trough this chat transport.
   *
   * @param message the message to send
   * @throws Exception if the send operation is interrupted
   */
  public void sendSmsMessage(String message) throws Exception {
    // If this chat transport does not support sms messaging we do
    // nothing here.
    if (!allowsSmsMessage()) return;

    SMSManager.sendSMS(contact, message);
  }

  /**
   * Sends a typing notification state.
   *
   * @param typingState the typing notification state to send
   * @return the result of this operation. One of the TYPING_NOTIFICATION_XXX constants defined in
   *     this class
   */
  public int sendTypingNotification(int typingState) {
    // If this chat transport does not support sms messaging we do
    // nothing here.
    if (!allowsTypingNotifications()) return -1;

    ProtocolProviderService protocolProvider = contact.getProtocolProvider();
    OperationSetTypingNotifications tnOperationSet =
        protocolProvider.getOperationSet(OperationSetTypingNotifications.class);

    // if protocol is not registered or contact is offline don't
    // try to send typing notifications
    if (protocolProvider.isRegistered()
        && contact.getPresenceStatus().getStatus() >= PresenceStatus.ONLINE_THRESHOLD) {
      try {
        tnOperationSet.sendTypingNotification(contact, typingState);

        return ChatPanel.TYPING_NOTIFICATION_SUCCESSFULLY_SENT;
      } catch (Exception ex) {
        logger.error("Failed to send typing notifications.", ex);

        return ChatPanel.TYPING_NOTIFICATION_SEND_FAILED;
      }
    }

    return ChatPanel.TYPING_NOTIFICATION_SEND_FAILED;
  }

  /**
   * Sends the given file through this chat transport file transfer operation set.
   *
   * @param file the file to send
   * @return the <tt>FileTransfer</tt> object charged to transfer the file
   * @throws Exception if anything goes wrong
   */
  public FileTransfer sendFile(File file) throws Exception {
    // If this chat transport does not support instant messaging we do
    // nothing here.
    if (!allowsFileTransfer()) return null;

    OperationSetFileTransfer ftOpSet =
        contact.getProtocolProvider().getOperationSet(OperationSetFileTransfer.class);

    if (FileUtils.isImage(file.getName())) {
      // Create a thumbnailed file if possible.
      OperationSetThumbnailedFileFactory tfOpSet =
          contact.getProtocolProvider().getOperationSet(OperationSetThumbnailedFileFactory.class);

      if (tfOpSet != null) {
        byte[] thumbnail = getFileThumbnail(file);

        if (thumbnail != null && thumbnail.length > 0) {
          file =
              tfOpSet.createFileWithThumbnail(
                  file, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, "image/png", thumbnail);
        }
      }
    }
    return ftOpSet.sendFile(contact, file);
  }

  /**
   * Returns the maximum file length supported by the protocol in bytes.
   *
   * @return the file length that is supported.
   */
  public long getMaximumFileLength() {
    OperationSetFileTransfer ftOpSet =
        contact.getProtocolProvider().getOperationSet(OperationSetFileTransfer.class);

    return ftOpSet.getMaximumFileLength();
  }

  public void inviteChatContact(String contactAddress, String reason) {}

  /**
   * Returns the parent session of this chat transport. A <tt>ChatSession</tt> could contain more
   * than one transports.
   *
   * @return the parent session of this chat transport
   */
  public ChatSession getParentChatSession() {
    return parentChatSession;
  }

  /**
   * Adds an SMS message listener to this chat transport.
   *
   * @param l The message listener to add.
   */
  public void addSmsMessageListener(MessageListener l) {
    // If this chat transport does not support sms messaging we do
    // nothing here.
    if (!allowsSmsMessage()) return;

    OperationSetSmsMessaging smsOpSet =
        contact.getProtocolProvider().getOperationSet(OperationSetSmsMessaging.class);

    smsOpSet.addMessageListener(l);
  }

  /**
   * Adds an instant message listener to this chat transport.
   *
   * @param l The message listener to add.
   */
  public void addInstantMessageListener(MessageListener l) {
    // If this chat transport does not support instant messaging we do
    // nothing here.
    if (!allowsInstantMessage()) return;

    OperationSetBasicInstantMessaging imOpSet =
        contact.getProtocolProvider().getOperationSet(OperationSetBasicInstantMessaging.class);

    imOpSet.addMessageListener(l);
  }

  /**
   * Removes the given sms message listener from this chat transport.
   *
   * @param l The message listener to remove.
   */
  public void removeSmsMessageListener(MessageListener l) {
    // If this chat transport does not support sms messaging we do
    // nothing here.
    if (!allowsSmsMessage()) return;

    OperationSetSmsMessaging smsOpSet =
        contact.getProtocolProvider().getOperationSet(OperationSetSmsMessaging.class);

    smsOpSet.removeMessageListener(l);
  }

  /**
   * Removes the instant message listener from this chat transport.
   *
   * @param l The message listener to remove.
   */
  public void removeInstantMessageListener(MessageListener l) {
    // If this chat transport does not support instant messaging we do
    // nothing here.
    if (!allowsInstantMessage()) return;

    OperationSetBasicInstantMessaging imOpSet =
        contact.getProtocolProvider().getOperationSet(OperationSetBasicInstantMessaging.class);

    imOpSet.removeMessageListener(l);
  }

  /**
   * Indicates that a contact has changed its status.
   *
   * @param evt The presence event containing information about the contact status change.
   */
  public void contactPresenceStatusChanged(ContactPresenceStatusChangeEvent evt) {
    if (evt.getSourceContact().equals(contact)
        && !evt.getOldStatus().equals(evt.getNewStatus())
        && contactResource == null) // If the contact source is set then the
    // status will be updated from the
    // MetaContactChatSession.
    {
      this.updateContactStatus();
    }
  }

  /** Updates the status of this contact with the new given status. */
  private void updateContactStatus() {
    // Update the status of the given contact in the "send via" selector
    // box.
    parentChatSession.getChatSessionRenderer().updateChatTransportStatus(this);
  }

  /** Removes all previously added listeners. */
  public void dispose() {
    if (presenceOpSet != null) presenceOpSet.removeContactPresenceStatusListener(this);
  }

  /**
   * Returns the descriptor of this chat transport.
   *
   * @return the descriptor of this chat transport
   */
  public Object getDescriptor() {
    return contact;
  }

  /**
   * Sets the icon for the given file.
   *
   * @param file the file to set an icon for
   * @return the byte array containing the thumbnail
   */
  private byte[] getFileThumbnail(File file) {
    byte[] bytes = null;
    if (FileUtils.isImage(file.getName())) {
      try {
        ImageIcon image = new ImageIcon(file.toURI().toURL());
        int width = image.getIconWidth();
        int height = image.getIconHeight();

        if (width > THUMBNAIL_WIDTH) width = THUMBNAIL_WIDTH;
        if (height > THUMBNAIL_HEIGHT) height = THUMBNAIL_HEIGHT;

        bytes = ImageUtils.getScaledInstanceInBytes(image.getImage(), width, height);
      } catch (MalformedURLException e) {
        if (logger.isDebugEnabled()) logger.debug("Could not locate image.", e);
      }
    }
    return bytes;
  }
}
/**
 * Whiteboard session manager.
 *
 * @author Julien Waechter
 */
public class WhiteboardSessionManager implements WhiteboardObjectListener {
  private static final Logger logger = Logger.getLogger(WhiteboardSessionManager.class);

  /** A protocol provider map. */
  private Map protocolProviderTable = new LinkedHashMap();

  /** The default start WhiteboardSession. */
  private WhiteboardSession wbTmpSession;

  /** List active WhitboarFrame started. */
  private Vector wbFrames = new Vector();

  /** List active WhitboarSession started. */
  private Vector wbSessions;

  private OperationSetWhiteboarding opSetWb;

  public WhiteboardSessionManager() {
    if (WhiteboardActivator.getWhiteboardOperationSets() == null) return;

    Iterator opSets = WhiteboardActivator.getWhiteboardOperationSets().iterator();

    while (opSets.hasNext()) {
      OperationSetWhiteboarding whiteboardOpSet = (OperationSetWhiteboarding) opSets.next();

      whiteboardOpSet.addInvitationListener(new InvitationListener());
      whiteboardOpSet.addPresenceListener(new PresenceListener());
    }
  }

  /**
   * Initialize (a new) Whiteboard with contact
   *
   * @param contact Contact used to init whiteboard
   */
  public void initWhiteboard(final Contact contact) {
    opSetWb =
        (OperationSetWhiteboarding)
            contact.getProtocolProvider().getOperationSet(OperationSetWhiteboarding.class);

    if (opSetWb == null) {
      logger.info("Contact does not support whiteboarding");
      return;
    }

    WhiteboardFrame wbf = getWhiteboardFrame(contact);
    if (wbf != null) {
      wbf.setVisible(true);
      return;
    }

    new Thread() {
      public void run() {
        try {
          WhiteboardSession wbSession =
              opSetWb.createWhiteboardSession(contact.getDisplayName(), null);

          WhiteboardFrame wbFrame = new WhiteboardFrame(WhiteboardSessionManager.this, wbSession);

          wbFrames.add(wbFrame);
          wbFrame.setContact(contact);
          wbFrame.setVisible(true);

          wbSession.join();

          wbSession.invite(contact.getAddress());

        } catch (OperationFailedException e) {
          logger.error("Creating a whiteboard session failed.", e);
        } catch (OperationNotSupportedException e) {
          logger.error("Do not support create of whiteboard session", e);
        }
      }
    }.start();
  }

  /**
   * Construct (with WhiteboardSession) and send a WhiteboardObject to a contact.
   *
   * @param wbSession the white-board session, to which the object would be send
   * @param ws WhiteboardShape to convert and send
   * @param c contact
   * @return WhiteboardObject sent
   */
  public WhiteboardObject sendWhiteboardObject(WhiteboardSession wbSession, WhiteboardShape ws)
      throws OperationFailedException {
    Vector supportedWBO = new Vector(Arrays.asList(wbSession.getSupportedWhiteboardObjects()));

    if (ws instanceof WhiteboardObjectPath) {
      if (!supportedWBO.contains(WhiteboardObjectPath.NAME)) return null;
      WhiteboardObjectPath obj =
          (WhiteboardObjectPath) wbSession.createWhiteboardObject(WhiteboardObjectPath.NAME);
      ws.setID(obj.getID());
      obj.setPoints(((WhiteboardObjectPath) ws).getPoints());
      obj.setColor(ws.getColor());
      obj.setThickness(ws.getThickness());
      wbSession.sendWhiteboardObject(obj);
      return obj;
    } else if (ws instanceof WhiteboardObjectPolyLine) {
      if (!supportedWBO.contains(WhiteboardObjectPolyLine.NAME)) return null;
      WhiteboardObjectPolyLine obj =
          (WhiteboardObjectPolyLine)
              wbSession.createWhiteboardObject(WhiteboardObjectPolyLine.NAME);
      ws.setID(obj.getID());
      obj.setPoints(((WhiteboardObjectPolyLine) ws).getPoints());
      obj.setColor(ws.getColor());
      obj.setThickness(ws.getThickness());
      wbSession.sendWhiteboardObject(obj);
      return obj;
    } else if (ws instanceof WhiteboardObjectPolygon) {
      if (!supportedWBO.contains(WhiteboardObjectPolygon.NAME)) return null;
      WhiteboardObjectPolygon obj =
          (WhiteboardObjectPolygon) wbSession.createWhiteboardObject(WhiteboardObjectPolygon.NAME);
      ws.setID(obj.getID());
      obj.setPoints(((WhiteboardObjectPolygon) ws).getPoints());
      obj.setBackgroundColor(((WhiteboardObjectPolygon) ws).getBackgroundColor());
      obj.setFill(((WhiteboardObjectPolygon) ws).isFill());
      obj.setColor(ws.getColor());
      obj.setThickness(ws.getThickness());
      wbSession.sendWhiteboardObject(obj);
      return obj;
    } else if (ws instanceof WhiteboardObjectLine) {
      if (!supportedWBO.contains(WhiteboardObjectLine.NAME)) return null;
      WhiteboardObjectLine obj =
          (WhiteboardObjectLine) wbSession.createWhiteboardObject(WhiteboardObjectLine.NAME);
      ws.setID(obj.getID());
      obj.setWhiteboardPointStart(((WhiteboardObjectLine) ws).getWhiteboardPointStart());
      obj.setWhiteboardPointEnd(((WhiteboardObjectLine) ws).getWhiteboardPointEnd());
      obj.setColor(ws.getColor());
      obj.setThickness(ws.getThickness());
      wbSession.sendWhiteboardObject(obj);
      return obj;
    } else if (ws instanceof WhiteboardObjectRect) {
      if (!supportedWBO.contains(WhiteboardObjectRect.NAME)) return null;
      WhiteboardObjectRect obj =
          (WhiteboardObjectRect) wbSession.createWhiteboardObject(WhiteboardObjectRect.NAME);
      ws.setID(obj.getID());
      obj.setFill(((WhiteboardObjectRect) ws).isFill());
      obj.setHeight(((WhiteboardObjectRect) ws).getHeight());
      obj.setWhiteboardPoint(((WhiteboardObjectRect) ws).getWhiteboardPoint());
      obj.setWidth((((WhiteboardObjectRect) ws)).getWidth());
      obj.setColor(ws.getColor());
      obj.setThickness(ws.getThickness());
      wbSession.sendWhiteboardObject(obj);
      return obj;
    } else if (ws instanceof WhiteboardObjectCircle) {
      if (!supportedWBO.contains(WhiteboardObjectCircle.NAME)) return null;
      WhiteboardObjectCircle obj =
          (WhiteboardObjectCircle) wbSession.createWhiteboardObject(WhiteboardObjectCircle.NAME);
      ws.setID(obj.getID());
      obj.setFill(((WhiteboardObjectCircle) ws).isFill());
      obj.setRadius(((WhiteboardObjectCircle) ws).getRadius());
      obj.setWhiteboardPoint(((WhiteboardObjectCircle) ws).getWhiteboardPoint());
      obj.setBackgroundColor((((WhiteboardObjectCircle) ws)).getBackgroundColor());
      obj.setColor(ws.getColor());
      obj.setThickness(ws.getThickness());
      wbSession.sendWhiteboardObject(obj);
      return obj;
    } else if (ws instanceof WhiteboardObjectText) {
      if (!supportedWBO.contains(WhiteboardObjectText.NAME)) return null;
      WhiteboardObjectText obj =
          (WhiteboardObjectText) wbSession.createWhiteboardObject(WhiteboardObjectText.NAME);
      ws.setID(obj.getID());
      obj.setFontName(((WhiteboardObjectText) ws).getFontName());
      obj.setFontSize(((WhiteboardObjectText) ws).getFontSize());
      obj.setText(((WhiteboardObjectText) ws).getText());
      obj.setWhiteboardPoint(((WhiteboardObjectText) ws).getWhiteboardPoint());
      obj.setColor(ws.getColor());
      obj.setThickness(ws.getThickness());
      wbSession.sendWhiteboardObject(obj);
      return obj;
    } else if (ws instanceof WhiteboardObjectImage) {
      if (!supportedWBO.contains(WhiteboardObjectImage.NAME)) return null;
      WhiteboardObjectImage obj =
          (WhiteboardObjectImage) wbSession.createWhiteboardObject(WhiteboardObjectImage.NAME);
      ws.setID(obj.getID());
      obj.setBackgroundImage(((WhiteboardObjectImage) ws).getBackgroundImage());
      obj.setHeight(((WhiteboardObjectImage) ws).getHeight());
      obj.setWhiteboardPoint(((WhiteboardObjectImage) ws).getWhiteboardPoint());
      obj.setWidth(((WhiteboardObjectImage) ws).getWidth());
      obj.setColor(ws.getColor());
      obj.setThickness(ws.getThickness());
      wbSession.sendWhiteboardObject(obj);

      return obj;
    }

    return null;
  }

  /**
   * Moves a <tt>WhiteboardShape</tt> from from one point to another on the board.
   *
   * @param wbSession the white-board session, to which the moved object belongs
   * @param ws the shape to move
   */
  public void moveWhiteboardObject(WhiteboardSession wbSession, WhiteboardShape ws) {
    try {
      wbSession.moveWhiteboardObject(ws);
    } catch (OperationFailedException ex) {
      ex.printStackTrace();
    }
  }

  /**
   * Deletes a <tt>WhiteboardShape</tt> from the white-board.
   *
   * @param wbSession the white-board session, to which the object belongs
   * @param ws the shape to delete
   */
  public void deleteWhiteboardObject(WhiteboardSession wbSession, WhiteboardShape ws) {
    try {
      wbSession.deleteWhiteboardObject(ws);
    } catch (OperationFailedException ex) {
      ex.printStackTrace();
    }
  }

  /**
   * Called when a modified <tt>WhiteboardObject</tt> has been received.
   *
   * @param evt the <tt>WhiteboardObjectReceivedEvent</tt> containing the modified whiteboardObject,
   *     its sender and other details.
   */
  public void whiteboardObjecModified(WhiteboardObjectModifiedEvent evt) {
    WhiteboardFrame wbf = getWhiteboardFrame(evt.getSourceWhiteboardSession());

    if (wbf == null) return;
    wbf.setVisible(true);
    WhiteboardObject wbo = evt.getSourceWhiteboardObject();
    wbf.receiveWhiteboardObject(wbo);
  }

  /**
   * Called when a new incoming <tt>WhiteboardObject</tt> has been received.
   *
   * @param evt the <tt>WhiteboardObjectReceivedEvent</tt> containing the newly received
   *     WhiteboardObject, its sender and other details.
   */
  public void whiteboardObjectReceived(WhiteboardObjectReceivedEvent evt) {
    /*
     * There are 2 cases when a message is received:
     * - an existing session
     * - or a new session
     */
    WhiteboardFrame wbFrame = getWhiteboardFrame(evt.getSourceWhiteboardSession());

    if (wbFrame == null) {
      logger.debug("New WBParticipant" + evt.getSourceContact().getDisplayName());

      wbFrame = new WhiteboardFrame(this, evt.getSourceWhiteboardSession());

      wbFrames.add(wbFrame);
    }

    wbFrame.setVisible(true);
    WhiteboardObject wbObject = evt.getSourceWhiteboardObject();
    wbFrame.receiveWhiteboardObject(wbObject);
  }

  /**
   * Called when the underlying implementation has received an indication that a WhiteboardObject,
   * sent earlier has been successfully received by the destination.
   *
   * @param evt the WhiteboardObjectDeliveredEvent containing the id of the WhiteboardObject that
   *     has caused the event.
   */
  public void whiteboardObjectDelivered(WhiteboardObjectDeliveredEvent evt) {
    logger.debug(
        "WBObjectDeliveredEvent: The following object: "
            + evt.getSourceWhiteboardObject()
            + " has been delivered to "
            + evt.getDestinationContact().getDisplayName());
  }

  /**
   * Called to indicate that delivery of a WhiteboardObject sent earlier has failed. Reason code and
   * phrase are contained by the <tt>WhiteboardObjectDeliveryFailedEvent</tt>
   *
   * @param evt the <tt>WhiteboardObjectDeliveryFailedEvent</tt> containing the ID of the
   *     WhiteboardObject whose delivery has failed.
   */
  public void whiteboardObjectDeliveryFailed(WhiteboardObjectDeliveryFailedEvent evt) {
    String errorMessage = null;

    if (evt.getErrorCode() == WhiteboardObjectDeliveryFailedEvent.NETWORK_FAILURE) {
      errorMessage = "Network failure.";
    } else if (evt.getErrorCode()
        == WhiteboardObjectDeliveryFailedEvent.OFFLINE_MESSAGES_NOT_SUPPORTED) {
      errorMessage = "Offline messages aren't supported.";
    } else if (evt.getErrorCode() == WhiteboardObjectDeliveryFailedEvent.PROVIDER_NOT_REGISTERED) {
      errorMessage = "Protocol provider is not registered.";
    } else if (evt.getErrorCode() == WhiteboardObjectDeliveryFailedEvent.INTERNAL_ERROR) {
      errorMessage = "An internal error occured.";
    } else if (evt.getErrorCode() == WhiteboardObjectDeliveryFailedEvent.UNKNOWN_ERROR) {
      errorMessage = "An unknown error occured.";
    }

    String debugErrorMessage =
        "WBObjectDeliveryFailedEvent: The following object: "
            + evt.getSourceWhiteboardObject()
            + " has NOT been delivered to "
            + evt.getDestinationContact().getDisplayName()
            + " because of the following error: "
            + errorMessage;

    logger.debug(debugErrorMessage);

    WhiteboardActivator.getUiService()
        .getPopupDialog()
        .showMessagePopupDialog(errorMessage, "Error", PopupDialog.ERROR_MESSAGE);
  }

  /**
   * Returns the WhiteboardFrame associated with the Contact.
   *
   * @param c contact
   * @return WhiteboardFrame with the Contact or null (if nothing found)
   */
  private WhiteboardFrame getWhiteboardFrame(WhiteboardSession session) {
    WhiteboardFrame whiteboardFrame = null;

    for (int i = 0; i < wbFrames.size(); i++) {
      whiteboardFrame = (WhiteboardFrame) wbFrames.get(i);

      if (whiteboardFrame.getWhiteboardSession().equals(session)) return whiteboardFrame;
    }
    return null;
  }

  /**
   * Returns the WhiteboardFrame associated with the Contact.
   *
   * @param c contact
   * @return WhiteboardFrame with the Contact or null (if nothing found)
   */
  private WhiteboardFrame getWhiteboardFrame(Contact contact) {
    WhiteboardFrame whiteboardFrame = null;

    for (int i = 0; i < wbFrames.size(); i++) {
      whiteboardFrame = (WhiteboardFrame) wbFrames.get(i);

      if (whiteboardFrame.getContact() != null && whiteboardFrame.getContact().equals(contact))
        return whiteboardFrame;
    }
    return null;
  }

  /**
   * Called when a deleted <tt>WhiteboardObject</tt> has been received.
   *
   * @param evt the <tt>WhiteboardObjectDeletedEvent</tt> containing the identification of the
   *     deleted WhiteboardObject, its sender and other details.
   */
  public void whiteboardObjectDeleted(WhiteboardObjectDeletedEvent evt) {
    WhiteboardFrame wbf = getWhiteboardFrame(evt.getSourceWhiteboardSession());

    if (wbf == null) {
      return;
    }

    wbf.setVisible(true);
    String id = evt.getId();
    wbf.receiveDeleteWhiteboardObject(id);
  }

  /**
   * Listens for <tt>WhiteboardInvitationReceivedEvent</tt>s and shows a dialog to the user, where
   * she could accept, reject or ignore an incoming invitation.
   */
  private class InvitationListener implements WhiteboardInvitationListener {
    public void invitationReceived(WhiteboardInvitationReceivedEvent evt) {
      OperationSetWhiteboarding whiteboardOpSet = evt.getSourceOperationSet();

      InvitationReceivedDialog dialog =
          new InvitationReceivedDialog(
              WhiteboardSessionManager.this, whiteboardOpSet, evt.getInvitation());

      dialog.pack();

      dialog.setLocation(
          Toolkit.getDefaultToolkit().getScreenSize().width / 2 - dialog.getWidth() / 2,
          Toolkit.getDefaultToolkit().getScreenSize().height / 2 - dialog.getHeight() / 2);

      dialog.setVisible(true);
    }
  }

  /**
   * Called to accept an incoming invitation. Adds the invitation chat room to the list of chat
   * rooms and joins it.
   *
   * @param invitation the invitation to accept.
   */
  public void acceptInvitation(WhiteboardInvitation invitation) {
    WhiteboardSession whiteboard = invitation.getTargetWhiteboard();

    byte[] password = invitation.getWhiteboardPassword();

    try {
      if (password == null) whiteboard.join();
      else whiteboard.join(password);
    } catch (OperationFailedException e) {
      WhiteboardActivator.getUiService()
          .getPopupDialog()
          .showMessagePopupDialog(
              Resources.getString(
                  "failedToJoinWhiteboard", new String[] {whiteboard.getWhiteboardID()}),
              Resources.getString("error"),
              PopupDialog.ERROR_MESSAGE);

      logger.error("Failed to join whiteboard: " + whiteboard.getWhiteboardID(), e);
    }
  }

  /**
   * Rejects the given invitation with the specified reason.
   *
   * @param whiteboardOpSet the operation set to use for rejecting the invitation
   * @param invitation the invitation to reject
   * @param reason the reason for the rejection
   */
  public void rejectInvitation(
      OperationSetWhiteboarding whiteboardOpSet, WhiteboardInvitation invitation, String reason) {
    whiteboardOpSet.rejectInvitation(invitation, reason);
  }

  /** Listens for presence events. */
  private class PresenceListener implements WhiteboardSessionPresenceListener {
    /**
     * Implements the <tt>WhiteboardSessionPresenceListener .whiteboardSessionPresenceChanged</tt>
     * method.
     */
    public void whiteboardSessionPresenceChanged(WhiteboardSessionPresenceChangeEvent evt) {
      WhiteboardSession whiteboardSession = evt.getWhiteboardSession();

      if (evt.getEventType().equals(WhiteboardSessionPresenceChangeEvent.LOCAL_USER_JOINED)) {
        whiteboardSession.addWhiteboardObjectListener(WhiteboardSessionManager.this);

        WhiteboardFrame frame = getWhiteboardFrame(evt.getWhiteboardSession());

        if (frame == null) {
          frame = new WhiteboardFrame(WhiteboardSessionManager.this, whiteboardSession);

          frame.setVisible(true);
          wbFrames.add(frame);
        }
      } else if (evt.getEventType()
          .equals(WhiteboardSessionPresenceChangeEvent.LOCAL_USER_JOIN_FAILED)) {
        WhiteboardActivator.getUiService()
            .getPopupDialog()
            .showMessagePopupDialog(
                Resources.getString(
                        "failedToJoinWhiteboard",
                        new String[] {whiteboardSession.getWhiteboardID()})
                    + evt.getReason(),
                Resources.getString("error"),
                PopupDialog.ERROR_MESSAGE);
      } else if (evt.getEventType().equals(WhiteboardSessionPresenceChangeEvent.LOCAL_USER_LEFT)) {
        WhiteboardFrame frame = getWhiteboardFrame(whiteboardSession);

        if (frame == null) return;

        wbFrames.remove(frame);
        frame.dispose();
        whiteboardSession.removeWhiteboardObjectListener(WhiteboardSessionManager.this);
      } else if (evt.getEventType()
          .equals(WhiteboardSessionPresenceChangeEvent.LOCAL_USER_KICKED)) {

      } else if (evt.getEventType()
          .equals(WhiteboardSessionPresenceChangeEvent.LOCAL_USER_DROPPED)) {

      }
    }
  }

  /**
   * Removes a white board frame.
   *
   * @param frame the frame to remove
   */
  public void removeWhiteboardWindow(WhiteboardFrame frame) {
    synchronized (wbFrames) {
      wbFrames.remove(frame);
    }
  }
}
예제 #14
0
/**
 * This class is the <tt>SipListener</tt> for all JAIN-SIP <tt>SipProvider</tt>s. It is in charge of
 * dispatching the received messages to the suitable <tt>ProtocolProviderServiceSipImpl</tt>s
 * registered with <tt>addSipListener</tt>. It also contains the JAIN-SIP pieces which are common
 * between all <tt>ProtocolProviderServiceSipImpl</tt>s (namely 1 <tt>SipStack</tt>, 2
 * <tt>SipProvider</tt>s, 3 <tt>ListeningPoint</tt>s).
 *
 * @author Emil Ivov
 * @author Lubomir Marinov
 * @author Alan Kelly
 * @author Sebastien Mazy
 */
public class SipStackSharing implements SipListener, NetworkConfigurationChangeListener {
  /**
   * We set a custom parameter in the contact address for registrar accounts, so as to ease
   * dispatching of incoming requests in case several accounts have the same username in their
   * contact address, eg: sip:[email protected]:5060;transport=udp;registering_acc=example_com
   */
  public static final String CONTACT_ADDRESS_CUSTOM_PARAM_NAME = "registering_acc";

  /** Logger for this class. */
  private static final Logger logger = Logger.getLogger(SipStackSharing.class);

  /** Our SIP stack (provided by JAIN-SIP). */
  private final SipStack stack;

  /** The JAIN-SIP provider that we use for clear UDP/TCP. */
  private SipProvider clearJainSipProvider = null;
  /** The JAIN-SIP provider that we use for TLS. */
  private SipProvider secureJainSipProvider = null;

  /**
   * The candidate recipients to choose from when dispatching messages received from one the
   * JAIN-SIP <tt>SipProvider</tt>-s. for thread safety issues reasons, better iterate on a copy of
   * that set using <tt>getSipListeners()</tt>.
   */
  private final Set<ProtocolProviderServiceSipImpl> listeners =
      new HashSet<ProtocolProviderServiceSipImpl>();

  /** The property indicating the preferred UDP and TCP port to bind to for clear communications. */
  private static final String PREFERRED_CLEAR_PORT_PROPERTY_NAME =
      "net.java.sip.communicator.SIP_PREFERRED_CLEAR_PORT";

  /** The property indicating the preferred TLS (TCP) port to bind to for secure communications. */
  private static final String PREFERRED_SECURE_PORT_PROPERTY_NAME =
      "net.java.sip.communicator.SIP_PREFERRED_SECURE_PORT";

  /**
   * Constructor for this class. Creates the JAIN-SIP stack.
   *
   * @throws OperationFailedException if creating the stack fails.
   */
  SipStackSharing() throws OperationFailedException {
    // init of the stack
    try {
      SipFactory sipFactory = SipFactory.getInstance();
      sipFactory.setPathName("org.jitsi.gov.nist");

      Properties sipStackProperties = new SipStackProperties();

      // Create SipStack object
      this.stack = sipFactory.createSipStack(sipStackProperties);
      if (logger.isTraceEnabled()) logger.trace("Created stack: " + this.stack);

      // set our custom address resolver managing SRV records
      AddressResolverImpl addressResolver = new AddressResolverImpl();
      ((SIPTransactionStack) this.stack).setAddressResolver(addressResolver);

      SipActivator.getNetworkAddressManagerService().addNetworkConfigurationChangeListener(this);
    } catch (Exception ex) {
      logger.fatal("Failed to get SIP Factory.", ex);
      throw new OperationFailedException(
          "Failed to get SIP Factory", OperationFailedException.INTERNAL_ERROR, ex);
    }
  }

  /**
   * Adds this <tt>listener</tt> as a candidate recipient for the dispatching of new messages
   * received from the JAIN-SIP <tt>SipProvider</tt>s.
   *
   * @param listener a new possible target for the dispatching process.
   * @throws OperationFailedException if creating one of the underlying <tt>SipProvider</tt>s fails
   *     for whatever reason.
   */
  public void addSipListener(ProtocolProviderServiceSipImpl listener)
      throws OperationFailedException {
    synchronized (this.listeners) {
      if (this.listeners.size() == 0) startListening();
      this.listeners.add(listener);
      if (logger.isTraceEnabled()) logger.trace(this.listeners.size() + " listeners now");
    }
  }

  /**
   * This <tt>listener</tt> will no longer be a candidate recipient for the dispatching of new
   * messages received from the JAIN-SIP <tt>SipProvider</tt>s.
   *
   * @param listener possible target to remove for the dispatching process.
   */
  public void removeSipListener(ProtocolProviderServiceSipImpl listener) {
    synchronized (this.listeners) {
      this.listeners.remove(listener);

      int listenerCount = listeners.size();
      if (logger.isTraceEnabled()) logger.trace(listenerCount + " listeners left");
      if (listenerCount == 0) stopListening();
    }
  }

  /**
   * Returns a copy of the <tt>listeners</tt> (= candidate recipients) set.
   *
   * @return a copy of the <tt>listeners</tt> set.
   */
  private Set<ProtocolProviderServiceSipImpl> getSipListeners() {
    synchronized (this.listeners) {
      return new HashSet<ProtocolProviderServiceSipImpl>(this.listeners);
    }
  }

  /**
   * Returns the JAIN-SIP <tt>ListeningPoint</tt> associated to the given transport string.
   *
   * @param transport a string like "UDP", "TCP" or "TLS".
   * @return the LP associated to the given transport.
   */
  @SuppressWarnings("unchecked") // jain-sip legacy code
  public ListeningPoint getLP(String transport) {
    ListeningPoint lp;
    Iterator<ListeningPoint> it = this.stack.getListeningPoints();

    while (it.hasNext()) {
      lp = it.next();
      // FIXME: JAIN-SIP stack is not consistent with case
      // (reported upstream)
      if (lp.getTransport().toLowerCase().equals(transport.toLowerCase())) return lp;
    }

    throw new IllegalArgumentException("Invalid transport: " + transport);
  }

  /**
   * Put the stack in a state where it can receive data on three UDP/TCP ports (2 for clear
   * communication, 1 for TLS). That is to say create the related JAIN-SIP <tt>ListeningPoint</tt>s
   * and <tt>SipProvider</tt>s.
   *
   * @throws OperationFailedException if creating one of the underlying <tt>SipProvider</tt>s fails
   *     for whatever reason.
   */
  private void startListening() throws OperationFailedException {
    try {
      int bindRetriesValue = getBindRetriesValue();

      this.createProvider(this.getPreferredClearPort(), bindRetriesValue, false);
      this.createProvider(this.getPreferredSecurePort(), bindRetriesValue, true);
      this.stack.start();
      if (logger.isTraceEnabled()) logger.trace("started listening");
    } catch (Exception ex) {
      logger.error(
          "An unexpected error happened while creating the" + "SipProviders and ListeningPoints.");
      throw new OperationFailedException(
          "An unexpected error hapenned" + "while initializing the SIP stack",
          OperationFailedException.INTERNAL_ERROR,
          ex);
    }
  }

  /**
   * Attach JAIN-SIP <tt>SipProvider</tt> and <tt>ListeningPoint</tt> to the stack either for clear
   * communications or TLS. Clear UDP and TCP <tt>ListeningPoint</tt>s are not handled separately as
   * the former is a fallback for the latter (depending on the size of the data transmitted). Both
   * <tt>ListeningPoint</tt>s must be bound to the same address and port in order for the related
   * <tt>SipProvider</tt> to be created. If a UDP or TCP <tt>ListeningPoint</tt> cannot bind, retry
   * for both on another port.
   *
   * @param preferredPort which port to try first to bind.
   * @param retries how many times should we try to find a free port to bind
   * @param secure whether to create the TLS SipProvider. or the clear UDP/TCP one.
   * @throws TransportNotSupportedException in case we try to create a provider for a transport not
   *     currently supported by jain-sip
   * @throws InvalidArgumentException if we try binding to an illegal port (which we won't)
   * @throws ObjectInUseException if another <tt>SipProvider</tt> is already associated with this
   *     <tt>ListeningPoint</tt>.
   * @throws TransportAlreadySupportedException if there is already a ListeningPoint associated to
   *     this <tt>SipProvider</tt> with the same transport of the <tt>ListeningPoint</tt>.
   * @throws TooManyListenersException if we try to add a new <tt>SipListener</tt> with a
   *     <tt>SipProvider</tt> when one was already registered.
   */
  private void createProvider(int preferredPort, int retries, boolean secure)
      throws TransportNotSupportedException, InvalidArgumentException, ObjectInUseException,
          TransportAlreadySupportedException, TooManyListenersException {
    String context = (secure ? "TLS: " : "clear UDP/TCP: ");

    if (retries < 0) {
      // very unlikely to happen with the default 50 retries
      logger.error(context + "couldn't find free ports to listen on.");
      return;
    }

    ListeningPoint tlsLP = null;
    ListeningPoint udpLP = null;
    ListeningPoint tcpLP = null;

    try {
      if (secure) {
        tlsLP =
            this.stack.createListeningPoint(
                NetworkUtils.IN_ADDR_ANY, preferredPort, ListeningPoint.TLS);
        if (logger.isTraceEnabled()) logger.trace("TLS secure ListeningPoint has been created.");

        this.secureJainSipProvider = this.stack.createSipProvider(tlsLP);
        this.secureJainSipProvider.addSipListener(this);
      } else {
        udpLP =
            this.stack.createListeningPoint(
                NetworkUtils.IN_ADDR_ANY, preferredPort, ListeningPoint.UDP);
        tcpLP =
            this.stack.createListeningPoint(
                NetworkUtils.IN_ADDR_ANY, preferredPort, ListeningPoint.TCP);
        if (logger.isTraceEnabled())
          logger.trace("UDP and TCP clear ListeningPoints have " + "been created.");

        this.clearJainSipProvider = this.stack.createSipProvider(udpLP);
        this.clearJainSipProvider.addListeningPoint(tcpLP);
        this.clearJainSipProvider.addSipListener(this);
      }

      if (logger.isTraceEnabled()) logger.trace(context + "SipProvider has been created.");
    } catch (InvalidArgumentException ex) {
      // makes sure we didn't leave an open listener
      // as both UDP and TCP listener have to bind to the same port
      if (tlsLP != null) this.stack.deleteListeningPoint(tlsLP);
      if (udpLP != null) this.stack.deleteListeningPoint(udpLP);
      if (tcpLP != null) this.stack.deleteListeningPoint(tcpLP);

      // FIXME: "Address already in use" is not working
      // as ex.getMessage() displays in the locale language in SC
      // (getMessage() is always supposed to be English though)
      // this should be a temporary workaround
      // if (ex.getMessage().indexOf("Address already in use") != -1)
      // another software is probably using the port
      if (ex.getCause() instanceof java.io.IOException) {
        if (logger.isDebugEnabled())
          logger.debug("Port " + preferredPort + " seems in use for either TCP or UDP.");

        // tries again on a new random port
        int currentlyTriedPort = NetworkUtils.getRandomPortNumber();
        if (logger.isDebugEnabled()) logger.debug("Retrying bind on port " + currentlyTriedPort);
        this.createProvider(currentlyTriedPort, retries - 1, secure);
      } else throw ex;
    }
  }

  /**
   * Put the JAIN-SIP stack in a state where it cannot receive any data and frees the network ports
   * used. That is to say remove JAIN-SIP <tt>ListeningPoint</tt>s and <tt>SipProvider</tt>s.
   */
  @SuppressWarnings("unchecked") // jain-sip legacy code
  private void stopListening() {
    try {
      this.secureJainSipProvider.removeSipListener(this);
      this.stack.deleteSipProvider(this.secureJainSipProvider);
      this.secureJainSipProvider = null;
      this.clearJainSipProvider.removeSipListener(this);
      this.stack.deleteSipProvider(this.clearJainSipProvider);
      this.clearJainSipProvider = null;

      Iterator<ListeningPoint> it = this.stack.getListeningPoints();
      Vector<ListeningPoint> lpointsToRemove = new Vector<ListeningPoint>();
      while (it.hasNext()) {
        lpointsToRemove.add(it.next());
      }

      it = lpointsToRemove.iterator();
      while (it.hasNext()) {
        this.stack.deleteListeningPoint(it.next());
      }

      this.stack.stop();
      if (logger.isTraceEnabled()) logger.trace("stopped listening");
    } catch (ObjectInUseException ex) {
      logger.fatal("Failed to stop listening", ex);
    }
  }

  /**
   * Returns the JAIN-SIP <tt>SipProvider</tt> in charge of this <tt>transport</tt>.
   *
   * @param transport a <tt>String</tt> like "TCP", "UDP" or "TLS"
   * @return the corresponding <tt>SipProvider</tt>
   */
  public SipProvider getJainSipProvider(String transport) {
    SipProvider sp = null;
    if (transport.equalsIgnoreCase(ListeningPoint.UDP)
        || transport.equalsIgnoreCase(ListeningPoint.TCP)) sp = this.clearJainSipProvider;
    else if (transport.equalsIgnoreCase(ListeningPoint.TLS)) sp = this.secureJainSipProvider;

    if (sp == null) throw new IllegalArgumentException("invalid transport");
    return sp;
  }

  /**
   * Fetches the preferred UDP and TCP port for clear communications in the user preferences or
   * search is default value set in settings or fallback on a default value.
   *
   * @return the preferred network port for clear communications.
   */
  private int getPreferredClearPort() {

    int preferredPort =
        SipActivator.getConfigurationService().getInt(PREFERRED_CLEAR_PORT_PROPERTY_NAME, -1);

    if (preferredPort <= 1) {
      // check for default value
      preferredPort =
          SipActivator.getResources().getSettingsInt(PREFERRED_CLEAR_PORT_PROPERTY_NAME);
    }

    if (preferredPort <= 1) return ListeningPoint.PORT_5060;
    else return preferredPort;
  }

  /**
   * Fetches the preferred TLS (TCP) port for secure communications in the user preferences or
   * search is default value set in settings or fallback on a default value.
   *
   * @return the preferred network port for secure communications.
   */
  private int getPreferredSecurePort() {
    int preferredPort =
        SipActivator.getConfigurationService().getInt(PREFERRED_SECURE_PORT_PROPERTY_NAME, -1);

    if (preferredPort <= 1) {
      // check for default value
      preferredPort =
          SipActivator.getResources().getSettingsInt(PREFERRED_SECURE_PORT_PROPERTY_NAME);
    }

    if (preferredPort <= 1) return ListeningPoint.PORT_5061;
    else return preferredPort;
  }

  /**
   * Fetches the number of times to retry when the binding of a JAIN-SIP <tt>ListeningPoint</tt>
   * fails. Looks in the user preferences or fallbacks on a default value.
   *
   * @return the number of times to retry a failed bind.
   */
  private int getBindRetriesValue() {
    return SipActivator.getConfigurationService()
        .getInt(
            ProtocolProviderService.BIND_RETRIES_PROPERTY_NAME,
            ProtocolProviderService.BIND_RETRIES_DEFAULT_VALUE);
  }

  /**
   * Dispatches the event received from a JAIN-SIP <tt>SipProvider</tt> to one of our "candidate
   * recipient" listeners.
   *
   * @param event the event received for a <tt>SipProvider</tt>.
   */
  public void processDialogTerminated(DialogTerminatedEvent event) {
    try {
      ProtocolProviderServiceSipImpl recipient =
          (ProtocolProviderServiceSipImpl)
              SipApplicationData.getApplicationData(
                  event.getDialog(), SipApplicationData.KEY_SERVICE);
      if (recipient == null) {
        logger.error(
            "Dialog wasn't marked, please report this to " + "*****@*****.**");
      } else {
        if (logger.isTraceEnabled()) logger.trace("service was found with dialog data");
        recipient.processDialogTerminated(event);
      }
    } catch (Throwable exc) {
      // any exception thrown within our code should be caught here
      // so that we could log it rather than interrupt stack activity with
      // it.
      this.logApplicationException(DialogTerminatedEvent.class, exc);
    }
  }

  /**
   * Dispatches the event received from a JAIN-SIP <tt>SipProvider</tt> to one of our "candidate
   * recipient" listeners.
   *
   * @param event the event received for a <tt>SipProvider</tt>.
   */
  public void processIOException(IOExceptionEvent event) {
    try {
      if (logger.isTraceEnabled()) logger.trace(event);

      // impossible to dispatch, log here
      if (logger.isDebugEnabled()) logger.debug("@todo implement processIOException()");
    } catch (Throwable exc) {
      // any exception thrown within our code should be caught here
      // so that we could log it rather than interrupt stack activity with
      // it.
      this.logApplicationException(DialogTerminatedEvent.class, exc);
    }
  }

  /**
   * Dispatches the event received from a JAIN-SIP <tt>SipProvider</tt> to one of our "candidate
   * recipient" listeners.
   *
   * @param event the event received for a <tt>SipProvider</tt>.
   */
  public void processRequest(RequestEvent event) {
    try {
      Request request = event.getRequest();
      if (logger.isTraceEnabled()) logger.trace("received request: " + request.getMethod());

      /*
       * Create the transaction if it doesn't exist yet. If it is a
       * dialog-creating request, the dialog will also be automatically
       * created by the stack.
       */
      if (event.getServerTransaction() == null) {
        try {
          // apply some hacks if needed on incoming request
          // to be compliant with some servers/clients
          // if needed stop further processing.
          if (applyNonConformanceHacks(event)) return;

          SipProvider source = (SipProvider) event.getSource();
          ServerTransaction transaction = source.getNewServerTransaction(request);

          /*
           * Update the event, otherwise getServerTransaction() and
           * getDialog() will still return their previous value.
           */
          event = new RequestEvent(source, transaction, transaction.getDialog(), request);
        } catch (SipException ex) {
          logger.error(
              "couldn't create transaction, please report "
                  + "this to [email protected]",
              ex);
        }
      }

      ProtocolProviderServiceSipImpl service = getServiceData(event.getServerTransaction());
      if (service != null) {
        service.processRequest(event);
      } else {
        service = findTargetFor(request);
        if (service == null) {
          logger.error("couldn't find a ProtocolProviderServiceSipImpl " + "to dispatch to");
          if (event.getServerTransaction() != null) event.getServerTransaction().terminate();
        } else {

          /*
           * Mark the dialog for the dispatching of later in-dialog
           * requests. If there is no dialog, we need to mark the
           * request to dispatch a possible timeout when sending the
           * response.
           */
          Object container = event.getDialog();
          if (container == null) container = request;
          SipApplicationData.setApplicationData(container, SipApplicationData.KEY_SERVICE, service);

          service.processRequest(event);
        }
      }
    } catch (Throwable exc) {

      /*
       * Any exception thrown within our code should be caught here so
       * that we could log it rather than interrupt stack activity with
       * it.
       */
      this.logApplicationException(DialogTerminatedEvent.class, exc);

      // Unfortunately, death can hardly be ignored.
      if (exc instanceof ThreadDeath) throw (ThreadDeath) exc;
    }
  }

  /**
   * Dispatches the event received from a JAIN-SIP <tt>SipProvider</tt> to one of our "candidate
   * recipient" listeners.
   *
   * @param event the event received for a <tt>SipProvider</tt>.
   */
  public void processResponse(ResponseEvent event) {
    try {
      // we don't have to accept the transaction since we
      // created the request
      ClientTransaction transaction = event.getClientTransaction();
      if (logger.isTraceEnabled())
        logger.trace(
            "received response: "
                + event.getResponse().getStatusCode()
                + " "
                + event.getResponse().getReasonPhrase());

      if (transaction == null) {
        logger.warn("Transaction is null, probably already expired!");
        return;
      }

      ProtocolProviderServiceSipImpl service = getServiceData(transaction);
      if (service != null) {
        // Mark the dialog for the dispatching of later in-dialog
        // responses. If there is no dialog then the initial request
        // sure is marked otherwise we won't have found the service with
        // getServiceData(). The request has to be marked in case we
        // receive one more response in an out-of-dialog transaction.
        if (event.getDialog() != null) {
          SipApplicationData.setApplicationData(
              event.getDialog(), SipApplicationData.KEY_SERVICE, service);
        }
        service.processResponse(event);
      } else {
        logger.error(
            "We received a response which "
                + "wasn't marked, please report this to "
                + "*****@*****.**");
      }
    } catch (Throwable exc) {
      // any exception thrown within our code should be caught here
      // so that we could log it rather than interrupt stack activity with
      // it.
      this.logApplicationException(DialogTerminatedEvent.class, exc);
    }
  }

  /**
   * Dispatches the event received from a JAIN-SIP <tt>SipProvider</tt> to one of our "candidate
   * recipient" listeners.
   *
   * @param event the event received for a <tt>SipProvider</tt>.
   */
  public void processTimeout(TimeoutEvent event) {
    try {
      Transaction transaction;
      if (event.isServerTransaction()) {
        transaction = event.getServerTransaction();
      } else {
        transaction = event.getClientTransaction();
      }

      ProtocolProviderServiceSipImpl recipient = getServiceData(transaction);
      if (recipient == null) {
        logger.error(
            "We received a timeout which wasn't "
                + "marked, please report this to "
                + "*****@*****.**");
      } else {
        recipient.processTimeout(event);
      }
    } catch (Throwable exc) {
      // any exception thrown within our code should be caught here
      // so that we could log it rather than interrupt stack activity with
      // it.
      this.logApplicationException(DialogTerminatedEvent.class, exc);
    }
  }

  /**
   * Dispatches the event received from a JAIN-SIP <tt>SipProvider</tt> to one of our "candidate
   * recipient" listeners.
   *
   * @param event the event received for a <tt>SipProvider</tt>.
   */
  public void processTransactionTerminated(TransactionTerminatedEvent event) {
    try {
      Transaction transaction;
      if (event.isServerTransaction()) transaction = event.getServerTransaction();
      else transaction = event.getClientTransaction();

      ProtocolProviderServiceSipImpl recipient = getServiceData(transaction);

      if (recipient == null) {
        logger.error(
            "We received a transaction terminated which wasn't"
                + " marked, please report this to"
                + " [email protected]");
      } else {
        recipient.processTransactionTerminated(event);
      }
    } catch (Throwable exc) {
      // any exception thrown within our code should be caught here
      // so that we could log it rather than interrupt stack activity with
      // it.
      this.logApplicationException(DialogTerminatedEvent.class, exc);
    }
  }

  /**
   * Find the <tt>ProtocolProviderServiceSipImpl</tt> (one of our "candidate recipient" listeners)
   * which this <tt>request</tt> should be dispatched to. The strategy is to look first at the
   * request URI, and then at the To field to find a matching candidate for dispatching. Note that
   * this method takes a <tt>Request</tt> as param, and not a <tt>ServerTransaction</tt>, because
   * sometimes <tt>RequestEvent</tt>s have no associated <tt>ServerTransaction</tt>.
   *
   * @param request the <tt>Request</tt> to find a recipient for.
   * @return a suitable <tt>ProtocolProviderServiceSipImpl</tt>.
   */
  private ProtocolProviderServiceSipImpl findTargetFor(Request request) {
    if (request == null) {
      logger.error("request shouldn't be null.");
      return null;
    }

    List<ProtocolProviderServiceSipImpl> currentListenersCopy =
        new ArrayList<ProtocolProviderServiceSipImpl>(this.getSipListeners());

    // Let's first narrow down candidate choice by comparing
    // addresses and ports (no point in delivering to a provider with a
    // non matching IP address  since they will reject it anyway).
    filterByAddress(currentListenersCopy, request);

    if (currentListenersCopy.size() == 0) {
      logger.error("no listeners");
      return null;
    }

    URI requestURI = request.getRequestURI();

    if (requestURI.isSipURI()) {
      String requestUser = ((SipURI) requestURI).getUser();

      List<ProtocolProviderServiceSipImpl> candidates =
          new ArrayList<ProtocolProviderServiceSipImpl>();

      // check if the Request-URI username is
      // one of ours usernames
      for (ProtocolProviderServiceSipImpl listener : currentListenersCopy) {
        String ourUserID = listener.getAccountID().getUserID();
        // logger.trace(ourUserID + " *** " + requestUser);
        if (ourUserID.equals(requestUser)) {
          if (logger.isTraceEnabled())
            logger.trace("suitable candidate found: " + listener.getAccountID());
          candidates.add(listener);
        }
      }

      // the perfect match
      // every other case is approximation
      if (candidates.size() == 1) {
        ProtocolProviderServiceSipImpl perfectMatch = candidates.get(0);

        if (logger.isTraceEnabled())
          logger.trace("Will dispatch to \"" + perfectMatch.getAccountID() + "\"");
        return perfectMatch;
      }

      // more than one account match
      if (candidates.size() > 1) {
        // check if a custom param exists in the contact
        // address (set for registrar accounts)
        for (ProtocolProviderServiceSipImpl candidate : candidates) {
          String hostValue =
              ((SipURI) requestURI).getParameter(SipStackSharing.CONTACT_ADDRESS_CUSTOM_PARAM_NAME);
          if (hostValue == null) continue;
          if (hostValue.equals(candidate.getContactAddressCustomParamValue())) {
            if (logger.isTraceEnabled())
              logger.trace(
                  "Will dispatch to \""
                      + candidate.getAccountID()
                      + "\" because "
                      + "\" the custom param was set");
            return candidate;
          }
        }

        // Past this point, our guess is not reliable. We try to find
        // the "least worst" match based on parameters like the To field

        // check if the To header field host part
        // matches any of our SIP hosts
        for (ProtocolProviderServiceSipImpl candidate : candidates) {
          URI fromURI = ((FromHeader) request.getHeader(FromHeader.NAME)).getAddress().getURI();
          if (fromURI.isSipURI() == false) continue;
          SipURI ourURI = (SipURI) candidate.getOurSipAddress((SipURI) fromURI).getURI();
          String ourHost = ourURI.getHost();

          URI toURI = ((ToHeader) request.getHeader(ToHeader.NAME)).getAddress().getURI();
          if (toURI.isSipURI() == false) continue;
          String toHost = ((SipURI) toURI).getHost();

          // logger.trace(toHost + "***" + ourHost);
          if (toHost.equals(ourHost)) {
            if (logger.isTraceEnabled())
              logger.trace(
                  "Will dispatch to \""
                      + candidate.getAccountID()
                      + "\" because "
                      + "host in the To: is the same as in our AOR");
            return candidate;
          }
        }

        // fallback on the first candidate
        ProtocolProviderServiceSipImpl target = candidates.iterator().next();
        logger.info(
            "Will randomly dispatch to \""
                + target.getAccountID()
                + "\" because there is ambiguity on the username from"
                + " the Request-URI");
        if (logger.isTraceEnabled()) logger.trace("\n" + request);
        return target;
      }

      // fallback on any account
      ProtocolProviderServiceSipImpl target = currentListenersCopy.iterator().next();
      if (logger.isDebugEnabled())
        logger.debug(
            "Will randomly dispatch to \""
                + target.getAccountID()
                + "\" because the username in the Request-URI "
                + "is unknown or empty");
      if (logger.isTraceEnabled()) logger.trace("\n" + request);
      return target;
    } else {
      logger.error("Request-URI is not a SIP URI, dropping");
    }
    return null;
  }

  /**
   * Removes from the specified list of candidates providers connected to a registrar that does not
   * match the IP address that we are receiving a request from.
   *
   * @param candidates the list of providers we've like to filter.
   * @param request the request that we are currently dispatching
   */
  private void filterByAddress(List<ProtocolProviderServiceSipImpl> candidates, Request request) {
    Iterator<ProtocolProviderServiceSipImpl> iterPP = candidates.iterator();
    while (iterPP.hasNext()) {
      ProtocolProviderServiceSipImpl candidate = iterPP.next();

      if (candidate.getRegistrarConnection() == null) {
        // RegistrarLess connections are ok
        continue;
      }

      if (!candidate.getRegistrarConnection().isRegistrarless()
          && !candidate.getRegistrarConnection().isRequestFromSameConnection(request)) {
        iterPP.remove();
      }
    }
  }

  /**
   * Retrieves and returns that ProtocolProviderService that this transaction belongs to, or
   * <tt>null</tt> if we couldn't associate it with a provider based on neither the request nor the
   * transaction itself.
   *
   * @param transaction the transaction that we'd like to determine a provider for.
   * @return a reference to the <tt>ProtocolProviderServiceSipImpl</tt> that <tt>transaction</tt>
   *     was associated with or <tt>null</tt> if we couldn't determine which one it is.
   */
  private ProtocolProviderServiceSipImpl getServiceData(Transaction transaction) {
    ProtocolProviderServiceSipImpl service =
        (ProtocolProviderServiceSipImpl)
            SipApplicationData.getApplicationData(
                transaction.getRequest(), SipApplicationData.KEY_SERVICE);

    if (service != null) {
      if (logger.isTraceEnabled()) logger.trace("service was found in request data");
      return service;
    }

    service =
        (ProtocolProviderServiceSipImpl)
            SipApplicationData.getApplicationData(
                transaction.getDialog(), SipApplicationData.KEY_SERVICE);
    if (service != null) {
      if (logger.isTraceEnabled()) logger.trace("service was found in dialog data");
    }

    return service;
  }

  /**
   * Logs exceptions that have occurred in the application while processing events originating from
   * the stack.
   *
   * @param eventClass the class of the jain-sip event that we were handling when the exception was
   *     thrown.
   * @param exc the exception that we need to log.
   */
  private void logApplicationException(Class<DialogTerminatedEvent> eventClass, Throwable exc) {
    String message = "An error occurred while processing event of type: " + eventClass.getName();

    logger.error(message, exc);
    if (logger.isDebugEnabled()) logger.debug(message, exc);
  }

  /**
   * Safely returns the transaction from the event if already exists. If not a new transaction is
   * created.
   *
   * @param event the request event
   * @return the server transaction
   * @throws javax.sip.TransactionAlreadyExistsException if transaction exists
   * @throws javax.sip.TransactionUnavailableException if unavailable
   */
  public static ServerTransaction getOrCreateServerTransaction(RequestEvent event)
      throws TransactionAlreadyExistsException, TransactionUnavailableException {
    ServerTransaction serverTransaction = event.getServerTransaction();

    if (serverTransaction == null) {
      SipProvider jainSipProvider = (SipProvider) event.getSource();

      serverTransaction = jainSipProvider.getNewServerTransaction(event.getRequest());
    }
    return serverTransaction;
  }

  /**
   * Returns a local address to use with the specified TCP destination. The method forces the
   * JAIN-SIP stack to create s and binds (if necessary) and return a socket connected to the
   * specified destination address and port and then return its local address.
   *
   * @param dst the destination address that the socket would need to connect to.
   * @param dstPort the port number that the connection would be established with.
   * @param localAddress the address that we would like to bind on (null for the "any" address).
   * @param transport the transport that will be used TCP ot TLS
   * @return the SocketAddress that this handler would use when connecting to the specified
   *     destination address and port.
   * @throws IOException if we fail binding the local socket
   */
  public java.net.InetSocketAddress getLocalAddressForDestination(
      java.net.InetAddress dst, int dstPort, java.net.InetAddress localAddress, String transport)
      throws IOException {
    if (ListeningPoint.TLS.equalsIgnoreCase(transport))
      return (java.net.InetSocketAddress)
          (((SipStackImpl) this.stack).getLocalAddressForTlsDst(dst, dstPort, localAddress));
    else
      return (java.net.InetSocketAddress)
          (((SipStackImpl) this.stack).getLocalAddressForTcpDst(dst, dstPort, localAddress, 0));
  }

  /**
   * Place to put some hacks if needed on incoming requests.
   *
   * @param event the incoming request event.
   * @return status <code>true</code> if we don't need to process this message, just discard it and
   *     <code>false</code> otherwise.
   */
  private boolean applyNonConformanceHacks(RequestEvent event) {
    Request request = event.getRequest();
    try {
      /*
       * Max-Forwards is required, yet there are UAs which do not
       * place it. SipProvider#getNewServerTransaction(Request)
       * will throw an exception in the case of a missing
       * Max-Forwards header and this method will eventually just
       * log it thus ignoring the whole event.
       */
      if (request.getHeader(MaxForwardsHeader.NAME) == null) {
        // it appears that some buggy providers do send requests
        // with no Max-Forwards headers, as we are at application level
        // and we know there will be no endless loops
        // there is no problem of adding headers and process normally
        // this messages
        MaxForwardsHeader maxForwards =
            SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70);
        request.setHeader(maxForwards);
      }
    } catch (Throwable ex) {
      logger.warn("Cannot apply incoming request modification!", ex);
    }

    try {
      // using asterisk voice mail initial notify for messages
      // is ok, but on the fly received messages their notify comes
      // without subscription-state, so we add it in order to be able to
      // process message.
      if (request.getMethod().equals(Request.NOTIFY)
          && request.getHeader(EventHeader.NAME) != null
          && ((EventHeader) request.getHeader(EventHeader.NAME))
              .getEventType()
              .equals(OperationSetMessageWaitingSipImpl.EVENT_PACKAGE)
          && request.getHeader(SubscriptionStateHeader.NAME) == null) {
        request.addHeader(
            new HeaderFactoryImpl().createSubscriptionStateHeader(SubscriptionStateHeader.ACTIVE));
      }
    } catch (Throwable ex) {
      logger.warn("Cannot apply incoming request modification!", ex);
    }

    try {
      // receiving notify message without subscription state
      // used for keep-alive pings, they have done their job
      // and are no more need. Skip processing them to avoid
      // filling logs with unneeded exceptions.
      if (request.getMethod().equals(Request.NOTIFY)
          && request.getHeader(SubscriptionStateHeader.NAME) == null) {
        return true;
      }
    } catch (Throwable ex) {
      logger.warn("Cannot apply incoming request modification!", ex);
    }

    return false;
  }

  /** List of currently waiting timers that will monitor the protocol provider */
  Map<String, TimerTask> resetListeningPointsTimers = new HashMap<String, TimerTask>();

  /**
   * Listens for network changes and if we have a down interface and we have a tcp/tls provider
   * which is staying for 20 seconds in unregistering state, it cannot unregister cause its using
   * the old address which is currently down, and we must recreate its listening points so it can
   * further reconnect.
   *
   * @param event the change event.
   */
  public void configurationChanged(ChangeEvent event) {
    if (event.isInitial()) return;

    if (event.getType() == ChangeEvent.ADDRESS_DOWN) {
      for (final ProtocolProviderServiceSipImpl pp : listeners) {
        if (pp.getRegistrarConnection().getTransport() != null
            && (pp.getRegistrarConnection().getTransport().equals(ListeningPoint.TCP)
                || pp.getRegistrarConnection().getTransport().equals(ListeningPoint.TLS))) {
          ResetListeningPoint reseter;
          synchronized (resetListeningPointsTimers) {
            // we do this only once for transport
            if (resetListeningPointsTimers.containsKey(pp.getRegistrarConnection().getTransport()))
              continue;

            reseter = new ResetListeningPoint(pp);
            resetListeningPointsTimers.put(pp.getRegistrarConnection().getTransport(), reseter);
          }
          pp.addRegistrationStateChangeListener(reseter);
        }
      }
    }
  }

  /**
   * If a tcp(tls) provider stays unregistering for a long time after connection changed most
   * probably it won't get registered after unregistering fails, cause underlying listening point
   * are conncted to wrong interfaces. So we will replace them.
   */
  private class ResetListeningPoint extends TimerTask implements RegistrationStateChangeListener {
    /** The time we wait before checking is the provider still unregistering. */
    private static final int TIME_FOR_PP_TO_UNREGISTER = 20000;

    /** The protocol provider we are checking. */
    private final ProtocolProviderServiceSipImpl protocolProvider;

    /**
     * Constructs this task.
     *
     * @param pp
     */
    ResetListeningPoint(ProtocolProviderServiceSipImpl pp) {
      this.protocolProvider = pp;
    }

    /**
     * Notified when registration state changed for a provider.
     *
     * @param evt
     */
    public void registrationStateChanged(RegistrationStateChangeEvent evt) {
      if (evt.getNewState() == RegistrationState.UNREGISTERING) {
        new Timer().schedule(this, TIME_FOR_PP_TO_UNREGISTER);
      } else {
        protocolProvider.removeRegistrationStateChangeListener(this);
        resetListeningPointsTimers.remove(protocolProvider.getRegistrarConnection().getTransport());
      }
    }

    /** The real task work, replace listening point. */
    public void run() {
      // if the provider is still unregistering it most probably won't
      // successes until we re-init the LP
      if (protocolProvider.getRegistrationState() == RegistrationState.UNREGISTERING) {
        String transport = protocolProvider.getRegistrarConnection().getTransport();

        ListeningPoint old = getLP(transport);

        try {
          stack.deleteListeningPoint(old);
        } catch (Throwable t) {
          logger.warn("Error replacing ListeningPoint for " + transport, t);
        }

        try {
          ListeningPoint tcpLP =
              stack.createListeningPoint(
                  NetworkUtils.IN_ADDR_ANY,
                  transport.equals(ListeningPoint.TCP)
                      ? getPreferredClearPort()
                      : getPreferredSecurePort(),
                  transport);
          clearJainSipProvider.addListeningPoint(tcpLP);
        } catch (Throwable t) {
          logger.warn(
              "Error replacing ListeningPoint for "
                  + protocolProvider.getRegistrarConnection().getTransport(),
              t);
        }
      }

      resetListeningPointsTimers.remove(protocolProvider.getRegistrarConnection().getTransport());
    }
  }
}
/**
 * Tests ICQ implementations of a Presence Operation Set. Tests in this class verify functionality
 * such as: Changing local (our own) status and corresponding event dispatching; Querying status of
 * contacts, Subscribing for presence notifications upong status changes of specific contacts.
 *
 * <p>Using a custom suite() method, we make sure that apart from standard test methods (those with
 * a <tt>test</tt> prefix) we also execute those that we want run in a specific order like for
 * example - postTestSubscribe() and postTestUnsubscribe().
 *
 * <p>
 *
 * @author Emil Ivov
 * @author Damian Minkov
 */
public class TestOperationSetPresence extends TestCase {
  private static final Logger logger = Logger.getLogger(TestOperationSetPresence.class);

  private IcqSlickFixture fixture = new IcqSlickFixture();
  private OperationSetPresence operationSetPresence = null;
  private String statusMessageRoot = new String("Our status is now: ");

  // be sure its only one
  private static AuthEventCollector authEventCollector = new AuthEventCollector();

  public TestOperationSetPresence(String name) {
    super(name);
  }

  protected void setUp() throws Exception {
    super.setUp();
    fixture.setUp();

    Map<String, OperationSet> supportedOperationSets = fixture.provider.getSupportedOperationSets();

    if (supportedOperationSets == null || supportedOperationSets.size() < 1)
      throw new NullPointerException(
          "No OperationSet implementations are supported by " + "this ICQ implementation. ");

    // get the operation set presence here.
    operationSetPresence =
        (OperationSetPresence) supportedOperationSets.get(OperationSetPresence.class.getName());

    // if the op set is null then the implementation doesn't offer a presence
    // operation set which is unacceptable for icq.
    if (operationSetPresence == null) {
      throw new NullPointerException(
          "An implementation of the ICQ service must provide an "
              + "implementation of at least the one of the Presence "
              + "Operation Sets");
    }
  }

  protected void tearDown() throws Exception {
    super.tearDown();

    fixture.tearDown();
  }

  /**
   * Creates a test suite containing all tests of this class followed by test methods that we want
   * executed in a specified order.
   *
   * @return Test
   */
  public static Test suite() {
    // return an (almost) empty suite if we're running in offline mode.
    if (IcqSlickFixture.onlineTestingDisabled) {
      TestSuite suite = new TestSuite();
      // the only test around here that we could run without net
      // connectivity
      suite.addTest(new TestOperationSetPresence("testSupportedStatusSetForCompleteness"));
      return suite;
    }

    TestSuite suite = new TestSuite(TestOperationSetPresence.class);

    // the following 2 need to be run in the specified order.
    // (postTestUnsubscribe() needs the subscription created from
    // postTestSubscribe() )
    suite.addTest(new TestOperationSetPresence("postTestSubscribe"));
    suite.addTest(new TestOperationSetPresence("postTestUnsubscribe"));

    // execute this test after postTestSubscribe
    // to be sure that AuthorizationHandler is installed
    suite.addTest(new TestOperationSetPresence("postTestReceiveAuthorizatinonRequest"));

    return suite;
  }

  /** Verifies that all necessary ICQ test states are supported by the implementation. */
  public void testSupportedStatusSetForCompleteness() {
    // first create a local list containing the presence status instances
    // supported by the underlying implementation.
    Iterator<PresenceStatus> supportedStatusSetIter = operationSetPresence.getSupportedStatusSet();

    List<PresenceStatus> supportedStatusSet = new LinkedList<PresenceStatus>();
    while (supportedStatusSetIter.hasNext()) {
      supportedStatusSet.add(supportedStatusSetIter.next());
    }

    // create a copy of the MUST status set and remove any matching status
    // that is also present in the supported set.
    List<?> requiredStatusSetCopy = (List<?>) IcqStatusEnum.icqStatusSet.clone();

    requiredStatusSetCopy.removeAll(supportedStatusSet);

    // if we have anything left then the implementation is wrong.
    int unsupported = requiredStatusSetCopy.size();
    assertTrue(
        "There are " + unsupported + " statuses as follows:" + requiredStatusSetCopy,
        unsupported == 0);
  }

  /**
   * Verify that changing state to AWAY works as supposed to and that it generates the corresponding
   * event.
   *
   * @throws Exception in case a failure occurs while the operation set is switching to the new
   *     state.
   */
  public void testChangingStateToAway() throws Exception {
    subtestStateTransition(IcqStatusEnum.AWAY);
  }

  /**
   * Verify that changing state to NOT_AVAILABLE works as supposed to and that it generates the
   * corresponding event.
   *
   * @throws Exception in case a failure occurs while the operation set is switching to the new
   *     state.
   */
  public void testChangingStateToNotAvailable() throws Exception {
    subtestStateTransition(IcqStatusEnum.NOT_AVAILABLE);
  }

  /**
   * Verify that changing state to DND works as supposed to and that it generates the corresponding
   * event.
   *
   * @throws Exception in case a failure occurs while the operation set is switching to the new
   *     state.
   */
  public void testChangingStateToDnd() throws Exception {
    subtestStateTransition(IcqStatusEnum.DO_NOT_DISTURB);
  }

  /**
   * Verify that changing state to INVISIBLE works as supposed to and that it generates the
   * corresponding event.
   *
   * @throws Exception in case a failure occurs while the operation set is switching to the new
   *     state.
   */
  public void testChangingStateToInvisible() throws Exception {
    subtestStateTransition(IcqStatusEnum.INVISIBLE);
  }

  /**
   * Verify that changing state to OCCUPIED works as supposed to and that it generates the
   * corresponding event.
   *
   * @throws Exception in case a failure occurs while the operation set is switching to the new
   *     state.
   */
  public void testChangingStateToOccupied() throws Exception {
    subtestStateTransition(IcqStatusEnum.OCCUPIED);
  }

  /**
   * Verify that changing state to FREE_FOR_CHAT works as supposed to and that it generates the
   * corresponding event.
   *
   * @throws Exception in case a failure occurs while the operation set is switching to the new
   *     state.
   */
  public void testChangingStateToFreeForChat() throws Exception {
    subtestStateTransition(IcqStatusEnum.FREE_FOR_CHAT);
  }

  /**
   * Verify that changing state to ONLINE works as supposed to and that it generates the
   * corresponding event.
   *
   * @throws Exception in case a failure occurs while the operation set is switching to the new
   *     state.
   */
  public void testChangingStateToOnline() throws Exception {
    //
    // java.util.logging.Logger.getLogger("net.kano").setLevel(java.util.logging.Level.FINEST);
    subtestStateTransition(IcqStatusEnum.ONLINE);
    //
    // java.util.logging.Logger.getLogger("net.kano").setLevel(java.util.logging.Level.WARNING);
  }

  /**
   * Used by methods testing state transiotions
   *
   * @param newStatus the IcqStatusEnum field corresponding to the status that we'd like the
   *     opeation set to enter.
   * @throws Exception in case changing the state causes an exception
   */
  public void subtestStateTransition(IcqStatusEnum newStatus) throws Exception {
    logger.trace(" --=== beginning state transition test ===--");

    PresenceStatus oldStatus = operationSetPresence.getPresenceStatus();
    String oldStatusMessage = operationSetPresence.getCurrentStatusMessage();
    String newStatusMessage = statusMessageRoot + newStatus;

    logger.debug(
        "old status is=" + oldStatus.getStatusName() + " new status=" + newStatus.getStatusName());

    // First register a listener to make sure that all corresponding
    // events have been generated.
    PresenceStatusEventCollector statusEventCollector = new PresenceStatusEventCollector();
    operationSetPresence.addProviderPresenceStatusListener(statusEventCollector);

    // change the status
    operationSetPresence.publishPresenceStatus(newStatus, newStatusMessage);

    // test event notification.
    statusEventCollector.waitForPresEvent(10000);
    statusEventCollector.waitForStatMsgEvent(10000);

    // sometimes we don't get response from the server for the
    // changed status. we will query it once again.
    // and wait for the response
    if (statusEventCollector.collectedPresEvents.size() == 0) {
      logger.trace("Will query again status as we haven't received one");
      operationSetPresence.queryContactStatus(fixture.icqAccountID.getUserID());
      statusEventCollector.waitForPresEvent(10000);
    }

    operationSetPresence.removeProviderPresenceStatusListener(statusEventCollector);

    assertEquals(
        "Events dispatched during an event transition.",
        1,
        statusEventCollector.collectedPresEvents.size());
    assertEquals(
        "A status changed event contained wrong old status.",
        oldStatus,
        ((ProviderPresenceStatusChangeEvent) statusEventCollector.collectedPresEvents.get(0))
            .getOldStatus());
    assertEquals(
        "A status changed event contained wrong new status.",
        newStatus,
        ((ProviderPresenceStatusChangeEvent) statusEventCollector.collectedPresEvents.get(0))
            .getNewStatus());

    // verify that the operation set itself is aware of the status change
    assertEquals(
        "opSet.getPresenceStatus() did not return properly.",
        newStatus,
        operationSetPresence.getPresenceStatus());

    IcqStatusEnum actualStatus =
        fixture.testerAgent.getBuddyStatus(fixture.icqAccountID.getUserID());
    assertEquals(
        "The underlying implementation did not switch to the " + "requested presence status.",
        newStatus,
        actualStatus);

    // check whether the server returned the status message that we've set.
    assertEquals(
        "No status message events.", 1, statusEventCollector.collectedStatMsgEvents.size());
    assertEquals(
        "A status message event contained wrong old value.",
        oldStatusMessage,
        ((PropertyChangeEvent) statusEventCollector.collectedStatMsgEvents.get(0)).getOldValue());
    assertEquals(
        "A status message event contained wrong new value.",
        newStatusMessage,
        ((PropertyChangeEvent) statusEventCollector.collectedStatMsgEvents.get(0)).getNewValue());

    // verify that the operation set itself is aware of the new status msg.
    assertEquals(
        "opSet.getCurrentStatusMessage() did not return properly.",
        newStatusMessage,
        operationSetPresence.getCurrentStatusMessage());

    logger.trace(" --=== finished test ===--");
    // make it sleep a bit cause the aol server gets mad otherwise.
    pauseBetweenStateChanges();
  }

  /**
   * The AIM server doesn't like it if we change states too often and we use this method to slow
   * things down.
   */
  private void pauseBetweenStateChanges() {
    try {
      Thread.sleep(5000);
    } catch (InterruptedException ex) {
      logger.debug("Pausing between state changes was interrupted", ex);
    }
  }
  /**
   * Verifies that querying status works fine. The ICQ tester agent would change status and the
   * operation set would have to return the right status after every change.
   *
   * @throws java.lang.Exception if one of the transitions fails
   */
  public void testQueryContactStatus() throws Exception {
    // --- AWAY ---
    logger.debug("Will Query an AWAY contact.");
    subtestQueryContactStatus(FullUserInfo.ICQSTATUS_AWAY, IcqStatusEnum.AWAY);

    pauseBetweenStateChanges();

    // --- NA ---
    logger.debug("Will Query an NA contact.");
    subtestQueryContactStatus(FullUserInfo.ICQSTATUS_NA, IcqStatusEnum.NOT_AVAILABLE);

    pauseBetweenStateChanges();

    // --- DND ---
    logger.debug("Will Query a DND contact.");
    subtestQueryContactStatus(FullUserInfo.ICQSTATUS_DND, IcqStatusEnum.DO_NOT_DISTURB);

    pauseBetweenStateChanges();

    // --- FFC ---
    logger.debug("Will Query a Free For Chat contact.");
    subtestQueryContactStatus(FullUserInfo.ICQSTATUS_FFC, IcqStatusEnum.FREE_FOR_CHAT);

    pauseBetweenStateChanges();

    // --- INVISIBLE ---
    logger.debug("Will Query an Invisible contact.");
    subtestQueryContactStatus(FullUserInfo.ICQSTATUS_INVISIBLE, IcqStatusEnum.INVISIBLE);

    pauseBetweenStateChanges();

    // --- Occupied ---
    logger.debug("Will Query an Occupied contact.");
    subtestQueryContactStatus(FullUserInfo.ICQSTATUS_OCCUPIED, IcqStatusEnum.OCCUPIED);

    pauseBetweenStateChanges();

    // --- Online ---
    logger.debug("Will Query an Online contact.");
    subtestQueryContactStatus(IcqTesterAgent.ICQ_ONLINE_MASK, IcqStatusEnum.ONLINE);

    pauseBetweenStateChanges();
  }

  /**
   * Used by functions testing the queryContactStatus method of the presence operation set.
   *
   * @param taStatusLong the icq status as specified by FullUserInfo, that the tester agent should
   *     switch to.
   * @param expectedReturn the PresenceStatus that the presence operation set should see the tester
   *     agent in once it has switched to taStatusLong.
   * @throws java.lang.Exception if querying the status causes some exception.
   */
  public void subtestQueryContactStatus(long taStatusLong, PresenceStatus expectedReturn)
      throws Exception {
    if (!fixture.testerAgent.enterStatus(taStatusLong)) {
      throw new RuntimeException(
          "Tester UserAgent Failed to switch to the " + expectedReturn.getStatusName() + " state.");
    }

    PresenceStatus actualReturn =
        operationSetPresence.queryContactStatus(fixture.testerAgent.getIcqUIN());
    assertEquals(
        "Querying a " + expectedReturn.getStatusName() + " state did not return as expected",
        expectedReturn,
        actualReturn);
  }

  /**
   * The method would add a subscription for a contact, wait for a subscription event confirming the
   * subscription, then change the status of the newly added contact (which is actually the
   * IcqTesterAgent) and make sure that the corresponding notification events have been generated.
   *
   * @throws java.lang.Exception if an exception occurs during testing.
   */
  public void postTestSubscribe() throws Exception {
    logger.debug("Testing Subscription and Subscription Event Dispatch.");

    // First create a subscription and verify that it really gets created.
    SubscriptionEventCollector subEvtCollector = new SubscriptionEventCollector();

    logger.trace("set Auth Handler");
    operationSetPresence.setAuthorizationHandler(authEventCollector);

    synchronized (authEventCollector) {
      authEventCollector.authorizationRequestReason = "Please deny my request!";
      fixture.testerAgent.getAuthCmdFactory().responseReasonStr =
          "First authorization I will Deny!!!";
      fixture.testerAgent.getAuthCmdFactory().ACCEPT = false;
      operationSetPresence.subscribe(fixture.testerAgent.getIcqUIN());

      // this one collects event that the buddy has been added
      // to the list as awaiting
      SubscriptionEventCollector moveEvtCollector = new SubscriptionEventCollector();
      operationSetPresence.addSubscriptionListener(moveEvtCollector);

      logger.debug("Waiting for authorization error and authorization response...");
      authEventCollector.waitForAuthResponse(15000);
      assertTrue(
          "Error adding buddy not recieved or the buddy("
              + fixture.testerAgent.getIcqUIN()
              + ") doesn't require authorization",
          authEventCollector.isAuthorizationRequestSent);

      assertNotNull(
          "Agent haven't received any reason for authorization",
          fixture.testerAgent.getAuthCmdFactory().requestReasonStr);
      assertEquals(
          "Error sent request reason is not as the received one",
          authEventCollector.authorizationRequestReason,
          fixture.testerAgent.getAuthCmdFactory().requestReasonStr);

      logger.debug(
          "authEventCollector.isAuthorizationResponseReceived "
              + authEventCollector.isAuthorizationResponseReceived);

      assertTrue("Response not received!", authEventCollector.isAuthorizationResponseReceived);

      boolean isAcceptedAuthReuest =
          authEventCollector.response.getResponseCode().equals(AuthorizationResponse.ACCEPT);
      assertEquals(
          "Response is not as the sent one",
          fixture.testerAgent.getAuthCmdFactory().ACCEPT,
          isAcceptedAuthReuest);
      assertNotNull(
          "We didn't receive any reason! ", authEventCollector.authorizationResponseString);

      assertEquals(
          "The sent response reason is not as the received one",
          fixture.testerAgent.getAuthCmdFactory().responseReasonStr,
          authEventCollector.authorizationResponseString);

      // here we must wait for server to move the awaiting buddy
      // to the first specified  group
      synchronized (moveEvtCollector) {
        moveEvtCollector.waitForEvent(20000);
        // don't want any more events
        operationSetPresence.removeSubscriptionListener(moveEvtCollector);
      }

      Contact c = operationSetPresence.findContactByID(fixture.testerAgent.getIcqUIN());
      logger.debug("I will remove " + c + " from group : " + c.getParentContactGroup());

      UnsubscribeWait unsubscribeEvtCollector = new UnsubscribeWait();
      operationSetPresence.addSubscriptionListener(unsubscribeEvtCollector);

      synchronized (unsubscribeEvtCollector) {
        operationSetPresence.unsubscribe(c);
        logger.debug("Waiting to be removed...");
        unsubscribeEvtCollector.waitForUnsubscribre(20000);

        logger.debug("Received unsubscribed ok or we lost patients!");

        // don't want any more events
        operationSetPresence.removeSubscriptionListener(unsubscribeEvtCollector);
      }

      // so we haven't asserted so everithing is fine lets try to be authorized
      authEventCollector.authorizationRequestReason = "Please accept my request!";
      fixture.testerAgent.getAuthCmdFactory().responseReasonStr =
          "Second authorization I will Accept!!!";
      fixture.testerAgent.getAuthCmdFactory().ACCEPT = true;

      // clear some things
      authEventCollector.isAuthorizationRequestSent = false;
      authEventCollector.isAuthorizationResponseReceived = false;
      authEventCollector.authorizationResponseString = null;

      logger.debug(
          "I will add buddy does it exists ?  "
              + (operationSetPresence.findContactByID(fixture.testerAgent.getIcqUIN()) != null));
      // add the listener beacuse now our authorization will be accepted
      // and so the buddy will be finally added to the list
      operationSetPresence.addSubscriptionListener(subEvtCollector);
      // subscribe again so we can trigger again the authorization procedure
      operationSetPresence.subscribe(fixture.testerAgent.getIcqUIN());

      logger.debug(
          "Waiting ... Subscribe must fail and the authorization process "
              + "to be trigered again so waiting for auth response ...");
      authEventCollector.waitForAuthResponse(15000);

      assertTrue(
          "Error adding buddy not recieved or the buddy("
              + fixture.testerAgent.getIcqUIN()
              + ") doesn't require authorization",
          authEventCollector.isAuthorizationRequestSent);

      assertNotNull(
          "Agent haven't received any reason for authorization",
          fixture.testerAgent.getAuthCmdFactory().requestReasonStr);

      // not working for now
      assertEquals(
          "Error sent request reason",
          authEventCollector.authorizationRequestReason,
          fixture.testerAgent.getAuthCmdFactory().requestReasonStr);

      // wait for authorization process to be finnished
      // the modification of buddy (server will inform us
      // that he removed - awaiting authorization flag)
      Object obj = new Object();
      synchronized (obj) {
        logger.debug("wait for authorization process to be finnished");
        obj.wait(10000);
        logger.debug("Stop waiting!");
      }

      subEvtCollector.waitForEvent(10000);
      // don't want any more events
      operationSetPresence.removeSubscriptionListener(subEvtCollector);
    }

    // after adding awaitingAuthorization group here are catched 3 events
    // 1 - creating unresolved contact
    // 2 - move of the contact to awaitingAuthorization group
    // 3 - move of the contact from awaitingAuthorization group to original group
    assertTrue(
        "Subscription event dispatching failed.", subEvtCollector.collectedEvents.size() > 0);

    EventObject evt = null;

    Iterator<EventObject> events = subEvtCollector.collectedEvents.iterator();
    while (events.hasNext()) {
      EventObject elem = events.next();
      if (elem instanceof SubscriptionEvent) {
        if (((SubscriptionEvent) elem).getEventID() == SubscriptionEvent.SUBSCRIPTION_CREATED)
          evt = (SubscriptionEvent) elem;
      }
    }

    Object source = null;
    Contact srcContact = null;
    ProtocolProviderService srcProvider = null;

    // the event can be SubscriptionEvent and the new added one
    // SubscriptionMovedEvent

    if (evt instanceof SubscriptionEvent) {
      SubscriptionEvent subEvt = (SubscriptionEvent) evt;

      source = subEvt.getSource();
      srcContact = subEvt.getSourceContact();
      srcProvider = subEvt.getSourceProvider();
    }

    assertEquals(
        "SubscriptionEvent Source:",
        fixture.testerAgent.getIcqUIN(),
        ((Contact) source).getAddress());
    assertEquals(
        "SubscriptionEvent Source Contact:",
        fixture.testerAgent.getIcqUIN(),
        srcContact.getAddress());
    assertSame("SubscriptionEvent Source Provider:", fixture.provider, srcProvider);

    subEvtCollector.collectedEvents.clear();

    // make the user agent tester change its states and make sure we are
    // notified
    logger.debug("Testing presence notifications.");
    IcqStatusEnum testerAgentOldStatus = fixture.testerAgent.getPresneceStatus();
    IcqStatusEnum testerAgentNewStatus = IcqStatusEnum.FREE_FOR_CHAT;
    long testerAgentNewStatusLong = FullUserInfo.ICQSTATUS_FFC;

    // in case we are by any chance already in a FREE_FOR_CHAT status, we'll
    // be changing to something else
    if (testerAgentOldStatus.equals(testerAgentNewStatus)) {
      testerAgentNewStatus = IcqStatusEnum.DO_NOT_DISTURB;
      testerAgentNewStatusLong = FullUserInfo.ICQSTATUS_DND;
    }

    // now do the actual status notification testing
    ContactPresenceEventCollector contactPresEvtCollector =
        new ContactPresenceEventCollector(fixture.testerAgent.getIcqUIN(), testerAgentNewStatus);
    operationSetPresence.addContactPresenceStatusListener(contactPresEvtCollector);

    synchronized (contactPresEvtCollector) {
      if (!fixture.testerAgent.enterStatus(testerAgentNewStatusLong)) {
        throw new RuntimeException(
            "Tester UserAgent Failed to switch to the "
                + testerAgentNewStatus.getStatusName()
                + " state.");
      }
      // we may already have the event, but it won't hurt to check.
      contactPresEvtCollector.waitForEvent(12000);
      operationSetPresence.removeContactPresenceStatusListener(contactPresEvtCollector);
    }

    if (contactPresEvtCollector.collectedEvents.size() == 0) {
      logger.info(
          "PROBLEM. Authorisation process doesn't have finnished "
              + "Server doesn't report us for changing authorization flag! Will try to authorize once again");

      fixture.testerAgent.sendAuthorizationReplay(
          fixture.icqAccountID.getUserID(),
          fixture.testerAgent.getAuthCmdFactory().responseReasonStr,
          fixture.testerAgent.getAuthCmdFactory().ACCEPT);

      Object obj = new Object();
      synchronized (obj) {
        logger.debug("wait for authorization process to be finnished for second time");
        obj.wait(10000);
        logger.debug("Stop waiting!");
      }

      testerAgentOldStatus = fixture.testerAgent.getPresneceStatus();
      testerAgentNewStatusLong = FullUserInfo.ICQSTATUS_FFC;

      // in case we are by any chance already in a FREE_FOR_CHAT status, we'll
      // be changing to something else
      if (testerAgentOldStatus.equals(testerAgentNewStatus)) {
        testerAgentNewStatus = IcqStatusEnum.OCCUPIED;
        testerAgentNewStatusLong = FullUserInfo.ICQSTATUS_OCCUPIED;
      }

      contactPresEvtCollector =
          new ContactPresenceEventCollector(fixture.testerAgent.getIcqUIN(), testerAgentNewStatus);
      operationSetPresence.addContactPresenceStatusListener(contactPresEvtCollector);

      synchronized (contactPresEvtCollector) {
        if (!fixture.testerAgent.enterStatus(testerAgentNewStatusLong)) {
          throw new RuntimeException(
              "Tester UserAgent Failed to switch to the "
                  + testerAgentNewStatus.getStatusName()
                  + " state.");
        }
        // we may already have the event, but it won't hurt to check.
        contactPresEvtCollector.waitForEvent(12000);
        operationSetPresence.removeContactPresenceStatusListener(contactPresEvtCollector);
      }
    }

    assertEquals(
        "Presence Notif. event dispatching failed.",
        1,
        contactPresEvtCollector.collectedEvents.size());
    ContactPresenceStatusChangeEvent presEvt =
        (ContactPresenceStatusChangeEvent) contactPresEvtCollector.collectedEvents.get(0);

    assertEquals(
        "Presence Notif. event  Source:",
        fixture.testerAgent.getIcqUIN(),
        ((Contact) presEvt.getSource()).getAddress());
    assertEquals(
        "Presence Notif. event  Source Contact:",
        fixture.testerAgent.getIcqUIN(),
        presEvt.getSourceContact().getAddress());
    assertSame(
        "Presence Notif. event  Source Provider:", fixture.provider, presEvt.getSourceProvider());

    PresenceStatus reportedNewStatus = presEvt.getNewStatus();
    PresenceStatus reportedOldStatus = presEvt.getOldStatus();

    assertEquals("Reported new PresenceStatus: ", testerAgentNewStatus, reportedNewStatus);

    // don't require equality between the reported old PresenceStatus and
    // the actual presence status of the tester agent because a first
    // notification is not supposed to have the old status as it really was.
    assertNotNull("Reported old PresenceStatus: ", reportedOldStatus);

    /** @todo tester agent changes status message we see the new message */
    /** @todo we should see the alias of the tester agent. */
    Object obj = new Object();
    synchronized (obj) {
      logger.debug("wait a moment. give time to server");
      obj.wait(4000);
    }
  }

  /**
   * We unsubscribe from presence notification deliveries concerning IcqTesterAgent's presence
   * status and verify that we receive the subscription removed event. We then make the tester agent
   * change status and make sure that no notifications are delivered.
   *
   * @throws java.lang.Exception in case unsubscribing fails.
   */
  public void postTestUnsubscribe() throws Exception {
    logger.debug("Testing Unsubscribe and unsubscription event dispatch.");

    // First create a subscription and verify that it really gets created.
    SubscriptionEventCollector subEvtCollector = new SubscriptionEventCollector();
    operationSetPresence.addSubscriptionListener(subEvtCollector);

    Contact icqTesterAgentContact =
        operationSetPresence.findContactByID(fixture.testerAgent.getIcqUIN());

    assertNotNull(
        "Failed to find an existing subscription for the tester agent", icqTesterAgentContact);

    synchronized (subEvtCollector) {
      operationSetPresence.unsubscribe(icqTesterAgentContact);
      subEvtCollector.waitForEvent(40000);
      // don't want any more events
      operationSetPresence.removeSubscriptionListener(subEvtCollector);
    }

    assertEquals(
        "Subscription event dispatching failed.", 1, subEvtCollector.collectedEvents.size());
    SubscriptionEvent subEvt = (SubscriptionEvent) subEvtCollector.collectedEvents.get(0);

    assertEquals("SubscriptionEvent Source:", icqTesterAgentContact, subEvt.getSource());

    assertEquals(
        "SubscriptionEvent Source Contact:", icqTesterAgentContact, subEvt.getSourceContact());

    assertSame("SubscriptionEvent Source Provider:", fixture.provider, subEvt.getSourceProvider());

    subEvtCollector.collectedEvents.clear();

    // make the user agent tester change its states and make sure we don't
    // get notifications as we're now unsubscribed.
    logger.debug("Testing (lack of) presence notifications.");
    IcqStatusEnum testerAgentOldStatus = fixture.testerAgent.getPresneceStatus();
    IcqStatusEnum testerAgentNewStatus = IcqStatusEnum.FREE_FOR_CHAT;
    long testerAgentNewStatusLong = FullUserInfo.ICQSTATUS_FFC;

    // in case we are by any chance already in a FREE_FOR_CHAT status, we'll
    // be changing to something else
    if (testerAgentOldStatus.equals(testerAgentNewStatus)) {
      testerAgentNewStatus = IcqStatusEnum.DO_NOT_DISTURB;
      testerAgentNewStatusLong = FullUserInfo.ICQSTATUS_DND;
    }

    // now do the actual status notification testing
    ContactPresenceEventCollector contactPresEvtCollector =
        new ContactPresenceEventCollector(fixture.testerAgent.getIcqUIN(), null);
    operationSetPresence.addContactPresenceStatusListener(contactPresEvtCollector);

    synchronized (contactPresEvtCollector) {
      if (!fixture.testerAgent.enterStatus(testerAgentNewStatusLong)) {
        throw new RuntimeException(
            "Tester UserAgent Failed to switch to the "
                + testerAgentNewStatus.getStatusName()
                + " state.");
      }
      // we may already have the event, but it won't hurt to check.
      contactPresEvtCollector.waitForEvent(10000);
      operationSetPresence.removeContactPresenceStatusListener(contactPresEvtCollector);
    }

    assertEquals(
        "Presence Notifications were received after unsubscibing.",
        0,
        contactPresEvtCollector.collectedEvents.size());
  }

  /**
   * An event collector that would collect all events generated by a provider after a status change.
   * The collector would also do a notidyAll every time it receives an event.
   */
  private class PresenceStatusEventCollector implements ProviderPresenceStatusListener {
    public ArrayList<EventObject> collectedPresEvents = new ArrayList<EventObject>();
    public ArrayList<EventObject> collectedStatMsgEvents = new ArrayList<EventObject>();

    public void providerStatusChanged(ProviderPresenceStatusChangeEvent evt) {
      synchronized (this) {
        logger.debug("Collected evt(" + collectedPresEvents.size() + ")= " + evt);
        collectedPresEvents.add(evt);
        notifyAll();
      }
    }

    public void providerStatusMessageChanged(PropertyChangeEvent evt) {
      synchronized (this) {
        logger.debug("Collected stat.msg. evt(" + collectedPresEvents.size() + ")= " + evt);
        collectedStatMsgEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Blocks until at least one event is received or until waitFor milliseconds pass (whichever
     * happens first).
     *
     * @param waitFor the number of milliseconds that we should be waiting for an event before
     *     simply bailing out.
     */
    public void waitForPresEvent(long waitFor) {
      logger.trace("Waiting for a change in provider status.");
      synchronized (this) {
        if (collectedPresEvents.size() > 0) {
          logger.trace("Change already received. " + collectedPresEvents);
          return;
        }

        try {
          wait(waitFor);
          if (collectedPresEvents.size() > 0) logger.trace("Received a change in provider status.");
          else logger.trace("No change received for " + waitFor + "ms.");
        } catch (InterruptedException ex) {
          logger.debug("Interrupted while waiting for a provider evt", ex);
        }
      }
    }

    /**
     * Blocks until at least one status message event is received or until waitFor milliseconds pass
     * (whichever happens first).
     *
     * @param waitFor the number of milliseconds that we should be waiting for a status message
     *     event before simply bailing out.
     */
    public void waitForStatMsgEvent(long waitFor) {
      logger.trace("Waiting for a provider status message event.");
      synchronized (this) {
        if (collectedStatMsgEvents.size() > 0) {
          logger.trace("Stat msg. evt already received. " + collectedStatMsgEvents);
          return;
        }

        try {
          wait(waitFor);
          if (collectedStatMsgEvents.size() > 0) logger.trace("Received a prov. stat. msg. evt.");
          else logger.trace("No prov. stat msg. received for " + waitFor + "ms.");
        } catch (InterruptedException ex) {
          logger.debug("Interrupted while waiting for a status msg evt", ex);
        }
      }
    }
  }

  /** The class would listen for and store received subscription modification events. */
  private class SubscriptionEventCollector implements SubscriptionListener {
    public ArrayList<EventObject> collectedEvents = new ArrayList<EventObject>();

    /**
     * Blocks until at least one event is received or until waitFor milliseconds pass (whichever
     * happens first).
     *
     * @param waitFor the number of milliseconds that we should be waiting for an event before
     *     simply bailing out.
     */
    public void waitForEvent(long waitFor) {
      synchronized (this) {
        if (collectedEvents.size() > 0) return;

        try {
          wait(waitFor);
        } catch (InterruptedException ex) {
          logger.debug("Interrupted while waiting for a subscription evt", ex);
        }
      }
    }

    /**
     * Stores the received subscription and notifies all waiting on this object
     *
     * @param evt the SubscriptionEvent containing the corresponding contact
     */
    public void subscriptionCreated(SubscriptionEvent evt) {
      synchronized (this) {
        logger.debug("Collected evt(" + collectedEvents.size() + ")= " + evt);
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Stores the received subscription and notifies all waiting on this object
     *
     * @param evt the SubscriptionEvent containing the corresponding contact
     */
    public void subscriptionRemoved(SubscriptionEvent evt) {
      synchronized (this) {
        logger.debug("Collected evt(" + collectedEvents.size() + ")= " + evt);
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Stores the received subscription and notifies all waiting on this object
     *
     * @param evt the SubscriptionEvent containing the corresponding contact
     */
    public void contactModified(ContactPropertyChangeEvent evt) {
      synchronized (this) {
        logger.debug("Collected evt(" + collectedEvents.size() + ")= " + evt);
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Stores the received subscription and notifies all waiting on this object
     *
     * @param evt the SubscriptionEvent containing the corresponding contact
     */
    public void subscriptionMoved(SubscriptionMovedEvent evt) {
      synchronized (this) {
        logger.debug("Collected evt(" + collectedEvents.size() + ")= " + evt);
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Stores the received subscription and notifies all waiting on this object
     *
     * @param evt the SubscriptionEvent containing the corresponding contact
     */
    public void subscriptionFailed(SubscriptionEvent evt) {
      synchronized (this) {
        logger.debug("Collected evt(" + collectedEvents.size() + ")= " + evt);
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Stores the received subscription and notifies all waiting on this object
     *
     * @param evt the SubscriptionEvent containing the corresponding contact
     */
    public void subscriptionResolved(SubscriptionEvent evt) {
      synchronized (this) {
        logger.debug("Collected evt(" + collectedEvents.size() + ")= " + evt);
        collectedEvents.add(evt);
        notifyAll();
      }
    }
  }

  /**
   * The class would listen for and store received events caused by changes in contact presence
   * states.
   */
  private class ContactPresenceEventCollector implements ContactPresenceStatusListener {
    public ArrayList<EventObject> collectedEvents = new ArrayList<EventObject>();
    private String trackedScreenName = null;
    private IcqStatusEnum status = null;

    ContactPresenceEventCollector(String screenname, IcqStatusEnum wantedStatus) {
      this.trackedScreenName = screenname;
      this.status = wantedStatus;
    }

    /**
     * Blocks until at least one event is received or until waitFor milliseconds pass (whichever
     * happens first).
     *
     * @param waitFor the number of milliseconds that we should be waiting for an event before
     *     simply bailing out.
     */
    public void waitForEvent(long waitFor) {
      synchronized (this) {
        if (collectedEvents.size() > 0) return;

        try {
          wait(waitFor);
        } catch (InterruptedException ex) {
          logger.debug("Interrupted while waiting for a subscription evt", ex);
        }
      }
    }

    /**
     * Stores the received status change event and notifies all waiting on this object
     *
     * @param evt the SubscriptionEvent containing the corresponding contact
     */
    public void contactPresenceStatusChanged(ContactPresenceStatusChangeEvent evt) {
      synchronized (this) {
        // if the user has specified event details and the received
        // event does not match - then ignore it.
        if (this.trackedScreenName != null
            && !evt.getSourceContact().getAddress().equals(trackedScreenName)) return;
        if (status != null && status != evt.getNewStatus()) return;

        logger.debug("Collected evt(" + collectedEvents.size() + ")= " + evt);
        collectedEvents.add(evt);
        notifyAll();
      }
    }
  }

  /**
   * Authorization handler for the implementation tests
   *
   * <p>1. when authorization request is received we answer with the already set Authorization
   * response, but before that wait some time as a normal user
   *
   * <p>2. When authorization request is required for adding buddy the request is made with already
   * set authorization reason
   *
   * <p>3. When authorization replay is received - we store that it is received and the reason that
   * was received
   */
  private static class AuthEventCollector implements AuthorizationHandler {
    boolean isAuthorizationRequestSent = false;
    String authorizationRequestReason = null;

    boolean isAuthorizationResponseReceived = false;
    AuthorizationResponse response = null;
    String authorizationResponseString = null;

    // receiving auth request
    AuthorizationResponse responseToRequest = null;
    boolean isAuthorizationRequestReceived = false;

    public AuthorizationResponse processAuthorisationRequest(
        AuthorizationRequest req, Contact sourceContact) {
      logger.debug("Processing in " + this);
      synchronized (this) {
        logger.trace("processAuthorisationRequest " + req + " " + sourceContact);

        isAuthorizationRequestReceived = true;
        authorizationRequestReason = req.getReason();

        notifyAll();

        // will wait as a normal user
        Object lock = new Object();
        synchronized (lock) {
          try {
            lock.wait(2000);
          } catch (Exception ex) {
          }
        }

        return responseToRequest;
      }
    }

    public AuthorizationRequest createAuthorizationRequest(Contact contact) {
      logger.trace("createAuthorizationRequest " + contact);

      AuthorizationRequest authReq = new AuthorizationRequest();
      authReq.setReason(authorizationRequestReason);

      isAuthorizationRequestSent = true;

      return authReq;
    }

    public void processAuthorizationResponse(
        AuthorizationResponse response, Contact sourceContact) {
      synchronized (this) {
        isAuthorizationResponseReceived = true;
        this.response = response;
        authorizationResponseString = response.getReason();

        logger.trace(
            "processAuthorizationResponse '"
                + authorizationResponseString
                + "' "
                + response.getResponseCode()
                + " "
                + sourceContact);

        notifyAll();
      }
    }

    public void waitForAuthResponse(long waitFor) {
      synchronized (this) {
        if (isAuthorizationResponseReceived) return;
        try {
          wait(waitFor);
        } catch (InterruptedException ex) {
          logger.debug("Interrupted while waiting for a subscription evt", ex);
        }
      }
    }

    public void waitForAuthRequest(long waitFor) {
      synchronized (this) {
        if (isAuthorizationRequestReceived) return;
        try {
          wait(waitFor);
        } catch (InterruptedException ex) {
          logger.debug("Interrupted while waiting for a subscription evt", ex);
        }
      }
    }
  }

  /**
   * Used to wait till buddy is removed from our contact list. Used in the authorization process
   * tests
   */
  private static class UnsubscribeWait extends SubscriptionAdapter {
    public void waitForUnsubscribre(long waitFor) {
      synchronized (this) {
        try {
          wait(waitFor);
        } catch (InterruptedException ex) {
          logger.debug("Interrupted while waiting for a subscription evt", ex);
        }
      }
    }

    public void subscriptionRemoved(SubscriptionEvent evt) {
      synchronized (this) {
        logger.debug("Got subscriptionRemoved " + evt);
        notifyAll();
      }
    }
  }

  /** Tests for receiving authorization requests */
  public void postTestReceiveAuthorizatinonRequest() {
    logger.debug("Testing receive of authorization request!");

    // set first response isAccepted and responseString
    // the first authorization process is negative
    // the agent try to add us to his contact list and ask us for
    // authorization but we deny him
    String firstRequestResponse = "First Request will be denied!!!";
    authEventCollector.responseToRequest =
        new AuthorizationResponse(AuthorizationResponse.REJECT, firstRequestResponse);
    logger.debug("authEventCollector " + authEventCollector);
    authEventCollector.isAuthorizationRequestReceived = false;
    authEventCollector.authorizationRequestReason = null;
    fixture.testerAgent.getAuthCmdFactory().requestReasonStr = "Deny my first request!";
    fixture.testerAgent.getAuthCmdFactory().isErrorAddingReceived = false;
    fixture.testerAgent.getAuthCmdFactory().responseReasonStr = null;
    fixture.testerAgent.getAuthCmdFactory().isRequestAccepted = false;

    // be sure buddy is not already in the list
    fixture.testerAgent.deleteBuddy(fixture.ourUserID);
    fixture.testerAgent.addBuddy(fixture.ourUserID);

    // wait agent to receive error and to request us for our authorization
    authEventCollector.waitForAuthRequest(25000);

    // check have we received authorization request?
    assertTrue(
        "Error adding buddy not recieved or the buddy("
            + fixture.ourUserID
            + ") doesn't require authorization 1",
        fixture.testerAgent.getAuthCmdFactory().isErrorAddingReceived);

    assertTrue(
        "We haven't received any authorization request ",
        authEventCollector.isAuthorizationRequestReceived);

    assertNotNull(
        "We haven't received any reason for authorization",
        authEventCollector.authorizationRequestReason);

    assertEquals(
        "Error sent request reason is not as the received one",
        fixture.testerAgent.getAuthCmdFactory().requestReasonStr,
        authEventCollector.authorizationRequestReason);

    // wait agent to receive our response
    Object lock = new Object();
    synchronized (lock) {
      try {
        lock.wait(5000);
      } catch (Exception ex) {
      }
    }

    // check is correct - the received response from the agent
    assertNotNull(
        "Agent haven't received any reason from authorization reply",
        authEventCollector.authorizationRequestReason);

    assertEquals(
        "Received auth response from agent is not as the sent one",
        fixture.testerAgent.getAuthCmdFactory().responseReasonStr,
        firstRequestResponse);

    boolean isAcceptedAuthReuest =
        authEventCollector.responseToRequest.getResponseCode().equals(AuthorizationResponse.ACCEPT);
    assertEquals(
        "Agent received Response is not as the sent one",
        fixture.testerAgent.getAuthCmdFactory().isRequestAccepted,
        isAcceptedAuthReuest);

    // delete us from his list
    // be sure buddy is not already in the list
    fixture.testerAgent.deleteBuddy(fixture.ourUserID);

    // set second response isAccepted and responseString
    // the second test is the same as first, but this time we accept
    // the request and check that everything is OK.
    String secondRequestResponse = "Second Request will be accepted!!!";
    authEventCollector.responseToRequest =
        new AuthorizationResponse(AuthorizationResponse.ACCEPT, secondRequestResponse);
    authEventCollector.isAuthorizationRequestReceived = false;
    authEventCollector.authorizationRequestReason = null;
    fixture.testerAgent.getAuthCmdFactory().requestReasonStr = "Accept my second request!";
    fixture.testerAgent.getAuthCmdFactory().isErrorAddingReceived = false;
    fixture.testerAgent.getAuthCmdFactory().responseReasonStr = null;
    fixture.testerAgent.getAuthCmdFactory().isRequestAccepted = false;

    // add us to his list again
    fixture.testerAgent.addBuddy(fixture.ourUserID);

    // wait agent to receive error and to request us for our authorization
    authEventCollector.waitForAuthRequest(25000);

    // check have we received authorization request?
    assertTrue(
        "Error adding buddy not recieved or the buddy("
            + fixture.ourUserID
            + ") doesn't require authorization 2",
        fixture.testerAgent.getAuthCmdFactory().isErrorAddingReceived);

    assertTrue(
        "We haven't received any authorization request ",
        authEventCollector.isAuthorizationRequestReceived);

    assertNotNull(
        "We haven't received any reason for authorization",
        authEventCollector.authorizationRequestReason);

    assertEquals(
        "Error sent request reason is not as the received one",
        fixture.testerAgent.getAuthCmdFactory().requestReasonStr,
        authEventCollector.authorizationRequestReason);
    // wait agent to receive our response
    synchronized (lock) {
      try {
        lock.wait(5000);
      } catch (Exception ex) {
      }
    }
    // check is correct the received response from the agent
    assertNotNull(
        "Agent haven't received any reason from authorization reply",
        authEventCollector.authorizationRequestReason);

    assertEquals(
        "Received auth response from agent is not as the sent one",
        fixture.testerAgent.getAuthCmdFactory().responseReasonStr,
        secondRequestResponse);

    isAcceptedAuthReuest =
        authEventCollector.responseToRequest.getResponseCode().equals(AuthorizationResponse.ACCEPT);
    assertEquals(
        "Agent received Response is not as the sent one",
        fixture.testerAgent.getAuthCmdFactory().isRequestAccepted,
        isAcceptedAuthReuest);
  }
}
/**
 * A straightforward implementation of the basic instant messaging operation set.
 *
 * @author Benoit Pradelle
 */
public class OperationSetBasicInstantMessagingSipImpl
    extends AbstractOperationSetBasicInstantMessaging {
  /** Our class logger. */
  private static final Logger logger =
      Logger.getLogger(OperationSetBasicInstantMessagingSipImpl.class);

  /** A list of processors registered for incoming sip messages. */
  private final List<SipMessageProcessor> messageProcessors = new Vector<SipMessageProcessor>();

  /** The provider that created us. */
  private final ProtocolProviderServiceSipImpl sipProvider;

  /**
   * A reference to the persistent presence operation set that we use to match incoming messages to
   * <tt>Contact</tt>s and vice versa.
   */
  private OperationSetPresenceSipImpl opSetPersPresence = null;

  /** Hashtable containing the CSeq of each discussion */
  private long seqN = hashCode();

  /** Hashtable containing the message sent */
  private final Map<String, Message> sentMsg = new Hashtable<String, Message>(3);

  /** It can be implemented in some servers. */
  private final boolean offlineMessageSupported;

  /** Gives access to presence states for the Sip protocol. */
  private final SipStatusEnum sipStatusEnum;

  /**
   * Creates an instance of this operation set.
   *
   * @param provider a ref to the <tt>ProtocolProviderServiceImpl</tt> that created us and that
   *     we'll use for retrieving the underlying aim connection.
   */
  OperationSetBasicInstantMessagingSipImpl(ProtocolProviderServiceSipImpl provider) {
    this.sipProvider = provider;

    provider.addRegistrationStateChangeListener(new RegistrationStateListener());

    offlineMessageSupported =
        provider.getAccountID().getAccountPropertyBoolean("OFFLINE_MSG_SUPPORTED", false);

    sipProvider.registerMethodProcessor(
        Request.MESSAGE, new BasicInstantMessagingMethodProcessor());

    this.sipStatusEnum = sipProvider.getSipStatusEnum();
  }

  /**
   * Registers a SipMessageProcessor with this operation set so that it gets notifications of
   * successful message delivery, failure or reception of incoming messages..
   *
   * @param processor the <tt>SipMessageProcessor</tt> to register.
   */
  void addMessageProcessor(SipMessageProcessor processor) {
    synchronized (this.messageProcessors) {
      if (!this.messageProcessors.contains(processor)) {
        this.messageProcessors.add(processor);
      }
    }
  }

  /**
   * Unregisters <tt>processor</tt> so that it won't receive any further notifications upon
   * successful message delivery, failure or reception of incoming messages..
   *
   * @param processor the <tt>SipMessageProcessor</tt> to unregister.
   */
  void removeMessageProcessor(SipMessageProcessor processor) {
    synchronized (this.messageProcessors) {
      this.messageProcessors.remove(processor);
    }
  }

  public Message createMessage(
      String content, String contentType, String encoding, String subject) {
    return new MessageSipImpl(content, contentType, encoding, subject);
  }

  /**
   * Determines whether the protocol provider (or the protocol itself) support sending and receiving
   * offline messages. Most often this method would return true for protocols that support offline
   * messages and false for those that don't. It is however possible for a protocol to support these
   * messages and yet have a particular account that does not (i.e. feature not enabled on the
   * protocol server). In cases like this it is possible for this method to return true even when
   * offline messaging is not supported, and then have the sendMessage method throw an
   * OperationFailedException with code - OFFLINE_MESSAGES_NOT_SUPPORTED.
   *
   * @return <tt>true</tt> if the protocol supports offline messages and <tt>false</tt> otherwise.
   */
  public boolean isOfflineMessagingSupported() {
    return offlineMessageSupported;
  }

  /**
   * Determines whether the protocol supports the supplied content type
   *
   * @param contentType the type we want to check
   * @return <tt>true</tt> if the protocol supports it and <tt>false</tt> otherwise.
   */
  public boolean isContentTypeSupported(String contentType) {
    if (contentType.equals(DEFAULT_MIME_TYPE) || contentType.equals(HTML_MIME_TYPE)) return true;
    else return false;
  }

  /**
   * Sends the <tt>message</tt> to the destination indicated by the <tt>to</tt> contact.
   *
   * @param to the <tt>Contact</tt> to send <tt>message</tt> to
   * @param message the <tt>Message</tt> to send.
   * @throws java.lang.IllegalStateException if the underlying stack is not registered and
   *     initialized.
   * @throws java.lang.IllegalArgumentException if <tt>to</tt> is not an instance of ContactImpl.
   */
  public void sendInstantMessage(Contact to, Message message)
      throws IllegalStateException, IllegalArgumentException {
    if (!(to instanceof ContactSipImpl))
      throw new IllegalArgumentException("The specified contact is not a Sip contact." + to);

    assertConnected();

    // offline message
    if (to.getPresenceStatus().equals(sipStatusEnum.getStatus(SipStatusEnum.OFFLINE))
        && !offlineMessageSupported) {
      if (logger.isDebugEnabled()) logger.debug("trying to send a message to an offline contact");
      fireMessageDeliveryFailed(
          message, to, MessageDeliveryFailedEvent.OFFLINE_MESSAGES_NOT_SUPPORTED);
      return;
    }

    // create the message
    Request mes;
    try {
      mes = createMessageRequest(to, message);
    } catch (OperationFailedException ex) {
      logger.error("Failed to create the message.", ex);

      fireMessageDeliveryFailed(message, to, MessageDeliveryFailedEvent.INTERNAL_ERROR);
      return;
    }

    try {
      sendMessageRequest(mes, to, message);
    } catch (TransactionUnavailableException ex) {
      logger.error(
          "Failed to create messageTransaction.\n"
              + "This is most probably a network connection error.",
          ex);

      fireMessageDeliveryFailed(message, to, MessageDeliveryFailedEvent.NETWORK_FAILURE);
      return;
    } catch (SipException ex) {
      logger.error("Failed to send the message.", ex);

      fireMessageDeliveryFailed(message, to, MessageDeliveryFailedEvent.INTERNAL_ERROR);
      return;
    }
  }

  /**
   * Sends <tt>messageRequest</tt> to the specified destination and logs <tt>messageContent</tt> for
   * later use.
   *
   * @param messageRequest the <tt>SipRequest</tt> that we are about to send.
   * @param to the Contact that we are sending <tt>messageRequest</tt> to.
   * @param messageContent the SC <tt>Message</tt> that was used to create the <tt>Request</tt> .
   * @throws TransactionUnavailableException if we fail creating the transaction required to send
   *     <tt>messageRequest</tt>.
   * @throws SipException if we fail sending <tt>messageRequest</tt>.
   */
  void sendMessageRequest(Request messageRequest, Contact to, Message messageContent)
      throws TransactionUnavailableException, SipException {
    // Transaction
    ClientTransaction messageTransaction;
    SipProvider jainSipProvider = this.sipProvider.getDefaultJainSipProvider();

    messageTransaction = jainSipProvider.getNewClientTransaction(messageRequest);

    // send the message
    messageTransaction.sendRequest();

    // we register the reference to this message to retrieve it when
    // we'll receive the response message
    String key = ((CallIdHeader) messageRequest.getHeader(CallIdHeader.NAME)).getCallId();

    this.sentMsg.put(key, messageContent);
  }

  /**
   * Construct a <tt>Request</tt> represent a new message.
   *
   * @param to the <tt>Contact</tt> to send <tt>message</tt> to
   * @param message the <tt>Message</tt> to send.
   * @return a Message Request destined to the contact
   * @throws OperationFailedException if an error occurred during the creation of the request
   */
  Request createMessageRequest(Contact to, Message message) throws OperationFailedException {
    Address toAddress = null;
    try {
      toAddress = sipProvider.parseAddressString(to.getAddress());
    } catch (ParseException exc) {
      // Shouldn't happen
      logger.error("An unexpected error occurred while" + "constructing the address", exc);
      throw new OperationFailedException(
          "An unexpected error occurred while" + "constructing the address",
          OperationFailedException.INTERNAL_ERROR,
          exc);
    }

    // Call ID
    CallIdHeader callIdHeader = this.sipProvider.getDefaultJainSipProvider().getNewCallId();

    // CSeq
    CSeqHeader cSeqHeader = null;

    try {
      // protect seqN
      synchronized (this) {
        cSeqHeader = this.sipProvider.getHeaderFactory().createCSeqHeader(seqN++, Request.MESSAGE);
      }
    } catch (InvalidArgumentException ex) {
      // Shouldn't happen
      logger.error("An unexpected error occurred while" + "constructing the CSeqHeadder", ex);
      throw new OperationFailedException(
          "An unexpected error occurred while" + "constructing the CSeqHeadder",
          OperationFailedException.INTERNAL_ERROR,
          ex);
    } catch (ParseException exc) {
      // shouldn't happen
      logger.error("An unexpected error occurred while" + "constructing the CSeqHeadder", exc);
      throw new OperationFailedException(
          "An unexpected error occurred while" + "constructing the CSeqHeadder",
          OperationFailedException.INTERNAL_ERROR,
          exc);
    }

    // FromHeader and ToHeader
    String localTag = SipMessageFactory.generateLocalTag();
    FromHeader fromHeader = null;
    ToHeader toHeader = null;
    try {
      // FromHeader
      fromHeader =
          this.sipProvider
              .getHeaderFactory()
              .createFromHeader(sipProvider.getOurSipAddress(toAddress), localTag);

      // ToHeader
      toHeader = this.sipProvider.getHeaderFactory().createToHeader(toAddress, null);
    } catch (ParseException ex) {
      // these two should never happen.
      logger.error(
          "An unexpected error occurred while" + "constructing the FromHeader or ToHeader", ex);
      throw new OperationFailedException(
          "An unexpected error occurred while" + "constructing the FromHeader or ToHeader",
          OperationFailedException.INTERNAL_ERROR,
          ex);
    }

    // ViaHeaders
    ArrayList<ViaHeader> viaHeaders = this.sipProvider.getLocalViaHeaders(toAddress);

    // MaxForwards
    MaxForwardsHeader maxForwards = this.sipProvider.getMaxForwardsHeader();

    // Content params
    ContentTypeHeader contTypeHeader;
    ContentLengthHeader contLengthHeader;
    try {
      contTypeHeader =
          this.sipProvider
              .getHeaderFactory()
              .createContentTypeHeader(getType(message), getSubType(message));

      if (!DEFAULT_MIME_ENCODING.equalsIgnoreCase(message.getEncoding()))
        contTypeHeader.setParameter("charset", message.getEncoding());

      contLengthHeader =
          this.sipProvider.getHeaderFactory().createContentLengthHeader(message.getSize());
    } catch (ParseException ex) {
      // these two should never happen.
      logger.error("An unexpected error occurred while" + "constructing the content headers", ex);
      throw new OperationFailedException(
          "An unexpected error occurred while" + "constructing the content headers",
          OperationFailedException.INTERNAL_ERROR,
          ex);
    } catch (InvalidArgumentException exc) {
      // these two should never happen.
      logger.error(
          "An unexpected error occurred while" + "constructing the content length header", exc);
      throw new OperationFailedException(
          "An unexpected error occurred while" + "constructing the content length header",
          OperationFailedException.INTERNAL_ERROR,
          exc);
    }

    Request req;
    try {
      req =
          this.sipProvider
              .getMessageFactory()
              .createRequest(
                  toHeader.getAddress().getURI(),
                  Request.MESSAGE,
                  callIdHeader,
                  cSeqHeader,
                  fromHeader,
                  toHeader,
                  viaHeaders,
                  maxForwards,
                  contTypeHeader,
                  message.getRawData());
    } catch (ParseException ex) {
      // shouldn't happen
      logger.error("Failed to create message Request!", ex);
      throw new OperationFailedException(
          "Failed to create message Request!", OperationFailedException.INTERNAL_ERROR, ex);
    }

    req.addHeader(contLengthHeader);

    return req;
  }

  /**
   * Parses the content type of a message and return the type
   *
   * @param msg the Message to scan
   * @return the type of the message
   */
  private String getType(Message msg) {
    String type = msg.getContentType();

    return type.substring(0, type.indexOf('/'));
  }

  /**
   * Parses the content type of a message and return the subtype
   *
   * @param msg the Message to scan
   * @return the subtype of the message
   */
  private String getSubType(Message msg) {
    String subtype = msg.getContentType();

    return subtype.substring(subtype.indexOf('/') + 1);
  }

  /**
   * Utility method throwing an exception if the stack is not properly initialized.
   *
   * @throws java.lang.IllegalStateException if the underlying stack is not registered and
   *     initialized.
   */
  private void assertConnected() throws IllegalStateException {
    if (this.sipProvider == null)
      throw new IllegalStateException(
          "The provider must be non-null and signed on the "
              + "service before being able to communicate.");
    if (!this.sipProvider.isRegistered())
      throw new IllegalStateException(
          "The provider must be signed on the service before " + "being able to communicate.");
  }

  /** Our listener that will tell us when we're registered to */
  private class RegistrationStateListener implements RegistrationStateChangeListener {
    /**
     * The method is called by a ProtocolProvider implementation whenever a change in the
     * registration state of the corresponding provider had occurred.
     *
     * @param evt ProviderStatusChangeEvent the event describing the status change.
     */
    public void registrationStateChanged(RegistrationStateChangeEvent evt) {
      if (logger.isDebugEnabled())
        logger.debug(
            "The provider changed state from: " + evt.getOldState() + " to: " + evt.getNewState());

      if (evt.getNewState() == RegistrationState.REGISTERED) {
        opSetPersPresence =
            (OperationSetPresenceSipImpl)
                sipProvider.getOperationSet(OperationSetPersistentPresence.class);
      }
    }
  }

  /** Class for listening incoming packets. */
  private class BasicInstantMessagingMethodProcessor extends MethodProcessorAdapter {
    @Override
    public boolean processTimeout(TimeoutEvent timeoutEvent) {
      synchronized (messageProcessors) {
        for (SipMessageProcessor listener : messageProcessors)
          if (!listener.processTimeout(timeoutEvent, sentMsg)) return true;
      }

      // this is normaly handled by the SIP stack
      logger.error("Timeout event thrown : " + timeoutEvent.toString());

      if (timeoutEvent.isServerTransaction()) {
        logger.warn("The sender has probably not received our OK");
        return false;
      }

      Request req = timeoutEvent.getClientTransaction().getRequest();

      // get the content
      String content = null;
      try {
        content = new String(req.getRawContent(), getCharset(req));
      } catch (UnsupportedEncodingException ex) {
        logger.warn("failed to convert the message charset", ex);
        content = new String(req.getRawContent());
      }

      // to who this request has been sent ?
      ToHeader toHeader = (ToHeader) req.getHeader(ToHeader.NAME);

      if (toHeader == null) {
        logger.error("received a request without a to header");
        return false;
      }

      Contact to = opSetPersPresence.resolveContactID(toHeader.getAddress().getURI().toString());

      Message failedMessage = null;

      if (to == null) {
        logger.error(
            "timeout on a message sent to an unknown contact : "
                + toHeader.getAddress().getURI().toString());

        // we don't know what message it concerns, so create a new
        // one
        failedMessage = createMessage(content);
      } else {
        // try to retrieve the original message
        String key = ((CallIdHeader) req.getHeader(CallIdHeader.NAME)).getCallId();
        failedMessage = sentMsg.get(key);

        if (failedMessage == null) {
          // should never happen
          logger.error("Couldn't find the sent message.");

          // we don't know what the message is so create a new one
          // based on the content of the failed request.
          failedMessage = createMessage(content);
        }
      }

      // error for delivering the message
      fireMessageDeliveryFailed(
          // we don't know what message it concerns
          failedMessage, to, MessageDeliveryFailedEvent.INTERNAL_ERROR);
      return true;
    }

    /**
     * Process a request from a distant contact
     *
     * @param requestEvent the <tt>RequestEvent</tt> containing the newly received request.
     * @return <tt>true</tt> if the specified event has been handled by this processor and shouldn't
     *     be offered to other processors registered for the same method; <tt>false</tt>, otherwise
     */
    @Override
    public boolean processRequest(RequestEvent requestEvent) {
      synchronized (messageProcessors) {
        for (SipMessageProcessor listener : messageProcessors)
          if (!listener.processMessage(requestEvent)) return true;
      }

      // get the content
      String content = null;
      Request req = requestEvent.getRequest();
      try {

        content = new String(req.getRawContent(), getCharset(req));
      } catch (UnsupportedEncodingException ex) {
        if (logger.isDebugEnabled()) logger.debug("failed to convert the message charset");
        content = new String(requestEvent.getRequest().getRawContent());
      }

      // who sent this request ?
      FromHeader fromHeader = (FromHeader) requestEvent.getRequest().getHeader(FromHeader.NAME);

      if (fromHeader == null) {
        logger.error("received a request without a from header");
        return false;
      }

      Contact from =
          opSetPersPresence.resolveContactID(fromHeader.getAddress().getURI().toString());

      ContentTypeHeader ctheader = (ContentTypeHeader) req.getHeader(ContentTypeHeader.NAME);

      String ctype = null;
      String cencoding = null;

      if (ctheader == null) {
        ctype = DEFAULT_MIME_TYPE;
      } else {
        ctype = ctheader.getContentType() + "/" + ctheader.getContentSubType();
        cencoding = ctheader.getParameter("charset");
      }

      if (cencoding == null) cencoding = DEFAULT_MIME_ENCODING;

      Message newMessage = createMessage(content, ctype, cencoding, null);

      if (from == null) {
        if (logger.isDebugEnabled())
          logger.debug(
              "received a message from an unknown contact: "
                  + fromHeader.getAddress().getURI().toString());
        // create the volatile contact
        from = opSetPersPresence.createVolatileContact(fromHeader.getAddress().getURI().toString());
      }

      // answer ok
      try {
        Response ok =
            sipProvider.getMessageFactory().createResponse(Response.OK, requestEvent.getRequest());
        SipStackSharing.getOrCreateServerTransaction(requestEvent).sendResponse(ok);
      } catch (ParseException exc) {
        logger.error("failed to build the response", exc);
      } catch (SipException exc) {
        logger.error("failed to send the response : " + exc.getMessage(), exc);
      } catch (InvalidArgumentException exc) {
        if (logger.isDebugEnabled())
          logger.debug("Invalid argument for createResponse : " + exc.getMessage(), exc);
      }

      // fire an event
      MessageReceivedEvent msgReceivedEvt =
          new MessageReceivedEvent(newMessage, from, System.currentTimeMillis());
      fireMessageEvent(msgReceivedEvt);

      return true;
    }

    /**
     * Process a response from a distant contact.
     *
     * @param responseEvent the <tt>ResponseEvent</tt> containing the newly received SIP response.
     * @return <tt>true</tt> if the specified event has been handled by this processor and shouldn't
     *     be offered to other processors registered for the same method; <tt>false</tt>, otherwise
     */
    @Override
    public boolean processResponse(ResponseEvent responseEvent) {
      synchronized (messageProcessors) {
        for (SipMessageProcessor listener : messageProcessors)
          if (!listener.processResponse(responseEvent, sentMsg)) return true;
      }

      Request req = responseEvent.getClientTransaction().getRequest();
      int status = responseEvent.getResponse().getStatusCode();
      // content of the response
      String content = null;

      try {
        content = new String(req.getRawContent(), getCharset(req));
      } catch (UnsupportedEncodingException exc) {
        if (logger.isDebugEnabled()) logger.debug("failed to convert the message charset", exc);
        content = new String(req.getRawContent());
      }

      // to who did we send the original message ?
      ToHeader toHeader = (ToHeader) req.getHeader(ToHeader.NAME);

      if (toHeader == null) {
        // should never happen
        logger.error("send a request without a to header");
        return false;
      }

      Contact to = opSetPersPresence.resolveContactID(toHeader.getAddress().getURI().toString());

      if (to == null) {
        logger.error(
            "Error received a response from an unknown contact : "
                + toHeader.getAddress().getURI().toString()
                + " : "
                + responseEvent.getResponse().getStatusCode()
                + " "
                + responseEvent.getResponse().getReasonPhrase());

        // error for delivering the message
        fireMessageDeliveryFailed(
            // we don't know what message it concerns
            createMessage(content), to, MessageDeliveryFailedEvent.INTERNAL_ERROR);
        return false;
      }

      // we retrieve the original message
      String key = ((CallIdHeader) req.getHeader(CallIdHeader.NAME)).getCallId();

      Message newMessage = sentMsg.get(key);

      if (newMessage == null) {
        // should never happen
        logger.error("Couldn't find the message sent");

        // error for delivering the message
        fireMessageDeliveryFailed(
            // we don't know what message it is
            createMessage(content), to, MessageDeliveryFailedEvent.INTERNAL_ERROR);
        return true;
      }

      // status 401/407 = proxy authentification
      if (status >= 400 && status != 401 && status != 407) {
        if (logger.isInfoEnabled())
          logger.info(
              responseEvent.getResponse().getStatusCode()
                  + " "
                  + responseEvent.getResponse().getReasonPhrase());

        // error for delivering the message
        MessageDeliveryFailedEvent evt =
            new MessageDeliveryFailedEvent(
                newMessage,
                to,
                MessageDeliveryFailedEvent.NETWORK_FAILURE,
                System.currentTimeMillis(),
                responseEvent.getResponse().getStatusCode()
                    + " "
                    + responseEvent.getResponse().getReasonPhrase());
        fireMessageEvent(evt);
        sentMsg.remove(key);
      } else if (status == 401 || status == 407) {
        // proxy ask for authentification
        if (logger.isDebugEnabled())
          logger.debug(
              "proxy asks authentication : "
                  + responseEvent.getResponse().getStatusCode()
                  + " "
                  + responseEvent.getResponse().getReasonPhrase());

        ClientTransaction clientTransaction = responseEvent.getClientTransaction();
        SipProvider sourceProvider = (SipProvider) responseEvent.getSource();

        try {
          processAuthenticationChallenge(
              clientTransaction, responseEvent.getResponse(), sourceProvider);
        } catch (OperationFailedException ex) {
          logger.error("can't solve the challenge", ex);

          // error for delivering the message
          MessageDeliveryFailedEvent evt =
              new MessageDeliveryFailedEvent(
                  newMessage,
                  to,
                  MessageDeliveryFailedEvent.NETWORK_FAILURE,
                  System.currentTimeMillis(),
                  ex.getMessage());
          fireMessageEvent(evt);
          sentMsg.remove(key);
        }
      } else if (status >= 200) {
        if (logger.isDebugEnabled())
          logger.debug(
              "Ack received from the network : "
                  + responseEvent.getResponse().getStatusCode()
                  + " "
                  + responseEvent.getResponse().getReasonPhrase());

        // we delivered the message
        MessageDeliveredEvent msgDeliveredEvt =
            new MessageDeliveredEvent(newMessage, to, System.currentTimeMillis());

        fireMessageEvent(msgDeliveredEvt);

        // we don't need this message anymore
        sentMsg.remove(key);
      }

      return true;
    }

    /**
     * Try to find a charset in a MESSAGE request for the text content. If no charset is defined,
     * the default charset for text messages is returned.
     *
     * @param req the MESSAGE request in which to look for a charset
     * @return defined charset in the request or DEFAULT_MIME_ENCODING if no charset is specified
     */
    private String getCharset(Request req) {
      String charset = null;
      Header contentTypeHeader = req.getHeader(ContentTypeHeader.NAME);
      if (contentTypeHeader instanceof ContentTypeHeader)
        charset = ((ContentTypeHeader) contentTypeHeader).getParameter("charset");
      if (charset == null) charset = DEFAULT_MIME_ENCODING;
      return charset;
    }

    /**
     * Attempts to re-generate the corresponding request with the proper credentials.
     *
     * @param clientTransaction the corresponding transaction
     * @param response the challenge
     * @param jainSipProvider the provider that received the challenge
     * @throws OperationFailedException if processing the authentication challenge fails.
     */
    private void processAuthenticationChallenge(
        ClientTransaction clientTransaction, Response response, SipProvider jainSipProvider)
        throws OperationFailedException {
      try {
        if (logger.isDebugEnabled()) logger.debug("Authenticating a message request.");

        ClientTransaction retryTran = null;

        // we synch here to protect seqN increment
        synchronized (this) {
          retryTran =
              sipProvider
                  .getSipSecurityManager()
                  .handleChallenge(response, clientTransaction, jainSipProvider, seqN++);
        }

        if (retryTran == null) {
          if (logger.isTraceEnabled()) logger.trace("No password supplied or error occured!");
          return;
        }

        retryTran.sendRequest();
        return;
      } catch (Exception exc) {
        logger.error("We failed to authenticate a message request.", exc);

        throw new OperationFailedException(
            "Failed to authenticate" + "a message request",
            OperationFailedException.INTERNAL_ERROR,
            exc);
      }
    }
  }
}
예제 #17
0
/**
 * Mock {@link ChatRoom} implementation.
 *
 * @author Pawel Domas
 */
public class MockMultiUserChat extends AbstractChatRoom implements ChatRoom2 {
  /** The logger */
  private static final Logger logger = Logger.getLogger(MockMultiUserChat.class);

  private final String roomName;

  private final ProtocolProviderService protocolProvider;

  private volatile boolean isJoined;

  private final List<ChatRoomMember> members = new CopyOnWriteArrayList<ChatRoomMember>();

  private ChatRoomMember me;

  /**
   * Listeners that will be notified of changes in member status in the room such as member joined,
   * left or being kicked or dropped.
   */
  private final Vector<ChatRoomMemberPresenceListener> memberListeners =
      new Vector<ChatRoomMemberPresenceListener>();

  private final Vector<ChatRoomLocalUserRoleListener> localUserRoleListeners =
      new Vector<ChatRoomLocalUserRoleListener>();

  private final Vector<ChatRoomMemberRoleListener> memberRoleListeners =
      new Vector<ChatRoomMemberRoleListener>();

  public MockMultiUserChat(String roomName, ProtocolProviderService protocolProviderService) {
    this.roomName = roomName;
    this.protocolProvider = protocolProviderService;
  }

  @Override
  public String getName() {
    return roomName;
  }

  @Override
  public String getIdentifier() {
    return null;
  }

  @Override
  public void join() throws OperationFailedException {
    joinAs(
        getParentProvider()
            .getAccountID()
            .getAccountPropertyString(ProtocolProviderFactory.DISPLAY_NAME));
  }

  @Override
  public void join(byte[] password) throws OperationFailedException {
    join();
  }

  @Override
  public void joinAs(String nickname) throws OperationFailedException {
    joinAs(nickname, null);
  }

  private String createAddressForName(String nickname) {
    return roomName + "/" + nickname;
  }

  @Override
  public void joinAs(String nickname, byte[] password) throws OperationFailedException {
    if (isJoined) throw new OperationFailedException("Alread joined the room", 0);

    isJoined = true;

    MockRoomMember member = new MockRoomMember(createAddressForName(nickname), this);

    // FIXME: for mock purposes we are always the owner on join()
    boolean isOwner = true; // = members.size() == 0;

    synchronized (members) {
      members.add(member);

      me = member;

      fireMemberPresenceEvent(me, me, ChatRoomMemberPresenceChangeEvent.MEMBER_JOINED, null);
    }

    ChatRoomMemberRole oldRole = me.getRole();
    if (isOwner) {
      me.setRole(ChatRoomMemberRole.OWNER);
    }

    fireLocalUserRoleEvent(me, oldRole, true);
  }

  public MockRoomMember mockOwnerJoin(String name) {
    MockRoomMember member = new MockRoomMember(name, this);

    member.setRole(ChatRoomMemberRole.OWNER);

    mockJoin(member);

    return member;
  }

  public MockRoomMember mockJoin(String nickname) {
    return mockJoin(createMockRoomMember(nickname));
  }

  public MockRoomMember createMockRoomMember(String nickname) {
    return new MockRoomMember(createAddressForName(nickname), this);
  }

  public MockRoomMember mockJoin(MockRoomMember member) {
    synchronized (members) {
      members.add(member);

      fireMemberPresenceEvent(
          member, member, ChatRoomMemberPresenceChangeEvent.MEMBER_JOINED, null);

      return member;
    }
  }

  public void mockLeave(String memberName) {
    for (ChatRoomMember member : members) {
      if (member.getName().equals(memberName)) {
        mockLeave((MockRoomMember) member);
      }
    }
  }

  private void mockLeave(MockRoomMember member) {
    synchronized (members) {
      if (!members.remove(member)) {
        throw new RuntimeException("Member is not in the room " + member);
      }

      fireMemberPresenceEvent(member, member, ChatRoomMemberPresenceChangeEvent.MEMBER_LEFT, null);
    }
  }

  @Override
  public boolean isJoined() {
    return isJoined;
  }

  @Override
  public void leave() {
    if (!isJoined) return;

    isJoined = false;

    synchronized (members) {
      members.remove(me);

      fireMemberPresenceEvent(me, me, ChatRoomMemberPresenceChangeEvent.MEMBER_LEFT, null);
    }

    me = null;
  }

  @Override
  public String getSubject() {
    return null;
  }

  @Override
  public void setSubject(String subject) throws OperationFailedException {}

  @Override
  public String getUserNickname() {
    return null;
  }

  @Override
  public ChatRoomMemberRole getUserRole() {
    return null;
  }

  @Override
  public void setLocalUserRole(ChatRoomMemberRole role) throws OperationFailedException {}

  @Override
  public void setUserNickname(String nickname) throws OperationFailedException {}

  @Override
  public void addMemberPresenceListener(ChatRoomMemberPresenceListener listener) {
    synchronized (memberListeners) {
      memberListeners.add(listener);
    }
  }

  @Override
  public void removeMemberPresenceListener(ChatRoomMemberPresenceListener listener) {
    synchronized (memberListeners) {
      memberListeners.remove(listener);
    }
  }

  @Override
  public void addLocalUserRoleListener(ChatRoomLocalUserRoleListener listener) {
    localUserRoleListeners.add(listener);
  }

  @Override
  public void removelocalUserRoleListener(ChatRoomLocalUserRoleListener listener) {
    localUserRoleListeners.remove(listener);
  }

  @Override
  public void addMemberRoleListener(ChatRoomMemberRoleListener listener) {
    memberRoleListeners.add(listener);
  }

  @Override
  public void removeMemberRoleListener(ChatRoomMemberRoleListener listener) {
    memberRoleListeners.remove(listener);
  }

  @Override
  public void addPropertyChangeListener(ChatRoomPropertyChangeListener listener) {}

  @Override
  public void removePropertyChangeListener(ChatRoomPropertyChangeListener listener) {}

  @Override
  public void addMemberPropertyChangeListener(ChatRoomMemberPropertyChangeListener listener) {}

  @Override
  public void removeMemberPropertyChangeListener(ChatRoomMemberPropertyChangeListener listener) {}

  @Override
  public void invite(String userAddress, String reason) {}

  @Override
  public List<ChatRoomMember> getMembers() {
    return members;
  }

  @Override
  public int getMembersCount() {
    return members.size();
  }

  @Override
  public void addMessageListener(ChatRoomMessageListener listener) {}

  @Override
  public void removeMessageListener(ChatRoomMessageListener listener) {}

  @Override
  public Message createMessage(
      byte[] content, String contentType, String contentEncoding, String subject) {
    return null;
  }

  @Override
  public Message createMessage(String messageText) {
    return null;
  }

  @Override
  public void sendMessage(Message message) throws OperationFailedException {}

  @Override
  public ProtocolProviderService getParentProvider() {
    return protocolProvider;
  }

  @Override
  public Iterator<ChatRoomMember> getBanList() throws OperationFailedException {
    return null;
  }

  @Override
  public void banParticipant(ChatRoomMember chatRoomMember, String reason)
      throws OperationFailedException {}

  @Override
  public void kickParticipant(ChatRoomMember chatRoomMember, String reason)
      throws OperationFailedException {}

  @Override
  public ChatRoomConfigurationForm getConfigurationForm() throws OperationFailedException {
    return null;
  }

  @Override
  public boolean isSystem() {
    return false;
  }

  @Override
  public boolean isPersistent() {
    return false;
  }

  @Override
  public Contact getPrivateContactByNickname(String name) {
    return null;
  }

  @Override
  public void grantAdmin(String address) {}

  @Override
  public void grantMembership(String address) {}

  @Override
  public void grantModerator(String nickname) {
    MockRoomMember member = findMember(nickname);
    if (member == null) {
      logger.error("Member not found for nickname: " + nickname);
      return;
    }

    if (ChatRoomMemberRole.MODERATOR.compareTo(member.getRole()) >= 0) {
      // No action required
      return;
    }

    ChatRoomMemberRole oldRole = member.getRole();

    member.setRole(ChatRoomMemberRole.MODERATOR);

    fireMemberRoleEvent(member, oldRole);
  }

  private MockRoomMember findMember(String nickname) {
    for (ChatRoomMember member : members) {
      if (nickname.equals(member.getName())) return (MockRoomMember) member;
    }
    return null;
  }

  @Override
  public void grantOwnership(String address) {}

  @Override
  public void grantVoice(String nickname) {}

  @Override
  public void revokeAdmin(String address) {}

  @Override
  public void revokeMembership(String address) {}

  @Override
  public void revokeModerator(String nickname) {}

  @Override
  public void revokeOwnership(String address) {}

  @Override
  public void revokeVoice(String nickname) {}

  @Override
  public ConferenceDescription publishConference(ConferenceDescription cd, String name) {
    return null;
  }

  @Override
  public void updatePrivateContactPresenceStatus(String nickname) {}

  @Override
  public void updatePrivateContactPresenceStatus(Contact contact) {}

  @Override
  public boolean destroy(String reason, String alternateAddress) {
    return false;
  }

  @Override
  public List<String> getMembersWhiteList() {
    return null;
  }

  @Override
  public void setMembersWhiteList(List<String> members) {}

  /**
   * Creates the corresponding ChatRoomMemberPresenceChangeEvent and notifies all
   * <tt>ChatRoomMemberPresenceListener</tt>s that a ChatRoomMember has joined or left this
   * <tt>ChatRoom</tt>.
   *
   * @param member the <tt>ChatRoomMember</tt> that changed its presence status
   * @param actor the <tt>ChatRoomMember</tt> that participated as an actor in this event
   * @param eventID the identifier of the event
   * @param eventReason the reason of this event
   */
  private void fireMemberPresenceEvent(
      ChatRoomMember member, ChatRoomMember actor, String eventID, String eventReason) {
    ChatRoomMemberPresenceChangeEvent evt =
        new ChatRoomMemberPresenceChangeEvent(this, member, actor, eventID, eventReason);

    Iterable<ChatRoomMemberPresenceListener> listeners;
    synchronized (memberListeners) {
      listeners = new ArrayList<ChatRoomMemberPresenceListener>(memberListeners);
    }

    for (ChatRoomMemberPresenceListener listener : listeners) listener.memberPresenceChanged(evt);
  }

  private void fireLocalUserRoleEvent(
      ChatRoomMember member, ChatRoomMemberRole oldRole, boolean isInitial) {
    ChatRoomLocalUserRoleChangeEvent evt =
        new ChatRoomLocalUserRoleChangeEvent(this, oldRole, member.getRole(), isInitial);

    Iterable<ChatRoomLocalUserRoleListener> listeners;
    synchronized (localUserRoleListeners) {
      listeners = new ArrayList<ChatRoomLocalUserRoleListener>(localUserRoleListeners);
    }

    for (ChatRoomLocalUserRoleListener listener : listeners) listener.localUserRoleChanged(evt);
  }

  private void fireMemberRoleEvent(ChatRoomMember member, ChatRoomMemberRole oldRole) {
    ChatRoomMemberRoleChangeEvent evt =
        new ChatRoomMemberRoleChangeEvent(this, member, oldRole, member.getRole());

    Iterable<ChatRoomMemberRoleListener> listeners;
    synchronized (memberRoleListeners) {
      listeners = new ArrayList<ChatRoomMemberRoleListener>(memberRoleListeners);
    }

    for (ChatRoomMemberRoleListener listener : listeners) listener.memberRoleChanged(evt);
  }

  @Override
  public String toString() {
    return "MockMUC@" + hashCode() + "[" + this.roomName + ", " + protocolProvider + "]";
  }

  @Override
  public XmppChatMember findChatMember(String mucJid) {
    String nick = MucUtil.extractNickname(mucJid);

    return findMember(nick);
  }
}
/**
 * A straightforward implementation of the basic instant messaging operation set.
 *
 * @author Damian Minkov
 * @author Matthieu Helleringer
 * @author Alain Knaebel
 * @author Emil Ivov
 * @author Hristo Terezov
 */
public class OperationSetBasicInstantMessagingJabberImpl
    extends AbstractOperationSetBasicInstantMessaging implements OperationSetMessageCorrection {
  /** Our class logger */
  private static final Logger logger =
      Logger.getLogger(OperationSetBasicInstantMessagingJabberImpl.class);

  /** The maximum number of unread threads that we'd be notifying the user of. */
  private static final String PNAME_MAX_GMAIL_THREADS_PER_NOTIFICATION =
      "net.java.sip.communicator.impl.protocol.jabber." + "MAX_GMAIL_THREADS_PER_NOTIFICATION";

  /**
   * A table mapping contact addresses to full jids that can be used to target a specific resource
   * (rather than sending a message to all logged instances of a user).
   */
  private Map<String, StoredThreadID> jids = new Hashtable<String, StoredThreadID>();

  /** The most recent full JID used for the contact address. */
  private Map<String, String> recentJIDForAddress = new Hashtable<String, String>();
  /**
   * The smackMessageListener instance listens for incoming messages. Keep a reference of it so if
   * anything goes wrong we don't add two different instances.
   */
  private SmackMessageListener smackMessageListener = null;

  /**
   * Contains the complete jid of a specific user and the time that it was last used so that we
   * could remove it after a certain point.
   */
  public static class StoredThreadID {
    /** The time that we last sent or received a message from this jid */
    long lastUpdatedTime;

    /** The last chat used, this way we will reuse the thread-id */
    String threadID;
  }

  /** A prefix helps to make sure that thread ID's are unique across mutliple instances. */
  private static String prefix = StringUtils.randomString(5);

  /**
   * Keeps track of the current increment, which is appended to the prefix to forum a unique thread
   * ID.
   */
  private static long id = 0;

  /**
   * The number of milliseconds that we preserve threads with no traffic before considering them
   * dead.
   */
  private static final long JID_INACTIVITY_TIMEOUT = 10 * 60 * 1000; // 10 min.

  /**
   * Indicates the time of the last Mailbox report that we received from Google (if this is a Google
   * server we are talking to). Should be included in all following mailbox queries
   */
  private long lastReceivedMailboxResultTime = -1;

  /** The provider that created us. */
  private final ProtocolProviderServiceJabberImpl jabberProvider;

  /**
   * A reference to the persistent presence operation set that we use to match incoming messages to
   * <tt>Contact</tt>s and vice versa.
   */
  private OperationSetPersistentPresenceJabberImpl opSetPersPresence = null;

  /** The opening BODY HTML TAG: &ltbody&gt */
  private static final String OPEN_BODY_TAG = "<body>";

  /** The closing BODY HTML TAG: &ltbody&gt */
  private static final String CLOSE_BODY_TAG = "</body>";

  /** The html namespace used as feature XHTMLManager.namespace */
  private static final String HTML_NAMESPACE = "http://jabber.org/protocol/xhtml-im";

  /** List of filters to be used to filter which messages to handle current Operation Set. */
  private List<PacketFilter> packetFilters = new ArrayList<PacketFilter>();

  /** Whether carbon is enabled or not. */
  private boolean isCarbonEnabled = false;

  /**
   * Creates an instance of this operation set.
   *
   * @param provider a reference to the <tt>ProtocolProviderServiceImpl</tt> that created us and
   *     that we'll use for retrieving the underlying aim connection.
   */
  OperationSetBasicInstantMessagingJabberImpl(ProtocolProviderServiceJabberImpl provider) {
    this.jabberProvider = provider;

    packetFilters.add(new GroupMessagePacketFilter());
    packetFilters.add(new PacketTypeFilter(org.jivesoftware.smack.packet.Message.class));

    provider.addRegistrationStateChangeListener(new RegistrationStateListener());

    ProviderManager man = ProviderManager.getInstance();
    MessageCorrectionExtensionProvider extProvider = new MessageCorrectionExtensionProvider();
    man.addExtensionProvider(
        MessageCorrectionExtension.ELEMENT_NAME, MessageCorrectionExtension.NAMESPACE, extProvider);
  }

  /**
   * Create a Message instance with the specified UID, content type and a default encoding. This
   * method can be useful when message correction is required. One can construct the corrected
   * message to have the same UID as the message before correction.
   *
   * @param messageText the string content of the message.
   * @param contentType the MIME-type for <tt>content</tt>
   * @param messageUID the unique identifier of this message.
   * @return Message the newly created message
   */
  public Message createMessageWithUID(String messageText, String contentType, String messageUID) {
    return new MessageJabberImpl(messageText, contentType, DEFAULT_MIME_ENCODING, null, messageUID);
  }

  /**
   * Create a Message instance for sending arbitrary MIME-encoding content.
   *
   * @param content content value
   * @param contentType the MIME-type for <tt>content</tt>
   * @return the newly created message.
   */
  public Message createMessage(String content, String contentType) {
    return createMessage(content, contentType, DEFAULT_MIME_ENCODING, null);
  }

  /**
   * Create a Message instance for sending arbitrary MIME-encoding content.
   *
   * @param content content value
   * @param contentType the MIME-type for <tt>content</tt>
   * @param subject the Subject of the message that we'd like to create.
   * @param encoding the enconding of the message that we will be sending.
   * @return the newly created message.
   */
  @Override
  public Message createMessage(
      String content, String contentType, String encoding, String subject) {
    return new MessageJabberImpl(content, contentType, encoding, subject);
  }

  Message createMessage(String content, String contentType, String messageUID) {
    return new MessageJabberImpl(content, contentType, DEFAULT_MIME_ENCODING, null, messageUID);
  }

  /**
   * Determines wheter the protocol provider (or the protocol itself) support sending and receiving
   * offline messages. Most often this method would return true for protocols that support offline
   * messages and false for those that don't. It is however possible for a protocol to support these
   * messages and yet have a particular account that does not (i.e. feature not enabled on the
   * protocol server). In cases like this it is possible for this method to return true even when
   * offline messaging is not supported, and then have the sendMessage method throw an
   * OperationFailedException with code - OFFLINE_MESSAGES_NOT_SUPPORTED.
   *
   * @return <tt>true</tt> if the protocol supports offline messages and <tt>false</tt> otherwise.
   */
  public boolean isOfflineMessagingSupported() {
    return true;
  }

  /**
   * Determines wheter the protocol supports the supplied content type
   *
   * @param contentType the type we want to check
   * @return <tt>true</tt> if the protocol supports it and <tt>false</tt> otherwise.
   */
  public boolean isContentTypeSupported(String contentType) {
    return (contentType.equals(DEFAULT_MIME_TYPE) || contentType.equals(HTML_MIME_TYPE));
  }

  /**
   * Determines whether the protocol supports the supplied content type for the given contact.
   *
   * @param contentType the type we want to check
   * @param contact contact which is checked for supported contentType
   * @return <tt>true</tt> if the contact supports it and <tt>false</tt> otherwise.
   */
  @Override
  public boolean isContentTypeSupported(String contentType, Contact contact) {
    // by default we support default mime type, for other mimetypes
    // method must be overriden
    if (contentType.equals(DEFAULT_MIME_TYPE)) return true;
    else if (contentType.equals(HTML_MIME_TYPE)) {
      String toJID = recentJIDForAddress.get(contact.getAddress());

      if (toJID == null) toJID = contact.getAddress();

      return jabberProvider.isFeatureListSupported(toJID, HTML_NAMESPACE);
    }

    return false;
  }

  /**
   * Remove from our <tt>jids</tt> map all entries that have not seen any activity (i.e. neither
   * outgoing nor incoming messags) for more than JID_INACTIVITY_TIMEOUT. Note that this method is
   * not synchronous and that it is only meant for use by the {@link #getThreadIDForAddress(String)}
   * and {@link #putJidForAddress(String, String)}
   */
  private void purgeOldJids() {
    long currentTime = System.currentTimeMillis();

    Iterator<Map.Entry<String, StoredThreadID>> entries = jids.entrySet().iterator();

    while (entries.hasNext()) {
      Map.Entry<String, StoredThreadID> entry = entries.next();
      StoredThreadID target = entry.getValue();

      if (currentTime - target.lastUpdatedTime > JID_INACTIVITY_TIMEOUT) entries.remove();
    }
  }

  /**
   * Returns the last jid that the party with the specified <tt>address</tt> contacted us from or
   * <tt>null</tt>(or bare jid) if we don't have a jid for the specified <tt>address</tt> yet. The
   * method would also purge all entries that haven't seen any activity (i.e. no one has tried to
   * get or remap it) for a delay longer than <tt>JID_INACTIVITY_TIMEOUT</tt>.
   *
   * @param jid the <tt>jid</tt> that we'd like to obtain a threadID for.
   * @return the last jid that the party with the specified <tt>address</tt> contacted us from or
   *     <tt>null</tt> if we don't have a jid for the specified <tt>address</tt> yet.
   */
  String getThreadIDForAddress(String jid) {
    synchronized (jids) {
      purgeOldJids();
      StoredThreadID ta = jids.get(jid);

      if (ta == null) return null;

      ta.lastUpdatedTime = System.currentTimeMillis();

      return ta.threadID;
    }
  }

  /**
   * Maps the specified <tt>address</tt> to <tt>jid</tt>. The point of this method is to allow us to
   * send all messages destined to the contact with the specified <tt>address</tt> to the
   * <tt>jid</tt> that they last contacted us from.
   *
   * @param threadID the threadID of conversation.
   * @param jid the jid (i.e. address/resource) that the contact with the specified <tt>address</tt>
   *     last contacted us from.
   */
  private void putJidForAddress(String jid, String threadID) {
    synchronized (jids) {
      purgeOldJids();

      StoredThreadID ta = jids.get(jid);

      if (ta == null) {
        ta = new StoredThreadID();
        jids.put(jid, ta);
      }

      recentJIDForAddress.put(StringUtils.parseBareAddress(jid), jid);

      ta.lastUpdatedTime = System.currentTimeMillis();
      ta.threadID = threadID;
    }
  }

  /**
   * Helper function used to send a message to a contact, with the given extensions attached.
   *
   * @param to The contact to send the message to.
   * @param toResource The resource to send the message to or null if no resource has been specified
   * @param message The message to send.
   * @param extensions The XMPP extensions that should be attached to the message before sending.
   * @return The MessageDeliveryEvent that resulted after attempting to send this message, so the
   *     calling function can modify it if needed.
   */
  private MessageDeliveredEvent sendMessage(
      Contact to, ContactResource toResource, Message message, PacketExtension[] extensions) {
    if (!(to instanceof ContactJabberImpl))
      throw new IllegalArgumentException("The specified contact is not a Jabber contact." + to);

    assertConnected();

    org.jivesoftware.smack.packet.Message msg = new org.jivesoftware.smack.packet.Message();

    String toJID = null;

    if (toResource != null) {
      if (toResource.equals(ContactResource.BASE_RESOURCE)) {
        toJID = to.getAddress();
      } else toJID = ((ContactResourceJabberImpl) toResource).getFullJid();
    }

    if (toJID == null) {
      toJID = to.getAddress();
    }

    msg.setPacketID(message.getMessageUID());
    msg.setTo(toJID);

    for (PacketExtension ext : extensions) {
      msg.addExtension(ext);
    }

    if (logger.isTraceEnabled())
      logger.trace("Will send a message to:" + toJID + " chat.jid=" + toJID);

    MessageDeliveredEvent msgDeliveryPendingEvt =
        new MessageDeliveredEvent(message, to, toResource);

    MessageDeliveredEvent[] transformedEvents =
        messageDeliveryPendingTransform(msgDeliveryPendingEvt);

    if (transformedEvents == null || transformedEvents.length == 0) return null;

    for (MessageDeliveredEvent event : transformedEvents) {
      String content = event.getSourceMessage().getContent();

      if (message.getContentType().equals(HTML_MIME_TYPE)) {
        msg.setBody(Html2Text.extractText(content));

        // Check if the other user supports XHTML messages
        // make sure we use our discovery manager as it caches calls
        if (jabberProvider.isFeatureListSupported(toJID, HTML_NAMESPACE)) {
          // Add the XHTML text to the message
          XHTMLManager.addBody(msg, OPEN_BODY_TAG + content + CLOSE_BODY_TAG);
        }
      } else {
        // this is plain text so keep it as it is.
        msg.setBody(content);
      }

      // msg.addExtension(new Version());

      if (event.isMessageEncrypted() && isCarbonEnabled) {
        msg.addExtension(new CarbonPacketExtension.PrivateExtension());
      }

      MessageEventManager.addNotificationsRequests(msg, true, false, false, true);

      String threadID = getThreadIDForAddress(toJID);
      if (threadID == null) threadID = nextThreadID();

      msg.setThread(threadID);
      msg.setType(org.jivesoftware.smack.packet.Message.Type.chat);
      msg.setFrom(jabberProvider.getConnection().getUser());

      jabberProvider.getConnection().sendPacket(msg);

      putJidForAddress(toJID, threadID);
    }

    return new MessageDeliveredEvent(message, to, toResource);
  }

  /**
   * Sends the <tt>message</tt> to the destination indicated by the <tt>to</tt> contact.
   *
   * @param to the <tt>Contact</tt> to send <tt>message</tt> to
   * @param message the <tt>Message</tt> to send.
   * @throws java.lang.IllegalStateException if the underlying stack is not registered and
   *     initialized.
   * @throws java.lang.IllegalArgumentException if <tt>to</tt> is not an instance of ContactImpl.
   */
  public void sendInstantMessage(Contact to, Message message)
      throws IllegalStateException, IllegalArgumentException {
    sendInstantMessage(to, null, message);
  }

  /**
   * Sends the <tt>message</tt> to the destination indicated by the <tt>to</tt>. Provides a default
   * implementation of this method.
   *
   * @param to the <tt>Contact</tt> to send <tt>message</tt> to
   * @param toResource the resource to which the message should be send
   * @param message the <tt>Message</tt> to send.
   * @throws java.lang.IllegalStateException if the underlying ICQ stack is not registered and
   *     initialized.
   * @throws java.lang.IllegalArgumentException if <tt>to</tt> is not an instance belonging to the
   *     underlying implementation.
   */
  @Override
  public void sendInstantMessage(Contact to, ContactResource toResource, Message message)
      throws IllegalStateException, IllegalArgumentException {
    MessageDeliveredEvent msgDelivered =
        sendMessage(to, toResource, message, new PacketExtension[0]);

    fireMessageEvent(msgDelivered);
  }

  /**
   * Replaces the message with ID <tt>correctedMessageUID</tt> sent to the contact <tt>to</tt> with
   * the message <tt>message</tt>
   *
   * @param to The contact to send the message to.
   * @param message The new message.
   * @param correctedMessageUID The ID of the message being replaced.
   */
  public void correctMessage(
      Contact to, ContactResource resource, Message message, String correctedMessageUID) {
    PacketExtension[] exts = new PacketExtension[1];
    exts[0] = new MessageCorrectionExtension(correctedMessageUID);
    MessageDeliveredEvent msgDelivered = sendMessage(to, resource, message, exts);
    msgDelivered.setCorrectedMessageUID(correctedMessageUID);
    fireMessageEvent(msgDelivered);
  }

  /**
   * Utility method throwing an exception if the stack is not properly initialized.
   *
   * @throws java.lang.IllegalStateException if the underlying stack is not registered and
   *     initialized.
   */
  private void assertConnected() throws IllegalStateException {
    if (opSetPersPresence == null) {
      throw new IllegalStateException(
          "The provider must be signed on the service before" + " being able to communicate.");
    } else opSetPersPresence.assertConnected();
  }

  /** Our listener that will tell us when we're registered to */
  private class RegistrationStateListener implements RegistrationStateChangeListener {
    /**
     * The method is called by a ProtocolProvider implementation whenever a change in the
     * registration state of the corresponding provider had occurred.
     *
     * @param evt ProviderStatusChangeEvent the event describing the status change.
     */
    public void registrationStateChanged(RegistrationStateChangeEvent evt) {
      if (logger.isDebugEnabled())
        logger.debug(
            "The provider changed state from: " + evt.getOldState() + " to: " + evt.getNewState());

      if (evt.getNewState() == RegistrationState.REGISTERING) {
        opSetPersPresence =
            (OperationSetPersistentPresenceJabberImpl)
                jabberProvider.getOperationSet(OperationSetPersistentPresence.class);

        if (smackMessageListener == null) {
          smackMessageListener = new SmackMessageListener();
        } else {
          // make sure this listener is not already installed in this
          // connection
          jabberProvider.getConnection().removePacketListener(smackMessageListener);
        }

        jabberProvider
            .getConnection()
            .addPacketListener(
                smackMessageListener,
                new AndFilter(packetFilters.toArray(new PacketFilter[packetFilters.size()])));
      } else if (evt.getNewState() == RegistrationState.REGISTERED) {
        new Thread(
                new Runnable() {
                  @Override
                  public void run() {
                    initAdditionalServices();
                  }
                })
            .start();
      } else if (evt.getNewState() == RegistrationState.UNREGISTERED
          || evt.getNewState() == RegistrationState.CONNECTION_FAILED
          || evt.getNewState() == RegistrationState.AUTHENTICATION_FAILED) {
        if (jabberProvider.getConnection() != null) {
          if (smackMessageListener != null)
            jabberProvider.getConnection().removePacketListener(smackMessageListener);
        }

        smackMessageListener = null;
      }
    }
  }

  /** Initialize additional services, like gmail notifications and message carbons. */
  private void initAdditionalServices() {
    // subscribe for Google (Gmail or Google Apps) notifications
    // for new mail messages.
    boolean enableGmailNotifications =
        jabberProvider
            .getAccountID()
            .getAccountPropertyBoolean("GMAIL_NOTIFICATIONS_ENABLED", false);

    if (enableGmailNotifications) subscribeForGmailNotifications();

    boolean enableCarbon =
        isCarbonSupported()
            && !jabberProvider
                .getAccountID()
                .getAccountPropertyBoolean(ProtocolProviderFactory.IS_CARBON_DISABLED, false);
    if (enableCarbon) {
      enableDisableCarbon(true);
    } else {
      isCarbonEnabled = false;
    }
  }

  /**
   * Sends enable or disable carbon packet to the server.
   *
   * @param enable if <tt>true</tt> sends enable packet otherwise sends disable packet.
   */
  private void enableDisableCarbon(final boolean enable) {
    IQ iq =
        new IQ() {

          @Override
          public String getChildElementXML() {
            return "<" + (enable ? "enable" : "disable") + " xmlns='urn:xmpp:carbons:2' />";
          }
        };

    Packet response = null;
    try {
      PacketCollector packetCollector =
          jabberProvider
              .getConnection()
              .createPacketCollector(new PacketIDFilter(iq.getPacketID()));
      iq.setFrom(jabberProvider.getOurJID());
      iq.setType(IQ.Type.SET);
      jabberProvider.getConnection().sendPacket(iq);
      response = packetCollector.nextResult(SmackConfiguration.getPacketReplyTimeout());

      packetCollector.cancel();
    } catch (Exception e) {
      logger.error("Failed to enable carbon.", e);
    }

    isCarbonEnabled = false;

    if (response == null) {
      logger.error("Failed to enable carbon. No response is received.");
    } else if (response.getError() != null) {
      logger.error("Failed to enable carbon: " + response.getError());
    } else if (!(response instanceof IQ) || !((IQ) response).getType().equals(IQ.Type.RESULT)) {
      logger.error("Failed to enable carbon. The response is not correct.");
    } else {
      isCarbonEnabled = true;
    }
  }

  /**
   * Checks whether the carbon is supported by the server or not.
   *
   * @return <tt>true</tt> if carbon is supported by the server and <tt>false</tt> if not.
   */
  private boolean isCarbonSupported() {
    try {
      return jabberProvider
          .getDiscoveryManager()
          .discoverInfo(jabberProvider.getAccountID().getService())
          .containsFeature(CarbonPacketExtension.NAMESPACE);
    } catch (XMPPException e) {
      logger.warn("Failed to retrieve carbon support." + e.getMessage());
    }
    return false;
  }

  /** The listener that we use in order to handle incoming messages. */
  @SuppressWarnings("unchecked")
  private class SmackMessageListener implements PacketListener {
    /**
     * Handles incoming messages and dispatches whatever events are necessary.
     *
     * @param packet the packet that we need to handle (if it is a message).
     */
    public void processPacket(Packet packet) {
      if (!(packet instanceof org.jivesoftware.smack.packet.Message)) return;

      org.jivesoftware.smack.packet.Message msg = (org.jivesoftware.smack.packet.Message) packet;

      boolean isForwardedSentMessage = false;
      if (msg.getBody() == null) {

        CarbonPacketExtension carbonExt =
            (CarbonPacketExtension) msg.getExtension(CarbonPacketExtension.NAMESPACE);
        if (carbonExt == null) return;

        isForwardedSentMessage =
            (carbonExt.getElementName() == CarbonPacketExtension.SENT_ELEMENT_NAME);
        List<ForwardedPacketExtension> extensions =
            carbonExt.getChildExtensionsOfType(ForwardedPacketExtension.class);
        if (extensions.isEmpty()) return;
        ForwardedPacketExtension forwardedExt = extensions.get(0);
        msg = forwardedExt.getMessage();
        if (msg == null || msg.getBody() == null) return;
      }

      Object multiChatExtension = msg.getExtension("x", "http://jabber.org/protocol/muc#user");

      // its not for us
      if (multiChatExtension != null) return;

      String userFullId = isForwardedSentMessage ? msg.getTo() : msg.getFrom();

      String userBareID = StringUtils.parseBareAddress(userFullId);

      boolean isPrivateMessaging = false;
      ChatRoom privateContactRoom = null;
      OperationSetMultiUserChatJabberImpl mucOpSet =
          (OperationSetMultiUserChatJabberImpl)
              jabberProvider.getOperationSet(OperationSetMultiUserChat.class);
      if (mucOpSet != null) privateContactRoom = mucOpSet.getChatRoom(userBareID);

      if (privateContactRoom != null) {
        isPrivateMessaging = true;
      }

      if (logger.isDebugEnabled()) {
        if (logger.isDebugEnabled())
          logger.debug("Received from " + userBareID + " the message " + msg.toXML());
      }

      Message newMessage = createMessage(msg.getBody(), DEFAULT_MIME_TYPE, msg.getPacketID());

      // check if the message is available in xhtml
      PacketExtension ext = msg.getExtension("http://jabber.org/protocol/xhtml-im");

      if (ext != null) {
        XHTMLExtension xhtmlExt = (XHTMLExtension) ext;

        // parse all bodies
        Iterator<String> bodies = xhtmlExt.getBodies();
        StringBuffer messageBuff = new StringBuffer();
        while (bodies.hasNext()) {
          String body = bodies.next();
          messageBuff.append(body);
        }

        if (messageBuff.length() > 0) {
          // we remove body tags around message cause their
          // end body tag is breaking
          // the visualization as html in the UI
          String receivedMessage =
              messageBuff
                  .toString()
                  // removes body start tag
                  .replaceAll("\\<[bB][oO][dD][yY].*?>", "")
                  // removes body end tag
                  .replaceAll("\\</[bB][oO][dD][yY].*?>", "");

          // for some reason &apos; is not rendered correctly
          // from our ui, lets use its equivalent. Other
          // similar chars(< > & ") seem ok.
          receivedMessage = receivedMessage.replaceAll("&apos;", "&#39;");

          newMessage = createMessage(receivedMessage, HTML_MIME_TYPE, msg.getPacketID());
        }
      }

      PacketExtension correctionExtension = msg.getExtension(MessageCorrectionExtension.NAMESPACE);
      String correctedMessageUID = null;
      if (correctionExtension != null) {
        correctedMessageUID =
            ((MessageCorrectionExtension) correctionExtension).getCorrectedMessageUID();
      }

      Contact sourceContact =
          opSetPersPresence.findContactByID((isPrivateMessaging ? userFullId : userBareID));
      if (msg.getType() == org.jivesoftware.smack.packet.Message.Type.error) {
        // error which is multichat and we don't know about the contact
        // is a muc message error which is missing muc extension
        // and is coming from the room, when we try to send message to
        // room which was deleted or offline on the server
        if (isPrivateMessaging && sourceContact == null) {
          if (privateContactRoom != null) {
            XMPPError error = packet.getError();
            int errorResultCode = ChatRoomMessageDeliveryFailedEvent.UNKNOWN_ERROR;

            if (error != null && error.getCode() == 403) {
              errorResultCode = ChatRoomMessageDeliveryFailedEvent.FORBIDDEN;
            }

            String errorReason = error.getMessage();

            ChatRoomMessageDeliveryFailedEvent evt =
                new ChatRoomMessageDeliveryFailedEvent(
                    privateContactRoom, null, errorResultCode, errorReason, new Date(), newMessage);
            ((ChatRoomJabberImpl) privateContactRoom).fireMessageEvent(evt);
          }

          return;
        }

        if (logger.isInfoEnabled()) logger.info("Message error received from " + userBareID);

        int errorResultCode = MessageDeliveryFailedEvent.UNKNOWN_ERROR;
        if (packet.getError() != null) {
          int errorCode = packet.getError().getCode();

          if (errorCode == 503) {
            org.jivesoftware.smackx.packet.MessageEvent msgEvent =
                (org.jivesoftware.smackx.packet.MessageEvent)
                    packet.getExtension("x", "jabber:x:event");
            if (msgEvent != null && msgEvent.isOffline()) {
              errorResultCode = MessageDeliveryFailedEvent.OFFLINE_MESSAGES_NOT_SUPPORTED;
            }
          }
        }

        if (sourceContact == null) {
          sourceContact = opSetPersPresence.createVolatileContact(userFullId, isPrivateMessaging);
        }

        MessageDeliveryFailedEvent ev =
            new MessageDeliveryFailedEvent(
                newMessage, sourceContact, correctedMessageUID, errorResultCode);

        // ev = messageDeliveryFailedTransform(ev);

        if (ev != null) fireMessageEvent(ev);
        return;
      }
      putJidForAddress(userFullId, msg.getThread());

      // In the second condition we filter all group chat messages,
      // because they are managed by the multi user chat operation set.
      if (sourceContact == null) {
        if (logger.isDebugEnabled())
          logger.debug("received a message from an unknown contact: " + userBareID);
        // create the volatile contact
        sourceContact = opSetPersPresence.createVolatileContact(userFullId, isPrivateMessaging);
      }

      Date timestamp = new Date();
      // Check for XEP-0091 timestamp (deprecated)
      PacketExtension delay = msg.getExtension("x", "jabber:x:delay");
      if (delay != null && delay instanceof DelayInformation) {
        timestamp = ((DelayInformation) delay).getStamp();
      }
      // check for XEP-0203 timestamp
      delay = msg.getExtension("delay", "urn:xmpp:delay");
      if (delay != null && delay instanceof DelayInfo) {
        timestamp = ((DelayInfo) delay).getStamp();
      }

      ContactResource resource = ((ContactJabberImpl) sourceContact).getResourceFromJid(userFullId);

      EventObject msgEvt = null;
      if (!isForwardedSentMessage)
        msgEvt =
            new MessageReceivedEvent(
                newMessage,
                sourceContact,
                resource,
                timestamp,
                correctedMessageUID,
                isPrivateMessaging,
                privateContactRoom);
      else msgEvt = new MessageDeliveredEvent(newMessage, sourceContact, timestamp);
      // msgReceivedEvt = messageReceivedTransform(msgReceivedEvt);
      if (msgEvt != null) fireMessageEvent(msgEvt);
    }
  }

  /** A filter that prevents this operation set from handling multi user chat messages. */
  private static class GroupMessagePacketFilter implements PacketFilter {
    /**
     * Returns <tt>true</tt> if <tt>packet</tt> is a <tt>Message</tt> and false otherwise.
     *
     * @param packet the packet that we need to check.
     * @return <tt>true</tt> if <tt>packet</tt> is a <tt>Message</tt> and false otherwise.
     */
    public boolean accept(Packet packet) {
      if (!(packet instanceof org.jivesoftware.smack.packet.Message)) return false;

      org.jivesoftware.smack.packet.Message msg = (org.jivesoftware.smack.packet.Message) packet;

      return !msg.getType().equals(org.jivesoftware.smack.packet.Message.Type.groupchat);
    }
  }

  /**
   * Subscribes this provider as interested in receiving notifications for new mail messages from
   * Google mail services such as Gmail or Google Apps.
   */
  private void subscribeForGmailNotifications() {
    // first check support for the notification service
    String accountIDService = jabberProvider.getAccountID().getService();
    boolean notificationsAreSupported =
        jabberProvider.isFeatureSupported(accountIDService, NewMailNotificationIQ.NAMESPACE);

    if (!notificationsAreSupported) {
      if (logger.isDebugEnabled())
        logger.debug(
            accountIDService
                + " does not seem to provide a Gmail notification "
                + " service so we won't be trying to subscribe for it");
      return;
    }

    if (logger.isDebugEnabled())
      logger.debug(
          accountIDService
              + " seems to provide a Gmail notification "
              + " service so we will try to subscribe for it");

    ProviderManager providerManager = ProviderManager.getInstance();

    providerManager.addIQProvider(
        MailboxIQ.ELEMENT_NAME, MailboxIQ.NAMESPACE, new MailboxIQProvider());
    providerManager.addIQProvider(
        NewMailNotificationIQ.ELEMENT_NAME,
        NewMailNotificationIQ.NAMESPACE,
        new NewMailNotificationProvider());

    Connection connection = jabberProvider.getConnection();

    connection.addPacketListener(new MailboxIQListener(), new PacketTypeFilter(MailboxIQ.class));
    connection.addPacketListener(
        new NewMailNotificationListener(), new PacketTypeFilter(NewMailNotificationIQ.class));

    if (opSetPersPresence.getCurrentStatusMessage().equals(JabberStatusEnum.OFFLINE)) return;

    // create a query with -1 values for newer-than-tid and
    // newer-than-time attributes
    MailboxQueryIQ mailboxQuery = new MailboxQueryIQ();

    if (logger.isTraceEnabled())
      logger.trace(
          "sending mailNotification for acc: "
              + jabberProvider.getAccountID().getAccountUniqueID());
    jabberProvider.getConnection().sendPacket(mailboxQuery);
  }

  /**
   * Creates an html description of the specified mailbox.
   *
   * @param mailboxIQ the mailboxIQ that we are to describe.
   * @return an html description of <tt>mailboxIQ</tt>
   */
  private String createMailboxDescription(MailboxIQ mailboxIQ) {
    int threadCount = mailboxIQ.getThreadCount();

    String resourceHeaderKey =
        threadCount > 1 ? "service.gui.NEW_GMAIL_MANY_HEADER" : "service.gui.NEW_GMAIL_HEADER";

    String resourceFooterKey =
        threadCount > 1 ? "service.gui.NEW_GMAIL_MANY_FOOTER" : "service.gui.NEW_GMAIL_FOOTER";

    // FIXME Escape HTML!
    String newMailHeader =
        JabberActivator.getResources()
            .getI18NString(
                resourceHeaderKey,
                new String[] {
                  jabberProvider.getAccountID().getService(), // {0} - service name
                  mailboxIQ.getUrl(), // {1} - inbox URI
                  Integer.toString(threadCount) // {2} - thread count
                });

    StringBuilder message = new StringBuilder(newMailHeader);

    // we now start an html table for the threads.
    message.append("<table width=100% cellpadding=2 cellspacing=0 ");
    message.append("border=0 bgcolor=#e8eef7>");

    Iterator<MailThreadInfo> threads = mailboxIQ.threads();

    String maxThreadsStr =
        (String)
            JabberActivator.getConfigurationService()
                .getProperty(PNAME_MAX_GMAIL_THREADS_PER_NOTIFICATION);

    int maxThreads = 5;

    try {
      if (maxThreadsStr != null) maxThreads = Integer.parseInt(maxThreadsStr);
    } catch (NumberFormatException e) {
      if (logger.isDebugEnabled())
        logger.debug("Failed to parse max threads count: " + maxThreads + ". Going for default.");
    }

    // print a maximum of MAX_THREADS
    for (int i = 0; i < maxThreads && threads.hasNext(); i++) {
      message.append(threads.next().createHtmlDescription());
    }
    message.append("</table><br/>");

    if (threadCount > maxThreads) {
      String messageFooter =
          JabberActivator.getResources()
              .getI18NString(
                  resourceFooterKey,
                  new String[] {
                    mailboxIQ.getUrl(), // {0} - inbox URI
                    Integer.toString(threadCount - maxThreads) // {1} - thread count
                  });
      message.append(messageFooter);
    }

    return message.toString();
  }

  public String getRecentJIDForAddress(String address) {
    return recentJIDForAddress.get(address);
  }

  /** Receives incoming MailNotification Packets */
  private class MailboxIQListener implements PacketListener {
    /**
     * Handles incoming <tt>MailboxIQ</tt> packets.
     *
     * @param packet the IQ that we need to handle in case it is a <tt>MailboxIQ</tt>.
     */
    public void processPacket(Packet packet) {
      if (packet != null && !(packet instanceof MailboxIQ)) return;

      MailboxIQ mailboxIQ = (MailboxIQ) packet;

      if (mailboxIQ.getTotalMatched() < 1) return;

      // Get a reference to a dummy volatile contact
      Contact sourceContact =
          opSetPersPresence.findContactByID(jabberProvider.getAccountID().getService());

      if (sourceContact == null)
        sourceContact =
            opSetPersPresence.createVolatileContact(jabberProvider.getAccountID().getService());

      lastReceivedMailboxResultTime = mailboxIQ.getResultTime();

      String newMail = createMailboxDescription(mailboxIQ);

      Message newMailMessage =
          new MessageJabberImpl(newMail, HTML_MIME_TYPE, DEFAULT_MIME_ENCODING, null);

      MessageReceivedEvent msgReceivedEvt =
          new MessageReceivedEvent(
              newMailMessage,
              sourceContact,
              new Date(),
              MessageReceivedEvent.SYSTEM_MESSAGE_RECEIVED);

      fireMessageEvent(msgReceivedEvt);
    }
  }

  /** Receives incoming NewMailNotification Packets. */
  private class NewMailNotificationListener implements PacketListener {
    /**
     * Handles incoming <tt>NewMailNotificationIQ</tt> packets.
     *
     * @param packet the IQ that we need to handle in case it is a <tt>NewMailNotificationIQ</tt>.
     */
    public void processPacket(Packet packet) {
      if (packet != null && !(packet instanceof NewMailNotificationIQ)) return;

      // check whether we are still enabled.
      boolean enableGmailNotifications =
          jabberProvider
              .getAccountID()
              .getAccountPropertyBoolean("GMAIL_NOTIFICATIONS_ENABLED", false);

      if (!enableGmailNotifications) return;

      if (opSetPersPresence.getCurrentStatusMessage().equals(JabberStatusEnum.OFFLINE)) return;

      MailboxQueryIQ mailboxQueryIQ = new MailboxQueryIQ();

      if (lastReceivedMailboxResultTime != -1)
        mailboxQueryIQ.setNewerThanTime(lastReceivedMailboxResultTime);

      if (logger.isTraceEnabled())
        logger.trace(
            "send mailNotification for acc: " + jabberProvider.getAccountID().getAccountUniqueID());

      jabberProvider.getConnection().sendPacket(mailboxQueryIQ);
    }
  }

  /**
   * Returns the inactivity timeout in milliseconds.
   *
   * @return The inactivity timeout in milliseconds. Or -1 if undefined
   */
  public long getInactivityTimeout() {
    return JID_INACTIVITY_TIMEOUT;
  }

  /**
   * Adds additional filters for incoming messages. To be able to skip some messages.
   *
   * @param filter to add
   */
  public void addMessageFilters(PacketFilter filter) {
    this.packetFilters.add(filter);
  }

  /**
   * Returns the next unique thread id. Each thread id made up of a short alphanumeric prefix along
   * with a unique numeric value.
   *
   * @return the next thread id.
   */
  public static synchronized String nextThreadID() {
    return prefix + Long.toString(id++);
  }
}
예제 #19
0
/**
 * The source contact service. The will show most recent messages.
 *
 * @author Damian Minkov
 */
public class MessageSourceService extends MetaContactListAdapter
    implements ContactSourceService,
        ContactPresenceStatusListener,
        ContactCapabilitiesListener,
        ProviderPresenceStatusListener,
        SubscriptionListener,
        LocalUserChatRoomPresenceListener,
        MessageListener,
        ChatRoomMessageListener,
        AdHocChatRoomMessageListener {
  /** The logger for this class. */
  private static Logger logger = Logger.getLogger(MessageSourceService.class);

  /** The display name of this contact source. */
  private final String MESSAGE_HISTORY_NAME;

  /** The type of the source service, the place to be shown in the ui. */
  private int sourceServiceType = CONTACT_LIST_TYPE;

  /**
   * Whether to show recent messages in history or in contactlist. By default we show it in
   * contactlist.
   */
  private static final String IN_HISTORY_PROPERTY =
      "net.java.sip.communicator.impl.msghistory.contactsrc.IN_HISTORY";

  /** Property to control number of recent messages. */
  private static final String NUMBER_OF_RECENT_MSGS_PROP =
      "net.java.sip.communicator.impl.msghistory.contactsrc.MSG_NUMBER";

  /** Property to control version of recent messages. */
  private static final String VER_OF_RECENT_MSGS_PROP =
      "net.java.sip.communicator.impl.msghistory.contactsrc.MSG_VER";

  /** Property to control messages type. Can query for message sub type. */
  private static final String IS_MESSAGE_SUBTYPE_SMS_PROP =
      "net.java.sip.communicator.impl.msghistory.contactsrc.IS_SMS_ENABLED";

  /**
   * The number of recent messages to store in the history, but will retrieve just
   * <tt>numberOfMessages</tt>
   */
  private static final int NUMBER_OF_MSGS_IN_HISTORY = 100;

  /** Number of messages to show. */
  private int numberOfMessages = 10;

  /** The structure to save recent messages list. */
  private static final String[] STRUCTURE_NAMES =
      new String[] {"provider", "contact", "timestamp", "ver"};

  /** The current version of recent messages. When changed the recent messages are recreated. */
  private static String RECENT_MSGS_VER = "2";

  /** The structure. */
  private static final HistoryRecordStructure recordStructure =
      new HistoryRecordStructure(STRUCTURE_NAMES);

  /** Recent messages history ID. */
  private static final HistoryID historyID =
      HistoryID.createFromRawID(new String[] {"recent_messages"});

  /** The cache for recent messages. */
  private History history = null;

  /** List of recent messages. */
  private final List<ComparableEvtObj> recentMessages = new LinkedList<ComparableEvtObj>();

  /** Date of the oldest shown message. */
  private Date oldestRecentMessage = null;

  /** The last query created. */
  private MessageSourceContactQuery recentQuery = null;

  /** The message subtype if any. */
  private boolean isSMSEnabled = false;

  /** Message history service that has created us. */
  private MessageHistoryServiceImpl messageHistoryService;

  /** Constructs MessageSourceService. */
  MessageSourceService(MessageHistoryServiceImpl messageHistoryService) {
    this.messageHistoryService = messageHistoryService;

    ConfigurationService conf = MessageHistoryActivator.getConfigurationService();

    if (conf.getBoolean(IN_HISTORY_PROPERTY, false)) {
      sourceServiceType = HISTORY_TYPE;
    }

    MESSAGE_HISTORY_NAME =
        MessageHistoryActivator.getResources().getI18NString("service.gui.RECENT_MESSAGES");

    numberOfMessages = conf.getInt(NUMBER_OF_RECENT_MSGS_PROP, numberOfMessages);

    isSMSEnabled = conf.getBoolean(IS_MESSAGE_SUBTYPE_SMS_PROP, isSMSEnabled);

    RECENT_MSGS_VER = conf.getString(VER_OF_RECENT_MSGS_PROP, RECENT_MSGS_VER);

    MessageSourceContactPresenceStatus.MSG_SRC_CONTACT_ONLINE.setStatusIcon(
        MessageHistoryActivator.getResources()
            .getImageInBytes("service.gui.icons.SMS_STATUS_ICON"));
  }

  /**
   * Returns the display name of this contact source.
   *
   * @return the display name of this contact source
   */
  @Override
  public String getDisplayName() {
    return MESSAGE_HISTORY_NAME;
  }

  /**
   * Returns default type to indicate that this contact source can be queried by default filters.
   *
   * @return the type of this contact source
   */
  @Override
  public int getType() {
    return sourceServiceType;
  }

  /**
   * Returns the index of the contact source in the result list.
   *
   * @return the index of the contact source in the result list
   */
  @Override
  public int getIndex() {
    return 0;
  }

  /**
   * Creates query for the given <tt>searchString</tt>.
   *
   * @param queryString the string to search for
   * @return the created query
   */
  @Override
  public ContactQuery createContactQuery(String queryString) {
    recentQuery = (MessageSourceContactQuery) createContactQuery(queryString, numberOfMessages);

    return recentQuery;
  }

  /**
   * Updates the contact sources in the recent query if any. Done here in order to sync with
   * recentMessages instance, and to check for already existing instances of contact sources.
   * Normally called from the query.
   */
  public void updateRecentMessages() {
    if (recentQuery == null) return;

    synchronized (recentMessages) {
      List<SourceContact> currentContactsInQuery = recentQuery.getQueryResults();

      for (ComparableEvtObj evtObj : recentMessages) {
        // the contains will use the correct equals method of
        // the object evtObj
        if (!currentContactsInQuery.contains(evtObj)) {
          MessageSourceContact newSourceContact =
              new MessageSourceContact(evtObj.getEventObject(), MessageSourceService.this);
          newSourceContact.initDetails(evtObj.getEventObject());

          recentQuery.addQueryResult(newSourceContact);
        }
      }
    }
  }

  /**
   * Searches for entries in cached recent messages in history.
   *
   * @param provider the provider which contact messages we will search
   * @param isStatusChanged is the search because of status changed
   * @return entries in cached recent messages in history.
   */
  private List<ComparableEvtObj> getCachedRecentMessages(
      ProtocolProviderService provider, boolean isStatusChanged) {
    String providerID = provider.getAccountID().getAccountUniqueID();
    List<String> recentMessagesContactIDs =
        getRecentContactIDs(
            providerID, recentMessages.size() < numberOfMessages ? null : oldestRecentMessage);

    List<ComparableEvtObj> cachedRecentMessages = new ArrayList<ComparableEvtObj>();

    for (String contactID : recentMessagesContactIDs) {
      Collection<EventObject> res =
          messageHistoryService.findRecentMessagesPerContact(
              numberOfMessages, providerID, contactID, isSMSEnabled);

      processEventObjects(res, cachedRecentMessages, isStatusChanged);
    }

    return cachedRecentMessages;
  }

  /**
   * Process list of event objects. Checks whether message source contact already exist for this
   * event object, if yes just update it with the new values (not sure whether we should do this, as
   * it may bring old messages) and if status of provider is changed, init its details, updates its
   * capabilities. It still adds the found messages source contact to the list of the new contacts,
   * as later we will detect this and fire update event. If nothing found a new contact is created.
   *
   * @param res list of event
   * @param cachedRecentMessages list of newly created source contacts or already existed but
   *     updated with corresponding event object
   * @param isStatusChanged whether provider status changed and we are processing
   */
  private void processEventObjects(
      Collection<EventObject> res,
      List<ComparableEvtObj> cachedRecentMessages,
      boolean isStatusChanged) {
    for (EventObject obj : res) {
      ComparableEvtObj oldMsg = findRecentMessage(obj, recentMessages);

      if (oldMsg != null) {
        oldMsg.update(obj); // update

        if (isStatusChanged && recentQuery != null) recentQuery.updateCapabilities(oldMsg, obj);

        // we still add it to cachedRecentMessages
        // later we will find it is duplicate and will fire
        // update event
        if (!cachedRecentMessages.contains(oldMsg)) cachedRecentMessages.add(oldMsg);

        continue;
      }

      oldMsg = findRecentMessage(obj, cachedRecentMessages);

      if (oldMsg == null) {
        oldMsg = new ComparableEvtObj(obj);

        if (isStatusChanged && recentQuery != null) recentQuery.updateCapabilities(oldMsg, obj);

        cachedRecentMessages.add(oldMsg);
      }
    }
  }

  /**
   * Access for source contacts impl.
   *
   * @return
   */
  boolean isSMSEnabled() {
    return isSMSEnabled;
  }

  /**
   * Add the ComparableEvtObj, newly added will fire new, for existing fire update and when trimming
   * the list to desired length fire remove for those that were removed
   *
   * @param contactsToAdd
   */
  private void addNewRecentMessages(List<ComparableEvtObj> contactsToAdd) {
    // now find object to fire new, and object to fire remove
    // let us find duplicates and fire update
    List<ComparableEvtObj> duplicates = new ArrayList<ComparableEvtObj>();
    for (ComparableEvtObj msgToAdd : contactsToAdd) {
      if (recentMessages.contains(msgToAdd)) {
        duplicates.add(msgToAdd);

        // save update
        updateRecentMessageToHistory(msgToAdd);
      }
    }
    recentMessages.removeAll(duplicates);

    // now contacts to add has no duplicates, add them all
    boolean changed = recentMessages.addAll(contactsToAdd);

    if (changed) {
      Collections.sort(recentMessages);

      if (recentQuery != null) {
        for (ComparableEvtObj obj : duplicates)
          recentQuery.updateContact(obj, obj.getEventObject());
      }
    }

    if (!recentMessages.isEmpty())
      oldestRecentMessage = recentMessages.get(recentMessages.size() - 1).getTimestamp();

    // trim
    List<ComparableEvtObj> removedItems = null;
    if (recentMessages.size() > numberOfMessages) {
      removedItems =
          new ArrayList<ComparableEvtObj>(
              recentMessages.subList(numberOfMessages, recentMessages.size()));

      recentMessages.removeAll(removedItems);
    }

    if (recentQuery != null) {
      // now fire, removed for all that were in the list
      // and now are removed after trim
      if (removedItems != null) {
        for (ComparableEvtObj msc : removedItems) {
          if (!contactsToAdd.contains(msc)) recentQuery.fireContactRemoved(msc);
        }
      }

      // fire new for all that were added, and not removed after trim
      for (ComparableEvtObj msc : contactsToAdd) {
        if ((removedItems == null || !removedItems.contains(msc)) && !duplicates.contains(msc)) {
          MessageSourceContact newSourceContact =
              new MessageSourceContact(msc.getEventObject(), MessageSourceService.this);
          newSourceContact.initDetails(msc.getEventObject());

          recentQuery.addQueryResult(newSourceContact);
        }
      }

      // if recent messages were changed, indexes have change lets
      // fire event for the last element which will reorder the whole
      // group if needed.
      if (changed) recentQuery.fireContactChanged(recentMessages.get(recentMessages.size() - 1));
    }
  }

  /**
   * When a provider is added, do not block and start executing in new thread.
   *
   * @param provider ProtocolProviderService
   */
  void handleProviderAdded(final ProtocolProviderService provider, final boolean isStatusChanged) {
    new Thread(
            new Runnable() {
              @Override
              public void run() {
                handleProviderAddedInSeparateThread(provider, isStatusChanged);
              }
            })
        .start();
  }

  /**
   * When a provider is added. As searching can be slow especially when handling special type of
   * messages (with subType) this need to be run in new Thread.
   *
   * @param provider ProtocolProviderService
   */
  private void handleProviderAddedInSeparateThread(
      ProtocolProviderService provider, boolean isStatusChanged) {
    // lets check if we have cached recent messages for this provider, and
    // fire events if found and are newer

    synchronized (recentMessages) {
      List<ComparableEvtObj> cachedRecentMessages =
          getCachedRecentMessages(provider, isStatusChanged);

      if (cachedRecentMessages.isEmpty()) {
        // maybe there is no cached history for this
        // let's check
        // load it not from cache, but do a local search
        Collection<EventObject> res =
            messageHistoryService.findRecentMessagesPerContact(
                numberOfMessages, provider.getAccountID().getAccountUniqueID(), null, isSMSEnabled);

        List<ComparableEvtObj> newMsc = new ArrayList<ComparableEvtObj>();

        processEventObjects(res, newMsc, isStatusChanged);

        addNewRecentMessages(newMsc);

        for (ComparableEvtObj msc : newMsc) {
          saveRecentMessageToHistory(msc);
        }
      } else addNewRecentMessages(cachedRecentMessages);
    }
  }

  /**
   * Tries to match the event object to already existing ComparableEvtObj in the supplied list.
   *
   * @param obj the object that we will try to match.
   * @param list the list we will search in.
   * @return the found ComparableEvtObj
   */
  private static ComparableEvtObj findRecentMessage(EventObject obj, List<ComparableEvtObj> list) {
    Contact contact = null;
    ChatRoom chatRoom = null;

    if (obj instanceof MessageDeliveredEvent) {
      contact = ((MessageDeliveredEvent) obj).getDestinationContact();
    } else if (obj instanceof MessageReceivedEvent) {
      contact = ((MessageReceivedEvent) obj).getSourceContact();
    } else if (obj instanceof ChatRoomMessageDeliveredEvent) {
      chatRoom = ((ChatRoomMessageDeliveredEvent) obj).getSourceChatRoom();
    } else if (obj instanceof ChatRoomMessageReceivedEvent) {
      chatRoom = ((ChatRoomMessageReceivedEvent) obj).getSourceChatRoom();
    }

    for (ComparableEvtObj evt : list) {
      if ((contact != null && contact.equals(evt.getContact()))
          || (chatRoom != null && chatRoom.equals(evt.getRoom()))) return evt;
    }

    return null;
  }

  /**
   * A provider has been removed.
   *
   * @param provider the ProtocolProviderService that has been unregistered.
   */
  void handleProviderRemoved(ProtocolProviderService provider) {
    // lets remove the recent messages for this provider, and update
    // with recent messages for the available providers
    synchronized (recentMessages) {
      if (provider != null) {
        List<ComparableEvtObj> removedItems = new ArrayList<ComparableEvtObj>();
        for (ComparableEvtObj msc : recentMessages) {
          if (msc.getProtocolProviderService().equals(provider)) removedItems.add(msc);
        }

        recentMessages.removeAll(removedItems);
        if (!recentMessages.isEmpty())
          oldestRecentMessage = recentMessages.get(recentMessages.size() - 1).getTimestamp();
        else oldestRecentMessage = null;

        if (recentQuery != null) {
          for (ComparableEvtObj msc : removedItems) {
            recentQuery.fireContactRemoved(msc);
          }
        }
      }

      // handleProviderRemoved can be invoked due to stopped
      // history service, if this is the case we do not want to
      // update messages
      if (!this.messageHistoryService.isHistoryLoggingEnabled()) return;

      // lets do the same as we enable provider
      // for all registered providers and finally fire events
      List<ComparableEvtObj> contactsToAdd = new ArrayList<ComparableEvtObj>();
      for (ProtocolProviderService pps : messageHistoryService.getCurrentlyAvailableProviders()) {
        contactsToAdd.addAll(getCachedRecentMessages(pps, true));
      }

      addNewRecentMessages(contactsToAdd);
    }
  }

  /**
   * Searches for contact ids in history of recent messages.
   *
   * @param provider
   * @param after
   * @return
   */
  List<String> getRecentContactIDs(String provider, Date after) {
    List<String> res = new ArrayList<String>();

    try {
      History history = getHistory();

      if (history != null) {
        Iterator<HistoryRecord> recs = history.getReader().findLast(NUMBER_OF_MSGS_IN_HISTORY);
        SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT);

        while (recs.hasNext()) {
          HistoryRecord hr = recs.next();

          String contact = null;
          String recordProvider = null;
          Date timestamp = null;

          for (int i = 0; i < hr.getPropertyNames().length; i++) {
            String propName = hr.getPropertyNames()[i];

            if (propName.equals(STRUCTURE_NAMES[0])) recordProvider = hr.getPropertyValues()[i];
            else if (propName.equals(STRUCTURE_NAMES[1])) contact = hr.getPropertyValues()[i];
            else if (propName.equals(STRUCTURE_NAMES[2])) {
              try {
                timestamp = sdf.parse(hr.getPropertyValues()[i]);
              } catch (ParseException e) {
                timestamp = new Date(Long.parseLong(hr.getPropertyValues()[i]));
              }
            }
          }

          if (recordProvider == null || contact == null) continue;

          if (after != null && timestamp != null && timestamp.before(after)) continue;

          if (recordProvider.equals(provider)) res.add(contact);
        }
      }
    } catch (IOException ex) {
      logger.error("cannot create recent_messages history", ex);
    }

    return res;
  }

  /**
   * Returns the cached recent messages history.
   *
   * @return
   * @throws IOException
   */
  private History getHistory() throws IOException {
    synchronized (historyID) {
      HistoryService historyService =
          MessageHistoryActivator.getMessageHistoryService().getHistoryService();

      if (history == null) {
        history = historyService.createHistory(historyID, recordStructure);

        // lets check the version if not our version, re-create
        // history (delete it)
        HistoryReader reader = history.getReader();
        boolean delete = false;
        QueryResultSet<HistoryRecord> res = reader.findLast(1);
        if (res != null && res.hasNext()) {
          HistoryRecord hr = res.next();
          if (hr.getPropertyValues().length >= 4) {
            if (!hr.getPropertyValues()[3].equals(RECENT_MSGS_VER)) delete = true;
          } else delete = true;
        }

        if (delete) {
          // delete it
          try {
            historyService.purgeLocallyStoredHistory(historyID);

            history = historyService.createHistory(historyID, recordStructure);
          } catch (IOException ex) {
            logger.error("Cannot delete recent_messages history", ex);
          }
        }
      }

      return history;
    }
  }

  /**
   * Returns the index of the source contact, in the list of recent messages.
   *
   * @param messageSourceContact
   * @return
   */
  int getIndex(MessageSourceContact messageSourceContact) {
    synchronized (recentMessages) {
      for (int i = 0; i < recentMessages.size(); i++)
        if (recentMessages.get(i).equals(messageSourceContact)) return i;

      return -1;
    }
  }

  /**
   * Creates query for the given <tt>searchString</tt>.
   *
   * @param queryString the string to search for
   * @param contactCount the maximum count of result contacts
   * @return the created query
   */
  @Override
  public ContactQuery createContactQuery(String queryString, int contactCount) {
    if (!StringUtils.isNullOrEmpty(queryString)) return null;

    recentQuery = new MessageSourceContactQuery(MessageSourceService.this);

    return recentQuery;
  }

  /**
   * Updates contact source contacts with status.
   *
   * @param evt the ContactPresenceStatusChangeEvent describing the status
   */
  @Override
  public void contactPresenceStatusChanged(ContactPresenceStatusChangeEvent evt) {
    if (recentQuery == null) return;

    synchronized (recentMessages) {
      for (ComparableEvtObj msg : recentMessages) {
        if (msg.getContact() != null && msg.getContact().equals(evt.getSourceContact())) {
          recentQuery.updateContactStatus(msg, evt.getNewStatus());
        }
      }
    }
  }

  @Override
  public void providerStatusChanged(ProviderPresenceStatusChangeEvent evt) {
    if (!evt.getNewStatus().isOnline() || evt.getOldStatus().isOnline()) return;

    handleProviderAdded(evt.getProvider(), true);
  }

  @Override
  public void providerStatusMessageChanged(PropertyChangeEvent evt) {}

  @Override
  public void localUserPresenceChanged(LocalUserChatRoomPresenceChangeEvent evt) {
    if (recentQuery == null) return;

    ComparableEvtObj srcContact = null;

    synchronized (recentMessages) {
      for (ComparableEvtObj msg : recentMessages) {
        if (msg.getRoom() != null && msg.getRoom().equals(evt.getChatRoom())) {
          srcContact = msg;
          break;
        }
      }
    }

    if (srcContact == null) return;

    String eventType = evt.getEventType();

    if (LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_JOINED.equals(eventType)) {
      recentQuery.updateContactStatus(srcContact, ChatRoomPresenceStatus.CHAT_ROOM_ONLINE);
    } else if ((LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_LEFT.equals(eventType)
        || LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_KICKED.equals(eventType)
        || LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_DROPPED.equals(eventType))) {
      recentQuery.updateContactStatus(srcContact, ChatRoomPresenceStatus.CHAT_ROOM_OFFLINE);
    }
  }

  /**
   * Handles new events.
   *
   * @param obj the event object
   * @param provider the provider
   * @param id the id of the source of the event
   */
  private void handle(EventObject obj, ProtocolProviderService provider, String id) {
    // check if provider - contact exist update message content
    synchronized (recentMessages) {
      ComparableEvtObj existingMsc = null;
      for (ComparableEvtObj msc : recentMessages) {
        if (msc.getProtocolProviderService().equals(provider)
            && msc.getContactAddress().equals(id)) {
          // update
          msc.update(obj);
          updateRecentMessageToHistory(msc);

          existingMsc = msc;
        }
      }

      if (existingMsc != null) {
        Collections.sort(recentMessages);
        oldestRecentMessage = recentMessages.get(recentMessages.size() - 1).getTimestamp();

        if (recentQuery != null) {
          recentQuery.updateContact(existingMsc, existingMsc.getEventObject());
          recentQuery.fireContactChanged(existingMsc);
        }

        return;
      }

      // if missing create source contact
      // and update recent messages, trim and sort
      MessageSourceContact newSourceContact =
          new MessageSourceContact(obj, MessageSourceService.this);
      newSourceContact.initDetails(obj);
      // we have already checked for duplicate
      ComparableEvtObj newMsg = new ComparableEvtObj(obj);
      recentMessages.add(newMsg);

      Collections.sort(recentMessages);
      oldestRecentMessage = recentMessages.get(recentMessages.size() - 1).getTimestamp();

      // trim
      List<ComparableEvtObj> removedItems = null;
      if (recentMessages.size() > numberOfMessages) {
        removedItems =
            new ArrayList<ComparableEvtObj>(
                recentMessages.subList(numberOfMessages, recentMessages.size()));

        recentMessages.removeAll(removedItems);
      }

      // save
      saveRecentMessageToHistory(newMsg);

      // no query nothing to fire
      if (recentQuery == null) return;

      // now fire
      if (removedItems != null) {
        for (ComparableEvtObj msc : removedItems) {
          recentQuery.fireContactRemoved(msc);
        }
      }

      recentQuery.addQueryResult(newSourceContact);
    }
  }

  /** Adds recent message in history. */
  private void saveRecentMessageToHistory(ComparableEvtObj msc) {
    synchronized (historyID) {
      // and create it
      try {
        History history = getHistory();
        HistoryWriter writer = history.getWriter();

        SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT);

        writer.addRecord(
            new String[] {
              msc.getProtocolProviderService().getAccountID().getAccountUniqueID(),
              msc.getContactAddress(),
              sdf.format(msc.getTimestamp()),
              RECENT_MSGS_VER
            },
            NUMBER_OF_MSGS_IN_HISTORY);
      } catch (IOException ex) {
        logger.error("cannot create recent_messages history", ex);
        return;
      }
    }
  }

  /** Updates recent message in history. */
  private void updateRecentMessageToHistory(final ComparableEvtObj msg) {
    synchronized (historyID) {
      // and create it
      try {
        History history = getHistory();

        HistoryWriter writer = history.getWriter();

        writer.updateRecord(
            new HistoryWriter.HistoryRecordUpdater() {
              HistoryRecord hr;

              @Override
              public void setHistoryRecord(HistoryRecord historyRecord) {
                this.hr = historyRecord;
              }

              @Override
              public boolean isMatching() {
                boolean providerFound = false;
                boolean contactFound = false;
                for (int i = 0; i < hr.getPropertyNames().length; i++) {
                  String propName = hr.getPropertyNames()[i];

                  if (propName.equals(STRUCTURE_NAMES[0])) {
                    if (msg.getProtocolProviderService()
                        .getAccountID()
                        .getAccountUniqueID()
                        .equals(hr.getPropertyValues()[i])) {
                      providerFound = true;
                    }
                  } else if (propName.equals(STRUCTURE_NAMES[1])) {
                    if (msg.getContactAddress().equals(hr.getPropertyValues()[i])) {
                      contactFound = true;
                    }
                  }
                }

                return contactFound && providerFound;
              }

              @Override
              public Map<String, String> getUpdateChanges() {
                HashMap<String, String> map = new HashMap<String, String>();
                SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT);

                for (int i = 0; i < hr.getPropertyNames().length; i++) {
                  String propName = hr.getPropertyNames()[i];

                  if (propName.equals(STRUCTURE_NAMES[0])) {
                    map.put(
                        propName,
                        msg.getProtocolProviderService().getAccountID().getAccountUniqueID());
                  } else if (propName.equals(STRUCTURE_NAMES[1])) {
                    map.put(propName, msg.getContactAddress());
                  } else if (propName.equals(STRUCTURE_NAMES[2])) {
                    map.put(propName, sdf.format(msg.getTimestamp()));
                  } else if (propName.equals(STRUCTURE_NAMES[3]))
                    map.put(propName, RECENT_MSGS_VER);
                }

                return map;
              }
            });
      } catch (IOException ex) {
        logger.error("cannot create recent_messages history", ex);
        return;
      }
    }
  }

  @Override
  public void messageReceived(MessageReceivedEvent evt) {
    if (isSMSEnabled && evt.getEventType() != MessageReceivedEvent.SMS_MESSAGE_RECEIVED) {
      return;
    }

    handle(evt, evt.getSourceContact().getProtocolProvider(), evt.getSourceContact().getAddress());
  }

  @Override
  public void messageDelivered(MessageDeliveredEvent evt) {
    if (isSMSEnabled && !evt.isSmsMessage()) return;

    handle(
        evt,
        evt.getDestinationContact().getProtocolProvider(),
        evt.getDestinationContact().getAddress());
  }

  /**
   * Not used.
   *
   * @param evt the <tt>MessageFailedEvent</tt> containing the ID of the
   */
  @Override
  public void messageDeliveryFailed(MessageDeliveryFailedEvent evt) {}

  @Override
  public void messageReceived(ChatRoomMessageReceivedEvent evt) {
    if (isSMSEnabled) return;

    // ignore non conversation messages
    if (evt.getEventType() != ChatRoomMessageReceivedEvent.CONVERSATION_MESSAGE_RECEIVED) return;

    handle(
        evt, evt.getSourceChatRoom().getParentProvider(), evt.getSourceChatRoom().getIdentifier());
  }

  @Override
  public void messageDelivered(ChatRoomMessageDeliveredEvent evt) {
    if (isSMSEnabled) return;

    handle(
        evt, evt.getSourceChatRoom().getParentProvider(), evt.getSourceChatRoom().getIdentifier());
  }

  /**
   * Not used.
   *
   * @param evt the <tt>ChatroomMessageDeliveryFailedEvent</tt> containing
   */
  @Override
  public void messageDeliveryFailed(ChatRoomMessageDeliveryFailedEvent evt) {}

  @Override
  public void messageReceived(AdHocChatRoomMessageReceivedEvent evt) {
    // TODO
  }

  @Override
  public void messageDelivered(AdHocChatRoomMessageDeliveredEvent evt) {
    // TODO
  }

  /**
   * Not used.
   *
   * @param evt the <tt>AdHocChatroomMessageDeliveryFailedEvent</tt>
   */
  @Override
  public void messageDeliveryFailed(AdHocChatRoomMessageDeliveryFailedEvent evt) {}

  @Override
  public void subscriptionCreated(SubscriptionEvent evt) {}

  @Override
  public void subscriptionFailed(SubscriptionEvent evt) {}

  @Override
  public void subscriptionRemoved(SubscriptionEvent evt) {}

  @Override
  public void subscriptionMoved(SubscriptionMovedEvent evt) {}

  @Override
  public void subscriptionResolved(SubscriptionEvent evt) {}

  /**
   * If a contact is renamed update the locally stored message if any.
   *
   * @param evt the <tt>ContactPropertyChangeEvent</tt> containing the source
   */
  @Override
  public void contactModified(ContactPropertyChangeEvent evt) {
    if (!evt.getPropertyName().equals(ContactPropertyChangeEvent.PROPERTY_DISPLAY_NAME)) return;

    Contact contact = evt.getSourceContact();

    if (contact == null) return;

    for (ComparableEvtObj msc : recentMessages) {
      if (contact.equals(msc.getContact())) {
        if (recentQuery != null)
          recentQuery.updateContactDisplayName(msc, contact.getDisplayName());

        return;
      }
    }
  }

  /**
   * Indicates that a MetaContact has been modified.
   *
   * @param evt the MetaContactListEvent containing the corresponding contact
   */
  public void metaContactRenamed(MetaContactRenamedEvent evt) {
    for (ComparableEvtObj msc : recentMessages) {
      if (evt.getSourceMetaContact().containsContact(msc.getContact())) {
        if (recentQuery != null) recentQuery.updateContactDisplayName(msc, evt.getNewDisplayName());
      }
    }
  }

  @Override
  public void supportedOperationSetsChanged(ContactCapabilitiesEvent event) {
    Contact contact = event.getSourceContact();

    if (contact == null) return;

    for (ComparableEvtObj msc : recentMessages) {
      if (contact.equals(msc.getContact())) {
        if (recentQuery != null) recentQuery.updateCapabilities(msc, contact);

        return;
      }
    }
  }

  /** Permanently removes all locally stored message history, remove recent contacts. */
  public void eraseLocallyStoredHistory() throws IOException {
    List<ComparableEvtObj> toRemove = null;
    synchronized (recentMessages) {
      toRemove = new ArrayList<ComparableEvtObj>(recentMessages);

      recentMessages.clear();
    }

    if (recentQuery != null) {
      for (ComparableEvtObj msc : toRemove) {
        recentQuery.fireContactRemoved(msc);
      }
    }
  }

  /**
   * Permanently removes locally stored message history for the metacontact, remove any recent
   * contacts if any.
   */
  public void eraseLocallyStoredHistory(MetaContact contact) throws IOException {
    List<ComparableEvtObj> toRemove = null;
    synchronized (recentMessages) {
      toRemove = new ArrayList<ComparableEvtObj>();
      Iterator<Contact> iter = contact.getContacts();
      while (iter.hasNext()) {
        Contact item = iter.next();
        String id = item.getAddress();
        ProtocolProviderService provider = item.getProtocolProvider();

        for (ComparableEvtObj msc : recentMessages) {
          if (msc.getProtocolProviderService().equals(provider)
              && msc.getContactAddress().equals(id)) {
            toRemove.add(msc);
          }
        }
      }

      recentMessages.removeAll(toRemove);
    }
    if (recentQuery != null) {
      for (ComparableEvtObj msc : toRemove) {
        recentQuery.fireContactRemoved(msc);
      }
    }
  }

  /**
   * Permanently removes locally stored message history for the chatroom, remove any recent contacts
   * if any.
   */
  public void eraseLocallyStoredHistory(ChatRoom room) {
    ComparableEvtObj toRemove = null;
    synchronized (recentMessages) {
      for (ComparableEvtObj msg : recentMessages) {
        if (msg.getRoom() != null && msg.getRoom().equals(room)) {
          toRemove = msg;
          break;
        }
      }

      if (toRemove == null) return;

      recentMessages.remove(toRemove);
    }

    if (recentQuery != null) recentQuery.fireContactRemoved(toRemove);
  }

  /** Object used to cache recent messages. */
  private class ComparableEvtObj implements Comparable<ComparableEvtObj> {
    private EventObject eventObject;

    /** The protocol provider. */
    private ProtocolProviderService ppService = null;

    /** The address. */
    private String address = null;

    /** The timestamp. */
    private Date timestamp = null;

    /** The contact instance. */
    private Contact contact = null;

    /** The room instance. */
    private ChatRoom room = null;

    /**
     * Constructs.
     *
     * @param source used to extract initial values.
     */
    ComparableEvtObj(EventObject source) {
      update(source);
    }

    /**
     * Extract values from <tt>EventObject</tt>.
     *
     * @param source
     */
    public void update(EventObject source) {
      this.eventObject = source;

      if (source instanceof MessageDeliveredEvent) {
        MessageDeliveredEvent e = (MessageDeliveredEvent) source;

        this.contact = e.getDestinationContact();

        this.address = contact.getAddress();
        this.ppService = contact.getProtocolProvider();
        this.timestamp = e.getTimestamp();
      } else if (source instanceof MessageReceivedEvent) {
        MessageReceivedEvent e = (MessageReceivedEvent) source;

        this.contact = e.getSourceContact();

        this.address = contact.getAddress();
        this.ppService = contact.getProtocolProvider();
        this.timestamp = e.getTimestamp();
      } else if (source instanceof ChatRoomMessageDeliveredEvent) {
        ChatRoomMessageDeliveredEvent e = (ChatRoomMessageDeliveredEvent) source;

        this.room = e.getSourceChatRoom();

        this.address = room.getIdentifier();
        this.ppService = room.getParentProvider();
        this.timestamp = e.getTimestamp();
      } else if (source instanceof ChatRoomMessageReceivedEvent) {
        ChatRoomMessageReceivedEvent e = (ChatRoomMessageReceivedEvent) source;

        this.room = e.getSourceChatRoom();

        this.address = room.getIdentifier();
        this.ppService = room.getParentProvider();
        this.timestamp = e.getTimestamp();
      }
    }

    @Override
    public String toString() {
      return "ComparableEvtObj{" + "address='" + address + '\'' + ", ppService=" + ppService + '}';
    }

    /**
     * The timestamp of the message.
     *
     * @return the timestamp of the message.
     */
    public Date getTimestamp() {
      return timestamp;
    }

    /**
     * The contact.
     *
     * @return the contact.
     */
    public Contact getContact() {
      return contact;
    }

    /**
     * The room.
     *
     * @return the room.
     */
    public ChatRoom getRoom() {
      return room;
    }

    /**
     * The protocol provider.
     *
     * @return the protocol provider.
     */
    public ProtocolProviderService getProtocolProviderService() {
      return ppService;
    }

    /**
     * The address.
     *
     * @return the address.
     */
    public String getContactAddress() {
      if (this.address != null) return this.address;

      return null;
    }

    /**
     * The event object.
     *
     * @return the event object.
     */
    public EventObject getEventObject() {
      return eventObject;
    }

    /**
     * Compares two ComparableEvtObj.
     *
     * @param o the object to compare with
     * @return 0, less than zero, greater than zero, if equals, less or greater.
     */
    @Override
    public int compareTo(ComparableEvtObj o) {
      if (o == null || o.getTimestamp() == null) return 1;

      return o.getTimestamp().compareTo(getTimestamp());
    }

    /**
     * Checks if equals, and if this event object is used to create a MessageSourceContact, if the
     * supplied <tt>Object</tt> is instance of MessageSourceContact.
     *
     * @param o the object to check.
     * @return <tt>true</tt> if equals.
     */
    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || (!(o instanceof MessageSourceContact) && getClass() != o.getClass()))
        return false;

      if (o instanceof ComparableEvtObj) {
        ComparableEvtObj that = (ComparableEvtObj) o;

        if (!address.equals(that.address)) return false;
        if (!ppService.equals(that.ppService)) return false;
      } else if (o instanceof MessageSourceContact) {
        MessageSourceContact that = (MessageSourceContact) o;

        if (!address.equals(that.getContactAddress())) return false;
        if (!ppService.equals(that.getProtocolProviderService())) return false;
      } else return false;

      return true;
    }

    @Override
    public int hashCode() {
      int result = address.hashCode();
      result = 31 * result + ppService.hashCode();
      return result;
    }
  }
}
/**
 * The Jabber implementation of the <tt>OperationSetFileTransfer</tt> interface.
 *
 * @author Gregory Bande
 * @author Nicolas Riegel
 * @author Yana Stamcheva
 */
public class OperationSetFileTransferJabberImpl implements OperationSetFileTransfer {
  /** The logger for this class. */
  private static final Logger logger = Logger.getLogger(OperationSetFileTransferJabberImpl.class);

  /** The provider that created us. */
  private final ProtocolProviderServiceJabberImpl jabberProvider;

  /** An active instance of the opSetPersPresence operation set. */
  private OperationSetPersistentPresenceJabberImpl opSetPersPresence = null;

  /** The Jabber file transfer manager. */
  private FileTransferManager manager = null;

  /** The Jabber file transfer listener. */
  private FileTransferRequestListener fileTransferRequestListener;

  /** A list of listeners registered for file transfer events. */
  private Vector<FileTransferListener> fileTransferListeners = new Vector<FileTransferListener>();

  // Register file transfer features on every established connection
  // to make sure we register them before creating our
  // ServiceDiscoveryManager
  static {
    Connection.addConnectionCreationListener(
        new ConnectionCreationListener() {
          public void connectionCreated(Connection connection) {
            FileTransferNegotiator.getInstanceFor(connection);
          }
        });
  }

  /**
   * Constructor
   *
   * @param provider is the provider that created us
   */
  public OperationSetFileTransferJabberImpl(ProtocolProviderServiceJabberImpl provider) {
    this.jabberProvider = provider;

    provider.addRegistrationStateChangeListener(new RegistrationStateListener());

    // use only ibb for file transfers
    FileTransferNegotiator.IBB_ONLY = true;
  }

  /**
   * Sends a file transfer request to the given <tt>toContact</tt>.
   *
   * @return the transfer object
   * @param toContact the contact that should receive the file
   * @param file file to send
   */
  public FileTransfer sendFile(Contact toContact, File file)
      throws IllegalStateException, IllegalArgumentException, OperationNotSupportedException {
    return sendFile(toContact, file, null);
  }

  /**
   * Sends a file transfer request to the given <tt>toContact</tt>.
   *
   * @return the transfer object
   * @param toContact the contact that should receive the file
   * @param file file to send
   * @param gw special gateway to be used for receiver if its jid misses the domain part
   */
  FileTransfer sendFile(Contact toContact, File file, String gw)
      throws IllegalStateException, IllegalArgumentException, OperationNotSupportedException {
    OutgoingFileTransferJabberImpl outgoingTransfer = null;

    try {
      assertConnected();

      if (file.length() > getMaximumFileLength())
        throw new IllegalArgumentException("File length exceeds the allowed one for this protocol");

      String fullJid = null;
      // Find the jid of the contact which support file transfer
      // and is with highest priority if more than one found
      // if we have equals priorities
      // choose the one that is more available
      OperationSetMultiUserChat mucOpSet =
          jabberProvider.getOperationSet(OperationSetMultiUserChat.class);
      if (mucOpSet != null && mucOpSet.isPrivateMessagingContact(toContact.getAddress())) {
        fullJid = toContact.getAddress();
      } else {
        Iterator<Presence> iter =
            jabberProvider.getConnection().getRoster().getPresences(toContact.getAddress());
        int bestPriority = -1;

        PresenceStatus jabberStatus = null;

        while (iter.hasNext()) {
          Presence presence = iter.next();

          if (jabberProvider.isFeatureListSupported(
              presence.getFrom(),
              new String[] {
                "http://jabber.org/protocol/si",
                "http://jabber.org/protocol/si/profile/file-transfer"
              })) {

            int priority =
                (presence.getPriority() == Integer.MIN_VALUE) ? 0 : presence.getPriority();

            if (priority > bestPriority) {
              bestPriority = priority;
              fullJid = presence.getFrom();
              jabberStatus =
                  OperationSetPersistentPresenceJabberImpl.jabberStatusToPresenceStatus(
                      presence, jabberProvider);
            } else if (priority == bestPriority && jabberStatus != null) {
              PresenceStatus tempStatus =
                  OperationSetPersistentPresenceJabberImpl.jabberStatusToPresenceStatus(
                      presence, jabberProvider);
              if (tempStatus.compareTo(jabberStatus) > 0) {
                fullJid = presence.getFrom();
                jabberStatus = tempStatus;
              }
            }
          }
        }
      }

      // First we check if file transfer is at all supported for this
      // contact.
      if (fullJid == null) {
        throw new OperationNotSupportedException(
            "Contact client or server does not support file transfers.");
      }

      if (gw != null && !fullJid.contains("@") && !fullJid.endsWith(gw)) {
        fullJid = fullJid + "@" + gw;
      }

      OutgoingFileTransfer transfer = manager.createOutgoingFileTransfer(fullJid);

      outgoingTransfer =
          new OutgoingFileTransferJabberImpl(toContact, file, transfer, jabberProvider);

      // Notify all interested listeners that a file transfer has been
      // created.
      FileTransferCreatedEvent event = new FileTransferCreatedEvent(outgoingTransfer, new Date());

      fireFileTransferCreated(event);

      // Send the file through the Jabber file transfer.
      transfer.sendFile(file, "Sending file");

      // Start the status and progress thread.
      new FileTransferProgressThread(transfer, outgoingTransfer).start();
    } catch (XMPPException e) {
      logger.error("Failed to send file.", e);
    }

    return outgoingTransfer;
  }

  /**
   * Sends a file transfer request to the given <tt>toContact</tt> by specifying the local and
   * remote file path and the <tt>fromContact</tt>, sending the file.
   *
   * @return the transfer object
   * @param toContact the contact that should receive the file
   * @param fromContact the contact sending the file
   * @param remotePath the remote file path
   * @param localPath the local file path
   */
  public FileTransfer sendFile(
      Contact toContact, Contact fromContact, String remotePath, String localPath)
      throws IllegalStateException, IllegalArgumentException, OperationNotSupportedException {
    return this.sendFile(toContact, new File(localPath));
  }

  /**
   * Adds the given <tt>FileTransferListener</tt> that would listen for file transfer requests and
   * created file transfers.
   *
   * @param listener the <tt>FileTransferListener</tt> to add
   */
  public void addFileTransferListener(FileTransferListener listener) {
    synchronized (fileTransferListeners) {
      if (!fileTransferListeners.contains(listener)) {
        this.fileTransferListeners.add(listener);
      }
    }
  }

  /**
   * Removes the given <tt>FileTransferListener</tt> that listens for file transfer requests and
   * created file transfers.
   *
   * @param listener the <tt>FileTransferListener</tt> to remove
   */
  public void removeFileTransferListener(FileTransferListener listener) {
    synchronized (fileTransferListeners) {
      this.fileTransferListeners.remove(listener);
    }
  }

  /**
   * Utility method throwing an exception if the stack is not properly initialized.
   *
   * @throws java.lang.IllegalStateException if the underlying stack is not registered and
   *     initialized.
   */
  private void assertConnected() throws IllegalStateException {
    if (jabberProvider == null)
      throw new IllegalStateException(
          "The provider must be non-null and signed on the "
              + "service before being able to send a file.");
    else if (!jabberProvider.isRegistered()) {
      // if we are not registered but the current status is online
      // change the current status
      if (opSetPersPresence.getPresenceStatus().isOnline()) {
        opSetPersPresence.fireProviderStatusChangeEvent(
            opSetPersPresence.getPresenceStatus(),
            jabberProvider.getJabberStatusEnum().getStatus(JabberStatusEnum.OFFLINE));
      }

      throw new IllegalStateException(
          "The provider must be signed on the service before " + "being able to send a file.");
    }
  }

  /**
   * Returns the maximum file length supported by the protocol in bytes. Supports up to 2GB.
   *
   * @return the file length that is supported.
   */
  public long getMaximumFileLength() {
    return 2147483648l; // = 2048*1024*1024;
  }

  /** Our listener that will tell us when we're registered to */
  private class RegistrationStateListener implements RegistrationStateChangeListener {
    /**
     * The method is called by a ProtocolProvider implementation whenever a change in the
     * registration state of the corresponding provider had occurred.
     *
     * @param evt ProviderStatusChangeEvent the event describing the status change.
     */
    public void registrationStateChanged(RegistrationStateChangeEvent evt) {
      if (logger.isDebugEnabled())
        logger.debug(
            "The provider changed state from: " + evt.getOldState() + " to: " + evt.getNewState());

      if (evt.getNewState() == RegistrationState.REGISTERED) {
        opSetPersPresence =
            (OperationSetPersistentPresenceJabberImpl)
                jabberProvider.getOperationSet(OperationSetPersistentPresence.class);

        // Create the Jabber FileTransferManager.
        manager = new FileTransferManager(jabberProvider.getConnection());

        fileTransferRequestListener = new FileTransferRequestListener();

        ProviderManager.getInstance()
            .addIQProvider(FileElement.ELEMENT_NAME, FileElement.NAMESPACE, new FileElement());

        ProviderManager.getInstance()
            .addIQProvider(ThumbnailIQ.ELEMENT_NAME, ThumbnailIQ.NAMESPACE, new ThumbnailIQ());

        jabberProvider
            .getConnection()
            .addPacketListener(
                fileTransferRequestListener,
                new AndFilter(
                    new PacketTypeFilter(StreamInitiation.class), new IQTypeFilter(IQ.Type.SET)));
      } else if (evt.getNewState() == RegistrationState.UNREGISTERED) {
        if (fileTransferRequestListener != null && jabberProvider.getConnection() != null) {
          jabberProvider.getConnection().removePacketListener(fileTransferRequestListener);
        }

        ProviderManager providerManager = ProviderManager.getInstance();
        if (providerManager != null) {
          ProviderManager.getInstance()
              .removeIQProvider(FileElement.ELEMENT_NAME, FileElement.NAMESPACE);

          ProviderManager.getInstance()
              .removeIQProvider(ThumbnailIQ.ELEMENT_NAME, ThumbnailIQ.NAMESPACE);
        }

        fileTransferRequestListener = null;
        manager = null;
      }
    }
  }

  /** Listener for Jabber incoming file transfer requests. */
  private class FileTransferRequestListener implements PacketListener {
    /**
     * Listens for file transfer packets.
     *
     * @param packet packet to be processed
     */
    public void processPacket(Packet packet) {
      if (!(packet instanceof StreamInitiation)) return;

      if (logger.isDebugEnabled()) logger.debug("Incoming Jabber file transfer request.");

      StreamInitiation streamInitiation = (StreamInitiation) packet;

      FileTransferRequest jabberRequest = new FileTransferRequest(manager, streamInitiation);

      // Create a global incoming file transfer request.
      IncomingFileTransferRequestJabberImpl incomingFileTransferRequest =
          new IncomingFileTransferRequestJabberImpl(
              jabberProvider, OperationSetFileTransferJabberImpl.this, jabberRequest);

      // Send a thumbnail request if a thumbnail is advertised in the
      // streamInitiation packet.
      org.jivesoftware.smackx.packet.StreamInitiation.File file = streamInitiation.getFile();

      boolean isThumbnailedFile = false;
      if (file instanceof FileElement) {
        ThumbnailElement thumbnailElement = ((FileElement) file).getThumbnailElement();

        if (thumbnailElement != null) {
          isThumbnailedFile = true;
          incomingFileTransferRequest.createThumbnailListeners(thumbnailElement.getCid());

          ThumbnailIQ thumbnailRequest =
              new ThumbnailIQ(
                  streamInitiation.getTo(),
                  streamInitiation.getFrom(),
                  thumbnailElement.getCid(),
                  IQ.Type.GET);

          if (logger.isDebugEnabled())
            logger.debug("Sending thumbnail request:" + thumbnailRequest.toXML());

          jabberProvider.getConnection().sendPacket(thumbnailRequest);
        }
      }

      if (!isThumbnailedFile) {
        // Create an event associated to this global request.
        FileTransferRequestEvent fileTransferRequestEvent =
            new FileTransferRequestEvent(
                OperationSetFileTransferJabberImpl.this, incomingFileTransferRequest, new Date());

        // Notify the global listener that a request has arrived.
        fireFileTransferRequest(fileTransferRequestEvent);
      }
    }
  }

  /**
   * Delivers the specified event to all registered file transfer listeners.
   *
   * @param event the <tt>EventObject</tt> that we'd like delivered to all registered file transfer
   *     listeners.
   */
  void fireFileTransferRequest(FileTransferRequestEvent event) {
    Iterator<FileTransferListener> listeners = null;
    synchronized (fileTransferListeners) {
      listeners = new ArrayList<FileTransferListener>(fileTransferListeners).iterator();
    }

    while (listeners.hasNext()) {
      FileTransferListener listener = listeners.next();

      listener.fileTransferRequestReceived(event);
    }
  }

  /**
   * Delivers the specified event to all registered file transfer listeners.
   *
   * @param event the <tt>EventObject</tt> that we'd like delivered to all registered file transfer
   *     listeners.
   */
  void fireFileTransferRequestRejected(FileTransferRequestEvent event) {
    Iterator<FileTransferListener> listeners = null;
    synchronized (fileTransferListeners) {
      listeners = new ArrayList<FileTransferListener>(fileTransferListeners).iterator();
    }

    while (listeners.hasNext()) {
      FileTransferListener listener = listeners.next();

      listener.fileTransferRequestRejected(event);
    }
  }

  /**
   * Delivers the file transfer to all registered listeners.
   *
   * @param event the <tt>FileTransferEvent</tt> that we'd like delivered to all registered file
   *     transfer listeners.
   */
  void fireFileTransferCreated(FileTransferCreatedEvent event) {
    Iterator<FileTransferListener> listeners = null;
    synchronized (fileTransferListeners) {
      listeners = new ArrayList<FileTransferListener>(fileTransferListeners).iterator();
    }

    while (listeners.hasNext()) {
      FileTransferListener listener = listeners.next();

      listener.fileTransferCreated(event);
    }
  }

  /** Updates file transfer progress and status while sending or receiving a file. */
  protected static class FileTransferProgressThread extends Thread {
    private final org.jivesoftware.smackx.filetransfer.FileTransfer jabberTransfer;
    private final AbstractFileTransfer fileTransfer;

    private long initialFileSize;

    public FileTransferProgressThread(
        org.jivesoftware.smackx.filetransfer.FileTransfer jabberTransfer,
        AbstractFileTransfer transfer,
        long initialFileSize) {
      this.jabberTransfer = jabberTransfer;
      this.fileTransfer = transfer;
      this.initialFileSize = initialFileSize;
    }

    public FileTransferProgressThread(
        org.jivesoftware.smackx.filetransfer.FileTransfer jabberTransfer,
        AbstractFileTransfer transfer) {
      this.jabberTransfer = jabberTransfer;
      this.fileTransfer = transfer;
    }

    /** Thread entry point. */
    @Override
    public void run() {
      int status;
      long progress;
      String statusReason = "";

      while (true) {
        try {
          Thread.sleep(10);

          status = parseJabberStatus(jabberTransfer.getStatus());
          progress = fileTransfer.getTransferedBytes();

          if (status == FileTransferStatusChangeEvent.FAILED
              || status == FileTransferStatusChangeEvent.COMPLETED
              || status == FileTransferStatusChangeEvent.CANCELED
              || status == FileTransferStatusChangeEvent.REFUSED) {
            if (fileTransfer instanceof OutgoingFileTransferJabberImpl) {
              ((OutgoingFileTransferJabberImpl) fileTransfer).removeThumbnailRequestListener();
            }

            // sometimes a filetransfer can be preparing
            // and than completed :
            // transfered in one iteration of current thread
            // so it won't go through intermediate state - inProgress
            // make sure this won't happen
            if (status == FileTransferStatusChangeEvent.COMPLETED
                && fileTransfer.getStatus() == FileTransferStatusChangeEvent.PREPARING) {
              fileTransfer.fireStatusChangeEvent(
                  FileTransferStatusChangeEvent.IN_PROGRESS, "Status changed");
              fileTransfer.fireProgressChangeEvent(System.currentTimeMillis(), progress);
            }

            break;
          }

          fileTransfer.fireStatusChangeEvent(status, "Status changed");
          fileTransfer.fireProgressChangeEvent(System.currentTimeMillis(), progress);
        } catch (InterruptedException e) {
          if (logger.isDebugEnabled()) logger.debug("Unable to sleep thread.", e);
        }
      }

      if (jabberTransfer.getError() != null) {
        logger.error(
            "An error occured while transfering file: " + jabberTransfer.getError().getMessage());
      }

      if (jabberTransfer.getException() != null) {
        logger.error(
            "An exception occured while transfering file: ", jabberTransfer.getException());

        if (jabberTransfer.getException() instanceof XMPPException) {
          XMPPError error = ((XMPPException) jabberTransfer.getException()).getXMPPError();
          if (error != null)
            if (error.getCode() == 406 || error.getCode() == 403)
              status = FileTransferStatusChangeEvent.REFUSED;
        }

        statusReason = jabberTransfer.getException().getMessage();
      }

      if (initialFileSize > 0
          && status == FileTransferStatusChangeEvent.COMPLETED
          && fileTransfer.getTransferedBytes() < initialFileSize) {
        status = FileTransferStatusChangeEvent.CANCELED;
      }

      fileTransfer.fireStatusChangeEvent(status, statusReason);
      fileTransfer.fireProgressChangeEvent(System.currentTimeMillis(), progress);
    }
  }

  /**
   * Parses the given Jabber status to a <tt>FileTransfer</tt> interface status.
   *
   * @param jabberStatus the Jabber status to parse
   * @return the parsed status
   */
  private static int parseJabberStatus(Status jabberStatus) {
    if (jabberStatus.equals(Status.complete)) return FileTransferStatusChangeEvent.COMPLETED;
    else if (jabberStatus.equals(Status.cancelled)) return FileTransferStatusChangeEvent.CANCELED;
    else if (jabberStatus.equals(Status.in_progress) || jabberStatus.equals(Status.negotiated))
      return FileTransferStatusChangeEvent.IN_PROGRESS;
    else if (jabberStatus.equals(Status.error)) return FileTransferStatusChangeEvent.FAILED;
    else if (jabberStatus.equals(Status.refused)) return FileTransferStatusChangeEvent.REFUSED;
    else if (jabberStatus.equals(Status.negotiating_transfer)
        || jabberStatus.equals(Status.negotiating_stream))
      return FileTransferStatusChangeEvent.PREPARING;
    else
      // FileTransfer.Status.initial
      return FileTransferStatusChangeEvent.WAITING;
  }
}
/**
 * The <tt>FileTransferConversationComponent</tt> is the parent of all file conversation components
 * - for incoming, outgoing and history file transfers.
 *
 * @author Yana Stamcheva
 * @author Adam Netocny
 */
public abstract class FileTransferConversationComponent extends ChatConversationComponent
    implements ActionListener, FileTransferProgressListener, Skinnable {
  /** The logger for this class. */
  private final Logger logger = Logger.getLogger(FileTransferConversationComponent.class);

  /** Image default width. */
  protected static final int IMAGE_WIDTH = 64;

  /** Image default height. */
  protected static final int IMAGE_HEIGHT = 64;

  /** The image label. */
  protected final FileImageLabel imageLabel;

  /** The title label. */
  protected final JLabel titleLabel = new JLabel();

  /** The file label. */
  protected final JLabel fileLabel = new JLabel();

  /** The error area. */
  private final JTextArea errorArea = new JTextArea();

  /** The error icon label. */
  private final JLabel errorIconLabel =
      new JLabel(new ImageIcon(ImageLoader.getImage(ImageLoader.EXCLAMATION_MARK)));

  /** The cancel button. */
  protected final ChatConversationButton cancelButton = new ChatConversationButton();

  /** The retry button. */
  protected final ChatConversationButton retryButton = new ChatConversationButton();

  /** The accept button. */
  protected final ChatConversationButton acceptButton = new ChatConversationButton();

  /** The reject button. */
  protected final ChatConversationButton rejectButton = new ChatConversationButton();

  /** The open file button. */
  protected final ChatConversationButton openFileButton = new ChatConversationButton();

  /** The open folder button. */
  protected final ChatConversationButton openFolderButton = new ChatConversationButton();

  /** The progress bar. */
  protected final JProgressBar progressBar = new JProgressBar();

  /** The progress properties panel. */
  private final TransparentPanel progressPropertiesPanel =
      new TransparentPanel(new FlowLayout(FlowLayout.RIGHT));

  /** The progress speed label. */
  private final JLabel progressSpeedLabel = new JLabel();

  /** The estimated time label. */
  private final JLabel estimatedTimeLabel = new JLabel();

  /** The download file. */
  private File downloadFile;

  /** The file transfer. */
  private FileTransfer fileTransfer;

  /** The speed calculated delay. */
  private static final int SPEED_CALCULATE_DELAY = 5000;

  /** The transferred file size. */
  private long transferredFileSize = 0;

  /** The time of the last calculated transfer speed. */
  private long lastSpeedTimestamp = 0;

  /** The last estimated time for the transfer. */
  private long lastEstimatedTimeTimestamp = 0;

  /** The number of bytes last transferred. */
  private long lastTransferredBytes = 0;

  /** The last calculated progress speed. */
  private long lastProgressSpeed;

  /** The last estimated time. */
  private long lastEstimatedTime;

  /** Creates a file conversation component. */
  public FileTransferConversationComponent() {
    imageLabel = new FileImageLabel();

    constraints.gridx = 0;
    constraints.gridy = 0;
    constraints.gridwidth = 1;
    constraints.gridheight = 4;
    constraints.anchor = GridBagConstraints.NORTHWEST;
    constraints.insets = new Insets(5, 5, 5, 5);

    add(imageLabel, constraints);
    imageLabel.setIcon(new ImageIcon(ImageLoader.getImage(ImageLoader.DEFAULT_FILE_ICON)));

    constraints.gridx = 1;
    constraints.gridy = 0;
    constraints.gridwidth = 3;
    constraints.gridheight = 1;
    constraints.fill = GridBagConstraints.HORIZONTAL;
    constraints.weightx = 1.0;
    constraints.anchor = GridBagConstraints.NORTHWEST;
    constraints.insets = new Insets(5, 5, 5, 5);

    add(titleLabel, constraints);
    titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD, 11f));

    constraints.gridx = 1;
    constraints.gridy = 1;
    constraints.anchor = GridBagConstraints.WEST;
    constraints.insets = new Insets(0, 5, 5, 5);

    add(fileLabel, constraints);

    constraints.gridx = 1;
    constraints.gridy = 2;
    constraints.gridwidth = 1;
    constraints.anchor = GridBagConstraints.WEST;
    constraints.insets = new Insets(0, 5, 0, 5);
    constraints.fill = GridBagConstraints.NONE;

    add(errorIconLabel, constraints);
    errorIconLabel.setVisible(false);

    constraints.gridx = 2;
    constraints.gridy = 2;
    constraints.gridwidth = 2;
    constraints.anchor = GridBagConstraints.WEST;
    constraints.insets = new Insets(0, 5, 0, 5);
    constraints.fill = GridBagConstraints.HORIZONTAL;

    add(errorArea, constraints);
    errorArea.setForeground(new Color(resources.getColor("service.gui.ERROR_FOREGROUND")));
    setTextAreaStyle(errorArea);
    errorArea.setVisible(false);

    constraints.gridx = 1;
    constraints.gridy = 3;
    constraints.gridwidth = 1;
    constraints.gridheight = 1;
    constraints.weightx = 0.0;
    constraints.anchor = GridBagConstraints.WEST;
    constraints.insets = new Insets(0, 5, 0, 5);

    add(retryButton, constraints);
    retryButton.setText(GuiActivator.getResources().getI18NString("service.gui.RETRY"));
    retryButton.setVisible(false);

    constraints.gridx = 1;
    constraints.gridy = 3;
    constraints.gridwidth = 1;
    constraints.gridheight = 1;
    constraints.weightx = 0.0;
    constraints.anchor = GridBagConstraints.WEST;
    constraints.insets = new Insets(0, 5, 0, 5);

    add(cancelButton, constraints);
    cancelButton.setText(GuiActivator.getResources().getI18NString("service.gui.CANCEL"));
    cancelButton.addActionListener(this);
    cancelButton.setVisible(false);

    constraints.gridx = 2;
    constraints.gridy = 3;
    constraints.gridwidth = GridBagConstraints.RELATIVE;
    constraints.gridheight = 1;
    constraints.weightx = 0.0;
    constraints.fill = GridBagConstraints.NONE;
    constraints.anchor = GridBagConstraints.EAST;
    constraints.insets = new Insets(0, 5, 0, 5);

    constraints.gridx = 3;
    constraints.gridy = 3;
    constraints.gridwidth = 1;
    constraints.gridheight = 1;
    constraints.weightx = 0.0;
    constraints.fill = GridBagConstraints.NONE;
    constraints.anchor = GridBagConstraints.LINE_END;
    constraints.insets = new Insets(0, 5, 0, 5);

    add(progressPropertiesPanel, constraints);

    estimatedTimeLabel.setFont(estimatedTimeLabel.getFont().deriveFont(11f));
    estimatedTimeLabel.setVisible(false);
    progressSpeedLabel.setFont(progressSpeedLabel.getFont().deriveFont(11f));
    progressSpeedLabel.setVisible(false);

    progressPropertiesPanel.add(progressSpeedLabel);
    progressPropertiesPanel.add(estimatedTimeLabel);

    constraints.gridx = 1;
    constraints.gridy = 3;
    constraints.gridwidth = 1;
    constraints.gridheight = 1;
    constraints.weightx = 0.0;
    constraints.anchor = GridBagConstraints.WEST;
    constraints.insets = new Insets(0, 5, 0, 5);
    constraints.fill = GridBagConstraints.NONE;

    add(acceptButton, constraints);
    acceptButton.setText(GuiActivator.getResources().getI18NString("service.gui.ACCEPT"));
    acceptButton.setVisible(false);

    constraints.gridx = 2;
    constraints.gridy = 3;
    constraints.gridwidth = 1;
    constraints.gridheight = 1;
    constraints.weightx = 0.0;
    constraints.anchor = GridBagConstraints.WEST;
    constraints.insets = new Insets(0, 5, 0, 5);
    constraints.fill = GridBagConstraints.NONE;

    add(rejectButton, constraints);
    rejectButton.setText(GuiActivator.getResources().getI18NString("service.gui.REJECT"));
    rejectButton.setVisible(false);

    constraints.gridx = 1;
    constraints.gridy = 3;
    constraints.gridwidth = 1;
    constraints.gridheight = 1;
    constraints.weightx = 0.0;
    constraints.anchor = GridBagConstraints.WEST;
    constraints.insets = new Insets(0, 5, 0, 5);
    constraints.fill = GridBagConstraints.NONE;

    add(openFileButton, constraints);
    openFileButton.setText(GuiActivator.getResources().getI18NString("service.gui.OPEN"));
    openFileButton.setVisible(false);
    openFileButton.addActionListener(this);

    constraints.gridx = 2;
    constraints.gridy = 3;
    constraints.gridwidth = 1;
    constraints.gridheight = 1;
    constraints.weightx = 0.0;
    constraints.anchor = GridBagConstraints.WEST;
    constraints.insets = new Insets(0, 5, 0, 5);
    constraints.fill = GridBagConstraints.NONE;

    add(openFolderButton, constraints);
    openFolderButton.setText(GuiActivator.getResources().getI18NString("service.gui.OPEN_FOLDER"));
    openFolderButton.setVisible(false);
    openFolderButton.addActionListener(this);

    constraints.gridx = 1;
    constraints.gridy = 2;
    constraints.gridwidth = 3;
    constraints.gridheight = 1;
    constraints.weightx = 1.0;
    constraints.anchor = GridBagConstraints.WEST;
    constraints.insets = new Insets(0, 5, 0, 5);
    constraints.ipadx = 150;
    constraints.fill = GridBagConstraints.HORIZONTAL;

    add(progressBar, constraints);
    progressBar.setVisible(false);
    progressBar.setStringPainted(true);
  }

  /**
   * Sets a custom style for the given text area.
   *
   * @param textArea the text area to style
   */
  private void setTextAreaStyle(JTextArea textArea) {
    textArea.setOpaque(false);
    textArea.setLineWrap(true);
    textArea.setWrapStyleWord(true);
  }

  /**
   * Shows the given error message in the error area of this component.
   *
   * @param message the message to show
   */
  protected void showErrorMessage(String message) {
    errorArea.setText(message);
    errorIconLabel.setVisible(true);
    errorArea.setVisible(true);
  }

  /**
   * Sets the download file.
   *
   * @param file the file that has been downloaded or sent
   */
  protected void setCompletedDownloadFile(File file) {
    this.downloadFile = file;

    imageLabel.setFile(downloadFile);

    imageLabel.setToolTipText(resources.getI18NString("service.gui.OPEN_FILE_FROM_IMAGE"));

    imageLabel.addMouseListener(
        new MouseAdapter() {
          public void mouseClicked(MouseEvent e) {
            if (e.getClickCount() > 1) {
              openFile(downloadFile);
            }
          }
        });
  }

  /**
   * Sets the file transfer.
   *
   * @param fileTransfer the file transfer
   * @param transferredFileSize the size of the transferred file
   */
  protected void setFileTransfer(FileTransfer fileTransfer, long transferredFileSize) {
    this.fileTransfer = fileTransfer;
    this.transferredFileSize = transferredFileSize;

    fileTransfer.addProgressListener(this);
  }

  /**
   * Handles buttons action events.
   *
   * @param evt the <tt>ActionEvent</tt> that notified us
   */
  public void actionPerformed(ActionEvent evt) {
    JButton sourceButton = (JButton) evt.getSource();

    if (sourceButton.equals(openFileButton)) {
      this.openFile(downloadFile);
    } else if (sourceButton.equals(openFolderButton)) {
      try {
        File downloadDir = GuiActivator.getFileAccessService().getDefaultDownloadDirectory();

        GuiActivator.getDesktopService().open(downloadDir);
      } catch (IllegalArgumentException e) {
        if (logger.isDebugEnabled()) logger.debug("Unable to open folder.", e);

        this.showErrorMessage(resources.getI18NString("service.gui.FOLDER_DOES_NOT_EXIST"));
      } catch (NullPointerException e) {
        if (logger.isDebugEnabled()) logger.debug("Unable to open folder.", e);

        this.showErrorMessage(resources.getI18NString("service.gui.FOLDER_DOES_NOT_EXIST"));
      } catch (UnsupportedOperationException e) {
        if (logger.isDebugEnabled()) logger.debug("Unable to open folder.", e);

        this.showErrorMessage(resources.getI18NString("service.gui.FILE_OPEN_NOT_SUPPORTED"));
      } catch (SecurityException e) {
        if (logger.isDebugEnabled()) logger.debug("Unable to open folder.", e);

        this.showErrorMessage(resources.getI18NString("service.gui.FOLDER_OPEN_NO_PERMISSION"));
      } catch (IOException e) {
        if (logger.isDebugEnabled()) logger.debug("Unable to open folder.", e);

        this.showErrorMessage(resources.getI18NString("service.gui.FOLDER_OPEN_NO_APPLICATION"));
      } catch (Exception e) {
        if (logger.isDebugEnabled()) logger.debug("Unable to open file.", e);

        this.showErrorMessage(resources.getI18NString("service.gui.FOLDER_OPEN_FAILED"));
      }
    } else if (sourceButton.equals(cancelButton)) {
      if (fileTransfer != null) fileTransfer.cancel();
    }
  }

  /**
   * Updates progress bar progress line every time a progress event has been received.
   *
   * @param event the <tt>FileTransferProgressEvent</tt> that notified us
   */
  public void progressChanged(FileTransferProgressEvent event) {
    progressBar.setValue((int) event.getProgress());

    long transferredBytes = event.getFileTransfer().getTransferedBytes();
    long progressTimestamp = event.getTimestamp();

    ByteFormat format = new ByteFormat();
    String bytesString = format.format(transferredBytes);

    if ((progressTimestamp - lastSpeedTimestamp) >= SPEED_CALCULATE_DELAY) {
      lastProgressSpeed = Math.round(calculateProgressSpeed(transferredBytes));

      this.lastSpeedTimestamp = progressTimestamp;
      this.lastTransferredBytes = transferredBytes;
    }

    if ((progressTimestamp - lastEstimatedTimeTimestamp) >= SPEED_CALCULATE_DELAY
        && lastProgressSpeed > 0) {
      lastEstimatedTime =
          Math.round(
              calculateEstimatedTransferTime(
                  lastProgressSpeed, transferredFileSize - transferredBytes));

      lastEstimatedTimeTimestamp = progressTimestamp;
    }

    progressBar.setString(getProgressLabel(bytesString));

    if (lastProgressSpeed > 0) {
      progressSpeedLabel.setText(
          resources.getI18NString("service.gui.SPEED") + format.format(lastProgressSpeed) + "/sec");
      progressSpeedLabel.setVisible(true);
    }

    if (lastEstimatedTime > 0) {
      estimatedTimeLabel.setText(
          resources.getI18NString("service.gui.ESTIMATED_TIME")
              + GuiUtils.formatSeconds(lastEstimatedTime * 1000));
      estimatedTimeLabel.setVisible(true);
    }
  }

  /**
   * Returns the string, showing information for the given file.
   *
   * @param file the file
   * @return the name of the given file
   */
  protected String getFileLabel(File file) {
    String fileName = file.getName();
    long fileSize = file.length();

    ByteFormat format = new ByteFormat();
    String text = format.format(fileSize);

    return fileName + " (" + text + ")";
  }

  /**
   * Returns the string, showing information for the given file.
   *
   * @param fileName the name of the file
   * @param fileSize the size of the file
   * @return the name of the given file
   */
  protected String getFileLabel(String fileName, long fileSize) {
    ByteFormat format = new ByteFormat();
    String text = format.format(fileSize);

    return fileName + " (" + text + ")";
  }

  /** Hides all progress related components. */
  protected void hideProgressRelatedComponents() {
    progressBar.setVisible(false);
    progressSpeedLabel.setVisible(false);
    estimatedTimeLabel.setVisible(false);
  }

  /**
   * Returns the label to show on the progress bar.
   *
   * @param bytesString the bytes that have been transfered
   * @return the label to show on the progress bar
   */
  protected abstract String getProgressLabel(String bytesString);

  /**
   * Returns the speed of the transfer.
   *
   * @param transferredBytes the number of bytes that have been transferred
   * @return the speed of the transfer
   */
  private double calculateProgressSpeed(long transferredBytes) {
    // Bytes per second = bytes / SPEED_CALCULATE_DELAY miliseconds * 1000.
    return (transferredBytes - lastTransferredBytes) / SPEED_CALCULATE_DELAY * 1000;
  }

  /**
   * Returns the estimated transfer time left.
   *
   * @param speed the speed of the transfer
   * @param bytesLeft the size of the file
   * @return the estimated transfer time left
   */
  private double calculateEstimatedTransferTime(double speed, long bytesLeft) {
    return bytesLeft / speed;
  }

  /** Reload images and colors. */
  public void loadSkin() {
    errorIconLabel.setIcon(new ImageIcon(ImageLoader.getImage(ImageLoader.EXCLAMATION_MARK)));

    if (downloadFile != null)
      imageLabel.setIcon(new ImageIcon(ImageLoader.getImage(ImageLoader.DEFAULT_FILE_ICON)));

    errorArea.setForeground(new Color(resources.getColor("service.gui.ERROR_FOREGROUND")));
  }
}
/**
 * The ICQ protocol filetransfer OperationSet.
 *
 * @author Anthony Schmitt
 * @author Damian Minkov
 */
public class OperationSetFileTransferIcqImpl
    implements OperationSetFileTransfer, RvConnectionManagerListener {
  private static final Logger logger = Logger.getLogger(OperationSetFileTransferIcqImpl.class);

  /** A call back to the ICQ provider that created us. */
  private ProtocolProviderServiceIcqImpl icqProvider = null;

  /** A list of listeners registered for file transfer events. */
  private ArrayList<FileTransferListener> fileTransferListeners =
      new ArrayList<FileTransferListener>();

  /**
   * Create a new FileTransfer OperationSet over the specified Icq provider
   *
   * @param icqProvider ICQ protocol provider service
   */
  public OperationSetFileTransferIcqImpl(ProtocolProviderServiceIcqImpl icqProvider) {
    this.icqProvider = icqProvider;

    icqProvider.addRegistrationStateChangeListener(new RegistrationStateListener());
  }

  /**
   * Sends a file transfer request to the given <tt>toContact</tt> by specifying the local and
   * remote file path and the <tt>fromContact</tt>, sending the file.
   *
   * @param toContact the contact that should receive the file
   * @param file the file to send
   * @return the transfer object
   * @throws IllegalStateException if the protocol provider is not registered or connected
   * @throws IllegalArgumentException if some of the arguments doesn't fit the protocol requirements
   */
  public FileTransfer sendFile(Contact toContact, File file)
      throws IllegalStateException, IllegalArgumentException {
    assertConnected();

    if (file.length() > getMaximumFileLength())
      throw new IllegalArgumentException("File length exceeds the allowed one for this protocol");

    // Get the aim connection
    AimConnection aimConnection = icqProvider.getAimConnection();

    // Create an outgoing file transfer instance
    OutgoingFileTransfer outgoingFileTransfer =
        aimConnection
            .getIcbmService()
            .getRvConnectionManager()
            .createOutgoingFileTransfer(new Screenname(toContact.getAddress()));

    String id =
        String.valueOf(outgoingFileTransfer.getRvSessionInfo().getRvSession().getRvSessionId());

    FileTransferImpl outFileTransfer =
        new FileTransferImpl(outgoingFileTransfer, id, toContact, file, FileTransfer.OUT);

    // Adding the file to the outgoing file transfer
    try {
      outgoingFileTransfer.setSingleFile(new File(file.getPath()));
    } catch (IOException e) {
      if (logger.isDebugEnabled()) logger.debug("Error sending file", e);
      return null;
    }

    // Notify all interested listeners that a file transfer has been
    // created.
    FileTransferCreatedEvent event = new FileTransferCreatedEvent(outFileTransfer, new Date());

    fireFileTransferCreated(event);

    // Sending the file
    outgoingFileTransfer.sendRequest(new InvitationMessage(""));

    outFileTransfer.fireStatusChangeEvent(FileTransferStatusChangeEvent.PREPARING);

    return outFileTransfer;
  }

  /**
   * Sends a file transfer request to the given <tt>toContact</tt> by specifying the local and
   * remote file path and the <tt>fromContact</tt>, sending the file.
   *
   * @param toContact the contact that should receive the file
   * @param fromContact the contact sending the file
   * @param remotePath the remote file path
   * @param localPath the local file path
   * @return the transfer object
   * @throws IllegalStateException if the protocol provider is not registered or connected
   * @throws IllegalArgumentException if some of the arguments doesn't fit the protocol requirements
   */
  public FileTransfer sendFile(
      Contact toContact, Contact fromContact, String remotePath, String localPath)
      throws IllegalStateException, IllegalArgumentException {
    return this.sendFile(toContact, new File(localPath));
  }

  /**
   * Adds the given <tt>FileTransferListener</tt> that would listen for file transfer requests and
   * created file transfers.
   *
   * @param listener the <tt>FileTransferListener</tt> to add
   */
  public void addFileTransferListener(FileTransferListener listener) {
    synchronized (fileTransferListeners) {
      if (!fileTransferListeners.contains(listener)) {
        this.fileTransferListeners.add(listener);
      }
    }
  }

  /**
   * Removes the given <tt>FileTransferListener</tt> that listens for file transfer requests and
   * created file transfers.
   *
   * @param listener the <tt>FileTransferListener</tt> to remove
   */
  public void removeFileTransferListener(FileTransferListener listener) {
    synchronized (fileTransferListeners) {
      this.fileTransferListeners.remove(listener);
    }
  }

  /**
   * Utility method throwing an exception if the stack is not properly initialized.
   *
   * @throws java.lang.IllegalStateException if the underlying stack is not registered and
   *     initialized.
   */
  private void assertConnected() throws IllegalStateException {
    if (icqProvider == null)
      throw new IllegalStateException(
          "The provider must be non-null and signed on the "
              + "service before being able to send a file.");
    else if (!icqProvider.isRegistered())
      throw new IllegalStateException(
          "The provider must be signed on the service before " + "being able to send a file.");
  }

  /**
   * Function called when a icq file transfer request arrive
   *
   * @param manager the joustsim manager
   * @param transfer the incoming transfer
   */
  public void handleNewIncomingConnection(
      RvConnectionManager manager, IncomingRvConnection transfer) {
    if (transfer instanceof IncomingFileTransfer) {
      if (logger.isTraceEnabled())
        logger.trace("Incoming Icq file transfer request " + transfer.getClass());

      if (!(transfer instanceof IncomingFileTransfer)) {
        logger.warn("Wrong file transfer.");
        return;
      }

      OperationSetPersistentPresenceIcqImpl opSetPersPresence =
          (OperationSetPersistentPresenceIcqImpl)
              icqProvider.getOperationSet(OperationSetPersistentPresence.class);

      Contact sender =
          opSetPersPresence.findContactByID(transfer.getBuddyScreenname().getFormatted());

      IncomingFileTransfer incomingFileTransfer = (IncomingFileTransfer) transfer;

      final Date newDate = new Date();
      final IncomingFileTransferRequest req =
          new IncomingFileTransferRequestIcqImpl(
              icqProvider, this, incomingFileTransfer, sender, newDate);

      // this handels when we receive request and before accept or decline
      // it we receive cancel
      transfer.addEventListener(
          new RvConnectionEventListener() {
            public void handleEventWithStateChange(
                RvConnection transfer, RvConnectionState state, RvConnectionEvent event) {
              if (state == FileTransferState.FAILED && event instanceof BuddyCancelledEvent) {
                fireFileTransferRequestCanceled(
                    new FileTransferRequestEvent(
                        OperationSetFileTransferIcqImpl.this, req, newDate));
              }
            }

            public void handleEvent(RvConnection arg0, RvConnectionEvent arg1) {}
          });

      fireFileTransferRequest(new FileTransferRequestEvent(this, req, newDate));
    }
  }

  /**
   * Delivers the specified event to all registered file transfer listeners.
   *
   * @param event the <tt>EventObject</tt> that we'd like delivered to all registered file transfer
   *     listeners.
   */
  private void fireFileTransferRequest(FileTransferRequestEvent event) {
    Iterator<FileTransferListener> listeners = null;
    synchronized (fileTransferListeners) {
      listeners = new ArrayList<FileTransferListener>(fileTransferListeners).iterator();
    }

    while (listeners.hasNext()) {
      FileTransferListener listener = listeners.next();

      listener.fileTransferRequestReceived(event);
    }
  }

  /**
   * Delivers the specified event to all registered file transfer listeners.
   *
   * @param event the <tt>EventObject</tt> that we'd like delivered to all registered file transfer
   *     listeners.
   */
  void fireFileTransferRequestRejected(FileTransferRequestEvent event) {
    Iterator<FileTransferListener> listeners = null;
    synchronized (fileTransferListeners) {
      listeners = new ArrayList<FileTransferListener>(fileTransferListeners).iterator();
    }

    while (listeners.hasNext()) {
      FileTransferListener listener = listeners.next();

      listener.fileTransferRequestRejected(event);
    }
  }

  /**
   * Delivers the specified event to all registered file transfer listeners.
   *
   * @param event the <tt>EventObject</tt> that we'd like delivered to all registered file transfer
   *     listeners.
   */
  void fireFileTransferRequestCanceled(FileTransferRequestEvent event) {
    Iterator<FileTransferListener> listeners = null;
    synchronized (fileTransferListeners) {
      listeners = new ArrayList<FileTransferListener>(fileTransferListeners).iterator();
    }

    while (listeners.hasNext()) {
      FileTransferListener listener = listeners.next();

      listener.fileTransferRequestCanceled(event);
    }
  }

  /**
   * Delivers the file transfer to all registered listeners.
   *
   * @param event the <tt>FileTransferEvent</tt> that we'd like delivered to all registered file
   *     transfer listeners.
   */
  void fireFileTransferCreated(FileTransferCreatedEvent event) {
    Iterator<FileTransferListener> listeners = null;
    synchronized (fileTransferListeners) {
      listeners = new ArrayList<FileTransferListener>(fileTransferListeners).iterator();
    }

    while (listeners.hasNext()) {
      FileTransferListener listener = listeners.next();
      listener.fileTransferCreated(event);
    }
  }

  /**
   * Returns the maximum file length supported by the protocol in bytes. Supports up to 2GB.
   *
   * @return the file length that is supported.
   */
  public long getMaximumFileLength() {
    return 2147483648l; // = 2048*1024*1024;
  }

  /** Our listener that will tell us when we're registered to */
  private class RegistrationStateListener implements RegistrationStateChangeListener {
    /**
     * The method is called by a ProtocolProvider implementation whenever a change in the
     * registration state of the corresponding provider had occurred.
     *
     * @param evt ProviderStatusChangeEvent the event describing the status change.
     */
    public void registrationStateChanged(RegistrationStateChangeEvent evt) {
      if (logger.isDebugEnabled())
        logger.debug(
            "The provider changed state from: " + evt.getOldState() + " to: " + evt.getNewState());

      if (evt.getNewState() == RegistrationState.REGISTERED) {
        AimConnection aimConnection = icqProvider.getAimConnection();
        aimConnection
            .getIcbmService()
            .getRvConnectionManager()
            .addConnectionManagerListener(OperationSetFileTransferIcqImpl.this);
      }
    }
  }
}
/**
 * Implements <tt>OperationSetVideoBridge</tt> for Jabber.
 *
 * @author Yana Stamcheva
 * @author Lyubomir Marinov
 */
public class OperationSetVideoBridgeImpl
    implements OperationSetVideoBridge,
        PacketFilter,
        PacketListener,
        RegistrationStateChangeListener {
  /**
   * The <tt>Logger</tt> used by the <tt>OperationSetVideoBridgeImpl</tt> class and its instances
   * for logging output.
   */
  private static final Logger logger = Logger.getLogger(OperationSetVideoBridgeImpl.class);

  /**
   * The <tt>ProtocolProviderService</tt> implementation which initialized this instance, owns it
   * and is often referred to as its parent.
   */
  private final ProtocolProviderServiceJabberImpl protocolProvider;

  /**
   * Creates an instance of <tt>OperationSetVideoBridgeImpl</tt> by specifying the parent
   * <tt>ProtocolProviderService</tt> announcing this operation set.
   *
   * @param protocolProvider the parent Jabber protocol provider
   */
  public OperationSetVideoBridgeImpl(ProtocolProviderServiceJabberImpl protocolProvider) {
    this.protocolProvider = protocolProvider;
    this.protocolProvider.addRegistrationStateChangeListener(this);
  }

  /**
   * Implements {@link PacketFilter}. Determines whether this instance is interested in a specific
   * {@link Packet}. <tt>OperationSetVideoBridgeImpl</tt> returns <tt>true</tt> if the specified
   * <tt>packet</tt> is a {@link ColibriConferenceIQ}; otherwise, <tt>false</tt>.
   *
   * @param packet the <tt>Packet</tt> to be determined whether this instance is interested in it
   * @return <tt>true</tt> if the specified <tt>packet</tt> is a <tt>ColibriConferenceIQ</tt>;
   *     otherwise, <tt>false</tt>
   */
  public boolean accept(Packet packet) {
    return (packet instanceof ColibriConferenceIQ);
  }

  /**
   * Creates a conference call with the specified callees as call peers via a video bridge provided
   * by the parent Jabber provider.
   *
   * @param callees the list of addresses that we should call
   * @return the newly created conference call containing all CallPeers
   * @throws OperationFailedException if establishing the conference call fails
   * @throws OperationNotSupportedException if the provider does not have any conferencing features.
   */
  public Call createConfCall(String[] callees)
      throws OperationFailedException, OperationNotSupportedException {
    return protocolProvider
        .getOperationSet(OperationSetTelephonyConferencing.class)
        .createConfCall(callees, new MediaAwareCallConference(true));
  }

  /**
   * Invites the callee represented by the specified uri to an already existing call using a video
   * bridge provided by the parent Jabber provider. The difference between this method and
   * createConfCall is that inviteCalleeToCall allows a user to add new peers to an already
   * established conference.
   *
   * @param uri the callee to invite to an existing conf call.
   * @param call the call that we should invite the callee to.
   * @return the CallPeer object corresponding to the callee represented by the specified uri.
   * @throws OperationFailedException if inviting the specified callee to the specified call fails
   * @throws OperationNotSupportedException if allowing additional callees to a pre-established call
   *     is not supported.
   */
  public CallPeer inviteCalleeToCall(String uri, Call call)
      throws OperationFailedException, OperationNotSupportedException {
    return protocolProvider
        .getOperationSet(OperationSetTelephonyConferencing.class)
        .inviteCalleeToCall(uri, call);
  }

  /**
   * Indicates if there's an active video bridge available at this moment. The Jabber provider may
   * announce support for video bridge, but it should not be used for calling until it becomes
   * actually active.
   *
   * @return <tt>true</tt> to indicate that there's currently an active available video bridge,
   *     <tt>false</tt> - otherwise
   */
  public boolean isActive() {
    String jitsiVideobridge = protocolProvider.getJitsiVideobridge();

    return ((jitsiVideobridge != null) && (jitsiVideobridge.length() > 0));
  }

  /**
   * Notifies this instance that a specific <tt>ColibriConferenceIQ</tt> has been received.
   *
   * @param conferenceIQ the <tt>ColibriConferenceIQ</tt> which has been received
   */
  private void processColibriConferenceIQ(ColibriConferenceIQ conferenceIQ) {
    /*
     * The application is not a Jitsi Videobridge server, it is a client.
     * Consequently, the specified ColibriConferenceIQ is sent to it in
     * relation to the part of the application's functionality which makes
     * requests to a Jitsi Videobridge server i.e. CallJabberImpl.
     *
     * Additionally, the method processColibriConferenceIQ is presently tasked
     * with processing ColibriConferenceIQ requests only. They are SET IQs
     * sent by the Jitsi Videobridge server to notify the application about
     * updates in the states of (colibri) conferences organized by the
     * application.
     */
    if (IQ.Type.SET.equals(conferenceIQ.getType()) && conferenceIQ.getID() != null) {
      OperationSetBasicTelephony<?> basicTelephony =
          protocolProvider.getOperationSet(OperationSetBasicTelephony.class);

      if (basicTelephony != null) {
        Iterator<? extends Call> i = basicTelephony.getActiveCalls();

        while (i.hasNext()) {
          Call call = i.next();

          if (call instanceof CallJabberImpl) {
            CallJabberImpl callJabberImpl = (CallJabberImpl) call;
            MediaAwareCallConference conference = callJabberImpl.getConference();

            if ((conference != null) && conference.isJitsiVideobridge()) {
              /*
               * TODO We may want to disallow rogue CallJabberImpl
               * instances which may throw an exception to prevent
               * the conferenceIQ from reaching the CallJabberImpl
               * instance which it was meant for.
               */
              if (callJabberImpl.processColibriConferenceIQ(conferenceIQ)) break;
            }
          }
        }
      }
    }
  }

  /**
   * Implements {@link PacketListener}. Notifies this instance that a specific {@link Packet} (which
   * this instance has already expressed interest into by returning <tt>true</tt> from {@link
   * #accept(Packet)}) has been received.
   *
   * @param packet the <tt>Packet</tt> which has been received and which this instance is given a
   *     chance to process
   */
  public void processPacket(Packet packet) {
    /*
     * As we do elsewhere, acknowledge the receipt of the Packet first and
     * then go about our business with it.
     */
    IQ iq = (IQ) packet;

    if (iq.getType() == IQ.Type.SET)
      protocolProvider.getConnection().sendPacket(IQ.createResultIQ(iq));

    /*
     * Now that the acknowledging is out of the way, do go about our
     * business with the Packet.
     */
    ColibriConferenceIQ conferenceIQ = (ColibriConferenceIQ) iq;
    boolean interrupted = false;

    try {
      processColibriConferenceIQ(conferenceIQ);
    } catch (Throwable t) {
      logger.error(
          "An error occurred during the processing of a " + packet.getClass().getName() + " packet",
          t);

      if (t instanceof InterruptedException) {
        /*
         * We cleared the interrupted state of the current Thread by
         * catching the InterruptedException. However, we do not really
         * care whether the current Thread has been interrupted - we
         * caught the InterruptedException because we want to swallow
         * any Throwable. Consequently, we should better restore the
         * interrupted state.
         */
        interrupted = true;
      } else if (t instanceof ThreadDeath) throw (ThreadDeath) t;
    }
    if (interrupted) Thread.currentThread().interrupt();
  }

  /**
   * {@inheritDoc}
   *
   * <p>Implements {@link RegistrationStateChangeListener}. Notifies this instance that there has
   * been a change in the <tt>RegistrationState</tt> of {@link #protocolProvider}. Subscribes this
   * instance to {@link ColibriConferenceIQ}s as soon as <tt>protocolProvider</tt> is registered and
   * unsubscribes it as soon as <tt>protocolProvider</tt> is unregistered.
   */
  public void registrationStateChanged(RegistrationStateChangeEvent ev) {
    RegistrationState registrationState = ev.getNewState();

    if (RegistrationState.REGISTERED.equals(registrationState)) {
      protocolProvider.getConnection().addPacketListener(this, this);
    } else if (RegistrationState.UNREGISTERED.equals(registrationState)) {
      XMPPConnection connection = protocolProvider.getConnection();

      if (connection != null) connection.removePacketListener(this);
    }
  }
}
/**
 * Tests in this class verify whether a precreated contact list is still there and whether it
 * creating contact groups works as expected.
 *
 * @author Emil Ivov
 */
public class TestOperationSetPersistentPresence extends TestCase {
  private static final Logger logger = Logger.getLogger(TestOperationSetPersistentPresence.class);

  private GibberishSlickFixture fixture = new GibberishSlickFixture();
  private OperationSetPersistentPresence opSetPersPresence1 = null;
  private OperationSetPersistentPresence opSetPersPresence2 = null;
  private static final String testGroupName = "NewGroup";
  private static final String testGroupName2 = "Renamed";

  public TestOperationSetPersistentPresence(String name) {
    super(name);
  }

  /**
   * Creates a test suite containing all tests of this class followed by test methods that we want
   * executed in a specified order.
   *
   * @return the Test suite to run
   */
  public static Test suite() {
    TestSuite suite = new TestSuite();

    // the following 2 need to be run in the specified order.
    // (postTestRemoveGroup() needs the group created from
    // postTestCreateGroup() )
    suite.addTest(new TestOperationSetPersistentPresence("postTestCreateGroup"));

    // rename
    suite.addTest(new TestOperationSetPersistentPresence("postTestRenameGroup"));

    suite.addTest(new TestOperationSetPersistentPresence("postTestRemoveGroup"));

    // create the contact list
    suite.addTest(new TestOperationSetPersistentPresence("prepareContactList"));

    suite.addTestSuite(TestOperationSetPersistentPresence.class);

    return suite;
  }

  @Override
  protected void setUp() throws Exception {
    super.setUp();
    fixture.setUp();

    Map<String, OperationSet> supportedOperationSets1 =
        fixture.provider1.getSupportedOperationSets();

    if (supportedOperationSets1 == null || supportedOperationSets1.size() < 1)
      throw new NullPointerException(
          "No OperationSet implementations are supported by " + "this Gibberish implementation. ");

    // get the operation set presence here.
    opSetPersPresence1 =
        (OperationSetPersistentPresence)
            supportedOperationSets1.get(OperationSetPersistentPresence.class.getName());

    // if still null then the implementation doesn't offer a presence
    // operation set which is unacceptable for gibberish.
    if (opSetPersPresence1 == null)
      throw new NullPointerException(
          "An implementation of the gibberish service must provide an "
              + "implementation of at least the one of the Presence "
              + "Operation Sets");

    // lets do it once again for the second provider
    Map<String, OperationSet> supportedOperationSets2 =
        fixture.provider2.getSupportedOperationSets();

    if (supportedOperationSets2 == null || supportedOperationSets2.size() < 1)
      throw new NullPointerException(
          "No OperationSet implementations are supported by " + "this Gibberish implementation. ");

    // get the operation set presence here.
    opSetPersPresence2 =
        (OperationSetPersistentPresence)
            supportedOperationSets2.get(OperationSetPersistentPresence.class.getName());

    // if still null then the implementation doesn't offer a presence
    // operation set which is unacceptable for Gibberish.
    if (opSetPersPresence2 == null)
      throw new NullPointerException(
          "An implementation of the Gibberish service must provide an "
              + "implementation of at least the one of the Presence "
              + "Operation Sets");
  }

  @Override
  protected void tearDown() throws Exception {
    fixture.tearDown();
    super.tearDown();
  }

  /**
   * Retrieves a server stored contact list and checks whether it contains all contacts that have
   * been added there during the initialization phase by the testerAgent.
   */
  public void testRetrievingServerStoredContactList() {
    ContactGroup rootGroup = opSetPersPresence1.getServerStoredContactListRoot();

    logger.debug("=========== Server Stored Contact List =================");

    logger.debug(
        "rootGroup="
            + rootGroup.getGroupName()
            + " rootGroup.childContacts="
            + rootGroup.countContacts()
            + "rootGroup.childGroups="
            + rootGroup.countSubgroups()
            + "Printing rootGroupContents=\n"
            + rootGroup.toString());

    Hashtable<String, List<String>> expectedContactList =
        GibberishSlickFixture.preInstalledBuddyList;

    logger.debug("============== Expected Contact List ===================");
    logger.debug(expectedContactList);

    // Go through the contact list retrieved by the persistence presence set
    // and remove the name of every contact and group that we find there from
    // the expected contct list hashtable.
    Iterator<ContactGroup> groups = rootGroup.subgroups();
    while (groups.hasNext()) {
      ContactGroup group = groups.next();

      List<String> expectedContactsInGroup = expectedContactList.get(group.getGroupName());

      // When sending the offline message
      // the sever creates a group NotInContactList,
      // because the buddy we are sending message to is not in
      // the contactlist. So this group must be ignored
      if (!group.getGroupName().equals("NotInContactList")) {
        assertNotNull(
            "Group "
                + group.getGroupName()
                + " was returned by "
                + "the server but was not in the expected contact list.",
            expectedContactsInGroup);

        Iterator<Contact> contactsIter = group.contacts();
        while (contactsIter.hasNext()) {
          String contactID = contactsIter.next().getAddress();
          expectedContactsInGroup.remove(contactID);
        }

        // If we've removed all the sub contacts, remove the group too.
        if (expectedContactsInGroup.size() == 0) expectedContactList.remove(group.getGroupName());
      }
    }

    // whatever we now have in the expected contact list snapshot are groups,
    // that have been added by the testerAgent but that were not retrieved
    // by the persistent presence operation set.
    assertTrue(
        "The following contacts were on the server sidec contact "
            + "list, but were not returned by the pers. pres. op. set"
            + expectedContactList.toString(),
        expectedContactList.isEmpty());
  }

  /**
   * Creates a group in the server stored contact list, makes sure that the corresponding event has
   * been generated and verifies that the group is in the list.
   *
   * @throws java.lang.Exception
   */
  public void postTestCreateGroup() throws Exception {
    // first clear the list
    fixture.clearProvidersLists();

    Object o = new Object();
    synchronized (o) {
      o.wait(3000);
    }

    logger.trace("testing creation of server stored groups");
    // first add a listener
    GroupChangeCollector groupChangeCollector = new GroupChangeCollector();
    opSetPersPresence1.addServerStoredGroupChangeListener(groupChangeCollector);

    // create the group
    opSetPersPresence1.createServerStoredContactGroup(
        opSetPersPresence1.getServerStoredContactListRoot(), testGroupName);

    groupChangeCollector.waitForEvent(10000);

    opSetPersPresence1.removeServerStoredGroupChangeListener(groupChangeCollector);

    // check whether we got group created event
    assertEquals("Collected Group Change events: ", 1, groupChangeCollector.collectedEvents.size());

    assertEquals(
        "Group name.",
        testGroupName,
        ((ServerStoredGroupEvent) groupChangeCollector.collectedEvents.get(0))
            .getSourceGroup()
            .getGroupName());

    // check whether the group is retrievable
    ContactGroup group =
        opSetPersPresence1.getServerStoredContactListRoot().getGroup(testGroupName);

    assertNotNull("A newly created group was not in the contact list.", group);

    assertEquals("New group name", testGroupName, group.getGroupName());

    // when opearting with groups . the group must have entries
    // so changes to take effect. Otherwise group will be lost after loggingout
    try {
      opSetPersPresence1.subscribe(group, fixture.userID2);

      synchronized (o) {
        o.wait(1500);
      }
    } catch (Exception ex) {
      fail("error adding entry to group : " + group.getGroupName() + " " + ex.getMessage());
    }
  }

  /**
   * Removes the group created in the server stored contact list by the create group test, makes
   * sure that the corresponding event has been generated and verifies that the group is not in the
   * list any more.
   */
  public void postTestRemoveGroup() {
    logger.trace("testing removal of server stored groups");

    // first add a listener
    GroupChangeCollector groupChangeCollector = new GroupChangeCollector();
    opSetPersPresence1.addServerStoredGroupChangeListener(groupChangeCollector);

    try {
      // remove the group
      opSetPersPresence1.removeServerStoredContactGroup(
          opSetPersPresence1.getServerStoredContactListRoot().getGroup(testGroupName2));
    } catch (OperationFailedException ex) {
      logger.error("error removing group", ex);
    }

    groupChangeCollector.waitForEvent(10000);

    opSetPersPresence1.removeServerStoredGroupChangeListener(groupChangeCollector);

    // check whether we got group created event
    assertEquals("Collected Group Change event", 1, groupChangeCollector.collectedEvents.size());

    assertEquals(
        "Group name.",
        testGroupName2,
        ((ServerStoredGroupEvent) groupChangeCollector.collectedEvents.get(0))
            .getSourceGroup()
            .getGroupName());

    // check whether the group is still on the contact list
    ContactGroup group =
        opSetPersPresence1.getServerStoredContactListRoot().getGroup(testGroupName2);

    assertNull("A freshly removed group was still on the contact list.", group);
  }

  /**
   * Renames our test group and checks whether corresponding events are triggered. Verifies whether
   * the group has really changed its name and whether it is findable by its new name. Also makes
   * sure that it does not exist under its previous name any more.
   */
  public void postTestRenameGroup() {
    logger.trace("Testing renaming groups.");

    ContactGroup group =
        opSetPersPresence1.getServerStoredContactListRoot().getGroup(testGroupName);

    // first add a listener
    GroupChangeCollector groupChangeCollector = new GroupChangeCollector();
    opSetPersPresence1.addServerStoredGroupChangeListener(groupChangeCollector);

    // change the name and wait for a confirmation event
    opSetPersPresence1.renameServerStoredContactGroup(group, testGroupName2);

    groupChangeCollector.waitForEvent(10000);

    opSetPersPresence1.removeServerStoredGroupChangeListener(groupChangeCollector);

    // examine the event
    assertEquals("Collected Group Change event", 1, groupChangeCollector.collectedEvents.size());

    assertEquals(
        "Group name.",
        testGroupName2,
        ((ServerStoredGroupEvent) groupChangeCollector.collectedEvents.get(0))
            .getSourceGroup()
            .getGroupName());

    // check whether the group is still on the contact list
    ContactGroup oldGroup =
        opSetPersPresence1.getServerStoredContactListRoot().getGroup(testGroupName);

    assertNull("A group was still findable by its old name after renaming.", oldGroup);

    // make sure that we could find the group by its new name.
    ContactGroup newGroup =
        opSetPersPresence1.getServerStoredContactListRoot().getGroup(testGroupName2);

    assertNotNull("Could not find a renamed group by its new name.", newGroup);
  }

  /**
   * Create the contact list. Later will be test to be sure that creating is ok
   *
   * @throws Exception
   */
  public void prepareContactList() throws Exception {
    fixture.clearProvidersLists();

    Object o = new Object();
    synchronized (o) {
      o.wait(3000);
    }

    String contactList =
        System.getProperty(GibberishProtocolProviderServiceLick.CONTACT_LIST_PROPERTY_NAME, null);

    logger.debug(
        "The "
            + GibberishProtocolProviderServiceLick.CONTACT_LIST_PROPERTY_NAME
            + " property is set to="
            + contactList);

    if (contactList == null || contactList.trim().length() < 6) // at least 4 for a UIN, 1 for the
      // dot and 1 for the grp name
      throw new IllegalArgumentException(
          "The "
              + GibberishProtocolProviderServiceLick.CONTACT_LIST_PROPERTY_NAME
              + " property did not contain a contact list.");
    StringTokenizer tokenizer = new StringTokenizer(contactList, " \n\t");

    logger.debug("tokens contained by the CL tokenized=" + tokenizer.countTokens());

    Hashtable<String, List<String>> contactListToCreate = new Hashtable<String, List<String>>();

    // go over all group.uin tokens
    while (tokenizer.hasMoreTokens()) {
      String groupUinToken = tokenizer.nextToken();
      int dotIndex = groupUinToken.indexOf(".");

      if (dotIndex == -1) {
        throw new IllegalArgumentException(groupUinToken + " is not a valid Group.UIN token");
      }

      String groupName = groupUinToken.substring(0, dotIndex);
      String uin = groupUinToken.substring(dotIndex + 1);

      if (groupName.trim().length() < 1 || uin.trim().length() < 4) {
        throw new IllegalArgumentException(
            groupName + " or " + uin + " are not a valid group name or Gibberish user id.");
      }

      // check if we've already seen this group and if not - add it
      List<String> uinInThisGroup = contactListToCreate.get(groupName);
      if (uinInThisGroup == null) {
        uinInThisGroup = new ArrayList<String>();
        contactListToCreate.put(groupName, uinInThisGroup);
      }

      uinInThisGroup.add(uin);
    }

    // now init the list
    Enumeration<String> newGroupsEnum = contactListToCreate.keys();

    // go over all groups in the contactsToAdd table
    while (newGroupsEnum.hasMoreElements()) {
      String groupName = newGroupsEnum.nextElement();
      logger.debug("Will add group " + groupName);

      opSetPersPresence1.createServerStoredContactGroup(
          opSetPersPresence1.getServerStoredContactListRoot(), groupName);

      ContactGroup newlyCreatedGroup =
          opSetPersPresence1.getServerStoredContactListRoot().getGroup(groupName);

      Iterator<String> contactsToAddToThisGroup = contactListToCreate.get(groupName).iterator();
      while (contactsToAddToThisGroup.hasNext()) {
        String id = contactsToAddToThisGroup.next();

        logger.debug("Will add buddy " + id);
        opSetPersPresence1.subscribe(newlyCreatedGroup, id);
      }
    }

    // store the created contact list for later reference
    GibberishSlickFixture.preInstalledBuddyList = contactListToCreate;
  }

  /**
   * The class would listen for and store received events delivered to
   * <tt>ServerStoredGroupListener</tt>s.
   */
  private class GroupChangeCollector implements ServerStoredGroupListener {
    public ArrayList<EventObject> collectedEvents = new ArrayList<EventObject>();

    /**
     * Blocks until at least one event is received or until waitFor miliseconds pass (whicever
     * happens first).
     *
     * @param waitFor the number of miliseconds that we should be waiting for an event before simply
     *     bailing out.
     */
    public void waitForEvent(long waitFor) {
      synchronized (this) {
        if (collectedEvents.size() > 0) return;

        try {
          wait(waitFor);
        } catch (InterruptedException ex) {
          logger.debug("Interrupted while waiting for a subscription evt", ex);
        }
      }
    }

    /**
     * Called whnever an indication is received that a new server stored group is created.
     *
     * @param evt a ServerStoredGroupChangeEvent containing a reference to the newly created group.
     */
    public void groupCreated(ServerStoredGroupEvent evt) {
      synchronized (this) {
        logger.debug("Collected evt(" + collectedEvents.size() + ")= " + evt);
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Called when an indication is received that the name of a server stored contact group has
     * changed.
     *
     * @param evt a ServerStoredGroupChangeEvent containing the details of the name change.
     */
    public void groupNameChanged(ServerStoredGroupEvent evt) {
      synchronized (this) {
        logger.debug("Collected evt(" + collectedEvents.size() + ")= " + evt);
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Called whnever an indication is received that an existing server stored group has been
     * removed.
     *
     * @param evt a ServerStoredGroupChangeEvent containing a reference to the newly created group.
     */
    public void groupRemoved(ServerStoredGroupEvent evt) {
      synchronized (this) {
        logger.debug("Collected evt(" + collectedEvents.size() + ")= " + evt);
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Called whnever an indication is received that an existing server stored group has been
     * resolved.
     *
     * @param evt a ServerStoredGroupChangeEvent containing a reference to the resolved group.
     */
    public void groupResolved(ServerStoredGroupEvent evt) {
      synchronized (this) {
        logger.debug("Collected evt(" + collectedEvents.size() + ")= " + evt);
        collectedEvents.add(evt);
        notifyAll();
      }
    }
  }

  /** The class would listen for and store received subscription modification events. */
  private class SubscriptionEventCollector implements SubscriptionListener {
    public ArrayList<EventObject> collectedEvents = new ArrayList<EventObject>();

    /**
     * Blocks until at least one event is received or until waitFor milliseconds pass (whichever
     * happens first).
     *
     * @param waitFor the number of milliseconds that we should be waiting for an event before
     *     simply bailing out.
     */
    public void waitForEvent(long waitFor) {
      logger.trace("Waiting for a persistent subscription event");

      synchronized (this) {
        if (collectedEvents.size() > 0) {
          logger.trace("SubEvt already received. " + collectedEvents);
          return;
        }

        try {
          wait(waitFor);
          if (collectedEvents.size() > 0) logger.trace("Received a SubEvt in provider status.");
          else logger.trace("No SubEvt received for " + waitFor + "ms.");
        } catch (InterruptedException ex) {
          logger.debug("Interrupted while waiting for a subscription evt", ex);
        }
      }
    }

    /**
     * Stores the received subsctiption and notifies all waiting on this object
     *
     * @param evt the SubscriptionEvent containing the corresponding contact
     */
    public void subscriptionCreated(SubscriptionEvent evt) {
      synchronized (this) {
        logger.debug("Collected evt(" + collectedEvents.size() + ")= " + evt);
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Stores the received subsctiption and notifies all waiting on this object
     *
     * @param evt the SubscriptionEvent containing the corresponding contact
     */
    public void subscriptionRemoved(SubscriptionEvent evt) {
      synchronized (this) {
        logger.debug("Collected evt(" + collectedEvents.size() + ")= " + evt);
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Stores the received subsctiption and notifies all waiting on this object
     *
     * @param evt the SubscriptionEvent containing the corresponding contact
     */
    public void subscriptionFailed(SubscriptionEvent evt) {
      synchronized (this) {
        logger.debug("Collected evt(" + collectedEvents.size() + ")= " + evt);
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Stores the received subsctiption and notifies all waiting on this object
     *
     * @param evt the SubscriptionEvent containing the corresponding contact
     */
    public void subscriptionResolved(SubscriptionEvent evt) {
      synchronized (this) {
        logger.debug("Collected evt(" + collectedEvents.size() + ")= " + evt);
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Stores the received subsctiption and notifies all waiting on this object
     *
     * @param evt the SubscriptionEvent containing the corresponding contact
     */
    public void subscriptionMoved(SubscriptionMovedEvent evt) {
      synchronized (this) {
        logger.debug("Collected evt(" + collectedEvents.size() + ")= " + evt);
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Stores the received subsctiption and notifies all waiting on this object
     *
     * @param evt the SubscriptionEvent containing the corresponding contact
     */
    public void contactModified(ContactPropertyChangeEvent evt) {
      synchronized (this) {
        logger.debug("Collected evt(" + collectedEvents.size() + ")= " + evt);
        collectedEvents.add(evt);
        notifyAll();
      }
    }
  }
}
/**
 * Performs testing of the basic instant messaging operation set. Tests include going over basic
 * functionality such as sending a message from the tested implementation and asserting reception by
 * the tester agent and vice versa.
 *
 * @author Emil Ivov
 */
public class TestOperationSetBasicInstantMessaging extends TestCase {
  private static final Logger logger =
      Logger.getLogger(TestOperationSetBasicInstantMessaging.class);

  private IcqSlickFixture fixture = new IcqSlickFixture();

  private OperationSetBasicInstantMessaging opSetBasicIM = null;
  private OperationSetPresence opSetPresence = null;

  public TestOperationSetBasicInstantMessaging(String name) {
    super(name);
  }

  /**
   * Get a reference to the basic IM operation set.
   *
   * @throws Exception if this is not a good day.
   */
  protected void setUp() throws Exception {
    super.setUp();
    fixture.setUp();

    Map supportedOperationSets = fixture.provider.getSupportedOperationSets();

    if (supportedOperationSets == null || supportedOperationSets.size() < 1)
      throw new NullPointerException(
          "No OperationSet implementations are supported by " + "this ICQ implementation. ");

    // get the operation set presence here.
    opSetBasicIM =
        (OperationSetBasicInstantMessaging)
            supportedOperationSets.get(OperationSetBasicInstantMessaging.class.getName());

    // if the op set is null then the implementation doesn't offer a typing.
    // operation set which is unacceptable for icq.
    if (opSetBasicIM == null) {
      throw new NullPointerException("No implementation for basic IM was found");
    }

    // we also need the presence op set in order to retrieve contacts.
    opSetPresence =
        (OperationSetPresence) supportedOperationSets.get(OperationSetPresence.class.getName());

    // if the op set is null show that we're not happy.
    if (opSetPresence == null) {
      throw new NullPointerException(
          "An implementation of the ICQ service must provide an "
              + "implementation of at least one of the PresenceOperationSets");
    }
  }

  protected void tearDown() throws Exception {
    super.tearDown();

    fixture.tearDown();
  }

  /**
   * Creates a test suite containing tests of this class in a specific order. We'll first execute
   * tests beginning with the "test" prefix and then go to ordered tests.We first execture tests for
   * receiving messagese, so that a volatile contact is created for the sender. we'll then be able
   * to retrieve this volatile contact and send them a message on our turn. We need to do things
   * this way as the contact corresponding to the tester agent has been removed in the previous test
   * and we no longer have it in our contact list.
   *
   * @return Test a testsuite containing all tests to execute.
   */
  public static Test suite() {
    TestSuite suite = new TestSuite(TestOperationSetBasicInstantMessaging.class);

    // the following 2 need to be run in the specified order.
    suite.addTest(new TestOperationSetBasicInstantMessaging("firstTestReceiveMessage"));
    suite.addTest(new TestOperationSetBasicInstantMessaging("thenTestSendMessage"));

    return suite;
  }

  /**
   * Send an instant message from the tested operation set and assert reception by the icq tester
   * agent.
   */
  public void firstTestReceiveMessage() {
    String body = "This is an IM coming from the tester agent" + " on " + new Date().toString();

    ImEventCollector evtCollector = new ImEventCollector();

    // add a msg listener and register to the op set and send an instant
    // msg from the tester agent.
    opSetBasicIM.addMessageListener(evtCollector);

    fixture.testerAgent.sendMessage(fixture.ourUserID, body);

    evtCollector.waitForEvent(10000);

    opSetBasicIM.removeMessageListener(evtCollector);

    // assert reception of a message event
    assertTrue(
        "No events delivered upon a received message", evtCollector.collectedEvents.size() > 0);

    // assert event instance of Message Received Evt
    assertTrue(
        "Received evt was not an instance of " + MessageReceivedEvent.class.getName(),
        evtCollector.collectedEvents.get(0) instanceof MessageReceivedEvent);

    // assert source contact == testAgent.uin
    MessageReceivedEvent evt = (MessageReceivedEvent) evtCollector.collectedEvents.get(0);
    assertEquals(
        "message sender ", evt.getSourceContact().getAddress(), fixture.testerAgent.getIcqUIN());

    // assert messageBody == body
    assertEquals("message body", body, evt.getSourceMessage().getContent());
  }

  /**
   * Send an instant message from the tester agent and assert reception by the tested implementation
   */
  public void thenTestSendMessage() {
    String body =
        "This is an IM coming from the tested implementation" + " on " + new Date().toString();

    // create the message
    net.java.sip.communicator.service.protocol.Message msg = opSetBasicIM.createMessage(body);

    // register a listener in the op set
    ImEventCollector imEvtCollector = new ImEventCollector();
    opSetBasicIM.addMessageListener(imEvtCollector);

    // register a listener in the tester agent
    JoustSimMessageEventCollector jsEvtCollector = new JoustSimMessageEventCollector();
    fixture.testerAgent.addConversationListener(fixture.ourUserID, jsEvtCollector);

    Contact testerAgentContact = opSetPresence.findContactByID(fixture.testerAgent.getIcqUIN());

    opSetBasicIM.sendInstantMessage(testerAgentContact, msg);

    imEvtCollector.waitForEvent(10000);
    jsEvtCollector.waitForEvent(10000);

    fixture.testerAgent.removeConversationListener(fixture.ourUserID, jsEvtCollector);
    opSetBasicIM.removeMessageListener(imEvtCollector);

    // verify that the message delivered event was dispatched
    assertTrue(
        "No events delivered when sending a message", imEvtCollector.collectedEvents.size() > 0);

    assertTrue(
        "Received evt was not an instance of " + MessageDeliveredEvent.class.getName(),
        imEvtCollector.collectedEvents.get(0) instanceof MessageDeliveredEvent);

    MessageDeliveredEvent evt = (MessageDeliveredEvent) imEvtCollector.collectedEvents.get(0);
    assertEquals(
        "message destination ",
        evt.getDestinationContact().getAddress(),
        fixture.testerAgent.getIcqUIN());

    assertSame("source message", msg, evt.getSourceMessage());

    // verify that the message has successfully arived at the destination
    assertTrue(
        "No messages received by the tester agent", jsEvtCollector.collectedMessageInfo.size() > 0);
    String receivedBody =
        ((MessageInfo) jsEvtCollector.collectedMessageInfo.get(0)).getMessage().getMessageBody();

    assertEquals("received message body", msg.getContent(), receivedBody);
  }

  /** Creates an Message through the simple createMessage() method and inspects its parameters. */
  public void testCreateMessage1() {
    String body =
        "This is an IM coming from the tested implementation" + " on " + new Date().toString();
    net.java.sip.communicator.service.protocol.Message msg = opSetBasicIM.createMessage(body);

    assertEquals("message body", body, msg.getContent());
    assertTrue("message body bytes", Arrays.equals(body.getBytes(), msg.getRawData()));
    assertEquals("message length", body.length(), msg.getSize());
    assertEquals(
        "message content type",
        OperationSetBasicInstantMessaging.DEFAULT_MIME_TYPE,
        msg.getContentType());

    assertEquals(
        "message encoding",
        OperationSetBasicInstantMessaging.DEFAULT_MIME_ENCODING,
        msg.getEncoding());

    assertNotNull("message uid", msg.getMessageUID());

    // a further test on message uid.
    net.java.sip.communicator.service.protocol.Message msg2 = opSetBasicIM.createMessage(body);
    assertFalse("message uid", msg.getMessageUID().equals(msg2.getMessageUID()));
  }

  /** Creates an Message through the advance createMessage() method and inspects its parameters. */
  public void testCreateMessage2() {
    String body =
        "This is an IM coming from the tested implementation" + " on " + new Date().toString();
    String contentType = "text/html";
    String encoding = "UTF-16";
    String subject = "test message";
    net.java.sip.communicator.service.protocol.Message msg =
        opSetBasicIM.createMessage(body.getBytes(), contentType, encoding, subject);

    assertEquals("message body", body, msg.getContent());
    assertTrue("message body bytes", Arrays.equals(body.getBytes(), msg.getRawData()));
    assertEquals("message length", body.length(), msg.getSize());
    assertEquals("message content type", contentType, msg.getContentType());
    assertEquals("message encoding", encoding, msg.getEncoding());
    assertNotNull("message uid", msg.getMessageUID());

    // a further test on message uid.
    net.java.sip.communicator.service.protocol.Message msg2 = opSetBasicIM.createMessage(body);
    assertFalse("message uid", msg.getMessageUID().equals(msg2.getMessageUID()));
  }

  /** Collects instant messaging events. */
  private class ImEventCollector implements MessageListener {
    private List collectedEvents = new LinkedList();
    /**
     * Called when a new incoming <tt>Message</tt> has been received.
     *
     * @param evt the <tt>MessageReceivedEvent</tt> containing the newly received message, its
     *     sender and other details.
     */
    public void messageReceived(MessageReceivedEvent evt) {
      logger.debug("Received a MessageReceivedEvent: " + evt);

      synchronized (this) {
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Called to indicated that delivery of a message sent earlier has failed. Reason code and
     * phrase are contained by the <tt>MessageFailedEvent</tt>
     *
     * @param evt the <tt>MessageFailedEvent</tt> containing the ID of the message whose delivery
     *     has failed.
     */
    public void messageDeliveryFailed(MessageDeliveryFailedEvent evt) {
      logger.debug("Received a MessageDeliveryFailedEvent: " + evt);

      synchronized (this) {
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Called when the underlying implementation has received an indication that a message, sent
     * earlier has been successfully received by the destination.
     *
     * @param evt the MessageDeliveredEvent containing the id of the message that has caused the
     *     event.
     */
    public void messageDelivered(MessageDeliveredEvent evt) {
      logger.debug("Received a MessageDeliveredEvent: " + evt);

      synchronized (this) {
        collectedEvents.add(evt);
        notifyAll();
      }
    }

    /**
     * Blocks until at least one event is received or until waitFor miliseconds pass (whichever
     * happens first).
     *
     * @param waitFor the number of miliseconds that we should be waiting for an event before simply
     *     bailing out.
     */
    public void waitForEvent(long waitFor) {
      synchronized (this) {
        if (collectedEvents.size() > 0) return;

        try {
          wait(waitFor);
        } catch (InterruptedException ex) {
          logger.debug("Interrupted while waiting for a message evt", ex);
        }
      }
    }
  }

  /** The oscar.jar lib sends us typing events through this listener. */
  private class JoustSimMessageEventCollector implements ConversationListener {
    private List collectedMessageInfo = new LinkedList();

    /**
     * Adds <tt>minfo</tt> into the list of collected messages.
     *
     * @param c Conversation
     * @param minfo MessageInfo
     */
    public void gotMessage(Conversation c, MessageInfo minfo) {

      logger.debug("Message: [" + minfo.getMessage() + "] received from: " + c.getBuddy());
      synchronized (this) {
        collectedMessageInfo.add(minfo);
        notifyAll();
      }
    }

    /**
     * Blocks until at least one event is received or until waitFor miliseconds pass (whicever
     * happens first).
     *
     * @param waitFor the number of miliseconds that we should be waiting for an event before simply
     *     bailing out.
     */
    public void waitForEvent(long waitFor) {
      synchronized (this) {
        if (collectedMessageInfo.size() > 0) {
          logger.trace("evt already received. " + collectedMessageInfo);
          return;
        }

        try {
          wait(waitFor);
        } catch (InterruptedException ex) {
          logger.debug("Interrupted while waiting for a subscription evt", ex);
        }
      }
    }

    // the follwoing methods only have dummy implementations here as they
    // do not interest us. complete implementatios are provider in the
    // basic instant messaging operation set.
    public void buddyInfoUpdated(IcbmService service, Screenname buddy, IcbmBuddyInfo info) {}

    public void conversationClosed(Conversation c) {}

    public void gotOtherEvent(Conversation conversation, ConversationEventInfo event) {}

    public void sentOtherEvent(Conversation conversation, ConversationEventInfo event) {}

    public void canSendMessageChanged(Conversation c, boolean canSend) {}

    public void conversationOpened(Conversation c) {}

    public void newConversation(IcbmService service, Conversation conv) {}

    public void sentMessage(Conversation c, MessageInfo minfo) {}
  }

  /**
   * A method that would simply send messages to a group of people so that they would get notified
   * that tests are being run.
   */
  public void testSendFunMessages() {
    String hostname = "";

    try {
      hostname = java.net.InetAddress.getLocalHost().getHostName() + ": ";
    } catch (UnknownHostException ex) {
    }

    String message =
        hostname
            + "Hello this is the SIP Communicator (version "
            + System.getProperty("sip-communicator.version")
            + ") build on: "
            + new Date().toString()
            + ". Have a very nice day!";

    String list = System.getProperty("accounts.reporting.ICQ_REPORT_LIST");

    logger.debug("Will send message " + message + " to: " + list);

    // if no property is specified - return
    if (list == null || list.trim().length() == 0) return;

    StringTokenizer tokenizer = new StringTokenizer(list, " ");

    while (tokenizer.hasMoreTokens()) {
      fixture.testerAgent.sendMessage(tokenizer.nextToken(), message);
    }
  }

  /** Tests whether there is a offline message received and whether is the one we have send */
  public void testReceiveOfflineMessages() {
    String messageText = fixture.offlineMsgCollector.getMessageText();

    Message receiveMessage = fixture.offlineMsgCollector.getReceivedMessage();

    assertNotNull("No Offline messages have been received", receiveMessage);
    assertEquals("message body", messageText, receiveMessage.getContent());
  }
}