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();
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 enableWindow() {
294 this.suspended = false;
295 // TODO: On 6.0, this causes a black screen on relaunch
297 clearTimeout(this.showTimer);
298 if (!this.backgroundRunnable())
299 this.mainWindow.setEnabled(true);
302 private initDisplayDelay() {
303 let splashShown = this.preloadStatus !== 'preload' && wrt.showSplashScreen();
304 if (splashShown || wrt.tv)
307 this.showTimer = setTimeout(() => {
308 if (!this.suspended) {
309 console.log('FrameRendered not obtained from engine. To show window, timer fired');
310 this.mainWindow.emit('ready-to-show');
315 private backgroundRunnable(): boolean {
316 return this.backgroundSupport || this.backgroundExecution;
319 private suspendByStatus() {
320 if (this.preloadStatus === 'readyToShow' ||
321 this.preloadStatus === 'preload' ||
322 this.runningStatus === 'behind') {
323 console.log('WebApplication : suspendByStatus');
324 console.log(`preloadStatus: ${this.preloadStatus}, runningStatus: ${this.runningStatus}`);
325 // TODO : Need to care this situation and decide to pass the addon event emitter to suspend()
327 if (this.runningStatus !== 'behind')
328 (wrt.tv as NativeWRTjs.TVExtension).notifyAppStatus('preload');
332 private showInspectorGuide() {
333 console.log('WebApplication : showInspectorGuide');
334 this.showInspectorGuide = () => {}; // call once
335 const message = `${this.debugPort.toString()}
336 Fast RWI is used, [about:blank] is loaded fist instead of
337 [${this.inspectorSrc}]
338 Click OK button will start the real loading.
340 Please connect to RWI in PC before click OK button.
341 Then you can get network log from the initial loading.
342 Please click Record button in Timeline panel in PC before click OK button,
343 Then you can get profile log from the initial loading.`;
344 let tv = wrt.tv as NativeWRTjs.TVExtension;
345 tv.showDialog(this.mainWindow.webContents, message);
347 if (this.preloadStatus !== 'none') {
349 tv.cancelDialogs(this.mainWindow.webContents);
354 handleAppControlEvent(appControl: any) {
355 let launchMode = appControl.getData('http://samsung.com/appcontrol/data/launch_mode');
356 this.handlePreloadState(launchMode);
358 let skipReload = appControl.getData('SkipReload');
359 if (skipReload == 'Yes') {
360 console.log('skipping reload');
361 // TODO : Need to care this situation and decide to pass the addon event emitter to resume()
366 let loadInfo = appControl.getLoadInfo();
367 let src = loadInfo.getSrc();
368 let reload = loadInfo.getReload() || this.needReload(src);
369 // handle http://tizen.org/appcontrol/operation/main operation specially.
370 // only menu-screen app can send launch request with main operation.
371 // in this case, web app should have to resume web app not reset.
372 if (reload && appControl.getOperation() == 'http://tizen.org/appcontrol/operation/main')
375 this.handleAppControlReload(src);
377 this.sendAppControlEvent();
380 private launchInspectorIfNeeded(appControl: NativeWRTjs.AppControl) {
381 console.log('launchInspectorIfNeeded');
382 let inspectorEnabledByVconf = wrt.tv ? wrt.tv.needUseInspector() : false;
383 if (inspectorEnabledByVconf && !this.backgroundExecution) {
384 this.inspectorSrc = this.contentSrc;
385 this.contentSrc = 'about:blank';
387 let hasAulDebug = (appControl.getData('__AUL_DEBUG__') === '1');
388 if (hasAulDebug || inspectorEnabledByVconf) {
389 let debugPort = wrt.startInspectorServer();
390 let data = { "port": [debugPort.toString()] };
391 this.debugPort = debugPort;
392 appControl.reply(data);
396 loadUrl(appControl: NativeWRTjs.AppControl) {
397 this.contentSrc = appControl.getLoadInfo().getSrc();
398 this.launchInspectorIfNeeded(appControl);
399 this.mainWindow.loadURL(this.contentSrc);
400 this.prelaunch(this.contentSrc);
402 this.mainWindow.emit('ready-to-show');
407 if (this.suspended || this.inQuit)
409 console.log('WebApplication : suspend');
410 addonManager.emit('lcSuspend', this.mainWindow.id);
411 this.suspended = true;
412 this.windowList[this.windowList.length - 1].hide();
414 if (!this.backgroundRunnable()) {
415 if (!this.multitaskingSupport) {
416 // FIXME : terminate app after visibilitychange event handling
418 console.log('multitasking is not supported; quitting app')
422 this.windowList.forEach((window) => window.setEnabled(false));
428 console.log('WebApplication : resume');
429 this.suspended = false;
430 addonManager.emit('lcResume', this.mainWindow.id);
432 if (!this.firstRendered) {
433 console.log('WebApplication : resume firstRendered is false');
436 if (!this.backgroundRunnable())
437 this.windowList.forEach((window) => window.setEnabled(true));
438 this.windowList[this.windowList.length - 1].show();
442 console.log('WebApplication : quit');
444 this.windowList.forEach((window) => window.removeAllListeners());
451 console.log('WebApplication : beforeQuit');
452 addonManager.emit('lcQuit', this.mainWindow.id);
454 this.inspectorSrc = '';
455 wrt.tv.cancelDialogs(this.mainWindow.webContents);
457 if (this.debugPort) {
458 console.log('stop inspector server');
460 wrt.stopInspectorServer();
465 private needReload(src: string) {
466 if (this.isAlwaysReload) {
470 let originalUrl = this.mainWindow.webContents.getURL();
472 console.log(`appcontrol src = ${src}, original url = ${originalUrl}`);
473 if (src && originalUrl) {
474 let appcontrolUrl = (new URL(src)).href;
475 let oldUrl = (new URL(originalUrl)).href;
476 console.log(`appcontrolUrl = ${appcontrolUrl}, oldUrl = ${oldUrl}`);
478 // Below case it must be distinguishable for known cases
479 // from 'file:///index.htmlx' to 'file:///index.html'
480 if (appcontrolUrl !== oldUrl.substr(0, appcontrolUrl.length))
485 } else if (src !== originalUrl) {
491 private handleAppControlReload(url: string) {
492 console.log('WebApplication : handleAppControlReload');
495 this.mainWindow.loadURL(url);
498 private handlePreloadState(launchMode: string) {
499 if (this.preloadStatus == 'readyToShow') {
502 if (launchMode != 'backgroundAtStartup')
503 this.preloadStatus = 'none';
507 private flushData() {
508 console.log('WebApplication : FlushData');
509 this.windowList.forEach((window) => window.webContents.session.flushStorageData());
512 sendAppControlEvent() {
513 const kAppControlEventScript = `(function(){
514 var __event = document.createEvent("CustomEvent");
515 __event.initCustomEvent("appcontrol", true, true, null);
516 document.dispatchEvent(__event);
517 for (var i=0; i < window.frames.length; i++)
518 window.frames[i].document.dispatchEvent(__event);
520 wrt.executeJS(this.mainWindow.webContents, kAppControlEventScript);
523 private activateIMEWebHelperClient() {
524 console.log('webApplication : activateIMEWebHelperClient');
525 const kImeActivateFunctionCallScript =
526 '(function(){WebHelperClient.impl.activate();})()';
527 wrt.executeJS(this.mainWindow.webContents, kImeActivateFunctionCallScript);
531 console.log('WebApplication : show');
532 this.preloadStatus = 'none';
533 if (this.backgroundExecution) {
534 console.log('skip showing while backgroundExecution mode');
535 } else if (!this.mainWindow.isVisible()) {
536 console.log('show window');
537 this.mainWindow.show();
541 private closeWindows() {
542 wrt.tv?.clearSurface(this.mainWindow.webContents);
543 this.windowList.forEach((window) => {
544 if (window != this.mainWindow)
549 keyEvent(key: string) {
550 console.log(`WebApplication : keyEvent[${key}]`);
554 addonManager.emit('hwUpkey', this.mainWindow.id);
558 addonManager.emit('hwDownkey', this.mainWindow.id);
561 console.log('No handler for ' + key);
566 prelaunch(url: string) {
567 console.log('WebApplication : prelaunch');
568 addonManager.emit('lcPrelaunch', this.mainWindow.id, url);
572 console.log('WebApplication : lowMemory to clearcache');
575 this.windowList.forEach((window) => {
576 //clear webframe cache
577 (wrt.tv as NativeWRTjs.TVExtension).clearWebCache(window.webContents);
578 window.webContents.session.clearCache(function() {
579 console.log('clear session Cache complete');
585 const kAmbientTickEventScript = `(function(){
586 var __event = document.createEvent("CustomEvent");
587 __event.initCustomEvent("timetick", true, true);
588 document.dispatchEvent(__event);
589 for (var i=0; i < window.frames.length; i++)
590 window.frames[i].document.dispatchEvent(__event);
592 wrt.executeJS(this.mainWindow.webContents, kAmbientTickEventScript);
595 ambientChanged(ambient_mode: boolean) {
596 const kAmbientChangedEventScript = `(function(){
597 var __event = document.createEvent(\"CustomEvent\");
599 __event.initCustomEvent(\"ambientmodechanged\",true,true,__detail);
600 __event.detail.ambientMode = ${ambient_mode ? 'true' : 'false'};
601 document.dispatchEvent(__event);
602 for (var i=0; i < window.frames.length; i++)
603 window.frames[i].document.dispatchEvent(__event);
605 wrt.executeJS(this.mainWindow.webContents, kAmbientChangedEventScript);