Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / chromeos / wallpaper_manager / js / wallpaper_manager.js
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.
4
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  */
15
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_();
29 }
30
31 // Anonymous 'namespace'.
32 // TODO(bshe): Get rid of anonymous namespace.
33 (function() {
34
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;
41
42   /**
43    * Index of the All category. It is the first category in wallpaper picker.
44    */
45   /** @const */ var AllCategoryIndex = 0;
46
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;
52
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   }
65
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   }
74
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   };
85
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     }
95
96     var urls = [
97         str('manifestBaseURL') + locale + '.json',
98         // Fallback url. Use 'en' locale by default.
99         str('manifestBaseURL') + 'en.json'];
100
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         },
113
114         success: function(response) {
115           successCallback(response);
116         },
117
118         failure: function() {
119           failureCallback();
120         }
121       };
122       loop.next();
123     };
124
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     };
146
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   };
158
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   };
168
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   };
179
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   };
192
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, true, 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   };
217
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   };
240
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_();
250
251     $('file-selector').addEventListener(
252         'change', this.onFileSelectorChanged_.bind(this));
253     $('set-wallpaper-layout').addEventListener(
254         'change', this.onWallpaperLayoutChanged_.bind(this));
255
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.WallpaperSyncStorage.get(Constants.AccessSurpriseMeEnabledKey,
262                                           function(items) {
263         // Surprise me has been moved from local to sync storage, prefer
264         // values from sync, but if unset check local and update synced pref
265         // if applicable.
266         if (!items.hasOwnProperty(Constants.AccessSurpriseMeEnabledKey)) {
267           Constants.WallpaperLocalStorage.get(
268               Constants.AccessSurpriseMeEnabledKey, function(values) {
269             if (values.hasOwnProperty(Constants.AccessSurpriseMeEnabledKey)) {
270               WallpaperUtil.saveToStorage(Constants.AccessSurpriseMeEnabledKey,
271                   values[Constants.AccessSurpriseMeEnabledKey], true);
272             }
273             if (values[Constants.AccessSurpriseMeEnabledKey]) {
274                 $('surprise-me').querySelector('#checkbox').classList.add(
275                     'checked');
276                 $('categories-list').disabled = true;
277                 $('wallpaper-grid').disabled = true;
278             }
279           });
280         } else if (items[Constants.AccessSurpriseMeEnabledKey]) {
281           $('surprise-me').querySelector('#checkbox').classList.add('checked');
282           $('categories-list').disabled = true;
283           $('wallpaper-grid').disabled = true;
284         }
285       });
286
287       window.addEventListener('offline', function() {
288         chrome.wallpaperPrivate.getOfflineWallpaperList(function(lists) {
289           if (!self.downloadedListMap_)
290             self.downloadedListMap_ = {};
291           for (var i = 0; i < lists.length; i++) {
292             self.downloadedListMap_[lists[i]] = true;
293           }
294           var thumbnails = self.document_.querySelectorAll('.thumbnail');
295           for (var i = 0; i < thumbnails.length; i++) {
296             var thumbnail = thumbnails[i];
297             var url = self.wallpaperGrid_.dataModel.item(i).baseURL;
298             var fileName = url.substring(url.lastIndexOf('/') + 1) +
299                 Constants.HighResolutionSuffix;
300             if (self.downloadedListMap_ &&
301                 self.downloadedListMap_.hasOwnProperty(encodeURI(fileName))) {
302               thumbnail.offline = true;
303             }
304           }
305         });
306         $('wallpaper-grid').classList.add('image-picker-offline');
307       });
308       window.addEventListener('online', function() {
309         self.downloadedListMap_ = null;
310         $('wallpaper-grid').classList.remove('image-picker-offline');
311       });
312     }
313
314     this.onResize_();
315     this.initContextMenuAndCommand_();
316   };
317
318   /**
319    * One-time initialization of context menu and command.
320    */
321   WallpaperManager.prototype.initContextMenuAndCommand_ = function() {
322     this.wallpaperContextMenu_ = $('wallpaper-context-menu');
323     cr.ui.Menu.decorate(this.wallpaperContextMenu_);
324     cr.ui.contextMenuHandler.setContextMenu(this.wallpaperGrid_,
325                                             this.wallpaperContextMenu_);
326     var commands = this.dialogDom_.querySelectorAll('command');
327     for (var i = 0; i < commands.length; i++)
328       cr.ui.Command.decorate(commands[i]);
329
330     var doc = this.document_;
331     doc.addEventListener('command', this.onCommand_.bind(this));
332     doc.addEventListener('canExecute', this.onCommandCanExecute_.bind(this));
333   };
334
335   /**
336    * Handles a command being executed.
337    * @param {Event} event A command event.
338    */
339   WallpaperManager.prototype.onCommand_ = function(event) {
340     if (event.command.id == 'delete') {
341       var wallpaperGrid = this.wallpaperGrid_;
342       var selectedIndex = wallpaperGrid.selectionModel.selectedIndex;
343       var item = wallpaperGrid.dataModel.item(selectedIndex);
344       if (!item || item.source != Constants.WallpaperSourceEnum.Custom)
345         return;
346       this.removeCustomWallpaper(item.baseURL);
347       wallpaperGrid.dataModel.splice(selectedIndex, 1);
348       // Calculate the number of remaining custom wallpapers. The add new button
349       // in data model needs to be excluded.
350       var customWallpaperCount = wallpaperGrid.dataModel.length - 1;
351       if (customWallpaperCount == 0) {
352         // Active custom wallpaper is also copied in chronos data dir. It needs
353         // to be deleted.
354         chrome.wallpaperPrivate.resetWallpaper();
355       } else {
356         selectedIndex = Math.min(selectedIndex, customWallpaperCount - 1);
357         wallpaperGrid.selectionModel.selectedIndex = selectedIndex;
358       }
359       event.cancelBubble = true;
360     }
361   };
362
363   /**
364    * Decides if a command can be executed on current target.
365    * @param {Event} event A command event.
366    */
367   WallpaperManager.prototype.onCommandCanExecute_ = function(event) {
368     switch (event.command.id) {
369       case 'delete':
370         var wallpaperGrid = this.wallpaperGrid_;
371         var selectedIndex = wallpaperGrid.selectionModel.selectedIndex;
372         var item = wallpaperGrid.dataModel.item(selectedIndex);
373         if (selectedIndex != this.wallpaperGrid_.dataModel.length - 1 &&
374           item && item.source == Constants.WallpaperSourceEnum.Custom) {
375           event.canExecute = true;
376           break;
377         }
378       default:
379         event.canExecute = false;
380     }
381   };
382
383   /**
384    * Preset to the category which contains current wallpaper.
385    */
386   WallpaperManager.prototype.presetCategory_ = function() {
387     this.currentWallpaper_ = str('currentWallpaper');
388     // The currentWallpaper_ is either a url contains HightResolutionSuffix or a
389     // custom wallpaper file name converted from an integer value represent
390     // time (e.g., 13006377367586070).
391     if (!this.enableOnlineWallpaper_ || (this.currentWallpaper_ &&
392         this.currentWallpaper_.indexOf(Constants.HighResolutionSuffix) == -1)) {
393       // Custom is the last one in the categories list.
394       this.categoriesList_.selectionModel.selectedIndex =
395           this.categoriesList_.dataModel.length - 1;
396       return;
397     }
398     var self = this;
399     var presetCategoryInner_ = function() {
400       // Selects the first category in the categories list of current
401       // wallpaper as the default selected category when showing wallpaper
402       // picker UI.
403       var presetCategory = AllCategoryIndex;
404       if (self.currentWallpaper_) {
405         for (var key in self.manifest_.wallpaper_list) {
406           var url = self.manifest_.wallpaper_list[key].base_url +
407               Constants.HighResolutionSuffix;
408           if (url.indexOf(self.currentWallpaper_) != -1 &&
409               self.manifest_.wallpaper_list[key].categories.length > 0) {
410             presetCategory = self.manifest_.wallpaper_list[key].categories[0] +
411                 OnlineCategoriesOffset;
412             break;
413           }
414         }
415       }
416       self.categoriesList_.selectionModel.selectedIndex = presetCategory;
417     };
418     if (navigator.onLine) {
419       presetCategoryInner_();
420     } else {
421       // If device is offline, gets the available offline wallpaper list first.
422       // Wallpapers which are not in the list will display a grayscaled
423       // thumbnail.
424       chrome.wallpaperPrivate.getOfflineWallpaperList(function(lists) {
425         if (!self.downloadedListMap_)
426           self.downloadedListMap_ = {};
427         for (var i = 0; i < lists.length; i++)
428           self.downloadedListMap_[lists[i]] = true;
429         presetCategoryInner_();
430       });
431     }
432   };
433
434   /**
435    * Constructs the thumbnails grid.
436    */
437   WallpaperManager.prototype.initThumbnailsGrid_ = function() {
438     this.wallpaperGrid_ = $('wallpaper-grid');
439     wallpapers.WallpaperThumbnailsGrid.decorate(this.wallpaperGrid_);
440     this.wallpaperGrid_.autoExpands = true;
441
442     this.wallpaperGrid_.addEventListener('change', this.onChange_.bind(this));
443     this.wallpaperGrid_.addEventListener('dblclick', this.onClose_.bind(this));
444   };
445
446   /**
447    * Handles change event dispatched by wallpaper grid.
448    */
449   WallpaperManager.prototype.onChange_ = function() {
450     // splice may dispatch a change event because the position of selected
451     // element changing. But the actual selected element may not change after
452     // splice. Check if the new selected element equals to the previous selected
453     // element before continuing. Otherwise, wallpaper may reset to previous one
454     // as described in http://crbug.com/229036.
455     if (this.selectedItem_ == this.wallpaperGrid_.selectedItem)
456       return;
457     this.selectedItem_ = this.wallpaperGrid_.selectedItem;
458     this.onSelectedItemChanged_();
459   };
460
461   /**
462    * Closes window if no pending wallpaper request.
463    */
464   WallpaperManager.prototype.onClose_ = function() {
465     if (this.wallpaperRequest_) {
466       this.wallpaperRequest_.addEventListener('loadend', function() {
467         // Close window on wallpaper loading finished.
468         window.close();
469       });
470     } else {
471       window.close();
472     }
473   };
474
475   /**
476     * Sets wallpaper to the corresponding wallpaper of selected thumbnail.
477     * @param {{baseURL: string, layout: string, source: string,
478     *          availableOffline: boolean, opt_dynamicURL: string,
479     *          opt_author: string, opt_authorWebsite: string}}
480     *     selectedItem the selected item in WallpaperThumbnailsGrid's data
481     *     model.
482     */
483   WallpaperManager.prototype.setSelectedWallpaper_ = function(selectedItem) {
484     var self = this;
485     switch (selectedItem.source) {
486       case Constants.WallpaperSourceEnum.Custom:
487         var errorHandler = this.onFileSystemError_.bind(this);
488         var setActive = function() {
489           self.wallpaperGrid_.activeItem = selectedItem;
490           self.currentWallpaper_ = selectedItem.baseURL;
491         };
492         var success = function(dirEntry) {
493           dirEntry.getFile(selectedItem.baseURL, {create: false},
494                            function(fileEntry) {
495             fileEntry.file(function(file) {
496               var reader = new FileReader();
497               reader.readAsArrayBuffer(file);
498               reader.addEventListener('error', errorHandler);
499               reader.addEventListener('load', function(e) {
500                 self.setCustomWallpaper(e.target.result,
501                                         selectedItem.layout,
502                                         false, selectedItem.baseURL,
503                                         setActive, errorHandler);
504               });
505             }, errorHandler);
506           }, errorHandler);
507         }
508         this.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.ORIGINAL,
509                                          success, errorHandler);
510         break;
511       case Constants.WallpaperSourceEnum.OEM:
512         // Resets back to default wallpaper.
513         chrome.wallpaperPrivate.resetWallpaper();
514         this.currentWallpaper_ = selectedItem.baseURL;
515         this.wallpaperGrid_.activeItem = selectedItem;
516         WallpaperUtil.saveWallpaperInfo(wallpaperURL, selectedItem.layout,
517                                         selectedItem.source);
518         break;
519       case Constants.WallpaperSourceEnum.Online:
520         var wallpaperURL = selectedItem.baseURL +
521             Constants.HighResolutionSuffix;
522         var selectedGridItem = this.wallpaperGrid_.getListItem(selectedItem);
523
524         chrome.wallpaperPrivate.setWallpaperIfExists(wallpaperURL,
525                                                      selectedItem.layout,
526                                                      function(exists) {
527           if (exists) {
528             self.currentWallpaper_ = wallpaperURL;
529             self.wallpaperGrid_.activeItem = selectedItem;
530             WallpaperUtil.saveWallpaperInfo(wallpaperURL, selectedItem.layout,
531                                             selectedItem.source);
532             return;
533           }
534
535           // Falls back to request wallpaper from server.
536           if (self.wallpaperRequest_)
537             self.wallpaperRequest_.abort();
538
539           self.wallpaperRequest_ = new XMLHttpRequest();
540           self.progressManager_.reset(self.wallpaperRequest_, selectedGridItem);
541
542           var onSuccess = function(xhr) {
543             var image = xhr.response;
544             chrome.wallpaperPrivate.setWallpaper(image, selectedItem.layout,
545                 wallpaperURL,
546                 self.onFinished_.bind(self, selectedGridItem, selectedItem));
547             self.currentWallpaper_ = wallpaperURL;
548             WallpaperUtil.saveWallpaperInfo(wallpaperURL, selectedItem.layout,
549                                             selectedItem.source);
550             self.wallpaperRequest_ = null;
551           };
552           var onFailure = function() {
553             self.progressManager_.hideProgressBar(selectedGridItem);
554             self.showError_(str('downloadFailed'));
555             self.wallpaperRequest_ = null;
556           };
557           WallpaperUtil.fetchURL(wallpaperURL, 'arraybuffer', onSuccess,
558                                  onFailure, self.wallpaperRequest_);
559         });
560         break;
561       default:
562         console.error('Unsupported wallpaper source.');
563     }
564   };
565
566   /*
567    * Removes the oldest custom wallpaper. If the oldest one is set as current
568    * wallpaper, removes the second oldest one to free some space. This should
569    * only be called when exceeding wallpaper quota.
570    */
571   WallpaperManager.prototype.removeOldestWallpaper_ = function() {
572     // Custom wallpapers should already sorted when put to the data model. The
573     // last element is the add new button, need to exclude it as well.
574     var oldestIndex = this.wallpaperGrid_.dataModel.length - 2;
575     var item = this.wallpaperGrid_.dataModel.item(oldestIndex);
576     if (!item || item.source != Constants.WallpaperSourceEnum.Custom)
577       return;
578     if (item.baseURL == this.currentWallpaper_)
579       item = this.wallpaperGrid_.dataModel.item(--oldestIndex);
580     if (item) {
581       this.removeCustomWallpaper(item.baseURL);
582       this.wallpaperGrid_.dataModel.splice(oldestIndex, 1);
583     }
584   };
585
586   /*
587    * Shows an error message to user and log the failed reason in console.
588    */
589   WallpaperManager.prototype.onFileSystemError_ = function(e) {
590     var msg = '';
591     switch (e.code) {
592       case FileError.QUOTA_EXCEEDED_ERR:
593         msg = 'QUOTA_EXCEEDED_ERR';
594         // Instead of simply remove oldest wallpaper, we should consider a
595         // better way to handle this situation. See crbug.com/180890.
596         this.removeOldestWallpaper_();
597         break;
598       case FileError.NOT_FOUND_ERR:
599         msg = 'NOT_FOUND_ERR';
600         break;
601       case FileError.SECURITY_ERR:
602         msg = 'SECURITY_ERR';
603         break;
604       case FileError.INVALID_MODIFICATION_ERR:
605         msg = 'INVALID_MODIFICATION_ERR';
606         break;
607       case FileError.INVALID_STATE_ERR:
608         msg = 'INVALID_STATE_ERR';
609         break;
610       default:
611         msg = 'Unknown Error';
612         break;
613     }
614     console.error('Error: ' + msg);
615     this.showError_(str('accessFileFailure'));
616   };
617
618   /**
619    * Handles changing of selectedItem in wallpaper manager.
620    */
621   WallpaperManager.prototype.onSelectedItemChanged_ = function() {
622     this.setWallpaperAttribution_(this.selectedItem_);
623
624     if (!this.selectedItem_ || this.selectedItem_.source == 'ADDNEW')
625       return;
626
627     if (this.selectedItem_.baseURL && !this.wallpaperGrid_.inProgramSelection) {
628       if (this.selectedItem_.source == Constants.WallpaperSourceEnum.Custom) {
629         var items = {};
630         var key = this.selectedItem_.baseURL;
631         var self = this;
632         Constants.WallpaperLocalStorage.get(key, function(items) {
633           self.selectedItem_.layout =
634               items[key] ? items[key] : 'CENTER_CROPPED';
635           self.setSelectedWallpaper_(self.selectedItem_);
636         });
637       } else {
638         this.setSelectedWallpaper_(this.selectedItem_);
639       }
640     }
641   };
642
643   /**
644    * Set attributions of wallpaper with given URL. If URL is not valid, clear
645    * the attributions.
646    * @param {{baseURL: string, dynamicURL: string, layout: string,
647    *          author: string, authorWebsite: string, availableOffline: boolean}}
648    *     selectedItem selected wallpaper item in grid.
649    * @private
650    */
651   WallpaperManager.prototype.setWallpaperAttribution_ = function(selectedItem) {
652     // Only online wallpapers have author and website attributes. All other type
653     // of wallpapers should not show attributions.
654     if (selectedItem &&
655         selectedItem.source == Constants.WallpaperSourceEnum.Online) {
656       $('author-name').textContent = selectedItem.author;
657       $('author-website').textContent = $('author-website').href =
658           selectedItem.authorWebsite;
659       chrome.wallpaperPrivate.getThumbnail(selectedItem.baseURL,
660                                            selectedItem.source,
661                                            function(data) {
662         var img = $('attribute-image');
663         if (data) {
664           var blob = new Blob([new Int8Array(data)], {'type' : 'image\/png'});
665           img.src = window.URL.createObjectURL(blob);
666           img.addEventListener('load', function(e) {
667             window.URL.revokeObjectURL(this.src);
668           });
669         } else {
670           img.src = '';
671         }
672       });
673       $('wallpaper-attribute').hidden = false;
674       $('attribute-image').hidden = false;
675       return;
676     }
677     $('wallpaper-attribute').hidden = true;
678     $('attribute-image').hidden = true;
679     $('author-name').textContent = '';
680     $('author-website').textContent = $('author-website').href = '';
681     $('attribute-image').src = '';
682   };
683
684   /**
685    * Resize thumbnails grid and categories list to fit the new window size.
686    */
687   WallpaperManager.prototype.onResize_ = function() {
688     this.wallpaperGrid_.redraw();
689     this.categoriesList_.redraw();
690   };
691
692   /**
693    * Close the last opened overlay on pressing the Escape key.
694    * @param {Event} event A keydown event.
695    */
696   WallpaperManager.prototype.onKeyDown_ = function(event) {
697     if (event.keyCode == 27) {
698       // The last opened overlay coincides with the first match of querySelector
699       // because the Error Container is declared in the DOM before the Wallpaper
700       // Selection Container.
701       // TODO(bshe): Make the overlay selection not dependent on the DOM.
702       var closeButtonSelector = '.overlay-container:not([hidden]) .close';
703       var closeButton = this.document_.querySelector(closeButtonSelector);
704       if (closeButton) {
705         closeButton.click();
706         event.preventDefault();
707       }
708     }
709   };
710
711   /**
712    * Constructs the categories list.
713    */
714   WallpaperManager.prototype.initCategoriesList_ = function() {
715     this.categoriesList_ = $('categories-list');
716     cr.ui.List.decorate(this.categoriesList_);
717     // cr.ui.list calculates items in view port based on client height and item
718     // height. However, categories list is displayed horizontally. So we should
719     // not calculate visible items here. Sets autoExpands to true to show every
720     // item in the list.
721     // TODO(bshe): Use ul to replace cr.ui.list for category list.
722     this.categoriesList_.autoExpands = true;
723
724     var self = this;
725     this.categoriesList_.itemConstructor = function(entry) {
726       return self.renderCategory_(entry);
727     };
728
729     this.categoriesList_.selectionModel = new cr.ui.ListSingleSelectionModel();
730     this.categoriesList_.selectionModel.addEventListener(
731         'change', this.onCategoriesChange_.bind(this));
732
733     var categoriesDataModel = new cr.ui.ArrayDataModel([]);
734     if (this.enableOnlineWallpaper_) {
735       // Adds all category as first category.
736       categoriesDataModel.push(str('allCategoryLabel'));
737       for (var key in this.manifest_.categories) {
738         categoriesDataModel.push(this.manifest_.categories[key]);
739       }
740     }
741     // Adds custom category as last category.
742     categoriesDataModel.push(str('customCategoryLabel'));
743     this.categoriesList_.dataModel = categoriesDataModel;
744   };
745
746   /**
747    * Constructs the element in categories list.
748    * @param {string} entry Text content of a category.
749    */
750   WallpaperManager.prototype.renderCategory_ = function(entry) {
751     var li = this.document_.createElement('li');
752     cr.defineProperty(li, 'custom', cr.PropertyKind.BOOL_ATTR);
753     li.custom = (entry == str('customCategoryLabel'));
754     cr.defineProperty(li, 'lead', cr.PropertyKind.BOOL_ATTR);
755     cr.defineProperty(li, 'selected', cr.PropertyKind.BOOL_ATTR);
756     var div = this.document_.createElement('div');
757     div.textContent = entry;
758     li.appendChild(div);
759     return li;
760   };
761
762   /**
763    * Handles the custom wallpaper which user selected from file manager. Called
764    * when users select a file.
765    */
766   WallpaperManager.prototype.onFileSelectorChanged_ = function() {
767     var files = $('file-selector').files;
768     if (files.length != 1)
769       console.error('More than one files are selected or no file selected');
770     if (!files[0].type.match('image/jpeg') &&
771         !files[0].type.match('image/png')) {
772       this.showError_(str('invalidWallpaper'));
773       return;
774     }
775     var layout = getSelectedLayout();
776     var self = this;
777     var errorHandler = this.onFileSystemError_.bind(this);
778     var setSelectedFile = function(file, layout, fileName) {
779       var saveThumbnail = function(thumbnail) {
780         var success = function(dirEntry) {
781           dirEntry.getFile(fileName, {create: true}, function(fileEntry) {
782             fileEntry.createWriter(function(fileWriter) {
783               fileWriter.onwriteend = function(e) {
784                 $('set-wallpaper-layout').disabled = false;
785                 var wallpaperInfo = {
786                   baseURL: fileName,
787                   layout: layout,
788                   source: Constants.WallpaperSourceEnum.Custom,
789                   availableOffline: true
790                 };
791                 self.wallpaperGrid_.dataModel.splice(0, 0, wallpaperInfo);
792                 self.wallpaperGrid_.selectedItem = wallpaperInfo;
793                 self.wallpaperGrid_.activeItem = wallpaperInfo;
794                 self.currentWallpaper_ = fileName;
795                 WallpaperUtil.saveToStorage(self.currentWallpaper_, layout,
796                                             false);
797               };
798
799               fileWriter.onerror = errorHandler;
800
801               var blob = new Blob([new Int8Array(thumbnail)],
802                                   {'type' : 'image\/jpeg'});
803               fileWriter.write(blob);
804             }, errorHandler);
805           }, errorHandler);
806         };
807         self.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.THUMBNAIL,
808             success, errorHandler);
809       };
810
811       var success = function(dirEntry) {
812         dirEntry.getFile(fileName, {create: true}, function(fileEntry) {
813           fileEntry.createWriter(function(fileWriter) {
814             fileWriter.addEventListener('writeend', function(e) {
815               var reader = new FileReader();
816               reader.readAsArrayBuffer(file);
817               reader.addEventListener('error', errorHandler);
818               reader.addEventListener('load', function(e) {
819                 self.setCustomWallpaper(e.target.result, layout, true, fileName,
820                                         saveThumbnail, function() {
821                   self.removeCustomWallpaper(fileName);
822                   errorHandler();
823                 });
824               });
825             });
826
827             fileWriter.addEventListener('error', errorHandler);
828             fileWriter.write(file);
829           }, errorHandler);
830         }, errorHandler);
831       };
832       self.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.ORIGINAL, success,
833                                        errorHandler);
834     };
835     setSelectedFile(files[0], layout, new Date().getTime().toString());
836   };
837
838   /**
839    * Removes wallpaper and thumbnail with fileName from FileSystem.
840    * @param {string} fileName The file name of wallpaper and thumbnail to be
841    *     removed.
842    */
843   WallpaperManager.prototype.removeCustomWallpaper = function(fileName) {
844     var errorHandler = this.onFileSystemError_.bind(this);
845     var self = this;
846     var removeFile = function(fileName) {
847       var success = function(dirEntry) {
848         dirEntry.getFile(fileName, {create: false}, function(fileEntry) {
849           fileEntry.remove(function() {
850           }, errorHandler);
851         }, errorHandler);
852       }
853
854       // Removes copy of original.
855       self.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.ORIGINAL, success,
856                                        errorHandler);
857
858       // Removes generated thumbnail.
859       self.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.THUMBNAIL, success,
860                                        errorHandler);
861     };
862     removeFile(fileName);
863   };
864
865   /**
866    * Sets current wallpaper and generate thumbnail if generateThumbnail is true.
867    * @param {ArrayBuffer} wallpaper The binary representation of wallpaper.
868    * @param {string} layout The user selected wallpaper layout.
869    * @param {boolean} generateThumbnail True if need to generate thumbnail.
870    * @param {string} fileName The unique file name of wallpaper.
871    * @param {function(thumbnail):void} success Success callback. If
872    *     generateThumbnail is true, the callback parameter should have the
873    *     generated thumbnail.
874    * @param {function(e):void} failure Failure callback. Called when there is an
875    *     error from FileSystem.
876    */
877   WallpaperManager.prototype.setCustomWallpaper = function(wallpaper,
878                                                            layout,
879                                                            generateThumbnail,
880                                                            fileName,
881                                                            success,
882                                                            failure) {
883     var self = this;
884     var onFinished = function(opt_thumbnail) {
885       if (chrome.runtime.lastError != undefined) {
886         self.showError_(chrome.runtime.lastError.message);
887         $('set-wallpaper-layout').disabled = true;
888         failure();
889       } else {
890         success(opt_thumbnail);
891         // Custom wallpapers are not synced yet. If login on a different
892         // computer after set a custom wallpaper, wallpaper wont change by sync.
893         WallpaperUtil.saveWallpaperInfo(fileName, layout,
894                                         Constants.WallpaperSourceEnum.Custom);
895       }
896     };
897
898     chrome.wallpaperPrivate.setCustomWallpaper(wallpaper, layout,
899                                                generateThumbnail,
900                                                fileName, onFinished);
901   };
902
903   /**
904    * Sets wallpaper finished. Displays error message if any.
905    * @param {WallpaperThumbnailsGridItem=} opt_selectedGridItem The wallpaper
906    *     thumbnail grid item. It extends from cr.ui.ListItem.
907    * @param {{baseURL: string, layout: string, source: string,
908    *          availableOffline: boolean, opt_dynamicURL: string,
909    *          opt_author: string, opt_authorWebsite: string}=}
910    *     opt_selectedItem the selected item in WallpaperThumbnailsGrid's data
911    *     model.
912    */
913   WallpaperManager.prototype.onFinished_ = function(opt_selectedGridItem,
914                                                     opt_selectedItem) {
915     if (opt_selectedGridItem)
916       this.progressManager_.hideProgressBar(opt_selectedGridItem);
917
918     if (chrome.runtime.lastError != undefined) {
919       this.showError_(chrome.runtime.lastError.message);
920     } else if (opt_selectedItem) {
921       this.wallpaperGrid_.activeItem = opt_selectedItem;
922     }
923   };
924
925   /**
926    * Handles the layout setting change of custom wallpaper.
927    */
928   WallpaperManager.prototype.onWallpaperLayoutChanged_ = function() {
929     var layout = getSelectedLayout();
930     var self = this;
931     chrome.wallpaperPrivate.setCustomWallpaperLayout(layout, function() {
932       if (chrome.runtime.lastError != undefined) {
933         self.showError_(chrome.runtime.lastError.message);
934         self.removeCustomWallpaper(fileName);
935         $('set-wallpaper-layout').disabled = true;
936       } else {
937         WallpaperUtil.saveToStorage(self.currentWallpaper_, layout, false);
938       }
939     });
940   };
941
942   /**
943    * Handles user clicking on a different category.
944    */
945   WallpaperManager.prototype.onCategoriesChange_ = function() {
946     var categoriesList = this.categoriesList_;
947     var selectedIndex = categoriesList.selectionModel.selectedIndex;
948     if (selectedIndex == -1)
949       return;
950     var selectedListItem = categoriesList.getListItemByIndex(selectedIndex);
951     var bar = $('bar');
952     bar.style.left = selectedListItem.offsetLeft + 'px';
953     bar.style.width = selectedListItem.offsetWidth + 'px';
954
955     var wallpapersDataModel = new cr.ui.ArrayDataModel([]);
956     var selectedItem;
957     if (selectedListItem.custom) {
958       this.document_.body.setAttribute('custom', '');
959       var errorHandler = this.onFileSystemError_.bind(this);
960       var toArray = function(list) {
961         return Array.prototype.slice.call(list || [], 0);
962       }
963
964       var self = this;
965       var processResults = function(entries) {
966         for (var i = 0; i < entries.length; i++) {
967           var entry = entries[i];
968           var wallpaperInfo = {
969                 baseURL: entry.name,
970                 // The layout will be replaced by the actual value saved in
971                 // local storage when requested later. Layout is not important
972                 // for constructing thumbnails grid, we use CENTER_CROPPED here
973                 // to speed up the process of constructing. So we do not need to
974                 // wait for fetching correct layout.
975                 layout: 'CENTER_CROPPED',
976                 source: Constants.WallpaperSourceEnum.Custom,
977                 availableOffline: true
978           };
979           wallpapersDataModel.push(wallpaperInfo);
980         }
981         if (loadTimeData.getBoolean('isOEMDefaultWallpaper')) {
982           var oemDefaultWallpaperElement = {
983               baseURL: 'OemDefaultWallpaper',
984               layout: 'CENTER_CROPPED',
985               source: Constants.WallpaperSourceEnum.OEM,
986               availableOffline: true
987           };
988           wallpapersDataModel.push(oemDefaultWallpaperElement);
989         }
990         for (var i = 0; i < wallpapersDataModel.length; i++) {
991           if (self.currentWallpaper_ == wallpapersDataModel.item(i).baseURL)
992             selectedItem = wallpapersDataModel.item(i);
993         }
994         var lastElement = {
995             baseURL: '',
996             layout: '',
997             source: Constants.WallpaperSourceEnum.AddNew,
998             availableOffline: true
999         };
1000         wallpapersDataModel.push(lastElement);
1001         self.wallpaperGrid_.dataModel = wallpapersDataModel;
1002         self.wallpaperGrid_.selectedItem = selectedItem;
1003         self.wallpaperGrid_.activeItem = selectedItem;
1004       }
1005
1006       var success = function(dirEntry) {
1007         var dirReader = dirEntry.createReader();
1008         var entries = [];
1009         // All of a directory's entries are not guaranteed to return in a single
1010         // call.
1011         var readEntries = function() {
1012           dirReader.readEntries(function(results) {
1013             if (!results.length) {
1014               processResults(entries.sort());
1015             } else {
1016               entries = entries.concat(toArray(results));
1017               readEntries();
1018             }
1019           }, errorHandler);
1020         };
1021         readEntries(); // Start reading dirs.
1022       }
1023       this.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.ORIGINAL,
1024                                        success, errorHandler);
1025     } else {
1026       this.document_.body.removeAttribute('custom');
1027       for (var key in this.manifest_.wallpaper_list) {
1028         if (selectedIndex == AllCategoryIndex ||
1029             this.manifest_.wallpaper_list[key].categories.indexOf(
1030                 selectedIndex - OnlineCategoriesOffset) != -1) {
1031           var wallpaperInfo = {
1032             baseURL: this.manifest_.wallpaper_list[key].base_url,
1033             layout: this.manifest_.wallpaper_list[key].default_layout,
1034             source: Constants.WallpaperSourceEnum.Online,
1035             availableOffline: false,
1036             author: this.manifest_.wallpaper_list[key].author,
1037             authorWebsite: this.manifest_.wallpaper_list[key].author_website,
1038             dynamicURL: this.manifest_.wallpaper_list[key].dynamic_url
1039           };
1040           var startIndex = wallpaperInfo.baseURL.lastIndexOf('/') + 1;
1041           var fileName = wallpaperInfo.baseURL.substring(startIndex) +
1042               Constants.HighResolutionSuffix;
1043           if (this.downloadedListMap_ &&
1044               this.downloadedListMap_.hasOwnProperty(encodeURI(fileName))) {
1045             wallpaperInfo.availableOffline = true;
1046           }
1047           wallpapersDataModel.push(wallpaperInfo);
1048           var url = this.manifest_.wallpaper_list[key].base_url +
1049               Constants.HighResolutionSuffix;
1050           if (url == this.currentWallpaper_) {
1051             selectedItem = wallpaperInfo;
1052           }
1053         }
1054       }
1055       this.wallpaperGrid_.dataModel = wallpapersDataModel;
1056       this.wallpaperGrid_.selectedItem = selectedItem;
1057       this.wallpaperGrid_.activeItem = selectedItem;
1058     }
1059   };
1060
1061 })();