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