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