1// Copyright 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'use strict'; 6 7/** 8 * Progress center at the background page. 9 * @constructor 10 */ 11var ProgressCenter = function() { 12 /** 13 * Current items managed by the progress center. 14 * @type {Array.<ProgressItem>} 15 * @private 16 */ 17 this.items_ = []; 18 19 /** 20 * Map of progress ID and notification ID. 21 * @type {Object.<string, string>} 22 * @private 23 */ 24 this.notifications_ = new ProgressCenter.Notifications_( 25 this.requestCancel.bind(this)); 26 27 /** 28 * List of panel UI managed by the progress center. 29 * @type {Array.<ProgressCenterPanel>} 30 * @private 31 */ 32 this.panels_ = []; 33 34 /** 35 * Timeout callback to remove items. 36 * @type {ProgressCenter.TimeoutManager_} 37 * @private 38 */ 39 this.resetTimeout_ = new ProgressCenter.TimeoutManager_( 40 this.reset_.bind(this)); 41 42 Object.seal(this); 43}; 44 45/** 46 * The default amount of milliseconds time, before a progress item will reset 47 * after the last complete. 48 * @type {number} 49 * @private 50 * @const 51 */ 52ProgressCenter.RESET_DELAY_TIME_MS_ = 5000; 53 54/** 55 * Notifications created by progress center. 56 * @param {function(string)} cancelCallback Callback to notify the progress 57 * center of cancel operation. 58 * @constructor 59 * @private 60 */ 61ProgressCenter.Notifications_ = function(cancelCallback) { 62 /** 63 * ID set of notifications that is progressing now. 64 * @type {Object.<string, ProgressCenter.Notifications_.NotificationState_>} 65 * @private 66 */ 67 this.ids_ = {}; 68 69 /** 70 * Async queue. 71 * @type {AsyncUtil.Queue} 72 * @private 73 */ 74 this.queue_ = new AsyncUtil.Queue(); 75 76 /** 77 * Callback to notify the progress center of cancel operation. 78 * @type {function(string)} 79 * @private 80 */ 81 this.cancelCallback_ = cancelCallback; 82 83 chrome.notifications.onButtonClicked.addListener( 84 this.onButtonClicked_.bind(this)); 85 chrome.notifications.onClosed.addListener(this.onClosed_.bind(this)); 86 87 Object.seal(this); 88}; 89 90/** 91 * State of notification. 92 * @enum {string} 93 * @const 94 * @private 95 */ 96ProgressCenter.Notifications_.NotificationState_ = Object.freeze({ 97 VISIBLE: 'visible', 98 DISMISSED: 'dismissed' 99}); 100 101/** 102 * Updates the notification according to the item. 103 * @param {ProgressCenterItem} item Item to contain new information. 104 * @param {boolean} newItemAcceptable Whether to accept new item or not. 105 */ 106ProgressCenter.Notifications_.prototype.updateItem = function( 107 item, newItemAcceptable) { 108 var NotificationState = ProgressCenter.Notifications_.NotificationState_; 109 var newlyAdded = !(item.id in this.ids_); 110 111 // If new item is not acceptable, just return. 112 if (newlyAdded && !newItemAcceptable) 113 return; 114 115 // Update the ID map and return if we does not show a notification for the 116 // item. 117 if (item.state === ProgressItemState.PROGRESSING) { 118 if (newlyAdded) 119 this.ids_[item.id] = NotificationState.VISIBLE; 120 else if (this.ids_[item.id] === NotificationState.DISMISSED) 121 return; 122 } else { 123 // This notification is no longer tracked. 124 var previousState = this.ids_[item.id]; 125 delete this.ids_[item.id]; 126 // Clear notifications for complete or canceled items. 127 if (item.state === ProgressItemState.CANCELED || 128 item.state === ProgressItemState.COMPLETED) { 129 if (previousState === NotificationState.VISIBLE) { 130 this.queue_.run(function(proceed) { 131 chrome.notifications.clear(item.id, proceed); 132 }); 133 } 134 return; 135 } 136 } 137 138 // Create/update the notification with the item. 139 this.queue_.run(function(proceed) { 140 var params = { 141 title: chrome.runtime.getManifest().name, 142 iconUrl: chrome.runtime.getURL('/common/images/icon96.png'), 143 type: item.state === ProgressItemState.PROGRESSING ? 'progress' : 'basic', 144 message: item.message, 145 buttons: item.cancelable ? [{title: str('CANCEL_LABEL')}] : undefined, 146 progress: item.state === ProgressItemState.PROGRESSING ? 147 item.progressRateByPercent : undefined 148 }; 149 if (newlyAdded) 150 chrome.notifications.create(item.id, params, proceed); 151 else 152 chrome.notifications.update(item.id, params, proceed); 153 }.bind(this)); 154}; 155 156/** 157 * Handles cancel button click. 158 * @param {string} id Item ID. 159 * @private 160 */ 161ProgressCenter.Notifications_.prototype.onButtonClicked_ = function(id) { 162 if (id in this.ids_) 163 this.cancelCallback_(id); 164}; 165 166/** 167 * Handles notification close. 168 * @param {string} id Item ID. 169 * @private 170 */ 171ProgressCenter.Notifications_.prototype.onClosed_ = function(id) { 172 if (id in this.ids_) 173 this.ids_[id] = ProgressCenter.Notifications_.NotificationState_.DISMISSED; 174}; 175 176/** 177 * Utility for timeout callback. 178 * 179 * @param {function(*):*} callback Callback function. 180 * @constructor 181 * @private 182 */ 183ProgressCenter.TimeoutManager_ = function(callback) { 184 this.callback_ = callback; 185 this.id_ = null; 186 Object.seal(this); 187}; 188 189/** 190 * Requests timeout. Previous request is canceled. 191 * @param {number} milliseconds Time to invoke the callback function. 192 */ 193ProgressCenter.TimeoutManager_.prototype.request = function(milliseconds) { 194 if (this.id_) 195 clearTimeout(this.id_); 196 this.id_ = setTimeout(function() { 197 this.id_ = null; 198 this.callback_(); 199 }.bind(this), milliseconds); 200}; 201 202/** 203 * Cancels the timeout and invoke the callback function synchronously. 204 */ 205ProgressCenter.TimeoutManager_.prototype.callImmediately = function() { 206 if (this.id_) 207 clearTimeout(this.id_); 208 this.id_ = null; 209 this.callback_(); 210}; 211 212/** 213 * Updates the item in the progress center. 214 * If the item has a new ID, the item is added to the item list. 215 * 216 * @param {ProgressCenterItem} item Updated item. 217 */ 218ProgressCenter.prototype.updateItem = function(item) { 219 // Update item. 220 var index = this.getItemIndex_(item.id); 221 if (index === -1) 222 this.items_.push(item); 223 else 224 this.items_[index] = item; 225 226 // Update panels. 227 var summarizedItem = this.getSummarizedItem_(); 228 for (var i = 0; i < this.panels_.length; i++) { 229 this.panels_[i].updateItem(item); 230 this.panels_[i].updateCloseView(summarizedItem); 231 } 232 233 // Update notifications. 234 this.notifications_.updateItem(item, !this.panels_.length); 235 236 // Reset if there is no item. 237 for (var i = 0; i < this.items_.length; i++) { 238 switch (this.items_[i].state) { 239 case ProgressItemState.PROGRESSING: 240 return; 241 case ProgressItemState.ERROR: 242 this.resetTimeout_.request(ProgressCenter.RESET_DELAY_TIME_MS_); 243 return; 244 } 245 } 246 247 // Cancel timeout and call reset function immediately. 248 this.resetTimeout_.callImmediately(); 249}; 250 251/** 252 * Requests to cancel the progress item. 253 * @param {string} id Progress ID to be requested to cancel. 254 */ 255ProgressCenter.prototype.requestCancel = function(id) { 256 var item = this.getItemById(id); 257 if (item && item.cancelCallback) 258 item.cancelCallback(); 259}; 260 261/** 262 * Adds a panel UI to the notification center. 263 * @param {ProgressCenterPanel} panel Panel UI. 264 */ 265ProgressCenter.prototype.addPanel = function(panel) { 266 if (this.panels_.indexOf(panel) !== -1) 267 return; 268 269 // Update the panel list. 270 this.panels_.push(panel); 271 272 // Set the current items. 273 for (var i = 0; i < this.items_.length; i++) 274 panel.updateItem(this.items_[i]); 275 var summarizedItem = this.getSummarizedItem_(); 276 if (summarizedItem) 277 panel.updateCloseView(summarizedItem); 278 279 // Register the cancel callback. 280 panel.cancelCallback = this.requestCancel.bind(this); 281}; 282 283/** 284 * Removes a panel UI from the notification center. 285 * @param {ProgressCenterPanel} panel Panel UI. 286 */ 287ProgressCenter.prototype.removePanel = function(panel) { 288 var index = this.panels_.indexOf(panel); 289 if (index === -1) 290 return; 291 292 this.panels_.splice(index, 1); 293 panel.cancelCallback = null; 294 295 // If there is no panel, show the notifications. 296 if (this.panels_.length) 297 return; 298 for (var i = 0; i < this.items_.length; i++) 299 this.notifications_.updateItem(this.items_[i], true); 300}; 301 302/** 303 * Obtains the summarized item to be displayed in the closed progress center 304 * panel. 305 * @return {ProgressCenterItem} Summarized item. Returns null if there is no 306 * item. 307 * @private 308 */ 309ProgressCenter.prototype.getSummarizedItem_ = function() { 310 var summarizedItem = new ProgressCenterItem(); 311 var progressingItems = []; 312 var completedItems = []; 313 var canceledItems = []; 314 var errorItems = []; 315 316 for (var i = 0; i < this.items_.length; i++) { 317 // Count states. 318 switch (this.items_[i].state) { 319 case ProgressItemState.PROGRESSING: 320 progressingItems.push(this.items_[i]); 321 break; 322 case ProgressItemState.COMPLETED: 323 completedItems.push(this.items_[i]); 324 break; 325 case ProgressItemState.CANCELED: 326 canceledItems.push(this.items_[i]); 327 break; 328 case ProgressItemState.ERROR: 329 errorItems.push(this.items_[i]); 330 break; 331 } 332 333 // If all of the progressing items have the same type, then use 334 // it. Otherwise use TRANSFER, since it is the most generic. 335 if (this.items_[i].state === ProgressItemState.PROGRESSING) { 336 if (summarizedItem.type === null) 337 summarizedItem.type = this.items_[i].type; 338 else if (summarizedItem.type !== this.items_[i].type) 339 summarizedItem.type = ProgressItemType.TRANSFER; 340 } 341 342 // Sum up the progress values. 343 if (this.items_[i].state === ProgressItemState.PROGRESSING || 344 this.items_[i].state === ProgressItemState.COMPLETED) { 345 summarizedItem.progressMax += this.items_[i].progressMax; 346 summarizedItem.progressValue += this.items_[i].progressValue; 347 } 348 } 349 350 // If there are multiple visible (progressing and error) items, show the 351 // summarized message. 352 if (progressingItems.length + errorItems.length > 1) { 353 // Set item message. 354 var messages = []; 355 if (progressingItems.length > 0) { 356 switch (summarizedItem.type) { 357 case ProgressItemType.COPY: 358 messages.push(str('COPY_PROGRESS_SUMMARY')); 359 break; 360 case ProgressItemType.MOVE: 361 messages.push(str('MOVE_PROGRESS_SUMMARY')); 362 break; 363 case ProgressItemType.DELETE: 364 messages.push(str('DELETE_PROGRESS_SUMMARY')); 365 break; 366 case ProgressItemType.ZIP: 367 messages.push(str('ZIP_PROGRESS_SUMMARY')); 368 break; 369 case ProgressItemType.TRANSFER: 370 messages.push(str('TRANSFER_PROGRESS_SUMMARY')); 371 break; 372 } 373 } 374 if (errorItems.length === 1) 375 messages.push(str('ERROR_PROGRESS_SUMMARY')); 376 else if (errorItems.length > 1) 377 messages.push(strf('ERROR_PROGRESS_SUMMARY_PLURAL', errorItems.length)); 378 379 summarizedItem.summarized = true; 380 summarizedItem.message = messages.join(' '); 381 summarizedItem.state = progressingItems.length > 0 ? 382 ProgressItemState.PROGRESSING : ProgressItemState.ERROR; 383 return summarizedItem; 384 } 385 386 // If there is 1 visible item, show the item message. 387 if (progressingItems.length + errorItems.length === 1) { 388 var visibleItem = progressingItems[0] || errorItems[0]; 389 summarizedItem.id = visibleItem.id; 390 summarizedItem.cancelCallback = visibleItem.cancelCallback; 391 summarizedItem.type = visibleItem.type; 392 summarizedItem.message = visibleItem.message; 393 summarizedItem.state = visibleItem.state; 394 return summarizedItem; 395 } 396 397 // If there is no visible item, the message can be empty. 398 if (completedItems.length > 0) { 399 summarizedItem.state = ProgressItemState.COMPLETED; 400 return summarizedItem; 401 } 402 if (canceledItems.length > 0) { 403 summarizedItem.state = ProgressItemState.CANCELED; 404 return summarizedItem; 405 } 406 407 // If there is no item, return null. 408 return null; 409}; 410 411/** 412 * Obtains item by ID. 413 * @param {string} id ID of progress item. 414 * @return {ProgressCenterItem} Progress center item having the specified 415 * ID. Null if the item is not found. 416 */ 417ProgressCenter.prototype.getItemById = function(id) { 418 return this.items_[this.getItemIndex_(id)]; 419}; 420 421/** 422 * Obtains item index that have the specifying ID. 423 * @param {string} id Item ID. 424 * @return {number} Item index. Returns -1 If the item is not found. 425 * @private 426 */ 427ProgressCenter.prototype.getItemIndex_ = function(id) { 428 for (var i = 0; i < this.items_.length; i++) { 429 if (this.items_[i].id === id) 430 return i; 431 } 432 return -1; 433}; 434 435/** 436 * Hides the progress center if there is no progressing items. 437 * @private 438 */ 439ProgressCenter.prototype.reset_ = function() { 440 // If we have a progressing item, stop reset. 441 for (var i = 0; i < this.items_.length; i++) { 442 if (this.items_[i].state == ProgressItemState.PROGRESSING) 443 return; 444 } 445 446 // Reset items. 447 this.items_.splice(0, this.items_.length); 448 449 // Dispatch a event. 450 for (var i = 0; i < this.panels_.length; i++) 451 this.panels_[i].reset(); 452}; 453