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: any, window: any) => {
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();
92 app.on('web-contents-created', (event: any, webContents: any) => {
93 webContents.on('crashed', function() {
94 console.error('webContents crashed');
98 webContents.session.setPermissionRequestHandler((webContents: any, permission: string, callback: any) => {
99 console.log(`handlePermissionRequests for ${permission}`);
100 if (permission === 'notifications') {
101 if (!this.notificationPermissionMap)
102 this.notificationPermissionMap = new Map();
103 else if (this.notificationPermissionMap.has(webContents)) {
104 process.nextTick(callback, this.notificationPermissionMap.get(webContents));
107 const id = ++this.pendingID;
108 console.log(`Raising a notification permission request with id: ${id}`);
109 this.pendingCallbacks.set(id, (result: boolean) => {
110 (this.notificationPermissionMap as Map<Electron.WebContents, boolean>).set(webContents, result);
113 wrt.handleNotificationPermissionRequest(id, webContents);
114 } else if (permission === 'media') {
115 const id = ++this.pendingID;
116 console.log(`Raising a media permission request with id: ${id}`);
117 this.pendingCallbacks.set(id, callback);
118 wrt.handleMediaPermissionRequest(id, webContents);
119 } else if (permission === 'geolocation') {
120 const id = ++this.pendingID;
121 console.log(`Raising a geolocation permission request with id: ${id}`);
122 this.pendingCallbacks.set(id, callback);
123 wrt.handleGeolocationPermissionRequest(id, webContents);
125 /* electron by default allows permission for all if no request handler
126 is there; so granting permission only temporarily to not have any
133 app.on('certificate-error', (event: any, webContents: any, url: string, error: string, certificate: any, callback: any) => {
134 console.log('A certificate error has occurred');
135 event.preventDefault();
136 if (certificate.data) {
137 const id = ++this.pendingID;
138 console.log(`Raising a certificate error response with id: ${id}`);
139 this.pendingCallbacks.set(id, callback);
140 wrt.handleCertificateError(id, webContents, certificate.data, url, error);
142 console.log('Certificate could not be opened');
147 app.on('login', (event: any, webContents: any, request: any, authInfo: any, callback: any) => {
148 console.log(`Login info is required, isproxy: ${authInfo.isProxy}`);
149 event.preventDefault();
152 if (wrt.tv && authInfo.isProxy) {
153 let vconfProxy = wrt.tv.getProxy();
155 let proxyInfo = new URL(vconfProxy);
156 usrname = proxyInfo.username;
157 passwd = proxyInfo.password;
159 if (usrname && passwd) {
160 callback(usrname, passwd);
162 console.log('Login, but usrname and passwd is empty!!!');
166 const id = ++this.pendingID;
167 console.log(`Raising a login info request with id: ${id}`);
168 this.pendingCallbacks.set(id, callback);
169 wrt.handleAuthRequest(id, webContents);
173 if (this.accessiblePath) {
174 console.log(`accessiblePath: ${this.accessiblePath}`);
175 protocol.interceptFileProtocol('file', (request: any, callback: any) => {
177 let parsed_info = new URL(request.url);
178 let access_path = parsed_info.host + decodeURI(parsed_info.pathname);
179 console.log(`check path: : ${access_path}`);
180 for (let path of (this.accessiblePath as string[])) {
181 if (access_path.startsWith(path)) {
182 callback(access_path);
186 if (access_path.indexOf("/shared/res/") > -1) {
187 callback(access_path);
191 console.log(`invalid accesspath: ${access_path}`);
192 (callback as any)(403);
195 console.log('request url is empty');
196 (callback as any)(403);
198 }, (error: Error) => {
203 wrt.on('permission-response', (event: any, id: number, result: boolean) => {
204 console.log(`permission-response for ${id} is ${result}`);
205 let callback = this.pendingCallbacks.get(id);
206 if (typeof callback === 'function') {
207 console.log('calling permission response callback');
209 this.pendingCallbacks.delete(id);
213 wrt.on('auth-response', (event: any, id: number, submit: boolean, user: string, password: string) => {
214 let callback = this.pendingCallbacks.get(id);
215 if (typeof callback === 'function') {
216 console.log('calling auth response callback');
218 callback(user, password);
221 this.pendingCallbacks.delete(id);
225 wrt.on('app-status-changed', (event: any, status: string) => {
226 console.log(`runningStatus: ${status}, ${this.loadFinished}`);
229 this.runningStatus = status;
230 if (this.runningStatus === 'DialogClose' && this.inspectorSrc) {
231 console.log(`runningStatus is DialogClose, src is ${this.inspectorSrc}`);
232 this.mainWindow.loadURL(this.inspectorSrc);
233 this.inspectorSrc = '';
234 } else if (this.runningStatus == 'behind' && this.loadFinished) {
235 // TODO : Need to care this situation and decide to pass the addon event emitter to suspend()
241 private getWindowOption(options: RuntimeOption): NativeWRTjs.WRTWindowConstructorOptions {
244 backgroundColor: this.defaultBackgroundColor,
245 transparent: this.defaultTransparent,
248 nodeIntegration: options.isAddonAvailable,
249 nodeIntegrationInWorker: false
251 webContents: WRTWebContents.create(),
255 private setupMainWindowEventListener() {
256 this.mainWindow.once('ready-to-show', () => {
257 console.log('mainWindow ready-to-show');
259 clearTimeout(this.showTimer);
260 wrt.hideSplashScreen(0);
261 this.firstRendered = true;
262 if (this.preloadStatus == 'preload') {
263 this.preloadStatus = 'readyToShow';
264 console.log('preloading show is skipped!');
270 this.mainWindow.webContents.on('did-start-loading', () => {
271 console.log('webContents did-start-loading');
272 this.loadFinished = false;
275 this.mainWindow.webContents.on('did-finish-load', () => {
276 console.log('webContents did-finish-load');
277 this.loadFinished = true;
278 wrt.hideSplashScreen(1);
279 if (wrt.isIMEWebApp()) {
280 this.activateIMEWebHelperClient();
282 if (this.inspectorSrc)
283 this.showInspectorGuide();
285 this.suspendByStatus();
287 addonManager.emit('contentDidFinishLoad', this.mainWindow.id);
291 private initDisplayDelay(firstLaunch: boolean) {
292 // TODO: On 6.0, this causes a black screen on relaunch
294 this.firstRendered = false;
295 this.suspended = false;
297 clearTimeout(this.showTimer);
298 let splashShown = this.preloadStatus !== 'preload' && firstLaunch && wrt.showSplashScreen();
299 if (!splashShown && !wrt.tv) {
300 this.showTimer = setTimeout(() => {
301 if (!this.suspended) {
302 console.log('FrameRendered not obtained from engine. To show window, timer fired');
303 this.mainWindow.emit('ready-to-show');
307 if (!firstLaunch && !this.backgroundRunnable())
308 this.mainWindow.setEnabled(true);
311 private backgroundRunnable(): boolean {
312 return this.backgroundSupport || this.backgroundExecution;
315 handleAppControlReload(url: string) {
316 console.log('WebApplication : handleAppControlReload');
318 this.initDisplayDelay(false);
319 this.mainWindow.loadURL(url);
322 private suspendByStatus() {
323 if (this.preloadStatus === 'readyToShow' ||
324 this.preloadStatus === 'preload' ||
325 this.runningStatus === 'behind') {
326 console.log('WebApplication : suspendByStatus');
327 console.log(`preloadStatus: ${this.preloadStatus}, runningStatus: ${this.runningStatus}`);
328 // TODO : Need to care this situation and decide to pass the addon event emitter to suspend()
330 if (this.runningStatus !== 'behind')
331 (wrt.tv as NativeWRTjs.TVExtension).notifyAppStatus('preload');
335 private showInspectorGuide() {
336 console.log('WebApplication : showInspectorGuide');
337 this.showInspectorGuide = () => {}; // call once
338 const message = `${this.debugPort.toString()}
339 Fast RWI is used, [about:blank] is loaded fist instead of
340 [${this.inspectorSrc}]
341 Click OK button will start the real loading.
343 Please connect to RWI in PC before click OK button.
344 Then you can get network log from the initial loading.
345 Please click Record button in Timeline panel in PC before click OK button,
346 Then you can get profile log from the initial loading.`;
347 let tv = wrt.tv as NativeWRTjs.TVExtension;
348 tv.showDialog(this.mainWindow.webContents, message);
350 if (this.preloadStatus !== 'none') {
352 tv.cancelDialogs(this.mainWindow.webContents);
358 if (this.suspended || this.inQuit)
360 console.log('WebApplication : suspend');
361 addonManager.emit('lcSuspend', this.mainWindow.id);
362 this.suspended = true;
363 this.windowList[this.windowList.length - 1].hide();
365 if (!this.backgroundRunnable()) {
366 if (!this.multitaskingSupport) {
367 // FIXME : terminate app after visibilitychange event handling
369 console.log('multitasking is not supported; quitting app')
373 this.windowList.forEach((window) => window.setEnabled(false));
379 console.log('WebApplication : resume');
380 this.suspended = false;
381 addonManager.emit('lcResume', this.mainWindow.id);
383 if (!this.firstRendered) {
384 console.log('WebApplication : resume firstRendered is false');
387 if (!this.backgroundRunnable())
388 this.windowList.forEach((window) => window.setEnabled(true));
389 this.windowList[this.windowList.length - 1].show();
393 console.log('WebApplication : quit');
395 this.windowList.forEach((window) => window.removeAllListeners());
402 console.log('WebApplication : beforeQuit');
403 addonManager.emit('lcQuit', this.mainWindow.id);
405 this.inspectorSrc = '';
406 wrt.tv.cancelDialogs(this.mainWindow.webContents);
408 if (this.debugPort) {
409 console.log('stop inspector server');
411 wrt.stopInspectorServer();
416 private flushData() {
417 console.log('WebApplication : FlushData');
418 this.windowList.forEach((window) => window.webContents.session.flushStorageData());
421 sendAppControlEvent() {
422 const kAppControlEventScript = `(function(){
423 var __event = document.createEvent("CustomEvent");
424 __event.initCustomEvent("appcontrol", true, true, null);
425 document.dispatchEvent(__event);
426 for (var i=0; i < window.frames.length; i++)
427 window.frames[i].document.dispatchEvent(__event);
429 wrt.executeJS(this.mainWindow.webContents, kAppControlEventScript);
432 private activateIMEWebHelperClient() {
433 console.log('webApplication : activateIMEWebHelperClient');
434 const kImeActivateFunctionCallScript =
435 '(function(){WebHelperClient.impl.activate();})()';
436 wrt.executeJS(this.mainWindow.webContents, kImeActivateFunctionCallScript);
440 console.log('WebApplication : show');
441 this.preloadStatus = 'none';
442 if (this.backgroundExecution) {
443 console.log('skip showing while backgroundExecution mode');
444 } else if (!this.mainWindow.isVisible()) {
445 console.log('show window');
446 this.mainWindow.show();
450 private closeWindows() {
451 wrt.tv?.clearSurface(this.mainWindow.webContents);
452 this.windowList.forEach((window) => {
453 if (window != this.mainWindow)
458 keyEvent(key: string) {
459 console.log(`WebApplication : keyEvent[${key}]`);
463 addonManager.emit('hwUpkey', this.mainWindow.id);
467 addonManager.emit('hwDownkey', this.mainWindow.id);
470 console.log('No handler for ' + key);
475 prelaunch(url: string) {
476 console.log('WebApplication : prelaunch');
477 addonManager.emit('lcPrelaunch', this.mainWindow.id, url);
481 console.log('WebApplication : lowMemory to clearcache');
484 this.windowList.forEach((window) => {
485 //clear webframe cache
486 (wrt.tv as NativeWRTjs.TVExtension).clearWebCache(window.webContents);
487 window.webContents.session.clearCache(function() {
488 console.log('clear session Cache complete');
494 const kAmbientTickEventScript = `(function(){
495 var __event = document.createEvent("CustomEvent");
496 __event.initCustomEvent("timetick", true, true);
497 document.dispatchEvent(__event);
498 for (var i=0; i < window.frames.length; i++)
499 window.frames[i].document.dispatchEvent(__event);
501 wrt.executeJS(this.mainWindow.webContents, kAmbientTickEventScript);
504 ambientChanged(ambient_mode: boolean) {
505 const kAmbientChangedEventScript = `(function(){
506 var __event = document.createEvent(\"CustomEvent\");
508 __event.initCustomEvent(\"ambientmodechanged\",true,true,__detail);
509 __event.detail.ambientMode = ${ambient_mode ? 'true' : 'false'};
510 document.dispatchEvent(__event);
511 for (var i=0; i < window.frames.length; i++)
512 window.frames[i].document.dispatchEvent(__event);
514 wrt.executeJS(this.mainWindow.webContents, kAmbientChangedEventScript);