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