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, session } 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';
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 WRTWindow(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): NativeWRTjs.WRTWindowConstructorOptions {
172 backgroundColor: this.defaultBackgroundColor,
173 transparent: this.defaultTransparent,
176 nodeIntegration: options.isAddonAvailable,
177 nodeIntegrationInSubFrames: options.isAddonAvailable,
178 nodeIntegrationInWorker: false,
179 nativeWindowOpen: true,
181 webContents: WRTWebContents.create(),
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');
230 this.loadFinished = true;
231 if (this.splashShown)
232 this.hideSplashScreen('complete');
234 addonManager.emit('contentDidFinishLoad', this.mainWindow.id);
235 if (wrt.isIMEWebApp()) {
236 this.activateIMEWebHelperClient();
238 this.profileDelegate.onDidFinishLoad();
243 private enableWindow() {
244 this.suspended = false;
245 // TODO: On 6.0, this causes a black screen on relaunch
247 clearTimeout(this.showTimer);
248 if (!this.backgroundRunnable())
249 this.mainWindow.setEnabled(true);
252 private initDisplayDelay() {
253 if (this.profileDelegate.isPreloading())
256 this.splashShown = wrt.showSplashScreen();
257 if (this.splashShown || !this.profileDelegate.needShowTimer())
260 this.showTimer = setTimeout(() => {
261 if (!this.suspended) {
262 console.log('FrameRendered not obtained from engine. To show window, timer fired');
263 this.mainWindow.emit('ready-to-show');
268 private backgroundRunnable() {
269 return this.backgroundSupport || this.profileDelegate.backgroundExecutableLaunchMode();
272 handleAppControlEvent(appControl: any) {
273 if (!this.profileDelegate.handleAppControlEvent(appControl)) {
277 let loadInfo = appControl.getLoadInfo();
278 let src = loadInfo.getSrc();
279 this.reload = loadInfo.getReload() || this.profileDelegate.needReload(src);
280 // handle http://tizen.org/appcontrol/operation/main operation specially.
281 // only menu-screen app can send launch request with main operation.
282 // in this case, web app should have to resume web app not reset.
283 if (this.reload && appControl.getOperation() == 'http://tizen.org/appcontrol/operation/main')
286 this.handleAppControlReload(src);
288 this.sendAppControlEvent();
291 private launchInspectorIfNeeded(appControl: NativeWRTjs.AppControl) {
292 console.log('launchInspectorIfNeeded');
293 let needInpectorGuide = this.profileDelegate.needInpectorGuide();
294 let hasAulDebug = (appControl.getData('__AUL_DEBUG__') === '1');
296 if (hasAulDebug || needInpectorGuide) {
297 let debugPort = wrt.startInspectorServer();
298 let data = { "port": [debugPort.toString()] };
299 this.debugPort = debugPort;
300 appControl.reply(data);
304 loadUrl(appControl: NativeWRTjs.AppControl) {
305 this.contentSrc = appControl.getLoadInfo().getSrc();
306 this.launchInspectorIfNeeded(appControl);
307 this.mainWindow.loadURL(this.contentSrc);
308 this.prelaunch(this.contentSrc);
310 this.mainWindow.emit('ready-to-show');
314 private isPausable() {
315 return !this.profileDelegate.canIgnoreSuspend() && !this.backgroundRunnable();
319 if (this.suspended || this.inQuit)
321 console.log('WebApplication : suspend');
322 this.suspended = true;
323 if (this.windowList.length > 0) {
324 addonManager.emit('lcSuspend', this.mainWindow.id);
325 this.windowList[this.windowList.length - 1].hide();
327 if (this.isPausable()) {
328 this.windowList.forEach((window) => window.setEnabled(false));
329 if (!this.multitaskingSupport && !this.profileDelegate.isPreloading()) {
331 console.log('multitasking is not supported; quitting app')
340 console.log('WebApplication : resume');
341 this.suspended = false;
342 addonManager.emit('lcResume', this.mainWindow.id, this.reload);
345 if (!this.backgroundRunnable())
346 this.windowList.forEach((window) => window.setEnabled(true));
347 this.windowList[this.windowList.length - 1].show();
351 console.log('WebApplication : quit');
352 this.windowList.forEach((window) => {
353 window.removeAllListeners();
354 window.setEnabled(false);
361 console.log('WebApplication : beforeQuit');
362 this.profileDelegate.beforeQuit();
363 addonManager.emit('lcQuit', this.mainWindow.id);
364 if (this.debugPort) {
365 console.log('stop inspector server');
367 wrt.stopInspectorServer();
372 private handleAppControlReload(url: string) {
373 console.log('WebApplication : handleAppControlReload');
376 this.mainWindow.loadURL(url);
379 private flushData() {
380 console.log('WebApplication : FlushData');
381 session.defaultSession?.flushStorageData();
384 sendAppControlEvent() {
385 const kAppControlEventScript = `(function(){
386 var __event = document.createEvent("CustomEvent");
387 __event.initCustomEvent("appcontrol", true, true, null);
388 document.dispatchEvent(__event);
389 for (var i=0; i < window.frames.length; i++)
390 window.frames[i].document.dispatchEvent(__event);
392 wrt.executeJS(this.mainWindow.webContents, kAppControlEventScript);
395 private activateIMEWebHelperClient() {
396 console.log('webApplication : activateIMEWebHelperClient');
397 const kImeActivateFunctionCallScript =
398 '(function(){WebHelperClient.impl.activate();})()';
399 wrt.executeJS(this.mainWindow.webContents, kImeActivateFunctionCallScript);
403 if (this.profileDelegate.isPreloading()) {
406 console.log('WebApplication : show');
407 if (this.profileDelegate.backgroundExecutableLaunchMode()) {
408 console.log('skip showing while backgroundExecutionLaunchMode mode');
409 } else if (!this.mainWindow.isVisible()) {
410 console.log(`show this.windowList.length : ${this.windowList.length}`);
411 this.mainWindow.show();
412 if (this.windowList.length > 1) {
413 this.windowList[this.windowList.length - 1].moveTop();
418 private closeWindows() {
419 this.profileDelegate.clearSuface(this.mainWindow.webContents);
420 this.windowList.slice().forEach((window) => {
421 if (window != this.mainWindow)
426 keyEvent(key: string) {
427 console.log(`WebApplication : keyEvent[${key}]`);
431 addonManager.emit('hwUpkey', this.mainWindow.id);
435 addonManager.emit('hwDownkey', this.mainWindow.id);
438 console.log('No handler for ' + key);
443 prelaunch(url: string) {
444 console.log('WebApplication : prelaunch');
445 addonManager.emit('lcPrelaunch', this.mainWindow.id, url);
449 this.profileDelegate.clearCache();
453 const kAmbientTickEventScript = `(function(){
454 var __event = document.createEvent("CustomEvent");
455 __event.initCustomEvent("timetick", true, true);
456 document.dispatchEvent(__event);
457 for (var i=0; i < window.frames.length; i++)
458 window.frames[i].document.dispatchEvent(__event);
460 wrt.executeJS(this.mainWindow.webContents, kAmbientTickEventScript);
463 ambientChanged(ambient_mode: boolean) {
464 const kAmbientChangedEventScript = `(function(){
465 var __event = document.createEvent(\"CustomEvent\");
467 __event.initCustomEvent(\"ambientmodechanged\",true,true,__detail);
468 __event.detail.ambientMode = ${ambient_mode ? 'true' : 'false'};
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, kAmbientChangedEventScript);