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 mainWindow: Electron.BrowserWindow;
31 multitaskingSupport: boolean;
32 notificationPermissionMap?: Map<Electron.WebContents, boolean>;
33 preloadStatus: string;
34 showTimer?: NodeJS.Timeout;
36 backgroundSupport = wrt.getBackgroundSupport();
38 firstRendered = false;
41 pendingCallbacks: Map<number, any> = new Map();
43 runningStatus = 'none';
45 windowList: Electron.BrowserWindow[] = [];
48 constructor(options: RuntimeOption) {
49 if (options.launchMode == 'backgroundAtStartup') {
50 console.log('backgroundAtStartup');
51 this.preloadStatus = 'preload';
53 this.preloadStatus = 'none';
55 if (options.launchMode == 'backgroundExecution') {
56 console.log('backgroundExecution');
57 this.backgroundExecution = true;
59 this.backgroundExecution = false;
61 this.accessiblePath = wrt.tv?.getAccessiblePath();
62 this.multitaskingSupport = (wrt.tv ? wrt.tv.getMultitaskingSupport() : true);
63 this.defaultBackgroundColor = (wrt.tv ? '#0000' :
64 ((wrt.getPlatformType() === "product_wearable") ? '#000' : '#FFF'));
65 this.defaultTransparent = (wrt.tv ? true : false);
67 this.setupEventListener(options);
69 this.mainWindow = new WRTWindow(this.getWindowOption(options));
70 this.initDisplayDelay(true);
71 this.setupMainWindowEventListener();
74 private setupEventListener(options: RuntimeOption) {
75 app.on('browser-window-created', (event: any, window: any) => {
76 if (this.windowList.length > 0)
77 this.windowList[this.windowList.length - 1].hide();
78 this.windowList.push(window);
79 console.log(`window created : #${this.windowList.length}`);
81 window.on('closed', () => {
82 console.log(`window closed : #${this.windowList.length}`);
83 let index = this.windowList.indexOf(window);
84 this.windowList.splice(index, 1);
85 if (index === this.windowList.length && this.windowList.length > 0)
86 this.windowList[this.windowList.length - 1].show();
90 app.on('web-contents-created', (event: any, webContents: any) => {
91 webContents.on('crashed', function() {
92 console.error('webContents crashed');
96 webContents.session.setPermissionRequestHandler((webContents: any, permission: string, callback: any) => {
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
131 app.on('certificate-error', (event: any, webContents: any, url: string, error: string, certificate: any, callback: any) => {
132 console.log('A certificate error has occurred');
133 event.preventDefault();
134 if (certificate.data) {
135 const id = ++this.pendingID;
136 console.log(`Raising a certificate error response with id: ${id}`);
137 this.pendingCallbacks.set(id, callback);
138 wrt.handleCertificateError(id, webContents, certificate.data, url, error);
140 console.log('Certificate could not be opened');
145 app.on('login', (event: any, webContents: any, request: any, authInfo: any, callback: any) => {
146 console.log(`Login info is required, isproxy: ${authInfo.isProxy}`);
147 event.preventDefault();
150 if (wrt.tv && authInfo.isProxy) {
151 let vconfProxy = wrt.tv.getProxy();
153 let proxyInfo = new URL(vconfProxy);
154 usrname = proxyInfo.username;
155 passwd = proxyInfo.password;
157 if (usrname && passwd) {
158 callback(usrname, passwd);
160 console.log('Login, but usrname and passwd is empty!!!');
164 const id = ++this.pendingID;
165 console.log(`Raising a login info request with id: ${id}`);
166 this.pendingCallbacks.set(id, callback);
167 wrt.handleAuthRequest(id, webContents);
171 if (this.accessiblePath) {
172 console.log(`accessiblePath: ${this.accessiblePath}`);
173 protocol.interceptFileProtocol('file', (request: any, callback: any) => {
175 let parsed_info = new URL(request.url);
176 let access_path = parsed_info.host + decodeURI(parsed_info.pathname);
177 console.log(`check path: : ${access_path}`);
178 for (let path of (this.accessiblePath as string[])) {
179 if (access_path.startsWith(path)) {
180 callback(access_path);
184 if (access_path.indexOf("/shared/res/") > -1) {
185 callback(access_path);
189 console.log(`invalid accesspath: ${access_path}`);
190 (callback as any)(403);
193 console.log('request url is empty');
194 (callback as any)(403);
196 }, (error: Error) => {
201 wrt.on('permission-response', (event: any, id: number, result: boolean) => {
202 console.log(`permission-response for ${id} is ${result}`);
203 let callback = this.pendingCallbacks.get(id);
204 if (typeof callback === 'function') {
205 console.log('calling permission response callback');
207 this.pendingCallbacks.delete(id);
211 wrt.on('auth-response', (event: any, id: number, submit: boolean, user: string, password: string) => {
212 let callback = this.pendingCallbacks.get(id);
213 if (typeof callback === 'function') {
214 console.log('calling auth response callback');
216 callback(user, password);
219 this.pendingCallbacks.delete(id);
223 wrt.on('app-status-changed', (event: any, status: string) => {
224 console.log(`runningStatus: ${status}, ${this.loadFinished}`);
227 this.runningStatus = status;
228 if (this.runningStatus === 'DialogClose' && this.inspectorSrc) {
229 console.log(`runningStatus is DialogClose, src is ${this.inspectorSrc}`);
230 this.mainWindow.loadURL(this.inspectorSrc);
231 this.inspectorSrc = '';
232 } else if (this.runningStatus == 'behind' && this.loadFinished) {
233 // TODO : Need to care this situation and decide to pass the addon event emitter to suspend()
239 private getWindowOption(options: RuntimeOption): NativeWRTjs.WRTWindowConstructorOptions {
242 backgroundColor: this.defaultBackgroundColor,
243 transparent: this.defaultTransparent,
246 nodeIntegration: options.isAddonAvailable,
247 nodeIntegrationInWorker: false
249 webContents: WRTWebContents.create(),
253 private setupMainWindowEventListener() {
254 this.mainWindow.once('ready-to-show', () => {
255 console.log('mainWindow ready-to-show');
257 clearTimeout(this.showTimer);
258 wrt.hideSplashScreen(0);
259 this.firstRendered = true;
260 if (this.preloadStatus == 'preload') {
261 this.preloadStatus = 'readyToShow';
262 console.log('preloading show is skipped!');
268 this.mainWindow.webContents.on('did-start-loading', () => {
269 console.log('webContents did-start-loading');
270 this.loadFinished = false;
273 this.mainWindow.webContents.on('did-finish-load', () => {
274 console.log('webContents did-finish-load');
275 this.loadFinished = true;
276 wrt.hideSplashScreen(1);
277 if (wrt.isIMEWebApp()) {
278 this.activateIMEWebHelperClient();
280 if (this.inspectorSrc)
281 this.showInspectorGuide();
283 this.suspendByStatus();
285 addonManager.emit('contentDidFinishLoad', this.mainWindow.id);
289 private initDisplayDelay(firstLaunch: boolean) {
290 // TODO: On 6.0, this causes a black screen on relaunch
292 this.firstRendered = false;
293 this.suspended = false;
295 clearTimeout(this.showTimer);
296 let splashShown = this.preloadStatus !== 'preload' && firstLaunch && wrt.showSplashScreen();
297 if (!splashShown && !wrt.tv) {
298 this.showTimer = setTimeout(() => {
299 if (!this.suspended) {
300 console.log('FrameRendered not obtained from engine. To show window, timer fired');
301 this.mainWindow.emit('ready-to-show');
305 if (!firstLaunch && !this.backgroundRunnable())
306 this.mainWindow.setEnabled(true);
309 private backgroundRunnable(): boolean {
310 return this.backgroundSupport || this.backgroundExecution;
313 private suspendByStatus() {
314 if (this.preloadStatus === 'readyToShow' ||
315 this.preloadStatus === 'preload' ||
316 this.runningStatus === 'behind') {
317 console.log('WebApplication : suspendByStatus');
318 console.log(`preloadStatus: ${this.preloadStatus}, runningStatus: ${this.runningStatus}`);
319 // TODO : Need to care this situation and decide to pass the addon event emitter to suspend()
321 if (this.runningStatus !== 'behind')
322 (wrt.tv as NativeWRTjs.TVExtension).notifyAppStatus('preload');
326 private showInspectorGuide() {
327 console.log('WebApplication : showInspectorGuide');
328 this.showInspectorGuide = () => {}; // call once
329 const message = `${this.debugPort.toString()}
330 Fast RWI is used, [about:blank] is loaded fist instead of
331 [${this.inspectorSrc}]
332 Click OK button will start the real loading.
334 Please connect to RWI in PC before click OK button.
335 Then you can get network log from the initial loading.
336 Please click Record button in Timeline panel in PC before click OK button,
337 Then you can get profile log from the initial loading.`;
338 let tv = wrt.tv as NativeWRTjs.TVExtension;
339 tv.showDialog(this.mainWindow.webContents, message);
341 if (this.preloadStatus !== 'none') {
343 tv.cancelDialogs(this.mainWindow.webContents);
348 handleAppControlEvent(appControl: any) {
349 let launchMode = appControl.getData('http://samsung.com/appcontrol/data/launch_mode');
350 this.handlePreloadState(launchMode);
352 let skipReload = appControl.getData('SkipReload');
353 if (skipReload == 'Yes') {
354 console.log('skipping reload');
355 // TODO : Need to care this situation and decide to pass the addon event emitter to resume()
360 let loadInfo = appControl.getLoadInfo();
361 let src = loadInfo.getSrc();
362 let reload = loadInfo.getReload() || this.needReload(src);
363 // handle http://tizen.org/appcontrol/operation/main operation specially.
364 // only menu-screen app can send launch request with main operation.
365 // in this case, web app should have to resume web app not reset.
366 if (reload && appControl.getOperation() == 'http://tizen.org/appcontrol/operation/main')
369 this.handleAppControlReload(src);
371 this.sendAppControlEvent();
374 loadUrl(src: string) {
375 this.mainWindow.loadURL(src);
378 this.mainWindow.emit('ready-to-show');
383 if (this.suspended || this.inQuit)
385 console.log('WebApplication : suspend');
386 addonManager.emit('lcSuspend', this.mainWindow.id);
387 this.suspended = true;
388 this.windowList[this.windowList.length - 1].hide();
390 if (!this.backgroundRunnable()) {
391 if (!this.multitaskingSupport) {
392 // FIXME : terminate app after visibilitychange event handling
394 console.log('multitasking is not supported; quitting app')
398 this.windowList.forEach((window) => window.setEnabled(false));
404 console.log('WebApplication : resume');
405 this.suspended = false;
406 addonManager.emit('lcResume', this.mainWindow.id);
408 if (!this.firstRendered) {
409 console.log('WebApplication : resume firstRendered is false');
412 if (!this.backgroundRunnable())
413 this.windowList.forEach((window) => window.setEnabled(true));
414 this.windowList[this.windowList.length - 1].show();
418 console.log('WebApplication : quit');
420 this.windowList.forEach((window) => window.removeAllListeners());
427 console.log('WebApplication : beforeQuit');
428 addonManager.emit('lcQuit', this.mainWindow.id);
430 this.inspectorSrc = '';
431 wrt.tv.cancelDialogs(this.mainWindow.webContents);
433 if (this.debugPort) {
434 console.log('stop inspector server');
436 wrt.stopInspectorServer();
441 private needReload(src: string) {
442 let isAlwaysReload = (wrt.tv ? wrt.tv.isAlwaysReload() : false);
443 if (isAlwaysReload) {
447 let originalUrl = this.mainWindow.webContents.getURL();
449 console.log(`appcontrol src = ${src}, original url = ${originalUrl}`);
450 if (src && originalUrl) {
451 let appcontrolUrl = (new URL(src)).href;
452 let oldUrl = (new URL(originalUrl)).href;
453 console.log(`appcontrolUrl = ${appcontrolUrl}, oldUrl = ${oldUrl}`);
455 // Below case it must be distinguishable for known cases
456 // from 'file:///index.htmlx' to 'file:///index.html'
457 if (appcontrolUrl !== oldUrl.substr(0, appcontrolUrl.length))
462 } else if (src !== originalUrl) {
468 private handleAppControlReload(url: string) {
469 console.log('WebApplication : handleAppControlReload');
471 this.initDisplayDelay(false);
472 this.mainWindow.loadURL(url);
475 private handlePreloadState(launchMode: string) {
476 if (this.preloadStatus == 'readyToShow') {
479 if (launchMode != 'backgroundAtStartup')
480 this.preloadStatus = 'none';
484 private flushData() {
485 console.log('WebApplication : FlushData');
486 this.windowList.forEach((window) => window.webContents.session.flushStorageData());
489 sendAppControlEvent() {
490 const kAppControlEventScript = `(function(){
491 var __event = document.createEvent("CustomEvent");
492 __event.initCustomEvent("appcontrol", true, true, null);
493 document.dispatchEvent(__event);
494 for (var i=0; i < window.frames.length; i++)
495 window.frames[i].document.dispatchEvent(__event);
497 wrt.executeJS(this.mainWindow.webContents, kAppControlEventScript);
500 private activateIMEWebHelperClient() {
501 console.log('webApplication : activateIMEWebHelperClient');
502 const kImeActivateFunctionCallScript =
503 '(function(){WebHelperClient.impl.activate();})()';
504 wrt.executeJS(this.mainWindow.webContents, kImeActivateFunctionCallScript);
508 console.log('WebApplication : show');
509 this.preloadStatus = 'none';
510 if (this.backgroundExecution) {
511 console.log('skip showing while backgroundExecution mode');
512 } else if (!this.mainWindow.isVisible()) {
513 console.log('show window');
514 this.mainWindow.show();
518 private closeWindows() {
519 wrt.tv?.clearSurface(this.mainWindow.webContents);
520 this.windowList.forEach((window) => {
521 if (window != this.mainWindow)
526 keyEvent(key: string) {
527 console.log(`WebApplication : keyEvent[${key}]`);
531 addonManager.emit('hwUpkey', this.mainWindow.id);
535 addonManager.emit('hwDownkey', this.mainWindow.id);
538 console.log('No handler for ' + key);
543 prelaunch(url: string) {
544 console.log('WebApplication : prelaunch');
545 addonManager.emit('lcPrelaunch', this.mainWindow.id, url);
549 console.log('WebApplication : lowMemory to clearcache');
552 this.windowList.forEach((window) => {
553 //clear webframe cache
554 (wrt.tv as NativeWRTjs.TVExtension).clearWebCache(window.webContents);
555 window.webContents.session.clearCache(function() {
556 console.log('clear session Cache complete');
562 const kAmbientTickEventScript = `(function(){
563 var __event = document.createEvent("CustomEvent");
564 __event.initCustomEvent("timetick", true, true);
565 document.dispatchEvent(__event);
566 for (var i=0; i < window.frames.length; i++)
567 window.frames[i].document.dispatchEvent(__event);
569 wrt.executeJS(this.mainWindow.webContents, kAmbientTickEventScript);
572 ambientChanged(ambient_mode: boolean) {
573 const kAmbientChangedEventScript = `(function(){
574 var __event = document.createEvent(\"CustomEvent\");
576 __event.initCustomEvent(\"ambientmodechanged\",true,true,__detail);
577 __event.detail.ambientMode = ${ambient_mode ? 'true' : 'false'};
578 document.dispatchEvent(__event);
579 for (var i=0; i < window.frames.length; i++)
580 window.frames[i].document.dispatchEvent(__event);
582 wrt.executeJS(this.mainWindow.webContents, kAmbientChangedEventScript);