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);
348 if (this.suspended || this.inQuit)
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()) {
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());
392 console.log('WebApplication : quit');
393 addonManager.emit('lcQuit', this.mainWindow.id);
395 this.inspectorSrc = '';
396 wrt.tv.cancelDialogs(this.mainWindow.webContents);
398 if (this.debugPort) {
399 console.log('stop inspector server');
401 wrt.stopInspectorServer();
406 private flushData() {
407 console.log('WebApplication : FlushData');
408 this.windowList.forEach((window) => window.webContents.session.flushStorageData());
411 sendAppControlEvent() {
412 const kAppControlEventScript = `(function(){
413 var __event = document.createEvent("CustomEvent");
414 __event.initCustomEvent("appcontrol", true, true, null);
415 document.dispatchEvent(__event);
416 for (var i=0; i < window.frames.length; i++)
417 window.frames[i].document.dispatchEvent(__event);
419 wrt.executeJS(this.mainWindow.webContents, kAppControlEventScript);
422 private activateIMEWebHelperClient() {
423 console.log('webApplication : activateIMEWebHelperClient');
424 const kImeActivateFunctionCallScript =
425 '(function(){WebHelperClient.impl.activate();})()';
426 wrt.executeJS(this.mainWindow.webContents, kImeActivateFunctionCallScript);
430 console.log('WebApplication : show');
431 this.preloadStatus = 'none';
432 if (this.backgroundExecution) {
433 console.log('skip showing while backgroundExecution mode');
434 } else if (!this.mainWindow.isVisible()) {
435 console.log('show window');
436 this.mainWindow.show();
440 private closeWindows() {
441 wrt.tv?.clearSurface(this.mainWindow.webContents);
442 this.windowList.forEach((window) => {
443 if (window != this.mainWindow)
448 keyEvent(key: string) {
449 console.log(`WebApplication : keyEvent[${key}]`);
453 addonManager.emit('hwUpkey', this.mainWindow.id);
457 addonManager.emit('hwDownkey', this.mainWindow.id);
460 console.log('No handler for ' + key);
465 prelaunch(url: string) {
466 console.log('WebApplication : prelaunch');
467 addonManager.emit('lcPrelaunch', this.mainWindow.id, url);
471 console.log('WebApplication : lowMemory to clearcache');
474 this.windowList.forEach((window) => {
475 //clear webframe cache
476 (wrt.tv as NativeWRTjs.TVExtension).clearWebCache(window.webContents);
477 window.webContents.session.clearCache(function() {
478 console.log('clear session Cache complete');
484 const kAmbientTickEventScript = `(function(){
485 var __event = document.createEvent("CustomEvent");
486 __event.initCustomEvent("timetick", true, true);
487 document.dispatchEvent(__event);
488 for (var i=0; i < window.frames.length; i++)
489 window.frames[i].document.dispatchEvent(__event);
491 wrt.executeJS(this.mainWindow.webContents, kAmbientTickEventScript);
494 ambientChanged(ambient_mode: boolean) {
495 const kAmbientChangedEventScript = `(function(){
496 var __event = document.createEvent(\"CustomEvent\");
498 __event.initCustomEvent(\"ambientmodechanged\",true,true,__detail);
499 __event.detail.ambientMode = ${ambient_mode ? 'true' : 'false'};
500 document.dispatchEvent(__event);
501 for (var i=0; i < window.frames.length; i++)
502 window.frames[i].document.dispatchEvent(__event);
504 wrt.executeJS(this.mainWindow.webContents, kAmbientChangedEventScript);