4 * Copyright (c) 2019 Samsung Electronics Co., Ltd All Rights Reserved
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
21 import { BrowserWindow, app, session } from 'electron';
22 import { wrt } from '../browser/wrt';
23 import { addonManager } from './addon_manager';
24 import { WebApplicationDelegate } from '../common/web_application_delegate';
25 import { WebApplicationDelegateTV } from './tv/web_application_tv';
27 export class WebApplication {
28 defaultBackgroundColor: string = (wrt.getPlatformType() === "product_wearable") ? '#000' : '#FFF';
29 defaultTransparent: boolean = false;
30 mainWindow: Electron.BrowserWindow;
31 multitaskingSupport: boolean = true;
32 notificationPermissionMap?: Map<Electron.WebContents, boolean>;
33 showTimer?: NodeJS.Timeout;
35 backgroundSupport: boolean = wrt.getBackgroundSupport();
36 debugPort: number = 0;
37 contentSrc: string = '';
38 loadFinished: boolean = false;
39 pendingCallbacks: Map<number, any> = new Map();
40 pendingID: number = 0;
41 suspended: boolean = false;
42 windowList: Electron.BrowserWindow[] = [];
43 inQuit: boolean = false;
44 profileDelegate: WebApplicationDelegate;
45 splashShown: boolean = false;
46 reload: boolean = false;
48 constructor(options: RuntimeOption) {
50 this.profileDelegate = new WebApplicationDelegateTV(this);
51 this.profileDelegate.initialize(options);
53 this.profileDelegate = new WebApplicationDelegate(this);
55 this.setupEventListener(options);
56 this.mainWindow = new BrowserWindow(this.getWindowOption(options));
57 this.initDisplayDelay();
58 this.setupMainWindowEventListener();
61 private setupEventListener(options: RuntimeOption) {
62 app.on('browser-window-created', (event: any, window: any) => {
63 if (this.windowList.length > 0)
64 this.windowList[this.windowList.length - 1].hide();
65 this.windowList.push(window);
66 console.log(`window created : #${this.windowList.length}`);
68 window.on('closed', () => {
69 console.log(`window closed : #${this.windowList.length}`);
70 let index = this.windowList.indexOf(window);
71 this.windowList.splice(index, 1);
72 if (!this.inQuit && index === this.windowList.length && this.windowList.length > 0) {
73 let lastWindow = this.windowList[this.windowList.length - 1];
75 this.profileDelegate.focus(lastWindow.webContents);
80 app.on('web-contents-created', (event: any, webContents: any) => {
81 webContents.on('crashed', function() {
82 console.error('webContents crashed');
86 webContents.session.setPermissionRequestHandler((webContents: any, permission: string, callback: any) => {
87 console.log(`handlePermissionRequests for ${permission}`);
88 if (permission === 'notifications') {
89 if (!this.notificationPermissionMap)
90 this.notificationPermissionMap = new Map();
91 else if (this.notificationPermissionMap.has(webContents)) {
92 process.nextTick(callback, this.notificationPermissionMap.get(webContents));
95 const id = ++this.pendingID;
96 console.log(`Raising a notification permission request with id: ${id}`);
97 this.pendingCallbacks.set(id, (result: boolean) => {
98 (this.notificationPermissionMap as Map<Electron.WebContents, boolean>).set(webContents, result);
101 wrt.handleNotificationPermissionRequest(id, webContents);
102 } else if (permission === 'media') {
103 const id = ++this.pendingID;
104 console.log(`Raising a media permission request with id: ${id}`);
105 this.pendingCallbacks.set(id, callback);
106 wrt.handleMediaPermissionRequest(id, webContents);
107 } else if (permission === 'geolocation') {
108 const id = ++this.pendingID;
109 console.log(`Raising a geolocation permission request with id: ${id}`);
110 this.pendingCallbacks.set(id, callback);
111 wrt.handleGeolocationPermissionRequest(id, webContents);
113 /* electron by default allows permission for all if no request handler
114 is there; so granting permission only temporarily to not have any
121 app.on('certificate-error', (event: any, webContents: any, url: string, error: string, certificate: any, callback: any) => {
122 console.log('A certificate error has occurred');
123 event.preventDefault();
124 if (certificate.data) {
125 const id = ++this.pendingID;
126 console.log(`Raising a certificate error response with id: ${id}`);
127 this.pendingCallbacks.set(id, callback);
128 wrt.handleCertificateError(id, webContents, certificate.data, url, error);
130 console.log('Certificate could not be opened');
135 app.on('login', (event: any, webContents: any, request: any, authInfo: any, callback: any) => {
136 console.log(`Login info is required, isproxy: ${authInfo.isProxy}`);
137 event.preventDefault();
138 if (!this.profileDelegate.handleProxyInfo(authInfo, callback)) {
139 const id = ++this.pendingID;
140 console.log(`Raising a login info request with id: ${id}`);
141 this.pendingCallbacks.set(id, callback);
142 wrt.handleAuthRequest(id, webContents);
146 wrt.on('permission-response', (event: any, id: number, result: boolean) => {
147 console.log(`permission-response for ${id} is ${result}`);
148 let callback = this.pendingCallbacks.get(id);
149 if (typeof callback === 'function') {
150 console.log('calling permission response callback');
152 this.pendingCallbacks.delete(id);
156 wrt.on('auth-response', (event: any, id: number, submit: boolean, user: string, password: string) => {
157 let callback = this.pendingCallbacks.get(id);
158 if (typeof callback === 'function') {
159 console.log('calling auth response callback');
161 callback(user, password);
164 this.pendingCallbacks.delete(id);
169 private getWindowOption(options: RuntimeOption): Electron.BrowserWindowConstructorOptions {
172 backgroundColor: this.defaultBackgroundColor,
173 transparent: this.defaultTransparent,
176 contextIsolation: options.isAddonAvailable,
177 nodeIntegration: options.isAddonAvailable,
178 nodeIntegrationInSubFrames: options.isAddonAvailable,
179 nodeIntegrationInWorker: false,
180 nativeWindowOpen: true,
185 hideSplashScreen(reason: string) {
187 case 'first-paint': {
188 if (wrt.hideSplashScreen(0) !== false)
193 if (wrt.hideSplashScreen(1) !== false)
198 if (wrt.hideSplashScreen(2) !== false)
202 case 'video-finished': {
211 private setupMainWindowEventListener() {
212 this.mainWindow.once('ready-to-show', () => {
213 console.log('mainWindow ready-to-show');
215 clearTimeout(this.showTimer);
217 if (this.splashShown)
218 this.hideSplashScreen('first-paint');
223 this.mainWindow.webContents.on('did-start-loading', () => {
224 console.log('webContents did-start-loading');
225 this.loadFinished = false;
228 this.mainWindow.webContents.on('did-finish-load', () => {
229 console.log(`webContents did-finish-load, window length is ${this.windowList.length}`);
230 this.loadFinished = true;
232 if (!this.windowList.length)
234 if (this.splashShown)
235 this.hideSplashScreen('complete');
237 addonManager.emit('contentDidFinishLoad', this.mainWindow.id);
238 if (wrt.isIMEWebApp()) {
239 this.activateIMEWebHelperClient();
241 this.profileDelegate.onDidFinishLoad();
246 private enableWindow() {
247 this.suspended = false;
248 // TODO: On 6.0, this causes a black screen on relaunch
250 clearTimeout(this.showTimer);
251 this.mainWindow.setEnabled(true);
254 private initDisplayDelay() {
255 if (this.profileDelegate.isBackgroundLaunch())
258 this.splashShown = wrt.showSplashScreen();
259 if (this.splashShown || !this.profileDelegate.needShowTimer())
262 this.showTimer = setTimeout(() => {
263 if (!this.suspended) {
264 console.log('FrameRendered not obtained from engine. To show window, timer fired');
265 this.mainWindow.emit('ready-to-show');
270 handleAppControlEvent(appControl: any) {
271 if (!this.profileDelegate.handleAppControlEvent(appControl)) {
275 let loadInfo = appControl.getLoadInfo();
276 let src = loadInfo.getSrc();
277 this.reload = loadInfo.getReload() || this.profileDelegate.needReload(src);
278 // handle http://tizen.org/appcontrol/operation/main operation specially.
279 // only menu-screen app can send launch request with main operation.
280 // in this case, web app should have to resume web app not reset.
281 if (this.reload && appControl.getOperation() == 'http://tizen.org/appcontrol/operation/main')
284 this.handleAppControlReload(src);
286 this.sendAppControlEvent();
289 private launchInspectorIfNeeded(appControl: NativeWRTjs.AppControl) {
290 console.log('launchInspectorIfNeeded');
291 let needInpectorGuide = this.profileDelegate.needInpectorGuide();
292 let hasAulDebug = (appControl.getData('__AUL_DEBUG__') === '1');
294 if (hasAulDebug || needInpectorGuide) {
295 let debugPort = wrt.startInspectorServer();
296 let data = { "port": [debugPort.toString()] };
297 this.debugPort = debugPort;
298 appControl.reply(data);
302 loadUrl(appControl: NativeWRTjs.AppControl) {
303 this.contentSrc = appControl.getLoadInfo().getSrc();
304 this.launchInspectorIfNeeded(appControl);
305 this.mainWindow.loadURL(this.contentSrc);
306 this.prelaunch(this.contentSrc);
308 this.mainWindow.emit('ready-to-show');
312 private isPausable() {
313 return !this.backgroundSupport && !this.profileDelegate.canIgnoreSuspend();
317 if (this.suspended || this.inQuit)
319 console.log('WebApplication : suspend');
320 this.suspended = true;
321 if (this.windowList.length > 0) {
322 addonManager.emit('lcSuspend', this.mainWindow.id);
323 this.windowList[this.windowList.length - 1].hide();
325 if (this.isPausable()) {
326 this.windowList.forEach((window) => window.setEnabled(false));
327 if (!this.multitaskingSupport && !this.profileDelegate.isBackgroundLaunch()) {
329 console.log('multitasking is not supported; quitting app')
338 console.log('WebApplication : resume');
339 this.suspended = false;
340 addonManager.emit('lcResume', this.mainWindow.id, this.reload);
343 this.windowList.forEach((window) => window.setEnabled(true));
344 this.windowList[this.windowList.length - 1].show();
348 console.log('WebApplication : quit');
349 this.windowList.forEach((window) => {
350 window.removeAllListeners();
351 window.setEnabled(false);
358 console.log('WebApplication : beforeQuit');
359 this.profileDelegate.beforeQuit();
360 addonManager.emit('lcQuit', this.mainWindow.id);
361 if (this.debugPort) {
362 console.log('stop inspector server');
364 wrt.stopInspectorServer();
369 private handleAppControlReload(url: string) {
370 console.log('WebApplication : handleAppControlReload');
373 this.mainWindow.loadURL(url);
376 private flushData() {
377 console.log('WebApplication : FlushData');
378 session.defaultSession?.flushStorageData();
381 sendAppControlEvent() {
382 const kAppControlEventScript = `(function(){
383 var __event = document.createEvent("CustomEvent");
384 __event.initCustomEvent("appcontrol", true, true, null);
385 document.dispatchEvent(__event);
386 for (var i=0; i < window.frames.length; i++)
387 window.frames[i].document.dispatchEvent(__event);
389 wrt.executeJS(this.mainWindow.webContents, kAppControlEventScript);
392 private activateIMEWebHelperClient() {
393 console.log('webApplication : activateIMEWebHelperClient');
394 const kImeActivateFunctionCallScript =
395 '(function(){WebHelperClient.impl.activate();})()';
396 wrt.executeJS(this.mainWindow.webContents, kImeActivateFunctionCallScript);
400 if (this.profileDelegate.isBackgroundLaunch()) {
401 console.log('show() will be skipped by background launch');
404 console.log('WebApplication : show');
405 if (!this.mainWindow.isVisible()) {
406 console.log(`show this.windowList.length : ${this.windowList.length}`);
407 this.mainWindow.show();
408 if (this.windowList.length > 1) {
409 this.windowList[this.windowList.length - 1].moveTop();
414 private closeWindows() {
415 this.profileDelegate.clearSurface(this.mainWindow.webContents);
416 this.windowList.slice().forEach((window) => {
417 if (window != this.mainWindow)
422 keyEvent(key: string) {
423 console.log(`WebApplication : keyEvent[${key}]`);
427 addonManager.emit('hwUpkey', this.mainWindow.id);
431 addonManager.emit('hwDownkey', this.mainWindow.id);
434 console.log('No handler for ' + key);
439 prelaunch(url: string) {
440 console.log('WebApplication : prelaunch');
441 addonManager.emit('lcPrelaunch', this.mainWindow.id, url);
445 this.profileDelegate.clearCache();
449 const kAmbientTickEventScript = `(function(){
450 var __event = document.createEvent("CustomEvent");
451 __event.initCustomEvent("timetick", true, true);
452 document.dispatchEvent(__event);
453 for (var i=0; i < window.frames.length; i++)
454 window.frames[i].document.dispatchEvent(__event);
456 wrt.executeJS(this.mainWindow.webContents, kAmbientTickEventScript);
459 ambientChanged(ambient_mode: boolean) {
460 const kAmbientChangedEventScript = `(function(){
461 var __event = document.createEvent(\"CustomEvent\");
463 __event.initCustomEvent(\"ambientmodechanged\",true,true,__detail);
464 __event.detail.ambientMode = ${ambient_mode ? 'true' : 'false'};
465 document.dispatchEvent(__event);
466 for (var i=0; i < window.frames.length; i++)
467 window.frames[i].document.dispatchEvent(__event);
469 wrt.executeJS(this.mainWindow.webContents, kAmbientChangedEventScript);