|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 this.EXPORTED_SYMBOLS = ["WebappManager"]; |
|
8 |
|
9 const { classes: Cc, interfaces: Ci, utils: Cu } = Components; |
|
10 |
|
11 const UPDATE_URL_PREF = "browser.webapps.updateCheckUrl"; |
|
12 |
|
13 Cu.import("resource://gre/modules/AppsUtils.jsm"); |
|
14 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
15 Cu.import("resource://gre/modules/Services.jsm"); |
|
16 Cu.import("resource://gre/modules/NetUtil.jsm"); |
|
17 Cu.import("resource://gre/modules/FileUtils.jsm"); |
|
18 Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); |
|
19 Cu.import("resource://gre/modules/Webapps.jsm"); |
|
20 Cu.import("resource://gre/modules/osfile.jsm"); |
|
21 Cu.import("resource://gre/modules/Promise.jsm"); |
|
22 Cu.import("resource://gre/modules/Task.jsm"); |
|
23 |
|
24 XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm"); |
|
25 XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava", "resource://gre/modules/Messaging.jsm"); |
|
26 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); |
|
27 |
|
28 XPCOMUtils.defineLazyGetter(this, "Strings", function() { |
|
29 return Services.strings.createBundle("chrome://browser/locale/webapp.properties"); |
|
30 }); |
|
31 |
|
32 function debug(aMessage) { |
|
33 // We use *dump* instead of Services.console.logStringMessage so the messages |
|
34 // have the INFO level of severity instead of the ERROR level. And we don't |
|
35 // append a newline character to the end of the message because *dump* spills |
|
36 // into the Android native logging system, which strips newlines from messages |
|
37 // and breaks messages into lines automatically at display time (i.e. logcat). |
|
38 #ifdef DEBUG |
|
39 dump(aMessage); |
|
40 #endif |
|
41 } |
|
42 |
|
43 this.WebappManager = { |
|
44 __proto__: DOMRequestIpcHelper.prototype, |
|
45 |
|
46 get _testing() { |
|
47 try { |
|
48 return Services.prefs.getBoolPref("browser.webapps.testing"); |
|
49 } catch(ex) { |
|
50 return false; |
|
51 } |
|
52 }, |
|
53 |
|
54 install: function(aMessage, aMessageManager) { |
|
55 if (this._testing) { |
|
56 // Go directly to DOM. Do not download/install APK, do not collect $200. |
|
57 DOMApplicationRegistry.doInstall(aMessage, aMessageManager); |
|
58 return; |
|
59 } |
|
60 |
|
61 this._installApk(aMessage, aMessageManager); |
|
62 }, |
|
63 |
|
64 installPackage: function(aMessage, aMessageManager) { |
|
65 if (this._testing) { |
|
66 // Go directly to DOM. Do not download/install APK, do not collect $200. |
|
67 DOMApplicationRegistry.doInstallPackage(aMessage, aMessageManager); |
|
68 return; |
|
69 } |
|
70 |
|
71 this._installApk(aMessage, aMessageManager); |
|
72 }, |
|
73 |
|
74 _installApk: function(aMessage, aMessageManager) { return Task.spawn((function*() { |
|
75 let filePath; |
|
76 |
|
77 try { |
|
78 filePath = yield this._downloadApk(aMessage.app.manifestURL); |
|
79 } catch(ex) { |
|
80 aMessage.error = ex; |
|
81 aMessageManager.sendAsyncMessage("Webapps:Install:Return:KO", aMessage); |
|
82 debug("error downloading APK: " + ex); |
|
83 return; |
|
84 } |
|
85 |
|
86 sendMessageToJava({ |
|
87 type: "Webapps:InstallApk", |
|
88 filePath: filePath, |
|
89 data: JSON.stringify(aMessage), |
|
90 }); |
|
91 }).bind(this)); }, |
|
92 |
|
93 _downloadApk: function(aManifestUrl) { |
|
94 debug("_downloadApk for " + aManifestUrl); |
|
95 let deferred = Promise.defer(); |
|
96 |
|
97 // Get the endpoint URL and convert it to an nsIURI/nsIURL object. |
|
98 const GENERATOR_URL_PREF = "browser.webapps.apkFactoryUrl"; |
|
99 const GENERATOR_URL_BASE = Services.prefs.getCharPref(GENERATOR_URL_PREF); |
|
100 let generatorUrl = NetUtil.newURI(GENERATOR_URL_BASE).QueryInterface(Ci.nsIURL); |
|
101 |
|
102 // Populate the query part of the URL with the manifest URL parameter. |
|
103 let params = { |
|
104 manifestUrl: aManifestUrl, |
|
105 }; |
|
106 generatorUrl.query = |
|
107 [p + "=" + encodeURIComponent(params[p]) for (p in params)].join("&"); |
|
108 debug("downloading APK from " + generatorUrl.spec); |
|
109 |
|
110 let file = Cc["@mozilla.org/download-manager;1"]. |
|
111 getService(Ci.nsIDownloadManager). |
|
112 defaultDownloadsDirectory. |
|
113 clone(); |
|
114 file.append(aManifestUrl.replace(/[^a-zA-Z0-9]/gi, "") + ".apk"); |
|
115 file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); |
|
116 debug("downloading APK to " + file.path); |
|
117 |
|
118 let worker = new ChromeWorker("resource://gre/modules/WebappManagerWorker.js"); |
|
119 worker.onmessage = function(event) { |
|
120 let { type, message } = event.data; |
|
121 |
|
122 worker.terminate(); |
|
123 |
|
124 if (type == "success") { |
|
125 deferred.resolve(file.path); |
|
126 } else { // type == "failure" |
|
127 debug("error downloading APK: " + message); |
|
128 deferred.reject(message); |
|
129 } |
|
130 } |
|
131 |
|
132 // Trigger the download. |
|
133 worker.postMessage({ url: generatorUrl.spec, path: file.path }); |
|
134 |
|
135 return deferred.promise; |
|
136 }, |
|
137 |
|
138 askInstall: function(aData) { |
|
139 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); |
|
140 file.initWithPath(aData.profilePath); |
|
141 |
|
142 // We don't yet support pre-installing an appcache because it isn't clear |
|
143 // how to do it without degrading the user experience (since users expect |
|
144 // apps to be available after the system tells them they've been installed, |
|
145 // which has already happened) and because nsCacheService shuts down |
|
146 // when we trigger the native install dialog and doesn't re-init itself |
|
147 // afterward (TODO: file bug about this behavior). |
|
148 if ("appcache_path" in aData.app.manifest) { |
|
149 debug("deleting appcache_path from manifest: " + aData.app.manifest.appcache_path); |
|
150 delete aData.app.manifest.appcache_path; |
|
151 } |
|
152 |
|
153 DOMApplicationRegistry.registryReady.then(() => { |
|
154 DOMApplicationRegistry.confirmInstall(aData, file, (function(aManifest) { |
|
155 let localeManifest = new ManifestHelper(aManifest, aData.app.origin); |
|
156 |
|
157 // aData.app.origin may now point to the app: url that hosts this app. |
|
158 sendMessageToJava({ |
|
159 type: "Webapps:Postinstall", |
|
160 apkPackageName: aData.app.apkPackageName, |
|
161 origin: aData.app.origin, |
|
162 }); |
|
163 |
|
164 this.writeDefaultPrefs(file, localeManifest); |
|
165 }).bind(this)); |
|
166 }); |
|
167 }, |
|
168 |
|
169 launch: function({ manifestURL, origin }) { |
|
170 debug("launchWebapp: " + manifestURL); |
|
171 |
|
172 sendMessageToJava({ |
|
173 type: "Webapps:Open", |
|
174 manifestURL: manifestURL, |
|
175 origin: origin |
|
176 }); |
|
177 }, |
|
178 |
|
179 uninstall: function(aData) { |
|
180 debug("uninstall: " + aData.manifestURL); |
|
181 |
|
182 if (this._testing) { |
|
183 // We don't have to do anything, as the registry does all the work. |
|
184 return; |
|
185 } |
|
186 |
|
187 // TODO: uninstall the APK. |
|
188 }, |
|
189 |
|
190 autoInstall: function(aData) { |
|
191 let oldApp = DOMApplicationRegistry.getAppByManifestURL(aData.manifestURL); |
|
192 if (oldApp) { |
|
193 // If the app is already installed, update the existing installation. |
|
194 this._autoUpdate(aData, oldApp); |
|
195 return; |
|
196 } |
|
197 |
|
198 let mm = { |
|
199 sendAsyncMessage: function (aMessageName, aData) { |
|
200 // TODO hook this back to Java to report errors. |
|
201 debug("sendAsyncMessage " + aMessageName + ": " + JSON.stringify(aData)); |
|
202 } |
|
203 }; |
|
204 |
|
205 let origin = Services.io.newURI(aData.manifestURL, null, null).prePath; |
|
206 |
|
207 let message = aData.request || { |
|
208 app: { |
|
209 origin: origin, |
|
210 receipts: [], |
|
211 } |
|
212 }; |
|
213 |
|
214 if (aData.updateManifest) { |
|
215 if (aData.zipFilePath) { |
|
216 aData.updateManifest.package_path = aData.zipFilePath; |
|
217 } |
|
218 message.app.updateManifest = aData.updateManifest; |
|
219 } |
|
220 |
|
221 // The manifest url may be subtly different between the |
|
222 // time the APK was built and the APK being installed. |
|
223 // Thus, we should take the APK as the source of truth. |
|
224 message.app.manifestURL = aData.manifestURL; |
|
225 message.app.manifest = aData.manifest; |
|
226 message.app.apkPackageName = aData.apkPackageName; |
|
227 message.profilePath = aData.profilePath; |
|
228 message.mm = mm; |
|
229 message.apkInstall = true; |
|
230 |
|
231 DOMApplicationRegistry.registryReady.then(() => { |
|
232 switch (aData.type) { // can be hosted or packaged. |
|
233 case "hosted": |
|
234 DOMApplicationRegistry.doInstall(message, mm); |
|
235 break; |
|
236 |
|
237 case "packaged": |
|
238 message.isPackage = true; |
|
239 DOMApplicationRegistry.doInstallPackage(message, mm); |
|
240 break; |
|
241 } |
|
242 }); |
|
243 }, |
|
244 |
|
245 _autoUpdate: function(aData, aOldApp) { return Task.spawn((function*() { |
|
246 debug("_autoUpdate app of type " + aData.type); |
|
247 |
|
248 if (aOldApp.apkPackageName != aData.apkPackageName) { |
|
249 // This happens when the app was installed as a shortcut via the old |
|
250 // runtime and is now being updated to an APK. |
|
251 debug("update apkPackageName from " + aOldApp.apkPackageName + " to " + aData.apkPackageName); |
|
252 aOldApp.apkPackageName = aData.apkPackageName; |
|
253 } |
|
254 |
|
255 if (aData.type == "hosted") { |
|
256 let oldManifest = yield DOMApplicationRegistry.getManifestFor(aData.manifestURL); |
|
257 DOMApplicationRegistry.updateHostedApp(aData, aOldApp.id, aOldApp, oldManifest, aData.manifest); |
|
258 } else { |
|
259 DOMApplicationRegistry.updatePackagedApp(aData, aOldApp.id, aOldApp, aData.manifest); |
|
260 } |
|
261 }).bind(this)); }, |
|
262 |
|
263 _checkingForUpdates: false, |
|
264 |
|
265 checkForUpdates: function(userInitiated) { return Task.spawn((function*() { |
|
266 debug("checkForUpdates"); |
|
267 |
|
268 // Don't start checking for updates if we're already doing so. |
|
269 // TODO: Consider cancelling the old one and starting a new one anyway |
|
270 // if the user requested this one. |
|
271 if (this._checkingForUpdates) { |
|
272 debug("already checking for updates"); |
|
273 return; |
|
274 } |
|
275 this._checkingForUpdates = true; |
|
276 |
|
277 try { |
|
278 let installedApps = yield this._getInstalledApps(); |
|
279 if (installedApps.length === 0) { |
|
280 return; |
|
281 } |
|
282 |
|
283 // Map APK names to APK versions. |
|
284 let apkNameToVersion = yield this._getAPKVersions(installedApps.map(app => |
|
285 app.apkPackageName).filter(apkPackageName => !!apkPackageName) |
|
286 ); |
|
287 |
|
288 // Map manifest URLs to APK versions, which is what the service needs |
|
289 // in order to tell us which apps are outdated; and also map them to app |
|
290 // objects, which the downloader/installer uses to download/install APKs. |
|
291 // XXX Will this cause us to update apps without packages, and if so, |
|
292 // does that satisfy the legacy migration story? |
|
293 let manifestUrlToApkVersion = {}; |
|
294 let manifestUrlToApp = {}; |
|
295 for (let app of installedApps) { |
|
296 manifestUrlToApkVersion[app.manifestURL] = apkNameToVersion[app.apkPackageName] || 0; |
|
297 manifestUrlToApp[app.manifestURL] = app; |
|
298 } |
|
299 |
|
300 let outdatedApps = yield this._getOutdatedApps(manifestUrlToApkVersion, userInitiated); |
|
301 |
|
302 if (outdatedApps.length === 0) { |
|
303 // If the user asked us to check for updates, tell 'em we came up empty. |
|
304 if (userInitiated) { |
|
305 this._notify({ |
|
306 title: Strings.GetStringFromName("noUpdatesTitle"), |
|
307 message: Strings.GetStringFromName("noUpdatesMessage"), |
|
308 icon: "drawable://alert_app", |
|
309 }); |
|
310 } |
|
311 return; |
|
312 } |
|
313 |
|
314 let names = [manifestUrlToApp[url].name for (url of outdatedApps)].join(", "); |
|
315 let accepted = yield this._notify({ |
|
316 title: PluralForm.get(outdatedApps.length, Strings.GetStringFromName("downloadUpdateTitle")). |
|
317 replace("#1", outdatedApps.length), |
|
318 message: Strings.formatStringFromName("downloadUpdateMessage", [names], 1), |
|
319 icon: "drawable://alert_app", |
|
320 }).dismissed; |
|
321 |
|
322 if (accepted) { |
|
323 yield this._updateApks([manifestUrlToApp[url] for (url of outdatedApps)]); |
|
324 } |
|
325 } |
|
326 // There isn't a catch block because we want the error to propagate through |
|
327 // the promise chain, so callers can receive it and choose to respond to it. |
|
328 finally { |
|
329 // Ensure we update the _checkingForUpdates flag even if there's an error; |
|
330 // otherwise the process will get stuck and never check for updates again. |
|
331 this._checkingForUpdates = false; |
|
332 } |
|
333 }).bind(this)); }, |
|
334 |
|
335 _getAPKVersions: function(packageNames) { |
|
336 let deferred = Promise.defer(); |
|
337 |
|
338 sendMessageToJava({ |
|
339 type: "Webapps:GetApkVersions", |
|
340 packageNames: packageNames |
|
341 }, data => deferred.resolve(data.versions)); |
|
342 |
|
343 return deferred.promise; |
|
344 }, |
|
345 |
|
346 _getInstalledApps: function() { |
|
347 let deferred = Promise.defer(); |
|
348 DOMApplicationRegistry.getAll(apps => deferred.resolve(apps)); |
|
349 return deferred.promise; |
|
350 }, |
|
351 |
|
352 _getOutdatedApps: function(installedApps, userInitiated) { |
|
353 let deferred = Promise.defer(); |
|
354 |
|
355 let data = JSON.stringify({ installed: installedApps }); |
|
356 |
|
357 let notification; |
|
358 if (userInitiated) { |
|
359 notification = this._notify({ |
|
360 title: Strings.GetStringFromName("checkingForUpdatesTitle"), |
|
361 message: Strings.GetStringFromName("checkingForUpdatesMessage"), |
|
362 // TODO: replace this with an animated icon. |
|
363 icon: "drawable://alert_app", |
|
364 progress: NaN, |
|
365 }); |
|
366 } |
|
367 |
|
368 let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. |
|
369 createInstance(Ci.nsIXMLHttpRequest). |
|
370 QueryInterface(Ci.nsIXMLHttpRequestEventTarget); |
|
371 request.mozBackgroundRequest = true; |
|
372 request.open("POST", Services.prefs.getCharPref(UPDATE_URL_PREF), true); |
|
373 request.channel.loadFlags = Ci.nsIChannel.LOAD_ANONYMOUS | |
|
374 Ci.nsIChannel.LOAD_BYPASS_CACHE | |
|
375 Ci.nsIChannel.INHIBIT_CACHING; |
|
376 request.onload = function() { |
|
377 if (userInitiated) { |
|
378 notification.cancel(); |
|
379 } |
|
380 deferred.resolve(JSON.parse(this.response).outdated); |
|
381 }; |
|
382 request.onerror = function() { |
|
383 if (userInitiated) { |
|
384 notification.cancel(); |
|
385 } |
|
386 deferred.reject(this.status || this.statusText); |
|
387 }; |
|
388 request.setRequestHeader("Content-Type", "application/json"); |
|
389 request.setRequestHeader("Content-Length", data.length); |
|
390 |
|
391 request.send(data); |
|
392 |
|
393 return deferred.promise; |
|
394 }, |
|
395 |
|
396 _updateApks: function(aApps) { return Task.spawn((function*() { |
|
397 // Notify the user that we're in the progress of downloading updates. |
|
398 let downloadingNames = [app.name for (app of aApps)].join(", "); |
|
399 let notification = this._notify({ |
|
400 title: PluralForm.get(aApps.length, Strings.GetStringFromName("downloadingUpdateTitle")). |
|
401 replace("#1", aApps.length), |
|
402 message: Strings.formatStringFromName("downloadingUpdateMessage", [downloadingNames], 1), |
|
403 // TODO: replace this with an animated icon. UpdateService uses |
|
404 // android.R.drawable.stat_sys_download, but I don't think we can reference |
|
405 // a system icon with a drawable: URL here, so we'll have to craft our own. |
|
406 icon: "drawable://alert_download", |
|
407 // TODO: make this a determinate progress indicator once we can determine |
|
408 // the sizes of the APKs and observe their progress. |
|
409 progress: NaN, |
|
410 }); |
|
411 |
|
412 // Download the APKs for the given apps. We do this serially to avoid |
|
413 // saturating the user's network connection. |
|
414 // TODO: download APKs in parallel (or at least more than one at a time) |
|
415 // if it seems reasonable. |
|
416 let downloadedApks = []; |
|
417 let downloadFailedApps = []; |
|
418 for (let app of aApps) { |
|
419 try { |
|
420 let filePath = yield this._downloadApk(app.manifestURL); |
|
421 downloadedApks.push({ app: app, filePath: filePath }); |
|
422 } catch(ex) { |
|
423 downloadFailedApps.push(app); |
|
424 } |
|
425 } |
|
426 |
|
427 notification.cancel(); |
|
428 |
|
429 // Notify the user if any downloads failed, but don't do anything |
|
430 // when the user accepts/cancels the notification. |
|
431 // In the future, we might prompt the user to retry the download. |
|
432 if (downloadFailedApps.length > 0) { |
|
433 let downloadFailedNames = [app.name for (app of downloadFailedApps)].join(", "); |
|
434 this._notify({ |
|
435 title: PluralForm.get(downloadFailedApps.length, Strings.GetStringFromName("downloadFailedTitle")). |
|
436 replace("#1", downloadFailedApps.length), |
|
437 message: Strings.formatStringFromName("downloadFailedMessage", [downloadFailedNames], 1), |
|
438 icon: "drawable://alert_app", |
|
439 }); |
|
440 } |
|
441 |
|
442 // If we weren't able to download any APKs, then there's nothing more to do. |
|
443 if (downloadedApks.length === 0) { |
|
444 return; |
|
445 } |
|
446 |
|
447 // Prompt the user to update the apps for which we downloaded APKs, and wait |
|
448 // until they accept/cancel the notification. |
|
449 let downloadedNames = [apk.app.name for (apk of downloadedApks)].join(", "); |
|
450 let accepted = yield this._notify({ |
|
451 title: PluralForm.get(downloadedApks.length, Strings.GetStringFromName("installUpdateTitle")). |
|
452 replace("#1", downloadedApks.length), |
|
453 message: Strings.formatStringFromName("installUpdateMessage", [downloadedNames], 1), |
|
454 icon: "drawable://alert_app", |
|
455 }).dismissed; |
|
456 |
|
457 if (accepted) { |
|
458 // The user accepted the notification, so install the downloaded APKs. |
|
459 for (let apk of downloadedApks) { |
|
460 let msg = { |
|
461 app: apk.app, |
|
462 // TODO: figure out why Webapps:InstallApk needs the "from" property. |
|
463 from: apk.app.installOrigin, |
|
464 }; |
|
465 sendMessageToJava({ |
|
466 type: "Webapps:InstallApk", |
|
467 filePath: apk.filePath, |
|
468 data: JSON.stringify(msg), |
|
469 }); |
|
470 } |
|
471 } else { |
|
472 // The user cancelled the notification, so remove the downloaded APKs. |
|
473 for (let apk of downloadedApks) { |
|
474 try { |
|
475 yield OS.file.remove(apk.filePath); |
|
476 } catch(ex) { |
|
477 debug("error removing " + apk.filePath + " for cancelled update: " + ex); |
|
478 } |
|
479 } |
|
480 } |
|
481 |
|
482 }).bind(this)); }, |
|
483 |
|
484 _notify: function(aOptions) { |
|
485 dump("_notify: " + aOptions.title); |
|
486 |
|
487 // Resolves to true if the notification is "clicked" (i.e. touched) |
|
488 // and false if the notification is "cancelled" by swiping it away. |
|
489 let dismissed = Promise.defer(); |
|
490 |
|
491 // TODO: make notifications expandable so users can expand them to read text |
|
492 // that gets cut off in standard notifications. |
|
493 let id = Notifications.create({ |
|
494 title: aOptions.title, |
|
495 message: aOptions.message, |
|
496 icon: aOptions.icon, |
|
497 progress: aOptions.progress, |
|
498 onClick: function(aId, aCookie) { |
|
499 dismissed.resolve(true); |
|
500 }, |
|
501 onCancel: function(aId, aCookie) { |
|
502 dismissed.resolve(false); |
|
503 }, |
|
504 }); |
|
505 |
|
506 // Return an object with a promise that resolves when the notification |
|
507 // is dismissed by the user along with a method for cancelling it, |
|
508 // so callers who want to wait for user action can do so, while those |
|
509 // who want to control the notification's lifecycle can do that instead. |
|
510 return { |
|
511 dismissed: dismissed.promise, |
|
512 cancel: function() { |
|
513 Notifications.cancel(id); |
|
514 }, |
|
515 }; |
|
516 }, |
|
517 |
|
518 autoUninstall: function(aData) { |
|
519 DOMApplicationRegistry.registryReady.then(() => { |
|
520 for (let id in DOMApplicationRegistry.webapps) { |
|
521 let app = DOMApplicationRegistry.webapps[id]; |
|
522 if (aData.apkPackageNames.indexOf(app.apkPackageName) > -1) { |
|
523 debug("attempting to uninstall " + app.name); |
|
524 DOMApplicationRegistry.uninstall( |
|
525 app.manifestURL, |
|
526 function() { |
|
527 debug("success uninstalling " + app.name); |
|
528 }, |
|
529 function(error) { |
|
530 debug("error uninstalling " + app.name + ": " + error); |
|
531 } |
|
532 ); |
|
533 } |
|
534 } |
|
535 }); |
|
536 }, |
|
537 |
|
538 writeDefaultPrefs: function(aProfile, aManifest) { |
|
539 // build any app specific default prefs |
|
540 let prefs = []; |
|
541 if (aManifest.orientation) { |
|
542 let orientation = aManifest.orientation; |
|
543 if (Array.isArray(orientation)) { |
|
544 orientation = orientation.join(","); |
|
545 } |
|
546 prefs.push({ name: "app.orientation.default", value: orientation }); |
|
547 } |
|
548 |
|
549 // write them into the app profile |
|
550 let defaultPrefsFile = aProfile.clone(); |
|
551 defaultPrefsFile.append(this.DEFAULT_PREFS_FILENAME); |
|
552 this._writeData(defaultPrefsFile, prefs); |
|
553 }, |
|
554 |
|
555 _writeData: function(aFile, aPrefs) { |
|
556 if (aPrefs.length > 0) { |
|
557 let array = new TextEncoder().encode(JSON.stringify(aPrefs)); |
|
558 OS.File.writeAtomic(aFile.path, array, { tmpPath: aFile.path + ".tmp" }).then(null, function onError(reason) { |
|
559 debug("Error writing default prefs: " + reason); |
|
560 }); |
|
561 } |
|
562 }, |
|
563 |
|
564 DEFAULT_PREFS_FILENAME: "default-prefs.js", |
|
565 |
|
566 }; |