1 // Copyright (c) 2012 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 * This object encapsulates everything related to tasks execution. 9 * 10 * @param {FileManager} fileManager FileManager instance. 11 * @param {Object=} opt_params File manager load parameters. 12 * @constructor 13 */ 14 function FileTasks(fileManager, opt_params) { 15 this.fileManager_ = fileManager; 16 this.params_ = opt_params; 17 this.tasks_ = null; 18 this.defaultTask_ = null; 19 20 /** 21 * List of invocations to be called once tasks are available. 22 * 23 * @private 24 * @type {Array.<Object>} 25 */ 26 this.pendingInvocations_ = []; 27 } 28 29 /** 30 * Location of the FAQ about the file actions. 31 * 32 * @const 33 * @type {string} 34 */ 35 FileTasks.NO_ACTION_FOR_FILE_URL = 'http://support.google.com/chromeos/bin/' + 36 'answer.py?answer=1700055&topic=29026&ctx=topic'; 37 38 /** 39 * Base URL of apps list in the Chrome Web Store. This constant is used in 40 * FileTasks.createWebStoreLink(). 41 * @const 42 * @type {string} 43 */ 44 FileTasks.WEB_STORE_HANDLER_BASE_URL = 45 'https://chrome.google.com/webstore/category/collection/file_handlers'; 46 47 /** 48 * Returns URL of the Chrome Web Store which show apps supporting the given 49 * file-extension and mime-type. 50 * 51 * @param {string} extension Extension of the file. 52 * @param {string} mimeType Mime type of the file. 53 * @return {string} URL 54 */ 55 FileTasks.createWebStoreLink = function(extension, mimeType) { 56 var url = FileTasks.WEB_STORE_HANDLER_BASE_URL; 57 url += '?_fe=' + extension.toLowerCase().replace(/[^\w]/g, ''); 58 if (mimeType) 59 url += '&_fmt=' + mimeType.replace(/[^-\w\/]/g, ''); 60 return url; 61 }; 62 63 /** 64 * Complete the initialization. 65 * 66 * @param {Array.<string>} urls List of file urls. 67 * @param {Array.<string>=} opt_mimeTypes List of MIME types for each 68 * of the files. 69 */ 70 FileTasks.prototype.init = function(urls, opt_mimeTypes) { 71 this.urls_ = urls; 72 if (urls.length > 0) 73 chrome.fileBrowserPrivate.getFileTasks(urls, opt_mimeTypes || [], 74 this.onTasks_.bind(this)); 75 }; 76 77 /** 78 * Returns amount of tasks. 79 * 80 * @return {number} amount of tasks. 81 */ 82 FileTasks.prototype.size = function() { 83 return (this.tasks_ && this.tasks_.length) || 0; 84 }; 85 86 /** 87 * Callback when tasks found. 88 * 89 * @param {Array.<Object>} tasks The tasks. 90 * @private 91 */ 92 FileTasks.prototype.onTasks_ = function(tasks) { 93 this.processTasks_(tasks); 94 for (var index = 0; index < this.pendingInvocations_.length; index++) { 95 var name = this.pendingInvocations_[index][0]; 96 var args = this.pendingInvocations_[index][1]; 97 this[name].apply(this, args); 98 } 99 this.pendingInvocations_ = []; 100 }; 101 102 /** 103 * The list of known extensions to record UMA. 104 * Note: Because the data is recorded by the index, so new item shouldn't be 105 * inserted. 106 * 107 * @const 108 * @type {Array.<string>} 109 * @private 110 */ 111 FileTasks.knownExtensions_ = [ 112 'other', '.3ga', '.3gp', '.aac', '.alac', '.asf', '.avi', '.bmp', '.csv', 113 '.doc', '.docx', '.flac', '.gif', '.jpeg', '.jpg', '.log', '.m3u', '.m3u8', 114 '.m4a', '.m4v', '.mid', '.mkv', '.mov', '.mp3', '.mp4', '.mpg', '.odf', 115 '.odp', '.ods', '.odt', '.oga', '.ogg', '.ogv', '.pdf', '.png', '.ppt', 116 '.pptx', '.ra', '.ram', '.rar', '.rm', '.rtf', '.wav', '.webm', '.webp', 117 '.wma', '.wmv', '.xls', '.xlsx', 118 ]; 119 120 /** 121 * Records trial of opening file grouped by extensions. 122 * 123 * @param {Array.<string>} urls The path to be opened. 124 * @private 125 */ 126 FileTasks.recordViewingFileTypeUMA_ = function(urls) { 127 for (var i = 0; i < urls.length; i++) { 128 var url = urls[i]; 129 var extension = FileType.getExtension(url).toLowerCase(); 130 if (FileTasks.knownExtensions_.indexOf(extension) < 0) { 131 extension = 'other'; 132 } 133 metrics.recordEnum( 134 'ViewingFileType', extension, FileTasks.knownExtensions_); 135 } 136 }; 137 138 /** 139 * Processes internal tasks. 140 * 141 * @param {Array.<Object>} tasks The tasks. 142 * @private 143 */ 144 FileTasks.prototype.processTasks_ = function(tasks) { 145 this.tasks_ = []; 146 var id = chrome.runtime.id; 147 var isOnDrive = false; 148 for (var index = 0; index < this.urls_.length; ++index) { 149 if (FileType.isOnDrive(this.urls_[index])) { 150 isOnDrive = true; 151 break; 152 } 153 } 154 155 for (var i = 0; i < tasks.length; i++) { 156 var task = tasks[i]; 157 var taskParts = task.taskId.split('|'); 158 159 // Skip Drive App if the file is not on Drive. 160 if (!isOnDrive && task.driveApp) 161 continue; 162 163 // Skip internal Files.app's handlers. 164 if (taskParts[0] == id && (taskParts[2] == 'auto-open' || 165 taskParts[2] == 'select' || taskParts[2] == 'open')) { 166 continue; 167 } 168 169 // Tweak images, titles of internal tasks. 170 if (taskParts[0] == id && taskParts[1] == 'file') { 171 if (taskParts[2] == 'play') { 172 // TODO(serya): This hack needed until task.iconUrl is working 173 // (see GetFileTasksFileBrowserFunction::RunImpl). 174 task.iconType = 'audio'; 175 task.title = loadTimeData.getString('ACTION_LISTEN'); 176 } else if (taskParts[2] == 'mount-archive') { 177 task.iconType = 'archive'; 178 task.title = loadTimeData.getString('MOUNT_ARCHIVE'); 179 } else if (taskParts[2] == 'gallery') { 180 task.iconType = 'image'; 181 task.title = loadTimeData.getString('ACTION_OPEN'); 182 } else if (taskParts[2] == 'watch') { 183 task.iconType = 'video'; 184 task.title = loadTimeData.getString('ACTION_WATCH'); 185 } else if (taskParts[2] == 'open-hosted-generic') { 186 if (this.urls_.length > 1) 187 task.iconType = 'generic'; 188 else // Use specific icon. 189 task.iconType = FileType.getIcon(this.urls_[0]); 190 task.title = loadTimeData.getString('ACTION_OPEN'); 191 } else if (taskParts[2] == 'open-hosted-gdoc') { 192 task.iconType = 'gdoc'; 193 task.title = loadTimeData.getString('ACTION_OPEN_GDOC'); 194 } else if (taskParts[2] == 'open-hosted-gsheet') { 195 task.iconType = 'gsheet'; 196 task.title = loadTimeData.getString('ACTION_OPEN_GSHEET'); 197 } else if (taskParts[2] == 'open-hosted-gslides') { 198 task.iconType = 'gslides'; 199 task.title = loadTimeData.getString('ACTION_OPEN_GSLIDES'); 200 } else if (taskParts[2] == 'view-swf') { 201 // Do not render this task if disabled. 202 if (!loadTimeData.getBoolean('SWF_VIEW_ENABLED')) 203 continue; 204 task.iconType = 'generic'; 205 task.title = loadTimeData.getString('ACTION_VIEW'); 206 } else if (taskParts[2] == 'view-pdf') { 207 // Do not render this task if disabled. 208 if (!loadTimeData.getBoolean('PDF_VIEW_ENABLED')) 209 continue; 210 task.iconType = 'pdf'; 211 task.title = loadTimeData.getString('ACTION_VIEW'); 212 } else if (taskParts[2] == 'view-in-browser') { 213 task.iconType = 'generic'; 214 task.title = loadTimeData.getString('ACTION_VIEW'); 215 } else if (taskParts[2] == 'install-crx') { 216 task.iconType = 'generic'; 217 task.title = loadTimeData.getString('INSTALL_CRX'); 218 } 219 } 220 221 if (!task.iconType && taskParts[1] == 'web-intent') { 222 task.iconType = 'generic'; 223 } 224 225 this.tasks_.push(task); 226 if (this.defaultTask_ == null && task.isDefault) { 227 this.defaultTask_ = task; 228 } 229 } 230 if (!this.defaultTask_ && this.tasks_.length > 0) { 231 // If we haven't picked a default task yet, then just pick the first one. 232 // This is not the preferred way we want to pick this, but better this than 233 // no default at all if the C++ code didn't set one. 234 this.defaultTask_ = this.tasks_[0]; 235 } 236 }; 237 238 /** 239 * Executes default task. 240 * 241 * @private 242 */ 243 FileTasks.prototype.executeDefault_ = function() { 244 var urls = this.urls_; 245 FileTasks.recordViewingFileTypeUMA_(urls); 246 this.executeDefaultInternal_(urls); 247 }; 248 249 /** 250 * Executes default task. 251 * 252 * @param {Array.<string>} urls Urls to execute. 253 * @private 254 */ 255 FileTasks.prototype.executeDefaultInternal_ = function(urls) { 256 if (this.defaultTask_ != null) { 257 this.executeInternal_(this.defaultTask_.taskId, urls); 258 return; 259 } 260 261 // We don't have tasks, so try to show a file in a browser tab. 262 // We only do that for single selection to avoid confusion. 263 if (urls.length == 1) { 264 var callback = function(success) { 265 if (!success) { 266 var filename = decodeURIComponent(urls[0]); 267 if (filename.indexOf('/') != -1) 268 filename = filename.substr(filename.lastIndexOf('/') + 1); 269 var extension = filename.lastIndexOf('.') != -1 ? 270 filename.substr(filename.lastIndexOf('.') + 1) : ''; 271 272 this.fileManager_.metadataCache_.get(urls, 'drive', function(props) { 273 var mimeType; 274 if (props && props[0] && props[0].contentMimeType) 275 mimeType = props[0].contentMimeType; 276 277 var messageString = extension == 'exe' ? 'NO_ACTION_FOR_EXECUTABLE' : 278 'NO_ACTION_FOR_FILE'; 279 var webStoreUrl = FileTasks.createWebStoreLink(extension, mimeType); 280 var text = loadTimeData.getStringF(messageString, 281 webStoreUrl, 282 FileTasks.NO_ACTION_FOR_FILE_URL); 283 this.fileManager_.alert.showHtml(filename, text, function() {}); 284 }.bind(this)); 285 } 286 }.bind(this); 287 288 this.checkAvailability_(function() { 289 chrome.fileBrowserPrivate.viewFiles(urls, callback); 290 }.bind(this)); 291 } 292 293 // Do nothing for multiple urls. 294 }; 295 296 /** 297 * Executes a single task. 298 * 299 * @param {string} taskId Task identifier. 300 * @param {Array.<string>=} opt_urls Urls to execute on instead of |this.urls_|. 301 * @private 302 */ 303 FileTasks.prototype.execute_ = function(taskId, opt_urls) { 304 var urls = opt_urls || this.urls_; 305 FileTasks.recordViewingFileTypeUMA_(urls); 306 this.executeInternal_(taskId, urls); 307 }; 308 309 /** 310 * The core implementation to execute a single task. 311 * 312 * @param {string} taskId Task identifier. 313 * @param {Array.<string>} urls Urls to execute. 314 * @private 315 */ 316 FileTasks.prototype.executeInternal_ = function(taskId, urls) { 317 this.checkAvailability_(function() { 318 var taskParts = taskId.split('|'); 319 if (taskParts[0] == chrome.runtime.id && taskParts[1] == 'file') { 320 // For internal tasks we do not listen to the event to avoid 321 // handling the same task instance from multiple tabs. 322 // So, we manually execute the task. 323 this.executeInternalTask_(taskParts[2], urls); 324 } else { 325 chrome.fileBrowserPrivate.executeTask(taskId, urls); 326 } 327 }.bind(this)); 328 }; 329 330 /** 331 * Checks whether the remote files are available right now. 332 * 333 * @param {function} callback The callback. 334 * @private 335 */ 336 FileTasks.prototype.checkAvailability_ = function(callback) { 337 var areAll = function(props, name) { 338 var isOne = function(e) { 339 // If got no properties, we safely assume that item is unavailable. 340 return e && e[name]; 341 }; 342 return props.filter(isOne).length == props.length; 343 }; 344 345 var fm = this.fileManager_; 346 var urls = this.urls_; 347 348 if (fm.isOnDrive() && fm.isDriveOffline()) { 349 fm.metadataCache_.get(urls, 'drive', function(props) { 350 if (areAll(props, 'availableOffline')) { 351 callback(); 352 return; 353 } 354 355 fm.alert.showHtml( 356 loadTimeData.getString('OFFLINE_HEADER'), 357 props[0].hosted ? 358 loadTimeData.getStringF( 359 urls.length == 1 ? 360 'HOSTED_OFFLINE_MESSAGE' : 361 'HOSTED_OFFLINE_MESSAGE_PLURAL') : 362 loadTimeData.getStringF( 363 urls.length == 1 ? 364 'OFFLINE_MESSAGE' : 365 'OFFLINE_MESSAGE_PLURAL', 366 loadTimeData.getString('OFFLINE_COLUMN_LABEL'))); 367 }); 368 return; 369 } 370 371 if (fm.isOnDrive() && fm.isDriveOnMeteredConnection()) { 372 fm.metadataCache_.get(urls, 'drive', function(driveProps) { 373 if (areAll(driveProps, 'availableWhenMetered')) { 374 callback(); 375 return; 376 } 377 378 fm.metadataCache_.get(urls, 'filesystem', function(fileProps) { 379 var sizeToDownload = 0; 380 for (var i = 0; i != urls.length; i++) { 381 if (!driveProps[i].availableWhenMetered) 382 sizeToDownload += fileProps[i].size; 383 } 384 fm.confirm.show( 385 loadTimeData.getStringF( 386 urls.length == 1 ? 387 'CONFIRM_MOBILE_DATA_USE' : 388 'CONFIRM_MOBILE_DATA_USE_PLURAL', 389 util.bytesToString(sizeToDownload)), 390 callback); 391 }); 392 }); 393 return; 394 } 395 396 callback(); 397 }; 398 399 /** 400 * Executes an internal task. 401 * 402 * @param {string} id The short task id. 403 * @param {Array.<string>} urls The urls to execute on. 404 * @private 405 */ 406 FileTasks.prototype.executeInternalTask_ = function(id, urls) { 407 var fm = this.fileManager_; 408 409 if (id == 'play') { 410 var position = 0; 411 if (urls.length == 1) { 412 // If just a single audio file is selected pass along every audio file 413 // in the directory. 414 var selectedUrl = urls[0]; 415 urls = fm.getAllUrlsInCurrentDirectory().filter(FileType.isAudio); 416 position = urls.indexOf(selectedUrl); 417 } 418 chrome.runtime.getBackgroundPage(function(background) { 419 background.launchAudioPlayer({ items: urls, position: position }); 420 }); 421 return; 422 } 423 424 if (id == 'watch') { 425 console.assert(urls.length == 1, 'Cannot open multiple videos'); 426 chrome.runtime.getBackgroundPage(function(background) { 427 background.launchVideoPlayer(urls[0]); 428 }); 429 return; 430 } 431 432 if (id == 'mount-archive') { 433 this.mountArchivesInternal_(urls); 434 return; 435 } 436 437 if (id == 'format-device') { 438 fm.confirm.show(loadTimeData.getString('FORMATTING_WARNING'), function() { 439 chrome.fileBrowserPrivate.formatDevice(urls[0]); 440 }); 441 return; 442 } 443 444 if (id == 'gallery') { 445 this.openGalleryInternal_(urls); 446 return; 447 } 448 449 if (id == 'view-pdf' || id == 'view-swf' || id == 'view-in-browser' || 450 id == 'install-crx' || id.match(/^open-hosted-/) || id == 'watch') { 451 chrome.fileBrowserPrivate.viewFiles(urls, function(success) { 452 if (!success) 453 console.error('chrome.fileBrowserPrivate.viewFiles failed', urls); 454 }); 455 } 456 }; 457 458 /** 459 * Mounts archives. 460 * 461 * @param {Array.<string>} urls Mount file urls list. 462 */ 463 FileTasks.prototype.mountArchives = function(urls) { 464 FileTasks.recordViewingFileTypeUMA_(urls); 465 this.mountArchivesInternal_(urls); 466 }; 467 468 /** 469 * The core implementation of mounts archives. 470 * 471 * @param {Array.<string>} urls Mount file urls list. 472 * @private 473 */ 474 FileTasks.prototype.mountArchivesInternal_ = function(urls) { 475 var fm = this.fileManager_; 476 477 var tracker = fm.directoryModel_.createDirectoryChangeTracker(); 478 tracker.start(); 479 480 fm.resolveSelectResults_(urls, function(urls) { 481 for (var index = 0; index < urls.length; ++index) { 482 fm.volumeManager_.mountArchive(urls[index], function(mountPath) { 483 tracker.stop(); 484 if (!tracker.hasChanged) 485 fm.directoryModel_.changeDirectory(mountPath); 486 }, function(url, error) { 487 var path = util.extractFilePath(url); 488 tracker.stop(); 489 var namePos = path.lastIndexOf('/'); 490 fm.alert.show(strf('ARCHIVE_MOUNT_FAILED', 491 path.substr(namePos + 1), error)); 492 }.bind(null, urls[index])); 493 } 494 }); 495 }; 496 497 /** 498 * Open the Gallery. 499 * 500 * @param {Array.<string>} urls List of selected urls. 501 */ 502 FileTasks.prototype.openGallery = function(urls) { 503 FileTasks.recordViewingFileTypeUMA_(urls); 504 this.openGalleryInternal_(urls); 505 }; 506 507 /** 508 * The core implementation to open the Gallery. 509 * 510 * @param {Array.<string>} urls List of selected urls. 511 * @private 512 */ 513 FileTasks.prototype.openGalleryInternal_ = function(urls) { 514 var fm = this.fileManager_; 515 516 var allUrls = 517 fm.getAllUrlsInCurrentDirectory().filter(FileType.isImageOrVideo); 518 519 var galleryFrame = fm.document_.createElement('iframe'); 520 galleryFrame.className = 'overlay-pane'; 521 galleryFrame.scrolling = 'no'; 522 galleryFrame.setAttribute('webkitallowfullscreen', true); 523 524 if (this.params_ && this.params_.gallery) { 525 // Remove the Gallery state from the location, we do not need it any more. 526 util.updateAppState(null /* keep path */, '' /* remove search. */); 527 } 528 529 var savedAppState = window.appState; 530 var savedTitle = document.title; 531 532 // Push a temporary state which will be replaced every time the selection 533 // changes in the Gallery and popped when the Gallery is closed. 534 util.updateAppState(); 535 536 var onBack = function(selectedUrls) { 537 fm.directoryModel_.selectUrls(selectedUrls); 538 fm.closeFilePopup_(); // Will call Gallery.unload. 539 window.appState = savedAppState; 540 util.saveAppState(); 541 document.title = savedTitle; 542 }; 543 544 var onClose = function() { 545 fm.onClose(); 546 }; 547 548 var onMaximize = function() { 549 fm.onMaximize(); 550 }; 551 552 galleryFrame.onload = function() { 553 galleryFrame.contentWindow.ImageUtil.metrics = metrics; 554 window.galleryTestAPI = galleryFrame.contentWindow.galleryTestAPI; 555 556 // TODO(haruki): isOnReadonlyDirectory() only checks the permission for the 557 // root. We should check more granular permission to know whether the file 558 // is writable or not. 559 var readonly = fm.isOnReadonlyDirectory(); 560 var currentDir = fm.directoryModel_.getCurrentDirEntry(); 561 var downloadsDir = fm.directoryModel_.getRootsList().item(0); 562 var readonlyDirName = null; 563 if (readonly) { 564 readonlyDirName = fm.isOnDrive() ? 565 PathUtil.getRootLabel(PathUtil.getRootPath(currentDir.fullPath)) : 566 fm.directoryModel_.getCurrentRootName(); 567 } 568 569 var context = { 570 // We show the root label in readonly warning (e.g. archive name). 571 readonlyDirName: readonlyDirName, 572 curDirEntry: currentDir, 573 saveDirEntry: readonly ? downloadsDir : null, 574 searchResults: fm.directoryModel_.isSearching(), 575 metadataCache: fm.metadataCache_, 576 pageState: this.params_, 577 appWindow: chrome.app.window.current(), 578 onBack: onBack, 579 onClose: onClose, 580 onMaximize: onMaximize, 581 displayStringFunction: strf 582 }; 583 galleryFrame.contentWindow.Gallery.open(context, allUrls, urls); 584 }.bind(this); 585 586 galleryFrame.src = 'gallery.html'; 587 fm.openFilePopup_(galleryFrame, fm.updateTitle_.bind(fm)); 588 }; 589 590 /** 591 * Displays the list of tasks in a task picker combobutton. 592 * 593 * @param {cr.ui.ComboButton} combobutton The task picker element. 594 * @private 595 */ 596 FileTasks.prototype.display_ = function(combobutton) { 597 if (this.tasks_.length == 0) { 598 combobutton.hidden = true; 599 return; 600 } 601 602 combobutton.clear(); 603 combobutton.hidden = false; 604 combobutton.defaultItem = this.createCombobuttonItem_(this.defaultTask_); 605 606 var items = this.createItems_(); 607 608 if (items.length > 1) { 609 var defaultIdx = 0; 610 611 for (var j = 0; j < items.length; j++) { 612 combobutton.addDropDownItem(items[j]); 613 if (items[j].task.taskId == this.defaultTask_.taskId) 614 defaultIdx = j; 615 } 616 617 combobutton.addSeparator(); 618 var changeDefaultMenuItem = combobutton.addDropDownItem({ 619 label: loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM') 620 }); 621 changeDefaultMenuItem.classList.add('change-default'); 622 } 623 }; 624 625 /** 626 * Creates sorted array of available task descriptions such as title and icon. 627 * 628 * @return {Array} created array can be used to feed combobox, menus and so on. 629 * @private 630 */ 631 FileTasks.prototype.createItems_ = function() { 632 var items = []; 633 var title = this.defaultTask_.title + ' ' + 634 loadTimeData.getString('DEFAULT_ACTION_LABEL'); 635 items.push(this.createCombobuttonItem_(this.defaultTask_, title, true)); 636 637 for (var index = 0; index < this.tasks_.length; index++) { 638 var task = this.tasks_[index]; 639 if (task != this.defaultTask_) 640 items.push(this.createCombobuttonItem_(task)); 641 } 642 643 items.sort(function(a, b) { 644 return a.label.localeCompare(b.label); 645 }); 646 647 return items; 648 }; 649 650 /** 651 * Updates context menu with default item. 652 * @private 653 */ 654 655 FileTasks.prototype.updateMenuItem_ = function() { 656 this.fileManager_.updateContextMenuActionItems(this.defaultTask_, 657 this.tasks_.length > 1); 658 }; 659 660 /** 661 * Creates combobutton item based on task. 662 * 663 * @param {Object} task Task to convert. 664 * @param {string=} opt_title Title. 665 * @param {boolean=} opt_bold Make a menu item bold. 666 * @return {Object} Item appendable to combobutton drop-down list. 667 * @private 668 */ 669 FileTasks.prototype.createCombobuttonItem_ = function(task, opt_title, 670 opt_bold) { 671 return { 672 label: opt_title || task.title, 673 iconUrl: task.iconUrl, 674 iconType: task.iconType, 675 task: task, 676 bold: opt_bold || false 677 }; 678 }; 679 680 681 /** 682 * Decorates a FileTasks method, so it will be actually executed after the tasks 683 * are available. 684 * This decorator expects an implementation called |method + '_'|. 685 * 686 * @param {string} method The method name. 687 */ 688 FileTasks.decorate = function(method) { 689 var privateMethod = method + '_'; 690 FileTasks.prototype[method] = function() { 691 if (this.tasks_) { 692 this[privateMethod].apply(this, arguments); 693 } else { 694 this.pendingInvocations_.push([privateMethod, arguments]); 695 } 696 return this; 697 }; 698 }; 699 700 /** 701 * Shows modal action picker dialog with currently available list of tasks. 702 * 703 * @param {DefaultActionDialog} actionDialog Action dialog to show and update. 704 * @param {string} title Title to use. 705 * @param {string} message Message to use. 706 * @param {function(Object)} onSuccess Callback to pass selected task. 707 */ 708 FileTasks.prototype.showTaskPicker = function(actionDialog, title, message, 709 onSuccess) { 710 var items = this.createItems_(); 711 712 var defaultIdx = 0; 713 for (var j = 0; j < items.length; j++) { 714 if (items[j].task.taskId == this.defaultTask_.taskId) 715 defaultIdx = j; 716 } 717 718 actionDialog.show( 719 title, 720 message, 721 items, defaultIdx, 722 function(item) { 723 onSuccess(item.task); 724 }); 725 }; 726 727 FileTasks.decorate('display'); 728 FileTasks.decorate('updateMenuItem'); 729 FileTasks.decorate('execute'); 730 FileTasks.decorate('executeDefault'); 731