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
16function 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, 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  };
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.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      });
269
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    }
296
297    this.onResize_();
298    this.initContextMenuAndCommand_();
299  };
300
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]);
312
313    var doc = this.document_;
314    doc.addEventListener('command', this.onCommand_.bind(this));
315    doc.addEventListener('canExecute', this.onCommandCanExecute_.bind(this));
316  };
317
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  };
345
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  };
365
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  };
416
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;
424
425    this.wallpaperGrid_.addEventListener('change', this.onChange_.bind(this));
426    this.wallpaperGrid_.addEventListener('dblclick', this.onClose_.bind(this));
427  };
428
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  };
443
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  };
457
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);
506
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          }
517
518          // Falls back to request wallpaper from server.
519          if (self.wallpaperRequest_)
520            self.wallpaperRequest_.abort();
521
522          self.wallpaperRequest_ = new XMLHttpRequest();
523          self.progressManager_.reset(self.wallpaperRequest_, selectedGridItem);
524
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  };
548
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  };
568
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  };
600
601  /**
602   * Handles changing of selectedItem in wallpaper manager.
603   */
604  WallpaperManager.prototype.onSelectedItemChanged_ = function() {
605    this.setWallpaperAttribution_(this.selectedItem_);
606
607    if (!this.selectedItem_ || this.selectedItem_.source == 'ADDNEW')
608      return;
609
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  };
625
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  };
666
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  };
674
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  };
693
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;
706
707    var self = this;
708    this.categoriesList_.itemConstructor = function(entry) {
709      return self.renderCategory_(entry);
710    };
711
712    this.categoriesList_.selectionModel = new cr.ui.ListSingleSelectionModel();
713    this.categoriesList_.selectionModel.addEventListener(
714        'change', this.onCategoriesChange_.bind(this));
715
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  };
728
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  };
744
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              };
781
782              fileWriter.onerror = errorHandler;
783
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      };
793
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            });
809
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  };
820
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      }
836
837      // Removes copy of original.
838      self.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.ORIGINAL, success,
839                                       errorHandler);
840
841      // Removes generated thumbnail.
842      self.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.THUMBNAIL, success,
843                                       errorHandler);
844    };
845    removeFile(fileName);
846  };
847
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    };
880
881    chrome.wallpaperPrivate.setCustomWallpaper(wallpaper, layout,
882                                               generateThumbnail,
883                                               fileName, onFinished);
884  };
885
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);
900
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  };
907
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  };
924
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';
937
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      }
946
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      }
988
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  };
1043
1044})();
1045