1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/modules/WebappManager.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,566 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +this.EXPORTED_SYMBOLS = ["WebappManager"]; 1.11 + 1.12 +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; 1.13 + 1.14 +const UPDATE_URL_PREF = "browser.webapps.updateCheckUrl"; 1.15 + 1.16 +Cu.import("resource://gre/modules/AppsUtils.jsm"); 1.17 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.18 +Cu.import("resource://gre/modules/Services.jsm"); 1.19 +Cu.import("resource://gre/modules/NetUtil.jsm"); 1.20 +Cu.import("resource://gre/modules/FileUtils.jsm"); 1.21 +Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); 1.22 +Cu.import("resource://gre/modules/Webapps.jsm"); 1.23 +Cu.import("resource://gre/modules/osfile.jsm"); 1.24 +Cu.import("resource://gre/modules/Promise.jsm"); 1.25 +Cu.import("resource://gre/modules/Task.jsm"); 1.26 + 1.27 +XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm"); 1.28 +XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava", "resource://gre/modules/Messaging.jsm"); 1.29 +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); 1.30 + 1.31 +XPCOMUtils.defineLazyGetter(this, "Strings", function() { 1.32 + return Services.strings.createBundle("chrome://browser/locale/webapp.properties"); 1.33 +}); 1.34 + 1.35 +function debug(aMessage) { 1.36 + // We use *dump* instead of Services.console.logStringMessage so the messages 1.37 + // have the INFO level of severity instead of the ERROR level. And we don't 1.38 + // append a newline character to the end of the message because *dump* spills 1.39 + // into the Android native logging system, which strips newlines from messages 1.40 + // and breaks messages into lines automatically at display time (i.e. logcat). 1.41 +#ifdef DEBUG 1.42 + dump(aMessage); 1.43 +#endif 1.44 +} 1.45 + 1.46 +this.WebappManager = { 1.47 + __proto__: DOMRequestIpcHelper.prototype, 1.48 + 1.49 + get _testing() { 1.50 + try { 1.51 + return Services.prefs.getBoolPref("browser.webapps.testing"); 1.52 + } catch(ex) { 1.53 + return false; 1.54 + } 1.55 + }, 1.56 + 1.57 + install: function(aMessage, aMessageManager) { 1.58 + if (this._testing) { 1.59 + // Go directly to DOM. Do not download/install APK, do not collect $200. 1.60 + DOMApplicationRegistry.doInstall(aMessage, aMessageManager); 1.61 + return; 1.62 + } 1.63 + 1.64 + this._installApk(aMessage, aMessageManager); 1.65 + }, 1.66 + 1.67 + installPackage: function(aMessage, aMessageManager) { 1.68 + if (this._testing) { 1.69 + // Go directly to DOM. Do not download/install APK, do not collect $200. 1.70 + DOMApplicationRegistry.doInstallPackage(aMessage, aMessageManager); 1.71 + return; 1.72 + } 1.73 + 1.74 + this._installApk(aMessage, aMessageManager); 1.75 + }, 1.76 + 1.77 + _installApk: function(aMessage, aMessageManager) { return Task.spawn((function*() { 1.78 + let filePath; 1.79 + 1.80 + try { 1.81 + filePath = yield this._downloadApk(aMessage.app.manifestURL); 1.82 + } catch(ex) { 1.83 + aMessage.error = ex; 1.84 + aMessageManager.sendAsyncMessage("Webapps:Install:Return:KO", aMessage); 1.85 + debug("error downloading APK: " + ex); 1.86 + return; 1.87 + } 1.88 + 1.89 + sendMessageToJava({ 1.90 + type: "Webapps:InstallApk", 1.91 + filePath: filePath, 1.92 + data: JSON.stringify(aMessage), 1.93 + }); 1.94 + }).bind(this)); }, 1.95 + 1.96 + _downloadApk: function(aManifestUrl) { 1.97 + debug("_downloadApk for " + aManifestUrl); 1.98 + let deferred = Promise.defer(); 1.99 + 1.100 + // Get the endpoint URL and convert it to an nsIURI/nsIURL object. 1.101 + const GENERATOR_URL_PREF = "browser.webapps.apkFactoryUrl"; 1.102 + const GENERATOR_URL_BASE = Services.prefs.getCharPref(GENERATOR_URL_PREF); 1.103 + let generatorUrl = NetUtil.newURI(GENERATOR_URL_BASE).QueryInterface(Ci.nsIURL); 1.104 + 1.105 + // Populate the query part of the URL with the manifest URL parameter. 1.106 + let params = { 1.107 + manifestUrl: aManifestUrl, 1.108 + }; 1.109 + generatorUrl.query = 1.110 + [p + "=" + encodeURIComponent(params[p]) for (p in params)].join("&"); 1.111 + debug("downloading APK from " + generatorUrl.spec); 1.112 + 1.113 + let file = Cc["@mozilla.org/download-manager;1"]. 1.114 + getService(Ci.nsIDownloadManager). 1.115 + defaultDownloadsDirectory. 1.116 + clone(); 1.117 + file.append(aManifestUrl.replace(/[^a-zA-Z0-9]/gi, "") + ".apk"); 1.118 + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); 1.119 + debug("downloading APK to " + file.path); 1.120 + 1.121 + let worker = new ChromeWorker("resource://gre/modules/WebappManagerWorker.js"); 1.122 + worker.onmessage = function(event) { 1.123 + let { type, message } = event.data; 1.124 + 1.125 + worker.terminate(); 1.126 + 1.127 + if (type == "success") { 1.128 + deferred.resolve(file.path); 1.129 + } else { // type == "failure" 1.130 + debug("error downloading APK: " + message); 1.131 + deferred.reject(message); 1.132 + } 1.133 + } 1.134 + 1.135 + // Trigger the download. 1.136 + worker.postMessage({ url: generatorUrl.spec, path: file.path }); 1.137 + 1.138 + return deferred.promise; 1.139 + }, 1.140 + 1.141 + askInstall: function(aData) { 1.142 + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); 1.143 + file.initWithPath(aData.profilePath); 1.144 + 1.145 + // We don't yet support pre-installing an appcache because it isn't clear 1.146 + // how to do it without degrading the user experience (since users expect 1.147 + // apps to be available after the system tells them they've been installed, 1.148 + // which has already happened) and because nsCacheService shuts down 1.149 + // when we trigger the native install dialog and doesn't re-init itself 1.150 + // afterward (TODO: file bug about this behavior). 1.151 + if ("appcache_path" in aData.app.manifest) { 1.152 + debug("deleting appcache_path from manifest: " + aData.app.manifest.appcache_path); 1.153 + delete aData.app.manifest.appcache_path; 1.154 + } 1.155 + 1.156 + DOMApplicationRegistry.registryReady.then(() => { 1.157 + DOMApplicationRegistry.confirmInstall(aData, file, (function(aManifest) { 1.158 + let localeManifest = new ManifestHelper(aManifest, aData.app.origin); 1.159 + 1.160 + // aData.app.origin may now point to the app: url that hosts this app. 1.161 + sendMessageToJava({ 1.162 + type: "Webapps:Postinstall", 1.163 + apkPackageName: aData.app.apkPackageName, 1.164 + origin: aData.app.origin, 1.165 + }); 1.166 + 1.167 + this.writeDefaultPrefs(file, localeManifest); 1.168 + }).bind(this)); 1.169 + }); 1.170 + }, 1.171 + 1.172 + launch: function({ manifestURL, origin }) { 1.173 + debug("launchWebapp: " + manifestURL); 1.174 + 1.175 + sendMessageToJava({ 1.176 + type: "Webapps:Open", 1.177 + manifestURL: manifestURL, 1.178 + origin: origin 1.179 + }); 1.180 + }, 1.181 + 1.182 + uninstall: function(aData) { 1.183 + debug("uninstall: " + aData.manifestURL); 1.184 + 1.185 + if (this._testing) { 1.186 + // We don't have to do anything, as the registry does all the work. 1.187 + return; 1.188 + } 1.189 + 1.190 + // TODO: uninstall the APK. 1.191 + }, 1.192 + 1.193 + autoInstall: function(aData) { 1.194 + let oldApp = DOMApplicationRegistry.getAppByManifestURL(aData.manifestURL); 1.195 + if (oldApp) { 1.196 + // If the app is already installed, update the existing installation. 1.197 + this._autoUpdate(aData, oldApp); 1.198 + return; 1.199 + } 1.200 + 1.201 + let mm = { 1.202 + sendAsyncMessage: function (aMessageName, aData) { 1.203 + // TODO hook this back to Java to report errors. 1.204 + debug("sendAsyncMessage " + aMessageName + ": " + JSON.stringify(aData)); 1.205 + } 1.206 + }; 1.207 + 1.208 + let origin = Services.io.newURI(aData.manifestURL, null, null).prePath; 1.209 + 1.210 + let message = aData.request || { 1.211 + app: { 1.212 + origin: origin, 1.213 + receipts: [], 1.214 + } 1.215 + }; 1.216 + 1.217 + if (aData.updateManifest) { 1.218 + if (aData.zipFilePath) { 1.219 + aData.updateManifest.package_path = aData.zipFilePath; 1.220 + } 1.221 + message.app.updateManifest = aData.updateManifest; 1.222 + } 1.223 + 1.224 + // The manifest url may be subtly different between the 1.225 + // time the APK was built and the APK being installed. 1.226 + // Thus, we should take the APK as the source of truth. 1.227 + message.app.manifestURL = aData.manifestURL; 1.228 + message.app.manifest = aData.manifest; 1.229 + message.app.apkPackageName = aData.apkPackageName; 1.230 + message.profilePath = aData.profilePath; 1.231 + message.mm = mm; 1.232 + message.apkInstall = true; 1.233 + 1.234 + DOMApplicationRegistry.registryReady.then(() => { 1.235 + switch (aData.type) { // can be hosted or packaged. 1.236 + case "hosted": 1.237 + DOMApplicationRegistry.doInstall(message, mm); 1.238 + break; 1.239 + 1.240 + case "packaged": 1.241 + message.isPackage = true; 1.242 + DOMApplicationRegistry.doInstallPackage(message, mm); 1.243 + break; 1.244 + } 1.245 + }); 1.246 + }, 1.247 + 1.248 + _autoUpdate: function(aData, aOldApp) { return Task.spawn((function*() { 1.249 + debug("_autoUpdate app of type " + aData.type); 1.250 + 1.251 + if (aOldApp.apkPackageName != aData.apkPackageName) { 1.252 + // This happens when the app was installed as a shortcut via the old 1.253 + // runtime and is now being updated to an APK. 1.254 + debug("update apkPackageName from " + aOldApp.apkPackageName + " to " + aData.apkPackageName); 1.255 + aOldApp.apkPackageName = aData.apkPackageName; 1.256 + } 1.257 + 1.258 + if (aData.type == "hosted") { 1.259 + let oldManifest = yield DOMApplicationRegistry.getManifestFor(aData.manifestURL); 1.260 + DOMApplicationRegistry.updateHostedApp(aData, aOldApp.id, aOldApp, oldManifest, aData.manifest); 1.261 + } else { 1.262 + DOMApplicationRegistry.updatePackagedApp(aData, aOldApp.id, aOldApp, aData.manifest); 1.263 + } 1.264 + }).bind(this)); }, 1.265 + 1.266 + _checkingForUpdates: false, 1.267 + 1.268 + checkForUpdates: function(userInitiated) { return Task.spawn((function*() { 1.269 + debug("checkForUpdates"); 1.270 + 1.271 + // Don't start checking for updates if we're already doing so. 1.272 + // TODO: Consider cancelling the old one and starting a new one anyway 1.273 + // if the user requested this one. 1.274 + if (this._checkingForUpdates) { 1.275 + debug("already checking for updates"); 1.276 + return; 1.277 + } 1.278 + this._checkingForUpdates = true; 1.279 + 1.280 + try { 1.281 + let installedApps = yield this._getInstalledApps(); 1.282 + if (installedApps.length === 0) { 1.283 + return; 1.284 + } 1.285 + 1.286 + // Map APK names to APK versions. 1.287 + let apkNameToVersion = yield this._getAPKVersions(installedApps.map(app => 1.288 + app.apkPackageName).filter(apkPackageName => !!apkPackageName) 1.289 + ); 1.290 + 1.291 + // Map manifest URLs to APK versions, which is what the service needs 1.292 + // in order to tell us which apps are outdated; and also map them to app 1.293 + // objects, which the downloader/installer uses to download/install APKs. 1.294 + // XXX Will this cause us to update apps without packages, and if so, 1.295 + // does that satisfy the legacy migration story? 1.296 + let manifestUrlToApkVersion = {}; 1.297 + let manifestUrlToApp = {}; 1.298 + for (let app of installedApps) { 1.299 + manifestUrlToApkVersion[app.manifestURL] = apkNameToVersion[app.apkPackageName] || 0; 1.300 + manifestUrlToApp[app.manifestURL] = app; 1.301 + } 1.302 + 1.303 + let outdatedApps = yield this._getOutdatedApps(manifestUrlToApkVersion, userInitiated); 1.304 + 1.305 + if (outdatedApps.length === 0) { 1.306 + // If the user asked us to check for updates, tell 'em we came up empty. 1.307 + if (userInitiated) { 1.308 + this._notify({ 1.309 + title: Strings.GetStringFromName("noUpdatesTitle"), 1.310 + message: Strings.GetStringFromName("noUpdatesMessage"), 1.311 + icon: "drawable://alert_app", 1.312 + }); 1.313 + } 1.314 + return; 1.315 + } 1.316 + 1.317 + let names = [manifestUrlToApp[url].name for (url of outdatedApps)].join(", "); 1.318 + let accepted = yield this._notify({ 1.319 + title: PluralForm.get(outdatedApps.length, Strings.GetStringFromName("downloadUpdateTitle")). 1.320 + replace("#1", outdatedApps.length), 1.321 + message: Strings.formatStringFromName("downloadUpdateMessage", [names], 1), 1.322 + icon: "drawable://alert_app", 1.323 + }).dismissed; 1.324 + 1.325 + if (accepted) { 1.326 + yield this._updateApks([manifestUrlToApp[url] for (url of outdatedApps)]); 1.327 + } 1.328 + } 1.329 + // There isn't a catch block because we want the error to propagate through 1.330 + // the promise chain, so callers can receive it and choose to respond to it. 1.331 + finally { 1.332 + // Ensure we update the _checkingForUpdates flag even if there's an error; 1.333 + // otherwise the process will get stuck and never check for updates again. 1.334 + this._checkingForUpdates = false; 1.335 + } 1.336 + }).bind(this)); }, 1.337 + 1.338 + _getAPKVersions: function(packageNames) { 1.339 + let deferred = Promise.defer(); 1.340 + 1.341 + sendMessageToJava({ 1.342 + type: "Webapps:GetApkVersions", 1.343 + packageNames: packageNames 1.344 + }, data => deferred.resolve(data.versions)); 1.345 + 1.346 + return deferred.promise; 1.347 + }, 1.348 + 1.349 + _getInstalledApps: function() { 1.350 + let deferred = Promise.defer(); 1.351 + DOMApplicationRegistry.getAll(apps => deferred.resolve(apps)); 1.352 + return deferred.promise; 1.353 + }, 1.354 + 1.355 + _getOutdatedApps: function(installedApps, userInitiated) { 1.356 + let deferred = Promise.defer(); 1.357 + 1.358 + let data = JSON.stringify({ installed: installedApps }); 1.359 + 1.360 + let notification; 1.361 + if (userInitiated) { 1.362 + notification = this._notify({ 1.363 + title: Strings.GetStringFromName("checkingForUpdatesTitle"), 1.364 + message: Strings.GetStringFromName("checkingForUpdatesMessage"), 1.365 + // TODO: replace this with an animated icon. 1.366 + icon: "drawable://alert_app", 1.367 + progress: NaN, 1.368 + }); 1.369 + } 1.370 + 1.371 + let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. 1.372 + createInstance(Ci.nsIXMLHttpRequest). 1.373 + QueryInterface(Ci.nsIXMLHttpRequestEventTarget); 1.374 + request.mozBackgroundRequest = true; 1.375 + request.open("POST", Services.prefs.getCharPref(UPDATE_URL_PREF), true); 1.376 + request.channel.loadFlags = Ci.nsIChannel.LOAD_ANONYMOUS | 1.377 + Ci.nsIChannel.LOAD_BYPASS_CACHE | 1.378 + Ci.nsIChannel.INHIBIT_CACHING; 1.379 + request.onload = function() { 1.380 + if (userInitiated) { 1.381 + notification.cancel(); 1.382 + } 1.383 + deferred.resolve(JSON.parse(this.response).outdated); 1.384 + }; 1.385 + request.onerror = function() { 1.386 + if (userInitiated) { 1.387 + notification.cancel(); 1.388 + } 1.389 + deferred.reject(this.status || this.statusText); 1.390 + }; 1.391 + request.setRequestHeader("Content-Type", "application/json"); 1.392 + request.setRequestHeader("Content-Length", data.length); 1.393 + 1.394 + request.send(data); 1.395 + 1.396 + return deferred.promise; 1.397 + }, 1.398 + 1.399 + _updateApks: function(aApps) { return Task.spawn((function*() { 1.400 + // Notify the user that we're in the progress of downloading updates. 1.401 + let downloadingNames = [app.name for (app of aApps)].join(", "); 1.402 + let notification = this._notify({ 1.403 + title: PluralForm.get(aApps.length, Strings.GetStringFromName("downloadingUpdateTitle")). 1.404 + replace("#1", aApps.length), 1.405 + message: Strings.formatStringFromName("downloadingUpdateMessage", [downloadingNames], 1), 1.406 + // TODO: replace this with an animated icon. UpdateService uses 1.407 + // android.R.drawable.stat_sys_download, but I don't think we can reference 1.408 + // a system icon with a drawable: URL here, so we'll have to craft our own. 1.409 + icon: "drawable://alert_download", 1.410 + // TODO: make this a determinate progress indicator once we can determine 1.411 + // the sizes of the APKs and observe their progress. 1.412 + progress: NaN, 1.413 + }); 1.414 + 1.415 + // Download the APKs for the given apps. We do this serially to avoid 1.416 + // saturating the user's network connection. 1.417 + // TODO: download APKs in parallel (or at least more than one at a time) 1.418 + // if it seems reasonable. 1.419 + let downloadedApks = []; 1.420 + let downloadFailedApps = []; 1.421 + for (let app of aApps) { 1.422 + try { 1.423 + let filePath = yield this._downloadApk(app.manifestURL); 1.424 + downloadedApks.push({ app: app, filePath: filePath }); 1.425 + } catch(ex) { 1.426 + downloadFailedApps.push(app); 1.427 + } 1.428 + } 1.429 + 1.430 + notification.cancel(); 1.431 + 1.432 + // Notify the user if any downloads failed, but don't do anything 1.433 + // when the user accepts/cancels the notification. 1.434 + // In the future, we might prompt the user to retry the download. 1.435 + if (downloadFailedApps.length > 0) { 1.436 + let downloadFailedNames = [app.name for (app of downloadFailedApps)].join(", "); 1.437 + this._notify({ 1.438 + title: PluralForm.get(downloadFailedApps.length, Strings.GetStringFromName("downloadFailedTitle")). 1.439 + replace("#1", downloadFailedApps.length), 1.440 + message: Strings.formatStringFromName("downloadFailedMessage", [downloadFailedNames], 1), 1.441 + icon: "drawable://alert_app", 1.442 + }); 1.443 + } 1.444 + 1.445 + // If we weren't able to download any APKs, then there's nothing more to do. 1.446 + if (downloadedApks.length === 0) { 1.447 + return; 1.448 + } 1.449 + 1.450 + // Prompt the user to update the apps for which we downloaded APKs, and wait 1.451 + // until they accept/cancel the notification. 1.452 + let downloadedNames = [apk.app.name for (apk of downloadedApks)].join(", "); 1.453 + let accepted = yield this._notify({ 1.454 + title: PluralForm.get(downloadedApks.length, Strings.GetStringFromName("installUpdateTitle")). 1.455 + replace("#1", downloadedApks.length), 1.456 + message: Strings.formatStringFromName("installUpdateMessage", [downloadedNames], 1), 1.457 + icon: "drawable://alert_app", 1.458 + }).dismissed; 1.459 + 1.460 + if (accepted) { 1.461 + // The user accepted the notification, so install the downloaded APKs. 1.462 + for (let apk of downloadedApks) { 1.463 + let msg = { 1.464 + app: apk.app, 1.465 + // TODO: figure out why Webapps:InstallApk needs the "from" property. 1.466 + from: apk.app.installOrigin, 1.467 + }; 1.468 + sendMessageToJava({ 1.469 + type: "Webapps:InstallApk", 1.470 + filePath: apk.filePath, 1.471 + data: JSON.stringify(msg), 1.472 + }); 1.473 + } 1.474 + } else { 1.475 + // The user cancelled the notification, so remove the downloaded APKs. 1.476 + for (let apk of downloadedApks) { 1.477 + try { 1.478 + yield OS.file.remove(apk.filePath); 1.479 + } catch(ex) { 1.480 + debug("error removing " + apk.filePath + " for cancelled update: " + ex); 1.481 + } 1.482 + } 1.483 + } 1.484 + 1.485 + }).bind(this)); }, 1.486 + 1.487 + _notify: function(aOptions) { 1.488 + dump("_notify: " + aOptions.title); 1.489 + 1.490 + // Resolves to true if the notification is "clicked" (i.e. touched) 1.491 + // and false if the notification is "cancelled" by swiping it away. 1.492 + let dismissed = Promise.defer(); 1.493 + 1.494 + // TODO: make notifications expandable so users can expand them to read text 1.495 + // that gets cut off in standard notifications. 1.496 + let id = Notifications.create({ 1.497 + title: aOptions.title, 1.498 + message: aOptions.message, 1.499 + icon: aOptions.icon, 1.500 + progress: aOptions.progress, 1.501 + onClick: function(aId, aCookie) { 1.502 + dismissed.resolve(true); 1.503 + }, 1.504 + onCancel: function(aId, aCookie) { 1.505 + dismissed.resolve(false); 1.506 + }, 1.507 + }); 1.508 + 1.509 + // Return an object with a promise that resolves when the notification 1.510 + // is dismissed by the user along with a method for cancelling it, 1.511 + // so callers who want to wait for user action can do so, while those 1.512 + // who want to control the notification's lifecycle can do that instead. 1.513 + return { 1.514 + dismissed: dismissed.promise, 1.515 + cancel: function() { 1.516 + Notifications.cancel(id); 1.517 + }, 1.518 + }; 1.519 + }, 1.520 + 1.521 + autoUninstall: function(aData) { 1.522 + DOMApplicationRegistry.registryReady.then(() => { 1.523 + for (let id in DOMApplicationRegistry.webapps) { 1.524 + let app = DOMApplicationRegistry.webapps[id]; 1.525 + if (aData.apkPackageNames.indexOf(app.apkPackageName) > -1) { 1.526 + debug("attempting to uninstall " + app.name); 1.527 + DOMApplicationRegistry.uninstall( 1.528 + app.manifestURL, 1.529 + function() { 1.530 + debug("success uninstalling " + app.name); 1.531 + }, 1.532 + function(error) { 1.533 + debug("error uninstalling " + app.name + ": " + error); 1.534 + } 1.535 + ); 1.536 + } 1.537 + } 1.538 + }); 1.539 + }, 1.540 + 1.541 + writeDefaultPrefs: function(aProfile, aManifest) { 1.542 + // build any app specific default prefs 1.543 + let prefs = []; 1.544 + if (aManifest.orientation) { 1.545 + let orientation = aManifest.orientation; 1.546 + if (Array.isArray(orientation)) { 1.547 + orientation = orientation.join(","); 1.548 + } 1.549 + prefs.push({ name: "app.orientation.default", value: orientation }); 1.550 + } 1.551 + 1.552 + // write them into the app profile 1.553 + let defaultPrefsFile = aProfile.clone(); 1.554 + defaultPrefsFile.append(this.DEFAULT_PREFS_FILENAME); 1.555 + this._writeData(defaultPrefsFile, prefs); 1.556 + }, 1.557 + 1.558 + _writeData: function(aFile, aPrefs) { 1.559 + if (aPrefs.length > 0) { 1.560 + let array = new TextEncoder().encode(JSON.stringify(aPrefs)); 1.561 + OS.File.writeAtomic(aFile.path, array, { tmpPath: aFile.path + ".tmp" }).then(null, function onError(reason) { 1.562 + debug("Error writing default prefs: " + reason); 1.563 + }); 1.564 + } 1.565 + }, 1.566 + 1.567 + DEFAULT_PREFS_FILENAME: "default-prefs.js", 1.568 + 1.569 +};