The service is pretty straightforward. Call Bootstrap.check() and if there’s an update it will prompt the user (once per app load) to download the update. If they choose OK, it does the download and then applies the update and reloads.
.factory('Bootstrap', function($ionicPopup, $rootScope, Toast, LoadingService) {
var checked = false;
return {
reset: function() {
checked = false;
},
hasBeenChecked: function() {
return checked;
},
check: function() {
// stuff for hot code push.
// only do one check.
if (checked) {
return;
}
checked = true;
// Initialize filesystem and loader
var fs = new CordovaPromiseFS({
persistent: $rootScope.vars.isCordovaApp, // Chrome should use temporary storage.
Promise: Promise
});
var loader = new CordovaAppLoader({
fs: fs,
localRoot: 'app',
serverRoot: $rootScope.vars.baseURL + 'webapp/',
mode: 'mirror',
cacheBuster: true,
checkTimeout: 10000
});
loader.check().then(
function (updateAvailable) {
if (updateAvailable) {
var confirmPopup = $ionicPopup.confirm({
title: 'Update Available!',
template: "<div align='center'>There's a new mini-update available! Get it now?</div>"
});
confirmPopup.then(function(res) {
if (res) {
LoadingService.show ("Downloading the update!");
loader.download().then(
function(manifest) {
loader.update().then(
function() {
LoadingService.hide();
Toast.show("App updated!");
},
function() {
LoadingService.hide();
Toast.show ("Couldn't apply the update, try again later!");
}
);
},
function(failedDownloadUrlArray) {
LoadingService.hide();
Toast.show ("Couldn't download the update, try again later!");
}
);
}
else {
// user has said not to update. do nothing.
Toast.show ("OK! We'll try again later...");
}
});
}
else {
// no update available, do nothing.
console.log ("NO UPDATE AVAILABLE");
}
},
function () {
// some problem, fail silently.
}
);
}
};
})
For Grunt:
I just took the Grunt code from here: https://gist.github.com/lylepratt/d8bf84b3b7d6932e3549 and integrated it into my existing Grunt script (which was from an old version of the Ionic Yeoman Generator: https://github.com/diegonetto/generator-ionic) to automatically create the manifest.json in my www folder. Then in my index.html file, where usemin was being used to concat/uglify/minify the js and css into a single file, I needed it to not write the result into index.html, since all those files are now loaded by the bootstrap.js loader.
So index.html now has sections that look like:
<!-- build:myjs scripts/scripts.js -->
<script src="scripts/config.js"></script>
<script src="scripts/app.js"></script>
<script src="scripts/controllers.js"></script>
<script src="scripts/services.js"></script>
<!-- endbuild -->
That use the “myjs” block replacement instead of the standard js one.
It also loads the bootstrap.js file:
<!-- bootstrap script (this will fail to load manifest.json during development) -->
<script type="text/javascript" timeout="10000" manifest="manifest.json" src="bootstrap/bootstrap.js"></script>
All the Cordova-app-loader files (bootstrap.js, cordova-app-loader-complete.min.js) get loaded from the new “bootstrap” directory.
My entire gruntfile is below. Note that I have no idea how most of it works 
// Generated on 2014-09-05 using generator-ionic 0.5.3
'use strict';
var _ = require('lodash');
var path = require('path');
var cordova = require('cordova');
var spawn = require('child_process').spawn;
module.exports = function (grunt) {
// Load grunt tasks automatically
require('load-grunt-tasks')(grunt);
// Time how long tasks take. Can help when optimizing build times
require('time-grunt')(grunt);
// Define the configuration for all the tasks
grunt.initConfig({
// Project settings
yeoman: {
// configurable paths
app: 'app',
scripts: 'scripts',
styles: 'styles',
images: 'images'
},
// Environment Variables for Angular App
// This creates an Angular Module that can be injected via ENV
// Add any desired constants to the ENV objects below.
// https://github.com/diegonetto/generator-ionic#environment-specific-configuration
ngconstant: {
options: {
space: ' ',
wrap: '"use strict";\n\n {%= __ngModule %}',
name: 'config',
dest: '<%= yeoman.app %>/scripts/config.js'
},
development: {
constants: {
ENV: {
name: 'development',
apiEndpoint: 'http://dev.yoursite.com:10000/'
}
}
},
production: {
constants: {
ENV: {
name: 'production',
apiEndpoint: 'http://api.yoursite.com/'
}
}
}
},
// Watches files for changes and runs tasks based on the changed files
watch: {
bower: {
files: ['bower.json'],
tasks: ['wiredep']
},
js: {
files: ['<%= yeoman.app %>/<%= yeoman.scripts %>/**/*.js'],
tasks: ['newer:jshint:all'],
options: {
livereload: true
}
},
styles: {
files: ['<%= yeoman.app %>/<%= yeoman.styles %>/**/*.css'],
tasks: ['newer:copy:styles', 'autoprefixer']
},
livereload: {
options: {
livereload: '<%= connect.options.livereload %>'
},
files: [
'<%= yeoman.app %>/*.html',
'<%= yeoman.app %>/templates/**/*.html',
'.tmp/<%= yeoman.styles %>/**/*.css',
'<%= yeoman.app %>/<%= yeoman.images %>/**/*.{png,jpg,jpeg,gif,webp,svg,caf}'
]
}
},
// The actual grunt server settings
connect: {
options: {
port: 9000,
// Change this to '0.0.0.0' to access the server from outside.
hostname: 'localhost',
livereload: 35729
},
livereload: {
options: {
open: true,
base: [
'.tmp',
'<%= yeoman.app %>'
]
}
},
dist: {
options: {
base: 'www'
}
},
coverage: {
options: {
port: 9002,
open: true,
base: ['coverage']
}
}
},
// Make sure code styles are up to par and there are no obvious mistakes
jshint: {
options: {
jshintrc: '.jshintrc',
reporter: require('jshint-stylish')
},
all: [
'Gruntfile.js',
'<%= yeoman.app %>/<%= yeoman.scripts %>/**/*.js'
],
test: {
options: {
jshintrc: 'test/.jshintrc'
},
src: ['test/unit/**/*.js']
}
},
// Empties folders to start fresh
clean: {
dist: {
files: [{
dot: true,
src: [
'.tmp',
'www/*',
'!www/.git*'
]
}]
},
server: '.tmp'
},
autoprefixer: {
options: {
browsers: ['last 1 version']
},
dist: {
files: [{
expand: true,
cwd: '.tmp/<%= yeoman.styles %>/',
src: '{,*/}*.css',
dest: '.tmp/<%= yeoman.styles %>/'
}]
}
},
// Automatically inject Bower components into the app
wiredep: {
options: {
cwd: '<%= yeoman.app %>'
},
app: {
src: ['<%= yeoman.app %>/index.html'],
ignorePath: /\.\.\//
}
},
// Reads HTML for usemin blocks to enable smart builds that automatically
// concat, minify and revision files. Creates configurations in memory so
// additional tasks can operate on them
useminPrepare: {
html: '<%= yeoman.app %>/index.html',
options: {
dest: 'www',
flow: {
html: {
steps: {
js: ['concat', 'uglifyjs'],
css: ['cssmin'],
myjs: ['concat', 'uglifyjs'],
mycss: ['cssmin']
},
post: {}
}
}
}
},
// Performs rewrites based on the useminPrepare configuration
usemin: {
html: ['www/**/*.html'],
css: ['www/<%= yeoman.styles %>/**/*.css'],
options: {
assetsDirs: ['www'],
blockReplacements: {
myjs: function(block) {
return '';
},
mycss: function(block) {
return '';
}
}
}
},
// The following *-min tasks produce minified files in the dist folder
cssmin: {
options: {
root: '<%= yeoman.app %>',
noRebase: true
}
},
htmlmin: {
dist: {
options: {
collapseWhitespace: true,
collapseBooleanAttributes: true,
removeCommentsFromCDATA: true,
removeOptionalTags: true
},
files: [{
expand: true,
cwd: 'www',
src: ['*.html', 'templates/**/*.html'],
dest: 'www'
}]
}
},
// Copies remaining files to places other tasks can use
copy: {
dist: {
files: [{
expand: true,
dot: true,
cwd: '<%= yeoman.app %>',
dest: 'www',
src: [
'images/**/*.{png,jpg,jpeg,gif,webp,svg,caf}',
'*.html',
'templates/**/*.html',
'fonts/*',
'bootstrap/*'
]
}, {
expand: true,
cwd: '.tmp/<%= yeoman.images %>',
dest: 'www/<%= yeoman.images %>',
src: ['generated/*']
}]
},
styles: {
expand: true,
cwd: '<%= yeoman.app %>/<%= yeoman.styles %>',
dest: '.tmp/<%= yeoman.styles %>/',
src: '{,*/}*.css'
},
fonts: {
expand: true,
cwd: 'app/bower_components/ionic/release/fonts/',
dest: '<%= yeoman.app %>/fonts/',
src: '*'
},
vendor: {
expand: true,
cwd: '<%= yeoman.app %>/vendor',
dest: '.tmp/<%= yeoman.styles %>/',
src: '{,*/}*.css'
},
all: {
expand: true,
cwd: '<%= yeoman.app %>/',
src: '**',
dest: 'www/'
}
},
concurrent: {
server: [
'copy:styles',
'copy:vendor',
'copy:fonts'
],
test: [
'copy:styles',
'copy:vendor',
'copy:fonts'
],
dist: [
'copy:styles',
'copy:vendor',
'copy:fonts'
]
},
// By default, your `index.html`'s <!-- Usemin block --> will take care of
// minification. These next options are pre-configured if you do not wish
// to use the Usemin blocks.
// cssmin: {
// dist: {
// files: {
// 'www/<%= yeoman.styles %>/main.css': [
// '.tmp/<%= yeoman.styles %>/**/*.css',
// '<%= yeoman.app %>/<%= yeoman.styles %>/**/*.css'
// ]
// }
// }
// },
// uglify: {
// dist: {
// files: {
// 'www/<%= yeoman.scripts %>/scripts.js': [
// 'www/<%= yeoman.scripts %>/scripts.js'
// ]
// }
// }
// },
// concat: {
// dist: {}
// },
// Test settings
// These will override any config options in karma.conf.js if you create it.
karma: {
options: {
basePath: '',
frameworks: ['mocha', 'chai'],
files: [
'<%= yeoman.app %>/bower_components/angular/angular.js',
'<%= yeoman.app %>/bower_components/angular-animate/angular-animate.js',
'<%= yeoman.app %>/bower_components/angular-sanitize/angular-sanitize.js',
'<%= yeoman.app %>/bower_components/angular-ui-router/release/angular-ui-router.js',
'<%= yeoman.app %>/bower_components/ionic/release/js/ionic.js',
'<%= yeoman.app %>/bower_components/ionic/release/js/ionic-angular.js',
'<%= yeoman.app %>/bower_components/angular-mocks/angular-mocks.js',
'<%= yeoman.app %>/<%= yeoman.scripts %>/**/*.js',
'test/mock/**/*.js',
'test/spec/**/*.js'
],
autoWatch: false,
reporters: ['dots', 'coverage'],
port: 8080,
singleRun: false,
preprocessors: {
// Update this if you change the yeoman config path
'app/scripts/**/*.js': ['coverage']
},
coverageReporter: {
reporters: [
{ type: 'html', dir: 'coverage/' },
{ type: 'text-summary' }
]
}
},
unit: {
// Change this to 'Chrome', 'Firefox', etc. Note that you will need
// to install a karma launcher plugin for browsers other than Chrome.
browsers: ['PhantomJS'],
background: true
},
continuous: {
browsers: ['PhantomJS'],
singleRun: true,
}
},
// ngAnnotate tries to make the code safe for minification automatically by
// using the Angular long form for dependency injection.
ngAnnotate: {
dist: {
files: [{
expand: true,
cwd: '.tmp/concat/<%= yeoman.scripts %>',
src: '*.js',
dest: '.tmp/concat/<%= yeoman.scripts %>'
}]
}
},
//jsonmanifest settings
jsonmanifest: {
generate: {
options: {
basePath: 'www',
exclude: [],
//load all found assets
loadall: false,
//manually add files to the manifest
files: {},
//manually define the files that should be injected into the page
load: ["bootstrap/cordova-app-loader-complete.min.js", "styles/vendor.css", "styles/main.css", "scripts/vendor.js", "scripts/scripts.js"],
// root location of files to be loaded in the load array.
root: "./"
},
src: [
'bootstrap/*.js',
'scripts/*.js',
'styles/*.css',
'templates/*.html',
'fonts/*.*',
'images/*.*'
],
dest: ['www/manifest.json']
}
}
});
// Register tasks for all Cordova commands, but namespace
// the cordova:build since we already have a build task.
_.functions(cordova).forEach(function (name) {
name = (name === 'build') ? 'cordova:build' : name;
grunt.registerTask(name, function () {
this.args.unshift(name.replace('cordova:', ''));
// Handle URL's being split up by Grunt because of `:` characters
if (_.contains(this.args, 'http') || _.contains(this.args, 'https')) {
this.args = this.args.slice(0, -2).concat(_.last(this.args, 2).join(':'));
}
var done = this.async();
var exec = process.platform === 'win32' ? 'cordova.cmd' : 'cordova';
var cmd = path.resolve('./node_modules/cordova/bin', exec);
var child = spawn(cmd, this.args);
child.stdout.on('data', function (data) {
grunt.log.writeln(data);
});
child.stderr.on('data', function (data) {
grunt.log.error(data);
});
child.on('close', function (code) {
code = (name === 'cordova:build') ? true : code ? false : true;
done(code);
});
});
});
//GRUNT TASK TO BUILD A JSON MANIFEST FILE FOR HOT CODE UPDATES
grunt.registerMultiTask('jsonmanifest', 'Generate JSON Manifest for Hot Updates', function () {
var options = this.options({loadall:true, root: "./", files: {}, load: []});
var done = this.async();
var path = require('path');
this.files.forEach(function (file) {
var files;
//manifest format
var json = {
"files": options.files,
"load": options.load,
"root": options.root
};
//clear load array if loading all found assets
if(options.loadall) {
json.load = [];
}
// check to see if src has been set
if (typeof file.src === "undefined") {
grunt.fatal('Need to specify which files to include in the json manifest.', 2);
}
// if a basePath is set, expand using the original file pattern
if (options.basePath) {
files = grunt.file.expand({cwd: options.basePath}, file.orig.src);
} else {
files = file.src;
}
// Exclude files
if (options.exclude) {
files = files.filter(function (item) {
return options.exclude.indexOf(item) === -1;
});
}
// Set default destination file
if (!file.dest) {
file.dest = ['manifest.json'];
}
// add files
if (files) {
files.forEach(function (item) {
var hasher = require('crypto').createHash('sha256');
var filename = encodeURI(item);
//var key = filename.split("-").slice(1).join('-');
var key = filename.split("/");
key = key[key.length -1];
json.files[key] = {}
json.files[key]['filename'] = filename;
json.files[key]['version'] = hasher.update(grunt.file.read(path.join(options.basePath, item))).digest("hex")
if(options.loadall) {
json.load.push(filename);
}
});
}
//write out the JSON to the manifest files
file.dest.forEach(function(f) {
grunt.file.write(f, JSON.stringify(json, null, 2));
});
done();
});
});
// Since Apache Ripple serves assets directly out of their respective platform
// directories, we watch all registered files and then copy all un-built assets
// over to www/. Last step is running cordova prepare so we can refresh the ripple
// browser tab to see the changes. Technically ripple runs `cordova prepare` on browser
// refreshes, but at this time you would need to re-run the emulator to see changes.
grunt.registerTask('ripple', ['wiredep', 'copy:all', 'ripple-emulator']);
grunt.registerTask('ripple-emulator', function () {
grunt.config.set('watch', {
all: {
files: _.flatten(_.pluck(grunt.config.get('watch'), 'files')),
tasks: ['copy:all', 'prepare']
}
});
var cmd = path.resolve('./node_modules/ripple-emulator/bin', 'ripple');
var child = spawn(cmd, ['emulate']);
child.stdout.on('data', function (data) {
grunt.log.writeln(data);
});
child.stderr.on('data', function (data) {
grunt.log.error(data);
});
process.on('exit', function (code) {
child.kill('SIGINT');
process.exit(code);
});
return grunt.task.run(['watch']);
});
// Dynamically configure `karma` target of `watch` task so that
// we don't have to run the karma test server as part of `grunt serve`
grunt.registerTask('watch:karma', function () {
var karma = {
files: ['<%= yeoman.app %>/<%= yeoman.scripts %>/**/*.js', 'test/spec/**/*.js'],
tasks: ['newer:jshint:test', 'karma:unit:run']
};
grunt.config.set('watch', karma);
return grunt.task.run(['watch']);
});
grunt.registerTask('serve', function (target) {
if (target === 'dist') {
return grunt.task.run(['build', 'connect:dist:keepalive']);
}
grunt.task.run([
'clean:server',
'wiredep',
'concurrent:server',
'autoprefixer',
'connect:livereload',
'watch'
]);
});
grunt.registerTask('test', [
'clean:server',
'concurrent:test',
'autoprefixer',
'karma:unit:start',
'watch:karma'
]);
grunt.registerTask('build', [
'clean:dist',
'wiredep',
'useminPrepare',
'concurrent:dist',
'autoprefixer',
'concat',
'ngAnnotate',
'copy:dist',
'cssmin',
'uglify',
'usemin',
'htmlmin',
'cordova:build'
]);
grunt.registerTask('buildweb', [
'clean:dist',
'wiredep',
'useminPrepare',
'concurrent:dist',
'autoprefixer',
'concat',
'ngAnnotate',
'copy:dist',
'cssmin',
'uglify',
'usemin',
'htmlmin',
'jsonmanifest',
'prepare'
]);
grunt.registerTask('cordova', ['copy:all', 'cordova:build']);
grunt.registerTask('web', ['copy:all']);
grunt.registerTask('android', ['copy:all', 'cordova:run:android']);
grunt.registerTask('coverage', ['karma:continuous', 'connect:coverage:keepalive']);
grunt.registerTask('default', [
'buildweb'
]);
/*
grunt.registerTask('default', [
'newer:jshint',
'karma:continuous',
'build'
]);
*/
};