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