Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / wallpaper_manager / js / wallpaper_manager.js
blobca52a508185b2c24cb9b50c1df61e003923315b9
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6  * WallpaperManager constructor.
7  *
8  * WallpaperManager objects encapsulate the functionality of the wallpaper
9  * manager extension.
10  *
11  * @constructor
12  * @param {HTMLElement} dialogDom The DOM node containing the prototypical
13  *     extension UI.
14  */
16 function WallpaperManager(dialogDom) {
17   this.dialogDom_ = dialogDom;
18   this.document_ = dialogDom.ownerDocument;
19   this.enableOnlineWallpaper_ = loadTimeData.valueExists('manifestBaseURL');
20   this.selectedCategory = null;
21   this.selectedItem_ = null;
22   this.progressManager_ = new ProgressManager();
23   this.customWallpaperData_ = null;
24   this.currentWallpaper_ = null;
25   this.wallpaperRequest_ = null;
26   this.wallpaperDirs_ = WallpaperDirectories.getInstance();
27   this.preManifestDomInit_();
28   this.fetchManifest_();
31 // Anonymous 'namespace'.
32 // TODO(bshe): Get rid of anonymous namespace.
33 (function() {
35   /**
36    * URL of the learn more page for wallpaper picker.
37    */
38   /** @const */ var LearnMoreURL =
39       'https://support.google.com/chromeos/?p=wallpaper_fileerror&hl=' +
40           navigator.language;
42   /**
43    * Index of the All category. It is the first category in wallpaper picker.
44    */
45   /** @const */ var AllCategoryIndex = 0;
47   /**
48    * Index offset of categories parsed from manifest. The All category is added
49    * before them. So the offset is 1.
50    */
51   /** @const */ var OnlineCategoriesOffset = 1;
53   /**
54    * Returns a translated string.
55    *
56    * Wrapper function to make dealing with translated strings more concise.
57    * Equivilant to localStrings.getString(id).
58    *
59    * @param {string} id The id of the string to return.
60    * @return {string} The translated string.
61    */
62   function str(id) {
63     return loadTimeData.getString(id);
64   }
66   /**
67    * Retruns the current selected layout.
68    * @return {string} The selected layout.
69    */
70   function getSelectedLayout() {
71     var setWallpaperLayout = $('set-wallpaper-layout');
72     return setWallpaperLayout.options[setWallpaperLayout.selectedIndex].value;
73   }
75   /**
76    * Loads translated strings.
77    */
78   WallpaperManager.initStrings = function(callback) {
79     chrome.wallpaperPrivate.getStrings(function(strings) {
80       loadTimeData.data = strings;
81       if (callback)
82         callback();
83     });
84   };
86   /**
87    * Requests wallpaper manifest file from server.
88    */
89   WallpaperManager.prototype.fetchManifest_ = function() {
90     var locale = navigator.language;
91     if (!this.enableOnlineWallpaper_) {
92       this.postManifestDomInit_();
93       return;
94     }
96     var urls = [
97         str('manifestBaseURL') + locale + '.json',
98         // Fallback url. Use 'en' locale by default.
99         str('manifestBaseURL') + 'en.json'];
101     var asyncFetchManifestFromUrls = function(urls, func, successCallback,
102                                               failureCallback) {
103       var index = 0;
104       var loop = {
105         next: function() {
106           if (index < urls.length) {
107             func(loop, urls[index]);
108             index++;
109           } else {
110             failureCallback();
111           }
112         },
114         success: function(response) {
115           successCallback(response);
116         },
118         failure: function() {
119           failureCallback();
120         }
121       };
122       loop.next();
123     };
125     var fetchManifestAsync = function(loop, url) {
126       var xhr = new XMLHttpRequest();
127       try {
128         xhr.addEventListener('loadend', function(e) {
129           if (this.status == 200 && this.responseText != null) {
130             try {
131               var manifest = JSON.parse(this.responseText);
132               loop.success(manifest);
133             } catch (e) {
134               loop.failure();
135             }
136           } else {
137             loop.next();
138           }
139         });
140         xhr.open('GET', url, true);
141         xhr.send(null);
142       } catch (e) {
143         loop.failure();
144       }
145     };
147     if (navigator.onLine) {
148       asyncFetchManifestFromUrls(urls, fetchManifestAsync,
149                                  this.onLoadManifestSuccess_.bind(this),
150                                  this.onLoadManifestFailed_.bind(this));
151     } else {
152       // If device is offline, fetches manifest from local storage.
153       // TODO(bshe): Always loading the offline manifest first and replacing
154       // with the online one when available.
155       this.onLoadManifestFailed_();
156     }
157   };
159   /**
160    * Shows error message in a centered dialog.
161    * @private
162    * @param {string} errroMessage The string to show in the error dialog.
163    */
164   WallpaperManager.prototype.showError_ = function(errorMessage) {
165     document.querySelector('.error-message').textContent = errorMessage;
166     $('error-container').hidden = false;
167   };
169   /**
170    * Sets manifest loaded from server. Called after manifest is successfully
171    * loaded.
172    * @param {object} manifest The parsed manifest file.
173    */
174   WallpaperManager.prototype.onLoadManifestSuccess_ = function(manifest) {
175     this.manifest_ = manifest;
176     WallpaperUtil.saveToStorage(Constants.AccessManifestKey, manifest, false);
177     this.postManifestDomInit_();
178   };
180   // Sets manifest to previously saved object if any and shows connection error.
181   // Called after manifest failed to load.
182   WallpaperManager.prototype.onLoadManifestFailed_ = function() {
183     var accessManifestKey = Constants.AccessManifestKey;
184     var self = this;
185     Constants.WallpaperLocalStorage.get(accessManifestKey, function(items) {
186       self.manifest_ = items[accessManifestKey] ? items[accessManifestKey] : {};
187       self.showError_(str('connectionFailed'));
188       self.postManifestDomInit_();
189       $('wallpaper-grid').classList.add('image-picker-offline');
190     });
191   };
193   /**
194    * Toggle surprise me feature of wallpaper picker. It fires an storage
195    * onChanged event. Event handler for that event is in event_page.js.
196    * @private
197    */
198   WallpaperManager.prototype.toggleSurpriseMe_ = function() {
199     var checkbox = $('surprise-me').querySelector('#checkbox');
200     var shouldEnable = !checkbox.classList.contains('checked');
201     WallpaperUtil.saveToStorage(Constants.AccessSurpriseMeEnabledKey,
202                                 shouldEnable, false, function() {
203       if (chrome.runtime.lastError == null) {
204           if (shouldEnable) {
205             checkbox.classList.add('checked');
206           } else {
207             checkbox.classList.remove('checked');
208           }
209           $('categories-list').disabled = shouldEnable;
210           $('wallpaper-grid').disabled = shouldEnable;
211         } else {
212           // TODO(bshe): show error message to user.
213           console.error('Failed to save surprise me option to chrome storage.');
214         }
215     });
216   };
218   /**
219    * One-time initialization of various DOM nodes. Fetching manifest may take a
220    * long time due to slow connection. Dom nodes that do not depend on manifest
221    * should be initialized here to unblock from manifest fetching.
222    */
223   WallpaperManager.prototype.preManifestDomInit_ = function() {
224     $('window-close-button').addEventListener('click', function() {
225       window.close();
226     });
227     this.document_.defaultView.addEventListener(
228         'resize', this.onResize_.bind(this));
229     this.document_.defaultView.addEventListener(
230         'keydown', this.onKeyDown_.bind(this));
231     $('learn-more').href = LearnMoreURL;
232     $('close-error').addEventListener('click', function() {
233       $('error-container').hidden = true;
234     });
235     $('close-wallpaper-selection').addEventListener('click', function() {
236       $('wallpaper-selection-container').hidden = true;
237       $('set-wallpaper-layout').disabled = true;
238     });
239   };
241   /**
242    * One-time initialization of various DOM nodes. Dom nodes that do depend on
243    * manifest should be initialized here.
244    */
245   WallpaperManager.prototype.postManifestDomInit_ = function() {
246     i18nTemplate.process(this.document_, loadTimeData);
247     this.initCategoriesList_();
248     this.initThumbnailsGrid_();
249     this.presetCategory_();
251     $('file-selector').addEventListener(
252         'change', this.onFileSelectorChanged_.bind(this));
253     $('set-wallpaper-layout').addEventListener(
254         'change', this.onWallpaperLayoutChanged_.bind(this));
256     if (this.enableOnlineWallpaper_) {
257       var self = this;
258       $('surprise-me').hidden = false;
259       $('surprise-me').addEventListener('click',
260                                         this.toggleSurpriseMe_.bind(this));
261       Constants.WallpaperLocalStorage.get(Constants.AccessSurpriseMeEnabledKey,
262                                           function(items) {
263         if (items[Constants.AccessSurpriseMeEnabledKey]) {
264           $('surprise-me').querySelector('#checkbox').classList.add('checked');
265           $('categories-list').disabled = true;
266           $('wallpaper-grid').disabled = true;
267         }
268       });
270       window.addEventListener('offline', function() {
271         chrome.wallpaperPrivate.getOfflineWallpaperList(function(lists) {
272           if (!self.downloadedListMap_)
273             self.downloadedListMap_ = {};
274           for (var i = 0; i < lists.length; i++) {
275             self.downloadedListMap_[lists[i]] = true;
276           }
277           var thumbnails = self.document_.querySelectorAll('.thumbnail');
278           for (var i = 0; i < thumbnails.length; i++) {
279             var thumbnail = thumbnails[i];
280             var url = self.wallpaperGrid_.dataModel.item(i).baseURL;
281             var fileName = url.substring(url.lastIndexOf('/') + 1) +
282                 Constants.HighResolutionSuffix;
283             if (self.downloadedListMap_ &&
284                 self.downloadedListMap_.hasOwnProperty(encodeURI(fileName))) {
285               thumbnail.offline = true;
286             }
287           }
288         });
289         $('wallpaper-grid').classList.add('image-picker-offline');
290       });
291       window.addEventListener('online', function() {
292         self.downloadedListMap_ = null;
293         $('wallpaper-grid').classList.remove('image-picker-offline');
294       });
295     }
297     this.onResize_();
298     this.initContextMenuAndCommand_();
299   };
301   /**
302    * One-time initialization of context menu and command.
303    */
304   WallpaperManager.prototype.initContextMenuAndCommand_ = function() {
305     this.wallpaperContextMenu_ = $('wallpaper-context-menu');
306     cr.ui.Menu.decorate(this.wallpaperContextMenu_);
307     cr.ui.contextMenuHandler.setContextMenu(this.wallpaperGrid_,
308                                             this.wallpaperContextMenu_);
309     var commands = this.dialogDom_.querySelectorAll('command');
310     for (var i = 0; i < commands.length; i++)
311       cr.ui.Command.decorate(commands[i]);
313     var doc = this.document_;
314     doc.addEventListener('command', this.onCommand_.bind(this));
315     doc.addEventListener('canExecute', this.onCommandCanExecute_.bind(this));
316   };
318   /**
319    * Handles a command being executed.
320    * @param {Event} event A command event.
321    */
322   WallpaperManager.prototype.onCommand_ = function(event) {
323     if (event.command.id == 'delete') {
324       var wallpaperGrid = this.wallpaperGrid_;
325       var selectedIndex = wallpaperGrid.selectionModel.selectedIndex;
326       var item = wallpaperGrid.dataModel.item(selectedIndex);
327       if (!item || item.source != Constants.WallpaperSourceEnum.Custom)
328         return;
329       this.removeCustomWallpaper(item.baseURL);
330       wallpaperGrid.dataModel.splice(selectedIndex, 1);
331       // Calculate the number of remaining custom wallpapers. The add new button
332       // in data model needs to be excluded.
333       var customWallpaperCount = wallpaperGrid.dataModel.length - 1;
334       if (customWallpaperCount == 0) {
335         // Active custom wallpaper is also copied in chronos data dir. It needs
336         // to be deleted.
337         chrome.wallpaperPrivate.resetWallpaper();
338       } else {
339         selectedIndex = Math.min(selectedIndex, customWallpaperCount - 1);
340         wallpaperGrid.selectionModel.selectedIndex = selectedIndex;
341       }
342       event.cancelBubble = true;
343     }
344   };
346   /**
347    * Decides if a command can be executed on current target.
348    * @param {Event} event A command event.
349    */
350   WallpaperManager.prototype.onCommandCanExecute_ = function(event) {
351     switch (event.command.id) {
352       case 'delete':
353         var wallpaperGrid = this.wallpaperGrid_;
354         var selectedIndex = wallpaperGrid.selectionModel.selectedIndex;
355         var item = wallpaperGrid.dataModel.item(selectedIndex);
356         if (selectedIndex != this.wallpaperGrid_.dataModel.length - 1 &&
357           item && item.source == Constants.WallpaperSourceEnum.Custom) {
358           event.canExecute = true;
359           break;
360         }
361       default:
362         event.canExecute = false;
363     }
364   };
366   /**
367    * Preset to the category which contains current wallpaper.
368    */
369   WallpaperManager.prototype.presetCategory_ = function() {
370     this.currentWallpaper_ = str('currentWallpaper');
371     // The currentWallpaper_ is either a url contains HightResolutionSuffix or a
372     // custom wallpaper file name converted from an integer value represent
373     // time (e.g., 13006377367586070).
374     if (!this.enableOnlineWallpaper_ || (this.currentWallpaper_ &&
375         this.currentWallpaper_.indexOf(Constants.HighResolutionSuffix) == -1)) {
376       // Custom is the last one in the categories list.
377       this.categoriesList_.selectionModel.selectedIndex =
378           this.categoriesList_.dataModel.length - 1;
379       return;
380     }
381     var self = this;
382     var presetCategoryInner_ = function() {
383       // Selects the first category in the categories list of current
384       // wallpaper as the default selected category when showing wallpaper
385       // picker UI.
386       var presetCategory = AllCategoryIndex;
387       if (self.currentWallpaper_) {
388         for (var key in self.manifest_.wallpaper_list) {
389           var url = self.manifest_.wallpaper_list[key].base_url +
390               Constants.HighResolutionSuffix;
391           if (url.indexOf(self.currentWallpaper_) != -1 &&
392               self.manifest_.wallpaper_list[key].categories.length > 0) {
393             presetCategory = self.manifest_.wallpaper_list[key].categories[0] +
394                 OnlineCategoriesOffset;
395             break;
396           }
397         }
398       }
399       self.categoriesList_.selectionModel.selectedIndex = presetCategory;
400     };
401     if (navigator.onLine) {
402       presetCategoryInner_();
403     } else {
404       // If device is offline, gets the available offline wallpaper list first.
405       // Wallpapers which are not in the list will display a grayscaled
406       // thumbnail.
407       chrome.wallpaperPrivate.getOfflineWallpaperList(function(lists) {
408         if (!self.downloadedListMap_)
409           self.downloadedListMap_ = {};
410         for (var i = 0; i < lists.length; i++)
411           self.downloadedListMap_[lists[i]] = true;
412         presetCategoryInner_();
413       });
414     }
415   };
417   /**
418    * Constructs the thumbnails grid.
419    */
420   WallpaperManager.prototype.initThumbnailsGrid_ = function() {
421     this.wallpaperGrid_ = $('wallpaper-grid');
422     wallpapers.WallpaperThumbnailsGrid.decorate(this.wallpaperGrid_);
423     this.wallpaperGrid_.autoExpands = true;
425     this.wallpaperGrid_.addEventListener('change', this.onChange_.bind(this));
426     this.wallpaperGrid_.addEventListener('dblclick', this.onClose_.bind(this));
427   };
429   /**
430    * Handles change event dispatched by wallpaper grid.
431    */
432   WallpaperManager.prototype.onChange_ = function() {
433     // splice may dispatch a change event because the position of selected
434     // element changing. But the actual selected element may not change after
435     // splice. Check if the new selected element equals to the previous selected
436     // element before continuing. Otherwise, wallpaper may reset to previous one
437     // as described in http://crbug.com/229036.
438     if (this.selectedItem_ == this.wallpaperGrid_.selectedItem)
439       return;
440     this.selectedItem_ = this.wallpaperGrid_.selectedItem;
441     this.onSelectedItemChanged_();
442   };
444   /**
445    * Closes window if no pending wallpaper request.
446    */
447   WallpaperManager.prototype.onClose_ = function() {
448     if (this.wallpaperRequest_) {
449       this.wallpaperRequest_.addEventListener('loadend', function() {
450         // Close window on wallpaper loading finished.
451         window.close();
452       });
453     } else {
454       window.close();
455     }
456   };
458   /**
459     * Sets wallpaper to the corresponding wallpaper of selected thumbnail.
460     * @param {{baseURL: string, layout: string, source: string,
461     *          availableOffline: boolean, opt_dynamicURL: string,
462     *          opt_author: string, opt_authorWebsite: string}}
463     *     selectedItem the selected item in WallpaperThumbnailsGrid's data
464     *     model.
465     */
466   WallpaperManager.prototype.setSelectedWallpaper_ = function(selectedItem) {
467     var self = this;
468     switch (selectedItem.source) {
469       case Constants.WallpaperSourceEnum.Custom:
470         var errorHandler = this.onFileSystemError_.bind(this);
471         var setActive = function() {
472           self.wallpaperGrid_.activeItem = selectedItem;
473           self.currentWallpaper_ = selectedItem.baseURL;
474         };
475         var success = function(dirEntry) {
476           dirEntry.getFile(selectedItem.baseURL, {create: false},
477                            function(fileEntry) {
478             fileEntry.file(function(file) {
479               var reader = new FileReader();
480               reader.readAsArrayBuffer(file);
481               reader.addEventListener('error', errorHandler);
482               reader.addEventListener('load', function(e) {
483                 self.setCustomWallpaper(e.target.result,
484                                         selectedItem.layout,
485                                         false, selectedItem.baseURL,
486                                         setActive, errorHandler);
487               });
488             }, errorHandler);
489           }, errorHandler);
490         }
491         this.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.ORIGINAL,
492                                          success, errorHandler);
493         break;
494       case Constants.WallpaperSourceEnum.OEM:
495         // Resets back to default wallpaper.
496         chrome.wallpaperPrivate.resetWallpaper();
497         this.currentWallpaper_ = selectedItem.baseURL;
498         this.wallpaperGrid_.activeItem = selectedItem;
499         WallpaperUtil.saveWallpaperInfo(wallpaperURL, selectedItem.layout,
500                                         selectedItem.source);
501         break;
502       case Constants.WallpaperSourceEnum.Online:
503         var wallpaperURL = selectedItem.baseURL +
504             Constants.HighResolutionSuffix;
505         var selectedGridItem = this.wallpaperGrid_.getListItem(selectedItem);
507         chrome.wallpaperPrivate.setWallpaperIfExists(wallpaperURL,
508                                                      selectedItem.layout,
509                                                      function(exists) {
510           if (exists) {
511             self.currentWallpaper_ = wallpaperURL;
512             self.wallpaperGrid_.activeItem = selectedItem;
513             WallpaperUtil.saveWallpaperInfo(wallpaperURL, selectedItem.layout,
514                                             selectedItem.source);
515             return;
516           }
518           // Falls back to request wallpaper from server.
519           if (self.wallpaperRequest_)
520             self.wallpaperRequest_.abort();
522           self.wallpaperRequest_ = new XMLHttpRequest();
523           self.progressManager_.reset(self.wallpaperRequest_, selectedGridItem);
525           var onSuccess = function(xhr) {
526             var image = xhr.response;
527             chrome.wallpaperPrivate.setWallpaper(image, selectedItem.layout,
528                 wallpaperURL,
529                 self.onFinished_.bind(self, selectedGridItem, selectedItem));
530             self.currentWallpaper_ = wallpaperURL;
531             WallpaperUtil.saveWallpaperInfo(wallpaperURL, selectedItem.layout,
532                                             selectedItem.source);
533             self.wallpaperRequest_ = null;
534           };
535           var onFailure = function() {
536             self.progressManager_.hideProgressBar(selectedGridItem);
537             self.showError_(str('downloadFailed'));
538             self.wallpaperRequest_ = null;
539           };
540           WallpaperUtil.fetchURL(wallpaperURL, 'arraybuffer', onSuccess,
541                                  onFailure, self.wallpaperRequest_);
542         });
543         break;
544       default:
545         console.error('Unsupported wallpaper source.');
546     }
547   };
549   /*
550    * Removes the oldest custom wallpaper. If the oldest one is set as current
551    * wallpaper, removes the second oldest one to free some space. This should
552    * only be called when exceeding wallpaper quota.
553    */
554   WallpaperManager.prototype.removeOldestWallpaper_ = function() {
555     // Custom wallpapers should already sorted when put to the data model. The
556     // last element is the add new button, need to exclude it as well.
557     var oldestIndex = this.wallpaperGrid_.dataModel.length - 2;
558     var item = this.wallpaperGrid_.dataModel.item(oldestIndex);
559     if (!item || item.source != Constants.WallpaperSourceEnum.Custom)
560       return;
561     if (item.baseURL == this.currentWallpaper_)
562       item = this.wallpaperGrid_.dataModel.item(--oldestIndex);
563     if (item) {
564       this.removeCustomWallpaper(item.baseURL);
565       this.wallpaperGrid_.dataModel.splice(oldestIndex, 1);
566     }
567   };
569   /*
570    * Shows an error message to user and log the failed reason in console.
571    */
572   WallpaperManager.prototype.onFileSystemError_ = function(e) {
573     var msg = '';
574     switch (e.code) {
575       case FileError.QUOTA_EXCEEDED_ERR:
576         msg = 'QUOTA_EXCEEDED_ERR';
577         // Instead of simply remove oldest wallpaper, we should consider a
578         // better way to handle this situation. See crbug.com/180890.
579         this.removeOldestWallpaper_();
580         break;
581       case FileError.NOT_FOUND_ERR:
582         msg = 'NOT_FOUND_ERR';
583         break;
584       case FileError.SECURITY_ERR:
585         msg = 'SECURITY_ERR';
586         break;
587       case FileError.INVALID_MODIFICATION_ERR:
588         msg = 'INVALID_MODIFICATION_ERR';
589         break;
590       case FileError.INVALID_STATE_ERR:
591         msg = 'INVALID_STATE_ERR';
592         break;
593       default:
594         msg = 'Unknown Error';
595         break;
596     }
597     console.error('Error: ' + msg);
598     this.showError_(str('accessFileFailure'));
599   };
601   /**
602    * Handles changing of selectedItem in wallpaper manager.
603    */
604   WallpaperManager.prototype.onSelectedItemChanged_ = function() {
605     this.setWallpaperAttribution_(this.selectedItem_);
607     if (!this.selectedItem_ || this.selectedItem_.source == 'ADDNEW')
608       return;
610     if (this.selectedItem_.baseURL && !this.wallpaperGrid_.inProgramSelection) {
611       if (this.selectedItem_.source == Constants.WallpaperSourceEnum.Custom) {
612         var items = {};
613         var key = this.selectedItem_.baseURL;
614         var self = this;
615         Constants.WallpaperLocalStorage.get(key, function(items) {
616           self.selectedItem_.layout =
617               items[key] ? items[key] : 'CENTER_CROPPED';
618           self.setSelectedWallpaper_(self.selectedItem_);
619         });
620       } else {
621         this.setSelectedWallpaper_(this.selectedItem_);
622       }
623     }
624   };
626   /**
627    * Set attributions of wallpaper with given URL. If URL is not valid, clear
628    * the attributions.
629    * @param {{baseURL: string, dynamicURL: string, layout: string,
630    *          author: string, authorWebsite: string, availableOffline: boolean}}
631    *     selectedItem selected wallpaper item in grid.
632    * @private
633    */
634   WallpaperManager.prototype.setWallpaperAttribution_ = function(selectedItem) {
635     // Only online wallpapers have author and website attributes. All other type
636     // of wallpapers should not show attributions.
637     if (selectedItem &&
638         selectedItem.source == Constants.WallpaperSourceEnum.Online) {
639       $('author-name').textContent = selectedItem.author;
640       $('author-website').textContent = $('author-website').href =
641           selectedItem.authorWebsite;
642       chrome.wallpaperPrivate.getThumbnail(selectedItem.baseURL,
643                                            selectedItem.source,
644                                            function(data) {
645         var img = $('attribute-image');
646         if (data) {
647           var blob = new Blob([new Int8Array(data)], {'type' : 'image\/png'});
648           img.src = window.URL.createObjectURL(blob);
649           img.addEventListener('load', function(e) {
650             window.URL.revokeObjectURL(this.src);
651           });
652         } else {
653           img.src = '';
654         }
655       });
656       $('wallpaper-attribute').hidden = false;
657       $('attribute-image').hidden = false;
658       return;
659     }
660     $('wallpaper-attribute').hidden = true;
661     $('attribute-image').hidden = true;
662     $('author-name').textContent = '';
663     $('author-website').textContent = $('author-website').href = '';
664     $('attribute-image').src = '';
665   };
667   /**
668    * Resize thumbnails grid and categories list to fit the new window size.
669    */
670   WallpaperManager.prototype.onResize_ = function() {
671     this.wallpaperGrid_.redraw();
672     this.categoriesList_.redraw();
673   };
675   /**
676    * Close the last opened overlay on pressing the Escape key.
677    * @param {Event} event A keydown event.
678    */
679   WallpaperManager.prototype.onKeyDown_ = function(event) {
680     if (event.keyCode == 27) {
681       // The last opened overlay coincides with the first match of querySelector
682       // because the Error Container is declared in the DOM before the Wallpaper
683       // Selection Container.
684       // TODO(bshe): Make the overlay selection not dependent on the DOM.
685       var closeButtonSelector = '.overlay-container:not([hidden]) .close';
686       var closeButton = this.document_.querySelector(closeButtonSelector);
687       if (closeButton) {
688         closeButton.click();
689         event.preventDefault();
690       }
691     }
692   };
694   /**
695    * Constructs the categories list.
696    */
697   WallpaperManager.prototype.initCategoriesList_ = function() {
698     this.categoriesList_ = $('categories-list');
699     cr.ui.List.decorate(this.categoriesList_);
700     // cr.ui.list calculates items in view port based on client height and item
701     // height. However, categories list is displayed horizontally. So we should
702     // not calculate visible items here. Sets autoExpands to true to show every
703     // item in the list.
704     // TODO(bshe): Use ul to replace cr.ui.list for category list.
705     this.categoriesList_.autoExpands = true;
707     var self = this;
708     this.categoriesList_.itemConstructor = function(entry) {
709       return self.renderCategory_(entry);
710     };
712     this.categoriesList_.selectionModel = new cr.ui.ListSingleSelectionModel();
713     this.categoriesList_.selectionModel.addEventListener(
714         'change', this.onCategoriesChange_.bind(this));
716     var categoriesDataModel = new cr.ui.ArrayDataModel([]);
717     if (this.enableOnlineWallpaper_) {
718       // Adds all category as first category.
719       categoriesDataModel.push(str('allCategoryLabel'));
720       for (var key in this.manifest_.categories) {
721         categoriesDataModel.push(this.manifest_.categories[key]);
722       }
723     }
724     // Adds custom category as last category.
725     categoriesDataModel.push(str('customCategoryLabel'));
726     this.categoriesList_.dataModel = categoriesDataModel;
727   };
729   /**
730    * Constructs the element in categories list.
731    * @param {string} entry Text content of a category.
732    */
733   WallpaperManager.prototype.renderCategory_ = function(entry) {
734     var li = this.document_.createElement('li');
735     cr.defineProperty(li, 'custom', cr.PropertyKind.BOOL_ATTR);
736     li.custom = (entry == str('customCategoryLabel'));
737     cr.defineProperty(li, 'lead', cr.PropertyKind.BOOL_ATTR);
738     cr.defineProperty(li, 'selected', cr.PropertyKind.BOOL_ATTR);
739     var div = this.document_.createElement('div');
740     div.textContent = entry;
741     li.appendChild(div);
742     return li;
743   };
745   /**
746    * Handles the custom wallpaper which user selected from file manager. Called
747    * when users select a file.
748    */
749   WallpaperManager.prototype.onFileSelectorChanged_ = function() {
750     var files = $('file-selector').files;
751     if (files.length != 1)
752       console.error('More than one files are selected or no file selected');
753     if (!files[0].type.match('image/jpeg') &&
754         !files[0].type.match('image/png')) {
755       this.showError_(str('invalidWallpaper'));
756       return;
757     }
758     var layout = getSelectedLayout();
759     var self = this;
760     var errorHandler = this.onFileSystemError_.bind(this);
761     var setSelectedFile = function(file, layout, fileName) {
762       var saveThumbnail = function(thumbnail) {
763         var success = function(dirEntry) {
764           dirEntry.getFile(fileName, {create: true}, function(fileEntry) {
765             fileEntry.createWriter(function(fileWriter) {
766               fileWriter.onwriteend = function(e) {
767                 $('set-wallpaper-layout').disabled = false;
768                 var wallpaperInfo = {
769                   baseURL: fileName,
770                   layout: layout,
771                   source: Constants.WallpaperSourceEnum.Custom,
772                   availableOffline: true
773                 };
774                 self.wallpaperGrid_.dataModel.splice(0, 0, wallpaperInfo);
775                 self.wallpaperGrid_.selectedItem = wallpaperInfo;
776                 self.wallpaperGrid_.activeItem = wallpaperInfo;
777                 self.currentWallpaper_ = fileName;
778                 WallpaperUtil.saveToStorage(self.currentWallpaper_, layout,
779                                             false);
780               };
782               fileWriter.onerror = errorHandler;
784               var blob = new Blob([new Int8Array(thumbnail)],
785                                   {'type' : 'image\/jpeg'});
786               fileWriter.write(blob);
787             }, errorHandler);
788           }, errorHandler);
789         };
790         self.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.THUMBNAIL,
791             success, errorHandler);
792       };
794       var success = function(dirEntry) {
795         dirEntry.getFile(fileName, {create: true}, function(fileEntry) {
796           fileEntry.createWriter(function(fileWriter) {
797             fileWriter.addEventListener('writeend', function(e) {
798               var reader = new FileReader();
799               reader.readAsArrayBuffer(file);
800               reader.addEventListener('error', errorHandler);
801               reader.addEventListener('load', function(e) {
802                 self.setCustomWallpaper(e.target.result, layout, true, fileName,
803                                         saveThumbnail, function() {
804                   self.removeCustomWallpaper(fileName);
805                   errorHandler();
806                 });
807               });
808             });
810             fileWriter.addEventListener('error', errorHandler);
811             fileWriter.write(file);
812           }, errorHandler);
813         }, errorHandler);
814       };
815       self.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.ORIGINAL, success,
816                                        errorHandler);
817     };
818     setSelectedFile(files[0], layout, new Date().getTime().toString());
819   };
821   /**
822    * Removes wallpaper and thumbnail with fileName from FileSystem.
823    * @param {string} fileName The file name of wallpaper and thumbnail to be
824    *     removed.
825    */
826   WallpaperManager.prototype.removeCustomWallpaper = function(fileName) {
827     var errorHandler = this.onFileSystemError_.bind(this);
828     var self = this;
829     var removeFile = function(fileName) {
830       var success = function(dirEntry) {
831         dirEntry.getFile(fileName, {create: false}, function(fileEntry) {
832           fileEntry.remove(function() {
833           }, errorHandler);
834         }, errorHandler);
835       }
837       // Removes copy of original.
838       self.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.ORIGINAL, success,
839                                        errorHandler);
841       // Removes generated thumbnail.
842       self.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.THUMBNAIL, success,
843                                        errorHandler);
844     };
845     removeFile(fileName);
846   };
848   /**
849    * Sets current wallpaper and generate thumbnail if generateThumbnail is true.
850    * @param {ArrayBuffer} wallpaper The binary representation of wallpaper.
851    * @param {string} layout The user selected wallpaper layout.
852    * @param {boolean} generateThumbnail True if need to generate thumbnail.
853    * @param {string} fileName The unique file name of wallpaper.
854    * @param {function(thumbnail):void} success Success callback. If
855    *     generateThumbnail is true, the callback parameter should have the
856    *     generated thumbnail.
857    * @param {function(e):void} failure Failure callback. Called when there is an
858    *     error from FileSystem.
859    */
860   WallpaperManager.prototype.setCustomWallpaper = function(wallpaper,
861                                                            layout,
862                                                            generateThumbnail,
863                                                            fileName,
864                                                            success,
865                                                            failure) {
866     var self = this;
867     var onFinished = function(opt_thumbnail) {
868       if (chrome.runtime.lastError != undefined) {
869         self.showError_(chrome.runtime.lastError.message);
870         $('set-wallpaper-layout').disabled = true;
871         failure();
872       } else {
873         success(opt_thumbnail);
874         // Custom wallpapers are not synced yet. If login on a different
875         // computer after set a custom wallpaper, wallpaper wont change by sync.
876         WallpaperUtil.saveWallpaperInfo(fileName, layout,
877                                         Constants.WallpaperSourceEnum.Custom);
878       }
879     };
881     chrome.wallpaperPrivate.setCustomWallpaper(wallpaper, layout,
882                                                generateThumbnail,
883                                                fileName, onFinished);
884   };
886   /**
887    * Sets wallpaper finished. Displays error message if any.
888    * @param {WallpaperThumbnailsGridItem=} opt_selectedGridItem The wallpaper
889    *     thumbnail grid item. It extends from cr.ui.ListItem.
890    * @param {{baseURL: string, layout: string, source: string,
891    *          availableOffline: boolean, opt_dynamicURL: string,
892    *          opt_author: string, opt_authorWebsite: string}=}
893    *     opt_selectedItem the selected item in WallpaperThumbnailsGrid's data
894    *     model.
895    */
896   WallpaperManager.prototype.onFinished_ = function(opt_selectedGridItem,
897                                                     opt_selectedItem) {
898     if (opt_selectedGridItem)
899       this.progressManager_.hideProgressBar(opt_selectedGridItem);
901     if (chrome.runtime.lastError != undefined) {
902       this.showError_(chrome.runtime.lastError.message);
903     } else if (opt_selectedItem) {
904       this.wallpaperGrid_.activeItem = opt_selectedItem;
905     }
906   };
908   /**
909    * Handles the layout setting change of custom wallpaper.
910    */
911   WallpaperManager.prototype.onWallpaperLayoutChanged_ = function() {
912     var layout = getSelectedLayout();
913     var self = this;
914     chrome.wallpaperPrivate.setCustomWallpaperLayout(layout, function() {
915       if (chrome.runtime.lastError != undefined) {
916         self.showError_(chrome.runtime.lastError.message);
917         self.removeCustomWallpaper(fileName);
918         $('set-wallpaper-layout').disabled = true;
919       } else {
920         WallpaperUtil.saveToStorage(self.currentWallpaper_, layout, false);
921       }
922     });
923   };
925   /**
926    * Handles user clicking on a different category.
927    */
928   WallpaperManager.prototype.onCategoriesChange_ = function() {
929     var categoriesList = this.categoriesList_;
930     var selectedIndex = categoriesList.selectionModel.selectedIndex;
931     if (selectedIndex == -1)
932       return;
933     var selectedListItem = categoriesList.getListItemByIndex(selectedIndex);
934     var bar = $('bar');
935     bar.style.left = selectedListItem.offsetLeft + 'px';
936     bar.style.width = selectedListItem.offsetWidth + 'px';
938     var wallpapersDataModel = new cr.ui.ArrayDataModel([]);
939     var selectedItem;
940     if (selectedListItem.custom) {
941       this.document_.body.setAttribute('custom', '');
942       var errorHandler = this.onFileSystemError_.bind(this);
943       var toArray = function(list) {
944         return Array.prototype.slice.call(list || [], 0);
945       }
947       var self = this;
948       var processResults = function(entries) {
949         for (var i = 0; i < entries.length; i++) {
950           var entry = entries[i];
951           var wallpaperInfo = {
952                 baseURL: entry.name,
953                 // The layout will be replaced by the actual value saved in
954                 // local storage when requested later. Layout is not important
955                 // for constructing thumbnails grid, we use CENTER_CROPPED here
956                 // to speed up the process of constructing. So we do not need to
957                 // wait for fetching correct layout.
958                 layout: 'CENTER_CROPPED',
959                 source: Constants.WallpaperSourceEnum.Custom,
960                 availableOffline: true
961           };
962           wallpapersDataModel.push(wallpaperInfo);
963         }
964         if (loadTimeData.getBoolean('isOEMDefaultWallpaper')) {
965           var oemDefaultWallpaperElement = {
966               baseURL: 'OemDefaultWallpaper',
967               layout: 'CENTER_CROPPED',
968               source: Constants.WallpaperSourceEnum.OEM,
969               availableOffline: true
970           };
971           wallpapersDataModel.push(oemDefaultWallpaperElement);
972         }
973         for (var i = 0; i < wallpapersDataModel.length; i++) {
974           if (self.currentWallpaper_ == wallpapersDataModel.item(i).baseURL)
975             selectedItem = wallpapersDataModel.item(i);
976         }
977         var lastElement = {
978             baseURL: '',
979             layout: '',
980             source: Constants.WallpaperSourceEnum.AddNew,
981             availableOffline: true
982         };
983         wallpapersDataModel.push(lastElement);
984         self.wallpaperGrid_.dataModel = wallpapersDataModel;
985         self.wallpaperGrid_.selectedItem = selectedItem;
986         self.wallpaperGrid_.activeItem = selectedItem;
987       }
989       var success = function(dirEntry) {
990         var dirReader = dirEntry.createReader();
991         var entries = [];
992         // All of a directory's entries are not guaranteed to return in a single
993         // call.
994         var readEntries = function() {
995           dirReader.readEntries(function(results) {
996             if (!results.length) {
997               processResults(entries.sort());
998             } else {
999               entries = entries.concat(toArray(results));
1000               readEntries();
1001             }
1002           }, errorHandler);
1003         };
1004         readEntries(); // Start reading dirs.
1005       }
1006       this.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.ORIGINAL,
1007                                        success, errorHandler);
1008     } else {
1009       this.document_.body.removeAttribute('custom');
1010       for (var key in this.manifest_.wallpaper_list) {
1011         if (selectedIndex == AllCategoryIndex ||
1012             this.manifest_.wallpaper_list[key].categories.indexOf(
1013                 selectedIndex - OnlineCategoriesOffset) != -1) {
1014           var wallpaperInfo = {
1015             baseURL: this.manifest_.wallpaper_list[key].base_url,
1016             layout: this.manifest_.wallpaper_list[key].default_layout,
1017             source: Constants.WallpaperSourceEnum.Online,
1018             availableOffline: false,
1019             author: this.manifest_.wallpaper_list[key].author,
1020             authorWebsite: this.manifest_.wallpaper_list[key].author_website,
1021             dynamicURL: this.manifest_.wallpaper_list[key].dynamic_url
1022           };
1023           var startIndex = wallpaperInfo.baseURL.lastIndexOf('/') + 1;
1024           var fileName = wallpaperInfo.baseURL.substring(startIndex) +
1025               Constants.HighResolutionSuffix;
1026           if (this.downloadedListMap_ &&
1027               this.downloadedListMap_.hasOwnProperty(encodeURI(fileName))) {
1028             wallpaperInfo.availableOffline = true;
1029           }
1030           wallpapersDataModel.push(wallpaperInfo);
1031           var url = this.manifest_.wallpaper_list[key].base_url +
1032               Constants.HighResolutionSuffix;
1033           if (url == this.currentWallpaper_) {
1034             selectedItem = wallpaperInfo;
1035           }
1036         }
1037       }
1038       this.wallpaperGrid_.dataModel = wallpapersDataModel;
1039       this.wallpaperGrid_.selectedItem = selectedItem;
1040       this.wallpaperGrid_.activeItem = selectedItem;
1041     }
1042   };
1044 })();