- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / 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  * Install a sensible toString() on the FileError object.
45  *
46  * FileError.prototype.code is a numeric code describing the cause of the
47  * error.  The FileError constructor has a named property for each possible
48  * error code, but provides no way to map the code to the named property.
49  * This toString() implementation fixes that.
50  */
51 util.installFileErrorToString = function() {
52   FileError.prototype.toString = function() {
53     return '[object FileError: ' + util.getFileErrorMnemonic(this.code) + ']';
54   };
55 };
56
57 /**
58  * @param {number} code The file error code.
59  * @return {string} The file error mnemonic.
60  */
61 util.getFileErrorMnemonic = function(code) {
62   for (var key in FileError) {
63     if (key.search(/_ERR$/) != -1 && FileError[key] == code)
64       return key;
65   }
66
67   return code;
68 };
69
70 /**
71  * @param {number} code File error code (from FileError object).
72  * @return {string} Translated file error string.
73  */
74 util.getFileErrorString = function(code) {
75   for (var key in FileError) {
76     var match = /(.*)_ERR$/.exec(key);
77     if (match && FileError[key] == code) {
78       // This would convert 1 to 'NOT_FOUND'.
79       code = match[1];
80       break;
81     }
82   }
83   console.warn('File error: ' + code);
84   return loadTimeData.getString('FILE_ERROR_' + code) ||
85       loadTimeData.getString('FILE_ERROR_GENERIC');
86 };
87
88 /**
89  * @param {string} str String to escape.
90  * @return {string} Escaped string.
91  */
92 util.htmlEscape = function(str) {
93   return str.replace(/[<>&]/g, function(entity) {
94     switch (entity) {
95       case '<': return '&lt;';
96       case '>': return '&gt;';
97       case '&': return '&amp;';
98     }
99   });
100 };
101
102 /**
103  * @param {string} str String to unescape.
104  * @return {string} Unescaped string.
105  */
106 util.htmlUnescape = function(str) {
107   return str.replace(/&(lt|gt|amp);/g, function(entity) {
108     switch (entity) {
109       case '&lt;': return '<';
110       case '&gt;': return '>';
111       case '&amp;': return '&';
112     }
113   });
114 };
115
116 /**
117  * Iterates the entries contained by dirEntry, and invokes callback once for
118  * each entry. On completion, successCallback will be invoked.
119  *
120  * @param {DirectoryEntry} dirEntry The entry of the directory.
121  * @param {function(Entry, function())} callback Invoked for each entry.
122  * @param {function()} successCallback Invoked on completion.
123  * @param {function(FileError)} errorCallback Invoked if an error is found on
124  *     directory entry reading.
125  */
126 util.forEachDirEntry = function(
127     dirEntry, callback, successCallback, errorCallback) {
128   var reader = dirEntry.createReader();
129   var iterate = function() {
130     reader.readEntries(function(entries) {
131       if (entries.length == 0) {
132         successCallback();
133         return;
134       }
135
136       AsyncUtil.forEach(
137           entries,
138           function(forEachCallback, entry) {
139             // Do not pass index nor entries.
140             callback(entry, forEachCallback);
141           },
142           iterate);
143     }, errorCallback);
144   };
145   iterate();
146 };
147
148 /**
149  * Reads contents of directory.
150  * @param {DirectoryEntry} root Root entry.
151  * @param {string} path Directory path.
152  * @param {function(Array.<Entry>)} callback List of entries passed to callback.
153  */
154 util.readDirectory = function(root, path, callback) {
155   var onError = function(e) {
156     callback([], e);
157   };
158   root.getDirectory(path, {create: false}, function(entry) {
159     var reader = entry.createReader();
160     var r = [];
161     var readNext = function() {
162       reader.readEntries(function(results) {
163         if (results.length == 0) {
164           callback(r, null);
165           return;
166         }
167         r.push.apply(r, results);
168         readNext();
169       }, onError);
170     };
171     readNext();
172   }, onError);
173 };
174
175 /**
176  * Utility function to resolve multiple directories with a single call.
177  *
178  * The successCallback will be invoked once for each directory object
179  * found.  The errorCallback will be invoked once for each
180  * path that could not be resolved.
181  *
182  * The successCallback is invoked with a null entry when all paths have
183  * been processed.
184  *
185  * @param {DirEntry} dirEntry The base directory.
186  * @param {Object} params The parameters to pass to the underlying
187  *     getDirectory calls.
188  * @param {Array.<string>} paths The list of directories to resolve.
189  * @param {function(!DirEntry)} successCallback The function to invoke for
190  *     each DirEntry found.  Also invoked once with null at the end of the
191  *     process.
192  * @param {function(FileError)} errorCallback The function to invoke
193  *     for each path that cannot be resolved.
194  */
195 util.getDirectories = function(dirEntry, params, paths, successCallback,
196                                errorCallback) {
197
198   // Copy the params array, since we're going to destroy it.
199   params = [].slice.call(params);
200
201   var onComplete = function() {
202     successCallback(null);
203   };
204
205   var getNextDirectory = function() {
206     var path = paths.shift();
207     if (!path)
208       return onComplete();
209
210     dirEntry.getDirectory(
211       path, params,
212       function(entry) {
213         successCallback(entry);
214         getNextDirectory();
215       },
216       function(err) {
217         errorCallback(err);
218         getNextDirectory();
219       });
220   };
221
222   getNextDirectory();
223 };
224
225 /**
226  * Utility function to resolve multiple files with a single call.
227  *
228  * The successCallback will be invoked once for each directory object
229  * found.  The errorCallback will be invoked once for each
230  * path that could not be resolved.
231  *
232  * The successCallback is invoked with a null entry when all paths have
233  * been processed.
234  *
235  * @param {DirEntry} dirEntry The base directory.
236  * @param {Object} params The parameters to pass to the underlying
237  *     getFile calls.
238  * @param {Array.<string>} paths The list of files to resolve.
239  * @param {function(!FileEntry)} successCallback The function to invoke for
240  *     each FileEntry found.  Also invoked once with null at the end of the
241  *     process.
242  * @param {function(FileError)} errorCallback The function to invoke
243  *     for each path that cannot be resolved.
244  */
245 util.getFiles = function(dirEntry, params, paths, successCallback,
246                          errorCallback) {
247   // Copy the params array, since we're going to destroy it.
248   params = [].slice.call(params);
249
250   var onComplete = function() {
251     successCallback(null);
252   };
253
254   var getNextFile = function() {
255     var path = paths.shift();
256     if (!path)
257       return onComplete();
258
259     dirEntry.getFile(
260       path, params,
261       function(entry) {
262         successCallback(entry);
263         getNextFile();
264       },
265       function(err) {
266         errorCallback(err);
267         getNextFile();
268       });
269   };
270
271   getNextFile();
272 };
273
274 /**
275  * Resolve a path to either a DirectoryEntry or a FileEntry, regardless of
276  * whether the path is a directory or file.
277  *
278  * @param {DirectoryEntry} root The root of the filesystem to search.
279  * @param {string} path The path to be resolved.
280  * @param {function(Entry)} resultCallback Called back when a path is
281  *     successfully resolved. Entry will be either a DirectoryEntry or
282  *     a FileEntry.
283  * @param {function(FileError)} errorCallback Called back if an unexpected
284  *     error occurs while resolving the path.
285  */
286 util.resolvePath = function(root, path, resultCallback, errorCallback) {
287   if (path == '' || path == '/') {
288     resultCallback(root);
289     return;
290   }
291
292   root.getFile(
293       path, {create: false},
294       resultCallback,
295       function(err) {
296         if (err.code == FileError.TYPE_MISMATCH_ERR) {
297           // Bah.  It's a directory, ask again.
298           root.getDirectory(
299               path, {create: false},
300               resultCallback,
301               errorCallback);
302         } else {
303           errorCallback(err);
304         }
305       });
306 };
307
308 /**
309  * Locate the file referred to by path, creating directories or the file
310  * itself if necessary.
311  * @param {DirEntry} root The root entry.
312  * @param {string} path The file path.
313  * @param {function(FileEntry)} successCallback The callback.
314  * @param {function(FileError)} errorCallback The callback.
315  */
316 util.getOrCreateFile = function(root, path, successCallback, errorCallback) {
317   var dirname = null;
318   var basename = null;
319
320   var onDirFound = function(dirEntry) {
321     dirEntry.getFile(basename, { create: true },
322                      successCallback, errorCallback);
323   };
324
325   var i = path.lastIndexOf('/');
326   if (i > -1) {
327     dirname = path.substr(0, i);
328     basename = path.substr(i + 1);
329   } else {
330     basename = path;
331   }
332
333   if (!dirname) {
334     onDirFound(root);
335     return;
336   }
337
338   util.getOrCreateDirectory(root, dirname, onDirFound, errorCallback);
339 };
340
341 /**
342  * Locate the directory referred to by path, creating directories along the
343  * way.
344  * @param {DirEntry} root The root entry.
345  * @param {string} path The directory path.
346  * @param {function(FileEntry)} successCallback The callback.
347  * @param {function(FileError)} errorCallback The callback.
348  */
349 util.getOrCreateDirectory = function(root, path, successCallback,
350                                      errorCallback) {
351   var names = path.split('/');
352
353   var getOrCreateNextName = function(dir) {
354     if (!names.length)
355       return successCallback(dir);
356
357     var name;
358     do {
359       name = names.shift();
360     } while (!name || name == '.');
361
362     dir.getDirectory(name, { create: true }, getOrCreateNextName,
363                      errorCallback);
364   };
365
366   getOrCreateNextName(root);
367 };
368
369 /**
370  * Renames the entry to newName.
371  * @param {Entry} entry The entry to be renamed.
372  * @param {string} newName The new name.
373  * @param {function(Entry)} successCallback Callback invoked when the rename
374  *     is successfully done.
375  * @param {function(FileError)} errorCallback Callback invoked when an error
376  *     is found.
377  */
378 util.rename = function(entry, newName, successCallback, errorCallback) {
379   entry.getParent(function(parent) {
380     // Before moving, we need to check if there is an existing entry at
381     // parent/newName, since moveTo will overwrite it.
382     // Note that this way has some timing issue. After existing check,
383     // a new entry may be create on background. However, there is no way not to
384     // overwrite the existing file, unfortunately. The risk should be low,
385     // assuming the unsafe period is very short.
386     (entry.isFile ? parent.getFile : parent.getDirectory).call(
387         parent, newName, {create: false},
388         function(entry) {
389           // The entry with the name already exists.
390           errorCallback(util.createFileError(FileError.PATH_EXISTS_ERR));
391         },
392         function(error) {
393           if (error.code != FileError.NOT_FOUND_ERR) {
394             // Unexpected error is found.
395             errorCallback(error);
396             return;
397           }
398
399           // No existing entry is found.
400           entry.moveTo(parent, newName, successCallback, errorCallback);
401         });
402   }, errorCallback);
403 };
404
405 /**
406  * Remove a file or a directory.
407  * @param {Entry} entry The entry to remove.
408  * @param {function()} onSuccess The success callback.
409  * @param {function(FileError)} onError The error callback.
410  */
411 util.removeFileOrDirectory = function(entry, onSuccess, onError) {
412   if (entry.isDirectory)
413     entry.removeRecursively(onSuccess, onError);
414   else
415     entry.remove(onSuccess, onError);
416 };
417
418 /**
419  * Checks if an entry exists at |relativePath| in |dirEntry|.
420  * If exists, tries to deduplicate the path by inserting parenthesized number,
421  * such as " (1)", before the extension. If it still exists, tries the
422  * deduplication again by increasing the number up to 10 times.
423  * For example, suppose "file.txt" is given, "file.txt", "file (1).txt",
424  * "file (2).txt", ..., "file (9).txt" will be tried.
425  *
426  * @param {DirectoryEntry} dirEntry The target directory entry.
427  * @param {string} relativePath The path to be deduplicated.
428  * @param {function(string)} onSuccess Called with the deduplicated path on
429  *     success.
430  * @param {function(FileError)} onError Called on error.
431  */
432 util.deduplicatePath = function(dirEntry, relativePath, onSuccess, onError) {
433   // The trial is up to 10.
434   var MAX_RETRY = 10;
435
436   // Crack the path into three part. The parenthesized number (if exists) will
437   // be replaced by incremented number for retry. For example, suppose
438   // |relativePath| is "file (10).txt", the second check path will be
439   // "file (11).txt".
440   var match = /^(.*?)(?: \((\d+)\))?(\.[^.]*?)?$/.exec(relativePath);
441   var prefix = match[1];
442   var copyNumber = match[2] ? parseInt(match[2], 10) : 0;
443   var ext = match[3] ? match[3] : '';
444
445   // The path currently checking the existence.
446   var trialPath = relativePath;
447
448   var onNotResolved = function(err) {
449     // We expect to be unable to resolve the target file, since we're going
450     // to create it during the copy.  However, if the resolve fails with
451     // anything other than NOT_FOUND, that's trouble.
452     if (err.code != FileError.NOT_FOUND_ERR) {
453       onError(err);
454       return;
455     }
456
457     // Found a path that doesn't exist.
458     onSuccess(trialPath);
459   }
460
461   var numRetry = MAX_RETRY;
462   var onResolved = function(entry) {
463     if (--numRetry == 0) {
464       // Hit the limit of the number of retrial.
465       // Note that we cannot create FileError object directly, so here we use
466       // Object.create instead.
467       onError(util.createFileError(FileError.PATH_EXISTS_ERR));
468       return;
469     }
470
471     ++copyNumber;
472     trialPath = prefix + ' (' + copyNumber + ')' + ext;
473     util.resolvePath(dirEntry, trialPath, onResolved, onNotResolved);
474   };
475
476   // Check to see if the target exists.
477   util.resolvePath(dirEntry, trialPath, onResolved, onNotResolved);
478 };
479
480 /**
481  * Convert a number of bytes into a human friendly format, using the correct
482  * number separators.
483  *
484  * @param {number} bytes The number of bytes.
485  * @return {string} Localized string.
486  */
487 util.bytesToString = function(bytes) {
488   // Translation identifiers for size units.
489   var UNITS = ['SIZE_BYTES',
490                'SIZE_KB',
491                'SIZE_MB',
492                'SIZE_GB',
493                'SIZE_TB',
494                'SIZE_PB'];
495
496   // Minimum values for the units above.
497   var STEPS = [0,
498                Math.pow(2, 10),
499                Math.pow(2, 20),
500                Math.pow(2, 30),
501                Math.pow(2, 40),
502                Math.pow(2, 50)];
503
504   var str = function(n, u) {
505     // TODO(rginda): Switch to v8Locale's number formatter when it's
506     // available.
507     return strf(u, n.toLocaleString());
508   };
509
510   var fmt = function(s, u) {
511     var rounded = Math.round(bytes / s * 10) / 10;
512     return str(rounded, u);
513   };
514
515   // Less than 1KB is displayed like '80 bytes'.
516   if (bytes < STEPS[1]) {
517     return str(bytes, UNITS[0]);
518   }
519
520   // Up to 1MB is displayed as rounded up number of KBs.
521   if (bytes < STEPS[2]) {
522     var rounded = Math.ceil(bytes / STEPS[1]);
523     return str(rounded, UNITS[1]);
524   }
525
526   // This loop index is used outside the loop if it turns out |bytes|
527   // requires the largest unit.
528   var i;
529
530   for (i = 2 /* MB */; i < UNITS.length - 1; i++) {
531     if (bytes < STEPS[i + 1])
532       return fmt(STEPS[i], UNITS[i]);
533   }
534
535   return fmt(STEPS[i], UNITS[i]);
536 };
537
538 /**
539  * Utility function to read specified range of bytes from file
540  * @param {File} file The file to read.
541  * @param {number} begin Starting byte(included).
542  * @param {number} end Last byte(excluded).
543  * @param {function(File, Uint8Array)} callback Callback to invoke.
544  * @param {function(FileError)} onError Error handler.
545  */
546 util.readFileBytes = function(file, begin, end, callback, onError) {
547   var fileReader = new FileReader();
548   fileReader.onerror = onError;
549   fileReader.onloadend = function() {
550     callback(file, new ByteReader(fileReader.result));
551   };
552   fileReader.readAsArrayBuffer(file.slice(begin, end));
553 };
554
555 /**
556  * Write a blob to a file.
557  * Truncates the file first, so the previous content is fully overwritten.
558  * @param {FileEntry} entry File entry.
559  * @param {Blob} blob The blob to write.
560  * @param {function(Event)} onSuccess Completion callback. The first argument is
561  *     a 'writeend' event.
562  * @param {function(FileError)} onError Error handler.
563  */
564 util.writeBlobToFile = function(entry, blob, onSuccess, onError) {
565   var truncate = function(writer) {
566     writer.onerror = onError;
567     writer.onwriteend = write.bind(null, writer);
568     writer.truncate(0);
569   };
570
571   var write = function(writer) {
572     writer.onwriteend = onSuccess;
573     writer.write(blob);
574   };
575
576   entry.createWriter(truncate, onError);
577 };
578
579 /**
580  * Returns a string '[Ctrl-][Alt-][Shift-][Meta-]' depending on the event
581  * modifiers. Convenient for writing out conditions in keyboard handlers.
582  *
583  * @param {Event} event The keyboard event.
584  * @return {string} Modifiers.
585  */
586 util.getKeyModifiers = function(event) {
587   return (event.ctrlKey ? 'Ctrl-' : '') +
588          (event.altKey ? 'Alt-' : '') +
589          (event.shiftKey ? 'Shift-' : '') +
590          (event.metaKey ? 'Meta-' : '');
591 };
592
593 /**
594  * @param {HTMLElement} element Element to transform.
595  * @param {Object} transform Transform object,
596  *                           contains scaleX, scaleY and rotate90 properties.
597  */
598 util.applyTransform = function(element, transform) {
599   element.style.webkitTransform =
600       transform ? 'scaleX(' + transform.scaleX + ') ' +
601                   'scaleY(' + transform.scaleY + ') ' +
602                   'rotate(' + transform.rotate90 * 90 + 'deg)' :
603       '';
604 };
605
606 /**
607  * Makes filesystem: URL from the path.
608  * @param {string} path File or directory path.
609  * @return {string} URL.
610  */
611 util.makeFilesystemUrl = function(path) {
612   path = path.split('/').map(encodeURIComponent).join('/');
613   var prefix = 'external';
614   return 'filesystem:' + chrome.runtime.getURL(prefix + path);
615 };
616
617 /**
618  * Extracts path from filesystem: URL.
619  * @param {string} url Filesystem URL.
620  * @return {string} The path.
621  */
622 util.extractFilePath = function(url) {
623   var match =
624       /^filesystem:[\w-]*:\/\/[\w]*\/(external|persistent|temporary)(\/.*)$/.
625       exec(url);
626   var path = match && match[2];
627   if (!path) return null;
628   return decodeURIComponent(path);
629 };
630
631 /**
632  * Traverses a directory tree whose root is the given entry, and invokes
633  * callback for each entry. Upon completion, successCallback will be called.
634  * On error, errorCallback will be called.
635  *
636  * @param {Entry} entry The root entry.
637  * @param {function(Entry):boolean} callback Callback invoked for each entry.
638  *     If this returns false, entries under it won't be traversed. Note that
639  *     its siblings (and their children) will be still traversed.
640  * @param {function()} successCallback Called upon successful completion.
641  * @param {function(error)} errorCallback Called upon error.
642  */
643 util.traverseTree = function(entry, callback, successCallback, errorCallback) {
644   if (!callback(entry)) {
645     successCallback();
646     return;
647   }
648
649   util.forEachDirEntry(
650       entry,
651       function(child, iterationCallback) {
652         util.traverseTree(child, callback, iterationCallback, errorCallback);
653       },
654       successCallback,
655       errorCallback);
656 };
657
658 /**
659  * A shortcut function to create a child element with given tag and class.
660  *
661  * @param {HTMLElement} parent Parent element.
662  * @param {string=} opt_className Class name.
663  * @param {string=} opt_tag Element tag, DIV is omitted.
664  * @return {Element} Newly created element.
665  */
666 util.createChild = function(parent, opt_className, opt_tag) {
667   var child = parent.ownerDocument.createElement(opt_tag || 'div');
668   if (opt_className)
669     child.className = opt_className;
670   parent.appendChild(child);
671   return child;
672 };
673
674 /**
675  * Update the app state.
676  *
677  * @param {string} path Path to be put in the address bar after the hash.
678  *   If null the hash is left unchanged.
679  * @param {string|Object=} opt_param Search parameter. Used directly if string,
680  *   stringified if object. If omitted the search query is left unchanged.
681  */
682 util.updateAppState = function(path, opt_param) {
683   window.appState = window.appState || {};
684   if (typeof opt_param == 'string')
685     window.appState.params = {};
686   else if (typeof opt_param == 'object')
687     window.appState.params = opt_param;
688   if (path)
689     window.appState.defaultPath = path;
690   util.saveAppState();
691   return;
692 };
693
694 /**
695  * Return a translated string.
696  *
697  * Wrapper function to make dealing with translated strings more concise.
698  * Equivalent to loadTimeData.getString(id).
699  *
700  * @param {string} id The id of the string to return.
701  * @return {string} The translated string.
702  */
703 function str(id) {
704   return loadTimeData.getString(id);
705 }
706
707 /**
708  * Return a translated string with arguments replaced.
709  *
710  * Wrapper function to make dealing with translated strings more concise.
711  * Equivalent to loadTimeData.getStringF(id, ...).
712  *
713  * @param {string} id The id of the string to return.
714  * @param {...string} var_args The values to replace into the string.
715  * @return {string} The translated string with replaced values.
716  */
717 function strf(id, var_args) {
718   return loadTimeData.getStringF.apply(loadTimeData, arguments);
719 }
720
721 /**
722  * Adapter object that abstracts away the the difference between Chrome app APIs
723  * v1 and v2. Is only necessary while the migration to v2 APIs is in progress.
724  * TODO(mtomasz): Clean up this. crbug.com/240606.
725  */
726 util.platform = {
727   /**
728    * @return {boolean} True if Files.app is running as an open files or a select
729    *     folder dialog. False otherwise.
730    */
731   runningInBrowser: function() {
732     return !window.appID;
733   },
734
735   /**
736    * @param {function(Object)} callback Function accepting a preference map.
737    */
738   getPreferences: function(callback) {
739     chrome.storage.local.get(callback);
740   },
741
742   /**
743    * @param {string} key Preference name.
744    * @param {function(string)} callback Function accepting the preference value.
745    */
746   getPreference: function(key, callback) {
747     chrome.storage.local.get(key, function(items) {
748       callback(items[key]);
749     });
750   },
751
752   /**
753    * @param {string} key Preference name.
754    * @param {string|Object} value Preference value.
755    * @param {function()=} opt_callback Completion callback.
756    */
757   setPreference: function(key, value, opt_callback) {
758     if (typeof value != 'string')
759       value = JSON.stringify(value);
760
761     var items = {};
762     items[key] = value;
763     chrome.storage.local.set(items, opt_callback);
764   }
765 };
766
767 /**
768  * Attach page load handler.
769  * @param {function()} handler Application-specific load handler.
770  */
771 util.addPageLoadHandler = function(handler) {
772   document.addEventListener('DOMContentLoaded', function() {
773     handler();
774   });
775 };
776
777 /**
778  * Save app launch data to the local storage.
779  */
780 util.saveAppState = function() {
781   if (window.appState)
782     util.platform.setPreference(window.appID, window.appState);
783 };
784
785 /**
786  *  AppCache is a persistent timestamped key-value storage backed by
787  *  HTML5 local storage.
788  *
789  *  It is not designed for frequent access. In order to avoid costly
790  *  localStorage iteration all data is kept in a single localStorage item.
791  *  There is no in-memory caching, so concurrent access is _almost_ safe.
792  *
793  *  TODO(kaznacheev) Reimplement this based on Indexed DB.
794  */
795 util.AppCache = function() {};
796
797 /**
798  * Local storage key.
799  */
800 util.AppCache.KEY = 'AppCache';
801
802 /**
803  * Max number of items.
804  */
805 util.AppCache.CAPACITY = 100;
806
807 /**
808  * Default lifetime.
809  */
810 util.AppCache.LIFETIME = 30 * 24 * 60 * 60 * 1000;  // 30 days.
811
812 /**
813  * @param {string} key Key.
814  * @param {function(number)} callback Callback accepting a value.
815  */
816 util.AppCache.getValue = function(key, callback) {
817   util.AppCache.read_(function(map) {
818     var entry = map[key];
819     callback(entry && entry.value);
820   });
821 };
822
823 /**
824  * Update the cache.
825  *
826  * @param {string} key Key.
827  * @param {string} value Value. Remove the key if value is null.
828  * @param {number=} opt_lifetime Maximum time to keep an item (in milliseconds).
829  */
830 util.AppCache.update = function(key, value, opt_lifetime) {
831   util.AppCache.read_(function(map) {
832     if (value != null) {
833       map[key] = {
834         value: value,
835         expire: Date.now() + (opt_lifetime || util.AppCache.LIFETIME)
836       };
837     } else if (key in map) {
838       delete map[key];
839     } else {
840       return;  // Nothing to do.
841     }
842     util.AppCache.cleanup_(map);
843     util.AppCache.write_(map);
844   });
845 };
846
847 /**
848  * @param {function(Object)} callback Callback accepting a map of timestamped
849  *   key-value pairs.
850  * @private
851  */
852 util.AppCache.read_ = function(callback) {
853   util.platform.getPreference(util.AppCache.KEY, function(json) {
854     if (json) {
855       try {
856         callback(JSON.parse(json));
857       } catch (e) {
858         // The local storage item somehow got messed up, start fresh.
859       }
860     }
861     callback({});
862   });
863 };
864
865 /**
866  * @param {Object} map A map of timestamped key-value pairs.
867  * @private
868  */
869 util.AppCache.write_ = function(map) {
870   util.platform.setPreference(util.AppCache.KEY, JSON.stringify(map));
871 };
872
873 /**
874  * Remove over-capacity and obsolete items.
875  *
876  * @param {Object} map A map of timestamped key-value pairs.
877  * @private
878  */
879 util.AppCache.cleanup_ = function(map) {
880   // Sort keys by ascending timestamps.
881   var keys = [];
882   for (var key in map) {
883     if (map.hasOwnProperty(key))
884       keys.push(key);
885   }
886   keys.sort(function(a, b) { return map[a].expire > map[b].expire });
887
888   var cutoff = Date.now();
889
890   var obsolete = 0;
891   while (obsolete < keys.length &&
892          map[keys[obsolete]].expire < cutoff) {
893     obsolete++;
894   }
895
896   var overCapacity = Math.max(0, keys.length - util.AppCache.CAPACITY);
897
898   var itemsToDelete = Math.max(obsolete, overCapacity);
899   for (var i = 0; i != itemsToDelete; i++) {
900     delete map[keys[i]];
901   }
902 };
903
904 /**
905  * Load an image.
906  *
907  * @param {Image} image Image element.
908  * @param {string} url Source url.
909  * @param {Object=} opt_options Hash array of options, eg. width, height,
910  *     maxWidth, maxHeight, scale, cache.
911  * @param {function()=} opt_isValid Function returning false iff the task
912  *     is not valid and should be aborted.
913  * @return {?number} Task identifier or null if fetched immediately from
914  *     cache.
915  */
916 util.loadImage = function(image, url, opt_options, opt_isValid) {
917   return ImageLoaderClient.loadToImage(url,
918                                       image,
919                                       opt_options || {},
920                                       function() {},
921                                       function() { image.onerror(); },
922                                       opt_isValid);
923 };
924
925 /**
926  * Cancels loading an image.
927  * @param {number} taskId Task identifier returned by util.loadImage().
928  */
929 util.cancelLoadImage = function(taskId) {
930   ImageLoaderClient.getInstance().cancel(taskId);
931 };
932
933 /**
934  * Finds proerty descriptor in the object prototype chain.
935  * @param {Object} object The object.
936  * @param {string} propertyName The property name.
937  * @return {Object} Property descriptor.
938  */
939 util.findPropertyDescriptor = function(object, propertyName) {
940   for (var p = object; p; p = Object.getPrototypeOf(p)) {
941     var d = Object.getOwnPropertyDescriptor(p, propertyName);
942     if (d)
943       return d;
944   }
945   return null;
946 };
947
948 /**
949  * Calls inherited property setter (useful when property is
950  * overriden).
951  * @param {Object} object The object.
952  * @param {string} propertyName The property name.
953  * @param {*} value Value to set.
954  */
955 util.callInheritedSetter = function(object, propertyName, value) {
956   var d = util.findPropertyDescriptor(Object.getPrototypeOf(object),
957                                       propertyName);
958   d.set.call(object, value);
959 };
960
961 /**
962  * Returns true if the board of the device matches the given prefix.
963  * @param {string} boardPrefix The board prefix to match against.
964  *     (ex. "x86-mario". Prefix is used as the actual board name comes with
965  *     suffix like "x86-mario-something".
966  * @return {boolean} True if the board of the device matches the given prefix.
967  */
968 util.boardIs = function(boardPrefix) {
969   // The board name should be lower-cased, but making it case-insensitive for
970   // backward compatibility just in case.
971   var board = str('CHROMEOS_RELEASE_BOARD');
972   var pattern = new RegExp('^' + boardPrefix, 'i');
973   return board.match(pattern) != null;
974 };
975
976 /**
977  * Adds an isFocused method to the current window object.
978  */
979 util.addIsFocusedMethod = function() {
980   var focused = true;
981
982   window.addEventListener('focus', function() {
983     focused = true;
984   });
985
986   window.addEventListener('blur', function() {
987     focused = false;
988   });
989
990   /**
991    * @return {boolean} True if focused.
992    */
993   window.isFocused = function() {
994     return focused;
995   };
996 };
997
998 /**
999  * Makes a redirect to the specified Files.app's window from another window.
1000  * @param {number} id Window id.
1001  * @param {string} url Target url.
1002  * @return {boolean} True if the window has been found. False otherwise.
1003  */
1004 util.redirectMainWindow = function(id, url) {
1005   // TODO(mtomasz): Implement this for Apps V2, once the photo importer is
1006   // restored.
1007   return false;
1008 };
1009
1010 /**
1011  * Checks, if the Files.app's window is in a full screen mode.
1012  *
1013  * @param {AppWindow} appWindow App window to be maximized.
1014  * @return {boolean} True if the full screen mode is enabled.
1015  */
1016 util.isFullScreen = function(appWindow) {
1017   if (appWindow) {
1018     return appWindow.isFullscreen();
1019   } else {
1020     console.error('App window not passed. Unable to check status of ' +
1021                   'the full screen mode.');
1022     return false;
1023   }
1024 };
1025
1026 /**
1027  * Toggles the full screen mode.
1028  *
1029  * @param {AppWindow} appWindow App window to be maximized.
1030  * @param {boolean} enabled True for enabling, false for disabling.
1031  */
1032 util.toggleFullScreen = function(appWindow, enabled) {
1033   if (appWindow) {
1034     if (enabled)
1035       appWindow.fullscreen();
1036     else
1037       appWindow.restore();
1038     return;
1039   }
1040
1041   console.error(
1042       'App window not passed. Unable to toggle the full screen mode.');
1043 };
1044
1045 /**
1046  * The type of a file operation.
1047  * @enum {string}
1048  */
1049 util.FileOperationType = {
1050   COPY: 'COPY',
1051   MOVE: 'MOVE',
1052   ZIP: 'ZIP',
1053 };
1054
1055 /**
1056  * The type of a file operation error.
1057  * @enum {number}
1058  */
1059 util.FileOperationErrorType = {
1060   UNEXPECTED_SOURCE_FILE: 0,
1061   TARGET_EXISTS: 1,
1062   FILESYSTEM_ERROR: 2,
1063 };
1064
1065 /**
1066  * The kind of an entry changed event.
1067  * @enum {number}
1068  */
1069 util.EntryChangedKind = {
1070   CREATED: 0,
1071   DELETED: 1,
1072 };
1073
1074 /**
1075  * @param {DirectoryEntry|Object} entry DirectoryEntry to be checked.
1076  * @return {boolean} True if the given entry is fake.
1077  */
1078 util.isFakeDirectoryEntry = function(entry) {
1079   // Currently, fake entry doesn't support createReader.
1080   return !('createReader' in entry);
1081 };
1082
1083 /**
1084  * Creates a FileError instance with given code.
1085  * Note that we cannot create FileError instance by "new FileError(code)",
1086  * unfortunately, so here we use Object.create.
1087  * @param {number} code Error code for the FileError.
1088  * @return {FileError} FileError instance
1089  */
1090 util.createFileError = function(code) {
1091   return Object.create(FileError.prototype, {
1092     code: { get: function() { return code; } }
1093   });
1094 };
1095
1096 /**
1097  * @param {Entry|Object} entry1 The entry to be compared. Can be a fake.
1098  * @param {Entry|Object} entry2 The entry to be compared. Can be a fake.
1099  * @return {boolean} True if the both entry represents a same file or directory.
1100  */
1101 util.isSameEntry = function(entry1, entry2) {
1102   // Currently, we can assume there is only one root.
1103   // When we support multi-file system, we need to look at filesystem, too.
1104   return entry1 === null ? entry2 === null : entry1.fullPath == entry2.fullPath;
1105 };
1106
1107 /**
1108  * @param {Entry|Object} parent The parent entry. Can be a fake.
1109  * @param {Entry|Object} child The child entry. Can be a fake.
1110  * @return {boolean} True if parent entry is actualy the parent of the child
1111  *     entry.
1112  */
1113 util.isParentEntry = function(parent, child) {
1114   // Currently, we can assume there is only one root.
1115   // When we support multi-file system, we need to look at filesystem, too.
1116   return PathUtil.isParentPath(parent.fullPath, child.fullPath);
1117 };
1118
1119 /**
1120  * Views files in the browser.
1121  *
1122  * @param {Array.<string>} urls URLs of files to view.
1123  * @param {function(bool)} callback Callback notifying success or not.
1124  */
1125 util.viewFilesInBrowser = function(urls, callback) {
1126   var taskId = chrome.runtime.id + '|file|view-in-browser';
1127   chrome.fileBrowserPrivate.executeTask(taskId, urls, callback);
1128 };
1129
1130 /**
1131  * Visit the URL.
1132  *
1133  * If the browser is opening, the url is opened in a new tag, otherwise the url
1134  * is opened in a new window.
1135  *
1136  * @param {string} url URL to visit.
1137  */
1138 util.visitURL = function(url) {
1139   var params = {url: url};
1140   chrome.tabs.create(params, function() {
1141     if (chrome.runtime.lastError)
1142       chrome.windows.create(params);
1143   });
1144 };
1145
1146 /**
1147  * Returns normalized current locale, or default locale - 'en'.
1148  * @return {string} Current locale
1149  */
1150 util.getCurrentLocaleOrDefault = function() {
1151   // chrome.i18n.getMessage('@@ui_locale') can't be used in packed app.
1152   // Instead, we pass it from C++-side with strings.
1153   return str('UI_LOCALE') || 'en';
1154 };
1155
1156 /**
1157  * Error type of VolumeManager.
1158  * @enum {string}
1159  */
1160 util.VolumeError = Object.freeze({
1161   /* Internal errors */
1162   NOT_MOUNTED: 'not_mounted',
1163   TIMEOUT: 'timeout',
1164
1165   /* System events */
1166   UNKNOWN: 'error_unknown',
1167   INTERNAL: 'error_internal',
1168   UNKNOWN_FILESYSTEM: 'error_unknown_filesystem',
1169   UNSUPPORTED_FILESYSTEM: 'error_unsupported_filesystem',
1170   INVALID_ARCHIVE: 'error_invalid_archive',
1171   AUTHENTICATION: 'error_authentication',
1172   PATH_UNMOUNTED: 'error_path_unmounted'
1173 });
1174
1175 /**
1176  * List of connection types of drive.
1177  *
1178  * Keep this in sync with the kDriveConnectionType* constants in
1179  * file_browser_private_api.cc.
1180  *
1181  * @enum {string}
1182  */
1183 util.DriveConnectionType = Object.freeze({
1184   OFFLINE: 'offline',  // Connection is offline or drive is unavailable.
1185   METERED: 'metered',  // Connection is metered. Should limit traffic.
1186   ONLINE: 'online'     // Connection is online.
1187 });
1188
1189 /**
1190  * List of reasons of DriveConnectionType.
1191  *
1192  * Keep this in sync with the kDriveConnectionReason constants in
1193  * file_browser_private_api.cc.
1194  *
1195  * @enum {string}
1196  */
1197 util.DriveConnectionReason = Object.freeze({
1198   NOT_READY: 'not_ready',    // Drive is not ready or authentication is failed.
1199   NO_NETWORK: 'no_network',  // Network connection is unavailable.
1200   NO_SERVICE: 'no_service'   // Drive service is unavailable.
1201 });
1202
1203 /**
1204  * The type of each volume.
1205  * @enum {string}
1206  */
1207 util.VolumeType = Object.freeze({
1208   DRIVE: 'drive',
1209   DOWNLOADS: 'downloads',
1210   REMOVABLE: 'removable',
1211   ARCHIVE: 'archive'
1212 });