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