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