• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2014 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 * Handler of device event.
9 * @constructor
10 */
11function DeviceHandler() {
12  /**
13   * Map of device path and mount status of devices.
14   * @type {Object.<string, DeviceHandler.MountStatus>}
15   * @private
16   */
17  this.mountStatus_ = {};
18
19  /**
20   * List of ID of notifications that have a button.
21   * @type {Array.<string>}
22   * @private
23   */
24  this.buttonNotifications_ = [];
25
26  chrome.fileBrowserPrivate.onDeviceChanged.addListener(
27      this.onDeviceChanged_.bind(this));
28  chrome.fileBrowserPrivate.onMountCompleted.addListener(
29      this.onMountCompleted_.bind(this));
30  chrome.notifications.onButtonClicked.addListener(
31      this.onNotificationButtonClicked_.bind(this));
32
33  Object.seal(this);
34}
35
36/**
37 * Notification type.
38 * @param {string} prefix Prefix of notification ID.
39 * @param {string} title String ID of title.
40 * @param {string} message String ID of message.
41 * @param {string=} opt_buttonLabel String ID of the button label.
42 * @constructor
43 */
44DeviceHandler.Notification = function(prefix, title, message, opt_buttonLabel) {
45  /**
46   * Prefix of notification ID.
47   * @type {string}
48   */
49  this.prefix = prefix;
50
51  /**
52   * String ID of title.
53   * @type {string}
54   */
55  this.title = title;
56
57  /**
58   * String ID of message.
59   * @type {string}
60   */
61  this.message = message;
62
63  /**
64   * String ID of button label.
65   * @type {?string}
66   */
67  this.buttonLabel = opt_buttonLabel || null;
68
69  /**
70   * Queue of API call.
71   * @type {AsyncUtil.Queue}
72   * @private
73   */
74  this.queue_ = new AsyncUtil.Queue();
75
76  /**
77   * Timeout ID.
78   * @type {number}
79   * @private
80   */
81  this.pendingShowTimerId_ = 0;
82
83  Object.seal(this);
84};
85
86/**
87 * @type {DeviceHandler.Notification}
88 * @const
89 */
90DeviceHandler.Notification.DEVICE = new DeviceHandler.Notification(
91    'device',
92    'REMOVABLE_DEVICE_DETECTION_TITLE',
93    'REMOVABLE_DEVICE_SCANNING_MESSAGE');
94
95/**
96 * @type {DeviceHandler.Notification}
97 * @const
98 */
99DeviceHandler.Notification.DEVICE_FAIL = new DeviceHandler.Notification(
100    'deviceFail',
101    'REMOVABLE_DEVICE_DETECTION_TITLE',
102    'DEVICE_UNSUPPORTED_DEFAULT_MESSAGE');
103
104/**
105 * @type {DeviceHandler.Notification}
106 * @const
107 */
108DeviceHandler.Notification.DEVICE_EXTERNAL_STORAGE_DISABLED =
109    new DeviceHandler.Notification(
110        'deviceFail',
111        'REMOVABLE_DEVICE_DETECTION_TITLE',
112        'EXTERNAL_STORAGE_DISABLED_MESSAGE');
113
114/**
115 * @type {DeviceHandler.Notification}
116 * @const
117 */
118DeviceHandler.Notification.DEVICE_HARD_UNPLUGGED =
119    new DeviceHandler.Notification(
120        'deviceFail',
121        'DEVICE_HARD_UNPLUGGED_TITLE',
122        'DEVICE_HARD_UNPLUGGED_MESSAGE',
123        'DEVICE_HARD_UNPLUGGED_BUTTON_LABEL');
124
125/**
126 * @type {DeviceHandler.Notification}
127 * @const
128 */
129DeviceHandler.Notification.FORMAT_START = new DeviceHandler.Notification(
130    'formatStart',
131    'FORMATTING_OF_DEVICE_PENDING_TITLE',
132    'FORMATTING_OF_DEVICE_PENDING_MESSAGE');
133
134/**
135 * @type {DeviceHandler.Notification}
136 * @const
137 */
138DeviceHandler.Notification.FORMAT_SUCCESS = new DeviceHandler.Notification(
139    'formatSuccess',
140    'FORMATTING_OF_DEVICE_FINISHED_TITLE',
141    'FORMATTING_FINISHED_SUCCESS_MESSAGE');
142
143/**
144 * @type {DeviceHandler.Notification}
145 * @const
146 */
147DeviceHandler.Notification.FORMAT_FAIL = new DeviceHandler.Notification(
148    'formatFail',
149    'FORMATTING_OF_DEVICE_FAILED_TITLE',
150    'FORMATTING_FINISHED_FAILURE_MESSAGE');
151
152/**
153 * Shows the notification for the device path.
154 * @param {string} devicePath Device path.
155 * @param {string=} opt_message Message overrides the default message.
156 * @return {string} Notification ID.
157 */
158DeviceHandler.Notification.prototype.show = function(devicePath, opt_message) {
159  this.clearTimeout_();
160  var notificationId = this.makeId_(devicePath);
161  this.queue_.run(function(callback) {
162    var buttons =
163        this.buttonLabel ? [{title: str(this.buttonLabel)}] : undefined;
164    chrome.notifications.create(
165        notificationId,
166        {
167          type: 'basic',
168          title: str(this.title),
169          message: opt_message || str(this.message),
170          iconUrl: chrome.runtime.getURL('/common/images/icon96.png'),
171          buttons: buttons
172        },
173        callback);
174  }.bind(this));
175  return notificationId;
176};
177
178/**
179 * Shows the notification after 5 seconds.
180 * @param {string} devicePath Device path.
181 */
182DeviceHandler.Notification.prototype.showLater = function(devicePath) {
183  this.clearTimeout_();
184  this.pendingShowTimerId_ = setTimeout(this.show.bind(this, devicePath), 5000);
185};
186
187/**
188 * Hides the notification for the device path.
189 * @param {string} devicePath Device path.
190 */
191DeviceHandler.Notification.prototype.hide = function(devicePath) {
192  this.clearTimeout_();
193  this.queue_.run(function(callback) {
194    chrome.notifications.clear(this.makeId_(devicePath), callback);
195  }.bind(this));
196};
197
198/**
199 * Makes a notification ID for the device path.
200 * @param {string} devicePath Device path.
201 * @return {string} Notification ID.
202 * @private
203 */
204DeviceHandler.Notification.prototype.makeId_ = function(devicePath) {
205  return this.prefix + ':' + devicePath;
206};
207
208/**
209 * Cancels the timeout request.
210 * @private
211 */
212DeviceHandler.Notification.prototype.clearTimeout_ = function() {
213  if (this.pendingShowTimerId_) {
214    clearTimeout(this.pendingShowTimerId_);
215    this.pendingShowTimerId_ = 0;
216  }
217};
218
219/**
220 * Handles notifications from C++ sides.
221 * @param {DeviceEvent} event Device event.
222 * @private
223 */
224DeviceHandler.prototype.onDeviceChanged_ = function(event) {
225  switch (event.type) {
226    case 'added':
227      DeviceHandler.Notification.DEVICE.showLater(event.devicePath);
228      this.mountStatus_[event.devicePath] = DeviceHandler.MountStatus.NO_RESULT;
229      break;
230    case 'disabled':
231      DeviceHandler.Notification.DEVICE_EXTERNAL_STORAGE_DISABLED.show(
232          event.devicePath);
233      break;
234    case 'scan_canceled':
235      DeviceHandler.Notification.DEVICE.hide(event.devicePath);
236      break;
237    case 'removed':
238      DeviceHandler.Notification.DEVICE.hide(event.devicePath);
239      DeviceHandler.Notification.DEVICE_FAIL.hide(event.devicePath);
240      DeviceHandler.Notification.DEVICE_EXTERNAL_STORAGE_DISABLED.hide(
241          event.devicePath);
242      delete this.mountStatus_[event.devicePath];
243      break;
244    case 'hard_unplugged':
245      var id = DeviceHandler.Notification.DEVICE_HARD_UNPLUGGED.show(
246          event.devicePath);
247      this.buttonNotifications_.push(id);
248      break;
249    case 'format_start':
250      DeviceHandler.Notification.FORMAT_START.show(event.devicePath);
251      break;
252    case 'format_success':
253      DeviceHandler.Notification.FORMAT_START.hide(event.devicePath);
254      DeviceHandler.Notification.FORMAT_SUCCESS.show(event.devicePath);
255      break;
256    case 'format_fail':
257      DeviceHandler.Notification.FORMAT_START.hide(event.devicePath);
258      DeviceHandler.Notification.FORMAT_FAIL.show(event.devicePath);
259      break;
260  }
261};
262
263/**
264 * Mount status for the device.
265 * Each multi-partition devices can obtain multiple mount completed events.
266 * This status shows what results are already obtained for the device.
267 * @enum {string}
268 * @const
269 */
270DeviceHandler.MountStatus = Object.freeze({
271  // There is no mount results on the device.
272  NO_RESULT: 'noResult',
273  // There is no error on the device.
274  SUCCESS: 'success',
275  // There is only parent errors, that can be overridden by child results.
276  ONLY_PARENT_ERROR: 'onlyParentError',
277  // There is one child error.
278  CHILD_ERROR: 'childError',
279  // There is multiple child results and at least one is failure.
280  MULTIPART_ERROR: 'multipartError'
281});
282
283/**
284 * Handles mount completed events to show notifications for removable devices.
285 * @param {MountCompletedEvent} event Mount completed event.
286 * @private
287 */
288DeviceHandler.prototype.onMountCompleted_ = function(event) {
289  // If this is remounting, which happens when resuming ChromeOS, the device has
290  // already inserted to the computer. So we suppress the notification.
291  var volume = event.volumeMetadata;
292  if (!volume.deviceType || event.isRemounting)
293    return;
294
295  var getFirstStatus = function(event) {
296    if (event.status === 'success')
297      return DeviceHandler.MountStatus.SUCCESS;
298    else if (event.volumeMetadata.isParentDevice)
299      return DeviceHandler.MountStatus.ONLY_PARENT_ERROR;
300    else
301      return DeviceHandler.MountStatus.CHILD_ERROR;
302  };
303
304  // Update the current status.
305  switch (this.mountStatus_[volume.devicePath]) {
306    // If there is no related device, do nothing.
307    case undefined:
308      return;
309    // If the multipart error message has already shown, do nothing because the
310    // message does not changed by the following mount results.
311    case DeviceHandler.MULTIPART_ERROR:
312      return;
313    // If this is the first result, hide the scanning notification.
314    case DeviceHandler.MountStatus.NO_RESULT:
315      DeviceHandler.Notification.DEVICE.hide(volume.devicePath);
316      this.mountStatus_[volume.devicePath] = getFirstStatus(event);
317      break;
318    // If there are only parent errors, and the new result is child's one, hide
319    // the parent error. (parent device contains partition table, which is
320    // unmountable)
321    case DeviceHandler.MountStatus.ONLY_PARENT_ERROR:
322      if (!volume.isParentDevice)
323        DeviceHandler.Notification.DEVICE_FAIL.hide(volume.devicePath);
324      this.mountStatus_[volume.devicePath] = getFirstStatus(event);
325      break;
326    // We have a multi-partition device for which at least one mount
327    // failed.
328    case DeviceHandler.MountStatus.SUCCESS:
329    case DeviceHandler.MountStatus.CHILD_ERROR:
330      if (this.mountStatus_[volume.devicePath] ===
331              DeviceHandler.MountStatus.SUCCESS &&
332          event.status === 'success') {
333        this.mountStatus_[volume.devicePath] =
334            DeviceHandler.MountStatus.SUCCESS;
335      } else {
336        this.mountStatus_[volume.devicePath] =
337            DeviceHandler.MountStatus.MULTIPART_ERROR;
338      }
339      break;
340  }
341
342  // Show the notification for the current errors.
343  // If there is no error, do not show/update the notification.
344  var message;
345  switch (this.mountStatus_[volume.devicePath]) {
346    case DeviceHandler.MountStatus.MULTIPART_ERROR:
347      message = volume.deviceLabel ?
348          strf('MULTIPART_DEVICE_UNSUPPORTED_MESSAGE', volume.deviceLabel) :
349          str('MULTIPART_DEVICE_UNSUPPORTED_DEFAULT_MESSAGE');
350      break;
351    case DeviceHandler.MountStatus.CHILD_ERROR:
352    case DeviceHandler.MountStatus.ONLY_PARENT_ERROR:
353      if (event.status === 'error_unsuported_filesystem') {
354        message = volume.deviceLabel ?
355            strf('DEVICE_UNSUPPORTED_MESSAGE', volume.deviceLabel) :
356            str('DEVICE_UNSUPPORTED_DEFAULT_MESSAGE');
357      } else {
358        message = volume.deviceLabel ?
359            strf('DEVICE_UNKNOWN_MESSAGE', volume.deviceLabel) :
360            str('DEVICE_UNKNOWN_DEFAULT_MESSAGE');
361      }
362      break;
363  }
364  if (message) {
365    DeviceHandler.Notification.DEVICE_FAIL.hide(volume.devicePath);
366    DeviceHandler.Notification.DEVICE_FAIL.show(volume.devicePath, message);
367  }
368};
369
370/**
371 * Handles notification button click.
372 * @param {string} id ID of the notification.
373 * @private
374 */
375DeviceHandler.prototype.onNotificationButtonClicked_ = function(id) {
376  var index = this.buttonNotifications_.indexOf(id);
377  if (index !== -1) {
378    chrome.notifications.clear(id, function() {});
379    this.buttonNotifications_.splice(index, 1);
380  }
381};
382