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;
47 earlyLoadedUrl: string = '';
49 constructor(options: RuntimeOption) {
51 this.profileDelegate = new WebApplicationDelegateTV(this);
52 this.profileDelegate.initialize(options);
54 this.profileDelegate = new WebApplicationDelegate(this);
56 this.setupEventListener(options);
57 this.mainWindow = new BrowserWindow(this.getWindowOption(options));
58 this.initDisplayDelay();
59 this.setupMainWindowEventListener();
62 private setupEventListener(options: RuntimeOption) {
63 app.on('browser-window-created', (event: any, window: any) => {
64 if (this.windowList.length > 0)
65 this.windowList[this.windowList.length - 1].hide();
66 this.windowList.push(window);
67 console.log(`window created : #${this.windowList.length}`);
69 window.on('closed', () => {
70 console.log(`window closed : #${this.windowList.length}`);
71 let index = this.windowList.indexOf(window);
72 this.windowList.splice(index, 1);
73 if (!this.inQuit && index === this.windowList.length && this.windowList.length > 0) {
74 let lastWindow = this.windowList[this.windowList.length - 1];
76 this.profileDelegate.focus(lastWindow.webContents);
81 app.on('web-contents-created', (event: any, webContents: any) => {
82 webContents.on('crashed', function() {
83 console.error('webContents crashed');
87 webContents.session.setPermissionRequestHandler((webContents: any, permission: string, callback: any) => {
88 console.log(`handlePermissionRequests for ${permission}`);
89 if (permission === 'notifications') {
90 if (!this.notificationPermissionMap)
91 this.notificationPermissionMap = new Map();
92 else if (this.notificationPermissionMap.has(webContents)) {
93 process.nextTick(callback, this.notificationPermissionMap.get(webContents));
96 const id = ++this.pendingID;
97 console.log(`Raising a notification permission request with id: ${id}`);
98 this.pendingCallbacks.set(id, (result: boolean) => {
99 (this.notificationPermissionMap as Map<Electron.WebContents, boolean>).set(webContents, result);
102 wrt.handleNotificationPermissionRequest(id, webContents);
103 } else if (permission === 'media') {
104 const id = ++this.pendingID;
105 console.log(`Raising a media permission request with id: ${id}`);
106 this.pendingCallbacks.set(id, callback);
107 wrt.handleMediaPermissionRequest(id, webContents);
108 } else if (permission === 'geolocation') {
109 const id = ++this.pendingID;
110 console.log(`Raising a geolocation permission request with id: ${id}`);
111 this.pendingCallbacks.set(id, callback);
112 wrt.handleGeolocationPermissionRequest(id, webContents);
114 /* electron by default allows permission for all if no request handler
115 is there; so granting permission only temporarily to not have any
122 app.on('certificate-error', (event: any, webContents: any, url: string, error: string, certificate: any, callback: any) => {
123 console.log('A certificate error has occurred');
124 event.preventDefault();
125 if (certificate.data) {
126 const id = ++this.pendingID;
127 console.log(`Raising a certificate error response with id: ${id}`);
128 this.pendingCallbacks.set(id, callback);
129 wrt.handleCertificateError(id, webContents, certificate.data, url, error);
131 console.log('Certificate could not be opened');
136 app.on('login', (event: any, webContents: any, request: any, authInfo: any, callback: any) => {
137 console.log(`Login info is required, isproxy: ${authInfo.isProxy}`);
138 event.preventDefault();
139 if (!this.profileDelegate.handleProxyInfo(authInfo, callback)) {
140 const id = ++this.pendingID;
141 console.log(`Raising a login info request with id: ${id}`);
142 this.pendingCallbacks.set(id, callback);
143 wrt.handleAuthRequest(id, webContents);
147 wrt.on('permission-response', (event: any, id: number, result: boolean) => {
148 console.log(`permission-response for ${id} is ${result}`);
149 let callback = this.pendingCallbacks.get(id);
150 if (typeof callback === 'function') {
151 console.log('calling permission response callback');
153 this.pendingCallbacks.delete(id);
157 wrt.on('auth-response', (event: any, id: number, submit: boolean, user: string, password: string) => {
158 let callback = this.pendingCallbacks.get(id);
159 if (typeof callback === 'function') {
160 console.log('calling auth response callback');
162 callback(user, password);
165 this.pendingCallbacks.delete(id);
170 private getWindowOption(options: RuntimeOption): Electron.BrowserWindowConstructorOptions {
173 backgroundColor: this.defaultBackgroundColor,
174 transparent: this.defaultTransparent,
177 contextIsolation: options.isAddonAvailable,
178 nodeIntegration: options.isAddonAvailable,
179 nodeIntegrationInSubFrames: options.isAddonAvailable,
180 nodeIntegrationInWorker: false,
181 nativeWindowOpen: true,
186 hideSplashScreen(reason: string) {
188 case 'first-paint': {
189 if (wrt.hideSplashScreen(0) !== false)
194 if (wrt.hideSplashScreen(1) !== false)
199 if (wrt.hideSplashScreen(2) !== false)
203 case 'video-finished': {
212 private setupMainWindowEventListener() {
213 this.mainWindow.once('ready-to-show', () => {
217 console.log('mainWindow ready-to-show');
219 clearTimeout(this.showTimer);
221 if (this.splashShown)
222 this.hideSplashScreen('first-paint');
227 this.mainWindow.webContents.on('did-start-loading', () => {
228 console.log('webContents did-start-loading');
229 this.loadFinished = false;
232 this.mainWindow.webContents.on('did-finish-load', () => {
233 console.log(`webContents did-finish-load, window length is ${this.windowList.length}`);
234 this.loadFinished = true;
236 if (!this.windowList.length)
238 if (this.splashShown)
239 this.hideSplashScreen('complete');
241 addonManager.emit('contentDidFinishLoad', this.mainWindow.id);
242 if (wrt.isIMEWebApp()) {
243 this.activateIMEWebHelperClient();
245 this.profileDelegate.onDidFinishLoad();
250 private enableWindow() {
251 this.suspended = false;
252 // TODO: On 6.0, this causes a black screen on relaunch
254 clearTimeout(this.showTimer);
255 this.mainWindow.setEnabled(true);
258 private initDisplayDelay() {
259 if (this.profileDelegate.isBackgroundLaunch())
262 this.splashShown = wrt.showSplashScreen();
263 if (this.splashShown || !this.profileDelegate.needShowTimer())
266 this.showTimer = setTimeout(() => {
267 if (!this.suspended) {
268 console.log('FrameRendered not obtained from engine. To show window, timer fired');
269 this.mainWindow.emit('ready-to-show');
274 handleAppControlEvent(appControl: any) {
275 if (!this.profileDelegate.handleAppControlEvent(appControl)) {
279 let loadInfo = appControl.getLoadInfo();
280 let src = loadInfo.getSrc();
281 this.reload = loadInfo.getReload() || this.profileDelegate.needReload(src);
282 // handle http://tizen.org/appcontrol/operation/main operation specially.
283 // only menu-screen app can send launch request with main operation.
284 // in this case, web app should have to resume web app not reset.
285 if (this.reload && appControl.getOperation() == 'http://tizen.org/appcontrol/operation/main')
288 this.handleAppControlReload(src);
290 this.sendAppControlEvent();
293 private launchInspectorIfNeeded(appControl: NativeWRTjs.AppControl) {
294 console.log('launchInspectorIfNeeded');
295 let needInpectorGuide = this.profileDelegate.needInpectorGuide();
296 let hasAulDebug = (appControl.getData('__AUL_DEBUG__') === '1');
298 if (hasAulDebug || needInpectorGuide) {
299 let debugPort = wrt.startInspectorServer();
300 let data = { "port": [debugPort.toString()] };
301 this.debugPort = debugPort;
302 appControl.reply(data);
307 if (this.debugPort) {
308 console.log('stop inspector server');
310 wrt.stopInspectorServer();
314 loadUrlEarly(url: string) {
315 console.log(`early load : ${url}`);
316 this.earlyLoadedUrl = url;
317 this.mainWindow.loadURL(url);
320 loadUrl(appControl: NativeWRTjs.AppControl) {
321 this.contentSrc = appControl.getLoadInfo().getSrc();
322 if (this.earlyLoadedUrl === this.contentSrc)
325 this.launchInspectorIfNeeded(appControl);
326 this.mainWindow.loadURL(this.contentSrc);
327 this.prelaunch(this.contentSrc);
329 this.mainWindow.emit('ready-to-show');
333 private isPausable() {
334 return !this.backgroundSupport && !this.profileDelegate.canIgnoreSuspend();
338 if (this.suspended || this.inQuit)
340 console.log('WebApplication : suspend');
341 this.suspended = true;
342 if (this.windowList.length > 0) {
343 addonManager.emit('lcSuspend', this.mainWindow.id);
344 this.windowList[this.windowList.length - 1].hide();
346 if (this.isPausable()) {
347 this.windowList.forEach((window) => window.setEnabled(false));
348 if (!this.multitaskingSupport && !this.profileDelegate.isBackgroundLaunch()) {
350 console.log('multitasking is not supported; quitting app')
359 console.log('WebApplication : resume');
360 this.suspended = false;
361 addonManager.emit('lcResume', this.mainWindow.id, this.reload);
364 this.windowList.forEach((window) => window.setEnabled(true));
365 this.windowList[this.windowList.length - 1].show();
369 console.log('WebApplication : quit');
370 this.windowList.forEach((window) => {
371 window.removeAllListeners();
372 window.setEnabled(false);
379 console.log('WebApplication : beforeQuit');
380 this.profileDelegate.beforeQuit();
381 addonManager.emit('lcQuit', this.mainWindow.id);
382 this.stopInspector();
386 private handleAppControlReload(url: string) {
387 console.log('WebApplication : handleAppControlReload');
390 this.mainWindow.loadURL(url);
393 private flushData() {
394 console.log('WebApplication : FlushData');
395 session.defaultSession?.flushStorageData();
398 sendAppControlEvent() {
399 const kAppControlEventScript = `(function(){
400 var __event = document.createEvent("CustomEvent");
401 __event.initCustomEvent("appcontrol", true, true, null);
402 document.dispatchEvent(__event);
403 for (var i=0; i < window.frames.length; i++)
404 window.frames[i].document.dispatchEvent(__event);
406 wrt.executeJS(this.mainWindow.webContents, kAppControlEventScript);
409 private activateIMEWebHelperClient() {
410 console.log('webApplication : activateIMEWebHelperClient');
411 const kImeActivateFunctionCallScript =
412 '(function(){WebHelperClient.impl.activate();})()';
413 wrt.executeJS(this.mainWindow.webContents, kImeActivateFunctionCallScript);
417 if (this.profileDelegate.isBackgroundLaunch()) {
418 console.log('show() will be skipped by background launch');
421 console.log('WebApplication : show');
422 if (!this.mainWindow.isVisible()) {
423 console.log(`show this.windowList.length : ${this.windowList.length}`);
424 this.mainWindow.show();
425 if (this.windowList.length > 1) {
426 this.windowList[this.windowList.length - 1].moveTop();
431 private closeWindows() {
432 this.profileDelegate.clearSurface(this.mainWindow.webContents);
433 this.windowList.slice().forEach((window) => {
434 if (window != this.mainWindow)
439 keyEvent(key: string) {
440 console.log(`WebApplication : keyEvent[${key}]`);
444 addonManager.emit('hwUpkey', this.mainWindow.id);
448 addonManager.emit('hwDownkey', this.mainWindow.id);
451 console.log('No handler for ' + key);
456 prelaunch(url: string) {
457 console.log('WebApplication : prelaunch');
458 addonManager.emit('lcPrelaunch', this.mainWindow.id, url);
462 this.profileDelegate.clearCache();
466 const kAmbientTickEventScript = `(function(){
467 var __event = document.createEvent("CustomEvent");
468 __event.initCustomEvent("timetick", true, true);
469 document.dispatchEvent(__event);
470 for (var i=0; i < window.frames.length; i++)
471 window.frames[i].document.dispatchEvent(__event);
473 wrt.executeJS(this.mainWindow.webContents, kAmbientTickEventScript);
476 ambientChanged(ambient_mode: boolean) {
477 const kAmbientChangedEventScript = `(function(){
478 var __event = document.createEvent(\"CustomEvent\");
480 __event.initCustomEvent(\"ambientmodechanged\",true,true,__detail);
481 __event.detail.ambientMode = ${ambient_mode ? 'true' : 'false'};
482 document.dispatchEvent(__event);
483 for (var i=0; i < window.frames.length; i++)
484 window.frames[i].document.dispatchEvent(__event);
486 wrt.executeJS(this.mainWindow.webContents, kAmbientChangedEventScript);