Extend the collaboration functionality

By default, PureWeb's built-in collaboration feature provides shallow collaboration: there is no difference, from a functionality perspective, between the host and the participant (the host does not have additional controls), and all can interact with the same features at the same time.

If this functionality is not sufficient for your application, you have the flexibility of adding the tighter controls that suit your particular requirements. This section describes various events and methods provided by the APIs to help you keep track of who's connected and who's the host, and manage session ownership.

How-to

Know who's joining, and when

When implementing deeper collaboration functionality, it is often necessary to know which users are joining or leaving the session, and when. Both the service and client APIs provide events to help you keep track of this information.

On the service

APIs:

SessionManager | C++ | .Net | Java
SessionEventArgs | C++ | .Net | Java

On the service, you can tell when a new user is joining or leaving the session by listening to the relevant connect and disconnect events.

You can then write handlers for these events to manage collaboration. For instance, the Asteroids sample application uses this approach to determine whether it should run in single-player or two-player mode.

The events can be found in the SessionManager class. They are called SessionConnected and SessionDisconnected (in C++ and .Net), and in Java the corresponding methods are addSessionConnectedHandler and addSessionDisconnectedHandler.

The event handler provides a SessionEventArgs object, which you can query to get ID of the client which just connected (or disconnected).

C++

sessionManager->SessionConnected() += Bind(this, &MyApp::OnSessionConnected);

void MyApp::OnSessionConnected(ISessionManager& sessionManager, SessionEventArgs&)
{
    // Do something useful once connected

    // Can get the session ID from the arguments, if needed:
    Guid sessionId = args.SessionId();
}				

.Net

sessionManager.SessionConnected += new EventHandler(OnSessionConnected);

private void OnSessionConnected(object sender, SessionEventArgs args)
{
    // Do something useful once connected

    // Can get the session ID from the arguments, if needed:
    Guid sessionId = args.SessionId();
}

Java

sessionManager.addSessionConnectedHandler(new SessionConnectedHandler());

private class SessionConnectedHandler implements EventHandler<SessionEventArgs>
{
    public void invoke(Object source, SessionEventArgs args)
    {
        // Do something useful once connected.
        
	// Can get the session ID from the arguments, if needed:
        UUID sessionId = args.getSessionId();
    }
}

On the client

On the client, the CollaborationManager class raises a SESSIONS_CHANGED whenever the host or a collaboration participant joins or leaves a session, and a OWNER_SESSION_CHANGED when session ownership has changed hands. You can write handlers for these events to manage collaboration.

HTML

A handler for the "OWNER_SESSION_CHANGED" event, triggered when the host joins or leaves the session.

// Register an event listener
pureweb.listen(client, pureweb.client.CollaborationManager.EventType.OWNER_SESSION_CHANGED, onOwnerSessionChanged);

// Handle the event
function onOwnerSessionChanged(e) {
    // Do something useful.
}

iOS (Obj-C)

A handler for the "ownerDidChange" event, triggered when the host joins or leaves the session.

// Register an event listener
[_collaborationManager.ownerSessionChanged addSubscriber:self action:@selector(ownerDidChange)];

// Handle the event
- (void)ownerDidChange
{
    // Do something useful.
}

Android

An example of the "OwnerSessionChangedHandler" method, triggered when the host joins or leaves the session.

collaborationManager.addOwnerSessionChangedHandler(new OwnerSessionChangedHandler());

private class OwnerSessionChangedHandler implements EventHandler<EmptyArgs>
{
    public void invoke(Object source, EmptyArgs args)
    {
        // Do something useful.
    }
}

Differentiate between host and guest

APIs:

CollaborationManager.getOwnerSession() | HTML5 | Android
CollaborationManager.ownerSession() | iOS

CollaborationManager.getSessionId() | HTML5 | Android
CollaborationManager.sessionId() | iOS

You may want to implement logic in your client so that it behaves differently when the user is the host, which requires a means to determine whether a connected user is the session owner.

To do this, first use getOwnerSession to obtain the ID of the host, then use getSessionId to obtain the ID of the collaboration session. Finally, compare the two identifiers; if they are equal, the user is the host, and if they are different, the user is a participant.

HTML

pureweb.getFramework().getCollaborationManager().getOwnerSession() === pureweb.getClient().getSessionId()

iOS (Obj-C)

[[PWFramework sharedInstance].collaborationManager ownerSession] == [[PWFramework sharedInstance].collaborationManager sessionId]

Android

framework.getCollaborationManager().getOwnerSession() == framework.getWebClient.getSessionId()

Transfer session ownership

APIs:

CollaborationManager.SetOwnerSession() | C++ | Java
CollaborationManager.OwnerSession() | .Net

It is possible to transfer ownership to a different user while the current host is still connected. To do this, you would use the SetOwnerSession method on the service.

Below is an example of a command handler for transferring ownership. This assumes that ownership transfer would be triggered by a "TakeOwnership" command sent from the client, and that the OnTakeOwnership handler has already been registered on the service. (See Set up a basic command.)

C++

void MyApp::OnTakeOwnership(CSI::Guid sessionid, const CSI::Typeless command, const CSI::Typeless& response)
{
    CollaborationManager::Instance().SetOwnerSession(sessionid);
}

.Net

private void OnTakeOwnership(Guid sessionId, XElement command, XElement responses)
{
    CollaborationManager.Instance.OwnerSession = sessionId;
}

Java

private class OnTakeOwnership implements CommandHandler
{
    @Override
    public void invoke(UUID sessionId, Element command, Element responses)
    {
        CollaborationManager.getInstance().setOwnerSession(sessionId);
    }
}

Set behavior on host disconnect

APIs:

(I)OwnerSessionProvider | C++ | .Net
OwnerSessionProvider | Java

The interface that governs the application's behavior when a host disconnects is IOwnerSessionProvider. The default implementation of this interface simply transfers session ownership to one of the other participants, based on session ID. However, you can create your own custom implementation to override the default behavior.

A common use case is to automatically disconnect all users on host disconnect. To achieve this, the custom session provider must be set to return an empty GUID. This ensures that the session will not be assigned to anyone else when the host disconnects. Your implementation will also need to set a value in application state to notify the collaboration clients to disconnect.

On the service

C++

#include "CSI/PureWeb/StateManager/CollaborationManager.h" 
				
using namespace CSI;
using namespace CSI::PureWeb;
using namespace CSI::PureWeb::Server;

class STATEMANAGER_API MyCustomOwnerSessionProvider : public CSI::PureWeb::Server::IOwnerSessionProvider
{
public:
   
    virtual CSI::Guid GetNextOwnerSession()
    {
        // Set an application state value for notifying non-host client sessions to disconnect. 
        StateManager::Instance()->XmlStateManager().SetValue("/Collaboration/SessionEnded", "true");
        
        // Return an empty GUID to prevent ownership from transferring to a non-host session.
        return CSI::Guid::Empty();
    }
};						

Your custom owner session provider will need to be registered with the service. This registration should be part of the function responsible for starting up the service.

C++

CollaborationManager::Instance().SetOwnerSessionProvider(new MyCustomOwnerSessionProvider());

On the client

On the client, a value changed handler should listen for changes to the application state path added by the service; the corresponding handling function can then be used to disconnect the client.

HTML5

// Register a value changed handler
var framework = pureweb.getFramework();
framework.getState().getStateManager().addValueChangedHandler('Collaboration/SessionEnded', onCollaborationSessionEnded);

// Define this handler to disconnect the non-host collaboration client
function onCollaborationSessionEnded(e) {
    if (pureweb.getClient().getSessionId() !==  pureweb.getFramework().getCollaborationManager().getOwnerSession()) {
        pureweb.getClient().disconnect(false);
        alert('The collaboration session has ended');
    }
}

Disconnect all users except host

In some applications, it may be necessary to provide the host with the ability to disconnect all collaboration participants while remaining connected. This can be handled using commands. (See Set up a basic command.)

For example, you could display a button labeled "End Collaboration" on the host's user interface, and use this button to trigger a command, let's say EndSession. The command handler on the service would be used to set a value in application state, to notify the collaboration clients to disconnect. The caveat is that the handler would only set this value if the user is the host.

On the service

C++

void MyApp::OnEndCollaborationSession(CSI::Guid sessionId, CSI::Typeless command, CSI::Typeless responses)
{
    // Only permit the owner session to set the state value ending the collaboration session
    if (sessionId == CollaborationManager::Instance().OwnerSession())
    {
        StateManager::Instance()->XmlStateManager().SetValue("/Collaboration/SessionEnded", "true");
    }
}

.Net

void OnEndCollaborationSession(Guid sessionId, XElement command, XElement responses)
{
    // Only permit the owner session to set the state value ending the collaboration session
    if (sessionId == CollaborationManager.Instance().OwnerSession())
    {
        StateManager.XmlStateManager.SetValue("/Collaboration/SessionEnded", "true");
    }
}

Java

private class OnEndCollaborationSession implements CommandHandler
    public void invoke(UUID sessionID, Element command, Element responses)
    {
         // Only permit the owner session to set the state value ending the collaboration session
         if (sessionId == CollaborationManager.getInstance().getOwnerSession())
         {
            stateManager.getXmlStateManager().setValue("/Collaboration/SessionEnded", "true");
        }
    }

On the client

The value changed handler on the client would be the same as described in the previous section (Set behavior on host disconnect).

Example

Managing player modes in Asteroids

The Asteroids sample application (available in Java only) can run in one-player or two-player mode. This is managed by creating a handler for the SessionConnected and SessionDisconnected events on the SessionManager class.

The handler for SessionConnected gets called whenever the service application detects the arrival of a new client. This handler responds to the event by getting the session ID for the new client and binding it to the first available ship, then performing various initialization actions. If a second player joins, the handler fires again and the game is reconfigured to enter two-player mode.

Similarly, when one of the two players disconnects, an event handler is triggered, which responds by unbinding the ship corresponding to the session ID of the disconnected player.

Java

The handler for session connected:

private class SessionConnectedHandler implements EventHandler<SessionEventArgs>
{
    public void invoke(Object source, SessionEventArgs args)
    {
        // Get the session ID
        UUID sessionId = args.getSessionId();
        String playerName = args.getCommand().getChildText("Name");
        
        // If this is the first connected player, bind the sessionId to the first ship
        // and run in single-player mode
        if (ship1Controller.getSessionId().equals(UUIDUtil.EmptyUUID))
        {
            ship1Controller.setSessionId(sessionId);
            ship1Controller.setConnected(true);
            Ship ship1 = ship1Controller.getShip();
             ship1.setName(playerName);
             ship1.setDefaultPosition(new Point2D.Double(getWidth() / 2.0, getHeight() / 2.0));
             initializeGame();
        
        // If this is the second connected player, bind the sessionId to the second ship
        // and run in two-player mode
        }else if (ship2Controller.getSessionId().equals(UUIDUtil.EmptyUUID))
        {
            Dimension size = getSize();
            ship2Controller.setSessionId(sessionId);
            ship2Controller.setConnected(true);
            Ship ship2 = ship2Controller.getShip();
            ship2.setColor(Color.GREEN);
            ship2.setName(playerName);
            ship2.setDefaultPosition(new Point2D.Double(size.width / 3.0, size.height / 2.0));
            ship2.initialize(MonotonicTimeUtil.currentTimeMillis());
            Ship ship1 = ship1Controller.getShip();
            ship1.setColor(Color.YELLOW);
            ship1.setDefaultPosition(new Point2D.Double((2.0 * size.width) / 3.0, size.height / 2.0));
            if (gameOver)
                ship1.initialize(MonotonicTimeUtil.currentTimeMillis());
            else
            {
                ship2.startGame();
                ship2.setVisible(false);
             }
         }
         else
            log.warn("ConnectSession with two players already connected, sessionId is " + sessionId.toString());
    }
}

The handler for session disconnected:

private class SessionDisconnectedHandler implements EventHandler<SessionEventArgs>
{
    public void invoke(Object source, SessionEventArgs args)
    {
        UUID sessionId = args.getSessionId();
        if (ship1Controller.getSessionId().equals(sessionId))
            ship1Controller.setConnected(false);
        else if (ship2Controller.getSessionId().equals(sessionId))
            ship2Controller.setConnected(false);
        else
            log.warn("DisconnectSession from unknown session, sessionId is " + sessionId.toString());
    }
}