ff110c66e23f9e35336edc1d6dcf820492517d6e
[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       console.log('mainWindow ready-to-show');
214       if (this.showTimer)
215         clearTimeout(this.showTimer);
216
217       if (this.splashShown)
218         this.hideSplashScreen('first-paint');
219       else
220         this.show();
221     });
222
223     this.mainWindow.webContents.on('did-start-loading', () => {
224       console.log('webContents did-start-loading');
225       this.loadFinished = false;
226     });
227
228     this.mainWindow.webContents.on('did-finish-load', () => {
229       console.log(`webContents did-finish-load, window length is ${this.windowList.length}`);
230       this.loadFinished = true;
231
232       if (!this.windowList.length)
233         return;
234       if (this.splashShown)
235         this.hideSplashScreen('complete');
236
237       addonManager.emit('contentDidFinishLoad', this.mainWindow.id);
238       if (wrt.isIMEWebApp()) {
239         this.activateIMEWebHelperClient();
240       } else {
241         this.profileDelegate.onDidFinishLoad();
242       }
243     });
244   }
245
246   private enableWindow() {
247     this.suspended = false;
248     // TODO: On 6.0, this causes a black screen on relaunch
249     if (this.showTimer)
250       clearTimeout(this.showTimer);
251     this.mainWindow.setEnabled(true);
252   }
253
254   private initDisplayDelay() {
255     if (this.profileDelegate.isBackgroundLaunch())
256       return;
257
258     this.splashShown = wrt.showSplashScreen();
259     if (this.splashShown || !this.profileDelegate.needShowTimer())
260       return;
261
262     this.showTimer = setTimeout(() => {
263       if (!this.suspended) {
264         console.log('FrameRendered not obtained from engine. To show window, timer fired');
265         this.mainWindow.emit('ready-to-show');
266       }
267     }, 2000);
268   }
269
270   handleAppControlEvent(appControl: any) {
271     if (!this.profileDelegate.handleAppControlEvent(appControl)) {
272       return;
273     }
274
275     let loadInfo = appControl.getLoadInfo();
276     let src = loadInfo.getSrc();
277     this.reload = loadInfo.getReload() || this.profileDelegate.needReload(src);
278     // handle http://tizen.org/appcontrol/operation/main operation specially.
279     // only menu-screen app can send launch request with main operation.
280     // in this case, web app should have to resume web app not reset.
281     if (this.reload && appControl.getOperation() == 'http://tizen.org/appcontrol/operation/main')
282       this.reload = false;
283     if (this.reload)
284       this.handleAppControlReload(src);
285     else
286       this.sendAppControlEvent();
287   }
288
289   private launchInspectorIfNeeded(appControl: NativeWRTjs.AppControl) {
290     console.log('launchInspectorIfNeeded');
291     let needInpectorGuide = this.profileDelegate.needInpectorGuide();
292     let hasAulDebug = (appControl.getData('__AUL_DEBUG__') === '1');
293
294     if (hasAulDebug || needInpectorGuide) {
295       let debugPort = wrt.startInspectorServer();
296       let data = { "port": [debugPort.toString()] };
297       this.debugPort = debugPort;
298       appControl.reply(data);
299     }
300   }
301
302   loadUrl(appControl: NativeWRTjs.AppControl) {
303     this.contentSrc = appControl.getLoadInfo().getSrc();
304     this.launchInspectorIfNeeded(appControl);
305     this.mainWindow.loadURL(this.contentSrc);
306     this.prelaunch(this.contentSrc);
307     if (wrt.da) {
308       this.mainWindow.emit('ready-to-show');
309     }
310   }
311
312   private isPausable() {
313     return !this.backgroundSupport && !this.profileDelegate.canIgnoreSuspend();
314   }
315
316   suspend() {
317     if (this.suspended || this.inQuit)
318       return;
319     console.log('WebApplication : suspend');
320     this.suspended = true;
321     if (this.windowList.length > 0) {
322       addonManager.emit('lcSuspend', this.mainWindow.id);
323       this.windowList[this.windowList.length - 1].hide();
324     }
325     if (this.isPausable()) {
326       this.windowList.forEach((window) => window.setEnabled(false));
327       if (!this.multitaskingSupport && !this.profileDelegate.isBackgroundLaunch()) {
328         setTimeout(() => {
329           console.log('multitasking is not supported; quitting app')
330           app.quit();
331         }, 0);
332       }
333     }
334     this.flushData();
335   }
336
337   resume() {
338     console.log('WebApplication : resume');
339     this.suspended = false;
340     addonManager.emit('lcResume', this.mainWindow.id, this.reload);
341     this.reload = false;
342
343     this.windowList.forEach((window) => window.setEnabled(true));
344     this.windowList[this.windowList.length - 1].show();
345   }
346
347   quit() {
348     console.log('WebApplication : quit');
349     this.windowList.forEach((window) => {
350       window.removeAllListeners();
351       window.setEnabled(false);
352     });
353     this.flushData();
354     this.inQuit = false;
355   }
356
357   beforeQuit() {
358     console.log('WebApplication : beforeQuit');
359     this.profileDelegate.beforeQuit();
360     addonManager.emit('lcQuit', this.mainWindow.id);
361     if (this.debugPort) {
362       console.log('stop inspector server');
363       this.debugPort = 0;
364       wrt.stopInspectorServer();
365     }
366     this.inQuit = true;
367   }
368
369   private handleAppControlReload(url: string) {
370     console.log('WebApplication : handleAppControlReload');
371     this.closeWindows();
372     this.enableWindow();
373     this.mainWindow.loadURL(url);
374   }
375
376   private flushData() {
377     console.log('WebApplication : FlushData');
378     session.defaultSession?.flushStorageData();
379   }
380
381   sendAppControlEvent() {
382     const kAppControlEventScript = `(function(){
383   var __event = document.createEvent("CustomEvent");
384   __event.initCustomEvent("appcontrol", true, true, null);
385   document.dispatchEvent(__event);
386   for (var i=0; i < window.frames.length; i++)
387     window.frames[i].document.dispatchEvent(__event);
388 })()`;
389     wrt.executeJS(this.mainWindow.webContents, kAppControlEventScript);
390   }
391
392   private activateIMEWebHelperClient() {
393     console.log('webApplication : activateIMEWebHelperClient');
394     const kImeActivateFunctionCallScript =
395         '(function(){WebHelperClient.impl.activate();})()';
396     wrt.executeJS(this.mainWindow.webContents, kImeActivateFunctionCallScript);
397   }
398
399   show() {
400     if (this.profileDelegate.isBackgroundLaunch()) {
401       console.log('show() will be skipped by background launch');
402       return;
403     }
404     console.log('WebApplication : show');
405     if (!this.mainWindow.isVisible()) {
406       console.log(`show this.windowList.length : ${this.windowList.length}`);
407       this.mainWindow.show();
408       if (this.windowList.length > 1) {
409         this.windowList[this.windowList.length - 1].moveTop();
410       }
411     }
412   }
413
414   private closeWindows() {
415     this.profileDelegate.clearSurface(this.mainWindow.webContents);
416     this.windowList.slice().forEach((window) => {
417       if (window != this.mainWindow)
418         window.destroy();
419     });
420   }
421
422   keyEvent(key: string) {
423     console.log(`WebApplication : keyEvent[${key}]`);
424     switch(key) {
425       case "ArrowUp":
426       case "Up":
427         addonManager.emit('hwUpkey', this.mainWindow.id);
428         break;
429       case "ArrowDown":
430       case "Down":
431         addonManager.emit('hwDownkey', this.mainWindow.id);
432         break;
433       default:
434         console.log('No handler for ' + key);
435         break;
436     }
437   }
438
439   prelaunch(url: string) {
440     console.log('WebApplication : prelaunch');
441     addonManager.emit('lcPrelaunch', this.mainWindow.id, url);
442   }
443
444   clearCache() {
445     this.profileDelegate.clearCache();
446   }
447
448   ambientTick() {
449     const kAmbientTickEventScript = `(function(){
450   var __event = document.createEvent("CustomEvent");
451   __event.initCustomEvent("timetick", true, true);
452   document.dispatchEvent(__event);
453   for (var i=0; i < window.frames.length; i++)
454     window.frames[i].document.dispatchEvent(__event);
455 })()`;
456     wrt.executeJS(this.mainWindow.webContents, kAmbientTickEventScript);
457   }
458
459   ambientChanged(ambient_mode: boolean) {
460     const kAmbientChangedEventScript = `(function(){
461   var __event = document.createEvent(\"CustomEvent\");
462   var __detail = {};
463   __event.initCustomEvent(\"ambientmodechanged\",true,true,__detail);
464   __event.detail.ambientMode = ${ambient_mode ? 'true' : 'false'};
465   document.dispatchEvent(__event);
466   for (var i=0; i < window.frames.length; i++)
467     window.frames[i].document.dispatchEvent(__event);
468 })()`;
469     wrt.executeJS(this.mainWindow.webContents, kAmbientChangedEventScript);
470   }
471 }