/*
* 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.sip;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.text.*;
import java.util.List;
import javax.sip.*;
import javax.sip.Dialog;
import javax.sip.address.*;
import javax.sip.header.*;
import javax.sip.message.*;
import javax.xml.parsers.*;
import net.java.sip.communicator.service.hid.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;
import org.jitsi.service.neomedia.MediaType;
import org.jitsi.service.neomedia.device.*;
import org.jitsi.service.neomedia.format.*;
import org.w3c.dom.*;
import org.xml.sax.*;
/**
* Implements all desktop sharing server-side related functions for SIP
* protocol.
*
* @author Sebastien Vincent
*/
public class OperationSetDesktopSharingServerSipImpl
extends OperationSetDesktopStreamingSipImpl
implements OperationSetDesktopSharingServer,
MethodProcessorListener
{
/**
* Our class logger.
*/
private static final Logger logger = Logger
.getLogger(OperationSetDesktopSharingServerSipImpl.class);
/**
* The <tt>CallPeerListener</tt> which listens to modifications in the
* properties/state of <tt>CallPeer</tt>.
*/
private final CallPeerListener callPeerListener = new CallPeerAdapter()
{
/**
* Indicates that a change has occurred in the status of the source
* <tt>CallPeer</tt>.
*
* @param evt the <tt>CallPeerChangeEvent</tt> instance containing the
* source event as well as its previous and its new status
*/
@Override
public void peerStateChanged(CallPeerChangeEvent evt)
{
CallPeer peer = evt.getSourceCallPeer();
CallPeerState state = peer.getState();
if (remoteControlEnabled && state != null &&
(state.equals(CallPeerState.DISCONNECTED) ||
state.equals(CallPeerState.FAILED)))
{
/* if the peer is disconnected or call has failed the SIP
* dialog is terminated and sending a SUBSCRIBE (with 0 as
* lifetime) will throw exception
*/
remoteControlEnabled = false;
try
{
subscriber.removeSubscription(parentProvider.
parseAddressString(peer.getAddress()));
}
catch(ParseException ex)
{
}
}
}
};
/**
* If the remote control is authorized and thus enabled.
*/
private boolean remoteControlEnabled = false;
/**
* The <tt>EventPackageNotifier</tt> which implements remote-control
* event-package subscriber support on behalf of this
* <tt>OperationSetDesktopSharingServer</tt> instance.
*/
private final EventPackageSubscriber subscriber;
/**
* The SIP <tt>ProtocolProviderService</tt> implementation which created
* this instance and for which telephony conferencing services are being
* provided by this instance.
*/
private final ProtocolProviderServiceSipImpl parentProvider;
/**
* The <tt>Timer</tt> which executes delayed tasks scheduled by
* {@link #subscriber}.
*/
private final TimerScheduler timer = new TimerScheduler();
/**
* HID service that will regenerates keyboard and mouse events received in
* SIP NOTIFY.
*/
private HIDService hidService = null;
/**
* Initializes a new <tt>OperationSetDesktopSharingSipImpl</tt> instance
* which builds upon the telephony-related functionality of a specific
* <tt>OperationSetBasicTelephonySipImpl</tt>.
*
* @param basicTelephony the <tt>OperationSetBasicTelephonySipImpl</tt>
* the new extension should build upon
*/
public OperationSetDesktopSharingServerSipImpl(
OperationSetBasicTelephonySipImpl basicTelephony)
{
super(basicTelephony);
parentProvider = basicTelephony.getProtocolProvider();
hidService = SipActivator.getHIDService();
subscriber = new EventPackageSubscriber(
this.parentProvider,
DesktopSharingProtocolSipImpl.EVENT_PACKAGE,
DesktopSharingProtocolSipImpl.SUBSCRIPTION_DURATION,
DesktopSharingProtocolSipImpl.CONTENT_SUB_TYPE,
this.timer,
DesktopSharingProtocolSipImpl.REFRESH_MARGIN);
}
/**
* Create a new video call and invite the specified CallPeer to it.
*
* @param uri the address of the callee that we should invite to a new
* call.
* @param device <tt>MediaDevice</tt> to use for this call
* @return CallPeer the CallPeer that will represented by the
* specified uri. All following state change events will be delivered
* through that call peer. The Call that this peer is a member
* of could be retrieved from the CallParticipatn instance with the use
* of the corresponding method.
* @throws OperationFailedException with the corresponding code if we fail
* to create the video call.
* @throws ParseException if <tt>callee</tt> is not a valid sip address
* string.
*/
@Override
public Call createVideoCall(String uri, MediaDevice device)
throws OperationFailedException, ParseException
{
CallSipImpl call = (CallSipImpl)super.createVideoCall(uri, device);
CallPeerSipImpl callPeer = call.getCallPeers().next();
callPeer.addMethodProcessorListener(this);
callPeer.addCallPeerListener(callPeerListener);
size = (((VideoMediaFormat)call.getDefaultDevice(MediaType.VIDEO).
getFormat()).getSize());
origin = getOriginForMediaDevice(device);
return call;
}
/**
* Create a new video call and invite the specified CallPeer to it.
*
* @param callee the address of the callee that we should invite to a new
* call.
* @param device <tt>MediaDevice</tt> to use for this call
* @return CallPeer the CallPeer that will represented by the
* specified uri. All following state change events will be delivered
* through that call peer. The Call that this peer is a member
* of could be retrieved from the CallParticipatn instance with the use
* of the corresponding method.
* @throws OperationFailedException with the corresponding code if we fail
* to create the video call.
*/
@Override
public Call createVideoCall(Contact callee, MediaDevice device)
throws OperationFailedException
{
CallSipImpl call = (CallSipImpl)super.createVideoCall(callee, device);
CallPeerSipImpl callPeer = call.getCallPeers().next();
callPeer.addMethodProcessorListener(this);
callPeer.addCallPeerListener(callPeerListener);
size
= (((VideoMediaFormat)
call.getDefaultDevice(MediaType.VIDEO).getFormat())
.getSize());
origin = getOriginForMediaDevice(device);
return call;
}
/**
* Enable desktop remote control. Local desktop can now regenerates keyboard
* and mouse events received from peer.
*
* @param callPeer call peer that will take control on local computer
*/
public void enableRemoteControl(CallPeer callPeer)
{
RemoteControlSubscriberSubscription subscription
= new RemoteControlSubscriberSubscription(
(CallPeerSipImpl) callPeer);
try
{
subscriber.subscribe(subscription);
}
catch (OperationFailedException ofe)
{
logger.error(
"Failed to create or send a remote-control subscription",
ofe);
}
}
/**
* Disable desktop remote control. Local desktop stop regenerates keyboard
* and mouse events received from peer.
*
* @param callPeer call peer that will stop controlling on local computer
*/
public void disableRemoteControl(CallPeer callPeer)
{
/* unsubscribe */
try
{
Address addr
= parentProvider.parseAddressString(
callPeer.getAddress());
subscriber.unsubscribe(addr, false);
}
catch(ParseException ex)
{
logger.error("Failed to parse address", ex);
}
catch (OperationFailedException ofe)
{
logger.error(
"Failed to create or send a remote-control unsubscription",
ofe);
return;
}
remoteControlEnabled = false;
}
/**
* Notifies this <tt>MethodProcessorListener</tt> that a specific
* <tt>CallPeer</tt> has processed a specific SIP <tt>Request</tt> and has
* replied to it with a specific SIP <tt>Response</tt>.
*
* @param sourceCallPeer the <tt>CallPeer</tt> which has processed the
* specified SIP <tt>Request</tt>
* @param request the SIP <tt>Request</tt> which has been processed by
* <tt>sourceCallPeer</tt>
* @param response the SIP <tt>Response</tt> sent by <tt>sourceCallPeer</tt>
* as a reply to the specified SIP <tt>request</tt>
* @see MethodProcessorListener#requestProcessed(CallPeerSipImpl, Request,
* Response)
*/
public void requestProcessed(
CallPeerSipImpl sourceCallPeer,
Request request,
Response response)
{
}
/**
* Notifies this <tt>MethodProcessorListener</tt> that a specific
* <tt>CallPeer</tt> has processed a specific SIP <tt>Response</tt> and has
* replied to it with a specific SIP <tt>Request</tt>.
*
* @param sourceCallPeer the <tt>CallPeer</tt> which has processed the
* specified SIP <tt>Response</tt>
* @param response the SIP <tt>Response</tt> which has been processed by
* <tt>sourceCallPeer</tt>
* @param request the SIP <tt>Request</tt> sent by <tt>sourceCallPeer</tt>
* as a reply to the specified SIP <tt>response</tt>
* @see MethodProcessorListener#responseProcessed(CallPeerSipImpl, Response,
* Request)
*/
public void responseProcessed(
CallPeerSipImpl sourceCallPeer,
Response response,
Request request)
{
if (Response.OK == response.getStatusCode())
{
CSeqHeader cseqHeader
= (CSeqHeader) response.getHeader(CSeqHeader.NAME);
if ((cseqHeader != null)
&& Request.INVITE.equalsIgnoreCase(cseqHeader.getMethod()))
{
/* if we have successfully established a SIP session, launch
* remote control
*/
/* we never launch directly sharing (future sharer is force to
* toggle remote control.
*/
//enableRemoteControl(sourceCallPeer);
}
}
}
/**
* Process keyboard notification received from remote peer.
*
* @param event <tt>KeyboardEvent</tt> that will be regenerated on
* local computer
*/
public void processKeyboardEvent(KeyEvent event)
{
/* ignore command if remote control is not enabled otherwise regenerates
* event on the computer
*/
if (remoteControlEnabled && hidService != null)
{
int keycode = 0;
/* process immediately a "key-typed" event via press/release */
if(event.getKeyChar() != 0 && event.getID() == KeyEvent.KEY_TYPED)
{
hidService.keyPress(event.getKeyChar());
hidService.keyRelease(event.getKeyChar());
return;
}
keycode = event.getKeyCode();
if(keycode == 0)
{
return;
}
switch(event.getID())
{
case KeyEvent.KEY_PRESSED:
hidService.keyPress(keycode);
break;
case KeyEvent.KEY_RELEASED:
hidService.keyRelease(keycode);
break;
default:
break;
}
}
}
/**
* Process mouse notification received from remote peer.
*
* @param event <tt>MouseEvent</tt> that will be regenerated on local
* computer
*/
public void processMouseEvent(MouseEvent event)
{
/* ignore command if remote control is not enabled otherwise regenerates
* event on the computer
*/
if (remoteControlEnabled && hidService != null)
{
switch(event.getID())
{
case MouseEvent.MOUSE_PRESSED:
hidService.mousePress(event.getModifiers());
break;
case MouseEvent.MOUSE_RELEASED:
hidService.mouseRelease(event.getModifiers());
break;
case MouseEvent.MOUSE_MOVED:
hidService.mouseMove(event.getX(), event.getY());
break;
case MouseEvent.MOUSE_WHEEL:
MouseWheelEvent evt = (MouseWheelEvent)event;
hidService.mouseWheel(evt.getWheelRotation());
break;
default:
break;
}
}
}
/**
* Implements <tt>EventPackageSubscriber.Subscription</tt> in order to
* represent the subscription of the local peer to the remote-control event
* package of a specific remote <tt>CallPeer</tt> acting as a desktop
* sharing server.
*/
private class RemoteControlSubscriberSubscription
extends EventPackageSubscriber.Subscription
{
/**
* The <tt>CallPeer</tt> which is acting as a remote-control focus in
* its <tt>Call</tt> with the local peer.
*/
private final CallPeerSipImpl callPeer;
/**
* Initializes a new <tt>RemoteControlSubscriberSubscription</tt>
* instance which is to represent the subscription of the local peer to
* the remote-control event package of a specific <tt>CallPeer</tt>
* acting as a desktop sharing server.
*
* @param callPeer
* the <tt>CallPeer</tt> acting as a desktop sharing server
* which the new instance is to subscribe to
*/
public RemoteControlSubscriberSubscription(CallPeerSipImpl callPeer)
{
super(callPeer.getPeerAddress());
this.callPeer = callPeer;
}
/**
* Gets the <tt>Dialog</tt> which was created by the SUBSCRIBE request
* associated with this <tt>Subscription</tt> or which was used to send
* that request in.
*
* @return the <tt>Dialog</tt> which was created by the SUBSCRIBE
* request associated with this <tt>Subscription</tt> or which
* was used to send that request in; <tt>null</tt> if the
* success of the SUBSCRIBE request has not been confirmed yet
* or this <tt>Subscription</tt> was removed from the list of
* the <tt>EventPackageSupport</tt> it used to be in
* @see EventPackageSubscriber.Subscription#getDialog()
*/
@Override
protected Dialog getDialog()
{
Dialog dialog = super.getDialog();
if ((dialog == null)
|| DialogState.TERMINATED.equals(dialog.getState()))
dialog = callPeer.getDialog();
return dialog;
}
/**
* Notifies this <tt>Subscription</tt> that an active NOTIFY
* <tt>Request</tt> has been received and it may process the specified
* raw content carried in it.
*
* @param requestEvent
* the <tt>RequestEvent</tt> carrying the full details of the
* received NOTIFY <tt>Request</tt> including the raw content
* which may be processed by this <tt>Subscription</tt>
* @param rawContent
* an array of bytes which represents the raw content carried
* in the body of the received NOTIFY <tt>Request</tt> and
* extracted from the specified <tt>RequestEvent</tt> for the
* convenience of the implementers
* @see EventPackageSubscriber.Subscription#processActiveRequest(
* RequestEvent, byte[])
*/
protected void processActiveRequest(RequestEvent requestEvent,
byte[] rawContent)
{
if(requestEvent.getDialog() != callPeer.getDialog())
return;
if (rawContent != null)
{
/* parse rawContent */
Document document = null;
Throwable exception = null;
try
{
document
= DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.parse(new ByteArrayInputStream(rawContent));
}
catch (IOException ioe)
{
exception = ioe;
}
catch (ParserConfigurationException pce)
{
exception = pce;
}
catch (SAXException saxe)
{
exception = saxe;
}
if (exception != null)
{
logger.error("Failed to parse remote-info XML", exception);
}
else
{
Element root = document.getDocumentElement();
List<ComponentEvent> events = null;
Point p = getOrigin();
events = DesktopSharingProtocolSipImpl.parse(root, size, p);
for(ComponentEvent evt : events)
{
if(evt instanceof MouseEvent)
processMouseEvent((MouseEvent)evt);
else if(evt instanceof KeyEvent)
processKeyboardEvent((KeyEvent)evt);
}
}
}
}
/**
* Notifies this <tt>Subscription</tt> that a <tt>Response</tt> to a
* previous SUBSCRIBE <tt>Request</tt> has been received with a status
* code in the failure range and it may process the status code carried
* in it.
*
* @param responseEvent
* the <tt>ResponseEvent</tt> carrying the full details of
* the received <tt>Response</tt> including the status code
* which may be processed by this <tt>Subscription</tt>
* @param statusCode
* the status code carried in the <tt>Response</tt> and
* extracted from the specified <tt>ResponseEvent</tt> for
* the convenience of the implementers
* @see EventPackageSubscriber.Subscription#processFailureResponse(
* ResponseEvent, int)
*/
protected void processFailureResponse(ResponseEvent responseEvent,
int statusCode)
{
/* we have not managed to subscribe to remote peer so it is better
* to disable remote control feature
*/
remoteControlEnabled = false;
}
/**
* Notifies this <tt>Subscription</tt> that a <tt>Response</tt> to a
* previous SUBSCRIBE <tt>Request</tt> has been received with a status
* code in the success range and it may process the status code carried
* in it.
*
* @param responseEvent
* the <tt>ResponseEvent</tt> carrying the full details of
* the received <tt>Response</tt> including the status code
* which may be processed by this <tt>Subscription</tt>
* @param statusCode
* the status code carried in the <tt>Response</tt> and
* extracted from the specified <tt>ResponseEvent</tt> for
* the convenience of the implementers
* @see EventPackageSubscriber.Subscription#processSuccessResponse(
* ResponseEvent, int)
*/
protected void processSuccessResponse(ResponseEvent responseEvent,
int statusCode)
{
switch (statusCode)
{
case Response.OK:
case Response.ACCEPTED:
/* we have succeeded to subscribe to remote peer */
remoteControlEnabled = true;
break;
}
}
/**
* Notifies this <tt>Subscription</tt> that a terminating NOTIFY
* <tt>Request</tt> has been received and it may process the reason code
* carried in it.
*
* @param requestEvent
* the <tt>RequestEvent</tt> carrying the full details of the
* received NOTIFY <tt>Request</tt> including the reason code
* which may be processed by this <tt>Subscription</tt>
* @param reasonCode
* the code of the reason for the termination carried in the
* NOTIFY <tt>Request</tt> and extracted from the specified
* <tt>RequestEvent</tt> for the convenience of the
* implementers
* @see EventPackageSubscriber.Subscription#processTerminatedRequest(
* RequestEvent, String)
*/
protected void processTerminatedRequest(RequestEvent requestEvent,
String reasonCode)
{
if (SubscriptionStateHeader.DEACTIVATED.equals(reasonCode))
{
try
{
subscriber.poll(this);
}
catch (OperationFailedException ofe)
{
logger.error(
"Failed to renew the remote-control subscription "
+ this, ofe);
}
}
}
}
/**
* Tells if the peer provided can be remotely controlled by this peer:
* - The server is able to grant/revoke remote access to its desktop.
* - The client (the call peer) is able to send mouse and keyboard events.
*
* @param callPeer The call peer which may remotely control the shared
* desktop.
*
* @return True if the server and the client are able to respectively grant
* remote access and send mouse/keyboard events. False, if one of the call
* participant (server or client) is not able to deal with remote controls.
*/
public boolean isRemoteControlAvailable(CallPeer callPeer)
{
// There is no mean to discover if the remote peer can sends mouse and
// keyboard events for SIP. Thus, let define it always to true.
return true;
}
}