• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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