/** * The ProtocolProviderFactory is what actually creates instances of a ProtocolProviderService * implementation. A provider factory would register, persistently store, and remove when necessary, * ProtocolProviders. The way things are in the SIP Communicator, a user account is represented (in * a 1:1 relationship) by an AccountID and a ProtocolProvider. In other words - one would have as * many protocol providers installed in a given moment as they would user account registered through * the various services. * * @author Emil Ivov * @author Lubomir Marinov */ public abstract class ProtocolProviderFactory { /** * The <tt>Logger</tt> used by the <tt>ProtocolProviderFactory</tt> class and its instances for * logging output. */ private static final Logger logger = Logger.getLogger(ProtocolProviderFactory.class); /** Then name of a property which represents a password. */ public static final String PASSWORD = "******"; /** * The name of a property representing the name of the protocol for an ProtocolProviderFactory. */ public static final String PROTOCOL = "PROTOCOL_NAME"; /** The name of a property representing the path to protocol icons. */ public static final String PROTOCOL_ICON_PATH = "PROTOCOL_ICON_PATH"; /** * The name of a property representing the path to the account icon to be used in the user * interface, when the protocol provider service is not available. */ public static final String ACCOUNT_ICON_PATH = "ACCOUNT_ICON_PATH"; /** * The name of a property which represents the AccountID of a ProtocolProvider and that, together * with a password is used to login on the protocol network.. */ public static final String USER_ID = "USER_ID"; /** The name that should be displayed to others when we are calling or writing them. */ public static final String DISPLAY_NAME = "DISPLAY_NAME"; /** The name that should be displayed to the user on call via and chat via lists. */ public static final String ACCOUNT_DISPLAY_NAME = "ACCOUNT_DISPLAY_NAME"; /** The name of the property under which we store protocol AccountID-s. */ public static final String ACCOUNT_UID = "ACCOUNT_UID"; /** * The name of the property under which we store protocol the address of a protocol centric entity * (any protocol server). */ public static final String SERVER_ADDRESS = "SERVER_ADDRESS"; /** * The name of the property under which we store the number of the port where the server stored * against the SERVER_ADDRESS property is expecting connections to be made via this protocol. */ public static final String SERVER_PORT = "SERVER_PORT"; /** * The name of the property under which we store the name of the transport protocol that needs to * be used to access the server. */ public static final String SERVER_TRANSPORT = "SERVER_TRANSPORT"; /** The name of the property under which we store protocol the address of a protocol proxy. */ public static final String PROXY_ADDRESS = "PROXY_ADDRESS"; /** * The name of the property under which we store the number of the port where the proxy stored * against the PROXY_ADDRESS property is expecting connections to be made via this protocol. */ public static final String PROXY_PORT = "PROXY_PORT"; /** * The name of the property which defines whether proxy is auto configured by the protocol by * using known methods such as specific DNS queries. */ public static final String PROXY_AUTO_CONFIG = "PROXY_AUTO_CONFIG"; /** The property indicating the preferred UDP and TCP port to bind to for clear communications. */ public 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. */ public static final String PREFERRED_SECURE_PORT_PROPERTY_NAME = "net.java.sip.communicator.SIP_PREFERRED_SECURE_PORT"; /** * The name of the property under which we store the the type of the proxy stored against the * PROXY_ADDRESS property. Exact type values depend on protocols and among them are socks4, * socks5, http and possibly others. */ public static final String PROXY_TYPE = "PROXY_TYPE"; /** * The name of the property under which we store the the username for the proxy stored against the * PROXY_ADDRESS property. */ public static final String PROXY_USERNAME = "******"; /** * The name of the property under which we store the the authorization name for the proxy stored * against the PROXY_ADDRESS property. */ public static final String AUTHORIZATION_NAME = "AUTHORIZATION_NAME"; /** * The name of the property under which we store the password for the proxy stored against the * PROXY_ADDRESS property. */ public static final String PROXY_PASSWORD = "******"; /** * The name of the property under which we store the name of the transport protocol that needs to * be used to access the proxy. */ public static final String PROXY_TRANSPORT = "PROXY_TRANSPORT"; /** * The name of the property that indicates whether loose routing should be forced for all traffic * in an account, rather than routing through an outbound proxy which is the default for Jitsi. */ public static final String FORCE_PROXY_BYPASS = "******"; /** * The name of the property under which we store the user preference for a transport protocol to * use (i.e. tcp or udp). */ public static final String PREFERRED_TRANSPORT = "PREFERRED_TRANSPORT"; /** * The name of the property under which we store whether we generate resource values or we just * use the stored one. */ public static final String AUTO_GENERATE_RESOURCE = "AUTO_GENERATE_RESOURCE"; /** * The name of the property under which we store resources such as the jabber resource property. */ public static final String RESOURCE = "RESOURCE"; /** The name of the property under which we store resource priority. */ public static final String RESOURCE_PRIORITY = "RESOURCE_PRIORITY"; /** The name of the property which defines that the call is encrypted by default */ public static final String DEFAULT_ENCRYPTION = "DEFAULT_ENCRYPTION"; /** The name of the property that indicates the encryption protocols for this account. */ public static final String ENCRYPTION_PROTOCOL = "ENCRYPTION_PROTOCOL"; /** * The name of the property that indicates the status (enabed or disabled) encryption protocols * for this account. */ public static final String ENCRYPTION_PROTOCOL_STATUS = "ENCRYPTION_PROTOCOL_STATUS"; /** The name of the property which defines if to include the ZRTP attribute to SIP/SDP */ public static final String DEFAULT_SIPZRTP_ATTRIBUTE = "DEFAULT_SIPZRTP_ATTRIBUTE"; /** * The name of the property which defines the ID of the client TLS certificate configuration * entry. */ public static final String CLIENT_TLS_CERTIFICATE = "CLIENT_TLS_CERTIFICATE"; /** * The name of the property under which we store the boolean value indicating if the user name * should be automatically changed if the specified name already exists. This property is meant to * be used by IRC implementations. */ public static final String AUTO_CHANGE_USER_NAME = "AUTO_CHANGE_USER_NAME"; /** * The name of the property under which we store the boolean value indicating if a password is * required. Initially this property is meant to be used by IRC implementations. */ public static final String NO_PASSWORD_REQUIRED = "NO_PASSWORD_REQUIRED"; /** The name of the property under which we store if the presence is enabled. */ public static final String IS_PRESENCE_ENABLED = "IS_PRESENCE_ENABLED"; /** The name of the property under which we store if the p2p mode for SIMPLE should be forced. */ public static final String FORCE_P2P_MODE = "FORCE_P2P_MODE"; /** * The name of the property under which we store the offline contact polling period for SIMPLE. */ public static final String POLLING_PERIOD = "POLLING_PERIOD"; /** * The name of the property under which we store the chosen default subscription expiration value * for SIMPLE. */ public static final String SUBSCRIPTION_EXPIRATION = "SUBSCRIPTION_EXPIRATION"; /** Indicates if the server address has been validated. */ public static final String SERVER_ADDRESS_VALIDATED = "SERVER_ADDRESS_VALIDATED"; /** Indicates if the server settings are over */ public static final String IS_SERVER_OVERRIDDEN = "IS_SERVER_OVERRIDDEN"; /** Indicates if the proxy address has been validated. */ public static final String PROXY_ADDRESS_VALIDATED = "PROXY_ADDRESS_VALIDATED"; /** Indicates the search strategy chosen for the DICT protocole. */ public static final String STRATEGY = "STRATEGY"; /** Indicates a protocol that would not be shown in the user interface as an account. */ public static final String IS_PROTOCOL_HIDDEN = "IS_PROTOCOL_HIDDEN"; /** Indicates if the given account is the preferred account. */ public static final String IS_PREFERRED_PROTOCOL = "IS_PREFERRED_PROTOCOL"; /** * The name of the property that would indicate if a given account is currently enabled or * disabled. */ public static final String IS_ACCOUNT_DISABLED = "IS_ACCOUNT_DISABLED"; /** Indicates if ICE should be used. */ public static final String IS_USE_ICE = "ICE_ENABLED"; /** Indicates if Google ICE should be used. */ public static final String IS_USE_GOOGLE_ICE = "GTALK_ICE_ENABLED"; /** Indicates if STUN server should be automatically discovered. */ public static final String AUTO_DISCOVER_STUN = "AUTO_DISCOVER_STUN"; /** Indicates if default STUN server would be used if no other STUN/TURN server are available. */ public static final String USE_DEFAULT_STUN_SERVER = "USE_DEFAULT_STUN_SERVER"; /** * The name of the boolean account property which indicates whether Jitsi VideoBridge is to be * used, if available and supported, for conference calls. */ public static final String USE_JITSI_VIDEO_BRIDGE = "USE_JITSI_VIDEO_BRIDGE"; /** * The property name prefix for all stun server properties. We generally use this prefix in * conjunction with an index which is how we store multiple servers. */ public static final String STUN_PREFIX = "STUN"; /** The base property name for address of additional STUN servers specified. */ public static final String STUN_ADDRESS = "ADDRESS"; /** The base property name for port of additional STUN servers specified. */ public static final String STUN_PORT = "PORT"; /** The base property name for username of additional STUN servers specified. */ public static final String STUN_USERNAME = "******"; /** The base property name for password of additional STUN servers specified. */ public static final String STUN_PASSWORD = "******"; /** * The base property name for the turn supported property of additional STUN servers specified. */ public static final String STUN_IS_TURN_SUPPORTED = "IS_TURN_SUPPORTED"; /** Indicates if JingleNodes should be used with ICE. */ public static final String IS_USE_JINGLE_NODES = "JINGLE_NODES_ENABLED"; /** Indicates if JingleNodes should be used with ICE. */ public static final String AUTO_DISCOVER_JINGLE_NODES = "AUTO_DISCOVER_JINGLE_NODES"; /** Indicates if JingleNodes should use buddies to search for nodes. */ public static final String JINGLE_NODES_SEARCH_BUDDIES = "JINGLE_NODES_SEARCH_BUDDIES"; /** Indicates if UPnP should be used with ICE. */ public static final String IS_USE_UPNP = "UPNP_ENABLED"; /** Indicates if we allow non-TLS connection. */ public static final String IS_ALLOW_NON_SECURE = "ALLOW_NON_SECURE"; /** Enable notifications for new voicemail messages. */ public static final String VOICEMAIL_ENABLED = "VOICEMAIL_ENABLED"; /** * Address used to reach voicemail box, by services able to subscribe for voicemail new messages * notifications. */ public static final String VOICEMAIL_URI = "VOICEMAIL_URI"; /** Address used to call to hear your messages stored on the server for your voicemail. */ public static final String VOICEMAIL_CHECK_URI = "VOICEMAIL_CHECK_URI"; /** Indicates if calling is disabled for a certain account. */ public static final String IS_CALLING_DISABLED_FOR_ACCOUNT = "CALLING_DISABLED"; /** Indicates if desktop streaming/sharing is disabled for a certain account. */ public static final String IS_DESKTOP_STREAMING_DISABLED = "DESKTOP_STREAMING_DISABLED"; /** The sms default server address. */ public static final String SMS_SERVER_ADDRESS = "SMS_SERVER_ADDRESS"; /** Keep-alive method used by the protocol. */ public static final String KEEP_ALIVE_METHOD = "KEEP_ALIVE_METHOD"; /** The interval for keep-alives if any. */ public static final String KEEP_ALIVE_INTERVAL = "KEEP_ALIVE_INTERVAL"; /** The minimal DTMF tone duration. */ public static final String DTMF_MINIMAL_TONE_DURATION = "DTMF_MINIMAL_TONE_DURATION"; /** Paranoia mode when turned on requires all calls to be secure and indicated as such. */ public static final String MODE_PARANOIA = "MODE_PARANOIA"; /** The name of the "override encodings" property */ public static final String OVERRIDE_ENCODINGS = "OVERRIDE_ENCODINGS"; /** The prefix used to store account encoding properties */ public static final String ENCODING_PROP_PREFIX = "Encodings"; /** * The <code>BundleContext</code> containing (or to contain) the service registration of this * factory. */ private final BundleContext bundleContext; /** * The name of the protocol this factory registers its <code>ProtocolProviderService</code>s with * and to be placed in the properties of the accounts created by this factory. */ private final String protocolName; /** * The table that we store our accounts in. * * <p>TODO Synchronize the access to the field which may in turn be better achieved by also hiding * it from protected into private access. */ protected final Hashtable<AccountID, ServiceRegistration> registeredAccounts = new Hashtable<AccountID, ServiceRegistration>(); /** * The name of the property that indicates the AVP type. * * <ul> * <li>{@link #SAVP_OFF} * <li>{@link #SAVP_MANDATORY} * <li>{@link #SAVP_OPTIONAL} * </ul> */ public static final String SAVP_OPTION = "SAVP_OPTION"; /** Always use RTP/AVP */ public static final int SAVP_OFF = 0; /** Always use RTP/SAVP */ public static final int SAVP_MANDATORY = 1; /** Sends two media description, with RTP/SAVP being first. */ public static final int SAVP_OPTIONAL = 2; /** * The name of the property that defines the enabled SDES cipher suites. Enabled suites are listed * as CSV by their RFC name. */ public static final String SDES_CIPHER_SUITES = "SDES_CIPHER_SUITES"; /** * Creates a new <tt>ProtocolProviderFactory</tt>. * * @param bundleContext the bundle context reference of the service * @param protocolName the name of the protocol */ protected ProtocolProviderFactory(BundleContext bundleContext, String protocolName) { this.bundleContext = bundleContext; this.protocolName = protocolName; } /** * Gets the <code>BundleContext</code> containing (or to contain) the service registration of this * factory. * * @return the <code>BundleContext</code> containing (or to contain) the service registration of * this factory */ public BundleContext getBundleContext() { return bundleContext; } /** * Initializes and creates an account corresponding to the specified accountProperties and * registers the resulting ProtocolProvider in the <tt>context</tt> BundleContext parameter. Note * that account registration is persistent and accounts that are registered during a particular * sip-communicator session would be automatically reloaded during all following sessions until * they are removed through the removeAccount method. * * @param userID the user identifier uniquely representing the newly created account within the * protocol namespace. * @param accountProperties a set of protocol (or implementation) specific properties defining the * new account. * @return the AccountID of the newly created account. * @throws java.lang.IllegalArgumentException if userID does not correspond to an identifier in * the context of the underlying protocol or if accountProperties does not contain a complete * set of account installation properties. * @throws java.lang.IllegalStateException if the account has already been installed. * @throws java.lang.NullPointerException if any of the arguments is null. */ public abstract AccountID installAccount(String userID, Map<String, String> accountProperties) throws IllegalArgumentException, IllegalStateException, NullPointerException; /** * Modifies the account corresponding to the specified accountID. This method is meant to be used * to change properties of already existing accounts. Note that if the given accountID doesn't * correspond to any registered account this method would do nothing. * * @param protocolProvider the protocol provider service corresponding to the modified account. * @param accountProperties a set of protocol (or implementation) specific properties defining the * new account. * @throws java.lang.NullPointerException if any of the arguments is null. */ public abstract void modifyAccount( ProtocolProviderService protocolProvider, Map<String, String> accountProperties) throws NullPointerException; /** * Returns a copy of the list containing the <tt>AccountID</tt>s of all accounts currently * registered in this protocol provider. * * @return a copy of the list containing the <tt>AccountID</tt>s of all accounts currently * registered in this protocol provider. */ public ArrayList<AccountID> getRegisteredAccounts() { synchronized (registeredAccounts) { return new ArrayList<AccountID>(registeredAccounts.keySet()); } } /** * Returns the ServiceReference for the protocol provider corresponding to the specified accountID * or null if the accountID is unknown. * * @param accountID the accountID of the protocol provider we'd like to get * @return a ServiceReference object to the protocol provider with the specified account id and * null if the account id is unknown to the provider factory. */ public ServiceReference getProviderForAccount(AccountID accountID) { ServiceRegistration registration; synchronized (registeredAccounts) { registration = registeredAccounts.get(accountID); } return (registration == null) ? null : registration.getReference(); } /** * Removes the specified account from the list of accounts that this provider factory is handling. * If the specified accountID is unknown to the ProtocolProviderFactory, the call has no effect * and false is returned. This method is persistent in nature and once called the account * corresponding to the specified ID will not be loaded during future runs of the project. * * @param accountID the ID of the account to remove. * @return true if an account with the specified ID existed and was removed and false otherwise. */ public boolean uninstallAccount(AccountID accountID) { // Unregister the protocol provider. ServiceReference serRef = getProviderForAccount(accountID); boolean wasAccountExisting = false; // If the protocol provider service is registered, first unregister the // service. if (serRef != null) { BundleContext bundleContext = getBundleContext(); ProtocolProviderService protocolProvider = (ProtocolProviderService) bundleContext.getService(serRef); try { protocolProvider.unregister(); } catch (OperationFailedException ex) { logger.error( "Failed to unregister protocol provider for account: " + accountID + " caused by: " + ex); } } ServiceRegistration registration; synchronized (registeredAccounts) { registration = registeredAccounts.remove(accountID); } // first remove the stored account so when PP is unregistered we can // distinguish between deleted or just disabled account wasAccountExisting = removeStoredAccount(accountID); if (registration != null) { // Kill the service. registration.unregister(); } return wasAccountExisting; } /** * The method stores the specified account in the configuration service under the package name of * the source factory. The restore and remove account methods are to be used to obtain access to * and control the stored accounts. * * <p>In order to store all account properties, the method would create an entry in the * configuration service corresponding (beginning with) the <tt>sourceFactory</tt>'s package name * and add to it a unique identifier (e.g. the current miliseconds.) * * @param accountID the AccountID corresponding to the account that we would like to store. */ protected void storeAccount(AccountID accountID) { this.storeAccount(accountID, true); } /** * The method stores the specified account in the configuration service under the package name of * the source factory. The restore and remove account methods are to be used to obtain access to * and control the stored accounts. * * <p>In order to store all account properties, the method would create an entry in the * configuration service corresponding (beginning with) the <tt>sourceFactory</tt>'s package name * and add to it a unique identifier (e.g. the current miliseconds.) * * @param accountID the AccountID corresponding to the account that we would like to store. * @param isModification if <tt>false</tt> there must be no such already loaded account, it * <tt>true</tt> ist modification of an existing account. Usually we use this method with * <tt>false</tt> in method installAccount and with <tt>true</tt> or the overridden method in * method modifyAccount. */ protected void storeAccount(AccountID accountID, boolean isModification) { if (!isModification && getAccountManager().getStoredAccounts().contains(accountID)) { throw new IllegalStateException( "An account for id " + accountID.getUserID() + " was already loaded!"); } try { getAccountManager().storeAccount(this, accountID); } catch (OperationFailedException ofex) { throw new UndeclaredThrowableException(ofex); } } /** * Saves the password for the specified account after scrambling it a bit so that it is not * visible from first sight. (The method remains highly insecure). * * @param accountID the AccountID for the account whose password we're storing * @param password the password itself * @throws IllegalArgumentException if no account corresponding to <code>accountID</code> has been * previously stored */ public void storePassword(AccountID accountID, String password) throws IllegalArgumentException { try { storePassword(getBundleContext(), accountID, password); } catch (OperationFailedException ofex) { throw new UndeclaredThrowableException(ofex); } } /** * Saves the password for the specified account after scrambling it a bit so that it is not * visible from first sight (Method remains highly insecure). * * <p>TODO Delegate the implementation to {@link AccountManager} because it knows the format in * which the password (among the other account properties) is to be saved. * * @param bundleContext a currently valid bundle context. * @param accountID the <tt>AccountID</tt> of the account whose password is to be stored * @param password the password to be stored * @throws IllegalArgumentException if no account corresponding to <tt>accountID</tt> has been * previously stored. * @throws OperationFailedException if anything goes wrong while storing the specified * <tt>password</tt> */ protected void storePassword(BundleContext bundleContext, AccountID accountID, String password) throws IllegalArgumentException, OperationFailedException { String accountPrefix = findAccountPrefix(bundleContext, accountID, getFactoryImplPackageName()); if (accountPrefix == null) { throw new IllegalArgumentException( "No previous records found for account ID: " + accountID.getAccountUniqueID() + " in package" + getFactoryImplPackageName()); } CredentialsStorageService credentialsStorage = ServiceUtils.getService(bundleContext, CredentialsStorageService.class); if (!credentialsStorage.storePassword(accountPrefix, password)) { throw new OperationFailedException( "CredentialsStorageService failed to storePassword", OperationFailedException.GENERAL_ERROR); } } /** * Returns the password last saved for the specified account. * * @param accountID the AccountID for the account whose password we're looking for * @return a String containing the password for the specified accountID */ public String loadPassword(AccountID accountID) { return loadPassword(getBundleContext(), accountID); } /** * Returns the password last saved for the specified account. * * <p>TODO Delegate the implementation to {@link AccountManager} because it knows the format in * which the password (among the other account properties) was saved. * * @param bundleContext a currently valid bundle context. * @param accountID the AccountID for the account whose password we're looking for.. * @return a String containing the password for the specified accountID. */ protected String loadPassword(BundleContext bundleContext, AccountID accountID) { String accountPrefix = findAccountPrefix(bundleContext, accountID, getFactoryImplPackageName()); if (accountPrefix == null) return null; CredentialsStorageService credentialsStorage = ServiceUtils.getService(bundleContext, CredentialsStorageService.class); return credentialsStorage.loadPassword(accountPrefix); } /** * Initializes and creates an account corresponding to the specified accountProperties and * registers the resulting ProtocolProvider in the <tt>context</tt> BundleContext parameter. This * method has a persistent effect. Once created the resulting account will remain installed until * removed through the uninstallAccount method. * * @param accountProperties a set of protocol (or implementation) specific properties defining the * new account. * @return the AccountID of the newly loaded account */ public AccountID loadAccount(Map<String, String> accountProperties) { AccountID accountID = createAccount(accountProperties); loadAccount(accountID); return accountID; } /** * Creates a protocol provider for the given <tt>accountID</tt> and registers it in the bundle * context. This method has a persistent effect. Once created the resulting account will remain * installed until removed through the uninstallAccount method. * * @param accountID the account identifier * @return <tt>true</tt> if the account with the given <tt>accountID</tt> is successfully loaded, * otherwise returns <tt>false</tt> */ public boolean loadAccount(AccountID accountID) { // Need to obtain the original user id property, instead of calling // accountID.getUserID(), because this method could return a modified // version of the user id property. String userID = accountID.getAccountPropertyString(ProtocolProviderFactory.USER_ID); ProtocolProviderService service = createService(userID, accountID); Dictionary<String, String> properties = new Hashtable<String, String>(); properties.put(PROTOCOL, protocolName); properties.put(USER_ID, userID); ServiceRegistration serviceRegistration = bundleContext.registerService(ProtocolProviderService.class.getName(), service, properties); if (serviceRegistration == null) return false; else { synchronized (registeredAccounts) { registeredAccounts.put(accountID, serviceRegistration); } return true; } } /** * Unloads the account corresponding to the given <tt>accountID</tt>. Unregisters the * corresponding protocol provider, but keeps the account in contrast to the uninstallAccount * method. * * @param accountID the account identifier * @return true if an account with the specified ID existed and was unloaded and false otherwise. */ public boolean unloadAccount(AccountID accountID) { // Unregister the protocol provider. ServiceReference serRef = getProviderForAccount(accountID); if (serRef == null) { return false; } BundleContext bundleContext = getBundleContext(); ProtocolProviderService protocolProvider = (ProtocolProviderService) bundleContext.getService(serRef); try { protocolProvider.unregister(); } catch (OperationFailedException ex) { logger.error( "Failed to unregister protocol provider for account : " + accountID + " caused by: " + ex); } ServiceRegistration registration; synchronized (registeredAccounts) { registration = registeredAccounts.remove(accountID); } if (registration == null) { return false; } // Kill the service. registration.unregister(); return true; } /** * Initializes and creates an account corresponding to the specified accountProperties. * * @param accountProperties a set of protocol (or implementation) specific properties defining the * new account. * @return the AccountID of the newly created account */ public AccountID createAccount(Map<String, String> accountProperties) { BundleContext bundleContext = getBundleContext(); if (bundleContext == null) throw new NullPointerException("The specified BundleContext was null"); if (accountProperties == null) throw new NullPointerException("The specified property map was null"); String userID = accountProperties.get(USER_ID); if (userID == null) throw new NullPointerException("The account properties contained no user id."); String protocolName = getProtocolName(); if (!accountProperties.containsKey(PROTOCOL)) accountProperties.put(PROTOCOL, protocolName); return createAccountID(userID, accountProperties); } /** * Creates a new <code>AccountID</code> instance with a specific user ID to represent a given set * of account properties. * * <p>The method is a pure factory allowing implementers to specify the runtime type of the * created <code>AccountID</code> and customize the instance. The returned <code>AccountID</code> * will later be associated with a <code>ProtocolProviderService</code> by the caller (e.g. using * {@link #createService(String, AccountID)}). * * @param userID the user ID of the new instance * @param accountProperties the set of properties to be represented by the new instance * @return a new <code>AccountID</code> instance with the specified user ID representing the given * set of account properties */ protected abstract AccountID createAccountID( String userID, Map<String, String> accountProperties); /** * Gets the name of the protocol this factory registers its <code>ProtocolProviderService</code>s * with and to be placed in the properties of the accounts created by this factory. * * @return the name of the protocol this factory registers its <code>ProtocolProviderService * </code>s with and to be placed in the properties of the accounts created by this factory */ public String getProtocolName() { return protocolName; } /** * Initializes a new <code>ProtocolProviderService</code> instance with a specific user ID to * represent a specific <code>AccountID</code>. * * <p>The method is a pure factory allowing implementers to specify the runtime type of the * created <code>ProtocolProviderService</code> and customize the instance. The caller will later * register the returned service with the <code>BundleContext</code> of this factory. * * @param userID the user ID to initialize the new instance with * @param accountID the <code>AccountID</code> to be represented by the new instance * @return a new <code>ProtocolProviderService</code> instance with the specific user ID * representing the specified <code>AccountID</code> */ protected abstract ProtocolProviderService createService(String userID, AccountID accountID); /** * Removes the account with <tt>accountID</tt> from the set of accounts that are persistently * stored inside the configuration service. * * @param accountID the AccountID of the account to remove. * @return true if an account has been removed and false otherwise. */ protected boolean removeStoredAccount(AccountID accountID) { return getAccountManager().removeStoredAccount(this, accountID); } /** * Returns the prefix for all persistently stored properties of the account with the specified id. * * @param bundleContext a currently valid bundle context. * @param accountID the AccountID of the account whose properties we're looking for. * @param sourcePackageName a String containing the package name of the concrete factory class * that extends us. * @return a String indicating the ConfigurationService property name prefix under which all * account properties are stored or null if no account corresponding to the specified id was * found. */ public static String findAccountPrefix( BundleContext bundleContext, AccountID accountID, String sourcePackageName) { ServiceReference confReference = bundleContext.getServiceReference(ConfigurationService.class.getName()); ConfigurationService configurationService = (ConfigurationService) bundleContext.getService(confReference); // first retrieve all accounts that we've registered List<String> storedAccounts = configurationService.getPropertyNamesByPrefix(sourcePackageName, true); // find an account with the corresponding id. for (String accountRootPropertyName : storedAccounts) { // unregister the account in the configuration service. // all the properties must have been registered in the following // hierarchy: // net.java.sip.communicator.impl.protocol.PROTO_NAME.ACC_ID.PROP_NAME String accountUID = configurationService.getString( accountRootPropertyName // node id + "." + ACCOUNT_UID); // propname if (accountID.getAccountUniqueID().equals(accountUID)) { return accountRootPropertyName; } } return null; } /** * Returns the name of the package that we're currently running in (i.e. the name of the package * containing the proto factory that extends us). * * @return a String containing the package name of the concrete factory class that extends us. */ private String getFactoryImplPackageName() { String className = getClass().getName(); return className.substring(0, className.lastIndexOf('.')); } /** Prepares the factory for bundle shutdown. */ public void stop() { if (logger.isTraceEnabled()) logger.trace("Preparing to stop all protocol providers of" + this); synchronized (registeredAccounts) { for (Enumeration<ServiceRegistration> registrations = registeredAccounts.elements(); registrations.hasMoreElements(); ) { ServiceRegistration reg = registrations.nextElement(); stop(reg); reg.unregister(); } registeredAccounts.clear(); } } /** * Shuts down the <code>ProtocolProviderService</code> representing an account registered with * this factory. * * @param registeredAccount the <code>ServiceRegistration</code> of the <code> * ProtocolProviderService</code> representing an account registered with this factory */ protected void stop(ServiceRegistration registeredAccount) { ProtocolProviderService protocolProviderService = (ProtocolProviderService) getBundleContext().getService(registeredAccount.getReference()); protocolProviderService.shutdown(); } /** * Get the <tt>AccountManager</tt> of the protocol. * * @return <tt>AccountManager</tt> of the protocol */ private AccountManager getAccountManager() { BundleContext bundleContext = getBundleContext(); ServiceReference serviceReference = bundleContext.getServiceReference(AccountManager.class.getName()); return (AccountManager) bundleContext.getService(serviceReference); } }
/** * Class is a transport layer for WebRTC data channels. It consists of SCTP connection running on * top of ICE/DTLS layer. Manages WebRTC data channels. See * http://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-08 for more info on WebRTC data * channels. * * <p>Control protocol: http://tools.ietf.org/html/draft-ietf-rtcweb-data-protocol-03 FIXME handle * closing of data channels(SCTP stream reset) * * @author Pawel Domas * @author Lyubomir Marinov * @author Boris Grozev */ public class SctpConnection extends Channel implements SctpDataCallback, SctpSocket.NotificationListener { /** Generator used to track debug IDs. */ private static int debugIdGen = -1; /** DTLS transport buffer size. Note: randomly chosen. */ private static final int DTLS_BUFFER_SIZE = 2048; /** Switch used for debugging SCTP traffic purposes. FIXME to be removed */ private static final boolean LOG_SCTP_PACKETS = false; /** The logger */ private static final Logger logger = Logger.getLogger(SctpConnection.class); /** * Message type used to acknowledge WebRTC data channel allocation on SCTP stream ID on which * <tt>MSG_OPEN_CHANNEL</tt> message arrives. */ private static final int MSG_CHANNEL_ACK = 0x2; private static final byte[] MSG_CHANNEL_ACK_BYTES = new byte[] {MSG_CHANNEL_ACK}; /** * Message with this type sent over control PPID in order to open new WebRTC data channel on SCTP * stream ID that this message is sent. */ private static final int MSG_OPEN_CHANNEL = 0x3; /** SCTP transport buffer size. */ private static final int SCTP_BUFFER_SIZE = DTLS_BUFFER_SIZE - 13; /** The pool of <tt>Thread</tt>s which run <tt>SctpConnection</tt>s. */ private static final ExecutorService threadPool = ExecutorUtils.newCachedThreadPool(true, SctpConnection.class.getName()); /** Payload protocol id that identifies binary data in WebRTC data channel. */ static final int WEB_RTC_PPID_BIN = 53; /** Payload protocol id for control data. Used for <tt>WebRtcDataStream</tt> allocation. */ static final int WEB_RTC_PPID_CTRL = 50; /** Payload protocol id that identifies text data UTF8 encoded in WebRTC data channels. */ static final int WEB_RTC_PPID_STRING = 51; /** * The <tt>String</tt> value of the <tt>Protocol</tt> field of the <tt>DATA_CHANNEL_OPEN</tt> * message. */ private static final String WEBRTC_DATA_CHANNEL_PROTOCOL = "http://jitsi.org/protocols/colibri"; private static synchronized int generateDebugId() { debugIdGen += 2; return debugIdGen; } /** * Indicates whether the STCP association is ready and has not been ended by a subsequent state * change. */ private boolean assocIsUp; /** Indicates if we have accepted incoming connection. */ private boolean acceptedIncomingConnection; /** Data channels mapped by SCTP stream identified(sid). */ private final Map<Integer, WebRtcDataStream> channels = new HashMap<Integer, WebRtcDataStream>(); /** Debug ID used to distinguish SCTP sockets in packet logs. */ private final int debugId; /** * The <tt>AsyncExecutor</tt> which is to asynchronously dispatch the events fired by this * instance in order to prevent possible listeners from blocking this <tt>SctpConnection</tt> in * general and {@link #sctpSocket} in particular for too long. The timeout of <tt>15</tt> is * chosen to be in accord with the time it takes to expire a <tt>Channel</tt>. */ private final AsyncExecutor<Runnable> eventDispatcher = new AsyncExecutor<Runnable>(15, TimeUnit.MILLISECONDS); /** Datagram socket for ICE/UDP layer. */ private IceSocketWrapper iceSocket; /** * List of <tt>WebRtcDataStreamListener</tt>s that will be notified whenever new WebRTC data * channel is opened. */ private final List<WebRtcDataStreamListener> listeners = new ArrayList<WebRtcDataStreamListener>(); /** Remote SCTP port. */ private final int remoteSctpPort; /** <tt>SctpSocket</tt> used for SCTP transport. */ private SctpSocket sctpSocket; /** * Flag prevents from starting this connection multiple times from {@link #maybeStartStream()}. */ private boolean started; /** * Initializes a new <tt>SctpConnection</tt> instance. * * @param id the string identifier of this connection instance * @param content the <tt>Content</tt> which is initializing the new instance * @param endpoint the <tt>Endpoint</tt> of newly created instance * @param remoteSctpPort the SCTP port used by remote peer * @param channelBundleId the ID of the channel-bundle this <tt>SctpConnection</tt> is to be a * part of (or <tt>null</tt> if no it is not to be a part of a channel-bundle). * @throws Exception if an error occurs while initializing the new instance */ public SctpConnection( String id, Content content, Endpoint endpoint, int remoteSctpPort, String channelBundleId) throws Exception { super(content, id, channelBundleId); setEndpoint(endpoint.getID()); this.remoteSctpPort = remoteSctpPort; this.debugId = generateDebugId(); } /** * Adds <tt>WebRtcDataStreamListener</tt> to the list of listeners. * * @param listener the <tt>WebRtcDataStreamListener</tt> to be added to the listeners list. */ public void addChannelListener(WebRtcDataStreamListener listener) { if (listener == null) { throw new NullPointerException("listener"); } else { synchronized (listeners) { if (!listeners.contains(listener)) { listeners.add(listener); } } } } /** {@inheritDoc} */ @Override protected void closeStream() throws IOException { try { synchronized (this) { assocIsUp = false; acceptedIncomingConnection = false; if (sctpSocket != null) { sctpSocket.close(); sctpSocket = null; } } } finally { if (iceSocket != null) { // It is now the responsibility of the transport manager to // close the socket. // iceUdpSocket.close(); } } } /** {@inheritDoc} */ @Override public void expire() { try { eventDispatcher.shutdown(); } finally { super.expire(); } } /** * Gets the <tt>WebRtcDataStreamListener</tt>s added to this instance. * * @return the <tt>WebRtcDataStreamListener</tt>s added to this instance or <tt>null</tt> if there * are no <tt>WebRtcDataStreamListener</tt>s added to this instance */ private WebRtcDataStreamListener[] getChannelListeners() { WebRtcDataStreamListener[] ls; synchronized (listeners) { if (listeners.isEmpty()) { ls = null; } else { ls = listeners.toArray(new WebRtcDataStreamListener[listeners.size()]); } } return ls; } /** * Returns default <tt>WebRtcDataStream</tt> if it's ready or <tt>null</tt> otherwise. * * @return <tt>WebRtcDataStream</tt> if it's ready or <tt>null</tt> otherwise. * @throws IOException */ public WebRtcDataStream getDefaultDataStream() throws IOException { WebRtcDataStream def; synchronized (this) { if (sctpSocket == null) { def = null; } else { // Channel that runs on sid 0 def = channels.get(0); if (def == null) { def = openChannel(0, 0, 0, 0, "default"); } // Pawel Domas: Must be acknowledged before use /* * XXX Lyubomir Marinov: We're always sending ordered. According * to "WebRTC Data Channel Establishment Protocol", we can start * sending messages containing user data after the * DATA_CHANNEL_OPEN message has been sent without waiting for * the reception of the corresponding DATA_CHANNEL_ACK message. */ // if (!def.isAcknowledged()) // def = null; } } return def; } /** * Returns <tt>true</tt> if this <tt>SctpConnection</tt> is connected to the remote peer and * operational. * * @return <tt>true</tt> if this <tt>SctpConnection</tt> is connected to the remote peer and * operational */ public boolean isReady() { return assocIsUp && acceptedIncomingConnection; } /** {@inheritDoc} */ @Override protected void maybeStartStream() throws IOException { // connector final StreamConnector connector = getStreamConnector(); if (connector == null) return; synchronized (this) { if (started) return; threadPool.execute( new Runnable() { @Override public void run() { try { Sctp.init(); runOnDtlsTransport(connector); } catch (IOException e) { logger.error(e, e); } finally { try { Sctp.finish(); } catch (IOException e) { logger.error("Failed to shutdown SCTP stack", e); } } } }); started = true; } } /** * Submits {@link #notifyChannelOpenedInEventDispatcher(WebRtcDataStream)} to {@link * #eventDispatcher} for asynchronous execution. * * @param dataChannel */ private void notifyChannelOpened(final WebRtcDataStream dataChannel) { if (!isExpired()) { eventDispatcher.execute( new Runnable() { @Override public void run() { notifyChannelOpenedInEventDispatcher(dataChannel); } }); } } private void notifyChannelOpenedInEventDispatcher(WebRtcDataStream dataChannel) { /* * When executing asynchronously in eventDispatcher, it is technically * possible that this SctpConnection may have expired by now. */ if (!isExpired()) { WebRtcDataStreamListener[] ls = getChannelListeners(); if (ls != null) { for (WebRtcDataStreamListener l : ls) { l.onChannelOpened(this, dataChannel); } } } } /** * Submits {@link #notifySctpConnectionReadyInEventDispatcher()} to {@link #eventDispatcher} for * asynchronous execution. */ private void notifySctpConnectionReady() { if (!isExpired()) { eventDispatcher.execute( new Runnable() { @Override public void run() { notifySctpConnectionReadyInEventDispatcher(); } }); } } /** * Notifies the <tt>WebRtcDataStreamListener</tt>s added to this instance that this * <tt>SctpConnection</tt> is ready i.e. it is connected to the remote peer and operational. */ private void notifySctpConnectionReadyInEventDispatcher() { /* * When executing asynchronously in eventDispatcher, it is technically * possible that this SctpConnection may have expired by now. */ if (!isExpired() && isReady()) { WebRtcDataStreamListener[] ls = getChannelListeners(); if (ls != null) { for (WebRtcDataStreamListener l : ls) { l.onSctpConnectionReady(this); } } } } /** * Handles control packet. * * @param data raw packet data that arrived on control PPID. * @param sid SCTP stream id on which the data has arrived. */ private synchronized void onCtrlPacket(byte[] data, int sid) throws IOException { ByteBuffer buffer = ByteBuffer.wrap(data); int messageType = /* 1 byte unsigned integer */ 0xFF & buffer.get(); if (messageType == MSG_CHANNEL_ACK) { if (logger.isDebugEnabled()) { logger.debug(getEndpoint().getID() + " ACK received SID: " + sid); } // Open channel ACK WebRtcDataStream channel = channels.get(sid); if (channel != null) { // Ack check prevents from firing multiple notifications // if we get more than one ACKs (by mistake/bug). if (!channel.isAcknowledged()) { channel.ackReceived(); notifyChannelOpened(channel); } else { logger.warn("Redundant ACK received for SID: " + sid); } } else { logger.error("No channel exists on sid: " + sid); } } else if (messageType == MSG_OPEN_CHANNEL) { int channelType = /* 1 byte unsigned integer */ 0xFF & buffer.get(); int priority = /* 2 bytes unsigned integer */ 0xFFFF & buffer.getShort(); long reliability = /* 4 bytes unsigned integer */ 0xFFFFFFFFL & buffer.getInt(); int labelLength = /* 2 bytes unsigned integer */ 0xFFFF & buffer.getShort(); int protocolLength = /* 2 bytes unsigned integer */ 0xFFFF & buffer.getShort(); String label; String protocol; if (labelLength == 0) { label = ""; } else { byte[] labelBytes = new byte[labelLength]; buffer.get(labelBytes); label = new String(labelBytes, "UTF-8"); } if (protocolLength == 0) { protocol = ""; } else { byte[] protocolBytes = new byte[protocolLength]; buffer.get(protocolBytes); protocol = new String(protocolBytes, "UTF-8"); } if (logger.isDebugEnabled()) { logger.debug( "!!! " + getEndpoint().getID() + " data channel open request on SID: " + sid + " type: " + channelType + " prio: " + priority + " reliab: " + reliability + " label: " + label + " proto: " + protocol); } if (channels.containsKey(sid)) { logger.error("Channel on sid: " + sid + " already exists"); } WebRtcDataStream newChannel = new WebRtcDataStream(sctpSocket, sid, label, true); channels.put(sid, newChannel); sendOpenChannelAck(sid); notifyChannelOpened(newChannel); } else { logger.error("Unexpected ctrl msg type: " + messageType); } } /** {@inheritDoc} */ @Override protected void onEndpointChanged(Endpoint oldValue, Endpoint newValue) { if (oldValue != null) oldValue.setSctpConnection(null); if (newValue != null) newValue.setSctpConnection(this); } /** Implements notification in order to track socket state. */ @Override public synchronized void onSctpNotification(SctpSocket socket, SctpNotification notification) { if (logger.isDebugEnabled()) { logger.debug("socket=" + socket + "; notification=" + notification); } switch (notification.sn_type) { case SctpNotification.SCTP_ASSOC_CHANGE: SctpNotification.AssociationChange assocChange = (SctpNotification.AssociationChange) notification; switch (assocChange.state) { case SctpNotification.AssociationChange.SCTP_COMM_UP: if (!assocIsUp) { boolean wasReady = isReady(); assocIsUp = true; if (isReady() && !wasReady) notifySctpConnectionReady(); } break; case SctpNotification.AssociationChange.SCTP_COMM_LOST: case SctpNotification.AssociationChange.SCTP_SHUTDOWN_COMP: case SctpNotification.AssociationChange.SCTP_CANT_STR_ASSOC: try { closeStream(); } catch (IOException e) { logger.error("Error closing SCTP socket", e); } break; } break; } } /** * {@inheritDoc} * * <p>SCTP input data callback. */ @Override public void onSctpPacket( byte[] data, int sid, int ssn, int tsn, long ppid, int context, int flags) { if (ppid == WEB_RTC_PPID_CTRL) { // Channel control PPID try { onCtrlPacket(data, sid); } catch (IOException e) { logger.error("IOException when processing ctrl packet", e); } } else if (ppid == WEB_RTC_PPID_STRING || ppid == WEB_RTC_PPID_BIN) { WebRtcDataStream channel; synchronized (this) { channel = channels.get(sid); } if (channel == null) { logger.error("No channel found for sid: " + sid); return; } if (ppid == WEB_RTC_PPID_STRING) { // WebRTC String String str; String charsetName = "UTF-8"; try { str = new String(data, charsetName); } catch (UnsupportedEncodingException uee) { logger.error("Unsupported charset encoding/name " + charsetName, uee); str = null; } channel.onStringMsg(str); } else { // WebRTC Binary channel.onBinaryMsg(data); } } else { logger.warn("Got message on unsupported PPID: " + ppid); } } /** * Opens new WebRTC data channel using specified parameters. * * @param type channel type as defined in control protocol description. Use 0 for "reliable". * @param prio channel priority. The higher the number, the lower the priority. * @param reliab Reliability Parameter<br> * This field is ignored if a reliable channel is used. If a partial reliable channel with * limited number of retransmissions is used, this field specifies the number of * retransmissions. If a partial reliable channel with limited lifetime is used, this field * specifies the maximum lifetime in milliseconds. The following table summarizes this:<br> * </br> * <p>+------------------------------------------------+------------------+ | Channel Type | * Reliability | | | Parameter | * +------------------------------------------------+------------------+ | * DATA_CHANNEL_RELIABLE | Ignored | | DATA_CHANNEL_RELIABLE_UNORDERED | Ignored | | * DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT | Number of RTX | | * DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT_UNORDERED | Number of RTX | | * DATA_CHANNEL_PARTIAL_RELIABLE_TIMED | Lifetime in ms | | * DATA_CHANNEL_PARTIAL_RELIABLE_TIMED_UNORDERED | Lifetime in ms | * +------------------------------------------------+------------------+ * @param sid SCTP stream id that will be used by new channel (it must not be already used). * @param label text label for the channel. * @return new instance of <tt>WebRtcDataStream</tt> that represents opened WebRTC data channel. * @throws IOException if IO error occurs. */ public synchronized WebRtcDataStream openChannel( int type, int prio, long reliab, int sid, String label) throws IOException { if (channels.containsKey(sid)) { throw new IOException("Channel on sid: " + sid + " already exists"); } // Label Length & Label byte[] labelBytes; int labelByteLength; if (label == null) { labelBytes = null; labelByteLength = 0; } else { labelBytes = label.getBytes("UTF-8"); labelByteLength = labelBytes.length; if (labelByteLength > 0xFFFF) labelByteLength = 0xFFFF; } // Protocol Length & Protocol String protocol = WEBRTC_DATA_CHANNEL_PROTOCOL; byte[] protocolBytes; int protocolByteLength; if (protocol == null) { protocolBytes = null; protocolByteLength = 0; } else { protocolBytes = protocol.getBytes("UTF-8"); protocolByteLength = protocolBytes.length; if (protocolByteLength > 0xFFFF) protocolByteLength = 0xFFFF; } ByteBuffer packet = ByteBuffer.allocate(12 + labelByteLength + protocolByteLength); // Message open new channel on current sid // Message Type packet.put((byte) MSG_OPEN_CHANNEL); // Channel Type packet.put((byte) type); // Priority packet.putShort((short) prio); // Reliability Parameter packet.putInt((int) reliab); // Label Length packet.putShort((short) labelByteLength); // Protocol Length packet.putShort((short) protocolByteLength); // Label if (labelByteLength != 0) { packet.put(labelBytes, 0, labelByteLength); } // Protocol if (protocolByteLength != 0) { packet.put(protocolBytes, 0, protocolByteLength); } int sentCount = sctpSocket.send(packet.array(), true, sid, WEB_RTC_PPID_CTRL); if (sentCount != packet.capacity()) { throw new IOException("Failed to open new chanel on sid: " + sid); } WebRtcDataStream channel = new WebRtcDataStream(sctpSocket, sid, label, false); channels.put(sid, channel); return channel; } /** * Removes <tt>WebRtcDataStreamListener</tt> from the list of listeners. * * @param listener the <tt>WebRtcDataStreamListener</tt> to be removed from the listeners list. */ public void removeChannelListener(WebRtcDataStreamListener listener) { if (listener != null) { synchronized (listeners) { listeners.remove(listener); } } } private void runOnDtlsTransport(StreamConnector connector) throws IOException { DtlsControlImpl dtlsControl = (DtlsControlImpl) getTransportManager().getDtlsControl(this); DtlsTransformEngine engine = dtlsControl.getTransformEngine(); final DtlsPacketTransformer transformer = (DtlsPacketTransformer) engine.getRTPTransformer(); byte[] receiveBuffer = new byte[SCTP_BUFFER_SIZE]; if (LOG_SCTP_PACKETS) { System.setProperty( ConfigurationService.PNAME_SC_HOME_DIR_LOCATION, System.getProperty("java.io.tmpdir")); System.setProperty( ConfigurationService.PNAME_SC_HOME_DIR_NAME, SctpConnection.class.getName()); } synchronized (this) { // FIXME local SCTP port is hardcoded in bridge offer SDP (Jitsi // Meet) sctpSocket = Sctp.createSocket(5000); assocIsUp = false; acceptedIncomingConnection = false; } // Implement output network link for SCTP stack on DTLS transport sctpSocket.setLink( new NetworkLink() { @Override public void onConnOut(SctpSocket s, byte[] packet) throws IOException { if (LOG_SCTP_PACKETS) { LibJitsi.getPacketLoggingService() .logPacket( PacketLoggingService.ProtocolName.ICE4J, new byte[] {0, 0, 0, (byte) debugId}, 5000, new byte[] {0, 0, 0, (byte) (debugId + 1)}, remoteSctpPort, PacketLoggingService.TransportName.UDP, true, packet); } // Send through DTLS transport transformer.sendApplicationData(packet, 0, packet.length); } }); if (logger.isDebugEnabled()) { logger.debug("Connecting SCTP to port: " + remoteSctpPort + " to " + getEndpoint().getID()); } sctpSocket.setNotificationListener(this); sctpSocket.listen(); // FIXME manage threads threadPool.execute( new Runnable() { @Override public void run() { SctpSocket sctpSocket = null; try { // sctpSocket is set to null on close sctpSocket = SctpConnection.this.sctpSocket; while (sctpSocket != null) { if (sctpSocket.accept()) { acceptedIncomingConnection = true; break; } Thread.sleep(100); sctpSocket = SctpConnection.this.sctpSocket; } if (isReady()) { notifySctpConnectionReady(); } } catch (Exception e) { logger.error("Error accepting SCTP connection", e); } if (sctpSocket == null && logger.isInfoEnabled()) { logger.info( "SctpConnection " + getID() + " closed" + " before SctpSocket accept()-ed."); } } }); // Notify that from now on SCTP connection is considered functional sctpSocket.setDataCallback(this); // Setup iceSocket DatagramSocket datagramSocket = connector.getDataSocket(); if (datagramSocket != null) { this.iceSocket = new IceUdpSocketWrapper(datagramSocket); } else { this.iceSocket = new IceTcpSocketWrapper(connector.getDataTCPSocket()); } DatagramPacket rcvPacket = new DatagramPacket(receiveBuffer, 0, receiveBuffer.length); // Receive loop, breaks when SCTP socket is closed try { do { iceSocket.receive(rcvPacket); RawPacket raw = new RawPacket(rcvPacket.getData(), rcvPacket.getOffset(), rcvPacket.getLength()); raw = transformer.reverseTransform(raw); // Check for app data if (raw == null) continue; if (LOG_SCTP_PACKETS) { LibJitsi.getPacketLoggingService() .logPacket( PacketLoggingService.ProtocolName.ICE4J, new byte[] {0, 0, 0, (byte) (debugId + 1)}, remoteSctpPort, new byte[] {0, 0, 0, (byte) debugId}, 5000, PacketLoggingService.TransportName.UDP, false, raw.getBuffer(), raw.getOffset(), raw.getLength()); } // Pass network packet to SCTP stack sctpSocket.onConnIn(raw.getBuffer(), raw.getOffset(), raw.getLength()); } while (true); } finally { // Eventually, close the socket although it should happen from // expire(). synchronized (this) { assocIsUp = false; acceptedIncomingConnection = false; if (sctpSocket != null) { sctpSocket.close(); sctpSocket = null; } } } } /** * Sends acknowledgment for open channel request on given SCTP stream ID. * * @param sid SCTP stream identifier to be used for sending ack. */ private void sendOpenChannelAck(int sid) throws IOException { // Send ACK byte[] ack = MSG_CHANNEL_ACK_BYTES; int sendAck = sctpSocket.send(ack, true, sid, WEB_RTC_PPID_CTRL); if (sendAck != ack.length) { logger.error("Failed to send open channel confirmation"); } } /** * {@inheritDoc} * * <p>Creates a <tt>TransportManager</tt> instance suitable for an <tt>SctpConnection</tt> (e.g. * with 1 component only). */ protected TransportManager createTransportManager(String xmlNamespace) throws IOException { if (IceUdpTransportPacketExtension.NAMESPACE.equals(xmlNamespace)) { Content content = getContent(); return new IceUdpTransportManager( content.getConference(), isInitiator(), 1 /* num components */, content.getName()); } else if (RawUdpTransportPacketExtension.NAMESPACE.equals(xmlNamespace)) { // TODO: support RawUdp once RawUdpTransportManager is updated // return new RawUdpTransportManager(this); throw new IllegalArgumentException("Unsupported Jingle transport " + xmlNamespace); } else { throw new IllegalArgumentException("Unsupported Jingle transport " + xmlNamespace); } } }
public class ShowPreviewDialog extends SIPCommDialog implements ActionListener, ChatLinkClickedListener { /** Serial version UID. */ private static final long serialVersionUID = 1L; /** * The <tt>Logger</tt> used by the <tt>ShowPreviewDialog</tt> class and its instances for logging * output. */ private static final Logger logger = Logger.getLogger(ShowPreviewDialog.class); ConfigurationService cfg = GuiActivator.getConfigurationService(); /** The Ok button. */ private final JButton okButton; /** The cancel button. */ private final JButton cancelButton; /** Checkbox that indicates whether or not to show this dialog next time. */ private final JCheckBox enableReplacementProposal; /** Checkbox that indicates whether or not to show previews automatically */ private final JCheckBox enableReplacement; /** The <tt>ChatConversationPanel</tt> that this dialog is associated with. */ private final ChatConversationPanel chatPanel; /** Mapping between messageID and the string representation of the chat message. */ private Map<String, String> msgIDToChatString = new ConcurrentHashMap<String, String>(); /** * Mapping between the pair (messageID, link position) and the actual link in the string * representation of the chat message. */ private Map<String, String> msgIDandPositionToLink = new ConcurrentHashMap<String, String>(); /** * Mapping between link and replacement for this link that is acquired from it's corresponding * <tt>ReplacementService</tt>. */ private Map<String, String> linkToReplacement = new ConcurrentHashMap<String, String>(); /** The id of the message that is currently associated with this dialog. */ private String currentMessageID = ""; /** The position of the link in the current message. */ private String currentLinkPosition = ""; /** * Creates an instance of <tt>ShowPreviewDialog</tt> * * @param chatPanel The <tt>ChatConversationPanel</tt> that is associated with this dialog. */ ShowPreviewDialog(final ChatConversationPanel chatPanel) { this.chatPanel = chatPanel; this.setTitle( GuiActivator.getResources().getI18NString("service.gui.SHOW_PREVIEW_DIALOG_TITLE")); okButton = new JButton(GuiActivator.getResources().getI18NString("service.gui.OK")); cancelButton = new JButton(GuiActivator.getResources().getI18NString("service.gui.CANCEL")); JPanel mainPanel = new TransparentPanel(); mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); // mainPanel.setPreferredSize(new Dimension(200, 150)); this.getContentPane().add(mainPanel); JTextPane descriptionMsg = new JTextPane(); descriptionMsg.setEditable(false); descriptionMsg.setOpaque(false); descriptionMsg.setText( GuiActivator.getResources().getI18NString("service.gui.SHOW_PREVIEW_WARNING_DESCRIPTION")); Icon warningIcon = null; try { warningIcon = new ImageIcon( ImageIO.read( GuiActivator.getResources().getImageURL("service.gui.icons.WARNING_ICON"))); } catch (IOException e) { logger.debug("failed to load the warning icon"); } JLabel warningSign = new JLabel(warningIcon); JPanel warningPanel = new TransparentPanel(); warningPanel.setLayout(new BoxLayout(warningPanel, BoxLayout.X_AXIS)); warningPanel.add(warningSign); warningPanel.add(Box.createHorizontalStrut(10)); warningPanel.add(descriptionMsg); enableReplacement = new JCheckBox( GuiActivator.getResources() .getI18NString("plugin.chatconfig.replacement.ENABLE_REPLACEMENT_STATUS")); enableReplacement.setOpaque(false); enableReplacement.setSelected(cfg.getBoolean(ReplacementProperty.REPLACEMENT_ENABLE, true)); enableReplacementProposal = new JCheckBox( GuiActivator.getResources() .getI18NString("plugin.chatconfig.replacement.ENABLE_REPLACEMENT_PROPOSAL")); enableReplacementProposal.setOpaque(false); JPanel checkBoxPanel = new TransparentPanel(); checkBoxPanel.setLayout(new BoxLayout(checkBoxPanel, BoxLayout.Y_AXIS)); checkBoxPanel.add(enableReplacement); checkBoxPanel.add(enableReplacementProposal); JPanel buttonsPanel = new TransparentPanel(new FlowLayout(FlowLayout.CENTER)); buttonsPanel.add(okButton); buttonsPanel.add(cancelButton); mainPanel.add(warningPanel); mainPanel.add(Box.createVerticalStrut(10)); mainPanel.add(checkBoxPanel); mainPanel.add(buttonsPanel); okButton.addActionListener(this); cancelButton.addActionListener(this); this.setPreferredSize(new Dimension(390, 230)); } @Override public void actionPerformed(ActionEvent arg0) { if (arg0.getSource().equals(okButton)) { cfg.setProperty(ReplacementProperty.REPLACEMENT_ENABLE, enableReplacement.isSelected()); cfg.setProperty( ReplacementProperty.REPLACEMENT_PROPOSAL, enableReplacementProposal.isSelected()); SwingWorker worker = new SwingWorker() { /** * Called on the event dispatching thread (not on the worker thread) after the <code> * construct</code> method has returned. */ @Override public void finished() { String newChatString = (String) get(); if (newChatString != null) { try { Element elem = chatPanel.document.getElement(currentMessageID); chatPanel.document.setOuterHTML(elem, newChatString); msgIDToChatString.put(currentMessageID, newChatString); } catch (BadLocationException ex) { logger.error("Could not replace chat message", ex); } catch (IOException ex) { logger.error("Could not replace chat message", ex); } } } @Override protected Object construct() throws Exception { String newChatString = msgIDToChatString.get(currentMessageID); try { String originalLink = msgIDandPositionToLink.get(currentMessageID + "#" + currentLinkPosition); String replacementLink = linkToReplacement.get(originalLink); String replacement; DirectImageReplacementService source = GuiActivator.getDirectImageReplacementSource(); if (originalLink.equals(replacementLink) && (!source.isDirectImage(originalLink) || source.getImageSize(originalLink) == -1)) { replacement = originalLink; } else { replacement = "<IMG HEIGHT=\"90\" WIDTH=\"120\" SRC=\"" + replacementLink + "\" BORDER=\"0\" ALT=\"" + originalLink + "\"></IMG>"; } String old = originalLink + "</A> <A href=\"jitsi://" + ShowPreviewDialog.this.getClass().getName() + "/SHOWPREVIEW?" + currentMessageID + "#" + currentLinkPosition + "\">" + GuiActivator.getResources().getI18NString("service.gui.SHOW_PREVIEW"); newChatString = newChatString.replace(old, replacement); } catch (Exception ex) { logger.error("Could not replace chat message", ex); } return newChatString; } }; worker.start(); this.setVisible(false); } else if (arg0.getSource().equals(cancelButton)) { this.setVisible(false); } } @Override public void chatLinkClicked(URI url) { String action = url.getPath(); if (action.equals("/SHOWPREVIEW")) { enableReplacement.setSelected(cfg.getBoolean(ReplacementProperty.REPLACEMENT_ENABLE, true)); enableReplacementProposal.setSelected( cfg.getBoolean(ReplacementProperty.REPLACEMENT_PROPOSAL, true)); currentMessageID = url.getQuery(); currentLinkPosition = url.getFragment(); this.setVisible(true); this.setLocationRelativeTo(chatPanel); } } /** * Returns mapping between messageID and the string representation of the chat message. * * @return mapping between messageID and chat string. */ Map<String, String> getMsgIDToChatString() { return msgIDToChatString; } /** * Returns mapping between the pair (messageID, link position) and the actual link in the string * representation of the chat message. * * @return mapping between (messageID, linkPosition) and link. */ Map<String, String> getMsgIDandPositionToLink() { return msgIDandPositionToLink; } /** * Returns mapping between link and replacement for this link that was acquired from it's * corresponding <tt>ReplacementService</tt>. * * @return mapping between link and it's corresponding replacement. */ Map<String, String> getLinkToReplacement() { return linkToReplacement; } }
/** * Activator for the swing notification service. * * @author Symphorien Wanko */ public class SwingNotificationActivator implements BundleActivator { /** The bundle context in which we started */ public static BundleContext bundleContext; /** A reference to the configuration service. */ private static ConfigurationService configService; /** Logger for this class. */ private static final Logger logger = Logger.getLogger(SwingNotificationActivator.class); /** A reference to the resource management service. */ private static ResourceManagementService resourcesService; /** * Start the swing notification service * * @param bc * @throws java.lang.Exception */ public void start(BundleContext bc) throws Exception { if (logger.isInfoEnabled()) logger.info("Swing Notification ...[ STARTING ]"); bundleContext = bc; PopupMessageHandler handler = null; handler = new PopupMessageHandlerSwingImpl(); getConfigurationService(); bc.registerService(PopupMessageHandler.class.getName(), handler, null); if (logger.isInfoEnabled()) logger.info("Swing Notification ...[REGISTERED]"); } public void stop(BundleContext arg0) throws Exception {} /** * Returns the <tt>ConfigurationService</tt> obtained from the bundle context. * * @return the <tt>ConfigurationService</tt> obtained from the bundle context */ public static ConfigurationService getConfigurationService() { if (configService == null) { ServiceReference configReference = bundleContext.getServiceReference(ConfigurationService.class.getName()); configService = (ConfigurationService) bundleContext.getService(configReference); } return configService; } /** * Returns the <tt>ResourceManagementService</tt> obtained from the bundle context. * * @return the <tt>ResourceManagementService</tt> obtained from the bundle context */ public static ResourceManagementService getResources() { if (resourcesService == null) resourcesService = ResourceManagementServiceUtils.getService(bundleContext); return resourcesService; } }
/** * Implements the {@link ReplacementService} to provide previews for direct image links. * * @author Purvesh Sahoo * @author Marin Dzhigarov */ public class ReplacementServiceDirectImageImpl implements DirectImageReplacementService { /** The logger for this class. */ private static final Logger logger = Logger.getLogger(ReplacementServiceDirectImageImpl.class); /** The regex used to match the link in the message. */ public static final String URL_PATTERN = "https?\\:\\/\\/(www\\.)*.*\\.(?:jpg|png|gif)"; /** Configuration label shown in the config form. */ public static final String DIRECT_IMAGE_CONFIG_LABEL = "Direct Image Link"; /** Source name; also used as property label. */ public static final String SOURCE_NAME = "DIRECTIMAGE"; /** Maximum allowed size of the image in bytes. The default size is 2MB. */ private long imgMaxSize = 2097152; /** Configuration property name for maximum allowed size of the image in bytes. */ private static final String MAX_IMG_SIZE = "net.java.sip.communicator.impl.replacement.directimage.MAX_IMG_SIZE"; /** Constructor for <tt>ReplacementServiceDirectImageImpl</tt>. */ public ReplacementServiceDirectImageImpl() { setMaxImgSizeFromConf(); logger.trace("Creating a Direct Image Link Source."); } /** * Gets the max allowed size value in bytes from Configuration service and sets the value to * <tt>imgMaxSize</tt> if succeed. If the configuration property isn't available or the value * can't be parsed correctly the value of <tt>imgMaxSize</tt> isn't changed. */ private void setMaxImgSizeFromConf() { ConfigurationService configService = DirectImageActivator.getConfigService(); if (configService != null) { String confImgSizeStr = (String) configService.getProperty(MAX_IMG_SIZE); try { if (confImgSizeStr != null) { imgMaxSize = Long.parseLong(confImgSizeStr); } else { configService.setProperty(MAX_IMG_SIZE, imgMaxSize); } } catch (NumberFormatException e) { if (logger.isDebugEnabled()) logger.debug( "Failed to parse max image size: " + confImgSizeStr + ". Going for default."); } } } /** * Returns the thumbnail URL of the image link provided. * * @param sourceString the original image link. * @return the thumbnail image link; the original link in case of no match. */ public String getReplacement(String sourceString) { return sourceString; } /** * Returns the source name * * @return the source name */ public String getSourceName() { return SOURCE_NAME; } /** * Returns the pattern of the source * * @return the source pattern */ public String getPattern() { return URL_PATTERN; } @Override /** * Returns the size of the image in bytes. * * @param sourceString the image link. * @return the file size in bytes of the image link provided; -1 if the size isn't available or * exceeds the max allowed image size. */ public int getImageSize(String sourceString) { int length = -1; try { URL url = new URL(sourceString); String protocol = url.getProtocol(); if (protocol.equals("http") || protocol.equals("https")) { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); length = connection.getContentLength(); connection.disconnect(); } else if (protocol.equals("ftp")) { FTPUtils ftp = new FTPUtils(sourceString); length = ftp.getSize(); ftp.disconnect(); } if (length > imgMaxSize) { length = -1; } } catch (Exception e) { logger.debug("Failed to get the length of the image in bytes", e); } return length; } /** * Returns true if the content type of the resource pointed by sourceString is an image. * * @param sourceString the original image link. * @return true if the content type of the resource pointed by sourceString is an image. */ @Override public boolean isDirectImage(String sourceString) { boolean isDirectImage = false; try { URL url = new URL(sourceString); String protocol = url.getProtocol(); if (protocol.equals("http") || protocol.equals("https")) { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); isDirectImage = connection.getContentType().contains("image"); connection.disconnect(); } else if (protocol.equals("ftp")) { if (sourceString.endsWith(".png") || sourceString.endsWith(".jpg") || sourceString.endsWith(".gif")) { isDirectImage = true; } } } catch (Exception e) { logger.debug("Failed to retrieve content type information for" + sourceString, e); } return isDirectImage; } }
/** * Implements {@link PacketTransformer} for DTLS-SRTP. It's capable of working in pure DTLS mode if * appropriate flag was set in <tt>DtlsControlImpl</tt>. * * @author Lyubomir Marinov */ public class DtlsPacketTransformer extends SinglePacketTransformer { private static final long CONNECT_RETRY_INTERVAL = 500; /** * The maximum number of times that {@link #runInConnectThread(DTLSProtocol, TlsPeer, * DatagramTransport)} is to retry the invocations of {@link DTLSClientProtocol#connect(TlsClient, * DatagramTransport)} and {@link DTLSServerProtocol#accept(TlsServer, DatagramTransport)} in * anticipation of a successful connection. */ private static final int CONNECT_TRIES = 3; /** * The indicator which determines whether unencrypted packets sent or received through * <tt>DtlsPacketTransformer</tt> are to be dropped. The default value is <tt>false</tt>. * * @see #DROP_UNENCRYPTED_PKTS_PNAME */ private static final boolean DROP_UNENCRYPTED_PKTS; /** * The name of the <tt>ConfigurationService</tt> and/or <tt>System</tt> property which indicates * whether unencrypted packets sent or received through <tt>DtlsPacketTransformer</tt> are to be * dropped. The default value is <tt>false</tt>. */ private static final String DROP_UNENCRYPTED_PKTS_PNAME = DtlsPacketTransformer.class.getName() + ".dropUnencryptedPkts"; /** The length of the header of a DTLS record. */ static final int DTLS_RECORD_HEADER_LENGTH = 13; /** * The number of milliseconds a <tt>DtlsPacketTransform</tt> is to wait on its {@link * #dtlsTransport} in order to receive a packet. */ private static final int DTLS_TRANSPORT_RECEIVE_WAITMILLIS = -1; /** * The <tt>Logger</tt> used by the <tt>DtlsPacketTransformer</tt> class and its instances to print * debug information. */ private static final Logger logger = Logger.getLogger(DtlsPacketTransformer.class); static { ConfigurationService cfg = LibJitsi.getConfigurationService(); boolean dropUnencryptedPkts = false; if (cfg == null) { String s = System.getProperty(DROP_UNENCRYPTED_PKTS_PNAME); if (s != null) dropUnencryptedPkts = Boolean.parseBoolean(s); } else { dropUnencryptedPkts = cfg.getBoolean(DROP_UNENCRYPTED_PKTS_PNAME, dropUnencryptedPkts); } DROP_UNENCRYPTED_PKTS = dropUnencryptedPkts; } /** * Determines whether a specific array of <tt>byte</tt>s appears to contain a DTLS record. * * @param buf the array of <tt>byte</tt>s to be analyzed * @param off the offset within <tt>buf</tt> at which the analysis is to start * @param len the number of bytes within <tt>buf</tt> starting at <tt>off</tt> to be analyzed * @return <tt>true</tt> if the specified <tt>buf</tt> appears to contain a DTLS record */ public static boolean isDtlsRecord(byte[] buf, int off, int len) { boolean b = false; if (len >= DTLS_RECORD_HEADER_LENGTH) { short type = TlsUtils.readUint8(buf, off); switch (type) { case ContentType.alert: case ContentType.application_data: case ContentType.change_cipher_spec: case ContentType.handshake: int major = buf[off + 1] & 0xff; int minor = buf[off + 2] & 0xff; ProtocolVersion version = null; if ((major == ProtocolVersion.DTLSv10.getMajorVersion()) && (minor == ProtocolVersion.DTLSv10.getMinorVersion())) { version = ProtocolVersion.DTLSv10; } if ((version == null) && (major == ProtocolVersion.DTLSv12.getMajorVersion()) && (minor == ProtocolVersion.DTLSv12.getMinorVersion())) { version = ProtocolVersion.DTLSv12; } if (version != null) { int length = TlsUtils.readUint16(buf, off + 11); if (DTLS_RECORD_HEADER_LENGTH + length <= len) b = true; } break; default: // Unless a new ContentType has been defined by the Bouncy // Castle Crypto APIs, the specified buf does not represent a // DTLS record. break; } } return b; } /** The ID of the component which this instance works for/is associated with. */ private final int componentID; /** The <tt>RTPConnector</tt> which uses this <tt>PacketTransformer</tt>. */ private AbstractRTPConnector connector; /** The background <tt>Thread</tt> which initializes {@link #dtlsTransport}. */ private Thread connectThread; /** * The <tt>DatagramTransport</tt> implementation which adapts {@link #connector} and this * <tt>PacketTransformer</tt> to the terms of the Bouncy Castle Crypto APIs. */ private DatagramTransportImpl datagramTransport; /** * The <tt>DTLSTransport</tt> through which the actual packet transformations are being performed * by this instance. */ private DTLSTransport dtlsTransport; /** The <tt>MediaType</tt> of the stream which this instance works for/is associated with. */ private MediaType mediaType; /** * Whether rtcp-mux is in use. * * <p>If enabled, and this is the transformer for RTCP, it will not establish a DTLS session on * its own, but rather wait for the RTP transformer to do so, and reuse it to initialize the SRTP * transformer. */ private boolean rtcpmux = false; /** * The value of the <tt>setup</tt> SDP attribute defined by RFC 4145 "TCP-Based Media * Transport in the Session Description Protocol (SDP)" which determines whether this * instance acts as a DTLS client or a DTLS server. */ private DtlsControl.Setup setup; /** The {@code SRTPTransformer} (to be) used by this instance. */ private SinglePacketTransformer _srtpTransformer; /** * The indicator which determines whether the <tt>TlsPeer</tt> employed by this * <tt>PacketTransformer</tt> has raised an <tt>AlertDescription.close_notify</tt> * <tt>AlertLevel.warning</tt> i.e. the remote DTLS peer has closed the write side of the * connection. */ private boolean tlsPeerHasRaisedCloseNotifyWarning; /** The <tt>TransformEngine</tt> which has initialized this instance. */ private final DtlsTransformEngine transformEngine; /** * Initializes a new <tt>DtlsPacketTransformer</tt> instance. * * @param transformEngine the <tt>TransformEngine</tt> which is initializing the new instance * @param componentID the ID of the component for which the new instance is to work */ public DtlsPacketTransformer(DtlsTransformEngine transformEngine, int componentID) { this.transformEngine = transformEngine; this.componentID = componentID; } /** {@inheritDoc} */ @Override public synchronized void close() { // SrtpControl.start(MediaType) starts its associated TransformEngine. // We will use that mediaType to signal the normal stop then as well // i.e. we will call setMediaType(null) first. setMediaType(null); setConnector(null); } /** * Closes {@link #datagramTransport} if it is non-<tt>null</tt> and logs and swallows any * <tt>IOException</tt>. */ private void closeDatagramTransport() { if (datagramTransport != null) { try { datagramTransport.close(); } catch (IOException ioe) { // DatagramTransportImpl has no reason to fail because it is // merely an adapter of #connector and this PacketTransformer to // the terms of the Bouncy Castle Crypto API. logger.error("Failed to (properly) close " + datagramTransport.getClass(), ioe); } datagramTransport = null; } } /** * Determines whether {@link #runInConnectThread(DTLSProtocol, TlsPeer, DatagramTransport)} is to * try to establish a DTLS connection. * * @param i the number of tries remaining after the current one * @param datagramTransport * @return <tt>true</tt> to try to establish a DTLS connection; otherwise, <tt>false</tt> */ private boolean enterRunInConnectThreadLoop(int i, DatagramTransport datagramTransport) { if (i < 0 || i > CONNECT_TRIES) { return false; } else { Thread currentThread = Thread.currentThread(); synchronized (this) { if (i > 0 && i < CONNECT_TRIES - 1) { boolean interrupted = false; try { wait(CONNECT_RETRY_INTERVAL); } catch (InterruptedException ie) { interrupted = true; } if (interrupted) currentThread.interrupt(); } return currentThread.equals(this.connectThread) && datagramTransport.equals(this.datagramTransport); } } } /** * Gets the <tt>DtlsControl</tt> implementation associated with this instance. * * @return the <tt>DtlsControl</tt> implementation associated with this instance */ DtlsControlImpl getDtlsControl() { return getTransformEngine().getDtlsControl(); } /** * Gets the <tt>TransformEngine</tt> which has initialized this instance. * * @return the <tt>TransformEngine</tt> which has initialized this instance */ DtlsTransformEngine getTransformEngine() { return transformEngine; } /** * Handles a specific <tt>IOException</tt> which was thrown during the execution of {@link * #runInConnectThread(DTLSProtocol, TlsPeer, DatagramTransport)} while trying to establish a DTLS * connection * * @param ioe the <tt>IOException</tt> to handle * @param msg the human-readable message to log about the specified <tt>ioe</tt> * @param i the number of tries remaining after the current one * @return <tt>true</tt> if the specified <tt>ioe</tt> was successfully handled; <tt>false</tt>, * otherwise */ private boolean handleRunInConnectThreadException(IOException ioe, String msg, int i) { // SrtpControl.start(MediaType) starts its associated TransformEngine. // We will use that mediaType to signal the normal stop then as well // i.e. we will ignore exception after the procedure to stop this // PacketTransformer has begun. if (mediaType == null) return false; if (ioe instanceof TlsFatalAlert) { TlsFatalAlert tfa = (TlsFatalAlert) ioe; short alertDescription = tfa.getAlertDescription(); if (alertDescription == AlertDescription.unexpected_message) { msg += " Received fatal unexpected message."; if (i == 0 || !Thread.currentThread().equals(connectThread) || connector == null || mediaType == null) { msg += " Giving up after " + (CONNECT_TRIES - i) + " retries."; } else { msg += " Will retry."; logger.error(msg, ioe); return true; } } else { msg += " Received fatal alert " + alertDescription + "."; } } logger.error(msg, ioe); return false; } /** * Tries to initialize {@link #_srtpTransformer} by using the <tt>DtlsPacketTransformer</tt> for * RTP. * * @return the (possibly updated) value of {@link #_srtpTransformer}. */ private SinglePacketTransformer initializeSRTCPTransformerFromRtp() { DtlsPacketTransformer rtpTransformer = (DtlsPacketTransformer) getTransformEngine().getRTPTransformer(); // Prevent recursion (that is pretty much impossible to ever happen). if (rtpTransformer != this) { PacketTransformer srtpTransformer = rtpTransformer.waitInitializeAndGetSRTPTransformer(); if (srtpTransformer != null && srtpTransformer instanceof SRTPTransformer) { synchronized (this) { if (_srtpTransformer == null) { _srtpTransformer = new SRTCPTransformer((SRTPTransformer) srtpTransformer); // For the sake of completeness, we notify whenever we // assign to _srtpTransformer. notifyAll(); } } } } return _srtpTransformer; } /** * Initializes a new <tt>SRTPTransformer</tt> instance with a specific (negotiated) * <tt>SRTPProtectionProfile</tt> and the keying material specified by a specific * <tt>TlsContext</tt>. * * @param srtpProtectionProfile the (negotiated) <tt>SRTPProtectionProfile</tt> to initialize the * new instance with * @param tlsContext the <tt>TlsContext</tt> which represents the keying material * @return a new <tt>SRTPTransformer</tt> instance initialized with <tt>srtpProtectionProfile</tt> * and <tt>tlsContext</tt> */ private SinglePacketTransformer initializeSRTPTransformer( int srtpProtectionProfile, TlsContext tlsContext) { boolean rtcp; switch (componentID) { case Component.RTCP: rtcp = true; break; case Component.RTP: rtcp = false; break; default: throw new IllegalStateException("componentID"); } int cipher_key_length; int cipher_salt_length; int cipher; int auth_function; int auth_key_length; int RTCP_auth_tag_length, RTP_auth_tag_length; switch (srtpProtectionProfile) { case SRTPProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_32: cipher_key_length = 128 / 8; cipher_salt_length = 112 / 8; cipher = SRTPPolicy.AESCM_ENCRYPTION; auth_function = SRTPPolicy.HMACSHA1_AUTHENTICATION; auth_key_length = 160 / 8; RTCP_auth_tag_length = 80 / 8; RTP_auth_tag_length = 32 / 8; break; case SRTPProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_80: cipher_key_length = 128 / 8; cipher_salt_length = 112 / 8; cipher = SRTPPolicy.AESCM_ENCRYPTION; auth_function = SRTPPolicy.HMACSHA1_AUTHENTICATION; auth_key_length = 160 / 8; RTCP_auth_tag_length = RTP_auth_tag_length = 80 / 8; break; case SRTPProtectionProfile.SRTP_NULL_HMAC_SHA1_32: cipher_key_length = 0; cipher_salt_length = 0; cipher = SRTPPolicy.NULL_ENCRYPTION; auth_function = SRTPPolicy.HMACSHA1_AUTHENTICATION; auth_key_length = 160 / 8; RTCP_auth_tag_length = 80 / 8; RTP_auth_tag_length = 32 / 8; break; case SRTPProtectionProfile.SRTP_NULL_HMAC_SHA1_80: cipher_key_length = 0; cipher_salt_length = 0; cipher = SRTPPolicy.NULL_ENCRYPTION; auth_function = SRTPPolicy.HMACSHA1_AUTHENTICATION; auth_key_length = 160 / 8; RTCP_auth_tag_length = RTP_auth_tag_length = 80 / 8; break; default: throw new IllegalArgumentException("srtpProtectionProfile"); } byte[] keyingMaterial = tlsContext.exportKeyingMaterial( ExporterLabel.dtls_srtp, null, 2 * (cipher_key_length + cipher_salt_length)); byte[] client_write_SRTP_master_key = new byte[cipher_key_length]; byte[] server_write_SRTP_master_key = new byte[cipher_key_length]; byte[] client_write_SRTP_master_salt = new byte[cipher_salt_length]; byte[] server_write_SRTP_master_salt = new byte[cipher_salt_length]; byte[][] keyingMaterialValues = { client_write_SRTP_master_key, server_write_SRTP_master_key, client_write_SRTP_master_salt, server_write_SRTP_master_salt }; for (int i = 0, keyingMaterialOffset = 0; i < keyingMaterialValues.length; i++) { byte[] keyingMaterialValue = keyingMaterialValues[i]; System.arraycopy( keyingMaterial, keyingMaterialOffset, keyingMaterialValue, 0, keyingMaterialValue.length); keyingMaterialOffset += keyingMaterialValue.length; } SRTPPolicy srtcpPolicy = new SRTPPolicy( cipher, cipher_key_length, auth_function, auth_key_length, RTCP_auth_tag_length, cipher_salt_length); SRTPPolicy srtpPolicy = new SRTPPolicy( cipher, cipher_key_length, auth_function, auth_key_length, RTP_auth_tag_length, cipher_salt_length); SRTPContextFactory clientSRTPContextFactory = new SRTPContextFactory( /* sender */ tlsContext instanceof TlsClientContext, client_write_SRTP_master_key, client_write_SRTP_master_salt, srtpPolicy, srtcpPolicy); SRTPContextFactory serverSRTPContextFactory = new SRTPContextFactory( /* sender */ tlsContext instanceof TlsServerContext, server_write_SRTP_master_key, server_write_SRTP_master_salt, srtpPolicy, srtcpPolicy); SRTPContextFactory forwardSRTPContextFactory; SRTPContextFactory reverseSRTPContextFactory; if (tlsContext instanceof TlsClientContext) { forwardSRTPContextFactory = clientSRTPContextFactory; reverseSRTPContextFactory = serverSRTPContextFactory; } else if (tlsContext instanceof TlsServerContext) { forwardSRTPContextFactory = serverSRTPContextFactory; reverseSRTPContextFactory = clientSRTPContextFactory; } else { throw new IllegalArgumentException("tlsContext"); } SinglePacketTransformer srtpTransformer; if (rtcp) { srtpTransformer = new SRTCPTransformer(forwardSRTPContextFactory, reverseSRTPContextFactory); } else { srtpTransformer = new SRTPTransformer(forwardSRTPContextFactory, reverseSRTPContextFactory); } return srtpTransformer; } /** * Notifies this instance that the DTLS record layer associated with a specific <tt>TlsPeer</tt> * has raised an alert. * * @param tlsPeer the <tt>TlsPeer</tt> whose associated DTLS record layer has raised an alert * @param alertLevel {@link AlertLevel} * @param alertDescription {@link AlertDescription} * @param message a human-readable message explaining what caused the alert. May be <tt>null</tt>. * @param cause the exception that caused the alert to be raised. May be <tt>null</tt>. */ void notifyAlertRaised( TlsPeer tlsPeer, short alertLevel, short alertDescription, String message, Exception cause) { if (AlertLevel.warning == alertLevel && AlertDescription.close_notify == alertDescription) { tlsPeerHasRaisedCloseNotifyWarning = true; } } /** {@inheritDoc} */ @Override public RawPacket reverseTransform(RawPacket pkt) { byte[] buf = pkt.getBuffer(); int off = pkt.getOffset(); int len = pkt.getLength(); if (isDtlsRecord(buf, off, len)) { if (rtcpmux && Component.RTCP == componentID) { // This should never happen. logger.warn( "Dropping a DTLS record, because it was received on the" + " RTCP channel while rtcpmux is in use."); return null; } boolean receive; synchronized (this) { if (datagramTransport == null) { receive = false; } else { datagramTransport.queueReceive(buf, off, len); receive = true; } } if (receive) { DTLSTransport dtlsTransport = this.dtlsTransport; if (dtlsTransport == null) { // The specified pkt looks like a DTLS record and it has // been consumed for the purposes of the secure channel // represented by this PacketTransformer. pkt = null; } else { try { int receiveLimit = dtlsTransport.getReceiveLimit(); int delta = receiveLimit - len; if (delta > 0) { pkt.grow(delta); buf = pkt.getBuffer(); off = pkt.getOffset(); len = pkt.getLength(); } else if (delta < 0) { pkt.shrink(-delta); buf = pkt.getBuffer(); off = pkt.getOffset(); len = pkt.getLength(); } int received = dtlsTransport.receive(buf, off, len, DTLS_TRANSPORT_RECEIVE_WAITMILLIS); if (received <= 0) { // No application data was decoded. pkt = null; } else { delta = len - received; if (delta > 0) pkt.shrink(delta); } } catch (IOException ioe) { pkt = null; // SrtpControl.start(MediaType) starts its associated // TransformEngine. We will use that mediaType to signal // the normal stop then as well i.e. we will ignore // exception after the procedure to stop this // PacketTransformer has begun. if (mediaType != null && !tlsPeerHasRaisedCloseNotifyWarning) { logger.error("Failed to decode a DTLS record!", ioe); } } } } else { // The specified pkt looks like a DTLS record but it is // unexpected in the current state of the secure channel // represented by this PacketTransformer. This PacketTransformer // has not been started (successfully) or has been closed. pkt = null; } } else if (transformEngine.isSrtpDisabled()) { // In pure DTLS mode only DTLS records pass through. pkt = null; } else { // DTLS-SRTP has not been initialized yet or has failed to // initialize. SinglePacketTransformer srtpTransformer = waitInitializeAndGetSRTPTransformer(); if (srtpTransformer != null) pkt = srtpTransformer.reverseTransform(pkt); else if (DROP_UNENCRYPTED_PKTS) pkt = null; // XXX Else, it is our explicit policy to let the received packet // pass through and rely on the SrtpListener to notify the user that // the session is not secured. } return pkt; } /** * Runs in {@link #connectThread} to initialize {@link #dtlsTransport}. * * @param dtlsProtocol * @param tlsPeer * @param datagramTransport */ private void runInConnectThread( DTLSProtocol dtlsProtocol, TlsPeer tlsPeer, DatagramTransport datagramTransport) { DTLSTransport dtlsTransport = null; final boolean srtp = !transformEngine.isSrtpDisabled(); int srtpProtectionProfile = 0; TlsContext tlsContext = null; // DTLS client if (dtlsProtocol instanceof DTLSClientProtocol) { DTLSClientProtocol dtlsClientProtocol = (DTLSClientProtocol) dtlsProtocol; TlsClientImpl tlsClient = (TlsClientImpl) tlsPeer; for (int i = CONNECT_TRIES - 1; i >= 0; i--) { if (!enterRunInConnectThreadLoop(i, datagramTransport)) break; try { dtlsTransport = dtlsClientProtocol.connect(tlsClient, datagramTransport); break; } catch (IOException ioe) { if (!handleRunInConnectThreadException( ioe, "Failed to connect this DTLS client to a DTLS" + " server!", i)) { break; } } } if (dtlsTransport != null && srtp) { srtpProtectionProfile = tlsClient.getChosenProtectionProfile(); tlsContext = tlsClient.getContext(); } } // DTLS server else if (dtlsProtocol instanceof DTLSServerProtocol) { DTLSServerProtocol dtlsServerProtocol = (DTLSServerProtocol) dtlsProtocol; TlsServerImpl tlsServer = (TlsServerImpl) tlsPeer; for (int i = CONNECT_TRIES - 1; i >= 0; i--) { if (!enterRunInConnectThreadLoop(i, datagramTransport)) break; try { dtlsTransport = dtlsServerProtocol.accept(tlsServer, datagramTransport); break; } catch (IOException ioe) { if (!handleRunInConnectThreadException( ioe, "Failed to accept a connection from a DTLS client!", i)) { break; } } } if (dtlsTransport != null && srtp) { srtpProtectionProfile = tlsServer.getChosenProtectionProfile(); tlsContext = tlsServer.getContext(); } } else { // It MUST be either a DTLS client or a DTLS server. throw new IllegalStateException("dtlsProtocol"); } SinglePacketTransformer srtpTransformer = (dtlsTransport == null || !srtp) ? null : initializeSRTPTransformer(srtpProtectionProfile, tlsContext); boolean closeSRTPTransformer; synchronized (this) { if (Thread.currentThread().equals(this.connectThread) && datagramTransport.equals(this.datagramTransport)) { this.dtlsTransport = dtlsTransport; _srtpTransformer = srtpTransformer; notifyAll(); } closeSRTPTransformer = (_srtpTransformer != srtpTransformer); } if (closeSRTPTransformer && srtpTransformer != null) srtpTransformer.close(); } /** * Sends the data contained in a specific byte array as application data through the DTLS * connection of this <tt>DtlsPacketTransformer</tt>. * * @param buf the byte array containing data to send. * @param off the offset in <tt>buf</tt> where the data begins. * @param len the length of data to send. */ public void sendApplicationData(byte[] buf, int off, int len) { DTLSTransport dtlsTransport = this.dtlsTransport; Throwable throwable = null; if (dtlsTransport != null) { try { dtlsTransport.send(buf, off, len); } catch (IOException ioe) { throwable = ioe; } } else { throwable = new NullPointerException("dtlsTransport"); } if (throwable != null) { // SrtpControl.start(MediaType) starts its associated // TransformEngine. We will use that mediaType to signal the normal // stop then as well i.e. we will ignore exception after the // procedure to stop this PacketTransformer has begun. if (mediaType != null && !tlsPeerHasRaisedCloseNotifyWarning) { logger.error("Failed to send application data over DTLS transport: ", throwable); } } } /** * Sets the <tt>RTPConnector</tt> which is to use or uses this <tt>PacketTransformer</tt>. * * @param connector the <tt>RTPConnector</tt> which is to use or uses this * <tt>PacketTransformer</tt> */ void setConnector(AbstractRTPConnector connector) { if (this.connector != connector) { this.connector = connector; DatagramTransportImpl datagramTransport = this.datagramTransport; if (datagramTransport != null) datagramTransport.setConnector(connector); } } /** * Sets the <tt>MediaType</tt> of the stream which this instance is to work for/be associated * with. * * @param mediaType the <tt>MediaType</tt> of the stream which this instance is to work for/be * associated with */ synchronized void setMediaType(MediaType mediaType) { if (this.mediaType != mediaType) { MediaType oldValue = this.mediaType; this.mediaType = mediaType; if (oldValue != null) stop(); if (this.mediaType != null) start(); } } /** * Enables/disables rtcp-mux. * * @param rtcpmux whether to enable or disable. */ void setRtcpmux(boolean rtcpmux) { this.rtcpmux = rtcpmux; } /** * Sets the DTLS protocol according to which this <tt>DtlsPacketTransformer</tt> is to act either * as a DTLS server or a DTLS client. * * @param setup the value of the <tt>setup</tt> SDP attribute to set on this instance in order to * determine whether this instance is to act as a DTLS client or a DTLS server */ void setSetup(DtlsControl.Setup setup) { if (this.setup != setup) this.setup = setup; } /** Starts this <tt>PacketTransformer</tt>. */ private synchronized void start() { if (this.datagramTransport != null) { if (this.connectThread == null && dtlsTransport == null) { logger.warn( getClass().getName() + " has been started but has failed to establish" + " the DTLS connection!"); } return; } if (rtcpmux && Component.RTCP == componentID) { // In the case of rtcp-mux, the RTCP transformer does not create // a DTLS session. The SRTP context (_srtpTransformer) will be // initialized on demand using initializeSRTCPTransformerFromRtp(). return; } AbstractRTPConnector connector = this.connector; if (connector == null) throw new NullPointerException("connector"); DtlsControl.Setup setup = this.setup; SecureRandom secureRandom = DtlsControlImpl.createSecureRandom(); final DTLSProtocol dtlsProtocolObj; final TlsPeer tlsPeer; if (DtlsControl.Setup.ACTIVE.equals(setup)) { dtlsProtocolObj = new DTLSClientProtocol(secureRandom); tlsPeer = new TlsClientImpl(this); } else { dtlsProtocolObj = new DTLSServerProtocol(secureRandom); tlsPeer = new TlsServerImpl(this); } tlsPeerHasRaisedCloseNotifyWarning = false; final DatagramTransportImpl datagramTransport = new DatagramTransportImpl(componentID); datagramTransport.setConnector(connector); Thread connectThread = new Thread() { @Override public void run() { try { runInConnectThread(dtlsProtocolObj, tlsPeer, datagramTransport); } finally { if (Thread.currentThread().equals(DtlsPacketTransformer.this.connectThread)) { DtlsPacketTransformer.this.connectThread = null; } } } }; connectThread.setDaemon(true); connectThread.setName(DtlsPacketTransformer.class.getName() + ".connectThread"); this.connectThread = connectThread; this.datagramTransport = datagramTransport; boolean started = false; try { connectThread.start(); started = true; } finally { if (!started) { if (connectThread.equals(this.connectThread)) this.connectThread = null; if (datagramTransport.equals(this.datagramTransport)) this.datagramTransport = null; } } notifyAll(); } /** Stops this <tt>PacketTransformer</tt>. */ private synchronized void stop() { if (connectThread != null) connectThread = null; try { // The dtlsTransport and _srtpTransformer SHOULD be closed, of // course. The datagramTransport MUST be closed. if (dtlsTransport != null) { try { dtlsTransport.close(); } catch (IOException ioe) { logger.error("Failed to (properly) close " + dtlsTransport.getClass(), ioe); } dtlsTransport = null; } if (_srtpTransformer != null) { _srtpTransformer.close(); _srtpTransformer = null; } } finally { try { closeDatagramTransport(); } finally { notifyAll(); } } } /** {@inheritDoc} */ @Override public RawPacket transform(RawPacket pkt) { byte[] buf = pkt.getBuffer(); int off = pkt.getOffset(); int len = pkt.getLength(); // If the specified pkt represents a DTLS record, then it should pass // through this PacketTransformer (e.g. it has been sent through // DatagramTransportImpl). if (isDtlsRecord(buf, off, len)) return pkt; // SRTP if (!transformEngine.isSrtpDisabled()) { // DTLS-SRTP has not been initialized yet or has failed to // initialize. SinglePacketTransformer srtpTransformer = waitInitializeAndGetSRTPTransformer(); if (srtpTransformer != null) pkt = srtpTransformer.transform(pkt); else if (DROP_UNENCRYPTED_PKTS) pkt = null; // XXX Else, it is our explicit policy to let the received packet // pass through and rely on the SrtpListener to notify the user that // the session is not secured. } // Pure/non-SRTP DTLS else { // The specified pkt will pass through this PacketTransformer only // if it gets transformed into a DTLS record. pkt = null; sendApplicationData(buf, off, len); } return pkt; } /** * Gets the {@code SRTPTransformer} used by this instance. If {@link #_srtpTransformer} does not * exist (yet) and the state of this instance indicates that its initialization is in progess, * then blocks until {@code _srtpTransformer} is initialized and returns it. * * @return the {@code SRTPTransformer} used by this instance */ private SinglePacketTransformer waitInitializeAndGetSRTPTransformer() { SinglePacketTransformer srtpTransformer = _srtpTransformer; if (srtpTransformer != null) return srtpTransformer; if (rtcpmux && Component.RTCP == componentID) return initializeSRTCPTransformerFromRtp(); // XXX It is our explicit policy to rely on the SrtpListener to notify // the user that the session is not secure. Unfortunately, (1) the // SrtpListener is not supported by this DTLS SrtpControl implementation // and (2) encrypted packets may arrive soon enough to be let through // while _srtpTransformer is still initializing. Consequently, we will // block and wait for _srtpTransformer to initialize. boolean interrupted = false; try { synchronized (this) { do { srtpTransformer = _srtpTransformer; if (srtpTransformer != null) break; // _srtpTransformer is initialized if (connectThread == null) { // Though _srtpTransformer is NOT initialized, there is // no point in waiting because there is no one to // initialize it. break; } try { // It does not really matter (enough) how much we wait // here because we wait in a loop. long timeout = CONNECT_TRIES * CONNECT_RETRY_INTERVAL; wait(timeout); } catch (InterruptedException ie) { interrupted = true; } } while (true); } } finally { if (interrupted) Thread.currentThread().interrupt(); } return srtpTransformer; } }
/** * @author Lyubomir Marinov * @author Damian Minkov * @author Yana Stamcheva */ public class MediaConfiguration { /** The <tt>Logger</tt> used by the <tt>MediaConfiguration</tt> class for logging output. */ private static final Logger logger = Logger.getLogger(MediaConfiguration.class); /** The <tt>MediaService</tt> implementation used by <tt>MediaConfiguration</tt>. */ private static final MediaServiceImpl mediaService = NeomediaActivator.getMediaServiceImpl(); /** The preferred width of all panels. */ private static final int WIDTH = 350; /** * Indicates if the Devices settings configuration tab should be disabled, i.e. not visible to the * user. */ private static final String DEVICES_DISABLED_PROP = "net.java.sip.communicator.impl.neomedia.devicesconfig.DISABLED"; /** * Indicates if the Audio/Video encodings configuration tab should be disabled, i.e. not visible * to the user. */ private static final String ENCODINGS_DISABLED_PROP = "net.java.sip.communicator.impl.neomedia.encodingsconfig.DISABLED"; /** * Indicates if the Video/More Settings configuration tab should be disabled, i.e. not visible to * the user. */ private static final String VIDEO_MORE_SETTINGS_DISABLED_PROP = "net.java.sip.communicator.impl.neomedia.videomoresettingsconfig.DISABLED"; /** * Returns the audio configuration panel. * * @return the audio configuration panel */ public static Component createAudioConfigPanel() { return createControls(DeviceConfigurationComboBoxModel.AUDIO); } /** * Returns the video configuration panel. * * @return the video configuration panel */ public static Component createVideoConfigPanel() { return createControls(DeviceConfigurationComboBoxModel.VIDEO); } private static void createAudioPreview( final AudioSystem audioSystem, final JComboBox comboBox, final SoundLevelIndicator soundLevelIndicator) { final ActionListener captureComboActionListener = new ActionListener() { private final SimpleAudioLevelListener audioLevelListener = new SimpleAudioLevelListener() { public void audioLevelChanged(int level) { soundLevelIndicator.updateSoundLevel(level); } }; private AudioMediaDeviceSession deviceSession; private final BufferTransferHandler transferHandler = new BufferTransferHandler() { public void transferData(PushBufferStream stream) { try { stream.read(transferHandlerBuffer); } catch (IOException ioe) { } } }; private final Buffer transferHandlerBuffer = new Buffer(); public void actionPerformed(ActionEvent event) { setDeviceSession(null); CaptureDeviceInfo cdi; if (comboBox == null) { cdi = soundLevelIndicator.isShowing() ? audioSystem.getCaptureDevice() : null; } else { Object selectedItem = soundLevelIndicator.isShowing() ? comboBox.getSelectedItem() : null; cdi = (selectedItem instanceof DeviceConfigurationComboBoxModel.CaptureDevice) ? ((DeviceConfigurationComboBoxModel.CaptureDevice) selectedItem).info : null; } if (cdi != null) { for (MediaDevice md : mediaService.getDevices(MediaType.AUDIO, MediaUseCase.ANY)) { if (md instanceof AudioMediaDeviceImpl) { AudioMediaDeviceImpl amd = (AudioMediaDeviceImpl) md; if (cdi.equals(amd.getCaptureDeviceInfo())) { try { MediaDeviceSession deviceSession = amd.createSession(); boolean setDeviceSession = false; try { if (deviceSession instanceof AudioMediaDeviceSession) { setDeviceSession((AudioMediaDeviceSession) deviceSession); setDeviceSession = true; } } finally { if (!setDeviceSession) deviceSession.close(); } } catch (Throwable t) { if (t instanceof ThreadDeath) throw (ThreadDeath) t; } break; } } } } } private void setDeviceSession(AudioMediaDeviceSession deviceSession) { if (this.deviceSession == deviceSession) return; if (this.deviceSession != null) { try { this.deviceSession.close(); } finally { this.deviceSession.setLocalUserAudioLevelListener(null); soundLevelIndicator.resetSoundLevel(); } } this.deviceSession = deviceSession; if (this.deviceSession != null) { this.deviceSession.setContentDescriptor(new ContentDescriptor(ContentDescriptor.RAW)); this.deviceSession.setLocalUserAudioLevelListener(audioLevelListener); this.deviceSession.start(MediaDirection.SENDONLY); try { DataSource dataSource = this.deviceSession.getOutputDataSource(); dataSource.connect(); PushBufferStream[] streams = ((PushBufferDataSource) dataSource).getStreams(); for (PushBufferStream stream : streams) stream.setTransferHandler(transferHandler); dataSource.start(); } catch (Throwable t) { if (t instanceof ThreadDeath) throw (ThreadDeath) t; else setDeviceSession(null); } } } }; if (comboBox != null) comboBox.addActionListener(captureComboActionListener); soundLevelIndicator.addHierarchyListener( new HierarchyListener() { public void hierarchyChanged(HierarchyEvent event) { if ((event.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) { SwingUtilities.invokeLater( new Runnable() { public void run() { captureComboActionListener.actionPerformed(null); } }); } } }); } /** * Creates the UI controls which are to control the details of a specific <tt>AudioSystem</tt>. * * @param audioSystem the <tt>AudioSystem</tt> for which the UI controls to control its details * are to be created * @param container the <tt>JComponent</tt> into which the UI controls which are to control the * details of the specified <tt>audioSystem</tt> are to be added */ public static void createAudioSystemControls(AudioSystem audioSystem, JComponent container) { GridBagConstraints constraints = new GridBagConstraints(); constraints.anchor = GridBagConstraints.NORTHWEST; constraints.fill = GridBagConstraints.HORIZONTAL; constraints.weighty = 0; int audioSystemFeatures = audioSystem.getFeatures(); boolean featureNotifyAndPlaybackDevices = ((audioSystemFeatures & AudioSystem.FEATURE_NOTIFY_AND_PLAYBACK_DEVICES) != 0); constraints.gridx = 0; constraints.insets = new Insets(3, 0, 3, 3); constraints.weightx = 0; constraints.gridy = 0; container.add( new JLabel(getLabelText(DeviceConfigurationComboBoxModel.AUDIO_CAPTURE)), constraints); if (featureNotifyAndPlaybackDevices) { constraints.gridy = 2; container.add( new JLabel(getLabelText(DeviceConfigurationComboBoxModel.AUDIO_PLAYBACK)), constraints); constraints.gridy = 3; container.add( new JLabel(getLabelText(DeviceConfigurationComboBoxModel.AUDIO_NOTIFY)), constraints); } constraints.gridx = 1; constraints.insets = new Insets(3, 3, 3, 0); constraints.weightx = 1; JComboBox captureCombo = null; if (featureNotifyAndPlaybackDevices) { captureCombo = new JComboBox(); captureCombo.setEditable(false); captureCombo.setModel( new DeviceConfigurationComboBoxModel( captureCombo, mediaService.getDeviceConfiguration(), DeviceConfigurationComboBoxModel.AUDIO_CAPTURE)); constraints.gridy = 0; container.add(captureCombo, constraints); } int anchor = constraints.anchor; SoundLevelIndicator capturePreview = new SoundLevelIndicator( SimpleAudioLevelListener.MIN_LEVEL, SimpleAudioLevelListener.MAX_LEVEL); constraints.anchor = GridBagConstraints.CENTER; constraints.gridy = (captureCombo == null) ? 0 : 1; container.add(capturePreview, constraints); constraints.anchor = anchor; constraints.gridy = GridBagConstraints.RELATIVE; if (featureNotifyAndPlaybackDevices) { JComboBox playbackCombo = new JComboBox(); playbackCombo.setEditable(false); playbackCombo.setModel( new DeviceConfigurationComboBoxModel( captureCombo, mediaService.getDeviceConfiguration(), DeviceConfigurationComboBoxModel.AUDIO_PLAYBACK)); container.add(playbackCombo, constraints); JComboBox notifyCombo = new JComboBox(); notifyCombo.setEditable(false); notifyCombo.setModel( new DeviceConfigurationComboBoxModel( captureCombo, mediaService.getDeviceConfiguration(), DeviceConfigurationComboBoxModel.AUDIO_NOTIFY)); container.add(notifyCombo, constraints); } if ((AudioSystem.FEATURE_ECHO_CANCELLATION & audioSystemFeatures) != 0) { final SIPCommCheckBox echoCancelCheckBox = new SIPCommCheckBox( NeomediaActivator.getResources().getI18NString("impl.media.configform.ECHOCANCEL")); /* * First set the selected one, then add the listener in order to * avoid saving the value when using the default one and only * showing to user without modification. */ echoCancelCheckBox.setSelected(mediaService.getDeviceConfiguration().isEchoCancel()); echoCancelCheckBox.addItemListener( new ItemListener() { public void itemStateChanged(ItemEvent e) { mediaService.getDeviceConfiguration().setEchoCancel(echoCancelCheckBox.isSelected()); } }); container.add(echoCancelCheckBox, constraints); } if ((AudioSystem.FEATURE_DENOISE & audioSystemFeatures) != 0) { final SIPCommCheckBox denoiseCheckBox = new SIPCommCheckBox( NeomediaActivator.getResources().getI18NString("impl.media.configform.DENOISE")); /* * First set the selected one, then add the listener in order to * avoid saving the value when using the default one and only * showing to user without modification. */ denoiseCheckBox.setSelected(mediaService.getDeviceConfiguration().isDenoise()); denoiseCheckBox.addItemListener( new ItemListener() { public void itemStateChanged(ItemEvent e) { mediaService.getDeviceConfiguration().setDenoise(denoiseCheckBox.isSelected()); } }); container.add(denoiseCheckBox, constraints); } createAudioPreview(audioSystem, captureCombo, capturePreview); } /** * Creates basic controls for a type (AUDIO or VIDEO). * * @param type the type. * @return the build Component. */ public static Component createBasicControls(final int type) { final JComboBox deviceComboBox = new JComboBox(); deviceComboBox.setEditable(false); deviceComboBox.setModel( new DeviceConfigurationComboBoxModel( deviceComboBox, mediaService.getDeviceConfiguration(), type)); JLabel deviceLabel = new JLabel(getLabelText(type)); deviceLabel.setDisplayedMnemonic(getDisplayedMnemonic(type)); deviceLabel.setLabelFor(deviceComboBox); final Container devicePanel = new TransparentPanel(new FlowLayout(FlowLayout.CENTER)); devicePanel.setMaximumSize(new Dimension(WIDTH, 25)); devicePanel.add(deviceLabel); devicePanel.add(deviceComboBox); final JPanel deviceAndPreviewPanel = new TransparentPanel(new BorderLayout()); int preferredDeviceAndPreviewPanelHeight; switch (type) { case DeviceConfigurationComboBoxModel.AUDIO: preferredDeviceAndPreviewPanelHeight = 225; break; case DeviceConfigurationComboBoxModel.VIDEO: preferredDeviceAndPreviewPanelHeight = 305; break; default: preferredDeviceAndPreviewPanelHeight = 0; break; } if (preferredDeviceAndPreviewPanelHeight > 0) deviceAndPreviewPanel.setPreferredSize( new Dimension(WIDTH, preferredDeviceAndPreviewPanelHeight)); deviceAndPreviewPanel.add(devicePanel, BorderLayout.NORTH); final ActionListener deviceComboBoxActionListener = new ActionListener() { public void actionPerformed(ActionEvent event) { boolean revalidateAndRepaint = false; for (int i = deviceAndPreviewPanel.getComponentCount() - 1; i >= 0; i--) { Component c = deviceAndPreviewPanel.getComponent(i); if (c != devicePanel) { deviceAndPreviewPanel.remove(i); revalidateAndRepaint = true; } } Component preview = null; if ((deviceComboBox.getSelectedItem() != null) && deviceComboBox.isShowing()) { preview = createPreview(type, deviceComboBox, deviceAndPreviewPanel.getPreferredSize()); } if (preview != null) { deviceAndPreviewPanel.add(preview, BorderLayout.CENTER); revalidateAndRepaint = true; } if (revalidateAndRepaint) { deviceAndPreviewPanel.revalidate(); deviceAndPreviewPanel.repaint(); } } }; deviceComboBox.addActionListener(deviceComboBoxActionListener); /* * We have to initialize the controls to reflect the configuration * at the time of creating this instance. Additionally, because the * video preview will stop when it and its associated controls * become unnecessary, we have to restart it when the mentioned * controls become necessary again. We'll address the two goals * described by pretending there's a selection in the video combo * box when the combo box in question becomes displayable. */ deviceComboBox.addHierarchyListener( new HierarchyListener() { public void hierarchyChanged(HierarchyEvent event) { if ((event.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) { SwingUtilities.invokeLater( new Runnable() { public void run() { deviceComboBoxActionListener.actionPerformed(null); } }); } } }); return deviceAndPreviewPanel; } /** * Creates all the controls (including encoding) for a type(AUDIO or VIDEO) * * @param type the type. * @return the build Component. */ private static Component createControls(int type) { ConfigurationService cfg = NeomediaActivator.getConfigurationService(); SIPCommTabbedPane container = new SIPCommTabbedPane(); ResourceManagementService res = NeomediaActivator.getResources(); if ((cfg == null) || !cfg.getBoolean(DEVICES_DISABLED_PROP, false)) { container.insertTab( res.getI18NString("impl.media.configform.DEVICES"), null, createBasicControls(type), null, 0); } if ((cfg == null) || !cfg.getBoolean(ENCODINGS_DISABLED_PROP, false)) { container.insertTab( res.getI18NString("impl.media.configform.ENCODINGS"), null, new PriorityTable( new EncodingConfigurationTableModel(mediaService.getEncodingConfiguration(), type), 100), null, 1); } if ((type == DeviceConfigurationComboBoxModel.VIDEO) && ((cfg == null) || !cfg.getBoolean(VIDEO_MORE_SETTINGS_DISABLED_PROP, false))) { container.insertTab( res.getI18NString("impl.media.configform.VIDEO_MORE_SETTINGS"), null, createVideoAdvancedSettings(), null, 2); } return container; } /** * Creates preview for the (video) device in the video container. * * @param device the device * @param videoContainer the video container * @throws IOException a problem accessing the device * @throws MediaException a problem getting preview */ private static void createVideoPreview(CaptureDeviceInfo device, JComponent videoContainer) throws IOException, MediaException { videoContainer.removeAll(); videoContainer.revalidate(); videoContainer.repaint(); if (device == null) return; for (MediaDevice mediaDevice : mediaService.getDevices(MediaType.VIDEO, MediaUseCase.ANY)) { if (((MediaDeviceImpl) mediaDevice).getCaptureDeviceInfo().equals(device)) { Dimension videoContainerSize = videoContainer.getPreferredSize(); Component preview = (Component) mediaService.getVideoPreviewComponent( mediaDevice, videoContainerSize.width, videoContainerSize.height); if (preview != null) videoContainer.add(preview); break; } } } /** * Create preview component. * * @param type type * @param comboBox the options. * @param prefSize the preferred size * @return the component. */ private static Component createPreview(int type, final JComboBox comboBox, Dimension prefSize) { JComponent preview = null; if (type == DeviceConfigurationComboBoxModel.AUDIO) { Object selectedItem = comboBox.getSelectedItem(); if (selectedItem instanceof AudioSystem) { AudioSystem audioSystem = (AudioSystem) selectedItem; if (!NoneAudioSystem.LOCATOR_PROTOCOL.equalsIgnoreCase(audioSystem.getLocatorProtocol())) { preview = new TransparentPanel(new GridBagLayout()); createAudioSystemControls(audioSystem, preview); } } } else if (type == DeviceConfigurationComboBoxModel.VIDEO) { JLabel noPreview = new JLabel( NeomediaActivator.getResources().getI18NString("impl.media.configform.NO_PREVIEW")); noPreview.setHorizontalAlignment(SwingConstants.CENTER); noPreview.setVerticalAlignment(SwingConstants.CENTER); preview = createVideoContainer(noPreview); preview.setPreferredSize(prefSize); Object selectedItem = comboBox.getSelectedItem(); CaptureDeviceInfo device = null; if (selectedItem instanceof DeviceConfigurationComboBoxModel.CaptureDevice) device = ((DeviceConfigurationComboBoxModel.CaptureDevice) selectedItem).info; Exception exception; try { createVideoPreview(device, preview); exception = null; } catch (IOException ex) { exception = ex; } catch (MediaException ex) { exception = ex; } if (exception != null) { logger.error("Failed to create preview for device " + device, exception); device = null; } } return preview; } /** * Creates the video container. * * @param noVideoComponent the container component. * @return the video container. */ private static JComponent createVideoContainer(Component noVideoComponent) { return new VideoContainer(noVideoComponent, false); } /** * The mnemonic for a type. * * @param type audio or video type. * @return the mnemonic. */ private static char getDisplayedMnemonic(int type) { switch (type) { case DeviceConfigurationComboBoxModel.AUDIO: return NeomediaActivator.getResources().getI18nMnemonic("impl.media.configform.AUDIO"); case DeviceConfigurationComboBoxModel.VIDEO: return NeomediaActivator.getResources().getI18nMnemonic("impl.media.configform.VIDEO"); default: throw new IllegalArgumentException("type"); } } /** * A label for a type. * * @param type the type. * @return the label. */ private static String getLabelText(int type) { switch (type) { case DeviceConfigurationComboBoxModel.AUDIO: return NeomediaActivator.getResources().getI18NString("impl.media.configform.AUDIO"); case DeviceConfigurationComboBoxModel.AUDIO_CAPTURE: return NeomediaActivator.getResources().getI18NString("impl.media.configform.AUDIO_IN"); case DeviceConfigurationComboBoxModel.AUDIO_NOTIFY: return NeomediaActivator.getResources().getI18NString("impl.media.configform.AUDIO_NOTIFY"); case DeviceConfigurationComboBoxModel.AUDIO_PLAYBACK: return NeomediaActivator.getResources().getI18NString("impl.media.configform.AUDIO_OUT"); case DeviceConfigurationComboBoxModel.VIDEO: return NeomediaActivator.getResources().getI18NString("impl.media.configform.VIDEO"); default: throw new IllegalArgumentException("type"); } } /** * Creates the video advanced settings. * * @return video advanced settings panel. */ private static Component createVideoAdvancedSettings() { ResourceManagementService resources = NeomediaActivator.getResources(); final DeviceConfiguration deviceConfig = mediaService.getDeviceConfiguration(); TransparentPanel centerPanel = new TransparentPanel(new GridBagLayout()); centerPanel.setMaximumSize(new Dimension(WIDTH, 150)); JButton resetDefaultsButton = new JButton(resources.getI18NString("impl.media.configform.VIDEO_RESET")); JPanel resetButtonPanel = new TransparentPanel(new FlowLayout(FlowLayout.RIGHT)); resetButtonPanel.add(resetDefaultsButton); final JPanel centerAdvancedPanel = new TransparentPanel(new BorderLayout()); centerAdvancedPanel.add(centerPanel, BorderLayout.NORTH); centerAdvancedPanel.add(resetButtonPanel, BorderLayout.SOUTH); GridBagConstraints constraints = new GridBagConstraints(); constraints.fill = GridBagConstraints.HORIZONTAL; constraints.anchor = GridBagConstraints.NORTHWEST; constraints.insets = new Insets(5, 5, 0, 0); constraints.gridx = 0; constraints.weightx = 0; constraints.weighty = 0; constraints.gridy = 0; centerPanel.add( new JLabel(resources.getI18NString("impl.media.configform.VIDEO_RESOLUTION")), constraints); constraints.gridy = 1; constraints.insets = new Insets(0, 0, 0, 0); final JCheckBox frameRateCheck = new SIPCommCheckBox(resources.getI18NString("impl.media.configform.VIDEO_FRAME_RATE")); centerPanel.add(frameRateCheck, constraints); constraints.gridy = 2; constraints.insets = new Insets(5, 5, 0, 0); centerPanel.add( new JLabel(resources.getI18NString("impl.media.configform.VIDEO_PACKETS_POLICY")), constraints); constraints.weightx = 1; constraints.gridx = 1; constraints.gridy = 0; constraints.insets = new Insets(5, 0, 0, 5); Object[] resolutionValues = new Object[DeviceConfiguration.SUPPORTED_RESOLUTIONS.length + 1]; System.arraycopy( DeviceConfiguration.SUPPORTED_RESOLUTIONS, 0, resolutionValues, 1, DeviceConfiguration.SUPPORTED_RESOLUTIONS.length); final JComboBox sizeCombo = new JComboBox(resolutionValues); sizeCombo.setRenderer(new ResolutionCellRenderer()); sizeCombo.setEditable(false); centerPanel.add(sizeCombo, constraints); // default value is 20 final JSpinner frameRate = new JSpinner(new SpinnerNumberModel(20, 5, 30, 1)); frameRate.addChangeListener( new ChangeListener() { public void stateChanged(ChangeEvent e) { deviceConfig.setFrameRate( ((SpinnerNumberModel) frameRate.getModel()).getNumber().intValue()); } }); constraints.gridy = 1; constraints.insets = new Insets(0, 0, 0, 5); centerPanel.add(frameRate, constraints); frameRateCheck.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { if (frameRateCheck.isSelected()) { deviceConfig.setFrameRate( ((SpinnerNumberModel) frameRate.getModel()).getNumber().intValue()); } else // unlimited framerate deviceConfig.setFrameRate(-1); frameRate.setEnabled(frameRateCheck.isSelected()); } }); final JSpinner videoMaxBandwidth = new JSpinner( new SpinnerNumberModel(deviceConfig.getVideoMaxBandwidth(), 1, Integer.MAX_VALUE, 1)); videoMaxBandwidth.addChangeListener( new ChangeListener() { public void stateChanged(ChangeEvent e) { deviceConfig.setVideoMaxBandwidth( ((SpinnerNumberModel) videoMaxBandwidth.getModel()).getNumber().intValue()); } }); constraints.gridx = 1; constraints.gridy = 2; constraints.insets = new Insets(0, 0, 5, 5); centerPanel.add(videoMaxBandwidth, constraints); resetDefaultsButton.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { // reset to defaults sizeCombo.setSelectedIndex(0); frameRateCheck.setSelected(false); frameRate.setEnabled(false); frameRate.setValue(20); // unlimited framerate deviceConfig.setFrameRate(-1); videoMaxBandwidth.setValue(DeviceConfiguration.DEFAULT_VIDEO_MAX_BANDWIDTH); } }); // load selected value or auto Dimension videoSize = deviceConfig.getVideoSize(); if ((videoSize.getHeight() != DeviceConfiguration.DEFAULT_VIDEO_HEIGHT) && (videoSize.getWidth() != DeviceConfiguration.DEFAULT_VIDEO_WIDTH)) sizeCombo.setSelectedItem(deviceConfig.getVideoSize()); else sizeCombo.setSelectedIndex(0); sizeCombo.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { Dimension selectedVideoSize = (Dimension) sizeCombo.getSelectedItem(); if (selectedVideoSize == null) { // the auto value, default one selectedVideoSize = new Dimension( DeviceConfiguration.DEFAULT_VIDEO_WIDTH, DeviceConfiguration.DEFAULT_VIDEO_HEIGHT); } deviceConfig.setVideoSize(selectedVideoSize); } }); frameRateCheck.setSelected( deviceConfig.getFrameRate() != DeviceConfiguration.DEFAULT_VIDEO_FRAMERATE); frameRate.setEnabled(frameRateCheck.isSelected()); if (frameRate.isEnabled()) frameRate.setValue(deviceConfig.getFrameRate()); return centerAdvancedPanel; } /** Renders the available resolutions in the combo box. */ private static class ResolutionCellRenderer extends DefaultListCellRenderer { /** * The serialization version number of the <tt>ResolutionCellRenderer</tt> class. Defined to the * value of <tt>0</tt> because the <tt>ResolutionCellRenderer</tt> instances do not have state * of their own. */ private static final long serialVersionUID = 0L; /** * Sets readable text describing the resolution if the selected value is null we return the * string "Auto". * * @param list * @param value * @param index * @param isSelected * @param cellHasFocus * @return Component */ @Override public Component getListCellRendererComponent( JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { // call super to set backgrounds and fonts super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); // now just change the text if (value == null) setText("Auto"); else if (value instanceof Dimension) { Dimension d = (Dimension) value; setText(((int) d.getWidth()) + "x" + ((int) d.getHeight())); } return this; } } }
/** * Global statuses service impl. Gives access to the outside for some methods to change the global * status and individual protocol provider status. Use to implement global status menu with list of * all account statuses. * * @author Damian Minkov */ public class GlobalStatusServiceImpl implements GlobalStatusService { /** The object used for logging. */ private final Logger logger = Logger.getLogger(GlobalStatusServiceImpl.class); /** * Returns the last status that was stored in the configuration for the given protocol provider. * * @param protocolProvider the protocol provider * @return the last status that was stored in the configuration for the given protocol provider */ public PresenceStatus getLastPresenceStatus(ProtocolProviderService protocolProvider) { String lastStatus = getLastStatusString(protocolProvider); if (lastStatus != null) { OperationSetPresence presence = protocolProvider.getOperationSet(OperationSetPresence.class); if (presence == null) return null; Iterator<PresenceStatus> i = presence.getSupportedStatusSet(); PresenceStatus status; while (i.hasNext()) { status = i.next(); if (status.getStatusName().equals(lastStatus)) return status; } } return null; } /** * Returns the last contact status saved in the configuration. * * @param protocolProvider the protocol provider to which the status corresponds * @return the last contact status saved in the configuration. */ public String getLastStatusString(ProtocolProviderService protocolProvider) { // find the last contact status saved in the configuration. String lastStatus = null; ConfigurationService configService = GuiActivator.getConfigurationService(); String prefix = "net.java.sip.communicator.impl.gui.accounts"; List<String> accounts = configService.getPropertyNamesByPrefix(prefix, true); String protocolProviderAccountUID = protocolProvider.getAccountID().getAccountUniqueID(); for (String accountRootPropName : accounts) { String accountUID = configService.getString(accountRootPropName); if (accountUID.equals(protocolProviderAccountUID)) { lastStatus = configService.getString(accountRootPropName + ".lastAccountStatus"); if (lastStatus != null) break; } } return lastStatus; } /** * Publish present status. We search for the highest value in the given interval. * * @param protocolProvider the protocol provider to which we change the status. * @param status the status tu publish. */ public void publishStatus( ProtocolProviderService protocolProvider, PresenceStatus status, boolean rememberStatus) { OperationSetPresence presence = protocolProvider.getOperationSet(OperationSetPresence.class); LoginManager loginManager = GuiActivator.getUIService().getLoginManager(); RegistrationState registrationState = protocolProvider.getRegistrationState(); if (registrationState == RegistrationState.REGISTERED && presence != null && !presence.getPresenceStatus().equals(status)) { if (status.isOnline()) { new PublishPresenceStatusThread(protocolProvider, presence, status).start(); } else { loginManager.setManuallyDisconnected(true); GuiActivator.getUIService().getLoginManager().logoff(protocolProvider); } } else if (registrationState != RegistrationState.REGISTERED && registrationState != RegistrationState.REGISTERING && registrationState != RegistrationState.AUTHENTICATING && status.isOnline()) { GuiActivator.getUIService().getLoginManager().login(protocolProvider); } else if (!status.isOnline() && !(registrationState == RegistrationState.UNREGISTERING)) { loginManager.setManuallyDisconnected(true); GuiActivator.getUIService().getLoginManager().logoff(protocolProvider); } if (rememberStatus) saveStatusInformation(protocolProvider, status.getStatusName()); } /** * Publish present status. We search for the highest value in the given interval. * * <p>change the status. * * @param globalStatus */ public void publishStatus(GlobalStatusEnum globalStatus) { String itemName = globalStatus.getStatusName(); Iterator<ProtocolProviderService> pProviders = GuiActivator.getUIService().getMainFrame().getProtocolProviders(); while (pProviders.hasNext()) { ProtocolProviderService protocolProvider = pProviders.next(); if (itemName.equals(GlobalStatusEnum.ONLINE_STATUS)) { if (!protocolProvider.isRegistered()) { saveStatusInformation(protocolProvider, itemName); GuiActivator.getUIService().getLoginManager().login(protocolProvider); } else { OperationSetPresence presence = protocolProvider.getOperationSet(OperationSetPresence.class); if (presence == null) { saveStatusInformation(protocolProvider, itemName); continue; } Iterator<PresenceStatus> statusSet = presence.getSupportedStatusSet(); while (statusSet.hasNext()) { PresenceStatus status = statusSet.next(); if (status.getStatus() < PresenceStatus.EAGER_TO_COMMUNICATE_THRESHOLD && status.getStatus() >= PresenceStatus.AVAILABLE_THRESHOLD) { new PublishPresenceStatusThread(protocolProvider, presence, status).start(); this.saveStatusInformation(protocolProvider, status.getStatusName()); break; } } } } else if (itemName.equals(GlobalStatusEnum.OFFLINE_STATUS)) { if (!protocolProvider.getRegistrationState().equals(RegistrationState.UNREGISTERED) && !protocolProvider.getRegistrationState().equals(RegistrationState.UNREGISTERING)) { OperationSetPresence presence = protocolProvider.getOperationSet(OperationSetPresence.class); if (presence == null) { saveStatusInformation(protocolProvider, itemName); GuiActivator.getUIService().getLoginManager().logoff(protocolProvider); continue; } Iterator<PresenceStatus> statusSet = presence.getSupportedStatusSet(); while (statusSet.hasNext()) { PresenceStatus status = statusSet.next(); if (status.getStatus() < PresenceStatus.ONLINE_THRESHOLD) { this.saveStatusInformation(protocolProvider, status.getStatusName()); break; } } try { protocolProvider.unregister(); } catch (OperationFailedException e1) { logger.error( "Unable to unregister the protocol provider: " + protocolProvider + " due to the following exception: " + e1); } } } else if (itemName.equals(GlobalStatusEnum.FREE_FOR_CHAT_STATUS)) { // we search for highest available status here publishStatus( protocolProvider, PresenceStatus.AVAILABLE_THRESHOLD, PresenceStatus.MAX_STATUS_VALUE); } else if (itemName.equals(GlobalStatusEnum.DO_NOT_DISTURB_STATUS)) { // status between online and away is DND publishStatus( protocolProvider, PresenceStatus.ONLINE_THRESHOLD, PresenceStatus.AWAY_THRESHOLD); } else if (itemName.equals(GlobalStatusEnum.AWAY_STATUS)) { // a status in the away interval publishStatus( protocolProvider, PresenceStatus.AWAY_THRESHOLD, PresenceStatus.AVAILABLE_THRESHOLD); } } } /** * Publish present status. We search for the highest value in the given interval. * * @param protocolProvider the protocol provider to which we change the status. * @param floorStatusValue the min status value. * @param ceilStatusValue the max status value. */ private void publishStatus( ProtocolProviderService protocolProvider, int floorStatusValue, int ceilStatusValue) { if (!protocolProvider.isRegistered()) return; OperationSetPresence presence = protocolProvider.getOperationSet(OperationSetPresence.class); if (presence == null) return; Iterator<PresenceStatus> statusSet = presence.getSupportedStatusSet(); PresenceStatus status = null; while (statusSet.hasNext()) { PresenceStatus currentStatus = statusSet.next(); if (status == null && currentStatus.getStatus() < ceilStatusValue && currentStatus.getStatus() >= floorStatusValue) { status = currentStatus; } if (status != null) { if (currentStatus.getStatus() < ceilStatusValue && currentStatus.getStatus() >= floorStatusValue && currentStatus.getStatus() > status.getStatus()) { status = currentStatus; } } } if (status != null) { new PublishPresenceStatusThread(protocolProvider, presence, status).start(); this.saveStatusInformation(protocolProvider, status.getStatusName()); } } /** * Saves the last status for all accounts. This information is used on loging. Each time user logs * in he's logged with the same status as he was the last time before closing the application. * * @param protocolProvider the protocol provider to save status information for * @param statusName the name of the status to save */ private void saveStatusInformation(ProtocolProviderService protocolProvider, String statusName) { ConfigurationService configService = GuiActivator.getConfigurationService(); String prefix = "net.java.sip.communicator.impl.gui.accounts"; List<String> accounts = configService.getPropertyNamesByPrefix(prefix, true); boolean savedAccount = false; for (String accountRootPropName : accounts) { String accountUID = configService.getString(accountRootPropName); if (accountUID.equals(protocolProvider.getAccountID().getAccountUniqueID())) { configService.setProperty(accountRootPropName + ".lastAccountStatus", statusName); savedAccount = true; } } if (!savedAccount) { String accNodeName = "acc" + Long.toString(System.currentTimeMillis()); String accountPackage = "net.java.sip.communicator.impl.gui.accounts." + accNodeName; configService.setProperty( accountPackage, protocolProvider.getAccountID().getAccountUniqueID()); configService.setProperty(accountPackage + ".lastAccountStatus", statusName); } } /** Publishes the given status to the given presence operation set. */ private class PublishPresenceStatusThread extends Thread { private ProtocolProviderService protocolProvider; private PresenceStatus status; private OperationSetPresence presence; /** * Publishes the given <tt>status</tt> through the given <tt>presence</tt> operation set. * * @param presence the operation set through which we publish the status * @param status the status to publish */ public PublishPresenceStatusThread( ProtocolProviderService protocolProvider, OperationSetPresence presence, PresenceStatus status) { this.protocolProvider = protocolProvider; this.presence = presence; this.status = status; } @Override public void run() { try { presence.publishPresenceStatus(status, ""); } catch (IllegalArgumentException e1) { logger.error("Error - changing status", e1); } catch (IllegalStateException e1) { logger.error("Error - changing status", e1); } catch (OperationFailedException e1) { if (e1.getErrorCode() == OperationFailedException.GENERAL_ERROR) { String msgText = GuiActivator.getResources() .getI18NString( "service.gui.STATUS_CHANGE_GENERAL_ERROR", new String[] { protocolProvider.getAccountID().getUserID(), protocolProvider.getAccountID().getService() }); new ErrorDialog( null, GuiActivator.getResources().getI18NString("service.gui.GENERAL_ERROR"), msgText, e1) .showDialog(); } else if (e1.getErrorCode() == OperationFailedException.NETWORK_FAILURE) { String msgText = GuiActivator.getResources() .getI18NString( "service.gui.STATUS_CHANGE_NETWORK_FAILURE", new String[] { protocolProvider.getAccountID().getUserID(), protocolProvider.getAccountID().getService() }); new ErrorDialog( null, msgText, GuiActivator.getResources().getI18NString("service.gui.NETWORK_FAILURE"), e1) .showDialog(); } else if (e1.getErrorCode() == OperationFailedException.PROVIDER_NOT_REGISTERED) { String msgText = GuiActivator.getResources() .getI18NString( "service.gui.STATUS_CHANGE_NETWORK_FAILURE", new String[] { protocolProvider.getAccountID().getUserID(), protocolProvider.getAccountID().getService() }); new ErrorDialog( null, GuiActivator.getResources().getI18NString("service.gui.NETWORK_FAILURE"), msgText, e1) .showDialog(); } logger.error("Error - changing status", e1); } } } }
/** * @author Emil Ivov * @author Lyubomir Marinov */ public class ConfigurationActivator implements BundleActivator { /** The <tt>Logger</tt> used by the <tt>ConfigurationActivator</tt> class for logging output. */ private static final Logger logger = Logger.getLogger(ConfigurationActivator.class); /** The currently registered {@link ConfigurationService} instance. */ private ConfigurationService cs; /** * Starts the configuration service * * @param bundleContext the <tt>BundleContext</tt> as provided by the OSGi framework. * @throws Exception if anything goes wrong */ public void start(BundleContext bundleContext) throws Exception { FileAccessService fas = ServiceUtils.getService(bundleContext, FileAccessService.class); if (fas != null) { File usePropFileConfig; try { usePropFileConfig = fas.getPrivatePersistentFile(".usepropfileconfig", FileCategory.PROFILE); } catch (Exception ise) { // There is somewhat of a chicken-and-egg dependency between // FileConfigurationServiceImpl and ConfigurationServiceImpl: // FileConfigurationServiceImpl throws IllegalStateException if // certain System properties are not set, // ConfigurationServiceImpl will make sure that these properties // are set but it will do that later. // A SecurityException is thrown when the destination // is not writable or we do not have access to that folder usePropFileConfig = null; } if (usePropFileConfig != null && usePropFileConfig.exists()) { logger.info("Using properties file configuration store."); this.cs = LibJitsi.getConfigurationService(); } } if (this.cs == null) { this.cs = new JdbcConfigService(fas); } bundleContext.registerService(ConfigurationService.class.getName(), this.cs, null); fixPermissions(this.cs); } /** * Causes the configuration service to store the properties object and unregisters the * configuration service. * * @param bundleContext <tt>BundleContext</tt> * @throws Exception if anything goes wrong while storing the properties managed by the * <tt>ConfigurationService</tt> implementation provided by this bundle and while * unregistering the service in question */ public void stop(BundleContext bundleContext) throws Exception { this.cs.storeConfiguration(); this.cs = null; } /** * Makes home folder and the configuration file readable and writable only to the owner. * * @param cs the <tt>ConfigurationService</tt> instance to check for home folder and configuration * file. */ private static void fixPermissions(ConfigurationService cs) { if (!OSUtils.IS_LINUX && !OSUtils.IS_MAC) return; try { // let's check config file and config folder File homeFolder = new File(cs.getScHomeDirLocation(), cs.getScHomeDirName()); Set<PosixFilePermission> perms = new HashSet<PosixFilePermission>() { { add(PosixFilePermission.OWNER_READ); add(PosixFilePermission.OWNER_WRITE); add(PosixFilePermission.OWNER_EXECUTE); } }; Files.setPosixFilePermissions(Paths.get(homeFolder.getAbsolutePath()), perms); String fileName = cs.getConfigurationFilename(); if (fileName != null) { File cf = new File(homeFolder, fileName); if (cf.exists()) { perms = new HashSet<PosixFilePermission>() { { add(PosixFilePermission.OWNER_READ); add(PosixFilePermission.OWNER_WRITE); } }; Files.setPosixFilePermissions(Paths.get(cf.getAbsolutePath()), perms); } } } catch (Throwable t) { logger.error("Error creating c lib instance for fixing file permissions", t); if (t instanceof InterruptedException) Thread.currentThread().interrupt(); else if (t instanceof ThreadDeath) throw (ThreadDeath) t; } } }
/** * 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; } } }
/** * Implements <tt>BundleActivator</tt> for the neomedia bundle. * * @author Martin Andre * @author Emil Ivov * @author Lyubomir Marinov * @author Boris Grozev */ public class NeomediaActivator implements BundleActivator { /** * The <tt>Logger</tt> used by the <tt>NeomediaActivator</tt> class and its instances for logging * output. */ private final Logger logger = Logger.getLogger(NeomediaActivator.class); /** Indicates if the audio configuration form should be disabled, i.e. not visible to the user. */ private static final String AUDIO_CONFIG_DISABLED_PROP = "net.java.sip.communicator.impl.neomedia.AUDIO_CONFIG_DISABLED"; /** Indicates if the video configuration form should be disabled, i.e. not visible to the user. */ private static final String VIDEO_CONFIG_DISABLED_PROP = "net.java.sip.communicator.impl.neomedia.VIDEO_CONFIG_DISABLED"; /** Indicates if the H.264 configuration form should be disabled, i.e. not visible to the user. */ private static final String H264_CONFIG_DISABLED_PROP = "net.java.sip.communicator.impl.neomedia.h264config.DISABLED"; /** Indicates if the ZRTP configuration form should be disabled, i.e. not visible to the user. */ private static final String ZRTP_CONFIG_DISABLED_PROP = "net.java.sip.communicator.impl.neomedia.zrtpconfig.DISABLED"; /** * Indicates if the call recording config form should be disabled, i.e. not visible to the user. */ private static final String CALL_RECORDING_CONFIG_DISABLED_PROP = "net.java.sip.communicator.impl.neomedia.callrecordingconfig.DISABLED"; /** * The name of the notification pop-up event displayed when the device configration has changed. */ private static final String DEVICE_CONFIGURATION_HAS_CHANGED = "DeviceConfigurationChanged"; /** * The context in which the one and only <tt>NeomediaActivator</tt> instance has started * executing. */ private static BundleContext bundleContext; /** * The <tt>ConfigurationService</tt> registered in {@link #bundleContext} and used by the * <tt>NeomediaActivator</tt> instance to read and write configuration properties. */ private static ConfigurationService configurationService; /** * The <tt>FileAccessService</tt> registered in {@link #bundleContext} and used by the * <tt>NeomediaActivator</tt> instance to safely access files. */ private static FileAccessService fileAccessService; /** The notifcation service to pop-up messages. */ private static NotificationService notificationService; /** * The one and only <tt>MediaServiceImpl</tt> instance registered in {@link #bundleContext} by the * <tt>NeomediaActivator</tt> instance. */ private static MediaServiceImpl mediaServiceImpl; /** * The <tt>ResourceManagementService</tt> registered in {@link #bundleContext} and representing * the resources such as internationalized and localized text and images used by the neomedia * bundle. */ private static ResourceManagementService resources; /** * The OSGi <tt>PacketLoggingService</tt> of {@link #mediaServiceImpl} in {@link #bundleContext} * and used for debugging. */ private static PacketLoggingService packetLoggingService = null; /** A listener to the click on the popup message concerning device configuration changes. */ private AudioDeviceConfigurationListener deviceConfigurationPropertyChangeListener; /** A {@link MediaConfigurationService} instance. */ // private static MediaConfigurationImpl mediaConfiguration; /** The audio configuration form used to define the capture/notify/playback audio devices. */ private static ConfigurationForm audioConfigurationForm; /** * Starts the execution of the neomedia bundle in the specified context. * * @param bundleContext the context in which the neomedia bundle is to start executing * @throws Exception if an error occurs while starting the execution of the neomedia bundle in the * specified context */ public void start(BundleContext bundleContext) throws Exception { if (logger.isDebugEnabled()) logger.debug("Started."); NeomediaActivator.bundleContext = bundleContext; // MediaService mediaServiceImpl = (MediaServiceImpl) LibJitsi.getMediaService(); bundleContext.registerService(MediaService.class.getName(), mediaServiceImpl, null); if (logger.isDebugEnabled()) logger.debug("Media Service ... [REGISTERED]"); // mediaConfiguration = new MediaConfigurationImpl(); // bundleContext.registerService( // MediaConfigurationService.class.getStatus(), // getMediaConfiguration(), // null); if (logger.isDebugEnabled()) logger.debug("Media Configuration ... [REGISTERED]"); ConfigurationService cfg = NeomediaActivator.getConfigurationService(); Dictionary<String, String> mediaProps = new Hashtable<String, String>(); mediaProps.put(ConfigurationForm.FORM_TYPE, ConfigurationForm.GENERAL_TYPE); // If the audio configuration form is disabled don't register it. // if ((cfg == null) || !cfg.getBoolean(AUDIO_CONFIG_DISABLED_PROP, false)) // { // audioConfigurationForm // = new LazyConfigurationForm( // AudioConfigurationPanel.class.getStatus(), // getClass().getClassLoader(), // "plugin.mediaconfig.AUDIO_ICON", // "impl.neomedia.configform.AUDIO", // 3); // // bundleContext.registerService( // ConfigurationForm.class.getStatus(), // audioConfigurationForm, // mediaProps); // // if (deviceConfigurationPropertyChangeListener == null) // { // // Initializes and registers the changed device configuration // // event ot the notification service. // getNotificationService(); // // deviceConfigurationPropertyChangeListener // = new AudioDeviceConfigurationListener(); // mediaServiceImpl // .getDeviceConfiguration() // .addPropertyChangeListener( // deviceConfigurationPropertyChangeListener); // } // } // If the video configuration form is disabled don't register it. // if ((cfg == null) || !cfg.getBoolean(VIDEO_CONFIG_DISABLED_PROP, false)) // { // bundleContext.registerService( // ConfigurationForm.class.getStatus(), // new LazyConfigurationForm( // VideoConfigurationPanel.class.getStatus(), // getClass().getClassLoader(), // "plugin.mediaconfig.VIDEO_ICON", // "impl.neomedia.configform.VIDEO", // 4), // mediaProps); // } // H.264 // If the H.264 configuration form is disabled don't register it. // if ((cfg == null) || !cfg.getBoolean(H264_CONFIG_DISABLED_PROP, false)) // { // Dictionary<String, String> h264Props // = new Hashtable<String, String>(); // // h264Props.put( // ConfigurationForm.FORM_TYPE, // ConfigurationForm.ADVANCED_TYPE); // bundleContext.registerService( // ConfigurationForm.class.getStatus(), // new LazyConfigurationForm( // ConfigurationPanel.class.getStatus(), // getClass().getClassLoader(), // "plugin.mediaconfig.VIDEO_ICON", // "impl.neomedia.configform.H264", // -1, // true), // h264Props); // } // ZRTP // If the ZRTP configuration form is disabled don't register it. // if ((cfg == null) || !cfg.getBoolean(ZRTP_CONFIG_DISABLED_PROP, false)) // { // Dictionary<String, String> securityProps // = new Hashtable<String, String>(); // // securityProps.put( ConfigurationForm.FORM_TYPE, // ConfigurationForm.SECURITY_TYPE); // bundleContext.registerService( // ConfigurationForm.class.getStatus(), // new LazyConfigurationForm( // SecurityConfigForm.class.getStatus(), // getClass().getClassLoader(), // "impl.media.security.zrtp.CONF_ICON", // "impl.media.security.zrtp.TITLE", // 0), // securityProps); // } // we use the nist-sdp stack to make parse sdp and we need to set the // following property to make sure that it would accept java generated // IPv6 addresses that contain address scope zones. System.setProperty("gov.nist.core.STRIP_ADDR_SCOPES", "true"); // AudioNotifierService AudioNotifierService audioNotifierService = LibJitsi.getAudioNotifierService(); audioNotifierService.setMute( (cfg == null) || !cfg.getBoolean("net.java.sip.communicator" + ".impl.sound.isSoundEnabled", true)); bundleContext.registerService(AudioNotifierService.class.getName(), audioNotifierService, null); if (logger.isInfoEnabled()) logger.info("Audio Notifier Service ...[REGISTERED]"); // Call Recording // If the call recording configuration form is disabled don't continue. // if ((cfg == null) // || !cfg.getBoolean(CALL_RECORDING_CONFIG_DISABLED_PROP, false)) // { // Dictionary<String, String> callRecordingProps // = new Hashtable<String, String>(); // // callRecordingProps.put( // ConfigurationForm.FORM_TYPE, // ConfigurationForm.ADVANCED_TYPE); // bundleContext.registerService( // ConfigurationForm.class.getStatus(), // new LazyConfigurationForm( // CallRecordingConfigForm.class.getStatus(), // getClass().getClassLoader(), // null, // "plugin.callrecordingconfig.CALL_RECORDING_CONFIG", // 1100, // true), // callRecordingProps); // } } /** * Stops the execution of the neomedia bundle in the specified context. * * @param bundleContext the context in which the neomedia bundle is to stop executing * @throws Exception if an error occurs while stopping the execution of the neomedia bundle in the * specified context */ public void stop(BundleContext bundleContext) throws Exception { try { if (deviceConfigurationPropertyChangeListener != null) { mediaServiceImpl .getDeviceConfiguration() .removePropertyChangeListener(deviceConfigurationPropertyChangeListener); if (deviceConfigurationPropertyChangeListener != null) { deviceConfigurationPropertyChangeListener.managePopupMessageListenerRegistration(false); deviceConfigurationPropertyChangeListener = null; } } } finally { configurationService = null; fileAccessService = null; mediaServiceImpl = null; resources = null; } } /** * Returns a reference to a ConfigurationService implementation currently registered in the bundle * context or null if no such implementation was found. * * @return a currently valid implementation of the ConfigurationService. */ public static ConfigurationService getConfigurationService() { if (configurationService == null) { configurationService = ServiceUtils.getService(bundleContext, ConfigurationService.class); } return configurationService; } /** * Returns a reference to a FileAccessService implementation currently registered in the bundle * context or null if no such implementation was found. * * @return a currently valid implementation of the FileAccessService . */ public static FileAccessService getFileAccessService() { if (fileAccessService == null) { fileAccessService = ServiceUtils.getService(bundleContext, FileAccessService.class); } return fileAccessService; } /** * Gets the <tt>MediaService</tt> implementation instance registered by the neomedia bundle. * * @return the <tt>MediaService</tt> implementation instance registered by the neomedia bundle */ public static MediaServiceImpl getMediaServiceImpl() { return mediaServiceImpl; } // public static MediaConfigurationService getMediaConfiguration() // { // return mediaConfiguration; // } /** * Gets the <tt>ResourceManagementService</tt> instance which represents the resources such as * internationalized and localized text and images used by the neomedia bundle. * * @return the <tt>ResourceManagementService</tt> instance which represents the resources such as * internationalized and localized text and images used by the neomedia bundle */ public static ResourceManagementService getResources() { if (resources == null) { resources = ResourceManagementServiceUtils.getService(bundleContext); } return resources; } /** * Returns a reference to the <tt>PacketLoggingService</tt> implementation currently registered in * the bundle context or null if no such implementation was found. * * @return a reference to a <tt>PacketLoggingService</tt> implementation currently registered in * the bundle context or null if no such implementation was found. */ public static PacketLoggingService getPacketLogging() { if (packetLoggingService == null) { packetLoggingService = ServiceUtils.getService(bundleContext, PacketLoggingService.class); } return packetLoggingService; } /** * Returns the <tt>NotificationService</tt> obtained from the bundle context. * * @return The <tt>NotificationService</tt> obtained from the bundle context. */ public static NotificationService getNotificationService() { if (notificationService == null) { // Get the notification service implementation ServiceReference notifReference = bundleContext.getServiceReference(NotificationService.class.getName()); notificationService = (NotificationService) bundleContext.getService(notifReference); if (notificationService != null) { // Register a popup message for a device configuration changed // notification. notificationService.registerDefaultNotificationForEvent( DEVICE_CONFIGURATION_HAS_CHANGED, net.java.sip.communicator.service.notification.NotificationAction.ACTION_POPUP_MESSAGE, "Device onfiguration has changed", null); } } return notificationService; } /** A listener to the click on the popup message concerning device configuration changes. */ private class AudioDeviceConfigurationListener implements PropertyChangeListener /*, SystrayPopupMessageListener*/ { /** * A boolean used to verify that this listener registers only once to the popup message * notification handler. */ private boolean isRegisteredToPopupMessageListener = false; /** * Registers or unregister as a popup message listener to detect when a user click on * notification saying that the device configuration has changed. * * @param enable True to register to the popup message notifcation handler. False to unregister. */ public void managePopupMessageListenerRegistration(boolean enable) { Iterator<NotificationHandler> notificationHandlers = notificationService .getActionHandlers( net.java.sip.communicator.service.notification.NotificationAction .ACTION_POPUP_MESSAGE) .iterator(); NotificationHandler notificationHandler; while (notificationHandlers.hasNext()) { notificationHandler = notificationHandlers.next(); if (notificationHandler instanceof PopupMessageNotificationHandler) { // Register. if (enable) { // ((PopupMessageNotificationHandler) notificationHandler) // .addPopupMessageListener(this); } // Unregister. else { // ((PopupMessageNotificationHandler) notificationHandler) // .removePopupMessageListener(this); } } } } /** * Function called when an audio device is plugged or unplugged. * * @param event The property change event which may concern the audio device. */ public void propertyChange(PropertyChangeEvent event) { if (DeviceConfiguration.PROP_AUDIO_SYSTEM_DEVICES.equals(event.getPropertyName())) { NotificationService notificationService = getNotificationService(); if (notificationService != null) { // Registers only once to the popup message notification // handler. if (!isRegisteredToPopupMessageListener) { isRegisteredToPopupMessageListener = true; managePopupMessageListenerRegistration(true); } // Fires the popup notification. ResourceManagementService resources = NeomediaActivator.getResources(); Map<String, Object> extras = new HashMap<String, Object>(); extras.put(NotificationData.POPUP_MESSAGE_HANDLER_TAG_EXTRA, this); notificationService.fireNotification( DEVICE_CONFIGURATION_HAS_CHANGED, resources.getI18NString("impl.media.configform" + ".AUDIO_DEVICE_CONFIG_CHANGED"), resources.getI18NString( "impl.media.configform" + ".AUDIO_DEVICE_CONFIG_MANAGMENT_CLICK"), null, extras); } } } /** * Indicates that user has clicked on the systray popup message. * * @param evt the event triggered when user clicks on the systray popup message */ // public void popupMessageClicked(SystrayPopupMessageEvent evt) // { // // Checks if this event is fired from one click on one of our popup // // message. // if(evt.getTag() == deviceConfigurationPropertyChangeListener) // { // // Get the UI service // ServiceReference uiReference = bundleContext // .getServiceReference(UIService.class.getStatus()); // // UIService uiService = (UIService) bundleContext // .getService(uiReference); // // if(uiService != null) // { // // Shows the audio configuration window. // ConfigurationContainer configurationContainer // = uiService.getConfigurationContainer(); // configurationContainer.setSelected(audioConfigurationForm); // configurationContainer.setVisible(true); // } // } // } } public static BundleContext getBundleContext() { return bundleContext; } }
/** * Keeps track of entity capabilities. * * <p>This work is based on Jonas Adahl's smack fork. * * @author Emil Ivov * @author Lyubomir Marinov */ public class EntityCapsManager { /** * The <tt>Logger</tt> used by the <tt>EntityCapsManager</tt> class and its instances for logging * output. */ private static final Logger logger = Logger.getLogger(EntityCapsManager.class); /** Static OSGi bundle context used by this class. */ private static BundleContext bundleContext; /** Configuration service instance used by this class. */ private static ConfigurationService configService; /** * The prefix of the <tt>ConfigurationService</tt> properties which persist {@link * #caps2discoverInfo}. */ private static final String CAPS_PROPERTY_NAME_PREFIX = "net.java.sip.communicator.impl.protocol.jabber.extensions.caps." + "EntityCapsManager.CAPS."; /** * An empty array of <tt>UserCapsNodeListener</tt> elements explicitly defined in order to reduce * unnecessary allocations. */ private static final UserCapsNodeListener[] NO_USER_CAPS_NODE_LISTENERS = new UserCapsNodeListener[0]; /** The node value to advertise. */ private static String entityNode = OSUtils.IS_ANDROID ? "http://android.jitsi.org" : "http://jitsi.org"; /** * The <tt>Map</tt> of <tt>Caps</tt> to <tt>DiscoverInfo</tt> which associates a node#ver with the * entity capabilities so that they don't have to be retrieved every time their necessary. Because * ver is constructed from the entity capabilities using a specific hash method, the hash method * is also associated with the entity capabilities along with the node and the ver in order to * disambiguate cases of equal ver values for different entity capabilities constructed using * different hash methods. */ private static final Map<Caps, DiscoverInfo> caps2discoverInfo = new ConcurrentHashMap<Caps, DiscoverInfo>(); /** * Map of Full JID -> DiscoverInfo/null. In case of c2s connection the key is formed as * user@server/resource (resource is required) In case of link-local connection the key is formed * as user@host (no resource) */ private final Map<String, Caps> userCaps = new ConcurrentHashMap<String, Caps>(); /** CapsVerListeners gets notified when the version string is changed. */ private final Set<CapsVerListener> capsVerListeners = new CopyOnWriteArraySet<CapsVerListener>(); /** The current hash of our version and supported features. */ private String currentCapsVersion = null; /** * The list of <tt>UserCapsNodeListener</tt>s interested in events notifying about changes in the * list of user caps nodes of this <tt>EntityCapsManager</tt>. */ private final List<UserCapsNodeListener> userCapsNodeListeners = new LinkedList<UserCapsNodeListener>(); static { ProviderManager.getInstance() .addExtensionProvider( CapsPacketExtension.ELEMENT_NAME, CapsPacketExtension.NAMESPACE, new CapsProvider()); } /** * Add {@link DiscoverInfo} to our caps database. * * <p><b>Warning</b>: The specified <tt>DiscoverInfo</tt> is trusted to be valid with respect to * the specified <tt>Caps</tt> for performance reasons because the <tt>DiscoverInfo</tt> should * have already been validated in order to be used elsewhere anyway. * * @param caps the <tt>Caps<tt/> i.e. the node, the hash and the ver for which a * <tt>DiscoverInfo</tt> is to be added to our caps database. * @param info {@link DiscoverInfo} for the specified <tt>Caps</tt>. */ public static void addDiscoverInfoByCaps(Caps caps, DiscoverInfo info) { cleanupDiscoverInfo(info); /* * DiscoverInfo carries the node we're now associating it with a * specific node so we'd better keep them in sync. */ info.setNode(caps.getNodeVer()); synchronized (caps2discoverInfo) { DiscoverInfo oldInfo = caps2discoverInfo.put(caps, info); /* * If the specified info is a new association for the specified * node, remember it across application instances in order to not * query for it over the network. */ if ((oldInfo == null) || !oldInfo.equals(info)) { String xml = info.getChildElementXML(); if ((xml != null) && (xml.length() != 0)) { getConfigService().setProperty(getCapsPropertyName(caps), xml); } } } } /** * Gets the name of the property in the <tt>ConfigurationService</tt> which is or is to be * associated with a specific <tt>Caps</tt> value. * * @param caps the <tt>Caps</tt> value for which the associated <tt>ConfigurationService</tt> * property name is to be returned * @return the name of the property in the <tt>ConfigurationService</tt> which is or is to be * associated with a specific <tt>Caps</tt> value */ private static String getCapsPropertyName(Caps caps) { return CAPS_PROPERTY_NAME_PREFIX + caps.node + '#' + caps.hash + '#' + caps.ver; } /** Returns cached instance of {@link ConfigurationService}. */ private static ConfigurationService getConfigService() { if (configService == null) { configService = ServiceUtils.getService(bundleContext, ConfigurationService.class); } return configService; } /** * Sets OSGi bundle context instance that will be used by this class. * * @param bundleContext the <tt>BundleContext</tt> instance to be used by this class or * <tt>null</tt> to clear the reference. */ public static void setBundleContext(BundleContext bundleContext) { if (bundleContext == null) { configService = null; } EntityCapsManager.bundleContext = bundleContext; } /** * Add a record telling what entity caps node a user has. * * @param user the user (Full JID) * @param node the node (of the caps packet extension) * @param hash the hashing algorithm used to calculate <tt>ver</tt> * @param ver the version (of the caps packet extension) * @param ext the ext (of the caps packet extension) * @param online indicates if the user is online */ private void addUserCapsNode( String user, String node, String hash, String ver, String ext, boolean online) { if ((user != null) && (node != null) && (hash != null) && (ver != null)) { Caps caps = userCaps.get(user); if ((caps == null) || !caps.node.equals(node) || !caps.hash.equals(hash) || !caps.ver.equals(ver)) { caps = new Caps(node, hash, ver, ext); userCaps.put(user, caps); } else return; // Fire userCapsNodeAdded. UserCapsNodeListener[] listeners; synchronized (userCapsNodeListeners) { listeners = userCapsNodeListeners.toArray(NO_USER_CAPS_NODE_LISTENERS); } if (listeners.length != 0) { String nodeVer = caps.getNodeVer(); for (UserCapsNodeListener listener : listeners) listener.userCapsNodeAdded(user, nodeVer, online); } } } /** * Adds a specific <tt>UserCapsNodeListener</tt> to the list of <tt>UserCapsNodeListener</tt>s * interested in events notifying about changes in the list of user caps nodes of this * <tt>EntityCapsManager</tt>. * * @param listener the <tt>UserCapsNodeListener</tt> which is interested in events notifying about * changes in the list of user caps nodes of this <tt>EntityCapsManager</tt> */ public void addUserCapsNodeListener(UserCapsNodeListener listener) { if (listener == null) throw new NullPointerException("listener"); synchronized (userCapsNodeListeners) { if (!userCapsNodeListeners.contains(listener)) userCapsNodeListeners.add(listener); } } /** * Remove records telling what entity caps node a contact has. * * @param contact the contact */ public void removeContactCapsNode(Contact contact) { Caps caps = null; String lastRemovedJid = null; Iterator<String> iter = userCaps.keySet().iterator(); while (iter.hasNext()) { String jid = iter.next(); if (StringUtils.parseBareAddress(jid).equals(contact.getAddress())) { caps = userCaps.get(jid); lastRemovedJid = jid; iter.remove(); } } // fire only for the last one, at the end the event out // of the protocol will be one and for the contact if (caps != null) { UserCapsNodeListener[] listeners; synchronized (userCapsNodeListeners) { listeners = userCapsNodeListeners.toArray(NO_USER_CAPS_NODE_LISTENERS); } if (listeners.length != 0) { String nodeVer = caps.getNodeVer(); for (UserCapsNodeListener listener : listeners) listener.userCapsNodeRemoved(lastRemovedJid, nodeVer, false); } } } /** * Remove a record telling what entity caps node a user has. * * @param user the user (Full JID) */ public void removeUserCapsNode(String user) { Caps caps = userCaps.remove(user); // Fire userCapsNodeRemoved. if (caps != null) { UserCapsNodeListener[] listeners; synchronized (userCapsNodeListeners) { listeners = userCapsNodeListeners.toArray(NO_USER_CAPS_NODE_LISTENERS); } if (listeners.length != 0) { String nodeVer = caps.getNodeVer(); for (UserCapsNodeListener listener : listeners) listener.userCapsNodeRemoved(user, nodeVer, false); } } } /** * Removes a specific <tt>UserCapsNodeListener</tt> from the list of * <tt>UserCapsNodeListener</tt>s interested in events notifying about changes in the list of user * caps nodes of this <tt>EntityCapsManager</tt>. * * @param listener the <tt>UserCapsNodeListener</tt> which is no longer interested in events * notifying about changes in the list of user caps nodes of this <tt>EntityCapsManager</tt> */ public void removeUserCapsNodeListener(UserCapsNodeListener listener) { if (listener != null) { synchronized (userCapsNodeListeners) { userCapsNodeListeners.remove(listener); } } } /** * Gets the <tt>Caps</tt> i.e. the node, the hash and the ver of a user. * * @param user the user (Full JID) * @return the <tt>Caps</tt> i.e. the node, the hash and the ver of <tt>user</tt> */ public Caps getCapsByUser(String user) { return userCaps.get(user); } /** * Get the discover info given a user name. The discover info is returned if the user has a * node#ver associated with it and the node#ver has a discover info associated with it. * * @param user user name (Full JID) * @return the discovered info */ public DiscoverInfo getDiscoverInfoByUser(String user) { Caps caps = userCaps.get(user); return (caps == null) ? null : getDiscoverInfoByCaps(caps); } /** * Get our own caps version. * * @return our own caps version */ public String getCapsVersion() { return currentCapsVersion; } /** * Get our own entity node. * * @return our own entity node. */ public String getNode() { return entityNode; } /** * Set our own entity node. * * @param node the new node */ public void setNode(String node) { entityNode = node; } /** * Retrieve DiscoverInfo for a specific node. * * @param caps the <tt>Caps</tt> i.e. the node, the hash and the ver * @return The corresponding DiscoverInfo or null if none is known. */ public static DiscoverInfo getDiscoverInfoByCaps(Caps caps) { synchronized (caps2discoverInfo) { DiscoverInfo discoverInfo = caps2discoverInfo.get(caps); /* * If we don't have the discoverInfo in the runtime cache yet, we * may have it remembered in a previous application instance. */ if (discoverInfo == null) { ConfigurationService configurationService = getConfigService(); String capsPropertyName = getCapsPropertyName(caps); String xml = configurationService.getString(capsPropertyName); if ((xml != null) && (xml.length() != 0)) { IQProvider discoverInfoProvider = (IQProvider) ProviderManager.getInstance() .getIQProvider("query", "http://jabber.org/protocol/disco#info"); if (discoverInfoProvider != null) { XmlPullParser parser = new MXParser(); try { parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); parser.setInput(new StringReader(xml)); // Start the parser. parser.next(); } catch (XmlPullParserException xppex) { parser = null; } catch (IOException ioex) { parser = null; } if (parser != null) { try { discoverInfo = (DiscoverInfo) discoverInfoProvider.parseIQ(parser); } catch (Exception ex) { } if (discoverInfo != null) { if (caps.isValid(discoverInfo)) caps2discoverInfo.put(caps, discoverInfo); else { logger.error( "Invalid DiscoverInfo for " + caps.getNodeVer() + ": " + discoverInfo); /* * The discoverInfo doesn't seem valid * according to the caps which means that we * must have stored invalid information. * Delete the invalid information in order * to not try to validate it again. */ configurationService.removeProperty(capsPropertyName); } } } } } } return discoverInfo; } } /** * Removes from, to and packet-id from <tt>info</tt>. * * @param info the {@link DiscoverInfo} that we'd like to cleanup. */ private static void cleanupDiscoverInfo(DiscoverInfo info) { info.setFrom(null); info.setTo(null); info.setPacketID(null); } /** * Gets the features of a specific <tt>DiscoverInfo</tt> in the form of a read-only * <tt>Feature</tt> <tt>Iterator<tt/> by calling the internal method {@link * DiscoverInfo#getFeatures()}. * * @param discoverInfo the <tt>DiscoverInfo</tt> the features of which are to be retrieved * @return a read-only <tt>Feature</tt> <tt>Iterator</tt> which lists the features of the * specified <tt>discoverInfo</tt> */ @SuppressWarnings("unchecked") private static Iterator<DiscoverInfo.Feature> getDiscoverInfoFeatures(DiscoverInfo discoverInfo) { Method getFeaturesMethod; try { getFeaturesMethod = DiscoverInfo.class.getDeclaredMethod("getFeatures"); } catch (NoSuchMethodException nsmex) { throw new UndeclaredThrowableException(nsmex); } getFeaturesMethod.setAccessible(true); try { return (Iterator<DiscoverInfo.Feature>) getFeaturesMethod.invoke(discoverInfo); } catch (IllegalAccessException iaex) { throw new UndeclaredThrowableException(iaex); } catch (InvocationTargetException itex) { throw new UndeclaredThrowableException(itex); } } /** * Registers this Manager's listener with <tt>connection</tt>. * * @param connection the connection that we'd like this manager to register with. */ public void addPacketListener(XMPPConnection connection) { PacketFilter filter = new AndFilter( new PacketTypeFilter(Presence.class), new PacketExtensionFilter( CapsPacketExtension.ELEMENT_NAME, CapsPacketExtension.NAMESPACE)); connection.addPacketListener(new CapsPacketListener(), filter); } /** * Adds <tt>listener</tt> to the list of {@link CapsVerListener}s that we notify when new features * occur and the version hash needs to be regenerated. The method would also notify * <tt>listener</tt> if our current caps version has been generated and is different than * <tt>null</tt>. * * @param listener the {@link CapsVerListener} we'd like to register. */ public void addCapsVerListener(CapsVerListener listener) { synchronized (capsVerListeners) { if (capsVerListeners.contains(listener)) return; capsVerListeners.add(listener); if (currentCapsVersion != null) listener.capsVerUpdated(currentCapsVersion); } } /** * Removes <tt>listener</tt> from the list of currently registered {@link CapsVerListener}s. * * @param listener the {@link CapsVerListener} we'd like to unregister. */ public void removeCapsVerListener(CapsVerListener listener) { synchronized (capsVerListeners) { capsVerListeners.remove(listener); } } /** * Notifies all currently registered {@link CapsVerListener}s that the version hash has changed. */ private void fireCapsVerChanged() { List<CapsVerListener> listenersCopy = null; synchronized (capsVerListeners) { listenersCopy = new ArrayList<CapsVerListener>(capsVerListeners); } for (CapsVerListener listener : listenersCopy) listener.capsVerUpdated(currentCapsVersion); } /** * Computes and returns the hash of the specified <tt>capsString</tt> using the specified * <tt>hashAlgorithm</tt>. * * @param hashAlgorithm the name of the algorithm to be used to generate the hash * @param capsString the capabilities string that we'd like to compute a hash for. * @return the hash of <tt>capsString</tt> computed by the specified <tt>hashAlgorithm</tt> or * <tt>null</tt> if generating the hash has failed */ private static String capsToHash(String hashAlgorithm, String capsString) { try { MessageDigest md = MessageDigest.getInstance(hashAlgorithm); byte[] digest = md.digest(capsString.getBytes()); return Base64.encodeBytes(digest); } catch (NoSuchAlgorithmException nsae) { logger.error("Unsupported XEP-0115: Entity Capabilities hash algorithm: " + hashAlgorithm); return null; } } /** * Converts the form field values in the <tt>ffValuesIter</tt> into a caps string. * * @param ffValuesIter the {@link Iterator} containing the form field values. * @param capsBldr a <tt>StringBuilder</tt> to which the caps string representing the form field * values is to be appended */ private static void formFieldValuesToCaps(Iterator<String> ffValuesIter, StringBuilder capsBldr) { SortedSet<String> fvs = new TreeSet<String>(); while (ffValuesIter.hasNext()) fvs.add(ffValuesIter.next()); for (String fv : fvs) capsBldr.append(fv).append('<'); } /** * Calculates the <tt>String</tt> for a specific <tt>DiscoverInfo</tt> which is to be hashed in * order to compute the ver string for that <tt>DiscoverInfo</tt>. * * @param discoverInfo the <tt>DiscoverInfo</tt> for which the <tt>String</tt> to be hashed in * order to compute its ver string is to be calculated * @return the <tt>String</tt> for <tt>discoverInfo</tt> which is to be hashed in order to compute * its ver string */ private static String calculateEntityCapsString(DiscoverInfo discoverInfo) { StringBuilder bldr = new StringBuilder(); // Add identities { Iterator<DiscoverInfo.Identity> identities = discoverInfo.getIdentities(); SortedSet<DiscoverInfo.Identity> is = new TreeSet<DiscoverInfo.Identity>( new Comparator<DiscoverInfo.Identity>() { public int compare(DiscoverInfo.Identity i1, DiscoverInfo.Identity i2) { int category = i1.getCategory().compareTo(i2.getCategory()); if (category != 0) return category; int type = i1.getType().compareTo(i2.getType()); if (type != 0) return type; /* * TODO Sort by xml:lang. * * Since sort by xml:lang is currently missing, * use the last supported sort criterion i.e. * type. */ return type; } }); if (identities != null) while (identities.hasNext()) is.add(identities.next()); for (DiscoverInfo.Identity i : is) { bldr.append(i.getCategory()) .append('/') .append(i.getType()) .append("//") .append(i.getName()) .append('<'); } } // Add features { Iterator<DiscoverInfo.Feature> features = getDiscoverInfoFeatures(discoverInfo); SortedSet<String> fs = new TreeSet<String>(); if (features != null) while (features.hasNext()) fs.add(features.next().getVar()); for (String f : fs) bldr.append(f).append('<'); } DataForm extendedInfo = (DataForm) discoverInfo.getExtension("x", "jabber:x:data"); if (extendedInfo != null) { synchronized (extendedInfo) { SortedSet<FormField> fs = new TreeSet<FormField>( new Comparator<FormField>() { public int compare(FormField f1, FormField f2) { return f1.getVariable().compareTo(f2.getVariable()); } }); FormField formType = null; for (Iterator<FormField> fieldsIter = extendedInfo.getFields(); fieldsIter.hasNext(); ) { FormField f = fieldsIter.next(); if (!f.getVariable().equals("FORM_TYPE")) fs.add(f); else formType = f; } // Add FORM_TYPE values if (formType != null) formFieldValuesToCaps(formType.getValues(), bldr); // Add the other values for (FormField f : fs) { bldr.append(f.getVariable()).append('<'); formFieldValuesToCaps(f.getValues(), bldr); } } } return bldr.toString(); } /** * Calculates the ver string for the specified <tt>discoverInfo</tt>, identity type, name * features, and extendedInfo. * * @param discoverInfo the {@link DiscoverInfo} we'd be creating a ver <tt>String</tt> for */ public void calculateEntityCapsVersion(DiscoverInfo discoverInfo) { setCurrentCapsVersion( discoverInfo, capsToHash(CapsPacketExtension.HASH_METHOD, calculateEntityCapsString(discoverInfo))); } /** * Set our own caps version. * * @param discoverInfo the {@link DiscoverInfo} that we'd like to map to the <tt>capsVersion</tt>. * @param capsVersion the new caps version */ public void setCurrentCapsVersion(DiscoverInfo discoverInfo, String capsVersion) { Caps caps = new Caps(getNode(), CapsPacketExtension.HASH_METHOD, capsVersion, null); /* * DiscoverInfo carries the node and the ver and we're now setting a new * ver so we should update the DiscoveryInfo. */ discoverInfo.setNode(caps.getNodeVer()); if (!caps.isValid(discoverInfo)) { throw new IllegalArgumentException( "The specified discoverInfo must be valid with respect" + " to the specified capsVersion"); } currentCapsVersion = capsVersion; addDiscoverInfoByCaps(caps, discoverInfo); fireCapsVerChanged(); } /** The {@link PacketListener} that will be registering incoming caps. */ private class CapsPacketListener implements PacketListener { /** * Handles incoming presence packets and maps jids to node#ver strings. * * @param packet the incoming presence <tt>Packet</tt> to be handled * @see PacketListener#processPacket(Packet) */ public void processPacket(Packet packet) { CapsPacketExtension ext = (CapsPacketExtension) packet.getExtension(CapsPacketExtension.ELEMENT_NAME, CapsPacketExtension.NAMESPACE); /* * Before Version 1.4 of XEP-0115: Entity Capabilities, the 'ver' * attribute was generated differently and the 'hash' attribute was * absent. The 'ver' attribute in Version 1.3 represents the * specific version of the client and thus does not provide a way to * validate the DiscoverInfo sent by the client. If * EntityCapsManager receives no 'hash' attribute, it will assume * the legacy format and will not cache it because the DiscoverInfo * to be received from the client later on will not be trustworthy. */ String hash = ext.getHash(); /* Google Talk web does not set hash but we need it to be cached */ if (hash == null) hash = ""; if (hash != null) { // Check it the packet indicates that the user is online. We // will use this information to decide if we're going to send // the discover info request. boolean online = (packet instanceof Presence) && ((Presence) packet).isAvailable(); if (online) { addUserCapsNode( packet.getFrom(), ext.getNode(), hash, ext.getVersion(), ext.getExtensions(), online); } else { removeUserCapsNode(packet.getFrom()); } } } } /** * Implements an immutable value which stands for a specific node, a specific hash (algorithm) and * a specific ver. * * @author Lyubomir Marinov */ public static class Caps { /** The hash (algorithm) of this <tt>Caps</tt> value. */ public final String hash; /** The node of this <tt>Caps</tt> value. */ public final String node; /** The ext info of this <tt>Caps</tt> value. */ public String ext; /** * The String which is the concatenation of {@link #node} and the {@link #ver} separated by the * character '#'. Cached for the sake of efficiency. */ private final String nodeVer; /** The ver of this <tt>Caps</tt> value. */ public final String ver; /** * Initializes a new <tt>Caps</tt> instance which is to represent a specific node, a specific * hash (algorithm) and a specific ver. * * @param node the node to be represented by the new instance * @param hash the hash (algorithm) to be represented by the new instance * @param ver the ver to be represented by the new instance * @param ext the ext to be represented by the new instance */ public Caps(String node, String hash, String ver, String ext) { if (node == null) throw new NullPointerException("node"); if (hash == null) throw new NullPointerException("hash"); if (ver == null) throw new NullPointerException("ver"); this.node = node; this.hash = hash; this.ver = ver; this.ext = ext; this.nodeVer = this.node + '#' + this.ver; } /** * Gets a <tt>String</tt> which represents the concatenation of the <tt>node</tt> property of * this instance, the character '#' and the <tt>ver</tt> property of this instance. * * @return a <tt>String</tt> which represents the concatenation of the <tt>node</tt> property of * this instance, the character '#' and the <tt>ver</tt> property of this instance */ public final String getNodeVer() { return nodeVer; } /** * Determines whether a specific <tt>DiscoverInfo</tt> is valid according to this <tt>Caps</tt> * i.e. whether the <tt>discoverInfo</tt> has the node and the ver of this <tt>Caps</tt> and the * ver calculated from the <tt>discoverInfo</tt> using the hash (algorithm) of this * <tt>Caps</tt> is equal to the ver of this <tt>Caps</tt>. * * @param discoverInfo the <tt>DiscoverInfo</tt> to be validated by this <tt>Caps</tt> * @return <tt>true</tt> if the specified <tt>DiscoverInfo</tt> has the node and the ver of this * <tt>Caps</tt> and the ver calculated from the <tt>discoverInfo</tt> using the hash * (algorithm) of this <tt>Caps</tt> is equal to the ver of this <tt>Caps</tt>; otherwise, * <tt>false</tt> */ public boolean isValid(DiscoverInfo discoverInfo) { if (discoverInfo != null) { // The "node" attribute is not necessary in the query element. // For example, Swift does not send back the "node" attribute in // the Disco#info response. Thus, if the node of the IQ response // is null, then we set it to the request one. if (discoverInfo.getNode() == null) { discoverInfo.setNode(getNodeVer()); } if (getNodeVer().equals(discoverInfo.getNode()) && !hash.equals("") && ver.equals(capsToHash(hash, calculateEntityCapsString(discoverInfo)))) { return true; } } return false; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Caps caps = (Caps) o; if (!hash.equals(caps.hash)) return false; if (!node.equals(caps.node)) return false; if (!ver.equals(caps.ver)) return false; return true; } @Override public int hashCode() { int result = hash.hashCode(); result = 31 * result + node.hashCode(); result = 31 * result + ver.hashCode(); return result; } } }