1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5'use strict';
6
7/**
8 * Namespace for utility functions.
9 */
10var util = {};
11
12/**
13 * Returns a function that console.log's its arguments, prefixed by |msg|.
14 *
15 * @param {string} msg The message prefix to use in the log.
16 * @param {function(...string)=} opt_callback A function to invoke after
17 *     logging.
18 * @return {function(...string)} Function that logs.
19 */
20util.flog = function(msg, opt_callback) {
21  return function() {
22    var ary = Array.apply(null, arguments);
23    console.log(msg + ': ' + ary.join(', '));
24    if (opt_callback)
25      opt_callback.apply(null, arguments);
26  };
27};
28
29/**
30 * Returns a function that throws an exception that includes its arguments
31 * prefixed by |msg|.
32 *
33 * @param {string} msg The message prefix to use in the exception.
34 * @return {function(...string)} Function that throws.
35 */
36util.ferr = function(msg) {
37  return function() {
38    var ary = Array.apply(null, arguments);
39    throw new Error(msg + ': ' + ary.join(', '));
40  };
41};
42
43/**
44 * 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 */
51util.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 */
61util.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 */
74util.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 */
92util.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 */
106util.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 */
126util.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 */
154util.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 */
195util.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 */
245util.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 */
286util.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 */
316util.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 */
349util.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 */
378util.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 */
411util.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 */
432util.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 */
487util.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 */
546util.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 */
564util.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 */
586util.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 */
598util.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 */
611util.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 */
622util.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 */
643util.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 */
666util.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 */
682util.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 */
703function 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 */
717function 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 */
726util.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 */
771util.addPageLoadHandler = function(handler) {
772  document.addEventListener('DOMContentLoaded', function() {
773    handler();
774  });
775};
776
777/**
778 * Save app launch data to the local storage.
779 */
780util.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 */
795util.AppCache = function() {};
796
797/**
798 * Local storage key.
799 */
800util.AppCache.KEY = 'AppCache';
801
802/**
803 * Max number of items.
804 */
805util.AppCache.CAPACITY = 100;
806
807/**
808 * Default lifetime.
809 */
810util.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 */
816util.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 */
830util.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 */
852util.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 */
869util.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 */
879util.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 */
916util.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 */
929util.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 */
939util.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 */
955util.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 */
968util.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 */
979util.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 */
1004util.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 */
1016util.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 */
1032util.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 */
1049util.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 */
1059util.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 */
1069util.EntryChangedKind = {
1070  CREATED: 0,
1071  DELETED: 1,
1072};
1073
1074/**
1075 * Obtains whether an entry is fake or not.
1076 * @param {Entry|Object} entry Entry of fake entry.
1077 * @return {boolean} True if the given entry is fake.
1078 */
1079util.isFakeEntry = function(entry) {
1080  return !('getParent' 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 */
1090util.createFileError = function(code) {
1091  return Object.create(FileError.prototype, {
1092    code: { get: function() { return code; } }
1093  });
1094};
1095
1096/**
1097 * Compares two entries.
1098 * @param {Entry|Object} entry1 The entry to be compared. Can be a fake.
1099 * @param {Entry|Object} entry2 The entry to be compared. Can be a fake.
1100 * @return {boolean} True if the both entry represents a same file or
1101 *     directory. Returns true if both entries are null.
1102 */
1103util.isSameEntry = function(entry1, entry2) {
1104  // Currently, we can assume there is only one root.
1105  // When we support multi-file system, we need to look at filesystem, too.
1106  return (entry1 && entry2 && entry1.fullPath === entry2.fullPath) ||
1107      (!entry1 && !entry2);
1108};
1109
1110/**
1111 * @param {Entry|Object} parent The parent entry. Can be a fake.
1112 * @param {Entry|Object} child The child entry. Can be a fake.
1113 * @return {boolean} True if parent entry is actualy the parent of the child
1114 *     entry.
1115 */
1116util.isParentEntry = function(parent, child) {
1117  // Currently, we can assume there is only one root.
1118  // When we support multi-file system, we need to look at filesystem, too.
1119  return PathUtil.isParentPath(parent.fullPath, child.fullPath);
1120};
1121
1122/**
1123 * Views files in the browser.
1124 *
1125 * @param {Array.<string>} urls URLs of files to view.
1126 * @param {function(bool)} callback Callback notifying success or not.
1127 */
1128util.viewFilesInBrowser = function(urls, callback) {
1129  var taskId = chrome.runtime.id + '|file|view-in-browser';
1130  chrome.fileBrowserPrivate.executeTask(taskId, urls, callback);
1131};
1132
1133/**
1134 * Visit the URL.
1135 *
1136 * If the browser is opening, the url is opened in a new tag, otherwise the url
1137 * is opened in a new window.
1138 *
1139 * @param {string} url URL to visit.
1140 */
1141util.visitURL = function(url) {
1142  var params = {url: url};
1143  chrome.tabs.create(params, function() {
1144    if (chrome.runtime.lastError)
1145      chrome.windows.create(params);
1146  });
1147};
1148
1149/**
1150 * Returns normalized current locale, or default locale - 'en'.
1151 * @return {string} Current locale
1152 */
1153util.getCurrentLocaleOrDefault = function() {
1154  // chrome.i18n.getMessage('@@ui_locale') can't be used in packed app.
1155  // Instead, we pass it from C++-side with strings.
1156  return str('UI_LOCALE') || 'en';
1157};
1158
1159/**
1160 * Converts array of entries to an array of corresponding URLs.
1161 * @param {Array.<Entry>} entries Input array of entries.
1162 * @return {Array.<string>} Output array of URLs.
1163 */
1164util.entriesToURLs = function(entries) {
1165  // TODO(mtomasz): Make all callers use entries instead of URLs, and then
1166  // remove this utility function.
1167  console.warn('Converting entries to URLs is deprecated.');
1168  return entries.map(function(entry) {
1169     return entry.toURL();
1170  });
1171};
1172
1173/**
1174 * Converts array of URLs to an array of corresponding Entries.
1175 *
1176 * @param {Array.<string>} urls Input array of URLs.
1177 * @param {function(Array.<Entry>)} callback Completion callback with array of
1178 *     Entries.
1179 */
1180util.URLsToEntries = function(urls, callback) {
1181  var result = [];
1182  AsyncUtil.forEach(
1183      urls,
1184      function(forEachCallback, url) {
1185        webkitResolveLocalFileSystemURL(url, function(entry) {
1186          result.push(entry);
1187          forEachCallback();
1188        }, function() {
1189          // Not an error. Possibly, the file is not accessible anymore.
1190          console.warn('Failed to resolve the file with url: ' + url + '.');
1191          forEachCallback();
1192        });
1193      },
1194      function() {
1195        callback(result);
1196      });
1197};
1198
1199/**
1200 * Error type of VolumeManager.
1201 * @enum {string}
1202 * @const
1203 */
1204util.VolumeError = Object.freeze({
1205  /* Internal errors */
1206  NOT_MOUNTED: 'not_mounted',
1207  TIMEOUT: 'timeout',
1208
1209  /* System events */
1210  UNKNOWN: 'error_unknown',
1211  INTERNAL: 'error_internal',
1212  UNKNOWN_FILESYSTEM: 'error_unknown_filesystem',
1213  UNSUPPORTED_FILESYSTEM: 'error_unsupported_filesystem',
1214  INVALID_ARCHIVE: 'error_invalid_archive',
1215  AUTHENTICATION: 'error_authentication',
1216  PATH_UNMOUNTED: 'error_path_unmounted'
1217});
1218
1219/**
1220 * List of connection types of drive.
1221 *
1222 * Keep this in sync with the kDriveConnectionType* constants in
1223 * private_api_dirve.cc.
1224 *
1225 * @enum {string}
1226 * @const
1227 */
1228util.DriveConnectionType = Object.freeze({
1229  OFFLINE: 'offline',  // Connection is offline or drive is unavailable.
1230  METERED: 'metered',  // Connection is metered. Should limit traffic.
1231  ONLINE: 'online'     // Connection is online.
1232});
1233
1234/**
1235 * List of reasons of DriveConnectionType.
1236 *
1237 * Keep this in sync with the kDriveConnectionReason constants in
1238 * private_api_drive.cc.
1239 *
1240 * @enum {string}
1241 * @const
1242 */
1243util.DriveConnectionReason = Object.freeze({
1244  NOT_READY: 'not_ready',    // Drive is not ready or authentication is failed.
1245  NO_NETWORK: 'no_network',  // Network connection is unavailable.
1246  NO_SERVICE: 'no_service'   // Drive service is unavailable.
1247});
1248
1249/**
1250 * The type of each volume.
1251 * @enum {string}
1252 * @const
1253 */
1254util.VolumeType = Object.freeze({
1255  DRIVE: 'drive',
1256  DOWNLOADS: 'downloads',
1257  REMOVABLE: 'removable',
1258  ARCHIVE: 'archive'
1259});
1260