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, 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