/**
 * The ProtocolProviderFactory is what actually creates instances of a ProtocolProviderService
 * implementation. A provider factory would register, persistently store, and remove when necessary,
 * ProtocolProviders. The way things are in the SIP Communicator, a user account is represented (in
 * a 1:1 relationship) by an AccountID and a ProtocolProvider. In other words - one would have as
 * many protocol providers installed in a given moment as they would user account registered through
 * the various services.
 *
 * @author Emil Ivov
 * @author Lubomir Marinov
 */
public abstract class ProtocolProviderFactory {
  /**
   * The <tt>Logger</tt> used by the <tt>ProtocolProviderFactory</tt> class and its instances for
   * logging output.
   */
  private static final Logger logger = Logger.getLogger(ProtocolProviderFactory.class);

  /** Then name of a property which represents a password. */
  public static final String PASSWORD = "******";

  /**
   * The name of a property representing the name of the protocol for an ProtocolProviderFactory.
   */
  public static final String PROTOCOL = "PROTOCOL_NAME";

  /** The name of a property representing the path to protocol icons. */
  public static final String PROTOCOL_ICON_PATH = "PROTOCOL_ICON_PATH";

  /**
   * The name of a property representing the path to the account icon to be used in the user
   * interface, when the protocol provider service is not available.
   */
  public static final String ACCOUNT_ICON_PATH = "ACCOUNT_ICON_PATH";

  /**
   * The name of a property which represents the AccountID of a ProtocolProvider and that, together
   * with a password is used to login on the protocol network..
   */
  public static final String USER_ID = "USER_ID";

  /** The name that should be displayed to others when we are calling or writing them. */
  public static final String DISPLAY_NAME = "DISPLAY_NAME";

  /** The name that should be displayed to the user on call via and chat via lists. */
  public static final String ACCOUNT_DISPLAY_NAME = "ACCOUNT_DISPLAY_NAME";

  /** The name of the property under which we store protocol AccountID-s. */
  public static final String ACCOUNT_UID = "ACCOUNT_UID";

  /**
   * The name of the property under which we store protocol the address of a protocol centric entity
   * (any protocol server).
   */
  public static final String SERVER_ADDRESS = "SERVER_ADDRESS";

  /**
   * The name of the property under which we store the number of the port where the server stored
   * against the SERVER_ADDRESS property is expecting connections to be made via this protocol.
   */
  public static final String SERVER_PORT = "SERVER_PORT";

  /**
   * The name of the property under which we store the name of the transport protocol that needs to
   * be used to access the server.
   */
  public static final String SERVER_TRANSPORT = "SERVER_TRANSPORT";

  /** The name of the property under which we store protocol the address of a protocol proxy. */
  public static final String PROXY_ADDRESS = "PROXY_ADDRESS";

  /**
   * The name of the property under which we store the number of the port where the proxy stored
   * against the PROXY_ADDRESS property is expecting connections to be made via this protocol.
   */
  public static final String PROXY_PORT = "PROXY_PORT";

  /**
   * The name of the property which defines whether proxy is auto configured by the protocol by
   * using known methods such as specific DNS queries.
   */
  public static final String PROXY_AUTO_CONFIG = "PROXY_AUTO_CONFIG";

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

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

  /**
   * The name of the property under which we store the the type of the proxy stored against the
   * PROXY_ADDRESS property. Exact type values depend on protocols and among them are socks4,
   * socks5, http and possibly others.
   */
  public static final String PROXY_TYPE = "PROXY_TYPE";

  /**
   * The name of the property under which we store the the username for the proxy stored against the
   * PROXY_ADDRESS property.
   */
  public static final String PROXY_USERNAME = "******";

  /**
   * The name of the property under which we store the the authorization name for the proxy stored
   * against the PROXY_ADDRESS property.
   */
  public static final String AUTHORIZATION_NAME = "AUTHORIZATION_NAME";

  /**
   * The name of the property under which we store the password for the proxy stored against the
   * PROXY_ADDRESS property.
   */
  public static final String PROXY_PASSWORD = "******";

  /**
   * The name of the property under which we store the name of the transport protocol that needs to
   * be used to access the proxy.
   */
  public static final String PROXY_TRANSPORT = "PROXY_TRANSPORT";

  /**
   * The name of the property that indicates whether loose routing should be forced for all traffic
   * in an account, rather than routing through an outbound proxy which is the default for Jitsi.
   */
  public static final String FORCE_PROXY_BYPASS = "******";

  /**
   * The name of the property under which we store the user preference for a transport protocol to
   * use (i.e. tcp or udp).
   */
  public static final String PREFERRED_TRANSPORT = "PREFERRED_TRANSPORT";

  /**
   * The name of the property under which we store whether we generate resource values or we just
   * use the stored one.
   */
  public static final String AUTO_GENERATE_RESOURCE = "AUTO_GENERATE_RESOURCE";

  /**
   * The name of the property under which we store resources such as the jabber resource property.
   */
  public static final String RESOURCE = "RESOURCE";

  /** The name of the property under which we store resource priority. */
  public static final String RESOURCE_PRIORITY = "RESOURCE_PRIORITY";

  /** The name of the property which defines that the call is encrypted by default */
  public static final String DEFAULT_ENCRYPTION = "DEFAULT_ENCRYPTION";

  /** The name of the property that indicates the encryption protocols for this account. */
  public static final String ENCRYPTION_PROTOCOL = "ENCRYPTION_PROTOCOL";

  /**
   * The name of the property that indicates the status (enabed or disabled) encryption protocols
   * for this account.
   */
  public static final String ENCRYPTION_PROTOCOL_STATUS = "ENCRYPTION_PROTOCOL_STATUS";

  /** The name of the property which defines if to include the ZRTP attribute to SIP/SDP */
  public static final String DEFAULT_SIPZRTP_ATTRIBUTE = "DEFAULT_SIPZRTP_ATTRIBUTE";

  /**
   * The name of the property which defines the ID of the client TLS certificate configuration
   * entry.
   */
  public static final String CLIENT_TLS_CERTIFICATE = "CLIENT_TLS_CERTIFICATE";

  /**
   * The name of the property under which we store the boolean value indicating if the user name
   * should be automatically changed if the specified name already exists. This property is meant to
   * be used by IRC implementations.
   */
  public static final String AUTO_CHANGE_USER_NAME = "AUTO_CHANGE_USER_NAME";

  /**
   * The name of the property under which we store the boolean value indicating if a password is
   * required. Initially this property is meant to be used by IRC implementations.
   */
  public static final String NO_PASSWORD_REQUIRED = "NO_PASSWORD_REQUIRED";

  /** The name of the property under which we store if the presence is enabled. */
  public static final String IS_PRESENCE_ENABLED = "IS_PRESENCE_ENABLED";

  /** The name of the property under which we store if the p2p mode for SIMPLE should be forced. */
  public static final String FORCE_P2P_MODE = "FORCE_P2P_MODE";

  /**
   * The name of the property under which we store the offline contact polling period for SIMPLE.
   */
  public static final String POLLING_PERIOD = "POLLING_PERIOD";

  /**
   * The name of the property under which we store the chosen default subscription expiration value
   * for SIMPLE.
   */
  public static final String SUBSCRIPTION_EXPIRATION = "SUBSCRIPTION_EXPIRATION";

  /** Indicates if the server address has been validated. */
  public static final String SERVER_ADDRESS_VALIDATED = "SERVER_ADDRESS_VALIDATED";

  /** Indicates if the server settings are over */
  public static final String IS_SERVER_OVERRIDDEN = "IS_SERVER_OVERRIDDEN";
  /** Indicates if the proxy address has been validated. */
  public static final String PROXY_ADDRESS_VALIDATED = "PROXY_ADDRESS_VALIDATED";

  /** Indicates the search strategy chosen for the DICT protocole. */
  public static final String STRATEGY = "STRATEGY";

  /** Indicates a protocol that would not be shown in the user interface as an account. */
  public static final String IS_PROTOCOL_HIDDEN = "IS_PROTOCOL_HIDDEN";

  /** Indicates if the given account is the preferred account. */
  public static final String IS_PREFERRED_PROTOCOL = "IS_PREFERRED_PROTOCOL";

  /**
   * The name of the property that would indicate if a given account is currently enabled or
   * disabled.
   */
  public static final String IS_ACCOUNT_DISABLED = "IS_ACCOUNT_DISABLED";

  /** Indicates if ICE should be used. */
  public static final String IS_USE_ICE = "ICE_ENABLED";

  /** Indicates if Google ICE should be used. */
  public static final String IS_USE_GOOGLE_ICE = "GTALK_ICE_ENABLED";

  /** Indicates if STUN server should be automatically discovered. */
  public static final String AUTO_DISCOVER_STUN = "AUTO_DISCOVER_STUN";

  /** Indicates if default STUN server would be used if no other STUN/TURN server are available. */
  public static final String USE_DEFAULT_STUN_SERVER = "USE_DEFAULT_STUN_SERVER";

  /**
   * The name of the boolean account property which indicates whether Jitsi VideoBridge is to be
   * used, if available and supported, for conference calls.
   */
  public static final String USE_JITSI_VIDEO_BRIDGE = "USE_JITSI_VIDEO_BRIDGE";

  /**
   * The property name prefix for all stun server properties. We generally use this prefix in
   * conjunction with an index which is how we store multiple servers.
   */
  public static final String STUN_PREFIX = "STUN";

  /** The base property name for address of additional STUN servers specified. */
  public static final String STUN_ADDRESS = "ADDRESS";

  /** The base property name for port of additional STUN servers specified. */
  public static final String STUN_PORT = "PORT";

  /** The base property name for username of additional STUN servers specified. */
  public static final String STUN_USERNAME = "******";

  /** The base property name for password of additional STUN servers specified. */
  public static final String STUN_PASSWORD = "******";

  /**
   * The base property name for the turn supported property of additional STUN servers specified.
   */
  public static final String STUN_IS_TURN_SUPPORTED = "IS_TURN_SUPPORTED";

  /** Indicates if JingleNodes should be used with ICE. */
  public static final String IS_USE_JINGLE_NODES = "JINGLE_NODES_ENABLED";

  /** Indicates if JingleNodes should be used with ICE. */
  public static final String AUTO_DISCOVER_JINGLE_NODES = "AUTO_DISCOVER_JINGLE_NODES";

  /** Indicates if JingleNodes should use buddies to search for nodes. */
  public static final String JINGLE_NODES_SEARCH_BUDDIES = "JINGLE_NODES_SEARCH_BUDDIES";

  /** Indicates if UPnP should be used with ICE. */
  public static final String IS_USE_UPNP = "UPNP_ENABLED";

  /** Indicates if we allow non-TLS connection. */
  public static final String IS_ALLOW_NON_SECURE = "ALLOW_NON_SECURE";

  /** Enable notifications for new voicemail messages. */
  public static final String VOICEMAIL_ENABLED = "VOICEMAIL_ENABLED";

  /**
   * Address used to reach voicemail box, by services able to subscribe for voicemail new messages
   * notifications.
   */
  public static final String VOICEMAIL_URI = "VOICEMAIL_URI";

  /** Address used to call to hear your messages stored on the server for your voicemail. */
  public static final String VOICEMAIL_CHECK_URI = "VOICEMAIL_CHECK_URI";

  /** Indicates if calling is disabled for a certain account. */
  public static final String IS_CALLING_DISABLED_FOR_ACCOUNT = "CALLING_DISABLED";

  /** Indicates if desktop streaming/sharing is disabled for a certain account. */
  public static final String IS_DESKTOP_STREAMING_DISABLED = "DESKTOP_STREAMING_DISABLED";

  /** The sms default server address. */
  public static final String SMS_SERVER_ADDRESS = "SMS_SERVER_ADDRESS";

  /** Keep-alive method used by the protocol. */
  public static final String KEEP_ALIVE_METHOD = "KEEP_ALIVE_METHOD";

  /** The interval for keep-alives if any. */
  public static final String KEEP_ALIVE_INTERVAL = "KEEP_ALIVE_INTERVAL";

  /** The minimal DTMF tone duration. */
  public static final String DTMF_MINIMAL_TONE_DURATION = "DTMF_MINIMAL_TONE_DURATION";

  /** Paranoia mode when turned on requires all calls to be secure and indicated as such. */
  public static final String MODE_PARANOIA = "MODE_PARANOIA";

  /** The name of the "override encodings" property */
  public static final String OVERRIDE_ENCODINGS = "OVERRIDE_ENCODINGS";

  /** The prefix used to store account encoding properties */
  public static final String ENCODING_PROP_PREFIX = "Encodings";

  /**
   * The <code>BundleContext</code> containing (or to contain) the service registration of this
   * factory.
   */
  private final BundleContext bundleContext;

  /**
   * The name of the protocol this factory registers its <code>ProtocolProviderService</code>s with
   * and to be placed in the properties of the accounts created by this factory.
   */
  private final String protocolName;

  /**
   * The table that we store our accounts in.
   *
   * <p>TODO Synchronize the access to the field which may in turn be better achieved by also hiding
   * it from protected into private access.
   */
  protected final Hashtable<AccountID, ServiceRegistration> registeredAccounts =
      new Hashtable<AccountID, ServiceRegistration>();

  /**
   * The name of the property that indicates the AVP type.
   *
   * <ul>
   *   <li>{@link #SAVP_OFF}
   *   <li>{@link #SAVP_MANDATORY}
   *   <li>{@link #SAVP_OPTIONAL}
   * </ul>
   */
  public static final String SAVP_OPTION = "SAVP_OPTION";

  /** Always use RTP/AVP */
  public static final int SAVP_OFF = 0;

  /** Always use RTP/SAVP */
  public static final int SAVP_MANDATORY = 1;

  /** Sends two media description, with RTP/SAVP being first. */
  public static final int SAVP_OPTIONAL = 2;

  /**
   * The name of the property that defines the enabled SDES cipher suites. Enabled suites are listed
   * as CSV by their RFC name.
   */
  public static final String SDES_CIPHER_SUITES = "SDES_CIPHER_SUITES";

  /**
   * Creates a new <tt>ProtocolProviderFactory</tt>.
   *
   * @param bundleContext the bundle context reference of the service
   * @param protocolName the name of the protocol
   */
  protected ProtocolProviderFactory(BundleContext bundleContext, String protocolName) {
    this.bundleContext = bundleContext;
    this.protocolName = protocolName;
  }

  /**
   * Gets the <code>BundleContext</code> containing (or to contain) the service registration of this
   * factory.
   *
   * @return the <code>BundleContext</code> containing (or to contain) the service registration of
   *     this factory
   */
  public BundleContext getBundleContext() {
    return bundleContext;
  }

  /**
   * Initializes and creates an account corresponding to the specified accountProperties and
   * registers the resulting ProtocolProvider in the <tt>context</tt> BundleContext parameter. Note
   * that account registration is persistent and accounts that are registered during a particular
   * sip-communicator session would be automatically reloaded during all following sessions until
   * they are removed through the removeAccount method.
   *
   * @param userID the user identifier uniquely representing the newly created account within the
   *     protocol namespace.
   * @param accountProperties a set of protocol (or implementation) specific properties defining the
   *     new account.
   * @return the AccountID of the newly created account.
   * @throws java.lang.IllegalArgumentException if userID does not correspond to an identifier in
   *     the context of the underlying protocol or if accountProperties does not contain a complete
   *     set of account installation properties.
   * @throws java.lang.IllegalStateException if the account has already been installed.
   * @throws java.lang.NullPointerException if any of the arguments is null.
   */
  public abstract AccountID installAccount(String userID, Map<String, String> accountProperties)
      throws IllegalArgumentException, IllegalStateException, NullPointerException;

  /**
   * Modifies the account corresponding to the specified accountID. This method is meant to be used
   * to change properties of already existing accounts. Note that if the given accountID doesn't
   * correspond to any registered account this method would do nothing.
   *
   * @param protocolProvider the protocol provider service corresponding to the modified account.
   * @param accountProperties a set of protocol (or implementation) specific properties defining the
   *     new account.
   * @throws java.lang.NullPointerException if any of the arguments is null.
   */
  public abstract void modifyAccount(
      ProtocolProviderService protocolProvider, Map<String, String> accountProperties)
      throws NullPointerException;

  /**
   * Returns a copy of the list containing the <tt>AccountID</tt>s of all accounts currently
   * registered in this protocol provider.
   *
   * @return a copy of the list containing the <tt>AccountID</tt>s of all accounts currently
   *     registered in this protocol provider.
   */
  public ArrayList<AccountID> getRegisteredAccounts() {
    synchronized (registeredAccounts) {
      return new ArrayList<AccountID>(registeredAccounts.keySet());
    }
  }

  /**
   * Returns the ServiceReference for the protocol provider corresponding to the specified accountID
   * or null if the accountID is unknown.
   *
   * @param accountID the accountID of the protocol provider we'd like to get
   * @return a ServiceReference object to the protocol provider with the specified account id and
   *     null if the account id is unknown to the provider factory.
   */
  public ServiceReference getProviderForAccount(AccountID accountID) {
    ServiceRegistration registration;

    synchronized (registeredAccounts) {
      registration = registeredAccounts.get(accountID);
    }
    return (registration == null) ? null : registration.getReference();
  }

  /**
   * Removes the specified account from the list of accounts that this provider factory is handling.
   * If the specified accountID is unknown to the ProtocolProviderFactory, the call has no effect
   * and false is returned. This method is persistent in nature and once called the account
   * corresponding to the specified ID will not be loaded during future runs of the project.
   *
   * @param accountID the ID of the account to remove.
   * @return true if an account with the specified ID existed and was removed and false otherwise.
   */
  public boolean uninstallAccount(AccountID accountID) {
    // Unregister the protocol provider.
    ServiceReference serRef = getProviderForAccount(accountID);

    boolean wasAccountExisting = false;

    // If the protocol provider service is registered, first unregister the
    // service.
    if (serRef != null) {
      BundleContext bundleContext = getBundleContext();
      ProtocolProviderService protocolProvider =
          (ProtocolProviderService) bundleContext.getService(serRef);

      try {
        protocolProvider.unregister();
      } catch (OperationFailedException ex) {
        logger.error(
            "Failed to unregister protocol provider for account: "
                + accountID
                + " caused by: "
                + ex);
      }
    }

    ServiceRegistration registration;

    synchronized (registeredAccounts) {
      registration = registeredAccounts.remove(accountID);
    }

    // first remove the stored account so when PP is unregistered we can
    // distinguish between deleted or just disabled account
    wasAccountExisting = removeStoredAccount(accountID);

    if (registration != null) {
      // Kill the service.
      registration.unregister();
    }

    return wasAccountExisting;
  }

  /**
   * The method stores the specified account in the configuration service under the package name of
   * the source factory. The restore and remove account methods are to be used to obtain access to
   * and control the stored accounts.
   *
   * <p>In order to store all account properties, the method would create an entry in the
   * configuration service corresponding (beginning with) the <tt>sourceFactory</tt>'s package name
   * and add to it a unique identifier (e.g. the current miliseconds.)
   *
   * @param accountID the AccountID corresponding to the account that we would like to store.
   */
  protected void storeAccount(AccountID accountID) {
    this.storeAccount(accountID, true);
  }

  /**
   * The method stores the specified account in the configuration service under the package name of
   * the source factory. The restore and remove account methods are to be used to obtain access to
   * and control the stored accounts.
   *
   * <p>In order to store all account properties, the method would create an entry in the
   * configuration service corresponding (beginning with) the <tt>sourceFactory</tt>'s package name
   * and add to it a unique identifier (e.g. the current miliseconds.)
   *
   * @param accountID the AccountID corresponding to the account that we would like to store.
   * @param isModification if <tt>false</tt> there must be no such already loaded account, it
   *     <tt>true</tt> ist modification of an existing account. Usually we use this method with
   *     <tt>false</tt> in method installAccount and with <tt>true</tt> or the overridden method in
   *     method modifyAccount.
   */
  protected void storeAccount(AccountID accountID, boolean isModification) {
    if (!isModification && getAccountManager().getStoredAccounts().contains(accountID)) {
      throw new IllegalStateException(
          "An account for id " + accountID.getUserID() + " was already loaded!");
    }

    try {
      getAccountManager().storeAccount(this, accountID);
    } catch (OperationFailedException ofex) {
      throw new UndeclaredThrowableException(ofex);
    }
  }

  /**
   * Saves the password for the specified account after scrambling it a bit so that it is not
   * visible from first sight. (The method remains highly insecure).
   *
   * @param accountID the AccountID for the account whose password we're storing
   * @param password the password itself
   * @throws IllegalArgumentException if no account corresponding to <code>accountID</code> has been
   *     previously stored
   */
  public void storePassword(AccountID accountID, String password) throws IllegalArgumentException {
    try {
      storePassword(getBundleContext(), accountID, password);
    } catch (OperationFailedException ofex) {
      throw new UndeclaredThrowableException(ofex);
    }
  }

  /**
   * Saves the password for the specified account after scrambling it a bit so that it is not
   * visible from first sight (Method remains highly insecure).
   *
   * <p>TODO Delegate the implementation to {@link AccountManager} because it knows the format in
   * which the password (among the other account properties) is to be saved.
   *
   * @param bundleContext a currently valid bundle context.
   * @param accountID the <tt>AccountID</tt> of the account whose password is to be stored
   * @param password the password to be stored
   * @throws IllegalArgumentException if no account corresponding to <tt>accountID</tt> has been
   *     previously stored.
   * @throws OperationFailedException if anything goes wrong while storing the specified
   *     <tt>password</tt>
   */
  protected void storePassword(BundleContext bundleContext, AccountID accountID, String password)
      throws IllegalArgumentException, OperationFailedException {
    String accountPrefix = findAccountPrefix(bundleContext, accountID, getFactoryImplPackageName());

    if (accountPrefix == null) {
      throw new IllegalArgumentException(
          "No previous records found for account ID: "
              + accountID.getAccountUniqueID()
              + " in package"
              + getFactoryImplPackageName());
    }

    CredentialsStorageService credentialsStorage =
        ServiceUtils.getService(bundleContext, CredentialsStorageService.class);

    if (!credentialsStorage.storePassword(accountPrefix, password)) {
      throw new OperationFailedException(
          "CredentialsStorageService failed to storePassword",
          OperationFailedException.GENERAL_ERROR);
    }
  }

  /**
   * Returns the password last saved for the specified account.
   *
   * @param accountID the AccountID for the account whose password we're looking for
   * @return a String containing the password for the specified accountID
   */
  public String loadPassword(AccountID accountID) {
    return loadPassword(getBundleContext(), accountID);
  }

  /**
   * Returns the password last saved for the specified account.
   *
   * <p>TODO Delegate the implementation to {@link AccountManager} because it knows the format in
   * which the password (among the other account properties) was saved.
   *
   * @param bundleContext a currently valid bundle context.
   * @param accountID the AccountID for the account whose password we're looking for..
   * @return a String containing the password for the specified accountID.
   */
  protected String loadPassword(BundleContext bundleContext, AccountID accountID) {
    String accountPrefix = findAccountPrefix(bundleContext, accountID, getFactoryImplPackageName());

    if (accountPrefix == null) return null;

    CredentialsStorageService credentialsStorage =
        ServiceUtils.getService(bundleContext, CredentialsStorageService.class);

    return credentialsStorage.loadPassword(accountPrefix);
  }

  /**
   * Initializes and creates an account corresponding to the specified accountProperties and
   * registers the resulting ProtocolProvider in the <tt>context</tt> BundleContext parameter. This
   * method has a persistent effect. Once created the resulting account will remain installed until
   * removed through the uninstallAccount method.
   *
   * @param accountProperties a set of protocol (or implementation) specific properties defining the
   *     new account.
   * @return the AccountID of the newly loaded account
   */
  public AccountID loadAccount(Map<String, String> accountProperties) {
    AccountID accountID = createAccount(accountProperties);

    loadAccount(accountID);

    return accountID;
  }

  /**
   * Creates a protocol provider for the given <tt>accountID</tt> and registers it in the bundle
   * context. This method has a persistent effect. Once created the resulting account will remain
   * installed until removed through the uninstallAccount method.
   *
   * @param accountID the account identifier
   * @return <tt>true</tt> if the account with the given <tt>accountID</tt> is successfully loaded,
   *     otherwise returns <tt>false</tt>
   */
  public boolean loadAccount(AccountID accountID) {
    // Need to obtain the original user id property, instead of calling
    // accountID.getUserID(), because this method could return a modified
    // version of the user id property.
    String userID = accountID.getAccountPropertyString(ProtocolProviderFactory.USER_ID);

    ProtocolProviderService service = createService(userID, accountID);

    Dictionary<String, String> properties = new Hashtable<String, String>();
    properties.put(PROTOCOL, protocolName);
    properties.put(USER_ID, userID);

    ServiceRegistration serviceRegistration =
        bundleContext.registerService(ProtocolProviderService.class.getName(), service, properties);

    if (serviceRegistration == null) return false;
    else {
      synchronized (registeredAccounts) {
        registeredAccounts.put(accountID, serviceRegistration);
      }
      return true;
    }
  }

  /**
   * Unloads the account corresponding to the given <tt>accountID</tt>. Unregisters the
   * corresponding protocol provider, but keeps the account in contrast to the uninstallAccount
   * method.
   *
   * @param accountID the account identifier
   * @return true if an account with the specified ID existed and was unloaded and false otherwise.
   */
  public boolean unloadAccount(AccountID accountID) {
    // Unregister the protocol provider.
    ServiceReference serRef = getProviderForAccount(accountID);

    if (serRef == null) {
      return false;
    }

    BundleContext bundleContext = getBundleContext();
    ProtocolProviderService protocolProvider =
        (ProtocolProviderService) bundleContext.getService(serRef);

    try {
      protocolProvider.unregister();
    } catch (OperationFailedException ex) {
      logger.error(
          "Failed to unregister protocol provider for account : "
              + accountID
              + " caused by: "
              + ex);
    }

    ServiceRegistration registration;

    synchronized (registeredAccounts) {
      registration = registeredAccounts.remove(accountID);
    }
    if (registration == null) {
      return false;
    }

    // Kill the service.
    registration.unregister();

    return true;
  }

  /**
   * Initializes and creates an account corresponding to the specified accountProperties.
   *
   * @param accountProperties a set of protocol (or implementation) specific properties defining the
   *     new account.
   * @return the AccountID of the newly created account
   */
  public AccountID createAccount(Map<String, String> accountProperties) {
    BundleContext bundleContext = getBundleContext();
    if (bundleContext == null)
      throw new NullPointerException("The specified BundleContext was null");

    if (accountProperties == null)
      throw new NullPointerException("The specified property map was null");

    String userID = accountProperties.get(USER_ID);
    if (userID == null)
      throw new NullPointerException("The account properties contained no user id.");

    String protocolName = getProtocolName();
    if (!accountProperties.containsKey(PROTOCOL)) accountProperties.put(PROTOCOL, protocolName);

    return createAccountID(userID, accountProperties);
  }

  /**
   * Creates a new <code>AccountID</code> instance with a specific user ID to represent a given set
   * of account properties.
   *
   * <p>The method is a pure factory allowing implementers to specify the runtime type of the
   * created <code>AccountID</code> and customize the instance. The returned <code>AccountID</code>
   * will later be associated with a <code>ProtocolProviderService</code> by the caller (e.g. using
   * {@link #createService(String, AccountID)}).
   *
   * @param userID the user ID of the new instance
   * @param accountProperties the set of properties to be represented by the new instance
   * @return a new <code>AccountID</code> instance with the specified user ID representing the given
   *     set of account properties
   */
  protected abstract AccountID createAccountID(
      String userID, Map<String, String> accountProperties);

  /**
   * Gets the name of the protocol this factory registers its <code>ProtocolProviderService</code>s
   * with and to be placed in the properties of the accounts created by this factory.
   *
   * @return the name of the protocol this factory registers its <code>ProtocolProviderService
   *     </code>s with and to be placed in the properties of the accounts created by this factory
   */
  public String getProtocolName() {
    return protocolName;
  }

  /**
   * Initializes a new <code>ProtocolProviderService</code> instance with a specific user ID to
   * represent a specific <code>AccountID</code>.
   *
   * <p>The method is a pure factory allowing implementers to specify the runtime type of the
   * created <code>ProtocolProviderService</code> and customize the instance. The caller will later
   * register the returned service with the <code>BundleContext</code> of this factory.
   *
   * @param userID the user ID to initialize the new instance with
   * @param accountID the <code>AccountID</code> to be represented by the new instance
   * @return a new <code>ProtocolProviderService</code> instance with the specific user ID
   *     representing the specified <code>AccountID</code>
   */
  protected abstract ProtocolProviderService createService(String userID, AccountID accountID);

  /**
   * Removes the account with <tt>accountID</tt> from the set of accounts that are persistently
   * stored inside the configuration service.
   *
   * @param accountID the AccountID of the account to remove.
   * @return true if an account has been removed and false otherwise.
   */
  protected boolean removeStoredAccount(AccountID accountID) {
    return getAccountManager().removeStoredAccount(this, accountID);
  }

  /**
   * Returns the prefix for all persistently stored properties of the account with the specified id.
   *
   * @param bundleContext a currently valid bundle context.
   * @param accountID the AccountID of the account whose properties we're looking for.
   * @param sourcePackageName a String containing the package name of the concrete factory class
   *     that extends us.
   * @return a String indicating the ConfigurationService property name prefix under which all
   *     account properties are stored or null if no account corresponding to the specified id was
   *     found.
   */
  public static String findAccountPrefix(
      BundleContext bundleContext, AccountID accountID, String sourcePackageName) {
    ServiceReference confReference =
        bundleContext.getServiceReference(ConfigurationService.class.getName());
    ConfigurationService configurationService =
        (ConfigurationService) bundleContext.getService(confReference);

    // first retrieve all accounts that we've registered
    List<String> storedAccounts =
        configurationService.getPropertyNamesByPrefix(sourcePackageName, true);

    // find an account with the corresponding id.
    for (String accountRootPropertyName : storedAccounts) {
      // unregister the account in the configuration service.
      // all the properties must have been registered in the following
      // hierarchy:
      // net.java.sip.communicator.impl.protocol.PROTO_NAME.ACC_ID.PROP_NAME
      String accountUID =
          configurationService.getString(
              accountRootPropertyName // node id
                  + "."
                  + ACCOUNT_UID); // propname

      if (accountID.getAccountUniqueID().equals(accountUID)) {
        return accountRootPropertyName;
      }
    }
    return null;
  }

  /**
   * Returns the name of the package that we're currently running in (i.e. the name of the package
   * containing the proto factory that extends us).
   *
   * @return a String containing the package name of the concrete factory class that extends us.
   */
  private String getFactoryImplPackageName() {
    String className = getClass().getName();

    return className.substring(0, className.lastIndexOf('.'));
  }

  /** Prepares the factory for bundle shutdown. */
  public void stop() {
    if (logger.isTraceEnabled()) logger.trace("Preparing to stop all protocol providers of" + this);

    synchronized (registeredAccounts) {
      for (Enumeration<ServiceRegistration> registrations = registeredAccounts.elements();
          registrations.hasMoreElements(); ) {
        ServiceRegistration reg = registrations.nextElement();

        stop(reg);

        reg.unregister();
      }

      registeredAccounts.clear();
    }
  }

  /**
   * Shuts down the <code>ProtocolProviderService</code> representing an account registered with
   * this factory.
   *
   * @param registeredAccount the <code>ServiceRegistration</code> of the <code>
   *     ProtocolProviderService</code> representing an account registered with this factory
   */
  protected void stop(ServiceRegistration registeredAccount) {
    ProtocolProviderService protocolProviderService =
        (ProtocolProviderService) getBundleContext().getService(registeredAccount.getReference());

    protocolProviderService.shutdown();
  }

  /**
   * Get the <tt>AccountManager</tt> of the protocol.
   *
   * @return <tt>AccountManager</tt> of the protocol
   */
  private AccountManager getAccountManager() {
    BundleContext bundleContext = getBundleContext();
    ServiceReference serviceReference =
        bundleContext.getServiceReference(AccountManager.class.getName());

    return (AccountManager) bundleContext.getService(serviceReference);
  }
}
Ejemplo n.º 2
0
/**
 * Class is a transport layer for WebRTC data channels. It consists of SCTP connection running on
 * top of ICE/DTLS layer. Manages WebRTC data channels. See
 * http://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-08 for more info on WebRTC data
 * channels.
 *
 * <p>Control protocol: http://tools.ietf.org/html/draft-ietf-rtcweb-data-protocol-03 FIXME handle
 * closing of data channels(SCTP stream reset)
 *
 * @author Pawel Domas
 * @author Lyubomir Marinov
 * @author Boris Grozev
 */
public class SctpConnection extends Channel
    implements SctpDataCallback, SctpSocket.NotificationListener {
  /** Generator used to track debug IDs. */
  private static int debugIdGen = -1;

  /** DTLS transport buffer size. Note: randomly chosen. */
  private static final int DTLS_BUFFER_SIZE = 2048;

  /** Switch used for debugging SCTP traffic purposes. FIXME to be removed */
  private static final boolean LOG_SCTP_PACKETS = false;

  /** The logger */
  private static final Logger logger = Logger.getLogger(SctpConnection.class);

  /**
   * Message type used to acknowledge WebRTC data channel allocation on SCTP stream ID on which
   * <tt>MSG_OPEN_CHANNEL</tt> message arrives.
   */
  private static final int MSG_CHANNEL_ACK = 0x2;

  private static final byte[] MSG_CHANNEL_ACK_BYTES = new byte[] {MSG_CHANNEL_ACK};

  /**
   * Message with this type sent over control PPID in order to open new WebRTC data channel on SCTP
   * stream ID that this message is sent.
   */
  private static final int MSG_OPEN_CHANNEL = 0x3;

  /** SCTP transport buffer size. */
  private static final int SCTP_BUFFER_SIZE = DTLS_BUFFER_SIZE - 13;

  /** The pool of <tt>Thread</tt>s which run <tt>SctpConnection</tt>s. */
  private static final ExecutorService threadPool =
      ExecutorUtils.newCachedThreadPool(true, SctpConnection.class.getName());

  /** Payload protocol id that identifies binary data in WebRTC data channel. */
  static final int WEB_RTC_PPID_BIN = 53;

  /** Payload protocol id for control data. Used for <tt>WebRtcDataStream</tt> allocation. */
  static final int WEB_RTC_PPID_CTRL = 50;

  /** Payload protocol id that identifies text data UTF8 encoded in WebRTC data channels. */
  static final int WEB_RTC_PPID_STRING = 51;

  /**
   * The <tt>String</tt> value of the <tt>Protocol</tt> field of the <tt>DATA_CHANNEL_OPEN</tt>
   * message.
   */
  private static final String WEBRTC_DATA_CHANNEL_PROTOCOL = "http://jitsi.org/protocols/colibri";

  private static synchronized int generateDebugId() {
    debugIdGen += 2;
    return debugIdGen;
  }

  /**
   * Indicates whether the STCP association is ready and has not been ended by a subsequent state
   * change.
   */
  private boolean assocIsUp;

  /** Indicates if we have accepted incoming connection. */
  private boolean acceptedIncomingConnection;

  /** Data channels mapped by SCTP stream identified(sid). */
  private final Map<Integer, WebRtcDataStream> channels = new HashMap<Integer, WebRtcDataStream>();

  /** Debug ID used to distinguish SCTP sockets in packet logs. */
  private final int debugId;

  /**
   * The <tt>AsyncExecutor</tt> which is to asynchronously dispatch the events fired by this
   * instance in order to prevent possible listeners from blocking this <tt>SctpConnection</tt> in
   * general and {@link #sctpSocket} in particular for too long. The timeout of <tt>15</tt> is
   * chosen to be in accord with the time it takes to expire a <tt>Channel</tt>.
   */
  private final AsyncExecutor<Runnable> eventDispatcher =
      new AsyncExecutor<Runnable>(15, TimeUnit.MILLISECONDS);

  /** Datagram socket for ICE/UDP layer. */
  private IceSocketWrapper iceSocket;

  /**
   * List of <tt>WebRtcDataStreamListener</tt>s that will be notified whenever new WebRTC data
   * channel is opened.
   */
  private final List<WebRtcDataStreamListener> listeners =
      new ArrayList<WebRtcDataStreamListener>();

  /** Remote SCTP port. */
  private final int remoteSctpPort;

  /** <tt>SctpSocket</tt> used for SCTP transport. */
  private SctpSocket sctpSocket;

  /**
   * Flag prevents from starting this connection multiple times from {@link #maybeStartStream()}.
   */
  private boolean started;

  /**
   * Initializes a new <tt>SctpConnection</tt> instance.
   *
   * @param id the string identifier of this connection instance
   * @param content the <tt>Content</tt> which is initializing the new instance
   * @param endpoint the <tt>Endpoint</tt> of newly created instance
   * @param remoteSctpPort the SCTP port used by remote peer
   * @param channelBundleId the ID of the channel-bundle this <tt>SctpConnection</tt> is to be a
   *     part of (or <tt>null</tt> if no it is not to be a part of a channel-bundle).
   * @throws Exception if an error occurs while initializing the new instance
   */
  public SctpConnection(
      String id, Content content, Endpoint endpoint, int remoteSctpPort, String channelBundleId)
      throws Exception {
    super(content, id, channelBundleId);

    setEndpoint(endpoint.getID());

    this.remoteSctpPort = remoteSctpPort;
    this.debugId = generateDebugId();
  }

  /**
   * Adds <tt>WebRtcDataStreamListener</tt> to the list of listeners.
   *
   * @param listener the <tt>WebRtcDataStreamListener</tt> to be added to the listeners list.
   */
  public void addChannelListener(WebRtcDataStreamListener listener) {
    if (listener == null) {
      throw new NullPointerException("listener");
    } else {
      synchronized (listeners) {
        if (!listeners.contains(listener)) {
          listeners.add(listener);
        }
      }
    }
  }

  /** {@inheritDoc} */
  @Override
  protected void closeStream() throws IOException {
    try {
      synchronized (this) {
        assocIsUp = false;
        acceptedIncomingConnection = false;
        if (sctpSocket != null) {
          sctpSocket.close();
          sctpSocket = null;
        }
      }
    } finally {
      if (iceSocket != null) {
        // It is now the responsibility of the transport manager to
        // close the socket.
        // iceUdpSocket.close();
      }
    }
  }

  /** {@inheritDoc} */
  @Override
  public void expire() {
    try {
      eventDispatcher.shutdown();
    } finally {
      super.expire();
    }
  }

  /**
   * Gets the <tt>WebRtcDataStreamListener</tt>s added to this instance.
   *
   * @return the <tt>WebRtcDataStreamListener</tt>s added to this instance or <tt>null</tt> if there
   *     are no <tt>WebRtcDataStreamListener</tt>s added to this instance
   */
  private WebRtcDataStreamListener[] getChannelListeners() {
    WebRtcDataStreamListener[] ls;

    synchronized (listeners) {
      if (listeners.isEmpty()) {
        ls = null;
      } else {
        ls = listeners.toArray(new WebRtcDataStreamListener[listeners.size()]);
      }
    }
    return ls;
  }

  /**
   * Returns default <tt>WebRtcDataStream</tt> if it's ready or <tt>null</tt> otherwise.
   *
   * @return <tt>WebRtcDataStream</tt> if it's ready or <tt>null</tt> otherwise.
   * @throws IOException
   */
  public WebRtcDataStream getDefaultDataStream() throws IOException {
    WebRtcDataStream def;

    synchronized (this) {
      if (sctpSocket == null) {
        def = null;
      } else {
        // Channel that runs on sid 0
        def = channels.get(0);
        if (def == null) {
          def = openChannel(0, 0, 0, 0, "default");
        }
        // Pawel Domas: Must be acknowledged before use
        /*
         * XXX Lyubomir Marinov: We're always sending ordered. According
         * to "WebRTC Data Channel Establishment Protocol", we can start
         * sending messages containing user data after the
         * DATA_CHANNEL_OPEN message has been sent without waiting for
         * the reception of the corresponding DATA_CHANNEL_ACK message.
         */
        //                if (!def.isAcknowledged())
        //                    def = null;
      }
    }
    return def;
  }

  /**
   * Returns <tt>true</tt> if this <tt>SctpConnection</tt> is connected to the remote peer and
   * operational.
   *
   * @return <tt>true</tt> if this <tt>SctpConnection</tt> is connected to the remote peer and
   *     operational
   */
  public boolean isReady() {
    return assocIsUp && acceptedIncomingConnection;
  }

  /** {@inheritDoc} */
  @Override
  protected void maybeStartStream() throws IOException {
    // connector
    final StreamConnector connector = getStreamConnector();

    if (connector == null) return;

    synchronized (this) {
      if (started) return;

      threadPool.execute(
          new Runnable() {
            @Override
            public void run() {
              try {
                Sctp.init();

                runOnDtlsTransport(connector);
              } catch (IOException e) {
                logger.error(e, e);
              } finally {
                try {
                  Sctp.finish();
                } catch (IOException e) {
                  logger.error("Failed to shutdown SCTP stack", e);
                }
              }
            }
          });

      started = true;
    }
  }

  /**
   * Submits {@link #notifyChannelOpenedInEventDispatcher(WebRtcDataStream)} to {@link
   * #eventDispatcher} for asynchronous execution.
   *
   * @param dataChannel
   */
  private void notifyChannelOpened(final WebRtcDataStream dataChannel) {
    if (!isExpired()) {
      eventDispatcher.execute(
          new Runnable() {
            @Override
            public void run() {
              notifyChannelOpenedInEventDispatcher(dataChannel);
            }
          });
    }
  }

  private void notifyChannelOpenedInEventDispatcher(WebRtcDataStream dataChannel) {
    /*
     * When executing asynchronously in eventDispatcher, it is technically
     * possible that this SctpConnection may have expired by now.
     */
    if (!isExpired()) {
      WebRtcDataStreamListener[] ls = getChannelListeners();

      if (ls != null) {
        for (WebRtcDataStreamListener l : ls) {
          l.onChannelOpened(this, dataChannel);
        }
      }
    }
  }

  /**
   * Submits {@link #notifySctpConnectionReadyInEventDispatcher()} to {@link #eventDispatcher} for
   * asynchronous execution.
   */
  private void notifySctpConnectionReady() {
    if (!isExpired()) {
      eventDispatcher.execute(
          new Runnable() {
            @Override
            public void run() {
              notifySctpConnectionReadyInEventDispatcher();
            }
          });
    }
  }

  /**
   * Notifies the <tt>WebRtcDataStreamListener</tt>s added to this instance that this
   * <tt>SctpConnection</tt> is ready i.e. it is connected to the remote peer and operational.
   */
  private void notifySctpConnectionReadyInEventDispatcher() {
    /*
     * When executing asynchronously in eventDispatcher, it is technically
     * possible that this SctpConnection may have expired by now.
     */
    if (!isExpired() && isReady()) {
      WebRtcDataStreamListener[] ls = getChannelListeners();

      if (ls != null) {
        for (WebRtcDataStreamListener l : ls) {
          l.onSctpConnectionReady(this);
        }
      }
    }
  }

  /**
   * Handles control packet.
   *
   * @param data raw packet data that arrived on control PPID.
   * @param sid SCTP stream id on which the data has arrived.
   */
  private synchronized void onCtrlPacket(byte[] data, int sid) throws IOException {
    ByteBuffer buffer = ByteBuffer.wrap(data);
    int messageType = /* 1 byte unsigned integer */ 0xFF & buffer.get();

    if (messageType == MSG_CHANNEL_ACK) {
      if (logger.isDebugEnabled()) {
        logger.debug(getEndpoint().getID() + " ACK received SID: " + sid);
      }
      // Open channel ACK
      WebRtcDataStream channel = channels.get(sid);
      if (channel != null) {
        // Ack check prevents from firing multiple notifications
        // if we get more than one ACKs (by mistake/bug).
        if (!channel.isAcknowledged()) {
          channel.ackReceived();
          notifyChannelOpened(channel);
        } else {
          logger.warn("Redundant ACK received for SID: " + sid);
        }
      } else {
        logger.error("No channel exists on sid: " + sid);
      }
    } else if (messageType == MSG_OPEN_CHANNEL) {
      int channelType = /* 1 byte unsigned integer */ 0xFF & buffer.get();
      int priority = /* 2 bytes unsigned integer */ 0xFFFF & buffer.getShort();
      long reliability = /* 4 bytes unsigned integer */ 0xFFFFFFFFL & buffer.getInt();
      int labelLength = /* 2 bytes unsigned integer */ 0xFFFF & buffer.getShort();
      int protocolLength = /* 2 bytes unsigned integer */ 0xFFFF & buffer.getShort();
      String label;
      String protocol;

      if (labelLength == 0) {
        label = "";
      } else {
        byte[] labelBytes = new byte[labelLength];

        buffer.get(labelBytes);
        label = new String(labelBytes, "UTF-8");
      }
      if (protocolLength == 0) {
        protocol = "";
      } else {
        byte[] protocolBytes = new byte[protocolLength];

        buffer.get(protocolBytes);
        protocol = new String(protocolBytes, "UTF-8");
      }

      if (logger.isDebugEnabled()) {
        logger.debug(
            "!!! "
                + getEndpoint().getID()
                + " data channel open request on SID: "
                + sid
                + " type: "
                + channelType
                + " prio: "
                + priority
                + " reliab: "
                + reliability
                + " label: "
                + label
                + " proto: "
                + protocol);
      }

      if (channels.containsKey(sid)) {
        logger.error("Channel on sid: " + sid + " already exists");
      }

      WebRtcDataStream newChannel = new WebRtcDataStream(sctpSocket, sid, label, true);
      channels.put(sid, newChannel);

      sendOpenChannelAck(sid);

      notifyChannelOpened(newChannel);
    } else {
      logger.error("Unexpected ctrl msg type: " + messageType);
    }
  }

  /** {@inheritDoc} */
  @Override
  protected void onEndpointChanged(Endpoint oldValue, Endpoint newValue) {
    if (oldValue != null) oldValue.setSctpConnection(null);
    if (newValue != null) newValue.setSctpConnection(this);
  }

  /** Implements notification in order to track socket state. */
  @Override
  public synchronized void onSctpNotification(SctpSocket socket, SctpNotification notification) {
    if (logger.isDebugEnabled()) {
      logger.debug("socket=" + socket + "; notification=" + notification);
    }
    switch (notification.sn_type) {
      case SctpNotification.SCTP_ASSOC_CHANGE:
        SctpNotification.AssociationChange assocChange =
            (SctpNotification.AssociationChange) notification;

        switch (assocChange.state) {
          case SctpNotification.AssociationChange.SCTP_COMM_UP:
            if (!assocIsUp) {
              boolean wasReady = isReady();

              assocIsUp = true;
              if (isReady() && !wasReady) notifySctpConnectionReady();
            }
            break;

          case SctpNotification.AssociationChange.SCTP_COMM_LOST:
          case SctpNotification.AssociationChange.SCTP_SHUTDOWN_COMP:
          case SctpNotification.AssociationChange.SCTP_CANT_STR_ASSOC:
            try {
              closeStream();
            } catch (IOException e) {
              logger.error("Error closing SCTP socket", e);
            }
            break;
        }
        break;
    }
  }

  /**
   * {@inheritDoc}
   *
   * <p>SCTP input data callback.
   */
  @Override
  public void onSctpPacket(
      byte[] data, int sid, int ssn, int tsn, long ppid, int context, int flags) {
    if (ppid == WEB_RTC_PPID_CTRL) {
      // Channel control PPID
      try {
        onCtrlPacket(data, sid);
      } catch (IOException e) {
        logger.error("IOException when processing ctrl packet", e);
      }
    } else if (ppid == WEB_RTC_PPID_STRING || ppid == WEB_RTC_PPID_BIN) {
      WebRtcDataStream channel;

      synchronized (this) {
        channel = channels.get(sid);
      }

      if (channel == null) {
        logger.error("No channel found for sid: " + sid);
        return;
      }
      if (ppid == WEB_RTC_PPID_STRING) {
        // WebRTC String
        String str;
        String charsetName = "UTF-8";

        try {
          str = new String(data, charsetName);
        } catch (UnsupportedEncodingException uee) {
          logger.error("Unsupported charset encoding/name " + charsetName, uee);
          str = null;
        }
        channel.onStringMsg(str);
      } else {
        // WebRTC Binary
        channel.onBinaryMsg(data);
      }
    } else {
      logger.warn("Got message on unsupported PPID: " + ppid);
    }
  }

  /**
   * Opens new WebRTC data channel using specified parameters.
   *
   * @param type channel type as defined in control protocol description. Use 0 for "reliable".
   * @param prio channel priority. The higher the number, the lower the priority.
   * @param reliab Reliability Parameter<br>
   *     This field is ignored if a reliable channel is used. If a partial reliable channel with
   *     limited number of retransmissions is used, this field specifies the number of
   *     retransmissions. If a partial reliable channel with limited lifetime is used, this field
   *     specifies the maximum lifetime in milliseconds. The following table summarizes this:<br>
   *     </br>
   *     <p>+------------------------------------------------+------------------+ | Channel Type |
   *     Reliability | | | Parameter |
   *     +------------------------------------------------+------------------+ |
   *     DATA_CHANNEL_RELIABLE | Ignored | | DATA_CHANNEL_RELIABLE_UNORDERED | Ignored | |
   *     DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT | Number of RTX | |
   *     DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT_UNORDERED | Number of RTX | |
   *     DATA_CHANNEL_PARTIAL_RELIABLE_TIMED | Lifetime in ms | |
   *     DATA_CHANNEL_PARTIAL_RELIABLE_TIMED_UNORDERED | Lifetime in ms |
   *     +------------------------------------------------+------------------+
   * @param sid SCTP stream id that will be used by new channel (it must not be already used).
   * @param label text label for the channel.
   * @return new instance of <tt>WebRtcDataStream</tt> that represents opened WebRTC data channel.
   * @throws IOException if IO error occurs.
   */
  public synchronized WebRtcDataStream openChannel(
      int type, int prio, long reliab, int sid, String label) throws IOException {
    if (channels.containsKey(sid)) {
      throw new IOException("Channel on sid: " + sid + " already exists");
    }

    // Label Length & Label
    byte[] labelBytes;
    int labelByteLength;

    if (label == null) {
      labelBytes = null;
      labelByteLength = 0;
    } else {
      labelBytes = label.getBytes("UTF-8");
      labelByteLength = labelBytes.length;
      if (labelByteLength > 0xFFFF) labelByteLength = 0xFFFF;
    }

    // Protocol Length & Protocol
    String protocol = WEBRTC_DATA_CHANNEL_PROTOCOL;
    byte[] protocolBytes;
    int protocolByteLength;

    if (protocol == null) {
      protocolBytes = null;
      protocolByteLength = 0;
    } else {
      protocolBytes = protocol.getBytes("UTF-8");
      protocolByteLength = protocolBytes.length;
      if (protocolByteLength > 0xFFFF) protocolByteLength = 0xFFFF;
    }

    ByteBuffer packet = ByteBuffer.allocate(12 + labelByteLength + protocolByteLength);

    // Message open new channel on current sid
    // Message Type
    packet.put((byte) MSG_OPEN_CHANNEL);
    // Channel Type
    packet.put((byte) type);
    // Priority
    packet.putShort((short) prio);
    // Reliability Parameter
    packet.putInt((int) reliab);
    // Label Length
    packet.putShort((short) labelByteLength);
    // Protocol Length
    packet.putShort((short) protocolByteLength);
    // Label
    if (labelByteLength != 0) {
      packet.put(labelBytes, 0, labelByteLength);
    }
    // Protocol
    if (protocolByteLength != 0) {
      packet.put(protocolBytes, 0, protocolByteLength);
    }

    int sentCount = sctpSocket.send(packet.array(), true, sid, WEB_RTC_PPID_CTRL);

    if (sentCount != packet.capacity()) {
      throw new IOException("Failed to open new chanel on sid: " + sid);
    }

    WebRtcDataStream channel = new WebRtcDataStream(sctpSocket, sid, label, false);

    channels.put(sid, channel);

    return channel;
  }

  /**
   * Removes <tt>WebRtcDataStreamListener</tt> from the list of listeners.
   *
   * @param listener the <tt>WebRtcDataStreamListener</tt> to be removed from the listeners list.
   */
  public void removeChannelListener(WebRtcDataStreamListener listener) {
    if (listener != null) {
      synchronized (listeners) {
        listeners.remove(listener);
      }
    }
  }

  private void runOnDtlsTransport(StreamConnector connector) throws IOException {
    DtlsControlImpl dtlsControl = (DtlsControlImpl) getTransportManager().getDtlsControl(this);
    DtlsTransformEngine engine = dtlsControl.getTransformEngine();
    final DtlsPacketTransformer transformer = (DtlsPacketTransformer) engine.getRTPTransformer();

    byte[] receiveBuffer = new byte[SCTP_BUFFER_SIZE];

    if (LOG_SCTP_PACKETS) {
      System.setProperty(
          ConfigurationService.PNAME_SC_HOME_DIR_LOCATION, System.getProperty("java.io.tmpdir"));
      System.setProperty(
          ConfigurationService.PNAME_SC_HOME_DIR_NAME, SctpConnection.class.getName());
    }

    synchronized (this) {
      // FIXME local SCTP port is hardcoded in bridge offer SDP (Jitsi
      // Meet)
      sctpSocket = Sctp.createSocket(5000);
      assocIsUp = false;
      acceptedIncomingConnection = false;
    }

    // Implement output network link for SCTP stack on DTLS transport
    sctpSocket.setLink(
        new NetworkLink() {
          @Override
          public void onConnOut(SctpSocket s, byte[] packet) throws IOException {
            if (LOG_SCTP_PACKETS) {
              LibJitsi.getPacketLoggingService()
                  .logPacket(
                      PacketLoggingService.ProtocolName.ICE4J,
                      new byte[] {0, 0, 0, (byte) debugId},
                      5000,
                      new byte[] {0, 0, 0, (byte) (debugId + 1)},
                      remoteSctpPort,
                      PacketLoggingService.TransportName.UDP,
                      true,
                      packet);
            }

            // Send through DTLS transport
            transformer.sendApplicationData(packet, 0, packet.length);
          }
        });

    if (logger.isDebugEnabled()) {
      logger.debug("Connecting SCTP to port: " + remoteSctpPort + " to " + getEndpoint().getID());
    }

    sctpSocket.setNotificationListener(this);
    sctpSocket.listen();

    // FIXME manage threads
    threadPool.execute(
        new Runnable() {
          @Override
          public void run() {
            SctpSocket sctpSocket = null;
            try {
              // sctpSocket is set to null on close
              sctpSocket = SctpConnection.this.sctpSocket;
              while (sctpSocket != null) {
                if (sctpSocket.accept()) {
                  acceptedIncomingConnection = true;
                  break;
                }
                Thread.sleep(100);
                sctpSocket = SctpConnection.this.sctpSocket;
              }
              if (isReady()) {
                notifySctpConnectionReady();
              }
            } catch (Exception e) {
              logger.error("Error accepting SCTP connection", e);
            }

            if (sctpSocket == null && logger.isInfoEnabled()) {
              logger.info(
                  "SctpConnection " + getID() + " closed" + " before SctpSocket accept()-ed.");
            }
          }
        });

    // Notify that from now on SCTP connection is considered functional
    sctpSocket.setDataCallback(this);

    // Setup iceSocket
    DatagramSocket datagramSocket = connector.getDataSocket();
    if (datagramSocket != null) {
      this.iceSocket = new IceUdpSocketWrapper(datagramSocket);
    } else {
      this.iceSocket = new IceTcpSocketWrapper(connector.getDataTCPSocket());
    }

    DatagramPacket rcvPacket = new DatagramPacket(receiveBuffer, 0, receiveBuffer.length);

    // Receive loop, breaks when SCTP socket is closed
    try {
      do {
        iceSocket.receive(rcvPacket);

        RawPacket raw =
            new RawPacket(rcvPacket.getData(), rcvPacket.getOffset(), rcvPacket.getLength());

        raw = transformer.reverseTransform(raw);
        // Check for app data
        if (raw == null) continue;

        if (LOG_SCTP_PACKETS) {
          LibJitsi.getPacketLoggingService()
              .logPacket(
                  PacketLoggingService.ProtocolName.ICE4J,
                  new byte[] {0, 0, 0, (byte) (debugId + 1)},
                  remoteSctpPort,
                  new byte[] {0, 0, 0, (byte) debugId},
                  5000,
                  PacketLoggingService.TransportName.UDP,
                  false,
                  raw.getBuffer(),
                  raw.getOffset(),
                  raw.getLength());
        }

        // Pass network packet to SCTP stack
        sctpSocket.onConnIn(raw.getBuffer(), raw.getOffset(), raw.getLength());
      } while (true);
    } finally {
      // Eventually, close the socket although it should happen from
      // expire().
      synchronized (this) {
        assocIsUp = false;
        acceptedIncomingConnection = false;
        if (sctpSocket != null) {
          sctpSocket.close();
          sctpSocket = null;
        }
      }
    }
  }

  /**
   * Sends acknowledgment for open channel request on given SCTP stream ID.
   *
   * @param sid SCTP stream identifier to be used for sending ack.
   */
  private void sendOpenChannelAck(int sid) throws IOException {
    // Send ACK
    byte[] ack = MSG_CHANNEL_ACK_BYTES;
    int sendAck = sctpSocket.send(ack, true, sid, WEB_RTC_PPID_CTRL);

    if (sendAck != ack.length) {
      logger.error("Failed to send open channel confirmation");
    }
  }

  /**
   * {@inheritDoc}
   *
   * <p>Creates a <tt>TransportManager</tt> instance suitable for an <tt>SctpConnection</tt> (e.g.
   * with 1 component only).
   */
  protected TransportManager createTransportManager(String xmlNamespace) throws IOException {
    if (IceUdpTransportPacketExtension.NAMESPACE.equals(xmlNamespace)) {
      Content content = getContent();
      return new IceUdpTransportManager(
          content.getConference(), isInitiator(), 1 /* num components */, content.getName());
    } else if (RawUdpTransportPacketExtension.NAMESPACE.equals(xmlNamespace)) {
      // TODO: support RawUdp once RawUdpTransportManager is updated
      // return new RawUdpTransportManager(this);
      throw new IllegalArgumentException("Unsupported Jingle transport " + xmlNamespace);
    } else {
      throw new IllegalArgumentException("Unsupported Jingle transport " + xmlNamespace);
    }
  }
}
Ejemplo n.º 3
0
public class ShowPreviewDialog extends SIPCommDialog
    implements ActionListener, ChatLinkClickedListener {
  /** Serial version UID. */
  private static final long serialVersionUID = 1L;

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

  ConfigurationService cfg = GuiActivator.getConfigurationService();

  /** The Ok button. */
  private final JButton okButton;

  /** The cancel button. */
  private final JButton cancelButton;

  /** Checkbox that indicates whether or not to show this dialog next time. */
  private final JCheckBox enableReplacementProposal;

  /** Checkbox that indicates whether or not to show previews automatically */
  private final JCheckBox enableReplacement;

  /** The <tt>ChatConversationPanel</tt> that this dialog is associated with. */
  private final ChatConversationPanel chatPanel;

  /** Mapping between messageID and the string representation of the chat message. */
  private Map<String, String> msgIDToChatString = new ConcurrentHashMap<String, String>();

  /**
   * Mapping between the pair (messageID, link position) and the actual link in the string
   * representation of the chat message.
   */
  private Map<String, String> msgIDandPositionToLink = new ConcurrentHashMap<String, String>();

  /**
   * Mapping between link and replacement for this link that is acquired from it's corresponding
   * <tt>ReplacementService</tt>.
   */
  private Map<String, String> linkToReplacement = new ConcurrentHashMap<String, String>();

  /** The id of the message that is currently associated with this dialog. */
  private String currentMessageID = "";

  /** The position of the link in the current message. */
  private String currentLinkPosition = "";

  /**
   * Creates an instance of <tt>ShowPreviewDialog</tt>
   *
   * @param chatPanel The <tt>ChatConversationPanel</tt> that is associated with this dialog.
   */
  ShowPreviewDialog(final ChatConversationPanel chatPanel) {
    this.chatPanel = chatPanel;

    this.setTitle(
        GuiActivator.getResources().getI18NString("service.gui.SHOW_PREVIEW_DIALOG_TITLE"));
    okButton = new JButton(GuiActivator.getResources().getI18NString("service.gui.OK"));
    cancelButton = new JButton(GuiActivator.getResources().getI18NString("service.gui.CANCEL"));

    JPanel mainPanel = new TransparentPanel();
    mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
    mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
    // mainPanel.setPreferredSize(new Dimension(200, 150));
    this.getContentPane().add(mainPanel);

    JTextPane descriptionMsg = new JTextPane();
    descriptionMsg.setEditable(false);
    descriptionMsg.setOpaque(false);
    descriptionMsg.setText(
        GuiActivator.getResources().getI18NString("service.gui.SHOW_PREVIEW_WARNING_DESCRIPTION"));

    Icon warningIcon = null;
    try {
      warningIcon =
          new ImageIcon(
              ImageIO.read(
                  GuiActivator.getResources().getImageURL("service.gui.icons.WARNING_ICON")));
    } catch (IOException e) {
      logger.debug("failed to load the warning icon");
    }
    JLabel warningSign = new JLabel(warningIcon);

    JPanel warningPanel = new TransparentPanel();
    warningPanel.setLayout(new BoxLayout(warningPanel, BoxLayout.X_AXIS));
    warningPanel.add(warningSign);
    warningPanel.add(Box.createHorizontalStrut(10));
    warningPanel.add(descriptionMsg);

    enableReplacement =
        new JCheckBox(
            GuiActivator.getResources()
                .getI18NString("plugin.chatconfig.replacement.ENABLE_REPLACEMENT_STATUS"));
    enableReplacement.setOpaque(false);
    enableReplacement.setSelected(cfg.getBoolean(ReplacementProperty.REPLACEMENT_ENABLE, true));
    enableReplacementProposal =
        new JCheckBox(
            GuiActivator.getResources()
                .getI18NString("plugin.chatconfig.replacement.ENABLE_REPLACEMENT_PROPOSAL"));
    enableReplacementProposal.setOpaque(false);

    JPanel checkBoxPanel = new TransparentPanel();
    checkBoxPanel.setLayout(new BoxLayout(checkBoxPanel, BoxLayout.Y_AXIS));
    checkBoxPanel.add(enableReplacement);
    checkBoxPanel.add(enableReplacementProposal);

    JPanel buttonsPanel = new TransparentPanel(new FlowLayout(FlowLayout.CENTER));
    buttonsPanel.add(okButton);
    buttonsPanel.add(cancelButton);

    mainPanel.add(warningPanel);
    mainPanel.add(Box.createVerticalStrut(10));
    mainPanel.add(checkBoxPanel);
    mainPanel.add(buttonsPanel);

    okButton.addActionListener(this);
    cancelButton.addActionListener(this);

    this.setPreferredSize(new Dimension(390, 230));
  }

  @Override
  public void actionPerformed(ActionEvent arg0) {
    if (arg0.getSource().equals(okButton)) {
      cfg.setProperty(ReplacementProperty.REPLACEMENT_ENABLE, enableReplacement.isSelected());
      cfg.setProperty(
          ReplacementProperty.REPLACEMENT_PROPOSAL, enableReplacementProposal.isSelected());
      SwingWorker worker =
          new SwingWorker() {
            /**
             * Called on the event dispatching thread (not on the worker thread) after the <code>
             * construct</code> method has returned.
             */
            @Override
            public void finished() {
              String newChatString = (String) get();

              if (newChatString != null) {
                try {
                  Element elem = chatPanel.document.getElement(currentMessageID);
                  chatPanel.document.setOuterHTML(elem, newChatString);
                  msgIDToChatString.put(currentMessageID, newChatString);
                } catch (BadLocationException ex) {
                  logger.error("Could not replace chat message", ex);
                } catch (IOException ex) {
                  logger.error("Could not replace chat message", ex);
                }
              }
            }

            @Override
            protected Object construct() throws Exception {
              String newChatString = msgIDToChatString.get(currentMessageID);
              try {
                String originalLink =
                    msgIDandPositionToLink.get(currentMessageID + "#" + currentLinkPosition);
                String replacementLink = linkToReplacement.get(originalLink);
                String replacement;
                DirectImageReplacementService source =
                    GuiActivator.getDirectImageReplacementSource();
                if (originalLink.equals(replacementLink)
                    && (!source.isDirectImage(originalLink)
                        || source.getImageSize(originalLink) == -1)) {
                  replacement = originalLink;
                } else {
                  replacement =
                      "<IMG HEIGHT=\"90\" WIDTH=\"120\" SRC=\""
                          + replacementLink
                          + "\" BORDER=\"0\" ALT=\""
                          + originalLink
                          + "\"></IMG>";
                }

                String old =
                    originalLink
                        + "</A> <A href=\"jitsi://"
                        + ShowPreviewDialog.this.getClass().getName()
                        + "/SHOWPREVIEW?"
                        + currentMessageID
                        + "#"
                        + currentLinkPosition
                        + "\">"
                        + GuiActivator.getResources().getI18NString("service.gui.SHOW_PREVIEW");

                newChatString = newChatString.replace(old, replacement);
              } catch (Exception ex) {
                logger.error("Could not replace chat message", ex);
              }
              return newChatString;
            }
          };
      worker.start();
      this.setVisible(false);
    } else if (arg0.getSource().equals(cancelButton)) {
      this.setVisible(false);
    }
  }

  @Override
  public void chatLinkClicked(URI url) {
    String action = url.getPath();
    if (action.equals("/SHOWPREVIEW")) {
      enableReplacement.setSelected(cfg.getBoolean(ReplacementProperty.REPLACEMENT_ENABLE, true));
      enableReplacementProposal.setSelected(
          cfg.getBoolean(ReplacementProperty.REPLACEMENT_PROPOSAL, true));

      currentMessageID = url.getQuery();
      currentLinkPosition = url.getFragment();

      this.setVisible(true);
      this.setLocationRelativeTo(chatPanel);
    }
  }

  /**
   * Returns mapping between messageID and the string representation of the chat message.
   *
   * @return mapping between messageID and chat string.
   */
  Map<String, String> getMsgIDToChatString() {
    return msgIDToChatString;
  }

  /**
   * Returns mapping between the pair (messageID, link position) and the actual link in the string
   * representation of the chat message.
   *
   * @return mapping between (messageID, linkPosition) and link.
   */
  Map<String, String> getMsgIDandPositionToLink() {
    return msgIDandPositionToLink;
  }

  /**
   * Returns mapping between link and replacement for this link that was acquired from it's
   * corresponding <tt>ReplacementService</tt>.
   *
   * @return mapping between link and it's corresponding replacement.
   */
  Map<String, String> getLinkToReplacement() {
    return linkToReplacement;
  }
}
Ejemplo n.º 4
0
/**
 * Activator for the swing notification service.
 *
 * @author Symphorien Wanko
 */
public class SwingNotificationActivator implements BundleActivator {
  /** The bundle context in which we started */
  public static BundleContext bundleContext;

  /** A reference to the configuration service. */
  private static ConfigurationService configService;

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

  /** A reference to the resource management service. */
  private static ResourceManagementService resourcesService;

  /**
   * Start the swing notification service
   *
   * @param bc
   * @throws java.lang.Exception
   */
  public void start(BundleContext bc) throws Exception {
    if (logger.isInfoEnabled()) logger.info("Swing Notification ...[  STARTING ]");

    bundleContext = bc;

    PopupMessageHandler handler = null;
    handler = new PopupMessageHandlerSwingImpl();

    getConfigurationService();

    bc.registerService(PopupMessageHandler.class.getName(), handler, null);

    if (logger.isInfoEnabled()) logger.info("Swing Notification ...[REGISTERED]");
  }

  public void stop(BundleContext arg0) throws Exception {}

  /**
   * Returns the <tt>ConfigurationService</tt> obtained from the bundle context.
   *
   * @return the <tt>ConfigurationService</tt> obtained from the bundle context
   */
  public static ConfigurationService getConfigurationService() {
    if (configService == null) {
      ServiceReference configReference =
          bundleContext.getServiceReference(ConfigurationService.class.getName());

      configService = (ConfigurationService) bundleContext.getService(configReference);
    }

    return configService;
  }

  /**
   * Returns the <tt>ResourceManagementService</tt> obtained from the bundle context.
   *
   * @return the <tt>ResourceManagementService</tt> obtained from the bundle context
   */
  public static ResourceManagementService getResources() {
    if (resourcesService == null)
      resourcesService = ResourceManagementServiceUtils.getService(bundleContext);
    return resourcesService;
  }
}
/**
 * Implements the {@link ReplacementService} to provide previews for direct image links.
 *
 * @author Purvesh Sahoo
 * @author Marin Dzhigarov
 */
public class ReplacementServiceDirectImageImpl implements DirectImageReplacementService {
  /** The logger for this class. */
  private static final Logger logger = Logger.getLogger(ReplacementServiceDirectImageImpl.class);

  /** The regex used to match the link in the message. */
  public static final String URL_PATTERN = "https?\\:\\/\\/(www\\.)*.*\\.(?:jpg|png|gif)";

  /** Configuration label shown in the config form. */
  public static final String DIRECT_IMAGE_CONFIG_LABEL = "Direct Image Link";

  /** Source name; also used as property label. */
  public static final String SOURCE_NAME = "DIRECTIMAGE";

  /** Maximum allowed size of the image in bytes. The default size is 2MB. */
  private long imgMaxSize = 2097152;

  /** Configuration property name for maximum allowed size of the image in bytes. */
  private static final String MAX_IMG_SIZE =
      "net.java.sip.communicator.impl.replacement.directimage.MAX_IMG_SIZE";

  /** Constructor for <tt>ReplacementServiceDirectImageImpl</tt>. */
  public ReplacementServiceDirectImageImpl() {
    setMaxImgSizeFromConf();
    logger.trace("Creating a Direct Image Link Source.");
  }

  /**
   * Gets the max allowed size value in bytes from Configuration service and sets the value to
   * <tt>imgMaxSize</tt> if succeed. If the configuration property isn't available or the value
   * can't be parsed correctly the value of <tt>imgMaxSize</tt> isn't changed.
   */
  private void setMaxImgSizeFromConf() {
    ConfigurationService configService = DirectImageActivator.getConfigService();

    if (configService != null) {
      String confImgSizeStr = (String) configService.getProperty(MAX_IMG_SIZE);
      try {
        if (confImgSizeStr != null) {
          imgMaxSize = Long.parseLong(confImgSizeStr);
        } else {
          configService.setProperty(MAX_IMG_SIZE, imgMaxSize);
        }
      } catch (NumberFormatException e) {
        if (logger.isDebugEnabled())
          logger.debug(
              "Failed to parse max image size: " + confImgSizeStr + ". Going for default.");
      }
    }
  }

  /**
   * Returns the thumbnail URL of the image link provided.
   *
   * @param sourceString the original image link.
   * @return the thumbnail image link; the original link in case of no match.
   */
  public String getReplacement(String sourceString) {
    return sourceString;
  }

  /**
   * Returns the source name
   *
   * @return the source name
   */
  public String getSourceName() {
    return SOURCE_NAME;
  }

  /**
   * Returns the pattern of the source
   *
   * @return the source pattern
   */
  public String getPattern() {
    return URL_PATTERN;
  }

  @Override
  /**
   * Returns the size of the image in bytes.
   *
   * @param sourceString the image link.
   * @return the file size in bytes of the image link provided; -1 if the size isn't available or
   *     exceeds the max allowed image size.
   */
  public int getImageSize(String sourceString) {
    int length = -1;
    try {

      URL url = new URL(sourceString);
      String protocol = url.getProtocol();
      if (protocol.equals("http") || protocol.equals("https")) {
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        length = connection.getContentLength();
        connection.disconnect();
      } else if (protocol.equals("ftp")) {
        FTPUtils ftp = new FTPUtils(sourceString);
        length = ftp.getSize();
        ftp.disconnect();
      }

      if (length > imgMaxSize) {
        length = -1;
      }
    } catch (Exception e) {
      logger.debug("Failed to get the length of the image in bytes", e);
    }
    return length;
  }

  /**
   * Returns true if the content type of the resource pointed by sourceString is an image.
   *
   * @param sourceString the original image link.
   * @return true if the content type of the resource pointed by sourceString is an image.
   */
  @Override
  public boolean isDirectImage(String sourceString) {
    boolean isDirectImage = false;
    try {
      URL url = new URL(sourceString);
      String protocol = url.getProtocol();
      if (protocol.equals("http") || protocol.equals("https")) {
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        isDirectImage = connection.getContentType().contains("image");
        connection.disconnect();
      } else if (protocol.equals("ftp")) {
        if (sourceString.endsWith(".png")
            || sourceString.endsWith(".jpg")
            || sourceString.endsWith(".gif")) {
          isDirectImage = true;
        }
      }
    } catch (Exception e) {
      logger.debug("Failed to retrieve content type information for" + sourceString, e);
    }
    return isDirectImage;
  }
}
Ejemplo n.º 6
0
/**
 * Implements {@link PacketTransformer} for DTLS-SRTP. It's capable of working in pure DTLS mode if
 * appropriate flag was set in <tt>DtlsControlImpl</tt>.
 *
 * @author Lyubomir Marinov
 */
public class DtlsPacketTransformer extends SinglePacketTransformer {
  private static final long CONNECT_RETRY_INTERVAL = 500;

  /**
   * The maximum number of times that {@link #runInConnectThread(DTLSProtocol, TlsPeer,
   * DatagramTransport)} is to retry the invocations of {@link DTLSClientProtocol#connect(TlsClient,
   * DatagramTransport)} and {@link DTLSServerProtocol#accept(TlsServer, DatagramTransport)} in
   * anticipation of a successful connection.
   */
  private static final int CONNECT_TRIES = 3;

  /**
   * The indicator which determines whether unencrypted packets sent or received through
   * <tt>DtlsPacketTransformer</tt> are to be dropped. The default value is <tt>false</tt>.
   *
   * @see #DROP_UNENCRYPTED_PKTS_PNAME
   */
  private static final boolean DROP_UNENCRYPTED_PKTS;

  /**
   * The name of the <tt>ConfigurationService</tt> and/or <tt>System</tt> property which indicates
   * whether unencrypted packets sent or received through <tt>DtlsPacketTransformer</tt> are to be
   * dropped. The default value is <tt>false</tt>.
   */
  private static final String DROP_UNENCRYPTED_PKTS_PNAME =
      DtlsPacketTransformer.class.getName() + ".dropUnencryptedPkts";

  /** The length of the header of a DTLS record. */
  static final int DTLS_RECORD_HEADER_LENGTH = 13;

  /**
   * The number of milliseconds a <tt>DtlsPacketTransform</tt> is to wait on its {@link
   * #dtlsTransport} in order to receive a packet.
   */
  private static final int DTLS_TRANSPORT_RECEIVE_WAITMILLIS = -1;

  /**
   * The <tt>Logger</tt> used by the <tt>DtlsPacketTransformer</tt> class and its instances to print
   * debug information.
   */
  private static final Logger logger = Logger.getLogger(DtlsPacketTransformer.class);

  static {
    ConfigurationService cfg = LibJitsi.getConfigurationService();
    boolean dropUnencryptedPkts = false;

    if (cfg == null) {
      String s = System.getProperty(DROP_UNENCRYPTED_PKTS_PNAME);

      if (s != null) dropUnencryptedPkts = Boolean.parseBoolean(s);
    } else {
      dropUnencryptedPkts = cfg.getBoolean(DROP_UNENCRYPTED_PKTS_PNAME, dropUnencryptedPkts);
    }
    DROP_UNENCRYPTED_PKTS = dropUnencryptedPkts;
  }

  /**
   * Determines whether a specific array of <tt>byte</tt>s appears to contain a DTLS record.
   *
   * @param buf the array of <tt>byte</tt>s to be analyzed
   * @param off the offset within <tt>buf</tt> at which the analysis is to start
   * @param len the number of bytes within <tt>buf</tt> starting at <tt>off</tt> to be analyzed
   * @return <tt>true</tt> if the specified <tt>buf</tt> appears to contain a DTLS record
   */
  public static boolean isDtlsRecord(byte[] buf, int off, int len) {
    boolean b = false;

    if (len >= DTLS_RECORD_HEADER_LENGTH) {
      short type = TlsUtils.readUint8(buf, off);

      switch (type) {
        case ContentType.alert:
        case ContentType.application_data:
        case ContentType.change_cipher_spec:
        case ContentType.handshake:
          int major = buf[off + 1] & 0xff;
          int minor = buf[off + 2] & 0xff;
          ProtocolVersion version = null;

          if ((major == ProtocolVersion.DTLSv10.getMajorVersion())
              && (minor == ProtocolVersion.DTLSv10.getMinorVersion())) {
            version = ProtocolVersion.DTLSv10;
          }
          if ((version == null)
              && (major == ProtocolVersion.DTLSv12.getMajorVersion())
              && (minor == ProtocolVersion.DTLSv12.getMinorVersion())) {
            version = ProtocolVersion.DTLSv12;
          }
          if (version != null) {
            int length = TlsUtils.readUint16(buf, off + 11);

            if (DTLS_RECORD_HEADER_LENGTH + length <= len) b = true;
          }
          break;
        default:
          // Unless a new ContentType has been defined by the Bouncy
          // Castle Crypto APIs, the specified buf does not represent a
          // DTLS record.
          break;
      }
    }
    return b;
  }

  /** The ID of the component which this instance works for/is associated with. */
  private final int componentID;

  /** The <tt>RTPConnector</tt> which uses this <tt>PacketTransformer</tt>. */
  private AbstractRTPConnector connector;

  /** The background <tt>Thread</tt> which initializes {@link #dtlsTransport}. */
  private Thread connectThread;

  /**
   * The <tt>DatagramTransport</tt> implementation which adapts {@link #connector} and this
   * <tt>PacketTransformer</tt> to the terms of the Bouncy Castle Crypto APIs.
   */
  private DatagramTransportImpl datagramTransport;

  /**
   * The <tt>DTLSTransport</tt> through which the actual packet transformations are being performed
   * by this instance.
   */
  private DTLSTransport dtlsTransport;

  /** The <tt>MediaType</tt> of the stream which this instance works for/is associated with. */
  private MediaType mediaType;

  /**
   * Whether rtcp-mux is in use.
   *
   * <p>If enabled, and this is the transformer for RTCP, it will not establish a DTLS session on
   * its own, but rather wait for the RTP transformer to do so, and reuse it to initialize the SRTP
   * transformer.
   */
  private boolean rtcpmux = false;

  /**
   * The value of the <tt>setup</tt> SDP attribute defined by RFC 4145 &quot;TCP-Based Media
   * Transport in the Session Description Protocol (SDP)&quot; which determines whether this
   * instance acts as a DTLS client or a DTLS server.
   */
  private DtlsControl.Setup setup;

  /** The {@code SRTPTransformer} (to be) used by this instance. */
  private SinglePacketTransformer _srtpTransformer;

  /**
   * The indicator which determines whether the <tt>TlsPeer</tt> employed by this
   * <tt>PacketTransformer</tt> has raised an <tt>AlertDescription.close_notify</tt>
   * <tt>AlertLevel.warning</tt> i.e. the remote DTLS peer has closed the write side of the
   * connection.
   */
  private boolean tlsPeerHasRaisedCloseNotifyWarning;

  /** The <tt>TransformEngine</tt> which has initialized this instance. */
  private final DtlsTransformEngine transformEngine;

  /**
   * Initializes a new <tt>DtlsPacketTransformer</tt> instance.
   *
   * @param transformEngine the <tt>TransformEngine</tt> which is initializing the new instance
   * @param componentID the ID of the component for which the new instance is to work
   */
  public DtlsPacketTransformer(DtlsTransformEngine transformEngine, int componentID) {
    this.transformEngine = transformEngine;
    this.componentID = componentID;
  }

  /** {@inheritDoc} */
  @Override
  public synchronized void close() {
    // SrtpControl.start(MediaType) starts its associated TransformEngine.
    // We will use that mediaType to signal the normal stop then as well
    // i.e. we will call setMediaType(null) first.
    setMediaType(null);
    setConnector(null);
  }

  /**
   * Closes {@link #datagramTransport} if it is non-<tt>null</tt> and logs and swallows any
   * <tt>IOException</tt>.
   */
  private void closeDatagramTransport() {
    if (datagramTransport != null) {
      try {
        datagramTransport.close();
      } catch (IOException ioe) {
        // DatagramTransportImpl has no reason to fail because it is
        // merely an adapter of #connector and this PacketTransformer to
        // the terms of the Bouncy Castle Crypto API.
        logger.error("Failed to (properly) close " + datagramTransport.getClass(), ioe);
      }
      datagramTransport = null;
    }
  }

  /**
   * Determines whether {@link #runInConnectThread(DTLSProtocol, TlsPeer, DatagramTransport)} is to
   * try to establish a DTLS connection.
   *
   * @param i the number of tries remaining after the current one
   * @param datagramTransport
   * @return <tt>true</tt> to try to establish a DTLS connection; otherwise, <tt>false</tt>
   */
  private boolean enterRunInConnectThreadLoop(int i, DatagramTransport datagramTransport) {
    if (i < 0 || i > CONNECT_TRIES) {
      return false;
    } else {
      Thread currentThread = Thread.currentThread();

      synchronized (this) {
        if (i > 0 && i < CONNECT_TRIES - 1) {
          boolean interrupted = false;

          try {
            wait(CONNECT_RETRY_INTERVAL);
          } catch (InterruptedException ie) {
            interrupted = true;
          }
          if (interrupted) currentThread.interrupt();
        }

        return currentThread.equals(this.connectThread)
            && datagramTransport.equals(this.datagramTransport);
      }
    }
  }

  /**
   * Gets the <tt>DtlsControl</tt> implementation associated with this instance.
   *
   * @return the <tt>DtlsControl</tt> implementation associated with this instance
   */
  DtlsControlImpl getDtlsControl() {
    return getTransformEngine().getDtlsControl();
  }

  /**
   * Gets the <tt>TransformEngine</tt> which has initialized this instance.
   *
   * @return the <tt>TransformEngine</tt> which has initialized this instance
   */
  DtlsTransformEngine getTransformEngine() {
    return transformEngine;
  }

  /**
   * Handles a specific <tt>IOException</tt> which was thrown during the execution of {@link
   * #runInConnectThread(DTLSProtocol, TlsPeer, DatagramTransport)} while trying to establish a DTLS
   * connection
   *
   * @param ioe the <tt>IOException</tt> to handle
   * @param msg the human-readable message to log about the specified <tt>ioe</tt>
   * @param i the number of tries remaining after the current one
   * @return <tt>true</tt> if the specified <tt>ioe</tt> was successfully handled; <tt>false</tt>,
   *     otherwise
   */
  private boolean handleRunInConnectThreadException(IOException ioe, String msg, int i) {
    // SrtpControl.start(MediaType) starts its associated TransformEngine.
    // We will use that mediaType to signal the normal stop then as well
    // i.e. we will ignore exception after the procedure to stop this
    // PacketTransformer has begun.
    if (mediaType == null) return false;

    if (ioe instanceof TlsFatalAlert) {
      TlsFatalAlert tfa = (TlsFatalAlert) ioe;
      short alertDescription = tfa.getAlertDescription();

      if (alertDescription == AlertDescription.unexpected_message) {
        msg += " Received fatal unexpected message.";
        if (i == 0
            || !Thread.currentThread().equals(connectThread)
            || connector == null
            || mediaType == null) {
          msg += " Giving up after " + (CONNECT_TRIES - i) + " retries.";
        } else {
          msg += " Will retry.";
          logger.error(msg, ioe);

          return true;
        }
      } else {
        msg += " Received fatal alert " + alertDescription + ".";
      }
    }

    logger.error(msg, ioe);
    return false;
  }

  /**
   * Tries to initialize {@link #_srtpTransformer} by using the <tt>DtlsPacketTransformer</tt> for
   * RTP.
   *
   * @return the (possibly updated) value of {@link #_srtpTransformer}.
   */
  private SinglePacketTransformer initializeSRTCPTransformerFromRtp() {
    DtlsPacketTransformer rtpTransformer =
        (DtlsPacketTransformer) getTransformEngine().getRTPTransformer();

    // Prevent recursion (that is pretty much impossible to ever happen).
    if (rtpTransformer != this) {
      PacketTransformer srtpTransformer = rtpTransformer.waitInitializeAndGetSRTPTransformer();

      if (srtpTransformer != null && srtpTransformer instanceof SRTPTransformer) {
        synchronized (this) {
          if (_srtpTransformer == null) {
            _srtpTransformer = new SRTCPTransformer((SRTPTransformer) srtpTransformer);
            // For the sake of completeness, we notify whenever we
            // assign to _srtpTransformer.
            notifyAll();
          }
        }
      }
    }

    return _srtpTransformer;
  }

  /**
   * Initializes a new <tt>SRTPTransformer</tt> instance with a specific (negotiated)
   * <tt>SRTPProtectionProfile</tt> and the keying material specified by a specific
   * <tt>TlsContext</tt>.
   *
   * @param srtpProtectionProfile the (negotiated) <tt>SRTPProtectionProfile</tt> to initialize the
   *     new instance with
   * @param tlsContext the <tt>TlsContext</tt> which represents the keying material
   * @return a new <tt>SRTPTransformer</tt> instance initialized with <tt>srtpProtectionProfile</tt>
   *     and <tt>tlsContext</tt>
   */
  private SinglePacketTransformer initializeSRTPTransformer(
      int srtpProtectionProfile, TlsContext tlsContext) {
    boolean rtcp;

    switch (componentID) {
      case Component.RTCP:
        rtcp = true;
        break;
      case Component.RTP:
        rtcp = false;
        break;
      default:
        throw new IllegalStateException("componentID");
    }

    int cipher_key_length;
    int cipher_salt_length;
    int cipher;
    int auth_function;
    int auth_key_length;
    int RTCP_auth_tag_length, RTP_auth_tag_length;

    switch (srtpProtectionProfile) {
      case SRTPProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_32:
        cipher_key_length = 128 / 8;
        cipher_salt_length = 112 / 8;
        cipher = SRTPPolicy.AESCM_ENCRYPTION;
        auth_function = SRTPPolicy.HMACSHA1_AUTHENTICATION;
        auth_key_length = 160 / 8;
        RTCP_auth_tag_length = 80 / 8;
        RTP_auth_tag_length = 32 / 8;
        break;
      case SRTPProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_80:
        cipher_key_length = 128 / 8;
        cipher_salt_length = 112 / 8;
        cipher = SRTPPolicy.AESCM_ENCRYPTION;
        auth_function = SRTPPolicy.HMACSHA1_AUTHENTICATION;
        auth_key_length = 160 / 8;
        RTCP_auth_tag_length = RTP_auth_tag_length = 80 / 8;
        break;
      case SRTPProtectionProfile.SRTP_NULL_HMAC_SHA1_32:
        cipher_key_length = 0;
        cipher_salt_length = 0;
        cipher = SRTPPolicy.NULL_ENCRYPTION;
        auth_function = SRTPPolicy.HMACSHA1_AUTHENTICATION;
        auth_key_length = 160 / 8;
        RTCP_auth_tag_length = 80 / 8;
        RTP_auth_tag_length = 32 / 8;
        break;
      case SRTPProtectionProfile.SRTP_NULL_HMAC_SHA1_80:
        cipher_key_length = 0;
        cipher_salt_length = 0;
        cipher = SRTPPolicy.NULL_ENCRYPTION;
        auth_function = SRTPPolicy.HMACSHA1_AUTHENTICATION;
        auth_key_length = 160 / 8;
        RTCP_auth_tag_length = RTP_auth_tag_length = 80 / 8;
        break;
      default:
        throw new IllegalArgumentException("srtpProtectionProfile");
    }

    byte[] keyingMaterial =
        tlsContext.exportKeyingMaterial(
            ExporterLabel.dtls_srtp, null, 2 * (cipher_key_length + cipher_salt_length));
    byte[] client_write_SRTP_master_key = new byte[cipher_key_length];
    byte[] server_write_SRTP_master_key = new byte[cipher_key_length];
    byte[] client_write_SRTP_master_salt = new byte[cipher_salt_length];
    byte[] server_write_SRTP_master_salt = new byte[cipher_salt_length];
    byte[][] keyingMaterialValues = {
      client_write_SRTP_master_key,
      server_write_SRTP_master_key,
      client_write_SRTP_master_salt,
      server_write_SRTP_master_salt
    };

    for (int i = 0, keyingMaterialOffset = 0; i < keyingMaterialValues.length; i++) {
      byte[] keyingMaterialValue = keyingMaterialValues[i];

      System.arraycopy(
          keyingMaterial, keyingMaterialOffset, keyingMaterialValue, 0, keyingMaterialValue.length);
      keyingMaterialOffset += keyingMaterialValue.length;
    }

    SRTPPolicy srtcpPolicy =
        new SRTPPolicy(
            cipher,
            cipher_key_length,
            auth_function,
            auth_key_length,
            RTCP_auth_tag_length,
            cipher_salt_length);
    SRTPPolicy srtpPolicy =
        new SRTPPolicy(
            cipher,
            cipher_key_length,
            auth_function,
            auth_key_length,
            RTP_auth_tag_length,
            cipher_salt_length);
    SRTPContextFactory clientSRTPContextFactory =
        new SRTPContextFactory(
            /* sender */ tlsContext instanceof TlsClientContext,
            client_write_SRTP_master_key,
            client_write_SRTP_master_salt,
            srtpPolicy,
            srtcpPolicy);
    SRTPContextFactory serverSRTPContextFactory =
        new SRTPContextFactory(
            /* sender */ tlsContext instanceof TlsServerContext,
            server_write_SRTP_master_key,
            server_write_SRTP_master_salt,
            srtpPolicy,
            srtcpPolicy);
    SRTPContextFactory forwardSRTPContextFactory;
    SRTPContextFactory reverseSRTPContextFactory;

    if (tlsContext instanceof TlsClientContext) {
      forwardSRTPContextFactory = clientSRTPContextFactory;
      reverseSRTPContextFactory = serverSRTPContextFactory;
    } else if (tlsContext instanceof TlsServerContext) {
      forwardSRTPContextFactory = serverSRTPContextFactory;
      reverseSRTPContextFactory = clientSRTPContextFactory;
    } else {
      throw new IllegalArgumentException("tlsContext");
    }

    SinglePacketTransformer srtpTransformer;

    if (rtcp) {
      srtpTransformer = new SRTCPTransformer(forwardSRTPContextFactory, reverseSRTPContextFactory);
    } else {
      srtpTransformer = new SRTPTransformer(forwardSRTPContextFactory, reverseSRTPContextFactory);
    }
    return srtpTransformer;
  }

  /**
   * Notifies this instance that the DTLS record layer associated with a specific <tt>TlsPeer</tt>
   * has raised an alert.
   *
   * @param tlsPeer the <tt>TlsPeer</tt> whose associated DTLS record layer has raised an alert
   * @param alertLevel {@link AlertLevel}
   * @param alertDescription {@link AlertDescription}
   * @param message a human-readable message explaining what caused the alert. May be <tt>null</tt>.
   * @param cause the exception that caused the alert to be raised. May be <tt>null</tt>.
   */
  void notifyAlertRaised(
      TlsPeer tlsPeer, short alertLevel, short alertDescription, String message, Exception cause) {
    if (AlertLevel.warning == alertLevel && AlertDescription.close_notify == alertDescription) {
      tlsPeerHasRaisedCloseNotifyWarning = true;
    }
  }

  /** {@inheritDoc} */
  @Override
  public RawPacket reverseTransform(RawPacket pkt) {
    byte[] buf = pkt.getBuffer();
    int off = pkt.getOffset();
    int len = pkt.getLength();

    if (isDtlsRecord(buf, off, len)) {
      if (rtcpmux && Component.RTCP == componentID) {
        // This should never happen.
        logger.warn(
            "Dropping a DTLS record, because it was received on the"
                + " RTCP channel while rtcpmux is in use.");
        return null;
      }

      boolean receive;

      synchronized (this) {
        if (datagramTransport == null) {
          receive = false;
        } else {
          datagramTransport.queueReceive(buf, off, len);
          receive = true;
        }
      }
      if (receive) {
        DTLSTransport dtlsTransport = this.dtlsTransport;

        if (dtlsTransport == null) {
          // The specified pkt looks like a DTLS record and it has
          // been consumed for the purposes of the secure channel
          // represented by this PacketTransformer.
          pkt = null;
        } else {
          try {
            int receiveLimit = dtlsTransport.getReceiveLimit();
            int delta = receiveLimit - len;

            if (delta > 0) {
              pkt.grow(delta);
              buf = pkt.getBuffer();
              off = pkt.getOffset();
              len = pkt.getLength();
            } else if (delta < 0) {
              pkt.shrink(-delta);
              buf = pkt.getBuffer();
              off = pkt.getOffset();
              len = pkt.getLength();
            }

            int received = dtlsTransport.receive(buf, off, len, DTLS_TRANSPORT_RECEIVE_WAITMILLIS);

            if (received <= 0) {
              // No application data was decoded.
              pkt = null;
            } else {
              delta = len - received;
              if (delta > 0) pkt.shrink(delta);
            }
          } catch (IOException ioe) {
            pkt = null;
            // SrtpControl.start(MediaType) starts its associated
            // TransformEngine. We will use that mediaType to signal
            // the normal stop then as well i.e. we will ignore
            // exception after the procedure to stop this
            // PacketTransformer has begun.
            if (mediaType != null && !tlsPeerHasRaisedCloseNotifyWarning) {
              logger.error("Failed to decode a DTLS record!", ioe);
            }
          }
        }
      } else {
        // The specified pkt looks like a DTLS record but it is
        // unexpected in the current state of the secure channel
        // represented by this PacketTransformer. This PacketTransformer
        // has not been started (successfully) or has been closed.
        pkt = null;
      }
    } else if (transformEngine.isSrtpDisabled()) {
      // In pure DTLS mode only DTLS records pass through.
      pkt = null;
    } else {
      // DTLS-SRTP has not been initialized yet or has failed to
      // initialize.
      SinglePacketTransformer srtpTransformer = waitInitializeAndGetSRTPTransformer();

      if (srtpTransformer != null) pkt = srtpTransformer.reverseTransform(pkt);
      else if (DROP_UNENCRYPTED_PKTS) pkt = null;
      // XXX Else, it is our explicit policy to let the received packet
      // pass through and rely on the SrtpListener to notify the user that
      // the session is not secured.
    }
    return pkt;
  }

  /**
   * Runs in {@link #connectThread} to initialize {@link #dtlsTransport}.
   *
   * @param dtlsProtocol
   * @param tlsPeer
   * @param datagramTransport
   */
  private void runInConnectThread(
      DTLSProtocol dtlsProtocol, TlsPeer tlsPeer, DatagramTransport datagramTransport) {
    DTLSTransport dtlsTransport = null;
    final boolean srtp = !transformEngine.isSrtpDisabled();
    int srtpProtectionProfile = 0;
    TlsContext tlsContext = null;

    // DTLS client
    if (dtlsProtocol instanceof DTLSClientProtocol) {
      DTLSClientProtocol dtlsClientProtocol = (DTLSClientProtocol) dtlsProtocol;
      TlsClientImpl tlsClient = (TlsClientImpl) tlsPeer;

      for (int i = CONNECT_TRIES - 1; i >= 0; i--) {
        if (!enterRunInConnectThreadLoop(i, datagramTransport)) break;
        try {
          dtlsTransport = dtlsClientProtocol.connect(tlsClient, datagramTransport);
          break;
        } catch (IOException ioe) {
          if (!handleRunInConnectThreadException(
              ioe, "Failed to connect this DTLS client to a DTLS" + " server!", i)) {
            break;
          }
        }
      }
      if (dtlsTransport != null && srtp) {
        srtpProtectionProfile = tlsClient.getChosenProtectionProfile();
        tlsContext = tlsClient.getContext();
      }
    }
    // DTLS server
    else if (dtlsProtocol instanceof DTLSServerProtocol) {
      DTLSServerProtocol dtlsServerProtocol = (DTLSServerProtocol) dtlsProtocol;
      TlsServerImpl tlsServer = (TlsServerImpl) tlsPeer;

      for (int i = CONNECT_TRIES - 1; i >= 0; i--) {
        if (!enterRunInConnectThreadLoop(i, datagramTransport)) break;
        try {
          dtlsTransport = dtlsServerProtocol.accept(tlsServer, datagramTransport);
          break;
        } catch (IOException ioe) {
          if (!handleRunInConnectThreadException(
              ioe, "Failed to accept a connection from a DTLS client!", i)) {
            break;
          }
        }
      }
      if (dtlsTransport != null && srtp) {
        srtpProtectionProfile = tlsServer.getChosenProtectionProfile();
        tlsContext = tlsServer.getContext();
      }
    } else {
      // It MUST be either a DTLS client or a DTLS server.
      throw new IllegalStateException("dtlsProtocol");
    }

    SinglePacketTransformer srtpTransformer =
        (dtlsTransport == null || !srtp)
            ? null
            : initializeSRTPTransformer(srtpProtectionProfile, tlsContext);
    boolean closeSRTPTransformer;

    synchronized (this) {
      if (Thread.currentThread().equals(this.connectThread)
          && datagramTransport.equals(this.datagramTransport)) {
        this.dtlsTransport = dtlsTransport;
        _srtpTransformer = srtpTransformer;
        notifyAll();
      }
      closeSRTPTransformer = (_srtpTransformer != srtpTransformer);
    }
    if (closeSRTPTransformer && srtpTransformer != null) srtpTransformer.close();
  }

  /**
   * Sends the data contained in a specific byte array as application data through the DTLS
   * connection of this <tt>DtlsPacketTransformer</tt>.
   *
   * @param buf the byte array containing data to send.
   * @param off the offset in <tt>buf</tt> where the data begins.
   * @param len the length of data to send.
   */
  public void sendApplicationData(byte[] buf, int off, int len) {
    DTLSTransport dtlsTransport = this.dtlsTransport;
    Throwable throwable = null;

    if (dtlsTransport != null) {
      try {
        dtlsTransport.send(buf, off, len);
      } catch (IOException ioe) {
        throwable = ioe;
      }
    } else {
      throwable = new NullPointerException("dtlsTransport");
    }
    if (throwable != null) {
      // SrtpControl.start(MediaType) starts its associated
      // TransformEngine. We will use that mediaType to signal the normal
      // stop then as well i.e. we will ignore exception after the
      // procedure to stop this PacketTransformer has begun.
      if (mediaType != null && !tlsPeerHasRaisedCloseNotifyWarning) {
        logger.error("Failed to send application data over DTLS transport: ", throwable);
      }
    }
  }

  /**
   * Sets the <tt>RTPConnector</tt> which is to use or uses this <tt>PacketTransformer</tt>.
   *
   * @param connector the <tt>RTPConnector</tt> which is to use or uses this
   *     <tt>PacketTransformer</tt>
   */
  void setConnector(AbstractRTPConnector connector) {
    if (this.connector != connector) {
      this.connector = connector;

      DatagramTransportImpl datagramTransport = this.datagramTransport;

      if (datagramTransport != null) datagramTransport.setConnector(connector);
    }
  }

  /**
   * Sets the <tt>MediaType</tt> of the stream which this instance is to work for/be associated
   * with.
   *
   * @param mediaType the <tt>MediaType</tt> of the stream which this instance is to work for/be
   *     associated with
   */
  synchronized void setMediaType(MediaType mediaType) {
    if (this.mediaType != mediaType) {
      MediaType oldValue = this.mediaType;

      this.mediaType = mediaType;

      if (oldValue != null) stop();
      if (this.mediaType != null) start();
    }
  }

  /**
   * Enables/disables rtcp-mux.
   *
   * @param rtcpmux whether to enable or disable.
   */
  void setRtcpmux(boolean rtcpmux) {
    this.rtcpmux = rtcpmux;
  }

  /**
   * Sets the DTLS protocol according to which this <tt>DtlsPacketTransformer</tt> is to act either
   * as a DTLS server or a DTLS client.
   *
   * @param setup the value of the <tt>setup</tt> SDP attribute to set on this instance in order to
   *     determine whether this instance is to act as a DTLS client or a DTLS server
   */
  void setSetup(DtlsControl.Setup setup) {
    if (this.setup != setup) this.setup = setup;
  }

  /** Starts this <tt>PacketTransformer</tt>. */
  private synchronized void start() {
    if (this.datagramTransport != null) {
      if (this.connectThread == null && dtlsTransport == null) {
        logger.warn(
            getClass().getName()
                + " has been started but has failed to establish"
                + " the DTLS connection!");
      }
      return;
    }

    if (rtcpmux && Component.RTCP == componentID) {
      // In the case of rtcp-mux, the RTCP transformer does not create
      // a DTLS session. The SRTP context (_srtpTransformer) will be
      // initialized on demand using initializeSRTCPTransformerFromRtp().
      return;
    }

    AbstractRTPConnector connector = this.connector;

    if (connector == null) throw new NullPointerException("connector");

    DtlsControl.Setup setup = this.setup;
    SecureRandom secureRandom = DtlsControlImpl.createSecureRandom();
    final DTLSProtocol dtlsProtocolObj;
    final TlsPeer tlsPeer;

    if (DtlsControl.Setup.ACTIVE.equals(setup)) {
      dtlsProtocolObj = new DTLSClientProtocol(secureRandom);
      tlsPeer = new TlsClientImpl(this);
    } else {
      dtlsProtocolObj = new DTLSServerProtocol(secureRandom);
      tlsPeer = new TlsServerImpl(this);
    }
    tlsPeerHasRaisedCloseNotifyWarning = false;

    final DatagramTransportImpl datagramTransport = new DatagramTransportImpl(componentID);

    datagramTransport.setConnector(connector);

    Thread connectThread =
        new Thread() {
          @Override
          public void run() {
            try {
              runInConnectThread(dtlsProtocolObj, tlsPeer, datagramTransport);
            } finally {
              if (Thread.currentThread().equals(DtlsPacketTransformer.this.connectThread)) {
                DtlsPacketTransformer.this.connectThread = null;
              }
            }
          }
        };

    connectThread.setDaemon(true);
    connectThread.setName(DtlsPacketTransformer.class.getName() + ".connectThread");

    this.connectThread = connectThread;
    this.datagramTransport = datagramTransport;

    boolean started = false;

    try {
      connectThread.start();
      started = true;
    } finally {
      if (!started) {
        if (connectThread.equals(this.connectThread)) this.connectThread = null;
        if (datagramTransport.equals(this.datagramTransport)) this.datagramTransport = null;
      }
    }

    notifyAll();
  }

  /** Stops this <tt>PacketTransformer</tt>. */
  private synchronized void stop() {
    if (connectThread != null) connectThread = null;
    try {
      // The dtlsTransport and _srtpTransformer SHOULD be closed, of
      // course. The datagramTransport MUST be closed.
      if (dtlsTransport != null) {
        try {
          dtlsTransport.close();
        } catch (IOException ioe) {
          logger.error("Failed to (properly) close " + dtlsTransport.getClass(), ioe);
        }
        dtlsTransport = null;
      }
      if (_srtpTransformer != null) {
        _srtpTransformer.close();
        _srtpTransformer = null;
      }
    } finally {
      try {
        closeDatagramTransport();
      } finally {
        notifyAll();
      }
    }
  }

  /** {@inheritDoc} */
  @Override
  public RawPacket transform(RawPacket pkt) {
    byte[] buf = pkt.getBuffer();
    int off = pkt.getOffset();
    int len = pkt.getLength();

    // If the specified pkt represents a DTLS record, then it should pass
    // through this PacketTransformer (e.g. it has been sent through
    // DatagramTransportImpl).
    if (isDtlsRecord(buf, off, len)) return pkt;

    // SRTP
    if (!transformEngine.isSrtpDisabled()) {
      // DTLS-SRTP has not been initialized yet or has failed to
      // initialize.
      SinglePacketTransformer srtpTransformer = waitInitializeAndGetSRTPTransformer();

      if (srtpTransformer != null) pkt = srtpTransformer.transform(pkt);
      else if (DROP_UNENCRYPTED_PKTS) pkt = null;
      // XXX Else, it is our explicit policy to let the received packet
      // pass through and rely on the SrtpListener to notify the user that
      // the session is not secured.
    }
    // Pure/non-SRTP DTLS
    else {
      // The specified pkt will pass through this PacketTransformer only
      // if it gets transformed into a DTLS record.
      pkt = null;

      sendApplicationData(buf, off, len);
    }
    return pkt;
  }

  /**
   * Gets the {@code SRTPTransformer} used by this instance. If {@link #_srtpTransformer} does not
   * exist (yet) and the state of this instance indicates that its initialization is in progess,
   * then blocks until {@code _srtpTransformer} is initialized and returns it.
   *
   * @return the {@code SRTPTransformer} used by this instance
   */
  private SinglePacketTransformer waitInitializeAndGetSRTPTransformer() {
    SinglePacketTransformer srtpTransformer = _srtpTransformer;

    if (srtpTransformer != null) return srtpTransformer;

    if (rtcpmux && Component.RTCP == componentID) return initializeSRTCPTransformerFromRtp();

    // XXX It is our explicit policy to rely on the SrtpListener to notify
    // the user that the session is not secure. Unfortunately, (1) the
    // SrtpListener is not supported by this DTLS SrtpControl implementation
    // and (2) encrypted packets may arrive soon enough to be let through
    // while _srtpTransformer is still initializing. Consequently, we will
    // block and wait for _srtpTransformer to initialize.
    boolean interrupted = false;

    try {
      synchronized (this) {
        do {
          srtpTransformer = _srtpTransformer;
          if (srtpTransformer != null) break; // _srtpTransformer is initialized

          if (connectThread == null) {
            // Though _srtpTransformer is NOT initialized, there is
            // no point in waiting because there is no one to
            // initialize it.
            break;
          }

          try {
            // It does not really matter (enough) how much we wait
            // here because we wait in a loop.
            long timeout = CONNECT_TRIES * CONNECT_RETRY_INTERVAL;

            wait(timeout);
          } catch (InterruptedException ie) {
            interrupted = true;
          }
        } while (true);
      }
    } finally {
      if (interrupted) Thread.currentThread().interrupt();
    }

    return srtpTransformer;
  }
}
Ejemplo n.º 7
0
/**
 * @author Lyubomir Marinov
 * @author Damian Minkov
 * @author Yana Stamcheva
 */
public class MediaConfiguration {
  /** The <tt>Logger</tt> used by the <tt>MediaConfiguration</tt> class for logging output. */
  private static final Logger logger = Logger.getLogger(MediaConfiguration.class);

  /** The <tt>MediaService</tt> implementation used by <tt>MediaConfiguration</tt>. */
  private static final MediaServiceImpl mediaService = NeomediaActivator.getMediaServiceImpl();

  /** The preferred width of all panels. */
  private static final int WIDTH = 350;

  /**
   * Indicates if the Devices settings configuration tab should be disabled, i.e. not visible to the
   * user.
   */
  private static final String DEVICES_DISABLED_PROP =
      "net.java.sip.communicator.impl.neomedia.devicesconfig.DISABLED";

  /**
   * Indicates if the Audio/Video encodings configuration tab should be disabled, i.e. not visible
   * to the user.
   */
  private static final String ENCODINGS_DISABLED_PROP =
      "net.java.sip.communicator.impl.neomedia.encodingsconfig.DISABLED";

  /**
   * Indicates if the Video/More Settings configuration tab should be disabled, i.e. not visible to
   * the user.
   */
  private static final String VIDEO_MORE_SETTINGS_DISABLED_PROP =
      "net.java.sip.communicator.impl.neomedia.videomoresettingsconfig.DISABLED";

  /**
   * Returns the audio configuration panel.
   *
   * @return the audio configuration panel
   */
  public static Component createAudioConfigPanel() {
    return createControls(DeviceConfigurationComboBoxModel.AUDIO);
  }

  /**
   * Returns the video configuration panel.
   *
   * @return the video configuration panel
   */
  public static Component createVideoConfigPanel() {
    return createControls(DeviceConfigurationComboBoxModel.VIDEO);
  }

  private static void createAudioPreview(
      final AudioSystem audioSystem,
      final JComboBox comboBox,
      final SoundLevelIndicator soundLevelIndicator) {
    final ActionListener captureComboActionListener =
        new ActionListener() {
          private final SimpleAudioLevelListener audioLevelListener =
              new SimpleAudioLevelListener() {
                public void audioLevelChanged(int level) {
                  soundLevelIndicator.updateSoundLevel(level);
                }
              };

          private AudioMediaDeviceSession deviceSession;

          private final BufferTransferHandler transferHandler =
              new BufferTransferHandler() {
                public void transferData(PushBufferStream stream) {
                  try {
                    stream.read(transferHandlerBuffer);
                  } catch (IOException ioe) {
                  }
                }
              };

          private final Buffer transferHandlerBuffer = new Buffer();

          public void actionPerformed(ActionEvent event) {
            setDeviceSession(null);

            CaptureDeviceInfo cdi;

            if (comboBox == null) {
              cdi = soundLevelIndicator.isShowing() ? audioSystem.getCaptureDevice() : null;
            } else {
              Object selectedItem =
                  soundLevelIndicator.isShowing() ? comboBox.getSelectedItem() : null;

              cdi =
                  (selectedItem instanceof DeviceConfigurationComboBoxModel.CaptureDevice)
                      ? ((DeviceConfigurationComboBoxModel.CaptureDevice) selectedItem).info
                      : null;
            }

            if (cdi != null) {
              for (MediaDevice md : mediaService.getDevices(MediaType.AUDIO, MediaUseCase.ANY)) {
                if (md instanceof AudioMediaDeviceImpl) {
                  AudioMediaDeviceImpl amd = (AudioMediaDeviceImpl) md;

                  if (cdi.equals(amd.getCaptureDeviceInfo())) {
                    try {
                      MediaDeviceSession deviceSession = amd.createSession();
                      boolean setDeviceSession = false;

                      try {
                        if (deviceSession instanceof AudioMediaDeviceSession) {
                          setDeviceSession((AudioMediaDeviceSession) deviceSession);
                          setDeviceSession = true;
                        }
                      } finally {
                        if (!setDeviceSession) deviceSession.close();
                      }
                    } catch (Throwable t) {
                      if (t instanceof ThreadDeath) throw (ThreadDeath) t;
                    }
                    break;
                  }
                }
              }
            }
          }

          private void setDeviceSession(AudioMediaDeviceSession deviceSession) {
            if (this.deviceSession == deviceSession) return;

            if (this.deviceSession != null) {
              try {
                this.deviceSession.close();
              } finally {
                this.deviceSession.setLocalUserAudioLevelListener(null);
                soundLevelIndicator.resetSoundLevel();
              }
            }

            this.deviceSession = deviceSession;

            if (this.deviceSession != null) {
              this.deviceSession.setContentDescriptor(new ContentDescriptor(ContentDescriptor.RAW));
              this.deviceSession.setLocalUserAudioLevelListener(audioLevelListener);
              this.deviceSession.start(MediaDirection.SENDONLY);

              try {
                DataSource dataSource = this.deviceSession.getOutputDataSource();

                dataSource.connect();

                PushBufferStream[] streams = ((PushBufferDataSource) dataSource).getStreams();

                for (PushBufferStream stream : streams) stream.setTransferHandler(transferHandler);

                dataSource.start();
              } catch (Throwable t) {
                if (t instanceof ThreadDeath) throw (ThreadDeath) t;
                else setDeviceSession(null);
              }
            }
          }
        };

    if (comboBox != null) comboBox.addActionListener(captureComboActionListener);

    soundLevelIndicator.addHierarchyListener(
        new HierarchyListener() {
          public void hierarchyChanged(HierarchyEvent event) {
            if ((event.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) {
              SwingUtilities.invokeLater(
                  new Runnable() {
                    public void run() {
                      captureComboActionListener.actionPerformed(null);
                    }
                  });
            }
          }
        });
  }
  /**
   * Creates the UI controls which are to control the details of a specific <tt>AudioSystem</tt>.
   *
   * @param audioSystem the <tt>AudioSystem</tt> for which the UI controls to control its details
   *     are to be created
   * @param container the <tt>JComponent</tt> into which the UI controls which are to control the
   *     details of the specified <tt>audioSystem</tt> are to be added
   */
  public static void createAudioSystemControls(AudioSystem audioSystem, JComponent container) {
    GridBagConstraints constraints = new GridBagConstraints();

    constraints.anchor = GridBagConstraints.NORTHWEST;
    constraints.fill = GridBagConstraints.HORIZONTAL;
    constraints.weighty = 0;

    int audioSystemFeatures = audioSystem.getFeatures();
    boolean featureNotifyAndPlaybackDevices =
        ((audioSystemFeatures & AudioSystem.FEATURE_NOTIFY_AND_PLAYBACK_DEVICES) != 0);

    constraints.gridx = 0;
    constraints.insets = new Insets(3, 0, 3, 3);
    constraints.weightx = 0;

    constraints.gridy = 0;
    container.add(
        new JLabel(getLabelText(DeviceConfigurationComboBoxModel.AUDIO_CAPTURE)), constraints);
    if (featureNotifyAndPlaybackDevices) {
      constraints.gridy = 2;
      container.add(
          new JLabel(getLabelText(DeviceConfigurationComboBoxModel.AUDIO_PLAYBACK)), constraints);
      constraints.gridy = 3;
      container.add(
          new JLabel(getLabelText(DeviceConfigurationComboBoxModel.AUDIO_NOTIFY)), constraints);
    }

    constraints.gridx = 1;
    constraints.insets = new Insets(3, 3, 3, 0);
    constraints.weightx = 1;

    JComboBox captureCombo = null;

    if (featureNotifyAndPlaybackDevices) {
      captureCombo = new JComboBox();
      captureCombo.setEditable(false);
      captureCombo.setModel(
          new DeviceConfigurationComboBoxModel(
              captureCombo,
              mediaService.getDeviceConfiguration(),
              DeviceConfigurationComboBoxModel.AUDIO_CAPTURE));
      constraints.gridy = 0;
      container.add(captureCombo, constraints);
    }

    int anchor = constraints.anchor;
    SoundLevelIndicator capturePreview =
        new SoundLevelIndicator(
            SimpleAudioLevelListener.MIN_LEVEL, SimpleAudioLevelListener.MAX_LEVEL);

    constraints.anchor = GridBagConstraints.CENTER;
    constraints.gridy = (captureCombo == null) ? 0 : 1;
    container.add(capturePreview, constraints);
    constraints.anchor = anchor;

    constraints.gridy = GridBagConstraints.RELATIVE;

    if (featureNotifyAndPlaybackDevices) {
      JComboBox playbackCombo = new JComboBox();

      playbackCombo.setEditable(false);
      playbackCombo.setModel(
          new DeviceConfigurationComboBoxModel(
              captureCombo,
              mediaService.getDeviceConfiguration(),
              DeviceConfigurationComboBoxModel.AUDIO_PLAYBACK));
      container.add(playbackCombo, constraints);

      JComboBox notifyCombo = new JComboBox();

      notifyCombo.setEditable(false);
      notifyCombo.setModel(
          new DeviceConfigurationComboBoxModel(
              captureCombo,
              mediaService.getDeviceConfiguration(),
              DeviceConfigurationComboBoxModel.AUDIO_NOTIFY));
      container.add(notifyCombo, constraints);
    }

    if ((AudioSystem.FEATURE_ECHO_CANCELLATION & audioSystemFeatures) != 0) {
      final SIPCommCheckBox echoCancelCheckBox =
          new SIPCommCheckBox(
              NeomediaActivator.getResources().getI18NString("impl.media.configform.ECHOCANCEL"));

      /*
       * First set the selected one, then add the listener in order to
       * avoid saving the value when using the default one and only
       * showing to user without modification.
       */
      echoCancelCheckBox.setSelected(mediaService.getDeviceConfiguration().isEchoCancel());
      echoCancelCheckBox.addItemListener(
          new ItemListener() {
            public void itemStateChanged(ItemEvent e) {
              mediaService.getDeviceConfiguration().setEchoCancel(echoCancelCheckBox.isSelected());
            }
          });
      container.add(echoCancelCheckBox, constraints);
    }

    if ((AudioSystem.FEATURE_DENOISE & audioSystemFeatures) != 0) {
      final SIPCommCheckBox denoiseCheckBox =
          new SIPCommCheckBox(
              NeomediaActivator.getResources().getI18NString("impl.media.configform.DENOISE"));

      /*
       * First set the selected one, then add the listener in order to
       * avoid saving the value when using the default one and only
       * showing to user without modification.
       */
      denoiseCheckBox.setSelected(mediaService.getDeviceConfiguration().isDenoise());
      denoiseCheckBox.addItemListener(
          new ItemListener() {
            public void itemStateChanged(ItemEvent e) {
              mediaService.getDeviceConfiguration().setDenoise(denoiseCheckBox.isSelected());
            }
          });
      container.add(denoiseCheckBox, constraints);
    }

    createAudioPreview(audioSystem, captureCombo, capturePreview);
  }

  /**
   * Creates basic controls for a type (AUDIO or VIDEO).
   *
   * @param type the type.
   * @return the build Component.
   */
  public static Component createBasicControls(final int type) {
    final JComboBox deviceComboBox = new JComboBox();

    deviceComboBox.setEditable(false);
    deviceComboBox.setModel(
        new DeviceConfigurationComboBoxModel(
            deviceComboBox, mediaService.getDeviceConfiguration(), type));

    JLabel deviceLabel = new JLabel(getLabelText(type));

    deviceLabel.setDisplayedMnemonic(getDisplayedMnemonic(type));
    deviceLabel.setLabelFor(deviceComboBox);

    final Container devicePanel = new TransparentPanel(new FlowLayout(FlowLayout.CENTER));

    devicePanel.setMaximumSize(new Dimension(WIDTH, 25));
    devicePanel.add(deviceLabel);
    devicePanel.add(deviceComboBox);

    final JPanel deviceAndPreviewPanel = new TransparentPanel(new BorderLayout());
    int preferredDeviceAndPreviewPanelHeight;

    switch (type) {
      case DeviceConfigurationComboBoxModel.AUDIO:
        preferredDeviceAndPreviewPanelHeight = 225;
        break;
      case DeviceConfigurationComboBoxModel.VIDEO:
        preferredDeviceAndPreviewPanelHeight = 305;
        break;
      default:
        preferredDeviceAndPreviewPanelHeight = 0;
        break;
    }
    if (preferredDeviceAndPreviewPanelHeight > 0)
      deviceAndPreviewPanel.setPreferredSize(
          new Dimension(WIDTH, preferredDeviceAndPreviewPanelHeight));
    deviceAndPreviewPanel.add(devicePanel, BorderLayout.NORTH);

    final ActionListener deviceComboBoxActionListener =
        new ActionListener() {
          public void actionPerformed(ActionEvent event) {
            boolean revalidateAndRepaint = false;

            for (int i = deviceAndPreviewPanel.getComponentCount() - 1; i >= 0; i--) {
              Component c = deviceAndPreviewPanel.getComponent(i);

              if (c != devicePanel) {
                deviceAndPreviewPanel.remove(i);
                revalidateAndRepaint = true;
              }
            }

            Component preview = null;

            if ((deviceComboBox.getSelectedItem() != null) && deviceComboBox.isShowing()) {
              preview =
                  createPreview(type, deviceComboBox, deviceAndPreviewPanel.getPreferredSize());
            }

            if (preview != null) {
              deviceAndPreviewPanel.add(preview, BorderLayout.CENTER);
              revalidateAndRepaint = true;
            }

            if (revalidateAndRepaint) {
              deviceAndPreviewPanel.revalidate();
              deviceAndPreviewPanel.repaint();
            }
          }
        };

    deviceComboBox.addActionListener(deviceComboBoxActionListener);
    /*
     * We have to initialize the controls to reflect the configuration
     * at the time of creating this instance. Additionally, because the
     * video preview will stop when it and its associated controls
     * become unnecessary, we have to restart it when the mentioned
     * controls become necessary again. We'll address the two goals
     * described by pretending there's a selection in the video combo
     * box when the combo box in question becomes displayable.
     */
    deviceComboBox.addHierarchyListener(
        new HierarchyListener() {
          public void hierarchyChanged(HierarchyEvent event) {
            if ((event.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) {
              SwingUtilities.invokeLater(
                  new Runnable() {
                    public void run() {
                      deviceComboBoxActionListener.actionPerformed(null);
                    }
                  });
            }
          }
        });

    return deviceAndPreviewPanel;
  }

  /**
   * Creates all the controls (including encoding) for a type(AUDIO or VIDEO)
   *
   * @param type the type.
   * @return the build Component.
   */
  private static Component createControls(int type) {
    ConfigurationService cfg = NeomediaActivator.getConfigurationService();
    SIPCommTabbedPane container = new SIPCommTabbedPane();
    ResourceManagementService res = NeomediaActivator.getResources();

    if ((cfg == null) || !cfg.getBoolean(DEVICES_DISABLED_PROP, false)) {
      container.insertTab(
          res.getI18NString("impl.media.configform.DEVICES"),
          null,
          createBasicControls(type),
          null,
          0);
    }
    if ((cfg == null) || !cfg.getBoolean(ENCODINGS_DISABLED_PROP, false)) {
      container.insertTab(
          res.getI18NString("impl.media.configform.ENCODINGS"),
          null,
          new PriorityTable(
              new EncodingConfigurationTableModel(mediaService.getEncodingConfiguration(), type),
              100),
          null,
          1);
    }
    if ((type == DeviceConfigurationComboBoxModel.VIDEO)
        && ((cfg == null) || !cfg.getBoolean(VIDEO_MORE_SETTINGS_DISABLED_PROP, false))) {
      container.insertTab(
          res.getI18NString("impl.media.configform.VIDEO_MORE_SETTINGS"),
          null,
          createVideoAdvancedSettings(),
          null,
          2);
    }
    return container;
  }

  /**
   * Creates preview for the (video) device in the video container.
   *
   * @param device the device
   * @param videoContainer the video container
   * @throws IOException a problem accessing the device
   * @throws MediaException a problem getting preview
   */
  private static void createVideoPreview(CaptureDeviceInfo device, JComponent videoContainer)
      throws IOException, MediaException {
    videoContainer.removeAll();

    videoContainer.revalidate();
    videoContainer.repaint();

    if (device == null) return;

    for (MediaDevice mediaDevice : mediaService.getDevices(MediaType.VIDEO, MediaUseCase.ANY)) {
      if (((MediaDeviceImpl) mediaDevice).getCaptureDeviceInfo().equals(device)) {
        Dimension videoContainerSize = videoContainer.getPreferredSize();
        Component preview =
            (Component)
                mediaService.getVideoPreviewComponent(
                    mediaDevice, videoContainerSize.width, videoContainerSize.height);

        if (preview != null) videoContainer.add(preview);
        break;
      }
    }
  }

  /**
   * Create preview component.
   *
   * @param type type
   * @param comboBox the options.
   * @param prefSize the preferred size
   * @return the component.
   */
  private static Component createPreview(int type, final JComboBox comboBox, Dimension prefSize) {
    JComponent preview = null;

    if (type == DeviceConfigurationComboBoxModel.AUDIO) {
      Object selectedItem = comboBox.getSelectedItem();

      if (selectedItem instanceof AudioSystem) {
        AudioSystem audioSystem = (AudioSystem) selectedItem;

        if (!NoneAudioSystem.LOCATOR_PROTOCOL.equalsIgnoreCase(audioSystem.getLocatorProtocol())) {
          preview = new TransparentPanel(new GridBagLayout());
          createAudioSystemControls(audioSystem, preview);
        }
      }
    } else if (type == DeviceConfigurationComboBoxModel.VIDEO) {
      JLabel noPreview =
          new JLabel(
              NeomediaActivator.getResources().getI18NString("impl.media.configform.NO_PREVIEW"));

      noPreview.setHorizontalAlignment(SwingConstants.CENTER);
      noPreview.setVerticalAlignment(SwingConstants.CENTER);

      preview = createVideoContainer(noPreview);
      preview.setPreferredSize(prefSize);

      Object selectedItem = comboBox.getSelectedItem();
      CaptureDeviceInfo device = null;
      if (selectedItem instanceof DeviceConfigurationComboBoxModel.CaptureDevice)
        device = ((DeviceConfigurationComboBoxModel.CaptureDevice) selectedItem).info;

      Exception exception;
      try {
        createVideoPreview(device, preview);
        exception = null;
      } catch (IOException ex) {
        exception = ex;
      } catch (MediaException ex) {
        exception = ex;
      }
      if (exception != null) {
        logger.error("Failed to create preview for device " + device, exception);
        device = null;
      }
    }

    return preview;
  }

  /**
   * Creates the video container.
   *
   * @param noVideoComponent the container component.
   * @return the video container.
   */
  private static JComponent createVideoContainer(Component noVideoComponent) {
    return new VideoContainer(noVideoComponent, false);
  }

  /**
   * The mnemonic for a type.
   *
   * @param type audio or video type.
   * @return the mnemonic.
   */
  private static char getDisplayedMnemonic(int type) {
    switch (type) {
      case DeviceConfigurationComboBoxModel.AUDIO:
        return NeomediaActivator.getResources().getI18nMnemonic("impl.media.configform.AUDIO");
      case DeviceConfigurationComboBoxModel.VIDEO:
        return NeomediaActivator.getResources().getI18nMnemonic("impl.media.configform.VIDEO");
      default:
        throw new IllegalArgumentException("type");
    }
  }

  /**
   * A label for a type.
   *
   * @param type the type.
   * @return the label.
   */
  private static String getLabelText(int type) {
    switch (type) {
      case DeviceConfigurationComboBoxModel.AUDIO:
        return NeomediaActivator.getResources().getI18NString("impl.media.configform.AUDIO");
      case DeviceConfigurationComboBoxModel.AUDIO_CAPTURE:
        return NeomediaActivator.getResources().getI18NString("impl.media.configform.AUDIO_IN");
      case DeviceConfigurationComboBoxModel.AUDIO_NOTIFY:
        return NeomediaActivator.getResources().getI18NString("impl.media.configform.AUDIO_NOTIFY");
      case DeviceConfigurationComboBoxModel.AUDIO_PLAYBACK:
        return NeomediaActivator.getResources().getI18NString("impl.media.configform.AUDIO_OUT");
      case DeviceConfigurationComboBoxModel.VIDEO:
        return NeomediaActivator.getResources().getI18NString("impl.media.configform.VIDEO");
      default:
        throw new IllegalArgumentException("type");
    }
  }

  /**
   * Creates the video advanced settings.
   *
   * @return video advanced settings panel.
   */
  private static Component createVideoAdvancedSettings() {
    ResourceManagementService resources = NeomediaActivator.getResources();

    final DeviceConfiguration deviceConfig = mediaService.getDeviceConfiguration();

    TransparentPanel centerPanel = new TransparentPanel(new GridBagLayout());
    centerPanel.setMaximumSize(new Dimension(WIDTH, 150));

    JButton resetDefaultsButton =
        new JButton(resources.getI18NString("impl.media.configform.VIDEO_RESET"));
    JPanel resetButtonPanel = new TransparentPanel(new FlowLayout(FlowLayout.RIGHT));
    resetButtonPanel.add(resetDefaultsButton);

    final JPanel centerAdvancedPanel = new TransparentPanel(new BorderLayout());
    centerAdvancedPanel.add(centerPanel, BorderLayout.NORTH);
    centerAdvancedPanel.add(resetButtonPanel, BorderLayout.SOUTH);

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

    centerPanel.add(
        new JLabel(resources.getI18NString("impl.media.configform.VIDEO_RESOLUTION")), constraints);
    constraints.gridy = 1;
    constraints.insets = new Insets(0, 0, 0, 0);
    final JCheckBox frameRateCheck =
        new SIPCommCheckBox(resources.getI18NString("impl.media.configform.VIDEO_FRAME_RATE"));
    centerPanel.add(frameRateCheck, constraints);
    constraints.gridy = 2;
    constraints.insets = new Insets(5, 5, 0, 0);
    centerPanel.add(
        new JLabel(resources.getI18NString("impl.media.configform.VIDEO_PACKETS_POLICY")),
        constraints);

    constraints.weightx = 1;
    constraints.gridx = 1;
    constraints.gridy = 0;
    constraints.insets = new Insets(5, 0, 0, 5);
    Object[] resolutionValues = new Object[DeviceConfiguration.SUPPORTED_RESOLUTIONS.length + 1];
    System.arraycopy(
        DeviceConfiguration.SUPPORTED_RESOLUTIONS,
        0,
        resolutionValues,
        1,
        DeviceConfiguration.SUPPORTED_RESOLUTIONS.length);
    final JComboBox sizeCombo = new JComboBox(resolutionValues);
    sizeCombo.setRenderer(new ResolutionCellRenderer());
    sizeCombo.setEditable(false);
    centerPanel.add(sizeCombo, constraints);

    // default value is 20
    final JSpinner frameRate = new JSpinner(new SpinnerNumberModel(20, 5, 30, 1));
    frameRate.addChangeListener(
        new ChangeListener() {
          public void stateChanged(ChangeEvent e) {
            deviceConfig.setFrameRate(
                ((SpinnerNumberModel) frameRate.getModel()).getNumber().intValue());
          }
        });
    constraints.gridy = 1;
    constraints.insets = new Insets(0, 0, 0, 5);
    centerPanel.add(frameRate, constraints);

    frameRateCheck.addActionListener(
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            if (frameRateCheck.isSelected()) {
              deviceConfig.setFrameRate(
                  ((SpinnerNumberModel) frameRate.getModel()).getNumber().intValue());
            } else // unlimited framerate
            deviceConfig.setFrameRate(-1);

            frameRate.setEnabled(frameRateCheck.isSelected());
          }
        });

    final JSpinner videoMaxBandwidth =
        new JSpinner(
            new SpinnerNumberModel(deviceConfig.getVideoMaxBandwidth(), 1, Integer.MAX_VALUE, 1));
    videoMaxBandwidth.addChangeListener(
        new ChangeListener() {
          public void stateChanged(ChangeEvent e) {
            deviceConfig.setVideoMaxBandwidth(
                ((SpinnerNumberModel) videoMaxBandwidth.getModel()).getNumber().intValue());
          }
        });
    constraints.gridx = 1;
    constraints.gridy = 2;
    constraints.insets = new Insets(0, 0, 5, 5);
    centerPanel.add(videoMaxBandwidth, constraints);

    resetDefaultsButton.addActionListener(
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            // reset to defaults
            sizeCombo.setSelectedIndex(0);
            frameRateCheck.setSelected(false);
            frameRate.setEnabled(false);
            frameRate.setValue(20);
            // unlimited framerate
            deviceConfig.setFrameRate(-1);
            videoMaxBandwidth.setValue(DeviceConfiguration.DEFAULT_VIDEO_MAX_BANDWIDTH);
          }
        });

    // load selected value or auto
    Dimension videoSize = deviceConfig.getVideoSize();

    if ((videoSize.getHeight() != DeviceConfiguration.DEFAULT_VIDEO_HEIGHT)
        && (videoSize.getWidth() != DeviceConfiguration.DEFAULT_VIDEO_WIDTH))
      sizeCombo.setSelectedItem(deviceConfig.getVideoSize());
    else sizeCombo.setSelectedIndex(0);
    sizeCombo.addActionListener(
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            Dimension selectedVideoSize = (Dimension) sizeCombo.getSelectedItem();

            if (selectedVideoSize == null) {
              // the auto value, default one
              selectedVideoSize =
                  new Dimension(
                      DeviceConfiguration.DEFAULT_VIDEO_WIDTH,
                      DeviceConfiguration.DEFAULT_VIDEO_HEIGHT);
            }
            deviceConfig.setVideoSize(selectedVideoSize);
          }
        });

    frameRateCheck.setSelected(
        deviceConfig.getFrameRate() != DeviceConfiguration.DEFAULT_VIDEO_FRAMERATE);
    frameRate.setEnabled(frameRateCheck.isSelected());

    if (frameRate.isEnabled()) frameRate.setValue(deviceConfig.getFrameRate());

    return centerAdvancedPanel;
  }

  /** Renders the available resolutions in the combo box. */
  private static class ResolutionCellRenderer extends DefaultListCellRenderer {
    /**
     * The serialization version number of the <tt>ResolutionCellRenderer</tt> class. Defined to the
     * value of <tt>0</tt> because the <tt>ResolutionCellRenderer</tt> instances do not have state
     * of their own.
     */
    private static final long serialVersionUID = 0L;

    /**
     * Sets readable text describing the resolution if the selected value is null we return the
     * string "Auto".
     *
     * @param list
     * @param value
     * @param index
     * @param isSelected
     * @param cellHasFocus
     * @return Component
     */
    @Override
    public Component getListCellRendererComponent(
        JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
      // call super to set backgrounds and fonts
      super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);

      // now just change the text
      if (value == null) setText("Auto");
      else if (value instanceof Dimension) {
        Dimension d = (Dimension) value;

        setText(((int) d.getWidth()) + "x" + ((int) d.getHeight()));
      }
      return this;
    }
  }
}
Ejemplo n.º 8
0
/**
 * Global statuses service impl. Gives access to the outside for some methods to change the global
 * status and individual protocol provider status. Use to implement global status menu with list of
 * all account statuses.
 *
 * @author Damian Minkov
 */
public class GlobalStatusServiceImpl implements GlobalStatusService {
  /** The object used for logging. */
  private final Logger logger = Logger.getLogger(GlobalStatusServiceImpl.class);

  /**
   * Returns the last status that was stored in the configuration for the given protocol provider.
   *
   * @param protocolProvider the protocol provider
   * @return the last status that was stored in the configuration for the given protocol provider
   */
  public PresenceStatus getLastPresenceStatus(ProtocolProviderService protocolProvider) {
    String lastStatus = getLastStatusString(protocolProvider);

    if (lastStatus != null) {
      OperationSetPresence presence = protocolProvider.getOperationSet(OperationSetPresence.class);

      if (presence == null) return null;

      Iterator<PresenceStatus> i = presence.getSupportedStatusSet();
      PresenceStatus status;

      while (i.hasNext()) {
        status = i.next();
        if (status.getStatusName().equals(lastStatus)) return status;
      }
    }
    return null;
  }

  /**
   * Returns the last contact status saved in the configuration.
   *
   * @param protocolProvider the protocol provider to which the status corresponds
   * @return the last contact status saved in the configuration.
   */
  public String getLastStatusString(ProtocolProviderService protocolProvider) {
    // find the last contact status saved in the configuration.
    String lastStatus = null;

    ConfigurationService configService = GuiActivator.getConfigurationService();
    String prefix = "net.java.sip.communicator.impl.gui.accounts";
    List<String> accounts = configService.getPropertyNamesByPrefix(prefix, true);
    String protocolProviderAccountUID = protocolProvider.getAccountID().getAccountUniqueID();

    for (String accountRootPropName : accounts) {
      String accountUID = configService.getString(accountRootPropName);

      if (accountUID.equals(protocolProviderAccountUID)) {
        lastStatus = configService.getString(accountRootPropName + ".lastAccountStatus");

        if (lastStatus != null) break;
      }
    }

    return lastStatus;
  }

  /**
   * Publish present status. We search for the highest value in the given interval.
   *
   * @param protocolProvider the protocol provider to which we change the status.
   * @param status the status tu publish.
   */
  public void publishStatus(
      ProtocolProviderService protocolProvider, PresenceStatus status, boolean rememberStatus) {
    OperationSetPresence presence = protocolProvider.getOperationSet(OperationSetPresence.class);

    LoginManager loginManager = GuiActivator.getUIService().getLoginManager();
    RegistrationState registrationState = protocolProvider.getRegistrationState();

    if (registrationState == RegistrationState.REGISTERED
        && presence != null
        && !presence.getPresenceStatus().equals(status)) {
      if (status.isOnline()) {
        new PublishPresenceStatusThread(protocolProvider, presence, status).start();
      } else {
        loginManager.setManuallyDisconnected(true);
        GuiActivator.getUIService().getLoginManager().logoff(protocolProvider);
      }
    } else if (registrationState != RegistrationState.REGISTERED
        && registrationState != RegistrationState.REGISTERING
        && registrationState != RegistrationState.AUTHENTICATING
        && status.isOnline()) {
      GuiActivator.getUIService().getLoginManager().login(protocolProvider);
    } else if (!status.isOnline() && !(registrationState == RegistrationState.UNREGISTERING)) {
      loginManager.setManuallyDisconnected(true);
      GuiActivator.getUIService().getLoginManager().logoff(protocolProvider);
    }

    if (rememberStatus) saveStatusInformation(protocolProvider, status.getStatusName());
  }

  /**
   * Publish present status. We search for the highest value in the given interval.
   *
   * <p>change the status.
   *
   * @param globalStatus
   */
  public void publishStatus(GlobalStatusEnum globalStatus) {
    String itemName = globalStatus.getStatusName();

    Iterator<ProtocolProviderService> pProviders =
        GuiActivator.getUIService().getMainFrame().getProtocolProviders();

    while (pProviders.hasNext()) {
      ProtocolProviderService protocolProvider = pProviders.next();

      if (itemName.equals(GlobalStatusEnum.ONLINE_STATUS)) {
        if (!protocolProvider.isRegistered()) {
          saveStatusInformation(protocolProvider, itemName);

          GuiActivator.getUIService().getLoginManager().login(protocolProvider);
        } else {
          OperationSetPresence presence =
              protocolProvider.getOperationSet(OperationSetPresence.class);

          if (presence == null) {
            saveStatusInformation(protocolProvider, itemName);

            continue;
          }

          Iterator<PresenceStatus> statusSet = presence.getSupportedStatusSet();

          while (statusSet.hasNext()) {
            PresenceStatus status = statusSet.next();

            if (status.getStatus() < PresenceStatus.EAGER_TO_COMMUNICATE_THRESHOLD
                && status.getStatus() >= PresenceStatus.AVAILABLE_THRESHOLD) {
              new PublishPresenceStatusThread(protocolProvider, presence, status).start();

              this.saveStatusInformation(protocolProvider, status.getStatusName());

              break;
            }
          }
        }
      } else if (itemName.equals(GlobalStatusEnum.OFFLINE_STATUS)) {
        if (!protocolProvider.getRegistrationState().equals(RegistrationState.UNREGISTERED)
            && !protocolProvider.getRegistrationState().equals(RegistrationState.UNREGISTERING)) {
          OperationSetPresence presence =
              protocolProvider.getOperationSet(OperationSetPresence.class);

          if (presence == null) {
            saveStatusInformation(protocolProvider, itemName);

            GuiActivator.getUIService().getLoginManager().logoff(protocolProvider);

            continue;
          }

          Iterator<PresenceStatus> statusSet = presence.getSupportedStatusSet();

          while (statusSet.hasNext()) {
            PresenceStatus status = statusSet.next();

            if (status.getStatus() < PresenceStatus.ONLINE_THRESHOLD) {
              this.saveStatusInformation(protocolProvider, status.getStatusName());

              break;
            }
          }

          try {
            protocolProvider.unregister();
          } catch (OperationFailedException e1) {
            logger.error(
                "Unable to unregister the protocol provider: "
                    + protocolProvider
                    + " due to the following exception: "
                    + e1);
          }
        }
      } else if (itemName.equals(GlobalStatusEnum.FREE_FOR_CHAT_STATUS)) {
        // we search for highest available status here
        publishStatus(
            protocolProvider, PresenceStatus.AVAILABLE_THRESHOLD, PresenceStatus.MAX_STATUS_VALUE);
      } else if (itemName.equals(GlobalStatusEnum.DO_NOT_DISTURB_STATUS)) {
        // status between online and away is DND
        publishStatus(
            protocolProvider, PresenceStatus.ONLINE_THRESHOLD, PresenceStatus.AWAY_THRESHOLD);
      } else if (itemName.equals(GlobalStatusEnum.AWAY_STATUS)) {
        // a status in the away interval
        publishStatus(
            protocolProvider, PresenceStatus.AWAY_THRESHOLD, PresenceStatus.AVAILABLE_THRESHOLD);
      }
    }
  }

  /**
   * Publish present status. We search for the highest value in the given interval.
   *
   * @param protocolProvider the protocol provider to which we change the status.
   * @param floorStatusValue the min status value.
   * @param ceilStatusValue the max status value.
   */
  private void publishStatus(
      ProtocolProviderService protocolProvider, int floorStatusValue, int ceilStatusValue) {
    if (!protocolProvider.isRegistered()) return;

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

    if (presence == null) return;

    Iterator<PresenceStatus> statusSet = presence.getSupportedStatusSet();

    PresenceStatus status = null;

    while (statusSet.hasNext()) {
      PresenceStatus currentStatus = statusSet.next();

      if (status == null
          && currentStatus.getStatus() < ceilStatusValue
          && currentStatus.getStatus() >= floorStatusValue) {
        status = currentStatus;
      }

      if (status != null) {
        if (currentStatus.getStatus() < ceilStatusValue
            && currentStatus.getStatus() >= floorStatusValue
            && currentStatus.getStatus() > status.getStatus()) {
          status = currentStatus;
        }
      }
    }

    if (status != null) {
      new PublishPresenceStatusThread(protocolProvider, presence, status).start();

      this.saveStatusInformation(protocolProvider, status.getStatusName());
    }
  }

  /**
   * Saves the last status for all accounts. This information is used on loging. Each time user logs
   * in he's logged with the same status as he was the last time before closing the application.
   *
   * @param protocolProvider the protocol provider to save status information for
   * @param statusName the name of the status to save
   */
  private void saveStatusInformation(ProtocolProviderService protocolProvider, String statusName) {
    ConfigurationService configService = GuiActivator.getConfigurationService();

    String prefix = "net.java.sip.communicator.impl.gui.accounts";

    List<String> accounts = configService.getPropertyNamesByPrefix(prefix, true);

    boolean savedAccount = false;

    for (String accountRootPropName : accounts) {
      String accountUID = configService.getString(accountRootPropName);

      if (accountUID.equals(protocolProvider.getAccountID().getAccountUniqueID())) {

        configService.setProperty(accountRootPropName + ".lastAccountStatus", statusName);

        savedAccount = true;
      }
    }

    if (!savedAccount) {
      String accNodeName = "acc" + Long.toString(System.currentTimeMillis());

      String accountPackage = "net.java.sip.communicator.impl.gui.accounts." + accNodeName;

      configService.setProperty(
          accountPackage, protocolProvider.getAccountID().getAccountUniqueID());

      configService.setProperty(accountPackage + ".lastAccountStatus", statusName);
    }
  }

  /** Publishes the given status to the given presence operation set. */
  private class PublishPresenceStatusThread extends Thread {
    private ProtocolProviderService protocolProvider;

    private PresenceStatus status;

    private OperationSetPresence presence;

    /**
     * Publishes the given <tt>status</tt> through the given <tt>presence</tt> operation set.
     *
     * @param presence the operation set through which we publish the status
     * @param status the status to publish
     */
    public PublishPresenceStatusThread(
        ProtocolProviderService protocolProvider,
        OperationSetPresence presence,
        PresenceStatus status) {
      this.protocolProvider = protocolProvider;
      this.presence = presence;
      this.status = status;
    }

    @Override
    public void run() {
      try {
        presence.publishPresenceStatus(status, "");
      } catch (IllegalArgumentException e1) {

        logger.error("Error - changing status", e1);
      } catch (IllegalStateException e1) {

        logger.error("Error - changing status", e1);
      } catch (OperationFailedException e1) {
        if (e1.getErrorCode() == OperationFailedException.GENERAL_ERROR) {
          String msgText =
              GuiActivator.getResources()
                  .getI18NString(
                      "service.gui.STATUS_CHANGE_GENERAL_ERROR",
                      new String[] {
                        protocolProvider.getAccountID().getUserID(),
                        protocolProvider.getAccountID().getService()
                      });

          new ErrorDialog(
                  null,
                  GuiActivator.getResources().getI18NString("service.gui.GENERAL_ERROR"),
                  msgText,
                  e1)
              .showDialog();
        } else if (e1.getErrorCode() == OperationFailedException.NETWORK_FAILURE) {
          String msgText =
              GuiActivator.getResources()
                  .getI18NString(
                      "service.gui.STATUS_CHANGE_NETWORK_FAILURE",
                      new String[] {
                        protocolProvider.getAccountID().getUserID(),
                        protocolProvider.getAccountID().getService()
                      });

          new ErrorDialog(
                  null,
                  msgText,
                  GuiActivator.getResources().getI18NString("service.gui.NETWORK_FAILURE"),
                  e1)
              .showDialog();
        } else if (e1.getErrorCode() == OperationFailedException.PROVIDER_NOT_REGISTERED) {
          String msgText =
              GuiActivator.getResources()
                  .getI18NString(
                      "service.gui.STATUS_CHANGE_NETWORK_FAILURE",
                      new String[] {
                        protocolProvider.getAccountID().getUserID(),
                        protocolProvider.getAccountID().getService()
                      });

          new ErrorDialog(
                  null,
                  GuiActivator.getResources().getI18NString("service.gui.NETWORK_FAILURE"),
                  msgText,
                  e1)
              .showDialog();
        }
        logger.error("Error - changing status", e1);
      }
    }
  }
}
Ejemplo n.º 9
0
/**
 * @author Emil Ivov
 * @author Lyubomir Marinov
 */
public class ConfigurationActivator implements BundleActivator {
  /** The <tt>Logger</tt> used by the <tt>ConfigurationActivator</tt> class for logging output. */
  private static final Logger logger = Logger.getLogger(ConfigurationActivator.class);

  /** The currently registered {@link ConfigurationService} instance. */
  private ConfigurationService cs;

  /**
   * Starts the configuration service
   *
   * @param bundleContext the <tt>BundleContext</tt> as provided by the OSGi framework.
   * @throws Exception if anything goes wrong
   */
  public void start(BundleContext bundleContext) throws Exception {
    FileAccessService fas = ServiceUtils.getService(bundleContext, FileAccessService.class);

    if (fas != null) {
      File usePropFileConfig;
      try {
        usePropFileConfig =
            fas.getPrivatePersistentFile(".usepropfileconfig", FileCategory.PROFILE);
      } catch (Exception ise) {
        // There is somewhat of a chicken-and-egg dependency between
        // FileConfigurationServiceImpl and ConfigurationServiceImpl:
        // FileConfigurationServiceImpl throws IllegalStateException if
        // certain System properties are not set,
        // ConfigurationServiceImpl will make sure that these properties
        // are set but it will do that later.
        // A SecurityException is thrown when the destination
        // is not writable or we do not have access to that folder
        usePropFileConfig = null;
      }

      if (usePropFileConfig != null && usePropFileConfig.exists()) {
        logger.info("Using properties file configuration store.");
        this.cs = LibJitsi.getConfigurationService();
      }
    }

    if (this.cs == null) {
      this.cs = new JdbcConfigService(fas);
    }

    bundleContext.registerService(ConfigurationService.class.getName(), this.cs, null);

    fixPermissions(this.cs);
  }

  /**
   * Causes the configuration service to store the properties object and unregisters the
   * configuration service.
   *
   * @param bundleContext <tt>BundleContext</tt>
   * @throws Exception if anything goes wrong while storing the properties managed by the
   *     <tt>ConfigurationService</tt> implementation provided by this bundle and while
   *     unregistering the service in question
   */
  public void stop(BundleContext bundleContext) throws Exception {
    this.cs.storeConfiguration();
    this.cs = null;
  }

  /**
   * Makes home folder and the configuration file readable and writable only to the owner.
   *
   * @param cs the <tt>ConfigurationService</tt> instance to check for home folder and configuration
   *     file.
   */
  private static void fixPermissions(ConfigurationService cs) {
    if (!OSUtils.IS_LINUX && !OSUtils.IS_MAC) return;

    try {
      // let's check config file and config folder
      File homeFolder = new File(cs.getScHomeDirLocation(), cs.getScHomeDirName());
      Set<PosixFilePermission> perms =
          new HashSet<PosixFilePermission>() {
            {
              add(PosixFilePermission.OWNER_READ);
              add(PosixFilePermission.OWNER_WRITE);
              add(PosixFilePermission.OWNER_EXECUTE);
            }
          };
      Files.setPosixFilePermissions(Paths.get(homeFolder.getAbsolutePath()), perms);

      String fileName = cs.getConfigurationFilename();
      if (fileName != null) {
        File cf = new File(homeFolder, fileName);
        if (cf.exists()) {
          perms =
              new HashSet<PosixFilePermission>() {
                {
                  add(PosixFilePermission.OWNER_READ);
                  add(PosixFilePermission.OWNER_WRITE);
                }
              };
          Files.setPosixFilePermissions(Paths.get(cf.getAbsolutePath()), perms);
        }
      }
    } catch (Throwable t) {
      logger.error("Error creating c lib instance for fixing file permissions", t);

      if (t instanceof InterruptedException) Thread.currentThread().interrupt();
      else if (t instanceof ThreadDeath) throw (ThreadDeath) t;
    }
  }
}
Ejemplo n.º 10
0
/**
 * The source contact service. The will show most recent messages.
 *
 * @author Damian Minkov
 */
public class MessageSourceService extends MetaContactListAdapter
    implements ContactSourceService,
        ContactPresenceStatusListener,
        ContactCapabilitiesListener,
        ProviderPresenceStatusListener,
        SubscriptionListener,
        LocalUserChatRoomPresenceListener,
        MessageListener,
        ChatRoomMessageListener,
        AdHocChatRoomMessageListener {
  /** The logger for this class. */
  private static Logger logger = Logger.getLogger(MessageSourceService.class);

  /** The display name of this contact source. */
  private final String MESSAGE_HISTORY_NAME;

  /** The type of the source service, the place to be shown in the ui. */
  private int sourceServiceType = CONTACT_LIST_TYPE;

  /**
   * Whether to show recent messages in history or in contactlist. By default we show it in
   * contactlist.
   */
  private static final String IN_HISTORY_PROPERTY =
      "net.java.sip.communicator.impl.msghistory.contactsrc.IN_HISTORY";

  /** Property to control number of recent messages. */
  private static final String NUMBER_OF_RECENT_MSGS_PROP =
      "net.java.sip.communicator.impl.msghistory.contactsrc.MSG_NUMBER";

  /** Property to control version of recent messages. */
  private static final String VER_OF_RECENT_MSGS_PROP =
      "net.java.sip.communicator.impl.msghistory.contactsrc.MSG_VER";

  /** Property to control messages type. Can query for message sub type. */
  private static final String IS_MESSAGE_SUBTYPE_SMS_PROP =
      "net.java.sip.communicator.impl.msghistory.contactsrc.IS_SMS_ENABLED";

  /**
   * The number of recent messages to store in the history, but will retrieve just
   * <tt>numberOfMessages</tt>
   */
  private static final int NUMBER_OF_MSGS_IN_HISTORY = 100;

  /** Number of messages to show. */
  private int numberOfMessages = 10;

  /** The structure to save recent messages list. */
  private static final String[] STRUCTURE_NAMES =
      new String[] {"provider", "contact", "timestamp", "ver"};

  /** The current version of recent messages. When changed the recent messages are recreated. */
  private static String RECENT_MSGS_VER = "2";

  /** The structure. */
  private static final HistoryRecordStructure recordStructure =
      new HistoryRecordStructure(STRUCTURE_NAMES);

  /** Recent messages history ID. */
  private static final HistoryID historyID =
      HistoryID.createFromRawID(new String[] {"recent_messages"});

  /** The cache for recent messages. */
  private History history = null;

  /** List of recent messages. */
  private final List<ComparableEvtObj> recentMessages = new LinkedList<ComparableEvtObj>();

  /** Date of the oldest shown message. */
  private Date oldestRecentMessage = null;

  /** The last query created. */
  private MessageSourceContactQuery recentQuery = null;

  /** The message subtype if any. */
  private boolean isSMSEnabled = false;

  /** Message history service that has created us. */
  private MessageHistoryServiceImpl messageHistoryService;

  /** Constructs MessageSourceService. */
  MessageSourceService(MessageHistoryServiceImpl messageHistoryService) {
    this.messageHistoryService = messageHistoryService;

    ConfigurationService conf = MessageHistoryActivator.getConfigurationService();

    if (conf.getBoolean(IN_HISTORY_PROPERTY, false)) {
      sourceServiceType = HISTORY_TYPE;
    }

    MESSAGE_HISTORY_NAME =
        MessageHistoryActivator.getResources().getI18NString("service.gui.RECENT_MESSAGES");

    numberOfMessages = conf.getInt(NUMBER_OF_RECENT_MSGS_PROP, numberOfMessages);

    isSMSEnabled = conf.getBoolean(IS_MESSAGE_SUBTYPE_SMS_PROP, isSMSEnabled);

    RECENT_MSGS_VER = conf.getString(VER_OF_RECENT_MSGS_PROP, RECENT_MSGS_VER);

    MessageSourceContactPresenceStatus.MSG_SRC_CONTACT_ONLINE.setStatusIcon(
        MessageHistoryActivator.getResources()
            .getImageInBytes("service.gui.icons.SMS_STATUS_ICON"));
  }

  /**
   * Returns the display name of this contact source.
   *
   * @return the display name of this contact source
   */
  @Override
  public String getDisplayName() {
    return MESSAGE_HISTORY_NAME;
  }

  /**
   * Returns default type to indicate that this contact source can be queried by default filters.
   *
   * @return the type of this contact source
   */
  @Override
  public int getType() {
    return sourceServiceType;
  }

  /**
   * Returns the index of the contact source in the result list.
   *
   * @return the index of the contact source in the result list
   */
  @Override
  public int getIndex() {
    return 0;
  }

  /**
   * Creates query for the given <tt>searchString</tt>.
   *
   * @param queryString the string to search for
   * @return the created query
   */
  @Override
  public ContactQuery createContactQuery(String queryString) {
    recentQuery = (MessageSourceContactQuery) createContactQuery(queryString, numberOfMessages);

    return recentQuery;
  }

  /**
   * Updates the contact sources in the recent query if any. Done here in order to sync with
   * recentMessages instance, and to check for already existing instances of contact sources.
   * Normally called from the query.
   */
  public void updateRecentMessages() {
    if (recentQuery == null) return;

    synchronized (recentMessages) {
      List<SourceContact> currentContactsInQuery = recentQuery.getQueryResults();

      for (ComparableEvtObj evtObj : recentMessages) {
        // the contains will use the correct equals method of
        // the object evtObj
        if (!currentContactsInQuery.contains(evtObj)) {
          MessageSourceContact newSourceContact =
              new MessageSourceContact(evtObj.getEventObject(), MessageSourceService.this);
          newSourceContact.initDetails(evtObj.getEventObject());

          recentQuery.addQueryResult(newSourceContact);
        }
      }
    }
  }

  /**
   * Searches for entries in cached recent messages in history.
   *
   * @param provider the provider which contact messages we will search
   * @param isStatusChanged is the search because of status changed
   * @return entries in cached recent messages in history.
   */
  private List<ComparableEvtObj> getCachedRecentMessages(
      ProtocolProviderService provider, boolean isStatusChanged) {
    String providerID = provider.getAccountID().getAccountUniqueID();
    List<String> recentMessagesContactIDs =
        getRecentContactIDs(
            providerID, recentMessages.size() < numberOfMessages ? null : oldestRecentMessage);

    List<ComparableEvtObj> cachedRecentMessages = new ArrayList<ComparableEvtObj>();

    for (String contactID : recentMessagesContactIDs) {
      Collection<EventObject> res =
          messageHistoryService.findRecentMessagesPerContact(
              numberOfMessages, providerID, contactID, isSMSEnabled);

      processEventObjects(res, cachedRecentMessages, isStatusChanged);
    }

    return cachedRecentMessages;
  }

  /**
   * Process list of event objects. Checks whether message source contact already exist for this
   * event object, if yes just update it with the new values (not sure whether we should do this, as
   * it may bring old messages) and if status of provider is changed, init its details, updates its
   * capabilities. It still adds the found messages source contact to the list of the new contacts,
   * as later we will detect this and fire update event. If nothing found a new contact is created.
   *
   * @param res list of event
   * @param cachedRecentMessages list of newly created source contacts or already existed but
   *     updated with corresponding event object
   * @param isStatusChanged whether provider status changed and we are processing
   */
  private void processEventObjects(
      Collection<EventObject> res,
      List<ComparableEvtObj> cachedRecentMessages,
      boolean isStatusChanged) {
    for (EventObject obj : res) {
      ComparableEvtObj oldMsg = findRecentMessage(obj, recentMessages);

      if (oldMsg != null) {
        oldMsg.update(obj); // update

        if (isStatusChanged && recentQuery != null) recentQuery.updateCapabilities(oldMsg, obj);

        // we still add it to cachedRecentMessages
        // later we will find it is duplicate and will fire
        // update event
        if (!cachedRecentMessages.contains(oldMsg)) cachedRecentMessages.add(oldMsg);

        continue;
      }

      oldMsg = findRecentMessage(obj, cachedRecentMessages);

      if (oldMsg == null) {
        oldMsg = new ComparableEvtObj(obj);

        if (isStatusChanged && recentQuery != null) recentQuery.updateCapabilities(oldMsg, obj);

        cachedRecentMessages.add(oldMsg);
      }
    }
  }

  /**
   * Access for source contacts impl.
   *
   * @return
   */
  boolean isSMSEnabled() {
    return isSMSEnabled;
  }

  /**
   * Add the ComparableEvtObj, newly added will fire new, for existing fire update and when trimming
   * the list to desired length fire remove for those that were removed
   *
   * @param contactsToAdd
   */
  private void addNewRecentMessages(List<ComparableEvtObj> contactsToAdd) {
    // now find object to fire new, and object to fire remove
    // let us find duplicates and fire update
    List<ComparableEvtObj> duplicates = new ArrayList<ComparableEvtObj>();
    for (ComparableEvtObj msgToAdd : contactsToAdd) {
      if (recentMessages.contains(msgToAdd)) {
        duplicates.add(msgToAdd);

        // save update
        updateRecentMessageToHistory(msgToAdd);
      }
    }
    recentMessages.removeAll(duplicates);

    // now contacts to add has no duplicates, add them all
    boolean changed = recentMessages.addAll(contactsToAdd);

    if (changed) {
      Collections.sort(recentMessages);

      if (recentQuery != null) {
        for (ComparableEvtObj obj : duplicates)
          recentQuery.updateContact(obj, obj.getEventObject());
      }
    }

    if (!recentMessages.isEmpty())
      oldestRecentMessage = recentMessages.get(recentMessages.size() - 1).getTimestamp();

    // trim
    List<ComparableEvtObj> removedItems = null;
    if (recentMessages.size() > numberOfMessages) {
      removedItems =
          new ArrayList<ComparableEvtObj>(
              recentMessages.subList(numberOfMessages, recentMessages.size()));

      recentMessages.removeAll(removedItems);
    }

    if (recentQuery != null) {
      // now fire, removed for all that were in the list
      // and now are removed after trim
      if (removedItems != null) {
        for (ComparableEvtObj msc : removedItems) {
          if (!contactsToAdd.contains(msc)) recentQuery.fireContactRemoved(msc);
        }
      }

      // fire new for all that were added, and not removed after trim
      for (ComparableEvtObj msc : contactsToAdd) {
        if ((removedItems == null || !removedItems.contains(msc)) && !duplicates.contains(msc)) {
          MessageSourceContact newSourceContact =
              new MessageSourceContact(msc.getEventObject(), MessageSourceService.this);
          newSourceContact.initDetails(msc.getEventObject());

          recentQuery.addQueryResult(newSourceContact);
        }
      }

      // if recent messages were changed, indexes have change lets
      // fire event for the last element which will reorder the whole
      // group if needed.
      if (changed) recentQuery.fireContactChanged(recentMessages.get(recentMessages.size() - 1));
    }
  }

  /**
   * When a provider is added, do not block and start executing in new thread.
   *
   * @param provider ProtocolProviderService
   */
  void handleProviderAdded(final ProtocolProviderService provider, final boolean isStatusChanged) {
    new Thread(
            new Runnable() {
              @Override
              public void run() {
                handleProviderAddedInSeparateThread(provider, isStatusChanged);
              }
            })
        .start();
  }

  /**
   * When a provider is added. As searching can be slow especially when handling special type of
   * messages (with subType) this need to be run in new Thread.
   *
   * @param provider ProtocolProviderService
   */
  private void handleProviderAddedInSeparateThread(
      ProtocolProviderService provider, boolean isStatusChanged) {
    // lets check if we have cached recent messages for this provider, and
    // fire events if found and are newer

    synchronized (recentMessages) {
      List<ComparableEvtObj> cachedRecentMessages =
          getCachedRecentMessages(provider, isStatusChanged);

      if (cachedRecentMessages.isEmpty()) {
        // maybe there is no cached history for this
        // let's check
        // load it not from cache, but do a local search
        Collection<EventObject> res =
            messageHistoryService.findRecentMessagesPerContact(
                numberOfMessages, provider.getAccountID().getAccountUniqueID(), null, isSMSEnabled);

        List<ComparableEvtObj> newMsc = new ArrayList<ComparableEvtObj>();

        processEventObjects(res, newMsc, isStatusChanged);

        addNewRecentMessages(newMsc);

        for (ComparableEvtObj msc : newMsc) {
          saveRecentMessageToHistory(msc);
        }
      } else addNewRecentMessages(cachedRecentMessages);
    }
  }

  /**
   * Tries to match the event object to already existing ComparableEvtObj in the supplied list.
   *
   * @param obj the object that we will try to match.
   * @param list the list we will search in.
   * @return the found ComparableEvtObj
   */
  private static ComparableEvtObj findRecentMessage(EventObject obj, List<ComparableEvtObj> list) {
    Contact contact = null;
    ChatRoom chatRoom = null;

    if (obj instanceof MessageDeliveredEvent) {
      contact = ((MessageDeliveredEvent) obj).getDestinationContact();
    } else if (obj instanceof MessageReceivedEvent) {
      contact = ((MessageReceivedEvent) obj).getSourceContact();
    } else if (obj instanceof ChatRoomMessageDeliveredEvent) {
      chatRoom = ((ChatRoomMessageDeliveredEvent) obj).getSourceChatRoom();
    } else if (obj instanceof ChatRoomMessageReceivedEvent) {
      chatRoom = ((ChatRoomMessageReceivedEvent) obj).getSourceChatRoom();
    }

    for (ComparableEvtObj evt : list) {
      if ((contact != null && contact.equals(evt.getContact()))
          || (chatRoom != null && chatRoom.equals(evt.getRoom()))) return evt;
    }

    return null;
  }

  /**
   * A provider has been removed.
   *
   * @param provider the ProtocolProviderService that has been unregistered.
   */
  void handleProviderRemoved(ProtocolProviderService provider) {
    // lets remove the recent messages for this provider, and update
    // with recent messages for the available providers
    synchronized (recentMessages) {
      if (provider != null) {
        List<ComparableEvtObj> removedItems = new ArrayList<ComparableEvtObj>();
        for (ComparableEvtObj msc : recentMessages) {
          if (msc.getProtocolProviderService().equals(provider)) removedItems.add(msc);
        }

        recentMessages.removeAll(removedItems);
        if (!recentMessages.isEmpty())
          oldestRecentMessage = recentMessages.get(recentMessages.size() - 1).getTimestamp();
        else oldestRecentMessage = null;

        if (recentQuery != null) {
          for (ComparableEvtObj msc : removedItems) {
            recentQuery.fireContactRemoved(msc);
          }
        }
      }

      // handleProviderRemoved can be invoked due to stopped
      // history service, if this is the case we do not want to
      // update messages
      if (!this.messageHistoryService.isHistoryLoggingEnabled()) return;

      // lets do the same as we enable provider
      // for all registered providers and finally fire events
      List<ComparableEvtObj> contactsToAdd = new ArrayList<ComparableEvtObj>();
      for (ProtocolProviderService pps : messageHistoryService.getCurrentlyAvailableProviders()) {
        contactsToAdd.addAll(getCachedRecentMessages(pps, true));
      }

      addNewRecentMessages(contactsToAdd);
    }
  }

  /**
   * Searches for contact ids in history of recent messages.
   *
   * @param provider
   * @param after
   * @return
   */
  List<String> getRecentContactIDs(String provider, Date after) {
    List<String> res = new ArrayList<String>();

    try {
      History history = getHistory();

      if (history != null) {
        Iterator<HistoryRecord> recs = history.getReader().findLast(NUMBER_OF_MSGS_IN_HISTORY);
        SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT);

        while (recs.hasNext()) {
          HistoryRecord hr = recs.next();

          String contact = null;
          String recordProvider = null;
          Date timestamp = null;

          for (int i = 0; i < hr.getPropertyNames().length; i++) {
            String propName = hr.getPropertyNames()[i];

            if (propName.equals(STRUCTURE_NAMES[0])) recordProvider = hr.getPropertyValues()[i];
            else if (propName.equals(STRUCTURE_NAMES[1])) contact = hr.getPropertyValues()[i];
            else if (propName.equals(STRUCTURE_NAMES[2])) {
              try {
                timestamp = sdf.parse(hr.getPropertyValues()[i]);
              } catch (ParseException e) {
                timestamp = new Date(Long.parseLong(hr.getPropertyValues()[i]));
              }
            }
          }

          if (recordProvider == null || contact == null) continue;

          if (after != null && timestamp != null && timestamp.before(after)) continue;

          if (recordProvider.equals(provider)) res.add(contact);
        }
      }
    } catch (IOException ex) {
      logger.error("cannot create recent_messages history", ex);
    }

    return res;
  }

  /**
   * Returns the cached recent messages history.
   *
   * @return
   * @throws IOException
   */
  private History getHistory() throws IOException {
    synchronized (historyID) {
      HistoryService historyService =
          MessageHistoryActivator.getMessageHistoryService().getHistoryService();

      if (history == null) {
        history = historyService.createHistory(historyID, recordStructure);

        // lets check the version if not our version, re-create
        // history (delete it)
        HistoryReader reader = history.getReader();
        boolean delete = false;
        QueryResultSet<HistoryRecord> res = reader.findLast(1);
        if (res != null && res.hasNext()) {
          HistoryRecord hr = res.next();
          if (hr.getPropertyValues().length >= 4) {
            if (!hr.getPropertyValues()[3].equals(RECENT_MSGS_VER)) delete = true;
          } else delete = true;
        }

        if (delete) {
          // delete it
          try {
            historyService.purgeLocallyStoredHistory(historyID);

            history = historyService.createHistory(historyID, recordStructure);
          } catch (IOException ex) {
            logger.error("Cannot delete recent_messages history", ex);
          }
        }
      }

      return history;
    }
  }

  /**
   * Returns the index of the source contact, in the list of recent messages.
   *
   * @param messageSourceContact
   * @return
   */
  int getIndex(MessageSourceContact messageSourceContact) {
    synchronized (recentMessages) {
      for (int i = 0; i < recentMessages.size(); i++)
        if (recentMessages.get(i).equals(messageSourceContact)) return i;

      return -1;
    }
  }

  /**
   * Creates query for the given <tt>searchString</tt>.
   *
   * @param queryString the string to search for
   * @param contactCount the maximum count of result contacts
   * @return the created query
   */
  @Override
  public ContactQuery createContactQuery(String queryString, int contactCount) {
    if (!StringUtils.isNullOrEmpty(queryString)) return null;

    recentQuery = new MessageSourceContactQuery(MessageSourceService.this);

    return recentQuery;
  }

  /**
   * Updates contact source contacts with status.
   *
   * @param evt the ContactPresenceStatusChangeEvent describing the status
   */
  @Override
  public void contactPresenceStatusChanged(ContactPresenceStatusChangeEvent evt) {
    if (recentQuery == null) return;

    synchronized (recentMessages) {
      for (ComparableEvtObj msg : recentMessages) {
        if (msg.getContact() != null && msg.getContact().equals(evt.getSourceContact())) {
          recentQuery.updateContactStatus(msg, evt.getNewStatus());
        }
      }
    }
  }

  @Override
  public void providerStatusChanged(ProviderPresenceStatusChangeEvent evt) {
    if (!evt.getNewStatus().isOnline() || evt.getOldStatus().isOnline()) return;

    handleProviderAdded(evt.getProvider(), true);
  }

  @Override
  public void providerStatusMessageChanged(PropertyChangeEvent evt) {}

  @Override
  public void localUserPresenceChanged(LocalUserChatRoomPresenceChangeEvent evt) {
    if (recentQuery == null) return;

    ComparableEvtObj srcContact = null;

    synchronized (recentMessages) {
      for (ComparableEvtObj msg : recentMessages) {
        if (msg.getRoom() != null && msg.getRoom().equals(evt.getChatRoom())) {
          srcContact = msg;
          break;
        }
      }
    }

    if (srcContact == null) return;

    String eventType = evt.getEventType();

    if (LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_JOINED.equals(eventType)) {
      recentQuery.updateContactStatus(srcContact, ChatRoomPresenceStatus.CHAT_ROOM_ONLINE);
    } else if ((LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_LEFT.equals(eventType)
        || LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_KICKED.equals(eventType)
        || LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_DROPPED.equals(eventType))) {
      recentQuery.updateContactStatus(srcContact, ChatRoomPresenceStatus.CHAT_ROOM_OFFLINE);
    }
  }

  /**
   * Handles new events.
   *
   * @param obj the event object
   * @param provider the provider
   * @param id the id of the source of the event
   */
  private void handle(EventObject obj, ProtocolProviderService provider, String id) {
    // check if provider - contact exist update message content
    synchronized (recentMessages) {
      ComparableEvtObj existingMsc = null;
      for (ComparableEvtObj msc : recentMessages) {
        if (msc.getProtocolProviderService().equals(provider)
            && msc.getContactAddress().equals(id)) {
          // update
          msc.update(obj);
          updateRecentMessageToHistory(msc);

          existingMsc = msc;
        }
      }

      if (existingMsc != null) {
        Collections.sort(recentMessages);
        oldestRecentMessage = recentMessages.get(recentMessages.size() - 1).getTimestamp();

        if (recentQuery != null) {
          recentQuery.updateContact(existingMsc, existingMsc.getEventObject());
          recentQuery.fireContactChanged(existingMsc);
        }

        return;
      }

      // if missing create source contact
      // and update recent messages, trim and sort
      MessageSourceContact newSourceContact =
          new MessageSourceContact(obj, MessageSourceService.this);
      newSourceContact.initDetails(obj);
      // we have already checked for duplicate
      ComparableEvtObj newMsg = new ComparableEvtObj(obj);
      recentMessages.add(newMsg);

      Collections.sort(recentMessages);
      oldestRecentMessage = recentMessages.get(recentMessages.size() - 1).getTimestamp();

      // trim
      List<ComparableEvtObj> removedItems = null;
      if (recentMessages.size() > numberOfMessages) {
        removedItems =
            new ArrayList<ComparableEvtObj>(
                recentMessages.subList(numberOfMessages, recentMessages.size()));

        recentMessages.removeAll(removedItems);
      }

      // save
      saveRecentMessageToHistory(newMsg);

      // no query nothing to fire
      if (recentQuery == null) return;

      // now fire
      if (removedItems != null) {
        for (ComparableEvtObj msc : removedItems) {
          recentQuery.fireContactRemoved(msc);
        }
      }

      recentQuery.addQueryResult(newSourceContact);
    }
  }

  /** Adds recent message in history. */
  private void saveRecentMessageToHistory(ComparableEvtObj msc) {
    synchronized (historyID) {
      // and create it
      try {
        History history = getHistory();
        HistoryWriter writer = history.getWriter();

        SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT);

        writer.addRecord(
            new String[] {
              msc.getProtocolProviderService().getAccountID().getAccountUniqueID(),
              msc.getContactAddress(),
              sdf.format(msc.getTimestamp()),
              RECENT_MSGS_VER
            },
            NUMBER_OF_MSGS_IN_HISTORY);
      } catch (IOException ex) {
        logger.error("cannot create recent_messages history", ex);
        return;
      }
    }
  }

  /** Updates recent message in history. */
  private void updateRecentMessageToHistory(final ComparableEvtObj msg) {
    synchronized (historyID) {
      // and create it
      try {
        History history = getHistory();

        HistoryWriter writer = history.getWriter();

        writer.updateRecord(
            new HistoryWriter.HistoryRecordUpdater() {
              HistoryRecord hr;

              @Override
              public void setHistoryRecord(HistoryRecord historyRecord) {
                this.hr = historyRecord;
              }

              @Override
              public boolean isMatching() {
                boolean providerFound = false;
                boolean contactFound = false;
                for (int i = 0; i < hr.getPropertyNames().length; i++) {
                  String propName = hr.getPropertyNames()[i];

                  if (propName.equals(STRUCTURE_NAMES[0])) {
                    if (msg.getProtocolProviderService()
                        .getAccountID()
                        .getAccountUniqueID()
                        .equals(hr.getPropertyValues()[i])) {
                      providerFound = true;
                    }
                  } else if (propName.equals(STRUCTURE_NAMES[1])) {
                    if (msg.getContactAddress().equals(hr.getPropertyValues()[i])) {
                      contactFound = true;
                    }
                  }
                }

                return contactFound && providerFound;
              }

              @Override
              public Map<String, String> getUpdateChanges() {
                HashMap<String, String> map = new HashMap<String, String>();
                SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT);

                for (int i = 0; i < hr.getPropertyNames().length; i++) {
                  String propName = hr.getPropertyNames()[i];

                  if (propName.equals(STRUCTURE_NAMES[0])) {
                    map.put(
                        propName,
                        msg.getProtocolProviderService().getAccountID().getAccountUniqueID());
                  } else if (propName.equals(STRUCTURE_NAMES[1])) {
                    map.put(propName, msg.getContactAddress());
                  } else if (propName.equals(STRUCTURE_NAMES[2])) {
                    map.put(propName, sdf.format(msg.getTimestamp()));
                  } else if (propName.equals(STRUCTURE_NAMES[3]))
                    map.put(propName, RECENT_MSGS_VER);
                }

                return map;
              }
            });
      } catch (IOException ex) {
        logger.error("cannot create recent_messages history", ex);
        return;
      }
    }
  }

  @Override
  public void messageReceived(MessageReceivedEvent evt) {
    if (isSMSEnabled && evt.getEventType() != MessageReceivedEvent.SMS_MESSAGE_RECEIVED) {
      return;
    }

    handle(evt, evt.getSourceContact().getProtocolProvider(), evt.getSourceContact().getAddress());
  }

  @Override
  public void messageDelivered(MessageDeliveredEvent evt) {
    if (isSMSEnabled && !evt.isSmsMessage()) return;

    handle(
        evt,
        evt.getDestinationContact().getProtocolProvider(),
        evt.getDestinationContact().getAddress());
  }

  /**
   * Not used.
   *
   * @param evt the <tt>MessageFailedEvent</tt> containing the ID of the
   */
  @Override
  public void messageDeliveryFailed(MessageDeliveryFailedEvent evt) {}

  @Override
  public void messageReceived(ChatRoomMessageReceivedEvent evt) {
    if (isSMSEnabled) return;

    // ignore non conversation messages
    if (evt.getEventType() != ChatRoomMessageReceivedEvent.CONVERSATION_MESSAGE_RECEIVED) return;

    handle(
        evt, evt.getSourceChatRoom().getParentProvider(), evt.getSourceChatRoom().getIdentifier());
  }

  @Override
  public void messageDelivered(ChatRoomMessageDeliveredEvent evt) {
    if (isSMSEnabled) return;

    handle(
        evt, evt.getSourceChatRoom().getParentProvider(), evt.getSourceChatRoom().getIdentifier());
  }

  /**
   * Not used.
   *
   * @param evt the <tt>ChatroomMessageDeliveryFailedEvent</tt> containing
   */
  @Override
  public void messageDeliveryFailed(ChatRoomMessageDeliveryFailedEvent evt) {}

  @Override
  public void messageReceived(AdHocChatRoomMessageReceivedEvent evt) {
    // TODO
  }

  @Override
  public void messageDelivered(AdHocChatRoomMessageDeliveredEvent evt) {
    // TODO
  }

  /**
   * Not used.
   *
   * @param evt the <tt>AdHocChatroomMessageDeliveryFailedEvent</tt>
   */
  @Override
  public void messageDeliveryFailed(AdHocChatRoomMessageDeliveryFailedEvent evt) {}

  @Override
  public void subscriptionCreated(SubscriptionEvent evt) {}

  @Override
  public void subscriptionFailed(SubscriptionEvent evt) {}

  @Override
  public void subscriptionRemoved(SubscriptionEvent evt) {}

  @Override
  public void subscriptionMoved(SubscriptionMovedEvent evt) {}

  @Override
  public void subscriptionResolved(SubscriptionEvent evt) {}

  /**
   * If a contact is renamed update the locally stored message if any.
   *
   * @param evt the <tt>ContactPropertyChangeEvent</tt> containing the source
   */
  @Override
  public void contactModified(ContactPropertyChangeEvent evt) {
    if (!evt.getPropertyName().equals(ContactPropertyChangeEvent.PROPERTY_DISPLAY_NAME)) return;

    Contact contact = evt.getSourceContact();

    if (contact == null) return;

    for (ComparableEvtObj msc : recentMessages) {
      if (contact.equals(msc.getContact())) {
        if (recentQuery != null)
          recentQuery.updateContactDisplayName(msc, contact.getDisplayName());

        return;
      }
    }
  }

  /**
   * Indicates that a MetaContact has been modified.
   *
   * @param evt the MetaContactListEvent containing the corresponding contact
   */
  public void metaContactRenamed(MetaContactRenamedEvent evt) {
    for (ComparableEvtObj msc : recentMessages) {
      if (evt.getSourceMetaContact().containsContact(msc.getContact())) {
        if (recentQuery != null) recentQuery.updateContactDisplayName(msc, evt.getNewDisplayName());
      }
    }
  }

  @Override
  public void supportedOperationSetsChanged(ContactCapabilitiesEvent event) {
    Contact contact = event.getSourceContact();

    if (contact == null) return;

    for (ComparableEvtObj msc : recentMessages) {
      if (contact.equals(msc.getContact())) {
        if (recentQuery != null) recentQuery.updateCapabilities(msc, contact);

        return;
      }
    }
  }

  /** Permanently removes all locally stored message history, remove recent contacts. */
  public void eraseLocallyStoredHistory() throws IOException {
    List<ComparableEvtObj> toRemove = null;
    synchronized (recentMessages) {
      toRemove = new ArrayList<ComparableEvtObj>(recentMessages);

      recentMessages.clear();
    }

    if (recentQuery != null) {
      for (ComparableEvtObj msc : toRemove) {
        recentQuery.fireContactRemoved(msc);
      }
    }
  }

  /**
   * Permanently removes locally stored message history for the metacontact, remove any recent
   * contacts if any.
   */
  public void eraseLocallyStoredHistory(MetaContact contact) throws IOException {
    List<ComparableEvtObj> toRemove = null;
    synchronized (recentMessages) {
      toRemove = new ArrayList<ComparableEvtObj>();
      Iterator<Contact> iter = contact.getContacts();
      while (iter.hasNext()) {
        Contact item = iter.next();
        String id = item.getAddress();
        ProtocolProviderService provider = item.getProtocolProvider();

        for (ComparableEvtObj msc : recentMessages) {
          if (msc.getProtocolProviderService().equals(provider)
              && msc.getContactAddress().equals(id)) {
            toRemove.add(msc);
          }
        }
      }

      recentMessages.removeAll(toRemove);
    }
    if (recentQuery != null) {
      for (ComparableEvtObj msc : toRemove) {
        recentQuery.fireContactRemoved(msc);
      }
    }
  }

  /**
   * Permanently removes locally stored message history for the chatroom, remove any recent contacts
   * if any.
   */
  public void eraseLocallyStoredHistory(ChatRoom room) {
    ComparableEvtObj toRemove = null;
    synchronized (recentMessages) {
      for (ComparableEvtObj msg : recentMessages) {
        if (msg.getRoom() != null && msg.getRoom().equals(room)) {
          toRemove = msg;
          break;
        }
      }

      if (toRemove == null) return;

      recentMessages.remove(toRemove);
    }

    if (recentQuery != null) recentQuery.fireContactRemoved(toRemove);
  }

  /** Object used to cache recent messages. */
  private class ComparableEvtObj implements Comparable<ComparableEvtObj> {
    private EventObject eventObject;

    /** The protocol provider. */
    private ProtocolProviderService ppService = null;

    /** The address. */
    private String address = null;

    /** The timestamp. */
    private Date timestamp = null;

    /** The contact instance. */
    private Contact contact = null;

    /** The room instance. */
    private ChatRoom room = null;

    /**
     * Constructs.
     *
     * @param source used to extract initial values.
     */
    ComparableEvtObj(EventObject source) {
      update(source);
    }

    /**
     * Extract values from <tt>EventObject</tt>.
     *
     * @param source
     */
    public void update(EventObject source) {
      this.eventObject = source;

      if (source instanceof MessageDeliveredEvent) {
        MessageDeliveredEvent e = (MessageDeliveredEvent) source;

        this.contact = e.getDestinationContact();

        this.address = contact.getAddress();
        this.ppService = contact.getProtocolProvider();
        this.timestamp = e.getTimestamp();
      } else if (source instanceof MessageReceivedEvent) {
        MessageReceivedEvent e = (MessageReceivedEvent) source;

        this.contact = e.getSourceContact();

        this.address = contact.getAddress();
        this.ppService = contact.getProtocolProvider();
        this.timestamp = e.getTimestamp();
      } else if (source instanceof ChatRoomMessageDeliveredEvent) {
        ChatRoomMessageDeliveredEvent e = (ChatRoomMessageDeliveredEvent) source;

        this.room = e.getSourceChatRoom();

        this.address = room.getIdentifier();
        this.ppService = room.getParentProvider();
        this.timestamp = e.getTimestamp();
      } else if (source instanceof ChatRoomMessageReceivedEvent) {
        ChatRoomMessageReceivedEvent e = (ChatRoomMessageReceivedEvent) source;

        this.room = e.getSourceChatRoom();

        this.address = room.getIdentifier();
        this.ppService = room.getParentProvider();
        this.timestamp = e.getTimestamp();
      }
    }

    @Override
    public String toString() {
      return "ComparableEvtObj{" + "address='" + address + '\'' + ", ppService=" + ppService + '}';
    }

    /**
     * The timestamp of the message.
     *
     * @return the timestamp of the message.
     */
    public Date getTimestamp() {
      return timestamp;
    }

    /**
     * The contact.
     *
     * @return the contact.
     */
    public Contact getContact() {
      return contact;
    }

    /**
     * The room.
     *
     * @return the room.
     */
    public ChatRoom getRoom() {
      return room;
    }

    /**
     * The protocol provider.
     *
     * @return the protocol provider.
     */
    public ProtocolProviderService getProtocolProviderService() {
      return ppService;
    }

    /**
     * The address.
     *
     * @return the address.
     */
    public String getContactAddress() {
      if (this.address != null) return this.address;

      return null;
    }

    /**
     * The event object.
     *
     * @return the event object.
     */
    public EventObject getEventObject() {
      return eventObject;
    }

    /**
     * Compares two ComparableEvtObj.
     *
     * @param o the object to compare with
     * @return 0, less than zero, greater than zero, if equals, less or greater.
     */
    @Override
    public int compareTo(ComparableEvtObj o) {
      if (o == null || o.getTimestamp() == null) return 1;

      return o.getTimestamp().compareTo(getTimestamp());
    }

    /**
     * Checks if equals, and if this event object is used to create a MessageSourceContact, if the
     * supplied <tt>Object</tt> is instance of MessageSourceContact.
     *
     * @param o the object to check.
     * @return <tt>true</tt> if equals.
     */
    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || (!(o instanceof MessageSourceContact) && getClass() != o.getClass()))
        return false;

      if (o instanceof ComparableEvtObj) {
        ComparableEvtObj that = (ComparableEvtObj) o;

        if (!address.equals(that.address)) return false;
        if (!ppService.equals(that.ppService)) return false;
      } else if (o instanceof MessageSourceContact) {
        MessageSourceContact that = (MessageSourceContact) o;

        if (!address.equals(that.getContactAddress())) return false;
        if (!ppService.equals(that.getProtocolProviderService())) return false;
      } else return false;

      return true;
    }

    @Override
    public int hashCode() {
      int result = address.hashCode();
      result = 31 * result + ppService.hashCode();
      return result;
    }
  }
}
/**
 * Implements <tt>BundleActivator</tt> for the neomedia bundle.
 *
 * @author Martin Andre
 * @author Emil Ivov
 * @author Lyubomir Marinov
 * @author Boris Grozev
 */
public class NeomediaActivator implements BundleActivator {

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

  /** Indicates if the audio configuration form should be disabled, i.e. not visible to the user. */
  private static final String AUDIO_CONFIG_DISABLED_PROP =
      "net.java.sip.communicator.impl.neomedia.AUDIO_CONFIG_DISABLED";

  /** Indicates if the video configuration form should be disabled, i.e. not visible to the user. */
  private static final String VIDEO_CONFIG_DISABLED_PROP =
      "net.java.sip.communicator.impl.neomedia.VIDEO_CONFIG_DISABLED";

  /** Indicates if the H.264 configuration form should be disabled, i.e. not visible to the user. */
  private static final String H264_CONFIG_DISABLED_PROP =
      "net.java.sip.communicator.impl.neomedia.h264config.DISABLED";

  /** Indicates if the ZRTP configuration form should be disabled, i.e. not visible to the user. */
  private static final String ZRTP_CONFIG_DISABLED_PROP =
      "net.java.sip.communicator.impl.neomedia.zrtpconfig.DISABLED";

  /**
   * Indicates if the call recording config form should be disabled, i.e. not visible to the user.
   */
  private static final String CALL_RECORDING_CONFIG_DISABLED_PROP =
      "net.java.sip.communicator.impl.neomedia.callrecordingconfig.DISABLED";

  /**
   * The name of the notification pop-up event displayed when the device configration has changed.
   */
  private static final String DEVICE_CONFIGURATION_HAS_CHANGED = "DeviceConfigurationChanged";

  /**
   * The context in which the one and only <tt>NeomediaActivator</tt> instance has started
   * executing.
   */
  private static BundleContext bundleContext;

  /**
   * The <tt>ConfigurationService</tt> registered in {@link #bundleContext} and used by the
   * <tt>NeomediaActivator</tt> instance to read and write configuration properties.
   */
  private static ConfigurationService configurationService;

  /**
   * The <tt>FileAccessService</tt> registered in {@link #bundleContext} and used by the
   * <tt>NeomediaActivator</tt> instance to safely access files.
   */
  private static FileAccessService fileAccessService;

  /** The notifcation service to pop-up messages. */
  private static NotificationService notificationService;

  /**
   * The one and only <tt>MediaServiceImpl</tt> instance registered in {@link #bundleContext} by the
   * <tt>NeomediaActivator</tt> instance.
   */
  private static MediaServiceImpl mediaServiceImpl;

  /**
   * The <tt>ResourceManagementService</tt> registered in {@link #bundleContext} and representing
   * the resources such as internationalized and localized text and images used by the neomedia
   * bundle.
   */
  private static ResourceManagementService resources;

  /**
   * The OSGi <tt>PacketLoggingService</tt> of {@link #mediaServiceImpl} in {@link #bundleContext}
   * and used for debugging.
   */
  private static PacketLoggingService packetLoggingService = null;

  /** A listener to the click on the popup message concerning device configuration changes. */
  private AudioDeviceConfigurationListener deviceConfigurationPropertyChangeListener;

  /** A {@link MediaConfigurationService} instance. */
  //    private static MediaConfigurationImpl mediaConfiguration;

  /** The audio configuration form used to define the capture/notify/playback audio devices. */
  private static ConfigurationForm audioConfigurationForm;

  /**
   * Starts the execution of the neomedia bundle in the specified context.
   *
   * @param bundleContext the context in which the neomedia bundle is to start executing
   * @throws Exception if an error occurs while starting the execution of the neomedia bundle in the
   *     specified context
   */
  public void start(BundleContext bundleContext) throws Exception {
    if (logger.isDebugEnabled()) logger.debug("Started.");

    NeomediaActivator.bundleContext = bundleContext;

    // MediaService
    mediaServiceImpl = (MediaServiceImpl) LibJitsi.getMediaService();

    bundleContext.registerService(MediaService.class.getName(), mediaServiceImpl, null);
    if (logger.isDebugEnabled()) logger.debug("Media Service ... [REGISTERED]");

    //        mediaConfiguration = new MediaConfigurationImpl();
    //        bundleContext.registerService(
    //                MediaConfigurationService.class.getStatus(),
    //                getMediaConfiguration(),
    //                null);
    if (logger.isDebugEnabled()) logger.debug("Media Configuration ... [REGISTERED]");

    ConfigurationService cfg = NeomediaActivator.getConfigurationService();
    Dictionary<String, String> mediaProps = new Hashtable<String, String>();

    mediaProps.put(ConfigurationForm.FORM_TYPE, ConfigurationForm.GENERAL_TYPE);

    // If the audio configuration form is disabled don't register it.
    //        if ((cfg == null) || !cfg.getBoolean(AUDIO_CONFIG_DISABLED_PROP, false))
    //        {
    //            audioConfigurationForm
    //                = new LazyConfigurationForm(
    //                        AudioConfigurationPanel.class.getStatus(),
    //                        getClass().getClassLoader(),
    //                        "plugin.mediaconfig.AUDIO_ICON",
    //                        "impl.neomedia.configform.AUDIO",
    //                        3);
    //
    //            bundleContext.registerService(
    //                    ConfigurationForm.class.getStatus(),
    //                    audioConfigurationForm,
    //                    mediaProps);
    //
    //            if (deviceConfigurationPropertyChangeListener == null)
    //            {
    //                // Initializes and registers the changed device configuration
    //                // event ot the notification service.
    //                getNotificationService();
    //
    //                deviceConfigurationPropertyChangeListener
    //                    = new AudioDeviceConfigurationListener();
    //                mediaServiceImpl
    //                    .getDeviceConfiguration()
    //                        .addPropertyChangeListener(
    //                                deviceConfigurationPropertyChangeListener);
    //            }
    //        }

    // If the video configuration form is disabled don't register it.
    //        if ((cfg == null) || !cfg.getBoolean(VIDEO_CONFIG_DISABLED_PROP, false))
    //        {
    //            bundleContext.registerService(
    //                    ConfigurationForm.class.getStatus(),
    //                    new LazyConfigurationForm(
    //                            VideoConfigurationPanel.class.getStatus(),
    //                            getClass().getClassLoader(),
    //                            "plugin.mediaconfig.VIDEO_ICON",
    //                            "impl.neomedia.configform.VIDEO",
    //                            4),
    //                    mediaProps);
    //        }

    // H.264
    // If the H.264 configuration form is disabled don't register it.
    //        if ((cfg == null) || !cfg.getBoolean(H264_CONFIG_DISABLED_PROP, false))
    //        {
    //            Dictionary<String, String> h264Props
    //                = new Hashtable<String, String>();
    //
    //            h264Props.put(
    //                    ConfigurationForm.FORM_TYPE,
    //                    ConfigurationForm.ADVANCED_TYPE);
    //            bundleContext.registerService(
    //                    ConfigurationForm.class.getStatus(),
    //                    new LazyConfigurationForm(
    //                            ConfigurationPanel.class.getStatus(),
    //                            getClass().getClassLoader(),
    //                            "plugin.mediaconfig.VIDEO_ICON",
    //                            "impl.neomedia.configform.H264",
    //                            -1,
    //                            true),
    //                    h264Props);
    //        }

    // ZRTP
    // If the ZRTP configuration form is disabled don't register it.
    //        if ((cfg == null) || !cfg.getBoolean(ZRTP_CONFIG_DISABLED_PROP, false))
    //        {
    //            Dictionary<String, String> securityProps
    //                = new Hashtable<String, String>();
    //
    //            securityProps.put( ConfigurationForm.FORM_TYPE,
    //                            ConfigurationForm.SECURITY_TYPE);
    //            bundleContext.registerService(
    //                ConfigurationForm.class.getStatus(),
    //                new LazyConfigurationForm(
    //                    SecurityConfigForm.class.getStatus(),
    //                    getClass().getClassLoader(),
    //                    "impl.media.security.zrtp.CONF_ICON",
    //                    "impl.media.security.zrtp.TITLE",
    //                    0),
    //                securityProps);
    //        }

    // we use the nist-sdp stack to make parse sdp and we need to set the
    // following property to make sure that it would accept java generated
    // IPv6 addresses that contain address scope zones.
    System.setProperty("gov.nist.core.STRIP_ADDR_SCOPES", "true");

    // AudioNotifierService
    AudioNotifierService audioNotifierService = LibJitsi.getAudioNotifierService();

    audioNotifierService.setMute(
        (cfg == null)
            || !cfg.getBoolean("net.java.sip.communicator" + ".impl.sound.isSoundEnabled", true));
    bundleContext.registerService(AudioNotifierService.class.getName(), audioNotifierService, null);

    if (logger.isInfoEnabled()) logger.info("Audio Notifier Service ...[REGISTERED]");

    // Call Recording
    // If the call recording configuration form is disabled don't continue.
    //        if ((cfg == null)
    //                || !cfg.getBoolean(CALL_RECORDING_CONFIG_DISABLED_PROP, false))
    //        {
    //            Dictionary<String, String> callRecordingProps
    //                = new Hashtable<String, String>();
    //
    //            callRecordingProps.put(
    //                    ConfigurationForm.FORM_TYPE,
    //                    ConfigurationForm.ADVANCED_TYPE);
    //            bundleContext.registerService(
    //                    ConfigurationForm.class.getStatus(),
    //                    new LazyConfigurationForm(
    //                            CallRecordingConfigForm.class.getStatus(),
    //                            getClass().getClassLoader(),
    //                            null,
    //                            "plugin.callrecordingconfig.CALL_RECORDING_CONFIG",
    //                            1100,
    //                            true),
    //                    callRecordingProps);
    //        }
  }

  /**
   * Stops the execution of the neomedia bundle in the specified context.
   *
   * @param bundleContext the context in which the neomedia bundle is to stop executing
   * @throws Exception if an error occurs while stopping the execution of the neomedia bundle in the
   *     specified context
   */
  public void stop(BundleContext bundleContext) throws Exception {
    try {
      if (deviceConfigurationPropertyChangeListener != null) {
        mediaServiceImpl
            .getDeviceConfiguration()
            .removePropertyChangeListener(deviceConfigurationPropertyChangeListener);
        if (deviceConfigurationPropertyChangeListener != null) {
          deviceConfigurationPropertyChangeListener.managePopupMessageListenerRegistration(false);
          deviceConfigurationPropertyChangeListener = null;
        }
      }
    } finally {
      configurationService = null;
      fileAccessService = null;
      mediaServiceImpl = null;
      resources = null;
    }
  }

  /**
   * Returns a reference to a ConfigurationService implementation currently registered in the bundle
   * context or null if no such implementation was found.
   *
   * @return a currently valid implementation of the ConfigurationService.
   */
  public static ConfigurationService getConfigurationService() {
    if (configurationService == null) {
      configurationService = ServiceUtils.getService(bundleContext, ConfigurationService.class);
    }
    return configurationService;
  }

  /**
   * Returns a reference to a FileAccessService implementation currently registered in the bundle
   * context or null if no such implementation was found.
   *
   * @return a currently valid implementation of the FileAccessService .
   */
  public static FileAccessService getFileAccessService() {
    if (fileAccessService == null) {
      fileAccessService = ServiceUtils.getService(bundleContext, FileAccessService.class);
    }
    return fileAccessService;
  }

  /**
   * Gets the <tt>MediaService</tt> implementation instance registered by the neomedia bundle.
   *
   * @return the <tt>MediaService</tt> implementation instance registered by the neomedia bundle
   */
  public static MediaServiceImpl getMediaServiceImpl() {
    return mediaServiceImpl;
  }

  //    public static MediaConfigurationService getMediaConfiguration()
  //    {
  //        return mediaConfiguration;
  //    }

  /**
   * Gets the <tt>ResourceManagementService</tt> instance which represents the resources such as
   * internationalized and localized text and images used by the neomedia bundle.
   *
   * @return the <tt>ResourceManagementService</tt> instance which represents the resources such as
   *     internationalized and localized text and images used by the neomedia bundle
   */
  public static ResourceManagementService getResources() {
    if (resources == null) {
      resources = ResourceManagementServiceUtils.getService(bundleContext);
    }
    return resources;
  }

  /**
   * Returns a reference to the <tt>PacketLoggingService</tt> implementation currently registered in
   * the bundle context or null if no such implementation was found.
   *
   * @return a reference to a <tt>PacketLoggingService</tt> implementation currently registered in
   *     the bundle context or null if no such implementation was found.
   */
  public static PacketLoggingService getPacketLogging() {
    if (packetLoggingService == null) {
      packetLoggingService = ServiceUtils.getService(bundleContext, PacketLoggingService.class);
    }
    return packetLoggingService;
  }

  /**
   * Returns the <tt>NotificationService</tt> obtained from the bundle context.
   *
   * @return The <tt>NotificationService</tt> obtained from the bundle context.
   */
  public static NotificationService getNotificationService() {
    if (notificationService == null) {
      // Get the notification service implementation
      ServiceReference notifReference =
          bundleContext.getServiceReference(NotificationService.class.getName());

      notificationService = (NotificationService) bundleContext.getService(notifReference);

      if (notificationService != null) {
        // Register a popup message for a device configuration changed
        // notification.
        notificationService.registerDefaultNotificationForEvent(
            DEVICE_CONFIGURATION_HAS_CHANGED,
            net.java.sip.communicator.service.notification.NotificationAction.ACTION_POPUP_MESSAGE,
            "Device onfiguration has changed",
            null);
      }
    }

    return notificationService;
  }

  /** A listener to the click on the popup message concerning device configuration changes. */
  private class AudioDeviceConfigurationListener implements PropertyChangeListener /*,
                   SystrayPopupMessageListener*/ {
    /**
     * A boolean used to verify that this listener registers only once to the popup message
     * notification handler.
     */
    private boolean isRegisteredToPopupMessageListener = false;

    /**
     * Registers or unregister as a popup message listener to detect when a user click on
     * notification saying that the device configuration has changed.
     *
     * @param enable True to register to the popup message notifcation handler. False to unregister.
     */
    public void managePopupMessageListenerRegistration(boolean enable) {
      Iterator<NotificationHandler> notificationHandlers =
          notificationService
              .getActionHandlers(
                  net.java.sip.communicator.service.notification.NotificationAction
                      .ACTION_POPUP_MESSAGE)
              .iterator();
      NotificationHandler notificationHandler;
      while (notificationHandlers.hasNext()) {
        notificationHandler = notificationHandlers.next();
        if (notificationHandler instanceof PopupMessageNotificationHandler) {
          // Register.
          if (enable) {
            //                        ((PopupMessageNotificationHandler) notificationHandler)
            //                            .addPopupMessageListener(this);
          }
          // Unregister.
          else {
            //                        ((PopupMessageNotificationHandler) notificationHandler)
            //                            .removePopupMessageListener(this);
          }
        }
      }
    }

    /**
     * Function called when an audio device is plugged or unplugged.
     *
     * @param event The property change event which may concern the audio device.
     */
    public void propertyChange(PropertyChangeEvent event) {
      if (DeviceConfiguration.PROP_AUDIO_SYSTEM_DEVICES.equals(event.getPropertyName())) {
        NotificationService notificationService = getNotificationService();

        if (notificationService != null) {
          // Registers only once to the  popup message notification
          // handler.
          if (!isRegisteredToPopupMessageListener) {
            isRegisteredToPopupMessageListener = true;
            managePopupMessageListenerRegistration(true);
          }

          // Fires the popup notification.
          ResourceManagementService resources = NeomediaActivator.getResources();
          Map<String, Object> extras = new HashMap<String, Object>();

          extras.put(NotificationData.POPUP_MESSAGE_HANDLER_TAG_EXTRA, this);
          notificationService.fireNotification(
              DEVICE_CONFIGURATION_HAS_CHANGED,
              resources.getI18NString("impl.media.configform" + ".AUDIO_DEVICE_CONFIG_CHANGED"),
              resources.getI18NString(
                  "impl.media.configform" + ".AUDIO_DEVICE_CONFIG_MANAGMENT_CLICK"),
              null,
              extras);
        }
      }
    }

    /**
     * Indicates that user has clicked on the systray popup message.
     *
     * @param evt the event triggered when user clicks on the systray popup message
     */
    //        public void popupMessageClicked(SystrayPopupMessageEvent evt)
    //        {
    //            // Checks if this event is fired from one click on one of our popup
    //            // message.
    //            if(evt.getTag() == deviceConfigurationPropertyChangeListener)
    //            {
    //                // Get the UI service
    //                ServiceReference uiReference = bundleContext
    //                    .getServiceReference(UIService.class.getStatus());
    //
    //                UIService uiService = (UIService) bundleContext
    //                    .getService(uiReference);
    //
    //                if(uiService != null)
    //                {
    //                    // Shows the audio configuration window.
    //                    ConfigurationContainer configurationContainer
    //                        = uiService.getConfigurationContainer();
    //                    configurationContainer.setSelected(audioConfigurationForm);
    //                    configurationContainer.setVisible(true);
    //                }
    //            }
    //        }
  }

  public static BundleContext getBundleContext() {
    return bundleContext;
  }
}
Ejemplo n.º 12
0
/**
 * Keeps track of entity capabilities.
 *
 * <p>This work is based on Jonas Adahl's smack fork.
 *
 * @author Emil Ivov
 * @author Lyubomir Marinov
 */
public class EntityCapsManager {
  /**
   * The <tt>Logger</tt> used by the <tt>EntityCapsManager</tt> class and its instances for logging
   * output.
   */
  private static final Logger logger = Logger.getLogger(EntityCapsManager.class);

  /** Static OSGi bundle context used by this class. */
  private static BundleContext bundleContext;

  /** Configuration service instance used by this class. */
  private static ConfigurationService configService;

  /**
   * The prefix of the <tt>ConfigurationService</tt> properties which persist {@link
   * #caps2discoverInfo}.
   */
  private static final String CAPS_PROPERTY_NAME_PREFIX =
      "net.java.sip.communicator.impl.protocol.jabber.extensions.caps." + "EntityCapsManager.CAPS.";

  /**
   * An empty array of <tt>UserCapsNodeListener</tt> elements explicitly defined in order to reduce
   * unnecessary allocations.
   */
  private static final UserCapsNodeListener[] NO_USER_CAPS_NODE_LISTENERS =
      new UserCapsNodeListener[0];

  /** The node value to advertise. */
  private static String entityNode =
      OSUtils.IS_ANDROID ? "http://android.jitsi.org" : "http://jitsi.org";

  /**
   * The <tt>Map</tt> of <tt>Caps</tt> to <tt>DiscoverInfo</tt> which associates a node#ver with the
   * entity capabilities so that they don't have to be retrieved every time their necessary. Because
   * ver is constructed from the entity capabilities using a specific hash method, the hash method
   * is also associated with the entity capabilities along with the node and the ver in order to
   * disambiguate cases of equal ver values for different entity capabilities constructed using
   * different hash methods.
   */
  private static final Map<Caps, DiscoverInfo> caps2discoverInfo =
      new ConcurrentHashMap<Caps, DiscoverInfo>();

  /**
   * Map of Full JID -&gt; DiscoverInfo/null. In case of c2s connection the key is formed as
   * user@server/resource (resource is required) In case of link-local connection the key is formed
   * as user@host (no resource)
   */
  private final Map<String, Caps> userCaps = new ConcurrentHashMap<String, Caps>();

  /** CapsVerListeners gets notified when the version string is changed. */
  private final Set<CapsVerListener> capsVerListeners = new CopyOnWriteArraySet<CapsVerListener>();

  /** The current hash of our version and supported features. */
  private String currentCapsVersion = null;

  /**
   * The list of <tt>UserCapsNodeListener</tt>s interested in events notifying about changes in the
   * list of user caps nodes of this <tt>EntityCapsManager</tt>.
   */
  private final List<UserCapsNodeListener> userCapsNodeListeners =
      new LinkedList<UserCapsNodeListener>();

  static {
    ProviderManager.getInstance()
        .addExtensionProvider(
            CapsPacketExtension.ELEMENT_NAME, CapsPacketExtension.NAMESPACE, new CapsProvider());
  }

  /**
   * Add {@link DiscoverInfo} to our caps database.
   *
   * <p><b>Warning</b>: The specified <tt>DiscoverInfo</tt> is trusted to be valid with respect to
   * the specified <tt>Caps</tt> for performance reasons because the <tt>DiscoverInfo</tt> should
   * have already been validated in order to be used elsewhere anyway.
   *
   * @param caps the <tt>Caps<tt/> i.e. the node, the hash and the ver for which a
   *     <tt>DiscoverInfo</tt> is to be added to our caps database.
   * @param info {@link DiscoverInfo} for the specified <tt>Caps</tt>.
   */
  public static void addDiscoverInfoByCaps(Caps caps, DiscoverInfo info) {
    cleanupDiscoverInfo(info);
    /*
     * DiscoverInfo carries the node we're now associating it with a
     * specific node so we'd better keep them in sync.
     */
    info.setNode(caps.getNodeVer());

    synchronized (caps2discoverInfo) {
      DiscoverInfo oldInfo = caps2discoverInfo.put(caps, info);

      /*
       * If the specified info is a new association for the specified
       * node, remember it across application instances in order to not
       * query for it over the network.
       */
      if ((oldInfo == null) || !oldInfo.equals(info)) {
        String xml = info.getChildElementXML();

        if ((xml != null) && (xml.length() != 0)) {
          getConfigService().setProperty(getCapsPropertyName(caps), xml);
        }
      }
    }
  }

  /**
   * Gets the name of the property in the <tt>ConfigurationService</tt> which is or is to be
   * associated with a specific <tt>Caps</tt> value.
   *
   * @param caps the <tt>Caps</tt> value for which the associated <tt>ConfigurationService</tt>
   *     property name is to be returned
   * @return the name of the property in the <tt>ConfigurationService</tt> which is or is to be
   *     associated with a specific <tt>Caps</tt> value
   */
  private static String getCapsPropertyName(Caps caps) {
    return CAPS_PROPERTY_NAME_PREFIX + caps.node + '#' + caps.hash + '#' + caps.ver;
  }

  /** Returns cached instance of {@link ConfigurationService}. */
  private static ConfigurationService getConfigService() {
    if (configService == null) {
      configService = ServiceUtils.getService(bundleContext, ConfigurationService.class);
    }
    return configService;
  }

  /**
   * Sets OSGi bundle context instance that will be used by this class.
   *
   * @param bundleContext the <tt>BundleContext</tt> instance to be used by this class or
   *     <tt>null</tt> to clear the reference.
   */
  public static void setBundleContext(BundleContext bundleContext) {
    if (bundleContext == null) {
      configService = null;
    }
    EntityCapsManager.bundleContext = bundleContext;
  }

  /**
   * Add a record telling what entity caps node a user has.
   *
   * @param user the user (Full JID)
   * @param node the node (of the caps packet extension)
   * @param hash the hashing algorithm used to calculate <tt>ver</tt>
   * @param ver the version (of the caps packet extension)
   * @param ext the ext (of the caps packet extension)
   * @param online indicates if the user is online
   */
  private void addUserCapsNode(
      String user, String node, String hash, String ver, String ext, boolean online) {
    if ((user != null) && (node != null) && (hash != null) && (ver != null)) {
      Caps caps = userCaps.get(user);

      if ((caps == null)
          || !caps.node.equals(node)
          || !caps.hash.equals(hash)
          || !caps.ver.equals(ver)) {
        caps = new Caps(node, hash, ver, ext);

        userCaps.put(user, caps);
      } else return;

      // Fire userCapsNodeAdded.
      UserCapsNodeListener[] listeners;

      synchronized (userCapsNodeListeners) {
        listeners = userCapsNodeListeners.toArray(NO_USER_CAPS_NODE_LISTENERS);
      }
      if (listeners.length != 0) {
        String nodeVer = caps.getNodeVer();

        for (UserCapsNodeListener listener : listeners)
          listener.userCapsNodeAdded(user, nodeVer, online);
      }
    }
  }

  /**
   * Adds a specific <tt>UserCapsNodeListener</tt> to the list of <tt>UserCapsNodeListener</tt>s
   * interested in events notifying about changes in the list of user caps nodes of this
   * <tt>EntityCapsManager</tt>.
   *
   * @param listener the <tt>UserCapsNodeListener</tt> which is interested in events notifying about
   *     changes in the list of user caps nodes of this <tt>EntityCapsManager</tt>
   */
  public void addUserCapsNodeListener(UserCapsNodeListener listener) {
    if (listener == null) throw new NullPointerException("listener");
    synchronized (userCapsNodeListeners) {
      if (!userCapsNodeListeners.contains(listener)) userCapsNodeListeners.add(listener);
    }
  }

  /**
   * Remove records telling what entity caps node a contact has.
   *
   * @param contact the contact
   */
  public void removeContactCapsNode(Contact contact) {
    Caps caps = null;
    String lastRemovedJid = null;

    Iterator<String> iter = userCaps.keySet().iterator();
    while (iter.hasNext()) {
      String jid = iter.next();

      if (StringUtils.parseBareAddress(jid).equals(contact.getAddress())) {
        caps = userCaps.get(jid);
        lastRemovedJid = jid;
        iter.remove();
      }
    }

    // fire only for the last one, at the end the event out
    // of the protocol will be one and for the contact
    if (caps != null) {
      UserCapsNodeListener[] listeners;
      synchronized (userCapsNodeListeners) {
        listeners = userCapsNodeListeners.toArray(NO_USER_CAPS_NODE_LISTENERS);
      }
      if (listeners.length != 0) {
        String nodeVer = caps.getNodeVer();

        for (UserCapsNodeListener listener : listeners)
          listener.userCapsNodeRemoved(lastRemovedJid, nodeVer, false);
      }
    }
  }

  /**
   * Remove a record telling what entity caps node a user has.
   *
   * @param user the user (Full JID)
   */
  public void removeUserCapsNode(String user) {
    Caps caps = userCaps.remove(user);

    // Fire userCapsNodeRemoved.
    if (caps != null) {
      UserCapsNodeListener[] listeners;

      synchronized (userCapsNodeListeners) {
        listeners = userCapsNodeListeners.toArray(NO_USER_CAPS_NODE_LISTENERS);
      }
      if (listeners.length != 0) {
        String nodeVer = caps.getNodeVer();

        for (UserCapsNodeListener listener : listeners)
          listener.userCapsNodeRemoved(user, nodeVer, false);
      }
    }
  }

  /**
   * Removes a specific <tt>UserCapsNodeListener</tt> from the list of
   * <tt>UserCapsNodeListener</tt>s interested in events notifying about changes in the list of user
   * caps nodes of this <tt>EntityCapsManager</tt>.
   *
   * @param listener the <tt>UserCapsNodeListener</tt> which is no longer interested in events
   *     notifying about changes in the list of user caps nodes of this <tt>EntityCapsManager</tt>
   */
  public void removeUserCapsNodeListener(UserCapsNodeListener listener) {
    if (listener != null) {
      synchronized (userCapsNodeListeners) {
        userCapsNodeListeners.remove(listener);
      }
    }
  }

  /**
   * Gets the <tt>Caps</tt> i.e. the node, the hash and the ver of a user.
   *
   * @param user the user (Full JID)
   * @return the <tt>Caps</tt> i.e. the node, the hash and the ver of <tt>user</tt>
   */
  public Caps getCapsByUser(String user) {
    return userCaps.get(user);
  }

  /**
   * Get the discover info given a user name. The discover info is returned if the user has a
   * node#ver associated with it and the node#ver has a discover info associated with it.
   *
   * @param user user name (Full JID)
   * @return the discovered info
   */
  public DiscoverInfo getDiscoverInfoByUser(String user) {
    Caps caps = userCaps.get(user);

    return (caps == null) ? null : getDiscoverInfoByCaps(caps);
  }

  /**
   * Get our own caps version.
   *
   * @return our own caps version
   */
  public String getCapsVersion() {
    return currentCapsVersion;
  }

  /**
   * Get our own entity node.
   *
   * @return our own entity node.
   */
  public String getNode() {
    return entityNode;
  }

  /**
   * Set our own entity node.
   *
   * @param node the new node
   */
  public void setNode(String node) {
    entityNode = node;
  }

  /**
   * Retrieve DiscoverInfo for a specific node.
   *
   * @param caps the <tt>Caps</tt> i.e. the node, the hash and the ver
   * @return The corresponding DiscoverInfo or null if none is known.
   */
  public static DiscoverInfo getDiscoverInfoByCaps(Caps caps) {
    synchronized (caps2discoverInfo) {
      DiscoverInfo discoverInfo = caps2discoverInfo.get(caps);

      /*
       * If we don't have the discoverInfo in the runtime cache yet, we
       * may have it remembered in a previous application instance.
       */
      if (discoverInfo == null) {
        ConfigurationService configurationService = getConfigService();
        String capsPropertyName = getCapsPropertyName(caps);
        String xml = configurationService.getString(capsPropertyName);

        if ((xml != null) && (xml.length() != 0)) {
          IQProvider discoverInfoProvider =
              (IQProvider)
                  ProviderManager.getInstance()
                      .getIQProvider("query", "http://jabber.org/protocol/disco#info");

          if (discoverInfoProvider != null) {
            XmlPullParser parser = new MXParser();

            try {
              parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
              parser.setInput(new StringReader(xml));
              // Start the parser.
              parser.next();
            } catch (XmlPullParserException xppex) {
              parser = null;
            } catch (IOException ioex) {
              parser = null;
            }

            if (parser != null) {
              try {
                discoverInfo = (DiscoverInfo) discoverInfoProvider.parseIQ(parser);
              } catch (Exception ex) {
              }

              if (discoverInfo != null) {
                if (caps.isValid(discoverInfo)) caps2discoverInfo.put(caps, discoverInfo);
                else {
                  logger.error(
                      "Invalid DiscoverInfo for " + caps.getNodeVer() + ": " + discoverInfo);
                  /*
                   * The discoverInfo doesn't seem valid
                   * according to the caps which means that we
                   * must have stored invalid information.
                   * Delete the invalid information in order
                   * to not try to validate it again.
                   */
                  configurationService.removeProperty(capsPropertyName);
                }
              }
            }
          }
        }
      }
      return discoverInfo;
    }
  }

  /**
   * Removes from, to and packet-id from <tt>info</tt>.
   *
   * @param info the {@link DiscoverInfo} that we'd like to cleanup.
   */
  private static void cleanupDiscoverInfo(DiscoverInfo info) {
    info.setFrom(null);
    info.setTo(null);
    info.setPacketID(null);
  }

  /**
   * Gets the features of a specific <tt>DiscoverInfo</tt> in the form of a read-only
   * <tt>Feature</tt> <tt>Iterator<tt/> by calling the internal method {@link
   * DiscoverInfo#getFeatures()}.
   *
   * @param discoverInfo the <tt>DiscoverInfo</tt> the features of which are to be retrieved
   * @return a read-only <tt>Feature</tt> <tt>Iterator</tt> which lists the features of the
   *     specified <tt>discoverInfo</tt>
   */
  @SuppressWarnings("unchecked")
  private static Iterator<DiscoverInfo.Feature> getDiscoverInfoFeatures(DiscoverInfo discoverInfo) {
    Method getFeaturesMethod;

    try {
      getFeaturesMethod = DiscoverInfo.class.getDeclaredMethod("getFeatures");
    } catch (NoSuchMethodException nsmex) {
      throw new UndeclaredThrowableException(nsmex);
    }
    getFeaturesMethod.setAccessible(true);
    try {
      return (Iterator<DiscoverInfo.Feature>) getFeaturesMethod.invoke(discoverInfo);
    } catch (IllegalAccessException iaex) {
      throw new UndeclaredThrowableException(iaex);
    } catch (InvocationTargetException itex) {
      throw new UndeclaredThrowableException(itex);
    }
  }

  /**
   * Registers this Manager's listener with <tt>connection</tt>.
   *
   * @param connection the connection that we'd like this manager to register with.
   */
  public void addPacketListener(XMPPConnection connection) {
    PacketFilter filter =
        new AndFilter(
            new PacketTypeFilter(Presence.class),
            new PacketExtensionFilter(
                CapsPacketExtension.ELEMENT_NAME, CapsPacketExtension.NAMESPACE));

    connection.addPacketListener(new CapsPacketListener(), filter);
  }

  /**
   * Adds <tt>listener</tt> to the list of {@link CapsVerListener}s that we notify when new features
   * occur and the version hash needs to be regenerated. The method would also notify
   * <tt>listener</tt> if our current caps version has been generated and is different than
   * <tt>null</tt>.
   *
   * @param listener the {@link CapsVerListener} we'd like to register.
   */
  public void addCapsVerListener(CapsVerListener listener) {
    synchronized (capsVerListeners) {
      if (capsVerListeners.contains(listener)) return;

      capsVerListeners.add(listener);

      if (currentCapsVersion != null) listener.capsVerUpdated(currentCapsVersion);
    }
  }

  /**
   * Removes <tt>listener</tt> from the list of currently registered {@link CapsVerListener}s.
   *
   * @param listener the {@link CapsVerListener} we'd like to unregister.
   */
  public void removeCapsVerListener(CapsVerListener listener) {
    synchronized (capsVerListeners) {
      capsVerListeners.remove(listener);
    }
  }

  /**
   * Notifies all currently registered {@link CapsVerListener}s that the version hash has changed.
   */
  private void fireCapsVerChanged() {
    List<CapsVerListener> listenersCopy = null;

    synchronized (capsVerListeners) {
      listenersCopy = new ArrayList<CapsVerListener>(capsVerListeners);
    }

    for (CapsVerListener listener : listenersCopy) listener.capsVerUpdated(currentCapsVersion);
  }

  /**
   * Computes and returns the hash of the specified <tt>capsString</tt> using the specified
   * <tt>hashAlgorithm</tt>.
   *
   * @param hashAlgorithm the name of the algorithm to be used to generate the hash
   * @param capsString the capabilities string that we'd like to compute a hash for.
   * @return the hash of <tt>capsString</tt> computed by the specified <tt>hashAlgorithm</tt> or
   *     <tt>null</tt> if generating the hash has failed
   */
  private static String capsToHash(String hashAlgorithm, String capsString) {
    try {
      MessageDigest md = MessageDigest.getInstance(hashAlgorithm);
      byte[] digest = md.digest(capsString.getBytes());

      return Base64.encodeBytes(digest);
    } catch (NoSuchAlgorithmException nsae) {
      logger.error("Unsupported XEP-0115: Entity Capabilities hash algorithm: " + hashAlgorithm);
      return null;
    }
  }

  /**
   * Converts the form field values in the <tt>ffValuesIter</tt> into a caps string.
   *
   * @param ffValuesIter the {@link Iterator} containing the form field values.
   * @param capsBldr a <tt>StringBuilder</tt> to which the caps string representing the form field
   *     values is to be appended
   */
  private static void formFieldValuesToCaps(Iterator<String> ffValuesIter, StringBuilder capsBldr) {
    SortedSet<String> fvs = new TreeSet<String>();

    while (ffValuesIter.hasNext()) fvs.add(ffValuesIter.next());

    for (String fv : fvs) capsBldr.append(fv).append('<');
  }

  /**
   * Calculates the <tt>String</tt> for a specific <tt>DiscoverInfo</tt> which is to be hashed in
   * order to compute the ver string for that <tt>DiscoverInfo</tt>.
   *
   * @param discoverInfo the <tt>DiscoverInfo</tt> for which the <tt>String</tt> to be hashed in
   *     order to compute its ver string is to be calculated
   * @return the <tt>String</tt> for <tt>discoverInfo</tt> which is to be hashed in order to compute
   *     its ver string
   */
  private static String calculateEntityCapsString(DiscoverInfo discoverInfo) {
    StringBuilder bldr = new StringBuilder();

    // Add identities
    {
      Iterator<DiscoverInfo.Identity> identities = discoverInfo.getIdentities();
      SortedSet<DiscoverInfo.Identity> is =
          new TreeSet<DiscoverInfo.Identity>(
              new Comparator<DiscoverInfo.Identity>() {
                public int compare(DiscoverInfo.Identity i1, DiscoverInfo.Identity i2) {
                  int category = i1.getCategory().compareTo(i2.getCategory());

                  if (category != 0) return category;

                  int type = i1.getType().compareTo(i2.getType());

                  if (type != 0) return type;

                  /*
                   * TODO Sort by xml:lang.
                   *
                   * Since sort by xml:lang is currently missing,
                   * use the last supported sort criterion i.e.
                   * type.
                   */
                  return type;
                }
              });

      if (identities != null) while (identities.hasNext()) is.add(identities.next());

      for (DiscoverInfo.Identity i : is) {
        bldr.append(i.getCategory())
            .append('/')
            .append(i.getType())
            .append("//")
            .append(i.getName())
            .append('<');
      }
    }

    // Add features
    {
      Iterator<DiscoverInfo.Feature> features = getDiscoverInfoFeatures(discoverInfo);
      SortedSet<String> fs = new TreeSet<String>();

      if (features != null) while (features.hasNext()) fs.add(features.next().getVar());

      for (String f : fs) bldr.append(f).append('<');
    }

    DataForm extendedInfo = (DataForm) discoverInfo.getExtension("x", "jabber:x:data");

    if (extendedInfo != null) {
      synchronized (extendedInfo) {
        SortedSet<FormField> fs =
            new TreeSet<FormField>(
                new Comparator<FormField>() {
                  public int compare(FormField f1, FormField f2) {
                    return f1.getVariable().compareTo(f2.getVariable());
                  }
                });

        FormField formType = null;

        for (Iterator<FormField> fieldsIter = extendedInfo.getFields(); fieldsIter.hasNext(); ) {
          FormField f = fieldsIter.next();
          if (!f.getVariable().equals("FORM_TYPE")) fs.add(f);
          else formType = f;
        }

        // Add FORM_TYPE values
        if (formType != null) formFieldValuesToCaps(formType.getValues(), bldr);

        // Add the other values
        for (FormField f : fs) {
          bldr.append(f.getVariable()).append('<');
          formFieldValuesToCaps(f.getValues(), bldr);
        }
      }
    }

    return bldr.toString();
  }

  /**
   * Calculates the ver string for the specified <tt>discoverInfo</tt>, identity type, name
   * features, and extendedInfo.
   *
   * @param discoverInfo the {@link DiscoverInfo} we'd be creating a ver <tt>String</tt> for
   */
  public void calculateEntityCapsVersion(DiscoverInfo discoverInfo) {
    setCurrentCapsVersion(
        discoverInfo,
        capsToHash(CapsPacketExtension.HASH_METHOD, calculateEntityCapsString(discoverInfo)));
  }

  /**
   * Set our own caps version.
   *
   * @param discoverInfo the {@link DiscoverInfo} that we'd like to map to the <tt>capsVersion</tt>.
   * @param capsVersion the new caps version
   */
  public void setCurrentCapsVersion(DiscoverInfo discoverInfo, String capsVersion) {
    Caps caps = new Caps(getNode(), CapsPacketExtension.HASH_METHOD, capsVersion, null);

    /*
     * DiscoverInfo carries the node and the ver and we're now setting a new
     * ver so we should update the DiscoveryInfo.
     */
    discoverInfo.setNode(caps.getNodeVer());

    if (!caps.isValid(discoverInfo)) {
      throw new IllegalArgumentException(
          "The specified discoverInfo must be valid with respect"
              + " to the specified capsVersion");
    }

    currentCapsVersion = capsVersion;
    addDiscoverInfoByCaps(caps, discoverInfo);
    fireCapsVerChanged();
  }

  /** The {@link PacketListener} that will be registering incoming caps. */
  private class CapsPacketListener implements PacketListener {
    /**
     * Handles incoming presence packets and maps jids to node#ver strings.
     *
     * @param packet the incoming presence <tt>Packet</tt> to be handled
     * @see PacketListener#processPacket(Packet)
     */
    public void processPacket(Packet packet) {
      CapsPacketExtension ext =
          (CapsPacketExtension)
              packet.getExtension(CapsPacketExtension.ELEMENT_NAME, CapsPacketExtension.NAMESPACE);

      /*
       * Before Version 1.4 of XEP-0115: Entity Capabilities, the 'ver'
       * attribute was generated differently and the 'hash' attribute was
       * absent. The 'ver' attribute in Version 1.3 represents the
       * specific version of the client and thus does not provide a way to
       * validate the DiscoverInfo sent by the client. If
       * EntityCapsManager receives no 'hash' attribute, it will assume
       * the legacy format and will not cache it because the DiscoverInfo
       * to be received from the client later on will not be trustworthy.
       */
      String hash = ext.getHash();

      /* Google Talk web does not set hash but we need it to be cached */
      if (hash == null) hash = "";

      if (hash != null) {
        // Check it the packet indicates  that the user is online. We
        // will use this information to decide if we're going to send
        // the discover info request.
        boolean online = (packet instanceof Presence) && ((Presence) packet).isAvailable();

        if (online) {
          addUserCapsNode(
              packet.getFrom(), ext.getNode(), hash, ext.getVersion(), ext.getExtensions(), online);
        } else {
          removeUserCapsNode(packet.getFrom());
        }
      }
    }
  }

  /**
   * Implements an immutable value which stands for a specific node, a specific hash (algorithm) and
   * a specific ver.
   *
   * @author Lyubomir Marinov
   */
  public static class Caps {
    /** The hash (algorithm) of this <tt>Caps</tt> value. */
    public final String hash;

    /** The node of this <tt>Caps</tt> value. */
    public final String node;

    /** The ext info of this <tt>Caps</tt> value. */
    public String ext;

    /**
     * The String which is the concatenation of {@link #node} and the {@link #ver} separated by the
     * character '#'. Cached for the sake of efficiency.
     */
    private final String nodeVer;

    /** The ver of this <tt>Caps</tt> value. */
    public final String ver;

    /**
     * Initializes a new <tt>Caps</tt> instance which is to represent a specific node, a specific
     * hash (algorithm) and a specific ver.
     *
     * @param node the node to be represented by the new instance
     * @param hash the hash (algorithm) to be represented by the new instance
     * @param ver the ver to be represented by the new instance
     * @param ext the ext to be represented by the new instance
     */
    public Caps(String node, String hash, String ver, String ext) {
      if (node == null) throw new NullPointerException("node");
      if (hash == null) throw new NullPointerException("hash");
      if (ver == null) throw new NullPointerException("ver");

      this.node = node;
      this.hash = hash;
      this.ver = ver;
      this.ext = ext;

      this.nodeVer = this.node + '#' + this.ver;
    }

    /**
     * Gets a <tt>String</tt> which represents the concatenation of the <tt>node</tt> property of
     * this instance, the character '#' and the <tt>ver</tt> property of this instance.
     *
     * @return a <tt>String</tt> which represents the concatenation of the <tt>node</tt> property of
     *     this instance, the character '#' and the <tt>ver</tt> property of this instance
     */
    public final String getNodeVer() {
      return nodeVer;
    }

    /**
     * Determines whether a specific <tt>DiscoverInfo</tt> is valid according to this <tt>Caps</tt>
     * i.e. whether the <tt>discoverInfo</tt> has the node and the ver of this <tt>Caps</tt> and the
     * ver calculated from the <tt>discoverInfo</tt> using the hash (algorithm) of this
     * <tt>Caps</tt> is equal to the ver of this <tt>Caps</tt>.
     *
     * @param discoverInfo the <tt>DiscoverInfo</tt> to be validated by this <tt>Caps</tt>
     * @return <tt>true</tt> if the specified <tt>DiscoverInfo</tt> has the node and the ver of this
     *     <tt>Caps</tt> and the ver calculated from the <tt>discoverInfo</tt> using the hash
     *     (algorithm) of this <tt>Caps</tt> is equal to the ver of this <tt>Caps</tt>; otherwise,
     *     <tt>false</tt>
     */
    public boolean isValid(DiscoverInfo discoverInfo) {
      if (discoverInfo != null) {
        // The "node" attribute is not necessary in the query element.
        // For example, Swift does not send back the "node" attribute in
        // the Disco#info response. Thus, if the node of the IQ response
        // is null, then we set it to the request one.
        if (discoverInfo.getNode() == null) {
          discoverInfo.setNode(getNodeVer());
        }

        if (getNodeVer().equals(discoverInfo.getNode())
            && !hash.equals("")
            && ver.equals(capsToHash(hash, calculateEntityCapsString(discoverInfo)))) {
          return true;
        }
      }
      return false;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      Caps caps = (Caps) o;

      if (!hash.equals(caps.hash)) return false;
      if (!node.equals(caps.node)) return false;
      if (!ver.equals(caps.ver)) return false;

      return true;
    }

    @Override
    public int hashCode() {
      int result = hash.hashCode();
      result = 31 * result + node.hashCode();
      result = 31 * result + ver.hashCode();
      return result;
    }
  }
}