bootloader.js 11.6 KB
/*
 * ADOBE CONFIDENTIAL
 *
 * Copyright 2015 Adobe Systems Incorporated
 * All Rights Reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any.  The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated and its
 * suppliers and may be covered by U.S. and Foreign Patents,
 * patents in process, and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 */

/* globals ContentSync, ES6Promise, cordova */
ES6Promise.polyfill();

/**
 * Bootloader script that is used to get or create the local copy of the 'www' directory of the firmware.
 * It automatically changes the window.location to the location where the firmware is loaded.
 *
 * @module bootloader
 */
(function(window) {
    'use strict';

    /**
     * Name of the content sync package.
     * @type {string}
     */
    var CONTENT_SYNC_PACKAGE_NAME = 'firmware';

    /**
     * Name of the app id to use for content sync
     * @type {string}
     */
    var CONTENT_SYNC_APP_ID = CONTENT_SYNC_PACKAGE_NAME + '/www';

    /**
     * the parent directory for the start page, relative to the 'sync' directory
     * @type {string}
     */
    var ROOT_DIRECTORY_PATH = '/libs/screens/player/content';

    /**
     * name of the file that contains the 'firmware' timestamp.
     * @type {string}
     */
    var TIMESTAMP_FILENAME = 'pge-package-update.json';

    /**
     * Name of the default start page
     * @type {string}
     */
    var DEFAULT_START_PAGE = 'firmware.html';

    /*
     * Name of session storage key that may contain the url set by the handleOpenURL method
     */
    var URLSCHEME_URL_STORAGE_KEY = 'com.adobe.cq.screens.player.urlscheme.url';

    // todo: change rendition based on desired resolution
    // var DEFAULT_START_PAGE = 'firmware.1920x1080.html';

    /**
     * Cordova FileSystem Utilities
     *
     * todo: use the one in shared/fs.js
     */
    var fs = {

        /**
         * Resolves the local file system url.
         * @param {String} url URL to the resource
         * @returns {Promise} A promise that resolves to the file or directory entry.
         */
        resolveLocalFileSystemURL: function(url) {
            return new Promise(function(resolve, reject) {
                console.log('resolveLocalFileSystemURL for ', url);
                window.resolveLocalFileSystemURL(url,
                    function(dir) {
                        console.log('resolveLocalFileSystemURL.success', dir.name);
                        resolve(dir);
                    }, function(error) {
                        console.log('resolveLocalFileSystemURL.error', error.code);
                        reject(error);
                    });
            });
        },

        /**
         * Retrieves the file relative to the given directory.
         * @param {DirectoryEntry} directory The Directory entry
         * @param {String} path path to the file
         * @returns {Promise} A promise that resolves to the file entry
         */
        getFileEntry: function(directory, path) {
            return new Promise(function(resolve, reject) {
                console.log('getFileEntry ', directory, path);
                directory.getFile(path, {create: false},
                    function(file) {
                        console.log('getFileEntry.success', file);
                        resolve(file);
                    }, function(error) {
                        console.log('getFileEntry.error', error);
                        reject(error);
                    });
            });
        },

        /**
         * Retrieves the file object that represents the current state of the file that the FileEntry represents.
         * @param {FileEntry} fileEntry The file entry
         * @returns {Promise} A promise that resolves to the file
         */
        getFile: function(fileEntry) {
            return new Promise(function(resolve, reject) {
                return fileEntry.file(resolve, reject);
            });
        },

        /**
         * Reads the given file as test.
         * @param {File} file The file
         * @returns {Promise} A promise that resolves to the text of the file
         */
        readFileAsText: function(file) {
            return new Promise(function(resolve, reject) {
                console.log('fileEntry.file.success ' + file);
                var reader = new FileReader();

                reader.onerror = reader.onabort = reject;
                reader.onload = function() {
                    console.log('readFileEntryAsText.success ' + this.result);
                    resolve(this.result);
                };
                reader.readAsText(file);
            });
        },

        /**
         * Deletes a directory and all of its contents. In the event of an error (such as trying to delete a directory
         * containing a file that can't be removed), some of the contents of the directory may be deleted.
         *
         * @param {DirectoryEntry} dirEntry The directory entry to remove
         *
         * @returns {Promise} A promise to remove the directory recursively
         */
        removeRecursively: function(dirEntry) {
            return new Promise(function(resolve, reject) {
                dirEntry.removeRecursively(function() {
                    console.log('removeRecursively.success', dirEntry);
                    resolve();
                }, function(e) {
                    console.log('removeRecursively.failed', e);
                    reject(e);
                });
            });
        }
    };


    /**
     * Returns a promise that resolves to the URL of the current start page.
     *
     * @returns {Promise} A promise to resolves the URL of the current start page.
     */
    function currentStartPage() {

        var syncRootURL;

        /**
         * Invokes the content-sync plugin in order to retrieve the location of the current copy. It copies
         * the original 'www' files if required.
         *
         * @returns {Promise} A promise that resolves to the local root URL.
         */
        function syncContent() {
            return new Promise(function(resolve, reject) {
                var sync = ContentSync.sync({
                    type: 'local',
                    id: CONTENT_SYNC_APP_ID,
                    // copyCordovaAssets: true,
                    copyRootApp: true
                });
                sync.on('complete', function(data) {
                    console.log('sync.complete', data);
                    syncRootURL = 'file://' + data.localPath;
                    resolve(syncRootURL);
                });
                sync.on('error', function(error) {
                    console.log('sync.error', error);
                    syncRootURL = '';
                    reject(error);
                });
            });
        }

        /**
         * Retrieves the firmware root directory entry.
         * @param {String} path the local content sync root path
         * @returns {Promise} A promise that resolves to the directory entry.
         */
        function getRootDirectory(path) {
            return fs.resolveLocalFileSystemURL(path + ROOT_DIRECTORY_PATH);
        }

        /**
         * Retrieves the 'firmware.html' file entry.
         * @param {DirectoryEntry} rootDir The root directory entry
         * @returns {Promise} A promise that resolves to the file entry
         */
        function getFirmwareHTML(rootDir) {
            return fs.getFileEntry(rootDir, DEFAULT_START_PAGE);
        }

        /**
         * Retrieves the native path of the file entry
         * @param {FileEntry} fileEntry The file entry
         * @returns {String} The Url of the file entry
         */
        function getNativePath(fileEntry) {
            console.log('getNativePath.success', fileEntry.toURL());
            return fileEntry.toURL();
        }

        /**
         * Returns the last modified timestamp stored in the json file addressed by URL
         * @param {String} url the url to the json file
         * @returns {Promise} A promise that resolves to the timestamp.
         */
        function getLastModified(url) {
            return fs.resolveLocalFileSystemURL(url)
                .then(fs.getFile)
                .then(fs.readFileAsText)
                .then(function(txt) {
                    var data = JSON.parse(txt);
                    console.log('timestamp contents for ' + url + ' is ', data);
                    return data.lastModified;
                }).catch(function(e) {
                    console.log('error reading timestamp file.', e);
                    return Promise.reject(e);
                });
        }

        /**
         * Returns a promise that appends the given suffix to the input value.
         * @param {String} suffix Suffix to add.
         * @returns {Promise} that resolves by adding the suffix to the input value
         */
        function append(suffix) {
            return function(source) {
                return source + suffix;
            };
        }

        // get timestamps
        var getTS1 = getLastModified(cordova.file.applicationDirectory + '/www/' + TIMESTAMP_FILENAME);
        var getTS2 = syncContent().then(append('/' + TIMESTAMP_FILENAME)).then(getLastModified);

        return Promise.all([getTS1, getTS2])
            .then(function(values) {
                var useBundled = values[0] > values[1];
                console.log('bundled timestamp ' + values[0] + (useBundled ? ' > ' : ' < ') + ' synced timestamp ' + values[1]);

                if (useBundled) {
                    // force re-sync. currently we need to delete the existing synced root and start again
                    return fs.resolveLocalFileSystemURL(syncRootURL)
                        .then(fs.removeRecursively)
                        .then(syncContent);
                }
                return syncRootURL;
            })
            .then(getRootDirectory)
            .then(getFirmwareHTML)
            .then(getNativePath);
    }

    // --------------------------------------------------------------
    if (window.cordova) {

        // START: handleOpenURL plugin handling
        // if handleOpenURL call happens in the bootloader phase, we just set the url in the sessionStorage
        // for further treatment
        window.sessionStorage.removeItem(URLSCHEME_URL_STORAGE_KEY);
        window.handleOpenURL = function(url) {
            window.sessionStorage.setItem(URLSCHEME_URL_STORAGE_KEY, url);
        };
        // END: handleOpenURL plugin handling

        document.addEventListener('deviceready', function() {
            var currentLocation = window.location.href;
            currentStartPage()
                .then(function(newLocation) {
                    console.log(currentLocation);
                    console.log(newLocation);
                    if (currentLocation !== newLocation) {
                        window.location.href = newLocation;
                    }
                })
                .catch(function(e) {
                    console.log('sync failed or start page not found. using default.', e);
                    window.location.href = './' + ROOT_DIRECTORY_PATH + '/' + DEFAULT_START_PAGE;
                }
            );
        });

    } else {
        window.document.writeln('<p style="color:red">Bootloader only useful in cordova environment...</p>');
        console.error('No cordova detected. Bootloader only useful in cordova environment.');
    }

}(window));