/*
* 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.jabber;
import java.awt.event.*;
import java.text.*;
import java.util.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.inputevt.*;
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.service.protocol.media.*;
import net.java.sip.communicator.util.*;
import org.jitsi.service.neomedia.*;
import org.jitsi.service.neomedia.device.*;
import org.jitsi.service.neomedia.format.*;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.filter.*;
import org.jivesoftware.smack.packet.*;
import org.jivesoftware.smackx.packet.*;
/**
* Implements all desktop sharing server-side related functions for Jabber
* protocol.
*
* @author Sebastien Vincent
* @author Vincent Lucas
*/
public class OperationSetDesktopSharingServerJabberImpl
extends OperationSetDesktopStreamingJabberImpl
implements OperationSetDesktopSharingServer,
RegistrationStateChangeListener,
PacketListener,
PacketFilter
{
/**
* Our class logger.
*/
private static final Logger logger = Logger
.getLogger(OperationSetDesktopSharingServerJabberImpl.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 (state != null
&& (state.equals(CallPeerState.DISCONNECTED)
|| state.equals(CallPeerState.FAILED)))
{
disableRemoteControl(peer);
}
}
};
/**
* HID service that will regenerates keyboard and mouse events received in
* Jabber messages.
*/
private HIDService hidService = null;
/**
* List of callPeers for the desktop sharing session with remote control
* granted.
*/
private List<String> callPeers = new ArrayList<String>();
/**
* Initializes a new <tt>OperationSetDesktopSharingJabberImpl</tt> instance
* which builds upon the telephony-related functionality of a specific
* <tt>OperationSetBasicTelephonyJabberImpl</tt>.
*
* @param basicTelephony the <tt>OperationSetBasicTelephonyJabberImpl</tt>
* the new extension should build upon
*/
public OperationSetDesktopSharingServerJabberImpl(
OperationSetBasicTelephonyJabberImpl basicTelephony)
{
super(basicTelephony);
parentProvider.addRegistrationStateChangeListener(this);
hidService = JabberActivator.getHIDService();
}
/**
* 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 video device that will be used to stream desktop.
* @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(String uri, MediaDevice device)
throws OperationFailedException, ParseException
{
MediaAwareCall<?,?,?> call
= (MediaAwareCall<?,?,?>) super.createVideoCall(uri, device);
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 video device that will be used to stream desktop.
* @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
{
MediaAwareCall<?,?,?> call
= (MediaAwareCall<?,?,?>) super.createVideoCall(callee, device);
size
= ((VideoMediaFormat)
call.getDefaultDevice(MediaType.VIDEO).getFormat())
.getSize();
origin = getOriginForMediaDevice(device);
return call;
}
/**
* Check if the remote part supports Jingle video.
*
* @param calleeAddress Contact address
* @return true if contact support Jingle video, false otherwise
*
* @throws OperationFailedException with the corresponding code if we fail
* to create the video call.
*/
@Override
protected Call createOutgoingVideoCall(String calleeAddress)
throws OperationFailedException
{
return createOutgoingVideoCall(calleeAddress, null);
}
/**
* Check if the remote part supports Jingle video.
*
* @param calleeAddress Contact address
* @param videoDevice specific video device to use (null to use default
* device)
* @return true if contact support Jingle video, false otherwise
*
* @throws OperationFailedException with the corresponding code if we fail
* to create the video call.
*/
@Override
protected Call createOutgoingVideoCall(
String calleeAddress,
MediaDevice videoDevice)
throws OperationFailedException
{
boolean supported = false;
String fullCalleeURI = null;
if (calleeAddress.indexOf('/') > 0)
{
fullCalleeURI = calleeAddress;
}
else
{
fullCalleeURI = parentProvider.getConnection()
.getRoster().getPresence(calleeAddress).getFrom();
}
if (logger.isInfoEnabled())
logger.info("creating outgoing desktop sharing call...");
DiscoverInfo di = null;
try
{
// check if the remote client supports inputevt (remote control)
di = parentProvider.getDiscoveryManager()
.discoverInfo(fullCalleeURI);
if (di.containsFeature(InputEvtIQ.NAMESPACE_CLIENT))
{
if (logger.isInfoEnabled())
logger.info(fullCalleeURI + ": remote-control supported");
supported = true;
}
else
{
if (logger.isInfoEnabled())
{
logger.info(
fullCalleeURI + ": remote-control not supported!");
}
// TODO fail or not?
/*
throw new OperationFailedException(
"Failed to create a true desktop sharing.\n"
+ fullCalleeURI + " does not support inputevt",
OperationFailedException.INTERNAL_ERROR);
*/
}
}
catch (XMPPException ex)
{
logger.warn("could not retrieve info for " + fullCalleeURI, ex);
}
if (parentProvider.getConnection() == null)
{
throw new OperationFailedException(
"Failed to create OutgoingJingleSession.\n"
+ "we don't have a valid XMPPConnection."
, OperationFailedException.INTERNAL_ERROR);
}
CallJabberImpl call = new CallJabberImpl(basicTelephony);
MediaUseCase useCase = getMediaUseCase();
if (videoDevice != null)
call.setVideoDevice(videoDevice, useCase);
/* enable video */
call.setLocalVideoAllowed(true, useCase);
/* enable remote-control */
call.setLocalInputEvtAware(supported);
size = (((VideoMediaFormat)videoDevice.getFormat()).getSize());
basicTelephony.createOutgoingCall(call, calleeAddress);
new CallPeerJabberImpl(calleeAddress, call);
return call;
}
/**
* Sets the indicator which determines whether the streaming of local video
* in a specific <tt>Call</tt> is allowed. The setting does not reflect
* the availability of actual video capture devices, it just expresses the
* desire of the user to have the local video streamed in the case the
* system is actually able to do so.
*
* @param call the <tt>Call</tt> to allow/disallow the streaming of local
* video for
* @param mediaDevice the media device to use for the desktop streaming
* @param allowed <tt>true</tt> to allow the streaming of local video for
* the specified <tt>Call</tt>; <tt>false</tt> to disallow it
*
* @throws OperationFailedException if initializing local video fails.
*/
@Override
public void setLocalVideoAllowed(Call call,
MediaDevice mediaDevice,
boolean allowed)
throws OperationFailedException
{
((AbstractCallJabberGTalkImpl<?>) call).setLocalInputEvtAware(allowed);
super.setLocalVideoAllowed(call, mediaDevice, allowed);
}
/**
* 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)
{
callPeer.addCallPeerListener(callPeerListener);
size = (((VideoMediaFormat)((CallJabberImpl)callPeer.getCall())
.getDefaultDevice(MediaType.VIDEO).getFormat()).getSize());
this.modifyRemoteControl(callPeer, true);
}
/**
* Disable desktop remote control. Local desktop stops regenerate keyboard
* and mouse events received from peer.
*
* @param callPeer call peer that will stop controlling on local computer
*/
public void disableRemoteControl(CallPeer callPeer)
{
this.modifyRemoteControl(callPeer, false);
callPeer.removeCallPeerListener(callPeerListener);
}
/**
* Implementation of method <tt>registrationStateChange</tt> from
* interface RegistrationStateChangeListener for setting up (or down)
* our <tt>InputEvtManager</tt> when an <tt>XMPPConnection</tt> is available
*
* @param evt the event received
*/
public void registrationStateChanged(RegistrationStateChangeEvent evt)
{
OperationSetDesktopSharingServerJabberImpl.registrationStateChanged(
evt,
this,
this,
this.parentProvider.getConnection());
}
/**
* Implementation of method <tt>registrationStateChange</tt> from
* interface RegistrationStateChangeListener for setting up (or down)
* our <tt>InputEvtManager</tt> when an <tt>XMPPConnection</tt> is available
*
* @param evt the event received
* @param packetListener the packet listener for InputEvtIQ of this
* connection. (OperationSetDesktopSharingServerJabberImpl or
* OperationSetDesktopSharingClientJabberImpl).
* @param packetFilter the packet filter for InputEvtIQ of this connection.
* @param connection The XMPP connection.
*/
public static void registrationStateChanged(
RegistrationStateChangeEvent evt,
PacketListener packetListener,
PacketFilter packetFilter,
Connection connection)
{
if(connection == null)
return;
if ((evt.getNewState() == RegistrationState.REGISTERING))
{
/* listen to specific inputevt IQ */
connection.addPacketListener(packetListener, packetFilter);
}
else if ((evt.getNewState() == RegistrationState.UNREGISTERING))
{
/* listen to specific inputevt IQ */
connection.removePacketListener(packetListener);
}
}
/**
* Handles incoming inputevt packets and passes them to the corresponding
* method based on their action.
*
* @param packet the packet to process.
*/
public void processPacket(Packet packet)
{
InputEvtIQ inputIQ = (InputEvtIQ)packet;
if(inputIQ.getType() == IQ.Type.SET
&& inputIQ.getAction() == InputEvtAction.NOTIFY)
{
//first ack all "set" requests.
IQ ack = IQ.createResultIQ(inputIQ);
parentProvider.getConnection().sendPacket(ack);
// Apply NOTIFY action only to peers which have remote control
// granted.
if(callPeers.contains(inputIQ.getFrom()))
{
for(RemoteControlExtension p : inputIQ.getRemoteControls())
{
ComponentEvent evt = p.getEvent();
processComponentEvent(evt);
}
}
}
}
/**
* Tests whether or not the specified packet should be handled by this
* operation set. This method is called by smack prior to packet delivery
* and it would only accept <tt>InputEvtIQ</tt>s.
*
* @param packet the packet to test.
* @return true if and only if <tt>packet</tt> passes the filter.
*/
public boolean accept(Packet packet)
{
//we only handle InputEvtIQ-s
return (packet instanceof InputEvtIQ);
}
/**
* Process an <tt>ComponentEvent</tt> received from remote peer.
*
* @param event <tt>ComponentEvent</tt> that will be regenerated on local
* computer
*/
public void processComponentEvent(ComponentEvent event)
{
if(event == null)
{
return;
}
if(event instanceof KeyEvent)
{
processKeyboardEvent((KeyEvent)event);
}
else if(event instanceof MouseEvent)
{
processMouseEvent((MouseEvent)event);
}
}
/**
* 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 (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 (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:
int originX = origin != null ? origin.x : 0;
int originY = origin != null ? origin.y : 0;
/* x and y position are sent in percentage but we multiply
* by 1000 in depacketizer because we cannot passed the size
* to the Provider
*/
int x = originX + ((event.getX() * size.width) / 1000);
int y = originY + ((event.getY() * size.height) / 1000);
hidService.mouseMove(x, y);
break;
case MouseEvent.MOUSE_WHEEL:
MouseWheelEvent evt = (MouseWheelEvent)event;
hidService.mouseWheel(evt.getWheelRotation());
break;
default:
break;
}
}
}
/**
* Sends IQ InputEvent START or STOP in order to enable/disable the remote
* peer to remotely control our PC.
*
* @param callPeer call peer that will stop controlling on local computer.
* @param enables True to enable remote peer to gain remote control on our
* PC. False Otherwise.
*/
public void modifyRemoteControl(CallPeer callPeer, boolean enables)
{
synchronized(callPeers)
{
if(callPeers.contains(callPeer.getAddress()) != enables)
{
if(isRemoteControlAvailable(callPeer))
{
if(logger.isInfoEnabled())
logger.info("Enables remote control: " + enables);
InputEvtIQ inputIQ = new InputEvtIQ();
if(enables)
{
inputIQ.setAction(InputEvtAction.START);
}
else
{
inputIQ.setAction(InputEvtAction.STOP);
}
inputIQ.setType(IQ.Type.SET);
inputIQ.setFrom(parentProvider.getOurJID());
inputIQ.setTo(callPeer.getAddress());
Connection connection = parentProvider.getConnection();
PacketCollector collector
= connection.createPacketCollector(
new PacketIDFilter(inputIQ.getPacketID()));
connection.sendPacket(inputIQ);
Packet p = collector.nextResult(
SmackConfiguration.getPacketReplyTimeout());
if(enables)
{
receivedResponseToIqStart(callPeer, p);
}
else
{
receivedResponseToIqStop(callPeer, p);
}
collector.cancel();
}
}
}
}
/**
* Manages response to IQ InputEvent START.
*
* @param callPeer call peer that will stop controlling on local computer.
* @param p The response to our IQ InputEvent START sent.
*/
private void receivedResponseToIqStart(CallPeer callPeer, Packet p)
{
// If the IQ has been correctly acknowledged, save the callPeer
// has a peer with remote control granted.
if(p != null && ((IQ) p).getType() == IQ.Type.RESULT)
{
callPeers.add(callPeer.getAddress());
}
else
{
String packetString = (p == null)
? "\n\tPacket is null (IQ request timeout)."
: "\n\tPacket: " + p.toXML();
logger.info(
"Remote peer has not received/accepted the START "
+ "action to grant the remote desktop control."
+ packetString);
}
}
/**
* Manages response to IQ InputEvent STOP.
*
* @param callPeer call peer that will stop controlling on local computer.
* @param p The response to our IQ InputEvent STOP sent.
*/
private void receivedResponseToIqStop(CallPeer callPeer, Packet p)
{
if(p == null || ((IQ) p).getType() == IQ.Type.ERROR)
{
String packetString = (p == null)
? "\n\tPacket is null (IQ request timeout)."
: "\n\tPacket: " + p.toXML();
logger.info(
"Remote peer has not received/accepted the STOP "
+ "action to grant the remote desktop control."
+ packetString);
}
// Even if the IQ has not been correctly acknowledged, save the
// callPeer has a peer with remote control revoked.
callPeers.remove(callPeer.getAddress());
}
/**
* 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)
{
DiscoverInfo discoverInfo
= ((AbstractCallPeerJabberGTalkImpl<?,?,?>) callPeer)
.getDiscoveryInfo();
return
parentProvider.getDiscoveryManager().includesFeature(
InputEvtIQ.NAMESPACE_SERVER)
&& (discoverInfo != null)
&& discoverInfo.containsFeature(InputEvtIQ.NAMESPACE_CLIENT);
}
}