diff --git a/config.xml b/config.xml
index 876755b160677f589e6ebef96c48d81190443b3d..5900c5d319bee356ea13ef7244d72d2df72bc843 100644
--- a/config.xml
+++ b/config.xml
@@ -48,20 +48,18 @@
     <feature name="MiniSodium">
         <param name="android-package" onload="true" value="me.lockate.plugins.MiniSodium" />
     </feature>
+    <feature name="Custom URL scheme">
+        <param name="id" value="cordova-plugin-customurlscheme" />
+        <param name="url" value="https://github.com/EddyVerbruggen/Custom-URL-scheme.git" />
+        <variable name="URL_SCHEME" value="june" />
+    </feature>
     <platform name="android">
         <preference name="AndroidXEnabled" value="true" />
         <preference name="cdvMinSdkVersion" value="16" />
         <preference name="cdvCompileSdkVersion" value="29" />
         <preference name="cdvBuildToolsVersion" value="29.0.2" />
-        <hook src="scripts/hooks/before_prepare/061_copy_build_extras.js" type="before_prepare" />
-        <hook src="scripts/hooks/after_prepare/010_add_platform_class.js" type="after_prepare" />
-        <hook src="scripts/hooks/after_prepare/021_template_cache.js" type="after_prepare" />
-        <hook src="scripts/hooks/after_prepare/022_translate.js" type="after_prepare" />
-        <hook src="scripts/hooks/after_prepare/040_useref.js" type="after_prepare" />
-        <hook src="scripts/hooks/after_prepare/040_useref.js" type="after_prepare" />
-        <hook src="scripts/hooks/after_prepare/050_clean_unused_directories.js" type="after_prepare" />
-        <hook src="scripts/hooks/after_prepare/061_copy_build_extras.js" type="after_prepare" />
-        <hook src="scripts/hooks/before_compile/060_prepare_android_manifest.js" type="after_prepare" />
+        <hook src="scripts/hooks/before_prepare.js" type="before_prepare" />
+        <hook src="scripts/hooks/after_prepare.js" type="after_prepare" />
         <icon density="ldpi" src="resources/android/icon/drawable-ldpi-icon.png" />
         <icon density="mdpi" src="resources/android/icon/drawable-mdpi-icon.png" />
         <icon density="hdpi" src="resources/android/icon/drawable-hdpi-icon.png" />
@@ -168,9 +166,6 @@
     </plugin>
     <plugin name="cordova-plugin-customurlscheme" spec="^5.0.2">
         <variable name="URL_SCHEME" value="june" />
-        <variable name="ANDROID_SCHEME" value=" " />
-        <variable name="ANDROID_HOST" value=" " />
-        <variable name="ANDROID_PATHPREFIX" value="/" />
     </plugin>
     <plugin name="ionic-plugin-keyboard" spec="^2.2.1" />
     <plugin name="cordova-plugin-androidx" spec="^1.0.2" />
diff --git a/gulpfile.js b/gulpfile.js
index f1b30e3a4f3262cbd81e29becc6170928a0534be..be0a2d265aaeb8f5dd66902344d5ea6ae491bad0 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -1,12 +1,14 @@
 'use strict';
 
 const gulp = require('gulp'),
+  path = require("path"),
   sass = require('gulp-sass'),
   cleanCss = require('gulp-clean-css'),
   base64 = require('gulp-base64-v2'),
   rename = require('gulp-rename'),
   ngConstant = require('gulp-ng-constant'),
   fs = require("fs"),
+  es = require('event-stream'),
   header = require('gulp-header'),
   footer = require('gulp-footer'),
   removeCode = require('gulp-remove-code'),
@@ -33,7 +35,8 @@ const gulp = require('gulp'),
   colors = require('ansi-colors'),
   argv = require('yargs').argv,
   sriHash = require('gulp-sri-hash'),
-  sort = require('gulp-sort');
+  sort = require('gulp-sort'),
+  gulpfile = this;
 
   // Workaround because @ioni/v1-toolkit use gulp v3.9.2 instead of gulp v4
   let jsonlint;
@@ -59,6 +62,25 @@ const paths = {
   ng_annotate_plugin: ['./www/plugins/*/**/*.js', '!./www/plugins/*/js/vendor/*.js']
 };
 
+const uglifyOptions = {
+  toplevel: true,
+  warnings: true,
+  mangle: {
+    reserved: ['qrcode', 'Base58']
+  },
+  compress: {
+    global_defs: {
+      "@console.log": "alert"
+    },
+    passes: 2
+  },
+  output: {
+    beautify: false,
+    preamble: "/* minified */",
+    max_line_len: 120000
+  }
+};
+
 function appAndPluginWatch(done) {
 
   log(colors.green('Watching source files...'));
@@ -311,14 +333,14 @@ function webCopyFiles() {
     logger: m => log(colors.grey(m))
   };
 
-  var tmpPath = './dist/web/www';
+  var targetPath = './dist/web/www';
   return merge(
     // Copy Js (and remove unused code)
     gulp.src('./www/js/**/*.js')
       .pipe(debug(debugOptions))
       .pipe(removeCode({"no-device": true}))
       .pipe(jshint())
-      .pipe(gulp.dest(tmpPath + '/js')),
+      .pipe(gulp.dest(targetPath + '/js')),
 
     // Copy HTML templates (and remove unused code)
     gulp.src('./www/templates/**/*.html')
@@ -326,7 +348,7 @@ function webCopyFiles() {
       .pipe(removeHtml('.hidden-no-device'))
       .pipe(removeHtml('[remove-if][remove-if="no-device"]'))
       .pipe(htmlmin(htmlminOptions))
-      .pipe(gulp.dest(tmpPath + '/templates')),
+      .pipe(gulp.dest(targetPath + '/templates')),
 
     // Copy index.html (and remove unused code)
     gulp.src('./www/index.html')
@@ -334,7 +356,7 @@ function webCopyFiles() {
       .pipe(removeHtml('.hidden-no-device'))
       .pipe(removeHtml('[remove-if][remove-if="no-device"]'))
       .pipe(htmlmin(/*no options, to keep comments*/))
-      .pipe(gulp.dest(tmpPath)),
+      .pipe(gulp.dest(targetPath)),
 
     // Copy API index.html
     gulp.src('./www/api/index.html')
@@ -342,17 +364,17 @@ function webCopyFiles() {
       .pipe(removeHtml('.hidden-no-device'))
       .pipe(removeHtml('[remove-if][remove-if="no-device"]'))
       .pipe(htmlmin())
-      .pipe(gulp.dest(tmpPath + '/api')),
+      .pipe(gulp.dest(targetPath + '/api')),
 
     // Copy fonts
     gulp.src('./www/fonts/**/*.*')
       .pipe(debug(debugOptions))
-      .pipe(gulp.dest(tmpPath + '/fonts')),
+      .pipe(gulp.dest(targetPath + '/fonts')),
 
     // Copy CSS
     gulp.src('./www/css/**/*.*')
       .pipe(debug(debugOptions))
-      .pipe(gulp.dest(tmpPath + '/css')),
+      .pipe(gulp.dest(targetPath + '/css')),
 
     // Copy i18n
     gulp.src('./www/i18n/locale-*.json')
@@ -361,68 +383,68 @@ function webCopyFiles() {
       .pipe(sort())
       .pipe(ngTranslate({standalone:true, module: 'cesium.translations'}))
       .pipe(debug(debugOptions))
-      .pipe(gulp.dest(tmpPath + '/js')),
+      .pipe(gulp.dest(targetPath + '/js')),
 
     // Copy img
     gulp.src('./www/img/**/*.*')
       .pipe(debug(debugOptions))
-      .pipe(gulp.dest(tmpPath + '/img')),
+      .pipe(gulp.dest(targetPath + '/img')),
 
     // Copy manifest.json
     gulp.src('./www/manifest.json')
       .pipe(debug(debugOptions))
-      .pipe(gulp.dest(tmpPath)),
+      .pipe(gulp.dest(targetPath)),
 
     // Copy lib (JS, CSS and fonts)
     gulp.src(['./www/lib/**/*.js', './www/lib/**/*.css', './www/lib/**/fonts/**/*.*'])
       .pipe(debug(debugOptions))
-      .pipe(gulp.dest(tmpPath + '/lib')),
+      .pipe(gulp.dest(targetPath + '/lib')),
 
     // Copy license into HTML
     gulp.src('./www/license/*.md')
       .pipe(markdown())
       .pipe(header('<html><header><meta charset="utf-8"></header><body>'))
       .pipe(footer('</body></html>'))
-      .pipe(gulp.dest(tmpPath + '/license')),
+      .pipe(gulp.dest(targetPath + '/license')),
 
     // Copy license into txt
     gulp.src('./www/license/*.md')
       .pipe(header('\ufeff')) // Need BOM character for UTF-8 files
       .pipe(rename({ extname: '.txt' }))
-      .pipe(gulp.dest(tmpPath + '/license'))
+      .pipe(gulp.dest(targetPath + '/license'))
   );
 }
 
 function webNgTemplate() {
-  var tmpPath = './dist/web/www';
-  return gulp.src(tmpPath + '/templates/**/*.html')
+  var targetPath = './dist/web/www';
+  return gulp.src(targetPath + '/templates/**/*.html')
     .pipe(sort())
     .pipe(templateCache({
       standalone:true,
       module:"cesium.templates",
       root: "templates/"
     }))
-    .pipe(gulp.dest(tmpPath + '/dist/dist_js/app'));
+    .pipe(gulp.dest(targetPath + '/dist/dist_js/app'));
 }
 
 function webAppNgAnnotate() {
-  var tmpPath = './dist/web/www';
+  var targetPath = './dist/web/www';
   var jsFilter = filter(["**/*.js", "!**/vendor/*"]);
 
-  return gulp.src(tmpPath + '/js/**/*.js')
+  return gulp.src(targetPath + '/js/**/*.js')
     .pipe(jsFilter)
     .pipe(ngAnnotate({single_quotes: true}))
-    .pipe(gulp.dest(tmpPath + '/dist/dist_js/app'));
+    .pipe(gulp.dest(targetPath + '/dist/dist_js/app'));
 }
 
 function webPluginCopyFiles() {
-  const tmpPath = './dist/web/www';
+  const targetPath = './dist/web/www';
   return merge(
     // Copy Js (and remove unused code)
     gulp.src('./www/plugins/**/*.js')
       .pipe(removeCode({"no-device": true}))
       .pipe(jshint())
-      .pipe(gulp.dest(tmpPath + '/plugins')),
+      .pipe(gulp.dest(targetPath + '/plugins')),
 
     // Copy HTML templates (and remove unused code)
     gulp.src('./www/plugins/**/*.html')
@@ -430,7 +452,7 @@ function webPluginCopyFiles() {
       .pipe(removeHtml('.hidden-no-device'))
       .pipe(removeHtml('[remove-if][remove-if="no-device"]'))
       .pipe(htmlmin())
-      .pipe(gulp.dest(tmpPath + '/plugins')),
+      .pipe(gulp.dest(targetPath + '/plugins')),
 
     // Transform i18n into JS
     gulp.src(paths.ng_translate_plugin)
@@ -438,43 +460,43 @@ function webPluginCopyFiles() {
       .pipe(jsonlint.reporter())
       .pipe(sort())
       .pipe(ngTranslate({standalone:true, module: 'cesium.plugins.translations'}))
-      .pipe(gulp.dest(tmpPath + '/dist/dist_js/plugins')),
+      .pipe(gulp.dest(targetPath + '/dist/dist_js/plugins')),
 
     // Copy plugin CSS
     gulp.src(paths.css_plugin)
-      .pipe(gulp.dest(tmpPath + '/dist/dist_css/plugins')),
+      .pipe(gulp.dest(targetPath + '/dist/dist_css/plugins')),
 
     // Copy Leaflet images
-    pluginLeafletImages(tmpPath + '/img'),
+    pluginLeafletImages(targetPath + '/img'),
 
     // Copy Leaflet CSS
     gulp.src('./www/css/**/leaflet.*')
-      .pipe(gulp.dest(tmpPath + '/css'))
+      .pipe(gulp.dest(targetPath + '/css'))
 
   );
 }
 
 function webPluginNgTemplate() {
-  var tmpPath = './dist/web/www';
-  return gulp.src(tmpPath + '/plugins/**/*.html')
+  var targetPath = './dist/web/www';
+  return gulp.src(targetPath + '/plugins/**/*.html')
     .pipe(sort())
     .pipe(templateCache({
       standalone:true,
       module:"cesium.plugins.templates",
       root: "plugins/"
     }))
-    .pipe(gulp.dest(tmpPath + '/dist/dist_js/plugins'));
+    .pipe(gulp.dest(targetPath + '/dist/dist_js/plugins'));
 }
 
 function webPluginNgAnnotate() {
-  var tmpPath = './dist/web/www';
-  return gulp.src(tmpPath + '/plugins/**/*.js')
+  var targetPath = './dist/web/www';
+  return gulp.src(targetPath + '/plugins/**/*.js')
     .pipe(ngAnnotate({single_quotes: true}))
-    .pipe(gulp.dest(tmpPath + '/dist/dist_js/plugins'));
+    .pipe(gulp.dest(targetPath + '/dist/dist_js/plugins'));
 }
 
-function webUglify(done) {
-  const wwwPath = './dist/web/www';
+function webUglify() {
+  const targetPath = './dist/web/www';
   const enableUglify = argv.release || argv.uglify || false;
   const version = JSON.parse(fs.readFileSync('./package.json', 'utf8')).version;
 
@@ -485,27 +507,9 @@ function webUglify(done) {
     const indexFilter = filter('**/index.html', {restore: true});
     const jsFilter = filter(["**/*.js", '!**/config.js'], {restore: true});
     const cssFilter = filter("**/*.css", {restore: true});
-    const uglifyOptions = {
-      toplevel: true,
-      warnings: true,
-      mangle: {
-        reserved: ['qrcode', 'Base58']
-      },
-      compress: {
-        global_defs: {
-          "@console.log": "alert"
-        },
-        passes: 2
-      },
-      output: {
-        beautify: false,
-        preamble: "/* minified */",
-        max_line_len: 120000
-      }
-    };
 
     // Process index.html
-    return gulp.src(wwwPath + '/index.html')
+    return gulp.src(targetPath + '/index.html')
       .pipe(useref({}, lazypipe().pipe(sourcemaps.init, { loadMaps: true })))  // Concatenate with gulp-useref
 
       // Process JS
@@ -526,34 +530,34 @@ function webUglify(done) {
 
       .pipe(sourcemaps.write('maps'))
 
-      .pipe(gulp.dest(wwwPath))
-      .on('end', done);
+      .pipe(gulp.dest(targetPath));
+  }
+  else {
+    return Promise.resolve();
   }
-
-  if (done) done();
 }
 
 function webIntegrity(done) {
-  const wwwPath = './dist/web/www';
+  const targetPath = './dist/web/www';
 
   const enableIntegrity = argv.release || false;
 
   if (enableIntegrity) {
-    log(colors.green('Add integrity hash to <script src> tag...'));
+    log(colors.green('Create index.integrity.html... '));
 
     // Process index.html
     return gulp.series(
-      gulp.src(wwwPath + '/index.html', {base: wwwPath})
+      gulp.src(targetPath + '/index.html', {base: targetPath})
 
       // Add an integrity hash
       .pipe(sriHash())
 
       .pipe(rename({ extname: '.integrity.html' }))
-      .pipe(gulp.dest(wwwPath)),
+      .pipe(gulp.dest(targetPath)),
 
-      gulp.src(wwwPath + '/index.html', {base: wwwPath})
+      gulp.src(targetPath + '/index.html', {base: targetPath})
         .pipe(rename({ extname: '.test.html' }))
-        .pipe(gulp.dest(wwwPath))
+        .pipe(gulp.dest(targetPath))
     )
       .on('end', done);
   }
@@ -562,7 +566,7 @@ function webIntegrity(done) {
 }
 
 function webApiUglify() {
-  const tmpPath = './dist/web/www';
+  const targetPath = './dist/web/www';
   const version = JSON.parse(fs.readFileSync('./package.json', 'utf8')).version;
 
   const jsFilter = filter(["**/*.js", '!**/config.js'], {restore: true});
@@ -570,27 +574,12 @@ function webApiUglify() {
   const indexFilter = filter('**/index.html', {restore: true});
 
   // Skip if not required
-  const enableUglify = argv.release || argv.useref || argv.uglify || false;
+  const enableUglify = argv.release || argv.uglify || false;
   if (enableUglify) {
     log(colors.green('API: Minify JS and CSS files...'));
-    const uglifyOptions = {
-      toplevel: true,
-      warnings: true,
-      compress: {
-        global_defs: {
-          "@console.log": "alert"
-        },
-        passes: 2
-      },
-      output: {
-        beautify: false,
-        preamble: "/* minified */",
-        max_line_len: 120000
-      }
-    };
 
     // Process api/index.html
-    return gulp.src(tmpPath + '/*/index.html')
+    return gulp.src(targetPath + '/*/index.html')
 
       .pipe(useref({}, lazypipe().pipe(sourcemaps.init, { loadMaps: true })))  // Concatenate with gulp-useref
 
@@ -617,13 +606,13 @@ function webApiUglify() {
 
       .pipe(sourcemaps.write('maps'))
 
-      .pipe(gulp.dest(tmpPath));
+      .pipe(gulp.dest(targetPath));
   }
 
   else {
     log(colors.red('API: Skipping minify JS and CSS files') + colors.grey(' (missing options --release or --uglify)'));
 
-    return gulp.src(tmpPath + '/*/index.html')
+    return gulp.src(targetPath + '/*/index.html')
       .pipe(useref())             // Concatenate with gulp-useref
 
       .pipe(indexFilter)
@@ -632,13 +621,14 @@ function webApiUglify() {
       .pipe(replace("config.js", "../config.js"))
       .pipe(indexFilter.restore)
 
-      .pipe(gulp.dest(tmpPath));
+      .pipe(gulp.dest(targetPath));
   }
 }
 
 function webCleanUnusedFiles(done) {
   log(colors.green('Clean unused files...'));
-  const enableUglify = argv.release || argv.useref || argv.uglify || false;
+  const targetPath = './dist/web/www';
+  const enableUglify = argv.release || argv.uglify || false;
   const cleanSources = enableUglify;
   const debugOptions = {
     title: 'Deleting',
@@ -648,25 +638,23 @@ function webCleanUnusedFiles(done) {
     logger: m => log(colors.grey(m))
   };
 
-  const wwwPath = './dist/web/www';
-
   if (cleanSources) {
     return merge(
       // Clean core JS
-      gulp.src(wwwPath + '/js/**/*.js', {read: false})
+      gulp.src(targetPath + '/js/**/*.js', {read: false})
         .pipe(debug(debugOptions))
         .pipe(clean()),
 
       // Clean plugins JS + CSS
-      gulp.src(wwwPath + '/plugins/**/*.js', {read: false})
+      gulp.src(targetPath + '/plugins/**/*.js', {read: false})
         .pipe(debug(debugOptions))
         .pipe(clean()),
-      gulp.src(wwwPath + '/plugins/**/*.css', {read: false})
+      gulp.src(targetPath + '/plugins/**/*.css', {read: false})
         .pipe(debug(debugOptions))
         .pipe(clean()),
 
       // Unused maps/config.js.map
-      gulp.src(wwwPath + '/maps/config.js.map', {read: false, allowEmpty: true})
+      gulp.src(targetPath + '/maps/config.js.map', {read: false, allowEmpty: true})
         .pipe(debug(debugOptions))
         .pipe(clean())
     )
@@ -812,6 +800,513 @@ function webExtBuildSuccess(done) {
   if (done) done();
 }
 
+function cdvAddPlatformToBodyTag() {
+  log(colors.green('Add platform CSS class to <body>... '));
+
+  const projectRoot = argv.root || '.';
+  const platform = argv.platform || 'android';
+  let wwwPath;
+  if (platform === 'android') {
+    wwwPath = path.join(projectRoot, 'platforms', platform, 'app','src','main','assets','www');
+  } else {
+    wwwPath = path.join(projectRoot, 'platforms', platform, 'www');
+  }
+  const indexPath = path.join(wwwPath, 'index.html');
+
+  // no opening body tag, something's wrong
+  if (!fs.existsSync(indexPath)) throw new Error('Unable to find the file ' + indexPath +'!');
+
+  // add the platform class to the body tag
+  try {
+    const platformClass = 'platform-' + platform;
+    const cordovaClass = 'platform-cordova platform-webview';
+
+    let html = fs.readFileSync(indexPath, 'utf8');
+
+    // get the body tag
+    let matches = html && html.match(/<body[^>/]+>/gi)
+    const bodyTag = matches && matches[0];
+    // no opening body tag, something's wrong
+    if (!bodyTag) throw new Error('No <body> element found in file ' + indexPath);
+
+    if (bodyTag.indexOf(platformClass) > -1) return; // already added
+
+    let newBodyTag = '' + bodyTag;
+    matches = bodyTag.match(/ class=["|'](.*?)["|']/gi);
+    const classAttr = matches && matches[0];
+    if (classAttr) {
+      // body tag has existing class attribute, add the classname
+      let endingQuote = classAttr.substring(classAttr.length - 1);
+      let newClassAttr = classAttr.substring(0, classAttr.length - 1);
+      newClassAttr += ' ' + platformClass + ' ' + cordovaClass + endingQuote;
+      newBodyTag = newBodyTag.replace(classAttr, newClassAttr);
+
+    } else {
+      // add class attribute to the body tag
+      newBodyTag = newBodyTag.replace('>', ' class="' + platformClass + ' ' + cordovaClass + '">');
+    }
+
+    html = html.replace(bodyTag, newBodyTag);
+
+    fs.writeFileSync(indexPath, html, 'utf8');
+
+    return Promise.resolve();
+  } catch (e) {
+    return Promise.reject(e);
+  }
+}
+
+function cdvCopyFiles() {
+  log(colors.green('Copying files... '));
+
+  const projectRoot = argv.root || '.';
+  const platform = argv.platform || 'android';
+  let wwwPath;
+  if (platform === 'android') {
+    wwwPath = path.join(projectRoot, 'platforms', platform, 'app','src','main','assets','www');
+  } else {
+    wwwPath = path.join(projectRoot, 'platforms', platform, 'www');
+  }
+
+  const jsFilter = filter(["**/*.js", "!**/vendor/*"]);
+
+  return merge(
+
+    // Ng annotate app JS file
+    gulp.src(wwwPath + '/js/**/*.js')
+      .pipe(jsFilter)
+      .pipe(ngAnnotate({single_quotes: true}))
+      .pipe(gulp.dest(wwwPath + '/dist/dist_js/app')),
+
+    // Ng annotate app JS file
+    gulp.src(wwwPath + '/plugins/**/*.js')
+      .pipe(ngAnnotate({single_quotes: true}))
+      .pipe(gulp.dest(wwwPath + '/dist/dist_js/plugins')),
+
+    // Copy plugin CSS
+    gulp.src(wwwPath + '/plugins/*/css/**/*.css')
+      .pipe(gulp.dest(wwwPath + '/dist/dist_css/plugins')),
+
+    // Copy Leaflet images
+    pluginLeafletImages(wwwPath + '/img'),
+
+    // Copy Leaflet CSS
+    gulp.src('./www/css/**/leaflet.*')
+      .pipe(gulp.dest(wwwPath + '/css'))
+
+  );
+}
+
+function cdvRemoveCode() {
+  log(colors.green('Removing code... '));
+
+  const projectRoot = argv.root || '.';
+  const platform = argv.platform || 'android';
+  let wwwPath;
+  if (platform === 'android') {
+    wwwPath = path.join(projectRoot, 'platforms', platform, 'app','src','main','assets','www');
+  } else {
+    wwwPath = path.join(projectRoot, 'platforms', platform, 'www');
+  }
+
+  const pluginPath = path.join(wwwPath, 'plugins', 'es');
+
+  // Compute options {device-<platform>: true}
+  let removeCodeOptions = {};
+  removeCodeOptions[platform] = true; // = {<platform>: true}
+
+  const htmlminOptions = {removeComments: true, collapseWhitespace: true};
+  const debugOptions = {
+    title: 'Processing',
+    minimal: true,
+    showFiles: argv.debug || false,
+    showCount: false,
+    logger: m => log(colors.grey(m))
+  };
+
+  // Do not remove desktop code for iOS and macOS (support for tablets and desktop macs)
+  if (platform !== 'ios' && platform !== 'osx') {
+    // Removing unused code for device...
+    return merge(
+      // Remove unused HTML tags
+      gulp.src(path.join(wwwPath, 'templates', '**', '*.html'))
+        .pipe(debug(debugOptions))
+        .pipe(removeCode({device: true}))
+        .pipe(removeCode(removeCodeOptions))
+        .pipe(removeHtml('.hidden-xs.hidden-sm'))
+        .pipe(removeHtml('.hidden-device'))
+        .pipe(removeHtml('[remove-if][remove-if="device"]'))
+        .pipe(htmlmin(htmlminOptions))
+        .pipe(gulp.dest(wwwPath + '/templates')),
+
+      gulp.src(path.join(pluginPath, '**', '*.html'))
+        .pipe(debug(debugOptions))
+        .pipe(removeCode({device: true}))
+        .pipe(removeCode(removeCodeOptions))
+        .pipe(removeHtml('.hidden-xs.hidden-sm'))
+        .pipe(removeHtml('.hidden-device'))
+        .pipe(removeHtml('[remove-if][remove-if="device"]'))
+        .pipe(htmlmin(htmlminOptions))
+        .pipe(gulp.dest(pluginPath)),
+
+      gulp.src(path.join(wwwPath, 'index.html'))
+        .pipe(debug(debugOptions))
+        .pipe(removeCode({device: true}))
+        .pipe(removeCode(removeCodeOptions))
+        .pipe(removeHtml('.hidden-xs.hidden-sm'))
+        .pipe(removeHtml('.hidden-device'))
+        .pipe(removeHtml('[remove-if][remove-if="device"]'))
+        .pipe(htmlmin(/*no options, to keep comments*/))
+        .pipe(gulp.dest(wwwPath)),
+
+      // Remove unused JS code + add ng annotations
+      gulp.src(path.join(wwwPath, 'js', '**', '*.js'))
+        .pipe(debug(debugOptions))
+        .pipe(removeCode({device: true}))
+        .pipe(removeCode(removeCodeOptions))
+        .pipe(ngAnnotate({single_quotes: true}))
+        .pipe(gulp.dest(wwwPath + '/dist/dist_js/app')),
+
+      gulp.src([pluginPath + '/js/**/*.js'])
+        .pipe(debug(debugOptions))
+        .pipe(removeCode({device: true}))
+        .pipe(removeCode(removeCodeOptions))
+        .pipe(ngAnnotate({single_quotes: true}))
+        .pipe(gulp.dest(wwwPath + '/dist/dist_js/plugins'))
+    );
+  } else {
+    return merge(
+      gulp.src(path.join(wwwPath, 'templates', '**', '*.html'))
+        .pipe(htmlmin(htmlminOptions))
+        .pipe(gulp.dest(wwwPath + '/templates')),
+
+      gulp.src(path.join(pluginPath, '**', '*.html'))
+        .pipe(htmlmin(htmlminOptions))
+        .pipe(gulp.dest(pluginPath)),
+
+      gulp.src(path.join(wwwPath, 'index.html'))
+        .pipe(gulp.dest(wwwPath)),
+
+      gulp.src(path.join(wwwPath, 'js', '**', '*.js'))
+        .pipe(ngAnnotate({single_quotes: true}))
+        .pipe(gulp.dest(wwwPath + '/dist/dist_js/app')),
+
+      gulp.src([pluginPath + '/js/**/*.js'])
+        .pipe(gulp.dest(wwwPath + '/dist/dist_js/plugins'))
+    );
+  }
+}
+
+function cdvNgTemplate() {
+  log(colors.green('Building template files...'));
+
+  const projectRoot = argv.root || '.';
+  const platform = argv.platform || 'android';
+
+  let wwwPath;
+  if (platform === 'android') {
+    wwwPath = path.join(projectRoot, 'platforms', platform, 'app','src','main','assets','www');
+  } else {
+    wwwPath = path.join(projectRoot, 'platforms', platform, 'www');
+  }
+  let distJsPath = path.join(wwwPath, 'dist', 'dist_js', 'app');
+  let pluginDistJsPath = path.join(wwwPath, 'dist', 'dist_js', 'plugins');
+  const debugOptions = {
+    title: 'Processing',
+    minimal: true,
+    showFiles: argv.debug || false,
+    showCount: false,
+    logger: m => log(colors.grey(m))
+  };
+
+  // Concat templates into a JS
+  return merge(
+    gulp.src(path.join(wwwPath, 'templates', '**', '*.html'))
+      .pipe(debug(debugOptions))
+      .pipe(templateCache({
+        standalone: true,
+        module: "cesium.templates",
+        root: "templates/"
+      }))
+      .pipe(gulp.dest(distJsPath)),
+
+    gulp.src(path.join(wwwPath, 'plugins', '*', 'templates', '**', '*.html'))
+      .pipe(debug(debugOptions))
+      .pipe(templateCache({
+        standalone: true,
+        module: "cesium.plugins.templates",
+        root: "plugins/"
+      }))
+      .pipe(gulp.dest(pluginDistJsPath))
+  );
+}
+function cdvNgTranslate() {
+  log(colors.green('Building translation files...'));
+
+  const projectRoot = argv.root || '.';
+  const platform = argv.platform || 'android';
+
+  let wwwPath;
+  if (platform === 'android') {
+    wwwPath = path.join(projectRoot, 'platforms', platform, 'app', 'src', 'main', 'assets', 'www');
+  } else {
+    wwwPath = path.join(projectRoot, 'platforms', platform, 'www');
+  }
+  let distJsPath = path.join(wwwPath, 'dist', 'dist_js', 'app');
+  let pluginDistJsPath = path.join(wwwPath, 'dist', 'dist_js', 'plugins');
+
+  const debugOptions = {
+    title: 'Processing',
+    minimal: true,
+    showFiles: argv.debug || false,
+    showCount: false,
+    logger: m => log(colors.grey(m))
+  };
+
+  // Concat templates into a JS
+  return merge(
+      gulp.src(wwwPath + '/i18n/locale-*.json')
+        .pipe(debug(debugOptions))
+        .pipe(ngTranslate({standalone: true, module: 'cesium.translations'}))
+        .pipe(gulp.dest(distJsPath)),
+
+      gulp.src(wwwPath + '/plugins/*/i18n/locale-*.json')
+        .pipe(debug(debugOptions))
+        .pipe(ngTranslate({standalone: true, module: 'cesium.plugins.translations'}))
+        .pipe(gulp.dest(pluginDistJsPath))
+    );
+}
+
+function cdvUglify() {
+
+  const projectRoot = argv.root || '.';
+  const platform = argv.platform || 'android';
+
+  let wwwPath;
+  if (platform === 'android') {
+    wwwPath = path.join(projectRoot, 'platforms', platform, 'app', 'src', 'main', 'assets', 'www');
+  } else {
+    wwwPath = path.join(projectRoot, 'platforms', platform, 'www');
+  }
+  let indexPath = path.join(wwwPath, 'index.html');
+
+  // Skip if not required
+  const enableUglify = argv.release || argv.uglify || false;
+  if (enableUglify) {
+    log(colors.green('Minify JS and CSS files...'));
+
+    // WARN: uglify only libs, to keep sources readable (need by free repo)
+    const jsLibFilter = filter(['*/lib/**/*.js', '*/js/vendor/**/*.js'], {restore: true}); // External libs only
+    const cssFilter = filter("**/*.css", {restore: true});
+    const cdvUglifyOptions = {
+      ...uglifyOptions,
+      ecma: '5'
+    };
+    const debugOptions = {
+      title: 'Minifying',
+      minimal: true,
+      showFiles: argv.debug || false,
+      showCount: false,
+      logger: m => log(colors.grey(m))
+    };
+
+    return gulp.src(indexPath)
+      .pipe(useref())             // Concatenate with gulp-useref
+
+      // Process JS
+      .pipe(jsLibFilter)
+      .pipe(debug(debugOptions))
+      .pipe(uglify(cdvUglifyOptions))// Minify javascript sources
+      .pipe(jsLibFilter.restore)
+
+      // Process CSS
+      .pipe(cssFilter)
+      .pipe(debug(debugOptions))
+      .pipe(csso())               // Minify any CSS sources
+      .pipe(cssFilter.restore)
+
+      .pipe(gulp.dest(wwwPath));
+  }
+  else {
+    log(colors.red('Skipping minify JS and CSS files') + colors.grey(' (missing options --release or --uglify)'));
+    return Promise.resolve();
+  }
+}
+
+function cdvCleanUnusedDirectories() {
+  log(colors.green('Clean unused directories...'));
+
+  const projectRoot = argv.root || '.';
+  const platform = argv.platform || 'android';
+
+  let wwwPath;
+  if (platform === 'android') {
+    wwwPath = path.join(projectRoot, 'platforms', platform, 'app', 'src', 'main', 'assets', 'www');
+  } else {
+    wwwPath = path.join(projectRoot, 'platforms', platform, 'www');
+  }
+
+  const enableUglify = argv.release || argv.uglify || false;
+  const debugOptions = {
+    title: 'Deleting',
+    minimal: true,
+    showFiles: argv.debug || false,
+    showCount: !argv.debug,
+    logger: m => log(colors.grey(m))
+  };
+
+  let patterns = [
+    wwwPath + '/api',
+
+    // Remove HTML templates - replaced by ngTemplate()
+    wwwPath + '/templates',
+
+    // Remove Cesium plugins
+    // (WARN: remove one by one, to keep Cordova plugins)
+    wwwPath + '/plugins/es',
+    wwwPath + '/plugins/graph',
+    wwwPath + '/plugins/map',
+    wwwPath + '/plugins/rml9',
+
+    // Remove translations - replaced by ngTranslate()
+    wwwPath + '/**/i18n',
+  ];
+
+  if (enableUglify) {
+    patterns = patterns.concat([
+      wwwPath + '/js',
+      wwwPath + '/css', // Have been replaced by useref(), into 'dist_css'
+      wwwPath + '/dist', // Have been replaced by useref(), into 'dist_js'
+      wwwPath + '/cordova-js-src',
+
+      // Clean lib directory...
+      wwwPath + '/lib/*',
+
+      // ...but Keep IonIcons font
+      '!' + wwwPath + '/lib/ionic',
+      wwwPath + '/lib/ionic/*',
+      '!' + wwwPath + '/lib/ionic/fonts',
+
+      // ...but Keep RobotoDraft font
+      '!' + wwwPath + '/lib/robotodraft',
+      wwwPath + '/lib/robotodraft/*',
+      '!' + wwwPath + '/lib/robotodraft/fonts'
+    ]);
+  }
+  else {
+    patterns = patterns.concat([
+      wwwPath + '/js/*', // Have been replace into dist/dist_js
+      '!' + wwwPath + '/js/vendor', // BUT keep vendor lib
+    ]);
+  }
+
+  return gulp.src(patterns, {read: false, allowEmpty: true})
+    .pipe(debug(debugOptions))
+    .pipe(clean());
+}
+
+
+function cdvCopyBuildFiles() {
+
+  log(colors.green('Copy build files... '));
+
+  const projectRoot = argv.root || '.';
+  const platform = argv.platform || 'android';
+
+  const srcPath = path.join(projectRoot, 'resources', platform, 'build');
+  const targetPath = path.join(projectRoot, 'platforms', platform);
+  const debugOptions = {
+    title: 'Copying',
+    minimal: true,
+    showFiles: argv.debug || false,
+    showCount: !argv.debug,
+    logger: m => log(colors.grey(m))
+  };
+
+  if (fs.existsSync(srcPath)) {
+    return gulp.src(srcPath + '/**/*.*')
+      .pipe(debug(debugOptions))
+      .pipe(gulp.dest(targetPath));
+  }
+  else {
+    log(colors.blue(' Directory ' + srcPath + 'not found. Skipping copy to ' + targetPath));
+    return Promise.resolve();
+  }
+}
+
+function cdvAndroidManifest() {
+
+  const projectRoot = argv.root || '.';
+  const platform = argv.platform || 'android';
+
+  const srcMainPath = path.join(projectRoot, 'platforms', platform, 'app', 'src', 'main');
+  const androidManifestFile = path.join(srcMainPath, 'AndroidManifest.xml');
+
+  log(colors.green(' Updating Android manifest... ') + colors.grey(androidManifestFile));
+
+  if (!fs.existsSync(androidManifestFile)) {
+    throw Error("Missing required file " + androidManifestFile);
+  }
+
+  return gulp.src(androidManifestFile)
+
+    // Add 'tools' namespace to root tag
+    .pipe(replace(/(xmlns:android="http:\/\/schemas.android.com\/apk\/res\/android")\s*>/g, '$1 xmlns:tools="http://schemas.android.com/tools">'))
+
+    // Use AndroidX
+    .pipe(replace(/\s+tools:replace="android:appComponentFactory"/, ''))
+    .pipe(replace(/\s+android:appComponentFactory="[^"]+"/, ''))
+    .pipe(replace(/(\s*<application)\s*/, '$1 tools:replace="android:appComponentFactory" android:appComponentFactory="androidx.core.app.CoreComponentFactory" '))
+
+    // remove all <uses-sdk>
+    .pipe(replace(/<uses-sdk [^>]+\/>/g, ''))
+
+    // add <uses-sdk> (tools:overrideLibrary)
+    .pipe(replace(/(<\/manifest>)/, '    <uses-sdk tools:overrideLibrary="org.kaliumjni.lib,org.apache.cordova" />\n$1'))
+
+    .pipe(gulp.dest(srcMainPath));
+}
+
+function cdvAndroidCheckSigning() {
+
+  const projectRoot = argv.root || '.';
+  const platform = argv.platform || 'android';
+  const targetPath = path.join(projectRoot, 'platforms', platform);
+  const signingFile = path.join(targetPath, 'release-signing.properties');
+
+  // Check signing file exists
+  if (fs.existsSync(targetPath) && !fs.existsSync(signingFile)) {
+    log(colors.blue('WARNING: Missing file ' + signingFile));
+    log(colors.blue('  Please create it manually, otherwise release APK files will NOT be signed! '));
+  }
+
+  return Promise.resolve();
+}
+
+function cdvAfterPrepare(done, projectRoot, platform) {
+
+  projectRoot = (typeof projectRoot === 'string' && projectRoot) || argv.root || '.';
+  platform = ((typeof platform === 'string' && platform) || argv.platform || 'android').toLowerCase();
+
+  // Override arguments, to pass it to other tasks
+  argv.root = projectRoot;
+  argv.platform = platform;
+
+  let wrapper = gulp.series(
+    gulp.parallel(cdvCopyFiles, cdvAddPlatformToBodyTag),
+    cdvRemoveCode,
+    gulp.parallel(cdvNgTemplate, cdvNgTranslate),
+    cdvUglify,
+    gulp.parallel(cdvCleanUnusedDirectories,cdvCopyBuildFiles)
+  );
+
+  if (platform === 'android') {
+    wrapper = gulp.series(wrapper, cdvAndroidManifest, cdvAndroidCheckSigning);
+  }
+
+  wrapper(done);
+}
+
 function help() {
   log(colors.green("Usage: gulp {config|webBuild|webExtBuild} OPTIONS"));
   log(colors.green(""));
@@ -871,8 +1366,19 @@ const webExtBuild = gulp.series(
   webExtBuildSuccess
 );
 
+
+exports.cdvRemoveCode = cdvRemoveCode;
+exports.cdvNgTemplate = cdvNgTemplate;
+exports.cdvNgTranslate = cdvNgTranslate;
+exports.cdvUglify = cdvUglify;
+exports.cdvCleanUnusedDirectories = cdvCleanUnusedDirectories;
+exports.cdvCopyBuildFiles = cdvCopyBuildFiles;
+exports.cdvAndroidManifest = cdvAndroidManifest;
+exports.cdvAndroidCheckSigning = cdvAndroidCheckSigning;
+exports.cdvAfterPrepare = cdvAfterPrepare;
+
 /* --------------------------------------------------------------------------
-   -- Define gulp public tasks
+   -- Define public tasks
    --------------------------------------------------------------------------*/
 
 exports.help = help;
diff --git a/package.json b/package.json
index c768593e6bfcd5ba5ddb67e938b182d45341c799..e321ac062dcd9792d17b906d62abc712e07a4fc5 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
   "scripts": {
     "clean": "trash www/dist/** dist/web/* dist/desktop/**/*.deb platforms/android/**/*.apk",
     "postinstall": "node scripts/node/postinstall.js",
+    "lint": "node scripts/node/jshint.js",
     "install-platforms": "ionic cordova prepare",
     "start": "ionic serve",
     "start:firefox": "gulp webExtCompile && web-ext run --source-dir ./dist/web/ext/",
diff --git a/resources/android/build/app/src/main/java/fr/duniter/cesium/MainActivity.java b/resources/android/build/app/src/main/java/fr/duniter/cesium/MainActivity.java
index 7271039ff0b3523b9714c3c23cc0bd09a9f3b0e2..3287e5747e28af4c332377406fb1cc5ac2e0c256 100644
--- a/resources/android/build/app/src/main/java/fr/duniter/cesium/MainActivity.java
+++ b/resources/android/build/app/src/main/java/fr/duniter/cesium/MainActivity.java
@@ -73,8 +73,14 @@ public class MainActivity extends CordovaActivity
       pathSegments = uri.getPathSegments();
     } else if ("web+june".equals(scheme) || "june".equals(scheme)) {
       pathSegments = new ArrayList<String>();
-      // Use the host as first path segment
-      pathSegments.add(uri.getHost());
+      // Use the host as first path segment, if any
+      if (uri.getHost() != null) {
+        pathSegments.add(uri.getHost());
+      }
+      // Or use
+      else if (uri.getEncodedSchemeSpecificPart() != null) {
+        pathSegments.add(uri.getEncodedSchemeSpecificPart());
+      }
       if (uri.getPathSegments() != null) pathSegments.addAll(uri.getPathSegments());
     } else {
       return; // Skip
@@ -92,11 +98,15 @@ public class MainActivity extends CordovaActivity
     if (appView == null) {
       init();
     }
-    this.appView.loadUrlIntoView(url, false);
+    runOnUiThread(new Runnable() {
+      public void run() {
+        MainActivity.this.appView.loadUrlIntoView(url, false);
+      }
+    });
   }
 
   protected String getLaunchUrlNoHash() {
-    String url = this.launchUrl;
+    String url = "http://localhost/";//this.launchUrl;
     // Remove hash path
     int hashIndex = url.indexOf('#');
     if (hashIndex != -1) {
@@ -117,89 +127,4 @@ public class MainActivity extends CordovaActivity
 
       return sb.toString();
   }
-
-  // Taken from commons StringEscapeUtils
-  protected void escapeJavaStyleString(Writer out, String str, boolean escapeSingleQuote,
-                                       boolean escapeForwardSlash) throws IOException {
-    if (out == null) {
-      throw new IllegalArgumentException("The Writer must not be null");
-    }
-    if (str == null) {
-      return;
-    }
-    int sz;
-    sz = str.length();
-    for (int i = 0; i < sz; i++) {
-      char ch = str.charAt(i);
-
-      // handle unicode
-      if (ch > 0xfff) {
-        out.write("\\u" + hex(ch));
-      } else if (ch > 0xff) {
-        out.write("\\u0" + hex(ch));
-      } else if (ch > 0x7f) {
-        out.write("\\u00" + hex(ch));
-      } else if (ch < 32) {
-        switch (ch) {
-          case '\b':
-            out.write('\\');
-            out.write('b');
-            break;
-          case '\n':
-            out.write('\\');
-            out.write('n');
-            break;
-          case '\t':
-            out.write('\\');
-            out.write('t');
-            break;
-          case '\f':
-            out.write('\\');
-            out.write('f');
-            break;
-          case '\r':
-            out.write('\\');
-            out.write('r');
-            break;
-          default:
-            if (ch > 0xf) {
-              out.write("\\u00" + hex(ch));
-            } else {
-              out.write("\\u000" + hex(ch));
-            }
-            break;
-        }
-      } else {
-        switch (ch) {
-          case '\'':
-            if (escapeSingleQuote) {
-              out.write('\\');
-            }
-            out.write('\'');
-            break;
-          case '"':
-            out.write('\\');
-            out.write('"');
-            break;
-          case '\\':
-            out.write('\\');
-            out.write('\\');
-            break;
-          case '/':
-            if (escapeForwardSlash) {
-              out.write('\\');
-            }
-            out.write('/');
-            break;
-          default:
-            out.write(ch);
-            break;
-        }
-      }
-    }
-  }
-
-  private static String hex(char ch) {
-    return Integer.toHexString(ch).toUpperCase(Locale.ENGLISH);
-  }
 }
diff --git a/resources/android/build/gradle/wrapper/gradle-wrapper.properties b/resources/android/build/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000000000000000000000000000000000..ee93d6819ddbcbcfe0ca286e71f4987343dcfc5d
--- /dev/null
+++ b/resources/android/build/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https://services.gradle.org/distributions/gradle-6.5.1-all.zip
diff --git a/scripts/hooks/after_prepare.js b/scripts/hooks/after_prepare.js
new file mode 100644
index 0000000000000000000000000000000000000000..9f42741279c04fb6451f1b81ef1aa130014177d2
--- /dev/null
+++ b/scripts/hooks/after_prepare.js
@@ -0,0 +1,25 @@
+#!/usr/bin/env node
+
+const gulp = require('gulp'),
+  path = require("path"),
+  log = require('fancy-log'),
+  colors = require('ansi-colors');
+
+module.exports = function(context) {
+  const now = Date.now();
+  log("Executing '" + colors.cyan("after_prepare") + "' hook...");
+
+  const projectRoot = context && context.opts && context.opts.projectRoot || '.';
+  const platforms = context && context.opts && context.opts.platforms || ['android'];
+  const gulpFile = require(path.join(projectRoot, 'gulpfile'));
+
+  if (!projectRoot || !platforms || !gulpFile) return; // Skip
+
+  return Promise.all(platforms
+    .map(platform => {
+      return new Promise(done => gulpFile.cdvAfterPrepare(done, projectRoot, platform.trim().toLowerCase()));
+    }))
+    .then(() => {
+      log(colors.grey("Hook 'after_prepare' finished in " + (Date.now() - now) + 'ms'));
+    });
+}
diff --git a/scripts/hooks/after_prepare/010_add_platform_class.js b/scripts/hooks/after_prepare/010_add_platform_class.js
deleted file mode 100755
index d0278aa6c0b39363b3713af91de5b28741809d8f..0000000000000000000000000000000000000000
--- a/scripts/hooks/after_prepare/010_add_platform_class.js
+++ /dev/null
@@ -1,92 +0,0 @@
-#!/usr/bin/env node
-
-const fs = require('fs'),
-  path = require('path');
-
-function addPlatformBodyTag(indexPath, platform) {
-  // add the platform class to the body tag
-  try {
-    const platformClass = 'platform-' + platform;
-    const cordovaClass = 'platform-cordova platform-webview';
-
-    let html = fs.readFileSync(indexPath, 'utf8');
-
-    const bodyTag = findBodyTag(html);
-    if (!bodyTag) return; // no opening body tag, something's wrong
-
-    if (bodyTag.indexOf(platformClass) > -1) return; // already added
-
-    let newBodyTag = bodyTag;
-
-    let classAttr = findClassAttr(bodyTag);
-    if (classAttr) {
-      // body tag has existing class attribute, add the classname
-      let endingQuote = classAttr.substring(classAttr.length - 1);
-      let newClassAttr = classAttr.substring(0, classAttr.length - 1);
-      newClassAttr += ' ' + platformClass + ' ' + cordovaClass + endingQuote;
-      newBodyTag = bodyTag.replace(classAttr, newClassAttr);
-
-    } else {
-      // add class attribute to the body tag
-      newBodyTag = bodyTag.replace('>', ' class="' + platformClass + ' ' + cordovaClass + '">');
-    }
-
-    html = html.replace(bodyTag, newBodyTag);
-
-    fs.writeFileSync(indexPath, html, 'utf8');
-
-    process.stdout.write('add to body class: ' + platformClass + '\n');
-  } catch (e) {
-    process.stdout.write(e);
-  }
-}
-
-function findBodyTag(html) {
-  // get the body tag
-  try {
-    return html.match(/<body(?=[\s>])(.*?)>/gi)[0];
-  } catch (e) {
-  }
-}
-
-function findClassAttr(bodyTag) {
-  // get the body tag's class attribute
-  try {
-    return bodyTag.match(/ class=["|'](.*?)["|']/gi)[0];
-  } catch (e) {
-  }
-}
-
-module.exports = function(context) {
-
-  const rootdir = context.opts.projectRoot;
-  const platforms = context.opts.platforms;
-
-  if (rootdir && platforms) {
-
-    // go through each of the platform directories that have been prepared
-    for (let x = 0; x < platforms.length; x++) {
-      // open up the index.html file at the www root
-      try {
-        const platform = platforms[x].trim().toLowerCase();
-        let indexPath;
-
-        if (platform === 'android') {
-          //indexPath = path.join(rootdir, 'platforms', platform, 'app', 'src', 'main', 'assets', 'www', 'index.html');
-          indexPath = path.join('platforms', platform, 'assets', 'www', 'index.html');
-        } else {
-          indexPath = path.join('platforms', platform, 'www', 'index.html');
-        }
-
-        if (fs.existsSync(indexPath)) {
-          addPlatformBodyTag(indexPath, platform);
-        }
-
-      } catch (e) {
-        process.stdout.write(e);
-      }
-    }
-
-  }
-
-}
diff --git a/scripts/hooks/after_prepare/020_remove_code.js b/scripts/hooks/after_prepare/020_remove_code.js
deleted file mode 100755
index 1889cb18664dc57568260355dbd87b0fe6a4025e..0000000000000000000000000000000000000000
--- a/scripts/hooks/after_prepare/020_remove_code.js
+++ /dev/null
@@ -1,111 +0,0 @@
-#!/usr/bin/env node
-
-module.exports = function(context) {
-
-  const gulp = require('gulp'),
-    path = require("path"),
-    removeCode = require('gulp-remove-code'),
-    removeHtml = require('gulp-html-remove'),
-    ngAnnotate = require('gulp-ng-annotate'),
-    htmlmin = require('gulp-htmlmin'),
-    merge = require('merge2');
-
-  const rootdir = context.opts.projectRoot;
-  const platforms = context.opts.platforms;
-
-  if (rootdir && platforms) {
-
-    // go through each of the platform directories that have been prepared
-    for(let x=0; x<platforms.length; x++) {
-
-      let platform = platforms[x].trim().toLowerCase();
-
-      let wwwPath;
-      if(platform === 'android') {
-        wwwPath = path.join(rootdir, 'platforms', platform, 'app/src/main/assets/www');
-      } else {
-        wwwPath = path.join(rootdir, 'platforms', platform, 'www');
-      }
-
-      var pluginPath = path.join(wwwPath, 'plugins') + '/es';
-
-      // Log
-      //console.log('['+process.mainModule.filename+'] Removing code for platform '+platform+'\n');
-
-      // Compute options {device-<platform>: true}
-      let platformRemoveCodeOptions = {};
-      platformRemoveCodeOptions[platform] = true; // = {<platform>: true}
-
-      let htmlminOptions = {removeComments: true, collapseWhitespace: true};
-
-      // Do not remove desktop code for iOS and macOS (support for tablets and desktop macs)
-      if (platform !== 'ios' && platform !== 'osx') {
-        // Removing unused code for device...
-        merge(
-          // Remove unused HTML tags
-          gulp.src(path.join(wwwPath, 'templates', '**', '*.html'))
-            .pipe(removeCode({device: true}))
-            .pipe(removeCode(platformRemoveCodeOptions))
-            .pipe(removeHtml('.hidden-xs.hidden-sm'))
-            .pipe(removeHtml('.hidden-device'))
-            .pipe(removeHtml('[remove-if][remove-if="device"]'))
-            .pipe(htmlmin(htmlminOptions))
-            .pipe(gulp.dest(wwwPath + '/templates')),
-
-          gulp.src(path.join(pluginPath, '**', '*.html'))
-            .pipe(removeCode({device: true}))
-            .pipe(removeCode(platformRemoveCodeOptions))
-            .pipe(removeHtml('.hidden-xs.hidden-sm'))
-            .pipe(removeHtml('.hidden-device'))
-            .pipe(removeHtml('[remove-if][remove-if="device"]'))
-            .pipe(htmlmin(htmlminOptions))
-            .pipe(gulp.dest(pluginPath)),
-
-          gulp.src(path.join(wwwPath, 'index.html'))
-            .pipe(removeCode({device: true}))
-            .pipe(removeCode(platformRemoveCodeOptions))
-            .pipe(removeHtml('.hidden-xs.hidden-sm'))
-            .pipe(removeHtml('.hidden-device'))
-            .pipe(removeHtml('[remove-if][remove-if="device"]'))
-            .pipe(htmlmin(/*no options, to keep comments*/))
-            .pipe(gulp.dest(wwwPath)),
-
-          // Remove unused JS code + add ng annotations
-          gulp.src(path.join(wwwPath, 'js', '**', '*.js'))
-            .pipe(removeCode({device: true}))
-            .pipe(removeCode(platformRemoveCodeOptions))
-            .pipe(ngAnnotate({single_quotes: true}))
-            .pipe(gulp.dest(wwwPath + '/dist/dist_js/app')),
-
-          gulp.src([pluginPath + '/js/**/*.js'])
-            .pipe(removeCode({device: true}))
-            .pipe(removeCode(platformRemoveCodeOptions))
-            .pipe(ngAnnotate({single_quotes: true}))
-            .pipe(gulp.dest(wwwPath + '/dist/dist_js/plugins'))
-        );
-      } else {
-        merge(
-          gulp.src(path.join(wwwPath, 'templates', '**', '*.html'))
-            .pipe(htmlmin(htmlminOptions))
-            .pipe(gulp.dest(wwwPath + '/templates')),
-
-          gulp.src(path.join(pluginPath, '**', '*.html'))
-            .pipe(htmlmin(htmlminOptions))
-            .pipe(gulp.dest(pluginPath)),
-
-          gulp.src(path.join(wwwPath, 'index.html'))
-            .pipe(gulp.dest(wwwPath)),
-
-          gulp.src(path.join(wwwPath, 'js', '**', '*.js'))
-            .pipe(ngAnnotate({single_quotes: true}))
-            .pipe(gulp.dest(wwwPath + '/dist/dist_js/app')),
-
-          gulp.src([pluginPath + '/js/**/*.js'])
-            .pipe(gulp.dest(wwwPath + '/dist/dist_js/plugins'))
-        );
-      }
-    }
-  }
-
-
-}
diff --git a/scripts/hooks/after_prepare/021_template_cache.js b/scripts/hooks/after_prepare/021_template_cache.js
deleted file mode 100755
index 539eb58adbc785c4ed20f796c628f1e00a594990..0000000000000000000000000000000000000000
--- a/scripts/hooks/after_prepare/021_template_cache.js
+++ /dev/null
@@ -1,50 +0,0 @@
-#!/usr/bin/env node
-
-const gulp = require('gulp'),
-  path = require("path"),
-  templateCache = require('gulp-angular-templatecache'),
-  merge = require('merge2');
-
-module.exports = function(context) {
-
-  const rootdir = context.opts.projectRoot;
-  const platforms = context.opts.platforms;
-
-  if (rootdir && platforms) {
-
-    // go through each of the platform directories that have been prepared
-    for (let x = 0; x < platforms.length; x++) {
-
-      let platform = platforms[x].trim().toLowerCase();
-
-      let wwwPath;
-      if (platform === 'android') {
-        wwwPath = path.join(rootdir, 'platforms', platform, 'app/src/main/assets/www');
-      } else {
-        wwwPath = path.join(rootdir, 'platforms', platform, 'www');
-      }
-
-      let distJsPath = path.join(wwwPath, 'dist', 'dist_js', 'app');
-      let pluginDistJsPath = path.join(wwwPath, 'dist', 'dist_js', 'plugins');
-
-      // Concat templates into a JS
-      merge(
-        gulp.src(path.join(wwwPath, 'templates', '**', '*.html'))
-          .pipe(templateCache({
-            standalone: true,
-            module: "cesium.templates",
-            root: "templates/"
-          }))
-          .pipe(gulp.dest(distJsPath)),
-
-        gulp.src(path.join(wwwPath, 'plugins', '*', 'templates', '**', '*.html'))
-          .pipe(templateCache({
-            standalone: true,
-            module: "cesium.plugins.templates",
-            root: "plugins/"
-          }))
-          .pipe(gulp.dest(pluginDistJsPath))
-      );
-    }
-  }
-}
diff --git a/scripts/hooks/after_prepare/022_translate.js b/scripts/hooks/after_prepare/022_translate.js
deleted file mode 100755
index 05d982fadadd0c154beafbb32aee7316f3207d1a..0000000000000000000000000000000000000000
--- a/scripts/hooks/after_prepare/022_translate.js
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/bin/env node
-
-const gulp = require('gulp'),
-  path = require("path"),
-  merge = require('merge2'),
-  ngTranslate = require('gulp-angular-translate');
-
-module.exports = function(context) {
-  const rootdir = context.opts.projectRoot;
-  const platforms = context.opts.platforms;
-
-  if (rootdir && platforms) {
-
-    // go through each of the platform directories that have been prepared
-    for (let x = 0; x < platforms.length; x++) {
-
-      let platform = platforms[x].trim().toLowerCase();
-
-      let wwwPath;
-      if (platform === 'android') {
-        wwwPath = path.join(rootdir, 'platforms', platform, 'app/src/main/assets/www');
-      } else {
-        wwwPath = path.join(rootdir, 'platforms', platform, 'www');
-      }
-
-      let distJsPath = path.join(wwwPath, 'dist', 'dist_js', 'app');
-      let pluginDistJsPath = path.join(wwwPath, 'dist', 'dist_js', 'plugins');
-
-      // Concat templates into a JS
-      merge(
-        gulp.src(wwwPath + '/i18n/locale-*.json')
-          .pipe(ngTranslate({standalone: true, module: 'cesium.translations'}))
-          .pipe(gulp.dest(distJsPath)),
-
-        gulp.src(wwwPath + '/plugins/*/i18n/locale-*.json')
-          .pipe(ngTranslate({standalone: true, module: 'cesium.plugins.translations'}))
-          .pipe(gulp.dest(pluginDistJsPath))
-      );
-    }
-  }
-
-}
diff --git a/scripts/hooks/after_prepare/040_useref.js b/scripts/hooks/after_prepare/040_useref.js
deleted file mode 100755
index c03dd700e41d6d6a412cecacd206609ca7e77f6f..0000000000000000000000000000000000000000
--- a/scripts/hooks/after_prepare/040_useref.js
+++ /dev/null
@@ -1,83 +0,0 @@
-#!/usr/bin/env node
-
-const gulp = require('gulp'),
-  path = require("path"),
-  es = require('event-stream'),
-  useref = require('gulp-useref'),
-  filter = require('gulp-filter'),
-  uglify = require('gulp-uglify-es').default,
-  csso = require('gulp-csso'),
-  log = require('fancy-log'),
-  colors = require('ansi-colors');
-
-module.exports = function(context) {
-
-  let skip = true;
-  if (context.cmdLine.indexOf("--release") > -1 || context.cmdLine.indexOf("--useref") > -1) {
-    skip = false;
-  } else {
-    log(colors.grey('Skipping useref'));
-  }
-
-  const rootdir = context.opts.projectRoot;
-  const platforms = context.opts.platforms;
-
-  if (rootdir && platforms && !skip) {
-
-    // go through each of the platform directories that have been prepared
-    const platforms = (process.env.CORDOVA_PLATFORMS ? process.env.CORDOVA_PLATFORMS.split(',') : []);
-
-    for (let x = 0; x < platforms.length; x++) {
-
-      let platform = platforms[x].trim().toLowerCase();
-
-      let wwwPath;
-      if (platform === 'android') {
-        wwwPath = path.join(rootdir, 'platforms', platform, 'app/src/main/assets/www');
-      } else {
-        wwwPath = path.join(rootdir, 'platforms', platform, 'www');
-      }
-
-      let indexPath = path.join(wwwPath, 'index.html');
-
-      const jsFilter = filter(['*/lib/**/*.js', '*/js/vendor/**/*.js'], {restore: true});
-      const cssFilter = filter('**/*.css', {restore: true});
-      const uglifyOptions = {
-        toplevel: true,
-        warnings: true,
-        ecma: '5',
-        mangle: {
-          reserved: ['qrcode', 'Base58']
-        },
-        compress: {
-          global_defs: {
-            "@console.log": "alert"
-          },
-          passes: 2
-        },
-        output: {
-          beautify: false,
-          preamble: "/* minified */",
-          max_line_len: 120000
-        }
-      };
-
-      // Removing unused code for device...
-      es.concat(
-        gulp.src(indexPath)
-          .pipe(useref())      // Concatenate with gulp-useref
-
-          .pipe(jsFilter)
-          .pipe(uglify(uglifyOptions)) // Minify any javascript sources
-          .pipe(jsFilter.restore)
-
-          .pipe(cssFilter)
-          .pipe(csso())               // Minify any CSS sources
-          .pipe(cssFilter.restore)
-
-          .pipe(gulp.dest(wwwPath))
-      );
-    }
-  }
-
-}
diff --git a/scripts/hooks/after_prepare/050_clean_unused_directories.js b/scripts/hooks/after_prepare/050_clean_unused_directories.js
deleted file mode 100755
index 652207869b8f853c3455dd75a0d28955d721a1e2..0000000000000000000000000000000000000000
--- a/scripts/hooks/after_prepare/050_clean_unused_directories.js
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/usr/bin/env node
-
-const path = require("path"),
-  del = require('del');
-
-module.exports = function(context) {
-
-  let skip = true;
-  if (context.cmdLine.indexOf("--release") > -1 || context.cmdLine.indexOf("--useref") > -1) {
-    skip = false;
-  }
-
-  const rootdir = context.opts.projectRoot;
-  const platforms = context.opts.platforms;
-
-  if (rootdir && platforms && !skip) {
-
-    // go through each of the platform directories that have been prepared
-    for(let x=0; x<platforms.length; x++) {
-
-      let platform = platforms[x].trim().toLowerCase();
-
-      let wwwPath;
-      if(platform === 'android') {
-        wwwPath = path.join(rootdir, 'platforms', platform, 'app/src/main/assets/www');
-      } else {
-        wwwPath = path.join(rootdir, 'platforms', platform, 'www');
-      }
-
-      // Log
-      console.log('['+process.mainModule.filename+'] Cleaning unused directories');
-
-      // Clean unused directories
-      del.sync([
-        path.join(wwwPath, 'api'),
-        path.join(wwwPath, 'i18n'),
-        path.join(wwwPath, 'js'),
-        path.join(wwwPath, 'templates'),
-        path.join(wwwPath, 'css'),
-        path.join(wwwPath, 'dist'),
-        path.join(wwwPath, 'cordova-js-src'),
-        path.join(wwwPath, 'plugins', 'es'),
-        path.join(wwwPath, 'plugins', 'graph'),
-        path.join(wwwPath, 'plugins', 'map'),
-        path.join(wwwPath, 'plugins', 'rml9'),
-
-        // Clean lib directory...
-        path.join(wwwPath, 'lib', '*'),
-
-        // ...but keep Ionic fonts directory
-        '!'+path.join(wwwPath, 'lib', 'ionic'),
-        path.join(wwwPath, 'lib', 'ionic', '*'),
-        '!'+path.join(wwwPath, 'lib', 'ionic', 'fonts'),
-
-        // ...and keep Robotodraft fonts directory
-        '!'+path.join(wwwPath, 'lib', 'robotdraft'),
-        path.join(wwwPath, 'lib', 'robotdraft', '*'),
-        '!'+path.join(wwwPath, 'lib', 'robotdraft', 'fonts')
-      ]);
-    }
-  }
-
-
-}
diff --git a/scripts/hooks/after_prepare/060_prepare_android_manifest.js b/scripts/hooks/after_prepare/060_prepare_android_manifest.js
deleted file mode 100755
index fe7a6a714a9483b9f3153b805ddf7cd9c2441bf8..0000000000000000000000000000000000000000
--- a/scripts/hooks/after_prepare/060_prepare_android_manifest.js
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/usr/bin/env node
-
-const gulp = require('gulp'),
-  path = require("path"),
-  replace = require('gulp-replace');
-
-module.exports = function(context) {
-  const rootdir = context.opts.projectRoot;
-  const platforms = context.opts.platforms;
-
-  if (rootdir && platforms) {
-
-    // go through each of the platform directories that have been prepared
-    for (let x = 0; x < platforms.length; x++) {
-
-      let platform = platforms[x].trim().toLowerCase();
-
-      if (platform === 'android') {
-        let srcMainPath = path.join(rootdir, 'platforms', platform, 'app/src/main');
-        let androidManifestFile = path.join(srcMainPath, 'AndroidManifest.xml');
-
-        // Clean unused directories
-        console.log('-----------------------------------------');
-        console.log(' Updating file: ' + androidManifestFile);
-        gulp.src(androidManifestFile)
-
-          // Add 'tools' namespace to root tag
-          .pipe(replace(/(xmlns:android="http:\/\/schemas.android.com\/apk\/res\/android")\s*>/g, '$1 xmlns:tools="http://schemas.android.com/tools">'))
-
-          // Add <application> (replace 'targetSdkversion' and add tools:overrideLibrary)
-          .pipe(replace(/\s+tools:replace="android:appComponentFactory"/, ''))
-          .pipe(replace(/\s+android:appComponentFactory="[^"]+"/, ''))
-          .pipe(replace(/(\s*<application)\s*/, '$1 tools:replace="android:appComponentFactory" android:appComponentFactory="androidx.core.app.CoreComponentFactory" '))
-
-          // remove all <uses-sdk>
-          .pipe(replace(/<uses-sdk [^>]+\/>/g, ''))
-
-          // add <uses-sdk> (replace 'targetSdkversion' and add tools:overrideLibrary)
-          .pipe(replace(/(<\/manifest>)/, '    <uses-sdk tools:overrideLibrary="org.kaliumjni.lib,org.apache.cordova" />\n$1'))
-
-          .pipe(gulp.dest(srcMainPath));
-
-        console.log('-----------------------------------------');
-      }
-
-    }
-  }
-}
diff --git a/scripts/hooks/after_prepare/061_copy_build_extras.js b/scripts/hooks/after_prepare/061_copy_build_extras.js
deleted file mode 100755
index 99d353d48228da80368f43bb3aa23018818bcef3..0000000000000000000000000000000000000000
--- a/scripts/hooks/after_prepare/061_copy_build_extras.js
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/usr/bin/env node
-
-const fs = require('fs'),
- glob = require('glob'),
- path = require('path'),
- log = require('fancy-log'),
- colors = require('ansi-colors');
-
-function mkdirp(dir) {
-  const parent = path.dirname(dir);
-  if (!fs.existsSync(parent)){
-    mkdirp(parent);
-  }
-  if (!fs.existsSync(dir)){
-    fs.mkdirSync(dir);
-  }
-}
-
-function copyFiles(src_dir, dest_dir) {
-  glob(src_dir + '/**/*.*', null, function(er, files) {
-    files.forEach(function(file) {
-      log(colors.grey(' Copy file ' + file + ' to ' + dest_dir));
-      const dest_file = file.replace(src_dir, dest_dir);
-      mkdirp(path.dirname(dest_file));
-      fs.copyFile(file, dest_file, (err) => {
-        if (err) {
-          log(colors.red(' ERROR: ' + err));
-          throw err;
-        }
-      });
-    });
-  });
-}
-
-// See: https://stackoverflow.com/questions/49162538/running-cordova-build-android-unable-to-find-attribute-androidfontvariation
-module.exports = function(context) {
-
-  const rootdir = context.opts.projectRoot;
-  const platforms = context.opts.platforms;
-
-  if (rootdir && platforms) {
-    // go through each of the platform directories that have been prepared
-    for (let x = 0; x < platforms.length; x++) {
-      try {
-        const platform = platforms[x].trim().toLowerCase();
-
-        if (platform === 'android') {
-          const gradle_dir = path.join(rootdir, 'gradle');
-          const buildRelativePath =  path.join('resources', 'android', 'build');
-          const build_dir = path.join(rootdir, buildRelativePath);
-          const android_dir = path.join(rootdir, 'platforms', 'android');
-
-          // Copy gradle files
-          if (fs.existsSync(gradle_dir)) {
-            copyFiles(gradle_dir, android_dir + '/gradle')
-          }
-
-          if (fs.existsSync(android_dir) && fs.existsSync(build_dir)) {
-
-            // Copy resources files
-            copyFiles(build_dir, android_dir);
-
-            // Copy signing stuff
-            const signing_file = build_dir + '/release-signing.properties';
-            if (!fs.existsSync(signing_file) && !fs.existsSync(android_dir + '/release-signing.properties')) {
-              log(colors.blue('WARNING: Missing file ' + buildRelativePath + '/release-signing.properties. Cannot copy it into ' + android_dir));
-              log(colors.blue('  Please create it manually at ' + android_dir + '/release-signing.properties'));
-              log(colors.blue('  otherwise release APK files will NOT be signed! '));
-            }
-
-          } else {
-            log(colors.red(' Directory ' + build_dir + 'not found. Skipping copy to ' + android_dir));
-          }
-        }
-      } catch (e) {
-        process.stdout.write(e);
-      }
-    }
-  }
-
-}
diff --git a/scripts/hooks/before_compile/060_prepare_android_manifest.js b/scripts/hooks/before_compile/060_prepare_android_manifest.js
deleted file mode 100755
index 27061d7b5cd42b8448864b892e88c1ad540ea0af..0000000000000000000000000000000000000000
--- a/scripts/hooks/before_compile/060_prepare_android_manifest.js
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/usr/bin/env node
-
-const gulp = require('gulp'),
-  path = require("path"),
-  replace = require('gulp-replace');
-
-module.exports = function(context) {
-  const rootdir = context.opts.projectRoot;
-  const platforms = context.opts.platforms;
-
-  if (rootdir && platforms) {
-
-    // go through each of the platform directories that have been prepared
-    for (let x = 0; x < platforms.length; x++) {
-
-      let platform = platforms[x].trim().toLowerCase();
-
-      if (platform === 'android') {
-        let srcMainPath = path.join(rootdir, 'platforms', platform, 'app/src/main');
-        let androidManifestFile = path.join(srcMainPath, 'AndroidManifest.xml');
-
-        // Clean unused directories
-        console.log('-----------------------------------------');
-        console.log(' Updating file: ' + androidManifestFile);
-        gulp.src(androidManifestFile)
-
-          // Add 'tools' namespace to root tag
-          .pipe(replace(/(xmlns:android="http:\/\/schemas.android.com\/apk\/res\/android")\s*>/g, '$1 xmlns:tools="http://schemas.android.com/tools">'))
-
-          // Add <application> (replace 'targetSdkversion' and add tools:overrideLibrary)
-          //.pipe(replace(/\s+tools:replace="android:appComponentFactory"/, ''))
-          //.pipe(replace(/\s+android:appComponentFactory="[^"]+"/, ''))
-          //.pipe(replace(/(\s*<application)\s*/, '$1 tools:replace="android:appComponentFactory" android:appComponentFactory="androidx.core.app.CoreComponentFactory" '))
-
-          // remove all <uses-sdk>
-          .pipe(replace(/<uses-sdk [^>]+\/>/g, ''))
-
-          // add <uses-sdk> (replace 'targetSdkversion' and add tools:overrideLibrary)
-          .pipe(replace(/(<\/manifest>)/, '    <uses-sdk tools:overrideLibrary="org.kaliumjni.lib,org.apache.cordova" />\n$1'))
-
-          .pipe(gulp.dest(srcMainPath));
-
-        console.log('-----------------------------------------');
-      }
-
-    }
-  }
-}
diff --git a/scripts/hooks/before_prepare.js b/scripts/hooks/before_prepare.js
new file mode 100644
index 0000000000000000000000000000000000000000..811e08822faeb44b3b136b9a33e0bd76021c9d44
--- /dev/null
+++ b/scripts/hooks/before_prepare.js
@@ -0,0 +1,20 @@
+#!/usr/bin/env node
+
+const log = require('fancy-log'),
+  colors = require('ansi-colors'),
+  jshint = require('../node/jshint-utils');
+
+module.exports = function(context) {
+  const now = Date.now();
+  log("Executing '" + colors.cyan("before_prepare") + "' hook...");
+
+  const projectRoot = context && context.opts && context.opts.projectRoot || '.';
+  const platforms = context && context.opts && context.opts.platforms || ['android'];
+  if (!projectRoot || !platforms) return; // Skip
+
+  // Run JS Lint
+  return jshint.validate(projectRoot)
+    .then(() => {
+      log(colors.grey("Hook 'before_prepare' finished in " + (Date.now() - now) + 'ms'));
+    });
+}
diff --git a/scripts/hooks/before_prepare/02_jshint.js b/scripts/hooks/before_prepare/02_jshint.js
deleted file mode 100755
index f2bac138147fb639f993a91202c1532af0f28534..0000000000000000000000000000000000000000
--- a/scripts/hooks/before_prepare/02_jshint.js
+++ /dev/null
@@ -1,111 +0,0 @@
-#!/usr/bin/env node
-
-const fs = require('fs'),
- path = require('path'),
- jshint = require('jshint').JSHINT,
- async = require('async'),
- log = require('fancy-log'),
- colors = require('ansi-colors'),
- glob = require("glob");
-
-
-function processFiles(dir) {
-  let errorCount = 0;
-  log(colors.grey('Processing folder ' + dir + '...'));
-  fs.readdir(dir, function(err, list) {
-    if (err) {
-      log(colors.red('processFiles err: ' + err));
-      return;
-    }
-    async.eachSeries(list, function(file, innercallback) {
-      file = dir + '/' + file;
-      log(colors.grey('Processing file ./' + file + '...'));
-      fs.stat(file, function(err, stat) {
-        if(!stat.isDirectory()) {
-          if(path.extname(file) === ".js") {
-            lintFile(file, function(hasError) {
-              if(hasError) {
-                errorCount++;
-              }
-              innercallback();
-            });
-          } else {
-            innercallback();
-          }
-        } else {
-          innercallback();
-        }
-      });
-    }, function(error) {
-      if(errorCount > 0) {
-        throw error;
-      }
-    });
-  });
-}
-
-function lintFile(file, callback) {
-  //log(colors.grey(`Linting ${colors.bold(file)}...`));
-  fs.readFile(file, (err, data) => {
-    if(err) {
-      log(colors.red('Error: ' + err));
-      return;
-    }
-    if(jshint(data.toString())) {
-      callback(false);
-    } else {
-      const out = jshint.data(),
-        errors = out.errors;
-      for(let j = 0; j < errors.length; j++) {
-        log(colors.red(`${colors.bold(file + ':' + errors[j].line + ':0' )} -> ${colors.bold(errors[j].evidence.trim())}`));
-        log(colors.red(` ${errors[j].reason}`));
-      }
-      log('-----------------------------------------');
-      callback(true);
-    }
-  });
-}
-
-function getJSFolder(rootDir) {
-  // Get folders, from files
-  const jsFolders =  glob.sync(rootDir + "/www/**/*.js", {nonull: true})
-    // Map to file's folder
-    .map(file => file.substring(0, file.lastIndexOf('/')))
-    // Reduce to a map of folders
-    .reduce((res, folder) => {
-      if (folder.indexOf('www/dist/') !== -1 || // Exclude dist js
-        folder.indexOf('/plugins/rml') !== -1 || // Exclude plugin tutorial
-        folder.indexOf('www/js/vendor') !== -1 || // exclude vendor libs
-        folder.indexOf('www/lib') !== -1 // exclude www/lib
-      ) {
-        return res;
-      }
-      res[folder] = res[folder] || true;
-      return res;
-    }, {});
-  return Object.keys(jsFolders);
-}
-
-
-module.exports = function(context) {
-  const rootdir = context.opts.projectRoot;
-
-  if (rootdir) {
-    const errors = [];
-
-    // Process each folder with Js file
-    getJSFolder(rootdir).forEach(folder => {
-      try {
-        processFiles(folder)
-      } catch (err) {
-        errors.push(err);
-      }
-    });
-
-    if (errors.length) {
-      log(colors.red(`Some JS files have errors`));
-      process.exit(1);
-    }
-  }
-
-}
diff --git a/scripts/node/jshint-utils.js b/scripts/node/jshint-utils.js
new file mode 100755
index 0000000000000000000000000000000000000000..7a591aca5d4c0e2e5bd369a2e649ebebfa81fb62
--- /dev/null
+++ b/scripts/node/jshint-utils.js
@@ -0,0 +1,100 @@
+'use strict';
+const fs = require('fs'),
+ path = require('path'),
+ jshint = require('jshint').JSHINT,
+ log = require('fancy-log'),
+ colors = require('ansi-colors'),
+ glob = require("glob");
+
+async function lintFolder(dir) {
+  log(colors.grey('Processing folder ' + dir + '...'));
+  const files = fs.readdirSync(dir);
+  return Promise.all(files.map(file => {
+    file = dir + '/' + file;
+    return new Promise((resolve, reject) => {
+      const stat = fs.statSync(file);
+      if (stat.isDirectory() || path.extname(file) !== ".js") return resolve(); // Skip
+      return lintFile(file).then(resolve).catch(reject);
+    })
+  }));
+}
+
+function lintFile(file) {
+  return new Promise((resolve, reject) => {
+    log(colors.grey('Processing file ./' + file + '...'));
+    fs.readFile(file, (err, data) => {
+      if(err) {
+        log(colors.red('Error: ' + err));
+        reject(err);
+        return;
+      }
+      if(jshint(data.toString())) {
+        resolve();
+      } else {
+        const out = jshint.data(),
+          errors = out.errors;
+        for(let j = 0; j < errors.length; j++) {
+          log(colors.red(`${colors.bold(file + ':' + errors[j].line + ':0' )} -> ${colors.bold(errors[j].evidence.trim())}`));
+          log(colors.red(` ${errors[j].reason}`));
+        }
+        log('-----------------------------------------');
+        reject();
+      }
+    });
+  });
+
+}
+
+function getJSFolder(rootDir) {
+  // Get folders, from files
+  const jsFolders =  glob.sync(rootDir + "/www/**/*.js", {nonull: true})
+    // Map to file's folder
+    .map(file => file.substring(0, file.lastIndexOf('/')))
+    // Reduce to a map of folders
+    .reduce((res, folder) => {
+      if (folder.indexOf('www/dist/') !== -1 || // Exclude dist js
+        folder.indexOf('/plugins/rml') !== -1 || // Exclude plugin tutorial
+        folder.indexOf('www/js/vendor') !== -1 || // exclude vendor libs
+        folder.indexOf('www/lib') !== -1 // exclude www/lib
+      ) {
+        return res;
+      }
+      res[folder] = res[folder] || true;
+      return res;
+    }, {});
+  return Object.keys(jsFolders);
+}
+
+
+function validate(projectRoot) {
+
+  projectRoot = projectRoot || '.';
+
+  const now = Date.now();
+  log(colors.green('Linting JS files... ' + projectRoot));
+
+  const jsFolders = getJSFolder(projectRoot);
+
+  // Process each folder with Js file
+  return Promise.all(
+    jsFolders.map(folder => lintFolder(folder))
+  )
+    .catch(err => {
+      console.log(err);
+      log(colors.red(`Some JS files have errors`));
+      process.exit(1);
+      throw err;
+    })
+    .then(() => {
+      // Success message
+      log(colors.grey('Linting JS files finished in ' + (Date.now() - now) + 'ms'));
+    });
+
+}
+
+/* --------------------------------------------------------------------------
+   -- Define public function
+   --------------------------------------------------------------------------*/
+
+exports.validate = validate;
+
diff --git a/scripts/node/jshint.js b/scripts/node/jshint.js
new file mode 100755
index 0000000000000000000000000000000000000000..1f4ef9015e5b75e78a8fe54bdd2f308bf7364707
--- /dev/null
+++ b/scripts/node/jshint.js
@@ -0,0 +1,6 @@
+#!/usr/bin/env node
+
+const jshint = require('./jshint-utils');
+
+jshint.validate();
+
diff --git a/scripts/run-android.sh b/scripts/run-android.sh
index 7ccd00625643e28caf80c06ec1a591e72062026d..0a87279875cd416650c6440e9e91de8a2418fbaa 100755
--- a/scripts/run-android.sh
+++ b/scripts/run-android.sh
@@ -15,13 +15,17 @@ cd ${PROJECT_DIR}
 
 # Run the build
 echo "Building Android application..."
-ionic cordova build android --warning-mode=none --verbose --color $*
+ionic cordova build android --warning-mode=none --color $*
 [[ $? -ne 0 ]] && exit 1
 
 echo "Running Android application..."
 if [[ "$1" == "--release" ]]; then
-  native-run android --app ${ANDROID_OUTPUT_APK_RELEASE}/app-release.apk
+  if [[ -f ${ANDROID_OUTPUT_APK_RELEASE}/app-release.apk ]]; then
+    native-run android --app ${ANDROID_OUTPUT_APK_RELEASE}/app-release-unsigned.apk
+  elif [[ -f ${ANDROID_OUTPUT_APK_RELEASE}/app-release.apk ]]; then
+    native-run android --app ${ANDROID_OUTPUT_APK_RELEASE}/app-release-unsigned.apk
+  fi
 else
-  native-run android --app ${ANDROID_OUTPUT_APK_DEBUG}/app-debug.apk
+  native-run android --app ${ls }/app-debug.apk
 fi
 
diff --git a/www/manifest.json b/www/manifest.json
index 7969871fe6f0206a12dc5d444460656a52ef3c6f..2fb9d5a172014fd3844650a5f8a7fa87ccd7a65a 100644
--- a/www/manifest.json
+++ b/www/manifest.json
@@ -31,5 +31,17 @@
   "theme_color": "black",
   "dir": "ltr",
   "start_url": "/#/app/home",
-  "display": "standalone"
+  "display": "standalone",
+  "protocol_handlers": [
+    {
+      "protocol": "june",
+      "name": "Cesium",
+      "uriTemplate": "/#/app/home?uri=%s"
+    },
+    {
+      "protocol": "web+june",
+      "name": "Cesium",
+      "uriTemplate": "/#/app/home?uri=%s"
+    }
+  ]
 }