UNPKG

ares-ide

Version:

A browser-based code editor and UI designer for Enyo 2 projects

1,255 lines (1,122 loc) 40.5 kB
/*global enyo, ares, async, Ares, Phonegap, XMLWriter, ServiceRegistry, ComponentsRegistry, next */ /** * Kind to manage the life cycle of building a mobile application using * the service Phonegap build. */ enyo.kind({ name: "Phonegap.Build", kind: "enyo.Component", events: { onLoginFailed: "", onShowWaitPopup: "", onFsEvent: "" }, published: { checkBuildResultInterval: 3000 }, components: [ {kind: "Signals", "plugin.phonegap.buildCanceled": "abortAjaxRequest"}, ], debug: false, /** * @private */ create: function() { ares.setupTraceLogger(this); this.inherited(arguments); this.config = {}; }, /** * Set Phonegap.Build base parameters. * * This method is not expected to be called by anyone else but * {ServiceRegistry}. * @param {Object} inConfig * @see ServiceRegistry.js */ setConfig: function(inConfig) { this.trace("config:", this.config, "+", inConfig); this.config = ares.extend(this.config, inConfig); this.trace("=> config:", this.config); if (this.config.origin && this.config.pathname) { this.url = this.config.origin + this.config.pathname; this.trace("url:", this.url); } this.folderKey = "build." + this.config.id + ".target.folderId"; this.appKey = "build." + this.config.id + ".app"; }, /** * @return {Object} the configuration this service was configured by */ getConfig: function() { return this.config; }, /** * Return an instance of the current selected project from the project list UI. * @public */ getSelectedProject: function() { return Ares.Workspace.projects.getSelectedProject(); }, /** * @return the human-friendly name of this service */ getName: function() { return this.config.name || this.config.id; }, /** * Default configuration used when a new project is created. * The Project configuration is transformed into a config.xml * file at each build. It is later expected to be modified by * the UI kind returned by * {PhoneGap.Build#getProjectPropertiesKind}. * * @public */ getDefaultProjectConfig: function() { return ares.clone(Phonegap.Build.DEFAULT_PROJECT_CONFIG); }, /** * Name of the kind to show in the {ProjectProperties} UI * @return the Enyo kind to use to set service-specific project properties * @public */ getProjectPropertiesKind: function() { return "Phonegap.ProjectProperties"; }, /** * @return true when configured, authenticated & authorized */ isOk: function() { return !!(this.config && this.config.auth && this.config.auth.token && this.config.auth.keys); }, /** * Shared enyo.Ajax error handler * @private */ _handleServiceError: function(message, next, inSender, inError) { var response = inSender.xhrResponse, contentType, html, text; if (response) { contentType = response.headers['content-type']; if (contentType) { if (contentType.match('^text/plain')) { text = response.body; } if (contentType.match('^text/html')) { html = response.body; } } } if (inError && inError.statusCode === 401) { // invalidate token this.config.auth.token = null; ServiceRegistry.instance.setConfig(this.config.id, {auth: this.config.auth}); } var err = new Error(message + " (" + inError.toString() + ")"); err.html = html; err.text = text; err.status = response.status; next(err); }, /** * Authenticate current user & retreive the associated token * * If successful, #username, #password & the token are save to * the browser client localStorage. * * @param {Object} auth contains the properties #username and #password * @param {Function} next is a CommonJS callback * @public */ authenticate: function(inAuth, next) { this.trace(); this.config.auth = { username: inAuth.username, password: inAuth.password }; this._getToken(next); }, /** * Get the project instance that contain the data of "project.json" file of * a project. Also check if the the project is configured for Phonegap service. * @param {Object} project contain informations about the Ares project. * @private */ getConfigInstance: function(project){ if (!project instanceof Ares.Model.Project) { next(new Error("Invalid parameters")); return; } var config = project.getConfig().getData(); if(!config || !config.providers || !config.providers.phonegap) { next(new Error("Project not configured for Phonegap Build")); return; } return config; }, /** * Get the parameter AppId of the configuration object instance. * @param {Object} project contain informations about the Ares project. * @return {String} The AppId of the current project's application. * @private */ getConfigAppId: function(project){ var config = this.getConfigInstance(project); return config.providers.phonegap.appId; }, /** * Authorize & then retrieve information about the currently registered user * * This includes registered applications & signing keys. * @public * @param {Function} next * @param next {Error} err * @param next {Object} userData user account data as returned by PhoneGap Build */ authorize: function(next) { var self = this; this.trace(); // in case of user cancellation, getUserData may need to call next(error) // to interrupt the build process. same for getToken. this.getUserData(function(err, userData) { if (err) { self._getToken(function(err) { if (err) { next(err); } else { self.getUserData(next); } }, next); } else { next(null, userData); } }, next); }, /** * Check if the AppId indicated in the file "project.json" exist * in the Phonegap account before launching the submit build action. * @param {Object} project project contain informations about the Ares project. * @param {Object} userData contains detailed informations about the owner of Phonegap Build account. * @param {Function} next is a CommonJS callback * @private */ checkAppId: function (project, userData, next) { var projectAppId = this.getConfigAppId(project); var appIdExist = false; // immediately go on if appId is blank if (projectAppId.toString().length === 0) { next(null, userData); return; } enyo.forEach(userData.user.apps.all, function(appId){ // depending on the origin (project.json, Entry in // projectConfig UI, XML from phonegap), AppId can be // a string or an integer. So '==' is used instead of // '===' if (projectAppId == appId.id) { appIdExist = true; } }, this); if (appIdExist){ next(null, userData); } else { var config = this.getConfigInstance(project); config.providers.phonegap.appId = ""; ServiceRegistry.instance.setConfig(config); var errorMsg = "The AppId \'"+ projectAppId +"\' does not exist in the Phonegap Build account " + userData.user.email + ". Please choose a correct AppId"; next(errorMsg); } }, /** * Get a developer token from user's credentials * @param {Function} next is a CommonJS callback * @private */ _getToken: function(next, abort) { this.trace(); if(this.config.auth && this.config.auth.token) { this.trace("skipping token obtention"); next(); return; } // Pass credential information to get a phonegapbuild token var data = "username=" + encodeURIComponent(this.config.auth.username) + "&password=" + encodeURIComponent(this.config.auth.password); // Get a phonegapbuild token for the Hermes build service var req = new enyo.Ajax({ url: this.url + '/token', method: 'POST', postBody: data, xhrFields: { withCredentials: true} }); // Get ready for cancellation - (re)set abortAjaxRequest to stop "req" in case of cancellation this.abortAjaxRequest= function() { this.abortAjaxRequest= function() {}; enyo.xhr.cancel(req.xhr); abort (Phonegap.Build.abortBuild); }; req.response(this, function(inSender, inData) { this.config.auth.token = inData.token; this.trace("Got phonegap token:", this.config.auth.token); // store token ServiceRegistry.instance.setConfig(this.config.id, {auth: this.config.auth}); next(); }); req.error(this, this._handleServiceError.bind(this, "Unable to obtain PhoneGap security token", next)); req.go(); }, /** * Get a developer account information * @param {Function} next is a CommonJS callback * @private */ getUserData: function(next, abort) { this.trace(); var req = new enyo.Ajax({ url: this.url + '/api/v1/me', xhrFields: { withCredentials: true} }); // Get ready for possible cancellation - (re)set abortAjaxRequest to stop "req" in case of cancellation this.abortAjaxRequest= function() { this.abortAjaxRequest= function() {}; enyo.xhr.cancel(req.xhr); abort (Phonegap.Build.abortBuild); }; req.response(this, function(inSender, inData) { this.trace("inData: ", inData); this._storeUserData(inData.user); next(null, inData); }); req.error(this, this._handleServiceError.bind(this, "Unable to get PhoneGap user data", next)); req.go(); }, /** * Get the AppData for the selected project. * * @param {Object} project contain informations about the Ares project * @param {Object} inData contains detailed informations about the built * application on Phonegap * @param {Function} next is a CommonJS callback * @private */ getProjectAppData: function(project, next){ var config = project.getConfig().getData(); var appId = config.providers.phonegap.appId; this.getAppData(appId, next); }, /** * Send an AJAX request to Phonegap Build in order to get the application data. * * @param {String} inAppId Phonegap's application identifier * @param {Function} next CommonJS callback. * @private */ getAppData: function(inAppId, next){ var url = this.url + '/api/v1/apps/' + inAppId; //Creation of the Ajax request var req = new enyo.Ajax({ url: url, xhrFields: { withCredentials: true} }); //in case of sucess send the obtained JSON object to the next function //in the Async.waterfall. req.response(this, function(inSender, inData) { // activate the pop up to view the results next(null, inData); }); req.error(this, this._handleServiceError.bind(this, "Unable to get application build status", next)); req.go(); }, /** * Store relevant user account data * @param {Object} user the PhoneGap account user data * @private */ _storeUserData: function(user) { var keys = this.config.auth.keys || {}; enyo.forEach(enyo.keys(user.keys), function(target) { if (target !== 'link') { var newKeys, oldKeys = keys[target], inKeys = user.keys[target].all; newKeys = enyo.map(inKeys, function(inKey) { var oldKey, newKey; newKey = { id: inKey.id, title: inKey.title }; oldKey = enyo.filter(oldKeys, function(oldKey) { return oldKey && (oldKey.id === inKey.id); })[0]; return enyo.mixin(newKey, oldKey); }); keys[target] = newKeys; } }); // FIXME do not log 'auth' this.trace("keys:", keys); this.config.auth.keys = keys; ServiceRegistry.instance.setConfig(this.config.id, {auth: this.config.auth}); }, /** * Get the key for the given target & id, or the list of keys for the given target * * @param {String} target the build target, one of ['ios', 'android', ...etc] as defined by PhoneGap * @param {String} id the signing key id, as defined by PhoneGap * * @return If the key id is not provided, this method returns * an {Array} of keys available for the given platform. If * the given key id does not represent an existing key, this * method returns undefined. * * @public */ getKey: function(target, id) { var keys = this.config.auth.keys && this.config.auth.keys[target], res; if (id) { res = enyo.filter(keys, function(key) { return (key.id === id); }, this)[0]; } else { res = keys; } this.trace("target:", target, "id:", id, "=> keys:", res); return res; }, /** * Set the given signing key for the given platform * * Unlike the key {Object} stored on PhoneGap build (which * only has an #id and #title property), the given key is * expected to contain the necessary credentails properties * for the current platform (#password for 'ios' and * 'blackberry', #key_pw and #keystore_pw for 'android'). * * This method automatically saves the full signing keys in * the browser client localStorage. * * @param {String} target the PhoneGap build target * @param {Object} key the signing key with credential properties * @return {undefined} */ setKey: function(target, inKey) { var keys, key; if (( ! inKey) || typeof inKey.id !== 'number' || typeof inKey.title !== 'string') { this.warn("Will not store an invalid signing key:", inKey); return; } // Sanity this.config.auth.keys = this.config.auth.keys || {}; this.config.auth.keys[target] = this.config.auth.keys[target] || []; // Look for existing values keys = this.config.auth.keys && this.config.auth.keys[target]; key = enyo.filter(keys, function(key) { return (key.id === inKey.id); }, this)[0]; if (key) { enyo.mixin(key, inKey); } else { keys.push(inKey); } this.trace("target:", target, "keys:", keys /*XXX*/); this.config.auth.keys[target] = keys; // Save a new authentication values for PhoneGap ServiceRegistry.instance.setConfig(this.config.id, {auth: this.config.auth}); }, /** * Initiates the phonegap build of the given project * @see HermesBuild.js * * The following actions will be performed: * - Get a phonegapbuild account token * - Get the file list of the project * - Download all the project files * - Build a multipart/form-data with all the project data * - Send it to nodejs which will submit the build request * - Save the appid * * @param {Ares.Model.Project} project * @param {Function} next is a CommonJS callback * @public */ build: function(project, next) { this.trace("Starting phonegap build: ", this.url, '/build'); async.waterfall([ enyo.bind(this, this.authorize), enyo.bind(this, this.checkAppId, project), enyo.bind(this, this._updateConfigXml, project), enyo.bind(this, this._getFiles, project), enyo.bind(this, this._submitBuildRequest, project, next /*only called in case of success*/) ], function(err) { if (err) { if (err !== Phonegap.Build.abortBuild) { enyo.warn("phonegap.Build#build()", "err:", err); next(err); } } }); }, /** * Download a built application for a given platform. * * @param {Ares.Model.Project} project contains meta-data on the project related to the application to download. * @param {String} platform targeted mobile platform for which the application was built. * @param {[type]} inData contains detailed informations about the built application on Phonegap * @param {Function} next CommonJS callback * @private */ downloadPackage: function(project, platform, inData, next) { async.waterfall([ enyo.bind(this, this._prepareStore, project, inData, platform), enyo.bind(this, this._waitFetchStore, project) ], next); }, /** * Collect & check information about current project, update config.xml * @param {Object} project * @param {Object} userData is passed by checkAppId() in waterfall * @param {Function} next is a CommonJS callback * @private */ _updateConfigXml: function(project, userData, next) { var config = this.getConfigInstance(project); if (config.providers.phonegap.autoGenerateXML) { var configXml = this._generateConfigXml(config); if (!configXml) { this.error("unable to generate config.xml from:", config); next(); } else { var req, aborted, fs; fs = project.getService(); aborted = false; req = fs.createFile(project.getFolderId(), "config.xml", configXml, { overwrite: true }); this.abortAjaxRequest= function() { this.abortAjaxRequest= function() {}; // in case of cancellation, we should let the update complete to prevent // file corruption but still error out to prevent a successful // continuation of the process aborted = true; }; req.response(this, function _savedConfigXml(inSender, inData) { this.trace("Phonegap.Build#_updateConfigXml()", "wrote config.xml:", inData); if (!aborted) { next(); } else { next (Phonegap.Build.abortBuild); } }); req.error(this, this._handleServiceError.bind(this, "Unable to write config.xml", next)); } } else { this.trace("skipping config.xml generation"); next(); } }, /** * Get the list of files of the project for further upload * @param {Object} project * @param {Function} next is a CommonJS callback * @private */ _getFiles: function(project, next) { this.trace("..."); var req, aborted = false; this.doShowWaitPopup({msg: $L("Building source archive"), service: "build"}); req = project.getService().exportAs(project.getFolderId(), -1 /*infinity*/); this.abortAjaxRequest= function() { this.abortAjaxRequest= function() {}; aborted = true ; }; req.response(this, function _gotFiles(inSender, inData) { this.trace("Phonegap.Build#_getFiles()", "Got the files data"); var ctype = req.xhrResponse.headers['x-content-type']; if (!aborted) { next(null, {content: inData, ctype: ctype}); } else { next (Phonegap.Build.abortBuild); } }); req.error(this, this._handleServiceError.bind(this, "Unable to fetch application source code", next)); }, /** * * @param {Object} project * @param {Function} buildStarted is a CommonJS callback * @param {FormData} data * @param {Function} next is a CommonJS callback * @private */ _submitBuildRequest: function(project, buildStarted, data, next) { var config = ares.clone(project.getConfig().getData()); this.trace("config: ", config); this.doShowWaitPopup({msg: $L("Uploading source archive to PhoneGap Build"), service: "build"}); var minification = config.providers.phonegap.minification; var keys = {}; var platforms = []; // mandatory parameters var query = { //provided by the cookie //token: this.config.auth.token, title: config.title, debug: !minification }; // Already-created apps have an appId (to be reused) if (config.providers.phonegap.appId) { this.trace("appId:", config.providers.phonegap.appId); query.appId = config.providers.phonegap.appId; } // Signing keys, if applicable to the target platform // & if chosen by the app developper. if (typeof config.providers.phonegap.targets === 'object') { enyo.forEach(enyo.keys(config.providers.phonegap.targets), function(target) { var pgTarget = config.providers.phonegap.targets[target]; if (pgTarget) { this.trace("platform:", target); platforms.push(target); if (typeof pgTarget === 'object') { var keyId = pgTarget.keyId; if (keyId) { keys[target] = enyo.clone(this.getKey(target, keyId)); //delete keys[target].title; //this.trace("platform:", target, "keys:", keys); } } } }, this); } if (typeof keys ==='object' && enyo.keys(keys).length > 0) { this.trace("keys:", keys); query.keys = enyo.json.stringify(keys); } // Target platforms -- defined by the Web API, but not implemented yet if (platforms.length > 0) { query.platforms = enyo.json.stringify(platforms); } else { next(new Error('No build platform selected')); return; } // Ask Hermes PhoneGap Build service to minify and zip the project var req = new enyo.Ajax({ url: this.url + '/op/build', method: 'POST', postBody: data.content, contentType: data.ctype, xhrFields: { withCredentials: true} }); this.abortAjaxRequest= function() { this.abortAjaxRequest= function() {}; enyo.xhr.cancel(req.xhr); next(Phonegap.Build.abortBuild); }; req.response(this, function(inSender, inData) { this.trace("Phonegap.Build#_submitBuildRequest(): response:", inData); if (inData) { config.providers.phonegap.appId = inData.id; var configKind = project.getConfig(); configKind.setData(config); configKind.save(); } buildStarted(); // Display the Phonegap Build panel. ComponentsRegistry.getComponent("projectView").$.projectWizardModify.showEditPopUp(project, 2); // Signal recieved by the "kind Phonegap.ProjectProperties". enyo.Signals.send("plugin.phonegap.userDataRefreshed"); next(); }); req.error(this, this._handleServiceError.bind(this, "Unable to build application", next)); req.go(query); }, abortAjaxRequest: function() { // abortAjaxRequest will store a cancel build function // generated when a build is on-going (See // _submitBuildRequest). This function is a NOP so no failure // happens even if a 'buildCanceled' signal is received out of // a build }, /** * Prepare the folder where to store the built package * @param {Object} project contain a description about the current selected * project * @param {Function} next a CommonJS callback * @private */ _prepareStore: function(project, inData, platform, next) { this.trace("PhoneGap.Build#_prepareStore()"); var folderId = project.getObject(this.folderKey); if (folderId) { next(null, inData, folderId, platform); } else { var req = project.getService().createFolder(project.getFolderId(), "target/" + this.config.id); req.response(this, function _folderCreated(inSender, inResponse) { this.trace("PhoneGap.Build#_prepareStore()", "response:", inResponse); folderId = inResponse.id; project.setObject(this.folderKey, folderId); this.doFsEvent({nodeId: folderId}); next(null, inData, folderId, platform); }); req.error(this, this._handleServiceError.bind(this, "Unable to prepare packages folder", next)); } }, /** * Store the built application file in the directory "<projectName>\target\Phonegap build". * * * @param {Object} project contain a description about the current selected * project * @param {String} folderId id used in Hermes File system to identify the * target folder where the downloaded applications * will be stored. * @param {Object} inData contains detailed informations about the build of * the project. * @param {Function} next a CommonJs callback. * @private */ _storePkg: function(project, folderId, inData, next) { this.trace("data content.ctype: ", inData.ctype); var req, fs = project.getService(); req = fs.createFiles(folderId, {content: inData.content, ctype: inData.ctype}, { overwrite: true }); req.response(this, function(inSender, inData) { this.trace("response:", inData); var config = project.getService().config; var pkgUrl = config.origin + config.pathname + '/file' + inData[0].path; // TODO: YDM: shortcut to be refined project.setObject("build.phonegap.target.pkgUrl", pkgUrl); this.doFsEvent({nodeId: inData[0].id}); next(); }); req.error(this, this._handleServiceError.bind(this, "Unable to store application package", next)); }, /** * After checking that the building of the project is finished in Phongap platform, this * function send an ajax request to the Ares server in order to launch * the download of the packaged application. * Ares server succeed in the downloading of this application, * an Ajax response is sent back in order to save the * file (contained in a multipart data form)in the folder * "Target/Phonegap build" of the curent built project. * * @param {Object} project contain a description of the current selected * project * @param {Object} folderId unique identifier of the project in Ares * @param {Object} appData contains detailed informations about the built * application on Phonegap * @param {Function} next a CommonJs callback * @private */ _waitFetchStore: function(project, appData, folderId, platform, next) { this.trace("Entering _store function project: ", project, "folderId:", folderId, "appData:", appData); project.setObject(this.appKey, appData); this._getPackageByPlatform(project, appData, folderId, platform, next); }, /** * Check continuously the build status of the build in a targeted mobile * platform on Phongap build service and launch the appropriate action * when the returned status of the build is * "complete" or "error". * @param {Object} project contain a description of the current * selected project * @param {String} platform targeted platfrom for the build * @param {Object} appData meta-data on the build of the actuel * project * @param {Object} folderId unique identifier of the project in Ares * @param {Function} next a CommonJS callback * @private */ _getPackageByPlatform: function(project, appData, folderId, platform, next){ var builder = this; async.whilst( function() { // Synchronous condition to keep waiting. return appData.status[platform] === "pending"; }, // ...condition satisfied _waitForApp, // ...condition no longer satisfied _downloadApp ); /** * Nested function that check the build status of the application * and update the appData each 3 sec * @param {Function} next a CommonJS callback * @private */ function _waitForApp (next){ async.waterfall([ function (next) { //Timeout before sending a new check status request setTimeout(next, builder.checkBuildResultInterval); }, function (next) { if(appData.status[platform] === "pending"){ builder.getProjectAppData(project, appData, next); } else{ next(null, null); } }, function(inData, next) { //get the result from the previous status check request if (inData !== null){ appData = inData.user; } next(); } ], next); } /** * Launch the appropriate action when an exception occurs or when * the status is no longer in the pending state. * @param {Object} err * @private */ function _downloadApp(err){ if (err) { next(err); } else { if (appData.status[platform] === "complete"){ _setApplicationToDownload(function(err) { if (err) { enyo.warn("phonegap.Build#getAllPackagedApplications._downloadApp()", "non-fatal err:", err); } next(); }); } else { next(); } } } /** * Create the URL to send the build request to Node.js * This URL contain the data to create the packaged file name. * * @param {Object} project contain a description of the current * selected project * @param {String} folderId unique identifier of the project in Ares * @param {String} platform targeted platfrom for the build * @param {Object} appData meta-data on the build of the actuel * project * @param {Function} next a CommonJS callback * @private */ function _setApplicationToDownload(next){ var config = ares.clone(project.getConfig().getData()), packageName = config.id, appId, title, version; async.waterfall([ function(next){ //make the download request. appId = appData.id; title = packageName; version = appData.version || "SNAPSHOT"; var urlSuffix = appId + '/' + platform + '/' + title + '/' + version; if(builder.debug){ builder.log("Application "+ platform + " ready for download"); } _sendDownloadRequest.bind(builder)(urlSuffix, next); }, //inData is a multipart/form containing the //built application function(inData, next){ builder._storePkg(project, folderId, inData, next); } ], next); } /** * Send an Ajax request to Node.js in order to initiate the download * of an application in a specific mobile platform. * * @param {Object} project contain a description about the * current selected project * @param {Object} urlSuffix is a url suffixe that contains: * the appId, the targeted build * platform, the title of the * application and its version. * @param {Function} next is a CommunJS callback. * @private */ function _sendDownloadRequest(urlSuffix, next){ var url = this.url + '/api/v1/apps/' + urlSuffix; this.trace("download URL is : ", url); var req = new enyo.Ajax({ url: url, handleAs: 'text', xhrFields: { withCredentials: true} }); req.response(this, function(inSender, inData) { this.trace("response: received ", inData.length, " bytes typeof: ", (typeof inData)); var ctype = req.xhrResponse.headers['content-type']; this.trace("response: received ctype: ", ctype); next(null, {content: inData, ctype: ctype}); }); req.error(this, this._handleServiceError.bind(this, "Unable to download application package", next)); req.go(); } }, /** * Generate PhoneGap's config.xml on the fly * * @param {Object} config PhoneGap Build config, as a Javascript object * @return {String} or undefined if PhoneGap build is disabled for this project * @private * FIXME: define a JSON schema */ _generateConfigXml: function(config) { var self = this; var phonegap = config.providers.phonegap; if (!phonegap) { this.trace("PhoneGap build disabled: will not generate the XML"); return undefined; } var createIconXMLRow = function(inTarget) { if (inTarget == 'sharedConfiguration') { // we should always declare at least the 'platform neutral' icon xw.writeStartElement( 'icon' ); // If the project does not define an icon, use Enyo's one xw.writeAttributeString('src', phonegap.icon[inTarget].src || 'icon.png'); if (phonegap.icon[inTarget].width) { xw.writeAttributeString('width', phonegap.icon[inTarget].width); } if (phonegap.icon[inTarget].height) { xw.writeAttributeString('height', phonegap.icon[inTarget].height); } } else { // platform specific icon should only be declared if they exists... // (i.e. if they differ from std icon) if (phonegap.icon[inTarget].src && phonegap.icon[inTarget].src.length > 0) { xw.writeStartElement( 'icon' ); xw.writeAttributeString('src', phonegap.icon[inTarget].src); xw.writeAttributeString('gap:platform', inTarget); if (inTarget === 'android'){ if (phonegap.icon[inTarget].density) { xw.writeAttributeString('gap:density', phonegap.icon[inTarget].density); } } else { if (phonegap.icon[inTarget].width) { xw.writeAttributeString('width', phonegap.icon[inTarget].width); } if (phonegap.icon[inTarget].height) { xw.writeAttributeString('height', phonegap.icon[inTarget].height); } } } } xw.writeEndElement(); }; var createSplashScreenXMLRow = function(inTarget) { if (inTarget == 'sharedConfiguration') { // we should always declare at least the 'platform neutral' splash // If the project does not define a splash , use Enyo icon xw.writeStartElement('gap:splash'); xw.writeAttributeString('src', phonegap.splashScreen[inTarget].src || 'icon.png'); if (phonegap.splashScreen[inTarget].width) { xw.writeAttributeString('width', phonegap.splashScreen[inTarget].width); } if (phonegap.splashScreen[inTarget].height) { xw.writeAttributeString('height', phonegap.splashScreen[inTarget].height); } } else { if (phonegap.splashScreen[inTarget].src && phonegap.splashScreen[inTarget].src.length > 0) { // for other platform, we only declare splash if there is a specific setting xw.writeStartElement('gap:splash'); xw.writeAttributeString('src', phonegap.splashScreen[inTarget].src); xw.writeAttributeString('gap:platform', inTarget); if (inTarget === 'android'){ if (phonegap.splashScreen.android.density) { xw.writeAttributeString('gap:density', phonegap.splashScreen.android.density); } } else { if (phonegap.splashScreen[inTarget].width) { xw.writeAttributeString('width', phonegap.splashScreen[inTarget].width); } if (phonegap.splashScreen[inTarget].height) { xw.writeAttributeString('height', phonegap.splashScreen[inTarget].height); } } } } xw.writeEndElement(); }; /** * Create an XML row in the file config.xml, this row describe a depence to * a hosted plugin, and may also contain sub-elements that contain a name and a value * of a parameter for the plugin. * * @param {Object} pluginList contains the plugins object defined in "project.json". * */ var createPluginXMLRow = function (plugin) { xw.writeStartElement('gap:plugin'); xw.writeAttributeString('name', plugin.name); xw.writeAttributeString('version', plugin.version); enyo.forEach(plugin.parameters && enyo.keys(plugin.parameters), function(parameter) { xw.writeStartElement("param"); xw.writeAttributeString('name', plugin.parameters[parameter].name); xw.writeAttributeString('value', plugin.parameters[parameter].value); xw.writeEndElement(); }, this); xw.writeEndElement(); }; // See http://flesler.blogspot.fr/2008/03/xmlwriter-for-javascript.html var str, xw = new XMLWriter('UTF-8'); xw.indentation = 4; xw.writeStartDocument(); xw.writeStartElement( 'widget' ); xw.writeAttributeString('xmlns','http://www.w3.org/ns/widgets'); xw.writeAttributeString('xmlns:gap','http://phonegap.com/ns/1.0'); xw.writeAttributeString('id', config.id); xw.writeAttributeString('version',config.version); xw.writeComment('*** WARNING ***'); xw.writeComment('*** This is an automatically generated document. ***'); xw.writeComment('*** Do not edit it: your changes would be automatically overwritten ***'); // we use 'title' (one-line description) here because // 'name' is made to be used by package names xw.writeElementString('name', config.title); // we have no multi-line 'description' of the // application, so use our one-line description xw.writeElementString('description', config.description); xw.writeStartElement( 'author' ); xw.writeAttributeString('href', config.author.href); xw.writeString(config.author.name); xw.writeEndElement(); // author xw.writeComment("Platforms"); if (phonegap.targets && (enyo.keys(phonegap.targets).length > 0)) { for (var platformName in phonegap.targets) { var platform = phonegap.targets[platformName]; if (platform !== false) { xw.writeStartElement('platform', 'gap'); xw.writeAttributeString('name', platformName); for (var propName in platform) { xw.writeAttributeString(propName, platform[propName]); } xw.writeEndElement(); // gap:platform } } } xw.writeComment("Features"); var featureUrl = "http://api.phonegap.com/1.0/"; // Check if all the permissions are disabled. // If it's the case this function return true var checkNoPermissions = function () { var noPermissions = true; for (var key in phonegap.features){ if (phonegap.features[key]){ noPermissions = false; } } return noPermissions; }; // If all the permissions are disabled, the tag generated is // <preference name="permissions" value="none" /> // Else the tag <feature name= <featureUrl> >is generated if(checkNoPermissions.call(this)) { xw.writeStartElement('preference'); xw.writeAttributeString('name', 'permissions'); xw.writeAttributeString('value', 'none'); xw.writeEndElement(); } else { enyo.forEach(phonegap.features && enyo.keys(phonegap.features), function(feature) { if (phonegap.features[feature]){ xw.writeStartElement('feature'); xw.writeAttributeString('name', featureUrl + feature); xw.writeEndElement(); } }, this); } xw.writeComment("Preferences"); enyo.forEach(phonegap.preferences && enyo.keys(phonegap.preferences), function(preference) { xw.writeStartElement('preference'); xw.writeAttributeString('name', preference); xw.writeAttributeString('value', phonegap.preferences[preference]); xw.writeEndElement(); // preference }, this); xw.writeComment("Plugins"); enyo.forEach(phonegap.plugins && enyo.keys(phonegap.plugins), function(pluginName) { createPluginXMLRow.call(self, phonegap.plugins[pluginName]); }, this); xw.writeComment("Define app icon for each platform"); enyo.forEach(phonegap.icon && enyo.keys(phonegap.icon), function(target) { createIconXMLRow.call(self, target); }, this); xw.writeComment("Define app splash screen for each platform"); enyo.forEach(phonegap.splashScreen && enyo.keys(phonegap.splashScreen), function(target) { createSplashScreenXMLRow.call(self, target); }, this); xw.writeComment("Access external websites ressources"); xw.writeStartElement('access'); xw.writeAttributeString('origin', phonegap.access.origin); xw.writeEndElement(); // access xw.writeEndElement(); // widget //xw.writeEndDocument(); called by flush() str = xw.flush(); xw.close(); this.trace("xml:", str); return str; }, statics: { DEFAULT_PROJECT_CONFIG: { enabled: false, autoGenerateXML: true, minification: true, features: { battery: false, camera: false, contact: false, file: false, geolocation: false, media: false, network: false, notification: false, device: false }, preferences: { //shared prefrences "phonegap-version": Phonegap.UIConfiguration.commonDrawersContent[2].rows[0].defaultValue, "orientation": Phonegap.UIConfiguration.commonDrawersContent[2].rows[1].defaultValue, "target-device": Phonegap.UIConfiguration.commonDrawersContent[2].rows[2].defaultValue, "fullscreen": Phonegap.UIConfiguration.commonDrawersContent[2].rows[3].defaultValue, //Android preferences "android-installLocation": Phonegap.UIConfiguration.platformDrawersContent[0].rows[0].defaultValue, "android-minSdkVersion": Phonegap.UIConfiguration.platformDrawersContent[0].rows[1].defaultValue, "android-targetSdkVersion": Phonegap.UIConfiguration.platformDrawersContent[0].rows[2].defaultValue, "android-maxSdkVersion": Phonegap.UIConfiguration.platformDrawersContent[0].rows[3].defaultValue, "android-windowSoftInputMode": Phonegap.UIConfiguration.platformDrawersContent[0].rows[4].defaultValue, "splash-screen-duration": Phonegap.UIConfiguration.platformDrawersContent[0].rows[5].defaultValue, "load-url-timeout": Phonegap.UIConfiguration.platformDrawersContent[0].rows[6].defaultValue, //IOS preferences "webviewbounce": Phonegap.UIConfiguration.platformDrawersContent[1].rows[0].defaultValue, "prerendered-icon": Phonegap.UIConfiguration.platformDrawersContent[1].rows[1].defaultValue, "ios-statusbarstyle": Phonegap.UIConfiguration.platformDrawersContent[1].rows[2].defaultValue, "detect-data-types": Phonegap.UIConfiguration.platformDrawersContent[1].rows[3].defaultValue, "exit-on-suspend": Phonegap.UIConfiguration.platformDrawersContent[1].rows[4].defaultValue, "show-splash-screen-spinner": Phonegap.UIConfiguration.platformDrawersContent[1].rows[5].defaultValue, "auto-hide-splash-screen": Phonegap.UIConfiguration.platformDrawersContent[1].rows[6].defaultValue, //BlackBerry preferences "disable-cursor": Phonegap.UIConfiguration.platformDrawersContent[3].rows[0].defaultValue }, icon: { sharedConfiguration: {src: "", role: "default"}, android: {src: "", density: "" }, ios: {src: "", height: "", width: ""}, winphone: {src: ""}, blackberry: {src: ""}, webos: {src: ""} }, splashScreen: { sharedConfiguration: {src: "", role: "default"}, android: {src: "", density: "" }, ios: {src: "", height: "", width: ""}, winphone: {src: ""}, blackberry: {src: ""}, webos: {src: ""} }, plugins: { }, access : { "origin": Phonegap.UIConfiguration.commonDrawersContent[1].rows[4].defaultValue } }, abortBuild: new Error ("Build canceled by user") } });