[WRTjs] If app is quiting, don't call show app
[platform/framework/web/wrtjs.git] / wrt_app / src / web_application.ts
1 // @ts-nocheck
2
3 /*
4  * Copyright (c) 2019 Samsung Electronics Co., Ltd All Rights Reserved
5  *
6  *    Licensed under the Apache License, Version 2.0 (the "License");
7  *    you may not use this file except in compliance with the License.
8  *    You may obtain a copy of the License at
9  *
10  *        http://www.apache.org/licenses/LICENSE-2.0
11  *
12  *    Unless required by applicable law or agreed to in writing, software
13  *    distributed under the License is distributed on an "AS IS" BASIS,
14  *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  *    See the License for the specific language governing permissions and
16  *    limitations under the License.
17  */
18
19 'use strict';
20
21 import { BrowserWindow, app, session } from 'electron';
22 import { wrt } from '../browser/wrt';
23 import { addonManager } from './addon_manager';
24 import { WebApplicationDelegate } from '../common/web_application_delegate';
25 import { WebApplicationDelegateTV } from './tv/web_application_tv';
26
27 export class WebApplication {
28   defaultBackgroundColor: string = (wrt.getPlatformType() === "product_wearable") ? '#000' : '#FFF';
29   defaultTransparent: boolean = false;
30   mainWindow: Electron.BrowserWindow;
31   multitaskingSupport: boolean = true;
32   notificationPermissionMap?: Map<Electron.WebContents, boolean>;
33   showTimer?: NodeJS.Timeout;
34
35   backgroundSupport: boolean = wrt.getBackgroundSupport();
36   debugPort: number = 0;
37   contentSrc: string = '';
38   loadFinished: boolean = false;
39   pendingCallbacks: Map<number, any> = new Map();
40   pendingID: number = 0;
41   suspended: boolean = false;
42   windowList: Electron.BrowserWindow[] = [];
43   inQuit: boolean = false;
44   profileDelegate: WebApplicationDelegate;
45   splashShown: boolean = false;
46   reload: boolean = false;
47
48   constructor(options: RuntimeOption) {
49     if (wrt.tv) {
50       this.profileDelegate = new WebApplicationDelegateTV(this);
51       this.profileDelegate.initialize(options);
52     } else {
53       this.profileDelegate = new WebApplicationDelegate(this);
54     }
55     this.setupEventListener(options);
56     this.mainWindow = new BrowserWindow(this.getWindowOption(options));
57     this.initDisplayDelay();
58     this.setupMainWindowEventListener();
59   }
60
61   private setupEventListener(options: RuntimeOption) {
62     app.on('browser-window-created', (event: any, window: any) => {
63       if (this.windowList.length > 0)
64         this.windowList[this.windowList.length - 1].hide();
65       this.windowList.push(window);
66       console.log(`window created : #${this.windowList.length}`);
67
68       window.on('closed', () => {
69         console.log(`window closed : #${this.windowList.length}`);
70         let index = this.windowList.indexOf(window);
71         this.windowList.splice(index, 1);
72         if (!this.inQuit && index === this.windowList.length && this.windowList.length > 0) {
73           let lastWindow = this.windowList[this.windowList.length - 1];
74           lastWindow.show();
75           this.profileDelegate.focus(lastWindow.webContents);
76         }
77       });
78     });
79
80     app.on('web-contents-created', (event: any, webContents: any) => {
81       webContents.on('crashed', function() {
82         console.error('webContents crashed');
83         app.exit(100);
84       });
85
86       webContents.session.setPermissionRequestHandler((webContents: any, permission: string, callback: any) => {
87         console.log(`handlePermissionRequests for ${permission}`);
88         if (permission === 'notifications') {
89           if (!this.notificationPermissionMap)
90             this.notificationPermissionMap = new Map();
91           else if (this.notificationPermissionMap.has(webContents)) {
92             process.nextTick(callback, this.notificationPermissionMap.get(webContents));
93             return;
94           }
95           const id = ++this.pendingID;
96           console.log(`Raising a notification permission request with id: ${id}`);
97           this.pendingCallbacks.set(id, (result: boolean) => {
98             (this.notificationPermissionMap as Map<Electron.WebContents, boolean>).set(webContents, result);
99             callback(result);
100           });
101           wrt.handleNotificationPermissionRequest(id, webContents);
102         } else if (permission === 'media') {
103           const id = ++this.pendingID;
104           console.log(`Raising a media permission request with id: ${id}`);
105           this.pendingCallbacks.set(id, callback);
106           wrt.handleMediaPermissionRequest(id, webContents);
107         } else if (permission === 'geolocation') {
108           const id = ++this.pendingID;
109           console.log(`Raising a geolocation permission request with id: ${id}`);
110           this.pendingCallbacks.set(id, callback);
111           wrt.handleGeolocationPermissionRequest(id, webContents);
112         } else {
113           /* electron by default allows permission for all if no request handler
114              is there; so granting permission only temporarily to not have any
115              side effects */
116           callback(true);
117         }
118       });
119     });
120
121     app.on('certificate-error', (event: any, webContents: any, url: string, error: string, certificate: any, callback: any) => {
122       console.log('A certificate error has occurred');
123       event.preventDefault();
124       if (certificate.data) {
125         const id = ++this.pendingID;
126         console.log(`Raising a certificate error response with id: ${id}`);
127         this.pendingCallbacks.set(id, callback);
128         wrt.handleCertificateError(id, webContents, certificate.data, url, error);
129       } else {
130         console.log('Certificate could not be opened');
131         callback(false);
132       }
133     });
134
135     app.on('login', (event: any, webContents: any, request: any, authInfo: any, callback: any) => {
136       console.log(`Login info is required, isproxy: ${authInfo.isProxy}`);
137       event.preventDefault();
138       if (!this.profileDelegate.handleProxyInfo(authInfo, callback)) {
139         const id = ++this.pendingID;
140         console.log(`Raising a login info request with id: ${id}`);
141         this.pendingCallbacks.set(id, callback);
142         wrt.handleAuthRequest(id, webContents);
143       }
144     });
145
146     wrt.on('permission-response', (event: any, id: number, result: boolean) => {
147       console.log(`permission-response for ${id} is ${result}`);
148       let callback = this.pendingCallbacks.get(id);
149       if (typeof callback === 'function') {
150         console.log('calling permission response callback');
151         callback(result);
152         this.pendingCallbacks.delete(id);
153       }
154     });
155
156     wrt.on('auth-response', (event: any, id: number, submit: boolean, user: string, password: string) => {
157       let callback = this.pendingCallbacks.get(id);
158       if (typeof callback === 'function') {
159         console.log('calling auth response callback');
160         if (submit)
161           callback(user, password);
162         else
163           callback();
164         this.pendingCallbacks.delete(id);
165       }
166     });
167   }
168
169   private getWindowOption(options: RuntimeOption): Electron.BrowserWindowConstructorOptions {
170     return {
171       fullscreen: false,
172       backgroundColor: this.defaultBackgroundColor,
173       transparent: this.defaultTransparent,
174       show: false,
175       webPreferences: {
176         contextIsolation: options.isAddonAvailable,
177         nodeIntegration: options.isAddonAvailable,
178         nodeIntegrationInSubFrames: options.isAddonAvailable,
179         nodeIntegrationInWorker: false,
180         nativeWindowOpen: true,
181       },
182     };
183   }
184
185   hideSplashScreen(reason: string) {
186     switch (reason) {
187       case 'first-paint': {
188         if (wrt.hideSplashScreen(0) !== false)
189           this.show();
190         break;
191       }
192       case 'complete': {
193         if (wrt.hideSplashScreen(1) !== false)
194           this.show();
195         break;
196       }
197       case 'custom': {
198         if (wrt.hideSplashScreen(2) !== false)
199           this.show();
200         break;
201       }
202       case 'video-finished': {
203         this.show();
204         break;
205       }
206       default:
207         break;
208     }
209   }
210
211   private setupMainWindowEventListener() {
212     this.mainWindow.once('ready-to-show', () => {
213       if (this.inQuit)
214         return;
215
216       console.log('mainWindow ready-to-show');
217       if (this.showTimer)
218         clearTimeout(this.showTimer);
219
220       if (this.splashShown)
221         this.hideSplashScreen('first-paint');
222       else
223         this.show();
224     });
225
226     this.mainWindow.webContents.on('did-start-loading', () => {
227       console.log('webContents did-start-loading');
228       this.loadFinished = false;
229     });
230
231     this.mainWindow.webContents.on('did-finish-load', () => {
232       console.log(`webContents did-finish-load, window length is ${this.windowList.length}`);
233       this.loadFinished = true;
234
235       if (!this.windowList.length)
236         return;
237       if (this.splashShown)
238         this.hideSplashScreen('complete');
239
240       addonManager.emit('contentDidFinishLoad', this.mainWindow.id);
241       if (wrt.isIMEWebApp()) {
242         this.activateIMEWebHelperClient();
243       } else {
244         this.profileDelegate.onDidFinishLoad();
245       }
246     });
247   }
248
249   private enableWindow() {
250     this.suspended = false;
251     // TODO: On 6.0, this causes a black screen on relaunch
252     if (this.showTimer)
253       clearTimeout(this.showTimer);
254     this.mainWindow.setEnabled(true);
255   }
256
257   private initDisplayDelay() {
258     if (this.profileDelegate.isBackgroundLaunch())
259       return;
260
261     this.splashShown = wrt.showSplashScreen();
262     if (this.splashShown || !this.profileDelegate.needShowTimer())
263       return;
264
265     this.showTimer = setTimeout(() => {
266       if (!this.suspended) {
267         console.log('FrameRendered not obtained from engine. To show window, timer fired');
268         this.mainWindow.emit('ready-to-show');
269       }
270     }, 2000);
271   }
272
273   handleAppControlEvent(appControl: any) {
274     if (!this.profileDelegate.handleAppControlEvent(appControl)) {
275       return;
276     }
277
278     let loadInfo = appControl.getLoadInfo();
279     let src = loadInfo.getSrc();
280     this.reload = loadInfo.getReload() || this.profileDelegate.needReload(src);
281     // handle http://tizen.org/appcontrol/operation/main operation specially.
282     // only menu-screen app can send launch request with main operation.
283     // in this case, web app should have to resume web app not reset.
284     if (this.reload && appControl.getOperation() == 'http://tizen.org/appcontrol/operation/main')
285       this.reload = false;
286     if (this.reload)
287       this.handleAppControlReload(src);
288     else
289       this.sendAppControlEvent();
290   }
291
292   private launchInspectorIfNeeded(appControl: NativeWRTjs.AppControl) {
293     console.log('launchInspectorIfNeeded');
294     let needInpectorGuide = this.profileDelegate.needInpectorGuide();
295     let hasAulDebug = (appControl.getData('__AUL_DEBUG__') === '1');
296
297     if (hasAulDebug || needInpectorGuide) {
298       let debugPort = wrt.startInspectorServer();
299       let data = { "port": [debugPort.toString()] };
300       this.debugPort = debugPort;
301       appControl.reply(data);
302     }
303   }
304
305   loadUrl(appControl: NativeWRTjs.AppControl) {
306     this.contentSrc = appControl.getLoadInfo().getSrc();
307     this.launchInspectorIfNeeded(appControl);
308     this.mainWindow.loadURL(this.contentSrc);
309     this.prelaunch(this.contentSrc);
310     if (wrt.da) {
311       this.mainWindow.emit('ready-to-show');
312     }
313   }
314
315   private isPausable() {
316     return !this.backgroundSupport && !this.profileDelegate.canIgnoreSuspend();
317   }
318
319   suspend() {
320     if (this.suspended || this.inQuit)
321       return;
322     console.log('WebApplication : suspend');
323     this.suspended = true;
324     if (this.windowList.length > 0) {
325       addonManager.emit('lcSuspend', this.mainWindow.id);
326       this.windowList[this.windowList.length - 1].hide();
327     }
328     if (this.isPausable()) {
329       this.windowList.forEach((window) => window.setEnabled(false));
330       if (!this.multitaskingSupport && !this.profileDelegate.isBackgroundLaunch()) {
331         setTimeout(() => {
332           console.log('multitasking is not supported; quitting app')
333           app.quit();
334         }, 0);
335       }
336     }
337     this.flushData();
338   }
339
340   resume() {
341     console.log('WebApplication : resume');
342     this.suspended = false;
343     addonManager.emit('lcResume', this.mainWindow.id, this.reload);
344     this.reload = false;
345
346     this.windowList.forEach((window) => window.setEnabled(true));
347     this.windowList[this.windowList.length - 1].show();
348   }
349
350   quit() {
351     console.log('WebApplication : quit');
352     this.windowList.forEach((window) => {
353       window.removeAllListeners();
354       window.setEnabled(false);
355     });
356     this.flushData();
357     this.inQuit = false;
358   }
359
360   beforeQuit() {
361     console.log('WebApplication : beforeQuit');
362     this.profileDelegate.beforeQuit();
363     addonManager.emit('lcQuit', this.mainWindow.id);
364     if (this.debugPort) {
365       console.log('stop inspector server');
366       this.debugPort = 0;
367       wrt.stopInspectorServer();
368     }
369     this.inQuit = true;
370   }
371
372   private handleAppControlReload(url: string) {
373     console.log('WebApplication : handleAppControlReload');
374     this.closeWindows();
375     this.enableWindow();
376     this.mainWindow.loadURL(url);
377   }
378
379   private flushData() {
380     console.log('WebApplication : FlushData');
381     session.defaultSession?.flushStorageData();
382   }
383
384   sendAppControlEvent() {
385     const kAppControlEventScript = `(function(){
386   var __event = document.createEvent("CustomEvent");
387   __event.initCustomEvent("appcontrol", true, true, null);
388   document.dispatchEvent(__event);
389   for (var i=0; i < window.frames.length; i++)
390     window.frames[i].document.dispatchEvent(__event);
391 })()`;
392     wrt.executeJS(this.mainWindow.webContents, kAppControlEventScript);
393   }
394
395   private activateIMEWebHelperClient() {
396     console.log('webApplication : activateIMEWebHelperClient');
397     const kImeActivateFunctionCallScript =
398         '(function(){WebHelperClient.impl.activate();})()';
399     wrt.executeJS(this.mainWindow.webContents, kImeActivateFunctionCallScript);
400   }
401
402   show() {
403     if (this.profileDelegate.isBackgroundLaunch()) {
404       console.log('show() will be skipped by background launch');
405       return;
406     }
407     console.log('WebApplication : show');
408     if (!this.mainWindow.isVisible()) {
409       console.log(`show this.windowList.length : ${this.windowList.length}`);
410       this.mainWindow.show();
411       if (this.windowList.length > 1) {
412         this.windowList[this.windowList.length - 1].moveTop();
413       }
414     }
415   }
416
417   private closeWindows() {
418     this.profileDelegate.clearSurface(this.mainWindow.webContents);
419     this.windowList.slice().forEach((window) => {
420       if (window != this.mainWindow)
421         window.destroy();
422     });
423   }
424
425   keyEvent(key: string) {
426     console.log(`WebApplication : keyEvent[${key}]`);
427     switch(key) {
428       case "ArrowUp":
429       case "Up":
430         addonManager.emit('hwUpkey', this.mainWindow.id);
431         break;
432       case "ArrowDown":
433       case "Down":
434         addonManager.emit('hwDownkey', this.mainWindow.id);
435         break;
436       default:
437         console.log('No handler for ' + key);
438         break;
439     }
440   }
441
442   prelaunch(url: string) {
443     console.log('WebApplication : prelaunch');
444     addonManager.emit('lcPrelaunch', this.mainWindow.id, url);
445   }
446
447   clearCache() {
448     this.profileDelegate.clearCache();
449   }
450
451   ambientTick() {
452     const kAmbientTickEventScript = `(function(){
453   var __event = document.createEvent("CustomEvent");
454   __event.initCustomEvent("timetick", true, true);
455   document.dispatchEvent(__event);
456   for (var i=0; i < window.frames.length; i++)
457     window.frames[i].document.dispatchEvent(__event);
458 })()`;
459     wrt.executeJS(this.mainWindow.webContents, kAmbientTickEventScript);
460   }
461
462   ambientChanged(ambient_mode: boolean) {
463     const kAmbientChangedEventScript = `(function(){
464   var __event = document.createEvent(\"CustomEvent\");
465   var __detail = {};
466   __event.initCustomEvent(\"ambientmodechanged\",true,true,__detail);
467   __event.detail.ambientMode = ${ambient_mode ? 'true' : 'false'};
468   document.dispatchEvent(__event);
469   for (var i=0; i < window.frames.length; i++)
470     window.frames[i].document.dispatchEvent(__event);
471 })()`;
472     wrt.executeJS(this.mainWindow.webContents, kAmbientChangedEventScript);
473   }
474 }