Make same function name with its event name
[platform/framework/web/wrtjs.git] / wrt_app / src / web_application.ts
1 /*
2  * Copyright (c) 2019 Samsung Electronics Co., Ltd All Rights Reserved
3  *
4  *    Licensed under the Apache License, Version 2.0 (the "License");
5  *    you may not use this file except in compliance with the License.
6  *    You may obtain a copy of the License at
7  *
8  *        http://www.apache.org/licenses/LICENSE-2.0
9  *
10  *    Unless required by applicable law or agreed to in writing, software
11  *    distributed under the License is distributed on an "AS IS" BASIS,
12  *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  *    See the License for the specific language governing permissions and
14  *    limitations under the License.
15  */
16
17 'use strict';
18
19 import { app, protocol } from 'electron';
20 import { wrt } from '../browser/wrt';
21 import * as WRTWebContents from '../browser/wrt_web_contents';
22 import { WRTWindow } from '../browser/wrt_window';
23 import { addonManager } from './addon_manager';
24
25 export class WebApplication {
26   accessiblePath?: string[];
27   backgroundExecution: boolean;
28   defaultBackgroundColor: string;
29   defaultTransparent: boolean;
30   isAlwaysReload: boolean;
31   mainWindow: Electron.BrowserWindow;
32   multitaskingSupport: boolean;
33   notificationPermissionMap?: Map<Electron.WebContents, boolean>;
34   preloadStatus: string;
35   showTimer?: NodeJS.Timeout;
36
37   backgroundSupport = wrt.getBackgroundSupport();
38   debugPort = 0;
39   firstRendered = false;
40   inspectorSrc = '';
41   loadFinished = false;
42   pendingCallbacks: Map<number, any> = new Map();
43   pendingID = 0;
44   runningStatus = 'none';
45   suspended = false;
46   windowList: Electron.BrowserWindow[] = [];
47   inQuit = false;
48
49   constructor(options: RuntimeOption) {
50     if (options.launchMode == 'backgroundAtStartup') {
51       console.log('backgroundAtStartup');
52       this.preloadStatus = 'preload';
53     } else {
54       this.preloadStatus = 'none';
55     }
56     if (options.launchMode == 'backgroundExecution') {
57       console.log('backgroundExecution');
58       this.backgroundExecution = true;
59     } else {
60       this.backgroundExecution = false;
61     }
62     this.accessiblePath = wrt.tv?.getAccessiblePath();
63     this.isAlwaysReload = (wrt.tv ? wrt.tv.isAlwaysReload() : false);
64     this.multitaskingSupport = (wrt.tv ? wrt.tv.getMultitaskingSupport() : true);
65     this.defaultBackgroundColor = (wrt.tv ? '#0000' :
66         ((wrt.getPlatformType() === "product_wearable") ? '#000' : '#FFF'));
67     this.defaultTransparent = (wrt.tv ? true : false);
68
69     this.setupEventListener(options);
70
71     this.mainWindow = new WRTWindow(this.getWindowOption(options));
72     this.initDisplayDelay(true);
73     this.setupMainWindowEventListener();
74   }
75
76   private setupEventListener(options: RuntimeOption) {
77     app.on('browser-window-created', (event: any, window: any) => {
78       if (this.windowList.length > 0)
79         this.windowList[this.windowList.length - 1].hide();
80       this.windowList.push(window);
81       console.log(`window created : #${this.windowList.length}`);
82
83       window.on('closed', () => {
84         console.log(`window closed : #${this.windowList.length}`);
85         let index = this.windowList.indexOf(window);
86         this.windowList.splice(index, 1);
87         if (index === this.windowList.length && this.windowList.length > 0)
88           this.windowList[this.windowList.length - 1].show();
89       });
90     });
91
92     app.on('web-contents-created', (event: any, webContents: any) => {
93       webContents.on('crashed', function() {
94         console.error('webContents crashed');
95         app.exit(100);
96       });
97
98       webContents.session.setPermissionRequestHandler((webContents: any, permission: string, callback: any) => {
99         console.log(`handlePermissionRequests for ${permission}`);
100         if (permission === 'notifications') {
101           if (!this.notificationPermissionMap)
102             this.notificationPermissionMap = new Map();
103           else if (this.notificationPermissionMap.has(webContents)) {
104             process.nextTick(callback, this.notificationPermissionMap.get(webContents));
105             return;
106           }
107           const id = ++this.pendingID;
108           console.log(`Raising a notification permission request with id: ${id}`);
109           this.pendingCallbacks.set(id, (result: boolean) => {
110             (this.notificationPermissionMap as Map<Electron.WebContents, boolean>).set(webContents, result);
111             callback(result);
112           });
113           wrt.handleNotificationPermissionRequest(id, webContents);
114         } else if (permission === 'media') {
115           const id = ++this.pendingID;
116           console.log(`Raising a media permission request with id: ${id}`);
117           this.pendingCallbacks.set(id, callback);
118           wrt.handleMediaPermissionRequest(id, webContents);
119         } else if (permission === 'geolocation') {
120           const id = ++this.pendingID;
121           console.log(`Raising a geolocation permission request with id: ${id}`);
122           this.pendingCallbacks.set(id, callback);
123           wrt.handleGeolocationPermissionRequest(id, webContents);
124         } else {
125           /* electron by default allows permission for all if no request handler
126              is there; so granting permission only temporarily to not have any
127              side effects */
128           callback(true);
129         }
130       });
131     });
132
133     app.on('certificate-error', (event: any, webContents: any, url: string, error: string, certificate: any, callback: any) => {
134       console.log('A certificate error has occurred');
135       event.preventDefault();
136       if (certificate.data) {
137         const id = ++this.pendingID;
138         console.log(`Raising a certificate error response with id: ${id}`);
139         this.pendingCallbacks.set(id, callback);
140         wrt.handleCertificateError(id, webContents, certificate.data, url, error);
141       } else {
142         console.log('Certificate could not be opened');
143         callback(false);
144       }
145     });
146
147     app.on('login', (event: any, webContents: any, request: any, authInfo: any, callback: any) => {
148       console.log(`Login info is required, isproxy: ${authInfo.isProxy}`);
149       event.preventDefault();
150       let usrname = '';
151       let passwd = '';
152       if (wrt.tv && authInfo.isProxy) {
153         let vconfProxy = wrt.tv.getProxy();
154         if (vconfProxy) {
155           let proxyInfo = new URL(vconfProxy);
156           usrname = proxyInfo.username;
157           passwd = proxyInfo.password;
158         }
159         if (usrname && passwd) {
160           callback(usrname, passwd);
161         } else {
162           console.log('Login, but usrname and passwd is empty!!!');
163           callback('', '');
164         }
165       } else {
166         const id = ++this.pendingID;
167         console.log(`Raising a login info request with id: ${id}`);
168         this.pendingCallbacks.set(id, callback);
169         wrt.handleAuthRequest(id, webContents);
170       }
171     });
172
173     if (this.accessiblePath) {
174       console.log(`accessiblePath: ${this.accessiblePath}`);
175       protocol.interceptFileProtocol('file', (request: any, callback: any) => {
176         if (request.url) {
177           let parsed_info = new URL(request.url);
178           let access_path = parsed_info.host + decodeURI(parsed_info.pathname);
179           console.log(`check path: : ${access_path}`);
180           for (let path of (this.accessiblePath as string[])) {
181             if (access_path.startsWith(path)) {
182               callback(access_path);
183               return;
184             }
185           }
186           if (access_path.indexOf("/shared/res/") > -1) {
187             callback(access_path);
188             return;
189           }
190           else {
191             console.log(`invalid accesspath: ${access_path}`);
192             (callback as any)(403);
193           }
194         } else {
195           console.log('request url is empty');
196           (callback as any)(403);
197         }
198       }, (error: Error) => {
199         console.log(error);
200       });
201     }
202
203     wrt.on('permission-response', (event: any, id: number, result: boolean) => {
204       console.log(`permission-response for ${id} is ${result}`);
205       let callback = this.pendingCallbacks.get(id);
206       if (typeof callback === 'function') {
207         console.log('calling permission response callback');
208         callback(result);
209         this.pendingCallbacks.delete(id);
210       }
211     });
212
213     wrt.on('auth-response', (event: any, id: number, submit: boolean, user: string, password: string) => {
214       let callback = this.pendingCallbacks.get(id);
215       if (typeof callback === 'function') {
216         console.log('calling auth response callback');
217         if (submit)
218           callback(user, password);
219         else
220           callback();
221         this.pendingCallbacks.delete(id);
222       }
223     });
224
225     wrt.on('app-status-changed', (event: any, status: string) => {
226       console.log(`runningStatus: ${status}, ${this.loadFinished}`);
227       if (!wrt.tv)
228         return;
229       this.runningStatus = status;
230       if (this.runningStatus === 'DialogClose' && this.inspectorSrc) {
231         console.log(`runningStatus is DialogClose, src is ${this.inspectorSrc}`);
232         this.mainWindow.loadURL(this.inspectorSrc);
233         this.inspectorSrc = '';
234       } else if (this.runningStatus == 'behind' && this.loadFinished) {
235         // TODO : Need to care this situation and decide to pass the addon event emitter to suspend()
236         this.suspend();
237       }
238     });
239   }
240
241   private getWindowOption(options: RuntimeOption): NativeWRTjs.WRTWindowConstructorOptions {
242     return {
243       fullscreen: false,
244       backgroundColor: this.defaultBackgroundColor,
245       transparent: this.defaultTransparent,
246       show: false,
247       webPreferences: {
248         nodeIntegration: options.isAddonAvailable,
249         nodeIntegrationInWorker: false
250       },
251       webContents: WRTWebContents.create(),
252     };
253   }
254
255   private setupMainWindowEventListener() {
256     this.mainWindow.once('ready-to-show', () => {
257       console.log('mainWindow ready-to-show');
258       if (this.showTimer)
259         clearTimeout(this.showTimer);
260       wrt.hideSplashScreen(0);
261       this.firstRendered = true;
262       if (this.preloadStatus == 'preload') {
263         this.preloadStatus = 'readyToShow';
264         console.log('preloading show is skipped!');
265         return;
266       }
267       this.show();
268     });
269
270     this.mainWindow.webContents.on('did-start-loading', () => {
271       console.log('webContents did-start-loading');
272       this.loadFinished = false;
273     });
274
275     this.mainWindow.webContents.on('did-finish-load', () => {
276       console.log('webContents did-finish-load');
277       this.loadFinished = true;
278       wrt.hideSplashScreen(1);
279       if (wrt.isIMEWebApp()) {
280         this.activateIMEWebHelperClient();
281       } else if (wrt.tv) {
282         if (this.inspectorSrc)
283           this.showInspectorGuide();
284         else
285           this.suspendByStatus();
286       }
287       addonManager.emit('contentDidFinishLoad', this.mainWindow.id);
288     });
289   }
290
291   private initDisplayDelay(firstLaunch: boolean) {
292     // TODO: On 6.0, this causes a black screen on relaunch
293     if (firstLaunch)
294       this.firstRendered = false;
295     this.suspended = false;
296     if (this.showTimer)
297       clearTimeout(this.showTimer);
298     let splashShown = this.preloadStatus !== 'preload' && firstLaunch && wrt.showSplashScreen();
299     if (!splashShown && !wrt.tv) {
300       this.showTimer = setTimeout(() => {
301         if (!this.suspended) {
302           console.log('FrameRendered not obtained from engine. To show window, timer fired');
303           this.mainWindow.emit('ready-to-show');
304         }
305       }, 2000);
306     }
307     if (!firstLaunch && !this.backgroundRunnable())
308       this.mainWindow.setEnabled(true);
309   }
310
311   private backgroundRunnable(): boolean {
312     return this.backgroundSupport || this.backgroundExecution;
313   }
314
315   handleAppControlReload(url: string) {
316     console.log('WebApplication : handleAppControlReload');
317     this.closeWindows();
318     this.initDisplayDelay(false);
319     this.mainWindow.loadURL(url);
320   }
321
322   private suspendByStatus() {
323     if (this.preloadStatus === 'readyToShow' ||
324         this.preloadStatus === 'preload' ||
325         this.runningStatus === 'behind') {
326       console.log('WebApplication : suspendByStatus');
327       console.log(`preloadStatus: ${this.preloadStatus}, runningStatus: ${this.runningStatus}`);
328       // TODO : Need to care this situation and decide to pass the addon event emitter to suspend()
329       this.suspend();
330       if (this.runningStatus !== 'behind')
331         (wrt.tv as NativeWRTjs.TVExtension).notifyAppStatus('preload');
332     }
333   }
334
335   private showInspectorGuide() {
336     console.log('WebApplication : showInspectorGuide');
337     this.showInspectorGuide = () => {}; // call once
338     const message = `${this.debugPort.toString()}
339 Fast RWI is used, [about:blank] is loaded fist instead of
340 [${this.inspectorSrc}]
341 Click OK button will start the real loading.
342 Notes:
343 Please connect to RWI in PC before click OK button.
344 Then you can get network log from the initial loading.
345 Please click Record button in Timeline panel in PC before click OK button,
346 Then you can get profile log from the initial loading.`;
347     let tv = wrt.tv as NativeWRTjs.TVExtension;
348     tv.showDialog(this.mainWindow.webContents, message);
349
350     if (this.preloadStatus !== 'none') {
351       setTimeout(() => {
352         tv.cancelDialogs(this.mainWindow.webContents);
353       }, 5000);
354     }
355   }
356
357   suspend() {
358     if (this.suspended || this.inQuit)
359       return;
360     console.log('WebApplication : suspend');
361     addonManager.emit('lcSuspend', this.mainWindow.id);
362     this.suspended = true;
363     this.windowList[this.windowList.length - 1].hide();
364     this.flushData();
365     if (!this.backgroundRunnable()) {
366       if (!this.multitaskingSupport) {
367         // FIXME : terminate app after visibilitychange event handling
368         setTimeout(() => {
369           console.log('multitasking is not supported; quitting app')
370           app.quit();
371         }, 1000);
372       } else {
373         this.windowList.forEach((window) => window.setEnabled(false));
374       }
375     }
376   }
377
378   resume() {
379     console.log('WebApplication : resume');
380     this.suspended = false;
381     addonManager.emit('lcResume', this.mainWindow.id);
382
383     if (!this.firstRendered) {
384       console.log('WebApplication : resume firstRendered is false');
385       return;
386     }
387     if (!this.backgroundRunnable())
388       this.windowList.forEach((window) => window.setEnabled(true));
389     this.windowList[this.windowList.length - 1].show();
390   }
391
392   quit() {
393     console.log('WebApplication : quit');
394     this.flushData();
395     this.windowList.forEach((window) => window.removeAllListeners());
396     this.inQuit = false;
397     if (!this.suspended)
398       this.suspend();
399   }
400
401   beforeQuit() {
402     console.log('WebApplication : beforeQuit');
403     addonManager.emit('lcQuit', this.mainWindow.id);
404     if (wrt.tv) {
405       this.inspectorSrc = '';
406       wrt.tv.cancelDialogs(this.mainWindow.webContents);
407     }
408     if (this.debugPort) {
409       console.log('stop inspector server');
410       this.debugPort = 0;
411       wrt.stopInspectorServer();
412     }
413     this.inQuit = true;
414   }
415
416   private flushData() {
417     console.log('WebApplication : FlushData');
418     this.windowList.forEach((window) => window.webContents.session.flushStorageData());
419   }
420
421   sendAppControlEvent() {
422     const kAppControlEventScript = `(function(){
423   var __event = document.createEvent("CustomEvent");
424   __event.initCustomEvent("appcontrol", true, true, null);
425   document.dispatchEvent(__event);
426   for (var i=0; i < window.frames.length; i++)
427     window.frames[i].document.dispatchEvent(__event);
428 })()`;
429     wrt.executeJS(this.mainWindow.webContents, kAppControlEventScript);
430   }
431
432   private activateIMEWebHelperClient() {
433     console.log('webApplication : activateIMEWebHelperClient');
434     const kImeActivateFunctionCallScript =
435         '(function(){WebHelperClient.impl.activate();})()';
436     wrt.executeJS(this.mainWindow.webContents, kImeActivateFunctionCallScript);
437   }
438
439   show() {
440     console.log('WebApplication : show');
441     this.preloadStatus = 'none';
442     if (this.backgroundExecution) {
443       console.log('skip showing while backgroundExecution mode');
444     } else if (!this.mainWindow.isVisible()) {
445       console.log('show window');
446       this.mainWindow.show();
447     }
448   }
449
450   private closeWindows() {
451     wrt.tv?.clearSurface(this.mainWindow.webContents);
452     this.windowList.forEach((window) => {
453       if (window != this.mainWindow)
454         window.destroy();
455     });
456   }
457
458   keyEvent(key: string) {
459     console.log(`WebApplication : keyEvent[${key}]`);
460     switch(key) {
461       case "ArrowUp":
462       case "Up":
463         addonManager.emit('hwUpkey', this.mainWindow.id);
464         break;
465       case "ArrowDown":
466       case "Down":
467         addonManager.emit('hwDownkey', this.mainWindow.id);
468         break;
469       default:
470         console.log('No handler for ' + key);
471         break;
472     }
473   }
474
475   prelaunch(url: string) {
476     console.log('WebApplication : prelaunch');
477     addonManager.emit('lcPrelaunch', this.mainWindow.id, url);
478   }
479
480   lowMemory() {
481     console.log('WebApplication : lowMemory to clearcache');
482     if (!wrt.tv)
483       return;
484     this.windowList.forEach((window) => {
485       //clear webframe cache
486       (wrt.tv as NativeWRTjs.TVExtension).clearWebCache(window.webContents);
487       window.webContents.session.clearCache(function() {
488         console.log('clear session Cache complete');
489       })
490     });
491   }
492
493   ambientTick() {
494     const kAmbientTickEventScript = `(function(){
495   var __event = document.createEvent("CustomEvent");
496   __event.initCustomEvent("timetick", true, true);
497   document.dispatchEvent(__event);
498   for (var i=0; i < window.frames.length; i++)
499     window.frames[i].document.dispatchEvent(__event);
500 })()`;
501     wrt.executeJS(this.mainWindow.webContents, kAmbientTickEventScript);
502   }
503
504   ambientChanged(ambient_mode: boolean) {
505     const kAmbientChangedEventScript = `(function(){
506   var __event = document.createEvent(\"CustomEvent\");
507   var __detail = {};
508   __event.initCustomEvent(\"ambientmodechanged\",true,true,__detail);
509   __event.detail.ambientMode = ${ambient_mode ? 'true' : 'false'};
510   document.dispatchEvent(__event);
511   for (var i=0; i < window.frames.length; i++)
512     window.frames[i].document.dispatchEvent(__event);
513 })()`;
514     wrt.executeJS(this.mainWindow.webContents, kAmbientChangedEventScript);
515   }
516 }