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