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;
43 pendingCallbacks: Map<number, any> = new Map();
45 runningStatus = 'none';
47 windowList: Electron.BrowserWindow[] = [];
50 constructor(options: RuntimeOption) {
51 if (options.launchMode == 'backgroundAtStartup') {
52 console.log('backgroundAtStartup');
53 this.preloadStatus = 'preload';
55 this.preloadStatus = 'none';
57 if (options.launchMode == 'backgroundExecution') {
58 console.log('backgroundExecution');
59 this.backgroundExecution = true;
61 this.backgroundExecution = false;
63 this.accessiblePath = wrt.tv?.getAccessiblePath();
64 this.isAlwaysReload = (wrt.tv ? wrt.tv.isAlwaysReload() : false);
65 this.multitaskingSupport = (wrt.tv ? wrt.tv.getMultitaskingSupport() : true);
66 this.defaultBackgroundColor = (wrt.tv ? '#0000' :
67 ((wrt.getPlatformType() === "product_wearable") ? '#000' : '#FFF'));
68 this.defaultTransparent = (wrt.tv ? true : false);
70 this.setupEventListener(options);
72 this.mainWindow = new WRTWindow(this.getWindowOption(options));
73 this.initDisplayDelay(true);
74 this.setupMainWindowEventListener();
77 private setupEventListener(options: RuntimeOption) {
78 app.on('browser-window-created', (event: any, window: any) => {
79 if (this.windowList.length > 0)
80 this.windowList[this.windowList.length - 1].hide();
81 this.windowList.push(window);
82 console.log(`window created : #${this.windowList.length}`);
84 window.on('closed', () => {
85 console.log(`window closed : #${this.windowList.length}`);
86 let index = this.windowList.indexOf(window);
87 this.windowList.splice(index, 1);
88 if (index === this.windowList.length && this.windowList.length > 0)
89 this.windowList[this.windowList.length - 1].show();
93 app.on('web-contents-created', (event: any, webContents: any) => {
94 webContents.on('crashed', function() {
95 console.error('webContents crashed');
99 webContents.session.setPermissionRequestHandler((webContents: any, permission: string, callback: any) => {
100 console.log(`handlePermissionRequests for ${permission}`);
101 if (permission === 'notifications') {
102 if (!this.notificationPermissionMap)
103 this.notificationPermissionMap = new Map();
104 else if (this.notificationPermissionMap.has(webContents)) {
105 process.nextTick(callback, this.notificationPermissionMap.get(webContents));
108 const id = ++this.pendingID;
109 console.log(`Raising a notification permission request with id: ${id}`);
110 this.pendingCallbacks.set(id, (result: boolean) => {
111 (this.notificationPermissionMap as Map<Electron.WebContents, boolean>).set(webContents, result);
114 wrt.handleNotificationPermissionRequest(id, webContents);
115 } else if (permission === 'media') {
116 const id = ++this.pendingID;
117 console.log(`Raising a media permission request with id: ${id}`);
118 this.pendingCallbacks.set(id, callback);
119 wrt.handleMediaPermissionRequest(id, webContents);
120 } else if (permission === 'geolocation') {
121 const id = ++this.pendingID;
122 console.log(`Raising a geolocation permission request with id: ${id}`);
123 this.pendingCallbacks.set(id, callback);
124 wrt.handleGeolocationPermissionRequest(id, webContents);
126 /* electron by default allows permission for all if no request handler
127 is there; so granting permission only temporarily to not have any
134 app.on('certificate-error', (event: any, webContents: any, url: string, error: string, certificate: any, callback: any) => {
135 console.log('A certificate error has occurred');
136 event.preventDefault();
137 if (certificate.data) {
138 const id = ++this.pendingID;
139 console.log(`Raising a certificate error response with id: ${id}`);
140 this.pendingCallbacks.set(id, callback);
141 wrt.handleCertificateError(id, webContents, certificate.data, url, error);
143 console.log('Certificate could not be opened');
148 app.on('login', (event: any, webContents: any, request: any, authInfo: any, callback: any) => {
149 console.log(`Login info is required, isproxy: ${authInfo.isProxy}`);
150 event.preventDefault();
153 if (wrt.tv && authInfo.isProxy) {
154 let vconfProxy = wrt.tv.getProxy();
156 let proxyInfo = new URL(vconfProxy);
157 usrname = proxyInfo.username;
158 passwd = proxyInfo.password;
160 if (usrname && passwd) {
161 callback(usrname, passwd);
163 console.log('Login, but usrname and passwd is empty!!!');
167 const id = ++this.pendingID;
168 console.log(`Raising a login info request with id: ${id}`);
169 this.pendingCallbacks.set(id, callback);
170 wrt.handleAuthRequest(id, webContents);
174 if (this.accessiblePath) {
175 console.log(`accessiblePath: ${this.accessiblePath}`);
176 protocol.interceptFileProtocol('file', (request: any, callback: any) => {
178 let parsed_info = new URL(request.url);
179 let access_path = parsed_info.host + decodeURI(parsed_info.pathname);
180 console.log(`check path: : ${access_path}`);
181 for (let path of (this.accessiblePath as string[])) {
182 if (access_path.startsWith(path)) {
183 callback(access_path);
187 if (access_path.indexOf("/shared/res/") > -1) {
188 callback(access_path);
192 console.log(`invalid accesspath: ${access_path}`);
193 (callback as any)(403);
196 console.log('request url is empty');
197 (callback as any)(403);
199 }, (error: Error) => {
204 wrt.on('permission-response', (event: any, id: number, result: boolean) => {
205 console.log(`permission-response for ${id} is ${result}`);
206 let callback = this.pendingCallbacks.get(id);
207 if (typeof callback === 'function') {
208 console.log('calling permission response callback');
210 this.pendingCallbacks.delete(id);
214 wrt.on('auth-response', (event: any, id: number, submit: boolean, user: string, password: string) => {
215 let callback = this.pendingCallbacks.get(id);
216 if (typeof callback === 'function') {
217 console.log('calling auth response callback');
219 callback(user, password);
222 this.pendingCallbacks.delete(id);
226 wrt.on('app-status-changed', (event: any, status: string) => {
227 console.log(`runningStatus: ${status}, ${this.loadFinished}`);
230 this.runningStatus = status;
231 if (this.runningStatus === 'DialogClose' && this.inspectorSrc) {
232 console.log(`runningStatus is DialogClose, src is ${this.inspectorSrc}`);
233 this.mainWindow.loadURL(this.inspectorSrc);
234 this.inspectorSrc = '';
235 } else if (this.runningStatus == 'behind' && this.loadFinished) {
236 // TODO : Need to care this situation and decide to pass the addon event emitter to suspend()
242 private getWindowOption(options: RuntimeOption): NativeWRTjs.WRTWindowConstructorOptions {
245 backgroundColor: this.defaultBackgroundColor,
246 transparent: this.defaultTransparent,
249 nodeIntegration: options.isAddonAvailable,
250 nodeIntegrationInWorker: false,
251 nativeWindowOpen: true,
253 webContents: WRTWebContents.create(),
257 private setupMainWindowEventListener() {
258 this.mainWindow.once('ready-to-show', () => {
259 console.log('mainWindow ready-to-show');
261 clearTimeout(this.showTimer);
262 wrt.hideSplashScreen(0);
263 this.firstRendered = true;
264 if (this.preloadStatus == 'preload') {
265 this.preloadStatus = 'readyToShow';
266 console.log('preloading show is skipped!');
272 this.mainWindow.webContents.on('did-start-loading', () => {
273 console.log('webContents did-start-loading');
274 this.loadFinished = false;
277 this.mainWindow.webContents.on('did-finish-load', () => {
278 console.log('webContents did-finish-load');
279 this.loadFinished = true;
280 wrt.hideSplashScreen(1);
281 if (wrt.isIMEWebApp()) {
282 this.activateIMEWebHelperClient();
284 if (this.inspectorSrc)
285 this.showInspectorGuide();
287 this.suspendByStatus();
289 addonManager.emit('contentDidFinishLoad', this.mainWindow.id);
293 private initDisplayDelay(firstLaunch: boolean) {
294 // TODO: On 6.0, this causes a black screen on relaunch
296 this.firstRendered = false;
297 this.suspended = false;
299 clearTimeout(this.showTimer);
300 let splashShown = this.preloadStatus !== 'preload' && firstLaunch && wrt.showSplashScreen();
301 if (!splashShown && !wrt.tv) {
302 this.showTimer = setTimeout(() => {
303 if (!this.suspended) {
304 console.log('FrameRendered not obtained from engine. To show window, timer fired');
305 this.mainWindow.emit('ready-to-show');
309 if (!firstLaunch && !this.backgroundRunnable())
310 this.mainWindow.setEnabled(true);
313 private backgroundRunnable(): boolean {
314 return this.backgroundSupport || this.backgroundExecution;
317 private suspendByStatus() {
318 if (this.preloadStatus === 'readyToShow' ||
319 this.preloadStatus === 'preload' ||
320 this.runningStatus === 'behind') {
321 console.log('WebApplication : suspendByStatus');
322 console.log(`preloadStatus: ${this.preloadStatus}, runningStatus: ${this.runningStatus}`);
323 // TODO : Need to care this situation and decide to pass the addon event emitter to suspend()
325 if (this.runningStatus !== 'behind')
326 (wrt.tv as NativeWRTjs.TVExtension).notifyAppStatus('preload');
330 private showInspectorGuide() {
331 console.log('WebApplication : showInspectorGuide');
332 this.showInspectorGuide = () => {}; // call once
333 const message = `${this.debugPort.toString()}
334 Fast RWI is used, [about:blank] is loaded fist instead of
335 [${this.inspectorSrc}]
336 Click OK button will start the real loading.
338 Please connect to RWI in PC before click OK button.
339 Then you can get network log from the initial loading.
340 Please click Record button in Timeline panel in PC before click OK button,
341 Then you can get profile log from the initial loading.`;
342 let tv = wrt.tv as NativeWRTjs.TVExtension;
343 tv.showDialog(this.mainWindow.webContents, message);
345 if (this.preloadStatus !== 'none') {
347 tv.cancelDialogs(this.mainWindow.webContents);
352 handleAppControlEvent(appControl: any) {
353 let launchMode = appControl.getData('http://samsung.com/appcontrol/data/launch_mode');
354 this.handlePreloadState(launchMode);
356 let skipReload = appControl.getData('SkipReload');
357 if (skipReload == 'Yes') {
358 console.log('skipping reload');
359 // TODO : Need to care this situation and decide to pass the addon event emitter to resume()
364 let loadInfo = appControl.getLoadInfo();
365 let src = loadInfo.getSrc();
366 let reload = loadInfo.getReload() || this.needReload(src);
367 // handle http://tizen.org/appcontrol/operation/main operation specially.
368 // only menu-screen app can send launch request with main operation.
369 // in this case, web app should have to resume web app not reset.
370 if (reload && appControl.getOperation() == 'http://tizen.org/appcontrol/operation/main')
373 this.handleAppControlReload(src);
375 this.sendAppControlEvent();
378 private launchInspectorIfNeeded(appControl: NativeWRTjs.AppControl) {
379 console.log('launchInspectorIfNeeded');
380 let inspectorEnabledByVconf = wrt.tv ? wrt.tv.needUseInspector() : false;
381 if (inspectorEnabledByVconf && !this.backgroundExecution) {
382 this.inspectorSrc = this.contentSrc;
383 this.contentSrc = 'about:blank';
385 let hasAulDebug = (appControl.getData('__AUL_DEBUG__') === '1');
386 if (hasAulDebug || inspectorEnabledByVconf) {
387 let debugPort = wrt.startInspectorServer();
388 let data = { "port": [debugPort.toString()] };
389 this.debugPort = debugPort;
390 appControl.reply(data);
394 loadUrl(appControl: NativeWRTjs.AppControl) {
395 this.contentSrc = appControl.getLoadInfo().getSrc();
396 this.launchInspectorIfNeeded(appControl);
397 this.mainWindow.loadURL(this.contentSrc);
398 this.prelaunch(this.contentSrc);
400 this.mainWindow.emit('ready-to-show');
405 if (this.suspended || this.inQuit)
407 console.log('WebApplication : suspend');
408 addonManager.emit('lcSuspend', this.mainWindow.id);
409 this.suspended = true;
410 this.windowList[this.windowList.length - 1].hide();
412 if (!this.backgroundRunnable()) {
413 if (!this.multitaskingSupport) {
414 // FIXME : terminate app after visibilitychange event handling
416 console.log('multitasking is not supported; quitting app')
420 this.windowList.forEach((window) => window.setEnabled(false));
426 console.log('WebApplication : resume');
427 this.suspended = false;
428 addonManager.emit('lcResume', this.mainWindow.id);
430 if (!this.firstRendered) {
431 console.log('WebApplication : resume firstRendered is false');
434 if (!this.backgroundRunnable())
435 this.windowList.forEach((window) => window.setEnabled(true));
436 this.windowList[this.windowList.length - 1].show();
440 console.log('WebApplication : quit');
442 this.windowList.forEach((window) => window.removeAllListeners());
449 console.log('WebApplication : beforeQuit');
450 addonManager.emit('lcQuit', this.mainWindow.id);
452 this.inspectorSrc = '';
453 wrt.tv.cancelDialogs(this.mainWindow.webContents);
455 if (this.debugPort) {
456 console.log('stop inspector server');
458 wrt.stopInspectorServer();
463 private needReload(src: string) {
464 if (this.isAlwaysReload) {
468 let originalUrl = this.mainWindow.webContents.getURL();
470 console.log(`appcontrol src = ${src}, original url = ${originalUrl}`);
471 if (src && originalUrl) {
472 let appcontrolUrl = (new URL(src)).href;
473 let oldUrl = (new URL(originalUrl)).href;
474 console.log(`appcontrolUrl = ${appcontrolUrl}, oldUrl = ${oldUrl}`);
476 // Below case it must be distinguishable for known cases
477 // from 'file:///index.htmlx' to 'file:///index.html'
478 if (appcontrolUrl !== oldUrl.substr(0, appcontrolUrl.length))
483 } else if (src !== originalUrl) {
489 private handleAppControlReload(url: string) {
490 console.log('WebApplication : handleAppControlReload');
492 this.initDisplayDelay(false);
493 this.mainWindow.loadURL(url);
496 private handlePreloadState(launchMode: string) {
497 if (this.preloadStatus == 'readyToShow') {
500 if (launchMode != 'backgroundAtStartup')
501 this.preloadStatus = 'none';
505 private flushData() {
506 console.log('WebApplication : FlushData');
507 this.windowList.forEach((window) => window.webContents.session.flushStorageData());
510 sendAppControlEvent() {
511 const kAppControlEventScript = `(function(){
512 var __event = document.createEvent("CustomEvent");
513 __event.initCustomEvent("appcontrol", true, true, null);
514 document.dispatchEvent(__event);
515 for (var i=0; i < window.frames.length; i++)
516 window.frames[i].document.dispatchEvent(__event);
518 wrt.executeJS(this.mainWindow.webContents, kAppControlEventScript);
521 private activateIMEWebHelperClient() {
522 console.log('webApplication : activateIMEWebHelperClient');
523 const kImeActivateFunctionCallScript =
524 '(function(){WebHelperClient.impl.activate();})()';
525 wrt.executeJS(this.mainWindow.webContents, kImeActivateFunctionCallScript);
529 console.log('WebApplication : show');
530 this.preloadStatus = 'none';
531 if (this.backgroundExecution) {
532 console.log('skip showing while backgroundExecution mode');
533 } else if (!this.mainWindow.isVisible()) {
534 console.log('show window');
535 this.mainWindow.show();
539 private closeWindows() {
540 wrt.tv?.clearSurface(this.mainWindow.webContents);
541 this.windowList.forEach((window) => {
542 if (window != this.mainWindow)
547 keyEvent(key: string) {
548 console.log(`WebApplication : keyEvent[${key}]`);
552 addonManager.emit('hwUpkey', this.mainWindow.id);
556 addonManager.emit('hwDownkey', this.mainWindow.id);
559 console.log('No handler for ' + key);
564 prelaunch(url: string) {
565 console.log('WebApplication : prelaunch');
566 addonManager.emit('lcPrelaunch', this.mainWindow.id, url);
570 console.log('WebApplication : lowMemory to clearcache');
573 this.windowList.forEach((window) => {
574 //clear webframe cache
575 (wrt.tv as NativeWRTjs.TVExtension).clearWebCache(window.webContents);
576 window.webContents.session.clearCache(function() {
577 console.log('clear session Cache complete');
583 const kAmbientTickEventScript = `(function(){
584 var __event = document.createEvent("CustomEvent");
585 __event.initCustomEvent("timetick", true, true);
586 document.dispatchEvent(__event);
587 for (var i=0; i < window.frames.length; i++)
588 window.frames[i].document.dispatchEvent(__event);
590 wrt.executeJS(this.mainWindow.webContents, kAmbientTickEventScript);
593 ambientChanged(ambient_mode: boolean) {
594 const kAmbientChangedEventScript = `(function(){
595 var __event = document.createEvent(\"CustomEvent\");
597 __event.initCustomEvent(\"ambientmodechanged\",true,true,__detail);
598 __event.detail.ambientMode = ${ambient_mode ? 'true' : 'false'};
599 document.dispatchEvent(__event);
600 for (var i=0; i < window.frames.length; i++)
601 window.frames[i].document.dispatchEvent(__event);
603 wrt.executeJS(this.mainWindow.webContents, kAmbientChangedEventScript);