[VD] Fix crash problem by low memory callback
[platform/framework/web/wrtjs.git] / wrt_app / src / web_application.js
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 const { app, protocol } = require('electron');
20 const WAS_EVENT = require('./was_event');
21 const wrt = require('../browser/wrt');
22 const WRTWebContents = require('../browser/wrt_web_contents');
23 const WRTWindow = require('../browser/wrt_window');
24
25 class WebApplication {
26     constructor(options) {
27         this.initialize(options);
28         this.createMainWindow(options);
29     }
30     initialize(options) {
31         this.pendingID = 0;
32         this.pendingCallbacks = new Map();
33         this.windowList = [];
34         this.firstRendered = false;
35         this.backgroundSupport = wrt.getBackgroundSupport();
36         this.multitaskingSupport = wrt.getMultitaskingSupport();
37         this.debugPort = 0;
38         this.inspectorSrc = '';
39         if (options.launchMode == 'backgroundAtStartup') {
40             console.log('backgroundAtStartup');
41             this.preloadStatus = 'preload';
42         } else {
43             this.preloadStatus = 'none';
44         }
45         if (options.launchMode == 'backgroundExecution') {
46             console.log('backgroundExecution');
47             this.backgroundExecution = true;
48         } else {
49             this.backgroundExecution = false;
50         }
51         this.accessiblePath = wrt.getAccessiblePath();
52         this.isAlwaysReload = wrt.isAlwaysReload();
53         this.suspended = true;
54         this.loadFinished = false;
55         this.runningStatus = 'none';
56         this.isTVProfile = (wrt.getPlatformType() === "product_tv");
57         this.defaultBackgroundColor = (this.isTVProfile
58             || (wrt.getPlatformType() === "product_wearable") ? '#000' : '#FFF');
59         this.defaultTransparent = (this.isTVProfile ? true : false);
60
61         let self = this;
62         app.on('browser-window-created', function(event, window) {
63             if (self.windowList.length > 0)
64                 self.windowList[self.windowList.length - 1].hide();
65             self.windowList.push(window);
66             console.log(`window created : #${self.windowList.length}`);
67
68             window.on('closed', function() {
69                 console.log(`window closed : #${self.windowList.length}`);
70                 let index = self.windowList.indexOf(window);
71                 self.windowList.splice(index, 1);
72                 if (index === self.windowList.length && self.windowList.length > 0)
73                     self.windowList[self.windowList.length - 1].show();
74             });
75         });
76         app.on('web-contents-created', function(event, webContents) {
77             webContents.on('crashed', function() {
78                 console.error('webContents crashed');
79                 app.exit(100);
80             });
81             webContents.session.setPermissionRequestHandler(function(webContents, permission, callback) {
82                 console.log(`handlePermissionRequests for ${permission}`);
83                 if (permission === 'notifications') {
84                     const id = ++self.pendingID;
85                     console.log(`Raising a notification permission request with id: ${id}`);
86                     self.pendingCallbacks.set(id, callback);
87                     wrt.handleNotificationPermissionRequest(id, webContents);
88                 } else if (permission === 'media') {
89                     const id = ++self.pendingID;
90                     console.log(`Raising a media permission request with id: ${id}`);
91                     self.pendingCallbacks.set(id, callback);
92                     wrt.handleMediaPermissionRequest(id, webContents);
93                 } else if (permission === 'geolocation') {
94                     const id = ++self.pendingID;
95                     console.log(`Raising a geolocation permission request with id: ${id}`);
96                     self.pendingCallbacks.set(id, callback);
97                     wrt.handleGeolocationPermissionRequest(id, webContents);
98                 } else {
99                     /* electron by default allows permission for all if no request handler
100                        is there; so granting permission only temporarily to not have any
101                        side effects */
102                     callback(true);
103                 }
104             });
105         });
106         app.on('certificate-error', function(event, webContents, url, error, certificate, callback) {
107             console.log('A certificate error has occurred');
108             event.preventDefault();
109             if (certificate.data) {
110                 const id = ++self.pendingID;
111                 console.log(`Raising a certificate error response with id: ${id}`);
112                 self.pendingCallbacks.set(id, callback);
113                 wrt.handleCertificateError(id, webContents, certificate.data, url);
114             } else {
115                 console.log('Certificate could not be opened');
116                 callback(false);
117             }
118         });
119         app.on('login', function(event, webContents, request, authInfo, callback) {
120             console.log('Login info is required');
121             event.preventDefault();
122             const id = ++self.pendingID;
123             console.log(`Raising a login info request with id: ${id}`);
124             self.pendingCallbacks.set(id, callback);
125             wrt.handleAuthRequest(id, webContents);
126         });
127         if (this.accessiblePath) {
128             console.log('accessiblePath : ' + this.accessiblePath);
129             protocol.interceptFileProtocol('file', (request, callback) => {
130                 const url = require('url');
131                 let access_path, parsed_info = url.parse(request.url);
132                 access_path = parsed_info.host + parsed_info.pathname;
133                 console.log("check path: " + access_path);
134
135                 for (let p in self.accessiblePath) {
136                     if (access_path.startsWith(self.accessiblePath[p])) {
137                         callback(access_path);
138                         return;
139                     }
140                 }
141                 if (access_path.indexOf("/shared/res/") > -1) {
142                     callback(access_path);
143                     return;
144                 }
145                 else {
146                     console.log("invalid access: " + access_path);
147                     callback(403);
148                 }
149             }, (error) => {
150                 console.log(error);
151             });
152         }
153         wrt.on('permission-response', function(event, id, result) {
154             console.log(`permission-response for ${id} is ${result}`);
155             let callback = self.pendingCallbacks.get(id);
156             if (typeof callback === 'function') {
157                 console.log('calling permission response callback');
158                 callback(result);
159                 self.pendingCallbacks.delete(id);
160             }
161         });
162         wrt.on('auth-response', function(event, id, submit, user, passwd) {
163             let callback = self.pendingCallbacks.get(id);
164             if (typeof callback === 'function') {
165                 console.log('calling auth response callback');
166                 if (submit) {
167                     callback(user, passwd);
168                 } else {
169                     callback();
170                 }
171                 self.pendingCallbacks.delete(id);
172             }
173         });
174         wrt.on('app-status-changed', function(event, status) {
175             console.log(`runningStatus: ${status}, ${self.loadFinished}`);
176             if (!self.isTVProfile) {
177                 return;
178             }
179             self.runningStatus = status;
180             if (self.runningStatus === 'DialogClose' && self.inspectorSrc) {
181                 console.log(`runningStatus is DialogClose, src is ${self.inspectorSrc}`);
182                 self.mainWindow.loadURL(self.inspectorSrc);
183                 self.inspectorSrc = '';
184             } else if (self.runningStatus == 'behind' && self.loadFinished) {
185                 // TODO : Need to care this situation and decide to pass the addon event emitter to suspend()
186                 self.suspend();
187             }
188         });
189     }
190     backgroundRunnable() {
191         return this.backgroundSupport || this.backgroundExecution;
192     }
193     getWindowOption(options) {
194         return {
195             fullscreen: false,
196             backgroundColor: this.defaultBackgroundColor,
197             transparent: this.defaultTransparent,
198             show: false,
199             webPreferences: {
200                 nodeIntegration: options.isAddonAvailable,
201                 nodeIntegrationInWorker: false
202             },
203             webContents: WRTWebContents.create(),
204             'web-preferences': {
205                 'direct-write': true,
206                 'subpixel-font-scaling': false,
207                 'web-security': false
208             }
209         };
210     }
211     createMainWindow(options) {
212         let winopt = this.getWindowOption(options);
213         this.mainWindow = new WRTWindow(winopt);
214         if (options.devMode) {
215             this.mainWindow.webContents.openDevTools({
216                 detached: true
217             });
218         }
219         let self = this;
220         if (!self.isTVProfile) {
221             self.showTimer = setTimeout(() => {
222                 if (!self.suspended) {
223                     console.log('FrameRendered not obtained from engine. To show window, timer fired');
224                     self.mainWindow.emit('ready-to-show');
225                 }
226             }, 2000);
227         }
228
229         this.mainWindow.once('ready-to-show', function() {
230             console.log('mainWindow ready-to-show');
231             if (self.showTimer)
232                 clearTimeout(self.showTimer);
233             wrt.hideSplashScreen(1);
234             self.firstRendered = true;
235             if (self.preloadStatus == 'preload') {
236                 self.preloadStatus = 'readyToShow';
237                 console.log('preloading show is skipped!');
238                 return;
239             }
240             self.show();
241         });
242         this.mainWindow.webContents.on('did-start-loading', function() {
243             console.log('webContents did-start-loading');
244             self.loadFinished = false;
245         });
246         this.mainWindow.webContents.on('did-finish-load', function() {
247             console.log('webContents did-finish-load');
248             self.loadFinished = true;
249             wrt.hideSplashScreen(2);
250             if (wrt.isIMEWebApp()) {
251                 self.activateIMEWebHelperClient();
252             } else if (self.isTVProfile) {
253                 if (self.inspectorSrc) {
254                     self.showInspectorGuide();
255                 } else {
256                     self.suspendByStatus();
257                 }
258             }
259         });
260     }
261     suspendByStatus() {
262         if (this.preloadStatus === 'readyToShow' ||
263             this.preloadStatus === 'preload' ||
264             this.runningStatus === 'behind') {
265             console.log(`preloadStatus: ${this.preloadStatus}, runningStatus: ${this.runningStatus}`);
266             // TODO : Need to care this situation and decide to pass the addon event emitter to suspend()
267             this.suspend();
268             if (this.runningStatus !== 'behind')
269                 wrt.notifyAppStatus('preload');
270         }
271     }
272     showInspectorGuide() {
273         this.showInspectorGuide = () => {}; // call once
274         let message = this.debugPort.toString() +
275             "\r\nFast RWI is used, [about:blank] is loaded fist instead of \r\n[" +
276             this.inspectorSrc +
277             "]\r\nClick OK button will start the real loading.\r\nNotes:\r\nPlease " +
278             "connect to RWI in PC before click OK button.\r\nThen you can get " +
279             "network log from the initial loading.\r\nPlease click Record button " +
280             "in Timeline panel in PC before click OK button,\r\nThen you can get " +
281             "profile log from the initial loading."
282         wrt.modalDialog(this.mainWindow.webContents, message);
283     }
284     suspend(evtEmitter) {
285         console.log('WebApplication : suspend');
286         if (evtEmitter) {
287             console.log('WebApplication : suspend - Found event emitter');
288             evtEmitter.emit('lcSuspend', this.mainWindow.id);
289         } else {
290             console.log('WebApplication : suspend - Invalid event emitter');
291         }
292         this.suspended = true;
293         if (this.isTVProfile) {
294             wrt.flushCookie();
295             this.windowList.forEach((window) => window.webContents.session.flushStorageData());
296         }
297         if (!this.multitaskingSupport) {
298             // FIXME : terminate app after visibilitychange event handling
299             setTimeout(() => {
300                 console.log('multitasking is not supported; quitting app')
301                 app.quit();
302             }, 1000);
303         } else if (!this.backgroundRunnable()) {
304             this.windowList.forEach((window) => window.setEnabled(false));
305         }
306         this.windowList[this.windowList.length - 1].hide();
307     }
308     resume(evtEmitter) {
309         console.log('WebApplication : resume');
310
311         this.suspended = false;
312
313         if (evtEmitter) {
314             console.log('WebApplication : resume - Found event emitter');
315             evtEmitter.emit('lcResume', this.mainWindow.id);
316         } else {
317             console.log('WebApplication : resume - Invalid event emitter');
318         }
319
320         if (!this.firstRendered) {
321             console.log('WebApplication : resume firstRendered is false');
322            return;
323         }
324         if (!this.backgroundRunnable()) {
325             this.windowList.forEach((window) => window.setEnabled(true));
326         }
327         this.windowList[this.windowList.length - 1].show();
328     }
329     finalize() {
330         this.inspectorSrc = '';
331         this.windowList.forEach((window) => {
332             window.removeAllListeners();
333         });
334     }
335     quit(evtEmitter) {
336         console.log('WebApplication : quit');
337         if (evtEmitter) {
338             console.log('WebApplication : quit - Found event emitter');
339             evtEmitter.emit('lcQuit', this.mainWindow.id);
340         } else {
341             console.log('WebApplication : quit - Invalid event emitter');
342         }
343     }
344     sendAppControlEvent() {
345         const kAppControlEventScript =
346             '(function(){' +
347               'var __event = document.createEvent("CustomEvent");' +
348               '__event.initCustomEvent("appcontrol", true, true, null);' +
349               'document.dispatchEvent(__event);' +
350               'for (var i=0; i < window.frames.length; i++)' +
351                 'window.frames[i].document.dispatchEvent(__event);' +
352             '})()';
353         wrt.executeJS(this.mainWindow.webContents, kAppControlEventScript);
354     }
355     activateIMEWebHelperClient() {
356         console.log('webApplication : activateIMEWebHelperClient');
357         const kImeActivateFunctionCallScript =
358             '(function(){WebHelperClient.impl.activate();})()';
359         wrt.executeJS(this.mainWindow.webContents, kImeActivateFunctionCallScript);
360     }
361     show() {
362         console.log('WebApplication : show');
363         this.preloadStatus = 'none';
364         if (this.backgroundExecution) {
365             console.log('skip showing while backgroundExecution mode');
366         } else if (!this.mainWindow.isVisible()) {
367             console.log('show window');
368             this.mainWindow.show();
369         }
370     }
371     closeWindows() {
372         this.windowList.forEach((window) => {
373             if (window != this.mainWindow)
374                 window.destroy();
375         });
376     }
377     keyEvent(evtEmitter, key) {
378         console.log('WebApplication : keyEvent');
379         if (!evtEmitter) {
380             console.log('Invalid event emitter for key hook');
381             return;
382         }
383         console.log('key event is ' + key);
384         switch(key) {
385             case "ArrowUp":
386             case "Up":
387                 evtEmitter.emit('hwUpkey', this.mainWindow.id);
388                 break;
389             case "ArrowDown":
390             case "Down":
391                 evtEmitter.emit('hwDownkey', this.mainWindow.id);
392                 break;
393             default:
394                 console.log('No handler for ' + key);
395                 break;
396         }
397     }
398     prelaunch(evtEmitter, origURL) {
399         console.log('WebApplication : prelaunch');
400         if (evtEmitter) {
401             console.log('WebApplication : prelaunch - Found event emitter');
402             evtEmitter.emit('lcPrelaunch', this.mainWindow.id, origURL);
403         } else {
404             console.log('WebApplication : prelaunch - Invalid event emitter');
405         }
406     }
407     lowMemory() {
408         console.log('WebApplication : lowMemory to clearcache');
409         if (this.isTVProfile) {
410             this.windowList.forEach((window) => {
411                 //clear webframe cache
412                 wrt.clearWebCache(window.webContents);
413                 /* FIXME: will unblock after chromium-efl released
414                 window.webContents.session.clearCache(function() {
415                     console.log('clear session Cache complete');
416                 })
417                 */
418             });
419         }
420     }
421 }
422 module.exports = WebApplication;