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.
8 * Namespace for utility functions.
13 * Returns a function that console.log's its arguments, prefixed by |msg|.
15 * @param {string} msg The message prefix to use in the log.
16 * @param {function(...string)=} opt_callback A function to invoke after
18 * @return {function(...string)} Function that logs.
20 util.flog = function(msg, opt_callback) {
22 var ary = Array.apply(null, arguments);
23 console.log(msg + ': ' + ary.join(', '));
25 opt_callback.apply(null, arguments);
30 * Returns a function that throws an exception that includes its arguments
33 * @param {string} msg The message prefix to use in the exception.
34 * @return {function(...string)} Function that throws.
36 util.ferr = function(msg) {
38 var ary = Array.apply(null, arguments);
39 throw new Error(msg + ': ' + ary.join(', '));
44 * @param {string} name File error name.
45 * @return {string} Translated file error string.
47 util.getFileErrorString = function(name) {
48 var candidateMessageFragment;
51 candidateMessageFragment = 'NOT_FOUND';
54 candidateMessageFragment = 'SECURITY';
56 case 'NotReadableError':
57 candidateMessageFragment = 'NOT_READABLE';
59 case 'NoModificationAllowedError':
60 candidateMessageFragment = 'NO_MODIFICATION_ALLOWED';
62 case 'InvalidStateError':
63 candidateMessageFragment = 'INVALID_STATE';
65 case 'InvalidModificationError':
66 candidateMessageFragment = 'INVALID_MODIFICATION';
68 case 'PathExistsError':
69 candidateMessageFragment = 'PATH_EXISTS';
71 case 'QuotaExceededError':
72 candidateMessageFragment = 'QUOTA_EXCEEDED';
76 return loadTimeData.getString('FILE_ERROR_' + candidateMessageFragment) ||
77 loadTimeData.getString('FILE_ERROR_GENERIC');
81 * Mapping table for FileError.code style enum to DOMError.name string.
86 util.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',
100 * @param {string} str String to escape.
101 * @return {string} Escaped string.
103 util.htmlEscape = function(str) {
104 return str.replace(/[<>&]/g, function(entity) {
106 case '<': return '<';
107 case '>': return '>';
108 case '&': return '&';
114 * @param {string} str String to unescape.
115 * @return {string} Unescaped string.
117 util.htmlUnescape = function(str) {
118 return str.replace(/&(lt|gt|amp);/g, function(entity) {
120 case '<': return '<';
121 case '>': return '>';
122 case '&': return '&';
128 * Iterates the entries contained by dirEntry, and invokes callback once for
129 * each entry. On completion, successCallback will be invoked.
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.
137 util.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) {
149 function(forEachCallback, entry) {
150 // Do not pass index nor entries.
151 callback(entry, forEachCallback);
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.
165 util.readDirectory = function(root, path, callback) {
166 var onError = function(e) {
169 root.getDirectory(path, {create: false}, function(entry) {
170 var reader = entry.createReader();
172 var readNext = function() {
173 reader.readEntries(function(results) {
174 if (results.length == 0) {
178 r.push.apply(r, results);
187 * Utility function to resolve multiple directories with a single call.
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.
193 * The successCallback is invoked with a null entry when all paths have
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
203 * @param {function(FileError)} errorCallback The function to invoke
204 * for each path that cannot be resolved.
206 util.getDirectories = function(dirEntry, params, paths, successCallback,
209 // Copy the params array, since we're going to destroy it.
210 params = [].slice.call(params);
212 var onComplete = function() {
213 successCallback(null);
216 var getNextDirectory = function() {
217 var path = paths.shift();
221 dirEntry.getDirectory(
224 successCallback(entry);
237 * Utility function to resolve multiple files with a single call.
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.
243 * The successCallback is invoked with a null entry when all paths have
246 * @param {DirEntry} dirEntry The base directory.
247 * @param {Object} params The parameters to pass to the underlying
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
253 * @param {function(FileError)} errorCallback The function to invoke
254 * for each path that cannot be resolved.
256 util.getFiles = function(dirEntry, params, paths, successCallback,
258 // Copy the params array, since we're going to destroy it.
259 params = [].slice.call(params);
261 var onComplete = function() {
262 successCallback(null);
265 var getNextFile = function() {
266 var path = paths.shift();
273 successCallback(entry);
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
294 util.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},
305 // The entry with the name already exists.
306 errorCallback(util.createDOMError(util.FileError.PATH_EXISTS_ERR));
309 if (error.name != util.FileError.NOT_FOUND_ERR) {
310 // Unexpected error is found.
311 errorCallback(error);
315 // No existing entry is found.
316 entry.moveTo(parent, newName, successCallback, errorCallback);
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.
327 util.removeFileOrDirectory = function(entry, onSuccess, onError) {
328 if (entry.isDirectory)
329 entry.removeRecursively(onSuccess, onError);
331 entry.remove(onSuccess, onError);
335 * Convert a number of bytes into a human friendly format, using the correct
338 * @param {number} bytes The number of bytes.
339 * @return {string} Localized string.
341 util.bytesToString = function(bytes) {
342 // Translation identifiers for size units.
343 var UNITS = ['SIZE_BYTES',
350 // Minimum values for the units above.
358 var str = function(n, u) {
359 // TODO(rginda): Switch to v8Locale's number formatter when it's
361 return strf(u, n.toLocaleString());
364 var fmt = function(s, u) {
365 var rounded = Math.round(bytes / s * 10) / 10;
366 return str(rounded, u);
369 // Less than 1KB is displayed like '80 bytes'.
370 if (bytes < STEPS[1]) {
371 return str(bytes, UNITS[0]);
374 // Up to 1MB is displayed as rounded up number of KBs.
375 if (bytes < STEPS[2]) {
376 var rounded = Math.ceil(bytes / STEPS[1]);
377 return str(rounded, UNITS[1]);
380 // This loop index is used outside the loop if it turns out |bytes|
381 // requires the largest unit.
384 for (i = 2 /* MB */; i < UNITS.length - 1; i++) {
385 if (bytes < STEPS[i + 1])
386 return fmt(STEPS[i], UNITS[i]);
389 return fmt(STEPS[i], UNITS[i]);
393 * Utility function to read specified range of bytes from file
394 * @param {File} file The file to read.
395 * @param {number} begin Starting byte(included).
396 * @param {number} end Last byte(excluded).
397 * @param {function(File, Uint8Array)} callback Callback to invoke.
398 * @param {function(FileError)} onError Error handler.
400 util.readFileBytes = function(file, begin, end, callback, onError) {
401 var fileReader = new FileReader();
402 fileReader.onerror = onError;
403 fileReader.onloadend = function() {
404 callback(file, new ByteReader(fileReader.result));
406 fileReader.readAsArrayBuffer(file.slice(begin, end));
410 * Write a blob to a file.
411 * Truncates the file first, so the previous content is fully overwritten.
412 * @param {FileEntry} entry File entry.
413 * @param {Blob} blob The blob to write.
414 * @param {function(Event)} onSuccess Completion callback. The first argument is
415 * a 'writeend' event.
416 * @param {function(FileError)} onError Error handler.
418 util.writeBlobToFile = function(entry, blob, onSuccess, onError) {
419 var truncate = function(writer) {
420 writer.onerror = onError;
421 writer.onwriteend = write.bind(null, writer);
425 var write = function(writer) {
426 writer.onwriteend = onSuccess;
430 entry.createWriter(truncate, onError);
434 * Returns a string '[Ctrl-][Alt-][Shift-][Meta-]' depending on the event
435 * modifiers. Convenient for writing out conditions in keyboard handlers.
437 * @param {Event} event The keyboard event.
438 * @return {string} Modifiers.
440 util.getKeyModifiers = function(event) {
441 return (event.ctrlKey ? 'Ctrl-' : '') +
442 (event.altKey ? 'Alt-' : '') +
443 (event.shiftKey ? 'Shift-' : '') +
444 (event.metaKey ? 'Meta-' : '');
448 * @param {HTMLElement} element Element to transform.
449 * @param {Object} transform Transform object,
450 * contains scaleX, scaleY and rotate90 properties.
452 util.applyTransform = function(element, transform) {
453 element.style.webkitTransform =
454 transform ? 'scaleX(' + transform.scaleX + ') ' +
455 'scaleY(' + transform.scaleY + ') ' +
456 'rotate(' + transform.rotate90 * 90 + 'deg)' :
461 * Makes filesystem: URL from the path.
462 * @param {string} path File or directory path.
463 * @return {string} URL.
465 util.makeFilesystemUrl = function(path) {
466 path = path.split('/').map(encodeURIComponent).join('/');
467 var prefix = 'external';
468 return 'filesystem:' + chrome.runtime.getURL(prefix + path);
472 * Extracts path from filesystem: URL.
473 * @param {string} url Filesystem URL.
474 * @return {string} The path.
476 util.extractFilePath = function(url) {
478 /^filesystem:[\w-]*:\/\/[\w]*\/(external|persistent|temporary)(\/.*)$/.
480 var path = match && match[2];
481 if (!path) return null;
482 return decodeURIComponent(path);
486 * Traverses a directory tree whose root is the given entry, and invokes
487 * callback for each entry. Upon completion, successCallback will be called.
488 * On error, errorCallback will be called.
490 * @param {Entry} entry The root entry.
491 * @param {function(Entry):boolean} callback Callback invoked for each entry.
492 * If this returns false, entries under it won't be traversed. Note that
493 * its siblings (and their children) will be still traversed.
494 * @param {function()} successCallback Called upon successful completion.
495 * @param {function(error)} errorCallback Called upon error.
497 util.traverseTree = function(entry, callback, successCallback, errorCallback) {
498 if (!callback(entry)) {
503 util.forEachDirEntry(
505 function(child, iterationCallback) {
506 util.traverseTree(child, callback, iterationCallback, errorCallback);
513 * A shortcut function to create a child element with given tag and class.
515 * @param {HTMLElement} parent Parent element.
516 * @param {string=} opt_className Class name.
517 * @param {string=} opt_tag Element tag, DIV is omitted.
518 * @return {Element} Newly created element.
520 util.createChild = function(parent, opt_className, opt_tag) {
521 var child = parent.ownerDocument.createElement(opt_tag || 'div');
523 child.className = opt_className;
524 parent.appendChild(child);
529 * Updates the app state.
531 * @param {string} currentDirectoryURL Currently opened directory as an URL.
532 * If null the value is left unchanged.
533 * @param {string} selectionURL Currently selected entry as an URL. If null the
534 * value is left unchanged.
535 * @param {string|Object=} opt_param Additional parameters, to be stored. If
536 * null, then left unchanged.
538 util.updateAppState = function(currentDirectoryURL, selectionURL, opt_param) {
539 window.appState = window.appState || {};
540 if (opt_param !== undefined && opt_param !== null)
541 window.appState.params = opt_param;
542 if (currentDirectoryURL !== null)
543 window.appState.currentDirectoryURL = currentDirectoryURL;
544 if (selectionURL !== null)
545 window.appState.selectionURL = selectionURL;
550 * Returns a translated string.
552 * Wrapper function to make dealing with translated strings more concise.
553 * Equivalent to loadTimeData.getString(id).
555 * @param {string} id The id of the string to return.
556 * @return {string} The translated string.
559 return loadTimeData.getString(id);
563 * Returns a translated string with arguments replaced.
565 * Wrapper function to make dealing with translated strings more concise.
566 * Equivalent to loadTimeData.getStringF(id, ...).
568 * @param {string} id The id of the string to return.
569 * @param {...string} var_args The values to replace into the string.
570 * @return {string} The translated string with replaced values.
572 function strf(id, var_args) {
573 return loadTimeData.getStringF.apply(loadTimeData, arguments);
577 * Adapter object that abstracts away the the difference between Chrome app APIs
578 * v1 and v2. Is only necessary while the migration to v2 APIs is in progress.
579 * TODO(mtomasz): Clean up this. crbug.com/240606.
583 * @return {boolean} True if Files.app is running as an open files or a select
584 * folder dialog. False otherwise.
586 runningInBrowser: function() {
587 return !window.appID;
591 * @param {function(Object)} callback Function accepting a preference map.
593 getPreferences: function(callback) {
594 chrome.storage.local.get(callback);
598 * @param {string} key Preference name.
599 * @param {function(string)} callback Function accepting the preference value.
601 getPreference: function(key, callback) {
602 chrome.storage.local.get(key, function(items) {
603 callback(items[key]);
608 * @param {string} key Preference name.
609 * @param {string|Object} value Preference value.
610 * @param {function()=} opt_callback Completion callback.
612 setPreference: function(key, value, opt_callback) {
613 if (typeof value != 'string')
614 value = JSON.stringify(value);
618 chrome.storage.local.set(items, opt_callback);
623 * Attach page load handler.
624 * @param {function()} handler Application-specific load handler.
626 util.addPageLoadHandler = function(handler) {
627 document.addEventListener('DOMContentLoaded', function() {
633 * Save app launch data to the local storage.
635 util.saveAppState = function() {
637 util.platform.setPreference(window.appID, window.appState);
641 * AppCache is a persistent timestamped key-value storage backed by
642 * HTML5 local storage.
644 * It is not designed for frequent access. In order to avoid costly
645 * localStorage iteration all data is kept in a single localStorage item.
646 * There is no in-memory caching, so concurrent access is _almost_ safe.
648 * TODO(kaznacheev) Reimplement this based on Indexed DB.
650 util.AppCache = function() {};
655 util.AppCache.KEY = 'AppCache';
658 * Max number of items.
660 util.AppCache.CAPACITY = 100;
665 util.AppCache.LIFETIME = 30 * 24 * 60 * 60 * 1000; // 30 days.
668 * @param {string} key Key.
669 * @param {function(number)} callback Callback accepting a value.
671 util.AppCache.getValue = function(key, callback) {
672 util.AppCache.read_(function(map) {
673 var entry = map[key];
674 callback(entry && entry.value);
681 * @param {string} key Key.
682 * @param {string} value Value. Remove the key if value is null.
683 * @param {number=} opt_lifetime Maximum time to keep an item (in milliseconds).
685 util.AppCache.update = function(key, value, opt_lifetime) {
686 util.AppCache.read_(function(map) {
690 expire: Date.now() + (opt_lifetime || util.AppCache.LIFETIME)
692 } else if (key in map) {
695 return; // Nothing to do.
697 util.AppCache.cleanup_(map);
698 util.AppCache.write_(map);
703 * @param {function(Object)} callback Callback accepting a map of timestamped
707 util.AppCache.read_ = function(callback) {
708 util.platform.getPreference(util.AppCache.KEY, function(json) {
711 callback(JSON.parse(json));
713 // The local storage item somehow got messed up, start fresh.
721 * @param {Object} map A map of timestamped key-value pairs.
724 util.AppCache.write_ = function(map) {
725 util.platform.setPreference(util.AppCache.KEY, JSON.stringify(map));
729 * Remove over-capacity and obsolete items.
731 * @param {Object} map A map of timestamped key-value pairs.
734 util.AppCache.cleanup_ = function(map) {
735 // Sort keys by ascending timestamps.
737 for (var key in map) {
738 if (map.hasOwnProperty(key))
741 keys.sort(function(a, b) { return map[a].expire > map[b].expire; });
743 var cutoff = Date.now();
746 while (obsolete < keys.length &&
747 map[keys[obsolete]].expire < cutoff) {
751 var overCapacity = Math.max(0, keys.length - util.AppCache.CAPACITY);
753 var itemsToDelete = Math.max(obsolete, overCapacity);
754 for (var i = 0; i != itemsToDelete; i++) {
762 * @param {Image} image Image element.
763 * @param {string} url Source url.
764 * @param {Object=} opt_options Hash array of options, eg. width, height,
765 * maxWidth, maxHeight, scale, cache.
766 * @param {function()=} opt_isValid Function returning false iff the task
767 * is not valid and should be aborted.
768 * @return {?number} Task identifier or null if fetched immediately from
771 util.loadImage = function(image, url, opt_options, opt_isValid) {
772 return ImageLoaderClient.loadToImage(url,
776 function() { image.onerror(); },
781 * Cancels loading an image.
782 * @param {number} taskId Task identifier returned by util.loadImage().
784 util.cancelLoadImage = function(taskId) {
785 ImageLoaderClient.getInstance().cancel(taskId);
789 * Finds proerty descriptor in the object prototype chain.
790 * @param {Object} object The object.
791 * @param {string} propertyName The property name.
792 * @return {Object} Property descriptor.
794 util.findPropertyDescriptor = function(object, propertyName) {
795 for (var p = object; p; p = Object.getPrototypeOf(p)) {
796 var d = Object.getOwnPropertyDescriptor(p, propertyName);
804 * Calls inherited property setter (useful when property is
806 * @param {Object} object The object.
807 * @param {string} propertyName The property name.
808 * @param {*} value Value to set.
810 util.callInheritedSetter = function(object, propertyName, value) {
811 var d = util.findPropertyDescriptor(Object.getPrototypeOf(object),
813 d.set.call(object, value);
817 * Returns true if the board of the device matches the given prefix.
818 * @param {string} boardPrefix The board prefix to match against.
819 * (ex. "x86-mario". Prefix is used as the actual board name comes with
820 * suffix like "x86-mario-something".
821 * @return {boolean} True if the board of the device matches the given prefix.
823 util.boardIs = function(boardPrefix) {
824 // The board name should be lower-cased, but making it case-insensitive for
825 // backward compatibility just in case.
826 var board = str('CHROMEOS_RELEASE_BOARD');
827 var pattern = new RegExp('^' + boardPrefix, 'i');
828 return board.match(pattern) != null;
832 * Adds an isFocused method to the current window object.
834 util.addIsFocusedMethod = function() {
837 window.addEventListener('focus', function() {
841 window.addEventListener('blur', function() {
846 * @return {boolean} True if focused.
848 window.isFocused = function() {
854 * Makes a redirect to the specified Files.app's window from another window.
855 * @param {number} id Window id.
856 * @param {string} url Target url.
857 * @return {boolean} True if the window has been found. False otherwise.
859 util.redirectMainWindow = function(id, url) {
860 // TODO(mtomasz): Implement this for Apps V2, once the photo importer is
866 * Checks, if the Files.app's window is in a full screen mode.
868 * @param {AppWindow} appWindow App window to be maximized.
869 * @return {boolean} True if the full screen mode is enabled.
871 util.isFullScreen = function(appWindow) {
873 return appWindow.isFullscreen();
875 console.error('App window not passed. Unable to check status of ' +
876 'the full screen mode.');
882 * Toggles the full screen mode.
884 * @param {AppWindow} appWindow App window to be maximized.
885 * @param {boolean} enabled True for enabling, false for disabling.
887 util.toggleFullScreen = function(appWindow, enabled) {
890 appWindow.fullscreen();
897 'App window not passed. Unable to toggle the full screen mode.');
901 * The type of a file operation.
905 util.FileOperationType = Object.freeze({
912 * The type of a file operation error.
916 util.FileOperationErrorType = Object.freeze({
917 UNEXPECTED_SOURCE_FILE: 0,
923 * The kind of an entry changed event.
927 util.EntryChangedKind = Object.freeze({
933 * Obtains whether an entry is fake or not.
934 * @param {!Entry|!Object} entry Entry or a fake entry.
935 * @return {boolean} True if the given entry is fake.
937 util.isFakeEntry = function(entry) {
938 return !('getParent' in entry);
942 * Creates an instance of UserDOMError with given error name that looks like a
943 * FileError except that it does not have the deprecated FileError.code member.
945 * TODO(uekawa): remove reference to FileError.
947 * @param {string} name Error name for the file error.
948 * @return {UserDOMError} FileError instance
950 util.createDOMError = function(name) {
951 return new util.UserDOMError(name);
955 * Creates a DOMError-like object to be used in place of returning file errors.
957 * @param {string} name Error name for the file error.
960 util.UserDOMError = function(name) {
969 util.UserDOMError.prototype = {
971 * @return {string} File error name.
979 * Compares two entries.
980 * @param {Entry|Object} entry1 The entry to be compared. Can be a fake.
981 * @param {Entry|Object} entry2 The entry to be compared. Can be a fake.
982 * @return {boolean} True if the both entry represents a same file or
983 * directory. Returns true if both entries are null.
985 util.isSameEntry = function(entry1, entry2) {
986 if (!entry1 && !entry2)
988 if (!entry1 || !entry2)
990 return entry1.toURL() === entry2.toURL();
994 * Compares two file systems.
995 * @param {DOMFileSystem} fileSystem1 The file system to be compared.
996 * @param {DOMFileSystem} fileSystem2 The file system to be compared.
997 * @return {boolean} True if the both file systems are equal. Also, returns true
998 * if both file systems are null.
1000 util.isSameFileSystem = function(fileSystem1, fileSystem2) {
1001 if (!fileSystem1 && !fileSystem2)
1003 if (!fileSystem1 || !fileSystem2)
1005 return util.isSameEntry(fileSystem1.root, fileSystem2.root);
1009 * Collator for sorting.
1010 * @type {Intl.Collator}
1012 util.collator = new Intl.Collator([], {usage: 'sort',
1014 sensitivity: 'base'});
1017 * Compare by name. The 2 entries must be in same directory.
1018 * @param {Entry} entry1 First entry.
1019 * @param {Entry} entry2 Second entry.
1020 * @return {number} Compare result.
1022 util.compareName = function(entry1, entry2) {
1023 return util.collator.compare(entry1.name, entry2.name);
1028 * @param {Entry} entry1 First entry.
1029 * @param {Entry} entry2 Second entry.
1030 * @return {number} Compare result.
1032 util.comparePath = function(entry1, entry2) {
1033 return util.collator.compare(entry1.fullPath, entry2.fullPath);
1037 * Checks if the child entry is a descendant of another entry. If the entries
1038 * point to the same file or directory, then returns false.
1040 * @param {DirectoryEntry|Object} ancestorEntry The ancestor directory entry.
1042 * @param {Entry|Object} childEntry The child entry. Can be a fake.
1043 * @return {boolean} True if the child entry is contained in the ancestor path.
1045 util.isDescendantEntry = function(ancestorEntry, childEntry) {
1046 if (!ancestorEntry.isDirectory)
1048 if (!util.isSameFileSystem(ancestorEntry.filesystem, childEntry.filesystem))
1050 if (util.isSameEntry(ancestorEntry, childEntry))
1052 if (util.isFakeEntry(ancestorEntry) || util.isFakeEntry(childEntry))
1055 // Check if the ancestor's path with trailing slash is a prefix of child's
1057 var ancestorPath = ancestorEntry.fullPath;
1058 if (ancestorPath.slice(-1) !== '/')
1059 ancestorPath += '/';
1060 return childEntry.fullPath.indexOf(ancestorPath) === 0;
1066 * If the browser is opening, the url is opened in a new tag, otherwise the url
1067 * is opened in a new window.
1069 * @param {string} url URL to visit.
1071 util.visitURL = function(url) {
1076 * Returns normalized current locale, or default locale - 'en'.
1077 * @return {string} Current locale
1079 util.getCurrentLocaleOrDefault = function() {
1080 // chrome.i18n.getMessage('@@ui_locale') can't be used in packed app.
1081 // Instead, we pass it from C++-side with strings.
1082 return str('UI_LOCALE') || 'en';
1086 * Converts array of entries to an array of corresponding URLs.
1087 * @param {Array.<Entry>} entries Input array of entries.
1088 * @return {Array.<string>} Output array of URLs.
1090 util.entriesToURLs = function(entries) {
1091 // TODO(mtomasz): Make all callers use entries instead of URLs, and then
1092 // remove this utility function.
1093 console.warn('Converting entries to URLs is deprecated.');
1094 return entries.map(function(entry) {
1095 return entry.toURL();
1100 * Converts array of URLs to an array of corresponding Entries.
1102 * @param {Array.<string>} urls Input array of URLs.
1103 * @param {function(Array.<Entry>, Array.<URL>)=} opt_callback Completion
1104 * callback with array of success Entries and failure URLs.
1105 * @return {Promise} Promise fulfilled with the object that has entries property
1106 * and failureUrls property. The promise is never rejected.
1108 util.URLsToEntries = function(urls, opt_callback) {
1109 var promises = urls.map(function(url) {
1110 return new Promise(webkitResolveLocalFileSystemURL.bind(null, url)).
1111 then(function(entry) {
1112 return {entry: entry};
1113 }, function(failureUrl) {
1114 // Not an error. Possibly, the file is not accessible anymore.
1115 console.warn('Failed to resolve the file with url: ' + url + '.');
1116 return {failureUrl: url};
1119 var resultPromise = Promise.all(promises).then(function(results) {
1121 var failureUrls = [];
1122 for (var i = 0; i < results.length; i++) {
1123 if ('entry' in results[i])
1124 entries.push(results[i].entry);
1125 if ('failureUrl' in results[i]) {
1126 failureUrls.push(results[i].failureUrl);
1131 failureUrls: failureUrls
1135 // Invoke the callback. If opt_callback is specified, resultPromise is still
1136 // returned and fulfilled with a result.
1138 resultPromise.then(function(result) {
1139 opt_callback(result.entries, result.failureUrls);
1141 catch(function(error) {
1143 'util.URLsToEntries is failed.',
1144 error.stack ? error.stack : error);
1148 return resultPromise;
1152 * Returns whether the window is teleported or not.
1153 * @param {DOMWindow} window Window.
1154 * @return {Promise.<boolean>} Whether the window is teleported or not.
1156 util.isTeleported = function(window) {
1157 return new Promise(function(onFulfilled) {
1158 window.chrome.fileBrowserPrivate.getProfiles(function(profiles,
1161 onFulfilled(currentId !== displayedId);
1167 * Sets up and shows the alert to inform a user the task is opened in the
1168 * desktop of the running profile.
1170 * TODO(hirono): Move the function from the util namespace.
1171 * @param {cr.ui.AlertDialog} alertDialog Alert dialog to be shown.
1172 * @param {Array.<Entry>} entries List of opened entries.
1174 util.showOpenInOtherDesktopAlert = function(alertDialog, entries) {
1175 if (!entries.length)
1177 chrome.fileBrowserPrivate.getProfiles(function(profiles,
1182 for (var i = 0; i < profiles.length; i++) {
1183 if (profiles[i].profileId === currentId) {
1184 displayName = profiles[i].displayName;
1189 console.warn('Display name is not found.');
1193 var title = entries.size > 1 ?
1194 entries[0].name + '\u2026' /* ellipsis */ : entries[0].name;
1195 var message = strf(entries.size > 1 ?
1196 'OPEN_IN_OTHER_DESKTOP_MESSAGE_PLURAL' :
1197 'OPEN_IN_OTHER_DESKTOP_MESSAGE',
1202 alertDialog.showWithTitle(title, message);
1207 * Runs chrome.test.sendMessage in test environment. Does nothing if running
1208 * in production environment.
1210 * @param {string} message Test message to send.
1212 util.testSendMessage = function(message) {
1213 var test = chrome.test || window.top.chrome.test;
1215 test.sendMessage(message);
1219 * Returns the localized name for the root type. If not available, then returns
1222 * @param {VolumeManagerCommon.RootType} rootType The root type.
1223 * @return {?string} The localized name, or null if not available.
1225 util.getRootTypeLabel = function(rootType) {
1226 var str = function(id) {
1227 return loadTimeData.getString(id);
1231 case VolumeManagerCommon.RootType.DOWNLOADS:
1232 return str('DOWNLOADS_DIRECTORY_LABEL');
1233 case VolumeManagerCommon.RootType.DRIVE:
1234 return str('DRIVE_MY_DRIVE_LABEL');
1235 case VolumeManagerCommon.RootType.DRIVE_OFFLINE:
1236 return str('DRIVE_OFFLINE_COLLECTION_LABEL');
1237 case VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME:
1238 return str('DRIVE_SHARED_WITH_ME_COLLECTION_LABEL');
1239 case VolumeManagerCommon.RootType.DRIVE_RECENT:
1240 return str('DRIVE_RECENT_COLLECTION_LABEL');
1243 // Translation not found.
1248 * Extracts the extension of the path.
1251 * util.splitExtension('abc.ext') -> ['abc', '.ext']
1252 * util.splitExtension('a/b/abc.ext') -> ['a/b/abc', '.ext']
1253 * util.splitExtension('a/b') -> ['a/b', '']
1254 * util.splitExtension('.cshrc') -> ['', '.cshrc']
1255 * util.splitExtension('a/b.backup/hoge') -> ['a/b.backup/hoge', '']
1257 * @param {string} path Path to be extracted.
1258 * @return {Array.<string>} Filename and extension of the given path.
1260 util.splitExtension = function(path) {
1261 var dotPosition = path.lastIndexOf('.');
1262 if (dotPosition <= path.lastIndexOf('/'))
1265 var filename = dotPosition != -1 ? path.substr(0, dotPosition) : path;
1266 var extension = dotPosition != -1 ? path.substr(dotPosition) : '';
1267 return [filename, extension];
1271 * Returns the localized name of the entry.
1273 * @param {VolumeManager} volumeManager The volume manager.
1274 * @param {Entry} entry The entry to be retrieve the name of.
1275 * @return {?string} The localized name.
1277 util.getEntryLabel = function(volumeManager, entry) {
1278 var locationInfo = volumeManager.getLocationInfo(entry);
1280 if (locationInfo && locationInfo.isRootEntry) {
1281 switch (locationInfo.rootType) {
1282 case VolumeManagerCommon.RootType.DOWNLOADS:
1283 return str('DOWNLOADS_DIRECTORY_LABEL');
1284 case VolumeManagerCommon.RootType.DRIVE:
1285 return str('DRIVE_MY_DRIVE_LABEL');
1286 case VolumeManagerCommon.RootType.DRIVE_OFFLINE:
1287 return str('DRIVE_OFFLINE_COLLECTION_LABEL');
1288 case VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME:
1289 return str('DRIVE_SHARED_WITH_ME_COLLECTION_LABEL');
1290 case VolumeManagerCommon.RootType.DRIVE_RECENT:
1291 return str('DRIVE_RECENT_COLLECTION_LABEL');
1292 case VolumeManagerCommon.RootType.DRIVE_OTHER:
1293 case VolumeManagerCommon.RootType.DOWNLOADS:
1294 case VolumeManagerCommon.RootType.ARCHIVE:
1295 case VolumeManagerCommon.RootType.REMOVABLE:
1296 case VolumeManagerCommon.RootType.MTP:
1297 case VolumeManagerCommon.RootType.PROVIDED:
1298 return locationInfo.volumeInfo.label;
1300 console.error('Unsupported root type: ' + locationInfo.rootType);
1301 return locationInfo.volumeInfo.label;
1309 * Checks if the specified set of allowed effects contains the given effect.
1310 * See: http://www.w3.org/TR/html5/editing.html#the-datatransfer-interface
1312 * @param {string} effectAllowed The string denoting the set of allowed effects.
1313 * @param {string} dropEffect The effect to be checked.
1314 * @return {boolean} True if |dropEffect| is included in |effectAllowed|.
1316 util.isDropEffectAllowed = function(effectAllowed, dropEffect) {
1317 return effectAllowed === 'all' ||
1318 effectAllowed.toLowerCase().indexOf(dropEffect) !== -1;
1322 * Verifies the user entered name for file or folder to be created or
1323 * renamed to. Name restrictions must correspond to File API restrictions
1324 * (see DOMFilePath::isValidPath). Curernt WebKit implementation is
1325 * out of date (spec is
1326 * http://dev.w3.org/2009/dap/file-system/file-dir-sys.html, 8.3) and going to
1327 * be fixed. Shows message box if the name is invalid.
1329 * It also verifies if the name length is in the limit of the filesystem.
1331 * @param {DirectoryEntry} parentEntry The URL of the parent directory entry.
1332 * @param {string} name New file or folder name.
1333 * @param {boolean} filterHiddenOn Whether to report the hidden file name error
1335 * @return {Promise} Promise fulfilled on success, or rejected with the error
1338 util.validateFileName = function(parentEntry, name, filterHiddenOn) {
1339 var testResult = /[\/\\\<\>\:\?\*\"\|]/.exec(name);
1342 return Promise.reject(strf('ERROR_INVALID_CHARACTER', testResult[0]));
1343 else if (/^\s*$/i.test(name))
1344 return Promise.reject(str('ERROR_WHITESPACE_NAME'));
1345 else if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(name))
1346 return Promise.reject(str('ERROR_RESERVED_NAME'));
1347 else if (filterHiddenOn && name[0] == '.')
1348 return Promise.reject(str('ERROR_HIDDEN_NAME'));
1350 return new Promise(function(fulfill, reject) {
1351 chrome.fileBrowserPrivate.validatePathNameLength(
1352 parentEntry.toURL(),
1358 reject(str('ERROR_LONG_NAME'));