Open Source Software Technical Articles

Want the Best of the Wazi Blogs Delivered Directly to your Inbox?

Subscribe to Wazi by Email

Your email:

Connect with Us!

Current Articles | RSS Feed RSS Feed

How to develop an Android app using Apache Cordova and jQuery Mobile

  
  
  

Apache Cordova is a platform for building native mobile applications using common web technologies, including HTML, CSS, and JavaScript. It offers a set of APIs that allow mobile application developers to access native mobile functions such as audio, camera, and filesystem using JavaScript. Another developer tool, jQuery Mobile, is one of the best mobile web application frameworks. It allows developers to create rich web applications that are mobile-friendly. You can use Apache Cordova with jQuery Mobile to create a complete Android application.

To create, develop, build, and test a Cordova application, you can use the Cordova command-line interface. From the Cordova CLI you can create new Cordova projects, build them on mobile platforms, and run them on real devices or within emulators.

Before you install the Cordova CLI, install the Android SDK and Node.js, and be sure you have Apache Ant installed, then use the Node.js command sudo npm install -g cordova to install Cordova. The latest Cordova version is 3.3.0.

Create a Cordova project by running the command cordova create voicememo com.xyz.voicememo VoiceMemo. The first parameter tells Cordova to generate a voicememo directory for the project. The directory will contain a www subdirectory that includes the application's home page (index.html), along with various resources under css, js, and img directories. The command also creates a config.xml file that contains important metadata Cordova needs to generate and distribute the application.

The second and the third parameters are optional. The second parameter, com.xyz.voicememo, provides a project namespace. For an Android project, such as the one we are building, the project namespace maps to a Java package with the same name. The last parameter, VoiceMemo, provides the application's display text. You can edit both of these values later in the config.xml file.

Now you have a Cordova project that you can use as a base for generating platform-specific code. Before you generate Android code, run the commands

cd voicememo
cordova platform add android

The cordova platform command depends on Apache Ant. After you run it, you will find a new platforms/android subdirectory under the voicememo directory.

To generate Android-specific code under platforms/android, build the project using the cordova build command under the voicememo directory. You can then run and test the generated Android project in the Cordova emulator by executing the command cordova emulate android.

Note: The Cordova project recommends you make your code changes in the root www directory, and not in the platforms/android/assets/www directory, because the platforms directory is overwritten every time you execute a cordova build command after you use Cordova CLI to initialize the project.

Figure 1
The home page of the VoiceMemo application, which represents the list of voice memos.

From the Voice Listing page, users can click on three buttons: New, to create a new recording; About, to open the application's About page; and Remove All Memos, to remove the saved voice memos.

When users click the New button, they are forwarded to the voice recording page:

Figure 2
Here users can enter the title and description of a voice memo, then click the Record button to invoke the voice recording application of the Android mobile device. When a recording is completed, users are returned to the voice recording page and can play back the recording or save it.

To work with voice recording and playback in Cordova, you can install several plugins; the first two below are mandatory. Run the following commands from the voicememo directory:

  • Media Capture plugin, for media capture:
    cordova plugin add https://git-wip-us.apache.org/repos/asf/cordova-plugin-media-capture.git
  • Media plugin, for working with media:
    cordova plugin add https://git-wip-us.apache.org/repos/asf/cordova-plugin-media.git
  • Device plugin, for accessing device information:
    cordova plugin add https://git-wip-us.apache.org/repos/asf/cordova-plugin-device.git
  • Dialog plugin, for displaying native-looking messages:
    cordova plugin add https://git-wip-us.apache.org/repos/asf/cordova-plugin-dialogs.git
  • File plugin, for accessing the mobile filesystem:
    cordova plugin add https://git-wip-us.apache.org/repos/asf/cordova-plugin-file.git

Apply these plugins to your Cordova project by running the cordova build command.

At this point you're done with the preparation of the application. You can now start writing the application custom code.

Let's look at the VoiceMemo application directory hierarchy. The www directory contains the following subdirectories:

Figure3
  • css contains the custom application Cascading Style Sheet.
  • jqueryMobile contains the jQuery Mobile framework and plugins JStorage and Page Params.
  • js contains all the custom application JavaScript code. It has three subdirectories:
    • api contains the application managers (VoiceManager and CacheManager) and utility files.
    • model contains the application model. In this application we have a single object that represents a Voice item called VoiceItem.
    • vc contains the application view controllers, which include the application action handlers. Action handlers usually create model objects and populate them with UI data, pass them to the application APIs, and display the results on the application view or page.

Finally, also under www, index.html contains all of the application pages, which for this project comprise three jQuery pages:

  • Voice Listing (home page)
  • Voice Recording
  • About

The following code snippet shows the Voice Listing jQuery page code in the index.html file.

<div data-role="page" id="voiceList">
   <div data-role="header">    
	  <a href="#about" data-role="button" data-icon="info">About</a>           
	  <h1>Memo List</h1>
	  <a href="#voiceRecording" data-role="button" data-icon="plus">New</a>                     
   </div>
<div data-role="content">
	<ul data-role="listview" id="voiceListView">
	</ul>
</div>
   <div data-role="footer" data-position="fixed">
	  <h1><a href="#" data-role="button" data-icon="delete" id="removeAllVoices">Remove All Memos</a></h1>
   </div>                
</div>   

Let's walk through the code. The page has a header (the div whose data-role="header") that contains two navigation buttons to the About page and to the Voice Recording page. It has content (the div whose data-role="content") that contains the list of the voice recordings that are populated when the page is shown. Finally, it has a footer (the div whose data-role="footer") that contains a button to remove all of the voice memos from the list.

Now let's look at the page view controller JavaScript object, which includes the action handlers of the page (voiceList.js). voiceList.js is included in the index.html page, so when it is loaded by the browser the script is automatically executed to register the JavaScript event handlers for this jQuery page.

(function() {
	
    var voiceManager = VoiceManager.getInstance();
	
    $(document).on("pageinit", "#voiceList", function(e) {
    	
    	$("#removeAllVoices").on("tap", function() {
    		e.preventDefault();
    		
    		voiceManager.removeAllVoices();
    		
    		updateVoiceList();
    	});      
    });
    
    $(document).on("pageshow", "#voiceList", function(e) {
        e.preventDefault();
        
        updateVoiceList();
    });
    
    function updateVoiceList() {
        var voices = voiceManager.getVoices();

        $("#voiceListView").empty();
                
        if (jQuery.isEmptyObject(voices)) {
            $("<li>No Memos Available</li>").appendTo("#voiceListView");
        } else {
            for (var voice in voices) {
            	$("<li><a href='#voiceRecording?voiceID=" + voices[voice].id + "'>" + 
            			voices[voice].title + "</a></li>").appendTo("#voiceListView");
            }
        }
        
        $("#voiceListView").listview('refresh');    	
    }
})();

The "pageinit" event handler, which is called once in the page initialization, registers the voice recordings removal tap event handler. The voice recordings removal tap event handler removes the list of voice recordings by calling the removeAllVoices() method of the VoiceManager object, then updates the voice listview.

The "pageshow" event handler, which is called every time the page is shown – it is triggered on the "to" transition page, after the transition animation completes – updates the voice list view with the current saved voice recordings. To do this it retrieves the current saved voice recordings by calling the getVoices() method of VoiceManager, then adds the voice items to the list view so that if any voice item in the list is clicked, its ID will be passed to the voice recording page to display the voice item details.

Note: In a jQuery Mobile list view you must call listview('refresh') to see list view updates.

The next code snippet shows the Voice Recording page code in the index.html file:

<div data-role="page" id="voiceRecording">
	 <div data-role="header">
		 <a href="#voiceList" data-role="button" data-icon="home">Home</a>                                
		 <h1>Record Voice</h1>                                  
		 <a href="#" data-role="button" data-rel="back" data-icon="back">Back</a> 
	  </div>
	  <div data-role="content">
		<div data-role="fieldcontain">
			<input type="hidden" id="vid"/>
		
			<label for="title">Title</label> 
			<input type="text" name="title" id="title"></input><br/>
				
			<label for="desc">Description</label> 
			<textarea name="desc" id="desc"></textarea><br/>		
			
			<label for="location">Recording</label>
			<input type="hidden" id="location"/>
			<input type="button" id="recordVoice" data-icon="gear" value="Record" data-inline="true"/>
			<input type="button" id="playVoice" data-icon="refresh" value="Play" data-inline="true"/><br/>
			
			<input type="button" value="Save" data-icon="plus" id="saveVoice"/>			
		</div>
	  </div>
</div>     

 

The Voice Recording page has header and content sections. The content div contains the voice recording elements (title, description, and the voice recording file) and a Save button. Let's see the page view controller JavaScript object, which includes the action handlers of the page (recordVoice.js). Like the event handler JavaScript we just looked at, recordVoice.js is included in the index.html page and is automatically executed when it is loaded by the browser.

(function() {
    
    var voiceManager = VoiceManager.getInstance();
    
    $(document).on("pageinit", "#voiceRecording", function(e) {
        e.preventDefault();
        
        $("#saveVoice").on("tap", function() {
            e.preventDefault();

            var voiceItem = new VoiceItem($("#title").val() || "Untitled", 
                                          $("#desc").val() || "", 
                                          $("#location").val() || "",
                                          $("#vid").val() || null);
            
            voiceManager.saveVoice(voiceItem);
            
            $.mobile.changePage("#voiceList");
        });        
        
        $("#recordVoice").on("tap", function() {
            e.preventDefault();
        
            var recordingCallback = {};
            
            recordingCallback.captureSuccess = handleCaptureSuccess;
            recordingCallback.captureError = handleCaptureError;
            
            voiceManager.recordVoice(recordingCallback);
         }); 
        
        $("#playVoice").on("tap", function() {
            e.preventDefault();
        
            var playCallback = {};
            
            playCallback.playSuccess = handlePlaySuccess;
            playCallback.playError = handlePlayError;
            
            voiceManager.playVoice($("#location").val(), playCallback);
        });           
        
    });
    
    $(document).on("pageshow", "#voiceRecording", function(e) {
        e.preventDefault();
        
        var voiceID = ($.mobile.pageData && $.mobile.pageData.voiceID) ? $.mobile.pageData.voiceID : null;
        var voiceItem = new VoiceItem("", "", "");
        
        if (voiceID) {
            
            //Update an existing voice
            voiceItem = voiceManager.getVoiceDetails(voiceID);
        } 
        
        populateRecordingFields(voiceItem);
        
        if (voiceItem.location.length > 0) {
            $("#playVoice").closest('.ui-btn').show();
        } else {
            $("#playVoice").closest('.ui-btn').hide();    
        }        
    });
    
    $(document).on("pagebeforehide", "#voiceRecording", function(e) {
        voiceManager.cleanUpResources();
    });
    
    function populateRecordingFields(voiceItem) {
        $("#vid").val(voiceItem.id);
        $("#title").val(voiceItem.title);
        $("#desc").val(voiceItem.desc);
        $("#location").val(voiceItem.location);
    }
    
    function handleCaptureSuccess(mediaFiles) {
        if (mediaFiles && mediaFiles[0]) {        
            currentFilePath = mediaFiles[0].fullPath;
            
            $("#location").val(currentFilePath);
            
            $("#playVoice").closest('.ui-btn').show();  
        }
    }
    
    function handleCaptureError(error) {
    	displayMediaError(error);
    }  
    
    function handlePlaySuccess() {
        console.log("Voice file is played successfully ...");
    }
    
    function handlePlayError(error) {
    	displayMediaError(error);
    }        
    
    function displayMediaError(error) {
        if (error.code == MediaError.MEDIA_ERR_ABORTED) {
            AppUtil.showMessage("Media aborted error");
        } else if (error.code == MediaError.MEDIA_ERR_NETWORK) {
            AppUtil.showMessage("Network error");
        } else if (error.code == MediaError.MEDIA_ERR_DECODE) {
            AppUtil.showMessage("Decode error");
        } else if (error.code ==  MediaError.MEDIA_ERR_NONE_SUPPORTED) {
            AppUtil.showMessage("Media is not supported error");
        } else {
        	console.log("General Error: code = " + error.code);
        }        
    }
})();

The "pageinit" event handler registers the voice save, record, and play tap event handlers. The voice saving tap event handler saves a voice recording by calling the saveVoice() method of the VoiceManager object. The user is then forwarded to the voice listing page using $.mobile.changePage("#voiceList"). This page can be used either to create a new voice recording or update an existing one. In the second case, the voice ID is passed from the list view of the Voice Listing page and is saved in a hidden field "vid" to be used by VoiceManager for updating the existing voice recording. In the first case, the "vid" hidden field value is empty, which signals VoiceManager that this is a new voice recording and not an update to an existing one.

The voice ID is retrieved on the "pageshow" event of the voice recording page. If there is a passed voice ID from the Voice Listing page then the code retrieves the full voice recording information using voiceManager.getVoiceDetails(voiceID) and populates the form elements using the retrieved information. Finally, in the "pageshow" handler, if there is an existing voice recording (that is, voiceItem.location.length > 0) then the program displays the Play button to allow users to play the voice recording.

Note that the application automatically passes parameters between the two pages thanks to the jQuery Mobile Page parameters plugin, which is included on the index.html page.

In the voice recording tap event handler, the code starts voice recording by calling voiceManager.recordVoice(recordingCallback). The recording callback contains two attributes: VoiceManager calls captureSuccess if the voice capture process succeeds, and captureError it fails. With the captureSuccess callback (handleCaptureSuccess), the full voice recording path is stored in a hidden field to be used later to play the voice. With the captureError callback (handleCaptureError), an error message is displayed.

The voice playing tap event handler starts voice playback by calling voiceManager.playVoice(voiceLocation, playCallback). The playing callback contains two attributes: playSuccess and playError. VoiceManager calls playSuccess if voice play succeeds, and playError if it fails. The handlePlaySuccess callback prints a statement in the console log to indicate that voice play succeeded, while the handlePlayError callback displays an error message to the user.

The jQuery Mobile framework calls the "pagebeforehide" event before a page is hidden. This event handler calls voiceManager.cleanUpResources() to stop any recording that is currently playing and to clean up the media object.

So much for the two main pages of the application. Here's the complete code of the index.html file:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="format-detection" content="telephone=no" />
        <!-- WARNING: for iOS 7, remove the width=device-width and height=device-height attributes. See https://issues.apache.org/jira/browse/CB-4323 -->
        <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi" />
        <link rel="stylesheet" type="text/css" href="css/voiceMemo.css" />
          <link rel="stylesheet" href="jqueryMobile/jquery.mobile-1.4.0.min.css">    
          
           <script src="jqueryMobile/json2.js"></script>            
           <script src="jqueryMobile/jquery-1.10.2.min.js"></script>                                  
           <script src="jqueryMobile/jquery.mobile-1.4.0.min.js"></script>   
           <script src="jqueryMobile/jqm.page.params.js"></script>               
           <script src="jqueryMobile/jstorage.js"></script>         
                     
        <title>Voice Memo</title>
    </head>
    <body>        
           <div data-role="page" id="voiceList">
               <div data-role="header">    
                  <a href="#about" data-role="button" data-icon="info">About</a>           
                  <h1>Memo List</h1>
                  <a href="#voiceRecording" data-role="button" data-icon="plus">New</a>                     
               </div>
            <div data-role="content">
                <ul data-role="listview" id="voiceListView">
                </ul>
            </div>
               <div data-role="footer" data-position="fixed">
                  <h1><a href="#" data-role="button" data-icon="delete" id="removeAllVoices">Remove All Memos</a></h1>
               </div>                
           </div>        
        
        <div data-role="page" id="voiceRecording">
             <div data-role="header">
                 <a href="#voiceList" data-role="button" data-icon="home">Home</a>                                
                 <h1>Record Voice</h1>                                  
                 <a href="#" data-role="button" data-rel="back" data-icon="back">Back</a> 
              </div>
              <div data-role="content">
				<div data-role="fieldcontain">
					<input type="hidden" id="vid"/>
				
					<label for="title">Title</label> 
					<input type="text" name="title" id="title"></input><br/>
						
					<label for="desc">Description</label> 
					<textarea name="desc" id="desc"></textarea><br/>		
					
				    <label for="location">Recording</label>
				    <input type="hidden" id="location"/>
	                <input type="button" id="recordVoice" data-icon="gear" value="Record" data-inline="true"/>
	                <input type="button" id="playVoice" data-icon="refresh" value="Play" data-inline="true"/><br/>
	                
	                <input type="button" value="Save" data-icon="plus" id="saveVoice"/>			
				</div>
           	  </div>
        </div>        
        
        <div data-role="page" id="about">
            <div data-role="header">
                  <a href="#voiceList" data-role="button" data-icon="home">Home</a>              
                  <h1>About</h1>             
                  <a href="#" data-role="button" data-rel="back" data-icon="back">Back</a>       
               </div>
               <div data-role="content">
                <p>This sample is developed for education purposes</p>
            </div>
        </div>               
        
        <script type="text/javascript" src="cordova.js"></script>
        
        <!-- Application JS files -->
        <script type="text/javascript" src="js/api/AppUtil.js"></script>  
        <script type="text/javascript" src="js/api/CacheManager.js"></script>        
        <script type="text/javascript" src="js/api/VoiceManager.js"></script> 
        
        <script type="text/javascript" src="js/model/VoiceItem.js"></script> 

        <script type="text/javascript" src="js/vc/common.js"></script>                          
        <script type="text/javascript" src="js/vc/voiceList.js"></script>           
        <script type="text/javascript" src="js/vc/recordVoice.js"></script>                         
    </body>
</html>

Let's look at the application APIs – that is, the scripts listed at the end of index.html, which are placed before the view controller objects so they can be used by them. The following code snippet shows the VoiceManager object, which is the main API object used by the view controller objects (voiceList and recordVoice):

var VoiceManager = (function () {     
  var instance;
 
  function createObject() {
      var cacheManager = CacheManager.getInstance();
      var VOICES_KEY = "voices";
      var voiceMap;
      var audioMedia;
      
      return {
          getVoices: function () {
              voiceMap = cacheManager.get(VOICES_KEY) || {};
              
              return voiceMap;
          }, 
          getVoiceDetails: function (voiceID) {
              voiceMap = cacheManager.get(VOICES_KEY) || {};
              
              return voiceMap[voiceID];
          },
          saveVoice: function (voiceItem) {  
              voiceMap = cacheManager.get(VOICES_KEY) || {};
              
              voiceMap[voiceItem.id] = voiceItem;
              
              cacheManager.put(VOICES_KEY, voiceMap);
          }, 
          removeAllVoices: function() {
              cacheManager.remove(VOICES_KEY);
          },
          recordVoice: function (recordingCallback) {
              navigator.device.capture.captureAudio(recordingCallback.captureSuccess, 
                                                    recordingCallback.captureError, 
                                                    {limit: 1});
          }, 
          playVoice: function (filePath, playCallback) {
              if (filePath) {
            	  
            	  //You have to make this in order to make this working on Android ...
                  filePath = filePath.replace("file:/","file://");
                  
	              this.cleanUpResources();
	                 
	              audioMedia = new Media(filePath, playCallback.playSuccess, playCallback.playError);
	            
	              // Play audio
	              audioMedia.play();
              }            
          }, 
          cleanUpResources: function() {
              if (audioMedia) {
            	  audioMedia.stop();
                  audioMedia.release();
                  audioMedia = null;
              } 
          }
    };
  };
 
  return {
    getInstance: function () {
      if (!instance) {
          instance = createObject();
      }
 
      return instance;
    }
  }; 
})();

As you can see, VoiceManager is a singleton object that has seven methods:

Method NameDescription
getVoices() Get all of the saved voices from the mobile local storage using the CacheManager object.
getVoiceDetails(voiceID) Get the voice details using the voice ID from the mobile local storage using the CacheManager object.
saveVoice(voiceItem) Save the voice item object in the mobile local storage using the CacheManager object.
RemoveAllVoices() Remove all the voice items from the mobile local storage using CacheManager object.
recordVoice(recordingCallback) Use Cordova navigator.device.capture.captureAudio to capture the voice recording. Call recordingCallback.captureSuccess if the operation succeeds and recordingCallback.captureError if it fails.
playVoice(filePath, playCallback) Uses Cordova Media object to play the voice recording, whose full location is specified in the filePath parameter. Call playCallback.playSuccess if the operation succeeds and playCallback.playError if it fails.
CleanUpResources() Stop any playing recording and clean up media resources.

VoiceManager uses CacheManager to persist, update, delete, and retrieve the voice items. The next code snippet shows the CacheManager object.

var CacheManager = (function () {     
  var instance;
 
  function createObject() {   
    return {
        put: function (key, value) {
            $.jStorage.set(key, value);
        },
        get: function (key) {
        	return $.jStorage.get(key);
        },
        remove: function (key) {
        	return $.jStorage.deleteKey(key);
        }
    };
  };
 
  return {
    getInstance: function () {
 
      if (!instance) {
        instance = createObject();
      }
 
      return instance;
    }
  }; 
})();

 

CacheManager is a singleton object that uses jStorage to access local storage. CacheManager has three methods:

Method NameDescription
put(key, value) Add an entry in the local storage with (key, value) pairs.
get(key) Get the entry value whose key is specified as a parameter.
remove(key) Remove the entry whose key is specified as a parameter.

The final code snippet shows the VoiceItem object, which represents the voice item with the attributes of title, description, location, and ID.

var VoiceItem = function(title, desc, location, id) {
	this.title = title || "";
	this.desc = desc || "";
	this.location = location || "";
	this.id = id || "Voice_" + (new Date()).getTime();
};

VoiceItem.prototype.toString = function () {
	return "Title = " + this.title + ", " +
		   "Description = " + this.desc + ", " +
		   "Location = " + this.location + ", " +
		   "ID = " + this.id;
};

At this point I've walked you through the application logic and what happens when users interact with each screen. Now let's see how it works. You can run the application from the command line under the voicememo directory with the command cordova emulate android. If you want to try it yourself you can download the complete code.

After you've run the cordova build command, you should find the generated Android APK file under the platforms/android/bin directory, and you can deploy it on your Android phone or tablet.

Conclusion

At this point I hope you can see how to design and implement a complete native Android mobile application that uses Apache Cordova as a platform for accessing mobile native features and jQuery Mobile as a powerful mobile application framework. Armed with this knowledge, you can start developing your own native Android applications using your HTML, CSS, and JavaScript skills.


Do you want to receive a compilation of Wazi's top
blog posts in the past year delivered directly to your inbox?






This work is licensed under a Creative Commons Attribution 3.0 Unported License
Creative Commons License.

Comments

That it was while doing this replica rolex uk time frame if Omega manufactured a variety of it has the a lot of treasured time frame types. Traditional Omega different watches that happen to be continue to really needed and are generally prominent even today. While in the replica rolex daytona Secondly Community Showdown, Omega made powerful track record to get ourselves by way of developing government plus initial different watches to get servicemen. People was really well-known with regard to their excellent needlework plus its superb swiss replica rolex focus on element. Alternative preferred different watches out of this times ended up being a Omega Speedmaster chronograph, a Omega Seamaster waterproof physical activities look at as well as Omega Constellation different watches, which have been all of continue to astonishingly preferred. So with Cartier Pasha fake rolex uk Fake look at is definitely created of your anxiously have the ability stainless-steel. A value of your look at is definitely unbelievable which will may make a unpleasant together with the blued arrow-formed possession. A stainless animate armlet once and for all varieties while in the Cartier fake rolex watches Pasha fake design and style. Cartier Pasha fake consider is undoubtedly an suitable archetype of your aboriginal Cartier look at. Top rated top-quality of your Cartier fake find is undoubtedly an aftereffect of your abstracts plus modern advances acclimated during it has the rolex replica developing. 
Posted @ Thursday, May 22, 2014 2:54 AM by sdggu
Thanks for helpful topics. I have found this clear stpes here
Posted @ Saturday, May 31, 2014 12:22 AM by meen
I downloaded the app code and it worked very slow on my emulator. I then ran it on Galaxy s3 and speed was ok but the rendered button/ text size was microscopic
Posted @ Saturday, June 07, 2014 5:41 PM by Dave
I like this post and want to keep it with me as reference for future use. Thank you very much.
Posted @ Sunday, August 24, 2014 3:25 AM by Kaivan Shah
Very useful information.thank you so much for share this information to us. 
Posted @ Monday, September 01, 2014 8:43 AM by android app development services
 
Amazing article about Processing & Android..I am glad to visit on this website..I am impressed by read all that great information..Your information is very useful and helpful for all..Thanks for sharing all that great information..For further details you can visit on this websitewww.gogetapps.ca about app development..They provide very great service to their customers..Thanks a again.. 
Posted @ Tuesday, September 02, 2014 10:34 AM by iphone games development
i want to say you, you are fantastic because you provide us a lots of info about the topic is Open Source Software Technical... now a days the open source is mainly used factor in market...people most like android phones... in your article i got How to develop an Android app using Apache Cordoba and j Query Mobile..this info is very important for me... thanks alot for providing us this great article...i have read it very well and i understand it....it is easy to use..i like the above article so much.... for more info please visit on this link: 
www.gogetapps.ca
Posted @ Tuesday, September 09, 2014 11:45 PM by mobile app development for android
We are introducing web development solution, and we would love to hear what you think   for our work 
Posted @ Wednesday, September 17, 2014 7:06 AM by android development
We are introducing web development solution, and we would love to hear what you think   for our work 
Posted @ Wednesday, September 17, 2014 8:02 AM by android development
Nice blog, they provide information for how to develop an Android app using Apache Cordova and jQuery Mobile. me and my friends are liked this blog. thanks for sharing. please visit on this linkwww.gogetapps.ca our website is provide more services. thanks again..
Posted @ Thursday, September 25, 2014 3:45 AM by custom app development
WOW... It was wonderful piece of info, You have shared with us. Thanks for sharing this helpful post. Going to bookmark your Blog, Please keep updating your blog...  
 
Roundcube Skins
Posted @ Friday, October 10, 2014 8:35 AM by Maikal Jaksan
Post Comment
Name
 *
Email
 *
Website (optional)
Comment
 *

Allowed tags: <a> link, <b> bold, <i> italics