Quantcast
Viewing latest article 3
Browse Latest Browse All 4

Creating A Mobile JavaScript Chat – Part 4: Cross-Domain Restrictions

In the former tutorial of this series, we created a sample mobile application that’ll run in an Android mobile device. All the HTML, CSS and JavaScript files, which the application was using, were stored in the /assets/www folder of our mobile project.


You can click here, to get the source code of this tutorial (92.9Kb zipped archive) »»

So in order to convert our chat application into a mobile app, it will be logical to start by simply putting our assets (i.e. HTML, CSS and JS) files into /assets/www folder.

Project Folder Structure

The structure of the folder will be as follows:


And the HTML of the page:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Chat Demo</title>
<meta name="description" content="Demo Mobile Chat Application">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="ico/chat/chat.ico">
<link rel="apple-touch-icon" href="ico/chat/chat.png">
<link rel="stylesheet" href="css/chat/style.css">
</head>
<body>
<div id="container">
	<header>
	</header>
	<div id="main" role="main">
		<div id="ChatConsole">
			<div id="Loading" class="loading" style="text-align:center"><img 
				src="image/loading.gif" style="vertical-align:middle"> loading...</div>
		</div>
		<div id="Controls">
			<label for="MessageToSend">message:</label>
			<input name="MessageToSend" type="text" id="MessageToSend">
			<input type="button" id="Action" value="Send!">
		</div>
	</div>
	<footer>
	</footer>
</div>
<script type="text/javascript" charset="utf-8" src="js/o2.js"></script>
<script type="text/javascript" charset="utf-8" src="js/o2.domhelper.ready.js"></script>
<script type="text/javascript" charset="utf-8" src="js/o2.eventhandler.core.js"></script>
<script type="text/javascript" charset="utf-8" src="js/o2.eventhandler.extend.js"></script>	
<script type="text/javascript" charset="utf-8" src="js/o2.stringhelper.core.js"></script>
<script type="text/javascript" charset="utf-8" src="js/o2.ajax.js"></script>
<script type="text/javascript" charset="utf-8" src="js/o2.ajaxcontroller.js"></script>
<script type="text/javascript" charset="utf-8" src="js/o2.ajaxstate.js"></script>
<script type="text/javascript" charset="utf-8" src="js/3rdparty/json2/json2.js"></script>
<script type="text/javascript" charset="utf-8" src="js/chat/main.js"></script>
</body>
</html>

I’ll intentionally modifyjs/chat/main.js, to alert any errors or exceptions:

function listen_error(status, statusText, xhr) {
	alert('error status:"' + status + '" text:"' + statusText + '"');
	
	//unregister(xhr);
	//retryListen();
}

function listen_exception(xhr, exception) {
	alert('exception ' + exception + ' ' + exception.message);

	//unregister(xhr);
	//retryListen();
}

Proxy = {
	listen: function() {
		var url = config.constants.url.chat.LISTEN;
		var get = o2.Ajax.get;
		var concat = o2.StringHelper.concat;
		var format = o2.StringHelper.format;

		state.lastIndex = state.lastIndex || 0;

		var request = get(url, {
			lastIndex: state.lastIndex
		}, {
			oncomplete: listen_complete,
			onerror: listen_error,
			onexception: listen_exception
		});

		/*request.controller = new o2.AjaxController(request, {
			timeout:config.constants.timeout.COMET_TIMED_OUT
		});*/
		
	}

};

Wait… What?

Let us run the project in Android emulator, and see what we get:

Image may be NSFW.
Clik here to view.
Status "0" generally means a connection problem.

Status "0" generally means a connection problem.


We get “0″ as the status text, but why?

Simple: Here are the URLs from the module configuration, that we make AJAX calls to:

var config = {
...
	LISTEN: 'http://10.0.0.2/service/chat_comet.php',
	SEND: 'http://10.0.0.2/service/chat_send.php'
...
};

And let us analyze how PhoneGap retrieves our static HTML file:

    package com.phonegap.exampleapp;

    import android.app.Activity;
    import android.os.Bundle;
    import com.phonegap.*;

    public class exampleapp extends DroidGap
    {
        @Override
        public void onCreate(Bundle savedInstanceState)
        {
            super.onCreate(savedInstanceState);
            super.loadUrl("file:///android_asset/www/home.html");
        }
    }

That is, PhoneGap simply loads a static resource from the device’s local file system.
However, in chat/main.js, we are trying to make an AJAX call to a PHP service (http://10.0.0.2/service/chat_comet.php) that’s outside the phone’s local file system.
This AJAX call is not allowed due to the cross-domain policy restriction of the browser.

JSONP to the Rescue

We will sort out this cross-domain connectivity problem by simply converting our AJAX calls into JSONP calls. So let’s convert our application to make JSONP requests to the server instead:

We’ll add o2.jsonp.js, o2.jsonpcontroller.js and o2.jsonpstate.js and remove o2.ajax.js.

The final scripts section will be like this:

<script type="text/javascript" charset="utf-8" src="js/o2.js"></script>
<script type="text/javascript" charset="utf-8" src="js/o2.domhelper.ready.js"></script>
<script type="text/javascript" charset="utf-8" src="js/o2.eventhandler.core.js"></script>
<script type="text/javascript" charset="utf-8" src="js/o2.eventhandler.extend.js"></script>	
<script type="text/javascript" charset="utf-8" src="js/o2.stringhelper.core.js"></script>
<script type="text/javascript" charset="utf-8" src="js/o2.jsonp.js"></script>
<script type="text/javascript" charset="utf-8" src="js/o2.ajaxcontroller.js"></script>
<script type="text/javascript" charset="utf-8" src="js/o2.ajaxstate.js"></script>
<script type="text/javascript" charset="utf-8" src="js/o2.jsonpcontroller.js"></script>
<script type="text/javascript" charset="utf-8" src="js/o2.jsonpstate.js"></script>
<script type="text/javascript" charset="utf-8" src="js/3rdparty/json2/json2.js"></script>
<script type="text/javascript" charset="utf-8" src="js/chat/main.jsonp.js"></script>

And we’ll also slightly modify main.js. I’ve just made a copy of it, renamed it main.jsonp.js and made my changes there, because main.js is being used in another comet tutorial, and altering it would have broken the code of that tutorial.

Here’s the final version of main.jsonp.js:

( function(o2, window, undefined) {
	/*
	 * 10.0.0.2 for device
	 * localhost for local server
	 * Using 10.0.0.2 just for development
	 * This should point to the actual service url
	 * (like www.example.com) in production.
	 */
	var kServiceUrl = 'http://localhost';
	
	var config = {
		constants: {
			timeout: {
				COMET_RETRY: 2000,
				COMET_TIMED_OUT: 30000
			},
			url : {
				chat : {
					LISTEN: [kServiceUrl, '/o2js.com/trunk/o2.js/service/chat_comet.php' ].join(''),
					SEND: [kServiceUrl, '/o2js.com/trunk/o2.js/service/chat_send.php' ].join('')
				}
			},
			keyCode: {
				NEW_LINE: 13
			},
			element: {
				TXT_SEND: 'MessageToSend',
				BTN_ACTION: 'Action',
				OVERLAY: 'Loading',
				CONSOLE: 'ChatConsole'
			},
			template: {
				CHAT_LINE: '<p>{0}</p>'
			},
			serviceKey: {
				MESSAGE : 'message',
				FORMAT: 'format',
				LAST_INDEX: 'lastIndex'
			},
			serviceValue: {
				JSONP: 'jsonp'
			}
		}
	};

	var state = {
		lastIndex: 0,
		retryTimerId: null
	};

	function sendMessage() {
		var $ = o2.$;
		var trim = o2.StringHelper.compact;
		var get = o2.Jsonp.get;

		var kTxtSend = config.constants.element.TXT_SEND;
		var kUrl = config.constants.url.chat.SEND;

		var txtMessage = $(kTxtSend);
		var message = trim(txtMessage.value);

		if(!message) {
			return;
		}

		txtMessage.value = '';
		
		var ccs = config.constants.serviceKey
		
		var params = {};
		params[ccs.MESSAGE] = message;
		params[ccs.FORMAT] = config.constants.serviceValue.JSONP

		get(kUrl, params);
	}

	function bindEvents() {
		var $ = o2.$;
		var handle = o2.EventHandler.addEventListener;
		var getCode = o2.EventHandler.getKeyCode;

		var cce = config.constants.element;
		var kAction = cce.BTN_ACTION;
		var kTxtSend = cce.TXT_SEND;

		var btnAction = $(kAction);
		var txtMessage = $(kTxtSend);

		handle(btnAction, 'click', function(evt) {
			o2.EventHandler.preventDefault(evt);
			sendMessage();
		});

		handle(txtMessage, 'keydown', function(evt) {
			var keyCode = getCode(evt);

			if(keyCode == config.constants.keyCode.NEW_LINE) {
				sendMessage();
			}
		});

	}

	//overloaded below.
	var Proxy = {};

	function retryListen() {
		clearTimeout(state.retryTimerId);
		state.retryTimerId = setTimeout( function() {
			Proxy.listen();
		}, config.constants.timeout.COMET_RETRY);
	}
	
	function unregister(controller) {
		if(!controller){return;}
		controller.unregister(o2.JsonpState);
	}	

	function listen_complete(result, xml, xhr) {		
		var $ = o2.$;
		var format = o2.StringHelper.format;

		var output = '';

		var cce = config.constants.element;
		var kLoading = cce.OVERLAY;
		var kConsole = cce.CONSOLE;
		var kRetryTimeout = config.constants.timeout.COMET_RETRY;
		var kChatLine = config.constants.template.CHAT_LINE;

		var divLoading = $(kLoading);
		var divChatConsole = $(kConsole);

		if(!result) {
			retryListen();
			return;
		}

		var message = result.message;

		state.lastIndex = result.lastIndex || state.lastIndex;

		if(!message || !message.length) {
			retryListen();
			return;
		}

		var buffer = [];

		for(var i=0,len = message.length; i<len; i++) {
			buffer.push(format(kChatLine, message[i]));
		}

		var container = document.createElement('div');
		container.innerHTML = buffer.join('');

		if(divLoading) {
			divChatConsole.innerHTML = '';
		}

		divChatConsole.appendChild(container);

		Proxy.listen();
	}

	Proxy = {
		listen: function() {
			var url = config.constants.url.chat.LISTEN;
			var get = o2.Jsonp.get;
			var concat = o2.StringHelper.concat;
			var format = o2.StringHelper.format;

			state.lastIndex = state.lastIndex || 0;
			
			var controller = null;
			
			var ccs = config.constants.serviceKey;
			
			var params = {};
			params[ccs.LAST_INDEX] = state.lastIndex;
			params[ccs.FORMAT] = config.constants.serviceValue.JSONP;
			
			var id = get(url, params, function(response){
				listen_complete(response);
				//We're done with the controller. Unregister before request times out.
				unregister(controller);				
			});

			controller = new o2.JsonpController(id, {
				ontimeout: function(){
					retryListen();
				},
				timeout:config.constants.timeout.COMET_TIMED_OUT
			});
			
		}

	};

	o2.ready( function() {
		bindEvents();
		Proxy.listen();
		o2.JsonpState.init();
	});

}(o2, this));

The application you develop, especially if it is a web application, is a living entity. It will inevitably, grow, change and evolve. In its growth phase, you’ll find yourself in need to rewrite/refactor certain parts of it. That’s when pattern oriented software architecture comes into play Image may be NSFW.
Clik here to view.
;)
.

Here are the changes we have done:

  • We’ve replaced all o2.Ajax.get calls with o2.Jsonp.get calls,
  • We’ve replaced all o2.AjaxState objects with o2.JsonpState objects,
  • We’ve removed listen_error and listen_exception callbacks, since they cannot be implemented with JSONP requests,
  • And finally, we’ve replaced o2.AjaxController with o2.JsonpController

Writing code without thinking of its architecture first, will cost many developer-hours, programming by coincidence, and a mixture of spaghetti code with dirty hacks, which no one else will ever want to touch with the fear of breaking it.

Don’t make your code smell Image may be NSFW.
Clik here to view.
;)
.

With less than 10 lines of code refactoring, we managed to convert our AJAX chat application to a JSONP chat application. That’s the beauty of the JavaScript Module Pattern, and creating re-usable modules with it. Using well-established design patterns (like the Observer pattern in this example) instead of re-inventing the wheel, also helped us a lot.

Making our Application Ready for Mobile Market

Once we’ve created our application we need to sign it to make it available for public. Different vendors have different strategies and procedures for this. Since we have been developing an Android application so far, we will be signing it for the Android Market. The process is described in depth at this “Signing Your Applications” developer.android.com article.

Creating a Keystore

Let’s begin by creating a keystore:

volkan@ronin:~$ keytool -genkey -v -keystore o2_js_keystore -alias o2_js -keyalg RSA -validity 10000
Enter keystore password:  
Re-enter new password: 
What is your first and last name?
  [Unknown]:  Volkan Ozcelik
What is the name of your organizational unit?
  [Unknown]:  o2js.com
What is the name of your organization?
  [Unknown]:  o2js.com
What is the name of your City or Locality?
  [Unknown]:  San Francisco
What is the name of your State or Province?
  [Unknown]:  California
What is the two-letter country code for this unit?
  [Unknown]:  US
Is CN=Volkan Ozcelik, OU=o2js.com, O=o2js.com, L=San Francisco, ST=California, C=US correct?
  [no]:  yes

Generating 1,024 bit RSA key pair and self-signed certificate (SHA1withRSA) with a validity of 10,000 days
	for: CN=Volkan Ozcelik, OU=o2js.com, O=o2js.com, L=San Francisco, ST=California, C=US
Enter key password for <o2_js>
	(RETURN if same as keystore password):  
[Storing o2_js_keystore]
volkan@ronin:~$ 

Exporting the Application

To export the application, open Eclipse, right click on the project and select “Export” from the menu.
Then select “Export Android Application“.

Image may be NSFW.
Clik here to view.
Export Android Application from Eclipse

Export Android Application from Eclipse


After that, select your project to be exported:


Then locate the keystore that we’ve just created:


After that, choose your key:


And finally our .apk file is ready to be deployed to an Android mobile device, or to be sent to the Android Market Image may be NSFW.
Clik here to view.
:)


Conclusion

That was the end of our “Creating A Mobile JavaScript Chat” series Image may be NSFW.
Clik here to view.
:)

Here’s a summary of what we have done so far:

As I’ve mentioned before, our final application is not even close to being production-ready.
But, at least, we’ve learned to building blocks and we’ve had an idea of how the overall mobile app deployment and release process works.

Hope you liked it Image may be NSFW.
Clik here to view.
:)

As always, please feel free to share your comments. I’d love to hear them Image may be NSFW.
Clik here to view.
:)
.


Viewing latest article 3
Browse Latest Browse All 4

Trending Articles