/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package net.java.sip.communicator.impl.protocol.jabber;
import java.lang.reflect.*;
import java.util.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.gtalk.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.util.*;
import org.jitsi.service.neomedia.*;
import org.jivesoftware.smack.packet.*;
/**
* Implements a Google Talk CallPeer.
*
* @author Sebastien Vincent
* @author Lyubomir Marinov
*/
public class CallPeerGTalkImpl
extends AbstractCallPeerJabberGTalkImpl
{
/**
* The Logger used by the CallPeerGTalkImpl class and its
* instances for logging output.
*/
private static final Logger logger
= Logger.getLogger(CallPeerGTalkImpl.class);
/**
* Returns whether or not the CallPeer is an Android phone or
* a call pass throught Google Voice or uses Google Talk client.
*
* We base the detection of the JID's resource which in the case of Android
* is android/Vtok/Talk.vXXXXXXX (where XXXXXX is a suite of
* numbers/letters).
*/
private static boolean isAndroidOrVtokOrTalkClient(String fullJID)
{
int idx = fullJID.indexOf('/');
if(idx != -1)
{
String res = fullJID.substring(idx + 1);
if(res.startsWith("android") || res.startsWith("Vtok") ||
res.startsWith("Talk.v"))
{
return true;
}
}
if(fullJID.contains(
"@" + ProtocolProviderServiceJabberImpl.GOOGLE_VOICE_DOMAIN))
return true;
return false;
}
/**
* Temporary variable for handling client like FreeSwitch that sends
* "accept" message before sending candidates.
*/
private SessionIQ sessAcceptedWithNoCands = null;
/**
* Session ID.
*/
private String sid = null;
/**
* Creates a new call peer with address peerAddress.
*
* @param peerAddress the Google Talk address of the new call peer.
* @param owningCall the call that contains this call peer.
*/
public CallPeerGTalkImpl(String peerAddress, CallGTalkImpl owningCall)
{
super(peerAddress, owningCall);
setMediaHandler(new CallPeerMediaHandlerGTalkImpl(this));
}
/**
* Indicates a user request to answer an incoming call from this
* CallPeer.
*
* Sends an OK response to callPeer. Make sure that the call
* peer contains an SDP description when you call this method.
*
* @throws OperationFailedException if we fail to create or send the
* response.
*/
public synchronized void answer()
throws OperationFailedException
{
RtpDescriptionPacketExtension answer = null;
try
{
getMediaHandler().getTransportManager().
wrapupConnectivityEstablishment();
answer = getMediaHandler().generateSessionAccept(true);
}
catch(IllegalArgumentException e)
{
sessAcceptedWithNoCands = new SessionIQ();
// HACK apparently FreeSwitch need to have accept before
answer = getMediaHandler().generateSessionAccept(false);
SessionIQ response
= GTalkPacketFactory.createSessionAccept(
sessionInitIQ.getTo(),
sessionInitIQ.getFrom(),
getSID(),
answer);
getProtocolProvider().getConnection().sendPacket(response);
return;
}
catch(Exception exc)
{
logger.info("Failed to answer an incoming call", exc);
//send an error response
String reasonText = "Error: " + exc.getMessage();
SessionIQ errResp
= GTalkPacketFactory.createSessionTerminate(
sessionInitIQ.getTo(),
sessionInitIQ.getFrom(),
sessionInitIQ.getID(),
Reason.FAILED_APPLICATION,
reasonText);
setState(CallPeerState.FAILED, reasonText);
getProtocolProvider().getConnection().sendPacket(errResp);
return;
}
SessionIQ response
= GTalkPacketFactory.createSessionAccept(
sessionInitIQ.getTo(),
sessionInitIQ.getFrom(),
getSID(),
answer);
//send the packet first and start the stream later in case the media
//relay needs to see it before letting hole punching techniques through.
if(sessAcceptedWithNoCands == null)
getProtocolProvider().getConnection().sendPacket(response);
try
{
getMediaHandler().start();
}
catch(UndeclaredThrowableException e)
{
Throwable exc = e.getUndeclaredThrowable();
logger.info("Failed to establish a connection", exc);
//send an error response
String reasonText = "Error: " + exc.getMessage();
SessionIQ errResp
= GTalkPacketFactory.createSessionTerminate(
sessionInitIQ.getTo(),
sessionInitIQ.getFrom(),
sessionInitIQ.getID(),
Reason.GENERAL_ERROR,
reasonText);
getMediaHandler().getTransportManager().close();
setState(CallPeerState.FAILED, reasonText);
getProtocolProvider().getConnection().sendPacket(errResp);
return;
}
//tell everyone we are connecting so that the audio notifications would
//stop
setState(CallPeerState.CONNECTED);
}
/**
* Returns the session ID of the Jingle session associated with this call.
*
* @return the session ID of the Jingle session associated with this call.
*/
@Override
public String getSID()
{
return sessionInitIQ != null ? sessionInitIQ.getID() : sid;
}
/**
* Ends the call with for this CallPeer. Depending on the state
* of the peer the method would send a CANCEL, BYE, or BUSY_HERE message
* and set the new state to DISCONNECTED.
*
* @param failed indicates if the hangup is following to a call failure or
* simply a disconnect
* @param reasonText the text, if any, to be set on the
* ReasonPacketExtension as the value of its
* @param reasonOtherExtension the PacketExtension, if any, to be
* set on the ReasonPacketExtension as the value of its
* otherExtension property
*/
public void hangup(boolean failed,
String reasonText,
PacketExtension reasonOtherExtension)
{
// do nothing if the call is already ended
if (CallPeerState.DISCONNECTED.equals(getState())
|| CallPeerState.FAILED.equals(getState()))
{
if (logger.isDebugEnabled())
logger.debug("Ignoring a request to hangup a call peer "
+ "that is already DISCONNECTED");
return;
}
CallPeerState prevPeerState = getState();
getMediaHandler().getTransportManager().close();
if (failed)
setState(CallPeerState.FAILED, reasonText);
else
setState(CallPeerState.DISCONNECTED, reasonText);
SessionIQ responseIQ = null;
if (prevPeerState.equals(CallPeerState.CONNECTED)
|| CallPeerState.isOnHold(prevPeerState))
{
responseIQ = GTalkPacketFactory.createBye(
getProtocolProvider().getOurJID(), peerJID, getSID());
responseIQ.setInitiator(isInitiator() ? getAddress() :
getProtocolProvider().getOurJID());
}
else if (CallPeerState.CONNECTING.equals(prevPeerState)
|| CallPeerState.CONNECTING_WITH_EARLY_MEDIA.equals(prevPeerState)
|| CallPeerState.ALERTING_REMOTE_SIDE.equals(prevPeerState))
{
responseIQ = GTalkPacketFactory.createCancel(
getProtocolProvider().getOurJID(), peerJID, getSID());
responseIQ.setInitiator(isInitiator() ? getAddress() :
getProtocolProvider().getOurJID());
}
else if (prevPeerState.equals(CallPeerState.INCOMING_CALL))
{
responseIQ = GTalkPacketFactory.createBusy(
getProtocolProvider().getOurJID(), peerJID, getSID());
responseIQ.setInitiator(isInitiator() ? getAddress() :
getProtocolProvider().getOurJID());
}
else if (prevPeerState.equals(CallPeerState.BUSY)
|| prevPeerState.equals(CallPeerState.FAILED))
{
// For FAILED and BUSY we only need to update CALL_STATUS
// as everything else has been done already.
}
else
{
logger.info("Could not determine call peer state!");
}
if (responseIQ != null)
{
if (reasonOtherExtension != null)
{
ReasonPacketExtension reason
= (ReasonPacketExtension)
responseIQ.getExtension(
ReasonPacketExtension.ELEMENT_NAME,
ReasonPacketExtension.NAMESPACE);
if (reason == null)
{
if (reasonOtherExtension instanceof ReasonPacketExtension)
{
responseIQ.setReason(
(ReasonPacketExtension) reasonOtherExtension);
}
}
else
reason.setOtherExtension(reasonOtherExtension);
}
getProtocolProvider().getConnection().sendPacket(responseIQ);
}
}
/**
* Initiate a Google Talk session {@link SessionIQ}.
*
* @param sessionInitiateExtensions a collection of additional and optional
* PacketExtensions to be added to the initiate
* {@link SessionIQ} which is to initiate the session with this
* CallPeerGTalkImpl
* @throws OperationFailedException exception
*/
protected synchronized void initiateSession(
Iterable sessionInitiateExtensions)
throws OperationFailedException
{
sid = SessionIQ.generateSID();
initiator = false;
//Create the media description that we'd like to send to the other side.
RtpDescriptionPacketExtension offer
= getMediaHandler().createDescription();
ProtocolProviderServiceJabberImpl protocolProvider
= getProtocolProvider();
sessionInitIQ
= GTalkPacketFactory.createSessionInitiate(
protocolProvider.getOurJID(),
this.peerJID,
sid,
offer);
if (sessionInitiateExtensions != null)
{
for (PacketExtension sessionInitiateExtension
: sessionInitiateExtensions)
{
sessionInitIQ.addExtension(sessionInitiateExtension);
}
}
protocolProvider.getConnection().sendPacket(sessionInitIQ);
// for Google Voice JID without resource we do not harvest and send
// candidates
if(getAddress().endsWith(
ProtocolProviderServiceJabberImpl.GOOGLE_VOICE_DOMAIN))
{
return;
}
getMediaHandler().harvestCandidates(offer.getPayloadTypes(),
new CandidatesSender()
{
public void sendCandidates(
Iterable candidates)
{
CallPeerGTalkImpl.this.sendCandidates(candidates);
}
});
}
/**
* Process candidates received.
*
* @param sessionInitIQ The {@link SessionIQ} that created the session we
* are handling here
*/
public void processCandidates(SessionIQ sessionInitIQ)
{
Collection extensions = sessionInitIQ.getExtensions();
List candidates =
new ArrayList();
for(PacketExtension ext : extensions)
{
if(ext.getElementName().equalsIgnoreCase(
GTalkCandidatePacketExtension.ELEMENT_NAME))
{
GTalkCandidatePacketExtension cand =
(GTalkCandidatePacketExtension)ext;
candidates.add(cand);
}
}
try
{
getMediaHandler().processCandidates(candidates);
}
catch (OperationFailedException ofe)
{
logger.warn("Failed to process an incoming candidates", ofe);
//send an error response
String reasonText = "Error: " + ofe.getMessage();
SessionIQ errResp
= GTalkPacketFactory.createSessionTerminate(
sessionInitIQ.getTo(),
sessionInitIQ.getFrom(),
sessionInitIQ.getID(),
Reason.GENERAL_ERROR,
reasonText);
getMediaHandler().getTransportManager().close();
setState(CallPeerState.FAILED, reasonText);
getProtocolProvider().getConnection().sendPacket(errResp);
return;
}
// HACK for FreeSwitch that send accept message before sending
// candidates
if(sessAcceptedWithNoCands != null)
{
if(isInitiator())
{
try
{
answer();
}
catch(OperationFailedException e)
{
logger.info("Failed to answer call (FreeSwitch hack)");
}
}
else
{
final SessionIQ sess = sessAcceptedWithNoCands;
sessAcceptedWithNoCands = null;
// run in another thread to not block smack receive thread and
// possibly delay others candidates messages.
new Thread()
{
@Override
public void run()
{
processSessionAccept(sess);
}
}.start();
}
sessAcceptedWithNoCands = null;
}
}
/**
* Processes the session initiation {@link SessionIQ} that we were created
* with, passing its content to the media handler.
*
* @param sessionInitIQ The {@link SessionIQ} that created the session that
* we are handling here.
*/
public void processSessionAccept(SessionIQ sessionInitIQ)
{
this.sessionInitIQ = sessionInitIQ;
CallPeerMediaHandlerGTalkImpl mediaHandler = getMediaHandler();
Collection extensions =
sessionInitIQ.getExtensions();
RtpDescriptionPacketExtension answer = null;
for(PacketExtension ext : extensions)
{
if(ext.getElementName().equalsIgnoreCase(
RtpDescriptionPacketExtension.ELEMENT_NAME))
{
answer = (RtpDescriptionPacketExtension)ext;
break;
}
}
try
{
mediaHandler.getTransportManager().
wrapupConnectivityEstablishment();
mediaHandler.processAnswer(answer);
}
catch(IllegalArgumentException e)
{
// HACK for FreeSwitch that send accept message before sending
// candidates
sessAcceptedWithNoCands = sessionInitIQ;
return;
}
catch(Exception exc)
{
if (logger.isInfoEnabled())
logger.info("Failed to process a session-accept", exc);
//send an error response
String reasonText = "Error: " + exc.getMessage();
SessionIQ errResp
= GTalkPacketFactory.createSessionTerminate(
sessionInitIQ.getTo(),
sessionInitIQ.getFrom(),
sessionInitIQ.getID(),
Reason.GENERAL_ERROR,
reasonText);
getMediaHandler().getTransportManager().close();
setState(CallPeerState.FAILED, reasonText);
getProtocolProvider().getConnection().sendPacket(errResp);
return;
}
//tell everyone we are connecting so that the audio notifications would
//stop
setState(CallPeerState.CONNECTED);
mediaHandler.start();
}
/**
* Processes the session initiation {@link SessionIQ} that we were created
* with, passing its content to the media handler and then sends either a
* "session-info/ringing" or a "terminate" response.
*
* @param sessionInitIQ The {@link SessionIQ} that created the session that
* we are handling here.
*/
protected synchronized void processSessionInitiate(SessionIQ sessionInitIQ)
{
// Do initiate the session.
this.sessionInitIQ = sessionInitIQ;
this.initiator = true;
RtpDescriptionPacketExtension description = null;
for(PacketExtension ext : sessionInitIQ.getExtensions())
{
if(ext.getElementName().equals(
RtpDescriptionPacketExtension.ELEMENT_NAME))
{
description = (RtpDescriptionPacketExtension)ext;
break;
}
}
if(description == null)
{
logger.info("No description in incoming session initiate");
//send an error response;
String reasonText = "Error: no description";
SessionIQ errResp
= GTalkPacketFactory.createSessionTerminate(
sessionInitIQ.getTo(),
sessionInitIQ.getFrom(),
sessionInitIQ.getID(),
Reason.INCOMPATIBLE_PARAMETERS,
reasonText);
setState(CallPeerState.FAILED, reasonText);
getProtocolProvider().getConnection().sendPacket(errResp);
return;
}
try
{
getMediaHandler().processOffer(description);
}
catch(Exception ex)
{
logger.info("Failed to process an incoming session initiate", ex);
//send an error response;
String reasonText = "Error: " + ex.getMessage();
SessionIQ errResp
= GTalkPacketFactory.createSessionTerminate(
sessionInitIQ.getTo(),
sessionInitIQ.getFrom(),
sessionInitIQ.getID(),
Reason.INCOMPATIBLE_PARAMETERS,
reasonText);
setState(CallPeerState.FAILED, reasonText);
getProtocolProvider().getConnection().sendPacket(errResp);
return;
}
// If we do not get the info about the remote peer yet. Get it right
// now.
if(this.getDiscoveryInfo() == null)
{
String calleeURI = sessionInitIQ.getFrom();
retrieveDiscoveryInfo(calleeURI);
}
}
/**
* Puts this peer into a {@link CallPeerState#DISCONNECTED}, indicating a
* reason to the user, if there is one.
*
* @param sessionIQ the {@link SessionIQ} that's terminating our session.
*/
public void processSessionReject(SessionIQ sessionIQ)
{
processSessionTerminate(sessionIQ);
}
/**
* Puts this peer into a {@link CallPeerState#DISCONNECTED}, indicating a
* reason to the user, if there is one.
*
* @param sessionIQ the {@link SessionIQ} that's terminating our session.
*/
public void processSessionTerminate(SessionIQ sessionIQ)
{
String reasonStr = "Call ended by remote side.";
ReasonPacketExtension reasonExt = sessionIQ.getReason();
if(reasonStr != null && reasonExt != null)
{
Reason reason = reasonExt.getReason();
if(reason != null)
reasonStr += " Reason: " + reason.toString() + ".";
String text = reasonExt.getText();
if(text != null)
reasonStr += " " + text;
}
getMediaHandler().getTransportManager().close();
setState(CallPeerState.DISCONNECTED, reasonStr);
}
/**
* Sends local candidate addresses from the local peer to the remote peer
* using the candidates {@link SessionIQ}.
*
* @param candidates the local candidate addresses to be sent from the local
* peer to the remote peer using the candidates
* {@link SessionIQ}
*/
protected void sendCandidates(
Iterable candidates)
{
ProtocolProviderServiceJabberImpl protocolProvider
= getProtocolProvider();
SessionIQ candidatesIQ = new SessionIQ();
candidatesIQ.setGTalkType(GTalkType.CANDIDATES);
candidatesIQ.setFrom(protocolProvider.getOurJID());
candidatesIQ.setInitiator(isInitiator() ? getAddress() :
protocolProvider.getOurJID());
candidatesIQ.setID(getSID());
candidatesIQ.setTo(getAddress());
candidatesIQ.setType(IQ.Type.SET);
for (GTalkCandidatePacketExtension candidate : candidates)
{
// Android phone and Google Talk client does not seems to like IPv6
// candidates since it reject the IQ candidates with an error
// so do not send IPv6 candidates to Android phone or Talk client
if(isAndroidOrVtokOrTalkClient(getAddress()) &&
NetworkUtils.isIPv6Address(candidate.getAddress()))
continue;
candidatesIQ.addExtension(candidate);
}
protocolProvider.getConnection().sendPacket(candidatesIQ);
}
/**
* {@inheritDoc}
*/
public String getEntity()
{
return getAddress();
}
/**
* {@inheritDoc}
*
* Uses the direction of the media stream as a fallback.
* TODO: return the direction of the GTalk session?
*/
@Override
public MediaDirection getDirection(MediaType mediaType)
{
MediaStream stream = getMediaHandler().getStream(mediaType);
if (stream != null)
{
MediaDirection direction = stream.getDirection();
return direction == null
? MediaDirection.INACTIVE
: direction;
}
return MediaDirection.INACTIVE;
}
}