Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / file_manager / background / js / background.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 /**
6  * Type of a Files.app's instance launch.
7  * @enum {number}
8  */
9 var LaunchType = {
10   ALWAYS_CREATE: 0,
11   FOCUS_ANY_OR_CREATE: 1,
12   FOCUS_SAME_OR_CREATE: 2
13 };
14 Object.freeze(LaunchType);
15
16 /**
17  * Root class of the background page.
18  * @constructor
19  * @extends {BackgroundBase}
20  */
21 function FileBrowserBackground() {
22   BackgroundBase.call(this);
23
24   /**
25    * Map of all currently open file dialogs. The key is an app ID.
26    * @type {Object.<string, Window>}
27    */
28   this.dialogs = {};
29
30   /**
31    * Synchronous queue for asynchronous calls.
32    * @type {AsyncUtil.Queue}
33    */
34   this.queue = new AsyncUtil.Queue();
35
36   /**
37    * Progress center of the background page.
38    * @type {ProgressCenter}
39    */
40   this.progressCenter = new ProgressCenter();
41
42   /**
43    * File operation manager.
44    * @type {FileOperationManager}
45    */
46   this.fileOperationManager = new FileOperationManager();
47
48   /**
49    * Manages loading of import history necessary for decorating files
50    * in some views and integral to local dedupling files during the
51    * cloud import process.
52    *
53    * @type {HistoryLoader}
54    */
55   this.historyLoader = null;
56
57   chrome.commandLinePrivate.hasSwitch(
58       'enable-cloud-backup',
59       /**
60        * @param {boolean} enabled
61        * @this {!FileBrowserBackground}
62        */
63       function(enabled) {
64         if (enabled) {
65           this.historyLoader = new SynchronizedHistoryLoader(
66               new ChromeSyncFileEntryProvider());
67         }
68       }.bind(this));
69
70   /**
71    * Event handler for progress center.
72    * @type {FileOperationHandler}
73    * @private
74    */
75   this.fileOperationHandler_ = new FileOperationHandler(this);
76
77   /**
78    * Event handler for C++ sides notifications.
79    * @type {DeviceHandler}
80    * @private
81    */
82   this.deviceHandler_ = new DeviceHandler();
83   this.deviceHandler_.addEventListener(
84       DeviceHandler.VOLUME_NAVIGATION_REQUESTED,
85       function(event) {
86         this.navigateToVolume_(event.devicePath);
87       }.bind(this));
88
89   /**
90    * Drive sync handler.
91    * @type {DriveSyncHandler}
92    */
93   this.driveSyncHandler = new DriveSyncHandler(this.progressCenter);
94   this.driveSyncHandler.addEventListener(
95       DriveSyncHandler.COMPLETED_EVENT,
96       function() { this.tryClose(); }.bind(this));
97
98   /**
99    * Promise of string data.
100    * @type {Promise}
101    */
102   this.stringDataPromise = new Promise(function(fulfill) {
103     chrome.fileManagerPrivate.getStrings(fulfill);
104   });
105
106   /**
107    * String assets.
108    * @type {Object.<string, string>}
109    */
110   this.stringData = null;
111
112   /**
113    * Callback list to be invoked after initialization.
114    * It turns to null after initialization.
115    *
116    * @type {Array.<function()>}
117    * @private
118    */
119   this.initializeCallbacks_ = [];
120
121   /**
122    * Last time when the background page can close.
123    *
124    * @type {?number}
125    * @private
126    */
127   this.lastTimeCanClose_ = null;
128
129   // Seal self.
130   Object.seal(this);
131
132   // Initialize handlers.
133   chrome.fileBrowserHandler.onExecute.addListener(this.onExecute_.bind(this));
134   chrome.app.runtime.onLaunched.addListener(this.onLaunched_.bind(this));
135   chrome.app.runtime.onRestarted.addListener(this.onRestarted_.bind(this));
136   chrome.contextMenus.onClicked.addListener(
137       this.onContextMenuClicked_.bind(this));
138
139   this.queue.run(function(callback) {
140     this.stringDataPromise.then(function(strings) {
141       // Init string data.
142       this.stringData = strings;
143       loadTimeData.data = strings;
144
145       // Init context menu.
146       this.initContextMenu_();
147
148       callback();
149     }.bind(this)).catch(function(error) {
150       console.error(error.stack || error);
151       callback();
152     });
153   }.bind(this));
154 }
155
156 /**
157  * A number of delay milliseconds from the first call of tryClose to the actual
158  * close action.
159  * @type {number}
160  * @const
161  * @private
162  */
163 FileBrowserBackground.CLOSE_DELAY_MS_ = 5000;
164
165 FileBrowserBackground.prototype = {
166   __proto__: BackgroundBase.prototype
167 };
168
169 /**
170  * Register callback to be invoked after initialization.
171  * If the initialization is already done, the callback is invoked immediately.
172  *
173  * @param {function()} callback Initialize callback to be registered.
174  */
175 FileBrowserBackground.prototype.ready = function(callback) {
176   this.stringDataPromise.then(callback);
177 };
178
179 /**
180  * Checks the current condition of background page.
181  * @return {boolean} True if the background page is closable, false if not.
182  */
183 FileBrowserBackground.prototype.canClose = function() {
184   // If the file operation is going, the background page cannot close.
185   if (this.fileOperationManager.hasQueuedTasks() ||
186       this.driveSyncHandler.syncing) {
187     this.lastTimeCanClose_ = null;
188     return false;
189   }
190
191   var views = chrome.extension.getViews();
192   var closing = false;
193   for (var i = 0; i < views.length; i++) {
194     // If the window that is not the background page itself and it is not
195     // closing, the background page cannot close.
196     if (views[i] !== window && !views[i].closing) {
197       this.lastTimeCanClose_ = null;
198       return false;
199     }
200     closing = closing || views[i].closing;
201   }
202
203   // If some windows are closing, or the background page can close but could not
204   // 5 seconds ago, We need more time for sure.
205   if (closing ||
206       this.lastTimeCanClose_ === null ||
207       (Date.now() - this.lastTimeCanClose_ <
208            FileBrowserBackground.CLOSE_DELAY_MS_)) {
209     if (this.lastTimeCanClose_ === null)
210       this.lastTimeCanClose_ = Date.now();
211     setTimeout(this.tryClose.bind(this), FileBrowserBackground.CLOSE_DELAY_MS_);
212     return false;
213   }
214
215   // Otherwise we can close the background page.
216   return true;
217 };
218
219 /**
220  * Opens the root directory of the volume in Files.app.
221  * @param {string} devicePath Device path to a volume to be opened.
222  * @private
223  */
224 FileBrowserBackground.prototype.navigateToVolume_ = function(devicePath) {
225   VolumeManager.getInstance().then(function(volumeManager) {
226     var volumeInfoList = volumeManager.volumeInfoList;
227     for (var i = 0; i < volumeInfoList.length; i++) {
228       if (volumeInfoList.item(i).devicePath == devicePath)
229         return volumeInfoList.item(i).resolveDisplayRoot();
230     }
231     return Promise.reject(
232         'Volume having the device path: ' + devicePath + ' is not found.');
233   }).then(function(entry) {
234     launchFileManager(
235         {currentDirectoryURL: entry.toURL()},
236         /* App ID */ undefined,
237         LaunchType.FOCUS_SAME_OR_CREATE);
238   }).catch(function(error) {
239     console.error(error.stack || error);
240   });
241 };
242
243 /**
244  * Prefix for the file manager window ID.
245  * @type {string}
246  * @const
247  */
248 var FILES_ID_PREFIX = 'files#';
249
250 /**
251  * Regexp matching a file manager window ID.
252  * @type {RegExp}
253  * @const
254  */
255 var FILES_ID_PATTERN = new RegExp('^' + FILES_ID_PREFIX + '(\\d*)$');
256
257 /**
258  * Prefix for the dialog ID.
259  * @type {string}
260  * @const
261  */
262 var DIALOG_ID_PREFIX = 'dialog#';
263
264 /**
265  * Value of the next file manager window ID.
266  * @type {number}
267  */
268 var nextFileManagerWindowID = 0;
269
270 /**
271  * Value of the next file manager dialog ID.
272  * @type {number}
273  */
274 var nextFileManagerDialogID = 0;
275
276 /**
277  * File manager window create options.
278  * @type {Object}
279  * @const
280  */
281 var FILE_MANAGER_WINDOW_CREATE_OPTIONS = Object.freeze({
282   bounds: Object.freeze({
283     left: Math.round(window.screen.availWidth * 0.1),
284     top: Math.round(window.screen.availHeight * 0.1),
285     width: Math.round(window.screen.availWidth * 0.8),
286     height: Math.round(window.screen.availHeight * 0.8)
287   }),
288   minWidth: 480,
289   minHeight: 300,
290   hidden: true
291 });
292
293 /**
294  * @param {Object=} opt_appState App state.
295  * @param {number=} opt_id Window id.
296  * @param {LaunchType=} opt_type Launch type. Default: ALWAYS_CREATE.
297  * @param {function(string)=} opt_callback Completion callback with the App ID.
298  */
299 function launchFileManager(opt_appState, opt_id, opt_type, opt_callback) {
300   var type = opt_type || LaunchType.ALWAYS_CREATE;
301   opt_appState =
302       /**
303        * @type {(undefined|
304        *         {currentDirectoryURL: (string|undefined),
305        *          selectionURL: (string|undefined),
306        *          displayedId: (string|undefined)})}
307        */
308       (opt_appState);
309
310   // Wait until all windows are created.
311   window.background.queue.run(function(onTaskCompleted) {
312     // Check if there is already a window with the same URL. If so, then
313     // reuse it instead of opening a new one.
314     if (type == LaunchType.FOCUS_SAME_OR_CREATE ||
315         type == LaunchType.FOCUS_ANY_OR_CREATE) {
316       if (opt_appState) {
317         for (var key in window.background.appWindows) {
318           if (!key.match(FILES_ID_PATTERN))
319             continue;
320
321           var contentWindow = window.background.appWindows[key].contentWindow;
322           if (!contentWindow.appState)
323             continue;
324
325           // Different current directories.
326           if (opt_appState.currentDirectoryURL !==
327                   contentWindow.appState.currentDirectoryURL) {
328             continue;
329           }
330
331           // Selection URL specified, and it is different.
332           if (opt_appState.selectionURL &&
333                   opt_appState.selectionURL !==
334                   contentWindow.appState.selectionURL) {
335             continue;
336           }
337
338           AppWindowWrapper.focusOnDesktop(
339               window.background.appWindows[key], opt_appState.displayedId);
340           if (opt_callback)
341             opt_callback(key);
342           onTaskCompleted();
343           return;
344         }
345       }
346     }
347
348     // Focus any window if none is focused. Try restored first.
349     if (type == LaunchType.FOCUS_ANY_OR_CREATE) {
350       // If there is already a focused window, then finish.
351       for (var key in window.background.appWindows) {
352         if (!key.match(FILES_ID_PATTERN))
353           continue;
354
355         // The isFocused() method should always be available, but in case
356         // Files.app's failed on some error, wrap it with try catch.
357         try {
358           if (window.background.appWindows[key].contentWindow.isFocused()) {
359             if (opt_callback)
360               opt_callback(key);
361             onTaskCompleted();
362             return;
363           }
364         } catch (e) {
365           console.error(e.message);
366         }
367       }
368       // Try to focus the first non-minimized window.
369       for (var key in window.background.appWindows) {
370         if (!key.match(FILES_ID_PATTERN))
371           continue;
372
373         if (!window.background.appWindows[key].isMinimized()) {
374           AppWindowWrapper.focusOnDesktop(
375               window.background.appWindows[key],
376               (opt_appState || {}).displayedId);
377           if (opt_callback)
378             opt_callback(key);
379           onTaskCompleted();
380           return;
381         }
382       }
383       // Restore and focus any window.
384       for (var key in window.background.appWindows) {
385         if (!key.match(FILES_ID_PATTERN))
386           continue;
387
388         AppWindowWrapper.focusOnDesktop(
389             window.background.appWindows[key],
390             (opt_appState || {}).displayedId);
391         if (opt_callback)
392           opt_callback(key);
393         onTaskCompleted();
394         return;
395       }
396     }
397
398     // Create a new instance in case of ALWAYS_CREATE type, or as a fallback
399     // for other types.
400
401     var id = opt_id || nextFileManagerWindowID;
402     nextFileManagerWindowID = Math.max(nextFileManagerWindowID, id + 1);
403     var appId = FILES_ID_PREFIX + id;
404
405     var appWindow = new AppWindowWrapper(
406         'main.html',
407         appId,
408         FILE_MANAGER_WINDOW_CREATE_OPTIONS);
409     appWindow.launch(opt_appState || {}, false, function() {
410       AppWindowWrapper.focusOnDesktop(
411           appWindow.rawAppWindow, (opt_appState || {}).displayedId);
412       if (opt_callback)
413         opt_callback(appId);
414       onTaskCompleted();
415     });
416   });
417 }
418
419 /**
420  * Registers dialog window to the background page.
421  *
422  * @param {Window} dialogWindow Window of the dialog.
423  */
424 function registerDialog(dialogWindow) {
425   var id = DIALOG_ID_PREFIX + (nextFileManagerDialogID++);
426   window.background.dialogs[id] = dialogWindow;
427   dialogWindow.addEventListener('pagehide', function() {
428     delete window.background.dialogs[id];
429   });
430 }
431
432 /**
433  * Executes a file browser task.
434  *
435  * @param {string} action Task id.
436  * @param {Object} details Details object.
437  * @private
438  */
439 FileBrowserBackground.prototype.onExecute_ = function(action, details) {
440   var appState = {
441     params: {action: action},
442     // It is not allowed to call getParent() here, since there may be
443     // no permissions to access it at this stage. Therefore we are passing
444     // the selectionURL only, and the currentDirectory will be resolved
445     // later.
446     selectionURL: details.entries[0].toURL()
447   };
448
449   // Every other action opens a Files app window.
450   // For mounted devices just focus any Files.app window. The mounted
451   // volume will appear on the navigation list.
452   launchFileManager(
453       appState,
454       /* App ID */ undefined,
455       LaunchType.FOCUS_SAME_OR_CREATE);
456 };
457
458 /**
459  * Launches the app.
460  * @private
461  */
462 FileBrowserBackground.prototype.onLaunched_ = function() {
463   if (nextFileManagerWindowID == 0) {
464     // The app just launched. Remove window state records that are not needed
465     // any more.
466     chrome.storage.local.get(function(items) {
467       for (var key in items) {
468         if (items.hasOwnProperty(key)) {
469           if (key.match(FILES_ID_PATTERN))
470             chrome.storage.local.remove(key);
471         }
472       }
473     });
474   }
475   launchFileManager(null, undefined, LaunchType.FOCUS_ANY_OR_CREATE);
476 };
477
478 /**
479  * Restarted the app, restore windows.
480  * @private
481  */
482 FileBrowserBackground.prototype.onRestarted_ = function() {
483   // Reopen file manager windows.
484   chrome.storage.local.get(function(items) {
485     for (var key in items) {
486       if (items.hasOwnProperty(key)) {
487         var match = key.match(FILES_ID_PATTERN);
488         if (match) {
489           var id = Number(match[1]);
490           try {
491             var appState = /** @type {Object} */ (JSON.parse(items[key]));
492             launchFileManager(appState, id);
493           } catch (e) {
494             console.error('Corrupt launch data for ' + id);
495           }
496         }
497       }
498     }
499   });
500 };
501
502 /**
503  * Handles clicks on a custom item on the launcher context menu.
504  * @param {!Object} info Event details.
505  * @private
506  */
507 FileBrowserBackground.prototype.onContextMenuClicked_ = function(info) {
508   if (info.menuItemId == 'new-window') {
509     // Find the focused window (if any) and use it's current url for the
510     // new window. If not found, then launch with the default url.
511     for (var key in window.background.appWindows) {
512       try {
513         if (window.background.appWindows[key].contentWindow.isFocused()) {
514           var appState = {
515             // Do not clone the selection url, only the current directory.
516             currentDirectoryURL: window.background.appWindows[key].
517                 contentWindow.appState.currentDirectoryURL
518           };
519           launchFileManager(appState);
520           return;
521         }
522       } catch (ignore) {
523         // The isFocused method may not be defined during initialization.
524         // Therefore, wrapped with a try-catch block.
525       }
526     }
527
528     // Launch with the default URL.
529     launchFileManager();
530   }
531 };
532
533 /**
534  * Initializes the context menu. Recreates if already exists.
535  * @private
536  */
537 FileBrowserBackground.prototype.initContextMenu_ = function() {
538   try {
539     // According to the spec [1], the callback is optional. But no callback
540     // causes an error for some reason, so we call it with null-callback to
541     // prevent the error. http://crbug.com/353877
542     // - [1] https://developer.chrome.com/extensions/contextMenus#method-remove
543     chrome.contextMenus.remove('new-window', function() {});
544   } catch (ignore) {
545     // There is no way to detect if the context menu is already added, therefore
546     // try to recreate it every time.
547   }
548   chrome.contextMenus.create({
549     id: 'new-window',
550     contexts: ['launcher'],
551     title: str('NEW_WINDOW_BUTTON_LABEL')
552   });
553 };
554
555 /**
556  * Singleton instance of Background.
557  * @type {FileBrowserBackground}
558  */
559 window.background = new FileBrowserBackground();