/** * 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); } } }
/** * 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); } } } } }
/** * 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()); } }
/** * 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); } } } } }
/** * 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); } } } } }
/** * 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); } }
/** * 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); } } }
/** * 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); } } } }
/** * 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: <body> */ private static final String OPEN_BODY_TAG = "<body>"; /** The closing BODY HTML TAG: <body> */ 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 ' is not rendered correctly // from our ui, lets use its equivalent. Other // similar chars(< > & ") seem ok. receivedMessage = receivedMessage.replaceAll("'", "'"); 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++); } }
/** * 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()); } }