/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.java.sip.communicator.impl.protocol.sip;
import java.text.*;
import java.util.*;
import net.java.sip.communicator.service.argdelegation.*;
import net.java.sip.communicator.service.gui.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;
import org.osgi.framework.*;
/**
* The sip implementation of the URI handler. This class handles sip URIs by
* trying to establish a call to them.
*
* @author Emil Ivov
* @author Lubomir Marinov
*/
public class UriHandlerSipImpl
implements UriHandler, ServiceListener, AccountManagerListener
{
/**
* Property to set the amount of time to wait for SIP registration
* to complete before trying to dial a URI from the command line.
* (value in milliseconds).
*/
public static final String INITIAL_REGISTRATION_TIMEOUT_PROP
= "net.java.sip.communicator.impl.protocol.sip.call.INITIAL_REGISTRATION_TIMEOUT";
/**
* Default value for INITIAL_REGISTRATION_TIMEOUT (milliseconds)
*/
public static final long DEFAULT_INITIAL_REGISTRATION_TIMEOUT
= 5000;
/**
* The Logger used by the UriHandlerSipImpl class and its
* instances for logging output.
*/
private static final Logger logger
= Logger.getLogger(UriHandlerSipImpl.class);
/**
* The protocol provider factory that created us.
*/
private final ProtocolProviderFactory protoFactory;
/**
* A reference to the OSGi registration we create with this handler.
*/
private ServiceRegistration ourServiceRegistration = null;
/**
* The object that we are using to synchronize our service registration.
*/
private final Object registrationLock = new Object();
/**
* The AccountManager
which loads the stored accounts of
* {@link #protoFactory} and to be monitored when the mentioned loading is
* complete so that any pending {@link #uris} can be handled
*/
private AccountManager accountManager;
/**
* The indicator (and its synchronization lock) which determines whether the
* stored accounts of {@link #protoFactory} have already been loaded.
*
* Before the loading of the stored accounts (even if there're none) of the
* protoFactory
is complete, no handling of URIs is to be
* performed because there's neither information which account to handle the
* URI in case there're stored accounts available nor ground for warning the
* user a registered account is necessary to handle URIs at all in case
* there're no stored accounts.
*
*/
private final boolean[] storedAccountsAreLoaded = new boolean[1];
/**
* The list of URIs which have received requests for handling before the
* stored accounts of the {@link #protoFactory} have been loaded. They will
* be handled as soon as the mentioned loading completes.
*/
private List uris;
/**
* Creates an instance of this uri handler, so that it would start handling
* URIs by passing them to the providers registered by protoFactory
* .
*
* @param protoFactory the provider that created us.
*
* @throws NullPointerException if protoFactory is null.
*/
public UriHandlerSipImpl(ProtocolProviderFactorySipImpl protoFactory)
throws NullPointerException
{
if (protoFactory == null)
{
throw new NullPointerException(
"The ProtocolProviderFactory that a UriHandler is created with "
+ " cannot be null.");
}
this.protoFactory = protoFactory;
hookStoredAccounts();
this.protoFactory.getBundleContext().addServiceListener(this);
/*
* Registering the UriHandler isn't strictly necessary if the
* requirement to register the protoFactory after creating this instance
* is met.
*/
registerHandlerService();
}
/**
* Disposes of this UriHandler
by, for example, removing the
* listeners it has added in its constructor (in order to prevent memory
* leaks, for one).
*/
public void dispose()
{
protoFactory.getBundleContext().removeServiceListener(this);
unregisterHandlerService();
unhookStoredAccounts();
}
/**
* Sets up (if not set up already) listening for the loading of the stored
* accounts of {@link #protoFactory} in order to make it possible to
* discover when the prerequisites for handling URIs are met.
*/
private void hookStoredAccounts()
{
if (accountManager == null)
{
BundleContext bundleContext = protoFactory.getBundleContext();
accountManager =
(AccountManager) bundleContext.getService(bundleContext
.getServiceReference(AccountManager.class.getName()));
accountManager.addListener(this);
}
}
/**
* Reverts (if not reverted already) the setup performed by a previous call
* to {@link #hookStoredAccounts()}.
*/
private void unhookStoredAccounts()
{
if (accountManager != null)
{
accountManager.removeListener(this);
accountManager = null;
}
}
/*
* (non-Javadoc)
*
* @see
* net.java.sip.communicator.service.protocol.event.AccountManagerListener
* #handleAccountManagerEvent
* (net.java.sip.communicator.service.protocol.event.AccountManagerEvent)
*/
public void handleAccountManagerEvent(AccountManagerEvent event)
{
/*
* When the loading of the stored accounts of protoFactory is complete,
* the prerequisites for handling URIs have been met so it's time to
* load any handling requests which have come before the loading and
* were thus delayed in uris.
*/
if ((AccountManagerEvent.STORED_ACCOUNTS_LOADED == event.getType())
&& (protoFactory == event.getFactory()))
{
List uris = null;
synchronized (storedAccountsAreLoaded)
{
storedAccountsAreLoaded[0] = true;
if (this.uris != null)
{
uris = this.uris;
this.uris = null;
}
}
unhookStoredAccounts();
if (uris != null)
{
for (Iterator uriIter = uris.iterator(); uriIter
.hasNext();)
{
handleUri(uriIter.next());
}
}
}
}
/**
* Registers this UriHandler with the bundle context so that it could start
* handling URIs
*/
public void registerHandlerService()
{
synchronized (registrationLock)
{
if (ourServiceRegistration != null)
{
// ... we are already registered (this is probably
// happening during startup)
return;
}
Hashtable registrationProperties =
new Hashtable();
for (String protocol : getProtocol())
{
registrationProperties.put(UriHandler.PROTOCOL_PROPERTY,
protocol);
}
ourServiceRegistration =
SipActivator.bundleContext.registerService(UriHandler.class
.getName(), this, registrationProperties);
}
}
/**
* Unregisters this UriHandler from the bundle context.
*/
public void unregisterHandlerService()
{
synchronized (registrationLock)
{
if (ourServiceRegistration != null)
{
ourServiceRegistration.unregister();
ourServiceRegistration = null;
}
}
}
/**
* {@inheritDoc}
*/
@Override
public String[] getProtocol()
{
return new String[]
{ "sip", "tel", "callto" };
}
/**
* Parses the specified URI and tries to create a call when online.
*
* @param uri the SIP URI that we have to call.
*/
public void handleUri(final String uri)
{
/*
* TODO If the requirement to register the factory service after
* creating this instance is broken, we'll end up not handling the URIs.
*/
synchronized (storedAccountsAreLoaded)
{
if (!storedAccountsAreLoaded[0])
{
if (uris == null)
{
uris = new LinkedList();
}
uris.add(uri);
return;
}
}
final ProtocolProviderService provider;
try
{
provider = selectHandlingProvider(uri);
}
catch (OperationFailedException exc)
{
// The operation has been canceled by the user. Bail out.
if (logger.isTraceEnabled())
logger.trace("User canceled handling of uri " + uri);
return;
}
// if provider is null then we need to tell the user to create an
// account
if (provider == null)
{
showErrorMessage(
"You need to configure at least one SIP account \n"
+ "to be able to call " + uri, null);
return;
}
if(provider.getRegistrationState() == RegistrationState.REGISTERED)
{
handleUri(uri, provider);
}
else
{
// Allow a grace period for the provider to register in case
// we have just started up
long initialRegistrationTimeout =
SipActivator.getConfigurationService()
.getLong(INITIAL_REGISTRATION_TIMEOUT_PROP,
DEFAULT_INITIAL_REGISTRATION_TIMEOUT);
final DelayRegistrationStateChangeListener listener =
new DelayRegistrationStateChangeListener(uri, provider);
provider.addRegistrationStateChangeListener(listener);
new Timer().schedule(new TimerTask()
{
@Override
public void run()
{
provider.removeRegistrationStateChangeListener(listener);
// Even if not registered after the timeout, try the call
// anyway and the error popup will appear to ask the
// user if they want to register
if(provider.getRegistrationState()
!= RegistrationState.REGISTERED)
{
handleUri(uri, provider);
}
}
}, initialRegistrationTimeout);
}
}
/**
* Listener on provider state changes that handles the passed URI if the
* provider becomes registered.
*/
private class DelayRegistrationStateChangeListener
implements RegistrationStateChangeListener
{
private String uri;
private ProtocolProviderService provider;
private boolean handled = false;
public DelayRegistrationStateChangeListener(String uri,
ProtocolProviderService provider)
{
this.uri = uri;
this.provider = provider;
}
@Override
public void registrationStateChanged(RegistrationStateChangeEvent evt)
{
if (evt.getNewState() == RegistrationState.REGISTERED && !handled)
{
provider.removeRegistrationStateChangeListener(this);
handled = true;
handleUri(uri, provider);
}
}
}
/**
* Creates a call with the currently active telephony operation set.
*
* @param uri the SIP URI that we have to call.
*/
protected void handleUri(String uri, ProtocolProviderService provider)
{
//handle "sip://" URIs as "sip:"
if(uri != null)
uri = uri.replace("sip://", "sip:");
OperationSetBasicTelephony> telephonyOpSet
= provider.getOperationSet(OperationSetBasicTelephony.class);
OperationSetVideoTelephony videoTelephonyOpSet
= provider.getOperationSet(OperationSetVideoTelephony.class);
boolean videoCall = false;
if(videoTelephonyOpSet != null
&& uri.contains("?"))
{
String params = uri.substring(uri.indexOf('?') + 1);
uri = uri.substring(0, uri.indexOf('?'));
StringTokenizer paramTokens = new StringTokenizer(params, "&");
while(paramTokens.hasMoreTokens())
{
String tok = paramTokens.nextToken();
String[] keyValue = tok.split("\\=");
if (keyValue.length == 2
&& keyValue[0].equalsIgnoreCase("video")
&& keyValue[1].equalsIgnoreCase("true"))
videoCall = true;
}
}
try
{
if(videoCall)
videoTelephonyOpSet.createVideoCall(uri);
else
telephonyOpSet.createCall(uri);
}
catch (OperationFailedException exc)
{
// make sure that we prompt for registration only if it is really
// required by the provider.
boolean handled = false;
if (exc.getErrorCode() == OperationFailedException.PROVIDER_NOT_REGISTERED)
{
handled = promptForRegistration(uri, provider);
}
if(!handled)
{
showErrorMessage("Failed to create a call to " + uri, exc);
}
}
catch (ParseException exc)
{
showErrorMessage(
uri + " does not appear to be a valid SIP address", exc);
}
}
/**
* Informs the user that they need to be registered before placing calls and
* asks them whether they would like us to do it for them.
*
* @param uri the uri that the user would like us to call after registering.
* @param provider the provider that we may have to reregister.
* @return true if user tried to start registration
*/
private boolean promptForRegistration(String uri,
ProtocolProviderService provider)
{
int answer =
SipActivator
.getUIService()
.getPopupDialog()
.showConfirmPopupDialog(
"You need to be online in order to make a call and your "
+ "account is currently offline. Do want to connect now?",
"Account is currently offline", PopupDialog.YES_NO_OPTION);
if (answer == PopupDialog.YES_OPTION)
{
new ProtocolRegistrationThread(uri, provider).start();
return true;
}
return false;
}
/**
* The point of implementing a service listener here is so that we would
* only register our own uri handling service and thus only handle URIs
* while the factory is available as an OSGi service. We remove ourselves
* when our factory unregisters its service reference.
*
* @param event the OSGi ServiceEvent
*/
public void serviceChanged(ServiceEvent event)
{
Object sourceService =
SipActivator.bundleContext.getService(event.getServiceReference());
// ignore anything but our protocol factory.
if (sourceService != protoFactory)
{
return;
}
switch (event.getType())
{
case ServiceEvent.REGISTERED:
// our factory has just been registered as a service ...
registerHandlerService();
break;
case ServiceEvent.UNREGISTERING:
// our factory just died - seppuku.
unregisterHandlerService();
break;
default:
// we don't care.
break;
}
}
/**
* Uses the UIService to show an error message and log and
* exception.
*
* @param message the message that we'd like to show to the user.
* @param exc the exception that we'd like to log
*/
private void showErrorMessage(String message, Exception exc)
{
SipActivator.getUIService().getPopupDialog().showMessagePopupDialog(
message, "Failed to create call!", PopupDialog.ERROR_MESSAGE);
logger.error(message, exc);
}
/**
* We use this class when launching a provider registration by ourselves in
* order to track for provider registration states and retry uri handling,
* once the provider is registered.
*
*/
private class ProtocolRegistrationThread
extends Thread
implements RegistrationStateChangeListener
{
private ProtocolProviderService handlerProvider = null;
/**
* The URI that we'd need to re-call.
*/
private String uri = null;
/**
* Configures this thread register our parent provider and re-attempt
* connection to the specified uri.
*
* @param uri the uri that we need to handle.
* @param handlerProvider the provider that we are going to make
* register and that we are going to use to handle the
* uri.
*/
public ProtocolRegistrationThread(String uri,
ProtocolProviderService handlerProvider)
{
super("UriHandlerProviderRegistrationThread:uri=" + uri);
this.uri = uri;
this.handlerProvider = handlerProvider;
}
/**
* Starts the registration process, ads this class as a registration
* listener and then tries to rehandle the uri this thread was initiaded
* with.
*/
@Override
public void run()
{
handlerProvider.addRegistrationStateChangeListener(this);
try
{
handlerProvider.register(SipActivator.getUIService()
.getDefaultSecurityAuthority(handlerProvider));
}
catch (OperationFailedException exc)
{
logger.error("Failed to manually register provider.");
logger.warn(exc.getMessage(), exc);
}
}
/**
* If the parent provider passes into the registration state, the method
* re-handles the URI that this thread was initiated with. The method
* would only rehandle the uri if the event shows successful
* registration. It would ignore intermediate states such as
* REGISTERING. Disconnection and failure events would simply cause this
* listener to remove itself from the list of registration listeners.
*
* @param evt the RegistrationStateChangeEvent that this thread
* was initiated with.
*/
public void registrationStateChanged(RegistrationStateChangeEvent evt)
{
if (evt.getNewState() == RegistrationState.REGISTERED)
{
Thread uriRehandleThread = new Thread()
{
@Override
public void run()
{
handleUri(uri);
}
};
uriRehandleThread.setName("UriRehandleThread:uri=" + uri);
uriRehandleThread.start();
}
// we're only interested in a single event so we stop listening
// (unless this was a REGISTERING notification)
if (evt.getNewState() == RegistrationState.REGISTERING)
return;
handlerProvider.removeRegistrationStateChangeListener(this);
}
}
/**
* Returns the default provider that we are supposed to handle URIs through
* or null if there aren't any. Depending on the implementation this method
* may require user intervention so make sure you don't rely on a quick
* outcome when calling it.
*
* @param uri the uri that we'd like to handle with the provider that we are
* about to select.
*
* @return the provider that we should handle URIs through.
*
* @throws OperationFailedException with code OPERATION_CANCELED if
* the users.
*/
public ProtocolProviderService selectHandlingProvider(String uri)
throws OperationFailedException
{
ArrayList registeredAccounts =
protoFactory.getRegisteredAccounts();
// if we don't have any providers - return null.
if (registeredAccounts.size() == 0)
{
return null;
}
// if we only have one provider - select it
if (registeredAccounts.size() == 1)
{
ServiceReference providerReference =
protoFactory.getProviderForAccount(registeredAccounts.get(0));
ProtocolProviderService provider =
(ProtocolProviderService) SipActivator.getBundleContext()
.getService(providerReference);
return provider;
}
// otherwise - ask the user.
ArrayList providers =
new ArrayList();
for (AccountID accountID : registeredAccounts)
{
ServiceReference providerReference =
protoFactory.getProviderForAccount(accountID);
ProtocolProviderService provider =
(ProtocolProviderService) SipActivator.getBundleContext()
.getService(providerReference);
providers.add(new ProviderComboBoxEntry(provider));
}
Object result =
SipActivator.getUIService().getPopupDialog().showInputPopupDialog(
"Please select the account that you would like \n"
+ "to use to call " + uri, "Account Selection",
PopupDialog.OK_CANCEL_OPTION, providers.toArray(),
providers.get(0));
if (result == null)
{
throw new OperationFailedException("Operation cancelled",
OperationFailedException.OPERATION_CANCELED);
}
return ((ProviderComboBoxEntry) result).provider;
}
/**
* A class that we use to wrap providers before showing them to the user
* through a selection popup dialog from the UIService.
*/
private static class ProviderComboBoxEntry
{
public final ProtocolProviderService provider;
public ProviderComboBoxEntry(ProtocolProviderService provider)
{
this.provider = provider;
}
/**
* Returns a human readable String representing the provider
* encapsulated by this class.
*
* @return a human readable string representing the provider.
*/
@Override
public String toString()
{
return provider.getAccountID().getAccountAddress();
}
}
}