Source: lockscreen/js/wallpaper_manager.js

  1. /* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
  2. /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
  3. /* global ImageUtils, LazyLoader */
  4. 'use strict';
  5. (function(exports) {
  6. const WALLPAPER_KEY = 'wallpaper.image';
  7. const WALLPAPER_VALID_KEY = 'wallpaper.image.valid';
  8. const DEFAULT_WALLPAPER_URL = '/resources/images/backgrounds/default.png';
  9. /**
  10. * This system module reads the system wallpaper setting on startup
  11. * and monitors changes to that setting, broadcasting a
  12. * 'wallpaperchange' event with a blob: URL to tell the system and
  13. * lockscreen about the new wallpaper.
  14. *
  15. * If the wallpaper value read from the settings DB is a URL this
  16. * module converts it to a blob. If the wallpaper image does not
  17. * exactly match the size of the screen, this module resizes it
  18. * (lazy-loading shared/js/image_utils.js when needed). If the
  19. * wallpaper value is converted to a blob or resized, the modified
  20. * value is saved back to the settings DB so that it will not need
  21. * to be modified the next time it is read.
  22. *
  23. * start(), stop(), and getBlobURL() are the only public methods,
  24. * and stop() is only exposed for the benefit of unit
  25. * tests. _setWallpaper() is called on startup and whenever the
  26. * wallpaper.image setting changes. Each call to _setWallpaper()
  27. * eventually causes a call to _publish() which broadcasts the new
  28. * wallpaper event to the lockscreen and the rest of the system
  29. * app. The call to _publish() does not always happen directly,
  30. * however: _setWallpaper() may call _checkSize(), which calls
  31. * _publish(), or it may call _toBlob() which calls _checkSize().
  32. * Unless the build is mis-configured and the wallpaper in the
  33. * settings db and the fallback default wallpaper is broken, every
  34. * call to _setWallpaper() ends up broadcasting a 'wallpaperchange'
  35. * event with a valid blob: url for a wallpaper image that has the
  36. * same size as the screen.
  37. *
  38. * @class WallpaperManager
  39. */
  40. function WallpaperManager() {
  41. this._started = false;
  42. this._blobURL = null;
  43. }
  44. WallpaperManager.prototype = {
  45. /**
  46. * Bootstrap the module. Read the current wallpaper from the
  47. * settings db and pass it to _setWallpaper(). Also listen for
  48. * changes to the wallpaper and invoke _setWallpaper() for each
  49. * one.
  50. */
  51. start: function() {
  52. if (this._started) {
  53. throw 'Instance should not be start()\'ed twice.';
  54. }
  55. this._started = true;
  56. debug('started');
  57. // Query the wallpaper
  58. var lock = navigator.mozSettings.createLock();
  59. var query = lock.get(WALLPAPER_KEY);
  60. query.onsuccess = function() {
  61. var wallpaper = query.result[WALLPAPER_KEY];
  62. if (!wallpaper) {
  63. debug('no wallpaper found at startup; using default');
  64. this._setWallpaper(DEFAULT_WALLPAPER_URL);
  65. }
  66. else if (wallpaper instanceof Blob) {
  67. // If the wallpaper is a blob, first go see if we have already
  68. // validated it size. Because if we have, we don't have to check
  69. // the size again or even load the code to check its size.
  70. var query2 = lock.get(WALLPAPER_VALID_KEY);
  71. query2.onsuccess = function() {
  72. var valid = query2.result[WALLPAPER_VALID_KEY];
  73. this._setWallpaper(wallpaper, valid);
  74. }.bind(this);
  75. }
  76. else {
  77. // If the wallpaper is not a blob, just pass it to _setWallpaper
  78. // and try to convert it to a blob there.
  79. this._setWallpaper(wallpaper);
  80. }
  81. }.bind(this);
  82. // And register a listener so we'll be notified of future changes
  83. // to the wallpaper
  84. this.observer = function(e) {
  85. this._setWallpaper(e.settingValue);
  86. }.bind(this);
  87. navigator.mozSettings.addObserver(WALLPAPER_KEY, this.observer);
  88. },
  89. /**
  90. * Stop the module an stop listening for changes to the wallpaper setting.
  91. * This method is only used by unit tests.
  92. */
  93. stop: function() {
  94. if (!this._started) { return; }
  95. navigator.mozSettings.removeObserver(WALLPAPER_KEY, this.observer);
  96. this._started = false;
  97. },
  98. /**
  99. * Return the blob URL saved from earlier wallpaper change event
  100. * The lockscreen may miss the event and needs to look the URL up here.
  101. * @returns {String} the blob URL
  102. */
  103. getBlobURL: function() {
  104. if (!this._started) { return; }
  105. return this._blobURL;
  106. },
  107. //
  108. // This method is called on startup and when the wallpaper
  109. // changes. It always causes _publish() to be invoked and a
  110. // "wallpaperchange" event to be broadcast to interested
  111. // listeners. If the new value is a blob that is already
  112. // validated, then _publish() is called directly. Otherwise, it is
  113. // called indirectly by _toBlob() or _checkSize().
  114. //
  115. _setWallpaper: function(value, valid) {
  116. if (!this._started) { return; }
  117. // If we are called because we just saved a resized blob back
  118. // to the settings db, then ignore the call.
  119. if (value instanceof Blob && value.size === this.savedBlobSize) {
  120. this.savedBlobSize = false;
  121. return;
  122. }
  123. debug('new wallpaper', valid ? 'size already validated' : '');
  124. if (typeof value === 'string') {
  125. this._toBlob(value);
  126. }
  127. else if (value instanceof Blob) {
  128. // If this blob has already been validated, we can just display it.
  129. // Otherwise we need to check its size first
  130. if (valid) {
  131. this._publish(value);
  132. }
  133. else {
  134. this._checkSize(value);
  135. }
  136. }
  137. else {
  138. // The value in the settings database is invalid, so
  139. // use the default image. Note that this will update the
  140. // settings db with a valid value.
  141. debug('Invalid wallpaper value in settings;',
  142. 'reverting to default wallpaper.');
  143. this._toBlob(DEFAULT_WALLPAPER_URL);
  144. }
  145. },
  146. //
  147. // This method expects a wallpaper URL (possibly a data: URL) and
  148. // uses XHR to convert it to a blob. If it succeeds, it passes the
  149. // blob to _checkSize() which resizes it if needed and calls
  150. // _publish() to broadcast the new wallpaper.
  151. //
  152. _toBlob: function(url) {
  153. if (!this._started) { return; }
  154. debug('converting wallpaper url to blob');
  155. // If we trying to convert the default wallpaper url to a blob
  156. // note that because there is some error recovery code that behaves
  157. // differently in that last resort case.
  158. this.tryingDefaultWallpaper = (url === DEFAULT_WALLPAPER_URL);
  159. // If the settings db had a string in it we assume it is a
  160. // relative url or data: url and try to read it with XHR.
  161. var xhr = new XMLHttpRequest();
  162. xhr.open('GET', url);
  163. xhr.responseType = 'blob';
  164. xhr.send();
  165. xhr.onload = function() {
  166. // Once we've loaded the wallpaper as a blob, verify its size.
  167. // We pass true as the second argument to force it to be saved
  168. // back to the db (as a blob) even if the size is okay.
  169. this._checkSize(xhr.response, true);
  170. }.bind(this);
  171. xhr.onerror = function() {
  172. // If we couldn't load the url and if it was something other
  173. // than the default wallpaper url, then try again with the default.
  174. if (!this.tryingDefaultWallpaper) {
  175. debug('corrupt wallpaper url in settings;',
  176. 'reverting to default wallpaper');
  177. this._toBlob(DEFAULT_WALLPAPER_URL);
  178. }
  179. else {
  180. // This was our last resort, and it failed, so no wallpaper
  181. // image is available.
  182. console.error('Cannot load wallpaper from', url);
  183. }
  184. }.bind(this);
  185. },
  186. //
  187. // This method checks the dimensions of the image blob and crops
  188. // and resizes the image if necessary so that it is exactly the
  189. // same size as the screen. If the image was resized, or if it was
  190. // read from a URL, then this method saves the new blob back to
  191. // the settings db and marks it as valid. If the image was not
  192. // resized, then the image is marked as valid so that the check
  193. // does not need to be performed when the phone is rebooted. In
  194. // either case, after the image is saved and/or validated, this
  195. // method calls _publish() to broadcast the new wallpaper.
  196. //
  197. // If the blob does not hold a valid image, that will be
  198. // discovered while attempting to check its size and in that case,
  199. // this method falls back on the default wallpaper by calling
  200. // _toBlob() with the default wallpaper URL.
  201. //
  202. // This method lazy-loads ImageUtils from shared/js/image_utils.js.
  203. // Once a wallpaper has had its size checked once, it is marked as
  204. // valid in the settings db, so these image utilities will not
  205. // need to be loaded into the system app on subsequent reboots.
  206. //
  207. _checkSize: function(blob, needsToBeSaved) {
  208. if (!this._started) { return; }
  209. debug('resizing wallpaper if needed');
  210. // How big (in device pixels) is the screen?
  211. var screenWidth = Math.ceil(screen.width * window.devicePixelRatio);
  212. var screenHeight = Math.ceil(screen.height * window.devicePixelRatio);
  213. // For performance we need to guarantee that the size of the wallpaper
  214. // is exactly the same as the size of the screen. LazyLoad the
  215. // ImageUtils module, and call its resizeAndCropToCover() method to
  216. // resize and crop the image as needed so that it is the right size.
  217. // Note that this utility funtion can determine the size of an image
  218. // without decoding it and if the image is already the right size
  219. // it will not modify it.
  220. LazyLoader.load('shared/js/image_utils.js', function() {
  221. ImageUtils
  222. .resizeAndCropToCover(blob, screenWidth, screenHeight, ImageUtils.PNG)
  223. .then(
  224. function resolve(resizedBlob) {
  225. // If the blob changed or if the second argument was true
  226. // then we need to save the blob back to the settings db
  227. if (resizedBlob !== blob || needsToBeSaved) {
  228. this._save(resizedBlob);
  229. }
  230. else {
  231. // If the blob didn't change we don't have to save it,
  232. // but we do need to mark it as valid
  233. this._validate();
  234. }
  235. // Display the wallpaper
  236. this._publish(resizedBlob);
  237. }.bind(this),
  238. function reject(error) {
  239. // This will only happen if the settings db contains a blob that
  240. // is not actually an image. If that happens for some reason,
  241. // fall back on the default wallpaper.
  242. if (!this.tryingDefaultWallpaper) {
  243. debug('Corrupt wallpaper image in settings;',
  244. 'reverting to default wallpaper.');
  245. this._toBlob(DEFAULT_WALLPAPER_URL);
  246. }
  247. else {
  248. // We were already trying the default wallpaper and it failed.
  249. // So we just give up in this case.
  250. console.error('Default wallpaper image is invalid');
  251. }
  252. }.bind(this)
  253. );
  254. }.bind(this));
  255. },
  256. //
  257. // This method sets a property in the settings db to indicate that
  258. // the current wallpaper is the same size as the screen. Setting
  259. // this property is an optimization that allows us to skip the
  260. // call to _checkSize() on subsequent startups. This method
  261. // returns synchronously and does not wait for the settings db
  262. // operation to complete.
  263. //
  264. _validate: function() {
  265. if (!this._started) { return; }
  266. debug('marking wallpaper as valid');
  267. var settings = {};
  268. settings[WALLPAPER_VALID_KEY] = true; // We've checked its size
  269. navigator.mozSettings.createLock().set(settings);
  270. },
  271. //
  272. // This method saves the wallpaper blob to the settings db and
  273. // also marks it as valid so that we know on subsequent startups
  274. // that its size has already been checked. This method returns
  275. // synchronously and does not wait for the settings db operation
  276. // to complete.
  277. //
  278. _save: function(blob) {
  279. if (!this._started) { return; }
  280. debug('saving converted or resized wallpaper to settings');
  281. // Set a flag so that we don't repeat this whole process when
  282. // we're notified about this save. The flag contains the size of
  283. // the blob we're saving so it is very unlikely that we'll have
  284. // a race condition.
  285. this.savedBlobSize = blob.size;
  286. // Now save the blob to the settings db, and also save a flag
  287. // that indicates that we've already checked the size of the image.
  288. // This allows us to skip the check at boot time.
  289. var settings = {};
  290. settings[WALLPAPER_KEY] = blob;
  291. settings[WALLPAPER_VALID_KEY] = true; // We've checked its size
  292. navigator.mozSettings.createLock().set(settings);
  293. },
  294. //
  295. // This method creates a blob: URL for the specfied blob and publishes
  296. // the URL via a 'wallpaperchange' event. If there was a previous
  297. // wallpaper, its blob: URL is revoked. This method is synchronous.
  298. //
  299. _publish: function(blob) {
  300. if (!this._started) { return; }
  301. debug('publishing wallpaperchange event');
  302. // If we have a blob:// url for previous wallpaper, release it now
  303. if (this._blobURL) {
  304. URL.revokeObjectURL(this._blobURL);
  305. }
  306. // Create a new blob:// url for this blob
  307. this._blobURL = URL.createObjectURL(blob);
  308. // And tell the system about it.
  309. window.dispatchEvent(new CustomEvent('wallpaperchange',
  310. { detail: { url: this._blobURL } }));
  311. }
  312. };
  313. // Log debug messages
  314. function debug(...args) {
  315. if (WallpaperManager.DEBUG) {
  316. args.unshift('[WallpaperManager]');
  317. console.log.apply(console, args);
  318. }
  319. }
  320. WallpaperManager.DEBUG = false; // Set to false to silence debug output
  321. /** @exports WallpaperManager */
  322. exports.WallpaperManager = WallpaperManager;
  323. }(window));