aa020c09821662b4caf187db3636c6868691a135
[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, session } 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 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 WRTWindow(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): NativeWRTjs.WRTWindowConstructorOptions {
170     return {
171       fullscreen: false,
172       backgroundColor: this.defaultBackgroundColor,
173       transparent: this.defaultTransparent,
174       show: false,
175       webPreferences: {
176         nodeIntegration: options.isAddonAvailable,
177         nodeIntegrationInSubFrames: options.isAddonAvailable,
178         nodeIntegrationInWorker: false,
179         nativeWindowOpen: true,
180       },
181       webContents: WRTWebContents.create(),
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');
230       this.loadFinished = true;
231       if (this.splashShown)
232         this.hideSplashScreen('complete');
233
234       addonManager.emit('contentDidFinishLoad', this.mainWindow.id);
235       if (wrt.isIMEWebApp()) {
236         this.activateIMEWebHelperClient();
237       } else {
238         this.profileDelegate.onDidFinishLoad();
239       }
240     });
241   }
242
243   private enableWindow() {
244     this.suspended = false;
245     // TODO: On 6.0, this causes a black screen on relaunch
246     if (this.showTimer)
247       clearTimeout(this.showTimer);
248     if (!this.backgroundRunnable())
249       this.mainWindow.setEnabled(true);
250   }
251
252   private initDisplayDelay() {
253     if (this.profileDelegate.isPreloading())
254       return;
255
256     this.splashShown = wrt.showSplashScreen();
257     if (this.splashShown || !this.profileDelegate.needShowTimer())
258       return;
259
260     this.showTimer = setTimeout(() => {
261       if (!this.suspended) {
262         console.log('FrameRendered not obtained from engine. To show window, timer fired');
263         this.mainWindow.emit('ready-to-show');
264       }
265     }, 2000);
266   }
267
268   private backgroundRunnable() {
269     return this.backgroundSupport || this.profileDelegate.backgroundExecutableLaunchMode();
270   }
271
272   handleAppControlEvent(appControl: any) {
273     if (!this.profileDelegate.handleAppControlEvent(appControl)) {
274       return;
275     }
276
277     let loadInfo = appControl.getLoadInfo();
278     let src = loadInfo.getSrc();
279     this.reload = loadInfo.getReload() || this.profileDelegate.needReload(src);
280     // handle http://tizen.org/appcontrol/operation/main operation specially.
281     // only menu-screen app can send launch request with main operation.
282     // in this case, web app should have to resume web app not reset.
283     if (this.reload && appControl.getOperation() == 'http://tizen.org/appcontrol/operation/main')
284       this.reload = false;
285     if (this.reload)
286       this.handleAppControlReload(src);
287     else
288       this.sendAppControlEvent();
289   }
290
291   private launchInspectorIfNeeded(appControl: NativeWRTjs.AppControl) {
292     console.log('launchInspectorIfNeeded');
293     let needInpectorGuide = this.profileDelegate.needInpectorGuide();
294     let hasAulDebug = (appControl.getData('__AUL_DEBUG__') === '1');
295
296     if (hasAulDebug || needInpectorGuide) {
297       let debugPort = wrt.startInspectorServer();
298       let data = { "port": [debugPort.toString()] };
299       this.debugPort = debugPort;
300       appControl.reply(data);
301     }
302   }
303
304   loadUrl(appControl: NativeWRTjs.AppControl) {
305     this.contentSrc = appControl.getLoadInfo().getSrc();
306     this.launchInspectorIfNeeded(appControl);
307     this.mainWindow.loadURL(this.contentSrc);
308     this.prelaunch(this.contentSrc);
309     if (wrt.da) {
310       this.mainWindow.emit('ready-to-show');
311     }
312   }
313
314   private isPausable() {
315     return !this.profileDelegate.canIgnoreSuspend() && !this.backgroundRunnable();
316   }
317
318   suspend() {
319     if (this.suspended || this.inQuit)
320       return;
321     console.log('WebApplication : suspend');
322     this.suspended = true;
323     if (this.windowList.length > 0) {
324       addonManager.emit('lcSuspend', this.mainWindow.id);
325       this.windowList[this.windowList.length - 1].hide();
326     }
327     if (this.isPausable()) {
328       this.windowList.forEach((window) => window.setEnabled(false));
329       if (!this.multitaskingSupport && !this.profileDelegate.isPreloading()) {
330         setTimeout(() => {
331           console.log('multitasking is not supported; quitting app')
332           app.quit();
333         }, 0);
334       }
335     }
336     this.flushData();
337   }
338
339   resume() {
340     console.log('WebApplication : resume');
341     this.suspended = false;
342     addonManager.emit('lcResume', this.mainWindow.id, this.reload);
343     this.reload = false;
344
345     if (!this.backgroundRunnable())
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.isPreloading()) {
404       return;
405     }
406     console.log('WebApplication : show');
407     if (this.profileDelegate.backgroundExecutableLaunchMode()) {
408       console.log('skip showing while backgroundExecutionLaunchMode mode');
409     } else if (!this.mainWindow.isVisible()) {
410       console.log(`show this.windowList.length : ${this.windowList.length}`);
411       this.mainWindow.show();
412       if (this.windowList.length > 1) {
413         this.windowList[this.windowList.length - 1].moveTop();
414       }
415     }
416   }
417
418   private closeWindows() {
419     this.profileDelegate.clearSuface(this.mainWindow.webContents);
420     this.windowList.slice().forEach((window) => {
421       if (window != this.mainWindow)
422         window.destroy();
423     });
424   }
425
426   keyEvent(key: string) {
427     console.log(`WebApplication : keyEvent[${key}]`);
428     switch(key) {
429       case "ArrowUp":
430       case "Up":
431         addonManager.emit('hwUpkey', this.mainWindow.id);
432         break;
433       case "ArrowDown":
434       case "Down":
435         addonManager.emit('hwDownkey', this.mainWindow.id);
436         break;
437       default:
438         console.log('No handler for ' + key);
439         break;
440     }
441   }
442
443   prelaunch(url: string) {
444     console.log('WebApplication : prelaunch');
445     addonManager.emit('lcPrelaunch', this.mainWindow.id, url);
446   }
447
448   clearCache() {
449     this.profileDelegate.clearCache();
450   }
451
452   ambientTick() {
453     const kAmbientTickEventScript = `(function(){
454   var __event = document.createEvent("CustomEvent");
455   __event.initCustomEvent("timetick", true, true);
456   document.dispatchEvent(__event);
457   for (var i=0; i < window.frames.length; i++)
458     window.frames[i].document.dispatchEvent(__event);
459 })()`;
460     wrt.executeJS(this.mainWindow.webContents, kAmbientTickEventScript);
461   }
462
463   ambientChanged(ambient_mode: boolean) {
464     const kAmbientChangedEventScript = `(function(){
465   var __event = document.createEvent(\"CustomEvent\");
466   var __detail = {};
467   __event.initCustomEvent(\"ambientmodechanged\",true,true,__detail);
468   __event.detail.ambientMode = ${ambient_mode ? 'true' : 'false'};
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, kAmbientChangedEventScript);
474   }
475 }