diff options
author | Wolfgang Wiedmeyer <wolfgit@wiedmeyer.de> | 2017-03-11 22:15:03 +0100 |
---|---|---|
committer | Wolfgang Wiedmeyer <wolfgit@wiedmeyer.de> | 2017-03-11 22:15:03 +0100 |
commit | 85901329b0794b136b96bf745f4ab1572806fc89 (patch) | |
tree | f23da7e97cae727f39d825f0fef8348cffb238e4 /src/net/java/sip/communicator/service/protocol | |
parent | 3db2e44f186c59429901b2c899e139ea60117a55 (diff) | |
parent | cf5da997da8820b4050f5b87ee9440a0ede36d1f (diff) | |
download | jitsi-85901329b0794b136b96bf745f4ab1572806fc89.zip jitsi-85901329b0794b136b96bf745f4ab1572806fc89.tar.gz jitsi-85901329b0794b136b96bf745f4ab1572806fc89.tar.bz2 |
Signed-off-by: Wolfgang Wiedmeyer <wolfgit@wiedmeyer.de>
Diffstat (limited to 'src/net/java/sip/communicator/service/protocol')
35 files changed, 7125 insertions, 7001 deletions
diff --git a/src/net/java/sip/communicator/service/protocol/AbstractOperationSetBasicInstantMessaging.java b/src/net/java/sip/communicator/service/protocol/AbstractOperationSetBasicInstantMessaging.java index 4270a78..6f038b5 100644 --- a/src/net/java/sip/communicator/service/protocol/AbstractOperationSetBasicInstantMessaging.java +++ b/src/net/java/sip/communicator/service/protocol/AbstractOperationSetBasicInstantMessaging.java @@ -137,14 +137,6 @@ public abstract class AbstractOperationSetBasicInstantMessaging return createMessage(messageText); } - /** - * {@inheritDoc} - */ - public Message createMessageWithUID(String messageText, String messageUID) - { - return createMessage(messageText); - } - public abstract Message createMessage( String content, String contentType, String encoding, String subject); diff --git a/src/net/java/sip/communicator/service/protocol/AbstractProtocolProviderService.java b/src/net/java/sip/communicator/service/protocol/AbstractProtocolProviderService.java index 9a78eec..897b74b 100644 --- a/src/net/java/sip/communicator/service/protocol/AbstractProtocolProviderService.java +++ b/src/net/java/sip/communicator/service/protocol/AbstractProtocolProviderService.java @@ -245,6 +245,19 @@ public abstract class AbstractProtocolProviderService } /** + * Default implementation that always returns true. + * + * @param contactId ignored. + * @param result ignored + * @return true + */ + @Override + public boolean validateContactAddress(String contactId, List<String> result) + { + return true; + } + + /** * Returns an array containing all operation sets supported by the current * implementation. When querying this method users must be prepared to * receive any subset of the OperationSet-s defined by this service. They diff --git a/src/net/java/sip/communicator/service/protocol/AccountManager.java b/src/net/java/sip/communicator/service/protocol/AccountManager.java index c608092..3ce85dc 100644 --- a/src/net/java/sip/communicator/service/protocol/AccountManager.java +++ b/src/net/java/sip/communicator/service/protocol/AccountManager.java @@ -1,1083 +1,1083 @@ -/*
- * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
- *
- * Copyright @ 2015 Atlassian Pty Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package net.java.sip.communicator.service.protocol;
-
-import java.util.*;
-
-import net.java.sip.communicator.service.credentialsstorage.*;
-import net.java.sip.communicator.service.protocol.event.*;
-import net.java.sip.communicator.util.*;
-import net.java.sip.communicator.util.Base64; //disambiguate from java.util.Base64
-
-import org.jitsi.service.configuration.*;
-import org.osgi.framework.*;
-
-/**
- * Represents an implementation of <tt>AccountManager</tt> which loads the
- * accounts in a separate thread.
- *
- * @author Lyubomir Marinov
- * @author Yana Stamcheva
- */
-public class AccountManager
-{
- /**
- * The delay in milliseconds the background <tt>Thread</tt> loading the
- * stored accounts should wait before dying so that it doesn't get recreated
- * for each <tt>ProtocolProviderFactory</tt> registration.
- */
- private static final long LOAD_STORED_ACCOUNTS_TIMEOUT = 30000;
-
- /**
- * The <tt>BundleContext</tt> this service is registered in.
- */
- private final BundleContext bundleContext;
-
- /**
- * The <tt>AccountManagerListener</tt>s currently interested in the
- * events fired by this manager.
- */
- private final List<AccountManagerListener> listeners =
- new LinkedList<AccountManagerListener>();
-
- /**
- * The queue of <tt>ProtocolProviderFactory</tt> services awaiting their
- * stored accounts to be loaded.
- */
- private final Queue<ProtocolProviderFactory> loadStoredAccountsQueue =
- new LinkedList<ProtocolProviderFactory>();
-
- /**
- * The <tt>Thread</tt> loading the stored accounts of the
- * <tt>ProtocolProviderFactory</tt> services waiting in
- * {@link #loadStoredAccountsQueue}.
- */
- private Thread loadStoredAccountsThread;
-
- /**
- * The <tt>Logger</tt> used by this <tt>AccountManagerImpl</tt> instance for
- * logging output.
- */
- private final Logger logger = Logger.getLogger(AccountManager.class);
-
- /**
- * The list of <tt>AccountID</tt>s, corresponding to all stored accounts.
- */
- private final Vector<AccountID> storedAccounts = new Vector<AccountID>();
-
- /**
- * The prefix of the account unique identifier.
- */
- private static final String ACCOUNT_UID_PREFIX = "acc";
-
- /**
- * Initializes a new <tt>AccountManagerImpl</tt> instance loaded in a
- * specific <tt>BundleContext</tt> (in which the caller will usually
- * later register it).
- *
- * @param bundleContext the <tt>BundleContext</tt> in which the new
- * instance is loaded (and in which the caller will usually later
- * register it as a service)
- */
- public AccountManager(BundleContext bundleContext)
- {
- this.bundleContext = bundleContext;
-
- this.bundleContext.addServiceListener(new ServiceListener()
- {
- public void serviceChanged(ServiceEvent serviceEvent)
- {
- AccountManager.this.serviceChanged(serviceEvent);
- }
- });
- }
-
- /**
- * Implements AccountManager#addListener(AccountManagerListener).
- * @param listener the <tt>AccountManagerListener</tt> to add
- */
- public void addListener(AccountManagerListener listener)
- {
- synchronized (listeners)
- {
- if (!listeners.contains(listener))
- listeners.add(listener);
- }
- }
-
- /**
- * Loads the accounts stored for a specific
- * <tt>ProtocolProviderFactory</tt>.
- *
- * @param factory the <tt>ProtocolProviderFactory</tt> to load the
- * stored accounts of
- */
- private void doLoadStoredAccounts(ProtocolProviderFactory factory)
- {
- ConfigurationService configService
- = ProtocolProviderActivator.getConfigurationService();
- String factoryPackage = getFactoryImplPackageName(factory);
- List<String> accounts
- = configService.getPropertyNamesByPrefix(factoryPackage, true);
-
- if (logger.isDebugEnabled())
- logger.debug("Discovered " + accounts.size() + " stored "
- + factoryPackage + " accounts");
-
- for (Iterator<String> storedAccountIter = accounts.iterator();
- storedAccountIter.hasNext();)
- {
- String storedAccount = storedAccountIter.next();
-
- // If the property is not related to an account we skip it.
- int dotIndex = storedAccount.lastIndexOf(".");
- if (!storedAccount.substring(dotIndex + 1)
- .startsWith(ACCOUNT_UID_PREFIX))
- continue;
-
- if (logger.isDebugEnabled())
- logger.debug("Loading account " + storedAccount);
-
- List<String> storedAccountProperties =
- configService.getPropertyNamesByPrefix(storedAccount, false);
- Map<String, String> accountProperties =
- new Hashtable<String, String>();
- boolean disabled = false;
- CredentialsStorageService credentialsStorage
- = ServiceUtils.getService(
- bundleContext,
- CredentialsStorageService.class);
-
- int prefLen = storedAccount.length() + 1;
- for (Iterator<String> storedAccountPropertyIter
- = storedAccountProperties.iterator();
- storedAccountPropertyIter.hasNext();)
- {
- String property = storedAccountPropertyIter.next();
- String value = configService.getString(property);
-
- //strip the package prefix
- if(prefLen > property.length())
- continue;
-
- property = property.substring(prefLen);
-
- if (ProtocolProviderFactory.IS_ACCOUNT_DISABLED.equals(property))
- disabled = Boolean.parseBoolean(value);
- // Decode passwords.
- else if (ProtocolProviderFactory.PASSWORD.equals(property)
- && !credentialsStorage.isStoredEncrypted(storedAccount))
- {
- if ((value != null) && value.length() != 0)
- {
-
- /*
- * TODO Converting byte[] to String using the platform's
- * default charset may result in an invalid password.
- */
- value = new String(Base64.decode(value));
- }
- }
-
- if (value != null)
- accountProperties.put(property, value);
- }
-
- try
- {
- AccountID accountID = factory.createAccount(accountProperties);
-
- // If for some reason the account id is not created we move to
- // the next account.
- if (accountID == null)
- continue;
-
- synchronized (storedAccounts)
- {
- storedAccounts.add(accountID);
- }
- if (!disabled)
- factory.loadAccount(accountID);
- }
- catch (Exception ex)
- {
- /*
- * Swallow the exception in order to prevent a single account
- * from halting the loading of subsequent accounts.
- */
- logger.error("Failed to load account " + accountProperties, ex);
- }
- catch (ExceptionInInitializerError ex)
- {
- // In case we fail to instantiate the ProtocolProviderService.
- logger.error(
- "Failed to create account service instance for account "
- + accountProperties, ex);
- }
- }
- }
-
- /**
- * Notifies the registered {@link #listeners} that the stored accounts of a
- * specific <tt>ProtocolProviderFactory</tt> have just been loaded.
- *
- * @param factory the <tt>ProtocolProviderFactory</tt> which had its
- * stored accounts just loaded
- */
- private void fireStoredAccountsLoaded(ProtocolProviderFactory factory)
- {
- AccountManagerListener[] listeners;
- synchronized (this.listeners)
- {
- listeners =
- this.listeners
- .toArray(new AccountManagerListener[this.listeners.size()]);
- }
-
- int listenerCount = listeners.length;
- if (listenerCount > 0)
- {
- AccountManagerEvent event =
- new AccountManagerEvent(this,
- AccountManagerEvent.STORED_ACCOUNTS_LOADED, factory);
-
- for (int listenerIndex = 0;
- listenerIndex < listenerCount; listenerIndex++)
- {
- listeners[listenerIndex].handleAccountManagerEvent(event);
- }
- }
- }
-
- /**
- * Returns the package name of the <tt>factory</tt>.
- * @param factory the factory which package will be returned.
- * @return the package name of the <tt>factory</tt>.
- */
- public String getFactoryImplPackageName(ProtocolProviderFactory factory)
- {
- String className = factory.getClass().getName();
-
- return className.substring(0, className.lastIndexOf('.'));
- }
-
- /**
- * Check for stored accounts for the supplied <tt>protocolName</tt>.
- * @param protocolName the protocol name to check for
- * @param includeHidden whether to include hidden providers
- * @return <tt>true</tt> if there is any account stored in configuration
- * service with <tt>protocolName</tt>, <tt>false</tt> otherwise.
- */
- public boolean hasStoredAccounts(String protocolName, boolean includeHidden)
- {
- return hasStoredAccount(protocolName, includeHidden, null);
- }
-
- /**
- * Checks whether a stored account with <tt>userID</tt> is stored
- * in configuration.
- *
- * @param protocolName the protocol name
- * @param includeHidden whether to check hidden providers
- * @param userID the user id to check.
- * @return <tt>true</tt> if there is any account stored in configuration
- * service with <tt>protocolName</tt> and <tt>userID</tt>,
- * <tt>false</tt> otherwise.
- */
- public boolean hasStoredAccount(String protocolName,
- boolean includeHidden,
- String userID)
- {
- Collection<ServiceReference<ProtocolProviderFactory>> factoryRefs
- = ServiceUtils.getServiceReferences(
- bundleContext,
- ProtocolProviderFactory.class);
- boolean hasStoredAccounts = false;
-
- if (!factoryRefs.isEmpty())
- {
- ConfigurationService configService
- = ProtocolProviderActivator.getConfigurationService();
-
- for (ServiceReference<ProtocolProviderFactory> factoryRef
- : factoryRefs)
- {
- ProtocolProviderFactory factory
- = bundleContext.getService(factoryRef);
-
- if ((protocolName != null)
- && !protocolName.equals(factory.getProtocolName()))
- {
- continue;
- }
-
- String factoryPackage = getFactoryImplPackageName(factory);
- List<String> storedAccounts
- = configService
- .getPropertyNamesByPrefix(factoryPackage + ".acc",
- false);
-
- /* Ignore the hidden accounts. */
- for (Iterator<String> storedAccountIter =
- storedAccounts.iterator(); storedAccountIter.hasNext();)
- {
- String storedAccount = storedAccountIter.next();
- List<String> storedAccountProperties =
- configService.getPropertyNamesByPrefix(storedAccount,
- true);
- boolean hidden = false;
- String accountUserID = null;
-
- if (!includeHidden || userID != null)
- {
- for (Iterator<String> storedAccountPropertyIter =
- storedAccountProperties.iterator();
- storedAccountPropertyIter.hasNext();)
- {
- String property = storedAccountPropertyIter.next();
- String value = configService.getString(property);
-
- property = stripPackagePrefix(property);
-
- if (ProtocolProviderFactory.IS_PROTOCOL_HIDDEN
- .equals(property))
- {
- hidden = (value != null);
- }
- else if (ProtocolProviderFactory.USER_ID
- .equals(property))
- {
- accountUserID = value;
- }
- }
- }
-
- if (includeHidden || !hidden)
- {
- if(accountUserID != null
- && userID != null
- && userID.equals(accountUserID))
- {
- hasStoredAccounts = true;
- break;
- }
- else if(userID == null)
- {
- hasStoredAccounts = true;
- break;
- }
- }
- }
-
- if (hasStoredAccounts || (protocolName != null))
- {
- break;
- }
- }
- }
- return hasStoredAccounts;
- }
-
- /**
- * Searches for stored account with <tt>uid</tt> in stored
- * configuration. The <tt>uid</tt> is the one generated when creating
- * accounts with prefix <tt>ACCOUNT_UID_PREFIX</tt>.
- *
- * @return <tt>AccountID</tt> if there is any account stored in configuration
- * service with <tt>uid</tt>,
- * <tt>null</tt> otherwise.
- */
- public AccountID findAccountID(String uid)
- {
- Collection<ServiceReference<ProtocolProviderFactory>> factoryRefs
- = ServiceUtils.getServiceReferences(
- bundleContext,
- ProtocolProviderFactory.class);
-
- if (!factoryRefs.isEmpty())
- {
- ConfigurationService configService
- = ProtocolProviderActivator.getConfigurationService();
-
- for (ServiceReference<ProtocolProviderFactory> factoryRef
- : factoryRefs)
- {
- ProtocolProviderFactory factory
- = bundleContext.getService(factoryRef);
-
- String factoryPackage = getFactoryImplPackageName(factory);
- List<String> storedAccountsProps
- = configService
- .getPropertyNamesByPrefix(factoryPackage, true);
-
- for (Iterator<String> storedAccountIter =
- storedAccountsProps.iterator();
- storedAccountIter.hasNext();)
- {
- String storedAccount = storedAccountIter.next();
-
- if(!storedAccount.endsWith(uid))
- continue;
-
- String accountUID = configService.getString(
- storedAccount //node id
- + "." + ProtocolProviderFactory.ACCOUNT_UID);// propname
-
- for(AccountID acc : storedAccounts)
- {
- if(acc.getAccountUniqueID().equals(accountUID))
- return acc;
- }
- }
- }
- }
- return null;
- }
-
- /**
- * Loads the accounts stored for a specific
- * <tt>ProtocolProviderFactory</tt> and notifies the registered
- * {@link #listeners} that the stored accounts of the specified
- * <tt>factory</tt> have just been loaded
- *
- * @param factory the <tt>ProtocolProviderFactory</tt> to load the
- * stored accounts of
- */
- private void loadStoredAccounts(ProtocolProviderFactory factory)
- {
- doLoadStoredAccounts(factory);
-
- fireStoredAccountsLoaded(factory);
- }
-
- /**
- * Notifies this manager that a specific
- * <tt>ProtocolProviderFactory</tt> has been registered as a service.
- * The current implementation queues the specified <tt>factory</tt> to
- * have its stored accounts as soon as possible.
- *
- * @param factory the <tt>ProtocolProviderFactory</tt> which has been
- * registered as a service.
- */
- private void protocolProviderFactoryRegistered(
- ProtocolProviderFactory factory)
- {
- queueLoadStoredAccounts(factory);
- }
-
- /**
- * Queues a specific <tt>ProtocolProviderFactory</tt> to have its stored
- * accounts loaded as soon as possible.
- *
- * @param factory the <tt>ProtocolProviderFactory</tt> to be queued for
- * loading its stored accounts as soon as possible
- */
- private void queueLoadStoredAccounts(ProtocolProviderFactory factory)
- {
- synchronized (loadStoredAccountsQueue)
- {
- loadStoredAccountsQueue.add(factory);
- loadStoredAccountsQueue.notifyAll();
-
- if (loadStoredAccountsThread == null)
- {
- loadStoredAccountsThread = new Thread()
- {
- @Override
- public void run()
- {
- runInLoadStoredAccountsThread();
- }
- };
- loadStoredAccountsThread.setDaemon(true);
- loadStoredAccountsThread.setName(
- "AccountManager.loadStoredAccounts");
- loadStoredAccountsThread.start();
- }
- }
- }
-
- /**
- * Implements AccountManager#removeListener(AccountManagerListener).
- * @param listener the <tt>AccountManagerListener</tt> to remove
- */
- public void removeListener(AccountManagerListener listener)
- {
- synchronized (listeners)
- {
- listeners.remove(listener);
- }
- }
-
- /**
- * Running in {@link #loadStoredAccountsThread}, loads the stored accounts
- * of the <tt>ProtocolProviderFactory</tt> services waiting in
- * {@link #loadStoredAccountsQueue}
- */
- private void runInLoadStoredAccountsThread()
- {
- boolean interrupted = false;
- while (!interrupted)
- {
- try
- {
- ProtocolProviderFactory factory;
-
- synchronized (loadStoredAccountsQueue)
- {
- factory = loadStoredAccountsQueue.poll();
- if (factory == null)
- {
- /*
- * Technically, we should be handing spurious wakeups.
- * However, we cannot check the condition in a queue.
- * Anyway, we just want to keep this Thread alive long
- * enough to allow it to not be re-created multiple
- * times and not handing a spurious wakeup will just
- * cause such an inconvenience.
- */
- try
- {
- loadStoredAccountsQueue
- .wait(LOAD_STORED_ACCOUNTS_TIMEOUT);
- }
- catch (InterruptedException ex)
- {
- logger
- .warn(
- "The loading of the stored accounts has"
- + " been interrupted",
- ex);
- interrupted = true;
- break;
- }
- factory = loadStoredAccountsQueue.poll();
- }
- if (factory != null)
- loadStoredAccountsQueue.notifyAll();
- }
-
- if (factory != null)
- {
- try
- {
- loadStoredAccounts(factory);
- }
- catch (Exception ex)
- {
-
- /*
- * Swallow the exception in order to prevent a single
- * factory from halting the loading of subsequent
- * factories.
- */
- logger.error("Failed to load accounts for " + factory,
- ex);
- }
- }
- }
- finally
- {
- synchronized (loadStoredAccountsQueue)
- {
- if (!interrupted && (loadStoredAccountsQueue.size() <= 0))
- {
- if (loadStoredAccountsThread == Thread.currentThread())
- {
- loadStoredAccountsThread = null;
- loadStoredAccountsQueue.notifyAll();
- }
- break;
- }
- }
- }
- }
- }
-
- /**
- * Notifies this manager that an OSGi service has changed. The current
- * implementation tracks the registrations of
- * <tt>ProtocolProviderFactory</tt> services in order to queue them for
- * loading their stored accounts.
- *
- * @param serviceEvent the <tt>ServiceEvent</tt> containing the event
- * data
- */
- private void serviceChanged(ServiceEvent serviceEvent)
- {
- switch (serviceEvent.getType())
- {
- case ServiceEvent.REGISTERED:
- Object service
- = bundleContext.getService(serviceEvent.getServiceReference());
-
- if (service instanceof ProtocolProviderFactory)
- {
- protocolProviderFactoryRegistered(
- (ProtocolProviderFactory) service);
- }
- break;
- default:
- break;
- }
- }
-
- /**
- * Stores an account represented in the form of an <tt>AccountID</tt>
- * created by a specific <tt>ProtocolProviderFactory</tt>.
- *
- * @param factory the <tt>ProtocolProviderFactory</tt> which created the
- * account to be stored
- * @param accountID the account in the form of <tt>AccountID</tt> to be
- * stored
- * @throws OperationFailedException if anything goes wrong while storing the
- * account
- */
- public void storeAccount(
- ProtocolProviderFactory factory,
- AccountID accountID)
- throws OperationFailedException
- {
- synchronized (storedAccounts)
- {
- if (!storedAccounts.contains(accountID))
- storedAccounts.add(accountID);
- }
-
- ConfigurationService configurationService
- = ProtocolProviderActivator.getConfigurationService();
- String factoryPackage = getFactoryImplPackageName(factory);
-
- String accountNodeName
- = getAccountNodeName( factory,
- accountID.getAccountUniqueID() );
-
- Map<String, Object> configurationProperties
- = new HashMap<String, Object>();
-
- // Create a unique node name of the properties node that will contain
- // this account's properties.
- if (accountNodeName == null)
- {
- accountNodeName
- = ACCOUNT_UID_PREFIX + Long.toString(System.currentTimeMillis());
-
- // set a value for the persistent node so that we could later
- // retrieve it as a property
- configurationProperties.put(
- factoryPackage /* prefix */ + "." + accountNodeName,
- accountNodeName);
-
- // register the account in the configuration service.
- // we register all the properties in the following hierarchy
- //net.java.sip.communicator.impl.protocol.PROTO_NAME.ACC_ID.PROP_NAME
- configurationProperties.put(factoryPackage// prefix
- + "." + accountNodeName // node name for the account id
- + "." + ProtocolProviderFactory.ACCOUNT_UID, // propname
- accountID.getAccountUniqueID()); // value
- }
-
- // store the rest of the properties
- Map<String, String> accountProperties = accountID.getAccountProperties();
-
- for (Map.Entry<String, String> entry : accountProperties.entrySet())
- {
- String property = entry.getKey();
- String value = entry.getValue();
- String secureStorePrefix = null;
-
- // If the property is a password, store it securely.
- if (property.equals(ProtocolProviderFactory.PASSWORD))
- {
- String accountPrefix = factoryPackage + "." + accountNodeName;
- secureStorePrefix = accountPrefix;
- }
- else if(property.endsWith("." + ProtocolProviderFactory.PASSWORD))
- {
- secureStorePrefix = factoryPackage + "." + accountNodeName +
- "." + property.substring(0, property.lastIndexOf("."));
- }
-
- if(secureStorePrefix != null)
- {
- CredentialsStorageService credentialsStorage
- = ServiceUtils.getService(
- bundleContext,
- CredentialsStorageService.class);
-
- // encrypt and store
- if ((value != null)
- && (value.length() != 0)
- && !credentialsStorage.storePassword(
- secureStorePrefix,
- value))
- {
- throw
- new OperationFailedException(
- "CredentialsStorageService failed to"
- + " storePassword",
- OperationFailedException.GENERAL_ERROR);
- }
- }
- else
- {
- configurationProperties.put(
- factoryPackage // prefix
- + "." + accountNodeName // a unique node name for the account id
- + "." + property, // propname
- value); // value
- }
- }
-
- // clear the password if missing property, modification can request
- // password delete
- if(!accountProperties.containsKey(ProtocolProviderFactory.PASSWORD)
- && // And only if it's not stored already in encrypted form.
- // Account registration object clears also this property
- // in order to forget the password
- !configurationProperties.containsKey(
- factoryPackage+"."+accountNodeName+".ENCRYPTED_PASSWORD"))
- {
- CredentialsStorageService credentialsStorage
- = ServiceUtils.getService(
- bundleContext,
- CredentialsStorageService.class);
- credentialsStorage.removePassword(
- factoryPackage + "." + accountNodeName);
- }
-
- if (configurationProperties.size() > 0)
- configurationService.setProperties(configurationProperties);
-
- if (logger.isDebugEnabled())
- logger.debug("Stored account for id " + accountID.getAccountUniqueID()
- + " for package " + factoryPackage);
- }
-
- /**
- * Gets account node name under which account configuration properties are
- * stored.
- *
- * @param factory account's protocol provider factory
- * @param accountUID account for which the prefix will be returned
- * @return configuration prefix for given <tt>accountID</tt> if exists or
- * <tt>null</tt> otherwise
- */
- public String getAccountNodeName( ProtocolProviderFactory factory,
- String accountUID )
- {
- ConfigurationService configurationService
- = ProtocolProviderActivator.getConfigurationService();
- String factoryPackage = getFactoryImplPackageName(factory);
-
- // First check if such accountID already exists in the configuration.
- List<String> storedAccounts =
- configurationService.getPropertyNamesByPrefix(factoryPackage, true);
- String accountNodeName = null;
-
- for (Iterator<String> storedAccountIter = storedAccounts.iterator();
- storedAccountIter.hasNext();)
- {
- String storedAccount = storedAccountIter.next();
-
- // If the property is not related to an account we skip it.
- int dotIndex = storedAccount.lastIndexOf(".");
- if (!storedAccount.substring(dotIndex + 1)
- .startsWith(ACCOUNT_UID_PREFIX))
- continue;
-
- String storedAccountUID
- = configurationService.getString(
- storedAccount + "." + ProtocolProviderFactory.ACCOUNT_UID);
-
- if(storedAccountUID == null)
- continue;
-
- if (storedAccountUID.equals(accountUID))
- accountNodeName = configurationService.getString(storedAccount);
- }
- return accountNodeName;
- }
-
- /**
- * Removes the account with <tt>accountID</tt> from the set of accounts
- * that are persistently stored inside the configuration service.
- *
- * @param factory the <tt>ProtocolProviderFactory</tt> which created the
- * account to be stored
- * @param accountID the AccountID of the account to remove.
- * @return true if an account has been removed and false otherwise.
- */
- public boolean removeStoredAccount(ProtocolProviderFactory factory,
- AccountID accountID)
- {
- synchronized (storedAccounts)
- {
- if (storedAccounts.contains(accountID))
- storedAccounts.remove(accountID);
- }
-
- /*
- * We're already doing it in #unloadAccount(AccountID) - we're figuring
- * out the ProtocolProviderFactory by the AccountID.
- */
- if (factory == null)
- {
- factory
- = ProtocolProviderActivator.getProtocolProviderFactory(
- accountID.getProtocolName());
- }
-
- String factoryPackage = getFactoryImplPackageName(factory);
-
- // remove the stored password explicitly using credentials service
- CredentialsStorageService credentialsStorage
- = ServiceUtils.getService(
- bundleContext,
- CredentialsStorageService.class);
- String accountPrefix =
- ProtocolProviderFactory.findAccountPrefix(bundleContext, accountID,
- factoryPackage);
-
- credentialsStorage.removePassword(accountPrefix);
-
- ConfigurationService configurationService
- = ServiceUtils.getService(
- bundleContext,
- ConfigurationService.class);
- //first retrieve all accounts that we've registered
- List<String> storedAccounts
- = configurationService.getPropertyNamesByPrefix(
- factoryPackage, 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
- + "." + ProtocolProviderFactory.ACCOUNT_UID); // propname
-
- if (accountID.getAccountUniqueID().equals(accountUID))
- {
- //retrieve the names of all properties registered for the
- //current account.
- List<String> accountPropertyNames
- = configurationService.getPropertyNamesByPrefix(
- accountRootPropertyName, false);
-
- //set all account properties to null in order to remove them.
- for (String propName : accountPropertyNames)
- configurationService.setProperty(propName, null);
-
- //and now remove the parent too.
- configurationService.setProperty(accountRootPropertyName, null);
- return true;
- }
- }
- return false;
- }
-
- /**
- * Removes all accounts which have been persistently stored.
- *
- * @see #removeStoredAccount(ProtocolProviderFactory, AccountID)
- */
- public void removeStoredAccounts()
- {
- synchronized (loadStoredAccountsQueue)
- {
- /*
- * Wait for the Thread which loads the stored account to complete so
- * that we can be sure later on that it will not load a stored
- * account while we are deleting it or another one for that matter.
- */
- boolean interrupted = false;
-
- while (loadStoredAccountsThread != null)
- try
- {
- loadStoredAccountsQueue.wait(LOAD_STORED_ACCOUNTS_TIMEOUT);
- }
- catch (InterruptedException ie)
- {
- interrupted = true;
- }
- if (interrupted)
- Thread.currentThread().interrupt();
-
- synchronized (this.storedAccounts)
- {
- AccountID[] storedAccounts
- = this.storedAccounts.toArray(
- new AccountID[this.storedAccounts.size()]);
-
- for (AccountID storedAccount : storedAccounts)
- {
- ProtocolProviderFactory ppf
- = ProtocolProviderActivator.getProtocolProviderFactory(
- storedAccount.getProtocolName());
-
- if (ppf != null)
- ppf.uninstallAccount(storedAccount);
- }
- }
- }
- }
-
- /**
- * Returns an <tt>Iterator</tt> over a list of all stored
- * <tt>AccountID</tt>s. The list of stored accounts include all registered
- * accounts and all disabled accounts. In other words in this list we could
- * find accounts that aren't loaded.
- * <p>
- * In order to check if an account is already loaded please use the
- * #isAccountLoaded(AccountID accountID) method. To load an account use the
- * #loadAccount(AccountID accountID) method.
- *
- * @return an <tt>Iterator</tt> over a list of all stored
- * <tt>AccountID</tt>s
- */
- public Collection<AccountID> getStoredAccounts()
- {
- synchronized (storedAccounts)
- {
- return new Vector<AccountID>(storedAccounts);
- }
- }
-
- /**
- * Loads the account corresponding to the given <tt>AccountID</tt>. An
- * account is loaded when its <tt>ProtocolProviderService</tt> is registered
- * in the bundle context. This method is meant to load the account through
- * the corresponding <tt>ProtocolProviderFactory</tt>.
- *
- * @param accountID the identifier of the account to load
- * @throws OperationFailedException if anything goes wrong while loading the
- * account corresponding to the specified <tt>accountID</tt>
- */
- public void loadAccount(AccountID accountID)
- throws OperationFailedException
- {
- // If the account with the given id is already loaded we have nothing
- // to do here.
- if (isAccountLoaded(accountID))
- return;
-
- ProtocolProviderFactory providerFactory
- = ProtocolProviderActivator.getProtocolProviderFactory(
- accountID.getProtocolName());
-
- if(providerFactory.loadAccount(accountID))
- {
- accountID.putAccountProperty(
- ProtocolProviderFactory.IS_ACCOUNT_DISABLED,
- String.valueOf(false));
- // Finally store the modified properties.
- storeAccount(providerFactory, accountID);
- }
- }
-
- /**
- * Unloads the account corresponding to the given <tt>AccountID</tt>. An
- * account is unloaded when its <tt>ProtocolProviderService</tt> is
- * unregistered in the bundle context. This method is meant to unload the
- * account through the corresponding <tt>ProtocolProviderFactory</tt>.
- *
- * @param accountID the identifier of the account to load
- * @throws OperationFailedException if anything goes wrong while unloading
- * the account corresponding to the specified <tt>accountID</tt>
- */
- public void unloadAccount(AccountID accountID)
- throws OperationFailedException
- {
- // If the account with the given id is already unloaded we have nothing
- // to do here.
- if (!isAccountLoaded(accountID))
- return;
-
- ProtocolProviderFactory providerFactory
- = ProtocolProviderActivator.getProtocolProviderFactory(
- accountID.getProtocolName());
-
- // Obtain the protocol provider.
- ServiceReference<ProtocolProviderService> serRef
- = providerFactory.getProviderForAccount(accountID);
-
- // If there's no such provider we have nothing to do here.
- if (serRef == null)
- return;
-
- ProtocolProviderService protocolProvider
- = bundleContext.getService(serRef);
-
- // Set the account icon path for unloaded accounts.
- String iconPathProperty = accountID.getAccountPropertyString(
- ProtocolProviderFactory.ACCOUNT_ICON_PATH);
-
- if (iconPathProperty == null)
- {
- accountID.putAccountProperty(
- ProtocolProviderFactory.ACCOUNT_ICON_PATH,
- protocolProvider.getProtocolIcon()
- .getIconPath(ProtocolIcon.ICON_SIZE_32x32));
- }
-
- accountID.putAccountProperty(
- ProtocolProviderFactory.IS_ACCOUNT_DISABLED,
- String.valueOf(true));
-
- if (!providerFactory.unloadAccount(accountID))
- {
- accountID.putAccountProperty(
- ProtocolProviderFactory.IS_ACCOUNT_DISABLED,
- String.valueOf(false));
- }
- // Finally store the modified properties.
- storeAccount(providerFactory, accountID);
- }
-
- /**
- * Checks if the account corresponding to the given <tt>accountID</tt> is
- * loaded. An account is loaded if its <tt>ProtocolProviderService</tt> is
- * registered in the bundle context. By default all accounts are loaded.
- * However the user could manually unload an account, which would be
- * unregistered from the bundle context, but would remain in the
- * configuration file.
- *
- * @param accountID the identifier of the account to load
- * @return <tt>true</tt> to indicate that the account with the given
- * <tt>accountID</tt> is loaded, <tt>false</tt> - otherwise
- */
- public boolean isAccountLoaded(AccountID accountID)
- {
- return storedAccounts.contains(accountID) && accountID.isEnabled();
- }
-
- private String stripPackagePrefix(String property)
- {
- int packageEndIndex = property.lastIndexOf('.');
-
- if (packageEndIndex != -1)
- property = property.substring(packageEndIndex + 1);
- return property;
- }
-}
+/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.java.sip.communicator.service.protocol; + +import java.util.*; + +import net.java.sip.communicator.service.credentialsstorage.*; +import net.java.sip.communicator.service.protocol.event.*; +import net.java.sip.communicator.util.*; +import net.java.sip.communicator.util.Base64; //disambiguate from java.util.Base64 + +import org.jitsi.service.configuration.*; +import org.osgi.framework.*; + +/** + * Represents an implementation of <tt>AccountManager</tt> which loads the + * accounts in a separate thread. + * + * @author Lyubomir Marinov + * @author Yana Stamcheva + */ +public class AccountManager +{ + /** + * The delay in milliseconds the background <tt>Thread</tt> loading the + * stored accounts should wait before dying so that it doesn't get recreated + * for each <tt>ProtocolProviderFactory</tt> registration. + */ + private static final long LOAD_STORED_ACCOUNTS_TIMEOUT = 30000; + + /** + * The <tt>BundleContext</tt> this service is registered in. + */ + private final BundleContext bundleContext; + + /** + * The <tt>AccountManagerListener</tt>s currently interested in the + * events fired by this manager. + */ + private final List<AccountManagerListener> listeners = + new LinkedList<AccountManagerListener>(); + + /** + * The queue of <tt>ProtocolProviderFactory</tt> services awaiting their + * stored accounts to be loaded. + */ + private final Queue<ProtocolProviderFactory> loadStoredAccountsQueue = + new LinkedList<ProtocolProviderFactory>(); + + /** + * The <tt>Thread</tt> loading the stored accounts of the + * <tt>ProtocolProviderFactory</tt> services waiting in + * {@link #loadStoredAccountsQueue}. + */ + private Thread loadStoredAccountsThread; + + /** + * The <tt>Logger</tt> used by this <tt>AccountManagerImpl</tt> instance for + * logging output. + */ + private final Logger logger = Logger.getLogger(AccountManager.class); + + /** + * The list of <tt>AccountID</tt>s, corresponding to all stored accounts. + */ + private final Vector<AccountID> storedAccounts = new Vector<AccountID>(); + + /** + * The prefix of the account unique identifier. + */ + private static final String ACCOUNT_UID_PREFIX = "acc"; + + /** + * Initializes a new <tt>AccountManagerImpl</tt> instance loaded in a + * specific <tt>BundleContext</tt> (in which the caller will usually + * later register it). + * + * @param bundleContext the <tt>BundleContext</tt> in which the new + * instance is loaded (and in which the caller will usually later + * register it as a service) + */ + public AccountManager(BundleContext bundleContext) + { + this.bundleContext = bundleContext; + + this.bundleContext.addServiceListener(new ServiceListener() + { + public void serviceChanged(ServiceEvent serviceEvent) + { + AccountManager.this.serviceChanged(serviceEvent); + } + }); + } + + /** + * Implements AccountManager#addListener(AccountManagerListener). + * @param listener the <tt>AccountManagerListener</tt> to add + */ + public void addListener(AccountManagerListener listener) + { + synchronized (listeners) + { + if (!listeners.contains(listener)) + listeners.add(listener); + } + } + + /** + * Loads the accounts stored for a specific + * <tt>ProtocolProviderFactory</tt>. + * + * @param factory the <tt>ProtocolProviderFactory</tt> to load the + * stored accounts of + */ + private void doLoadStoredAccounts(ProtocolProviderFactory factory) + { + ConfigurationService configService + = ProtocolProviderActivator.getConfigurationService(); + String factoryPackage = getFactoryImplPackageName(factory); + List<String> accounts + = configService.getPropertyNamesByPrefix(factoryPackage, true); + + if (logger.isDebugEnabled()) + logger.debug("Discovered " + accounts.size() + " stored " + + factoryPackage + " accounts"); + + for (Iterator<String> storedAccountIter = accounts.iterator(); + storedAccountIter.hasNext();) + { + String storedAccount = storedAccountIter.next(); + + // If the property is not related to an account we skip it. + int dotIndex = storedAccount.lastIndexOf("."); + if (!storedAccount.substring(dotIndex + 1) + .startsWith(ACCOUNT_UID_PREFIX)) + continue; + + if (logger.isDebugEnabled()) + logger.debug("Loading account " + storedAccount); + + List<String> storedAccountProperties = + configService.getPropertyNamesByPrefix(storedAccount, false); + Map<String, String> accountProperties = + new Hashtable<String, String>(); + boolean disabled = false; + CredentialsStorageService credentialsStorage + = ServiceUtils.getService( + bundleContext, + CredentialsStorageService.class); + + int prefLen = storedAccount.length() + 1; + for (Iterator<String> storedAccountPropertyIter + = storedAccountProperties.iterator(); + storedAccountPropertyIter.hasNext();) + { + String property = storedAccountPropertyIter.next(); + String value = configService.getString(property); + + //strip the package prefix + if(prefLen > property.length()) + continue; + + property = property.substring(prefLen); + + if (ProtocolProviderFactory.IS_ACCOUNT_DISABLED.equals(property)) + disabled = Boolean.parseBoolean(value); + // Decode passwords. + else if (ProtocolProviderFactory.PASSWORD.equals(property) + && !credentialsStorage.isStoredEncrypted(storedAccount)) + { + if ((value != null) && value.length() != 0) + { + + /* + * TODO Converting byte[] to String using the platform's + * default charset may result in an invalid password. + */ + value = new String(Base64.decode(value)); + } + } + + if (value != null) + accountProperties.put(property, value); + } + + try + { + AccountID accountID = factory.createAccount(accountProperties); + + // If for some reason the account id is not created we move to + // the next account. + if (accountID == null) + continue; + + synchronized (storedAccounts) + { + storedAccounts.add(accountID); + } + if (!disabled) + factory.loadAccount(accountID); + } + catch (Exception ex) + { + /* + * Swallow the exception in order to prevent a single account + * from halting the loading of subsequent accounts. + */ + logger.error("Failed to load account " + accountProperties, ex); + } + catch (ExceptionInInitializerError ex) + { + // In case we fail to instantiate the ProtocolProviderService. + logger.error( + "Failed to create account service instance for account " + + accountProperties, ex); + } + } + } + + /** + * Notifies the registered {@link #listeners} that the stored accounts of a + * specific <tt>ProtocolProviderFactory</tt> have just been loaded. + * + * @param factory the <tt>ProtocolProviderFactory</tt> which had its + * stored accounts just loaded + */ + private void fireStoredAccountsLoaded(ProtocolProviderFactory factory) + { + AccountManagerListener[] listeners; + synchronized (this.listeners) + { + listeners = + this.listeners + .toArray(new AccountManagerListener[this.listeners.size()]); + } + + int listenerCount = listeners.length; + if (listenerCount > 0) + { + AccountManagerEvent event = + new AccountManagerEvent(this, + AccountManagerEvent.STORED_ACCOUNTS_LOADED, factory); + + for (int listenerIndex = 0; + listenerIndex < listenerCount; listenerIndex++) + { + listeners[listenerIndex].handleAccountManagerEvent(event); + } + } + } + + /** + * Returns the package name of the <tt>factory</tt>. + * @param factory the factory which package will be returned. + * @return the package name of the <tt>factory</tt>. + */ + public String getFactoryImplPackageName(ProtocolProviderFactory factory) + { + String className = factory.getClass().getName(); + + return className.substring(0, className.lastIndexOf('.')); + } + + /** + * Check for stored accounts for the supplied <tt>protocolName</tt>. + * @param protocolName the protocol name to check for + * @param includeHidden whether to include hidden providers + * @return <tt>true</tt> if there is any account stored in configuration + * service with <tt>protocolName</tt>, <tt>false</tt> otherwise. + */ + public boolean hasStoredAccounts(String protocolName, boolean includeHidden) + { + return hasStoredAccount(protocolName, includeHidden, null); + } + + /** + * Checks whether a stored account with <tt>userID</tt> is stored + * in configuration. + * + * @param protocolName the protocol name + * @param includeHidden whether to check hidden providers + * @param userID the user id to check. + * @return <tt>true</tt> if there is any account stored in configuration + * service with <tt>protocolName</tt> and <tt>userID</tt>, + * <tt>false</tt> otherwise. + */ + public boolean hasStoredAccount(String protocolName, + boolean includeHidden, + String userID) + { + Collection<ServiceReference<ProtocolProviderFactory>> factoryRefs + = ServiceUtils.getServiceReferences( + bundleContext, + ProtocolProviderFactory.class); + boolean hasStoredAccounts = false; + + if (!factoryRefs.isEmpty()) + { + ConfigurationService configService + = ProtocolProviderActivator.getConfigurationService(); + + for (ServiceReference<ProtocolProviderFactory> factoryRef + : factoryRefs) + { + ProtocolProviderFactory factory + = bundleContext.getService(factoryRef); + + if ((protocolName != null) + && !protocolName.equals(factory.getProtocolName())) + { + continue; + } + + String factoryPackage = getFactoryImplPackageName(factory); + List<String> storedAccounts + = configService + .getPropertyNamesByPrefix(factoryPackage + ".acc", + false); + + /* Ignore the hidden accounts. */ + for (Iterator<String> storedAccountIter = + storedAccounts.iterator(); storedAccountIter.hasNext();) + { + String storedAccount = storedAccountIter.next(); + List<String> storedAccountProperties = + configService.getPropertyNamesByPrefix(storedAccount, + true); + boolean hidden = false; + String accountUserID = null; + + if (!includeHidden || userID != null) + { + for (Iterator<String> storedAccountPropertyIter = + storedAccountProperties.iterator(); + storedAccountPropertyIter.hasNext();) + { + String property = storedAccountPropertyIter.next(); + String value = configService.getString(property); + + property = stripPackagePrefix(property); + + if (ProtocolProviderFactory.IS_PROTOCOL_HIDDEN + .equals(property)) + { + hidden = (value != null); + } + else if (ProtocolProviderFactory.USER_ID + .equals(property)) + { + accountUserID = value; + } + } + } + + if (includeHidden || !hidden) + { + if(accountUserID != null + && userID != null + && userID.equals(accountUserID)) + { + hasStoredAccounts = true; + break; + } + else if(userID == null) + { + hasStoredAccounts = true; + break; + } + } + } + + if (hasStoredAccounts || (protocolName != null)) + { + break; + } + } + } + return hasStoredAccounts; + } + + /** + * Searches for stored account with <tt>uid</tt> in stored + * configuration. The <tt>uid</tt> is the one generated when creating + * accounts with prefix <tt>ACCOUNT_UID_PREFIX</tt>. + * + * @return <tt>AccountID</tt> if there is any account stored in configuration + * service with <tt>uid</tt>, + * <tt>null</tt> otherwise. + */ + public AccountID findAccountID(String uid) + { + Collection<ServiceReference<ProtocolProviderFactory>> factoryRefs + = ServiceUtils.getServiceReferences( + bundleContext, + ProtocolProviderFactory.class); + + if (!factoryRefs.isEmpty()) + { + ConfigurationService configService + = ProtocolProviderActivator.getConfigurationService(); + + for (ServiceReference<ProtocolProviderFactory> factoryRef + : factoryRefs) + { + ProtocolProviderFactory factory + = bundleContext.getService(factoryRef); + + String factoryPackage = getFactoryImplPackageName(factory); + List<String> storedAccountsProps + = configService + .getPropertyNamesByPrefix(factoryPackage, true); + + for (Iterator<String> storedAccountIter = + storedAccountsProps.iterator(); + storedAccountIter.hasNext();) + { + String storedAccount = storedAccountIter.next(); + + if(!storedAccount.endsWith(uid)) + continue; + + String accountUID = configService.getString( + storedAccount //node id + + "." + ProtocolProviderFactory.ACCOUNT_UID);// propname + + for(AccountID acc : storedAccounts) + { + if(acc.getAccountUniqueID().equals(accountUID)) + return acc; + } + } + } + } + return null; + } + + /** + * Loads the accounts stored for a specific + * <tt>ProtocolProviderFactory</tt> and notifies the registered + * {@link #listeners} that the stored accounts of the specified + * <tt>factory</tt> have just been loaded + * + * @param factory the <tt>ProtocolProviderFactory</tt> to load the + * stored accounts of + */ + private void loadStoredAccounts(ProtocolProviderFactory factory) + { + doLoadStoredAccounts(factory); + + fireStoredAccountsLoaded(factory); + } + + /** + * Notifies this manager that a specific + * <tt>ProtocolProviderFactory</tt> has been registered as a service. + * The current implementation queues the specified <tt>factory</tt> to + * have its stored accounts as soon as possible. + * + * @param factory the <tt>ProtocolProviderFactory</tt> which has been + * registered as a service. + */ + private void protocolProviderFactoryRegistered( + ProtocolProviderFactory factory) + { + queueLoadStoredAccounts(factory); + } + + /** + * Queues a specific <tt>ProtocolProviderFactory</tt> to have its stored + * accounts loaded as soon as possible. + * + * @param factory the <tt>ProtocolProviderFactory</tt> to be queued for + * loading its stored accounts as soon as possible + */ + private void queueLoadStoredAccounts(ProtocolProviderFactory factory) + { + synchronized (loadStoredAccountsQueue) + { + loadStoredAccountsQueue.add(factory); + loadStoredAccountsQueue.notifyAll(); + + if (loadStoredAccountsThread == null) + { + loadStoredAccountsThread = new Thread() + { + @Override + public void run() + { + runInLoadStoredAccountsThread(); + } + }; + loadStoredAccountsThread.setDaemon(true); + loadStoredAccountsThread.setName( + "AccountManager.loadStoredAccounts"); + loadStoredAccountsThread.start(); + } + } + } + + /** + * Implements AccountManager#removeListener(AccountManagerListener). + * @param listener the <tt>AccountManagerListener</tt> to remove + */ + public void removeListener(AccountManagerListener listener) + { + synchronized (listeners) + { + listeners.remove(listener); + } + } + + /** + * Running in {@link #loadStoredAccountsThread}, loads the stored accounts + * of the <tt>ProtocolProviderFactory</tt> services waiting in + * {@link #loadStoredAccountsQueue} + */ + private void runInLoadStoredAccountsThread() + { + boolean interrupted = false; + while (!interrupted) + { + try + { + ProtocolProviderFactory factory; + + synchronized (loadStoredAccountsQueue) + { + factory = loadStoredAccountsQueue.poll(); + if (factory == null) + { + /* + * Technically, we should be handing spurious wakeups. + * However, we cannot check the condition in a queue. + * Anyway, we just want to keep this Thread alive long + * enough to allow it to not be re-created multiple + * times and not handing a spurious wakeup will just + * cause such an inconvenience. + */ + try + { + loadStoredAccountsQueue + .wait(LOAD_STORED_ACCOUNTS_TIMEOUT); + } + catch (InterruptedException ex) + { + logger + .warn( + "The loading of the stored accounts has" + + " been interrupted", + ex); + interrupted = true; + break; + } + factory = loadStoredAccountsQueue.poll(); + } + if (factory != null) + loadStoredAccountsQueue.notifyAll(); + } + + if (factory != null) + { + try + { + loadStoredAccounts(factory); + } + catch (Exception ex) + { + + /* + * Swallow the exception in order to prevent a single + * factory from halting the loading of subsequent + * factories. + */ + logger.error("Failed to load accounts for " + factory, + ex); + } + } + } + finally + { + synchronized (loadStoredAccountsQueue) + { + if (!interrupted && (loadStoredAccountsQueue.size() <= 0)) + { + if (loadStoredAccountsThread == Thread.currentThread()) + { + loadStoredAccountsThread = null; + loadStoredAccountsQueue.notifyAll(); + } + break; + } + } + } + } + } + + /** + * Notifies this manager that an OSGi service has changed. The current + * implementation tracks the registrations of + * <tt>ProtocolProviderFactory</tt> services in order to queue them for + * loading their stored accounts. + * + * @param serviceEvent the <tt>ServiceEvent</tt> containing the event + * data + */ + private void serviceChanged(ServiceEvent serviceEvent) + { + switch (serviceEvent.getType()) + { + case ServiceEvent.REGISTERED: + Object service + = bundleContext.getService(serviceEvent.getServiceReference()); + + if (service instanceof ProtocolProviderFactory) + { + protocolProviderFactoryRegistered( + (ProtocolProviderFactory) service); + } + break; + default: + break; + } + } + + /** + * Stores an account represented in the form of an <tt>AccountID</tt> + * created by a specific <tt>ProtocolProviderFactory</tt>. + * + * @param factory the <tt>ProtocolProviderFactory</tt> which created the + * account to be stored + * @param accountID the account in the form of <tt>AccountID</tt> to be + * stored + * @throws OperationFailedException if anything goes wrong while storing the + * account + */ + public void storeAccount( + ProtocolProviderFactory factory, + AccountID accountID) + throws OperationFailedException + { + synchronized (storedAccounts) + { + if (!storedAccounts.contains(accountID)) + storedAccounts.add(accountID); + } + + ConfigurationService configurationService + = ProtocolProviderActivator.getConfigurationService(); + String factoryPackage = getFactoryImplPackageName(factory); + + String accountNodeName + = getAccountNodeName( factory, + accountID.getAccountUniqueID() ); + + Map<String, Object> configurationProperties + = new HashMap<String, Object>(); + + // Create a unique node name of the properties node that will contain + // this account's properties. + if (accountNodeName == null) + { + accountNodeName + = ACCOUNT_UID_PREFIX + Long.toString(System.currentTimeMillis()); + + // set a value for the persistent node so that we could later + // retrieve it as a property + configurationProperties.put( + factoryPackage /* prefix */ + "." + accountNodeName, + accountNodeName); + + // register the account in the configuration service. + // we register all the properties in the following hierarchy + //net.java.sip.communicator.impl.protocol.PROTO_NAME.ACC_ID.PROP_NAME + configurationProperties.put(factoryPackage// prefix + + "." + accountNodeName // node name for the account id + + "." + ProtocolProviderFactory.ACCOUNT_UID, // propname + accountID.getAccountUniqueID()); // value + } + + // store the rest of the properties + Map<String, String> accountProperties = accountID.getAccountProperties(); + + for (Map.Entry<String, String> entry : accountProperties.entrySet()) + { + String property = entry.getKey(); + String value = entry.getValue(); + String secureStorePrefix = null; + + // If the property is a password, store it securely. + if (property.equals(ProtocolProviderFactory.PASSWORD)) + { + String accountPrefix = factoryPackage + "." + accountNodeName; + secureStorePrefix = accountPrefix; + } + else if(property.endsWith("." + ProtocolProviderFactory.PASSWORD)) + { + secureStorePrefix = factoryPackage + "." + accountNodeName + + "." + property.substring(0, property.lastIndexOf(".")); + } + + if(secureStorePrefix != null) + { + CredentialsStorageService credentialsStorage + = ServiceUtils.getService( + bundleContext, + CredentialsStorageService.class); + + // encrypt and store + if ((value != null) + && (value.length() != 0) + && !credentialsStorage.storePassword( + secureStorePrefix, + value)) + { + throw + new OperationFailedException( + "CredentialsStorageService failed to" + + " storePassword", + OperationFailedException.GENERAL_ERROR); + } + } + else + { + configurationProperties.put( + factoryPackage // prefix + + "." + accountNodeName // a unique node name for the account id + + "." + property, // propname + value); // value + } + } + + // clear the password if missing property, modification can request + // password delete + if(!accountProperties.containsKey(ProtocolProviderFactory.PASSWORD) + && // And only if it's not stored already in encrypted form. + // Account registration object clears also this property + // in order to forget the password + !configurationProperties.containsKey( + factoryPackage+"."+accountNodeName+".ENCRYPTED_PASSWORD")) + { + CredentialsStorageService credentialsStorage + = ServiceUtils.getService( + bundleContext, + CredentialsStorageService.class); + credentialsStorage.removePassword( + factoryPackage + "." + accountNodeName); + } + + if (configurationProperties.size() > 0) + configurationService.setProperties(configurationProperties); + + if (logger.isDebugEnabled()) + logger.debug("Stored account for id " + accountID.getAccountUniqueID() + + " for package " + factoryPackage); + } + + /** + * Gets account node name under which account configuration properties are + * stored. + * + * @param factory account's protocol provider factory + * @param accountUID account for which the prefix will be returned + * @return configuration prefix for given <tt>accountID</tt> if exists or + * <tt>null</tt> otherwise + */ + public String getAccountNodeName( ProtocolProviderFactory factory, + String accountUID ) + { + ConfigurationService configurationService + = ProtocolProviderActivator.getConfigurationService(); + String factoryPackage = getFactoryImplPackageName(factory); + + // First check if such accountID already exists in the configuration. + List<String> storedAccounts = + configurationService.getPropertyNamesByPrefix(factoryPackage, true); + String accountNodeName = null; + + for (Iterator<String> storedAccountIter = storedAccounts.iterator(); + storedAccountIter.hasNext();) + { + String storedAccount = storedAccountIter.next(); + + // If the property is not related to an account we skip it. + int dotIndex = storedAccount.lastIndexOf("."); + if (!storedAccount.substring(dotIndex + 1) + .startsWith(ACCOUNT_UID_PREFIX)) + continue; + + String storedAccountUID + = configurationService.getString( + storedAccount + "." + ProtocolProviderFactory.ACCOUNT_UID); + + if(storedAccountUID == null) + continue; + + if (storedAccountUID.equals(accountUID)) + accountNodeName = configurationService.getString(storedAccount); + } + return accountNodeName; + } + + /** + * Removes the account with <tt>accountID</tt> from the set of accounts + * that are persistently stored inside the configuration service. + * + * @param factory the <tt>ProtocolProviderFactory</tt> which created the + * account to be stored + * @param accountID the AccountID of the account to remove. + * @return true if an account has been removed and false otherwise. + */ + public boolean removeStoredAccount(ProtocolProviderFactory factory, + AccountID accountID) + { + synchronized (storedAccounts) + { + if (storedAccounts.contains(accountID)) + storedAccounts.remove(accountID); + } + + /* + * We're already doing it in #unloadAccount(AccountID) - we're figuring + * out the ProtocolProviderFactory by the AccountID. + */ + if (factory == null) + { + factory + = ProtocolProviderActivator.getProtocolProviderFactory( + accountID.getProtocolName()); + } + + String factoryPackage = getFactoryImplPackageName(factory); + + // remove the stored password explicitly using credentials service + CredentialsStorageService credentialsStorage + = ServiceUtils.getService( + bundleContext, + CredentialsStorageService.class); + String accountPrefix = + ProtocolProviderFactory.findAccountPrefix(bundleContext, accountID, + factoryPackage); + + credentialsStorage.removePassword(accountPrefix); + + ConfigurationService configurationService + = ServiceUtils.getService( + bundleContext, + ConfigurationService.class); + //first retrieve all accounts that we've registered + List<String> storedAccounts + = configurationService.getPropertyNamesByPrefix( + factoryPackage, 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 + + "." + ProtocolProviderFactory.ACCOUNT_UID); // propname + + if (accountID.getAccountUniqueID().equals(accountUID)) + { + //retrieve the names of all properties registered for the + //current account. + List<String> accountPropertyNames + = configurationService.getPropertyNamesByPrefix( + accountRootPropertyName, false); + + //set all account properties to null in order to remove them. + for (String propName : accountPropertyNames) + configurationService.setProperty(propName, null); + + //and now remove the parent too. + configurationService.setProperty(accountRootPropertyName, null); + return true; + } + } + return false; + } + + /** + * Removes all accounts which have been persistently stored. + * + * @see #removeStoredAccount(ProtocolProviderFactory, AccountID) + */ + public void removeStoredAccounts() + { + synchronized (loadStoredAccountsQueue) + { + /* + * Wait for the Thread which loads the stored account to complete so + * that we can be sure later on that it will not load a stored + * account while we are deleting it or another one for that matter. + */ + boolean interrupted = false; + + while (loadStoredAccountsThread != null) + try + { + loadStoredAccountsQueue.wait(LOAD_STORED_ACCOUNTS_TIMEOUT); + } + catch (InterruptedException ie) + { + interrupted = true; + } + if (interrupted) + Thread.currentThread().interrupt(); + + synchronized (this.storedAccounts) + { + AccountID[] storedAccounts + = this.storedAccounts.toArray( + new AccountID[this.storedAccounts.size()]); + + for (AccountID storedAccount : storedAccounts) + { + ProtocolProviderFactory ppf + = ProtocolProviderActivator.getProtocolProviderFactory( + storedAccount.getProtocolName()); + + if (ppf != null) + ppf.uninstallAccount(storedAccount); + } + } + } + } + + /** + * Returns an <tt>Iterator</tt> over a list of all stored + * <tt>AccountID</tt>s. The list of stored accounts include all registered + * accounts and all disabled accounts. In other words in this list we could + * find accounts that aren't loaded. + * <p> + * In order to check if an account is already loaded please use the + * #isAccountLoaded(AccountID accountID) method. To load an account use the + * #loadAccount(AccountID accountID) method. + * + * @return an <tt>Iterator</tt> over a list of all stored + * <tt>AccountID</tt>s + */ + public Collection<AccountID> getStoredAccounts() + { + synchronized (storedAccounts) + { + return new Vector<AccountID>(storedAccounts); + } + } + + /** + * Loads the account corresponding to the given <tt>AccountID</tt>. An + * account is loaded when its <tt>ProtocolProviderService</tt> is registered + * in the bundle context. This method is meant to load the account through + * the corresponding <tt>ProtocolProviderFactory</tt>. + * + * @param accountID the identifier of the account to load + * @throws OperationFailedException if anything goes wrong while loading the + * account corresponding to the specified <tt>accountID</tt> + */ + public void loadAccount(AccountID accountID) + throws OperationFailedException + { + // If the account with the given id is already loaded we have nothing + // to do here. + if (isAccountLoaded(accountID)) + return; + + ProtocolProviderFactory providerFactory + = ProtocolProviderActivator.getProtocolProviderFactory( + accountID.getProtocolName()); + + if(providerFactory.loadAccount(accountID)) + { + accountID.putAccountProperty( + ProtocolProviderFactory.IS_ACCOUNT_DISABLED, + String.valueOf(false)); + // Finally store the modified properties. + storeAccount(providerFactory, accountID); + } + } + + /** + * Unloads the account corresponding to the given <tt>AccountID</tt>. An + * account is unloaded when its <tt>ProtocolProviderService</tt> is + * unregistered in the bundle context. This method is meant to unload the + * account through the corresponding <tt>ProtocolProviderFactory</tt>. + * + * @param accountID the identifier of the account to load + * @throws OperationFailedException if anything goes wrong while unloading + * the account corresponding to the specified <tt>accountID</tt> + */ + public void unloadAccount(AccountID accountID) + throws OperationFailedException + { + // If the account with the given id is already unloaded we have nothing + // to do here. + if (!isAccountLoaded(accountID)) + return; + + ProtocolProviderFactory providerFactory + = ProtocolProviderActivator.getProtocolProviderFactory( + accountID.getProtocolName()); + + // Obtain the protocol provider. + ServiceReference<ProtocolProviderService> serRef + = providerFactory.getProviderForAccount(accountID); + + // If there's no such provider we have nothing to do here. + if (serRef == null) + return; + + ProtocolProviderService protocolProvider + = bundleContext.getService(serRef); + + // Set the account icon path for unloaded accounts. + String iconPathProperty = accountID.getAccountPropertyString( + ProtocolProviderFactory.ACCOUNT_ICON_PATH); + + if (iconPathProperty == null) + { + accountID.putAccountProperty( + ProtocolProviderFactory.ACCOUNT_ICON_PATH, + protocolProvider.getProtocolIcon() + .getIconPath(ProtocolIcon.ICON_SIZE_32x32)); + } + + accountID.putAccountProperty( + ProtocolProviderFactory.IS_ACCOUNT_DISABLED, + String.valueOf(true)); + + if (!providerFactory.unloadAccount(accountID)) + { + accountID.putAccountProperty( + ProtocolProviderFactory.IS_ACCOUNT_DISABLED, + String.valueOf(false)); + } + // Finally store the modified properties. + storeAccount(providerFactory, accountID); + } + + /** + * Checks if the account corresponding to the given <tt>accountID</tt> is + * loaded. An account is loaded if its <tt>ProtocolProviderService</tt> is + * registered in the bundle context. By default all accounts are loaded. + * However the user could manually unload an account, which would be + * unregistered from the bundle context, but would remain in the + * configuration file. + * + * @param accountID the identifier of the account to load + * @return <tt>true</tt> to indicate that the account with the given + * <tt>accountID</tt> is loaded, <tt>false</tt> - otherwise + */ + public boolean isAccountLoaded(AccountID accountID) + { + return storedAccounts.contains(accountID) && accountID.isEnabled(); + } + + private String stripPackagePrefix(String property) + { + int packageEndIndex = property.lastIndexOf('.'); + + if (packageEndIndex != -1) + property = property.substring(packageEndIndex + 1); + return property; + } +} diff --git a/src/net/java/sip/communicator/service/protocol/AccountManagerUtils.java b/src/net/java/sip/communicator/service/protocol/AccountManagerUtils.java index 5b64e06..1400a2d 100644 --- a/src/net/java/sip/communicator/service/protocol/AccountManagerUtils.java +++ b/src/net/java/sip/communicator/service/protocol/AccountManagerUtils.java @@ -1,4 +1,4 @@ -/*
+/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd @@ -15,168 +15,168 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package net.java.sip.communicator.service.protocol;
-
-import java.util.*;
-
-import net.java.sip.communicator.service.protocol.event.*;
-
-import org.osgi.framework.*;
-
-/**
- * Provides utilities to aid the manipulation of {@link AccountManager}.
- *
- * @author Lyubomir Marinov
- */
-public final class AccountManagerUtils
-{
- private static AccountManager getAccountManager(BundleContext bundleContext)
- {
- return
- bundleContext.getService(
- bundleContext.getServiceReference(AccountManager.class));
- }
-
- /**
- * Starts a specific <code>Bundle</code> and waits for the
- * <code>AccountManager</code> available in a specific
- * <code>BundleContext</code> to load the stored accounts of a
- * <code>ProtocolProviderFactory</code> with a specific protocol name.
- *
- * @param bundleContextWithAccountManager
- * the <code>BundleContext</code> in which an
- * <code>AccountManager</code> service is registered
- * @param bundleToStart
- * the <code>Bundle</code> to be started
- * @param protocolNameToWait
- * the protocol name of a <code>ProtocolProviderFactory</code> to
- * wait the end of the loading of the stored accounts for
- * @throws BundleException
- * @throws InterruptedException
- * if any thread interrupted the current thread before or while
- * the current thread was waiting for the loading of the stored
- * accounts
- */
- public static void startBundleAndWaitStoredAccountsLoaded(
- BundleContext bundleContextWithAccountManager,
- final Bundle bundleToStart,
- final String protocolNameToWait)
- throws BundleException,
- InterruptedException
- {
- AccountManager accountManager
- = getAccountManager(bundleContextWithAccountManager);
- final boolean[] storedAccountsAreLoaded = new boolean[1];
- AccountManagerListener listener = new AccountManagerListener()
- {
- public void handleAccountManagerEvent(AccountManagerEvent event)
- {
- if (AccountManagerEvent.STORED_ACCOUNTS_LOADED
- != event.getType())
- return;
-
- ProtocolProviderFactory factory = event.getFactory();
-
- /*
- * If the event is for a factory with a protocol name other than
- * protocolNameToWait, it's not the one we're waiting for.
- */
- if ((factory != null)
- && !protocolNameToWait
- .equals(factory.getProtocolName()))
- return;
-
- /*
- * If the event if for a factory which is no longer registered,
- * then it's not the one we're waiting for because we're waiting
- * for the specified bundle to start and register a factory.
- */
- if (factory != null)
- {
- BundleContext bundleContext
- = bundleToStart.getBundleContext();
-
- /*
- * If the specified bundle still hasn't started, the event
- * cannot be the one we're waiting for.
- */
- if (bundleContext == null)
- return;
-
- Collection<ServiceReference<ProtocolProviderFactory>> factoryRefs;
-
- try
- {
- factoryRefs
- = bundleContext.getServiceReferences(
- ProtocolProviderFactory.class,
- "("
- + ProtocolProviderFactory.PROTOCOL
- + "="
- + protocolNameToWait
- + ")");
- }
- catch (InvalidSyntaxException isex)
- {
- /*
- * Not likely so ignore it and assume the event is for
- * a valid factory.
- */
- factoryRefs = null;
- }
- if ((factoryRefs != null) && !factoryRefs.isEmpty())
- {
- boolean factoryIsRegistered = false;
-
- for (ServiceReference<ProtocolProviderFactory> factoryRef
- : factoryRefs)
- {
- if (factory == bundleContext.getService(factoryRef))
- {
- factoryIsRegistered = true;
- break;
- }
- }
- if (!factoryIsRegistered)
- return;
- }
- }
-
- synchronized (storedAccountsAreLoaded)
- {
- storedAccountsAreLoaded[0] = true;
- storedAccountsAreLoaded.notify();
- }
- }
- };
-
- accountManager.addListener(listener);
- try
- {
- bundleToStart.start();
-
- while (true)
- {
- synchronized (storedAccountsAreLoaded)
- {
- if (storedAccountsAreLoaded[0])
- {
- break;
- }
- storedAccountsAreLoaded.wait();
- }
- }
- }
- finally
- {
- accountManager.removeListener(listener);
- }
- }
-
- /**
- * Prevents the creation of <code>AccountManagerUtils</code> instances.
- */
- private AccountManagerUtils()
- {
- }
-}
+package net.java.sip.communicator.service.protocol; + +import java.util.*; + +import net.java.sip.communicator.service.protocol.event.*; + +import org.osgi.framework.*; + +/** + * Provides utilities to aid the manipulation of {@link AccountManager}. + * + * @author Lyubomir Marinov + */ +public final class AccountManagerUtils +{ + private static AccountManager getAccountManager(BundleContext bundleContext) + { + return + bundleContext.getService( + bundleContext.getServiceReference(AccountManager.class)); + } + + /** + * Starts a specific <code>Bundle</code> and waits for the + * <code>AccountManager</code> available in a specific + * <code>BundleContext</code> to load the stored accounts of a + * <code>ProtocolProviderFactory</code> with a specific protocol name. + * + * @param bundleContextWithAccountManager + * the <code>BundleContext</code> in which an + * <code>AccountManager</code> service is registered + * @param bundleToStart + * the <code>Bundle</code> to be started + * @param protocolNameToWait + * the protocol name of a <code>ProtocolProviderFactory</code> to + * wait the end of the loading of the stored accounts for + * @throws BundleException + * @throws InterruptedException + * if any thread interrupted the current thread before or while + * the current thread was waiting for the loading of the stored + * accounts + */ + public static void startBundleAndWaitStoredAccountsLoaded( + BundleContext bundleContextWithAccountManager, + final Bundle bundleToStart, + final String protocolNameToWait) + throws BundleException, + InterruptedException + { + AccountManager accountManager + = getAccountManager(bundleContextWithAccountManager); + final boolean[] storedAccountsAreLoaded = new boolean[1]; + AccountManagerListener listener = new AccountManagerListener() + { + public void handleAccountManagerEvent(AccountManagerEvent event) + { + if (AccountManagerEvent.STORED_ACCOUNTS_LOADED + != event.getType()) + return; + + ProtocolProviderFactory factory = event.getFactory(); + + /* + * If the event is for a factory with a protocol name other than + * protocolNameToWait, it's not the one we're waiting for. + */ + if ((factory != null) + && !protocolNameToWait + .equals(factory.getProtocolName())) + return; + + /* + * If the event if for a factory which is no longer registered, + * then it's not the one we're waiting for because we're waiting + * for the specified bundle to start and register a factory. + */ + if (factory != null) + { + BundleContext bundleContext + = bundleToStart.getBundleContext(); + + /* + * If the specified bundle still hasn't started, the event + * cannot be the one we're waiting for. + */ + if (bundleContext == null) + return; + + Collection<ServiceReference<ProtocolProviderFactory>> factoryRefs; + + try + { + factoryRefs + = bundleContext.getServiceReferences( + ProtocolProviderFactory.class, + "(" + + ProtocolProviderFactory.PROTOCOL + + "=" + + protocolNameToWait + + ")"); + } + catch (InvalidSyntaxException isex) + { + /* + * Not likely so ignore it and assume the event is for + * a valid factory. + */ + factoryRefs = null; + } + if ((factoryRefs != null) && !factoryRefs.isEmpty()) + { + boolean factoryIsRegistered = false; + + for (ServiceReference<ProtocolProviderFactory> factoryRef + : factoryRefs) + { + if (factory == bundleContext.getService(factoryRef)) + { + factoryIsRegistered = true; + break; + } + } + if (!factoryIsRegistered) + return; + } + } + + synchronized (storedAccountsAreLoaded) + { + storedAccountsAreLoaded[0] = true; + storedAccountsAreLoaded.notify(); + } + } + }; + + accountManager.addListener(listener); + try + { + bundleToStart.start(); + + while (true) + { + synchronized (storedAccountsAreLoaded) + { + if (storedAccountsAreLoaded[0]) + { + break; + } + storedAccountsAreLoaded.wait(); + } + } + } + finally + { + accountManager.removeListener(listener); + } + } + + /** + * Prevents the creation of <code>AccountManagerUtils</code> instances. + */ + private AccountManagerUtils() + { + } +} diff --git a/src/net/java/sip/communicator/service/protocol/AdHocChatRoom.java b/src/net/java/sip/communicator/service/protocol/AdHocChatRoom.java index 6f07bd1..e2af0a8 100644 --- a/src/net/java/sip/communicator/service/protocol/AdHocChatRoom.java +++ b/src/net/java/sip/communicator/service/protocol/AdHocChatRoom.java @@ -25,8 +25,7 @@ import net.java.sip.communicator.service.protocol.event.*; * Represents an ad-hoc rendez-vous point where multiple chat users could * communicate together. This interface describes the main methods used by some * protocols for multi user chat, without useless methods (such as kicking a - * participant) which aren't supported by these protocols (MSN, ICQ, Yahoo!, - * etc.). + * participant) which aren't supported by these protocols (MSN, ICQ, etc.). * * <tt>AdHocChatRoom</tt> acts like a simplified <tt>ChatRoom</tt>. * diff --git a/src/net/java/sip/communicator/service/protocol/Call.java b/src/net/java/sip/communicator/service/protocol/Call.java index d342373..a952cfd 100644 --- a/src/net/java/sip/communicator/service/protocol/Call.java +++ b/src/net/java/sip/communicator/service/protocol/Call.java @@ -111,6 +111,13 @@ public abstract class Call private boolean isAutoAnswer = false; /** + * The indicator which determines whether any telephony conference + * represented by this instance is mixing or relaying. + * By default what can be mixed is mixed (audio) and rest is relayed. + */ + protected final boolean useTranslator; + + /** * Creates a new Call instance. * * @param sourceProvider the proto provider that created us. @@ -133,6 +140,11 @@ public abstract class Call = accountID.getAccountPropertyBoolean( ProtocolProviderFactory.DEFAULT_SIPZRTP_ATTRIBUTE, true); + + useTranslator + = accountID.getAccountPropertyBoolean( + ProtocolProviderFactory.USE_TRANSLATOR_IN_CONFERENCE, + false); } /** diff --git a/src/net/java/sip/communicator/service/protocol/CallConference.java b/src/net/java/sip/communicator/service/protocol/CallConference.java index 890b0cb..b6752cd 100644 --- a/src/net/java/sip/communicator/service/protocol/CallConference.java +++ b/src/net/java/sip/communicator/service/protocol/CallConference.java @@ -1,4 +1,4 @@ -/*
+/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd @@ -15,931 +15,931 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package net.java.sip.communicator.service.protocol;
-
-import java.util.*;
-
-import net.java.sip.communicator.service.protocol.event.*;
-
-import org.jitsi.util.event.*;
-
-/**
- * Represents the telephony conference-related state of a <tt>Call</tt>.
- * Multiple <tt>Call</tt> instances share a single <tt>CallConference</tt>
- * instance when the former are into a telephony conference i.e. the local
- * peer/user is the conference focus. <tt>CallConference</tt> is
- * protocol-agnostic and thus enables cross-protocol conferences. Since a
- * non-conference <tt>Call</tt> may be converted into a conference <tt>Call</tt>
- * at any time, every <tt>Call</tt> instance maintains a <tt>CallConference</tt>
- * instance regardless of whether the <tt>Call</tt> in question is participating
- * in a telephony conference.
- *
- * @author Lyubomir Marinov
- */
-public class CallConference
- extends PropertyChangeNotifier
-{
- /**
- * The name of the <tt>CallConference</tt> property which specifies the list
- * of <tt>Call</tt>s participating in a telephony conference. A change in
- * the value of the property is delivered in the form of a
- * <tt>PropertyChangeEvent</tt> which has its <tt>oldValue</tt> or
- * <tt>newValue</tt> set to the <tt>Call</tt> which has been removed or
- * added to the list of <tt>Call</tt>s participating in the telephony
- * conference.
- */
- public static final String CALLS = "calls";
-
- /**
- * Gets the number of <tt>CallPeer</tt>s associated with the <tt>Call</tt>s
- * participating in the telephony conference-related state of a specific
- * <tt>Call</tt>.
- *
- * @param call the <tt>Call</tt> for which the number of <tt>CallPeer</tt>s
- * associated with the <tt>Call</tt>s participating in its associated
- * telephony conference-related state
- * @return the number of <tt>CallPeer</tt>s associated with the
- * <tt>Call</tt>s participating in the telephony conference-related state
- * of the specified <tt>Call</tt>
- */
- public static int getCallPeerCount(Call call)
- {
- CallConference conference = call.getConference();
-
- /*
- * A Call instance is supposed to always maintain a CallConference
- * instance. Anyway, if it turns out that it is not the case, we will
- * consider the Call as a representation of a telephony conference.
- */
- return
- (conference == null)
- ? call.getCallPeerCount()
- : conference.getCallPeerCount();
- }
-
- /**
- * Gets a list of the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s
- * participating in the telephony conference in which a specific
- * <tt>Call</tt> is participating.
- *
- * @param call the <tt>Call</tt> which specifies the telephony conference
- * the <tt>CallPeer</tt>s of which are to be retrieved
- * @return a list of the <tt>CallPeer</tt>s associated with the
- * <tt>Call</tt>s participating in the telephony conference in which the
- * specified <tt>call</tt> is participating
- */
- public static List<CallPeer> getCallPeers(Call call)
- {
- CallConference conference = call.getConference();
- List<CallPeer> callPeers = new ArrayList<CallPeer>();
-
- if (conference == null)
- {
- Iterator<? extends CallPeer> callPeerIt = call.getCallPeers();
-
- while (callPeerIt.hasNext())
- callPeers.add(callPeerIt.next());
- }
- else
- conference.getCallPeers(callPeers);
- return callPeers;
- }
-
- /**
- * Gets the list of <tt>Call</tt>s participating in the telephony conference
- * in which a specific <tt>Call</tt> is participating.
- *
- * @param call the <tt>Call</tt> which participates in the telephony
- * conference the list of participating <tt>Call</tt>s of which is to be
- * returned
- * @return the list of <tt>Call</tt>s participating in the telephony
- * conference in which the specified <tt>call</tt> is participating
- */
- public static List<Call> getCalls(Call call)
- {
- CallConference conference = call.getConference();
- List<Call> calls;
-
- if (conference == null)
- calls = Collections.emptyList();
- else
- calls = conference.getCalls();
- return calls;
- }
-
- /**
- * Determines whether a <tt>CallConference</tt> is to report the local
- * peer/user as a conference focus judging by a specific list of
- * <tt>Call</tt>s.
- *
- * @param calls the list of <tt>Call</tt> which are to be judged whether
- * the local peer/user that they represent is to be considered as a
- * conference focus
- * @return <tt>true</tt> if the local peer/user represented by the specified
- * <tt>calls</tt> is judged to be a conference focus; otherwise,
- * <tt>false</tt>
- */
- private static boolean isConferenceFocus(List<Call> calls)
- {
- int callCount = calls.size();
- boolean conferenceFocus;
-
- if (callCount < 1)
- conferenceFocus = false;
- else if (callCount > 1)
- conferenceFocus = true;
- else
- conferenceFocus = (calls.get(0).getCallPeerCount() > 1);
- return conferenceFocus;
- }
-
- /**
- * The <tt>CallChangeListener</tt> which listens to changes in the
- * <tt>Call</tt>s participating in this telephony conference.
- */
- private final CallChangeListener callChangeListener
- = new CallChangeListener()
- {
- @Override
- public void callPeerAdded(CallPeerEvent ev)
- {
- CallConference.this.onCallPeerEvent(ev);
- }
-
- @Override
- public void callPeerRemoved(CallPeerEvent ev)
- {
- CallConference.this.onCallPeerEvent(ev);
- }
-
- @Override
- public void callStateChanged(CallChangeEvent ev)
- {
- CallConference.this.callStateChanged(ev);
- }
- };
-
- /**
- * The list of <tt>CallChangeListener</tt>s added to the <tt>Call</tt>s
- * participating in this telephony conference via
- * {@link #addCallChangeListener(CallChangeListener)}.
- */
- private final List<CallChangeListener> callChangeListeners
- = new LinkedList<CallChangeListener>();
-
- /**
- * The <tt>CallPeerConferenceListener</tt> which listens to the
- * <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating in
- * this telephony conference.
- */
- private final CallPeerConferenceListener callPeerConferenceListener
- = new CallPeerConferenceAdapter()
- {
- /**
- * {@inheritDoc}
- *
- * Invokes
- * {@link CallConference#onCallPeerConferenceEvent(
- * CallPeerConferenceEvent)}.
- */
- @Override
- protected void onCallPeerConferenceEvent(CallPeerConferenceEvent ev)
- {
- CallConference.this.onCallPeerConferenceEvent(ev);
- }
-
- /**
- * {@inheritDoc}
- *
- * Invokes
- * {@link CallConference#onCallPeerConferenceEvent(
- * CallPeerConferenceEvent)}.
- */
- @Override
- public void conferenceMemberErrorReceived(
- CallPeerConferenceEvent ev)
- {
- CallConference.this.onCallPeerConferenceEvent(ev);
- }
- };
-
- /**
- * The list of <tt>CallPeerConferenceListener</tt>s added to the
- * <tt>CallPeer</tt>s associated with the <tt>CallPeer</tt>s participating
- * in this telephony conference via
- * {@link #addCallPeerConferenceListener}.
- */
- private final List<CallPeerConferenceListener> callPeerConferenceListeners
- = new LinkedList<CallPeerConferenceListener>();
-
- /**
- * The synchronization root/<tt>Object</tt> which protects the access to
- * {@link #immutableCalls} and {@link #mutableCalls}.
- */
- private final Object callsSyncRoot = new Object();
-
- /**
- * The indicator which determines whether the local peer represented by this
- * instance and the <tt>Call</tt>s participating in it is acting as a
- * conference focus. The SIP protocol, for example, will add the
- * "isfocus" parameter to the Contact headers of its outgoing
- * signaling if <tt>true</tt>.
- */
- private boolean conferenceFocus = false;
-
- /**
- * The list of <tt>Call</tt>s participating in this telephony conference as
- * an immutable <tt>List</tt> which can be exposed out of this instance
- * without the need to make a copy. In other words, it is an unmodifiable
- * view of {@link #mutableCalls}.
- */
- private List<Call> immutableCalls;
-
- /**
- * The indicator which determines whether the telephony conference
- * represented by this instance is utilizing the Jitsi Videobridge
- * server-side telephony conferencing technology.
- */
- private final boolean jitsiVideobridge;
-
- /**
- * The list of <tt>Call</tt>s participating in this telephony conference as
- * a mutable <tt>List</tt> which should not be exposed out of this instance.
- */
- private List<Call> mutableCalls;
-
- /**
- * Initializes a new <tt>CallConference</tt> instance.
- */
- public CallConference()
- {
- this(false);
- }
-
- /**
- * Initializes a new <tt>CallConference</tt> instance which is to optionally
- * utilize the Jitsi Videobridge server-side telephony conferencing
- * technology.
- *
- * @param jitsiVideobridge <tt>true</tt> if the telephony conference
- * represented by the new instance is to utilize the Jitsi Videobridge
- * server-side telephony conferencing technology; otherwise, <tt>false</tt>
- */
- public CallConference(boolean jitsiVideobridge)
- {
- this.jitsiVideobridge = jitsiVideobridge;
-
- mutableCalls = new ArrayList<Call>();
- immutableCalls = Collections.unmodifiableList(mutableCalls);
- }
-
- /**
- * Adds a specific <tt>Call</tt> to the list of <tt>Call</tt>s participating
- * in this telephony conference.
- *
- * @param call the <tt>Call</tt> to add to the list of <tt>Call</tt>s
- * participating in this telephony conference
- * @return <tt>true</tt> if the list of <tt>Call</tt>s participating in this
- * telephony conference changed as a result of the method call; otherwise,
- * <tt>false</tt>
- * @throws NullPointerException if <tt>call</tt> is <tt>null</tt>
- */
- boolean addCall(Call call)
- {
- if (call == null)
- throw new NullPointerException("call");
-
- synchronized (callsSyncRoot)
- {
- if (mutableCalls.contains(call))
- return false;
-
- /*
- * Implement the List of Calls participating in this telephony
- * conference as a copy-on-write storage in order to optimize the
- * getCalls method which is likely to be executed much more often
- * than the addCall and removeCall methods.
- */
- List<Call> newMutableCalls = new ArrayList<Call>(mutableCalls);
-
- if (newMutableCalls.add(call))
- {
- mutableCalls = newMutableCalls;
- immutableCalls = Collections.unmodifiableList(mutableCalls);
- }
- else
- return false;
- }
-
- callAdded(call);
- return true;
- }
-
- /**
- * Adds a <tt>CallChangeListener</tt> to the <tt>Call</tt>s participating in
- * this telephony conference. The method is a convenience that takes on the
- * responsibility of tracking the <tt>Call</tt>s that get added/removed
- * to/from this telephony conference.
- *
- * @param listener the <tt>CallChangeListner</tt> to be added to the
- * <tt>Call</tt>s participating in this telephony conference
- * @throws NullPointerException if <tt>listener</tt> is <tt>null</tt>
- */
- public void addCallChangeListener(CallChangeListener listener)
- {
- if (listener == null)
- throw new NullPointerException("listener");
- else
- {
- synchronized (callChangeListeners)
- {
- if (!callChangeListeners.contains(listener))
- callChangeListeners.add(listener);
- }
- }
- }
-
- /**
- * Adds {@link #callPeerConferenceListener} to the <tt>CallPeer</tt>s
- * associated with a specific <tt>Call</tt>.
- *
- * @param call the <tt>Call</tt> to whose associated <tt>CallPeer</tt>s
- * <tt>callPeerConferenceListener</tt> is to be added
- */
- private void addCallPeerConferenceListener(Call call)
- {
- Iterator<? extends CallPeer> callPeerIter = call.getCallPeers();
-
- while (callPeerIter.hasNext())
- {
- callPeerIter.next().addCallPeerConferenceListener(
- callPeerConferenceListener);
- }
- }
-
- /**
- * Adds a <tt>CallPeerConferenceListener</tt> to the <tt>CallPeer</tt>s
- * associated with the <tt>Call</tt>s participating in this telephony
- * conference. The method is a convenience that takes on the responsibility
- * of tracking the <tt>Call</tt>s that get added/removed to/from this
- * telephony conference and the <tt>CallPeer</tt> that get added/removed
- * to/from these <tt>Call</tt>s.
- *
- * @param listener the <tt>CallPeerConferenceListener</tt> to be added to
- * the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating
- * in this telephony conference
- * @throws NullPointerException if <tt>listener</tt> is <tt>null</tt>
- */
- public void addCallPeerConferenceListener(
- CallPeerConferenceListener listener)
- {
- if (listener == null)
- throw new NullPointerException("listener");
- else
- {
- synchronized (callPeerConferenceListeners)
- {
- if (!callPeerConferenceListeners.contains(listener))
- callPeerConferenceListeners.add(listener);
- }
- }
- }
-
- /**
- * Notifies this <tt>CallConference</tt> that a specific <tt>Call</tt> has
- * been added to the list of <tt>Call</tt>s participating in this telephony
- * conference.
- *
- * @param call the <tt>Call</tt> which has been added to the list of
- * <tt>Call</tt>s participating in this telephony conference
- */
- protected void callAdded(Call call)
- {
- call.addCallChangeListener(callChangeListener);
- addCallPeerConferenceListener(call);
-
- /*
- * Update the conferenceFocus state. Because the public
- * setConferenceFocus method allows forcing a specific value on the
- * state in question and because it does not sound right to have the
- * adding of a Call set conferenceFocus to false, only update it if the
- * new conferenceFocus value is true,
- */
- boolean conferenceFocus = isConferenceFocus(getCalls());
-
- if (conferenceFocus)
- setConferenceFocus(conferenceFocus);
-
- firePropertyChange(CALLS, null, call);
- }
-
- /**
- * Notifies this <tt>CallConference</tt> that a specific <tt>Call</tt> has
- * been removed from the list of <tt>Call</tt>s participating in this
- * telephony conference.
- *
- * @param call the <tt>Call</tt> which has been removed from the list of
- * <tt>Call</tt>s participating in this telephony conference
- */
- protected void callRemoved(Call call)
- {
- call.removeCallChangeListener(callChangeListener);
- removeCallPeerConferenceListener(call);
-
- /*
- * Update the conferenceFocus state. Following the line of thinking
- * expressed in the callAdded method, only update it if the new
- * conferenceFocus value is false.
- */
- boolean conferenceFocus = isConferenceFocus(getCalls());
-
- if (!conferenceFocus)
- setConferenceFocus(conferenceFocus);
-
- firePropertyChange(CALLS, call, null);
- }
-
- /**
- * Notifies this telephony conference that the <tt>CallState</tt> of a
- * <tt>Call</tt> has changed.
- *
- * @param ev a <tt>CallChangeEvent</tt> which specifies the <tt>Call</tt>
- * which had its <tt>CallState</tt> changed and the old and new
- * <tt>CallState</tt>s of that <tt>Call</tt>
- */
- private void callStateChanged(CallChangeEvent ev)
- {
- Call call = ev.getSourceCall();
-
- if (containsCall(call))
- {
- try
- {
- // Forward the CallChangeEvent to the callChangeListeners.
- for (CallChangeListener l : getCallChangeListeners())
- l.callStateChanged(ev);
- }
- finally
- {
- if (CallChangeEvent.CALL_STATE_CHANGE
- .equals(ev.getPropertyName())
- && CallState.CALL_ENDED.equals(ev.getNewValue()))
- {
- /*
- * Should not be vital because Call will remove itself.
- * Anyway, do it for the sake of completeness.
- */
- removeCall(call);
- }
- }
- }
- }
-
- /**
- * Notifies this <tt>CallConference</tt> that the value of its
- * <tt>conferenceFocus</tt> property has changed from a specific old value
- * to a specific new value.
- *
- * @param oldValue the value of the <tt>conferenceFocus</tt> property of
- * this instance before the change
- * @param newValue the value of the <tt>conferenceFocus</tt> property of
- * this instance after the change
- */
- protected void conferenceFocusChanged(boolean oldValue, boolean newValue)
- {
- firePropertyChange(Call.CONFERENCE_FOCUS, oldValue, newValue);
- }
-
- /**
- * Determines whether a specific <tt>Call</tt> is participating in this
- * telephony conference.
- *
- * @param call the <tt>Call</tt> which is to be checked whether it is
- * participating in this telephony conference
- * @return <tt>true</tt> if the specified <tt>call</tt> is participating in
- * this telephony conference
- */
- public boolean containsCall(Call call)
- {
- synchronized (callsSyncRoot)
- {
- return mutableCalls.contains(call);
- }
- }
-
- /**
- * Gets the list of <tt>CallChangeListener</tt>s added to the <tt>Call</tt>s
- * participating in this telephony conference via
- * {@link #addCallChangeListener(CallChangeListener)}.
- *
- * @return the list of <tt>CallChangeListener</tt>s added to the
- * <tt>Call</tt>s participating in this telephony conference via
- * {@link #addCallChangeListener(CallChangeListener)}
- */
- private CallChangeListener[] getCallChangeListeners()
- {
- synchronized (callChangeListeners)
- {
- return
- callChangeListeners.toArray(
- new CallChangeListener[callChangeListeners.size()]);
- }
- }
-
- /**
- * Gets the number of <tt>Call</tt>s that are participating in this
- * telephony conference.
- *
- * @return the number of <tt>Call</tt>s that are participating in this
- * telephony conference
- */
- public int getCallCount()
- {
- synchronized (callsSyncRoot)
- {
- return mutableCalls.size();
- }
- }
-
- /**
- * Gets the list of <tt>CallPeerConferenceListener</tt>s added to the
- * <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating in
- * this telephony conference via
- * {@link #addCallPeerConferenceListener(CallPeerConferenceListener)}.
- *
- * @return the list of <tt>CallPeerConferenceListener</tt>s added to the
- * <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating in
- * this telephony conference via
- * {@link #addCallPeerConferenceListener(CallPeerConferenceListener)}
- */
- private CallPeerConferenceListener[] getCallPeerConferenceListeners()
- {
- synchronized (callPeerConferenceListeners)
- {
- return
- callPeerConferenceListeners.toArray(
- new CallPeerConferenceListener[
- callPeerConferenceListeners.size()]);
- }
- }
-
- /**
- * Gets the number of <tt>CallPeer</tt>s associated with the <tt>Call</tt>s
- * participating in this telephony conference.
- *
- * @return the number of <tt>CallPeer</tt>s associated with the
- * <tt>Call</tt>s participating in this telephony conference
- */
- public int getCallPeerCount()
- {
- int callPeerCount = 0;
-
- for (Call call : getCalls())
- callPeerCount += call.getCallPeerCount();
- return callPeerCount;
- }
-
- /**
- * Gets a list of the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s
- * participating in this telephony conference.
- *
- * @return a list of the <tt>CallPeer</tt>s associated with the
- * <tt>Call</tt>s participating in this telephony conference
- */
- public List<CallPeer> getCallPeers()
- {
- List<CallPeer> callPeers = new ArrayList<CallPeer>();
-
- getCallPeers(callPeers);
- return callPeers;
- }
-
- /**
- * Adds the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s
- * participating in this telephony conference into a specific <tt>List</tt>.
- *
- * @param callPeers a <tt>List</tt> into which the <tt>CallPeer</tt>s
- * associated with the <tt>Call</tt>s participating in this telephony
- * conference are to be added
- */
- protected void getCallPeers(List<CallPeer> callPeers)
- {
- for (Call call : getCalls())
- {
- Iterator<? extends CallPeer> callPeerIt = call.getCallPeers();
-
- while (callPeerIt.hasNext())
- callPeers.add(callPeerIt.next());
- }
- }
-
- /**
- * Gets the list of <tt>Call</tt> participating in this telephony
- * conference.
- *
- * @return the list of <tt>Call</tt>s participating in this telephony
- * conference. An empty array of <tt>Call</tt> element type is returned if
- * there are no <tt>Call</tt>s in this telephony conference-related state.
- */
- public List<Call> getCalls()
- {
- synchronized (callsSyncRoot)
- {
- return immutableCalls;
- }
- }
-
- /**
- * Determines whether the local peer/user associated with this instance and
- * represented by the <tt>Call</tt>s participating into it is acting as a
- * conference focus.
- *
- * @return <tt>true</tt> if the local peer/user associated by this instance
- * is acting as a conference focus; otherwise, <tt>false</tt>
- */
- public boolean isConferenceFocus()
- {
- return conferenceFocus;
- }
-
- /**
- * Determines whether the current state of this instance suggests that the
- * telephony conference it represents has ended. Iterates over the
- * <tt>Call</tt>s participating in this telephony conference and looks for a
- * <tt>Call</tt> which is not in the {@link CallState#CALL_ENDED} state.
- *
- * @return <tt>true</tt> if the current state of this instance suggests that
- * the telephony conference it represents has ended; otherwise,
- * <tt>false</tt>
- */
- public boolean isEnded()
- {
- for (Call call : getCalls())
- {
- if (!CallState.CALL_ENDED.equals(call.getCallState()))
- return false;
- }
- return true;
- }
-
- /**
- * Determines whether the telephony conference represented by this instance
- * is utilizing the Jitsi Videobridge server-side telephony conferencing
- * technology.
- *
- * @return <tt>true</tt> if the telephony conference represented by this
- * instance is utilizing the Jitsi Videobridge server-side telephony
- * conferencing technology
- */
- public boolean isJitsiVideobridge()
- {
- return jitsiVideobridge;
- }
-
- /**
- * Notifies this telephony conference that a
- * <tt>CallPeerConferenceEvent</tt> was fired by a <tt>CallPeer</tt>
- * associated with a <tt>Call</tt> participating in this telephony
- * conference. Forwards the specified <tt>CallPeerConferenceEvent</tt> to
- * {@link #callPeerConferenceListeners}.
- *
- * @param ev the <tt>CallPeerConferenceEvent</tt> which was fired
- */
- private void onCallPeerConferenceEvent(CallPeerConferenceEvent ev)
- {
- int eventID = ev.getEventID();
-
- for (CallPeerConferenceListener l : getCallPeerConferenceListeners())
- {
- switch (eventID)
- {
- case CallPeerConferenceEvent.CONFERENCE_FOCUS_CHANGED:
- l.conferenceFocusChanged(ev);
- break;
- case CallPeerConferenceEvent.CONFERENCE_MEMBER_ADDED:
- l.conferenceMemberAdded(ev);
- break;
- case CallPeerConferenceEvent.CONFERENCE_MEMBER_REMOVED:
- l.conferenceMemberRemoved(ev);
- break;
- case CallPeerConferenceEvent.CONFERENCE_MEMBER_ERROR_RECEIVED:
- l.conferenceMemberErrorReceived(ev);
- break;
- default:
- throw new UnsupportedOperationException(
- "Unsupported CallPeerConferenceEvent eventID.");
- }
- }
- }
-
- /**
- * Notifies this telephony conference about a specific
- * <tt>CallPeerEvent</tt> i.e. that a <tt>CallPeer</tt> was either added to
- * or removed from a <tt>Call</tt>.
- *
- * @param ev a <tt>CallPeerEvent</tt> which specifies the <tt>CallPeer</tt>
- * which was added or removed and the <tt>Call</tt> to which it was added or
- * from which is was removed
- */
- private void onCallPeerEvent(CallPeerEvent ev)
- {
- Call call = ev.getSourceCall();
-
- if (containsCall(call))
- {
- /*
- * Update the conferenceFocus state. Following the line of thinking
- * expressed in the callAdded and callRemoved methods, only update
- * it if the new conferenceFocus value is in accord with the
- * expectations.
- */
- int eventID = ev.getEventID();
- boolean conferenceFocus = isConferenceFocus(getCalls());
-
- switch (eventID)
- {
- case CallPeerEvent.CALL_PEER_ADDED:
- if (conferenceFocus)
- setConferenceFocus(conferenceFocus);
- break;
- case CallPeerEvent.CALL_PEER_REMOVED:
- if (!conferenceFocus)
- setConferenceFocus(conferenceFocus);
- break;
- default:
- /*
- * We're interested in the adding and removing of CallPeers
- * only.
- */
- break;
- }
-
- try
- {
- // Forward the CallPeerEvent to the callChangeListeners.
- for (CallChangeListener l : getCallChangeListeners())
- {
- switch (eventID)
- {
- case CallPeerEvent.CALL_PEER_ADDED:
- l.callPeerAdded(ev);
- break;
- case CallPeerEvent.CALL_PEER_REMOVED:
- l.callPeerRemoved(ev);
- break;
- default:
- break;
- }
- }
- }
- finally
- {
- /*
- * Add/remove the callPeerConferenceListener to/from the source
- * CallPeer (for the purposes of the
- * addCallPeerConferenceListener method of this CallConference).
- */
- CallPeer callPeer = ev.getSourceCallPeer();
-
- switch (eventID)
- {
- case CallPeerEvent.CALL_PEER_ADDED:
- callPeer.addCallPeerConferenceListener(
- callPeerConferenceListener);
- break;
- case CallPeerEvent.CALL_PEER_REMOVED:
- callPeer.removeCallPeerConferenceListener(
- callPeerConferenceListener);
- break;
- default:
- break;
- }
- }
- }
- }
-
- /**
- * Removes a specific <tt>Call</tt> from the list of <tt>Call</tt>s
- * participating in this telephony conference.
- *
- * @param call the <tt>Call</tt> to remove from the list of <tt>Call</tt>s
- * participating in this telephony conference
- * @return <tt>true</tt> if the list of <tt>Call</tt>s participating in this
- * telephony conference changed as a result of the method call; otherwise,
- * <tt>false</tt>
- */
- boolean removeCall(Call call)
- {
- if (call == null)
- return false;
-
- synchronized (callsSyncRoot)
- {
- if (!mutableCalls.contains(call))
- return false;
-
- /*
- * Implement the List of Calls participating in this telephony
- * conference as a copy-on-write storage in order to optimize the
- * getCalls method which is likely to be executed much more often
- * than the addCall and removeCall methods.
- */
- List<Call> newMutableCalls = new ArrayList<Call>(mutableCalls);
-
- if (newMutableCalls.remove(call))
- {
- mutableCalls = newMutableCalls;
- immutableCalls = Collections.unmodifiableList(mutableCalls);
- }
- else
- return false;
- }
-
- callRemoved(call);
- return true;
- }
-
- /**
- * Removes a <tt>CallChangeListener</tt> from the <tt>Call</tt>s
- * participating in this telephony conference.
- *
- * @param listener the <tt>CallChangeListener</tt> to be removed from the
- * <tt>Call</tt>s participating in this telephony conference
- * @see #addCallChangeListener(CallChangeListener)
- */
- public void removeCallChangeListener(CallChangeListener listener)
- {
- if (listener != null)
- {
- synchronized (callChangeListeners)
- {
- callChangeListeners.remove(listener);
- }
- }
- }
-
- /**
- * Removes {@link #callPeerConferenceListener} from the <tt>CallPeer</tt>s
- * associated with a specific <tt>Call</tt>.
- *
- * @param call the <tt>Call</tt> from whose associated <tt>CallPeer</tt>s
- * <tt>callPeerConferenceListener</tt> is to be removed
- */
- private void removeCallPeerConferenceListener(Call call)
- {
- Iterator<? extends CallPeer> callPeerIter = call.getCallPeers();
-
- while (callPeerIter.hasNext())
- {
- callPeerIter.next().removeCallPeerConferenceListener(
- callPeerConferenceListener);
- }
- }
-
- /**
- * Removes a <tt>CallPeerConferenceListener</tt> from the <tt>CallPeer</tt>s
- * associated with the <tt>Call</tt>s participating in this telephony
- * conference.
- *
- * @param listener the <tt>CallPeerConferenceListener</tt> to be removed
- * from the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s
- * participating in this telephony conference
- * @see #addCallPeerConferenceListener(CallPeerConferenceListener)
- */
- public void removeCallPeerConferenceListener(
- CallPeerConferenceListener listener)
- {
- if (listener != null)
- {
- synchronized (callPeerConferenceListeners)
- {
- callPeerConferenceListeners.remove(listener);
- }
- }
- }
-
- /**
- * Sets the indicator which determines whether the local peer represented by
- * this instance and the <tt>Call</tt>s participating in it is acting as a
- * conference focus (and thus may, for example, need to send the
- * corresponding parameters in its outgoing signaling).
- *
- * @param conferenceFocus <tt>true</tt> if the local peer represented by
- * this instance and the <tt>Call</tt>s participating in it is to act as a
- * conference focus; otherwise, <tt>false</tt>
- */
- public void setConferenceFocus(boolean conferenceFocus)
- {
- if (this.conferenceFocus != conferenceFocus)
- {
- boolean oldValue = isConferenceFocus();
-
- this.conferenceFocus = conferenceFocus;
-
- boolean newValue = isConferenceFocus();
-
- if (oldValue != newValue)
- conferenceFocusChanged(oldValue, newValue);
- }
- }
-}
+package net.java.sip.communicator.service.protocol; + +import java.util.*; + +import net.java.sip.communicator.service.protocol.event.*; + +import org.jitsi.util.event.*; + +/** + * Represents the telephony conference-related state of a <tt>Call</tt>. + * Multiple <tt>Call</tt> instances share a single <tt>CallConference</tt> + * instance when the former are into a telephony conference i.e. the local + * peer/user is the conference focus. <tt>CallConference</tt> is + * protocol-agnostic and thus enables cross-protocol conferences. Since a + * non-conference <tt>Call</tt> may be converted into a conference <tt>Call</tt> + * at any time, every <tt>Call</tt> instance maintains a <tt>CallConference</tt> + * instance regardless of whether the <tt>Call</tt> in question is participating + * in a telephony conference. + * + * @author Lyubomir Marinov + */ +public class CallConference + extends PropertyChangeNotifier +{ + /** + * The name of the <tt>CallConference</tt> property which specifies the list + * of <tt>Call</tt>s participating in a telephony conference. A change in + * the value of the property is delivered in the form of a + * <tt>PropertyChangeEvent</tt> which has its <tt>oldValue</tt> or + * <tt>newValue</tt> set to the <tt>Call</tt> which has been removed or + * added to the list of <tt>Call</tt>s participating in the telephony + * conference. + */ + public static final String CALLS = "calls"; + + /** + * Gets the number of <tt>CallPeer</tt>s associated with the <tt>Call</tt>s + * participating in the telephony conference-related state of a specific + * <tt>Call</tt>. + * + * @param call the <tt>Call</tt> for which the number of <tt>CallPeer</tt>s + * associated with the <tt>Call</tt>s participating in its associated + * telephony conference-related state + * @return the number of <tt>CallPeer</tt>s associated with the + * <tt>Call</tt>s participating in the telephony conference-related state + * of the specified <tt>Call</tt> + */ + public static int getCallPeerCount(Call call) + { + CallConference conference = call.getConference(); + + /* + * A Call instance is supposed to always maintain a CallConference + * instance. Anyway, if it turns out that it is not the case, we will + * consider the Call as a representation of a telephony conference. + */ + return + (conference == null) + ? call.getCallPeerCount() + : conference.getCallPeerCount(); + } + + /** + * Gets a list of the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s + * participating in the telephony conference in which a specific + * <tt>Call</tt> is participating. + * + * @param call the <tt>Call</tt> which specifies the telephony conference + * the <tt>CallPeer</tt>s of which are to be retrieved + * @return a list of the <tt>CallPeer</tt>s associated with the + * <tt>Call</tt>s participating in the telephony conference in which the + * specified <tt>call</tt> is participating + */ + public static List<CallPeer> getCallPeers(Call call) + { + CallConference conference = call.getConference(); + List<CallPeer> callPeers = new ArrayList<CallPeer>(); + + if (conference == null) + { + Iterator<? extends CallPeer> callPeerIt = call.getCallPeers(); + + while (callPeerIt.hasNext()) + callPeers.add(callPeerIt.next()); + } + else + conference.getCallPeers(callPeers); + return callPeers; + } + + /** + * Gets the list of <tt>Call</tt>s participating in the telephony conference + * in which a specific <tt>Call</tt> is participating. + * + * @param call the <tt>Call</tt> which participates in the telephony + * conference the list of participating <tt>Call</tt>s of which is to be + * returned + * @return the list of <tt>Call</tt>s participating in the telephony + * conference in which the specified <tt>call</tt> is participating + */ + public static List<Call> getCalls(Call call) + { + CallConference conference = call.getConference(); + List<Call> calls; + + if (conference == null) + calls = Collections.emptyList(); + else + calls = conference.getCalls(); + return calls; + } + + /** + * Determines whether a <tt>CallConference</tt> is to report the local + * peer/user as a conference focus judging by a specific list of + * <tt>Call</tt>s. + * + * @param calls the list of <tt>Call</tt> which are to be judged whether + * the local peer/user that they represent is to be considered as a + * conference focus + * @return <tt>true</tt> if the local peer/user represented by the specified + * <tt>calls</tt> is judged to be a conference focus; otherwise, + * <tt>false</tt> + */ + private static boolean isConferenceFocus(List<Call> calls) + { + int callCount = calls.size(); + boolean conferenceFocus; + + if (callCount < 1) + conferenceFocus = false; + else if (callCount > 1) + conferenceFocus = true; + else + conferenceFocus = (calls.get(0).getCallPeerCount() > 1); + return conferenceFocus; + } + + /** + * The <tt>CallChangeListener</tt> which listens to changes in the + * <tt>Call</tt>s participating in this telephony conference. + */ + private final CallChangeListener callChangeListener + = new CallChangeListener() + { + @Override + public void callPeerAdded(CallPeerEvent ev) + { + CallConference.this.onCallPeerEvent(ev); + } + + @Override + public void callPeerRemoved(CallPeerEvent ev) + { + CallConference.this.onCallPeerEvent(ev); + } + + @Override + public void callStateChanged(CallChangeEvent ev) + { + CallConference.this.callStateChanged(ev); + } + }; + + /** + * The list of <tt>CallChangeListener</tt>s added to the <tt>Call</tt>s + * participating in this telephony conference via + * {@link #addCallChangeListener(CallChangeListener)}. + */ + private final List<CallChangeListener> callChangeListeners + = new LinkedList<CallChangeListener>(); + + /** + * The <tt>CallPeerConferenceListener</tt> which listens to the + * <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating in + * this telephony conference. + */ + private final CallPeerConferenceListener callPeerConferenceListener + = new CallPeerConferenceAdapter() + { + /** + * {@inheritDoc} + * + * Invokes + * {@link CallConference#onCallPeerConferenceEvent( + * CallPeerConferenceEvent)}. + */ + @Override + protected void onCallPeerConferenceEvent(CallPeerConferenceEvent ev) + { + CallConference.this.onCallPeerConferenceEvent(ev); + } + + /** + * {@inheritDoc} + * + * Invokes + * {@link CallConference#onCallPeerConferenceEvent( + * CallPeerConferenceEvent)}. + */ + @Override + public void conferenceMemberErrorReceived( + CallPeerConferenceEvent ev) + { + CallConference.this.onCallPeerConferenceEvent(ev); + } + }; + + /** + * The list of <tt>CallPeerConferenceListener</tt>s added to the + * <tt>CallPeer</tt>s associated with the <tt>CallPeer</tt>s participating + * in this telephony conference via + * {@link #addCallPeerConferenceListener}. + */ + private final List<CallPeerConferenceListener> callPeerConferenceListeners + = new LinkedList<CallPeerConferenceListener>(); + + /** + * The synchronization root/<tt>Object</tt> which protects the access to + * {@link #immutableCalls} and {@link #mutableCalls}. + */ + private final Object callsSyncRoot = new Object(); + + /** + * The indicator which determines whether the local peer represented by this + * instance and the <tt>Call</tt>s participating in it is acting as a + * conference focus. The SIP protocol, for example, will add the + * "isfocus" parameter to the Contact headers of its outgoing + * signaling if <tt>true</tt>. + */ + private boolean conferenceFocus = false; + + /** + * The list of <tt>Call</tt>s participating in this telephony conference as + * an immutable <tt>List</tt> which can be exposed out of this instance + * without the need to make a copy. In other words, it is an unmodifiable + * view of {@link #mutableCalls}. + */ + private List<Call> immutableCalls; + + /** + * The indicator which determines whether the telephony conference + * represented by this instance is utilizing the Jitsi Videobridge + * server-side telephony conferencing technology. + */ + private final boolean jitsiVideobridge; + + /** + * The list of <tt>Call</tt>s participating in this telephony conference as + * a mutable <tt>List</tt> which should not be exposed out of this instance. + */ + private List<Call> mutableCalls; + + /** + * Initializes a new <tt>CallConference</tt> instance. + */ + public CallConference() + { + this(false); + } + + /** + * Initializes a new <tt>CallConference</tt> instance which is to optionally + * utilize the Jitsi Videobridge server-side telephony conferencing + * technology. + * + * @param jitsiVideobridge <tt>true</tt> if the telephony conference + * represented by the new instance is to utilize the Jitsi Videobridge + * server-side telephony conferencing technology; otherwise, <tt>false</tt> + */ + public CallConference(boolean jitsiVideobridge) + { + this.jitsiVideobridge = jitsiVideobridge; + + mutableCalls = new ArrayList<Call>(); + immutableCalls = Collections.unmodifiableList(mutableCalls); + } + + /** + * Adds a specific <tt>Call</tt> to the list of <tt>Call</tt>s participating + * in this telephony conference. + * + * @param call the <tt>Call</tt> to add to the list of <tt>Call</tt>s + * participating in this telephony conference + * @return <tt>true</tt> if the list of <tt>Call</tt>s participating in this + * telephony conference changed as a result of the method call; otherwise, + * <tt>false</tt> + * @throws NullPointerException if <tt>call</tt> is <tt>null</tt> + */ + boolean addCall(Call call) + { + if (call == null) + throw new NullPointerException("call"); + + synchronized (callsSyncRoot) + { + if (mutableCalls.contains(call)) + return false; + + /* + * Implement the List of Calls participating in this telephony + * conference as a copy-on-write storage in order to optimize the + * getCalls method which is likely to be executed much more often + * than the addCall and removeCall methods. + */ + List<Call> newMutableCalls = new ArrayList<Call>(mutableCalls); + + if (newMutableCalls.add(call)) + { + mutableCalls = newMutableCalls; + immutableCalls = Collections.unmodifiableList(mutableCalls); + } + else + return false; + } + + callAdded(call); + return true; + } + + /** + * Adds a <tt>CallChangeListener</tt> to the <tt>Call</tt>s participating in + * this telephony conference. The method is a convenience that takes on the + * responsibility of tracking the <tt>Call</tt>s that get added/removed + * to/from this telephony conference. + * + * @param listener the <tt>CallChangeListner</tt> to be added to the + * <tt>Call</tt>s participating in this telephony conference + * @throws NullPointerException if <tt>listener</tt> is <tt>null</tt> + */ + public void addCallChangeListener(CallChangeListener listener) + { + if (listener == null) + throw new NullPointerException("listener"); + else + { + synchronized (callChangeListeners) + { + if (!callChangeListeners.contains(listener)) + callChangeListeners.add(listener); + } + } + } + + /** + * Adds {@link #callPeerConferenceListener} to the <tt>CallPeer</tt>s + * associated with a specific <tt>Call</tt>. + * + * @param call the <tt>Call</tt> to whose associated <tt>CallPeer</tt>s + * <tt>callPeerConferenceListener</tt> is to be added + */ + private void addCallPeerConferenceListener(Call call) + { + Iterator<? extends CallPeer> callPeerIter = call.getCallPeers(); + + while (callPeerIter.hasNext()) + { + callPeerIter.next().addCallPeerConferenceListener( + callPeerConferenceListener); + } + } + + /** + * Adds a <tt>CallPeerConferenceListener</tt> to the <tt>CallPeer</tt>s + * associated with the <tt>Call</tt>s participating in this telephony + * conference. The method is a convenience that takes on the responsibility + * of tracking the <tt>Call</tt>s that get added/removed to/from this + * telephony conference and the <tt>CallPeer</tt> that get added/removed + * to/from these <tt>Call</tt>s. + * + * @param listener the <tt>CallPeerConferenceListener</tt> to be added to + * the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating + * in this telephony conference + * @throws NullPointerException if <tt>listener</tt> is <tt>null</tt> + */ + public void addCallPeerConferenceListener( + CallPeerConferenceListener listener) + { + if (listener == null) + throw new NullPointerException("listener"); + else + { + synchronized (callPeerConferenceListeners) + { + if (!callPeerConferenceListeners.contains(listener)) + callPeerConferenceListeners.add(listener); + } + } + } + + /** + * Notifies this <tt>CallConference</tt> that a specific <tt>Call</tt> has + * been added to the list of <tt>Call</tt>s participating in this telephony + * conference. + * + * @param call the <tt>Call</tt> which has been added to the list of + * <tt>Call</tt>s participating in this telephony conference + */ + protected void callAdded(Call call) + { + call.addCallChangeListener(callChangeListener); + addCallPeerConferenceListener(call); + + /* + * Update the conferenceFocus state. Because the public + * setConferenceFocus method allows forcing a specific value on the + * state in question and because it does not sound right to have the + * adding of a Call set conferenceFocus to false, only update it if the + * new conferenceFocus value is true, + */ + boolean conferenceFocus = isConferenceFocus(getCalls()); + + if (conferenceFocus) + setConferenceFocus(conferenceFocus); + + firePropertyChange(CALLS, null, call); + } + + /** + * Notifies this <tt>CallConference</tt> that a specific <tt>Call</tt> has + * been removed from the list of <tt>Call</tt>s participating in this + * telephony conference. + * + * @param call the <tt>Call</tt> which has been removed from the list of + * <tt>Call</tt>s participating in this telephony conference + */ + protected void callRemoved(Call call) + { + call.removeCallChangeListener(callChangeListener); + removeCallPeerConferenceListener(call); + + /* + * Update the conferenceFocus state. Following the line of thinking + * expressed in the callAdded method, only update it if the new + * conferenceFocus value is false. + */ + boolean conferenceFocus = isConferenceFocus(getCalls()); + + if (!conferenceFocus) + setConferenceFocus(conferenceFocus); + + firePropertyChange(CALLS, call, null); + } + + /** + * Notifies this telephony conference that the <tt>CallState</tt> of a + * <tt>Call</tt> has changed. + * + * @param ev a <tt>CallChangeEvent</tt> which specifies the <tt>Call</tt> + * which had its <tt>CallState</tt> changed and the old and new + * <tt>CallState</tt>s of that <tt>Call</tt> + */ + private void callStateChanged(CallChangeEvent ev) + { + Call call = ev.getSourceCall(); + + if (containsCall(call)) + { + try + { + // Forward the CallChangeEvent to the callChangeListeners. + for (CallChangeListener l : getCallChangeListeners()) + l.callStateChanged(ev); + } + finally + { + if (CallChangeEvent.CALL_STATE_CHANGE + .equals(ev.getPropertyName()) + && CallState.CALL_ENDED.equals(ev.getNewValue())) + { + /* + * Should not be vital because Call will remove itself. + * Anyway, do it for the sake of completeness. + */ + removeCall(call); + } + } + } + } + + /** + * Notifies this <tt>CallConference</tt> that the value of its + * <tt>conferenceFocus</tt> property has changed from a specific old value + * to a specific new value. + * + * @param oldValue the value of the <tt>conferenceFocus</tt> property of + * this instance before the change + * @param newValue the value of the <tt>conferenceFocus</tt> property of + * this instance after the change + */ + protected void conferenceFocusChanged(boolean oldValue, boolean newValue) + { + firePropertyChange(Call.CONFERENCE_FOCUS, oldValue, newValue); + } + + /** + * Determines whether a specific <tt>Call</tt> is participating in this + * telephony conference. + * + * @param call the <tt>Call</tt> which is to be checked whether it is + * participating in this telephony conference + * @return <tt>true</tt> if the specified <tt>call</tt> is participating in + * this telephony conference + */ + public boolean containsCall(Call call) + { + synchronized (callsSyncRoot) + { + return mutableCalls.contains(call); + } + } + + /** + * Gets the list of <tt>CallChangeListener</tt>s added to the <tt>Call</tt>s + * participating in this telephony conference via + * {@link #addCallChangeListener(CallChangeListener)}. + * + * @return the list of <tt>CallChangeListener</tt>s added to the + * <tt>Call</tt>s participating in this telephony conference via + * {@link #addCallChangeListener(CallChangeListener)} + */ + private CallChangeListener[] getCallChangeListeners() + { + synchronized (callChangeListeners) + { + return + callChangeListeners.toArray( + new CallChangeListener[callChangeListeners.size()]); + } + } + + /** + * Gets the number of <tt>Call</tt>s that are participating in this + * telephony conference. + * + * @return the number of <tt>Call</tt>s that are participating in this + * telephony conference + */ + public int getCallCount() + { + synchronized (callsSyncRoot) + { + return mutableCalls.size(); + } + } + + /** + * Gets the list of <tt>CallPeerConferenceListener</tt>s added to the + * <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating in + * this telephony conference via + * {@link #addCallPeerConferenceListener(CallPeerConferenceListener)}. + * + * @return the list of <tt>CallPeerConferenceListener</tt>s added to the + * <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating in + * this telephony conference via + * {@link #addCallPeerConferenceListener(CallPeerConferenceListener)} + */ + private CallPeerConferenceListener[] getCallPeerConferenceListeners() + { + synchronized (callPeerConferenceListeners) + { + return + callPeerConferenceListeners.toArray( + new CallPeerConferenceListener[ + callPeerConferenceListeners.size()]); + } + } + + /** + * Gets the number of <tt>CallPeer</tt>s associated with the <tt>Call</tt>s + * participating in this telephony conference. + * + * @return the number of <tt>CallPeer</tt>s associated with the + * <tt>Call</tt>s participating in this telephony conference + */ + public int getCallPeerCount() + { + int callPeerCount = 0; + + for (Call call : getCalls()) + callPeerCount += call.getCallPeerCount(); + return callPeerCount; + } + + /** + * Gets a list of the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s + * participating in this telephony conference. + * + * @return a list of the <tt>CallPeer</tt>s associated with the + * <tt>Call</tt>s participating in this telephony conference + */ + public List<CallPeer> getCallPeers() + { + List<CallPeer> callPeers = new ArrayList<CallPeer>(); + + getCallPeers(callPeers); + return callPeers; + } + + /** + * Adds the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s + * participating in this telephony conference into a specific <tt>List</tt>. + * + * @param callPeers a <tt>List</tt> into which the <tt>CallPeer</tt>s + * associated with the <tt>Call</tt>s participating in this telephony + * conference are to be added + */ + protected void getCallPeers(List<CallPeer> callPeers) + { + for (Call call : getCalls()) + { + Iterator<? extends CallPeer> callPeerIt = call.getCallPeers(); + + while (callPeerIt.hasNext()) + callPeers.add(callPeerIt.next()); + } + } + + /** + * Gets the list of <tt>Call</tt> participating in this telephony + * conference. + * + * @return the list of <tt>Call</tt>s participating in this telephony + * conference. An empty array of <tt>Call</tt> element type is returned if + * there are no <tt>Call</tt>s in this telephony conference-related state. + */ + public List<Call> getCalls() + { + synchronized (callsSyncRoot) + { + return immutableCalls; + } + } + + /** + * Determines whether the local peer/user associated with this instance and + * represented by the <tt>Call</tt>s participating into it is acting as a + * conference focus. + * + * @return <tt>true</tt> if the local peer/user associated by this instance + * is acting as a conference focus; otherwise, <tt>false</tt> + */ + public boolean isConferenceFocus() + { + return conferenceFocus; + } + + /** + * Determines whether the current state of this instance suggests that the + * telephony conference it represents has ended. Iterates over the + * <tt>Call</tt>s participating in this telephony conference and looks for a + * <tt>Call</tt> which is not in the {@link CallState#CALL_ENDED} state. + * + * @return <tt>true</tt> if the current state of this instance suggests that + * the telephony conference it represents has ended; otherwise, + * <tt>false</tt> + */ + public boolean isEnded() + { + for (Call call : getCalls()) + { + if (!CallState.CALL_ENDED.equals(call.getCallState())) + return false; + } + return true; + } + + /** + * Determines whether the telephony conference represented by this instance + * is utilizing the Jitsi Videobridge server-side telephony conferencing + * technology. + * + * @return <tt>true</tt> if the telephony conference represented by this + * instance is utilizing the Jitsi Videobridge server-side telephony + * conferencing technology + */ + public boolean isJitsiVideobridge() + { + return jitsiVideobridge; + } + + /** + * Notifies this telephony conference that a + * <tt>CallPeerConferenceEvent</tt> was fired by a <tt>CallPeer</tt> + * associated with a <tt>Call</tt> participating in this telephony + * conference. Forwards the specified <tt>CallPeerConferenceEvent</tt> to + * {@link #callPeerConferenceListeners}. + * + * @param ev the <tt>CallPeerConferenceEvent</tt> which was fired + */ + private void onCallPeerConferenceEvent(CallPeerConferenceEvent ev) + { + int eventID = ev.getEventID(); + + for (CallPeerConferenceListener l : getCallPeerConferenceListeners()) + { + switch (eventID) + { + case CallPeerConferenceEvent.CONFERENCE_FOCUS_CHANGED: + l.conferenceFocusChanged(ev); + break; + case CallPeerConferenceEvent.CONFERENCE_MEMBER_ADDED: + l.conferenceMemberAdded(ev); + break; + case CallPeerConferenceEvent.CONFERENCE_MEMBER_REMOVED: + l.conferenceMemberRemoved(ev); + break; + case CallPeerConferenceEvent.CONFERENCE_MEMBER_ERROR_RECEIVED: + l.conferenceMemberErrorReceived(ev); + break; + default: + throw new UnsupportedOperationException( + "Unsupported CallPeerConferenceEvent eventID."); + } + } + } + + /** + * Notifies this telephony conference about a specific + * <tt>CallPeerEvent</tt> i.e. that a <tt>CallPeer</tt> was either added to + * or removed from a <tt>Call</tt>. + * + * @param ev a <tt>CallPeerEvent</tt> which specifies the <tt>CallPeer</tt> + * which was added or removed and the <tt>Call</tt> to which it was added or + * from which is was removed + */ + private void onCallPeerEvent(CallPeerEvent ev) + { + Call call = ev.getSourceCall(); + + if (containsCall(call)) + { + /* + * Update the conferenceFocus state. Following the line of thinking + * expressed in the callAdded and callRemoved methods, only update + * it if the new conferenceFocus value is in accord with the + * expectations. + */ + int eventID = ev.getEventID(); + boolean conferenceFocus = isConferenceFocus(getCalls()); + + switch (eventID) + { + case CallPeerEvent.CALL_PEER_ADDED: + if (conferenceFocus) + setConferenceFocus(conferenceFocus); + break; + case CallPeerEvent.CALL_PEER_REMOVED: + if (!conferenceFocus) + setConferenceFocus(conferenceFocus); + break; + default: + /* + * We're interested in the adding and removing of CallPeers + * only. + */ + break; + } + + try + { + // Forward the CallPeerEvent to the callChangeListeners. + for (CallChangeListener l : getCallChangeListeners()) + { + switch (eventID) + { + case CallPeerEvent.CALL_PEER_ADDED: + l.callPeerAdded(ev); + break; + case CallPeerEvent.CALL_PEER_REMOVED: + l.callPeerRemoved(ev); + break; + default: + break; + } + } + } + finally + { + /* + * Add/remove the callPeerConferenceListener to/from the source + * CallPeer (for the purposes of the + * addCallPeerConferenceListener method of this CallConference). + */ + CallPeer callPeer = ev.getSourceCallPeer(); + + switch (eventID) + { + case CallPeerEvent.CALL_PEER_ADDED: + callPeer.addCallPeerConferenceListener( + callPeerConferenceListener); + break; + case CallPeerEvent.CALL_PEER_REMOVED: + callPeer.removeCallPeerConferenceListener( + callPeerConferenceListener); + break; + default: + break; + } + } + } + } + + /** + * Removes a specific <tt>Call</tt> from the list of <tt>Call</tt>s + * participating in this telephony conference. + * + * @param call the <tt>Call</tt> to remove from the list of <tt>Call</tt>s + * participating in this telephony conference + * @return <tt>true</tt> if the list of <tt>Call</tt>s participating in this + * telephony conference changed as a result of the method call; otherwise, + * <tt>false</tt> + */ + boolean removeCall(Call call) + { + if (call == null) + return false; + + synchronized (callsSyncRoot) + { + if (!mutableCalls.contains(call)) + return false; + + /* + * Implement the List of Calls participating in this telephony + * conference as a copy-on-write storage in order to optimize the + * getCalls method which is likely to be executed much more often + * than the addCall and removeCall methods. + */ + List<Call> newMutableCalls = new ArrayList<Call>(mutableCalls); + + if (newMutableCalls.remove(call)) + { + mutableCalls = newMutableCalls; + immutableCalls = Collections.unmodifiableList(mutableCalls); + } + else + return false; + } + + callRemoved(call); + return true; + } + + /** + * Removes a <tt>CallChangeListener</tt> from the <tt>Call</tt>s + * participating in this telephony conference. + * + * @param listener the <tt>CallChangeListener</tt> to be removed from the + * <tt>Call</tt>s participating in this telephony conference + * @see #addCallChangeListener(CallChangeListener) + */ + public void removeCallChangeListener(CallChangeListener listener) + { + if (listener != null) + { + synchronized (callChangeListeners) + { + callChangeListeners.remove(listener); + } + } + } + + /** + * Removes {@link #callPeerConferenceListener} from the <tt>CallPeer</tt>s + * associated with a specific <tt>Call</tt>. + * + * @param call the <tt>Call</tt> from whose associated <tt>CallPeer</tt>s + * <tt>callPeerConferenceListener</tt> is to be removed + */ + private void removeCallPeerConferenceListener(Call call) + { + Iterator<? extends CallPeer> callPeerIter = call.getCallPeers(); + + while (callPeerIter.hasNext()) + { + callPeerIter.next().removeCallPeerConferenceListener( + callPeerConferenceListener); + } + } + + /** + * Removes a <tt>CallPeerConferenceListener</tt> from the <tt>CallPeer</tt>s + * associated with the <tt>Call</tt>s participating in this telephony + * conference. + * + * @param listener the <tt>CallPeerConferenceListener</tt> to be removed + * from the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s + * participating in this telephony conference + * @see #addCallPeerConferenceListener(CallPeerConferenceListener) + */ + public void removeCallPeerConferenceListener( + CallPeerConferenceListener listener) + { + if (listener != null) + { + synchronized (callPeerConferenceListeners) + { + callPeerConferenceListeners.remove(listener); + } + } + } + + /** + * Sets the indicator which determines whether the local peer represented by + * this instance and the <tt>Call</tt>s participating in it is acting as a + * conference focus (and thus may, for example, need to send the + * corresponding parameters in its outgoing signaling). + * + * @param conferenceFocus <tt>true</tt> if the local peer represented by + * this instance and the <tt>Call</tt>s participating in it is to act as a + * conference focus; otherwise, <tt>false</tt> + */ + public void setConferenceFocus(boolean conferenceFocus) + { + if (this.conferenceFocus != conferenceFocus) + { + boolean oldValue = isConferenceFocus(); + + this.conferenceFocus = conferenceFocus; + + boolean newValue = isConferenceFocus(); + + if (oldValue != newValue) + conferenceFocusChanged(oldValue, newValue); + } + } +} diff --git a/src/net/java/sip/communicator/service/protocol/OperationSetBasicInstantMessaging.java b/src/net/java/sip/communicator/service/protocol/OperationSetBasicInstantMessaging.java index c0918cb..55dec9a 100644 --- a/src/net/java/sip/communicator/service/protocol/OperationSetBasicInstantMessaging.java +++ b/src/net/java/sip/communicator/service/protocol/OperationSetBasicInstantMessaging.java @@ -80,22 +80,6 @@ public interface OperationSetBasicInstantMessaging String messageText, String contentType, String messageUID); /** - * Create a Message instance with the specified UID and a default - * (text/plain) content type and encoding. - * This method can be useful when message correction is required. One can - * construct the corrected message to have the same UID as the message - * before correction. - * - * @param messageText the string content of the message. - * @param messageUID the unique identifier of this message. - * @return Message the newly created message - * - * @deprecated Method will be removed once OTR bundle is updated on Android. - */ - @Deprecated - public Message createMessageWithUID(String messageText, String messageUID); - - /** * Sends the <tt>message</tt> to the destination indicated by the * <tt>to</tt> contact. * diff --git a/src/net/java/sip/communicator/service/protocol/OperationSetJitsiMeetTools.java b/src/net/java/sip/communicator/service/protocol/OperationSetJitsiMeetTools.java index 08c8cff..41af2b4 100644 --- a/src/net/java/sip/communicator/service/protocol/OperationSetJitsiMeetTools.java +++ b/src/net/java/sip/communicator/service/protocol/OperationSetJitsiMeetTools.java @@ -56,6 +56,17 @@ public interface OperationSetJitsiMeetTools PacketExtension extension); /** + * Removes given <tt>PacketExtension</tt> from the multi user chat presence + * and sends presence update packet to the chat room. + * @param chatRoom the <tt>ChatRoom</tt> for which the presence will be + * updated. + * @param extension the <tt>PacketExtension</tt> to be removed from the MUC + * presence. + */ + public void removePresenceExtension(ChatRoom chatRoom, + PacketExtension extension); + + /** * Sets the status message of our MUC presence and sends presence status * update packet to the server. * @param chatRoom the <tt>ChatRoom</tt> for which the presence status diff --git a/src/net/java/sip/communicator/service/protocol/OperationSetSecureSDesTelephony.java b/src/net/java/sip/communicator/service/protocol/OperationSetSecureSDesTelephony.java index e0c75fd..f336de1 100644 --- a/src/net/java/sip/communicator/service/protocol/OperationSetSecureSDesTelephony.java +++ b/src/net/java/sip/communicator/service/protocol/OperationSetSecureSDesTelephony.java @@ -1,4 +1,4 @@ -/*
+/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd @@ -15,15 +15,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package net.java.sip.communicator.service.protocol;
-
-/**
- * Marker interface to indicate that a protocol supports SDES encryption.
- *
- * @author Ingo Bauersachs
- */
-public interface OperationSetSecureSDesTelephony
- extends OperationSetSecureTelephony
-{
-
-}
+package net.java.sip.communicator.service.protocol; + +/** + * Marker interface to indicate that a protocol supports SDES encryption. + * + * @author Ingo Bauersachs + */ +public interface OperationSetSecureSDesTelephony + extends OperationSetSecureTelephony +{ + +} diff --git a/src/net/java/sip/communicator/service/protocol/OperationSetSecureZrtpTelephony.java b/src/net/java/sip/communicator/service/protocol/OperationSetSecureZrtpTelephony.java index 7213af4..eb5dcdd 100644 --- a/src/net/java/sip/communicator/service/protocol/OperationSetSecureZrtpTelephony.java +++ b/src/net/java/sip/communicator/service/protocol/OperationSetSecureZrtpTelephony.java @@ -1,4 +1,4 @@ -/*
+/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd @@ -15,15 +15,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package net.java.sip.communicator.service.protocol;
-
-/**
- * Marker interface to indicate that a protocol supports ZRTP encryption.
- *
- * @author Ingo Bauersachs
- */
-public interface OperationSetSecureZrtpTelephony
- extends OperationSetSecureTelephony
-{
-
-}
+package net.java.sip.communicator.service.protocol; + +/** + * Marker interface to indicate that a protocol supports ZRTP encryption. + * + * @author Ingo Bauersachs + */ +public interface OperationSetSecureZrtpTelephony + extends OperationSetSecureTelephony +{ + +} diff --git a/src/net/java/sip/communicator/service/protocol/OperationSetTelephonyBLF.java b/src/net/java/sip/communicator/service/protocol/OperationSetTelephonyBLF.java index 028c2ae..f6a50dc 100644 --- a/src/net/java/sip/communicator/service/protocol/OperationSetTelephonyBLF.java +++ b/src/net/java/sip/communicator/service/protocol/OperationSetTelephonyBLF.java @@ -80,6 +80,11 @@ public interface OperationSetTelephonyBLF private String group; /** + * Asterisk pickup prefix. + */ + private String pickupTemplate; + + /** * The parent provider. */ private ProtocolProviderService provider; @@ -90,14 +95,16 @@ public interface OperationSetTelephonyBLF * @param address the address of the line. * @param name the display name if any * @param group the group name if any + * @param pickup the pickup dial template * @param provider the parent provider. */ - public Line(String address, String name, String group, + public Line(String address, String name, String group, String pickup, ProtocolProviderService provider) { this.address = address; this.name = name; this.group = group; + this.pickupTemplate = pickup; this.provider = provider; } @@ -129,6 +136,15 @@ public interface OperationSetTelephonyBLF } /** + * The pickup template. + * @return the pickup template. + */ + public String getPickupTemplate() + { + return pickupTemplate; + } + + /** * The provider. * @return the provider. */ diff --git a/src/net/java/sip/communicator/service/protocol/PhoneNumberI18nService.java b/src/net/java/sip/communicator/service/protocol/PhoneNumberI18nService.java index df71e0c..521afc5 100644 --- a/src/net/java/sip/communicator/service/protocol/PhoneNumberI18nService.java +++ b/src/net/java/sip/communicator/service/protocol/PhoneNumberI18nService.java @@ -1,4 +1,4 @@ -/*
+/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd @@ -15,49 +15,59 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package net.java.sip.communicator.service.protocol;
-
-/**
- * Implements <tt>PhoneNumberI18nService</tt> which aids the parsing, formatting
- * and validating of international phone numbers.
- *
- * @author Lyubomir Marinov
- * @author Vincent Lucas
- * @author Damian Minkov
- */
-public interface PhoneNumberI18nService
-{
- /**
- * Normalizes a <tt>String</tt> which may be a phone number or a identifier
- * by removing useless characters and, if necessary, replacing the alpahe
- * characters in corresponding dial pad numbers.
- *
- * @param possibleNumber a <tt>String</tt> which may represents a phone
- * number or an identifier to normalize.
- *
- * @return a <tt>String</tt> which is a normalized form of the specified
- * <tt>possibleNumber</tt>.
- */
- public String normalize(String possibleNumber);
-
- /**
- * Determines whether two <tt>String</tt> phone numbers match.
- *
- * @param aPhoneNumber a <tt>String</tt> which represents a phone number to
- * match to <tt>bPhoneNumber</tt>
- * @param bPhoneNumber a <tt>String</tt> which represents a phone number to
- * match to <tt>aPhoneNumber</tt>
- * @return <tt>true</tt> if the specified <tt>String</tt>s match as phone
- * numbers; otherwise, <tt>false</tt>
- */
- public boolean phoneNumbersMatch(String aPhoneNumber, String bPhoneNumber);
-
- /**
- * Indicates if the given string is possibly a phone number.
- *
- * @param possibleNumber the string to be verified
- * @return <tt>true</tt> if the possibleNumber is a phone number,
- * <tt>false</tt> - otherwise
- */
- public boolean isPhoneNumber(String possibleNumber);
-}
+package net.java.sip.communicator.service.protocol; + +/** + * Implements <tt>PhoneNumberI18nService</tt> which aids the parsing, formatting + * and validating of international phone numbers. + * + * @author Lyubomir Marinov + * @author Vincent Lucas + * @author Damian Minkov + */ +public interface PhoneNumberI18nService +{ + /** + * Normalizes a <tt>String</tt> which may be a phone number or a identifier + * by removing useless characters and, if necessary, replacing the alpahe + * characters in corresponding dial pad numbers. + * + * @param possibleNumber a <tt>String</tt> which may represents a phone + * number or an identifier to normalize. + * + * @return a <tt>String</tt> which is a normalized form of the specified + * <tt>possibleNumber</tt>. + */ + public String normalize(String possibleNumber); + + /** + * Tries to format the passed phone number into the international format. If + * parsing fails or the string is not recognized as a valid phone number, + * the input is returned as is. + * + * @param phoneNumber The phone number to format. + * @return the formatted phone number in the international format. + */ + public String formatForDisplay(String phoneNumber); + + /** + * Determines whether two <tt>String</tt> phone numbers match. + * + * @param aPhoneNumber a <tt>String</tt> which represents a phone number to + * match to <tt>bPhoneNumber</tt> + * @param bPhoneNumber a <tt>String</tt> which represents a phone number to + * match to <tt>aPhoneNumber</tt> + * @return <tt>true</tt> if the specified <tt>String</tt>s match as phone + * numbers; otherwise, <tt>false</tt> + */ + public boolean phoneNumbersMatch(String aPhoneNumber, String bPhoneNumber); + + /** + * Indicates if the given string is possibly a phone number. + * + * @param possibleNumber the string to be verified + * @return <tt>true</tt> if the possibleNumber is a phone number, + * <tt>false</tt> - otherwise + */ + public boolean isPhoneNumber(String possibleNumber); +} diff --git a/src/net/java/sip/communicator/service/protocol/ProtocolNames.java b/src/net/java/sip/communicator/service/protocol/ProtocolNames.java index 8332557..ba51c91 100644 --- a/src/net/java/sip/communicator/service/protocol/ProtocolNames.java +++ b/src/net/java/sip/communicator/service/protocol/ProtocolNames.java @@ -44,16 +44,6 @@ public interface ProtocolNames public static final String IRC = "IRC"; /** - * The Gadu-Gadu protocol. - */ - public static final String GADU_GADU = "Gadu-Gadu"; - - /** - * The GroupWise protocol. - */ - public static final String GROUP_WISE = "GroupWise"; - - /** * The ICQ service protocol. */ public static final String ICQ = "ICQ"; @@ -64,26 +54,11 @@ public interface ProtocolNames public static final String AIM = "AIM"; /** - * The Yahoo! messenger protocol. - */ - public static final String YAHOO = "Yahoo!"; - - /** - * The Skype protocol. - */ - public static final String SKYPE = "Skype"; - - /** * The SIP Communicator MOCK protocol. */ public static final String SIP_COMMUNICATOR_MOCK = "sip-communicator-mock"; /** - * The Zeroconf protocol. - */ - public static final String ZEROCONF = "Zeroconf"; - - /** * The SSH protocol. */ public static final String SSH = "SSH"; @@ -92,14 +67,4 @@ public interface ProtocolNames * The Gibberish protocol. */ public static final String GIBBERISH = "Gibberish"; - - /** - * The Dict protocol. - */ - public static final String DICT = "Dict"; - - /** - * The Facebook protocol. - */ - public static final String FACEBOOK = "Facebook"; } diff --git a/src/net/java/sip/communicator/service/protocol/ProtocolProviderActivator.java b/src/net/java/sip/communicator/service/protocol/ProtocolProviderActivator.java index 60cdf5c..a9d7c7c 100644 --- a/src/net/java/sip/communicator/service/protocol/ProtocolProviderActivator.java +++ b/src/net/java/sip/communicator/service/protocol/ProtocolProviderActivator.java @@ -1,4 +1,4 @@ -/*
+/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd @@ -15,295 +15,295 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package net.java.sip.communicator.service.protocol;
-
-import net.java.sip.communicator.service.calendar.*;
-import net.java.sip.communicator.util.*;
-
-import org.jitsi.service.configuration.*;
-import org.jitsi.service.resources.*;
-import org.osgi.framework.*;
-
-import java.util.*;
-
-/**
- * Implements <code>BundleActivator</code> for the purposes of
- * protocol.jar/protocol.provider.manifest.mf and in order to register and start
- * services independent of the specifics of a particular protocol.
- *
- * @author Lubomir Marinov
- * @author Yana Stamcheva
- */
-public class ProtocolProviderActivator
- implements BundleActivator
-{
- /**
- * The object used for logging.
- */
- private final static Logger logger
- = Logger.getLogger(ProtocolProviderActivator.class);
-
- /**
- * The <code>ServiceRegistration</code> of the <code>AccountManager</code>
- * implementation registered as a service by this activator and cached so
- * that the service in question can be properly disposed of upon stopping
- * this activator.
- */
- private ServiceRegistration accountManagerServiceRegistration;
-
- /**
- * The account manager.
- */
- private static AccountManager accountManager;
-
- /**
- * The <code>BundleContext</code> of the one and only
- * <code>ProtocolProviderActivator</code> instance which is currently
- * started.
- */
- private static BundleContext bundleContext;
-
- /**
- * The <code>ConfigurationService</code> used by the classes in the bundle
- * represented by <code>ProtocolProviderActivator</code>.
- */
- private static ConfigurationService configurationService;
-
- /**
- * The resource service through which we obtain localized strings.
- */
- private static ResourceManagementService resourceService;
-
- /**
- * The calendar service instance.
- */
- private static CalendarService calendarService;
-
- /**
- * The <code>SingleCallInProgressPolicy</code> making sure that the
- * <code>Call</code>s accessible in the <code>BundleContext</code> of this
- * activator will obey to the rule that a new <code>Call</code> should put
- * the other existing <code>Call</code>s on hold.
- */
- private SingleCallInProgressPolicy singleCallInProgressPolicy;
-
- /**
- * Gets the <code>ConfigurationService</code> to be used by the classes in
- * the bundle represented by <code>ProtocolProviderActivator</code>.
- *
- * @return the <code>ConfigurationService</code> to be used by the classes
- * in the bundle represented by
- * <code>ProtocolProviderActivator</code>
- */
- public static ConfigurationService getConfigurationService()
- {
- if (configurationService == null)
- {
- configurationService
- = (ConfigurationService)
- bundleContext.getService(
- bundleContext.getServiceReference(
- ConfigurationService.class.getName()));
- }
- return configurationService;
- }
-
- /**
- * Gets the <code>ResourceManagementService</code> to be used by the classes
- * in the bundle represented by <code>ProtocolProviderActivator</code>.
- *
- * @return the <code>ResourceManagementService</code> to be used by the
- * classes in the bundle represented by
- * <code>ProtocolProviderActivator</code>
- */
- public static ResourceManagementService getResourceService()
- {
- if (resourceService == null)
- {
- resourceService
- = (ResourceManagementService)
- bundleContext.getService(
- bundleContext.getServiceReference(
- ResourceManagementService.class.getName()));
- }
- return resourceService;
- }
-
- /**
- * Gets the <code>CalendarService</code> to be used by the classes
- * in the bundle represented by <code>ProtocolProviderActivator</code>.
- *
- * @return the <code>CalendarService</code> to be used by the
- * classes in the bundle represented by
- * <code>ProtocolProviderActivator</code>
- */
- public static CalendarService getCalendarService()
- {
- if (calendarService == null)
- {
- ServiceReference serviceReference
- = bundleContext.getServiceReference(
- CalendarService.class.getName());
- if(serviceReference == null)
- return null;
- calendarService
- = (CalendarService)
- bundleContext.getService(serviceReference);
- }
- return calendarService;
- }
-
- /**
- * Returns a <tt>ProtocolProviderFactory</tt> for a given protocol
- * provider.
- * @param protocolName the name of the protocol, which factory we're
- * looking for
- * @return a <tt>ProtocolProviderFactory</tt> for a given protocol
- * provider
- */
- public static ProtocolProviderFactory getProtocolProviderFactory(
- String protocolName)
- {
- String osgiFilter
- = "(" + ProtocolProviderFactory.PROTOCOL + "=" + protocolName + ")";
- ProtocolProviderFactory protocolProviderFactory = null;
-
- try
- {
- ServiceReference[] serRefs
- = bundleContext.getServiceReferences(
- ProtocolProviderFactory.class.getName(),
- osgiFilter);
-
- if ((serRefs != null) && (serRefs.length != 0))
- {
- protocolProviderFactory
- = (ProtocolProviderFactory)
- bundleContext.getService(serRefs[0]);
- }
- }
- catch (InvalidSyntaxException ex)
- {
- if (logger.isInfoEnabled())
- logger.info("ProtocolProviderActivator : " + ex);
- }
-
- return protocolProviderFactory;
- }
-
- /**
- * Registers a new <code>AccountManagerImpl</code> instance as an
- * <code>AccountManager</code> service and starts a new
- * <code>SingleCallInProgressPolicy</code> instance to ensure that only one
- * of the <code>Call</code>s accessible in the <code>BundleContext</code>
- * in which this activator is to execute will be in progress and the others
- * will automatically be put on hold.
- *
- * @param bundleContext the <code>BundleContext</code> in which the bundle
- * activation represented by this <code>BundleActivator</code>
- * executes
- */
- public void start(BundleContext bundleContext)
- {
- ProtocolProviderActivator.bundleContext = bundleContext;
-
- accountManager = new AccountManager(bundleContext);
- accountManagerServiceRegistration =
- bundleContext.registerService(AccountManager.class.getName(),
- accountManager, null);
- if(logger.isTraceEnabled())
- {
- logger.trace("ProtocolProviderActivator will create "
- + "SingleCallInProgressPolicy instance.");
- }
-
- singleCallInProgressPolicy =
- new SingleCallInProgressPolicy(bundleContext);
- }
-
- /**
- * Unregisters the <code>AccountManagerImpl</code> instance registered as an
- * <code>AccountManager</code> service in {@link #start(BundleContext)} and
- * stops the <code>SingleCallInProgressPolicy</code> started there as well.
- *
- * @param bundleContext the <code>BundleContext</code> in which the bundle
- * activation represented by this <code>BundleActivator</code>
- * executes
- */
- public void stop(BundleContext bundleContext)
- {
- if (accountManagerServiceRegistration != null)
- {
- accountManagerServiceRegistration.unregister();
- accountManagerServiceRegistration = null;
- accountManager = null;
- }
-
- if (singleCallInProgressPolicy != null)
- {
- singleCallInProgressPolicy.dispose();
- singleCallInProgressPolicy = null;
- }
-
- if (bundleContext.equals(ProtocolProviderActivator.bundleContext))
- ProtocolProviderActivator.bundleContext = null;
-
- configurationService = null;
- resourceService = null;
- }
-
- /**
- * Returns all protocol providers currently registered.
- * @return all protocol providers currently registered.
- */
- public static List<ProtocolProviderService>
- getProtocolProviders()
- {
- ServiceReference[] serRefs = null;
- try
- {
- // get all registered provider factories
- serRefs = bundleContext.getServiceReferences(
- ProtocolProviderService.class.getName(),
- null);
- }
- catch (InvalidSyntaxException e)
- {
- logger.error("ProtocolProviderActivator : " + e);
- }
-
- List<ProtocolProviderService>
- providersList = new ArrayList<ProtocolProviderService>();
-
- if (serRefs != null)
- {
- for (ServiceReference serRef : serRefs)
- {
- ProtocolProviderService pp
- = (ProtocolProviderService)bundleContext.getService(serRef);
-
- providersList.add(pp);
- }
- }
- return providersList;
- }
-
- /**
- * Get the <tt>AccountManager</tt> of the protocol.
- *
- * @return <tt>AccountManager</tt> of the protocol
- */
- public static AccountManager getAccountManager()
- {
- return accountManager;
- }
-
- /**
- * Returns OSGI bundle context.
- * @return OSGI bundle context.
- */
- public static BundleContext getBundleContext()
- {
- return bundleContext;
- }
-}
+package net.java.sip.communicator.service.protocol; + +import net.java.sip.communicator.service.calendar.*; +import net.java.sip.communicator.util.*; + +import org.jitsi.service.configuration.*; +import org.jitsi.service.resources.*; +import org.osgi.framework.*; + +import java.util.*; + +/** + * Implements <code>BundleActivator</code> for the purposes of + * protocol.jar/protocol.provider.manifest.mf and in order to register and start + * services independent of the specifics of a particular protocol. + * + * @author Lubomir Marinov + * @author Yana Stamcheva + */ +public class ProtocolProviderActivator + implements BundleActivator +{ + /** + * The object used for logging. + */ + private final static Logger logger + = Logger.getLogger(ProtocolProviderActivator.class); + + /** + * The <code>ServiceRegistration</code> of the <code>AccountManager</code> + * implementation registered as a service by this activator and cached so + * that the service in question can be properly disposed of upon stopping + * this activator. + */ + private ServiceRegistration accountManagerServiceRegistration; + + /** + * The account manager. + */ + private static AccountManager accountManager; + + /** + * The <code>BundleContext</code> of the one and only + * <code>ProtocolProviderActivator</code> instance which is currently + * started. + */ + private static BundleContext bundleContext; + + /** + * The <code>ConfigurationService</code> used by the classes in the bundle + * represented by <code>ProtocolProviderActivator</code>. + */ + private static ConfigurationService configurationService; + + /** + * The resource service through which we obtain localized strings. + */ + private static ResourceManagementService resourceService; + + /** + * The calendar service instance. + */ + private static CalendarService calendarService; + + /** + * The <code>SingleCallInProgressPolicy</code> making sure that the + * <code>Call</code>s accessible in the <code>BundleContext</code> of this + * activator will obey to the rule that a new <code>Call</code> should put + * the other existing <code>Call</code>s on hold. + */ + private SingleCallInProgressPolicy singleCallInProgressPolicy; + + /** + * Gets the <code>ConfigurationService</code> to be used by the classes in + * the bundle represented by <code>ProtocolProviderActivator</code>. + * + * @return the <code>ConfigurationService</code> to be used by the classes + * in the bundle represented by + * <code>ProtocolProviderActivator</code> + */ + public static ConfigurationService getConfigurationService() + { + if (configurationService == null) + { + configurationService + = (ConfigurationService) + bundleContext.getService( + bundleContext.getServiceReference( + ConfigurationService.class.getName())); + } + return configurationService; + } + + /** + * Gets the <code>ResourceManagementService</code> to be used by the classes + * in the bundle represented by <code>ProtocolProviderActivator</code>. + * + * @return the <code>ResourceManagementService</code> to be used by the + * classes in the bundle represented by + * <code>ProtocolProviderActivator</code> + */ + public static ResourceManagementService getResourceService() + { + if (resourceService == null) + { + resourceService + = (ResourceManagementService) + bundleContext.getService( + bundleContext.getServiceReference( + ResourceManagementService.class.getName())); + } + return resourceService; + } + + /** + * Gets the <code>CalendarService</code> to be used by the classes + * in the bundle represented by <code>ProtocolProviderActivator</code>. + * + * @return the <code>CalendarService</code> to be used by the + * classes in the bundle represented by + * <code>ProtocolProviderActivator</code> + */ + public static CalendarService getCalendarService() + { + if (calendarService == null) + { + ServiceReference serviceReference + = bundleContext.getServiceReference( + CalendarService.class.getName()); + if(serviceReference == null) + return null; + calendarService + = (CalendarService) + bundleContext.getService(serviceReference); + } + return calendarService; + } + + /** + * Returns a <tt>ProtocolProviderFactory</tt> for a given protocol + * provider. + * @param protocolName the name of the protocol, which factory we're + * looking for + * @return a <tt>ProtocolProviderFactory</tt> for a given protocol + * provider + */ + public static ProtocolProviderFactory getProtocolProviderFactory( + String protocolName) + { + String osgiFilter + = "(" + ProtocolProviderFactory.PROTOCOL + "=" + protocolName + ")"; + ProtocolProviderFactory protocolProviderFactory = null; + + try + { + ServiceReference[] serRefs + = bundleContext.getServiceReferences( + ProtocolProviderFactory.class.getName(), + osgiFilter); + + if ((serRefs != null) && (serRefs.length != 0)) + { + protocolProviderFactory + = (ProtocolProviderFactory) + bundleContext.getService(serRefs[0]); + } + } + catch (InvalidSyntaxException ex) + { + if (logger.isInfoEnabled()) + logger.info("ProtocolProviderActivator : " + ex); + } + + return protocolProviderFactory; + } + + /** + * Registers a new <code>AccountManagerImpl</code> instance as an + * <code>AccountManager</code> service and starts a new + * <code>SingleCallInProgressPolicy</code> instance to ensure that only one + * of the <code>Call</code>s accessible in the <code>BundleContext</code> + * in which this activator is to execute will be in progress and the others + * will automatically be put on hold. + * + * @param bundleContext the <code>BundleContext</code> in which the bundle + * activation represented by this <code>BundleActivator</code> + * executes + */ + public void start(BundleContext bundleContext) + { + ProtocolProviderActivator.bundleContext = bundleContext; + + accountManager = new AccountManager(bundleContext); + accountManagerServiceRegistration = + bundleContext.registerService(AccountManager.class.getName(), + accountManager, null); + if(logger.isTraceEnabled()) + { + logger.trace("ProtocolProviderActivator will create " + + "SingleCallInProgressPolicy instance."); + } + + singleCallInProgressPolicy = + new SingleCallInProgressPolicy(bundleContext); + } + + /** + * Unregisters the <code>AccountManagerImpl</code> instance registered as an + * <code>AccountManager</code> service in {@link #start(BundleContext)} and + * stops the <code>SingleCallInProgressPolicy</code> started there as well. + * + * @param bundleContext the <code>BundleContext</code> in which the bundle + * activation represented by this <code>BundleActivator</code> + * executes + */ + public void stop(BundleContext bundleContext) + { + if (accountManagerServiceRegistration != null) + { + accountManagerServiceRegistration.unregister(); + accountManagerServiceRegistration = null; + accountManager = null; + } + + if (singleCallInProgressPolicy != null) + { + singleCallInProgressPolicy.dispose(); + singleCallInProgressPolicy = null; + } + + if (bundleContext.equals(ProtocolProviderActivator.bundleContext)) + ProtocolProviderActivator.bundleContext = null; + + configurationService = null; + resourceService = null; + } + + /** + * Returns all protocol providers currently registered. + * @return all protocol providers currently registered. + */ + public static List<ProtocolProviderService> + getProtocolProviders() + { + ServiceReference[] serRefs = null; + try + { + // get all registered provider factories + serRefs = bundleContext.getServiceReferences( + ProtocolProviderService.class.getName(), + null); + } + catch (InvalidSyntaxException e) + { + logger.error("ProtocolProviderActivator : " + e); + } + + List<ProtocolProviderService> + providersList = new ArrayList<ProtocolProviderService>(); + + if (serRefs != null) + { + for (ServiceReference serRef : serRefs) + { + ProtocolProviderService pp + = (ProtocolProviderService)bundleContext.getService(serRef); + + providersList.add(pp); + } + } + return providersList; + } + + /** + * Get the <tt>AccountManager</tt> of the protocol. + * + * @return <tt>AccountManager</tt> of the protocol + */ + public static AccountManager getAccountManager() + { + return accountManager; + } + + /** + * Returns OSGI bundle context. + * @return OSGI bundle context. + */ + public static BundleContext getBundleContext() + { + return bundleContext; + } +} diff --git a/src/net/java/sip/communicator/service/protocol/ProtocolProviderFactory.java b/src/net/java/sip/communicator/service/protocol/ProtocolProviderFactory.java index 7e8b9c8..561a092 100644 --- a/src/net/java/sip/communicator/service/protocol/ProtocolProviderFactory.java +++ b/src/net/java/sip/communicator/service/protocol/ProtocolProviderFactory.java @@ -1,4 +1,4 @@ -/*
+/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd @@ -15,1302 +15,1311 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package net.java.sip.communicator.service.protocol;
-
-import java.lang.reflect.*;
-import java.util.*;
-
-import net.java.sip.communicator.service.credentialsstorage.*;
-import net.java.sip.communicator.util.*;
-
-import org.jitsi.service.configuration.*;
-import org.osgi.framework.*;
-
-/**
- * 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 = "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 = "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 = "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 = "FORCE_PROXY_BYPASS";
-
- /**
- * The name of the property that indicates whether the client must
- * be registered with a registrar when making outgoing calls.
- */
- public static final String MUST_REGISTER_TO_CALL = "MUST_REGISTER_TO_CALL";
-
- /**
- * 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";
-
- /**
- * The name of the property that would indicate if a given account
- * configuration form is currently hidden.
- */
- public static final String IS_ACCOUNT_CONFIG_HIDDEN = "IS_CONFIG_HIDDEN";
-
- /**
- * The name of the property that would indicate if a given account
- * status menu is currently hidden.
- */
- public static final String IS_ACCOUNT_STATUS_MENU_HIDDEN =
- "IS_STATUS_MENU_HIDDEN";
-
- /**
- * The name of the property that would indicate if a given account
- * configuration is read only.
- */
- public static final String IS_ACCOUNT_READ_ONLY = "IS_READ_ONLY";
-
- /**
- * The name of the property that would indicate if a given account
- * groups are readonly, values can be all or a comma separated
- * group names including root.
- */
- public static final String ACCOUNT_READ_ONLY_GROUPS = "READ_ONLY_GROUPS";
-
- /**
- * Indicates if ICE should be used.
- */
- public static final String IS_USE_ICE = "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 = "USERNAME";
-
- /**
- * The base property name for password of additional STUN servers specified.
- */
- public static final String STUN_PASSWORD = "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";
-
- /**
- * Indicates if desktop remote control is disabled for a certain account.
- */
- public static final String IS_DESKTOP_REMOTE_CONTROL_DISABLED
- = "DESKTOP_REMOTE_CONTROL_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 name of the property holding DTMF method.
- */
- public static final String DTMF_METHOD = "DTMF_METHOD";
-
- /**
- * 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";
-
- /**
- * An account property to provide a connected account to check for
- * its status. Used when the current provider need to reject calls
- * but is missing presence operation set and need to check other
- * provider for status.
- */
- public static final String CUSAX_PROVIDER_ACCOUNT_PROP
- = "cusax.XMPP_ACCOUNT_ID";
-
- /**
- * 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.
- * </p>
- */
- protected final Map<AccountID, ServiceRegistration<ProtocolProviderService>>
- registeredAccounts
- = new HashMap<AccountID, ServiceRegistration<ProtocolProviderService>>();
-
- /**
- * The name of the property that indicates the AVP type.
- * <ul>
- * <li>{@link #SAVP_OFF}</li>
- * <li>{@link #SAVP_MANDATORY}</li>
- * <li>{@link #SAVP_OPTIONAL}</li>
- * </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";
-
- /**
- * The name of the property that defines the enabled/disabled state of
- * message carbons.
- */
- public static final String IS_CARBON_DISABLED = "CARBON_DISABLED";
-
- /**
- * 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<ProtocolProviderService> getProviderForAccount(
- AccountID accountID)
- {
- ServiceRegistration<ProtocolProviderService> registration;
-
- synchronized (registeredAccounts)
- {
- registration = registeredAccounts.get(accountID);
- }
-
- try
- {
- if (registration != null)
- return registration.getReference();
- }
- catch (IllegalStateException ise)
- {
- synchronized (registeredAccounts)
- {
- registeredAccounts.remove(accountID);
- }
- }
-
- return null;
- }
-
- /**
- * 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<ProtocolProviderService> 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
- = bundleContext.getService(serRef);
-
- try
- {
- protocolProvider.unregister();
- }
- catch (OperationFailedException ex)
- {
- logger.error(
- "Failed to unregister protocol provider for account: "
- + accountID + " caused by: " + ex);
- }
- }
-
- ServiceRegistration<ProtocolProviderService> 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.)
- * </p>
- *
- * @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.)
- * </p>
- *
- * @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.
- * </p>
- *
- * @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);
- }
-
- // Update password property also in the AccountID
- // to prevent it from being removed during account reload
- // in some cases.
- accountID.setPassword(password);
-
- }
-
- /**
- * 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.
- * </p>
- *
- * @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<ProtocolProviderService> serviceRegistration
- = bundleContext.registerService(
- ProtocolProviderService.class,
- 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<ProtocolProviderService> serRef
- = getProviderForAccount(accountID);
-
- if (serRef == null)
- {
- return false;
- }
-
- BundleContext bundleContext = getBundleContext();
- ProtocolProviderService protocolProvider
- = bundleContext.getService(serRef);
-
- try
- {
- protocolProvider.unregister();
- }
- catch (OperationFailedException ex)
- {
- logger.error(
- "Failed to unregister protocol provider for account: "
- + accountID + " caused by: " + ex);
- }
-
- ServiceRegistration<ProtocolProviderService> 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)}).
- * </p>
- *
- * @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.
- * </p>
- *
- * @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<ConfigurationService> confReference
- = bundleContext.getServiceReference(ConfigurationService.class);
- 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 (ServiceRegistration<ProtocolProviderService> reg
- : registeredAccounts.values())
- {
- 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<ProtocolProviderService> registeredAccount)
- {
- 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<AccountManager> serviceReference
- = bundleContext.getServiceReference(AccountManager.class);
-
- return bundleContext.getService(serviceReference);
- }
-
-
- /**
- * Finds registered <tt>ProtocolProviderFactory</tt> for given
- * <tt>protocolName</tt>.
- * @param bundleContext the OSGI bundle context that will be used.
- * @param protocolName the protocol name.
- * @return Registered <tt>ProtocolProviderFactory</tt> for given protocol
- * name or <tt>null</tt> if no provider was found.
- */
- static public ProtocolProviderFactory getProtocolProviderFactory(
- BundleContext bundleContext,
- String protocolName)
- {
- Collection<ServiceReference<ProtocolProviderFactory>> serRefs;
- String osgiFilter
- = "(" + ProtocolProviderFactory.PROTOCOL + "=" + protocolName + ")";
-
- try
- {
- serRefs
- = bundleContext.getServiceReferences(
- ProtocolProviderFactory.class,
- osgiFilter);
- }
- catch (InvalidSyntaxException ex)
- {
- serRefs = null;
- logger.error(ex);
- }
- if ((serRefs == null) || serRefs.isEmpty())
- return null;
- else
- return bundleContext.getService(serRefs.iterator().next());
- }
-}
+package net.java.sip.communicator.service.protocol; + +import java.lang.reflect.*; +import java.util.*; + +import net.java.sip.communicator.service.credentialsstorage.*; +import net.java.sip.communicator.util.*; + +import org.jitsi.service.configuration.*; +import org.osgi.framework.*; + +/** + * 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 = "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 = "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 = "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 = "FORCE_PROXY_BYPASS"; + + /** + * The name of the property that indicates whether the client must + * be registered with a registrar when making outgoing calls. + */ + public static final String MUST_REGISTER_TO_CALL = "MUST_REGISTER_TO_CALL"; + + /** + * 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"; + + /** + * The name of the property that would indicate if a given account + * configuration form is currently hidden. + */ + public static final String IS_ACCOUNT_CONFIG_HIDDEN = "IS_CONFIG_HIDDEN"; + + /** + * The name of the property that would indicate if a given account + * status menu is currently hidden. + */ + public static final String IS_ACCOUNT_STATUS_MENU_HIDDEN = + "IS_STATUS_MENU_HIDDEN"; + + /** + * The name of the property that would indicate if a given account + * configuration is read only. + */ + public static final String IS_ACCOUNT_READ_ONLY = "IS_READ_ONLY"; + + /** + * The name of the property that would indicate if a given account + * groups are readonly, values can be all or a comma separated + * group names including root. + */ + public static final String ACCOUNT_READ_ONLY_GROUPS = "READ_ONLY_GROUPS"; + + /** + * Indicates if ICE should be used. + */ + public static final String IS_USE_ICE = "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 name of the boolean account property which indicates whether Jitsi + * will use translator for media, instead of mixing, for conference + * calls. + * By default if supported mixing is used (audio mixed, video relayed). + */ + public static final String USE_TRANSLATOR_IN_CONFERENCE + = "USE_TRANSLATOR_IN_CONFERENCE"; + + /** + * 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 = "USERNAME"; + + /** + * The base property name for password of additional STUN servers specified. + */ + public static final String STUN_PASSWORD = "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"; + + /** + * Indicates if desktop remote control is disabled for a certain account. + */ + public static final String IS_DESKTOP_REMOTE_CONTROL_DISABLED + = "DESKTOP_REMOTE_CONTROL_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 name of the property holding DTMF method. + */ + public static final String DTMF_METHOD = "DTMF_METHOD"; + + /** + * 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"; + + /** + * An account property to provide a connected account to check for + * its status. Used when the current provider need to reject calls + * but is missing presence operation set and need to check other + * provider for status. + */ + public static final String CUSAX_PROVIDER_ACCOUNT_PROP + = "cusax.XMPP_ACCOUNT_ID"; + + /** + * 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. + * </p> + */ + protected final Map<AccountID, ServiceRegistration<ProtocolProviderService>> + registeredAccounts + = new HashMap<AccountID, ServiceRegistration<ProtocolProviderService>>(); + + /** + * The name of the property that indicates the AVP type. + * <ul> + * <li>{@link #SAVP_OFF}</li> + * <li>{@link #SAVP_MANDATORY}</li> + * <li>{@link #SAVP_OPTIONAL}</li> + * </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"; + + /** + * The name of the property that defines the enabled/disabled state of + * message carbons. + */ + public static final String IS_CARBON_DISABLED = "CARBON_DISABLED"; + + /** + * 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<ProtocolProviderService> getProviderForAccount( + AccountID accountID) + { + ServiceRegistration<ProtocolProviderService> registration; + + synchronized (registeredAccounts) + { + registration = registeredAccounts.get(accountID); + } + + try + { + if (registration != null) + return registration.getReference(); + } + catch (IllegalStateException ise) + { + synchronized (registeredAccounts) + { + registeredAccounts.remove(accountID); + } + } + + return null; + } + + /** + * 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<ProtocolProviderService> 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 + = bundleContext.getService(serRef); + + try + { + protocolProvider.unregister(); + } + catch (OperationFailedException ex) + { + logger.error( + "Failed to unregister protocol provider for account: " + + accountID + " caused by: " + ex); + } + } + + ServiceRegistration<ProtocolProviderService> 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.) + * </p> + * + * @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.) + * </p> + * + * @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. + * </p> + * + * @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); + } + + // Update password property also in the AccountID + // to prevent it from being removed during account reload + // in some cases. + accountID.setPassword(password); + + } + + /** + * 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. + * </p> + * + * @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<ProtocolProviderService> serviceRegistration + = bundleContext.registerService( + ProtocolProviderService.class, + 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<ProtocolProviderService> serRef + = getProviderForAccount(accountID); + + if (serRef == null) + { + return false; + } + + BundleContext bundleContext = getBundleContext(); + ProtocolProviderService protocolProvider + = bundleContext.getService(serRef); + + try + { + protocolProvider.unregister(); + } + catch (OperationFailedException ex) + { + logger.error( + "Failed to unregister protocol provider for account: " + + accountID + " caused by: " + ex); + } + + ServiceRegistration<ProtocolProviderService> 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)}). + * </p> + * + * @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. + * </p> + * + * @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<ConfigurationService> confReference + = bundleContext.getServiceReference(ConfigurationService.class); + 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 (ServiceRegistration<ProtocolProviderService> reg + : registeredAccounts.values()) + { + 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<ProtocolProviderService> registeredAccount) + { + 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<AccountManager> serviceReference + = bundleContext.getServiceReference(AccountManager.class); + + return bundleContext.getService(serviceReference); + } + + + /** + * Finds registered <tt>ProtocolProviderFactory</tt> for given + * <tt>protocolName</tt>. + * @param bundleContext the OSGI bundle context that will be used. + * @param protocolName the protocol name. + * @return Registered <tt>ProtocolProviderFactory</tt> for given protocol + * name or <tt>null</tt> if no provider was found. + */ + static public ProtocolProviderFactory getProtocolProviderFactory( + BundleContext bundleContext, + String protocolName) + { + Collection<ServiceReference<ProtocolProviderFactory>> serRefs; + String osgiFilter + = "(" + ProtocolProviderFactory.PROTOCOL + "=" + protocolName + ")"; + + try + { + serRefs + = bundleContext.getServiceReferences( + ProtocolProviderFactory.class, + osgiFilter); + } + catch (InvalidSyntaxException ex) + { + serRefs = null; + logger.error(ex); + } + if ((serRefs == null) || serRefs.isEmpty()) + return null; + else + return bundleContext.getService(serRefs.iterator().next()); + } +} diff --git a/src/net/java/sip/communicator/service/protocol/ProtocolProviderService.java b/src/net/java/sip/communicator/service/protocol/ProtocolProviderService.java index 0c85e98..ffc356c 100644 --- a/src/net/java/sip/communicator/service/protocol/ProtocolProviderService.java +++ b/src/net/java/sip/communicator/service/protocol/ProtocolProviderService.java @@ -211,13 +211,6 @@ public interface ProtocolProviderService public void shutdown(); /** - * A hashcode allowing usage of protocol providers as keys in Hashtables. - * @return an int that may be used when storing protocol providers as - * hashtable keys. - */ - public int hashCode(); - - /** * Returns the AccountID that uniquely identifies the account represented by * this instance of the ProtocolProviderService. * @return the id of the account represented by this provider. @@ -225,6 +218,23 @@ public interface ProtocolProviderService public AccountID getAccountID(); /** + * Validates the given protocol specific contact identifier and returns an + * error message if applicable and a suggested correction. + * + * @param contactId the contact identifier to validate + * @param result Must be supplied as an empty a list. Implementors add + * items: + * <ol> + * <li>is the error message if applicable + * <li>a suggested correction. Index 1 is optional and can only + * be present if there was a validation failure. + * </ol> + * @return true if the contact id is valid, false otherwise + */ + public boolean validateContactAddress(String contactId, + List<String> result); + + /** * Indicate if the signaling transport of this protocol instance uses a * secure (e.g. via TLS) connection. * diff --git a/src/net/java/sip/communicator/service/protocol/RegistrationState.java b/src/net/java/sip/communicator/service/protocol/RegistrationState.java index 9bd3eb0..368e5b1 100644 --- a/src/net/java/sip/communicator/service/protocol/RegistrationState.java +++ b/src/net/java/sip/communicator/service/protocol/RegistrationState.java @@ -177,4 +177,10 @@ public class RegistrationState && obj != null && statusString.equals(((RegistrationState)obj).statusString); } + + @Override + public int hashCode() + { + return statusString.hashCode(); + } } diff --git a/src/net/java/sip/communicator/service/protocol/ServerStoredDetails.java b/src/net/java/sip/communicator/service/protocol/ServerStoredDetails.java index 23cb895..4d21864 100644 --- a/src/net/java/sip/communicator/service/protocol/ServerStoredDetails.java +++ b/src/net/java/sip/communicator/service/protocol/ServerStoredDetails.java @@ -156,6 +156,12 @@ public class ServerStoredDetails else return false; } + + @Override + public int hashCode() + { + return Objects.hash(detailDisplayName, value); + } } /** diff --git a/src/net/java/sip/communicator/service/protocol/WhiteboardPoint.java b/src/net/java/sip/communicator/service/protocol/WhiteboardPoint.java index 1774718..3e26e7a 100644 --- a/src/net/java/sip/communicator/service/protocol/WhiteboardPoint.java +++ b/src/net/java/sip/communicator/service/protocol/WhiteboardPoint.java @@ -17,6 +17,8 @@ */ package net.java.sip.communicator.service.protocol; +import java.util.Objects; + /** * A point representing a location in {@code (x,y)} coordinate space, * specified in integer precision. @@ -126,6 +128,12 @@ public class WhiteboardPoint implements Cloneable return false; } + @Override + public int hashCode() + { + return Objects.hash(x, y); + } + /** * Returns a string representation of this point and its location * in the {@code (x,y)} coordinate space. This method is intended to be diff --git a/src/net/java/sip/communicator/service/protocol/event/AccountManagerEvent.java b/src/net/java/sip/communicator/service/protocol/event/AccountManagerEvent.java index db594bf..ecbd971 100644 --- a/src/net/java/sip/communicator/service/protocol/event/AccountManagerEvent.java +++ b/src/net/java/sip/communicator/service/protocol/event/AccountManagerEvent.java @@ -1,4 +1,4 @@ -/*
+/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd @@ -15,86 +15,86 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package net.java.sip.communicator.service.protocol.event;
-
-import java.util.*;
-
-import net.java.sip.communicator.service.protocol.*;
-
-/**
- * Represents a notifying event fired by a specific {@link AccountManager}.
- *
- * @author Lubomir Marinov
- */
-public class AccountManagerEvent
- extends EventObject
-{
- /**
- * Serial version UID.
- */
- private static final long serialVersionUID = 0L;
-
- /**
- * The type of event notifying that the loading of the stored accounts of a
- * specific <code>ProtocolProviderFactory</code> has finished.
- */
- public static final int STORED_ACCOUNTS_LOADED = 1;
-
- /**
- * The <code>ProtocolProviderFactory</code> being worked on at the time this
- * event has been fired.
- */
- private final ProtocolProviderFactory factory;
-
- /**
- * The (detail) type of this event which is one of
- * {@link #STORED_ACCOUNTS_LOADED}.
- */
- private final int type;
-
- /**
- * Initializes a new <code>AccountManagerEvent</code> instance fired by a
- * specific <code>AccountManager</code> in order to notify of an event of a
- * specific type occurring while working on a specific
- * <code>ProtocolProviderFactory</code>.
- *
- * @param accountManager the <code>AccountManager</code> issuing the
- * notification i.e. the source of the event
- * @param type the type of the event which is one of
- * {@link #STORED_ACCOUNTS_LOADED}
- * @param factory the <code>ProtocolProviderFactory</code> being worked on
- * at the time this event has been fired
- */
- public AccountManagerEvent(AccountManager accountManager, int type,
- ProtocolProviderFactory factory)
- {
- super(accountManager);
-
- this.type = type;
- this.factory = factory;
- }
-
- /**
- * Gets the <code>ProtocolProviderFactory</code> being worked on at the time
- * this event has been fired.
- *
- * @return the <code>ProtocolProviderFactory</code> being worked on at the
- * time this event has been fired
- */
- public ProtocolProviderFactory getFactory()
- {
- return factory;
- }
-
- /**
- * Gets the (detail) type of this event which is one of
- * <code>STORED_ACCOUNTS_LOADED</code>.
- *
- * @return the (detail) type of this event which is one of
- * <code>STORED_ACCOUNTS_LOADED</code>
- */
- public int getType()
- {
- return type;
- }
-}
+package net.java.sip.communicator.service.protocol.event; + +import java.util.*; + +import net.java.sip.communicator.service.protocol.*; + +/** + * Represents a notifying event fired by a specific {@link AccountManager}. + * + * @author Lubomir Marinov + */ +public class AccountManagerEvent + extends EventObject +{ + /** + * Serial version UID. + */ + private static final long serialVersionUID = 0L; + + /** + * The type of event notifying that the loading of the stored accounts of a + * specific <code>ProtocolProviderFactory</code> has finished. + */ + public static final int STORED_ACCOUNTS_LOADED = 1; + + /** + * The <code>ProtocolProviderFactory</code> being worked on at the time this + * event has been fired. + */ + private final ProtocolProviderFactory factory; + + /** + * The (detail) type of this event which is one of + * {@link #STORED_ACCOUNTS_LOADED}. + */ + private final int type; + + /** + * Initializes a new <code>AccountManagerEvent</code> instance fired by a + * specific <code>AccountManager</code> in order to notify of an event of a + * specific type occurring while working on a specific + * <code>ProtocolProviderFactory</code>. + * + * @param accountManager the <code>AccountManager</code> issuing the + * notification i.e. the source of the event + * @param type the type of the event which is one of + * {@link #STORED_ACCOUNTS_LOADED} + * @param factory the <code>ProtocolProviderFactory</code> being worked on + * at the time this event has been fired + */ + public AccountManagerEvent(AccountManager accountManager, int type, + ProtocolProviderFactory factory) + { + super(accountManager); + + this.type = type; + this.factory = factory; + } + + /** + * Gets the <code>ProtocolProviderFactory</code> being worked on at the time + * this event has been fired. + * + * @return the <code>ProtocolProviderFactory</code> being worked on at the + * time this event has been fired + */ + public ProtocolProviderFactory getFactory() + { + return factory; + } + + /** + * Gets the (detail) type of this event which is one of + * <code>STORED_ACCOUNTS_LOADED</code>. + * + * @return the (detail) type of this event which is one of + * <code>STORED_ACCOUNTS_LOADED</code> + */ + public int getType() + { + return type; + } +} diff --git a/src/net/java/sip/communicator/service/protocol/event/AccountManagerListener.java b/src/net/java/sip/communicator/service/protocol/event/AccountManagerListener.java index 607caea..6bcf4e0 100644 --- a/src/net/java/sip/communicator/service/protocol/event/AccountManagerListener.java +++ b/src/net/java/sip/communicator/service/protocol/event/AccountManagerListener.java @@ -1,4 +1,4 @@ -/*
+/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd @@ -15,28 +15,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package net.java.sip.communicator.service.protocol.event;
-
-import java.util.*;
-
-import net.java.sip.communicator.service.protocol.*;
-
-/**
- * Represents a listener receiving notifications from {@link AccountManager}.
- *
- * @author Lubomir Marinov
- */
-public interface AccountManagerListener
- extends EventListener
-{
-
- /**
- * Notifies this listener about an event fired by a specific
- * <code>AccountManager</code>.
- *
- * @param event the <code>AccountManagerEvent</code> describing the
- * <code>AccountManager</code> firing the notification and the
- * other details of the specific notification.
- */
- void handleAccountManagerEvent(AccountManagerEvent event);
-}
+package net.java.sip.communicator.service.protocol.event; + +import java.util.*; + +import net.java.sip.communicator.service.protocol.*; + +/** + * Represents a listener receiving notifications from {@link AccountManager}. + * + * @author Lubomir Marinov + */ +public interface AccountManagerListener + extends EventListener +{ + + /** + * Notifies this listener about an event fired by a specific + * <code>AccountManager</code>. + * + * @param event the <code>AccountManagerEvent</code> describing the + * <code>AccountManager</code> firing the notification and the + * other details of the specific notification. + */ + void handleAccountManagerEvent(AccountManagerEvent event); +} diff --git a/src/net/java/sip/communicator/service/protocol/event/CallPeerSecurityMessageEvent.java b/src/net/java/sip/communicator/service/protocol/event/CallPeerSecurityMessageEvent.java index bcbb66a..92a3f31 100644 --- a/src/net/java/sip/communicator/service/protocol/event/CallPeerSecurityMessageEvent.java +++ b/src/net/java/sip/communicator/service/protocol/event/CallPeerSecurityMessageEvent.java @@ -1,8 +1,19 @@ /* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * - * Distributable under LGPL license. - * See terms of license at gnu.org. + * Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package net.java.sip.communicator.service.protocol.event; diff --git a/src/net/java/sip/communicator/service/protocol/event/CallPeerSecurityStatusEvent.java b/src/net/java/sip/communicator/service/protocol/event/CallPeerSecurityStatusEvent.java index 6720f4b..931998b 100644 --- a/src/net/java/sip/communicator/service/protocol/event/CallPeerSecurityStatusEvent.java +++ b/src/net/java/sip/communicator/service/protocol/event/CallPeerSecurityStatusEvent.java @@ -1,8 +1,19 @@ /* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * - * Distributable under LGPL license. - * See terms of license at gnu.org. + * Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package net.java.sip.communicator.service.protocol.event; diff --git a/src/net/java/sip/communicator/service/protocol/jabber/AbstractSmackInteroperabilityLayer.java b/src/net/java/sip/communicator/service/protocol/jabber/AbstractSmackInteroperabilityLayer.java new file mode 100644 index 0000000..277da84 --- /dev/null +++ b/src/net/java/sip/communicator/service/protocol/jabber/AbstractSmackInteroperabilityLayer.java @@ -0,0 +1,141 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.java.sip.communicator.service.protocol.jabber; + +import net.java.sip.communicator.util.*; +import org.jivesoftware.smack.provider.*; + + +/** + * + * This class abstracts interactions within Smack XMPP library (mostly with + * its <tt>ProviderManager</tt> class). + * This exists because <tt>JingleIQProvider</tt> and <tt>ColibriIQProvider</tt> + * are in need to be used with Smack v4, where <tt>ProviderManager</tt> + * has a different interface. + * + * @author Maksym Kulish + */ +abstract public class AbstractSmackInteroperabilityLayer { + + /** + * The <tt>Logger</tt> used by the + * <tt>AbstractSmackInteroperabilityLayer</tt> class for + * reporting on improper implementations instantiation + */ + private static final Logger logger = Logger.getLogger( + AbstractSmackInteroperabilityLayer.class); + + /** + * The implementation class to be used within its Jitsi application + */ + private static Class<AbstractSmackInteroperabilityLayer> + implementationClass; + + /** + * The instance of Smack interoperability layer implementation class + */ + private static AbstractSmackInteroperabilityLayer + interopLayerInstance; + + /** + * Get the instance of Smack interoperability layer implementation class + * + * @return Smack interoperation layer implementation class + */ + public static AbstractSmackInteroperabilityLayer getInstance() + { + if (interopLayerInstance == null) + { + try + { + interopLayerInstance = + implementationClass.newInstance(); + } + catch (IllegalAccessException e) + { + // Never thrown within proper implementation + logger.fatal("Your AbstractSmackInteroperabilityLayer " + + "implementation " + + "cannot be accessed properly. " + + "Please fix the implementation"); + } + catch (InstantiationException e) + { + // Never thrown within proper implementation + logger.fatal("Your AbstractSmackInteroperabilityLayer " + + "implementation " + + "cannot be instantiated properly. " + + "Please fix the implementation"); + } + } + return interopLayerInstance; + } + + /** + * Set the Smack interoperation layer + * implementation class to be used within this Jitsi application + * @param implementationClass Smack interoperation layer + * implementation class + */ + public static void setImplementationClass( + Class implementationClass) + { + AbstractSmackInteroperabilityLayer.implementationClass + = implementationClass; + } + + + /** + * Add <tt>PacketExtensionProvider</tt> to the list of known + * providers + * + * @param elementName The element name where the matching is happening + * @param namespace The XML namespace used in that element + * @param provider <tt>PacketExtensionProvider</tt> implementation to be + * used + */ + abstract public void addExtensionProvider( + String elementName, String namespace, Object provider); + + /** + * Add <tt>IQProvider</tt> to the list of known + * providers + * + * @param elementName The element name where the matching is happening + * @param namespace The XML namespace used in that element + * @param provider <tt>IQProvider</tt> implementation to be + * used + */ + abstract public void addIQProvider( + String elementName, String namespace, Object provider); + + /** + * Get the <tt>PacketExtensionProvider</tt> for given element name and XML + * namespace + * + * @param elementName The element name where the matching is happening + * @param namespace The XML namespace used in that element + * @return <tt>PacketExtensionProvider</tt> implementation to be + * used + */ + abstract public PacketExtensionProvider getExtensionProvider( + String elementName, String namespace); + + +} diff --git a/src/net/java/sip/communicator/service/protocol/jabber/JabberAccountID.java b/src/net/java/sip/communicator/service/protocol/jabber/JabberAccountID.java index 81473c2..dce28d6 100644 --- a/src/net/java/sip/communicator/service/protocol/jabber/JabberAccountID.java +++ b/src/net/java/sip/communicator/service/protocol/jabber/JabberAccountID.java @@ -44,6 +44,12 @@ public class JabberAccountID public static final String ANONYMOUS_AUTH = "ANONYMOUS_AUTH"; /** + * Configures the URL which is to be used with BOSH transport. If the value + * is <tt>null</tt> or empty then the TCP transport will be used instead. + */ + public static final String BOSH_URL = "BOSH_URL"; + + /** * Account suffix for Google service. */ public static final String GOOGLE_USER_SUFFIX = "gmail.com"; @@ -119,6 +125,29 @@ public class JabberAccountID } /** + * Returns the BOSH URL which should be used to connect to the XMPP server. + * If the value is set then BOSH transport instead of TCP will be used. + * + * @return a <tt>String</tt> with the URL which should be used for BOSH + * transport or <tt>null</tt> if disabled. + */ + public String getBoshUrl() + { + return getAccountPropertyString(BOSH_URL); + } + + /** + * Sets new URL which should be used for the BOSH transport. + * + * @param boshPath a <tt>String</tt> with the new BOSH URL or <tt>null</tt> + * to disable BOSH. + */ + public void setBoshUrl(String boshPath) + { + putAccountProperty(BOSH_URL, boshPath); + } + + /** * Returns the override phone suffix. * * @return the phone suffix diff --git a/src/net/java/sip/communicator/service/protocol/media/CallPeerMediaHandler.java b/src/net/java/sip/communicator/service/protocol/media/CallPeerMediaHandler.java index b0d62ed..ead01d1 100644 --- a/src/net/java/sip/communicator/service/protocol/media/CallPeerMediaHandler.java +++ b/src/net/java/sip/communicator/service/protocol/media/CallPeerMediaHandler.java @@ -87,6 +87,16 @@ public abstract class CallPeerMediaHandler<T extends MediaAwareCallPeer<?,?,?>> public static final String VIDEO_REMOTE_SSRC = "VIDEO_REMOTE_SSRC"; /** + * The initial content of a hole punch packet. It has some fields pre-set. + * Like rtp verion, sequence number and timestamp. + */ + private static final byte[] HOLE_PUNCH_PACKET = + { + (byte)0x80, 0x00, 0x02, (byte)0x9E, 0x00, 0x09, + (byte)0xD0, (byte)0x80, 0x00, 0x00, 0x00, (byte)0x00, + }; + + /** * List of advertised encryption methods. Indicated before establishing the * call. */ @@ -374,7 +384,7 @@ public abstract class CallPeerMediaHandler<T extends MediaAwareCallPeer<?,?,?>> if (call == null) return; - for (MediaType mediaType : MediaType.values()) + for (MediaType mediaType : new MediaType[] {MediaType.AUDIO, MediaType.VIDEO} ) { MediaStream stream = getStream(mediaType); @@ -622,10 +632,8 @@ public abstract class CallPeerMediaHandler<T extends MediaAwareCallPeer<?,?,?>> return audioDirectionUserPreference; case VIDEO: return videoDirectionUserPreference; - case DATA: - return MediaDirection.INACTIVE; default: - throw new IllegalArgumentException("mediaType"); + return MediaDirection.INACTIVE; } } @@ -689,7 +697,7 @@ public abstract class CallPeerMediaHandler<T extends MediaAwareCallPeer<?,?,?>> return (transportManager == null) - ? null + ? 0 : transportManager.getHarvestingTime(harvesterName); } @@ -942,7 +950,9 @@ public abstract class CallPeerMediaHandler<T extends MediaAwareCallPeer<?,?,?>> /** * Returns the number of harvesting for this agent. * - * @return The number of harvesting for this agent. + * @return The number of harvesting for this agent. 0 if this harvester + * does not exists, if the ICE agent is null, or + * if the agent has never harvested with this harvester. */ public int getNbHarvesting() { @@ -950,7 +960,7 @@ public abstract class CallPeerMediaHandler<T extends MediaAwareCallPeer<?,?,?>> return (transportManager == null) - ? null + ? 0 : transportManager.getNbHarvesting(); } @@ -961,7 +971,8 @@ public abstract class CallPeerMediaHandler<T extends MediaAwareCallPeer<?,?,?>> * @param harvesterName The class name if the harvester. * * @return The number of harvesting time for the harvester given in - * parameter. + * parameter. 0 if this harvester does not exists, if the ICE agent is null, or if the + * agent has never harvested with this harvester. */ public int getNbHarvesting(String harvesterName) { @@ -969,7 +980,7 @@ public abstract class CallPeerMediaHandler<T extends MediaAwareCallPeer<?,?,?>> return (transportManager == null) - ? null + ? 0 : transportManager.getNbHarvesting(harvesterName); } @@ -1036,17 +1047,10 @@ public abstract class CallPeerMediaHandler<T extends MediaAwareCallPeer<?,?,?>> { case AUDIO: return audioStream; - case DATA: - /* - * DATA is a valid MediaType value and CallPeerMediaHandler does not - * utilize it at this time so no IllegalArgumentException is thrown - * and null is returned (as documented). - */ - return null; case VIDEO: return videoStream; default: - throw new IllegalArgumentException("mediaType"); + return null; } } @@ -1054,7 +1058,7 @@ public abstract class CallPeerMediaHandler<T extends MediaAwareCallPeer<?,?,?>> * Returns the total harvesting time (in ms) for all harvesters. * * @return The total harvesting time (in ms) for all the harvesters. 0 if - * the ICE agent is null, or if the agent has nevers harvested. + * the ICE agent is null, or if the agent has never harvested. */ public long getTotalHarvestingTime() { @@ -1062,7 +1066,7 @@ public abstract class CallPeerMediaHandler<T extends MediaAwareCallPeer<?,?,?>> return (transportManager == null) - ? null + ? 0 : transportManager.getTotalHarvestingTime(); } @@ -1581,11 +1585,21 @@ public abstract class CallPeerMediaHandler<T extends MediaAwareCallPeer<?,?,?>> * to open port on NAT or RTP proxy if any. In order to be really efficient, * this method should be called after we send our offer or answer. * - * @param target <tt>MediaStreamTarget</tt> + * @param stream <tt>MediaStream</tt> non-null stream + * @param mediaType <tt>MediaType</tt> */ - protected void sendHolePunchPacket(MediaStreamTarget target) + protected void sendHolePunchPacket(MediaStream stream, MediaType mediaType) { - getTransportManager().sendHolePunchPacket(target, MediaType.VIDEO); + // send as a hole punch packet a constructed rtp packet + // has the correct payload type and ssrc + RawPacket packet = new RawPacket( + HOLE_PUNCH_PACKET, 0, RawPacket.FIXED_HEADER_SIZE); + packet.setPayloadType( + dynamicPayloadTypes.getPayloadType(stream.getFormat())); + packet.setSSRC((int)stream.getLocalSourceID()); + + getTransportManager().sendHolePunchPacket( + stream.getTarget(), mediaType, packet); } /** @@ -1942,6 +1956,8 @@ public abstract class CallPeerMediaHandler<T extends MediaAwareCallPeer<?,?,?>> stream.getTarget(), MediaType.AUDIO); stream.start(); + + sendHolePunchPacket(stream, MediaType.AUDIO); } stream = getStream(MediaType.VIDEO); @@ -1970,7 +1986,7 @@ public abstract class CallPeerMediaHandler<T extends MediaAwareCallPeer<?,?,?>> * send the hole-punch packet anyway to let the remote video * reach this local peer. */ - sendHolePunchPacket(stream.getTarget()); + sendHolePunchPacket(stream, MediaType.VIDEO); } } } diff --git a/src/net/java/sip/communicator/service/protocol/media/MediaAwareCall.java b/src/net/java/sip/communicator/service/protocol/media/MediaAwareCall.java index f9b6c2f..2bf802b 100644 --- a/src/net/java/sip/communicator/service/protocol/media/MediaAwareCall.java +++ b/src/net/java/sip/communicator/service/protocol/media/MediaAwareCall.java @@ -906,7 +906,7 @@ public abstract class MediaAwareCall< @Override protected CallConference createConference() { - return new MediaAwareCallConference(); + return new MediaAwareCallConference(false, this.useTranslator); } /** diff --git a/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallConference.java b/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallConference.java index 7a58e4a..81e917c 100644 --- a/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallConference.java +++ b/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallConference.java @@ -1,4 +1,4 @@ -/*
+/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd @@ -15,623 +15,667 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package net.java.sip.communicator.service.protocol.media;
-
-import java.beans.*;
-import java.lang.ref.*;
-import java.util.*;
-
-import org.jitsi.service.neomedia.*;
-import org.jitsi.service.neomedia.device.*;
-import org.jitsi.util.*;
-import org.jitsi.util.event.*;
-
-import net.java.sip.communicator.service.protocol.*;
-
-/**
- * Extends <tt>CallConference</tt> to represent the media-specific information
- * associated with the telephony conference-related state of a
- * <tt>MediaAwareCall</tt>.
- *
- * @author Lyubomir Marinov
- */
-public class MediaAwareCallConference
- extends CallConference
-{
- /**
- * The <tt>PropertyChangeListener</tt> which will listen to the
- * <tt>MediaService</tt> about <tt>PropertyChangeEvent</tt>s.
- */
- private static WeakPropertyChangeListener
- mediaServicePropertyChangeListener;
-
- /**
- * The <tt>MediaDevice</tt>s indexed by <tt>MediaType</tt> ordinal which are
- * to be used by this telephony conference for media capture and/or
- * playback. If the <tt>MediaDevice</tt> for a specific <tt>MediaType</tt>
- * is <tt>null</tt>,
- * {@link MediaService#getDefaultDevice(MediaType, MediaUseCase)} is called.
- */
- private final MediaDevice[] devices;
-
- /**
- * The <tt>MediaDevice</tt>s which implement media mixing on the respective
- * <tt>MediaDevice</tt> in {@link #devices} for the purposes of this
- * telephony conference.
- */
- private final MediaDevice[] mixers;
-
- /**
- * The <tt>VolumeControl</tt> implementation which is to control the volume
- * (level) of the audio played back the telephony conference represented by
- * this instance.
- */
- private final VolumeControl outputVolumeControl
- = new BasicVolumeControl(
- VolumeControl.PLAYBACK_VOLUME_LEVEL_PROPERTY_NAME);
-
- /**
- * The <tt>PropertyChangeListener</tt> which listens to sources of
- * <tt>PropertyChangeEvent</tt>s on behalf of this instance.
- */
- private final PropertyChangeListener propertyChangeListener
- = new PropertyChangeListener()
- {
- @Override
- public void propertyChange(PropertyChangeEvent ev)
- {
- MediaAwareCallConference.this.propertyChange(ev);
- }
- };
-
- /**
- * The <tt>RTPTranslator</tt> which forwards video RTP and RTCP traffic
- * between the <tt>CallPeer</tt>s of the <tt>Call</tt>s participating in
- * this telephony conference when the local peer is acting as a conference
- * focus.
- */
- private RTPTranslator videoRTPTranslator;
-
- /**
- * Initializes a new <tt>MediaAwareCallConference</tt> instance.
- */
- public MediaAwareCallConference()
- {
- this(false);
- }
-
- /**
- * Initializes a new <tt>MediaAwareCallConference</tt> instance which is to
- * optionally utilize the Jitsi Videobridge server-side telephony
- * conferencing technology.
- *
- * @param jitsiVideobridge <tt>true</tt> if the telephony conference
- * represented by the new instance is to utilize the Jitsi Videobridge
- * server-side telephony conferencing technology; otherwise, <tt>false</tt>
- */
- public MediaAwareCallConference(boolean jitsiVideobridge)
- {
- super(jitsiVideobridge);
-
- int mediaTypeCount = MediaType.values().length;
-
- devices = new MediaDevice[mediaTypeCount];
- mixers = new MediaDevice[mediaTypeCount];
-
- /*
- * Listen to the MediaService in order to reflect changes in the user's
- * selection with respect to the default media device.
- */
- addMediaServicePropertyChangeListener(propertyChangeListener);
- }
-
- /**
- * Adds a specific <tt>PropertyChangeListener</tt> to be notified about
- * <tt>PropertyChangeEvent</tt>s fired by the current <tt>MediaService</tt>
- * implementation. The implementation adds a <tt>WeakReference</tt> to the
- * specified <tt>listener</tt> because <tt>MediaAwareCallConference</tt>
- * is unable to determine when the <tt>PropertyChangeListener</tt> is to be
- * removed.
- *
- * @param listener the <tt>PropertyChangeListener</tt> to add
- */
- private static synchronized void addMediaServicePropertyChangeListener(
- PropertyChangeListener listener)
- {
- if (mediaServicePropertyChangeListener == null)
- {
- final MediaService mediaService
- = ProtocolMediaActivator.getMediaService();
-
- if (mediaService != null)
- {
- mediaServicePropertyChangeListener
- = new WeakPropertyChangeListener()
- {
- @Override
- protected void addThisToNotifier()
- {
- mediaService.addPropertyChangeListener(this);
- }
-
- @Override
- protected void removeThisFromNotifier()
- {
- mediaService.removePropertyChangeListener(this);
- }
- };
- }
- }
- if (mediaServicePropertyChangeListener != null)
- {
- mediaServicePropertyChangeListener.addPropertyChangeListener(
- listener);
- }
- }
-
- /**
- * {@inheritDoc}
- *
- * If this telephony conference switches from being a conference focus to
- * not being such, disposes of the mixers used by this instance when it was
- * a conference focus
- */
- @Override
- protected void conferenceFocusChanged(boolean oldValue, boolean newValue)
- {
- /*
- * If this telephony conference switches from being a conference
- * focus to not being one, dispose of the mixers used when it was a
- * conference focus.
- */
- if (oldValue && !newValue)
- {
- Arrays.fill(mixers, null);
-
- /* Disposing the video translator is not needed when the conference
- changes as we have video and we will want to continue with
- the video
- Removed when chasing a bug where video call becomes conference
- call and then back again video call and the video from the
- conference focus side is not transmitted.
- if (videoRTPTranslator != null)
- {
- videoRTPTranslator.dispose();
- videoRTPTranslator = null;
- }
- */
- }
-
- super.conferenceFocusChanged(oldValue, newValue);
- }
-
- /**
- * {@inheritDoc}
- *
- * Disposes of <tt>this.videoRTPTranslator</tt> if the removed <tt>Call</tt>
- * was the last <tt>Call</tt> in this <tt>CallConference</tt>.
- *
- * @param call the <tt>Call</tt> which has been removed from the list of
- * <tt>Call</tt>s participating in this telephony conference.
- */
- @Override
- protected void callRemoved(Call call)
- {
- super.callRemoved(call);
-
- if (getCallCount() == 0 && (videoRTPTranslator != null))
- {
- videoRTPTranslator.dispose();
- videoRTPTranslator = null;
- }
- }
-
- /**
- * Gets a <tt>MediaDevice</tt> which is capable of capture and/or playback
- * of media of the specified <tt>MediaType</tt> and is the default choice of
- * the user with respect to such a <tt>MediaDevice</tt>.
- *
- * @param mediaType the <tt>MediaType</tt> in which the retrieved
- * <tt>MediaDevice</tt> is to capture and/or play back media
- * @param useCase the <tt>MediaUseCase</tt> associated with the intended
- * utilization of the <tt>MediaDevice</tt> to be retrieved
- * @return a <tt>MediaDevice</tt> which is capable of capture and/or
- * playback of media of the specified <tt>mediaType</tt> and is the default
- * choice of the user with respect to such a <tt>MediaDevice</tt>
- */
- public MediaDevice getDefaultDevice(
- MediaType mediaType,
- MediaUseCase useCase)
- {
- int mediaTypeIndex = mediaType.ordinal();
- MediaDevice device = devices[mediaTypeIndex];
- MediaService mediaService = ProtocolMediaActivator.getMediaService();
-
- if (device == null)
- device = mediaService.getDefaultDevice(mediaType, useCase);
-
- /*
- * Make sure that the device is capable of mixing in order to support
- * conferencing and call recording.
- */
- if (device != null)
- {
- MediaDevice mixer = mixers[mediaTypeIndex];
-
- if (mixer == null)
- {
- switch (mediaType)
- {
- case AUDIO:
- /*
- * TODO AudioMixer leads to very poor audio quality on
- * Android so do not use it unless it is really really
- * necessary.
- */
- if ((!OSUtils.IS_ANDROID || isConferenceFocus())
- /*
- * We can use the AudioMixer only if the device is
- * able to capture (because the AudioMixer will push
- * when the capture device pushes).
- */
- && device.getDirection().allowsSending())
- {
- mixer = mediaService.createMixer(device);
- }
- break;
-
- case VIDEO:
- if (isConferenceFocus())
- mixer = mediaService.createMixer(device);
- break;
- }
-
- mixers[mediaTypeIndex] = mixer;
- }
-
- if (mixer != null)
- device = mixer;
- }
-
- return device;
- }
-
- /**
- * Gets the <tt>VolumeControl</tt> which controls the volume (level) of the
- * audio played back in the telephony conference represented by this
- * instance.
- *
- * @return the <tt>VolumeControl</tt> which controls the volume (level) of
- * the audio played back in the telephony conference represented by this
- * instance
- */
- public VolumeControl getOutputVolumeControl()
- {
- return outputVolumeControl;
- }
-
- /**
- * Gets the <tt>RTPTranslator</tt> which forwards RTP and RTCP traffic
- * between the <tt>CallPeer</tt>s of the <tt>Call</tt>s participating in
- * this telephony conference when the local peer is acting as a conference
- * focus.
- *
- * @param mediaType the <tt>MediaType</tt> of the <tt>MediaStream</tt> which
- * RTP and RTCP traffic is to be forwarded between
- * @return the <tt>RTPTranslator</tt> which forwards RTP and RTCP traffic
- * between the <tt>CallPeer</tt>s of the <tt>Call</tt>s participating in
- * this telephony conference when the local peer is acting as a conference
- * focus
- */
- public RTPTranslator getRTPTranslator(MediaType mediaType)
- {
- RTPTranslator rtpTranslator = null;
-
- /*
- * XXX A mixer is created for audio even when the local peer is not a
- * conference focus in order to enable additional functionality.
- * Similarly, the videoRTPTranslator is created even when the local peer
- * is not a conference focus in order to enable the local peer to turn
- * into a conference focus at a later time. More specifically,
- * MediaStreamImpl is unable to accommodate an RTPTranslator after it
- * has created its RTPManager. Yet again like the audio mixer, we'd
- * better not try to use it on Android at this time because of
- * performance issues that might arise.
- */
- if (MediaType.VIDEO.equals(mediaType)
- && (!OSUtils.IS_ANDROID || isConferenceFocus()))
- {
- if (videoRTPTranslator == null)
- {
- videoRTPTranslator
- = ProtocolMediaActivator
- .getMediaService()
- .createRTPTranslator();
- }
- rtpTranslator = videoRTPTranslator;
- }
- return rtpTranslator;
- }
-
- /**
- * Notifies this <tt>MediaAwareCallConference</tt> about changes in the
- * values of the properties of sources of <tt>PropertyChangeEvent</tt>s. For
- * example, this instance listens to changes of the value of
- * {@link MediaService#DEFAULT_DEVICE} which represents the user's choice
- * with respect to the default audio device.
- *
- * @param ev a <tt>PropertyChangeEvent</tt> which specifies the name of the
- * property which had its value changed and the old and new values of that
- * property
- */
- private void propertyChange(PropertyChangeEvent ev)
- {
- String propertyName = ev.getPropertyName();
-
- if (MediaService.DEFAULT_DEVICE.equals(propertyName))
- {
- Object source = ev.getSource();
-
- if (source instanceof MediaService)
- {
- /*
- * XXX We only support changing the default audio device at the
- * time of this writing.
- */
- int mediaTypeIndex = MediaType.AUDIO.ordinal();
- MediaDevice mixer = mixers[mediaTypeIndex];
- MediaDevice oldValue
- = (mixer instanceof MediaDeviceWrapper)
- ? ((MediaDeviceWrapper) mixer).getWrappedDevice()
- : null;
- MediaDevice newValue = devices[mediaTypeIndex];
-
- if (newValue == null)
- {
- newValue
- = ProtocolMediaActivator
- .getMediaService()
- .getDefaultDevice(
- MediaType.AUDIO,
- MediaUseCase.ANY);
- }
-
- /*
- * XXX If MediaService#getDefaultDevice(MediaType, MediaUseCase)
- * above returns null and its earlier return value was not null,
- * we will not notify of an actual change in the value of the
- * user's choice with respect to the default audio device.
- */
- if (oldValue != newValue)
- {
- mixers[mediaTypeIndex] = null;
- firePropertyChange(
- MediaAwareCall.DEFAULT_DEVICE,
- oldValue, newValue);
- }
- }
- }
- }
-
- /**
- * Sets the <tt>MediaDevice</tt> to be used by this telephony conference for
- * capture and/or playback of media of a specific <tt>MediaType</tt>.
- *
- * @param mediaType the <tt>MediaType</tt> of the media which is to be
- * captured and/or played back by the specified <tt>device</tt>
- * @param device the <tt>MediaDevice</tt> to be used by this telephony
- * conference for capture and/or playback of media of the specified
- * <tt>mediaType</tt>
- */
- void setDevice(MediaType mediaType, MediaDevice device)
- {
- int mediaTypeIndex = mediaType.ordinal();
- MediaDevice oldValue = devices[mediaTypeIndex];
-
- /*
- * XXX While we know the old and the new master/wrapped devices, we
- * are not sure whether the mixer has been used. Anyway, we have to
- * report different values in order to have PropertyChangeSupport
- * really fire an event.
- */
- MediaDevice mixer = mixers[mediaTypeIndex];
-
- if (mixer instanceof MediaDeviceWrapper)
- oldValue = ((MediaDeviceWrapper) mixer).getWrappedDevice();
-
- MediaDevice newValue = devices[mediaTypeIndex] = device;
-
- if (oldValue != newValue)
- {
- mixers[mediaTypeIndex] = null;
- firePropertyChange(
- MediaAwareCall.DEFAULT_DEVICE,
- oldValue, newValue);
- }
- }
-
- /**
- * Implements a <tt>PropertyChangeListener</tt> which weakly references and
- * delegates to specific <tt>PropertyChangeListener</tt>s and automatically
- * adds itself to and removes itself from a specific
- * <tt>PropertyChangeNotifier</tt> depending on whether there are
- * <tt>PropertyChangeListener</tt>s to delegate to. Thus enables listening
- * to a <tt>PropertyChangeNotifier</tt> by invoking
- * {@link PropertyChangeNotifier#addPropertyChangeListener(
- * PropertyChangeListener)} without
- * {@link PropertyChangeNotifier#removePropertyChangeListener(
- * PropertyChangeListener)}.
- */
- private static class WeakPropertyChangeListener
- implements PropertyChangeListener
- {
- /**
- * The indicator which determines whether this
- * <tt>PropertyChangeListener</tt> has been added to {@link #notifier}.
- */
- private boolean added = false;
-
- /**
- * The list of <tt>PropertyChangeListener</tt>s which are to be notified
- * about <tt>PropertyChangeEvent</tt>s fired by {@link #notifier}.
- */
- private final List<WeakReference<PropertyChangeListener>> listeners
- = new LinkedList<WeakReference<PropertyChangeListener>>();
-
- /**
- * The <tt>PropertyChangeNotifier</tt> this instance is to listen to
- * about <tt>PropertyChangeEvent</tt>s which are to be forwarded to
- * {@link #listeners}.
- */
- private final PropertyChangeNotifier notifier;
-
- /**
- * Initializes a new <tt>WeakPropertyChangeListener</tt> instance.
- */
- protected WeakPropertyChangeListener()
- {
- this(null);
- }
-
- /**
- * Initializes a new <tt>WeakPropertyChangeListener</tt> instance which
- * is to listen to a specific <tt>PropertyChangeNotifier</tt>.
- *
- * @param notifier the <tt>PropertyChangeNotifier</tt> the new instance
- * is to listen to
- */
- public WeakPropertyChangeListener(PropertyChangeNotifier notifier)
- {
- this.notifier = notifier;
- }
-
- /**
- * Adds a specific <tt>PropertyChangeListener</tt> to the list of
- * <tt>PropertyChangeListener</tt>s to be notified about
- * <tt>PropertyChangeEvent</tt>s fired by the
- * <tt>PropertyChangeNotifier</tt> associated with this instance.
- *
- * @param listener the <tt>PropertyChangeListener</tt> to add
- */
- public synchronized void addPropertyChangeListener(
- PropertyChangeListener listener)
- {
- Iterator<WeakReference<PropertyChangeListener>> i
- = listeners.iterator();
- boolean add = true;
-
- while (i.hasNext())
- {
- PropertyChangeListener l = i.next().get();
-
- if (l == null)
- i.remove();
- else if (l.equals(listener))
- add = false;
- }
- if (add
- && listeners.add(
- new WeakReference<PropertyChangeListener>(listener))
- && !this.added)
- {
- addThisToNotifier();
- this.added = true;
- }
- }
-
- /**
- * Adds this as a <tt>PropertyChangeListener</tt> to {@link #notifier}.
- */
- protected void addThisToNotifier()
- {
- if (notifier != null)
- notifier.addPropertyChangeListener(this);
- }
-
- /**
- * {@inheritDoc}
- *
- * Notifies this instance about a <tt>PropertyChangeEvent</tt> fired by
- * {@link #notifier}.
- */
- @Override
- public void propertyChange(PropertyChangeEvent ev)
- {
- PropertyChangeListener[] ls;
- int n;
-
- synchronized (this)
- {
- Iterator<WeakReference<PropertyChangeListener>> i
- = listeners.iterator();
-
- ls = new PropertyChangeListener[listeners.size()];
- n = 0;
- while (i.hasNext())
- {
- PropertyChangeListener l = i.next().get();
-
- if (l == null)
- i.remove();
- else
- ls[n++] = l;
- }
- if ((n == 0) && this.added)
- {
- removeThisFromNotifier();
- this.added = false;
- }
- }
-
- if (n != 0)
- {
- for (PropertyChangeListener l : ls)
- {
- if (l == null)
- break;
- else
- l.propertyChange(ev);
- }
- }
- }
-
- /**
- * Removes a specific <tt>PropertyChangeListener</tt> from the list of
- * <tt>PropertyChangeListener</tt>s to be notified about
- * <tt>PropertyChangeEvent</tt>s fired by the
- * <tt>PropertyChangeNotifier</tt> associated with this instance.
- *
- * @param listener the <tt>PropertyChangeListener</tt> to remove
- */
- @SuppressWarnings("unused")
- public synchronized void removePropertyChangeListener(
- PropertyChangeListener listener)
- {
- Iterator<WeakReference<PropertyChangeListener>> i
- = listeners.iterator();
-
- while (i.hasNext())
- {
- PropertyChangeListener l = i.next().get();
-
- if ((l == null) || l.equals(listener))
- i.remove();
- }
- if (this.added && (listeners.size() == 0))
- {
- removeThisFromNotifier();
- this.added = false;
- }
- }
-
- /**
- * Removes this as a <tt>PropertyChangeListener</tt> from
- * {@link #notifier}.
- */
- protected void removeThisFromNotifier()
- {
- if (notifier != null)
- notifier.removePropertyChangeListener(this);
- }
- }
-}
+package net.java.sip.communicator.service.protocol.media; + +import java.beans.*; +import java.lang.ref.*; +import java.util.*; + +import org.jitsi.service.neomedia.*; +import org.jitsi.service.neomedia.device.*; +import org.jitsi.util.*; +import org.jitsi.util.event.*; + +import net.java.sip.communicator.service.protocol.*; + +/** + * Extends <tt>CallConference</tt> to represent the media-specific information + * associated with the telephony conference-related state of a + * <tt>MediaAwareCall</tt>. + * + * @author Lyubomir Marinov + */ +public class MediaAwareCallConference + extends CallConference +{ + /** + * The <tt>PropertyChangeListener</tt> which will listen to the + * <tt>MediaService</tt> about <tt>PropertyChangeEvent</tt>s. + */ + private static WeakPropertyChangeListener + mediaServicePropertyChangeListener; + + /** + * The <tt>MediaDevice</tt>s indexed by <tt>MediaType</tt> ordinal which are + * to be used by this telephony conference for media capture and/or + * playback. If the <tt>MediaDevice</tt> for a specific <tt>MediaType</tt> + * is <tt>null</tt>, + * {@link MediaService#getDefaultDevice(MediaType, MediaUseCase)} is called. + */ + private final MediaDevice[] devices; + + /** + * The <tt>MediaDevice</tt>s which implement media mixing on the respective + * <tt>MediaDevice</tt> in {@link #devices} for the purposes of this + * telephony conference. + */ + private final MediaDevice[] mixers; + + /** + * The <tt>VolumeControl</tt> implementation which is to control the volume + * (level) of the audio played back the telephony conference represented by + * this instance. + */ + private final VolumeControl outputVolumeControl + = new BasicVolumeControl( + VolumeControl.PLAYBACK_VOLUME_LEVEL_PROPERTY_NAME); + + /** + * The <tt>PropertyChangeListener</tt> which listens to sources of + * <tt>PropertyChangeEvent</tt>s on behalf of this instance. + */ + private final PropertyChangeListener propertyChangeListener + = new PropertyChangeListener() + { + @Override + public void propertyChange(PropertyChangeEvent ev) + { + MediaAwareCallConference.this.propertyChange(ev); + } + }; + + /** + * The <tt>RTPTranslator</tt> which forwards video RTP and RTCP traffic + * between the <tt>CallPeer</tt>s of the <tt>Call</tt>s participating in + * this telephony conference when the local peer is acting as a conference + * focus. + */ + private RTPTranslator videoRTPTranslator; + + /** + * The <tt>RTPTranslator</tt> which forwards autio RTP and RTCP traffic + * between the <tt>CallPeer</tt>s of the <tt>Call</tt>s participating in + * this telephony conference when the local peer is acting as a conference + * focus. + */ + private RTPTranslator audioRTPTranslator; + + /** + * The indicator which determines whether the telephony conference + * represented by this instance is mixing or relaying. + * By default what can be mixed is mixed (audio) and rest is relayed. + */ + private boolean translator = false; + + /** + * Initializes a new <tt>MediaAwareCallConference</tt> instance. + */ + public MediaAwareCallConference() + { + this(false); + } + + /** + * Initializes a new <tt>MediaAwareCallConference</tt> instance which is to + * optionally utilize the Jitsi Videobridge server-side telephony + * conferencing technology. + * + * @param jitsiVideobridge <tt>true</tt> if the telephony conference + * represented by the new instance is to utilize the Jitsi Videobridge + * server-side telephony conferencing technology; otherwise, <tt>false</tt> + */ + public MediaAwareCallConference(boolean jitsiVideobridge) + { + this(jitsiVideobridge, false); + } + + /** + * Initializes a new <tt>MediaAwareCallConference</tt> instance which is to + * optionally utilize the Jitsi Videobridge server-side telephony + * conferencing technology. + * + * @param jitsiVideobridge <tt>true</tt> if the telephony conference + * represented by the new instance is to utilize the Jitsi Videobridge + * server-side telephony conferencing technology; otherwise, <tt>false</tt> + */ + public MediaAwareCallConference(boolean jitsiVideobridge, + boolean translator) + { + super(jitsiVideobridge); + + this.translator = translator; + + int mediaTypeCount = MediaType.values().length; + + devices = new MediaDevice[mediaTypeCount]; + mixers = new MediaDevice[mediaTypeCount]; + + /* + * Listen to the MediaService in order to reflect changes in the user's + * selection with respect to the default media device. + */ + addMediaServicePropertyChangeListener(propertyChangeListener); + } + + /** + * Adds a specific <tt>PropertyChangeListener</tt> to be notified about + * <tt>PropertyChangeEvent</tt>s fired by the current <tt>MediaService</tt> + * implementation. The implementation adds a <tt>WeakReference</tt> to the + * specified <tt>listener</tt> because <tt>MediaAwareCallConference</tt> + * is unable to determine when the <tt>PropertyChangeListener</tt> is to be + * removed. + * + * @param listener the <tt>PropertyChangeListener</tt> to add + */ + private static synchronized void addMediaServicePropertyChangeListener( + PropertyChangeListener listener) + { + if (mediaServicePropertyChangeListener == null) + { + final MediaService mediaService + = ProtocolMediaActivator.getMediaService(); + + if (mediaService != null) + { + mediaServicePropertyChangeListener + = new WeakPropertyChangeListener() + { + @Override + protected void addThisToNotifier() + { + mediaService.addPropertyChangeListener(this); + } + + @Override + protected void removeThisFromNotifier() + { + mediaService.removePropertyChangeListener(this); + } + }; + } + } + if (mediaServicePropertyChangeListener != null) + { + mediaServicePropertyChangeListener.addPropertyChangeListener( + listener); + } + } + + /** + * {@inheritDoc} + * + * If this telephony conference switches from being a conference focus to + * not being such, disposes of the mixers used by this instance when it was + * a conference focus + */ + @Override + protected void conferenceFocusChanged(boolean oldValue, boolean newValue) + { + /* + * If this telephony conference switches from being a conference + * focus to not being one, dispose of the mixers used when it was a + * conference focus. + */ + if (oldValue && !newValue) + { + Arrays.fill(mixers, null); + + /* Disposing the video translator is not needed when the conference + changes as we have video and we will want to continue with + the video + Removed when chasing a bug where video call becomes conference + call and then back again video call and the video from the + conference focus side is not transmitted. + if (videoRTPTranslator != null) + { + videoRTPTranslator.dispose(); + videoRTPTranslator = null; + } + */ + } + + super.conferenceFocusChanged(oldValue, newValue); + } + + /** + * {@inheritDoc} + * + * Disposes of <tt>this.videoRTPTranslator</tt> if the removed <tt>Call</tt> + * was the last <tt>Call</tt> in this <tt>CallConference</tt>. + * + * @param call the <tt>Call</tt> which has been removed from the list of + * <tt>Call</tt>s participating in this telephony conference. + */ + @Override + protected void callRemoved(Call call) + { + super.callRemoved(call); + + if (getCallCount() == 0 && (videoRTPTranslator != null)) + { + videoRTPTranslator.dispose(); + videoRTPTranslator = null; + } + } + + /** + * Gets a <tt>MediaDevice</tt> which is capable of capture and/or playback + * of media of the specified <tt>MediaType</tt> and is the default choice of + * the user with respect to such a <tt>MediaDevice</tt>. + * + * @param mediaType the <tt>MediaType</tt> in which the retrieved + * <tt>MediaDevice</tt> is to capture and/or play back media + * @param useCase the <tt>MediaUseCase</tt> associated with the intended + * utilization of the <tt>MediaDevice</tt> to be retrieved + * @return a <tt>MediaDevice</tt> which is capable of capture and/or + * playback of media of the specified <tt>mediaType</tt> and is the default + * choice of the user with respect to such a <tt>MediaDevice</tt> + */ + public MediaDevice getDefaultDevice( + MediaType mediaType, + MediaUseCase useCase) + { + int mediaTypeIndex = mediaType.ordinal(); + MediaDevice device = devices[mediaTypeIndex]; + MediaService mediaService = ProtocolMediaActivator.getMediaService(); + + if (device == null) + device = mediaService.getDefaultDevice(mediaType, useCase); + + /* + * Make sure that the device is capable of mixing in order to support + * conferencing and call recording. + */ + if (device != null) + { + MediaDevice mixer = mixers[mediaTypeIndex]; + + if (mixer == null) + { + switch (mediaType) + { + case AUDIO: + /* + * TODO AudioMixer leads to very poor audio quality on + * Android so do not use it unless it is really really + * necessary. + */ + if ((!OSUtils.IS_ANDROID || isConferenceFocus()) + && !this.translator + /* + * We can use the AudioMixer only if the device is + * able to capture (because the AudioMixer will push + * when the capture device pushes). + */ + && device.getDirection().allowsSending()) + { + mixer = mediaService.createMixer(device); + } + break; + + case VIDEO: + if (isConferenceFocus()) + mixer = mediaService.createMixer(device); + break; + } + + mixers[mediaTypeIndex] = mixer; + } + + if (mixer != null) + device = mixer; + } + + return device; + } + + /** + * Gets the <tt>VolumeControl</tt> which controls the volume (level) of the + * audio played back in the telephony conference represented by this + * instance. + * + * @return the <tt>VolumeControl</tt> which controls the volume (level) of + * the audio played back in the telephony conference represented by this + * instance + */ + public VolumeControl getOutputVolumeControl() + { + return outputVolumeControl; + } + + /** + * Gets the <tt>RTPTranslator</tt> which forwards RTP and RTCP traffic + * between the <tt>CallPeer</tt>s of the <tt>Call</tt>s participating in + * this telephony conference when the local peer is acting as a conference + * focus. + * + * @param mediaType the <tt>MediaType</tt> of the <tt>MediaStream</tt> which + * RTP and RTCP traffic is to be forwarded between + * @return the <tt>RTPTranslator</tt> which forwards RTP and RTCP traffic + * between the <tt>CallPeer</tt>s of the <tt>Call</tt>s participating in + * this telephony conference when the local peer is acting as a conference + * focus + */ + public RTPTranslator getRTPTranslator(MediaType mediaType) + { + /* + * XXX A mixer is created for audio even when the local peer is not a + * conference focus in order to enable additional functionality. + * Similarly, the videoRTPTranslator is created even when the local peer + * is not a conference focus in order to enable the local peer to turn + * into a conference focus at a later time. More specifically, + * MediaStreamImpl is unable to accommodate an RTPTranslator after it + * has created its RTPManager. Yet again like the audio mixer, we'd + * better not try to use it on Android at this time because of + * performance issues that might arise. + */ + if (MediaType.VIDEO.equals(mediaType) + && (!OSUtils.IS_ANDROID || isConferenceFocus())) + { + if (videoRTPTranslator == null) + { + videoRTPTranslator + = ProtocolMediaActivator + .getMediaService() + .createRTPTranslator(); + } + return videoRTPTranslator; + } + + if (this.translator) + { + if(audioRTPTranslator == null) + { + audioRTPTranslator + = ProtocolMediaActivator + .getMediaService() + .createRTPTranslator(); + } + return audioRTPTranslator; + } + + return null; + } + + /** + * Notifies this <tt>MediaAwareCallConference</tt> about changes in the + * values of the properties of sources of <tt>PropertyChangeEvent</tt>s. For + * example, this instance listens to changes of the value of + * {@link MediaService#DEFAULT_DEVICE} which represents the user's choice + * with respect to the default audio device. + * + * @param ev a <tt>PropertyChangeEvent</tt> which specifies the name of the + * property which had its value changed and the old and new values of that + * property + */ + private void propertyChange(PropertyChangeEvent ev) + { + String propertyName = ev.getPropertyName(); + + if (MediaService.DEFAULT_DEVICE.equals(propertyName)) + { + Object source = ev.getSource(); + + if (source instanceof MediaService) + { + /* + * XXX We only support changing the default audio device at the + * time of this writing. + */ + int mediaTypeIndex = MediaType.AUDIO.ordinal(); + MediaDevice mixer = mixers[mediaTypeIndex]; + MediaDevice oldValue + = (mixer instanceof MediaDeviceWrapper) + ? ((MediaDeviceWrapper) mixer).getWrappedDevice() + : null; + MediaDevice newValue = devices[mediaTypeIndex]; + + if (newValue == null) + { + newValue + = ProtocolMediaActivator + .getMediaService() + .getDefaultDevice( + MediaType.AUDIO, + MediaUseCase.ANY); + } + + /* + * XXX If MediaService#getDefaultDevice(MediaType, MediaUseCase) + * above returns null and its earlier return value was not null, + * we will not notify of an actual change in the value of the + * user's choice with respect to the default audio device. + */ + if (oldValue != newValue) + { + mixers[mediaTypeIndex] = null; + firePropertyChange( + MediaAwareCall.DEFAULT_DEVICE, + oldValue, newValue); + } + } + } + } + + /** + * Sets the <tt>MediaDevice</tt> to be used by this telephony conference for + * capture and/or playback of media of a specific <tt>MediaType</tt>. + * + * @param mediaType the <tt>MediaType</tt> of the media which is to be + * captured and/or played back by the specified <tt>device</tt> + * @param device the <tt>MediaDevice</tt> to be used by this telephony + * conference for capture and/or playback of media of the specified + * <tt>mediaType</tt> + */ + void setDevice(MediaType mediaType, MediaDevice device) + { + int mediaTypeIndex = mediaType.ordinal(); + MediaDevice oldValue = devices[mediaTypeIndex]; + + /* + * XXX While we know the old and the new master/wrapped devices, we + * are not sure whether the mixer has been used. Anyway, we have to + * report different values in order to have PropertyChangeSupport + * really fire an event. + */ + MediaDevice mixer = mixers[mediaTypeIndex]; + + if (mixer instanceof MediaDeviceWrapper) + oldValue = ((MediaDeviceWrapper) mixer).getWrappedDevice(); + + MediaDevice newValue = devices[mediaTypeIndex] = device; + + if (oldValue != newValue) + { + mixers[mediaTypeIndex] = null; + firePropertyChange( + MediaAwareCall.DEFAULT_DEVICE, + oldValue, newValue); + } + } + + /** + * Implements a <tt>PropertyChangeListener</tt> which weakly references and + * delegates to specific <tt>PropertyChangeListener</tt>s and automatically + * adds itself to and removes itself from a specific + * <tt>PropertyChangeNotifier</tt> depending on whether there are + * <tt>PropertyChangeListener</tt>s to delegate to. Thus enables listening + * to a <tt>PropertyChangeNotifier</tt> by invoking + * {@link PropertyChangeNotifier#addPropertyChangeListener( + * PropertyChangeListener)} without + * {@link PropertyChangeNotifier#removePropertyChangeListener( + * PropertyChangeListener)}. + */ + private static class WeakPropertyChangeListener + implements PropertyChangeListener + { + /** + * The indicator which determines whether this + * <tt>PropertyChangeListener</tt> has been added to {@link #notifier}. + */ + private boolean added = false; + + /** + * The list of <tt>PropertyChangeListener</tt>s which are to be notified + * about <tt>PropertyChangeEvent</tt>s fired by {@link #notifier}. + */ + private final List<WeakReference<PropertyChangeListener>> listeners + = new LinkedList<WeakReference<PropertyChangeListener>>(); + + /** + * The <tt>PropertyChangeNotifier</tt> this instance is to listen to + * about <tt>PropertyChangeEvent</tt>s which are to be forwarded to + * {@link #listeners}. + */ + private final PropertyChangeNotifier notifier; + + /** + * Initializes a new <tt>WeakPropertyChangeListener</tt> instance. + */ + protected WeakPropertyChangeListener() + { + this(null); + } + + /** + * Initializes a new <tt>WeakPropertyChangeListener</tt> instance which + * is to listen to a specific <tt>PropertyChangeNotifier</tt>. + * + * @param notifier the <tt>PropertyChangeNotifier</tt> the new instance + * is to listen to + */ + public WeakPropertyChangeListener(PropertyChangeNotifier notifier) + { + this.notifier = notifier; + } + + /** + * Adds a specific <tt>PropertyChangeListener</tt> to the list of + * <tt>PropertyChangeListener</tt>s to be notified about + * <tt>PropertyChangeEvent</tt>s fired by the + * <tt>PropertyChangeNotifier</tt> associated with this instance. + * + * @param listener the <tt>PropertyChangeListener</tt> to add + */ + public synchronized void addPropertyChangeListener( + PropertyChangeListener listener) + { + Iterator<WeakReference<PropertyChangeListener>> i + = listeners.iterator(); + boolean add = true; + + while (i.hasNext()) + { + PropertyChangeListener l = i.next().get(); + + if (l == null) + i.remove(); + else if (l.equals(listener)) + add = false; + } + if (add + && listeners.add( + new WeakReference<PropertyChangeListener>(listener)) + && !this.added) + { + addThisToNotifier(); + this.added = true; + } + } + + /** + * Adds this as a <tt>PropertyChangeListener</tt> to {@link #notifier}. + */ + protected void addThisToNotifier() + { + if (notifier != null) + notifier.addPropertyChangeListener(this); + } + + /** + * {@inheritDoc} + * + * Notifies this instance about a <tt>PropertyChangeEvent</tt> fired by + * {@link #notifier}. + */ + @Override + public void propertyChange(PropertyChangeEvent ev) + { + PropertyChangeListener[] ls; + int n; + + synchronized (this) + { + Iterator<WeakReference<PropertyChangeListener>> i + = listeners.iterator(); + + ls = new PropertyChangeListener[listeners.size()]; + n = 0; + while (i.hasNext()) + { + PropertyChangeListener l = i.next().get(); + + if (l == null) + i.remove(); + else + ls[n++] = l; + } + if ((n == 0) && this.added) + { + removeThisFromNotifier(); + this.added = false; + } + } + + if (n != 0) + { + for (PropertyChangeListener l : ls) + { + if (l == null) + break; + else + l.propertyChange(ev); + } + } + } + + /** + * Removes a specific <tt>PropertyChangeListener</tt> from the list of + * <tt>PropertyChangeListener</tt>s to be notified about + * <tt>PropertyChangeEvent</tt>s fired by the + * <tt>PropertyChangeNotifier</tt> associated with this instance. + * + * @param listener the <tt>PropertyChangeListener</tt> to remove + */ + @SuppressWarnings("unused") + public synchronized void removePropertyChangeListener( + PropertyChangeListener listener) + { + Iterator<WeakReference<PropertyChangeListener>> i + = listeners.iterator(); + + while (i.hasNext()) + { + PropertyChangeListener l = i.next().get(); + + if ((l == null) || l.equals(listener)) + i.remove(); + } + if (this.added && (listeners.size() == 0)) + { + removeThisFromNotifier(); + this.added = false; + } + } + + /** + * Removes this as a <tt>PropertyChangeListener</tt> from + * {@link #notifier}. + */ + protected void removeThisFromNotifier() + { + if (notifier != null) + notifier.removePropertyChangeListener(this); + } + } +} diff --git a/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallPeer.java b/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallPeer.java index 4e15038..23e42d4 100644 --- a/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallPeer.java +++ b/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallPeer.java @@ -1,4 +1,4 @@ -/*
+/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd @@ -15,1247 +15,1247 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package net.java.sip.communicator.service.protocol.media;
-
-import java.beans.*;
-import java.util.*;
-
-import net.java.sip.communicator.service.protocol.*;
-import net.java.sip.communicator.service.protocol.event.*;
-import net.java.sip.communicator.util.*;
-
-import org.jitsi.service.neomedia.*;
-import org.jitsi.service.neomedia.event.*;
-
-/**
- * A utility class implementing media control code shared between current
- * telephony implementations. This class is only meant for use by protocol
- * implementations and should/could not be accessed by bundles that are simply
- * using the telephony functionalities.
- *
- * @param <T> the peer extension class like for example <tt>CallSipImpl</tt>
- * or <tt>CallJabberImpl</tt>
- * @param <U> the media handler extension class like for example
- * <tt>CallPeerMediaHandlerSipImpl</tt> or
- * <tt>CallPeerMediaHandlerJabberImpl</tt>
- * @param <V> the provider extension class like for example
- * <tt>ProtocolProviderServiceSipImpl</tt> or
- * <tt>ProtocolProviderServiceJabberImpl</tt>
- *
- * @author Emil Ivov
- * @author Lyubomir Marinov
- * @author Boris Grozev
- */
-public abstract class MediaAwareCallPeer
- <T extends MediaAwareCall<?, ?, V>,
- U extends CallPeerMediaHandler<?>,
- V extends ProtocolProviderService>
- extends AbstractCallPeer<T, V>
- implements SrtpListener,
- CallPeerConferenceListener,
- CsrcAudioLevelListener,
- SimpleAudioLevelListener
-{
- /**
- * The <tt>Logger</tt> used by the <tt>MediaAwareCallPeer</tt> class and its
- * instances for logging output.
- */
- private static final Logger logger
- = Logger.getLogger(MediaAwareCallPeer.class);
-
- /**
- * The call this peer belongs to.
- */
- private T call;
-
- /**
- * The listeners registered for level changes in the audio of participants
- * that this peer might be mixing and that we are not directly communicating
- * with.
- */
- private final List<ConferenceMembersSoundLevelListener>
- conferenceMembersSoundLevelListeners
- = new ArrayList<ConferenceMembersSoundLevelListener>();
-
- /**
- * A byte array containing the image/photo representing the call peer.
- */
- private byte[] image;
-
- /**
- * The media handler class handles all media management for a single
- * <tt>CallPeer</tt>. This includes initializing and configuring streams,
- * generating SDP, handling ICE, etc. One instance of <tt>CallPeer</tt>always
- * corresponds to exactly one instance of <tt>CallPeerMediaHandler</tt> and
- * both classes are only separated for reasons of readability.
- */
- private U mediaHandler;
-
- /**
- * The <tt>PropertyChangeListener</tt> which listens to
- * {@link CallPeerMediaHandler} for changes in the values of its properties.
- */
- private PropertyChangeListener mediaHandlerPropertyChangeListener;
-
- /**
- * A string uniquely identifying the peer.
- */
- private String peerID;
-
- /**
- * The protocol provider that this peer belongs to.
- */
- private final V protocolProvider;
-
- /**
- * The list of <tt>SoundLevelListener</tt>s interested in level changes in
- * the audio we are getting from the remote peer.
- * <p>
- * It is implemented as a copy-on-write storage because the number of
- * additions and removals of <tt>SoundLevelListener</tt>s is expected to be
- * far smaller than the number of audio level changes. The access to it is
- * to be synchronized using {@link #streamSoundLevelListenersSyncRoot}.
- * </p>
- */
- private List<SoundLevelListener> streamSoundLevelListeners;
-
- /**
- * The <tt>Object</tt> to synchronize the access to
- * {@link #streamSoundLevelListeners}.
- */
- private final Object streamSoundLevelListenersSyncRoot = new Object();
-
- /**
- * The <tt>List</tt> of <tt>PropertyChangeListener</tt>s listening to this
- * <tt>CallPeer</tt> for changes in the values of its properties related to
- * video.
- */
- private final List<PropertyChangeListener> videoPropertyChangeListeners
- = new LinkedList<PropertyChangeListener>();
-
- /**
- * Represents the last Conference Information (RFC4575) document sent to
- * this <tt>CallPeer</tt>. This is always a document with state "full", even
- * if the last document actually sent was a "partial"
- */
- private ConferenceInfoDocument lastConferenceInfoSent = null;
-
- /**
- * The time (as obtained by <tt>System.currentTimeMillis()</tt>) at which
- * a Conference Information (RFC4575) document was last sent to this
- * <tt>CallPeer</tt>.
- */
- private long lastConferenceInfoSentTimestamp = -1;
-
- /**
- * The last Conference Information (RFC4575) document sent to us by this
- * <tt>CallPeer</tt>. This is always a document with state "full", which is
- * only gets updated by "partial" or "deleted" documents.
- */
- private ConferenceInfoDocument lastConferenceInfoReceived = null;
-
- /**
- * Whether a conference-info document has been scheduled to be sent to this
- * <tt>CallPeer</tt>
- */
- private boolean confInfoScheduled = false;
-
- /**
- * Synchronization object for confInfoScheduled
- */
- private final Object confInfoScheduledSyncRoot = new Object();
-
- /**
- * Creates a new call peer with address <tt>peerAddress</tt>.
- *
- * @param owningCall the call that contains this call peer.
- */
- public MediaAwareCallPeer(T owningCall)
- {
- this.call = owningCall;
- this.protocolProvider = owningCall.getProtocolProvider();
-
- // create the uid
- this.peerID
- = String.valueOf(System.currentTimeMillis())
- + String.valueOf(hashCode());
-
- // we listen for events when the call will become focus or not
- // of a conference so we will add or remove our sound level listeners
- super.addCallPeerConferenceListener(this);
- }
-
- /**
- * Adds a specific <tt>ConferenceMembersSoundLevelListener</tt> to the list
- * of listeners interested in and notified about changes in conference
- * members sound level.
- *
- * @param listener the <tt>ConferenceMembersSoundLevelListener</tt> to add
- * @throws NullPointerException if <tt>listener</tt> is <tt>null</tt>
- */
- public void addConferenceMembersSoundLevelListener(
- ConferenceMembersSoundLevelListener listener)
- {
- /*
- * XXX The uses of the method at the time of this writing rely on being
- * able to add a null listener so make it a no-op here.
- */
- if (listener == null)
- return;
-
- synchronized (conferenceMembersSoundLevelListeners)
- {
- if (conferenceMembersSoundLevelListeners.size() == 0)
- {
- // if this is the first listener that's being registered with
- // us, we also need to register ourselves as a CSRC audio level
- // listener with the media handler.
- getMediaHandler().setCsrcAudioLevelListener(this);
- }
- conferenceMembersSoundLevelListeners.add(listener);
- }
- }
-
- /**
- * Adds a specific <tt>SoundLevelListener</tt> to the list of listeners
- * interested in and notified about changes in the sound level of the audio
- * sent by the remote party. When the first listener is being registered
- * the method also registers its single listener with the media handler so
- * that it would receive level change events and delegate them to the
- * listeners that have registered with us.
- *
- * @param listener the <tt>SoundLevelListener</tt> to add
- */
- public void addStreamSoundLevelListener(SoundLevelListener listener)
- {
- synchronized (streamSoundLevelListenersSyncRoot)
- {
- if ((streamSoundLevelListeners == null)
- || streamSoundLevelListeners.isEmpty())
- {
- CallPeerMediaHandler<?> mediaHandler = getMediaHandler();
-
- if (isJitsiVideobridge())
- {
- /*
- * When the local user/peer has organized a telephony
- * conference utilizing the Jitsi Videobridge server-side
- * technology, the server will calculate the audio levels
- * and not the client.
- */
- mediaHandler.setCsrcAudioLevelListener(this);
- }
- else
- {
- /*
- * If this is the first listener that's being registered
- * with us, we also need to register ourselves as an audio
- * level listener with the media handler. We do this so that
- * audio levels would only be calculated if anyone is
- * interested in receiving them.
- */
- mediaHandler.setStreamAudioLevelListener(this);
- }
- }
-
- /*
- * Implement streamAudioLevelListeners as a copy-on-write storage so
- * that iterators over it can iterate without
- * ConcurrentModificationExceptions.
- */
- streamSoundLevelListeners
- = (streamSoundLevelListeners == null)
- ? new ArrayList<SoundLevelListener>()
- : new ArrayList<SoundLevelListener>(
- streamSoundLevelListeners);
- streamSoundLevelListeners.add(listener);
- }
- }
-
- /**
- * Adds a specific <tt>PropertyChangeListener</tt> to the list of
- * listeners which get notified when the properties (e.g.
- * LOCAL_VIDEO_STREAMING) associated with this <tt>CallPeer</tt> change
- * their values.
- *
- * @param listener the <tt>PropertyChangeListener</tt> to be notified
- * when the properties associated with the specified <tt>Call</tt> change
- * their values
- */
- public void addVideoPropertyChangeListener(PropertyChangeListener listener)
- {
- if (listener == null)
- throw new NullPointerException("listener");
-
- synchronized (videoPropertyChangeListeners)
- {
- /*
- * The video is part of the media-related functionality and thus it
- * is the responsibility of mediaHandler. So listen to mediaHandler
- * for video-related property changes and re-fire them as
- * originating from this instance.
- */
- if (!videoPropertyChangeListeners.contains(listener)
- && videoPropertyChangeListeners.add(listener)
- && (mediaHandlerPropertyChangeListener == null))
- {
- mediaHandlerPropertyChangeListener
- = new PropertyChangeListener()
- {
- public void propertyChange(PropertyChangeEvent event)
- {
- Iterable<PropertyChangeListener> listeners;
-
- synchronized (videoPropertyChangeListeners)
- {
- listeners
- = new LinkedList<PropertyChangeListener>(
- videoPropertyChangeListeners);
- }
-
- PropertyChangeEvent thisEvent
- = new PropertyChangeEvent(
- this,
- event.getPropertyName(),
- event.getOldValue(),
- event.getNewValue());
-
- for (PropertyChangeListener listener : listeners)
- listener.propertyChange(thisEvent);
- }
- };
- getMediaHandler()
- .addPropertyChangeListener(
- mediaHandlerPropertyChangeListener);
- }
- }
- }
-
- /**
- * Notified by its very majesty the media service about changes in the audio
- * level of the stream coming from this peer, the method generates the
- * corresponding events and delivers them to the listeners that have
- * registered here.
- *
- * @param newLevel the new audio level of the audio stream received from the
- * remote peer
- */
- public void audioLevelChanged(int newLevel)
- {
- /*
- * If we're in a conference in which this CallPeer is the focus and
- * we're the only member in it besides the focus, we will not receive
- * audio levels in the RTP and our media will instead measure the audio
- * levels of the received media. In order to make the UI oblivious of
- * the difference, we have to translate the event to the appropriate
- * type of listener.
- *
- * We may end up in a conference call with 0 members if the server
- * for some reason doesn't support sip conference (our subscribes
- * doesn't go to the focus of the conference) and so we must
- * pass the sound levels measured on the stream so we can see
- * the stream activity of the call.
- */
- int conferenceMemberCount = getConferenceMemberCount();
-
- if ((conferenceMemberCount > 0) && (conferenceMemberCount < 3))
- {
- long audioRemoteSSRC
- = getMediaHandler().getRemoteSSRC(MediaType.AUDIO);
-
- if (audioRemoteSSRC != CallPeerMediaHandler.SSRC_UNKNOWN)
- {
- audioLevelsReceived(new long[] { audioRemoteSSRC, newLevel });
- return;
- }
- }
-
- fireStreamSoundLevelChanged(newLevel);
- }
-
-
- /**
- * Implements {@link CsrcAudioLevelListener#audioLevelsReceived(long[])}.
- * Delivers the received audio levels to the
- * {@link ConferenceMembersSoundLevelListener}s registered with this
- * <tt>MediaAwareCallPeer</tt>..
- *
- * @param audioLevels the levels that we need to dispatch to all registered
- * <tt>ConferenceMemberSoundLevelListeners</tt>.
- */
- public void audioLevelsReceived(long[] audioLevels)
- {
- /*
- * When the local user/peer has organized a telephony conference
- * utilizing the Jitsi Videobridge server-side technology, the server
- * will calculate the audio levels and not the client.
- */
- if (isJitsiVideobridge())
- {
- long audioRemoteSSRC
- = getMediaHandler().getRemoteSSRC(MediaType.AUDIO);
-
- if (audioRemoteSSRC != CallPeerMediaHandler.SSRC_UNKNOWN)
- {
- for (int i = 0; i < audioLevels.length; i += 2)
- {
- if (audioLevels[i] == audioRemoteSSRC)
- {
- fireStreamSoundLevelChanged((int) audioLevels[i + 1]);
- break;
- }
- }
- }
- }
-
- if (getConferenceMemberCount() == 0)
- return;
-
- Map<ConferenceMember, Integer> levelsMap
- = new HashMap<ConferenceMember, Integer>();
-
- for (int i = 0; i < audioLevels.length; i += 2)
- {
- ConferenceMember mmbr = findConferenceMember(audioLevels[i]);
-
- if (mmbr != null)
- levelsMap.put(mmbr, (int) audioLevels[i + 1]);
- }
-
- synchronized (conferenceMembersSoundLevelListeners)
- {
- int conferenceMemberSoundLevelListenerCount
- = conferenceMembersSoundLevelListeners.size();
-
- if (conferenceMemberSoundLevelListenerCount > 0)
- {
- ConferenceMembersSoundLevelEvent ev
- = new ConferenceMembersSoundLevelEvent(this, levelsMap);
-
- for (int i = 0;
- i < conferenceMemberSoundLevelListenerCount;
- i++)
- {
- conferenceMembersSoundLevelListeners
- .get(i)
- .soundLevelChanged(ev);
- }
- }
- }
- }
-
- /**
- * Does nothing.
- * @param evt the event.
- */
- public void callPeerAdded(CallPeerEvent evt) {}
-
- /**
- * Does nothing.
- * @param evt the event.
- */
- public void callPeerRemoved(CallPeerEvent evt) {}
-
- /**
- * Dummy implementation of {@link CallPeerConferenceListener
- * #conferenceFocusChanged(CallPeerConferenceEvent)}.
- *
- * @param evt ignored
- */
- public void conferenceFocusChanged(CallPeerConferenceEvent evt)
- {
- }
-
- /**
- * Called when this peer becomes a mixer. The method add removes this class
- * as the stream audio level listener for the media coming from this peer
- * because the levels it delivers no longer represent the level of a
- * particular member. The method also adds this class as a member (CSRC)
- * audio level listener.
- *
- * @param conferenceEvent the event containing information (that we don't
- * really use) on the newly add member.
- */
- public void conferenceMemberAdded(CallPeerConferenceEvent conferenceEvent)
- {
- if (getConferenceMemberCount() > 2)
- {
- /*
- * This peer is now a conference focus with more than three
- * participants. It means that this peer is mixing and sending us
- * audio for at least two separate participants. We therefore need
- * to switch from stream to CSRC level listening.
- */
- CallPeerMediaHandler<?> mediaHandler = getMediaHandler();
-
- mediaHandler.setStreamAudioLevelListener(null);
- mediaHandler.setCsrcAudioLevelListener(this);
- }
- }
-
- /**
- * Dummy implementation of {@link CallPeerConferenceListener
- * #conferenceMemberErrorReceived(CallPeerConferenceEvent)}.
- *
- * @param ev the event
- */
- public void conferenceMemberErrorReceived(CallPeerConferenceEvent ev) {};
-
- /**
- * Called when this peer stops being a mixer. The method add removes this
- * class as the stream audio level listener for the media coming from this
- * peer because the levels it delivers no longer represent the level of a
- * particular member. The method also adds this class as a member (CSRC)
- * audio level listener.
- *
- * @param conferenceEvent the event containing information (that we don't
- * really use) on the freshly removed member.
- */
- public void conferenceMemberRemoved(CallPeerConferenceEvent conferenceEvent)
- {
- if (getConferenceMemberCount() < 3)
- {
- /*
- * This call peer is no longer mixing audio from multiple sources
- * since there's only us and her in the call. We therefore need to
- * switch from CSRC to stream level listening.
- */
- CallPeerMediaHandler<?> mediaHandler = getMediaHandler();
-
- mediaHandler.setStreamAudioLevelListener(this);
- mediaHandler.setCsrcAudioLevelListener(null);
- }
- }
-
- /**
- * Invokes {@link SoundLevelListener#soundLevelChanged(Object, int) on
- * the <tt>SoundLevelListener</tt>s interested in the changes of the audio
- * stream received from the remote peer i.e. in
- * {@link #streamSoundLevelListeners}.
- *
- * @param newLevel the new value of the sound level to notify
- * <tt>streamSoundLevelListeners</tt> about
- */
- private void fireStreamSoundLevelChanged(int newLevel)
- {
- List<SoundLevelListener> streamSoundLevelListeners;
-
- synchronized (streamSoundLevelListenersSyncRoot)
- {
- /*
- * Since the streamAudioLevelListeners field of this
- * MediaAwareCallPeer is implemented as a copy-on-write storage,
- * just get a reference to it and it should be safe to iterate over it
- * without ConcurrentModificationExceptions.
- */
- streamSoundLevelListeners = this.streamSoundLevelListeners;
- }
-
- if (streamSoundLevelListeners != null)
- {
- /*
- * Iterate over streamAudioLevelListeners using an index rather than
- * an Iterator in order to try to reduce the number of allocations
- * (as the number of audio level changes is expected to be very
- * large).
- */
- int streamSoundLevelListenerCount
- = streamSoundLevelListeners.size();
-
- for(int i = 0; i < streamSoundLevelListenerCount; i++)
- {
- streamSoundLevelListeners.get(i).soundLevelChanged(
- this,
- newLevel);
- }
- }
- }
-
- /**
- * Returns a reference to the call that this peer belongs to. Calls
- * are created by underlying telephony protocol implementations.
- *
- * @return a reference to the call containing this peer.
- */
- @Override
- public T getCall()
- {
- return call;
- }
-
- /**
- * The method returns an image representation of the call peer if one is
- * available.
- *
- * @return byte[] a byte array containing the image or null if no image is
- * available.
- */
- public byte[] getImage()
- {
- return image;
- }
-
- /**
- * Returns a reference to the <tt>CallPeerMediaHandler</tt> used by this
- * peer. The media handler class handles all media management for a single
- * <tt>CallPeer</tt>. This includes initializing and configuring streams,
- * generating SDP, handling ICE, etc. One instance of <tt>CallPeer</tt>
- * always corresponds to exactly one instance of
- * <tt>CallPeerMediaHandler</tt> and both classes are only separated for
- * reasons of readability.
- *
- * @return a reference to the <tt>CallPeerMediaHandler</tt> instance that
- * this peer uses for media related tips and tricks.
- */
- public U getMediaHandler()
- {
- return mediaHandler;
- }
-
- /**
- * Returns a unique identifier representing this peer.
- *
- * @return an identifier representing this call peer.
- */
- public String getPeerID()
- {
- return peerID;
- }
-
- /**
- * Returns the protocol provider that this peer belongs to.
- *
- * @return a reference to the <tt>ProtocolProviderService</tt> that this
- * peer belongs to.
- */
- @Override
- public V getProtocolProvider()
- {
- return protocolProvider;
- }
-
- /**
- * Determines whether this <tt>CallPeer</tt> is participating in a telephony
- * conference organized by the local user/peer utilizing the Jitsi
- * Videobridge server-side technology.
- *
- * @return <tt>true</tt> if this <tt>CallPeer</tt> is participating in a
- * telephony conference organized by the local user/peer utilizing the Jitsi
- * Videobridge server-side technology; otherwise, <tt>false</tt>
- */
- public final boolean isJitsiVideobridge()
- {
- Call call = getCall();
-
- if (call != null)
- {
- CallConference conference = call.getConference();
-
- if (conference != null)
- return conference.isJitsiVideobridge();
- }
- return false;
- }
-
- /**
- * Determines whether we are currently streaming video toward whoever this
- * <tt>MediaAwareCallPeer</tt> represents.
- *
- * @return <tt>true</tt> if we are currently streaming video toward this
- * <tt>CallPeer</tt> and <tt>false</tt> otherwise.
- */
- public boolean isLocalVideoStreaming()
- {
- return getMediaHandler().isLocalVideoTransmissionEnabled();
- }
-
- /**
- * Determines whether the audio stream (if any) being sent to this
- * peer is mute.
- *
- * @return <tt>true</tt> if an audio stream is being sent to this
- * peer and it is currently mute; <tt>false</tt>, otherwise
- */
- @Override
- public boolean isMute()
- {
- return getMediaHandler().isMute();
- }
-
- /**
- * Logs <tt>message</tt> and <tt>cause</tt> and sets this <tt>peer</tt>'s
- * state to <tt>CallPeerState.FAILED</tt>
- *
- * @param message a message to log and display to the user.
- * @param throwable the exception that cause the error we are logging
- */
- public void logAndFail(String message, Throwable throwable)
- {
- logger.error(message, throwable);
- setState(CallPeerState.FAILED, message);
- }
-
- /**
- * Updates the state of this <tt>CallPeer</tt> to match the locally-on-hold
- * status of our media handler.
- */
- public void reevalLocalHoldStatus()
- {
- CallPeerState state = getState();
- boolean locallyOnHold = getMediaHandler().isLocallyOnHold();
-
- if (CallPeerState.ON_HOLD_LOCALLY.equals(state))
- {
- if (!locallyOnHold)
- setState(CallPeerState.CONNECTED);
- }
- else if (CallPeerState.ON_HOLD_MUTUALLY.equals(state))
- {
- if (!locallyOnHold)
- setState(CallPeerState.ON_HOLD_REMOTELY);
- }
- else if (CallPeerState.ON_HOLD_REMOTELY.equals(state))
- {
- if (locallyOnHold)
- setState(CallPeerState.ON_HOLD_MUTUALLY);
- }
- else if (locallyOnHold)
- {
- setState(CallPeerState.ON_HOLD_LOCALLY);
- }
- }
-
- /**
- * Updates the state of this <tt>CallPeer</tt> to match the remotely-on-hold
- * status of our media handler.
- */
- public void reevalRemoteHoldStatus()
- {
- boolean remotelyOnHold = getMediaHandler().isRemotelyOnHold();
-
- CallPeerState state = getState();
- if (CallPeerState.ON_HOLD_LOCALLY.equals(state))
- {
- if (remotelyOnHold)
- setState(CallPeerState.ON_HOLD_MUTUALLY);
- }
- else if (CallPeerState.ON_HOLD_MUTUALLY.equals(state))
- {
- if (!remotelyOnHold)
- setState(CallPeerState.ON_HOLD_LOCALLY);
- }
- else if (CallPeerState.ON_HOLD_REMOTELY.equals(state))
- {
- if (!remotelyOnHold)
- setState(CallPeerState.CONNECTED);
- }
- else if (remotelyOnHold)
- {
- setState(CallPeerState.ON_HOLD_REMOTELY);
- }
- }
-
- /**
- * Removes a specific <tt>ConferenceMembersSoundLevelListener</tt> of the
- * list of listeners interested in and notified about changes in conference
- * members sound level.
- *
- * @param listener the <tt>ConferenceMembersSoundLevelListener</tt> to
- * remove
- */
- public void removeConferenceMembersSoundLevelListener(
- ConferenceMembersSoundLevelListener listener)
- {
- synchronized (conferenceMembersSoundLevelListeners)
- {
- if (conferenceMembersSoundLevelListeners.remove(listener)
- && (conferenceMembersSoundLevelListeners.size() == 0))
- {
- // if this was the last listener then we also remove ourselves
- // as a CSRC audio level listener from the handler so that we
- // don't have to create new events and maps for something no one
- // is interested in.
- getMediaHandler().setCsrcAudioLevelListener(null);
- }
- }
- }
-
- /**
- * Removes a specific <tt>SoundLevelListener</tt> of the list of
- * listeners interested in and notified about changes in stream sound level
- * related information.
- *
- * @param listener the <tt>SoundLevelListener</tt> to remove
- */
- public void removeStreamSoundLevelListener(SoundLevelListener listener)
- {
- synchronized (streamSoundLevelListenersSyncRoot)
- {
- /*
- * Implement streamAudioLevelListeners as a copy-on-write storage so
- * that iterators over it can iterate over it without
- * ConcurrentModificationExceptions.
- */
- if (streamSoundLevelListeners != null)
- {
- streamSoundLevelListeners
- = new ArrayList<SoundLevelListener>(
- streamSoundLevelListeners);
- if (streamSoundLevelListeners.remove(listener)
- && streamSoundLevelListeners.isEmpty())
- streamSoundLevelListeners = null;
- }
-
- if ((streamSoundLevelListeners == null)
- || streamSoundLevelListeners.isEmpty())
- {
- // if this was the last listener then we also need to remove
- // ourselves as an audio level so that audio levels would only
- // be calculated if anyone is interested in receiving them.
- getMediaHandler().setStreamAudioLevelListener(null);
- }
- }
- }
-
- /**
- * Removes a specific <tt>PropertyChangeListener</tt> from the list of
- * listeners which get notified when the properties (e.g.
- * LOCAL_VIDEO_STREAMING) associated with this <tt>CallPeer</tt> change
- * their values.
- *
- * @param listener the <tt>PropertyChangeListener</tt> to no longer be
- * notified when the properties associated with the specified <tt>Call</tt>
- * change their values
- */
- public void removeVideoPropertyChangeListener(
- PropertyChangeListener listener)
- {
- if (listener != null)
- synchronized (videoPropertyChangeListeners)
- {
- /*
- * The video is part of the media-related functionality and thus
- * it is the responsibility of mediaHandler. So we're listening
- * to mediaHandler for video-related property changes and w're
- * re-firing them as originating from this instance. Make sure
- * that we're not listening to mediaHandler if noone is
- * interested in video-related property changes originating from
- * this instance.
- */
- if (videoPropertyChangeListeners.remove(listener)
- && videoPropertyChangeListeners.isEmpty()
- && (mediaHandlerPropertyChangeListener != null))
- {
-// getMediaHandler()
-// .removePropertyChangeListener(
-// mediaHandlerPropertyChangeListener);
- mediaHandlerPropertyChangeListener = null;
- }
- }
- }
-
- /**
- * Sets the security message associated with a failure/warning or
- * information coming from the encryption protocol.
- *
- * @param messageType the type of the message.
- * @param i18nMessage the message
- * @param severity severity level
- */
- public void securityMessageReceived(
- String messageType,
- String i18nMessage,
- int severity)
- {
- fireCallPeerSecurityMessageEvent(messageType,
- i18nMessage,
- severity);
- }
-
- /**
- * Indicates that the other party has timeouted replying to our
- * offer to secure the connection.
- *
- * @param mediaType the <tt>MediaType</tt> of the call session
- * @param sender the security controller that caused the event
- */
- public void securityNegotiationStarted(
- MediaType mediaType,
- SrtpControl sender)
- {
- fireCallPeerSecurityNegotiationStartedEvent(
- new CallPeerSecurityNegotiationStartedEvent(
- this,
- toSessionType(mediaType),
- sender));
- }
-
- /**
- * Indicates that the other party has timeouted replying to our
- * offer to secure the connection.
- *
- * @param mediaType the <tt>MediaType</tt> of the call session
- */
- public void securityTimeout(MediaType mediaType)
- {
- fireCallPeerSecurityTimeoutEvent(
- new CallPeerSecurityTimeoutEvent(
- this,
- toSessionType(mediaType)));
- }
-
- /**
- * Sets the security status to OFF for this call peer.
- *
- * @param mediaType the <tt>MediaType</tt> of the call session
- */
- public void securityTurnedOff(MediaType mediaType)
- {
- // If this event has been triggered because of a call end event and the
- // call is already ended we don't need to alert the user for
- // security off.
- if((call != null) && !call.getCallState().equals(CallState.CALL_ENDED))
- {
- fireCallPeerSecurityOffEvent(
- new CallPeerSecurityOffEvent(
- this,
- toSessionType(mediaType)));
- }
- }
-
- /**
- * Sets the security status to ON for this call peer.
- *
- * @param mediaType the <tt>MediaType</tt> of the call session
- * @param cipher the cipher
- * @param sender the security controller that caused the event
- */
- public void securityTurnedOn(
- MediaType mediaType,
- String cipher,
- SrtpControl sender)
- {
- getMediaHandler().startSrtpMultistream(sender);
- fireCallPeerSecurityOnEvent(
- new CallPeerSecurityOnEvent(
- this,
- toSessionType(mediaType),
- cipher,
- sender));
- }
-
- /**
- * Sets the call containing this peer.
- *
- * @param call the call that this call peer is participating in.
- */
- public void setCall(T call)
- {
- this.call = call;
- }
-
- /**
- * Sets the byte array containing an image representation (photo or picture)
- * of the call peer.
- *
- * @param image a byte array containing the image
- */
- public void setImage(byte[] image)
- {
- byte[] oldImage = getImage();
- this.image = image;
-
- //Fire the Event
- fireCallPeerChangeEvent(
- CallPeerChangeEvent.CALL_PEER_IMAGE_CHANGE,
- oldImage,
- image);
- }
-
- /**
- * Modifies the local media setup to reflect the requested setting for the
- * streaming of the local video and then re-invites the peer represented by
- * this class using a corresponding SDP description..
- *
- * @param allowed <tt>true</tt> if local video transmission is allowed and
- * <tt>false</tt> otherwise.
- *
- * @throws OperationFailedException if video initialization fails.
- */
- public void setLocalVideoAllowed(boolean allowed)
- throws OperationFailedException
- {
- CallPeerMediaHandler<?> mediaHandler = getMediaHandler();
-
- if(mediaHandler.isLocalVideoTransmissionEnabled() != allowed)
- {
- // Modify the local media setup to reflect the requested setting for
- // the streaming of the local video.
- mediaHandler.setLocalVideoTransmissionEnabled(allowed);
- }
- }
-
- /**
- * Sets a reference to the <tt>CallPeerMediaHandler</tt> used by this
- * peer. The media handler class handles all media management for a single
- * <tt>CallPeer</tt>. This includes initializing and configuring streams,
- * generating SDP, handling ICE, etc. One instance of <tt>CallPeer</tt>
- * always corresponds to exactly one instance of
- * <tt>CallPeerMediaHandler</tt> and both classes are only separated for
- * reasons of readability.
- *
- * @param mediaHandler a reference to the <tt>CallPeerMediaHandler</tt>
- * instance that this peer uses for media related tips and tricks.
- */
- protected void setMediaHandler(U mediaHandler)
- {
- this.mediaHandler = mediaHandler;
- }
-
- /**
- * Sets the mute property for this call peer.
- *
- * @param newMuteValue the new value of the mute property for this call peer
- */
- @Override
- public void setMute(boolean newMuteValue)
- {
- getMediaHandler().setMute(newMuteValue);
- super.setMute(newMuteValue);
- }
-
- /**
- * Sets the String that serves as a unique identifier of this
- * CallPeer.
- * @param peerID the ID of this call peer.
- */
- public void setPeerID(String peerID)
- {
- this.peerID = peerID;
- }
-
- /**
- * Overrides the parent set state method in order to make sure that we
- * close our media handler whenever we enter a disconnected state.
- *
- * @param newState the <tt>CallPeerState</tt> that we are about to enter and
- * that we pass to our predecessor.
- * @param reason a reason phrase explaining the state (e.g. if newState
- * indicates a failure) and that we pass to our predecessor.
- * @param reasonCode the code for the reason of the state change.
- */
- @Override
- public void setState(CallPeerState newState, String reason, int reasonCode)
- {
- // synchronized to mediaHandler if there are currently jobs of
- // initializing, configuring and starting streams (method processAnswer
- // of CallPeerMediaHandler) we won't set and fire the current state
- // to Disconnected. Before closing the mediaHandler is setting the state
- // in order to deliver states as quick as possible.
- CallPeerMediaHandler<?> mediaHandler = getMediaHandler();
-
- synchronized(mediaHandler)
- {
- try
- {
- super.setState(newState, reason, reasonCode);
- }
- finally
- {
- // make sure whatever happens to close the media
- if (CallPeerState.DISCONNECTED.equals(newState)
- || CallPeerState.FAILED.equals(newState))
- mediaHandler.close();
- }
- }
- }
-
- /**
- * Returns the last <tt>ConferenceInfoDocument</tt> sent by us to this
- * <tt>CallPeer</tt>. It is a document with state <tt>full</tt>
- * @return the last <tt>ConferenceInfoDocument</tt> sent by us to this
- * <tt>CallPeer</tt>. It is a document with state <tt>full</tt>
- */
- public ConferenceInfoDocument getLastConferenceInfoSent()
- {
- return lastConferenceInfoSent;
- }
-
- /**
- * Sets the last <tt>ConferenceInfoDocument</tt> sent by us to this
- * <tt>CallPeer</tt>.
- * @param confInfo the document to set.
- */
- public void setLastConferenceInfoSent(ConferenceInfoDocument confInfo)
- {
- lastConferenceInfoSent = confInfo;
- }
-
- /**
- * Gets the time (as obtained by <tt>System.currentTimeMillis()</tt>)
- * at which we last sent a <tt>ConferenceInfoDocument</tt> to this
- * <tt>CallPeer</tt>.
- * @return the time (as obtained by <tt>System.currentTimeMillis()</tt>)
- * at which we last sent a <tt>ConferenceInfoDocument</tt> to this
- * <tt>CallPeer</tt>.
- */
- public long getLastConferenceInfoSentTimestamp()
- {
- return lastConferenceInfoSentTimestamp;
- }
-
- /**
- * Sets the time (as obtained by <tt>System.currentTimeMillis()</tt>)
- * at which we last sent a <tt>ConferenceInfoDocument</tt> to this
- * <tt>CallPeer</tt>.
- * @param newTimestamp the time to set
- */
- public void setLastConferenceInfoSentTimestamp(long newTimestamp)
- {
- lastConferenceInfoSentTimestamp = newTimestamp;
- }
-
- /**
- * Gets the last <tt>ConferenceInfoDocument</tt> sent to us by this
- * <tt>CallPeer</tt>.
- * @return the last <tt>ConferenceInfoDocument</tt> sent to us by this
- * <tt>CallPeer</tt>.
- */
- public ConferenceInfoDocument getLastConferenceInfoReceived()
- {
- return lastConferenceInfoReceived;
- }
-
- /**
- * Gets the last <tt>ConferenceInfoDocument</tt> sent to us by this
- * <tt>CallPeer</tt>.
- * @return the last <tt>ConferenceInfoDocument</tt> sent to us by this
- * <tt>CallPeer</tt>.
- */
- public void setLastConferenceInfoReceived(ConferenceInfoDocument confInfo)
- {
- lastConferenceInfoReceived = confInfo;
- }
-
- /**
- * Gets the <tt>version</tt> of the last <tt>ConferenceInfoDocument</tt>
- * sent to us by this <tt>CallPeer</tt>, or -1 if we haven't (yet) received
- * a <tt>ConferenceInformationDocument</tt> from this <tt>CallPeer</tt>.
- * @return
- */
- public int getLastConferenceInfoReceivedVersion()
- {
- return (lastConferenceInfoReceived == null)
- ? -1
- : lastConferenceInfoReceived.getVersion();
- }
-
- /**
- * Gets the <tt>String</tt> to be used for this <tt>CallPeer</tt> when
- * we describe it in a <tt>ConferenceInfoDocument</tt> (e.g. the
- * <tt>entity</tt> key attribute which to use for the <tt>user</tt>
- * element corresponding to this <tt>CallPeer</tt>)
- *
- * @return the <tt>String</tt> to be used for this <tt>CallPeer</tt> when
- * we describe it in a <tt>ConferenceInfoDocument</tt> (e.g. the
- * <tt>entity</tt> key attribute which to use for the <tt>user</tt>
- * element corresponding to this <tt>CallPeer</tt>)
- */
- public abstract String getEntity();
-
- /**
- * Check whether a conference-info document is scheduled to be sent to
- * this <tt>CallPeer</tt> (i.e. there is a thread which will eventually
- * (after sleeping a certain amount of time) trigger a document to be sent)
- * @return <tt>true</tt> if there is a conference-info document scheduled
- * to be sent to this <tt>CallPeer</tt> and <tt>false</tt> otherwise.
- */
- public boolean isConfInfoScheduled()
- {
- synchronized (confInfoScheduledSyncRoot)
- {
- return confInfoScheduled;
- }
- }
-
- /**
- * Sets the property which indicates whether a conference-info document
- * is scheduled to be sent to this <tt>CallPeer</tt>.
- * @param confInfoScheduled
- */
- public void setConfInfoScheduled(boolean confInfoScheduled)
- {
- synchronized (confInfoScheduledSyncRoot)
- {
- this.confInfoScheduled = confInfoScheduled;
- }
- }
-
- /**
- * Returns the direction of the session for media of type <tt>mediaType</tt>
- * that we have with this <tt>CallPeer</tt>. This is the direction of the
- * session negotiated in the signaling protocol, and it may or may not
- * coincide with the direction of the media stream.
- * For example, if we are the focus of a videobridge conference and another
- * peer is sending video to us, we have a <tt>RECVONLY</tt> video stream,
- * but <tt>SENDONLY</tt> or <tt>SENDRECV</tt> (Jingle) sessions with the
- * rest of the conference members.
- * Should always return non-null.
- *
- * @param mediaType the <tt>MediaType</tt> to use
- * @return Returns the direction of the session for media of type
- * <tt>mediaType</tt> that we have with this <tt>CallPeer</tt>.
- */
- public abstract MediaDirection getDirection(MediaType mediaType);
-
- /**
- * {@inheritDoc}
- *
- * When a <tt>ConferenceMember</tt> is removed from a conference with a
- * Jitsi-videobridge, an RTCP BYE packet is not always sent. Therefore,
- * if the <tt>ConferenceMember</tt> had an associated video SSRC, the stream
- * isn't be removed until it times out, leaving a blank video container in
- * the interface for a few seconds.
- * TODO: This works around the problem by removing the
- * <tt>ConferenceMember</tt>'s <tt>ReceiveStream</tt> when the
- * <tt>ConferenceMember</tt> is removed. The proper solution is to ensure
- * that RTCP BYEs are sent whenever necessary, and when it is deployed this
- * code should be removed.
- *
- * @param conferenceMember a <tt>ConferenceMember</tt> to be removed from
- * the list of <tt>ConferenceMember</tt> reported by this peer. If the
- * specified <tt>ConferenceMember</tt> is no contained in the list, no event
- */
- @Override
- public void removeConferenceMember(ConferenceMember conferenceMember)
- {
- MediaStream videoStream = getMediaHandler().getStream(MediaType.VIDEO);
- if (videoStream != null)
- videoStream.removeReceiveStreamForSsrc(
- conferenceMember.getVideoSsrc());
-
- super.removeConferenceMember(conferenceMember);
- }
-
- /**
- * Converts a specific <tt>MediaType</tt> into a <tt>sessionType</tt> value
- * in the terms of the <tt>CallPeerSecurityStatusEvent</tt> class.
- *
- * @param mediaType the <tt>MediaType</tt> to be converted
- * @return the <tt>sessionType</tt> value in the terms of the
- * <tt>CallPeerSecurityStatusEvent</tt> class that is equivalent to the
- * specified <tt>mediaType</tt>
- */
- private static int toSessionType(MediaType mediaType)
- {
- switch (mediaType)
- {
- case AUDIO:
- return CallPeerSecurityStatusEvent.AUDIO_SESSION;
- case VIDEO:
- return CallPeerSecurityStatusEvent.VIDEO_SESSION;
- default:
- throw new IllegalArgumentException("mediaType");
- }
- }
-}
+package net.java.sip.communicator.service.protocol.media; + +import java.beans.*; +import java.util.*; + +import net.java.sip.communicator.service.protocol.*; +import net.java.sip.communicator.service.protocol.event.*; +import net.java.sip.communicator.util.*; + +import org.jitsi.service.neomedia.*; +import org.jitsi.service.neomedia.event.*; + +/** + * A utility class implementing media control code shared between current + * telephony implementations. This class is only meant for use by protocol + * implementations and should/could not be accessed by bundles that are simply + * using the telephony functionalities. + * + * @param <T> the peer extension class like for example <tt>CallSipImpl</tt> + * or <tt>CallJabberImpl</tt> + * @param <U> the media handler extension class like for example + * <tt>CallPeerMediaHandlerSipImpl</tt> or + * <tt>CallPeerMediaHandlerJabberImpl</tt> + * @param <V> the provider extension class like for example + * <tt>ProtocolProviderServiceSipImpl</tt> or + * <tt>ProtocolProviderServiceJabberImpl</tt> + * + * @author Emil Ivov + * @author Lyubomir Marinov + * @author Boris Grozev + */ +public abstract class MediaAwareCallPeer + <T extends MediaAwareCall<?, ?, V>, + U extends CallPeerMediaHandler<?>, + V extends ProtocolProviderService> + extends AbstractCallPeer<T, V> + implements SrtpListener, + CallPeerConferenceListener, + CsrcAudioLevelListener, + SimpleAudioLevelListener +{ + /** + * The <tt>Logger</tt> used by the <tt>MediaAwareCallPeer</tt> class and its + * instances for logging output. + */ + private static final Logger logger + = Logger.getLogger(MediaAwareCallPeer.class); + + /** + * The call this peer belongs to. + */ + private T call; + + /** + * The listeners registered for level changes in the audio of participants + * that this peer might be mixing and that we are not directly communicating + * with. + */ + private final List<ConferenceMembersSoundLevelListener> + conferenceMembersSoundLevelListeners + = new ArrayList<ConferenceMembersSoundLevelListener>(); + + /** + * A byte array containing the image/photo representing the call peer. + */ + private byte[] image; + + /** + * The media handler class handles all media management for a single + * <tt>CallPeer</tt>. This includes initializing and configuring streams, + * generating SDP, handling ICE, etc. One instance of <tt>CallPeer</tt>always + * corresponds to exactly one instance of <tt>CallPeerMediaHandler</tt> and + * both classes are only separated for reasons of readability. + */ + private U mediaHandler; + + /** + * The <tt>PropertyChangeListener</tt> which listens to + * {@link CallPeerMediaHandler} for changes in the values of its properties. + */ + private PropertyChangeListener mediaHandlerPropertyChangeListener; + + /** + * A string uniquely identifying the peer. + */ + private String peerID; + + /** + * The protocol provider that this peer belongs to. + */ + private final V protocolProvider; + + /** + * The list of <tt>SoundLevelListener</tt>s interested in level changes in + * the audio we are getting from the remote peer. + * <p> + * It is implemented as a copy-on-write storage because the number of + * additions and removals of <tt>SoundLevelListener</tt>s is expected to be + * far smaller than the number of audio level changes. The access to it is + * to be synchronized using {@link #streamSoundLevelListenersSyncRoot}. + * </p> + */ + private List<SoundLevelListener> streamSoundLevelListeners; + + /** + * The <tt>Object</tt> to synchronize the access to + * {@link #streamSoundLevelListeners}. + */ + private final Object streamSoundLevelListenersSyncRoot = new Object(); + + /** + * The <tt>List</tt> of <tt>PropertyChangeListener</tt>s listening to this + * <tt>CallPeer</tt> for changes in the values of its properties related to + * video. + */ + private final List<PropertyChangeListener> videoPropertyChangeListeners + = new LinkedList<PropertyChangeListener>(); + + /** + * Represents the last Conference Information (RFC4575) document sent to + * this <tt>CallPeer</tt>. This is always a document with state "full", even + * if the last document actually sent was a "partial" + */ + private ConferenceInfoDocument lastConferenceInfoSent = null; + + /** + * The time (as obtained by <tt>System.currentTimeMillis()</tt>) at which + * a Conference Information (RFC4575) document was last sent to this + * <tt>CallPeer</tt>. + */ + private long lastConferenceInfoSentTimestamp = -1; + + /** + * The last Conference Information (RFC4575) document sent to us by this + * <tt>CallPeer</tt>. This is always a document with state "full", which is + * only gets updated by "partial" or "deleted" documents. + */ + private ConferenceInfoDocument lastConferenceInfoReceived = null; + + /** + * Whether a conference-info document has been scheduled to be sent to this + * <tt>CallPeer</tt> + */ + private boolean confInfoScheduled = false; + + /** + * Synchronization object for confInfoScheduled + */ + private final Object confInfoScheduledSyncRoot = new Object(); + + /** + * Creates a new call peer with address <tt>peerAddress</tt>. + * + * @param owningCall the call that contains this call peer. + */ + public MediaAwareCallPeer(T owningCall) + { + this.call = owningCall; + this.protocolProvider = owningCall.getProtocolProvider(); + + // create the uid + this.peerID + = String.valueOf(System.currentTimeMillis()) + + String.valueOf(hashCode()); + + // we listen for events when the call will become focus or not + // of a conference so we will add or remove our sound level listeners + super.addCallPeerConferenceListener(this); + } + + /** + * Adds a specific <tt>ConferenceMembersSoundLevelListener</tt> to the list + * of listeners interested in and notified about changes in conference + * members sound level. + * + * @param listener the <tt>ConferenceMembersSoundLevelListener</tt> to add + * @throws NullPointerException if <tt>listener</tt> is <tt>null</tt> + */ + public void addConferenceMembersSoundLevelListener( + ConferenceMembersSoundLevelListener listener) + { + /* + * XXX The uses of the method at the time of this writing rely on being + * able to add a null listener so make it a no-op here. + */ + if (listener == null) + return; + + synchronized (conferenceMembersSoundLevelListeners) + { + if (conferenceMembersSoundLevelListeners.size() == 0) + { + // if this is the first listener that's being registered with + // us, we also need to register ourselves as a CSRC audio level + // listener with the media handler. + getMediaHandler().setCsrcAudioLevelListener(this); + } + conferenceMembersSoundLevelListeners.add(listener); + } + } + + /** + * Adds a specific <tt>SoundLevelListener</tt> to the list of listeners + * interested in and notified about changes in the sound level of the audio + * sent by the remote party. When the first listener is being registered + * the method also registers its single listener with the media handler so + * that it would receive level change events and delegate them to the + * listeners that have registered with us. + * + * @param listener the <tt>SoundLevelListener</tt> to add + */ + public void addStreamSoundLevelListener(SoundLevelListener listener) + { + synchronized (streamSoundLevelListenersSyncRoot) + { + if ((streamSoundLevelListeners == null) + || streamSoundLevelListeners.isEmpty()) + { + CallPeerMediaHandler<?> mediaHandler = getMediaHandler(); + + if (isJitsiVideobridge()) + { + /* + * When the local user/peer has organized a telephony + * conference utilizing the Jitsi Videobridge server-side + * technology, the server will calculate the audio levels + * and not the client. + */ + mediaHandler.setCsrcAudioLevelListener(this); + } + else + { + /* + * If this is the first listener that's being registered + * with us, we also need to register ourselves as an audio + * level listener with the media handler. We do this so that + * audio levels would only be calculated if anyone is + * interested in receiving them. + */ + mediaHandler.setStreamAudioLevelListener(this); + } + } + + /* + * Implement streamAudioLevelListeners as a copy-on-write storage so + * that iterators over it can iterate without + * ConcurrentModificationExceptions. + */ + streamSoundLevelListeners + = (streamSoundLevelListeners == null) + ? new ArrayList<SoundLevelListener>() + : new ArrayList<SoundLevelListener>( + streamSoundLevelListeners); + streamSoundLevelListeners.add(listener); + } + } + + /** + * Adds a specific <tt>PropertyChangeListener</tt> to the list of + * listeners which get notified when the properties (e.g. + * LOCAL_VIDEO_STREAMING) associated with this <tt>CallPeer</tt> change + * their values. + * + * @param listener the <tt>PropertyChangeListener</tt> to be notified + * when the properties associated with the specified <tt>Call</tt> change + * their values + */ + public void addVideoPropertyChangeListener(PropertyChangeListener listener) + { + if (listener == null) + throw new NullPointerException("listener"); + + synchronized (videoPropertyChangeListeners) + { + /* + * The video is part of the media-related functionality and thus it + * is the responsibility of mediaHandler. So listen to mediaHandler + * for video-related property changes and re-fire them as + * originating from this instance. + */ + if (!videoPropertyChangeListeners.contains(listener) + && videoPropertyChangeListeners.add(listener) + && (mediaHandlerPropertyChangeListener == null)) + { + mediaHandlerPropertyChangeListener + = new PropertyChangeListener() + { + public void propertyChange(PropertyChangeEvent event) + { + Iterable<PropertyChangeListener> listeners; + + synchronized (videoPropertyChangeListeners) + { + listeners + = new LinkedList<PropertyChangeListener>( + videoPropertyChangeListeners); + } + + PropertyChangeEvent thisEvent + = new PropertyChangeEvent( + this, + event.getPropertyName(), + event.getOldValue(), + event.getNewValue()); + + for (PropertyChangeListener listener : listeners) + listener.propertyChange(thisEvent); + } + }; + getMediaHandler() + .addPropertyChangeListener( + mediaHandlerPropertyChangeListener); + } + } + } + + /** + * Notified by its very majesty the media service about changes in the audio + * level of the stream coming from this peer, the method generates the + * corresponding events and delivers them to the listeners that have + * registered here. + * + * @param newLevel the new audio level of the audio stream received from the + * remote peer + */ + public void audioLevelChanged(int newLevel) + { + /* + * If we're in a conference in which this CallPeer is the focus and + * we're the only member in it besides the focus, we will not receive + * audio levels in the RTP and our media will instead measure the audio + * levels of the received media. In order to make the UI oblivious of + * the difference, we have to translate the event to the appropriate + * type of listener. + * + * We may end up in a conference call with 0 members if the server + * for some reason doesn't support sip conference (our subscribes + * doesn't go to the focus of the conference) and so we must + * pass the sound levels measured on the stream so we can see + * the stream activity of the call. + */ + int conferenceMemberCount = getConferenceMemberCount(); + + if ((conferenceMemberCount > 0) && (conferenceMemberCount < 3)) + { + long audioRemoteSSRC + = getMediaHandler().getRemoteSSRC(MediaType.AUDIO); + + if (audioRemoteSSRC != CallPeerMediaHandler.SSRC_UNKNOWN) + { + audioLevelsReceived(new long[] { audioRemoteSSRC, newLevel }); + return; + } + } + + fireStreamSoundLevelChanged(newLevel); + } + + + /** + * Implements {@link CsrcAudioLevelListener#audioLevelsReceived(long[])}. + * Delivers the received audio levels to the + * {@link ConferenceMembersSoundLevelListener}s registered with this + * <tt>MediaAwareCallPeer</tt>.. + * + * @param audioLevels the levels that we need to dispatch to all registered + * <tt>ConferenceMemberSoundLevelListeners</tt>. + */ + public void audioLevelsReceived(long[] audioLevels) + { + /* + * When the local user/peer has organized a telephony conference + * utilizing the Jitsi Videobridge server-side technology, the server + * will calculate the audio levels and not the client. + */ + if (isJitsiVideobridge()) + { + long audioRemoteSSRC + = getMediaHandler().getRemoteSSRC(MediaType.AUDIO); + + if (audioRemoteSSRC != CallPeerMediaHandler.SSRC_UNKNOWN) + { + for (int i = 0; i < audioLevels.length; i += 2) + { + if (audioLevels[i] == audioRemoteSSRC) + { + fireStreamSoundLevelChanged((int) audioLevels[i + 1]); + break; + } + } + } + } + + if (getConferenceMemberCount() == 0) + return; + + Map<ConferenceMember, Integer> levelsMap + = new HashMap<ConferenceMember, Integer>(); + + for (int i = 0; i < audioLevels.length; i += 2) + { + ConferenceMember mmbr = findConferenceMember(audioLevels[i]); + + if (mmbr != null) + levelsMap.put(mmbr, (int) audioLevels[i + 1]); + } + + synchronized (conferenceMembersSoundLevelListeners) + { + int conferenceMemberSoundLevelListenerCount + = conferenceMembersSoundLevelListeners.size(); + + if (conferenceMemberSoundLevelListenerCount > 0) + { + ConferenceMembersSoundLevelEvent ev + = new ConferenceMembersSoundLevelEvent(this, levelsMap); + + for (int i = 0; + i < conferenceMemberSoundLevelListenerCount; + i++) + { + conferenceMembersSoundLevelListeners + .get(i) + .soundLevelChanged(ev); + } + } + } + } + + /** + * Does nothing. + * @param evt the event. + */ + public void callPeerAdded(CallPeerEvent evt) {} + + /** + * Does nothing. + * @param evt the event. + */ + public void callPeerRemoved(CallPeerEvent evt) {} + + /** + * Dummy implementation of {@link CallPeerConferenceListener + * #conferenceFocusChanged(CallPeerConferenceEvent)}. + * + * @param evt ignored + */ + public void conferenceFocusChanged(CallPeerConferenceEvent evt) + { + } + + /** + * Called when this peer becomes a mixer. The method add removes this class + * as the stream audio level listener for the media coming from this peer + * because the levels it delivers no longer represent the level of a + * particular member. The method also adds this class as a member (CSRC) + * audio level listener. + * + * @param conferenceEvent the event containing information (that we don't + * really use) on the newly add member. + */ + public void conferenceMemberAdded(CallPeerConferenceEvent conferenceEvent) + { + if (getConferenceMemberCount() > 2) + { + /* + * This peer is now a conference focus with more than three + * participants. It means that this peer is mixing and sending us + * audio for at least two separate participants. We therefore need + * to switch from stream to CSRC level listening. + */ + CallPeerMediaHandler<?> mediaHandler = getMediaHandler(); + + mediaHandler.setStreamAudioLevelListener(null); + mediaHandler.setCsrcAudioLevelListener(this); + } + } + + /** + * Dummy implementation of {@link CallPeerConferenceListener + * #conferenceMemberErrorReceived(CallPeerConferenceEvent)}. + * + * @param ev the event + */ + public void conferenceMemberErrorReceived(CallPeerConferenceEvent ev) {}; + + /** + * Called when this peer stops being a mixer. The method add removes this + * class as the stream audio level listener for the media coming from this + * peer because the levels it delivers no longer represent the level of a + * particular member. The method also adds this class as a member (CSRC) + * audio level listener. + * + * @param conferenceEvent the event containing information (that we don't + * really use) on the freshly removed member. + */ + public void conferenceMemberRemoved(CallPeerConferenceEvent conferenceEvent) + { + if (getConferenceMemberCount() < 3) + { + /* + * This call peer is no longer mixing audio from multiple sources + * since there's only us and her in the call. We therefore need to + * switch from CSRC to stream level listening. + */ + CallPeerMediaHandler<?> mediaHandler = getMediaHandler(); + + mediaHandler.setStreamAudioLevelListener(this); + mediaHandler.setCsrcAudioLevelListener(null); + } + } + + /** + * Invokes {@link SoundLevelListener#soundLevelChanged(Object, int) on + * the <tt>SoundLevelListener</tt>s interested in the changes of the audio + * stream received from the remote peer i.e. in + * {@link #streamSoundLevelListeners}. + * + * @param newLevel the new value of the sound level to notify + * <tt>streamSoundLevelListeners</tt> about + */ + private void fireStreamSoundLevelChanged(int newLevel) + { + List<SoundLevelListener> streamSoundLevelListeners; + + synchronized (streamSoundLevelListenersSyncRoot) + { + /* + * Since the streamAudioLevelListeners field of this + * MediaAwareCallPeer is implemented as a copy-on-write storage, + * just get a reference to it and it should be safe to iterate over it + * without ConcurrentModificationExceptions. + */ + streamSoundLevelListeners = this.streamSoundLevelListeners; + } + + if (streamSoundLevelListeners != null) + { + /* + * Iterate over streamAudioLevelListeners using an index rather than + * an Iterator in order to try to reduce the number of allocations + * (as the number of audio level changes is expected to be very + * large). + */ + int streamSoundLevelListenerCount + = streamSoundLevelListeners.size(); + + for(int i = 0; i < streamSoundLevelListenerCount; i++) + { + streamSoundLevelListeners.get(i).soundLevelChanged( + this, + newLevel); + } + } + } + + /** + * Returns a reference to the call that this peer belongs to. Calls + * are created by underlying telephony protocol implementations. + * + * @return a reference to the call containing this peer. + */ + @Override + public T getCall() + { + return call; + } + + /** + * The method returns an image representation of the call peer if one is + * available. + * + * @return byte[] a byte array containing the image or null if no image is + * available. + */ + public byte[] getImage() + { + return image; + } + + /** + * Returns a reference to the <tt>CallPeerMediaHandler</tt> used by this + * peer. The media handler class handles all media management for a single + * <tt>CallPeer</tt>. This includes initializing and configuring streams, + * generating SDP, handling ICE, etc. One instance of <tt>CallPeer</tt> + * always corresponds to exactly one instance of + * <tt>CallPeerMediaHandler</tt> and both classes are only separated for + * reasons of readability. + * + * @return a reference to the <tt>CallPeerMediaHandler</tt> instance that + * this peer uses for media related tips and tricks. + */ + public U getMediaHandler() + { + return mediaHandler; + } + + /** + * Returns a unique identifier representing this peer. + * + * @return an identifier representing this call peer. + */ + public String getPeerID() + { + return peerID; + } + + /** + * Returns the protocol provider that this peer belongs to. + * + * @return a reference to the <tt>ProtocolProviderService</tt> that this + * peer belongs to. + */ + @Override + public V getProtocolProvider() + { + return protocolProvider; + } + + /** + * Determines whether this <tt>CallPeer</tt> is participating in a telephony + * conference organized by the local user/peer utilizing the Jitsi + * Videobridge server-side technology. + * + * @return <tt>true</tt> if this <tt>CallPeer</tt> is participating in a + * telephony conference organized by the local user/peer utilizing the Jitsi + * Videobridge server-side technology; otherwise, <tt>false</tt> + */ + public final boolean isJitsiVideobridge() + { + Call call = getCall(); + + if (call != null) + { + CallConference conference = call.getConference(); + + if (conference != null) + return conference.isJitsiVideobridge(); + } + return false; + } + + /** + * Determines whether we are currently streaming video toward whoever this + * <tt>MediaAwareCallPeer</tt> represents. + * + * @return <tt>true</tt> if we are currently streaming video toward this + * <tt>CallPeer</tt> and <tt>false</tt> otherwise. + */ + public boolean isLocalVideoStreaming() + { + return getMediaHandler().isLocalVideoTransmissionEnabled(); + } + + /** + * Determines whether the audio stream (if any) being sent to this + * peer is mute. + * + * @return <tt>true</tt> if an audio stream is being sent to this + * peer and it is currently mute; <tt>false</tt>, otherwise + */ + @Override + public boolean isMute() + { + return getMediaHandler().isMute(); + } + + /** + * Logs <tt>message</tt> and <tt>cause</tt> and sets this <tt>peer</tt>'s + * state to <tt>CallPeerState.FAILED</tt> + * + * @param message a message to log and display to the user. + * @param throwable the exception that cause the error we are logging + */ + public void logAndFail(String message, Throwable throwable) + { + logger.error(message, throwable); + setState(CallPeerState.FAILED, message); + } + + /** + * Updates the state of this <tt>CallPeer</tt> to match the locally-on-hold + * status of our media handler. + */ + public void reevalLocalHoldStatus() + { + CallPeerState state = getState(); + boolean locallyOnHold = getMediaHandler().isLocallyOnHold(); + + if (CallPeerState.ON_HOLD_LOCALLY.equals(state)) + { + if (!locallyOnHold) + setState(CallPeerState.CONNECTED); + } + else if (CallPeerState.ON_HOLD_MUTUALLY.equals(state)) + { + if (!locallyOnHold) + setState(CallPeerState.ON_HOLD_REMOTELY); + } + else if (CallPeerState.ON_HOLD_REMOTELY.equals(state)) + { + if (locallyOnHold) + setState(CallPeerState.ON_HOLD_MUTUALLY); + } + else if (locallyOnHold) + { + setState(CallPeerState.ON_HOLD_LOCALLY); + } + } + + /** + * Updates the state of this <tt>CallPeer</tt> to match the remotely-on-hold + * status of our media handler. + */ + public void reevalRemoteHoldStatus() + { + boolean remotelyOnHold = getMediaHandler().isRemotelyOnHold(); + + CallPeerState state = getState(); + if (CallPeerState.ON_HOLD_LOCALLY.equals(state)) + { + if (remotelyOnHold) + setState(CallPeerState.ON_HOLD_MUTUALLY); + } + else if (CallPeerState.ON_HOLD_MUTUALLY.equals(state)) + { + if (!remotelyOnHold) + setState(CallPeerState.ON_HOLD_LOCALLY); + } + else if (CallPeerState.ON_HOLD_REMOTELY.equals(state)) + { + if (!remotelyOnHold) + setState(CallPeerState.CONNECTED); + } + else if (remotelyOnHold) + { + setState(CallPeerState.ON_HOLD_REMOTELY); + } + } + + /** + * Removes a specific <tt>ConferenceMembersSoundLevelListener</tt> of the + * list of listeners interested in and notified about changes in conference + * members sound level. + * + * @param listener the <tt>ConferenceMembersSoundLevelListener</tt> to + * remove + */ + public void removeConferenceMembersSoundLevelListener( + ConferenceMembersSoundLevelListener listener) + { + synchronized (conferenceMembersSoundLevelListeners) + { + if (conferenceMembersSoundLevelListeners.remove(listener) + && (conferenceMembersSoundLevelListeners.size() == 0)) + { + // if this was the last listener then we also remove ourselves + // as a CSRC audio level listener from the handler so that we + // don't have to create new events and maps for something no one + // is interested in. + getMediaHandler().setCsrcAudioLevelListener(null); + } + } + } + + /** + * Removes a specific <tt>SoundLevelListener</tt> of the list of + * listeners interested in and notified about changes in stream sound level + * related information. + * + * @param listener the <tt>SoundLevelListener</tt> to remove + */ + public void removeStreamSoundLevelListener(SoundLevelListener listener) + { + synchronized (streamSoundLevelListenersSyncRoot) + { + /* + * Implement streamAudioLevelListeners as a copy-on-write storage so + * that iterators over it can iterate over it without + * ConcurrentModificationExceptions. + */ + if (streamSoundLevelListeners != null) + { + streamSoundLevelListeners + = new ArrayList<SoundLevelListener>( + streamSoundLevelListeners); + if (streamSoundLevelListeners.remove(listener) + && streamSoundLevelListeners.isEmpty()) + streamSoundLevelListeners = null; + } + + if ((streamSoundLevelListeners == null) + || streamSoundLevelListeners.isEmpty()) + { + // if this was the last listener then we also need to remove + // ourselves as an audio level so that audio levels would only + // be calculated if anyone is interested in receiving them. + getMediaHandler().setStreamAudioLevelListener(null); + } + } + } + + /** + * Removes a specific <tt>PropertyChangeListener</tt> from the list of + * listeners which get notified when the properties (e.g. + * LOCAL_VIDEO_STREAMING) associated with this <tt>CallPeer</tt> change + * their values. + * + * @param listener the <tt>PropertyChangeListener</tt> to no longer be + * notified when the properties associated with the specified <tt>Call</tt> + * change their values + */ + public void removeVideoPropertyChangeListener( + PropertyChangeListener listener) + { + if (listener != null) + synchronized (videoPropertyChangeListeners) + { + /* + * The video is part of the media-related functionality and thus + * it is the responsibility of mediaHandler. So we're listening + * to mediaHandler for video-related property changes and w're + * re-firing them as originating from this instance. Make sure + * that we're not listening to mediaHandler if noone is + * interested in video-related property changes originating from + * this instance. + */ + if (videoPropertyChangeListeners.remove(listener) + && videoPropertyChangeListeners.isEmpty() + && (mediaHandlerPropertyChangeListener != null)) + { +// getMediaHandler() +// .removePropertyChangeListener( +// mediaHandlerPropertyChangeListener); + mediaHandlerPropertyChangeListener = null; + } + } + } + + /** + * Sets the security message associated with a failure/warning or + * information coming from the encryption protocol. + * + * @param messageType the type of the message. + * @param i18nMessage the message + * @param severity severity level + */ + public void securityMessageReceived( + String messageType, + String i18nMessage, + int severity) + { + fireCallPeerSecurityMessageEvent(messageType, + i18nMessage, + severity); + } + + /** + * Indicates that the other party has timeouted replying to our + * offer to secure the connection. + * + * @param mediaType the <tt>MediaType</tt> of the call session + * @param sender the security controller that caused the event + */ + public void securityNegotiationStarted( + MediaType mediaType, + SrtpControl sender) + { + fireCallPeerSecurityNegotiationStartedEvent( + new CallPeerSecurityNegotiationStartedEvent( + this, + toSessionType(mediaType), + sender)); + } + + /** + * Indicates that the other party has timeouted replying to our + * offer to secure the connection. + * + * @param mediaType the <tt>MediaType</tt> of the call session + */ + public void securityTimeout(MediaType mediaType) + { + fireCallPeerSecurityTimeoutEvent( + new CallPeerSecurityTimeoutEvent( + this, + toSessionType(mediaType))); + } + + /** + * Sets the security status to OFF for this call peer. + * + * @param mediaType the <tt>MediaType</tt> of the call session + */ + public void securityTurnedOff(MediaType mediaType) + { + // If this event has been triggered because of a call end event and the + // call is already ended we don't need to alert the user for + // security off. + if((call != null) && !call.getCallState().equals(CallState.CALL_ENDED)) + { + fireCallPeerSecurityOffEvent( + new CallPeerSecurityOffEvent( + this, + toSessionType(mediaType))); + } + } + + /** + * Sets the security status to ON for this call peer. + * + * @param mediaType the <tt>MediaType</tt> of the call session + * @param cipher the cipher + * @param sender the security controller that caused the event + */ + public void securityTurnedOn( + MediaType mediaType, + String cipher, + SrtpControl sender) + { + getMediaHandler().startSrtpMultistream(sender); + fireCallPeerSecurityOnEvent( + new CallPeerSecurityOnEvent( + this, + toSessionType(mediaType), + cipher, + sender)); + } + + /** + * Sets the call containing this peer. + * + * @param call the call that this call peer is participating in. + */ + public void setCall(T call) + { + this.call = call; + } + + /** + * Sets the byte array containing an image representation (photo or picture) + * of the call peer. + * + * @param image a byte array containing the image + */ + public void setImage(byte[] image) + { + byte[] oldImage = getImage(); + this.image = image; + + //Fire the Event + fireCallPeerChangeEvent( + CallPeerChangeEvent.CALL_PEER_IMAGE_CHANGE, + oldImage, + image); + } + + /** + * Modifies the local media setup to reflect the requested setting for the + * streaming of the local video and then re-invites the peer represented by + * this class using a corresponding SDP description.. + * + * @param allowed <tt>true</tt> if local video transmission is allowed and + * <tt>false</tt> otherwise. + * + * @throws OperationFailedException if video initialization fails. + */ + public void setLocalVideoAllowed(boolean allowed) + throws OperationFailedException + { + CallPeerMediaHandler<?> mediaHandler = getMediaHandler(); + + if(mediaHandler.isLocalVideoTransmissionEnabled() != allowed) + { + // Modify the local media setup to reflect the requested setting for + // the streaming of the local video. + mediaHandler.setLocalVideoTransmissionEnabled(allowed); + } + } + + /** + * Sets a reference to the <tt>CallPeerMediaHandler</tt> used by this + * peer. The media handler class handles all media management for a single + * <tt>CallPeer</tt>. This includes initializing and configuring streams, + * generating SDP, handling ICE, etc. One instance of <tt>CallPeer</tt> + * always corresponds to exactly one instance of + * <tt>CallPeerMediaHandler</tt> and both classes are only separated for + * reasons of readability. + * + * @param mediaHandler a reference to the <tt>CallPeerMediaHandler</tt> + * instance that this peer uses for media related tips and tricks. + */ + protected void setMediaHandler(U mediaHandler) + { + this.mediaHandler = mediaHandler; + } + + /** + * Sets the mute property for this call peer. + * + * @param newMuteValue the new value of the mute property for this call peer + */ + @Override + public void setMute(boolean newMuteValue) + { + getMediaHandler().setMute(newMuteValue); + super.setMute(newMuteValue); + } + + /** + * Sets the String that serves as a unique identifier of this + * CallPeer. + * @param peerID the ID of this call peer. + */ + public void setPeerID(String peerID) + { + this.peerID = peerID; + } + + /** + * Overrides the parent set state method in order to make sure that we + * close our media handler whenever we enter a disconnected state. + * + * @param newState the <tt>CallPeerState</tt> that we are about to enter and + * that we pass to our predecessor. + * @param reason a reason phrase explaining the state (e.g. if newState + * indicates a failure) and that we pass to our predecessor. + * @param reasonCode the code for the reason of the state change. + */ + @Override + public void setState(CallPeerState newState, String reason, int reasonCode) + { + // synchronized to mediaHandler if there are currently jobs of + // initializing, configuring and starting streams (method processAnswer + // of CallPeerMediaHandler) we won't set and fire the current state + // to Disconnected. Before closing the mediaHandler is setting the state + // in order to deliver states as quick as possible. + CallPeerMediaHandler<?> mediaHandler = getMediaHandler(); + + synchronized(mediaHandler) + { + try + { + super.setState(newState, reason, reasonCode); + } + finally + { + // make sure whatever happens to close the media + if (CallPeerState.DISCONNECTED.equals(newState) + || CallPeerState.FAILED.equals(newState)) + mediaHandler.close(); + } + } + } + + /** + * Returns the last <tt>ConferenceInfoDocument</tt> sent by us to this + * <tt>CallPeer</tt>. It is a document with state <tt>full</tt> + * @return the last <tt>ConferenceInfoDocument</tt> sent by us to this + * <tt>CallPeer</tt>. It is a document with state <tt>full</tt> + */ + public ConferenceInfoDocument getLastConferenceInfoSent() + { + return lastConferenceInfoSent; + } + + /** + * Sets the last <tt>ConferenceInfoDocument</tt> sent by us to this + * <tt>CallPeer</tt>. + * @param confInfo the document to set. + */ + public void setLastConferenceInfoSent(ConferenceInfoDocument confInfo) + { + lastConferenceInfoSent = confInfo; + } + + /** + * Gets the time (as obtained by <tt>System.currentTimeMillis()</tt>) + * at which we last sent a <tt>ConferenceInfoDocument</tt> to this + * <tt>CallPeer</tt>. + * @return the time (as obtained by <tt>System.currentTimeMillis()</tt>) + * at which we last sent a <tt>ConferenceInfoDocument</tt> to this + * <tt>CallPeer</tt>. + */ + public long getLastConferenceInfoSentTimestamp() + { + return lastConferenceInfoSentTimestamp; + } + + /** + * Sets the time (as obtained by <tt>System.currentTimeMillis()</tt>) + * at which we last sent a <tt>ConferenceInfoDocument</tt> to this + * <tt>CallPeer</tt>. + * @param newTimestamp the time to set + */ + public void setLastConferenceInfoSentTimestamp(long newTimestamp) + { + lastConferenceInfoSentTimestamp = newTimestamp; + } + + /** + * Gets the last <tt>ConferenceInfoDocument</tt> sent to us by this + * <tt>CallPeer</tt>. + * @return the last <tt>ConferenceInfoDocument</tt> sent to us by this + * <tt>CallPeer</tt>. + */ + public ConferenceInfoDocument getLastConferenceInfoReceived() + { + return lastConferenceInfoReceived; + } + + /** + * Gets the last <tt>ConferenceInfoDocument</tt> sent to us by this + * <tt>CallPeer</tt>. + * @return the last <tt>ConferenceInfoDocument</tt> sent to us by this + * <tt>CallPeer</tt>. + */ + public void setLastConferenceInfoReceived(ConferenceInfoDocument confInfo) + { + lastConferenceInfoReceived = confInfo; + } + + /** + * Gets the <tt>version</tt> of the last <tt>ConferenceInfoDocument</tt> + * sent to us by this <tt>CallPeer</tt>, or -1 if we haven't (yet) received + * a <tt>ConferenceInformationDocument</tt> from this <tt>CallPeer</tt>. + * @return + */ + public int getLastConferenceInfoReceivedVersion() + { + return (lastConferenceInfoReceived == null) + ? -1 + : lastConferenceInfoReceived.getVersion(); + } + + /** + * Gets the <tt>String</tt> to be used for this <tt>CallPeer</tt> when + * we describe it in a <tt>ConferenceInfoDocument</tt> (e.g. the + * <tt>entity</tt> key attribute which to use for the <tt>user</tt> + * element corresponding to this <tt>CallPeer</tt>) + * + * @return the <tt>String</tt> to be used for this <tt>CallPeer</tt> when + * we describe it in a <tt>ConferenceInfoDocument</tt> (e.g. the + * <tt>entity</tt> key attribute which to use for the <tt>user</tt> + * element corresponding to this <tt>CallPeer</tt>) + */ + public abstract String getEntity(); + + /** + * Check whether a conference-info document is scheduled to be sent to + * this <tt>CallPeer</tt> (i.e. there is a thread which will eventually + * (after sleeping a certain amount of time) trigger a document to be sent) + * @return <tt>true</tt> if there is a conference-info document scheduled + * to be sent to this <tt>CallPeer</tt> and <tt>false</tt> otherwise. + */ + public boolean isConfInfoScheduled() + { + synchronized (confInfoScheduledSyncRoot) + { + return confInfoScheduled; + } + } + + /** + * Sets the property which indicates whether a conference-info document + * is scheduled to be sent to this <tt>CallPeer</tt>. + * @param confInfoScheduled + */ + public void setConfInfoScheduled(boolean confInfoScheduled) + { + synchronized (confInfoScheduledSyncRoot) + { + this.confInfoScheduled = confInfoScheduled; + } + } + + /** + * Returns the direction of the session for media of type <tt>mediaType</tt> + * that we have with this <tt>CallPeer</tt>. This is the direction of the + * session negotiated in the signaling protocol, and it may or may not + * coincide with the direction of the media stream. + * For example, if we are the focus of a videobridge conference and another + * peer is sending video to us, we have a <tt>RECVONLY</tt> video stream, + * but <tt>SENDONLY</tt> or <tt>SENDRECV</tt> (Jingle) sessions with the + * rest of the conference members. + * Should always return non-null. + * + * @param mediaType the <tt>MediaType</tt> to use + * @return Returns the direction of the session for media of type + * <tt>mediaType</tt> that we have with this <tt>CallPeer</tt>. + */ + public abstract MediaDirection getDirection(MediaType mediaType); + + /** + * {@inheritDoc} + * + * When a <tt>ConferenceMember</tt> is removed from a conference with a + * Jitsi-videobridge, an RTCP BYE packet is not always sent. Therefore, + * if the <tt>ConferenceMember</tt> had an associated video SSRC, the stream + * isn't be removed until it times out, leaving a blank video container in + * the interface for a few seconds. + * TODO: This works around the problem by removing the + * <tt>ConferenceMember</tt>'s <tt>ReceiveStream</tt> when the + * <tt>ConferenceMember</tt> is removed. The proper solution is to ensure + * that RTCP BYEs are sent whenever necessary, and when it is deployed this + * code should be removed. + * + * @param conferenceMember a <tt>ConferenceMember</tt> to be removed from + * the list of <tt>ConferenceMember</tt> reported by this peer. If the + * specified <tt>ConferenceMember</tt> is no contained in the list, no event + */ + @Override + public void removeConferenceMember(ConferenceMember conferenceMember) + { + MediaStream videoStream = getMediaHandler().getStream(MediaType.VIDEO); + if (videoStream != null) + videoStream.removeReceiveStreamForSsrc( + conferenceMember.getVideoSsrc()); + + super.removeConferenceMember(conferenceMember); + } + + /** + * Converts a specific <tt>MediaType</tt> into a <tt>sessionType</tt> value + * in the terms of the <tt>CallPeerSecurityStatusEvent</tt> class. + * + * @param mediaType the <tt>MediaType</tt> to be converted + * @return the <tt>sessionType</tt> value in the terms of the + * <tt>CallPeerSecurityStatusEvent</tt> class that is equivalent to the + * specified <tt>mediaType</tt> + */ + private static int toSessionType(MediaType mediaType) + { + switch (mediaType) + { + case AUDIO: + return CallPeerSecurityStatusEvent.AUDIO_SESSION; + case VIDEO: + return CallPeerSecurityStatusEvent.VIDEO_SESSION; + default: + throw new IllegalArgumentException("mediaType"); + } + } +} diff --git a/src/net/java/sip/communicator/service/protocol/media/TransportManager.java b/src/net/java/sip/communicator/service/protocol/media/TransportManager.java index 973ebcb..051a302 100644 --- a/src/net/java/sip/communicator/service/protocol/media/TransportManager.java +++ b/src/net/java/sip/communicator/service/protocol/media/TransportManager.java @@ -1,4 +1,4 @@ -/*
+/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd @@ -15,870 +15,890 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package net.java.sip.communicator.service.protocol.media;
-
-import java.net.*;
-
-import net.java.sip.communicator.service.netaddr.*;
-import net.java.sip.communicator.service.protocol.*;
-import net.java.sip.communicator.util.*;
-
-import org.ice4j.ice.*;
-import org.jitsi.service.configuration.*;
-import org.jitsi.service.neomedia.*;
-
-/**
- * <tt>TransportManager</tt>s are responsible for allocating ports, gathering
- * local candidates and managing ICE whenever we are using it.
- *
- * @param <U> the peer extension class like for example <tt>CallPeerSipImpl</tt>
- * or <tt>CallPeerJabberImpl</tt>
- *
- * @author Emil Ivov
- * @author Lyubomir Marinov
- * @author Sebastien Vincent
- */
-public abstract class TransportManager<U extends MediaAwareCallPeer<?, ?, ?>>
-{
- /**
- * The <tt>Logger</tt> used by the <tt>TransportManager</tt>
- * class and its instances for logging output.
- */
- private static final Logger logger
- = Logger.getLogger(TransportManager.class);
-
- /**
- * The port tracker that we should use when binding generic media streams.
- * <p>
- * Initialized by {@link #initializePortNumbers()}.
- * </p>
- */
- private static final PortTracker defaultPortTracker
- = new PortTracker(5000, 6000);
-
- /**
- * The port tracker that we should use when binding video media streams.
- * <p>
- * Potentially initialized by {@link #initializePortNumbers()} if the
- * necessary properties are set.
- * </p>
- */
- private static PortTracker videoPortTracker;
-
- /**
- * The port tracker that we should use when binding data channels.
- * <p>
- * Potentially initialized by {@link #initializePortNumbers()} if the
- * necessary properties are set.
- * </p>
- */
- private static PortTracker dataPortTracker;
-
- /**
- * The port tracker that we should use when binding data media streams.
- * <p>
- * Potentially initialized by {@link #initializePortNumbers()} if the
- * necessary properties are set.
- * </p>
- */
- private static PortTracker audioPortTracker;
-
- /**
- * RTP audio DSCP configuration property name.
- */
- private static final String RTP_AUDIO_DSCP_PROPERTY =
- "net.java.sip.communicator.impl.protocol.RTP_AUDIO_DSCP";
-
- /**
- * RTP video DSCP configuration property name.
- */
- private static final String RTP_VIDEO_DSCP_PROPERTY =
- "net.java.sip.communicator.impl.protocol.RTP_VIDEO_DSCP";
-
- /**
- * Number of empty UDP packets to send for NAT hole punching.
- */
- private static final String HOLE_PUNCH_PKT_COUNT_PROPERTY =
- "net.java.sip.communicator.impl.protocol.HOLE_PUNCH_PKT_COUNT";
-
- /**
- * Number of empty UDP packets to send for NAT hole punching.
- */
- private static final int DEFAULT_HOLE_PUNCH_PKT_COUNT = 3;
-
- /**
- * The {@link MediaAwareCallPeer} whose traffic we will be taking care of.
- */
- private U callPeer;
-
- /**
- * The RTP/RTCP socket couples that this <tt>TransportManager</tt> uses to
- * send and receive media flows through indexed by <tt>MediaType</tt>
- * (ordinal).
- */
- private final StreamConnector[] streamConnectors
- = new StreamConnector[MediaType.values().length];
-
- /**
- * Creates a new instance of this transport manager, binding it to the
- * specified peer.
- *
- * @param callPeer the {@link MediaAwareCallPeer} whose traffic we will be
- * taking care of.
- */
- protected TransportManager(U callPeer)
- {
- this.callPeer = callPeer;
- }
-
- /**
- * Returns the <tt>StreamConnector</tt> instance that this media handler
- * should use for streams of the specified <tt>mediaType</tt>. The method
- * would also create a new <tt>StreamConnector</tt> if no connector has
- * been initialized for this <tt>mediaType</tt> yet or in case one
- * of its underlying sockets has been closed.
- *
- * @param mediaType the <tt>MediaType</tt> that we'd like to create a
- * connector for.
- * @return this media handler's <tt>StreamConnector</tt> for the specified
- * <tt>mediaType</tt>.
- *
- * @throws OperationFailedException in case we failed to initialize our
- * connector.
- */
- public StreamConnector getStreamConnector(MediaType mediaType)
- throws OperationFailedException
- {
- int streamConnectorIndex = mediaType.ordinal();
- StreamConnector streamConnector
- = streamConnectors[streamConnectorIndex];
-
- if((streamConnector == null)
- || (streamConnector.getProtocol() == StreamConnector.Protocol.UDP))
- {
- DatagramSocket controlSocket;
-
- if((streamConnector == null)
- || streamConnector.getDataSocket().isClosed()
- || (((controlSocket = streamConnector.getControlSocket())
- != null)
- && controlSocket.isClosed()))
- {
- streamConnectors[streamConnectorIndex]
- = streamConnector
- = createStreamConnector(mediaType);
- }
- }
- else if(streamConnector.getProtocol() == StreamConnector.Protocol.TCP)
- {
- Socket controlTCPSocket;
-
- if(streamConnector.getDataTCPSocket().isClosed()
- || (((controlTCPSocket = streamConnector.getControlTCPSocket())
- != null)
- && controlTCPSocket.isClosed()))
- {
- streamConnectors[streamConnectorIndex]
- = streamConnector
- = createStreamConnector(mediaType);
- }
- }
- return streamConnector;
- }
-
- /**
- * Closes the existing <tt>StreamConnector</tt>, if any, associated with a
- * specific <tt>MediaType</tt> and removes its reference from this
- * <tt>TransportManager</tt>.
- *
- * @param mediaType the <tt>MediaType</tt> associated with the
- * <tt>StreamConnector</tt> to close
- */
- public void closeStreamConnector(MediaType mediaType)
- {
- int index = mediaType.ordinal();
- StreamConnector streamConnector = streamConnectors[index];
-
- if (streamConnector != null)
- {
- closeStreamConnector(mediaType, streamConnector);
- streamConnectors[index] = null;
- }
- }
-
- /**
- * Closes a specific <tt>StreamConnector</tt> associated with a specific
- * <tt>MediaType</tt>. If this <tt>TransportManager</tt> has a reference to
- * the specified <tt>streamConnector</tt>, it remains. Allows extenders to
- * override and perform additional customizations to the closing of the
- * specified <tt>streamConnector</tt>.
- *
- * @param mediaType the <tt>MediaType</tt> associated with the specified
- * <tt>streamConnector</tt>
- * @param streamConnector the <tt>StreamConnector</tt> to be closed
- * @see #closeStreamConnector(MediaType)
- */
- protected void closeStreamConnector(
- MediaType mediaType,
- StreamConnector streamConnector)
- {
- /*
- * XXX The connected owns the sockets so it is important that it
- * decides whether to close them i.e. this TransportManager is not
- * allowed to explicitly close the sockets by itself.
- */
- streamConnector.close();
- }
-
- /**
- * Creates a media <tt>StreamConnector</tt> for a stream of a specific
- * <tt>MediaType</tt>. The minimum and maximum of the media port boundaries
- * are taken into account.
- *
- * @param mediaType the <tt>MediaType</tt> of the stream for which a
- * <tt>StreamConnector</tt> is to be created
- * @return a <tt>StreamConnector</tt> for the stream of the specified
- * <tt>mediaType</tt>
- * @throws OperationFailedException if the binding of the sockets fails
- */
- protected StreamConnector createStreamConnector(MediaType mediaType)
- throws OperationFailedException
- {
- NetworkAddressManagerService nam
- = ProtocolMediaActivator.getNetworkAddressManagerService();
- InetAddress intendedDestination = getIntendedDestination(getCallPeer());
- InetAddress localHostForPeer = nam.getLocalHost(intendedDestination);
-
- PortTracker portTracker = getPortTracker(mediaType);
-
- //create the RTP socket.
- DatagramSocket rtpSocket = null;
- try
- {
- rtpSocket = nam.createDatagramSocket(
- localHostForPeer, portTracker.getPort(),
- portTracker.getMinPort(), portTracker.getMaxPort());
- }
- catch (Exception exc)
- {
- throw new OperationFailedException(
- "Failed to allocate the network ports necessary for the call.",
- OperationFailedException.INTERNAL_ERROR, exc);
- }
-
- //make sure that next time we don't try to bind on occupied ports
- //also, refuse validation in case someone set the tracker range to 1
- portTracker.setNextPort( rtpSocket.getLocalPort() + 1, false);
-
- //create the RTCP socket, preferably on the port following our RTP one.
- DatagramSocket rtcpSocket = null;
- try
- {
- rtcpSocket = nam.createDatagramSocket(
- localHostForPeer, portTracker.getPort(),
- portTracker.getMinPort(), portTracker.getMaxPort());
- }
- catch (Exception exc)
- {
- throw new OperationFailedException(
- "Failed to allocate the network ports necessary for the call.",
- OperationFailedException.INTERNAL_ERROR,
- exc);
- }
-
- //make sure that next time we don't try to bind on occupied ports
- portTracker.setNextPort( rtcpSocket.getLocalPort() + 1);
-
- return new DefaultStreamConnector(rtpSocket, rtcpSocket);
- }
-
- /**
- * Tries to set the ranges of the <tt>PortTracker</tt>s (e.g. default,
- * audio, video, data channel) to the values specified in the
- * <tt>ConfigurationService</tt>.
- */
- protected synchronized static void initializePortNumbers()
- {
- //try the default tracker first
- ConfigurationService cfg
- = ProtocolMediaActivator.getConfigurationService();
- String minPort, maxPort;
-
- minPort
- = cfg.getString(
- OperationSetBasicTelephony
- .MIN_MEDIA_PORT_NUMBER_PROPERTY_NAME);
- if (minPort != null)
- {
- maxPort
- = cfg.getString(
- OperationSetBasicTelephony
- .MAX_MEDIA_PORT_NUMBER_PROPERTY_NAME);
- if (maxPort != null)
- {
- //Try the specified range; otherwise, leave the tracker as it
- //is: [5000, 6000].
- defaultPortTracker.tryRange(minPort, maxPort);
- }
- }
-
- //try the VIDEO tracker
- minPort
- = cfg.getString(
- OperationSetBasicTelephony
- .MIN_VIDEO_PORT_NUMBER_PROPERTY_NAME);
- if (minPort != null)
- {
- maxPort
- = cfg.getString(
- OperationSetBasicTelephony
- .MAX_VIDEO_PORT_NUMBER_PROPERTY_NAME);
- if (maxPort != null)
- {
- //Try the specified range; otherwise, leave the tracker to null.
- if (videoPortTracker == null)
- {
- videoPortTracker
- = PortTracker.createTracker(minPort, maxPort);
- }
- else
- {
- videoPortTracker.tryRange(minPort, maxPort);
- }
- }
- }
-
- //try the AUDIO tracker
- minPort
- = cfg.getString(
- OperationSetBasicTelephony
- .MIN_AUDIO_PORT_NUMBER_PROPERTY_NAME);
- if (minPort != null)
- {
- maxPort
- = cfg.getString(
- OperationSetBasicTelephony
- .MAX_AUDIO_PORT_NUMBER_PROPERTY_NAME);
- if (maxPort != null)
- {
- //Try the specified range; otherwise, leave the tracker to null.
- if (audioPortTracker == null)
- {
- audioPortTracker
- = PortTracker.createTracker(minPort, maxPort);
- }
- else
- {
- audioPortTracker.tryRange(minPort, maxPort);
- }
- }
- }
-
- //try the DATA CHANNEL tracker
- minPort
- = cfg.getString(
- OperationSetBasicTelephony
- .MIN_DATA_CHANNEL_PORT_NUMBER_PROPERTY_NAME);
- if (minPort != null)
- {
- maxPort
- = cfg.getString(
- OperationSetBasicTelephony
- .MAX_DATA_CHANNEL_PORT_NUMBER_PROPERTY_NAME);
- if (maxPort != null)
- {
- //Try the specified range; otherwise, leave the tracker to null.
- if (dataPortTracker == null)
- {
- dataPortTracker
- = PortTracker.createTracker(minPort, maxPort);
- }
- else
- {
- dataPortTracker.tryRange(minPort, maxPort);
- }
- }
- }
- }
-
- /**
- * Returns the <tt>InetAddress</tt> that we are using in one of our
- * <tt>StreamConnector</tt>s or, in case we don't have any connectors yet
- * the address returned by the our network address manager as the best local
- * address to use when contacting the <tt>CallPeer</tt> associated with this
- * <tt>MediaHandler</tt>. This method is primarily meant for use with the
- * o= and c= fields of a newly created session description. The point is
- * that we create our <tt>StreamConnector</tt>s when constructing the media
- * descriptions so we already have a specific local address assigned to them
- * at the time we get ready to create the c= and o= fields. It is therefore
- * better to try and return one of these addresses before trying the net
- * address manager again and running the slight risk of getting a different
- * address.
- *
- * @return an <tt>InetAddress</tt> that we use in one of the
- * <tt>StreamConnector</tt>s in this class.
- */
- public InetAddress getLastUsedLocalHost()
- {
- for (MediaType mediaType : MediaType.values())
- {
- StreamConnector streamConnector
- = streamConnectors[mediaType.ordinal()];
-
- if (streamConnector != null)
- return streamConnector.getDataSocket().getLocalAddress();
- }
-
- NetworkAddressManagerService nam
- = ProtocolMediaActivator.getNetworkAddressManagerService();
- InetAddress intendedDestination = getIntendedDestination(getCallPeer());
-
- return nam.getLocalHost(intendedDestination);
- }
-
- /**
- * Sends empty UDP packets to target destination data/control ports in order
- * to open ports on NATs or and help RTP proxies latch onto our RTP ports.
- *
- * @param target <tt>MediaStreamTarget</tt>
- * @param type the {@link MediaType} of the connector we'd like to send the
- * hole punching packet through.
- */
- public void sendHolePunchPacket(MediaStreamTarget target, MediaType type)
- {
- logger.info("Send NAT hole punch packets");
-
- //check how many hole punch packets we would be supposed to send:
- int packetCount
- = ProtocolMediaActivator.getConfigurationService().getInt(
- HOLE_PUNCH_PKT_COUNT_PROPERTY,
- DEFAULT_HOLE_PUNCH_PKT_COUNT);
-
- if (packetCount < 0)
- packetCount = DEFAULT_HOLE_PUNCH_PKT_COUNT;
- if (packetCount == 0)
- return;
-
- try
- {
- StreamConnector connector = getStreamConnector(type);
-
- if(connector.getProtocol() == StreamConnector.Protocol.TCP)
- return;
-
- byte[] buf = new byte[0];
-
- synchronized(connector)
- {
- //we may want to send more than one packet in case they get lost
- for(int i=0; i < packetCount; i++)
- {
- DatagramSocket socket;
-
- // data/RTP
- if((socket = connector.getDataSocket()) != null)
- {
- InetSocketAddress dataAddress = target.getDataAddress();
-
- socket.send(
- new DatagramPacket(
- buf,
- buf.length,
- dataAddress.getAddress(),
- dataAddress.getPort()));
- }
-
- // control/RTCP
- if((socket = connector.getControlSocket()) != null)
- {
- InetSocketAddress controlAddress
- = target.getControlAddress();
-
- socket.send(
- new DatagramPacket(
- buf,
- buf.length,
- controlAddress.getAddress(),
- controlAddress.getPort()));
- }
- }
- }
- }
- catch(Exception e)
- {
- logger.error("Error cannot send to remote peer", e);
- }
- }
-
- /**
- * Set traffic class (QoS) for the RTP socket.
- *
- * @param target <tt>MediaStreamTarget</tt>
- * @param type the {@link MediaType} of the connector we'd like to set
- * traffic class
- */
- protected void setTrafficClass(MediaStreamTarget target, MediaType type)
- {
- // get traffic class value for RTP audio/video
- int trafficClass = getDSCP(type);
-
- if(trafficClass <= 0)
- return;
-
- if (logger.isInfoEnabled())
- logger.info(
- "Set traffic class for " + type + " to " + trafficClass);
- try
- {
- StreamConnector connector = getStreamConnector(type);
-
- synchronized(connector)
- {
- if(connector.getProtocol() == StreamConnector.Protocol.TCP)
- {
- connector.getDataTCPSocket().setTrafficClass(trafficClass);
-
- Socket controlTCPSocket = connector.getControlTCPSocket();
-
- if (controlTCPSocket != null)
- controlTCPSocket.setTrafficClass(trafficClass);
- }
- else
- {
- /* data port (RTP) */
- connector.getDataSocket().setTrafficClass(trafficClass);
-
- /* control port (RTCP) */
- DatagramSocket controlSocket = connector.getControlSocket();
-
- if (controlSocket != null)
- controlSocket.setTrafficClass(trafficClass);
- }
- }
- }
- catch(Exception e)
- {
- logger.error(
- "Failed to set traffic class for " + type + " to "
- + trafficClass,
- e);
- }
- }
-
- /**
- * Gets the SIP traffic class associated with a specific <tt>MediaType</tt>
- * from the configuration.
- *
- * @param type the <tt>MediaType</tt> to get the associated SIP traffic
- * class of
- * @return the SIP traffic class associated with the specified
- * <tt>MediaType</tt> or <tt>0</tt> if not configured
- */
- private int getDSCP(MediaType type)
- {
- String dscpPropertyName;
-
- switch (type)
- {
- case AUDIO:
- dscpPropertyName = RTP_AUDIO_DSCP_PROPERTY;
- break;
- case VIDEO:
- dscpPropertyName = RTP_VIDEO_DSCP_PROPERTY;
- break;
- default:
- dscpPropertyName = null;
- break;
- }
-
- return
- (dscpPropertyName == null)
- ? 0
- : (ProtocolMediaActivator.getConfigurationService().getInt(
- dscpPropertyName,
- 0)
- << 2);
- }
-
- /**
- * Returns the <tt>InetAddress</tt> that is most likely to be used as a
- * next hop when contacting the specified <tt>destination</tt>. This is
- * an utility method that is used whenever we have to choose one of our
- * local addresses to put in the Via, Contact or (in the case of no
- * registrar accounts) From headers.
- *
- * @param peer the CallPeer that we would contact.
- *
- * @return the <tt>InetAddress</tt> that is most likely to be to be used
- * as a next hop when contacting the specified <tt>destination</tt>.
- *
- * @throws IllegalArgumentException if <tt>destination</tt> is not a valid
- * host/ip/fqdn
- */
- protected abstract InetAddress getIntendedDestination(U peer);
-
- /**
- * Returns the {@link MediaAwareCallPeer} that this transport manager is
- * serving.
- *
- * @return the {@link MediaAwareCallPeer} that this transport manager is
- * serving.
- */
- public U getCallPeer()
- {
- return callPeer;
- }
-
- /**
- * Returns the port tracker that we are supposed to use when binding ports
- * for the specified {@link MediaType}.
- *
- * @param mediaType the media type that we want to obtain the port tracker
- * for. Use <tt>null</tt> to obtain the default port tracker.
- *
- * @return the port tracker that we are supposed to use when binding ports
- * for the specified {@link MediaType}.
- */
- protected static PortTracker getPortTracker(MediaType mediaType)
- {
- //make sure our port numbers reflect the configuration service settings
- initializePortNumbers();
-
- if (mediaType != null)
- {
- switch (mediaType)
- {
- case AUDIO:
- if (audioPortTracker != null)
- return audioPortTracker;
- else
- break;
- case VIDEO:
- if (videoPortTracker != null)
- return videoPortTracker;
- else
- break;
- case DATA:
- if (dataPortTracker != null)
- return dataPortTracker;
- else
- break;
- }
- }
-
- return defaultPortTracker;
- }
-
- /**
- * Returns the port tracker that we are supposed to use when binding ports
- * for the {@link MediaType} indicated by the string param. If we do not
- * recognize the string as a valid media type, we simply return the default
- * port tracker.
- *
- * @param mediaTypeStr the name of the media type that we want to obtain a
- * port tracker for.
- *
- * @return the port tracker that we are supposed to use when binding ports
- * for the {@link MediaType} with the specified name or the default tracker
- * in case the name doesn't ring a bell.
- */
- protected static PortTracker getPortTracker(String mediaTypeStr)
- {
- try
- {
- return getPortTracker(MediaType.parseString(mediaTypeStr));
- }
- catch (Exception e)
- {
- logger.info(
- "Returning default port tracker for unrecognized media type: "
- + mediaTypeStr);
-
- return defaultPortTracker;
- }
- }
-
- /**
- * Returns the extended type of the candidate selected if this transport
- * manager is using ICE.
- *
- * @param streamName The stream name (AUDIO, VIDEO);
- *
- * @return The extended type of the candidate selected if this transport
- * manager is using ICE. Otherwise, returns null.
- */
- public abstract String getICECandidateExtendedType(String streamName);
-
- /**
- * Returns the current state of ICE processing.
- *
- * @return the current state of ICE processing if this transport
- * manager is using ICE. Otherwise, returns null.
- */
- public abstract String getICEState();
-
- /**
- * Returns the ICE local host address.
- *
- * @param streamName The stream name (AUDIO, VIDEO);
- *
- * @return the ICE local host address if this transport
- * manager is using ICE. Otherwise, returns null.
- */
- public abstract InetSocketAddress getICELocalHostAddress(String streamName);
-
- /**
- * Returns the ICE remote host address.
- *
- * @param streamName The stream name (AUDIO, VIDEO);
- *
- * @return the ICE remote host address if this transport
- * manager is using ICE. Otherwise, returns null.
- */
- public abstract InetSocketAddress getICERemoteHostAddress(
- String streamName);
-
- /**
- * Returns the ICE local reflexive address (server or peer reflexive).
- *
- * @param streamName The stream name (AUDIO, VIDEO);
- *
- * @return the ICE local reflexive address. May be null if this transport
- * manager is not using ICE or if there is no reflexive address for the
- * local candidate used.
- */
- public abstract InetSocketAddress getICELocalReflexiveAddress(
- String streamName);
-
- /**
- * Returns the ICE remote reflexive address (server or peer reflexive).
- *
- * @param streamName The stream name (AUDIO, VIDEO);
- *
- * @return the ICE remote reflexive address. May be null if this transport
- * manager is not using ICE or if there is no reflexive address for the
- * remote candidate used.
- */
- public abstract InetSocketAddress getICERemoteReflexiveAddress(
- String streamName);
-
- /**
- * Returns the ICE local relayed address (server or peer relayed).
- *
- * @param streamName The stream name (AUDIO, VIDEO);
- *
- * @return the ICE local relayed address. May be null if this transport
- * manager is not using ICE or if there is no relayed address for the
- * local candidate used.
- */
- public abstract InetSocketAddress getICELocalRelayedAddress(
- String streamName);
-
- /**
- * Returns the ICE remote relayed address (server or peer relayed).
- *
- * @param streamName The stream name (AUDIO, VIDEO);
- *
- * @return the ICE remote relayed address. May be null if this transport
- * manager is not using ICE or if there is no relayed address for the
- * remote candidate used.
- */
- public abstract InetSocketAddress getICERemoteRelayedAddress(
- String streamName);
-
- /**
- * Returns the total harvesting time (in ms) for all harvesters.
- *
- * @return The total harvesting time (in ms) for all the harvesters. 0 if
- * the ICE agent is null, or if the agent has nevers harvested.
- */
- public abstract long getTotalHarvestingTime();
-
- /**
- * Returns the harvesting time (in ms) for the harvester given in parameter.
- *
- * @param harvesterName The class name if the harvester.
- *
- * @return The harvesting time (in ms) for the harvester given in parameter.
- * 0 if this harvester does not exists, if the ICE agent is null, or if the
- * agent has never harvested with this harvester.
- */
- public abstract long getHarvestingTime(String harvesterName);
-
- /**
- * Returns the number of harvesting for this agent.
- *
- * @return The number of harvesting for this agent.
- */
- public abstract int getNbHarvesting();
-
- /**
- * Returns the number of harvesting time for the harvester given in
- * parameter.
- *
- * @param harvesterName The class name if the harvester.
- *
- * @return The number of harvesting time for the harvester given in
- * parameter.
- */
- public abstract int getNbHarvesting(String harvesterName);
-
- /**
- * Returns the ICE candidate extended type selected by the given agent.
- *
- * @param iceAgent The ICE agent managing the ICE offer/answer exchange,
- * collecting and selecting the candidate.
- * @param streamName The stream name (AUDIO, VIDEO);
- *
- * @return The ICE candidate extended type selected by the given agent. null
- * if the iceAgent is null or if there is no candidate selected or
- * available.
- */
- public static String getICECandidateExtendedType(
- Agent iceAgent,
- String streamName)
- {
- if(iceAgent != null)
- {
- LocalCandidate localCandidate
- = iceAgent.getSelectedLocalCandidate(streamName);
-
- if(localCandidate != null)
- return localCandidate.getExtendedType().toString();
- }
- return null;
- }
-
-
- /**
- * Creates the ICE agent that we would be using in this transport manager
- * for all negotiation.
- *
- * @return the ICE agent to use for all the ICE negotiation that this
- * transport manager would be going through
- */
- protected Agent createIceAgent()
- {
- //work in progress
- return null;
- }
-
- /**
- * Creates an {@link IceMediaStream} with the specified <tt>media</tt>
- * name.
- *
- * @param media the name of the stream we'd like to create.
- * @param agent the ICE {@link Agent} that we will be appending the stream
- * to.
- *
- * @return the newly created {@link IceMediaStream}
- *
- * @throws OperationFailedException if binding on the specified media stream
- * fails for some reason.
- */
- protected IceMediaStream createIceStream(String media, Agent agent)
- throws OperationFailedException
- {
- return null;
- }
-}
+package net.java.sip.communicator.service.protocol.media; + +import java.net.*; + +import net.java.sip.communicator.service.netaddr.*; +import net.java.sip.communicator.service.protocol.*; +import net.java.sip.communicator.util.*; + +import org.ice4j.ice.*; +import org.jitsi.service.configuration.*; +import org.jitsi.service.neomedia.*; + +/** + * <tt>TransportManager</tt>s are responsible for allocating ports, gathering + * local candidates and managing ICE whenever we are using it. + * + * @param <U> the peer extension class like for example <tt>CallPeerSipImpl</tt> + * or <tt>CallPeerJabberImpl</tt> + * + * @author Emil Ivov + * @author Lyubomir Marinov + * @author Sebastien Vincent + */ +public abstract class TransportManager<U extends MediaAwareCallPeer<?, ?, ?>> +{ + /** + * The <tt>Logger</tt> used by the <tt>TransportManager</tt> + * class and its instances for logging output. + */ + private static final Logger logger + = Logger.getLogger(TransportManager.class); + + /** + * The port tracker that we should use when binding generic media streams. + * <p> + * Initialized by {@link #initializePortNumbers()}. + * </p> + */ + private static final PortTracker defaultPortTracker + = new PortTracker(5000, 6000); + + /** + * The port tracker that we should use when binding video media streams. + * <p> + * Potentially initialized by {@link #initializePortNumbers()} if the + * necessary properties are set. + * </p> + */ + private static PortTracker videoPortTracker; + + /** + * The port tracker that we should use when binding data channels. + * <p> + * Potentially initialized by {@link #initializePortNumbers()} if the + * necessary properties are set. + * </p> + */ + private static PortTracker dataPortTracker; + + /** + * The port tracker that we should use when binding data media streams. + * <p> + * Potentially initialized by {@link #initializePortNumbers()} if the + * necessary properties are set. + * </p> + */ + private static PortTracker audioPortTracker; + + /** + * RTP audio DSCP configuration property name. + */ + private static final String RTP_AUDIO_DSCP_PROPERTY = + "net.java.sip.communicator.impl.protocol.RTP_AUDIO_DSCP"; + + /** + * RTP video DSCP configuration property name. + */ + private static final String RTP_VIDEO_DSCP_PROPERTY = + "net.java.sip.communicator.impl.protocol.RTP_VIDEO_DSCP"; + + /** + * Number of empty UDP packets to send for NAT hole punching. + */ + private static final String HOLE_PUNCH_PKT_COUNT_PROPERTY = + "net.java.sip.communicator.impl.protocol.HOLE_PUNCH_PKT_COUNT"; + + /** + * Number of empty UDP packets to send for NAT hole punching. + */ + private static final int DEFAULT_HOLE_PUNCH_PKT_COUNT = 3; + + /** + * Returns the port tracker that we are supposed to use when binding ports + * for the specified {@link MediaType}. + * + * @param mediaType the media type that we want to obtain the port tracker + * for. Use <tt>null</tt> to obtain the default port tracker. + * + * @return the port tracker that we are supposed to use when binding ports + * for the specified {@link MediaType}. + */ + protected static PortTracker getPortTracker(MediaType mediaType) + { + //make sure our port numbers reflect the configuration service settings + initializePortNumbers(); + + if (mediaType != null) + { + switch (mediaType) + { + case AUDIO: + if (audioPortTracker != null) + return audioPortTracker; + else + break; + case VIDEO: + if (videoPortTracker != null) + return videoPortTracker; + else + break; + case DATA: + if (dataPortTracker != null) + return dataPortTracker; + else + break; + } + } + + return defaultPortTracker; + } + + /** + * Returns the port tracker that we are supposed to use when binding ports + * for the {@link MediaType} indicated by the string param. If we do not + * recognize the string as a valid media type, we simply return the default + * port tracker. + * + * @param mediaTypeStr the name of the media type that we want to obtain a + * port tracker for. + * + * @return the port tracker that we are supposed to use when binding ports + * for the {@link MediaType} with the specified name or the default tracker + * in case the name doesn't ring a bell. + */ + protected static PortTracker getPortTracker(String mediaTypeStr) + { + try + { + return getPortTracker(MediaType.parseString(mediaTypeStr)); + } + catch (Exception e) + { + logger.info( + "Returning default port tracker for unrecognized media type: " + + mediaTypeStr); + + return defaultPortTracker; + } + } + + + /** + * The {@link MediaAwareCallPeer} whose traffic we will be taking care of. + */ + private U callPeer; + + /** + * The RTP/RTCP socket couples that this <tt>TransportManager</tt> uses to + * send and receive media flows through indexed by <tt>MediaType</tt> + * (ordinal). + */ + private final StreamConnector[] streamConnectors + = new StreamConnector[MediaType.values().length]; + + /** + * Creates a new instance of this transport manager, binding it to the + * specified peer. + * + * @param callPeer the {@link MediaAwareCallPeer} whose traffic we will be + * taking care of. + */ + protected TransportManager(U callPeer) + { + this.callPeer = callPeer; + } + + /** + * Returns the <tt>StreamConnector</tt> instance that this media handler + * should use for streams of the specified <tt>mediaType</tt>. The method + * would also create a new <tt>StreamConnector</tt> if no connector has + * been initialized for this <tt>mediaType</tt> yet or in case one + * of its underlying sockets has been closed. + * + * @param mediaType the <tt>MediaType</tt> that we'd like to create a + * connector for. + * @return this media handler's <tt>StreamConnector</tt> for the specified + * <tt>mediaType</tt>. + * + * @throws OperationFailedException in case we failed to initialize our + * connector. + */ + public StreamConnector getStreamConnector(MediaType mediaType) + throws OperationFailedException + { + int streamConnectorIndex = mediaType.ordinal(); + StreamConnector streamConnector + = streamConnectors[streamConnectorIndex]; + + if((streamConnector == null) + || (streamConnector.getProtocol() == StreamConnector.Protocol.UDP)) + { + DatagramSocket controlSocket; + + if((streamConnector == null) + || streamConnector.getDataSocket().isClosed() + || (((controlSocket = streamConnector.getControlSocket()) + != null) + && controlSocket.isClosed())) + { + streamConnectors[streamConnectorIndex] + = streamConnector + = createStreamConnector(mediaType); + } + } + else if(streamConnector.getProtocol() == StreamConnector.Protocol.TCP) + { + Socket controlTCPSocket; + + if(streamConnector.getDataTCPSocket().isClosed() + || (((controlTCPSocket = streamConnector.getControlTCPSocket()) + != null) + && controlTCPSocket.isClosed())) + { + streamConnectors[streamConnectorIndex] + = streamConnector + = createStreamConnector(mediaType); + } + } + return streamConnector; + } + + /** + * Closes the existing <tt>StreamConnector</tt>, if any, associated with a + * specific <tt>MediaType</tt> and removes its reference from this + * <tt>TransportManager</tt>. + * + * @param mediaType the <tt>MediaType</tt> associated with the + * <tt>StreamConnector</tt> to close + */ + public void closeStreamConnector(MediaType mediaType) + { + int index = mediaType.ordinal(); + StreamConnector streamConnector = streamConnectors[index]; + + if (streamConnector != null) + { + closeStreamConnector(mediaType, streamConnector); + streamConnectors[index] = null; + } + } + + /** + * Closes a specific <tt>StreamConnector</tt> associated with a specific + * <tt>MediaType</tt>. If this <tt>TransportManager</tt> has a reference to + * the specified <tt>streamConnector</tt>, it remains. Allows extenders to + * override and perform additional customizations to the closing of the + * specified <tt>streamConnector</tt>. + * + * @param mediaType the <tt>MediaType</tt> associated with the specified + * <tt>streamConnector</tt> + * @param streamConnector the <tt>StreamConnector</tt> to be closed + * @see #closeStreamConnector(MediaType) + */ + protected void closeStreamConnector( + MediaType mediaType, + StreamConnector streamConnector) + { + /* + * XXX The connected owns the sockets so it is important that it + * decides whether to close them i.e. this TransportManager is not + * allowed to explicitly close the sockets by itself. + */ + streamConnector.close(); + } + + /** + * Creates a media <tt>StreamConnector</tt> for a stream of a specific + * <tt>MediaType</tt>. The minimum and maximum of the media port boundaries + * are taken into account. + * + * @param mediaType the <tt>MediaType</tt> of the stream for which a + * <tt>StreamConnector</tt> is to be created + * @return a <tt>StreamConnector</tt> for the stream of the specified + * <tt>mediaType</tt> + * @throws OperationFailedException if the binding of the sockets fails + */ + protected StreamConnector createStreamConnector(MediaType mediaType) + throws OperationFailedException + { + NetworkAddressManagerService nam + = ProtocolMediaActivator.getNetworkAddressManagerService(); + InetAddress intendedDestination = getIntendedDestination(getCallPeer()); + InetAddress localHostForPeer = nam.getLocalHost(intendedDestination); + + PortTracker portTracker = getPortTracker(mediaType); + + //create the RTP socket. + DatagramSocket rtpSocket = null; + try + { + rtpSocket = nam.createDatagramSocket( + localHostForPeer, portTracker.getPort(), + portTracker.getMinPort(), portTracker.getMaxPort()); + } + catch (Exception exc) + { + throw new OperationFailedException( + "Failed to allocate the network ports necessary for the call.", + OperationFailedException.INTERNAL_ERROR, exc); + } + + //make sure that next time we don't try to bind on occupied ports + //also, refuse validation in case someone set the tracker range to 1 + portTracker.setNextPort( rtpSocket.getLocalPort() + 1, false); + + //create the RTCP socket, preferably on the port following our RTP one. + DatagramSocket rtcpSocket = null; + try + { + rtcpSocket = nam.createDatagramSocket( + localHostForPeer, portTracker.getPort(), + portTracker.getMinPort(), portTracker.getMaxPort()); + } + catch (Exception exc) + { + throw new OperationFailedException( + "Failed to allocate the network ports necessary for the call.", + OperationFailedException.INTERNAL_ERROR, + exc); + } + + //make sure that next time we don't try to bind on occupied ports + portTracker.setNextPort( rtcpSocket.getLocalPort() + 1); + + return new DefaultStreamConnector(rtpSocket, rtcpSocket); + } + + /** + * Tries to set the ranges of the <tt>PortTracker</tt>s (e.g. default, + * audio, video, data channel) to the values specified in the + * <tt>ConfigurationService</tt>. + */ + protected synchronized static void initializePortNumbers() + { + //try the default tracker first + ConfigurationService cfg + = ProtocolMediaActivator.getConfigurationService(); + String minPort, maxPort; + + minPort + = cfg.getString( + OperationSetBasicTelephony + .MIN_MEDIA_PORT_NUMBER_PROPERTY_NAME); + if (minPort != null) + { + maxPort + = cfg.getString( + OperationSetBasicTelephony + .MAX_MEDIA_PORT_NUMBER_PROPERTY_NAME); + if (maxPort != null) + { + //Try the specified range; otherwise, leave the tracker as it + //is: [5000, 6000]. + defaultPortTracker.tryRange(minPort, maxPort); + } + } + + //try the VIDEO tracker + minPort + = cfg.getString( + OperationSetBasicTelephony + .MIN_VIDEO_PORT_NUMBER_PROPERTY_NAME); + if (minPort != null) + { + maxPort + = cfg.getString( + OperationSetBasicTelephony + .MAX_VIDEO_PORT_NUMBER_PROPERTY_NAME); + if (maxPort != null) + { + //Try the specified range; otherwise, leave the tracker to null. + if (videoPortTracker == null) + { + videoPortTracker + = PortTracker.createTracker(minPort, maxPort); + } + else + { + videoPortTracker.tryRange(minPort, maxPort); + } + } + } + + //try the AUDIO tracker + minPort + = cfg.getString( + OperationSetBasicTelephony + .MIN_AUDIO_PORT_NUMBER_PROPERTY_NAME); + if (minPort != null) + { + maxPort + = cfg.getString( + OperationSetBasicTelephony + .MAX_AUDIO_PORT_NUMBER_PROPERTY_NAME); + if (maxPort != null) + { + //Try the specified range; otherwise, leave the tracker to null. + if (audioPortTracker == null) + { + audioPortTracker + = PortTracker.createTracker(minPort, maxPort); + } + else + { + audioPortTracker.tryRange(minPort, maxPort); + } + } + } + + //try the DATA CHANNEL tracker + minPort + = cfg.getString( + OperationSetBasicTelephony + .MIN_DATA_CHANNEL_PORT_NUMBER_PROPERTY_NAME); + if (minPort != null) + { + maxPort + = cfg.getString( + OperationSetBasicTelephony + .MAX_DATA_CHANNEL_PORT_NUMBER_PROPERTY_NAME); + if (maxPort != null) + { + //Try the specified range; otherwise, leave the tracker to null. + if (dataPortTracker == null) + { + dataPortTracker + = PortTracker.createTracker(minPort, maxPort); + } + else + { + dataPortTracker.tryRange(minPort, maxPort); + } + } + } + } + + /** + * Returns the <tt>InetAddress</tt> that we are using in one of our + * <tt>StreamConnector</tt>s or, in case we don't have any connectors yet + * the address returned by the our network address manager as the best local + * address to use when contacting the <tt>CallPeer</tt> associated with this + * <tt>MediaHandler</tt>. This method is primarily meant for use with the + * o= and c= fields of a newly created session description. The point is + * that we create our <tt>StreamConnector</tt>s when constructing the media + * descriptions so we already have a specific local address assigned to them + * at the time we get ready to create the c= and o= fields. It is therefore + * better to try and return one of these addresses before trying the net + * address manager again and running the slight risk of getting a different + * address. + * + * @return an <tt>InetAddress</tt> that we use in one of the + * <tt>StreamConnector</tt>s in this class. + */ + public InetAddress getLastUsedLocalHost() + { + for (MediaType mediaType : MediaType.values()) + { + StreamConnector streamConnector + = streamConnectors[mediaType.ordinal()]; + + if (streamConnector != null) + return streamConnector.getDataSocket().getLocalAddress(); + } + + NetworkAddressManagerService nam + = ProtocolMediaActivator.getNetworkAddressManagerService(); + InetAddress intendedDestination = getIntendedDestination(getCallPeer()); + + return nam.getLocalHost(intendedDestination); + } + + /** + * Sends empty UDP packets to target destination data/control ports in order + * to open ports on NATs or and help RTP proxies latch onto our RTP ports. + * + * @param target <tt>MediaStreamTarget</tt> + * @param type the {@link MediaType} of the connector we'd like to send the + * hole punching packet through. + */ + public void sendHolePunchPacket(MediaStreamTarget target, MediaType type) + { + this.sendHolePunchPacket(target, type, null); + } + + /** + * Sends empty UDP packets to target destination data/control ports in order + * to open ports on NATs or and help RTP proxies latch onto our RTP ports. + * + * @param target <tt>MediaStreamTarget</tt> + * @param type the {@link MediaType} of the connector we'd like to send the + * hole punching packet through. + * @param packet (optional) use a pre-generated packet that will be sent + */ + public void sendHolePunchPacket( + MediaStreamTarget target, MediaType type, RawPacket packet) + { + logger.info("Send NAT hole punch packets"); + + //check how many hole punch packets we would be supposed to send: + int packetCount + = ProtocolMediaActivator.getConfigurationService().getInt( + HOLE_PUNCH_PKT_COUNT_PROPERTY, + DEFAULT_HOLE_PUNCH_PKT_COUNT); + + if (packetCount < 0) + packetCount = DEFAULT_HOLE_PUNCH_PKT_COUNT; + if (packetCount == 0) + return; + + try + { + final StreamConnector connector = getStreamConnector(type); + + if(connector.getProtocol() == StreamConnector.Protocol.TCP) + return; + + byte[] buf; + if (packet != null) + buf = packet.getBuffer(); + else + buf = new byte[0]; + + synchronized(connector) + { + //we may want to send more than one packet in case they get lost + for(int i=0; i < packetCount; i++) + { + DatagramSocket socket; + + // data/RTP + if((socket = connector.getDataSocket()) != null) + { + InetSocketAddress dataAddress = target.getDataAddress(); + + socket.send( + new DatagramPacket( + buf, + buf.length, + dataAddress.getAddress(), + dataAddress.getPort())); + } + + // control/RTCP + if((socket = connector.getControlSocket()) != null) + { + InetSocketAddress controlAddress + = target.getControlAddress(); + + socket.send( + new DatagramPacket( + buf, + buf.length, + controlAddress.getAddress(), + controlAddress.getPort())); + } + } + } + } + catch(Exception e) + { + logger.error("Error cannot send to remote peer", e); + } + } + + /** + * Set traffic class (QoS) for the RTP socket. + * + * @param target <tt>MediaStreamTarget</tt> + * @param type the {@link MediaType} of the connector we'd like to set + * traffic class + */ + protected void setTrafficClass(MediaStreamTarget target, MediaType type) + { + // get traffic class value for RTP audio/video + int trafficClass = getDSCP(type); + + if(trafficClass <= 0) + return; + + if (logger.isInfoEnabled()) + logger.info( + "Set traffic class for " + type + " to " + trafficClass); + try + { + final StreamConnector connector = getStreamConnector(type); + + synchronized(connector) + { + if(connector.getProtocol() == StreamConnector.Protocol.TCP) + { + connector.getDataTCPSocket().setTrafficClass(trafficClass); + + Socket controlTCPSocket = connector.getControlTCPSocket(); + + if (controlTCPSocket != null) + controlTCPSocket.setTrafficClass(trafficClass); + } + else + { + /* data port (RTP) */ + connector.getDataSocket().setTrafficClass(trafficClass); + + /* control port (RTCP) */ + DatagramSocket controlSocket = connector.getControlSocket(); + + if (controlSocket != null) + controlSocket.setTrafficClass(trafficClass); + } + } + } + catch(Exception e) + { + logger.error( + "Failed to set traffic class for " + type + " to " + + trafficClass, + e); + } + } + + /** + * Gets the SIP traffic class associated with a specific <tt>MediaType</tt> + * from the configuration. + * + * @param type the <tt>MediaType</tt> to get the associated SIP traffic + * class of + * @return the SIP traffic class associated with the specified + * <tt>MediaType</tt> or <tt>0</tt> if not configured + */ + private int getDSCP(MediaType type) + { + String dscpPropertyName; + + switch (type) + { + case AUDIO: + dscpPropertyName = RTP_AUDIO_DSCP_PROPERTY; + break; + case VIDEO: + dscpPropertyName = RTP_VIDEO_DSCP_PROPERTY; + break; + default: + dscpPropertyName = null; + break; + } + + return + (dscpPropertyName == null) + ? 0 + : (ProtocolMediaActivator.getConfigurationService().getInt( + dscpPropertyName, + 0) + << 2); + } + + /** + * Returns the <tt>InetAddress</tt> that is most likely to be used as a + * next hop when contacting the specified <tt>destination</tt>. This is + * an utility method that is used whenever we have to choose one of our + * local addresses to put in the Via, Contact or (in the case of no + * registrar accounts) From headers. + * + * @param peer the CallPeer that we would contact. + * + * @return the <tt>InetAddress</tt> that is most likely to be to be used + * as a next hop when contacting the specified <tt>destination</tt>. + * + * @throws IllegalArgumentException if <tt>destination</tt> is not a valid + * host/ip/fqdn + */ + protected abstract InetAddress getIntendedDestination(U peer); + + /** + * Returns the {@link MediaAwareCallPeer} that this transport manager is + * serving. + * + * @return the {@link MediaAwareCallPeer} that this transport manager is + * serving. + */ + public U getCallPeer() + { + return callPeer; + } + + /** + * Returns the extended type of the candidate selected if this transport + * manager is using ICE. + * + * @param streamName The stream name (AUDIO, VIDEO); + * + * @return The extended type of the candidate selected if this transport + * manager is using ICE. Otherwise, returns null. + */ + public abstract String getICECandidateExtendedType(String streamName); + + /** + * Returns the current state of ICE processing. + * + * @return the current state of ICE processing if this transport + * manager is using ICE. Otherwise, returns null. + */ + public abstract String getICEState(); + + /** + * Returns the ICE local host address. + * + * @param streamName The stream name (AUDIO, VIDEO); + * + * @return the ICE local host address if this transport + * manager is using ICE. Otherwise, returns null. + */ + public abstract InetSocketAddress getICELocalHostAddress(String streamName); + + /** + * Returns the ICE remote host address. + * + * @param streamName The stream name (AUDIO, VIDEO); + * + * @return the ICE remote host address if this transport + * manager is using ICE. Otherwise, returns null. + */ + public abstract InetSocketAddress getICERemoteHostAddress( + String streamName); + + /** + * Returns the ICE local reflexive address (server or peer reflexive). + * + * @param streamName The stream name (AUDIO, VIDEO); + * + * @return the ICE local reflexive address. May be null if this transport + * manager is not using ICE or if there is no reflexive address for the + * local candidate used. + */ + public abstract InetSocketAddress getICELocalReflexiveAddress( + String streamName); + + /** + * Returns the ICE remote reflexive address (server or peer reflexive). + * + * @param streamName The stream name (AUDIO, VIDEO); + * + * @return the ICE remote reflexive address. May be null if this transport + * manager is not using ICE or if there is no reflexive address for the + * remote candidate used. + */ + public abstract InetSocketAddress getICERemoteReflexiveAddress( + String streamName); + + /** + * Returns the ICE local relayed address (server or peer relayed). + * + * @param streamName The stream name (AUDIO, VIDEO); + * + * @return the ICE local relayed address. May be null if this transport + * manager is not using ICE or if there is no relayed address for the + * local candidate used. + */ + public abstract InetSocketAddress getICELocalRelayedAddress( + String streamName); + + /** + * Returns the ICE remote relayed address (server or peer relayed). + * + * @param streamName The stream name (AUDIO, VIDEO); + * + * @return the ICE remote relayed address. May be null if this transport + * manager is not using ICE or if there is no relayed address for the + * remote candidate used. + */ + public abstract InetSocketAddress getICERemoteRelayedAddress( + String streamName); + + /** + * Returns the total harvesting time (in ms) for all harvesters. + * + * @return The total harvesting time (in ms) for all the harvesters. 0 if + * the ICE agent is null, or if the agent has nevers harvested. + */ + public abstract long getTotalHarvestingTime(); + + /** + * Returns the harvesting time (in ms) for the harvester given in parameter. + * + * @param harvesterName The class name if the harvester. + * + * @return The harvesting time (in ms) for the harvester given in parameter. + * 0 if this harvester does not exists, if the ICE agent is null, or if the + * agent has never harvested with this harvester. + */ + public abstract long getHarvestingTime(String harvesterName); + + /** + * Returns the number of harvesting for this agent. + * + * @return The number of harvesting for this agent. + */ + public abstract int getNbHarvesting(); + + /** + * Returns the number of harvesting time for the harvester given in + * parameter. + * + * @param harvesterName The class name if the harvester. + * + * @return The number of harvesting time for the harvester given in + * parameter. + */ + public abstract int getNbHarvesting(String harvesterName); + + /** + * Returns the ICE candidate extended type selected by the given agent. + * + * @param iceAgent The ICE agent managing the ICE offer/answer exchange, + * collecting and selecting the candidate. + * @param streamName The stream name (AUDIO, VIDEO); + * + * @return The ICE candidate extended type selected by the given agent. null + * if the iceAgent is null or if there is no candidate selected or + * available. + */ + public static String getICECandidateExtendedType( + Agent iceAgent, + String streamName) + { + if(iceAgent != null) + { + LocalCandidate localCandidate + = iceAgent.getSelectedLocalCandidate(streamName); + + if(localCandidate != null) + return localCandidate.getExtendedType().toString(); + } + return null; + } + + + /** + * Creates the ICE agent that we would be using in this transport manager + * for all negotiation. + * + * @return the ICE agent to use for all the ICE negotiation that this + * transport manager would be going through + */ + protected Agent createIceAgent() + { + //work in progress + return null; + } + + /** + * Creates an {@link IceMediaStream} with the specified <tt>media</tt> + * name. + * + * @param media the name of the stream we'd like to create. + * @param agent the ICE {@link Agent} that we will be appending the stream + * to. + * + * @return the newly created {@link IceMediaStream} + * + * @throws OperationFailedException if binding on the specified media stream + * fails for some reason. + */ + protected IceMediaStream createIceStream(String media, Agent agent) + throws OperationFailedException + { + return null; + } +} diff --git a/src/net/java/sip/communicator/service/protocol/media/protocol.media.manifest.mf b/src/net/java/sip/communicator/service/protocol/media/protocol.media.manifest.mf index 4436e93..7bac7aa 100644 --- a/src/net/java/sip/communicator/service/protocol/media/protocol.media.manifest.mf +++ b/src/net/java/sip/communicator/service/protocol/media/protocol.media.manifest.mf @@ -15,7 +15,6 @@ Import-Package: javax.xml.parsers, org.ice4j, org.ice4j.ice, org.ice4j.ice.harvest, - org.ice4j.ice.sdp, org.ice4j.security, org.ice4j.socket, org.ice4j.stack, diff --git a/src/net/java/sip/communicator/service/protocol/protocol.provider.manifest.mf b/src/net/java/sip/communicator/service/protocol/protocol.provider.manifest.mf index 271af97..233eb84 100644 --- a/src/net/java/sip/communicator/service/protocol/protocol.provider.manifest.mf +++ b/src/net/java/sip/communicator/service/protocol/protocol.provider.manifest.mf @@ -17,12 +17,10 @@ Import-Package: net.java.sip.communicator.service.credentialsstorage, org.jitsi.util.event, org.osgi.framework Export-Package: net.java.sip.communicator.service.protocol, - net.java.sip.communicator.service.protocol.aimconstants, net.java.sip.communicator.service.protocol.event, net.java.sip.communicator.service.protocol.globalstatus, net.java.sip.communicator.service.protocol.icqconstants, net.java.sip.communicator.service.protocol.jabber, net.java.sip.communicator.service.protocol.jabberconstants, net.java.sip.communicator.service.protocol.sip, - net.java.sip.communicator.service.protocol.whiteboardobjects, - net.java.sip.communicator.service.protocol.yahooconstants + net.java.sip.communicator.service.protocol.whiteboardobjects diff --git a/src/net/java/sip/communicator/service/protocol/sip/SIPAccountRegistration.java b/src/net/java/sip/communicator/service/protocol/sip/SIPAccountRegistration.java index 9719b8c..5355855 100644 --- a/src/net/java/sip/communicator/service/protocol/sip/SIPAccountRegistration.java +++ b/src/net/java/sip/communicator/service/protocol/sip/SIPAccountRegistration.java @@ -24,7 +24,7 @@ import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.util.*; import org.jitsi.service.neomedia.*; - +import org.jitsi.util.StringUtils; import org.osgi.framework.*; /** @@ -257,23 +257,37 @@ public class SIPAccountRegistration encodingsRegistration.storeProperties(this.accountProperties); - if (isModification) + if(isMessageWaitingIndicationsEnabled()) { - if (isMessageWaitingIndicationsEnabled()) + if(!StringUtils.isNullOrEmpty(getVoicemailURI(), true)) + accountProperties.put( + ProtocolProviderFactory.VOICEMAIL_URI, + getVoicemailURI()); + else if(isModification) + accountProperties.put(ProtocolProviderFactory.VOICEMAIL_URI, ""); + + if(!StringUtils.isNullOrEmpty( + getVoicemailCheckURI(), true)) + accountProperties.put( + ProtocolProviderFactory.VOICEMAIL_CHECK_URI, + getVoicemailCheckURI()); + else if(isModification) + accountProperties.put( + ProtocolProviderFactory.VOICEMAIL_CHECK_URI, ""); + + if(isModification) { - setVoicemailURI(""); - setVoicemailCheckURI(""); // remove the property as true is by default, // and null removes property - removeAccountProperty( - ProtocolProviderFactory.VOICEMAIL_ENABLED); - } else - { - accountProperties.put( - ProtocolProviderFactory.VOICEMAIL_ENABLED, - Boolean.FALSE.toString()); + accountProperties.put(ProtocolProviderFactory.VOICEMAIL_ENABLED, + null); } } + else if(isModification) + { + accountProperties.put(ProtocolProviderFactory.VOICEMAIL_ENABLED, + Boolean.FALSE.toString()); + } super.storeProperties( protocolIconPath, accountIconPath, accountProperties); diff --git a/src/net/java/sip/communicator/service/protocol/yahooconstants/YahooStatusEnum.java b/src/net/java/sip/communicator/service/protocol/yahooconstants/YahooStatusEnum.java deleted file mode 100644 index d21ec033..0000000 --- a/src/net/java/sip/communicator/service/protocol/yahooconstants/YahooStatusEnum.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Jitsi, the OpenSource Java VoIP and Instant Messaging client. - * - * Copyright @ 2015 Atlassian Pty Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package net.java.sip.communicator.service.protocol.yahooconstants; - -import java.io.*; -import java.util.*; - -import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.util.*; - -/** - * An enumeration containing all status instances that MUST be supported by - * an implementation of the yahoo protocol. Implementations may - * support other forms of PresenceStatus but they MUST ALL support those - * enumerated here. - * <p> - * For testing purposes, this class also provides a <tt>List</tt> containing - * all of the status fields. - * - * @author Damian Minkov - */ -public class YahooStatusEnum - extends PresenceStatus -{ - /** - * The <tt>Logger</tt> used by the <tt>YahooStatusEnum</tt> class and its - * instances for logging output. - */ - private static Logger logger = Logger.getLogger(YahooStatusEnum.class); - - /** - * The Online status. Indicate that the user is able and willing to - * communicate. - */ - public static final YahooStatusEnum AVAILABLE - = new YahooStatusEnum(65, "Available", - loadIcon("resources/images/protocol/yahoo/yahoo16x16-online.png")); - - /** - * The Not Available status. Indicates that the user has connectivity - * but might not be able to immediately act (i.e. even less immediately than - * when in an Away status ;-P ) upon initiation of communication. - */ - public static final YahooStatusEnum BE_RIGHT_BACK - = new YahooStatusEnum(48, "Be Right Back", - loadIcon("resources/images/protocol/yahoo/yahoo16x16-away.png")); - - /** - * The Idle status. Indicates that the user is not using the messanger. - */ - public static final YahooStatusEnum IDLE - = new YahooStatusEnum(46, "Idle", - loadIcon("resources/images/protocol/yahoo/yahoo16x16-idle.png")); - - /** - * The Invisible status. Indicates that the user has connectivity even - * though it may appear otherwise to others, to whom she would appear to be - * offline. - */ - public static final YahooStatusEnum INVISIBLE - = new YahooStatusEnum(45, "Invisible", - loadIcon("resources/images/protocol/yahoo/yahoo16x16-invisible.png")); - - /** - * The STEPPED_OUT status. Indicates that the user has connectivity but might - * not be able to immediately act upon initiation of communication. - */ - public static final YahooStatusEnum STEPPED_OUT - = new YahooStatusEnum(40, "Stepped out", - loadIcon("resources/images/protocol/yahoo/yahoo16x16-away.png")); - - /** - * The Out to lunch status. Indicates that the user is eating. - */ - public static final YahooStatusEnum OUT_TO_LUNCH - = new YahooStatusEnum(39, "Out to lunch", - loadIcon("resources/images/protocol/yahoo/yahoo16x16-lunch.png")); - - /** - * The Not at home status. Indicates that the user is not at home. - */ - public static final YahooStatusEnum NOT_AT_HOME - = new YahooStatusEnum(38, "Not at home", - loadIcon("resources/images/protocol/yahoo/yahoo16x16-na.png")); - - /** - * The Not at desk status. Indicates that the user is not at his desk, but - * somewhere in the office. - */ - public static final YahooStatusEnum NOT_AT_DESK - = new YahooStatusEnum(36, "Not at desk", - loadIcon("resources/images/protocol/yahoo/yahoo16x16-na.png")); - - /** - * The Not in office status. Indicates that the user is out of the office. - */ - public static final YahooStatusEnum NOT_IN_OFFICE - = new YahooStatusEnum(34, "Not in office", - loadIcon("resources/images/protocol/yahoo/yahoo16x16-na.png")); - - /** - * The On vacation status. Indicates that the user is somewhere on the - * beach or skiing. - */ - public static final YahooStatusEnum ON_VACATION - = new YahooStatusEnum(33, "On vacation", - loadIcon("resources/images/protocol/yahoo/yahoo16x16-vacation.png")); - - /** - * The On the phone status. Indicates that the user is talking to the phone. - */ - public static final YahooStatusEnum ON_THE_PHONE - = new YahooStatusEnum(31, "On the phone", - loadIcon("resources/images/protocol/yahoo/yahoo16x16-phone.png")); - - /** - * The DND status. Indicates that the user has connectivity but prefers - * not to be contacted. - */ - public static final YahooStatusEnum BUSY - = new YahooStatusEnum(30, "Busy", - loadIcon("resources/images/protocol/yahoo/yahoo16x16-busy.png")); - - /** - * The Offline status. Indicates the user does not seem to be connected - * to the network or at least does not want us to know she is - */ - public static final YahooStatusEnum OFFLINE - = new YahooStatusEnum(0, "Offline", - loadIcon("resources/images/protocol/yahoo/yahoo16x16-offline.png")); - - /** - * The minimal set of states that any implementation must support. - */ - public static final ArrayList<YahooStatusEnum> yahooStatusSet - = new ArrayList<YahooStatusEnum>(); - static{ - yahooStatusSet.add(AVAILABLE); - yahooStatusSet.add(BE_RIGHT_BACK); - yahooStatusSet.add(BUSY); - yahooStatusSet.add(IDLE); - yahooStatusSet.add(INVISIBLE); - yahooStatusSet.add(NOT_AT_DESK); - yahooStatusSet.add(NOT_AT_HOME); - yahooStatusSet.add(NOT_IN_OFFICE); - yahooStatusSet.add(OFFLINE); - yahooStatusSet.add(ON_THE_PHONE); - yahooStatusSet.add(ON_VACATION); - yahooStatusSet.add(OUT_TO_LUNCH); - yahooStatusSet.add(STEPPED_OUT); - } - - /** - * Creates a status with the specified connectivity coeff, name and icon. - * @param status the connectivity coefficient for the specified status - * @param statusName String - * @param statusIcon the icon associated with this status - */ - protected YahooStatusEnum(int status, String statusName, byte[] statusIcon) - { - super(status, statusName, statusIcon); - } - - /** - * Loads an image from a given image path. - * @param imagePath The identifier of the image. - * @return The image for the given identifier. - */ - public static byte[] loadIcon(String imagePath) { - InputStream is = YahooStatusEnum.class.getClassLoader() - .getResourceAsStream(imagePath); - - if(is == null) - return null; - - byte[] icon = null; - try { - icon = new byte[is.available()]; - is.read(icon); - } catch (IOException exc) { - logger.error("Failed to load icon: " + imagePath, exc); - } - return icon; - } -} |