Getting Started with an Android Widget Populated with Javascript and Apache Cordova

Written by NickWal

This is the forth part of a tutorial demonstrating the ability to re-use JavaScript in many different environments. The first part dealt with the creation of the re-usable service, a simple command line wrapper which called the service as well as a simple test which checks the service is performing as expected. The second part of the tutorial discussed the development of a simple web application based on that service, using browserify to wrap the service so that it will run in any modern browser. The third part took the web app, and showed you how to use Apache Cordova, a platform independent JavaScript mobile phone development framework, to make your simple app run on modern smartphones.

This fourth part of the tutorial assumes that you have understood the concepts so far raised in earlier parts of the tutorial.

Project Set-up

You can continue to use the old directory containing the files from the previous tutorial, but I recommend you create yourself an new project environment like this:

Create a new directory mkdir crprice or md crprice or whatever your operating system supports and cd to it. Then use the following:

   npm install -g npm-package-bin xo ava gulp-cli
   npm init -y
   xo --init --esnext
   ava --init
   npm install --save angular bluebird request request-promise woofwoof 
   npm install --save-dev ava browser-sync browserify browserify-css fs-extra glob grunt grunt-browserify grunt-contrib-connect grunt-contrib-watch gulp gulp-rename gulp-util imagemagick vinyl-source-stream xo 

   npm-bin add crprice ./price-tool.js -n

Price Service

The price service is exactly the same as in the previous examples. Interestingly although the environment will be different - instead of running in a nodejs server environment, the application will be running in the sandboxed environment of the mobile phone, nevertheless we can still use exactly the same code, even to access network resources, by using the tool browserify. This not only resolves code that has been required through npm modules, but also provides a wrapper which allows the developer to access services that nodejs would provide natively through the browser instead.

Below the full source code of the service. Just copy and paste this and save with a name price-service.js

   /* jshint node:true */
   /* jshint esversion:6 */
   'use strict';
   const rp = require('request-promise');

   // https://coinmarketcap.com/api/

   module.exports = function () {
       this.getPrice = function (id) {
           const uri = 'https://api.coinmarketcap.com/v1/ticker/' + id + '/';
           const options = {
               url: uri,
               json: true
           };
           return rp.get(options).then(a => {
               return a;
           });
       };
   };

Android Application built with Cordova and Gulp

For the android application, nothing changes from the previous tutorial.

Please refer to the previous tutorial for a more in depth explanation. The source code is included here for your convenience.

Below the full source code of the android Application.

Firstly the main page of the application - copy and paste this and save with a name app/index.hml

   <html>
   <head>
     <title></title>
       <!-- Cordova reference, this is added to your app when it's built. -->
     <script src="cordova.js"></script>
    <script src="scripts/app.js"></script>
    </head>
   <body >
   <div ng-controller="MyAppCtrl">
     {{variable}}
     <button ng-click="updateprice()">Update Price</button>
   </div>
   </body>
   </html>

The application logic of the application - copy and paste this and save with a name app/js/app.js

   /* jshint node:true */
   /* jshint esversion:6 */
   /*globals parent, window, document, ace */


   'use strict';

   var angular = require('angular');

   angular.module('app', [])
       .controller('MyAppCtrl', ['$scope', 'priceService', function ($scope, priceService) {
           $scope.variable = '-none-';
           $scope.updateprice = function () {
               console.log('requesting price');
               priceService.getPrice('bitcoin').then(result => {
                   var price = result[0];
                   $scope.variable = price.symbol + ' ' + price.price_usd + ' USD';
                   $scope.$apply(); // make sure async update is visible
                   ace.android.appWidget.clear();
                   ace.android.appWidget.add($scope.variable);
               });
           };
       }]).service('priceService', require('../../price-service.js'))
       .run(['$rootScope', 'priceService', function ($rootScope, priceService) {


           console.log("angular is now running");
           if (ace.platform == "Android") {


               var checkForWidgetActivation = function () {
                   if (ace.platform != "Android") {
                       return;
                   }

                   ace.android.getIntent().invoke("getIntExtra", "widgetSelectionIndex", -1, function (value) {
                       // value is the index of the item clicked
                       // or -1 if no item has been clicked
                       console.log("widget clicked");
                   });
               }


               var setupWidget = function () {
                   // Handle the app being resumed by a widget click:
                   console.log("log1 setupWidget");
                   ace.addEventListener("android.intentchanged", checkForWidgetActivation);

                   ace.android.appWidget.clear();

                   /*
                       for (var i = 0; i < 10; i++) {
                           ace.android.appWidget.add("Item with index " + i);
                       }
                       */

                   console.log("calling price service inside setupWidget");
                   priceService.getPrice('bitcoin').then(result => {
                       var price = result[0];
                       var variable = price.symbol + ' ' + price.price_usd + ' USD';
                       console.log("adding price to widget");
                       ace.android.appWidget.add(variable);
                   });
               }

               setupWidget();
           }
       }]);


   angular.element(document).ready(function () {
       if (window.cordova) {
           console.log("Running in Cordova, will bootstrap AngularJS once 'deviceready' event fires.");
           document.addEventListener('deviceready', function () {
               console.log("Deviceready event has fired, bootstrapping AngularJS.");
               angular.bootstrap(document.body, ['app']);
           }, false);
       } else {
           console.log("Running in browser, bootstrapping AngularJS now.");
           angular.bootstrap(document.body, ['app']);
       }
   });

The build script helper - copy and paste this and save with a name gulpfile.js

   var path = require('path');
   var gulp = require('gulp');
   var browserSync = require('browser-sync').create();
   var gutil = require('gulp-util');
   var browserify = require('browserify');
   var sourceStream = require('vinyl-source-stream');
   var fse = require('fs-extra');
   var rename = require('gulp-rename');

   // process JS files and return the stream.
   gulp.task('js', function () {
       var bundleStream = browserify()
           .add('app/js/app.js')
           .ignore('cls-bluebird')
           .transform('browserify-css', {
       autoInject: true,
       rootDir: 'www',
       processRelativeUrl: function (relativeUrl) {
           var stripQueryStringAndHashFromPath = function (url) {
               return url.split('?')[0].split('#')[0];
           };
           var replaceAll = function (find, replace, str) {
               var found = find.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
               return str.replace(new RegExp(found, 'g'), replace);
           };
           console.log(relativeUrl);
           var rootDir = path.resolve(process.cwd(), 'www');
           var relativePath = stripQueryStringAndHashFromPath(relativeUrl);
           console.log(relativePath);
           var queryStringAndHash = relativeUrl.substring(relativePath.length);

                   //
                   // Copying files from '../node_modules/bootstrap/' to 'dist/vendor/bootstrap/'
                   //
           var sep = path.sep;
           var prefix = '..' + sep + 'node_modules' + sep;
           if (relativePath.startsWith(prefix)) {
               var vendorPath = 'vendor' + sep + relativePath.substring(prefix.length);
               var source = path.join(rootDir, relativePath);
               var target = path.join(rootDir, vendorPath);

               gutil.log('Copying file from ' + JSON.stringify(source) + ' to ' + JSON.stringify(target));
               fse.copySync(source, target);

                       // Returns a new path string with original query string and hash fragments
               var result = replaceAll('\\', '/', (vendorPath + queryStringAndHash));
               console.log(result);
               return result;
           }

           return relativeUrl;
       }

   })
           .bundle();

       return bundleStream
           .pipe(sourceStream('app.js'))
           .pipe(gulp.dest('www/scripts'));
   });

   gulp.task('copyview', function () {
       return gulp.src('app/index.html')
           .pipe(rename({
       dirname: ''
   }))
           .pipe(gulp.dest('www'));
   });

   // Static server
   gulp.task('dev', ['js', 'copyview'], function () {
       browserSync.init({
           server: {
               baseDir: 'www/'
           }
       });
       gulp.watch('app/**/*.*', ['js-watch']);
   });

   gulp.task('js-watch', ['js', 'copyview'], function () {
       browserSync.reload();
   });
   gulp.task('vs', ['js', 'copyview']);
   gulp.task('default', ['dev']);

The configuration file for Apache Cordova - copy and paste this and save with a name config.xml

   <?xml version='1.0' encoding='utf-8'?>
   <widget id="net.log1.apps.priceApp" version="1.0.0" 
       xmlns="http://www.w3.org/ns/widgets" 
       xmlns:cdv="http://cordova.apache.org/ns/1.0">
       <name>PriceTools-App</name>
       <description>        A simple multiplatform mobile application that allows you to view the price of some well known crytpocurrencies in real time    </description>
       <author email="[email protected]" href="https://www.infohit.net">        Nicholas Waltham    </author>
       <content src="index.html" />
       <plugin name="cordova-plugin-whitelist" spec="1" />
       <access origin="*" />
       <allow-intent href="http://*/*" />
       <allow-intent href="https://*/*" />
       <allow-intent href="tel:*" />
       <allow-intent href="sms:*" />
       <allow-intent href="mailto:*" />
       <allow-intent href="geo:*" />
       <platform name="android">
           <allow-intent href="market:*" />
           <config-file target="AndroidManifest.xml" parent="./application">
               <receiver android:label="List" android:name="net.log1.apps.priceApp.ListWidgetProvider" xmlns:android="http://schemas.android.com/apk/res/android">
                   <intent-filter>
                       <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
                   </intent-filter>
                   <meta-data android:name="android.appwidget.provider" android:resource="@xml/list_widget_info" />
               </receiver>
               <service android:exported="false" android:name="run.ace.AppWidgetService" android:permission="android.permission.BIND_REMOTEVIEWS" xmlns:android="http://schemas.android.com/apk/res/android" />
           </config-file>
       </platform>
       <platform name="ios">
           <allow-intent href="itms:*" />
           <allow-intent href="itms-apps:*" />
       </platform>
       <engine name="android" spec="~6.0.0" />
       <plugin name="cordova-plugin-ace" spec="~0.1.2" />
       <plugin name="cordova-custom-config" spec="~3.0.14" />
       <hook type="before_build" src="hook.js" />
   </widget>

The hook file which ensures that browserify if run at the right moment in the build process - copy and paste this and save with a name hook.js

   #!/usr/bin/env node

   var gutil = require('gulp-util');
   var chalk = require('chalk');
   var prettyTime = require('pretty-hrtime');


   module.exports = function (context) {
       var Q = context.requireCordovaModule('q');
       var deferral = new Q.defer();
       var taskCount = 0;

       var path = require('path'),
           gulp = require('gulp'),
           gulpfile = path.join(__dirname, 'gulpfile');

       require(gulpfile);
       var gulpInst = gulp;

       gulpInst.on('err', function () {
           failed = true;
       });

       gulpInst.on('task_start', function (e) {
           gutil.log('Starting', '\'' + chalk.cyan(e.task) + '\'...');
           taskCount = taskCount + 1;
       });

       gulpInst.on('task_stop', function (e) {
           var time = prettyTime(e.hrDuration);
           gutil.log(
               'Finished', '\'' + chalk.cyan(e.task) + '\'',
               'after', chalk.magenta(time)
           );
           taskCount = taskCount - 1;
           if (taskCount === 0) {
               deferral.resolve();
           }
       });

       gulpInst.on('task_err', function (e) {
           var msg = formatError(e);
           var time = prettyTime(e.hrDuration);
           gutil.log(
               '\'' + chalk.cyan(e.task) + '\'',
               chalk.red('errored after'),
               chalk.magenta(time)
           );
           gutil.log(msg);
           process.exit(1);
       });

       gulpInst.on('task_not_found', function (err) {
           gutil.log(
               chalk.red('Task \'' + err.task + '\' is not in your gulpfile')
           );
           gutil.log('Please check the documentation for proper gulpfile formatting');
           process.exit(1);
       });


       gulp.start('vs');

       return deferral.promise;
   }

Remember to use gulp vs to build the web app files. You’ll need to do this at least once before moving on to the next part of the tutorial

A Native Java Class to Populate the widget

You’ll notice in the config.xml file a reference to a plugin cordova-plugin-ace. This is a plugin written by some Microsoft employees which facilitate building native UI apps with cordova. Sadly, as of December 2016, this project is no-longer maintained by Microsoft, but in the spirit of open source, they invite you to fork it.

Taking advantage of ace, its necessary to create only a very simple java class to interface with the javascript and populate the widget. The source is here below:

Copy and paste this and save with a name native/android/src/net/log1/apps/priceApp/ListWidgetProvider.java

   package net.log1.apps.priceApp;

   public class ListWidgetProvider extends run.ace.AppWidgetProvider {
     @Override protected int getLayoutResourceId(android.content.Context context) {
       return run.ace.NativeHost.getResourceId("list_widget_layout", "layout", context);
     }

     @Override protected int getViewResourceId(android.content.Context context) {
       return run.ace.NativeHost.getResourceId("list_widget_view", "id", context);
     }

     @Override protected int getItemResourceId(android.content.Context context) {
       return run.ace.NativeHost.getResourceId("list_widget_item", "id", context);
     }

     @Override protected int getItemTextResourceId(android.content.Context context) {
       return run.ace.NativeHost.getResourceId("list_widget_item_text", "id", context);
     }

     @Override protected int getItemLayoutResourceId(android.content.Context context) {
     return run.ace.NativeHost.getResourceId("list_widget_view", "layout", context);
      //return 0;
     }
   }

This file will compile and be included in the final project as part of the standard build process