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