3e5a683d8a7d75b44f1b405c7385ad272c2817d4
[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, protocol } 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
25 export class WebApplication {
26   accessiblePath?: string[];
27   backgroundExecution: boolean;
28   defaultBackgroundColor: string;
29   defaultTransparent: boolean;
30   isAlwaysReload: boolean;
31   mainWindow: Electron.BrowserWindow;
32   multitaskingSupport: boolean;
33   notificationPermissionMap?: Map<Electron.WebContents, boolean>;
34   preloadStatus: string;
35   showTimer?: NodeJS.Timeout;
36
37   backgroundSupport = wrt.getBackgroundSupport();
38   debugPort = 0;
39   firstRendered = false;
40   inspectorSrc = '';
41   loadFinished = false;
42   pendingCallbacks: Map<number, any> = new Map();
43   pendingID = 0;
44   runningStatus = 'none';
45   suspended = false;
46   windowList: Electron.BrowserWindow[] = [];
47   inQuit = false;
48
49   constructor(options: RuntimeOption) {
50     if (options.launchMode == 'backgroundAtStartup') {
51       console.log('backgroundAtStartup');
52       this.preloadStatus = 'preload';
53     } else {
54       this.preloadStatus = 'none';
55     }
56     if (options.launchMode == 'backgroundExecution') {
57       console.log('backgroundExecution');
58       this.backgroundExecution = true;
59     } else {
60       this.backgroundExecution = false;
61     }
62     this.accessiblePath = wrt.tv?.getAccessiblePath();
63     this.isAlwaysReload = (wrt.tv ? wrt.tv.isAlwaysReload() : false);
64     this.multitaskingSupport = (wrt.tv ? wrt.tv.getMultitaskingSupport() : true);
65     this.defaultBackgroundColor = (wrt.tv ? '#0000' :
66         ((wrt.getPlatformType() === "product_wearable") ? '#000' : '#FFF'));
67     this.defaultTransparent = (wrt.tv ? true : false);
68
69     this.setupEventListener(options);
70
71     this.mainWindow = new WRTWindow(this.getWindowOption(options));
72     this.initDisplayDelay(true);
73     this.setupMainWindowEventListener();
74   }
75
76   private setupEventListener(options: RuntimeOption) {
77     app.on('browser-window-created', (event, window) => {
78       if (this.windowList.length > 0)
79         this.windowList[this.windowList.length - 1].hide();
80       this.windowList.push(window);
81       console.log(`window created : #${this.windowList.length}`);
82
83       window.on('closed', () => {
84         console.log(`window closed : #${this.windowList.length}`);
85         let index = this.windowList.indexOf(window);
86         this.windowList.splice(index, 1);
87         if (index === this.windowList.length && this.windowList.length > 0)
88           this.windowList[this.windowList.length - 1].show();
89       });
90     });
91     app.on('web-contents-created', (event, webContents) => {
92       webContents.on('crashed', function() {
93         console.error('webContents crashed');
94         app.exit(100);
95       });
96       webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
97         console.log(`handlePermissionRequests for ${permission}`);
98         if (permission === 'notifications') {
99           if (!this.notificationPermissionMap)
100             this.notificationPermissionMap = new Map();
101           else if (this.notificationPermissionMap.has(webContents)) {
102             process.nextTick(callback, this.notificationPermissionMap.get(webContents));
103             return;
104           }
105           const id = ++this.pendingID;
106           console.log(`Raising a notification permission request with id: ${id}`);
107           this.pendingCallbacks.set(id, (result: boolean) => {
108             (this.notificationPermissionMap as Map<Electron.WebContents, boolean>).set(webContents, result);
109             callback(result);
110           });
111           wrt.handleNotificationPermissionRequest(id, webContents);
112         } else if (permission === 'media') {
113           const id = ++this.pendingID;
114           console.log(`Raising a media permission request with id: ${id}`);
115           this.pendingCallbacks.set(id, callback);
116           wrt.handleMediaPermissionRequest(id, webContents);
117         } else if (permission === 'geolocation') {
118           const id = ++this.pendingID;
119           console.log(`Raising a geolocation permission request with id: ${id}`);
120           this.pendingCallbacks.set(id, callback);
121           wrt.handleGeolocationPermissionRequest(id, webContents);
122         } else {
123           /* electron by default allows permission for all if no request handler
124              is there; so granting permission only temporarily to not have any
125              side effects */
126           callback(true);
127         }
128       });
129     });
130     app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
131       console.log('A certificate error has occurred');
132       event.preventDefault();
133       if (certificate.data) {
134         const id = ++this.pendingID;
135         console.log(`Raising a certificate error response with id: ${id}`);
136         this.pendingCallbacks.set(id, callback);
137         wrt.handleCertificateError(id, webContents, certificate.data, url, error);
138       } else {
139         console.log('Certificate could not be opened');
140         callback(false);
141       }
142     });
143     app.on('login', (event, webContents, request, authInfo, callback) => {
144       console.log(`Login info is required, isproxy: ${authInfo.isProxy}`);
145       event.preventDefault();
146       let usrname = '';
147       let passwd = '';
148       if (wrt.tv && authInfo.isProxy) {
149         let vconfProxy = wrt.tv.getProxy();
150         if (vconfProxy) {
151           let proxyInfo = new URL(vconfProxy);
152           usrname = proxyInfo.username;
153           passwd = proxyInfo.password;
154         }
155         if (usrname && passwd) {
156           callback(usrname, passwd);
157         } else {
158           console.log('Login, but usrname and passwd is empty!!!');
159           callback('', '');
160         }
161       } else {
162         const id = ++this.pendingID;
163         console.log(`Raising a login info request with id: ${id}`);
164         this.pendingCallbacks.set(id, callback);
165         wrt.handleAuthRequest(id, webContents);
166       }
167     });
168     if (this.accessiblePath) {
169       console.log(`accessiblePath: ${this.accessiblePath}`);
170       protocol.interceptFileProtocol('file', (request, callback) => {
171         if (request.url) {
172           let parsed_info = new URL(request.url);
173           let access_path = parsed_info.host + decodeURI(parsed_info.pathname);
174           console.log(`check path: : ${access_path}`);
175           for (let path of (this.accessiblePath as string[])) {
176             if (access_path.startsWith(path)) {
177               callback(access_path);
178               return;
179             }
180           }
181           if (access_path.indexOf("/shared/res/") > -1) {
182             callback(access_path);
183             return;
184           }
185           else {
186             console.log(`invalid accesspath: ${access_path}`);
187             (callback as any)(403);
188           }
189         } else {
190           console.log('request url is empty');
191           (callback as any)(403);
192         }
193       }, (error) => {
194         console.log(error);
195       });
196     }
197     wrt.on('permission-response', (event, id, result) => {
198       console.log(`permission-response for ${id} is ${result}`);
199       let callback = this.pendingCallbacks.get(id);
200       if (typeof callback === 'function') {
201         console.log('calling permission response callback');
202         callback(result);
203         this.pendingCallbacks.delete(id);
204       }
205     });
206     wrt.on('auth-response', (event, id, submit, user, password) => {
207       let callback = this.pendingCallbacks.get(id);
208       if (typeof callback === 'function') {
209         console.log('calling auth response callback');
210         if (submit)
211           callback(user, password);
212         else
213           callback();
214         this.pendingCallbacks.delete(id);
215       }
216     });
217     wrt.on('app-status-changed', (event, status) => {
218       console.log(`runningStatus: ${status}, ${this.loadFinished}`);
219       if (!wrt.tv)
220         return;
221       this.runningStatus = status;
222       if (this.runningStatus === 'DialogClose' && this.inspectorSrc) {
223         console.log(`runningStatus is DialogClose, src is ${this.inspectorSrc}`);
224         this.mainWindow.loadURL(this.inspectorSrc);
225         this.inspectorSrc = '';
226       } else if (this.runningStatus == 'behind' && this.loadFinished) {
227         // TODO : Need to care this situation and decide to pass the addon event emitter to suspend()
228         this.suspend();
229       }
230     });
231   }
232
233   private getWindowOption(options: RuntimeOption): NativeWRTjs.WRTWindowConstructorOptions {
234     return {
235       fullscreen: false,
236       backgroundColor: this.defaultBackgroundColor,
237       transparent: this.defaultTransparent,
238       show: false,
239       webPreferences: {
240         nodeIntegration: options.isAddonAvailable,
241         nodeIntegrationInWorker: false
242       },
243       webContents: WRTWebContents.create(),
244     };
245   }
246
247   private setupMainWindowEventListener() {
248     this.mainWindow.once('ready-to-show', () => {
249       console.log('mainWindow ready-to-show');
250       if (this.showTimer)
251         clearTimeout(this.showTimer);
252       wrt.hideSplashScreen(0);
253       this.firstRendered = true;
254       if (this.preloadStatus == 'preload') {
255         this.preloadStatus = 'readyToShow';
256         console.log('preloading show is skipped!');
257         return;
258       }
259       this.show();
260     });
261     this.mainWindow.webContents.on('did-start-loading', () => {
262       console.log('webContents did-start-loading');
263       this.loadFinished = false;
264     });
265     this.mainWindow.webContents.on('did-finish-load', () => {
266       console.log('webContents did-finish-load');
267       this.loadFinished = true;
268       wrt.hideSplashScreen(1);
269       if (wrt.isIMEWebApp()) {
270         this.activateIMEWebHelperClient();
271       } else if (wrt.tv) {
272         if (this.inspectorSrc)
273           this.showInspectorGuide();
274         else
275           this.suspendByStatus();
276       }
277       addonManager.emit('contentDidFinishLoad', this.mainWindow.id);
278     });
279   }
280
281   private initDisplayDelay(firstLaunch: boolean) {
282     // TODO: On 6.0, this causes a black screen on relaunch
283     if (firstLaunch)
284       this.firstRendered = false;
285     this.suspended = false;
286     if (this.showTimer)
287       clearTimeout(this.showTimer);
288     let splashShown = firstLaunch && wrt.showSplashScreen();
289     if (!splashShown && !wrt.tv) {
290       this.showTimer = setTimeout(() => {
291         if (!this.suspended) {
292           console.log('FrameRendered not obtained from engine. To show window, timer fired');
293           this.mainWindow.emit('ready-to-show');
294         }
295       }, 2000);
296     }
297     if (!firstLaunch && !this.backgroundRunnable())
298       this.mainWindow.setEnabled(true);
299   }
300
301   private backgroundRunnable(): boolean {
302     return this.backgroundSupport || this.backgroundExecution;
303   }
304
305   handleAppControlReload(url: string) {
306     console.log('WebApplication : handleAppControlReload');
307     this.closeWindows();
308     this.initDisplayDelay(false);
309     this.mainWindow.loadURL(url);
310   }
311
312   private suspendByStatus() {
313     if (this.preloadStatus === 'readyToShow' ||
314         this.preloadStatus === 'preload' ||
315         this.runningStatus === 'behind') {
316       console.log('WebApplication : suspendByStatus');
317       console.log(`preloadStatus: ${this.preloadStatus}, runningStatus: ${this.runningStatus}`);
318       // TODO : Need to care this situation and decide to pass the addon event emitter to suspend()
319       this.suspend();
320       if (this.runningStatus !== 'behind')
321         (wrt.tv as NativeWRTjs.TVExtension).notifyAppStatus('preload');
322     }
323   }
324
325   private showInspectorGuide() {
326     console.log('WebApplication : showInspectorGuide');
327     this.showInspectorGuide = () => {}; // call once
328     const message = `${this.debugPort.toString()}
329 Fast RWI is used, [about:blank] is loaded fist instead of
330 [${this.inspectorSrc}]
331 Click OK button will start the real loading.
332 Notes:
333 Please connect to RWI in PC before click OK button.
334 Then you can get network log from the initial loading.
335 Please click Record button in Timeline panel in PC before click OK button,
336 Then you can get profile log from the initial loading.`;
337     let tv = wrt.tv as NativeWRTjs.TVExtension;
338     tv.showDialog(this.mainWindow.webContents, message);
339
340     if (this.preloadStatus !== 'none') {
341       setTimeout(() => {
342         tv.cancelDialogs(this.mainWindow.webContents);
343       }, 5000);
344     }
345   }
346
347   suspend() {
348     if (this.suspended)
349       return;
350     console.log('WebApplication : suspend');
351     addonManager.emit('lcSuspend', this.mainWindow.id);
352     this.suspended = true;
353     this.windowList[this.windowList.length - 1].hide();
354     this.flushData();
355     if (!this.backgroundRunnable() || this.inQuit) {
356       if (!this.multitaskingSupport) {
357         // FIXME : terminate app after visibilitychange event handling
358         setTimeout(() => {
359           console.log('multitasking is not supported; quitting app')
360           app.quit();
361         }, 1000);
362       } else {
363         this.windowList.forEach((window) => window.setEnabled(false));
364       }
365     }
366   }
367
368   resume() {
369     console.log('WebApplication : resume');
370     this.suspended = false;
371     addonManager.emit('lcResume', this.mainWindow.id);
372
373     if (!this.firstRendered) {
374       console.log('WebApplication : resume firstRendered is false');
375       return;
376     }
377     if (!this.backgroundRunnable())
378       this.windowList.forEach((window) => window.setEnabled(true));
379     this.windowList[this.windowList.length - 1].show();
380   }
381
382   finalize() {
383     console.log('WebApplication : finalize');
384     if (wrt.tv) {
385       this.inspectorSrc = '';
386       wrt.tv.cancelDialogs(this.mainWindow.webContents);
387     }
388     this.flushData();
389     if (this.debugPort) {
390       console.log('stop inspector server');
391       this.debugPort = 0;
392       wrt.stopInspectorServer();
393     }
394     this.windowList.forEach((window) => window.removeAllListeners());
395   }
396
397   quit() {
398     console.log('WebApplication : quit');
399     addonManager.emit('lcQuit', this.mainWindow.id);
400     this.inQuit = true;
401     if (!this.suspended)
402       this.suspend();
403   }
404
405   private flushData() {
406     console.log('WebApplication : FlushData');
407     if (wrt.tv) {
408       wrt.tv.flushCookie();
409       this.windowList.forEach((window) => window.webContents.session.flushStorageData());
410     }
411   }
412
413   sendAppControlEvent() {
414     const kAppControlEventScript = `(function(){
415   var __event = document.createEvent("CustomEvent");
416   __event.initCustomEvent("appcontrol", true, true, null);
417   document.dispatchEvent(__event);
418   for (var i=0; i < window.frames.length; i++)
419     window.frames[i].document.dispatchEvent(__event);
420 })()`;
421     wrt.executeJS(this.mainWindow.webContents, kAppControlEventScript);
422   }
423
424   private activateIMEWebHelperClient() {
425     console.log('webApplication : activateIMEWebHelperClient');
426     const kImeActivateFunctionCallScript =
427         '(function(){WebHelperClient.impl.activate();})()';
428     wrt.executeJS(this.mainWindow.webContents, kImeActivateFunctionCallScript);
429   }
430
431   show() {
432     console.log('WebApplication : show');
433     this.preloadStatus = 'none';
434     if (this.backgroundExecution) {
435       console.log('skip showing while backgroundExecution mode');
436     } else if (!this.mainWindow.isVisible()) {
437       console.log('show window');
438       this.mainWindow.show();
439     }
440   }
441
442   private closeWindows() {
443     wrt.tv?.clearSurface(this.mainWindow.webContents);
444     this.windowList.forEach((window) => {
445       if (window != this.mainWindow)
446         window.destroy();
447     });
448   }
449
450   keyEvent(key: string) {
451     console.log(`WebApplication : keyEvent[${key}]`);
452     switch(key) {
453       case "ArrowUp":
454       case "Up":
455         addonManager.emit('hwUpkey', this.mainWindow.id);
456         break;
457       case "ArrowDown":
458       case "Down":
459         addonManager.emit('hwDownkey', this.mainWindow.id);
460         break;
461       default:
462         console.log('No handler for ' + key);
463         break;
464     }
465   }
466
467   prelaunch(url: string) {
468     console.log('WebApplication : prelaunch');
469     addonManager.emit('lcPrelaunch', this.mainWindow.id, url);
470   }
471
472   lowMemory() {
473     console.log('WebApplication : lowMemory to clearcache');
474     if (!wrt.tv)
475       return;
476     this.windowList.forEach((window) => {
477       //clear webframe cache
478       (wrt.tv as NativeWRTjs.TVExtension).clearWebCache(window.webContents);
479       window.webContents.session.clearCache(function() {
480         console.log('clear session Cache complete');
481       })
482     });
483   }
484
485   ambientTick() {
486     const kAmbientTickEventScript = `(function(){
487   var __event = document.createEvent("CustomEvent");
488   __event.initCustomEvent("timetick", true, true);
489   document.dispatchEvent(__event);
490   for (var i=0; i < window.frames.length; i++)
491     window.frames[i].document.dispatchEvent(__event);
492 })()`;
493     wrt.executeJS(this.mainWindow.webContents, kAmbientTickEventScript);
494   }
495
496   ambientChanged(ambient_mode: boolean) {
497     const kAmbientChangedEventScript = `(function(){
498   var __event = document.createEvent(\"CustomEvent\");
499   var __detail = {};
500   __event.initCustomEvent(\"ambientmodechanged\",true,true,__detail);
501   __event.detail.ambientMode = ${ambient_mode ? 'true' : 'false'};
502   document.dispatchEvent(__event);
503   for (var i=0; i < window.frames.length; i++)
504     window.frames[i].document.dispatchEvent(__event);
505 })()`;
506     wrt.executeJS(this.mainWindow.webContents, kAmbientChangedEventScript);
507   }
508 }