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 * Namespace for utility functions. 9 */ 10var util = {}; 11 12/** 13 * Returns a function that console.log's its arguments, prefixed by |msg|. 14 * 15 * @param {string} msg The message prefix to use in the log. 16 * @param {function(...string)=} opt_callback A function to invoke after 17 * logging. 18 * @return {function(...string)} Function that logs. 19 */ 20util.flog = function(msg, opt_callback) { 21 return function() { 22 var ary = Array.apply(null, arguments); 23 console.log(msg + ': ' + ary.join(', ')); 24 if (opt_callback) 25 opt_callback.apply(null, arguments); 26 }; 27}; 28 29/** 30 * Returns a function that throws an exception that includes its arguments 31 * prefixed by |msg|. 32 * 33 * @param {string} msg The message prefix to use in the exception. 34 * @return {function(...string)} Function that throws. 35 */ 36util.ferr = function(msg) { 37 return function() { 38 var ary = Array.apply(null, arguments); 39 throw new Error(msg + ': ' + ary.join(', ')); 40 }; 41}; 42 43/** 44 * @param {string} name File error name. 45 * @return {string} Translated file error string. 46 */ 47util.getFileErrorString = function(name) { 48 var candidateMessageFragment; 49 switch (name) { 50 case 'NotFoundError': 51 candidateMessageFragment = 'NOT_FOUND'; 52 break; 53 case 'SecurityError': 54 candidateMessageFragment = 'SECURITY'; 55 break; 56 case 'NotReadableError': 57 candidateMessageFragment = 'NOT_READABLE'; 58 break; 59 case 'NoModificationAllowedError': 60 candidateMessageFragment = 'NO_MODIFICATION_ALLOWED'; 61 break; 62 case 'InvalidStateError': 63 candidateMessageFragment = 'INVALID_STATE'; 64 break; 65 case 'InvalidModificationError': 66 candidateMessageFragment = 'INVALID_MODIFICATION'; 67 break; 68 case 'PathExistsError': 69 candidateMessageFragment = 'PATH_EXISTS'; 70 break; 71 case 'QuotaExceededError': 72 candidateMessageFragment = 'QUOTA_EXCEEDED'; 73 break; 74 } 75 76 return loadTimeData.getString('FILE_ERROR_' + candidateMessageFragment) || 77 loadTimeData.getString('FILE_ERROR_GENERIC'); 78}; 79 80/** 81 * Mapping table for FileError.code style enum to DOMError.name string. 82 * 83 * @enum {string} 84 * @const 85 */ 86util.FileError = Object.freeze({ 87 ABORT_ERR: 'AbortError', 88 INVALID_MODIFICATION_ERR: 'InvalidModificationError', 89 INVALID_STATE_ERR: 'InvalidStateError', 90 NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError', 91 NOT_FOUND_ERR: 'NotFoundError', 92 NOT_READABLE_ERR: 'NotReadable', 93 PATH_EXISTS_ERR: 'PathExistsError', 94 QUOTA_EXCEEDED_ERR: 'QuotaExceededError', 95 TYPE_MISMATCH_ERR: 'TypeMismatchError', 96 ENCODING_ERR: 'EncodingError', 97}); 98 99/** 100 * @param {string} str String to escape. 101 * @return {string} Escaped string. 102 */ 103util.htmlEscape = function(str) { 104 return str.replace(/[<>&]/g, function(entity) { 105 switch (entity) { 106 case '<': return '<'; 107 case '>': return '>'; 108 case '&': return '&'; 109 } 110 }); 111}; 112 113/** 114 * @param {string} str String to unescape. 115 * @return {string} Unescaped string. 116 */ 117util.htmlUnescape = function(str) { 118 return str.replace(/&(lt|gt|amp);/g, function(entity) { 119 switch (entity) { 120 case '<': return '<'; 121 case '>': return '>'; 122 case '&': return '&'; 123 } 124 }); 125}; 126 127/** 128 * Iterates the entries contained by dirEntry, and invokes callback once for 129 * each entry. On completion, successCallback will be invoked. 130 * 131 * @param {DirectoryEntry} dirEntry The entry of the directory. 132 * @param {function(Entry, function())} callback Invoked for each entry. 133 * @param {function()} successCallback Invoked on completion. 134 * @param {function(FileError)} errorCallback Invoked if an error is found on 135 * directory entry reading. 136 */ 137util.forEachDirEntry = function( 138 dirEntry, callback, successCallback, errorCallback) { 139 var reader = dirEntry.createReader(); 140 var iterate = function() { 141 reader.readEntries(function(entries) { 142 if (entries.length == 0) { 143 successCallback(); 144 return; 145 } 146 147 AsyncUtil.forEach( 148 entries, 149 function(forEachCallback, entry) { 150 // Do not pass index nor entries. 151 callback(entry, forEachCallback); 152 }, 153 iterate); 154 }, errorCallback); 155 }; 156 iterate(); 157}; 158 159/** 160 * Reads contents of directory. 161 * @param {DirectoryEntry} root Root entry. 162 * @param {string} path Directory path. 163 * @param {function(Array.<Entry>)} callback List of entries passed to callback. 164 */ 165util.readDirectory = function(root, path, callback) { 166 var onError = function(e) { 167 callback([], e); 168 }; 169 root.getDirectory(path, {create: false}, function(entry) { 170 var reader = entry.createReader(); 171 var r = []; 172 var readNext = function() { 173 reader.readEntries(function(results) { 174 if (results.length == 0) { 175 callback(r, null); 176 return; 177 } 178 r.push.apply(r, results); 179 readNext(); 180 }, onError); 181 }; 182 readNext(); 183 }, onError); 184}; 185 186/** 187 * Utility function to resolve multiple directories with a single call. 188 * 189 * The successCallback will be invoked once for each directory object 190 * found. The errorCallback will be invoked once for each 191 * path that could not be resolved. 192 * 193 * The successCallback is invoked with a null entry when all paths have 194 * been processed. 195 * 196 * @param {DirEntry} dirEntry The base directory. 197 * @param {Object} params The parameters to pass to the underlying 198 * getDirectory calls. 199 * @param {Array.<string>} paths The list of directories to resolve. 200 * @param {function(!DirEntry)} successCallback The function to invoke for 201 * each DirEntry found. Also invoked once with null at the end of the 202 * process. 203 * @param {function(FileError)} errorCallback The function to invoke 204 * for each path that cannot be resolved. 205 */ 206util.getDirectories = function(dirEntry, params, paths, successCallback, 207 errorCallback) { 208 209 // Copy the params array, since we're going to destroy it. 210 params = [].slice.call(params); 211 212 var onComplete = function() { 213 successCallback(null); 214 }; 215 216 var getNextDirectory = function() { 217 var path = paths.shift(); 218 if (!path) 219 return onComplete(); 220 221 dirEntry.getDirectory( 222 path, params, 223 function(entry) { 224 successCallback(entry); 225 getNextDirectory(); 226 }, 227 function(err) { 228 errorCallback(err); 229 getNextDirectory(); 230 }); 231 }; 232 233 getNextDirectory(); 234}; 235 236/** 237 * Utility function to resolve multiple files with a single call. 238 * 239 * The successCallback will be invoked once for each directory object 240 * found. The errorCallback will be invoked once for each 241 * path that could not be resolved. 242 * 243 * The successCallback is invoked with a null entry when all paths have 244 * been processed. 245 * 246 * @param {DirEntry} dirEntry The base directory. 247 * @param {Object} params The parameters to pass to the underlying 248 * getFile calls. 249 * @param {Array.<string>} paths The list of files to resolve. 250 * @param {function(!FileEntry)} successCallback The function to invoke for 251 * each FileEntry found. Also invoked once with null at the end of the 252 * process. 253 * @param {function(FileError)} errorCallback The function to invoke 254 * for each path that cannot be resolved. 255 */ 256util.getFiles = function(dirEntry, params, paths, successCallback, 257 errorCallback) { 258 // Copy the params array, since we're going to destroy it. 259 params = [].slice.call(params); 260 261 var onComplete = function() { 262 successCallback(null); 263 }; 264 265 var getNextFile = function() { 266 var path = paths.shift(); 267 if (!path) 268 return onComplete(); 269 270 dirEntry.getFile( 271 path, params, 272 function(entry) { 273 successCallback(entry); 274 getNextFile(); 275 }, 276 function(err) { 277 errorCallback(err); 278 getNextFile(); 279 }); 280 }; 281 282 getNextFile(); 283}; 284 285/** 286 * Renames the entry to newName. 287 * @param {Entry} entry The entry to be renamed. 288 * @param {string} newName The new name. 289 * @param {function(Entry)} successCallback Callback invoked when the rename 290 * is successfully done. 291 * @param {function(FileError)} errorCallback Callback invoked when an error 292 * is found. 293 */ 294util.rename = function(entry, newName, successCallback, errorCallback) { 295 entry.getParent(function(parent) { 296 // Before moving, we need to check if there is an existing entry at 297 // parent/newName, since moveTo will overwrite it. 298 // Note that this way has some timing issue. After existing check, 299 // a new entry may be create on background. However, there is no way not to 300 // overwrite the existing file, unfortunately. The risk should be low, 301 // assuming the unsafe period is very short. 302 (entry.isFile ? parent.getFile : parent.getDirectory).call( 303 parent, newName, {create: false}, 304 function(entry) { 305 // The entry with the name already exists. 306 errorCallback(util.createDOMError(util.FileError.PATH_EXISTS_ERR)); 307 }, 308 function(error) { 309 if (error.name != util.FileError.NOT_FOUND_ERR) { 310 // Unexpected error is found. 311 errorCallback(error); 312 return; 313 } 314 315 // No existing entry is found. 316 entry.moveTo(parent, newName, successCallback, errorCallback); 317 }); 318 }, errorCallback); 319}; 320 321/** 322 * Remove a file or a directory. 323 * @param {Entry} entry The entry to remove. 324 * @param {function()} onSuccess The success callback. 325 * @param {function(FileError)} onError The error callback. 326 */ 327util.removeFileOrDirectory = function(entry, onSuccess, onError) { 328 if (entry.isDirectory) 329 entry.removeRecursively(onSuccess, onError); 330 else 331 entry.remove(onSuccess, onError); 332}; 333 334/** 335 * Convert a number of bytes into a human friendly format, using the correct 336 * number separators. 337 * 338 * @param {number} bytes The number of bytes. 339 * @return {string} Localized string. 340 */ 341util.bytesToString = function(bytes) { 342 // Translation identifiers for size units. 343 var UNITS = ['SIZE_BYTES', 344 'SIZE_KB', 345 'SIZE_MB', 346 'SIZE_GB', 347 'SIZE_TB', 348 'SIZE_PB']; 349 350 // Minimum values for the units above. 351 var STEPS = [0, 352 Math.pow(2, 10), 353 Math.pow(2, 20), 354 Math.pow(2, 30), 355 Math.pow(2, 40), 356 Math.pow(2, 50)]; 357 358 var str = function(n, u) { 359 return strf(u, n.toLocaleString()); 360 }; 361 362 var fmt = function(s, u) { 363 var rounded = Math.round(bytes / s * 10) / 10; 364 return str(rounded, u); 365 }; 366 367 // Less than 1KB is displayed like '80 bytes'. 368 if (bytes < STEPS[1]) { 369 return str(bytes, UNITS[0]); 370 } 371 372 // Up to 1MB is displayed as rounded up number of KBs. 373 if (bytes < STEPS[2]) { 374 var rounded = Math.ceil(bytes / STEPS[1]); 375 return str(rounded, UNITS[1]); 376 } 377 378 // This loop index is used outside the loop if it turns out |bytes| 379 // requires the largest unit. 380 var i; 381 382 for (i = 2 /* MB */; i < UNITS.length - 1; i++) { 383 if (bytes < STEPS[i + 1]) 384 return fmt(STEPS[i], UNITS[i]); 385 } 386 387 return fmt(STEPS[i], UNITS[i]); 388}; 389 390/** 391 * Utility function to read specified range of bytes from file 392 * @param {File} file The file to read. 393 * @param {number} begin Starting byte(included). 394 * @param {number} end Last byte(excluded). 395 * @param {function(File, Uint8Array)} callback Callback to invoke. 396 * @param {function(FileError)} onError Error handler. 397 */ 398util.readFileBytes = function(file, begin, end, callback, onError) { 399 var fileReader = new FileReader(); 400 fileReader.onerror = onError; 401 fileReader.onloadend = function() { 402 callback(file, new ByteReader(fileReader.result)); 403 }; 404 fileReader.readAsArrayBuffer(file.slice(begin, end)); 405}; 406 407/** 408 * Write a blob to a file. 409 * Truncates the file first, so the previous content is fully overwritten. 410 * @param {FileEntry} entry File entry. 411 * @param {Blob} blob The blob to write. 412 * @param {function(Event)} onSuccess Completion callback. The first argument is 413 * a 'writeend' event. 414 * @param {function(FileError)} onError Error handler. 415 */ 416util.writeBlobToFile = function(entry, blob, onSuccess, onError) { 417 var truncate = function(writer) { 418 writer.onerror = onError; 419 writer.onwriteend = write.bind(null, writer); 420 writer.truncate(0); 421 }; 422 423 var write = function(writer) { 424 writer.onwriteend = onSuccess; 425 writer.write(blob); 426 }; 427 428 entry.createWriter(truncate, onError); 429}; 430 431/** 432 * Returns a string '[Ctrl-][Alt-][Shift-][Meta-]' depending on the event 433 * modifiers. Convenient for writing out conditions in keyboard handlers. 434 * 435 * @param {Event} event The keyboard event. 436 * @return {string} Modifiers. 437 */ 438util.getKeyModifiers = function(event) { 439 return (event.ctrlKey ? 'Ctrl-' : '') + 440 (event.altKey ? 'Alt-' : '') + 441 (event.shiftKey ? 'Shift-' : '') + 442 (event.metaKey ? 'Meta-' : ''); 443}; 444 445/** 446 * @param {HTMLElement} element Element to transform. 447 * @param {Object} transform Transform object, 448 * contains scaleX, scaleY and rotate90 properties. 449 */ 450util.applyTransform = function(element, transform) { 451 element.style.webkitTransform = 452 transform ? 'scaleX(' + transform.scaleX + ') ' + 453 'scaleY(' + transform.scaleY + ') ' + 454 'rotate(' + transform.rotate90 * 90 + 'deg)' : 455 ''; 456}; 457 458/** 459 * Extracts path from filesystem: URL. 460 * @param {string} url Filesystem URL. 461 * @return {string} The path. 462 */ 463util.extractFilePath = function(url) { 464 var match = 465 /^filesystem:[\w-]*:\/\/[\w]*\/(external|persistent|temporary)(\/.*)$/. 466 exec(url); 467 var path = match && match[2]; 468 if (!path) return null; 469 return decodeURIComponent(path); 470}; 471 472/** 473 * Traverses a directory tree whose root is the given entry, and invokes 474 * callback for each entry. Upon completion, successCallback will be called. 475 * On error, errorCallback will be called. 476 * 477 * @param {Entry} entry The root entry. 478 * @param {function(Entry):boolean} callback Callback invoked for each entry. 479 * If this returns false, entries under it won't be traversed. Note that 480 * its siblings (and their children) will be still traversed. 481 * @param {function()} successCallback Called upon successful completion. 482 * @param {function(error)} errorCallback Called upon error. 483 */ 484util.traverseTree = function(entry, callback, successCallback, errorCallback) { 485 if (!callback(entry)) { 486 successCallback(); 487 return; 488 } 489 490 util.forEachDirEntry( 491 entry, 492 function(child, iterationCallback) { 493 util.traverseTree(child, callback, iterationCallback, errorCallback); 494 }, 495 successCallback, 496 errorCallback); 497}; 498 499/** 500 * A shortcut function to create a child element with given tag and class. 501 * 502 * @param {HTMLElement} parent Parent element. 503 * @param {string=} opt_className Class name. 504 * @param {string=} opt_tag Element tag, DIV is omitted. 505 * @return {Element} Newly created element. 506 */ 507util.createChild = function(parent, opt_className, opt_tag) { 508 var child = parent.ownerDocument.createElement(opt_tag || 'div'); 509 if (opt_className) 510 child.className = opt_className; 511 parent.appendChild(child); 512 return child; 513}; 514 515/** 516 * Updates the app state. 517 * 518 * @param {string} currentDirectoryURL Currently opened directory as an URL. 519 * If null the value is left unchanged. 520 * @param {string} selectionURL Currently selected entry as an URL. If null the 521 * value is left unchanged. 522 * @param {string|Object=} opt_param Additional parameters, to be stored. If 523 * null, then left unchanged. 524 */ 525util.updateAppState = function(currentDirectoryURL, selectionURL, opt_param) { 526 window.appState = window.appState || {}; 527 if (opt_param !== undefined && opt_param !== null) 528 window.appState.params = opt_param; 529 if (currentDirectoryURL !== null) 530 window.appState.currentDirectoryURL = currentDirectoryURL; 531 if (selectionURL !== null) 532 window.appState.selectionURL = selectionURL; 533 util.saveAppState(); 534}; 535 536/** 537 * Returns a translated string. 538 * 539 * Wrapper function to make dealing with translated strings more concise. 540 * Equivalent to loadTimeData.getString(id). 541 * 542 * @param {string} id The id of the string to return. 543 * @return {string} The translated string. 544 */ 545function str(id) { 546 return loadTimeData.getString(id); 547} 548 549/** 550 * Returns a translated string with arguments replaced. 551 * 552 * Wrapper function to make dealing with translated strings more concise. 553 * Equivalent to loadTimeData.getStringF(id, ...). 554 * 555 * @param {string} id The id of the string to return. 556 * @param {...string} var_args The values to replace into the string. 557 * @return {string} The translated string with replaced values. 558 */ 559function strf(id, var_args) { 560 return loadTimeData.getStringF.apply(loadTimeData, arguments); 561} 562 563/** 564 * @return {boolean} True if Files.app is running as an open files or a select 565 * folder dialog. False otherwise. 566 */ 567util.runningInBrowser = function() { 568 return !window.appID; 569}; 570 571/** 572 * Attach page load handler. 573 * @param {function()} handler Application-specific load handler. 574 */ 575util.addPageLoadHandler = function(handler) { 576 document.addEventListener('DOMContentLoaded', function() { 577 handler(); 578 }); 579}; 580 581/** 582 * Save app launch data to the local storage. 583 */ 584util.saveAppState = function() { 585 if (!window.appState) 586 return; 587 var items = {}; 588 589 items[window.appID] = JSON.stringify(window.appState); 590 chrome.storage.local.set(items); 591}; 592 593/** 594 * AppCache is a persistent timestamped key-value storage backed by 595 * HTML5 local storage. 596 * 597 * It is not designed for frequent access. In order to avoid costly 598 * localStorage iteration all data is kept in a single localStorage item. 599 * There is no in-memory caching, so concurrent access is _almost_ safe. 600 * 601 * TODO(kaznacheev) Reimplement this based on Indexed DB. 602 */ 603util.AppCache = function() {}; 604 605/** 606 * Local storage key. 607 */ 608util.AppCache.KEY = 'AppCache'; 609 610/** 611 * Max number of items. 612 */ 613util.AppCache.CAPACITY = 100; 614 615/** 616 * Default lifetime. 617 */ 618util.AppCache.LIFETIME = 30 * 24 * 60 * 60 * 1000; // 30 days. 619 620/** 621 * @param {string} key Key. 622 * @param {function(number)} callback Callback accepting a value. 623 */ 624util.AppCache.getValue = function(key, callback) { 625 util.AppCache.read_(function(map) { 626 var entry = map[key]; 627 callback(entry && entry.value); 628 }); 629}; 630 631/** 632 * Update the cache. 633 * 634 * @param {string} key Key. 635 * @param {string} value Value. Remove the key if value is null. 636 * @param {number=} opt_lifetime Maximum time to keep an item (in milliseconds). 637 */ 638util.AppCache.update = function(key, value, opt_lifetime) { 639 util.AppCache.read_(function(map) { 640 if (value != null) { 641 map[key] = { 642 value: value, 643 expire: Date.now() + (opt_lifetime || util.AppCache.LIFETIME) 644 }; 645 } else if (key in map) { 646 delete map[key]; 647 } else { 648 return; // Nothing to do. 649 } 650 util.AppCache.cleanup_(map); 651 util.AppCache.write_(map); 652 }); 653}; 654 655/** 656 * @param {function(Object)} callback Callback accepting a map of timestamped 657 * key-value pairs. 658 * @private 659 */ 660util.AppCache.read_ = function(callback) { 661 chrome.storage.local.get(util.AppCache.KEY, function(values) { 662 var json = values[util.AppCache.KEY]; 663 if (json) { 664 try { 665 callback(JSON.parse(json)); 666 } catch (e) { 667 // The local storage item somehow got messed up, start fresh. 668 } 669 } 670 callback({}); 671 }); 672}; 673 674/** 675 * @param {Object} map A map of timestamped key-value pairs. 676 * @private 677 */ 678util.AppCache.write_ = function(map) { 679 var items = {}; 680 items[util.AppCache.KEY] = JSON.stringify(map); 681 chrome.storage.local.set(items); 682}; 683 684/** 685 * Remove over-capacity and obsolete items. 686 * 687 * @param {Object} map A map of timestamped key-value pairs. 688 * @private 689 */ 690util.AppCache.cleanup_ = function(map) { 691 // Sort keys by ascending timestamps. 692 var keys = []; 693 for (var key in map) { 694 if (map.hasOwnProperty(key)) 695 keys.push(key); 696 } 697 keys.sort(function(a, b) { return map[a].expire > map[b].expire; }); 698 699 var cutoff = Date.now(); 700 701 var obsolete = 0; 702 while (obsolete < keys.length && 703 map[keys[obsolete]].expire < cutoff) { 704 obsolete++; 705 } 706 707 var overCapacity = Math.max(0, keys.length - util.AppCache.CAPACITY); 708 709 var itemsToDelete = Math.max(obsolete, overCapacity); 710 for (var i = 0; i != itemsToDelete; i++) { 711 delete map[keys[i]]; 712 } 713}; 714 715/** 716 * Load an image. 717 * 718 * @param {Image} image Image element. 719 * @param {string} url Source url. 720 * @param {Object=} opt_options Hash array of options, eg. width, height, 721 * maxWidth, maxHeight, scale, cache. 722 * @param {function()=} opt_isValid Function returning false iff the task 723 * is not valid and should be aborted. 724 * @return {?number} Task identifier or null if fetched immediately from 725 * cache. 726 */ 727util.loadImage = function(image, url, opt_options, opt_isValid) { 728 return ImageLoaderClient.loadToImage(url, 729 image, 730 opt_options || {}, 731 function() {}, 732 function() { image.onerror(); }, 733 opt_isValid); 734}; 735 736/** 737 * Cancels loading an image. 738 * @param {number} taskId Task identifier returned by util.loadImage(). 739 */ 740util.cancelLoadImage = function(taskId) { 741 ImageLoaderClient.getInstance().cancel(taskId); 742}; 743 744/** 745 * Finds proerty descriptor in the object prototype chain. 746 * @param {Object} object The object. 747 * @param {string} propertyName The property name. 748 * @return {Object} Property descriptor. 749 */ 750util.findPropertyDescriptor = function(object, propertyName) { 751 for (var p = object; p; p = Object.getPrototypeOf(p)) { 752 var d = Object.getOwnPropertyDescriptor(p, propertyName); 753 if (d) 754 return d; 755 } 756 return null; 757}; 758 759/** 760 * Calls inherited property setter (useful when property is 761 * overridden). 762 * @param {Object} object The object. 763 * @param {string} propertyName The property name. 764 * @param {*} value Value to set. 765 */ 766util.callInheritedSetter = function(object, propertyName, value) { 767 var d = util.findPropertyDescriptor(Object.getPrototypeOf(object), 768 propertyName); 769 d.set.call(object, value); 770}; 771 772/** 773 * Returns true if the board of the device matches the given prefix. 774 * @param {string} boardPrefix The board prefix to match against. 775 * (ex. "x86-mario". Prefix is used as the actual board name comes with 776 * suffix like "x86-mario-something". 777 * @return {boolean} True if the board of the device matches the given prefix. 778 */ 779util.boardIs = function(boardPrefix) { 780 // The board name should be lower-cased, but making it case-insensitive for 781 // backward compatibility just in case. 782 var board = str('CHROMEOS_RELEASE_BOARD'); 783 var pattern = new RegExp('^' + boardPrefix, 'i'); 784 return board.match(pattern) != null; 785}; 786 787/** 788 * Adds an isFocused method to the current window object. 789 */ 790util.addIsFocusedMethod = function() { 791 var focused = true; 792 793 window.addEventListener('focus', function() { 794 focused = true; 795 }); 796 797 window.addEventListener('blur', function() { 798 focused = false; 799 }); 800 801 /** 802 * @return {boolean} True if focused. 803 */ 804 window.isFocused = function() { 805 return focused; 806 }; 807}; 808 809/** 810 * Makes a redirect to the specified Files.app's window from another window. 811 * @param {number} id Window id. 812 * @param {string} url Target url. 813 * @return {boolean} True if the window has been found. False otherwise. 814 */ 815util.redirectMainWindow = function(id, url) { 816 // TODO(mtomasz): Implement this for Apps V2, once the photo importer is 817 // restored. 818 return false; 819}; 820 821/** 822 * Checks, if the Files.app's window is in a full screen mode. 823 * 824 * @param {AppWindow} appWindow App window to be maximized. 825 * @return {boolean} True if the full screen mode is enabled. 826 */ 827util.isFullScreen = function(appWindow) { 828 if (appWindow) { 829 return appWindow.isFullscreen(); 830 } else { 831 console.error('App window not passed. Unable to check status of ' + 832 'the full screen mode.'); 833 return false; 834 } 835}; 836 837/** 838 * Toggles the full screen mode. 839 * 840 * @param {AppWindow} appWindow App window to be maximized. 841 * @param {boolean} enabled True for enabling, false for disabling. 842 */ 843util.toggleFullScreen = function(appWindow, enabled) { 844 if (appWindow) { 845 if (enabled) 846 appWindow.fullscreen(); 847 else 848 appWindow.restore(); 849 return; 850 } 851 852 console.error( 853 'App window not passed. Unable to toggle the full screen mode.'); 854}; 855 856/** 857 * The type of a file operation. 858 * @enum {string} 859 * @const 860 */ 861util.FileOperationType = Object.freeze({ 862 COPY: 'COPY', 863 MOVE: 'MOVE', 864 ZIP: 'ZIP', 865}); 866 867/** 868 * The type of a file operation error. 869 * @enum {number} 870 * @const 871 */ 872util.FileOperationErrorType = Object.freeze({ 873 UNEXPECTED_SOURCE_FILE: 0, 874 TARGET_EXISTS: 1, 875 FILESYSTEM_ERROR: 2, 876}); 877 878/** 879 * The kind of an entry changed event. 880 * @enum {number} 881 * @const 882 */ 883util.EntryChangedKind = Object.freeze({ 884 CREATED: 0, 885 DELETED: 1, 886}); 887 888/** 889 * Obtains whether an entry is fake or not. 890 * @param {!Entry|!Object} entry Entry or a fake entry. 891 * @return {boolean} True if the given entry is fake. 892 */ 893util.isFakeEntry = function(entry) { 894 return !('getParent' in entry); 895}; 896 897/** 898 * Creates an instance of UserDOMError with given error name that looks like a 899 * FileError except that it does not have the deprecated FileError.code member. 900 * 901 * TODO(uekawa): remove reference to FileError. 902 * 903 * @param {string} name Error name for the file error. 904 * @return {UserDOMError} FileError instance 905 */ 906util.createDOMError = function(name) { 907 return new util.UserDOMError(name); 908}; 909 910/** 911 * Creates a DOMError-like object to be used in place of returning file errors. 912 * 913 * @param {string} name Error name for the file error. 914 * @constructor 915 */ 916util.UserDOMError = function(name) { 917 /** 918 * @type {string} 919 * @private 920 */ 921 this.name_ = name; 922 Object.freeze(this); 923}; 924 925util.UserDOMError.prototype = { 926 /** 927 * @return {string} File error name. 928 */ 929 get name() { return this.name_; 930 } 931}; 932 933/** 934 * Compares two entries. 935 * @param {Entry|Object} entry1 The entry to be compared. Can be a fake. 936 * @param {Entry|Object} entry2 The entry to be compared. Can be a fake. 937 * @return {boolean} True if the both entry represents a same file or 938 * directory. Returns true if both entries are null. 939 */ 940util.isSameEntry = function(entry1, entry2) { 941 if (!entry1 && !entry2) 942 return true; 943 if (!entry1 || !entry2) 944 return false; 945 return entry1.toURL() === entry2.toURL(); 946}; 947 948/** 949 * Compares two file systems. 950 * @param {DOMFileSystem} fileSystem1 The file system to be compared. 951 * @param {DOMFileSystem} fileSystem2 The file system to be compared. 952 * @return {boolean} True if the both file systems are equal. Also, returns true 953 * if both file systems are null. 954 */ 955util.isSameFileSystem = function(fileSystem1, fileSystem2) { 956 if (!fileSystem1 && !fileSystem2) 957 return true; 958 if (!fileSystem1 || !fileSystem2) 959 return false; 960 return util.isSameEntry(fileSystem1.root, fileSystem2.root); 961}; 962 963/** 964 * Collator for sorting. 965 * @type {Intl.Collator} 966 */ 967util.collator = new Intl.Collator( 968 [], {usage: 'sort', numeric: true, sensitivity: 'base'}); 969 970/** 971 * Compare by name. The 2 entries must be in same directory. 972 * @param {Entry} entry1 First entry. 973 * @param {Entry} entry2 Second entry. 974 * @return {number} Compare result. 975 */ 976util.compareName = function(entry1, entry2) { 977 return util.collator.compare(entry1.name, entry2.name); 978}; 979 980/** 981 * Compare by path. 982 * @param {Entry} entry1 First entry. 983 * @param {Entry} entry2 Second entry. 984 * @return {number} Compare result. 985 */ 986util.comparePath = function(entry1, entry2) { 987 return util.collator.compare(entry1.fullPath, entry2.fullPath); 988}; 989 990/** 991 * Checks if the child entry is a descendant of another entry. If the entries 992 * point to the same file or directory, then returns false. 993 * 994 * @param {DirectoryEntry|Object} ancestorEntry The ancestor directory entry. 995 * Can be a fake. 996 * @param {Entry|Object} childEntry The child entry. Can be a fake. 997 * @return {boolean} True if the child entry is contained in the ancestor path. 998 */ 999util.isDescendantEntry = function(ancestorEntry, childEntry) { 1000 if (!ancestorEntry.isDirectory) 1001 return false; 1002 if (!util.isSameFileSystem(ancestorEntry.filesystem, childEntry.filesystem)) 1003 return false; 1004 if (util.isSameEntry(ancestorEntry, childEntry)) 1005 return false; 1006 if (util.isFakeEntry(ancestorEntry) || util.isFakeEntry(childEntry)) 1007 return false; 1008 1009 // Check if the ancestor's path with trailing slash is a prefix of child's 1010 // path. 1011 var ancestorPath = ancestorEntry.fullPath; 1012 if (ancestorPath.slice(-1) !== '/') 1013 ancestorPath += '/'; 1014 return childEntry.fullPath.indexOf(ancestorPath) === 0; 1015}; 1016 1017/** 1018 * Visit the URL. 1019 * 1020 * If the browser is opening, the url is opened in a new tag, otherwise the url 1021 * is opened in a new window. 1022 * 1023 * @param {string} url URL to visit. 1024 */ 1025util.visitURL = function(url) { 1026 window.open(url); 1027}; 1028 1029/** 1030 * Returns normalized current locale, or default locale - 'en'. 1031 * @return {string} Current locale 1032 */ 1033util.getCurrentLocaleOrDefault = function() { 1034 // chrome.i18n.getMessage('@@ui_locale') can't be used in packed app. 1035 // Instead, we pass it from C++-side with strings. 1036 return str('UI_LOCALE') || 'en'; 1037}; 1038 1039/** 1040 * Converts array of entries to an array of corresponding URLs. 1041 * @param {Array.<Entry>} entries Input array of entries. 1042 * @return {Array.<string>} Output array of URLs. 1043 */ 1044util.entriesToURLs = function(entries) { 1045 return entries.map(function(entry) { 1046 return entry.toURL(); 1047 }); 1048}; 1049 1050/** 1051 * Converts array of URLs to an array of corresponding Entries. 1052 * 1053 * @param {Array.<string>} urls Input array of URLs. 1054 * @param {function(Array.<Entry>, Array.<URL>)=} opt_callback Completion 1055 * callback with array of success Entries and failure URLs. 1056 * @return {Promise} Promise fulfilled with the object that has entries property 1057 * and failureUrls property. The promise is never rejected. 1058 */ 1059util.URLsToEntries = function(urls, opt_callback) { 1060 var promises = urls.map(function(url) { 1061 return new Promise(webkitResolveLocalFileSystemURL.bind(null, url)). 1062 then(function(entry) { 1063 return {entry: entry}; 1064 }, function(failureUrl) { 1065 // Not an error. Possibly, the file is not accessible anymore. 1066 console.warn('Failed to resolve the file with url: ' + url + '.'); 1067 return {failureUrl: url}; 1068 }); 1069 }); 1070 var resultPromise = Promise.all(promises).then(function(results) { 1071 var entries = []; 1072 var failureUrls = []; 1073 for (var i = 0; i < results.length; i++) { 1074 if ('entry' in results[i]) 1075 entries.push(results[i].entry); 1076 if ('failureUrl' in results[i]) { 1077 failureUrls.push(results[i].failureUrl); 1078 } 1079 } 1080 return { 1081 entries: entries, 1082 failureUrls: failureUrls 1083 }; 1084 }); 1085 1086 // Invoke the callback. If opt_callback is specified, resultPromise is still 1087 // returned and fulfilled with a result. 1088 if (opt_callback) { 1089 resultPromise.then(function(result) { 1090 opt_callback(result.entries, result.failureUrls); 1091 }).catch(function(error) { 1092 console.error( 1093 'util.URLsToEntries is failed.', 1094 error.stack ? error.stack : error); 1095 }); 1096 } 1097 1098 return resultPromise; 1099}; 1100 1101/** 1102 * Returns whether the window is teleported or not. 1103 * @param {DOMWindow} window Window. 1104 * @return {Promise.<boolean>} Whether the window is teleported or not. 1105 */ 1106util.isTeleported = function(window) { 1107 return new Promise(function(onFulfilled) { 1108 window.chrome.fileManagerPrivate.getProfiles( 1109 function(profiles, currentId, displayedId) { 1110 onFulfilled(currentId !== displayedId); 1111 }); 1112 }); 1113}; 1114 1115/** 1116 * Sets up and shows the alert to inform a user the task is opened in the 1117 * desktop of the running profile. 1118 * 1119 * TODO(hirono): Move the function from the util namespace. 1120 * @param {cr.ui.AlertDialog} alertDialog Alert dialog to be shown. 1121 * @param {Array.<Entry>} entries List of opened entries. 1122 */ 1123util.showOpenInOtherDesktopAlert = function(alertDialog, entries) { 1124 if (!entries.length) 1125 return; 1126 chrome.fileManagerPrivate.getProfiles( 1127 function(profiles, currentId, displayedId) { 1128 // Find strings. 1129 var displayName; 1130 for (var i = 0; i < profiles.length; i++) { 1131 if (profiles[i].profileId === currentId) { 1132 displayName = profiles[i].displayName; 1133 break; 1134 } 1135 } 1136 if (!displayName) { 1137 console.warn('Display name is not found.'); 1138 return; 1139 } 1140 1141 var title = entries.size > 1 ? 1142 entries[0].name + '\u2026' /* ellipsis */ : entries[0].name; 1143 var message = strf(entries.size > 1 ? 1144 'OPEN_IN_OTHER_DESKTOP_MESSAGE_PLURAL' : 1145 'OPEN_IN_OTHER_DESKTOP_MESSAGE', 1146 displayName, 1147 currentId); 1148 1149 // Show the dialog. 1150 alertDialog.showWithTitle(title, message); 1151 }.bind(this)); 1152}; 1153 1154/** 1155 * Runs chrome.test.sendMessage in test environment. Does nothing if running 1156 * in production environment. 1157 * 1158 * @param {string} message Test message to send. 1159 */ 1160util.testSendMessage = function(message) { 1161 var test = chrome.test || window.top.chrome.test; 1162 if (test) 1163 test.sendMessage(message); 1164}; 1165 1166/** 1167 * Returns the localized name for the root type. If not available, then returns 1168 * null. 1169 * 1170 * @param {VolumeManagerCommon.RootType} rootType The root type. 1171 * @return {?string} The localized name, or null if not available. 1172 */ 1173util.getRootTypeLabel = function(rootType) { 1174 var str = function(id) { 1175 return loadTimeData.getString(id); 1176 }; 1177 1178 switch (rootType) { 1179 case VolumeManagerCommon.RootType.DOWNLOADS: 1180 return str('DOWNLOADS_DIRECTORY_LABEL'); 1181 case VolumeManagerCommon.RootType.DRIVE: 1182 return str('DRIVE_MY_DRIVE_LABEL'); 1183 case VolumeManagerCommon.RootType.DRIVE_OFFLINE: 1184 return str('DRIVE_OFFLINE_COLLECTION_LABEL'); 1185 case VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME: 1186 return str('DRIVE_SHARED_WITH_ME_COLLECTION_LABEL'); 1187 case VolumeManagerCommon.RootType.DRIVE_RECENT: 1188 return str('DRIVE_RECENT_COLLECTION_LABEL'); 1189 } 1190 1191 // Translation not found. 1192 return null; 1193}; 1194 1195/** 1196 * Extracts the extension of the path. 1197 * 1198 * Examples: 1199 * util.splitExtension('abc.ext') -> ['abc', '.ext'] 1200 * util.splitExtension('a/b/abc.ext') -> ['a/b/abc', '.ext'] 1201 * util.splitExtension('a/b') -> ['a/b', ''] 1202 * util.splitExtension('.cshrc') -> ['', '.cshrc'] 1203 * util.splitExtension('a/b.backup/hoge') -> ['a/b.backup/hoge', ''] 1204 * 1205 * @param {string} path Path to be extracted. 1206 * @return {Array.<string>} Filename and extension of the given path. 1207 */ 1208util.splitExtension = function(path) { 1209 var dotPosition = path.lastIndexOf('.'); 1210 if (dotPosition <= path.lastIndexOf('/')) 1211 dotPosition = -1; 1212 1213 var filename = dotPosition != -1 ? path.substr(0, dotPosition) : path; 1214 var extension = dotPosition != -1 ? path.substr(dotPosition) : ''; 1215 return [filename, extension]; 1216}; 1217 1218/** 1219 * Returns the localized name of the entry. 1220 * 1221 * @param {VolumeManager} volumeManager The volume manager. 1222 * @param {Entry} entry The entry to be retrieve the name of. 1223 * @return {?string} The localized name. 1224 */ 1225util.getEntryLabel = function(volumeManager, entry) { 1226 var locationInfo = volumeManager.getLocationInfo(entry); 1227 1228 if (locationInfo && locationInfo.isRootEntry) { 1229 switch (locationInfo.rootType) { 1230 case VolumeManagerCommon.RootType.DOWNLOADS: 1231 return str('DOWNLOADS_DIRECTORY_LABEL'); 1232 case VolumeManagerCommon.RootType.DRIVE: 1233 return str('DRIVE_MY_DRIVE_LABEL'); 1234 case VolumeManagerCommon.RootType.DRIVE_OFFLINE: 1235 return str('DRIVE_OFFLINE_COLLECTION_LABEL'); 1236 case VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME: 1237 return str('DRIVE_SHARED_WITH_ME_COLLECTION_LABEL'); 1238 case VolumeManagerCommon.RootType.DRIVE_RECENT: 1239 return str('DRIVE_RECENT_COLLECTION_LABEL'); 1240 case VolumeManagerCommon.RootType.DRIVE_OTHER: 1241 case VolumeManagerCommon.RootType.DOWNLOADS: 1242 case VolumeManagerCommon.RootType.ARCHIVE: 1243 case VolumeManagerCommon.RootType.REMOVABLE: 1244 case VolumeManagerCommon.RootType.MTP: 1245 case VolumeManagerCommon.RootType.PROVIDED: 1246 return locationInfo.volumeInfo.label; 1247 default: 1248 console.error('Unsupported root type: ' + locationInfo.rootType); 1249 return locationInfo.volumeInfo.label; 1250 } 1251 } 1252 1253 return entry.name; 1254}; 1255 1256/** 1257 * Checks if the specified set of allowed effects contains the given effect. 1258 * See: http://www.w3.org/TR/html5/editing.html#the-datatransfer-interface 1259 * 1260 * @param {string} effectAllowed The string denoting the set of allowed effects. 1261 * @param {string} dropEffect The effect to be checked. 1262 * @return {boolean} True if |dropEffect| is included in |effectAllowed|. 1263 */ 1264util.isDropEffectAllowed = function(effectAllowed, dropEffect) { 1265 return effectAllowed === 'all' || 1266 effectAllowed.toLowerCase().indexOf(dropEffect) !== -1; 1267}; 1268 1269/** 1270 * Verifies the user entered name for file or folder to be created or 1271 * renamed to. Name restrictions must correspond to File API restrictions 1272 * (see DOMFilePath::isValidPath). Curernt WebKit implementation is 1273 * out of date (spec is 1274 * http://dev.w3.org/2009/dap/file-system/file-dir-sys.html, 8.3) and going to 1275 * be fixed. Shows message box if the name is invalid. 1276 * 1277 * It also verifies if the name length is in the limit of the filesystem. 1278 * 1279 * @param {DirectoryEntry} parentEntry The URL of the parent directory entry. 1280 * @param {string} name New file or folder name. 1281 * @param {boolean} filterHiddenOn Whether to report the hidden file name error 1282 * or not. 1283 * @return {Promise} Promise fulfilled on success, or rejected with the error 1284 * message. 1285 */ 1286util.validateFileName = function(parentEntry, name, filterHiddenOn) { 1287 var testResult = /[\/\\\<\>\:\?\*\"\|]/.exec(name); 1288 var msg; 1289 if (testResult) 1290 return Promise.reject(strf('ERROR_INVALID_CHARACTER', testResult[0])); 1291 else if (/^\s*$/i.test(name)) 1292 return Promise.reject(str('ERROR_WHITESPACE_NAME')); 1293 else if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(name)) 1294 return Promise.reject(str('ERROR_RESERVED_NAME')); 1295 else if (filterHiddenOn && /\.crdownload$/i.test(name)) 1296 return Promise.reject(str('ERROR_RESERVED_NAME')); 1297 else if (filterHiddenOn && name[0] == '.') 1298 return Promise.reject(str('ERROR_HIDDEN_NAME')); 1299 1300 return new Promise(function(fulfill, reject) { 1301 chrome.fileManagerPrivate.validatePathNameLength( 1302 parentEntry.toURL(), 1303 name, 1304 function(valid) { 1305 if (valid) 1306 fulfill(); 1307 else 1308 reject(str('ERROR_LONG_NAME')); 1309 }); 1310 }); 1311}; 1312