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