Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / file_manager / common / js / util.js
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  */
10 var 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  */
20 util.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  */
36 util.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  */
47 util.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  */
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',
97 });
98
99 /**
100  * @param {string} str String to escape.
101  * @return {string} Escaped string.
102  */
103 util.htmlEscape = function(str) {
104   return str.replace(/[<>&]/g, function(entity) {
105     switch (entity) {
106       case '<': return '&lt;';
107       case '>': return '&gt;';
108       case '&': return '&amp;';
109     }
110   });
111 };
112
113 /**
114  * @param {string} str String to unescape.
115  * @return {string} Unescaped string.
116  */
117 util.htmlUnescape = function(str) {
118   return str.replace(/&(lt|gt|amp);/g, function(entity) {
119     switch (entity) {
120       case '&lt;': return '<';
121       case '&gt;': return '>';
122       case '&amp;': 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  */
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) {
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  */
165 util.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  */
206 util.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  */
256 util.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  */
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},
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  */
327 util.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  */
341 util.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     // TODO(rginda): Switch to v8Locale's number formatter when it's
360     // available.
361     return strf(u, n.toLocaleString());
362   };
363
364   var fmt = function(s, u) {
365     var rounded = Math.round(bytes / s * 10) / 10;
366     return str(rounded, u);
367   };
368
369   // Less than 1KB is displayed like '80 bytes'.
370   if (bytes < STEPS[1]) {
371     return str(bytes, UNITS[0]);
372   }
373
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]);
378   }
379
380   // This loop index is used outside the loop if it turns out |bytes|
381   // requires the largest unit.
382   var i;
383
384   for (i = 2 /* MB */; i < UNITS.length - 1; i++) {
385     if (bytes < STEPS[i + 1])
386       return fmt(STEPS[i], UNITS[i]);
387   }
388
389   return fmt(STEPS[i], UNITS[i]);
390 };
391
392 /**
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.
399  */
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));
405   };
406   fileReader.readAsArrayBuffer(file.slice(begin, end));
407 };
408
409 /**
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.
417  */
418 util.writeBlobToFile = function(entry, blob, onSuccess, onError) {
419   var truncate = function(writer) {
420     writer.onerror = onError;
421     writer.onwriteend = write.bind(null, writer);
422     writer.truncate(0);
423   };
424
425   var write = function(writer) {
426     writer.onwriteend = onSuccess;
427     writer.write(blob);
428   };
429
430   entry.createWriter(truncate, onError);
431 };
432
433 /**
434  * Returns a string '[Ctrl-][Alt-][Shift-][Meta-]' depending on the event
435  * modifiers. Convenient for writing out conditions in keyboard handlers.
436  *
437  * @param {Event} event The keyboard event.
438  * @return {string} Modifiers.
439  */
440 util.getKeyModifiers = function(event) {
441   return (event.ctrlKey ? 'Ctrl-' : '') +
442          (event.altKey ? 'Alt-' : '') +
443          (event.shiftKey ? 'Shift-' : '') +
444          (event.metaKey ? 'Meta-' : '');
445 };
446
447 /**
448  * @param {HTMLElement} element Element to transform.
449  * @param {Object} transform Transform object,
450  *                           contains scaleX, scaleY and rotate90 properties.
451  */
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)' :
457       '';
458 };
459
460 /**
461  * Makes filesystem: URL from the path.
462  * @param {string} path File or directory path.
463  * @return {string} URL.
464  */
465 util.makeFilesystemUrl = function(path) {
466   path = path.split('/').map(encodeURIComponent).join('/');
467   var prefix = 'external';
468   return 'filesystem:' + chrome.runtime.getURL(prefix + path);
469 };
470
471 /**
472  * Extracts path from filesystem: URL.
473  * @param {string} url Filesystem URL.
474  * @return {string} The path.
475  */
476 util.extractFilePath = function(url) {
477   var match =
478       /^filesystem:[\w-]*:\/\/[\w]*\/(external|persistent|temporary)(\/.*)$/.
479       exec(url);
480   var path = match && match[2];
481   if (!path) return null;
482   return decodeURIComponent(path);
483 };
484
485 /**
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.
489  *
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.
496  */
497 util.traverseTree = function(entry, callback, successCallback, errorCallback) {
498   if (!callback(entry)) {
499     successCallback();
500     return;
501   }
502
503   util.forEachDirEntry(
504       entry,
505       function(child, iterationCallback) {
506         util.traverseTree(child, callback, iterationCallback, errorCallback);
507       },
508       successCallback,
509       errorCallback);
510 };
511
512 /**
513  * A shortcut function to create a child element with given tag and class.
514  *
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.
519  */
520 util.createChild = function(parent, opt_className, opt_tag) {
521   var child = parent.ownerDocument.createElement(opt_tag || 'div');
522   if (opt_className)
523     child.className = opt_className;
524   parent.appendChild(child);
525   return child;
526 };
527
528 /**
529  * Updates the app state.
530  *
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.
537  */
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;
546   util.saveAppState();
547 };
548
549 /**
550  * Returns a translated string.
551  *
552  * Wrapper function to make dealing with translated strings more concise.
553  * Equivalent to loadTimeData.getString(id).
554  *
555  * @param {string} id The id of the string to return.
556  * @return {string} The translated string.
557  */
558 function str(id) {
559   return loadTimeData.getString(id);
560 }
561
562 /**
563  * Returns a translated string with arguments replaced.
564  *
565  * Wrapper function to make dealing with translated strings more concise.
566  * Equivalent to loadTimeData.getStringF(id, ...).
567  *
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.
571  */
572 function strf(id, var_args) {
573   return loadTimeData.getStringF.apply(loadTimeData, arguments);
574 }
575
576 /**
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.
580  */
581 util.platform = {
582   /**
583    * @return {boolean} True if Files.app is running as an open files or a select
584    *     folder dialog. False otherwise.
585    */
586   runningInBrowser: function() {
587     return !window.appID;
588   },
589
590   /**
591    * @param {function(Object)} callback Function accepting a preference map.
592    */
593   getPreferences: function(callback) {
594     chrome.storage.local.get(callback);
595   },
596
597   /**
598    * @param {string} key Preference name.
599    * @param {function(string)} callback Function accepting the preference value.
600    */
601   getPreference: function(key, callback) {
602     chrome.storage.local.get(key, function(items) {
603       callback(items[key]);
604     });
605   },
606
607   /**
608    * @param {string} key Preference name.
609    * @param {string|Object} value Preference value.
610    * @param {function()=} opt_callback Completion callback.
611    */
612   setPreference: function(key, value, opt_callback) {
613     if (typeof value != 'string')
614       value = JSON.stringify(value);
615
616     var items = {};
617     items[key] = value;
618     chrome.storage.local.set(items, opt_callback);
619   }
620 };
621
622 /**
623  * Attach page load handler.
624  * @param {function()} handler Application-specific load handler.
625  */
626 util.addPageLoadHandler = function(handler) {
627   document.addEventListener('DOMContentLoaded', function() {
628     handler();
629   });
630 };
631
632 /**
633  * Save app launch data to the local storage.
634  */
635 util.saveAppState = function() {
636   if (window.appState)
637     util.platform.setPreference(window.appID, window.appState);
638 };
639
640 /**
641  *  AppCache is a persistent timestamped key-value storage backed by
642  *  HTML5 local storage.
643  *
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.
647  *
648  *  TODO(kaznacheev) Reimplement this based on Indexed DB.
649  */
650 util.AppCache = function() {};
651
652 /**
653  * Local storage key.
654  */
655 util.AppCache.KEY = 'AppCache';
656
657 /**
658  * Max number of items.
659  */
660 util.AppCache.CAPACITY = 100;
661
662 /**
663  * Default lifetime.
664  */
665 util.AppCache.LIFETIME = 30 * 24 * 60 * 60 * 1000;  // 30 days.
666
667 /**
668  * @param {string} key Key.
669  * @param {function(number)} callback Callback accepting a value.
670  */
671 util.AppCache.getValue = function(key, callback) {
672   util.AppCache.read_(function(map) {
673     var entry = map[key];
674     callback(entry && entry.value);
675   });
676 };
677
678 /**
679  * Update the cache.
680  *
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).
684  */
685 util.AppCache.update = function(key, value, opt_lifetime) {
686   util.AppCache.read_(function(map) {
687     if (value != null) {
688       map[key] = {
689         value: value,
690         expire: Date.now() + (opt_lifetime || util.AppCache.LIFETIME)
691       };
692     } else if (key in map) {
693       delete map[key];
694     } else {
695       return;  // Nothing to do.
696     }
697     util.AppCache.cleanup_(map);
698     util.AppCache.write_(map);
699   });
700 };
701
702 /**
703  * @param {function(Object)} callback Callback accepting a map of timestamped
704  *   key-value pairs.
705  * @private
706  */
707 util.AppCache.read_ = function(callback) {
708   util.platform.getPreference(util.AppCache.KEY, function(json) {
709     if (json) {
710       try {
711         callback(JSON.parse(json));
712       } catch (e) {
713         // The local storage item somehow got messed up, start fresh.
714       }
715     }
716     callback({});
717   });
718 };
719
720 /**
721  * @param {Object} map A map of timestamped key-value pairs.
722  * @private
723  */
724 util.AppCache.write_ = function(map) {
725   util.platform.setPreference(util.AppCache.KEY, JSON.stringify(map));
726 };
727
728 /**
729  * Remove over-capacity and obsolete items.
730  *
731  * @param {Object} map A map of timestamped key-value pairs.
732  * @private
733  */
734 util.AppCache.cleanup_ = function(map) {
735   // Sort keys by ascending timestamps.
736   var keys = [];
737   for (var key in map) {
738     if (map.hasOwnProperty(key))
739       keys.push(key);
740   }
741   keys.sort(function(a, b) { return map[a].expire > map[b].expire; });
742
743   var cutoff = Date.now();
744
745   var obsolete = 0;
746   while (obsolete < keys.length &&
747          map[keys[obsolete]].expire < cutoff) {
748     obsolete++;
749   }
750
751   var overCapacity = Math.max(0, keys.length - util.AppCache.CAPACITY);
752
753   var itemsToDelete = Math.max(obsolete, overCapacity);
754   for (var i = 0; i != itemsToDelete; i++) {
755     delete map[keys[i]];
756   }
757 };
758
759 /**
760  * Load an image.
761  *
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
769  *     cache.
770  */
771 util.loadImage = function(image, url, opt_options, opt_isValid) {
772   return ImageLoaderClient.loadToImage(url,
773                                       image,
774                                       opt_options || {},
775                                       function() {},
776                                       function() { image.onerror(); },
777                                       opt_isValid);
778 };
779
780 /**
781  * Cancels loading an image.
782  * @param {number} taskId Task identifier returned by util.loadImage().
783  */
784 util.cancelLoadImage = function(taskId) {
785   ImageLoaderClient.getInstance().cancel(taskId);
786 };
787
788 /**
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.
793  */
794 util.findPropertyDescriptor = function(object, propertyName) {
795   for (var p = object; p; p = Object.getPrototypeOf(p)) {
796     var d = Object.getOwnPropertyDescriptor(p, propertyName);
797     if (d)
798       return d;
799   }
800   return null;
801 };
802
803 /**
804  * Calls inherited property setter (useful when property is
805  * overridden).
806  * @param {Object} object The object.
807  * @param {string} propertyName The property name.
808  * @param {*} value Value to set.
809  */
810 util.callInheritedSetter = function(object, propertyName, value) {
811   var d = util.findPropertyDescriptor(Object.getPrototypeOf(object),
812                                       propertyName);
813   d.set.call(object, value);
814 };
815
816 /**
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.
822  */
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;
829 };
830
831 /**
832  * Adds an isFocused method to the current window object.
833  */
834 util.addIsFocusedMethod = function() {
835   var focused = true;
836
837   window.addEventListener('focus', function() {
838     focused = true;
839   });
840
841   window.addEventListener('blur', function() {
842     focused = false;
843   });
844
845   /**
846    * @return {boolean} True if focused.
847    */
848   window.isFocused = function() {
849     return focused;
850   };
851 };
852
853 /**
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.
858  */
859 util.redirectMainWindow = function(id, url) {
860   // TODO(mtomasz): Implement this for Apps V2, once the photo importer is
861   // restored.
862   return false;
863 };
864
865 /**
866  * Checks, if the Files.app's window is in a full screen mode.
867  *
868  * @param {AppWindow} appWindow App window to be maximized.
869  * @return {boolean} True if the full screen mode is enabled.
870  */
871 util.isFullScreen = function(appWindow) {
872   if (appWindow) {
873     return appWindow.isFullscreen();
874   } else {
875     console.error('App window not passed. Unable to check status of ' +
876                   'the full screen mode.');
877     return false;
878   }
879 };
880
881 /**
882  * Toggles the full screen mode.
883  *
884  * @param {AppWindow} appWindow App window to be maximized.
885  * @param {boolean} enabled True for enabling, false for disabling.
886  */
887 util.toggleFullScreen = function(appWindow, enabled) {
888   if (appWindow) {
889     if (enabled)
890       appWindow.fullscreen();
891     else
892       appWindow.restore();
893     return;
894   }
895
896   console.error(
897       'App window not passed. Unable to toggle the full screen mode.');
898 };
899
900 /**
901  * The type of a file operation.
902  * @enum {string}
903  * @const
904  */
905 util.FileOperationType = Object.freeze({
906   COPY: 'COPY',
907   MOVE: 'MOVE',
908   ZIP: 'ZIP',
909 });
910
911 /**
912  * The type of a file operation error.
913  * @enum {number}
914  * @const
915  */
916 util.FileOperationErrorType = Object.freeze({
917   UNEXPECTED_SOURCE_FILE: 0,
918   TARGET_EXISTS: 1,
919   FILESYSTEM_ERROR: 2,
920 });
921
922 /**
923  * The kind of an entry changed event.
924  * @enum {number}
925  * @const
926  */
927 util.EntryChangedKind = Object.freeze({
928   CREATED: 0,
929   DELETED: 1,
930 });
931
932 /**
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.
936  */
937 util.isFakeEntry = function(entry) {
938   return !('getParent' in entry);
939 };
940
941 /**
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.
944  *
945  * TODO(uekawa): remove reference to FileError.
946  *
947  * @param {string} name Error name for the file error.
948  * @return {UserDOMError} FileError instance
949  */
950 util.createDOMError = function(name) {
951   return new util.UserDOMError(name);
952 };
953
954 /**
955  * Creates a DOMError-like object to be used in place of returning file errors.
956  *
957  * @param {string} name Error name for the file error.
958  * @constructor
959  */
960 util.UserDOMError = function(name) {
961   /**
962    * @type {string}
963    * @private
964    */
965   this.name_ = name;
966   Object.freeze(this);
967 };
968
969 util.UserDOMError.prototype = {
970   /**
971    * @return {string} File error name.
972    */
973   get name() {
974     return this.name_;
975   }
976 };
977
978 /**
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.
984  */
985 util.isSameEntry = function(entry1, entry2) {
986   if (!entry1 && !entry2)
987     return true;
988   if (!entry1 || !entry2)
989     return false;
990   return entry1.toURL() === entry2.toURL();
991 };
992
993 /**
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.
999  */
1000 util.isSameFileSystem = function(fileSystem1, fileSystem2) {
1001   if (!fileSystem1 && !fileSystem2)
1002     return true;
1003   if (!fileSystem1 || !fileSystem2)
1004     return false;
1005   return util.isSameEntry(fileSystem1.root, fileSystem2.root);
1006 };
1007
1008 /**
1009  * Collator for sorting.
1010  * @type {Intl.Collator}
1011  */
1012 util.collator = new Intl.Collator([], {usage: 'sort',
1013                                        numeric: true,
1014                                        sensitivity: 'base'});
1015
1016 /**
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.
1021  */
1022 util.compareName = function(entry1, entry2) {
1023   return util.collator.compare(entry1.name, entry2.name);
1024 };
1025
1026 /**
1027  * Compare by path.
1028  * @param {Entry} entry1 First entry.
1029  * @param {Entry} entry2 Second entry.
1030  * @return {number} Compare result.
1031  */
1032 util.comparePath = function(entry1, entry2) {
1033   return util.collator.compare(entry1.fullPath, entry2.fullPath);
1034 };
1035
1036 /**
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.
1039  *
1040  * @param {DirectoryEntry|Object} ancestorEntry The ancestor directory entry.
1041  *     Can be a fake.
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.
1044  */
1045 util.isDescendantEntry = function(ancestorEntry, childEntry) {
1046   if (!ancestorEntry.isDirectory)
1047     return false;
1048   if (!util.isSameFileSystem(ancestorEntry.filesystem, childEntry.filesystem))
1049     return false;
1050   if (util.isSameEntry(ancestorEntry, childEntry))
1051     return false;
1052   if (util.isFakeEntry(ancestorEntry) || util.isFakeEntry(childEntry))
1053     return false;
1054
1055   // Check if the ancestor's path with trailing slash is a prefix of child's
1056   // path.
1057   var ancestorPath = ancestorEntry.fullPath;
1058   if (ancestorPath.slice(-1) !== '/')
1059     ancestorPath += '/';
1060   return childEntry.fullPath.indexOf(ancestorPath) === 0;
1061 };
1062
1063 /**
1064  * Visit the URL.
1065  *
1066  * If the browser is opening, the url is opened in a new tag, otherwise the url
1067  * is opened in a new window.
1068  *
1069  * @param {string} url URL to visit.
1070  */
1071 util.visitURL = function(url) {
1072   window.open(url);
1073 };
1074
1075 /**
1076  * Returns normalized current locale, or default locale - 'en'.
1077  * @return {string} Current locale
1078  */
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';
1083 };
1084
1085 /**
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.
1089  */
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();
1096   });
1097 };
1098
1099 /**
1100  * Converts array of URLs to an array of corresponding Entries.
1101  *
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.
1107  */
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};
1117         });
1118   });
1119   var resultPromise = Promise.all(promises).then(function(results) {
1120     var entries = [];
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);
1127       }
1128     }
1129     return {
1130       entries: entries,
1131       failureUrls: failureUrls
1132     };
1133   });
1134
1135   // Invoke the callback. If opt_callback is specified, resultPromise is still
1136   // returned and fulfilled with a result.
1137   if (opt_callback) {
1138     resultPromise.then(function(result) {
1139       opt_callback(result.entries, result.failureUrls);
1140     }).
1141     catch(function(error) {
1142       console.error(
1143           'util.URLsToEntries is failed.',
1144           error.stack ? error.stack : error);
1145     });
1146   }
1147
1148   return resultPromise;
1149 };
1150
1151 /**
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.
1155  */
1156 util.isTeleported = function(window) {
1157   return new Promise(function(onFulfilled) {
1158     window.chrome.fileBrowserPrivate.getProfiles(function(profiles,
1159                                                           currentId,
1160                                                           displayedId) {
1161       onFulfilled(currentId !== displayedId);
1162     });
1163   });
1164 };
1165
1166 /**
1167  * Sets up and shows the alert to inform a user the task is opened in the
1168  * desktop of the running profile.
1169  *
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.
1173  */
1174 util.showOpenInOtherDesktopAlert = function(alertDialog, entries) {
1175   if (!entries.length)
1176     return;
1177   chrome.fileBrowserPrivate.getProfiles(function(profiles,
1178                                                  currentId,
1179                                                  displayedId) {
1180     // Find strings.
1181     var displayName;
1182     for (var i = 0; i < profiles.length; i++) {
1183       if (profiles[i].profileId === currentId) {
1184         displayName = profiles[i].displayName;
1185         break;
1186       }
1187     }
1188     if (!displayName) {
1189       console.warn('Display name is not found.');
1190       return;
1191     }
1192
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',
1198                        displayName,
1199                        currentId);
1200
1201     // Show the dialog.
1202     alertDialog.showWithTitle(title, message);
1203   }.bind(this));
1204 };
1205
1206 /**
1207  * Runs chrome.test.sendMessage in test environment. Does nothing if running
1208  * in production environment.
1209  *
1210  * @param {string} message Test message to send.
1211  */
1212 util.testSendMessage = function(message) {
1213   var test = chrome.test || window.top.chrome.test;
1214   if (test)
1215     test.sendMessage(message);
1216 };
1217
1218 /**
1219  * Returns the localized name for the root type. If not available, then returns
1220  * null.
1221  *
1222  * @param {VolumeManagerCommon.RootType} rootType The root type.
1223  * @return {?string} The localized name, or null if not available.
1224  */
1225 util.getRootTypeLabel = function(rootType) {
1226   var str = function(id) {
1227     return loadTimeData.getString(id);
1228   };
1229
1230   switch (rootType) {
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');
1241   }
1242
1243   // Translation not found.
1244   return null;
1245 };
1246
1247 /**
1248  * Extracts the extension of the path.
1249  *
1250  * Examples:
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', '']
1256  *
1257  * @param {string} path Path to be extracted.
1258  * @return {Array.<string>} Filename and extension of the given path.
1259  */
1260 util.splitExtension = function(path) {
1261   var dotPosition = path.lastIndexOf('.');
1262   if (dotPosition <= path.lastIndexOf('/'))
1263     dotPosition = -1;
1264
1265   var filename = dotPosition != -1 ? path.substr(0, dotPosition) : path;
1266   var extension = dotPosition != -1 ? path.substr(dotPosition) : '';
1267   return [filename, extension];
1268 };
1269
1270 /**
1271  * Returns the localized name of the entry.
1272  *
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.
1276  */
1277 util.getEntryLabel = function(volumeManager, entry) {
1278   var locationInfo = volumeManager.getLocationInfo(entry);
1279
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;
1299       default:
1300         console.error('Unsupported root type: ' + locationInfo.rootType);
1301         return locationInfo.volumeInfo.label;
1302     }
1303   }
1304
1305   return entry.name;
1306 };
1307
1308 /**
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
1311  *
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|.
1315  */
1316 util.isDropEffectAllowed = function(effectAllowed, dropEffect) {
1317   return effectAllowed === 'all' ||
1318       effectAllowed.toLowerCase().indexOf(dropEffect) !== -1;
1319 };
1320
1321 /**
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.
1328  *
1329  * It also verifies if the name length is in the limit of the filesystem.
1330  *
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
1334  *     or not.
1335  * @return {Promise} Promise fulfilled on success, or rejected with the error
1336  *     message.
1337  */
1338 util.validateFileName = function(parentEntry, name, filterHiddenOn) {
1339   var testResult = /[\/\\\<\>\:\?\*\"\|]/.exec(name);
1340   var msg;
1341   if (testResult)
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'));
1349
1350   return new Promise(function(fulfill, reject) {
1351     chrome.fileBrowserPrivate.validatePathNameLength(
1352         parentEntry.toURL(),
1353         name,
1354         function(valid) {
1355           if (valid)
1356             fulfill();
1357           else
1358             reject(str('ERROR_LONG_NAME'));
1359         });
1360   });
1361 };