cros: Remove default pinned apps trial.
[chromium-blink-merge.git] / chrome / browser / resources / file_manager / background / js / background.js
blobe5fb6b85fc790d6c106df867537e597d6c694fc4
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.
5 'use strict';
7 /**
8  * Number of runtime errors catched in the background page.
9  * @type {number}
10  */
11 var JSErrorCount = 0;
13 /**
14  * Counts runtime JavaScript errors.
15  */
16 window.onerror = function() { JSErrorCount++; };
18 /**
19  * Type of a Files.app's instance launch.
20  * @enum {number}
21  */
22 var LaunchType = Object.freeze({
23   ALWAYS_CREATE: 0,
24   FOCUS_ANY_OR_CREATE: 1,
25   FOCUS_SAME_OR_CREATE: 2
26 });
28 /**
29  * Root class of the background page.
30  * @constructor
31  */
32 function Background() {
33   /**
34    * Map of all currently open app windows. The key is an app id.
35    * @type {Object.<string, AppWindow>}
36    */
37   this.appWindows = {};
39   /**
40    * Synchronous queue for asynchronous calls.
41    * @type {AsyncUtil.Queue}
42    */
43   this.queue = new AsyncUtil.Queue();
45   /**
46    * Progress center of the background page.
47    * @type {ProgressCenter}
48    */
49   this.progressCenter = new ProgressCenter();
51   /**
52    * File operation manager.
53    * @type {FileOperationManager}
54    */
55   this.fileOperationManager = FileOperationManager.getInstance();
57   /**
58    * Event handler for progress center.
59    * @type {FileOperationHandler}
60    * @private
61    */
62   this.fileOperationHandler_ = new FileOperationHandler(this);
64   /**
65    * String assets.
66    * @type {Object.<string, string>}
67    */
68   this.stringData = null;
70   /**
71    * Callback list to be invoked after initialization.
72    * It turns to null after initialization.
73    *
74    * @type {Array.<function()>}
75    * @private
76    */
77   this.initializeCallbacks_ = [];
79   /**
80    * Last time when the background page can close.
81    *
82    * @type {number}
83    * @private
84    */
85   this.lastTimeCanClose_ = null;
87   // Seal self.
88   Object.seal(this);
90   // Initialize handlers.
91   chrome.fileBrowserHandler.onExecute.addListener(this.onExecute_.bind(this));
92   chrome.app.runtime.onLaunched.addListener(this.onLaunched_.bind(this));
93   chrome.app.runtime.onRestarted.addListener(this.onRestarted_.bind(this));
94   chrome.contextMenus.onClicked.addListener(
95       this.onContextMenuClicked_.bind(this));
97   // Fetch strings and initialize the context menu.
98   this.queue.run(function(callNextStep) {
99     chrome.fileBrowserPrivate.getStrings(function(strings) {
100       // Initialize string assets.
101       this.stringData = strings;
102       loadTimeData.data = strings;
103       this.initContextMenu_();
105       // Invoke initialize callbacks.
106       for (var i = 0; i < this.initializeCallbacks_.length; i++) {
107         this.initializeCallbacks_[i]();
108       }
109       this.initializeCallbacks_ = null;
111       callNextStep();
112     }.bind(this));
113   }.bind(this));
117  * A number of delay milliseconds from the first call of tryClose to the actual
118  * close action.
119  * @type {number}
120  * @const
121  * @private
122  */
123 Background.CLOSE_DELAY_MS_ = 5000;
126  * Make a key of window geometry preferences for the given initial URL.
127  * @param {string} url Initialize URL that the window has.
128  * @return {string} Key of window geometry preferences.
129  */
130 Background.makeGeometryKey = function(url) {
131   return 'windowGeometry' + ':' + url;
135  * Register callback to be invoked after initialization.
136  * If the initialization is already done, the callback is invoked immediately.
138  * @param {function()} callback Initialize callback to be registered.
139  */
140 Background.prototype.ready = function(callback) {
141   if (this.initializeCallbacks_ !== null)
142     this.initializeCallbacks_.push(callback);
143   else
144     callback();
148  * Checks the current condition of background page and closes it if possible.
149  */
150 Background.prototype.tryClose = function() {
151   // If the file operation is going, the background page cannot close.
152   if (this.fileOperationManager.hasQueuedTasks()) {
153     this.lastTimeCanClose_ = null;
154     return;
155   }
157   var views = chrome.extension.getViews();
158   var closing = false;
159   for (var i = 0; i < views.length; i++) {
160     // If the window that is not the background page itself and it is not
161     // closing, the background page cannot close.
162     if (views[i] !== window && !views[i].closing) {
163       this.lastTimeCanClose_ = null;
164       return;
165     }
166     closing = closing || views[i].closing;
167   }
169   // If some windows are closing, or the background page can close but could not
170   // 5 seconds ago, We need more time for sure.
171   if (closing ||
172       this.lastTimeCanClose_ === null ||
173       Date.now() - this.lastTimeCanClose_ < Background.CLOSE_DELAY_MS_) {
174     if (this.lastTimeCanClose_ === null)
175       this.lastTimeCanClose_ = Date.now();
176     setTimeout(this.tryClose.bind(this), Background.CLOSE_DELAY_MS_);
177     return;
178   }
180   // Otherwise we can close the background page.
181   close();
185  * Gets similar windows, it means with the same initial url.
186  * @param {string} url URL that the obtained windows have.
187  * @return {Array.<AppWindow>} List of similar windows.
188  */
189 Background.prototype.getSimilarWindows = function(url) {
190   var result = [];
191   for (var appID in this.appWindows) {
192     if (this.appWindows[appID].contentWindow.appInitialURL === url)
193       result.push(this.appWindows[appID]);
194   }
195   return result;
199  * Wrapper for an app window.
201  * Expects the following from the app scripts:
202  * 1. The page load handler should initialize the app using |window.appState|
203  *    and call |util.platform.saveAppState|.
204  * 2. Every time the app state changes the app should update |window.appState|
205  *    and call |util.platform.saveAppState| .
206  * 3. The app may have |unload| function to persist the app state that does not
207  *    fit into |window.appState|.
209  * @param {string} url App window content url.
210  * @param {string} id App window id.
211  * @param {Object} options Options object to create it.
212  * @constructor
213  */
214 function AppWindowWrapper(url, id, options) {
215   this.url_ = url;
216   this.id_ = id;
217   // Do deep copy for the template of options to assign customized params later.
218   this.options_ = JSON.parse(JSON.stringify(options));
219   this.window_ = null;
220   this.appState_ = null;
221   this.openingOrOpened_ = false;
222   this.queue = new AsyncUtil.Queue();
223   Object.seal(this);
227  * Shift distance to avoid overlapping windows.
228  * @type {number}
229  * @const
230  */
231 AppWindowWrapper.SHIFT_DISTANCE = 40;
235  * Opens the window.
237  * @param {Object} appState App state.
238  * @param {function()=} opt_callback Completion callback.
239  */
240 AppWindowWrapper.prototype.launch = function(appState, opt_callback) {
241   // Check if the window is opened or not.
242   if (this.openingOrOpened_) {
243     console.error('The window is already opened.');
244     if (opt_callback)
245       opt_callback();
246     return;
247   }
248   this.openingOrOpened_ = true;
250   // Save application state.
251   this.appState_ = appState;
253   // Get similar windows, it means with the same initial url, eg. different
254   // main windows of Files.app.
255   var similarWindows = background.getSimilarWindows(this.url_);
257   // Restore maximized windows, to avoid hiding them to tray, which can be
258   // confusing for users.
259   this.queue.run(function(callback) {
260     for (var index = 0; index < similarWindows.length; index++) {
261       if (similarWindows[index].isMaximized()) {
262         var createWindowAndRemoveListener = function() {
263           similarWindows[index].onRestored.removeListener(
264               createWindowAndRemoveListener);
265           callback();
266         };
267         similarWindows[index].onRestored.addListener(
268             createWindowAndRemoveListener);
269         similarWindows[index].restore();
270         return;
271       }
272     }
273     // If no maximized windows, then create the window immediately.
274     callback();
275   });
277   // Obtains the last geometry.
278   var lastBounds;
279   this.queue.run(function(callback) {
280     var key = Background.makeGeometryKey(this.url_);
281     chrome.storage.local.get(key, function(preferences) {
282       if (!chrome.runtime.lastError)
283         lastBounds = preferences[key];
284       callback();
285     });
286   }.bind(this));
288   // Closure creating the window, once all preprocessing tasks are finished.
289   this.queue.run(function(callback) {
290     // Apply the last bounds.
291     if (lastBounds)
292       this.options_.bounds = lastBounds;
294     // Create a window.
295     chrome.app.window.create(this.url_, this.options_, function(appWindow) {
296       this.window_ = appWindow;
297       callback();
298     }.bind(this));
299   }.bind(this));
301   // After creating.
302   this.queue.run(function(callback) {
303     // If there is another window in the same position, shift the window.
304     var makeBoundsKey = function(bounds) {
305       return bounds.left + '/' + bounds.top;
306     };
307     var notAvailablePositions = {};
308     for (var i = 0; i < similarWindows.length; i++) {
309       var key = makeBoundsKey(similarWindows[i].getBounds());
310       notAvailablePositions[key] = true;
311     }
312     var candidateBounds = this.window_.getBounds();
313     while (true) {
314       var key = makeBoundsKey(candidateBounds);
315       if (!notAvailablePositions[key])
316         break;
317       // Make the position available to avoid an infinite loop.
318       notAvailablePositions[key] = false;
319       var nextLeft = candidateBounds.left + AppWindowWrapper.SHIFT_DISTANCE;
320       var nextRight = nextLeft + candidateBounds.width;
321       candidateBounds.left = nextRight >= screen.availWidth ?
322           nextRight % screen.availWidth : nextLeft;
323       var nextTop = candidateBounds.top + AppWindowWrapper.SHIFT_DISTANCE;
324       var nextBottom = nextTop + candidateBounds.height;
325       candidateBounds.top = nextBottom >= screen.availHeight ?
326           nextBottom % screen.availHeight : nextTop;
327     }
328     this.window_.moveTo(candidateBounds.left, candidateBounds.top);
330     // Save the properties.
331     var appWindow = this.window_;
332     background.appWindows[this.id_] = appWindow;
333     var contentWindow = appWindow.contentWindow;
334     contentWindow.appID = this.id_;
335     contentWindow.appState = this.appState_;
336     contentWindow.appInitialURL = this.url_;
337     if (window.IN_TEST)
338       contentWindow.IN_TEST = true;
340     // Register event listners.
341     appWindow.onBoundsChanged.addListener(this.onBoundsChanged_.bind(this));
342     appWindow.onClosed.addListener(this.onClosed_.bind(this));
344     // Callback.
345     if (opt_callback)
346       opt_callback();
347     callback();
348   }.bind(this));
352  * Handles the onClosed extension API event.
353  * @private
354  */
355 AppWindowWrapper.prototype.onClosed_ = function() {
356   // Unload the window.
357   var appWindow = this.window_;
358   var contentWindow = this.window_.contentWindow;
359   if (contentWindow.unload)
360     contentWindow.unload();
361   this.window_ = null;
362   this.openingOrOpened_ = false;
364   // Updates preferences.
365   if (contentWindow.saveOnExit) {
366     contentWindow.saveOnExit.forEach(function(entry) {
367       util.AppCache.update(entry.key, entry.value);
368     });
369   }
370   chrome.storage.local.remove(this.id_);  // Forget the persisted state.
372   // Remove the window from the set.
373   delete background.appWindows[this.id_];
375   // If there is no application window, reset window ID.
376   if (!Object.keys(background.appWindows).length)
377     nextFileManagerWindowID = 0;
378   background.tryClose();
382  * Handles onBoundsChanged extension API event.
383  * @private
384  */
385 AppWindowWrapper.prototype.onBoundsChanged_ = function() {
386   var preferences = {};
387   preferences[Background.makeGeometryKey(this.url_)] =
388       this.window_.getBounds();
389   chrome.storage.local.set(preferences);
393  * Wrapper for a singleton app window.
395  * In addition to the AppWindowWrapper requirements the app scripts should
396  * have |reload| method that re-initializes the app based on a changed
397  * |window.appState|.
399  * @param {string} url App window content url.
400  * @param {Object|function()} options Options object or a function to return it.
401  * @constructor
402  */
403 function SingletonAppWindowWrapper(url, options) {
404   AppWindowWrapper.call(this, url, url, options);
408  * Inherits from AppWindowWrapper.
409  */
410 SingletonAppWindowWrapper.prototype = {__proto__: AppWindowWrapper.prototype};
413  * Open the window.
415  * Activates an existing window or creates a new one.
417  * @param {Object} appState App state.
418  * @param {function()=} opt_callback Completion callback.
419  */
420 SingletonAppWindowWrapper.prototype.launch = function(appState, opt_callback) {
421   // If the window is not opened yet, just call the parent method.
422   if (!this.openingOrOpened_) {
423     AppWindowWrapper.prototype.launch.call(this, appState, opt_callback);
424     return;
425   }
427   // If the window is already opened, reload the window.
428   // The queue is used to wait until the window is opened.
429   this.queue.run(function(nextStep) {
430     this.window_.contentWindow.appState = appState;
431     this.window_.contentWindow.reload();
432     this.window_.focus();
433     if (opt_callback)
434       opt_callback();
435     nextStep();
436   }.bind(this));
440  * Reopen a window if its state is saved in the local storage.
441  */
442 SingletonAppWindowWrapper.prototype.reopen = function() {
443   chrome.storage.local.get(this.id_, function(items) {
444     var value = items[this.id_];
445     if (!value)
446       return;  // No app state persisted.
448     try {
449       var appState = JSON.parse(value);
450     } catch (e) {
451       console.error('Corrupt launch data for ' + this.id_, value);
452       return;
453     }
454     this.launch(appState);
455   }.bind(this));
459  * Prefix for the file manager window ID.
460  */
461 var FILES_ID_PREFIX = 'files#';
464  * Regexp matching a file manager window ID.
465  */
466 var FILES_ID_PATTERN = new RegExp('^' + FILES_ID_PREFIX + '(\\d*)$');
469  * Value of the next file manager window ID.
470  */
471 var nextFileManagerWindowID = 0;
474  * File manager window create options.
475  * @type {Object}
476  * @const
477  */
478 var FILE_MANAGER_WINDOW_CREATE_OPTIONS = Object.freeze({
479   bounds: Object.freeze({
480     left: Math.round(window.screen.availWidth * 0.1),
481     top: Math.round(window.screen.availHeight * 0.1),
482     width: Math.round(window.screen.availWidth * 0.8),
483     height: Math.round(window.screen.availHeight * 0.8)
484   }),
485   minWidth: 320,
486   minHeight: 240,
487   frame: 'none',
488   hidden: true,
489   transparentBackground: true
493  * @param {Object=} opt_appState App state.
494  * @param {number=} opt_id Window id.
495  * @param {LaunchType=} opt_type Launch type. Default: ALWAYS_CREATE.
496  * @param {function(string)=} opt_callback Completion callback with the App ID.
497  */
498 function launchFileManager(opt_appState, opt_id, opt_type, opt_callback) {
499   var type = opt_type || LaunchType.ALWAYS_CREATE;
501   // Wait until all windows are created.
502   background.queue.run(function(onTaskCompleted) {
503     // Check if there is already a window with the same path. If so, then
504     // reuse it instead of opening a new one.
505     if (type == LaunchType.FOCUS_SAME_OR_CREATE ||
506         type == LaunchType.FOCUS_ANY_OR_CREATE) {
507       if (opt_appState && opt_appState.defaultPath) {
508         for (var key in background.appWindows) {
509           if (!key.match(FILES_ID_PATTERN))
510             continue;
512           var contentWindow = background.appWindows[key].contentWindow;
513           if (contentWindow.appState &&
514               opt_appState.defaultPath == contentWindow.appState.defaultPath) {
515             background.appWindows[key].focus();
516             if (opt_callback)
517               opt_callback(key);
518             onTaskCompleted();
519             return;
520           }
521         }
522       }
523     }
525     // Focus any window if none is focused. Try restored first.
526     if (type == LaunchType.FOCUS_ANY_OR_CREATE) {
527       // If there is already a focused window, then finish.
528       for (var key in background.appWindows) {
529         if (!key.match(FILES_ID_PATTERN))
530           continue;
532         // The isFocused() method should always be available, but in case
533         // Files.app's failed on some error, wrap it with try catch.
534         try {
535           if (background.appWindows[key].contentWindow.isFocused()) {
536             if (opt_callback)
537               opt_callback(key);
538             onTaskCompleted();
539             return;
540           }
541         } catch (e) {
542           console.error(e.message);
543         }
544       }
545       // Try to focus the first non-minimized window.
546       for (var key in background.appWindows) {
547         if (!key.match(FILES_ID_PATTERN))
548           continue;
550         if (!background.appWindows[key].isMinimized()) {
551           background.appWindows[key].focus();
552           if (opt_callback)
553             opt_callback(key);
554           onTaskCompleted();
555           return;
556         }
557       }
558       // Restore and focus any window.
559       for (var key in background.appWindows) {
560         if (!key.match(FILES_ID_PATTERN))
561           continue;
563         background.appWindows[key].focus();
564         if (opt_callback)
565           opt_callback(key);
566         onTaskCompleted();
567         return;
568       }
569     }
571     // Create a new instance in case of ALWAYS_CREATE type, or as a fallback
572     // for other types.
574     var id = opt_id || nextFileManagerWindowID;
575     nextFileManagerWindowID = Math.max(nextFileManagerWindowID, id + 1);
576     var appId = FILES_ID_PREFIX + id;
578     var appWindow = new AppWindowWrapper(
579         'main.html',
580         appId,
581         FILE_MANAGER_WINDOW_CREATE_OPTIONS);
582     appWindow.launch(opt_appState || {}, function() {
583       if (opt_callback)
584         opt_callback(appId);
585       onTaskCompleted();
586     });
587   });
591  * Executes a file browser task.
593  * @param {string} action Task id.
594  * @param {Object} details Details object.
595  * @private
596  */
597 Background.prototype.onExecute_ = function(action, details) {
598   var urls = details.entries.map(function(e) { return e.toURL(); });
600   switch (action) {
601     case 'play':
602       launchAudioPlayer({items: urls, position: 0});
603       break;
605     case 'watch':
606       launchVideoPlayer(urls[0]);
607       break;
609     default:
610       var launchEnable = null;
611       var queue = new AsyncUtil.Queue();
612       queue.run(function(nextStep) {
613         // If it is not auto-open (triggered by mounting external devices), we
614         // always launch Files.app.
615         if (action != 'auto-open') {
616           launchEnable = true;
617           nextStep();
618           return;
619         }
620         // If the disable-default-apps flag is on, Files.app is not opened
621         // automatically on device mount because it obstculs the manual test.
622         chrome.commandLinePrivate.hasSwitch('disable-default-apps',
623                                             function(flag) {
624           launchEnable = !flag;
625           nextStep();
626         });
627       });
628       queue.run(function(nextStep) {
629         if (!launchEnable) {
630           nextStep();
631           return;
632         }
634         // Every other action opens a Files app window.
635         var appState = {
636           params: {
637             action: action
638           },
639           defaultPath: details.entries[0].fullPath
640         };
641         // For mounted devices just focus any Files.app window. The mounted
642         // volume will appear on the navigation list.
643         var type = action == 'auto-open' ? LaunchType.FOCUS_ANY_OR_CREATE :
644             LaunchType.FOCUS_SAME_OR_CREATE;
645         launchFileManager(appState, /* App ID */ undefined, type, nextStep);
646       });
647       break;
648   }
652  * Audio player window create options.
653  * @type {Object}
654  * @const
655  */
656 var AUDIO_PLAYER_CREATE_OPTIONS = Object.freeze({
657   type: 'panel',
658   hidden: true,
659   minHeight: 35 + 58,
660   minWidth: 280,
661   height: 35 + 58,
662   width: 280
665 var audioPlayer = new SingletonAppWindowWrapper('mediaplayer.html',
666                                                 AUDIO_PLAYER_CREATE_OPTIONS);
669  * Launch the audio player.
670  * @param {Object} playlist Playlist.
671  */
672 function launchAudioPlayer(playlist) {
673   audioPlayer.launch(playlist);
676 var videoPlayer = new SingletonAppWindowWrapper('video_player.html',
677                                                 {hidden: true});
680  * Launch the video player.
681  * @param {string} url Video url.
682  */
683 function launchVideoPlayer(url) {
684   videoPlayer.launch({url: url});
688  * Launches the app.
689  * @private
690  */
691 Background.prototype.onLaunched_ = function() {
692   if (nextFileManagerWindowID == 0) {
693     // The app just launched. Remove window state records that are not needed
694     // any more.
695     chrome.storage.local.get(function(items) {
696       for (var key in items) {
697         if (items.hasOwnProperty(key)) {
698           if (key.match(FILES_ID_PATTERN))
699             chrome.storage.local.remove(key);
700         }
701       }
702     });
703   }
704   launchFileManager(null, null, LaunchType.FOCUS_ANY_OR_CREATE);
708  * Restarted the app, restore windows.
709  * @private
710  */
711 Background.prototype.onRestarted_ = function() {
712   // Reopen file manager windows.
713   chrome.storage.local.get(function(items) {
714     for (var key in items) {
715       if (items.hasOwnProperty(key)) {
716         var match = key.match(FILES_ID_PATTERN);
717         if (match) {
718           var id = Number(match[1]);
719           try {
720             var appState = JSON.parse(items[key]);
721             launchFileManager(appState, id);
722           } catch (e) {
723             console.error('Corrupt launch data for ' + id);
724           }
725         }
726       }
727     }
728   });
730   // Reopen sub-applications.
731   audioPlayer.reopen();
732   videoPlayer.reopen();
736  * Handles clicks on a custom item on the launcher context menu.
737  * @param {OnClickData} info Event details.
738  * @private
739  */
740 Background.prototype.onContextMenuClicked_ = function(info) {
741   if (info.menuItemId == 'new-window') {
742     // Find the focused window (if any) and use it's current path for the
743     // new window. If not found, then launch with the default path.
744     for (var key in background.appWindows) {
745       try {
746         if (background.appWindows[key].contentWindow.isFocused()) {
747           var appState = {
748             defaultPath: background.appWindows[key].contentWindow.
749                 appState.defaultPath
750           };
751           launchFileManager(appState);
752           return;
753         }
754       } catch (ignore) {
755         // The isFocused method may not be defined during initialization.
756         // Therefore, wrapped with a try-catch block.
757       }
758     }
760     // Launch with the default path.
761     launchFileManager();
762   }
766  * Initializes the context menu. Recreates if already exists.
767  * @private
768  */
769 Background.prototype.initContextMenu_ = function() {
770   try {
771     chrome.contextMenus.remove('new-window');
772   } catch (ignore) {
773     // There is no way to detect if the context menu is already added, therefore
774     // try to recreate it every time.
775   }
776   chrome.contextMenus.create({
777     id: 'new-window',
778     contexts: ['launcher'],
779     title: str('NEW_WINDOW_BUTTON_LABEL')
780   });
784  * Singleton instance of Background.
785  * @type {Background}
786  */
787 window.background = new Background();