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', () => {
216 console.log('mainWindow ready-to-show');
218 clearTimeout(this.showTimer);
220 if (this.splashShown)
221 this.hideSplashScreen('first-paint');
226 this.mainWindow.webContents.on('did-start-loading', () => {
227 console.log('webContents did-start-loading');
228 this.loadFinished = false;
231 this.mainWindow.webContents.on('did-finish-load', () => {
232 console.log(`webContents did-finish-load, window length is ${this.windowList.length}`);
233 this.loadFinished = true;
235 if (!this.windowList.length)
237 if (this.splashShown)
238 this.hideSplashScreen('complete');
240 addonManager.emit('contentDidFinishLoad', this.mainWindow.id);
241 if (wrt.isIMEWebApp()) {
242 this.activateIMEWebHelperClient();
244 this.profileDelegate.onDidFinishLoad();
249 private enableWindow() {
250 this.suspended = false;
251 // TODO: On 6.0, this causes a black screen on relaunch
253 clearTimeout(this.showTimer);
254 this.mainWindow.setEnabled(true);
257 private initDisplayDelay() {
258 if (this.profileDelegate.isBackgroundLaunch())
261 this.splashShown = wrt.showSplashScreen();
262 if (this.splashShown || !this.profileDelegate.needShowTimer())
265 this.showTimer = setTimeout(() => {
266 if (!this.suspended) {
267 console.log('FrameRendered not obtained from engine. To show window, timer fired');
268 this.mainWindow.emit('ready-to-show');
273 handleAppControlEvent(appControl: any) {
274 if (!this.profileDelegate.handleAppControlEvent(appControl)) {
278 let loadInfo = appControl.getLoadInfo();
279 let src = loadInfo.getSrc();
280 this.reload = loadInfo.getReload() || this.profileDelegate.needReload(src);
281 // handle http://tizen.org/appcontrol/operation/main operation specially.
282 // only menu-screen app can send launch request with main operation.
283 // in this case, web app should have to resume web app not reset.
284 if (this.reload && appControl.getOperation() == 'http://tizen.org/appcontrol/operation/main')
287 this.handleAppControlReload(src);
289 this.sendAppControlEvent();
292 private launchInspectorIfNeeded(appControl: NativeWRTjs.AppControl) {
293 console.log('launchInspectorIfNeeded');
294 let needInpectorGuide = this.profileDelegate.needInpectorGuide();
295 let hasAulDebug = (appControl.getData('__AUL_DEBUG__') === '1');
297 if (hasAulDebug || needInpectorGuide) {
298 let debugPort = wrt.startInspectorServer();
299 let data = { "port": [debugPort.toString()] };
300 this.debugPort = debugPort;
301 appControl.reply(data);
305 loadUrl(appControl: NativeWRTjs.AppControl) {
306 this.contentSrc = appControl.getLoadInfo().getSrc();
307 this.launchInspectorIfNeeded(appControl);
308 this.mainWindow.loadURL(this.contentSrc);
309 this.prelaunch(this.contentSrc);
311 this.mainWindow.emit('ready-to-show');
315 private isPausable() {
316 return !this.backgroundSupport && !this.profileDelegate.canIgnoreSuspend();
320 if (this.suspended || this.inQuit)
322 console.log('WebApplication : suspend');
323 this.suspended = true;
324 if (this.windowList.length > 0) {
325 addonManager.emit('lcSuspend', this.mainWindow.id);
326 this.windowList[this.windowList.length - 1].hide();
328 if (this.isPausable()) {
329 this.windowList.forEach((window) => window.setEnabled(false));
330 if (!this.multitaskingSupport && !this.profileDelegate.isBackgroundLaunch()) {
332 console.log('multitasking is not supported; quitting app')
341 console.log('WebApplication : resume');
342 this.suspended = false;
343 addonManager.emit('lcResume', this.mainWindow.id, this.reload);
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.isBackgroundLaunch()) {
404 console.log('show() will be skipped by background launch');
407 console.log('WebApplication : show');
408 if (!this.mainWindow.isVisible()) {
409 console.log(`show this.windowList.length : ${this.windowList.length}`);
410 this.mainWindow.show();
411 if (this.windowList.length > 1) {
412 this.windowList[this.windowList.length - 1].moveTop();
417 private closeWindows() {
418 this.profileDelegate.clearSurface(this.mainWindow.webContents);
419 this.windowList.slice().forEach((window) => {
420 if (window != this.mainWindow)
425 keyEvent(key: string) {
426 console.log(`WebApplication : keyEvent[${key}]`);
430 addonManager.emit('hwUpkey', this.mainWindow.id);
434 addonManager.emit('hwDownkey', this.mainWindow.id);
437 console.log('No handler for ' + key);
442 prelaunch(url: string) {
443 console.log('WebApplication : prelaunch');
444 addonManager.emit('lcPrelaunch', this.mainWindow.id, url);
448 this.profileDelegate.clearCache();
452 const kAmbientTickEventScript = `(function(){
453 var __event = document.createEvent("CustomEvent");
454 __event.initCustomEvent("timetick", true, true);
455 document.dispatchEvent(__event);
456 for (var i=0; i < window.frames.length; i++)
457 window.frames[i].document.dispatchEvent(__event);
459 wrt.executeJS(this.mainWindow.webContents, kAmbientTickEventScript);
462 ambientChanged(ambient_mode: boolean) {
463 const kAmbientChangedEventScript = `(function(){
464 var __event = document.createEvent(\"CustomEvent\");
466 __event.initCustomEvent(\"ambientmodechanged\",true,true,__detail);
467 __event.detail.ambientMode = ${ambient_mode ? 'true' : 'false'};
468 document.dispatchEvent(__event);
469 for (var i=0; i < window.frames.length; i++)
470 window.frames[i].document.dispatchEvent(__event);
472 wrt.executeJS(this.mainWindow.webContents, kAmbientChangedEventScript);