Home  >  

Introduction to Adobe Flash Collaborative Services

Author photo
AddThis Social Bookmark Button

Introduction

AFCS (Adobe Flash Collaboration Service) previously known as 'Cocomo' is a Platform as a Service (PaaS) that that allows Flex developers to address a class of applications known as collaborative applications.

Collaborative Applications have progressed from a nice cool thing into serious applications. Almost all of us would have participated in an online chat or a web meeting, which allows a group of users to chat, share files, do screen sharing, take polls, ask questions and receive answers, etc. AFCS aims to lower the barrier to entry for developers to bring such collaborative features into their applications.

A developer would have general questions like:
  • Why do we need a set of components to enable collaborative features in my application?
  • It should not be that complex to build a collaborative application, right? Isn't chat and sharing some files the maximum that I would want?
  • …and many more.
Well, building collaborative applications is not as simple as it sound. If you wish to build a collaborative application - you will need to consider the following in your application:
  • Handle Audio, Video and all other forms of transcoding data
  • Ensure that your application can scale to a large number of users.
  • Enable shared state in your application and ensure that multiple users are co-coordinated with each other so maintain data integrity
  • Reuse commonly used components like chat, notes, whiteboard, etc so that you can build the applications quickly and not reinvent the wheel.
  • Handle User Management and Permissions
The challenges keep adding up and it ends up being quite a task for the developer. AFCS precisely tries to address these issues by providing to the developer the following:
  • A set of collaborative components that can be simply dropped into your application to speed up development. These components range from simple chat window to a shared whiteboard.
  • A set of APIs that can be used to address collaborative application needs like maintaining shared state, different kinds of media, user management, permissions and authentication. These APIs and components can be extended should you need to build upon them.
  • A server side cloud which alleviates you from common deployment issues like scaling, reliability, etc.

It would be fair to summarize the AFCS in its current incarnation is primed for developers to give it a spin and build some fun as well as serious collaborative applications. So let us start our collaboration to build AFCS applications as we through the rest of the article.

The full source for the samples in this article is available for download here.

Getting Started: My First AFCS Application

The AFCS home page is located at http://labs.adobe.com/technologies/afcs/ and that will be the first stop in our journey.

Our goal in this section will be to get an AFCS application up and running in the minimum possible time.

The series of steps that we will go through are given below:
  1. Sign up and get an AFCS Account
  2. Download the AFCS SDK
  3. Understand what the AFCS SDK contains
  4. Build our first AFCS Application

Step 1: Sign up for AFCS

Go to the AFCS home page. You need to first sign up for AFCS by clicking on any of the signing up links as shown below:

Alt Text

Once you do that, you will be lead to an application as shown below:

Alt Text

If you already have an Adobe Developer Account, you could use that and you are good to go. If not, you will need to click on the purple New Dev? Sign up! button. Clicking on the purple button, leads you to the Signup dialog which is pretty much normal stuff. Enter in your details and pay special attention to the Account Name field, which we will come to later.

Depending on whether you signed up fresh or used your existing Adobe Developer Account, you could login from the previous screen. This will bring up your AFCS Account Portal as shown below. Note that the Account URL currently is http://connectnow.acrobat.com/[YourAccountName].

Now that we are done with registering and creating our AFCS Account, it is time to download the SDK.

Step 2: Download the AFCS SDK

At the time of writing, the latest version of the AFCS SDK was 0.92. To download the SDK click on the purple button Download the SDK.

Once you are done with the downloading, you should have the following file: CocomoSDK_0.92.zip

Step 3: Contents of the AFCS SDK

Expand this zip file to a folder of your choice. For e.g. I expanded it to my root folder and the contents are listed below:

C:\CocomoSDK_0.92>dir /b

  • book.xml
  • docs
  • examples
  • extras
  • index.html
  • lib
  • plugin.xml
  • RTC SDK License 091808.pdf
  • src
Let us categorize the contents of the SDK into the following areas:

AFCS SWC File

To start enabling AFCS functionality into your Flex Applications, all you need to do is add the appropriate .SWC library to your Flex Project Classpath. AFCS SDK provides .SWC files for Flash Player 9 and Flash Player 10 respectively. You can take the afcs.swc file of your choice by navigating to the lib\player9 or lib\player10 directory.

Help Contents

AFCS comes along with useful developer documentation present in the docs folder. It ships with great examples that are well documented in source code. They are present in the examples folder. I strongly suggest that once you are done with this article, your next step should be to go through these samples. They will give you a good sense of the wide range of possibilities with AFCS.

Sources

This is an optional step but recommended. In case you need to debug along and step into the AFCS Code, you could add the contents of this folder to the src folder of your Flex project.

Extra Utilities

AFCS ships with a couple of Adobe AIR applications that are crucial to your developing applications. They are shipped as Adobe AIR applications so please ensure that you have the AIR runtime installed. They are present in the extras folder.

The applications are:

AFCSDevConsole.air

The AFCS Development Console is a desktop AIR program that allows the developer to login to their online AFCS account and manage all administration operations like account management, room management, monitoring the application, etc.

Double-click on the file and install the Development Console. Please do this step since we will be using the Development Console through the rest of the article. We will use the Development Console in detail later.

LocalConnectionServer.air

The LocalConnectionServer application allows for offline testing of your AFCS application. The AFCS functionality is hosted in the Adobe cloud and to test your applications, it means that you need to be connected to the Internet. To enable local testing of your applications without being connected to the cloud, you could use the LocalConnectionServer which mimics the cloud thereby avoiding the need to be online.

Please do note that the LocalConnectionServer does not support all AFCS functionality especially related to streaming components (Audio/Video).We do not plan to use this in our article here.

Step 4: Build our first AFCS Application -AFCS 101

Before we get down to the basics of building our first AFCS application, let us take a look at when it runs:

Alt Text

The application is an online chat room where several users can login and chat with each other. On the left is a roster of participants that gets updated on its own when participants login and logout. On the right is a chat console that allows a participant to send public/private messages, receive private messages and view all public messages as they happen. While this is not an ideal application that demonstrates all of AFCS' capabilities, it is just about enough to get ourselves started. So let us now look at how it came together.

Create a room

We need to create a room first. You can think of a room logically as some sort of container in which your AFCS application will run under. Users will login and on successful authentication be allowed into the room. We will look at this in more detail later but for now this will suffice.

You can create a room in two ways: using the online AFCS Developer Portal or using the local AFCS Developer Console. We will focus on the AFCS Developer Console (you could try doing the same step through the online AFCS Developer Portal too if you wish). Please note that you need to be connected to the Internet to allow the AFCS Developer Console application to function normally since it interacts with the hosted AFCS cloud.

To create the room, launch the AFCSDevConsole AIR Program. This will bring up the application as shown below:

Alt Text

As you can see, there is currently no Account defined. Since you have already created a Developer Account at the online AFCS site, you can use the same account here. To do that, simple click on the Add button at the bottom of the Accounts panel. This will bring up a dialog as shown below:

Alt Text

Simply enter your existing account details over here. Remember we had discussed that the Account URL is http://connectnow.acrobat.com/[YourAccountId]. So enter the Account URL accordingly and fill in the rest of the details. Give a name for your account. This can be any name since you can manage multiple accounts from this Developer Console. Select the other options as shown and click on Save.

This will bring up the screen where you will see your recently added account as shown below:

Alt Text

Click on the Account Name, it will display the Templates and Rooms. Do not worry about Templates at this point. We will focus on Rooms. Currently the room list is empty since we have not created any room. So all we need to do now is to add a room. Select the default Template first and then click on the Add button at the bottom of the Rooms panel. This will bring up a dialog where you can enter the Room name as shown below:

Alt Text

Note that I am using demoroom as the name of my room here. Click on the Save button to create the room. This will add the room to the Room list as shown below:

Alt Text

If you click on the room (as I have done on the demoroom above), you will see the Room URL show for you. The Room URL is an important attribute that you will need to note down when you get coding your AFCS Flex Application. The format for the Room URL is as follows:

http://connecnow.acrobat.com/[AccountName]/[roomname]

So in the example so far, the account name is: http://connectnow.acrobat.com/romin/demoroom where romin is my Account Name at the AFCS Service and demoroom is the sample room that I have created. You should use your own account name.

Note that Account Name is the account name that you used while signing up for the AFCS Service and not the Account Name that used for adding to the Developer Console. We will come back to the Developer Console a lot more after we have first got our application ready and running.

Develop the AFCS Flex Application using Flex Builder

We shall now develop our AFCS 101 Application using Flex Builder. The steps are:

  1. Launch Flex Builder. Create a New Flex Project and choose Web Application. Click on Finish for now after entering a Project name. I have given it the name AFCS101. A link to download the sources is given in the the Resources section.
  2. Go to the Project Properties ' Flex Build Path ' Library Path. Here we need to link the AFCS SWC file.
  3. Click on the Add SWC button and choose the appropriate afcs.swc file depending on the Flash Player version that you have. For e.g. if you are using Flash Player 10, the afcs.swc file is located in the \lib\player10 folder of the AFCS SDK that you download and expanded on your machine. A sample screenshot of the SWC linking on my project is shown below:

    Alt Text

  4. Finally, we write some code. I promise this is the last step. We had called this Flex Project AFCS101, so the main Application MXML file i.e. AFCS101.mxml is shown below:
 
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" 
	xmlns:rtc="AfcsNameSpace">

	<rtc:AdobeHSAuthenticator id="auth" 
		userName="<YourUserName>" 
		password="<YourPassword"/>

	<rtc:ConnectSessionContainer 
		roomURL="http://connectnow.acrobat.com/romin/demoroom"  
		authenticator="{auth}" 
		autoLogin="true" width="100%" height="100%">

		<mx:HDividedBox width="100%" height="100%">
			<rtc:Roster width="30%" height="100%"/>
			<rtc:SimpleChat width="30%" height="100%"/>
		</mx:HDividedBox>

	</rtc:ConnectSessionContainer>

</mx:Application>

This is not an ideal way to write an AFCS application. It does not handle important Events or Exceptions, but we do not want to worry about that for now. We will go through the details of all that is happening in the next few sections, but for the moment, we will keep it simple by discussing some aspects of the code.

  1. In the tag, we have defined the AfcsNamespace to make it easier to include the AFCS classes.
  2. The most important class here is the ConnectSessionContainer. This is based on the UIComponent and is responsible for the following:
    • Logging in and authenticating the user to the room and establishing a session
    • Managing state along with synchronization
    • Host for ready to use AFCS Pods. An AFCS Pod is a ready to use mini application that implements several common collaborative applications like Chat, File Sharing, Webcam viewer, etc.
The ConnectSessionContainer has 3 important attributes:
  1. roomURL: This is the room URL that we wish to connect to. We have specified the url of the room that we created earlier
  2. authenticator: This attribute is used to indicate how the ConnectSessionContainer will authenticate the user to the room. You can see in the code that we are referring an AdobeHSAuthenticator class. This class authenticates the user to the AFCS Service. You need to provide your AFCS UserName and password. The UserName is typically your email id that you used to sign up at the AFCS site.
  3. autoLogin: This is set to true for this sample. What it means is that on creation of the ConnectionSessionContainer, it will attempt to login the user using the authenticator it is associated with and establish a session.
Once the authentication is done successfully, the ConnectSessionContainer which acts as a container for other UI components, will instantiate its children. We have currently added two Pods (A Roster Pod and a Chat Pod).

Run it

Launch the Application and you should see the output as shown below:

Alt Text

I have provided my AFCS username and password while logging in, hence my name. To simulate several users, you can launch multiple instances of the same Flex application. Since the Flex Application uses the same username/password to login, it will log you in with a user display name as [Name] 2, [Name] 3, and so on. This is just a peculiarity at this point, which we will address when we expand on our AFCS 101 Sample.

For now, you can experiment with more Pods. As mentioned, AFCS Pods are ready to use mini applications that you could use and they give you rich functionality that would take a lot of time to develop. AFCS comes with several pods that you should try out at this point. Remember to add them as children within the tag.

Here is a snippet of code to try out. I suggest to run this by launching multiple applications so that you can see how updating stuff in one application reflects correctly on the other application instances.

 
<rtc:ConnectSessionContainer   
	roomURL="http://connectnow.acrobat.com/romin/demoroom"  
	authenticator="{auth}" 
	autoLogin="true" 
	width="100%" height="100%">
	<mx:VBox width="100%" height="100%">
		<mx:HBox width="100%" height="50%">
			<mx:VBox width="30%" height="100%">
				<rtc:WebCamera height="30%"/>
				<rtc:Roster width="100%" height="70%"/>
			</mx:VBox>
			<rtc:SimpleChat width="40%" height="100%"/>
			<rtc:Note width="30%" height="100%"/>
		</mx:HBox>
		<mx:HDividedBox width="100%" height="50%">
			<rtc:SharedWhiteBoard height="100%" width="50%"/>
			<rtc:FileShare height="100%" width="50%"/>
		</mx:HDividedBox>
	</mx:VBox>
</rtc:ConnectSessionContainer>

Expanding on the AFCS 101 Sample

So far we have covered a simple AFCS 101 example where we saw a couple of AFCS Pods in action. In this section, we are going to expand on that code by addressing a couple of important areas: User Authentication and handling basic AFCS Events.

Let us address User Login first. You must have noticed that we used the following snippet for the class. This class is also present in the com.adobe.rtc.authentication package.

 
<rtc:AdobeHSAuthenticator id="auth" 
                          userName="<YourUserName>" 
                          password="<YourPassword"/>

This is not a perfect mechanism and should only be used by the developer to do their own testing and not for a hosted collaborative application. This is because you do not want to hardcode your user credentials into your client application.

The ideal scenario would be for your application to allow the user to enter his/her credentials either as a guest entry (where a user enters a friendly name) or an authenticated user (where your application verifies the username/password entered). Adobe AFCS does this for you via the class as follows:

  • To login as guest, you need to only set the userName attribute. Set the password attribute to null.
  • To enter as an authenticated user, you need to provide both the userName and password attributes as we have done so far. The only catch here is that this Authenticator Service i.e. AdobeHSAuthenticator authenticates only AFCS Account username/password combination.

External Authentication

If you wish to provide your own Authentication mechanism and do not wish to use the AdobeHSAuthenticator class. In that case you can go for External Authentication. This is supported by AFCS. What this means is that your server side application will need to provide its own authentication and then interact with the AFCS Service. This is known as External Authentication in AFCS parlance. The AFCS also provides server side scripts present in \extras\scripts folder of the Cocomo SDK distribution. We will not cover External Authentication in this article.

Remember we had covered the ConnectSessionContainer class as the important class that initializes the flow correctly. To reiterate:

  1. The ConnectSessionContainer (present in package com.adobe.rtc.session) on creation will perform the authentication. It does it on creation only if you have set the autoLogin attribute to true. It has a reference to the AdobeHSAuthenticator class that it will use to perform the authentication. You specify the room that you wish to connect to via the roomURL attribute of the ConnectSessionContainer.
  2. If the authentication is successful, it will hold a session to the room. If Authentication is successful or it fails, com.adobe.rtc.events.AuthenticationEvent will be fired which you can listen to in your program and respond appropriately.
  3. If the authentication is successful, it will create a session to the room and all the existing AFCS Pods will synchronize themselves with any data that is currently present in that room for those respective pods (for e.g. existing chat messages). We can get notified of successful synchronization via the com.adobe.rtc.events.SessionEvent, which we will listen to in our code shortly. It is a good practice to make the Container with the Pods (most likely ConnectionSessionContainer) visible only when the Session has been properly synchronized.

Let us make appropriate code changes now to our running example so that we provide for:

  1. User Login (either as a guest or AFCS Account)
  2. Listen to Authentication and Session Events and respond to them

The code is show below:

 
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" xmlns:rtc="AfcsNameSpace" creationComplete="initApp()">
<mx:Script>
	<![CDATA[
		import com.adobe.rtc.events.SessionEvent;
		import mx.controls.Alert;
		import com.adobe.rtc.events.AuthenticationEvent;
	
		public function initApp():void {
 			auth.addEventListener(AuthenticationEvent.AUTHENTICATION_SUCCESS,
 			onAuthenticationResponse);
 		
 	       
 			auth.addEventListener(AuthenticationEvent.AUTHENTICATION_FAILURE,
  			onAuthenticationResponse);
 		
 			afcsSession.addEventListener(SessionEvent.SYNCHRONIZATION_CHANGE,
 			onSessionEventResponse);
 
 			afcsSession.addEventListener(SessionEvent.ERROR,
 			onSessionEventResponse);
 		}
		
		public function onSessionEventResponse(event:Event):void {
 			if (event.type == SessionEvent.SYNCHRONIZATION_CHANGE) {
  				if (afcsSession.isSynchronized) {
   					//Now we are connected and the Pods have synchronized themselves, so switch to main Screen
   					//Switch to Collaborative Pods i.e. ConnectSessionContainer
   					vsMain.selectedIndex = 1;
   				}
  				else {
   					//We are disconnected now 
   					afcsSession.roomURL = null;
   					vsMain.selectedIndex = 0;
   				}
  			}
 			else if (event.type == SessionEvent.ERROR) {
  				var sError:SessionEvent = event as SessionEvent;
  				Alert.show(sError.error.name + " : " + sError.error.message);
  			}
 		}
		
		public function onAuthenticationResponse(event:AuthenticationEvent):void {
 			if (event.type == AuthenticationEvent.AUTHENTICATION_SUCCESS) {
  				trace("Authentication Succeeded");
  			}
 			else if (event.type == AuthenticationEvent.AUTHENTICATION_FAILURE) {
  				Alert.show("Authentication Error : " + event.toString());
  			}
 		}
	
		public function login():void {
 			auth.userName = username.text;
 			auth.password = password.text;
 			if (rolename.selectedLabel == "Guest") auth.password = null;
 			afcsSession.login();
 		}
		
		public function logout():void {
 			afcsSession.logout();
 		}
	]]>
</mx:Script>
<rtc:AdobeHSAuthenticator id="auth"/>
<mx:ViewStack id="vsMain" width="100%" height="100%">
	<mx:Form>
		<mx:FormHeading label="AFCS 101 Login"/>
		<mx:FormItem label="UserName:">
			<mx:TextInput id="username"/>
		</mx:FormItem>
		<mx:FormItem label="Password:">
			<mx:TextInput id="password"/>
		</mx:FormItem>
		<mx:FormItem label="Role:">
			<mx:ComboBox id="rolename">
				<mx:dataProvider>
					<mx:Array>
						<mx:String>User</mx:String>
						<mx:String>Guest</mx:String>
					</mx:Array>
				</mx:dataProvider>
			</mx:ComboBox>
		</mx:FormItem>
		<mx:FormItem>
			<mx:Button label="Login" click="login()"/>
		</mx:FormItem>
	</mx:Form>
	<rtc:ConnectSessionContainer id="afcsSession" roomURL="http://connectnow.acrobat.com/romin/demoroom" authenticator="{auth}" autoLogin="false" width="100%" height="100%">
		<mx:VBox width="100%" height="100%">
			<mx:Button label="Logout" click="logout()"/>
			<mx:HDividedBox width="100%" height="100%">
				<rtc:Roster width="30%" height="100%"/>
				<rtc:SimpleChat width="30%" height="100%"/>
			</mx:HDividedBox>
		</mx:VBox>
		
	</rtc:ConnectSessionContainer>	
</mx:ViewStack>

</mx:Application>

Let us understand the key pieces of the code:

  1. We restructured the presentation part of the code by introducing a . This ViewStack will have two child components: A User Login form and our existing ConnectSessionContainer.
  2. The User Login form takes as user input the username, password and the role (either a guest or a user). On click on the Login button, we call the login() method.
  3. The username and password attributes are no longer set in the AdobeHSAuthenticator class. They are set in the login() method depending on the username/password and role entered by the user. To login a user as a guest, no password is provided. This will set the password attribute in the Authenticator class to null, which will enable one to login as a guest.
  4. The autoLogin attribute of the ConnectSessionContainer is set to false now, since we do not want the container to login and establish a session on creation. This will now be done explicitly in the login() method by calling the login() method on the ConnectSessionContainer instance i.e. afcsSession.
  5. On creationComplete event of the main Application, we invoke a method named initApp(), which sets us up as listeners for the events that we are interested in. The events that are important are the Authentication Events on the AdobeHSAuthenticator instance (auth) and the Session Events on the ConnectSessionContainer instance (afcsSession).
  6. Note that we do not switch the selected Child in the ViewStack if the AuthenticationEvent.AUTHENTICATION_SUCCESS Event is fired. As mentioned, we need to show the container only once all the AFCS Pods are initialized and setup correctly. This is notified to us by AFCS through the SessionEvent.SYNCHRONIZATION_CHANGE Event.
  7. Finally we have a Logout button, which simply invokes the logout() method, which in turn invokes the logout() method on the afcsSession object.

Auto Promoting Users

Before we run this example, we need to understand a few key points about AFCS and how users and role management is handled. Currently there are 3 user roles in AFCS:

  • OWNER
  • PUBLISHER
  • VIEWER

At this point it is sufficient to understand that in order to participate and contribute to the collaborative activies i.e. posting a chat message, uploading file, etc., you need to be a PUBLISHER i.e. you need to have the PUBLISHER role. By default all guests are given the VIEWER role. What this means is that while they can be logged into a room and observe stuff happening in there, they cannot publish their own items.

So coming back to our example, in case a guest logs in, he/she will be given a VIEWER role. This will not allow them to post messages. To enable that someone needs to be able to raise their role to that of a PUBLISHER. AFCS does provide low levels APIs where you can programming change a users role but we will keep it simple for now. Luckily for us, AFCS has a room feature called Auto-Promote Users. What this means is that all users with a role of VIEWER will be automatically raised to that of a PUBLISHER on successfully entering the room. Perfect!

To enable this, we need to do a little bit of Administration and this is done through the Developer Console. Launch the Developer Console program (in case you do not have it running). The screenshot for our demoroom navigation is shown below:

Alt Text

Click on the Enter Room button or alternately you could select the Automatically Enter Room checkbox at the bottom to enter you automatically once you select the room. Once you are taken into your room, you will see the second tab Manage selected as shown below:

Alt Text

In the Room Settings Panel (on the extreme left), you will find a checkbox aptly named Auto-Promote Users. By default it is not selected. Please select it. That is all you need to do. The Developer Console will update the Room Settings on the Server for you.

Let us run the application now and see collaborative chat in action. On Application launch, you will first see the following screen (two instances shown here just for illustrating that there could be two types of users):

You could either login as a guest by simply specifying the username and selecting a role as Guest. Or you could login with your AFCS Account by entering that along with a role as User. Here is a snapshot of the application after several users have logged in:

Alt Text

In the AFCS Developer Console, navigate to your room and on the Manage Tab, you should see a screen that looks something like this:

Alt Text

We will get to other fields a little later in the article but the area of attention now should be the Current Users panel. Note that I launched the Developer Console after using 3 users i.e. my account, guest1 and guest2 as we saw previously. They are visible. But why does it show my name twice. This is because the Developer Console is also considered as a user of the room since you choose the room and entered it. And since we used the same AFCS Account of mine, it shows the name with the number suffix at the end.

You can experiment by closing one of the User Applications (guest1) and you will find that the Developer Console updates itself correctly and guest1 will no longer be seen in the list of participants. Try it!

AFCS Architecture

Now that we are comfortable with creating a simple AFCS application, it is time to move on to the details and explore various other powerful features within AFCS that make it possible to create rich collaborative applications.

We will start first with the architecture diagram taken from the AFCS documentation. The idea is not to cover each aspect in detail but to give you enough overview to understand the architectural pieces of AFCS.

Alt Text

Let us now look at each of the layers in brief.

Session and Authentication

These are responsible for authenticating users and establishing session. They are the first step that you need to do in using any of the AFCS services. Note that we have used the Authentication class via the com.adobe.rtc.authentication.AdobeHSAuthenticator class. The Session classes are present in the com.adobe.rtc.session package.

There are two core classes: ConnectSessionContainer which we have seen already and the ConnectSession class. You could use the ConnectSession class too instead of the ConnectSessionContainer, the key difference being that it is not based on the UIComponent class.

Shared Managers

Shared Managers in AFCS are the workhorses of the AFCS Architecture. They are singleton classes that give you the ability to perform Room, User, Stream and File Management of your application programmatically. This is powerful because it means that you can programmatically do all the things that one can from the Administrative Panels available via the Developer Console.

The Shared Manager classes are present in the com.adobe.rtc.sharedManagers package. All the manager instances are available to you from the session instance in your application.

To recap, the session object in our application so far was based on the ConnectSessionContainer class. To get the appropriate manager instance from the session class, all you need to do is to access it via the session object.

For example: Our session object in the previous examples was called afcsSession. We can get the managers as shown below:

  • Room Manager: afcsSession.roomManager
  • User Manager: afcsSession.userManager
  • File Manager: afcsSession.fileManager
  • Stream Manager: afcsStreamManager

We shall see the use of a Shared Manager later on in the article. You can refer to the documentation to learn all the functionalities available through the shared managers.

Shared Model Components

It is a common misnomer that building collaborative applications is easy. The most important factor in any collaborative application is maintaining the correct state of the application. Maintaining the correct state can mean different things. It comprises of several things like making sure that data is synchronized across to all participants, controlled access is given to certain kinds of data, making sure that only one participant modifies shared information at a time, etc.

Creating such data models is not an easy task for the developer. AFCS does it through several classes that are present in the com.adobe.rtc.sharedmodel package. There are several classes in this package namely:

  • CollectionNode
  • SharedCollection
  • SharedProperty
  • Baton
  • BatonProperty

The fundamental pattern for sharing data across multiple users is via the publish and subscribe method. To get notified of any data changes, we need to be subscribed to it. To set data into any shared model, we need to publish the data. The smallest unit of data is the MessageItem object, which we shall see in a short while.

If all this is a little bit overwhelming and too theoretical, be patient. We will cover some of these classes via working examples in the remaining sections of the article that should help clarify things.

A note about Permissions

Permissions are central to any collaborative application. As mentioned earlier, there are 3 kinds of roles that control what permissions (functionality) the user has:

  • OWNER
  • This role has permission to publish and subscribe messages. In addition to that, all administrative functions relating to rooms, users, streaming and file management can be performed by this role.
  • PUBLISHER
  • This role has permission to publish and subscribe to messages.
  • VIEWER
  • This role can only subscribe to messages.

In AFCS, permission is central to the theme. Starting from rooms to all the shared models, all activities are governed by permissions. This amount of granularity is key to any collaborative application.

Collaboration UI Components and Pods

The Pods are ready to use mini-Applications that one can plug into our application. The Pods implement common user interface modules that are present in most collaborative applications. The architecture diagram shows some of the Pods that are available today in AFCS. Many others are in development. The Pods are present in the com.adobe.rtc.pods package. The names of the Pods as given in the Architecture diagram are intuitive enough for you to understand what they do.

The Pods are created using existing UI classes. These foundation classes are known as the Collaboration UI Components. They are multi-user aware interface components and greatly reduce the time it would take for anyone to build their own collaboration components based on them. These classes are present in the com.adobe.rtc.collaboration package. For e.g. the Webcam pod in com.adobe.rtc.pods uses the com.adobe.rtc.collaboration.WebcamPublisher and com.adobe.rtc.collaboration.WebcamSubscriber classes.

Developers are encouraged to go through the source code and build their own components based on this or even extend these foundation classes.

Shared Manager Example

In this section, we are going to look at a simple example of a Shared Manager. The Shared Manager that we are going to use is the UserManager. This class is present in the com.adobe.rtc.sharedManagers package.

What we shall do is simply display a list of users as they enter a room. We already used a Roster Pod in the earlier samples to achieve that, but here we will attempt to build a crude but simple one. We are going to simply modify the code that we have been using all along.

The example illustrates how you could use some of the foundation classes to build your own customized components.

The source code for the Flex application is shown below:

 
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" xmlns:rtc="AfcsNameSpace" creationComplete="initApp()">
<mx:Script>
	<![CDATA[
		import com.adobe.rtc.events.SessionEvent;
		import mx.controls.Alert;
		import com.adobe.rtc.events.AuthenticationEvent;

		public function initApp():void {
 			auth.addEventListener(AuthenticationEvent.AUTHENTICATION_SUCCESS,onAuthenticationResponse);
 			auth.addEventListener(AuthenticationEvent.AUTHENTICATION_FAILURE,onAuthenticationResponse);
 			afcsSession.addEventListener(SessionEvent.SYNCHRONIZATION_CHANGE,onSessionEventResponse);
 			afcsSession.addEventListener(SessionEvent.ERROR,onSessionEventResponse);
 		}
		
		public function onSessionEventResponse(event:Event):void {
 			if (event.type == SessionEvent.SYNCHRONIZATION_CHANGE) {
  				if (afcsSession.isSynchronized) {
   					//Now we are connected and the Pods have synchronized themselves, so switch to main Screen
   					//Switch to Collaborative Pods i.e. ConnectSessionContainer
   					vsMain.selectedIndex = 1;
   				}
  				else {
   					//We are disconnected now 
   					afcsSession.roomURL = null;
   					vsMain.selectedIndex = 0;
   				}
  			}
 			else if (event.type == SessionEvent.ERROR) {
  				var sError:SessionEvent = event as SessionEvent;
  				Alert.show(sError.error.name + " : " + sError.error.message);
  			}
 		}
		
		public function onAuthenticationResponse(event:AuthenticationEvent):void {
 			if (event.type == AuthenticationEvent.AUTHENTICATION_SUCCESS) {
  				trace("Authentication Succeeded");
  			}
 			else if (event.type == AuthenticationEvent.AUTHENTICATION_FAILURE) {
  				Alert.show("Authentication Error : " + event.toString());
  			}
 		}
	
		public function login():void {
 			auth.userName = username.text;
 			auth.password = password.text;
 			if (rolename.selectedLabel == "Guest") auth.password = null;
 			afcsSession.login();
 		}
		
		public function logout():void {
 			afcsSession.logout();
 		}
	]]>
</mx:Script>
<rtc:AdobeHSAuthenticator id="auth"/>
<mx:ViewStack id="vsMain" width="100%" height="100%">
	<mx:Form>
		<mx:FormHeading label="AFCS 101 Login"/>
		<mx:FormItem label="UserName:">
			<mx:TextInput id="username"/>
		</mx:FormItem>
		<mx:FormItem label="Password:">
			<mx:TextInput id="password" displayAsPassword="true"/>
		</mx:FormItem>
		<mx:FormItem label="Role:">
			<mx:ComboBox id="rolename">
				<mx:dataProvider>
					<mx:Array>
						<mx:String>User</mx:String>
						<mx:String>Guest</mx:String>
					</mx:Array>
				</mx:dataProvider>
			</mx:ComboBox>
		</mx:FormItem>
		<mx:FormItem>
			<mx:Button label="Login" click="login()"/>
		</mx:FormItem>
	</mx:Form>
	<rtc:ConnectSessionContainer id="afcsSession" roomURL="http://connectnow.acrobat.com/romin/demoroom" authenticator="{auth}" autoLogin="false" width="100%" height="100%">
		<mx:VBox width="100%" height="100%">
			<mx:Button label="Logout" click="logout()"/>
			<mx:Label text="Welcome {afcsSession.userManager.getUserDescriptor(afcsSession.userManager.myUserID).displayName}!"/>
			<mx:Label text="Current Room Users:"/>
			<mx:List id="userList" dataProvider="{afcsSession.userManager.userCollection}" labelField="displayName" width="20%"/>
			
			
		</mx:VBox>
		
	</rtc:ConnectSessionContainer>	
</mx:ViewStack>

</mx:Application>

Let us go through the key parts of the code:

  1. Within the main ConnectionSessionContainer, we have a element whose data provider is set to the afcsSession.userManager.userCollection.
  2. Note that here we are using the userManager singleton class and as mentioned earlier, one can get access to all the shared manager class instances via the session instance, which is afcsSession in our case.
  3. Once we have a handle to the userManager, we invoke appropriate methods on it.

Run several instances of this application. You should see an output similar to the one shown below:

Alt Text

Sharing Data - Sample Applications

In the next few sections, we are going to dig deeper into the AFCS Shared Data model since sharing a state across multiple users is the key to building collaborative examples. The examples are not complete in any sense but they demonstrate key concepts for you to build on.

Note: Please keep the Developer Console application running since we are going to use that in conjunction with the samples that we will cover. It is important to understand what goes on behind the scenes to understand how AFCS works. And the Developer Console is a great tool as we shall see.

Let us first take a look at the some of the concepts behind AFCS Data Sharing components.

  1. Publish and subscribe pattern is used to send and receive messages. Since the pub/sub model is used, it is natural to think of subscribing and publishing to a destination.
  2. Each room can have one or more destinations.
  3. The destination is governed by permissions so you need to be a user with atleast PUBLISHER role to publish messages to it. If you have a VIEWER role, you can only subscribe to messages. A user with OWNER role can even configure these destinations.
  4. This destination in AFCS terms is known as a Collection Node. A CollectionNode contains several nodes. A collection node can be thought of a container for a particular kind of collaboration shared data. It can have several nodes and each Collection node has one or more Message Items.
  5. The data structure that you will publish and receive from the destination is known as the MessageItem. In other words, MessageItem is the payload. Publishers can publish the MessageItem via the CollectionNode'publishItem method. And subscribers can receive the MessageItems via the CollectionNodeEvent.ITEM_RECEIVE event.

Let us first look at an existing sample that we ran at the beginning of the article, the AFCS101 Sample. To recap, it was bare minimum AFCS application that had several Rosters in it. We will relist here the code for your reference:

 
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" xmlns:rtc="AfcsNameSpace">
<rtc:AdobeHSAuthenticator id="auth" userName="YourUserName" password="YourPassword"/>
<rtc:ConnectSessionContainer roomURL="http://connectnow.acrobat.com/romin/demoroom" authenticator="{auth}" autoLogin="true" width="100%" height="100%">
	<mx:VBox width="100%" height="100%">
		<mx:HBox width="100%" height="50%">
			<mx:VBox width="30%" height="100%">
				<rtc:WebCamera height="30%"/>
				<rtc:Roster width="100%" height="70%"/>
			</mx:VBox>
			<rtc:SimpleChat width="40%" height="100%"/>
			<rtc:Note width="30%" height="100%"/>
		</mx:HBox>
		<mx:HDividedBox width="100%" height="50%">
			<rtc:SharedWhiteBoard height="100%" width="50%"/>
			<rtc:FileShare height="100%" width="50%"/>
		</mx:HDividedBox>
	</mx:VBox>
</rtc:ConnectSessionContainer>
</mx:Application>

Run several instances of this application and launch the Developer Console. We will navigate directly into the room as described below:

Go to the Explore tab and look at the CollectionNodes listed. Your observations should match with this:

  • You will find that for our Pods, several default CollectionNodes created as shown below:
  • Click on the default_SimpleChat CollectionNode, you will find several nodes listed under them if you have published several chat messages. If not, go ahead and publish some chat messages.
  • The node of interest to us at this point is the history node. Under this node, you will find several objects of type MessageItem that contain as payload the chat messages that all users enter.
  • Take a look at the screen below:

Alt Text

In my application I posted 4 messages and all of them appear under the history node as Items. You can click on any of the Items and you will find the payload present in the msg attribute of the MessageItem. There are additional attributes too of the MessageItem that could be of interest depending on what you are trying to do in your application.

Now that we are here, let us look at a few more areas of the Node:

  • You will notice that Permissions are present here too. For each node, you have the Access Model and the Publish Model. These define what is the minimum role that is allowed to access (subscribe to) and publish the message to the node.
  • There is a property called persistItems. This is an interesting property. By default this is checked (true). This means that all messages published to this node are persisted even after the sessions end and the room is closed. If you go back later and relaunch the application, you will find that the Chat Pod will synchronize itself with the Shared Data and retrieve all the messages for you. You can refer to the documentation for the other properties. Each property will affect directly the kind of collaborative behavior that you expect from your room, its users and its shared state.

Now that we have a fair idea of the Collection Nodes and how data is published, let us look at two examples: one which demonstrates how we can define our own custom node and share the data across multiple subscribers and the other which demonstrates the use of a SharedProperty (another kind of Shared Data Model).

CollectionNode Example

In this example, we are going to replace the Chat application but in a simplified manner. The application will allow for users to publish messages (a web site url that they wish to share) to a Collection Node. The application in turn is also a subscriber to the Collection Node and is therefore notified whenever a message is published. We aggregate this and show the messages in a Text Area.

A Note about Permissions

To run these examples successfully remember that you need to first login as an owner i.e. use your AFCS Account to login. This is first time when the application is run, there is no data. The first time a message is published, the Collection Nodes need to be created and they will be created. However permissions kick in again and only if the logged in user has the OWNER role can the Collection Node be created. Once you have set it all up for the first time, then subsequently guests could log in.

In summary remember that in most situations the Administrator will need to come in and setup all the configuration aspects including Collection Nodes, its nodes, etc. If you do not do that and if guests go in for the first time, then you will get Security Exceptions thrown by ASF.

Let us look at the application in use. If you are running it for the first time, login with your AFCS Account name. Once you log in, you will see the application as shown below with no messages:

Alt Text

At this point in time, if you look at the Developer Console, you will find a new CollectionNode named chatMessageList created as shown below:

Alt Text

Now, go back to your application and enter a Web Site URL (or any text message if you want).

Alt Text

Click on the Set Value button. This will publish the item to the chatMessageList CollectionNode. Since this application is subscribed to that Node, it will receive an Event which will contain the message that we posted in the payload. We simply update the Text Area with it as shown below:

Alt Text

If you now look at the Developer Console, you will find that it created a node and added a MessageItem under it as shown below:

Alt Text

If you published several more messages, you will find the Message Items stored under the chatMessage Collection Node. The next two screens show the Application screen and the Developer Console after several messages have been published:

Alt Text

Alt Text

Now that we have seen the application work, here is the entire code listing.

 
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" xmlns:rtc="AfcsNameSpace" creationComplete="initApp()">
<mx:Script>
	<![CDATA[
		import com.adobe.rtc.messaging.MessageItem;
		import com.adobe.rtc.sharedModel.CollectionNode;
		import com.adobe.rtc.events.CollectionNodeEvent;
		import com.adobe.rtc.events.SessionEvent;
		import mx.controls.Alert;
		import com.adobe.rtc.events.AuthenticationEvent;
		
		private var _chatCollectionNode:CollectionNode = null;
		
		public function onItemReceive(event:CollectionNodeEvent):void {
 			trace("Item Received");
 			if (event.type == CollectionNodeEvent.ITEM_RECEIVE) {
  				var item:MessageItem = event.item as MessageItem;
  				txtWebURLMessages.text = txtWebURLMessages.text + "\n" + item.body;
  			}
 		}
		
		public function setValue():void {
 			var msg:String = txtURL.text;
 			var dt:Date = new Date();
 			var _item:MessageItem = new MessageItem("chatMessage",msg,dt.time.toString());
 			_chatCollectionNode.publishItem(_item);
 		}
	
		public function initApp():void {
 			auth.addEventListener(AuthenticationEvent.AUTHENTICATION_SUCCESS,onAuthenticationResponse);
 			auth.addEventListener(AuthenticationEvent.AUTHENTICATION_FAILURE,onAuthenticationResponse);
 			afcsSession.addEventListener(SessionEvent.SYNCHRONIZATION_CHANGE,onSessionEventResponse);
 			afcsSession.addEventListener(SessionEvent.ERROR,onSessionEventResponse);
 			
 		}
		
		public function onSessionEventResponse(event:Event):void {
 			if (event.type == SessionEvent.SYNCHRONIZATION_CHANGE) {
  				if (afcsSession.isSynchronized) {
   					//Now we are connected and the Pods have synchronized themselves, so switch to main Screen
   					//Switch to Collaborative Pods i.e. ConnectSessionContainer
   
   					//Setup the Collection Node
   					_chatCollectionNode = new CollectionNode();
   					_chatCollectionNode.connectSession = afcsSession;
   					_chatCollectionNode.sharedID = "chatMessageList";
   					_chatCollectionNode.subscribe();
   					_chatCollectionNode.addEventListener(CollectionNodeEvent.ITEM_RECEIVE,onItemReceive);
   
   					vsMain.selectedIndex = 1;
   				}
  				else {
   					//We are disconnected now 
   					afcsSession.roomURL = null;
   					vsMain.selectedIndex = 0;
   				}
  			}
 			else if (event.type == SessionEvent.ERROR) {
  				var sError:SessionEvent = event as SessionEvent;
  				Alert.show(sError.error.name + " : " + sError.error.message);
  			}
 		}
		
		public function onAuthenticationResponse(event:AuthenticationEvent):void {
 			if (event.type == AuthenticationEvent.AUTHENTICATION_SUCCESS) {
  				trace("Authentication Succeeded");
  			}
 			else if (event.type == AuthenticationEvent.AUTHENTICATION_FAILURE) {
  				Alert.show("Authentication Error : " + event.toString());
  			}
 		}
	
		public function login():void {
 			auth.userName = username.text;
 			auth.password = password.text;
 			if (rolename.selectedLabel == "Guest") auth.password = null;
 			afcsSession.login();
 		}
		
		public function logout():void {
 			afcsSession.logout();
 		}
	]]>
</mx:Script>
<rtc:AdobeHSAuthenticator id="auth"/>
<mx:ViewStack id="vsMain" width="100%" height="100%">
	<mx:Form>
		<mx:FormHeading label="AFCS 101 Login"/>
		<mx:FormItem label="UserName:">
			<mx:TextInput id="username"/>
		</mx:FormItem>
		<mx:FormItem label="Password:">
			<mx:TextInput id="password" displayAsPassword="true"/>
		</mx:FormItem>
		<mx:FormItem label="Role:">
			<mx:ComboBox id="rolename">
				<mx:dataProvider>
					<mx:Array>
						<mx:String>User</mx:String>
						<mx:String>Guest</mx:String>
					</mx:Array>
				</mx:dataProvider>
			</mx:ComboBox>
		</mx:FormItem>
		<mx:FormItem>
			<mx:Button label="Login" click="login()"/>
		</mx:FormItem>
	</mx:Form>
	<rtc:ConnectSessionContainer id="afcsSession" roomURL="http://connectnow.acrobat.com/romin/demoroom" authenticator="{auth}" autoLogin="false" width="100%" height="100%">
		<mx:VBox width="100%" height="100%">
			<mx:Button label="Logout" click="logout()"/>
			<mx:Label text="Welcome {afcsSession.userManager.getUserDescriptor(afcsSession.userManager.myUserID).displayName}!"/>
			<mx:HBox>
				<mx:Label text="Web URL:"/><mx:TextInput id="txtURL"/>
				<mx:Button label="Set Value" click="setValue()"/>
			</mx:HBox>
			<mx:Label text="Web URL List"/>
			<mx:TextArea id="txtWebURLMessages" width="100%" height="100%"/>
		</mx:VBox>
		
	</rtc:ConnectSessionContainer>	
</mx:ViewStack>

</mx:Application>

Let us go to important areas of the code:

  • On creationComplete event, the standard events are subscribed to as we have seen in the earlier examples.
  • Once the Session is synchronized, we setup the Collection node as shown below:
  •  
    //Setup the Collection Node
    _chatCollectionNode = new CollectionNode();
    _chatCollectionNode.connectSession = afcsSession;
    _chatCollectionNode.sharedID = "chatMessageList";
    _chatCollectionNode.subscribe();
    _chatCollectionNode.addEventListener(CollectionNodeEvent.ITEM_RECEIVE,
    	onItemReceive);
  • Note that we initiate the CollectionNode and then associate the session that we have with its connectSession attribute.
  • The SharedID attribute is like a unique name for your Shared Data Collection and we notice that AFCS uses this to set the name of the CollectionNodes as we saw through the Developer Console.
  • We need this application to be setup as a subscriber via the subscribe() method on the CollectionNode.
  • Finally, we listen to the ITEM_RECEIVE event which calls our function onItemReceive.
  • The onItemReceive method is shown below:

     
    public function onItemReceive(event:CollectionNodeEvent):void {
     	trace("Item Received");
     	if (event.type == CollectionNodeEvent.ITEM_RECEIVE) {
      		var item:MessageItem = event.item as MessageItem;
      		txtWebURLMessages.text = txtWebURLMessages.text + "\n" + item.body;
      	}
    }

    This method simply pulls out the MessageItem object from the event object and extracts out the body from it. It then simply appends it to the text area.

  • To publish a message, we need to use the publishItem method of the CollectionNode as shown below.
  •  
    var _item:MessageItem = new MessageItem("chatMessage",msg,dt.time.toString());
    _chatCollectionNode.publishItem(_item);
  • The publishItem method simply takes the MessageItem object that we built in the previous statement.
  • The MessageItem constructor takes the following 3 parameters:
    • Node Name (String)
    • The payload (String)
    • Unique Message Id (String). Here we have selected the time as the unique id but you can choose your own unique messaging scheme. This will help distinguish the messages. If you do not set this optional parameter then you might end up overwriting the same message.

SharedProperty Example

In the previous sample, we looked at the most fundamental Shared Data Model AFCS offers, the CollectionNode. We also looked at how at runtime the CollectionNodes, its nodes and message items are built up.

In this example, we will look at a very specific model called the SharedProperty. This is a simple and quick way to set a single property i.e. value across the application. The application demonstrates a single text field that is shared and updated concurrently by multiple users. The code is fairly explanatory. Some notes around that are:

  • On Session Synchronization, we setup a SharedProperty as shown below:
     
    //Setup the Shared Property
    sp = new SharedProperty();
    sp.connectSession = afcsSession;
    sp.sharedID = "idURL";
    sp.subscribe();
    sp.addEventListener(SharedPropertyEvent.CHANGE,onSharedPropertyChange);
  • We listen on the SharedPropertyEvent.CHANGE event. When it fires, we invoke the onSharedPropertyChangemethod, which simply updates the Text Field:

    public function onSharedPropertyChange(event:SharedPropertyEvent):void {
     	trace("Shared Property Changed");
     	txtURL.text = event.value;
    }
  • Similarly when we need to publish the message, we simply set the value on the shared property since it envelopes the MessageItem and we do not have to deal with it directly.
    public function setValue():void {
     	sp.value = txtURL.text;
    }
  • The entire code listing is shown below:

     
    <?xml version="1.0" encoding="utf-8"?>
    <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" xmlns:rtc="AfcsNameSpace" creationComplete="initApp()">
    <mx:Script>
    	<![CDATA[
    		import com.adobe.rtc.events.CollectionNodeEvent;
    		import com.adobe.rtc.events.SharedPropertyEvent;
    		import com.adobe.rtc.sharedModel.SharedProperty;
    		import com.adobe.rtc.events.SessionEvent;
    		import mx.controls.Alert;
    		import com.adobe.rtc.events.AuthenticationEvent;
    		
    		private var sp:SharedProperty = null;
    	
    		public function initApp():void {
     			auth.addEventListener(AuthenticationEvent.AUTHENTICATION_SUCCESS,onAuthenticationResponse);
     			auth.addEventListener(AuthenticationEvent.AUTHENTICATION_FAILURE,onAuthenticationResponse);
     			afcsSession.addEventListener(SessionEvent.SYNCHRONIZATION_CHANGE,onSessionEventResponse);
     			afcsSession.addEventListener(SessionEvent.ERROR,onSessionEventResponse);
     			
     			//Setup the Shared Property
     			sp = new SharedProperty();
     			sp.connectSession = afcsSession;
     			sp.sharedID = "idURL";
     			sp.subscribe();
     			sp.addEventListener(SharedPropertyEvent.CHANGE,onSharedPropertyChange);
     			sp.addEventListener(CollectionNodeEvent.SYNCHRONIZATION_CHANGE,onSyncChange);
     		}
    		
    		public function onSharedPropertyChange(event:SharedPropertyEvent):void {
     			trace("Shared Property Changed");
     			txtURL.text = event.value;
     		}
    		
    		public function onSyncChange(event:CollectionNodeEvent):void {
     			trace("Sync Change happened");
     		}
    		
    		public function setValue():void {
     			sp.value = txtURL.text;
     		}
    		
    		public function onSessionEventResponse(event:Event):void {
     			if (event.type == SessionEvent.SYNCHRONIZATION_CHANGE) {
      				if (afcsSession.isSynchronized) {
       					//Now we are connected and the Pods have synchronized themselves, so switch to main Screen
       					//Switch to Collaborative Pods i.e. ConnectSessionContainer
       					vsMain.selectedIndex = 1;
       				}
      				else {
       					//We are disconnected now 
       					afcsSession.roomURL = null;
       					vsMain.selectedIndex = 0;
       				}
      			}
     			else if (event.type == SessionEvent.ERROR) {
      				var sError:SessionEvent = event as SessionEvent;
      				Alert.show(sError.error.name + " : " + sError.error.message);
      			}
     		}
    		
    		public function onAuthenticationResponse(event:AuthenticationEvent):void {
     			if (event.type == AuthenticationEvent.AUTHENTICATION_SUCCESS) {
      				trace("Authentication Succeeded");
      			}
     			else if (event.type == AuthenticationEvent.AUTHENTICATION_FAILURE) {
      				Alert.show("Authentication Error : " + event.toString());
      			}
     		}
    	
    		public function login():void {
     			auth.userName = username.text;
     			auth.password = password.text;
     			if (rolename.selectedLabel == "Guest") auth.password = null;
     			afcsSession.login();
     		}
    		
    		public function logout():void {
     			afcsSession.logout();
     		}
    	]]>
    </mx:Script>
    <rtc:AdobeHSAuthenticator id="auth"/>
    <mx:ViewStack id="vsMain" width="100%" height="100%">
    	<mx:Form>
    		<mx:FormHeading label="AFCS 101 Login"/>
    		<mx:FormItem label="UserName:">
    			<mx:TextInput id="username"/>
    		</mx:FormItem>
    		<mx:FormItem label="Password:">
    			<mx:TextInput id="password" displayAsPassword="true"/>
    		</mx:FormItem>
    		<mx:FormItem label="Role:">
    			<mx:ComboBox id="rolename">
    				<mx:dataProvider>
    					<mx:Array>
    						<mx:String>User</mx:String>
    						<mx:String>Guest</mx:String>
    					</mx:Array>
    				</mx:dataProvider>
    			</mx:ComboBox>
    		</mx:FormItem>
    		<mx:FormItem>
    			<mx:Button label="Login" click="login()"/>
    		</mx:FormItem>
    	</mx:Form>
    	<rtc:ConnectSessionContainer id="afcsSession" roomURL="http://connectnow.acrobat.com/romin/demoroom" authenticator="{auth}" autoLogin="false" width="100%" height="100%">
    		<mx:VBox width="100%" height="100%">
    			<mx:Button label="Logout" click="logout()"/>
    			<mx:HBox>
    				<mx:Label text="Web Site URL: "/>
    				<mx:TextInput id="txtURL"/>
    				<mx:Button label="Set Value" click="setValue()"/>
    			</mx:HBox>
    		</mx:VBox>
    		
    	</rtc:ConnectSessionContainer>	
    </mx:ViewStack>
    
    </mx:Application>

    When we run the application, any user will see the following screen:

    The above value is a SharedProperty that can be set by any user. A snapshot of the Developer Console is shown below:

    Alt Text

    You will find our sharedID value of idURL is used as the CollectionNodes entry. The rest of the entries are self explanatory.

    The reader is encouraged to look at another Shared Data Model known as SharedCollection. The SharedCollection as compared to the SharedProperty allows list of records that can be shared by multiple users. You can envision binding this SharedCollection to a Grid and other Form elements to allow multiple users to work on several records simultaneously.

    Advanced Example

    In this section, we will cover a multi-user application where participants can co-search for a list of restaurants in a particular city based on the cuisine of their search. By co-search it means that any user can update the city and cuisine choice. This data will be shaared across with all participants and the search results will reflect accordingly for all participants.

    Let us take a look at the application working for a single user. On successful login, the application screen is shown below. It consists of two pods, the Roster Pod and the Chat Pod which are standard AFCS Pods. The user can enter the city and type of cuisine and click on Search. This will retrieve the restaurants serving that type of cuisine in the city selected. The search results are obtained from Yahoo and the integration is done via the Yahoo Query Language.

    Alt Text

    A sample is shown below, where a list of restaurants serving Chinese cuisine are retrieved from Yahoo. The user can click on any of the restaurant records in the grid and it will show some more detail in the right bottom panel. There are also 2 links where you can view the static Google map and go to their web site in case they have any.

    Alt Text

    If another user logs into the application, they will get the last value set for the City and cuisine type and the records will be retrieved for them too. A sample for another user is shown below:

    Alt Text

    The magic happens through the CollectionNode and Baton classes available in AFCS. The CollectionNode data model that we setup here is going to contain information that is shown in the following bar:

    The co-searching happens in the following fashion:

    1. The Application is subscribed to the CollectionNode so that it gets notified in case of changes. And it is also going to publish to the CollectionNode when the Search button is clicked.
    2. When the Search button is clicked, the data value is changed in the CollectionNode for all applications to update themselves.
    3. Once the value changes, a Yahoo Search is fired in the client application that will retrieve the results for restaurant listings and display it.
    4. The user can then select any record and view more details. Note that this is not synchronized and is completely local to the application.

    Take a look at another interesting feature of this application via the screenshot below. The scenario is like this:

    1. There are 2 users in the system.
    2. When one user has modified the value, we want the other user to get notified that the value is being modified and thereby disable their Search button.
    3. When the value is correctly updated on the screen, the Search button is again enabled.

    In the screenshot below, we find that a user Romin Irani gets a notified on his application that John has modified the value. The Search button is disabled for this user. John has modified the value and it is currently being updated, hence you see the message below:

    Alt Text

    The code listing shown below is straightforward and has been covered below. The key points are:

    1. When the session is synchronized, we setup the CollectionNode which will hold the city and cuisine type values. We also setup a Baton class. The Baton class is a new thing that we are covering here and it is a kind of Shared Model which helps to synchronize changes to a shared data value i.e. the CollectionNode. The rule is simple, only if you hold the baton can you make the change. So if you hold the baton, make the change and release it. And if you do not hold the baton, grab it, make the change and release it.
    2. The Baton is setup during synchronization as shown:
    3.  
      //Setup the Baton
      _baton = new Baton();
      _baton.collectionNode = _restaurantCollectionNode;
      _baton.timeOut = 5;
      _baton.sharedID = "OwnerId";
      _baton.subscribe();
      _baton.addEventListener(SharedModelEvent.BATON_HOLDER_CHANGE,doBatonChange);
    4. We subscribe to the Baton and listen to any changes in the current holder of the Baton.
    5. In the doBatonChange method, we do the following. If you are the current holder of the baton or if no one has it, you can click on the Search button so that the shared value can get updated. If you are not the current holder, it means that someone currently holds the baton and hence we disable the Search button.
    6.  
      if (_baton.holderID==afcsSession.userManager.myUserID || _baton.holderID==null) {
       	SearchBtn.enabled = true;
       	lblStatus.text = "";
      } else {
       	lblStatus.text = afcsSession.userManager.getUserDescriptor(_baton.holderID).displayName + " has set a value. Please hold!";
       	SearchBtn.enabled = false;
      }
    7. On click of the Search button, we need to publish changes to the city and cuisine type. This is also done by taking control of the baton as shown below:
    8.  
      public function search():void {
       	var _searchCriteria:Object = new Object();
       	_searchCriteria.city = txtCity.text;
       	_searchCriteria.foodType = foodType.selectedLabel.toString();
       	if (_baton.amIHolding) {
        		_baton.extendTimer();
        	} else if (_baton.canIGrab) {
        		_baton.grab();
        	} else {
        		return;
        	}
       	var msg:MessageItem = new MessageItem("currentCriteria", {city:txtCity.text, foodType:foodType.selectedLabel});
       	_restaurantCollectionNode.publishItem(msg);		
      }

    The entire code listing is shown below:

     
    <?xml version="1.0" encoding="utf-8"?>
    <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" xmlns:rtc="AfcsNameSpace" creationComplete="initApp()">
    <mx:HTTPService id="yahooService" resultFormat="object" method="GET" result="onYahooResult(event)"/>
    
    <mx:Script>
    	<![CDATA[
    		import mx.managers.CursorManager;
    		import com.adobe.rtc.sharedModel.Baton;
    		import com.adobe.rtc.events.SharedModelEvent;
    		import com.adobe.rtc.events.CollectionNodeEvent;
    		import com.adobe.rtc.sharedModel.CollectionNode;
    		import com.adobe.rtc.messaging.MessageItem;
    		import com.adobe.rtc.events.SessionEvent;
    		import mx.controls.Alert;
    		import mx.rpc.events.ResultEvent;
    		import mx.collections.ArrayCollection;
    		import com.adobe.rtc.events.AuthenticationEvent;
    		
    		private var _restaurantCollectionNode:CollectionNode = null;
    		private var _baton:Baton = null;
    
    	
    		public function initApp():void {
     			auth.addEventListener(AuthenticationEvent.AUTHENTICATION_SUCCESS,onAuthenticationResponse);
     			auth.addEventListener(AuthenticationEvent.AUTHENTICATION_FAILURE,onAuthenticationResponse);
     			afcsSession.addEventListener(SessionEvent.SYNCHRONIZATION_CHANGE,onSessionEventResponse);
     			afcsSession.addEventListener(SessionEvent.ERROR,onSessionEventResponse);
     		}
    		
    		public function doBatonChange(event:SharedModelEvent):void {
     			trace("Baton Changed ..");
     			if (_baton.holderID==afcsSession.userManager.myUserID || _baton.holderID==null) {
      				SearchBtn.enabled = true;
      				lblStatus.text = "";
      			}
     			else {
      				lblStatus.text = afcsSession.userManager.getUserDescriptor(_baton.holderID).displayName + " has set a value. Please hold!";
      				SearchBtn.enabled = false;
      			}
     		}
    		
    		
    		public function onItemReceive(event:CollectionNodeEvent):void {
     			trace("Item Received");
     			if (event.nodeName == "currentCriteria") {
      				trace("Got new values for Criteria");
      				txtCity.text = event.item.body.city;
      				foodType.selectedItem = event.item.body.foodType;
      				doSearch(txtCity.text,foodType.selectedLabel);
      			}
     		}
    		
    		public function doRestaurantChange(event:Event):void {
     			var _restaurant:Object = dgRestaurants.selectedItem;
     			lblRating.text = _restaurant.Rating.AverageRating;
     			lblPhone.text = _restaurant.Phone;
     			txtLastReview.text = _restaurant.Rating.LastReviewIntro;
     		}
    
    		
    		public function doSearch(_city:String,_foodType:String):void {
     				trace("Firing a search... on city = " + _city + " and Food Type = " + _foodType);
     	            var yahooURL:String = "http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20local.search%20where%20query%3D%22" + _foodType +"%22%20and%20location%3D%22" + _city + "%22&format=xml";
     		    	yahooService.url = yahooURL;
     		    	yahooService.send();
     		}
    		
    	    public function onYahooResult(event:ResultEvent):void {
     	    	trace("Got Results...");
     	    	var results:ArrayCollection = event.result.query.results.Result as ArrayCollection;
     	    	dgRestaurants.dataProvider = results;
     	    }
    		
    		
    		public function search():void {
     			var _searchCriteria:Object = new Object();
     			_searchCriteria.city = txtCity.text;
     			_searchCriteria.foodType = foodType.selectedLabel.toString();
     			if (_baton.amIHolding) {
      				_baton.extendTimer();
      			} else if (_baton.canIGrab) {
      				_baton.grab();
      			} else {
      				return;
      			}
     			var msg:MessageItem = new MessageItem("currentCriteria", {city:txtCity.text, foodType:foodType.selectedLabel});
     			_restaurantCollectionNode.publishItem(msg);
     		}
    		
    		public function onSessionEventResponse(event:Event):void {
     			if (event.type == SessionEvent.SYNCHRONIZATION_CHANGE) {
      				if (afcsSession.isSynchronized) {
       					//Now we are connected and the Pods have synchronized themselves, so switch to main Screen
       					//Switch to Collaborative Pods i.e. ConnectSessionContainer
       					vsMain.selectedIndex = 1;
       					
       					//Setup the Collection Node
       					_restaurantCollectionNode = new CollectionNode();
       					_restaurantCollectionNode.connectSession = afcsSession;
       					_restaurantCollectionNode.sharedID = "restaurantCoSearch";
       					_restaurantCollectionNode.subscribe();
       					_restaurantCollectionNode.addEventListener(CollectionNodeEvent.ITEM_RECEIVE,onItemReceive);
       					
       					//Setup the Baton
       					_baton = new Baton();
       					_baton.collectionNode = _restaurantCollectionNode;
       					_baton.timeOut = 5;
       					_baton.sharedID = "OwnerId";
       					_baton.subscribe();
       					_baton.addEventListener(SharedModelEvent.BATON_HOLDER_CHANGE,doBatonChange);
       				}
      				else {
       					//We are disconnected now 
       					afcsSession.roomURL = null;
       					vsMain.selectedIndex = 0;
       				}
      			}
     			else if (event.type == SessionEvent.ERROR) {
      				var sError:SessionEvent = event as SessionEvent;
      				Alert.show(sError.error.name + " : " + sError.error.message);
      			}
     		}
    		
    		public function onAuthenticationResponse(event:AuthenticationEvent):void {
     			if (event.type == AuthenticationEvent.AUTHENTICATION_SUCCESS) {
      				trace("Authentication Succeeded");
      			}
     			else if (event.type == AuthenticationEvent.AUTHENTICATION_FAILURE) {
      				Alert.show("Authentication Error : " + event.toString());
      			}
     		}
    	
    		public function login():void {
     			auth.userName = username.text;
     			auth.password = password.text;
     			if (rolename.selectedLabel == "Guest") auth.password = null;
     			afcsSession.login();
     		}
    		
    		public function logout():void {
     			afcsSession.logout();
     		}
    	]]>
    </mx:Script>
    <rtc:AdobeHSAuthenticator id="auth"/>
    <mx:ViewStack id="vsMain" width="100%" height="100%">
    	<mx:Form>
    		<mx:FormHeading label="AFCS 101 Login"/>
    		<mx:FormItem label="UserName:">
    			<mx:TextInput id="username"/>
    		</mx:FormItem>
    		<mx:FormItem label="Password:">
    			<mx:TextInput id="password"  displayAsPassword="true"/>
    		</mx:FormItem>
    		<mx:FormItem label="Role:">
    			<mx:ComboBox id="rolename">
    				<mx:dataProvider>
    					<mx:Array>
    						<mx:String>User</mx:String>
    						<mx:String>Guest</mx:String>
    					</mx:Array>
    				</mx:dataProvider>
    			</mx:ComboBox>
    		</mx:FormItem>
    		<mx:FormItem>
    			<mx:Button label="Login" click="login()"/>
    		</mx:FormItem>
    	</mx:Form>
    	<rtc:ConnectSessionContainer id="afcsSession" roomURL="http://connectnow.acrobat.com/romin/demoroom" authenticator="{auth}" autoLogin="false" width="100%" height="100%">
    		<mx:VBox width="100%" height="100%">
    			<mx:Button label="Logout" click="logout()"/>
    			<mx:HDividedBox width="100%" height="50%">
    				<rtc:Roster width="30%" height="100%"/>
    				<rtc:SimpleChat width="70%" height="100%"/>
    			</mx:HDividedBox>
    			<mx:VBox width="100%" height="50%">
    				<mx:ApplicationControlBar width="100%">
    					<mx:Label text="City:"/>
    					<mx:TextInput id="txtCity"/>
    					<mx:ComboBox  id="foodType">
    						<mx:dataProvider>
    							<mx:String>Chinese</mx:String>
    							<mx:String>Japanese</mx:String>
    							<mx:String>Thai</mx:String>
    							<mx:String>Indian</mx:String>
    							<mx:String>BBQ</mx:String>
    							<mx:String>Fast Food</mx:String>
    						</mx:dataProvider>
    					</mx:ComboBox>
    					<mx:Button label="Search" click="search()" id="SearchBtn"/>
    					<mx:Label id="lblStatus"/>
    				</mx:ApplicationControlBar>
    				<mx:HDividedBox width="100%" height="100%">
    					<mx:DataGrid id="dgRestaurants" width="50%" height="100%" change="doRestaurantChange(event)">
    						<mx:columns>
    							<mx:DataGridColumn dataField="Title"/>
    							<mx:DataGridColumn dataField="Address"/>
    							<mx:DataGridColumn dataField="City"/>
    							<mx:DataGridColumn dataField="State"/>
    							<mx:DataGridColumn dataField="Phone"/>
    						</mx:columns>
    					</mx:DataGrid>
    					<mx:Panel title="Restaurant Details" width="50%" height="100%" layout="vertical">
    						<mx:HBox>
    							<mx:Label text="Rating: "/>
    							<mx:Label id="lblRating"/>
    						</mx:HBox>
    						<mx:HBox width="100%" height="100%">
    							<mx:Label text="Last Review: "/>
    							<mx:TextArea id="txtLastReview" editable="false" width="100%" height="100%"/>
    						</mx:HBox>
    						<mx:HBox>
    							<mx:Label text="Phone: "/>
    							<mx:Label id="lblPhone"/>
    						</mx:HBox>
    						<mx:LinkButton visible="{dgRestaurants.selectedIndex != -1}" label="Web Site" click="navigateToURL(new URLRequest(dgRestaurants.selectedItem.BusinessUrl))"/>
    						<mx:LinkButton visible="{dgRestaurants.selectedIndex != -1}" label="Map &amp; Driving Directions" click="navigateToURL(new URLRequest(dgRestaurants.selectedItem.MapUrl))"/>
    					</mx:Panel>
    				</mx:HDividedBox>
    			</mx:VBox>
    		</mx:VBox>
    		
    	</rtc:ConnectSessionContainer>	
    </mx:ViewStack>
    
    </mx:Application>

    The Advanced Example shown here is just a starting up for you to take and extend it further. You can add a Yahoo Map or a Google Map to the application and co-browse the map together; you could much better visualizations to indicate that the values are being changed by another user, etc.

    This completes our coverage of AFCS. In the next section we will look at AFCS Pricing.

    AFCS Pricing Model

    An important announcement was made recently about the AFCS Pricing Model. This is important so as to send across a message to individual developers and organizations that there is a commitment to creating an online hosting service that can be used to host serious applications and in which Adobe will ensure that your application can get the right scalability and reliability.

    Does this mean that it will not be possible for an individual developer to try out AFCS without paying some fees upfront? Fear not!

    Adobe has clearly stated that there is no cost to entry. Any developer/organization can get an account and try out AFCS. It will be free to allow individuals and organizations alike to test drive AFCS and determine how it would fit within their application ideas. There is a slight catch though -- it is free only till it does not cross a certain usage limit in terms of users, resources, bandwidth, etc.

    Let us break that down into roughly 3 categories that any of you would fall under and see what is the current thought process or pricing proposals in place suggested by Adobe.

    1. If you are an individual developer or organization that wants to try out AFCS, the process still remains the same. You can sign up at the AFCS site, download the SDK and start with writing your sample programs. This is free to try out and you can continue to host your application provided you do not exceed certain usage limits.
    2. In case you think that your hosted application is likely to cross the free usage limit, you will need to do your groundwork in terms of what is the usage you are expecting. The AFCS Portal along with its usage graphs can help you determine that. The AFCS team could also work with you on this. The nice thing here is that typical models include pay per use and you could get a monthly bill depending on the usage that month. This could end up being huge for someone starting out in case your application is wildly successful. To solve this, Adobe has come out with a maximum bill amount per month (that is the initial thought process at this point but this is not final and could change). You define the maximum amount that you might like to get charged in a month and Adobe does the rest of the mechanics to stay within that range.
    3. The last category could be a situation where your application needs to scale to large limits and you want to ensure that AFCS has the capability at their data centres to tackle that. To address that, AFCS has teamed up with Connect Solutions to help you plan out your application and provide you more scalability, dedicated customer service, etc.

    The AFCS team is looking for feedback on their pricing model. They have hosted a pricing survey page and wish to use feedback to see what is on the minds of developers as far as pricing their offering is concerned.

    AFCS Showcase

    Adobe is also a showcase site where your AFCS application can be added to the showcase gallery. The AFCS Showcase site is hosted at http://labs.adobe.com/showcase/afcs/ and it contains some interesting applications that are powered behind the scenes by AFCS.

    Here is a sampler of the applications at the showcase gallery:

    • Sudocomo: A multi-player Sudoko game built with Adobe Flash Collaboration Service where you can work on the same puzzle and communicate with others.
    • Multi-User Yahoo Maps: Allows multiple users to co-browse a Yahoo! map using the Adobe Flash Collaboration Service. Zoom, pan, and draw over top of a map in real-time with other people.
    • Acesis Medical Peer Review Application: Leveraging Adobe Flash Collaboration Service, Acesis medical peer review application enables doctors to collectively discuss medical cases, co-browse through charts, adjust zoom levels of the graph, and share cursor positions.

    Conclusion

    Adobe AFCS is an exciting set of technologies that helps developers design, code and deploy collaborative applications quickly. The set of components is by no means complete since AFCS is still a Beta product but it is available at just the right moment for developers to exercise it and start understanding what it takes to create this class of applications.

    The full source for the samples in this article is available for download here.

    Read more from Romin Irani. Romin Irani's Atom feed iromin on Twitter

Comments

3 Comments

Hi,
Amazing article, thanks for sharing your knowledge on AFCS. Would it be ok if i translate that article in french for a flex related website http://www.flex-tutorial.fr ? I would give you the credits for it, of course.
Let me know what you think

Fabien

Graham Blake said:

The Terms of Service attachment for the AFCS seems to state (under the 'License to Use Service" heading) that the service can currently only be used in North America. Not so good if you're based in Europe.

Romin Irani said:

@Fabien : Thanks for your comments. You can translate the article to French. The only requirement is that they include a link back to this article.

Thanks,
Romin

Leave a comment


Tag Cloud

Question of the Week: Dream App

If you had an unlimited budget and unlimited resources what application would you build and why would you build it?

Answer

Latest Features

Recommended for You

@InsideRIA on Twitter

Archives

  • Or, visit our complete archive.  

About This Site

Welcome to the premiere community site for all things RIA sponsored by O'Reilly Media and Adobe Systems Incorporated.