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