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 * An event handler of the background page for file operaitons.
9 * @param {Background} background Background page.
10 * @constructor
11 */
12var FileOperationHandler = function(background) {
13  /**
14   * Background page.
15   * @type {Background}
16   * @private
17   */
18  this.background_ = background;
19
20  /**
21   * File operation manager.
22   * @type {FileOperationManager}
23   * @private
24   */
25  this.fileOperationManager_ = background.fileOperationManager;
26
27  /**
28   * Progress center.
29   * @type {progressCenter}
30   * @private
31   */
32  this.progressCenter_ = background.progressCenter;
33
34  /**
35   * Pending items of delete operation.
36   *
37   * Delete operations are usually complete quickly.
38   * So we would not like to show the progress bar at first.
39   * If the operation takes more than FileOperationHandler.PENDING_TIME_MS_,
40   * we adds the item to the progress center.
41   *
42   * @type {Object.<string, ProgressCenterItem>}}
43   * @private
44   */
45  this.pendingItems_ = {};
46
47  // Register event.
48  this.fileOperationManager_.addEventListener(
49      'copy-progress',
50      this.onCopyProgress_.bind(this));
51  this.fileOperationManager_.addEventListener(
52      'delete',
53      this.onDeleteProgress_.bind(this));
54
55  // Seal the object.
56  Object.seal(this);
57};
58
59/**
60 * Pending time before a delete item is added to the progress center.
61 *
62 * @type {number}
63 * @const
64 * @private
65 */
66FileOperationHandler.PENDING_TIME_MS_ = 500;
67
68/**
69 * Generate a progress message from the event.
70 * @param {Event} event Progress event.
71 * @return {string} message.
72 * @private
73 */
74FileOperationHandler.getMessage_ = function(event) {
75  if (event.reason === 'ERROR') {
76    switch (event.error.code) {
77      case util.FileOperationErrorType.TARGET_EXISTS:
78        var name = event.error.data.name;
79        if (event.error.data.isDirectory)
80          name += '/';
81        switch (event.status.operationType) {
82          case 'COPY': return strf('COPY_TARGET_EXISTS_ERROR', name);
83          case 'MOVE': return strf('MOVE_TARGET_EXISTS_ERROR', name);
84          case 'ZIP': return strf('ZIP_TARGET_EXISTS_ERROR', name);
85          default: return strf('TRANSFER_TARGET_EXISTS_ERROR', name);
86        }
87
88      case util.FileOperationErrorType.FILESYSTEM_ERROR:
89        var detail = util.getFileErrorString(event.error.data.code);
90        switch (event.status.operationType) {
91          case 'COPY': return strf('COPY_FILESYSTEM_ERROR', detail);
92          case 'MOVE': return strf('MOVE_FILESYSTEM_ERROR', detail);
93          case 'ZIP': return strf('ZIP_FILESYSTEM_ERROR', detail);
94          default: return strf('TRANSFER_FILESYSTEM_ERROR', detail);
95        }
96
97      default:
98        switch (event.status.operationType) {
99          case 'COPY': return strf('COPY_UNEXPECTED_ERROR', event.error.code);
100          case 'MOVE': return strf('MOVE_UNEXPECTED_ERROR', event.error.code);
101          case 'ZIP': return strf('ZIP_UNEXPECTED_ERROR', event.error.code);
102          default: return strf('TRANSFER_UNEXPECTED_ERROR', event.error.code);
103        }
104    }
105  } else if (event.status.numRemainingItems === 1) {
106    var name = event.status.processingEntry.name;
107    switch (event.status.operationType) {
108      case 'COPY': return strf('COPY_FILE_NAME', name);
109      case 'MOVE': return strf('MOVE_FILE_NAME', name);
110      case 'ZIP': return strf('ZIP_FILE_NAME', name);
111      default: return strf('TRANSFER_FILE_NAME', name);
112    }
113  } else {
114    var remainNumber = event.status.numRemainingItems;
115    switch (event.status.operationType) {
116      case 'COPY': return strf('COPY_ITEMS_REMAINING', remainNumber);
117      case 'MOVE': return strf('MOVE_ITEMS_REMAINING', remainNumber);
118      case 'ZIP': return strf('ZIP_ITEMS_REMAINING', remainNumber);
119      default: return strf('TRANSFER_ITEMS_REMAINING', remainNumber);
120    }
121  }
122};
123
124/**
125 * Generates a delete message from the event.
126 * @param {Event} event Progress event.
127 * @return {string} message.
128 * @private
129 */
130FileOperationHandler.getDeleteMessage_ = function(event) {
131  if (event.reason === 'ERROR') {
132    return str('DELETE_ERROR');
133  } else if (event.entries.length == 1) {
134    var fileName = event.entries[0].name;
135    return strf('DELETE_FILE_NAME', fileName);
136  } else if (event.entries.length > 1) {
137    return strf('DELETE_ITEMS_REMAINING', event.entries.length);
138  } else {
139    return '';
140  }
141};
142
143/**
144 * Obtains ProgressItemType from OperationType of FileTransferManager.
145 * @param {string} operationType OperationType of FileTransferManager.
146 * @return {ProgressItemType} ProgreeType corresponding to the specified
147 *     operation type.
148 * @private
149 */
150FileOperationHandler.getType_ = function(operationType) {
151  switch (operationType) {
152    case 'COPY': return ProgressItemType.COPY;
153    case 'MOVE': return ProgressItemType.MOVE;
154    case 'ZIP': return ProgressItemType.ZIP;
155    default:
156      console.error('Unknown operation type.');
157      return ProgressItemType.TRANSFER;
158  }
159};
160
161/**
162 * Handles the copy-progress event.
163 * @param {Event} event The copy-progress event.
164 * @private
165 */
166FileOperationHandler.prototype.onCopyProgress_ = function(event) {
167  // If the copy is finished, may be we can close the background page.
168  if (event.reason !== 'BEGIN' && event.reason !== 'PROGRESS')
169    this.background_.tryClose();
170
171  // Update progress center.
172  var progressCenter = this.progressCenter_;
173  var item;
174  switch (event.reason) {
175    case 'BEGIN':
176      item = new ProgressCenterItem();
177      item.id = event.taskId;
178      item.type = FileOperationHandler.getType_(event.status.operationType);
179      item.message = FileOperationHandler.getMessage_(event);
180      item.progressMax = event.status.totalBytes;
181      item.progressValue = event.status.processedBytes;
182      item.cancelCallback = this.fileOperationManager_.requestTaskCancel.bind(
183          this.fileOperationManager_,
184          event.taskId);
185      progressCenter.updateItem(item);
186      break;
187
188    case 'PROGRESS':
189      item = progressCenter.getItemById(event.taskId);
190      if (!item) {
191        console.error('Cannot find copying item.');
192        return;
193      }
194      item.message = FileOperationHandler.getMessage_(event);
195      item.progressValue = event.status.processedBytes;
196      progressCenter.updateItem(item);
197      break;
198
199    case 'SUCCESS':
200    case 'CANCELED':
201    case 'ERROR':
202      item = progressCenter.getItemById(event.taskId);
203      if (!item) {
204        // ERROR events can be dispatched before BEGIN events.
205        item = new ProgressCenterItem();
206        item.type = FileOperationHandler.getType_(event.status.operationType);
207        item.id = event.taskId;
208        item.progressMax = 1;
209      }
210      if (event.reason === 'SUCCESS') {
211        item.message = '';
212        item.state = ProgressItemState.COMPLETED;
213        item.progressValue = item.progressMax;
214      } else if (event.reason === 'CANCELED') {
215        item.message = '';
216        item.state = ProgressItemState.CANCELED;
217      } else {
218        item.message = FileOperationHandler.getMessage_(event);
219        item.state = ProgressItemState.ERROR;
220      }
221      progressCenter.updateItem(item);
222      break;
223  }
224};
225
226/**
227 * Handles the delete event.
228 * @param {Event} event The delete event.
229 * @private
230 */
231FileOperationHandler.prototype.onDeleteProgress_ = function(event) {
232  // If the copy is finished, may be we can close the background page.
233  if (event.reason !== 'BEGIN' && event.reason !== 'PROGRESS')
234    this.background_.tryClose();
235
236  // Update progress center.
237  var progressCenter = this.progressCenter_;
238  var item;
239  var pending;
240  switch (event.reason) {
241    case 'BEGIN':
242      item = new ProgressCenterItem();
243      item.id = event.taskId;
244      item.type = ProgressItemType.DELETE;
245      item.message = FileOperationHandler.getDeleteMessage_(event);
246      item.progressMax = event.totalBytes;
247      item.progressValue = event.processedBytes;
248      item.cancelCallback = this.fileOperationManager_.requestTaskCancel.bind(
249          this.fileOperationManager_,
250          event.taskId);
251      this.pendingItems_[item.id] = item;
252      setTimeout(this.showPendingItem_.bind(this, item),
253                 FileOperationHandler.PENDING_TIME_MS_);
254      break;
255
256    case 'PROGRESS':
257      pending = event.taskId in this.pendingItems_;
258      item = this.pendingItems_[event.taskId] ||
259          progressCenter.getItemById(event.taskId);
260      if (!item) {
261        console.error('Cannot find deleting item.');
262        return;
263      }
264      item.message = FileOperationHandler.getDeleteMessage_(event);
265      item.progressMax = event.totalBytes;
266      item.progressValue = event.processedBytes;
267      if (!pending)
268        progressCenter.updateItem(item);
269      break;
270
271    case 'SUCCESS':
272    case 'CANCELED':
273    case 'ERROR':
274      // Obtain working variable.
275      pending = event.taskId in this.pendingItems_;
276      item = this.pendingItems_[event.taskId] ||
277          progressCenter.getItemById(event.taskId);
278      if (!item) {
279        console.error('Cannot find deleting item.');
280        return;
281      }
282
283      // Update the item.
284      item.message = FileOperationHandler.getDeleteMessage_(event);
285      if (event.reason === 'SUCCESS') {
286        item.state = ProgressItemState.COMPLETED;
287        item.progressValue = item.progressMax;
288      } else if (event.reason === 'CANCELED') {
289        item.state = ProgressItemState.CANCELED;
290      } else {
291        item.state = ProgressItemState.ERROR;
292      }
293
294      // Apply the change.
295      if (!pending || event.reason === 'ERROR')
296        progressCenter.updateItem(item);
297      if (pending)
298        delete this.pendingItems_[event.taskId];
299      break;
300  }
301};
302
303/**
304 * Shows the pending item.
305 *
306 * @param {ProgressCenterItem} item Pending item.
307 * @private
308 */
309FileOperationHandler.prototype.showPendingItem_ = function(item) {
310  // The item is already gone.
311  if (!this.pendingItems_[item.id])
312    return;
313  delete this.pendingItems_[item.id];
314  this.progressCenter_.updateItem(item);
315};
316