2 * Copyright (c) 2019 Samsung Electronics Co., Ltd All Rights Reserved
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
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';
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;
37 backgroundSupport = wrt.getBackgroundSupport();
39 firstRendered = false;
42 pendingCallbacks: Map<number, any> = new Map();
44 runningStatus = 'none';
46 windowList: Electron.BrowserWindow[] = [];
49 constructor(options: RuntimeOption) {
50 if (options.launchMode == 'backgroundAtStartup') {
51 console.log('backgroundAtStartup');
52 this.preloadStatus = 'preload';
54 this.preloadStatus = 'none';
56 if (options.launchMode == 'backgroundExecution') {
57 console.log('backgroundExecution');
58 this.backgroundExecution = true;
60 this.backgroundExecution = false;
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);
69 this.setupEventListener(options);
71 this.mainWindow = new WRTWindow(this.getWindowOption(options));
72 this.initDisplayDelay(true);
73 this.setupMainWindowEventListener();
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}`);
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();
91 app.on('web-contents-created', (event, webContents) => {
92 webContents.on('crashed', function() {
93 console.error('webContents crashed');
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));
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);
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);
123 /* electron by default allows permission for all if no request handler
124 is there; so granting permission only temporarily to not have any
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);
139 console.log('Certificate could not be opened');
143 app.on('login', (event, webContents, request, authInfo, callback) => {
144 console.log(`Login info is required, isproxy: ${authInfo.isProxy}`);
145 event.preventDefault();
148 if (wrt.tv && authInfo.isProxy) {
149 let vconfProxy = wrt.tv.getProxy();
151 let proxyInfo = new URL(vconfProxy);
152 usrname = proxyInfo.username;
153 passwd = proxyInfo.password;
155 if (usrname && passwd) {
156 callback(usrname, passwd);
158 console.log('Login, but usrname and passwd is empty!!!');
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);
168 if (this.accessiblePath) {
169 console.log(`accessiblePath: ${this.accessiblePath}`);
170 protocol.interceptFileProtocol('file', (request, callback) => {
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);
181 if (access_path.indexOf("/shared/res/") > -1) {
182 callback(access_path);
186 console.log(`invalid accesspath: ${access_path}`);
187 (callback as any)(403);
190 console.log('request url is empty');
191 (callback as any)(403);
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');
203 this.pendingCallbacks.delete(id);
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');
211 callback(user, password);
214 this.pendingCallbacks.delete(id);
217 wrt.on('app-status-changed', (event, status) => {
218 console.log(`runningStatus: ${status}, ${this.loadFinished}`);
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()
233 private getWindowOption(options: RuntimeOption): NativeWRTjs.WRTWindowConstructorOptions {
236 backgroundColor: this.defaultBackgroundColor,
237 transparent: this.defaultTransparent,
240 nodeIntegration: options.isAddonAvailable,
241 nodeIntegrationInWorker: false
243 webContents: WRTWebContents.create(),
247 private setupMainWindowEventListener() {
248 this.mainWindow.once('ready-to-show', () => {
249 console.log('mainWindow ready-to-show');
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!');
261 this.mainWindow.webContents.on('did-start-loading', () => {
262 console.log('webContents did-start-loading');
263 this.loadFinished = false;
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();
272 if (this.inspectorSrc)
273 this.showInspectorGuide();
275 this.suspendByStatus();
277 addonManager.emit('contentDidFinishLoad', this.mainWindow.id);
281 private initDisplayDelay(firstLaunch: boolean) {
282 // TODO: On 6.0, this causes a black screen on relaunch
284 this.firstRendered = false;
285 this.suspended = false;
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');
297 if (!firstLaunch && !this.backgroundRunnable())
298 this.mainWindow.setEnabled(true);
301 private backgroundRunnable(): boolean {
302 return this.backgroundSupport || this.backgroundExecution;
305 handleAppControlReload(url: string) {
306 console.log('WebApplication : handleAppControlReload');
308 this.initDisplayDelay(false);
309 this.mainWindow.loadURL(url);
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()
320 if (this.runningStatus !== 'behind')
321 (wrt.tv as NativeWRTjs.TVExtension).notifyAppStatus('preload');
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.
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);
340 if (this.preloadStatus !== 'none') {
342 tv.cancelDialogs(this.mainWindow.webContents);
350 console.log('WebApplication : suspend');
351 addonManager.emit('lcSuspend', this.mainWindow.id);
352 this.suspended = true;
353 this.windowList[this.windowList.length - 1].hide();
355 if (!this.backgroundRunnable() || this.inQuit) {
356 if (!this.multitaskingSupport) {
357 // FIXME : terminate app after visibilitychange event handling
359 console.log('multitasking is not supported; quitting app')
363 this.windowList.forEach((window) => window.setEnabled(false));
369 console.log('WebApplication : resume');
370 this.suspended = false;
371 addonManager.emit('lcResume', this.mainWindow.id);
373 if (!this.firstRendered) {
374 console.log('WebApplication : resume firstRendered is false');
377 if (!this.backgroundRunnable())
378 this.windowList.forEach((window) => window.setEnabled(true));
379 this.windowList[this.windowList.length - 1].show();
383 console.log('WebApplication : finalize');
385 this.windowList.forEach((window) => window.removeAllListeners());
389 console.log('WebApplication : quit');
390 addonManager.emit('lcQuit', this.mainWindow.id);
392 this.inspectorSrc = '';
393 wrt.tv.cancelDialogs(this.mainWindow.webContents);
395 if (this.debugPort) {
396 console.log('stop inspector server');
398 wrt.stopInspectorServer();
405 private flushData() {
406 console.log('WebApplication : FlushData');
408 wrt.tv.flushCookie();
409 this.windowList.forEach((window) => window.webContents.session.flushStorageData());
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);
421 wrt.executeJS(this.mainWindow.webContents, kAppControlEventScript);
424 private activateIMEWebHelperClient() {
425 console.log('webApplication : activateIMEWebHelperClient');
426 const kImeActivateFunctionCallScript =
427 '(function(){WebHelperClient.impl.activate();})()';
428 wrt.executeJS(this.mainWindow.webContents, kImeActivateFunctionCallScript);
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();
442 private closeWindows() {
443 wrt.tv?.clearSurface(this.mainWindow.webContents);
444 this.windowList.forEach((window) => {
445 if (window != this.mainWindow)
450 keyEvent(key: string) {
451 console.log(`WebApplication : keyEvent[${key}]`);
455 addonManager.emit('hwUpkey', this.mainWindow.id);
459 addonManager.emit('hwDownkey', this.mainWindow.id);
462 console.log('No handler for ' + key);
467 prelaunch(url: string) {
468 console.log('WebApplication : prelaunch');
469 addonManager.emit('lcPrelaunch', this.mainWindow.id, url);
473 console.log('WebApplication : lowMemory to clearcache');
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');
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);
493 wrt.executeJS(this.mainWindow.webContents, kAmbientTickEventScript);
496 ambientChanged(ambient_mode: boolean) {
497 const kAmbientChangedEventScript = `(function(){
498 var __event = document.createEvent(\"CustomEvent\");
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);
506 wrt.executeJS(this.mainWindow.webContents, kAmbientChangedEventScript);