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 this.windowList[this.windowList.length - 1].show();
77 app.on('web-contents-created', (event: any, webContents: any) => {
78 webContents.on('crashed', function() {
79 console.error('webContents crashed');
83 webContents.session.setPermissionRequestHandler((webContents: any, permission: string, callback: any) => {
84 console.log(`handlePermissionRequests for ${permission}`);
85 if (permission === 'notifications') {
86 if (!this.notificationPermissionMap)
87 this.notificationPermissionMap = new Map();
88 else if (this.notificationPermissionMap.has(webContents)) {
89 process.nextTick(callback, this.notificationPermissionMap.get(webContents));
92 const id = ++this.pendingID;
93 console.log(`Raising a notification permission request with id: ${id}`);
94 this.pendingCallbacks.set(id, (result: boolean) => {
95 (this.notificationPermissionMap as Map<Electron.WebContents, boolean>).set(webContents, result);
98 wrt.handleNotificationPermissionRequest(id, webContents);
99 } else if (permission === 'media') {
100 const id = ++this.pendingID;
101 console.log(`Raising a media permission request with id: ${id}`);
102 this.pendingCallbacks.set(id, callback);
103 wrt.handleMediaPermissionRequest(id, webContents);
104 } else if (permission === 'geolocation') {
105 const id = ++this.pendingID;
106 console.log(`Raising a geolocation permission request with id: ${id}`);
107 this.pendingCallbacks.set(id, callback);
108 wrt.handleGeolocationPermissionRequest(id, webContents);
110 /* electron by default allows permission for all if no request handler
111 is there; so granting permission only temporarily to not have any
118 app.on('certificate-error', (event: any, webContents: any, url: string, error: string, certificate: any, callback: any) => {
119 console.log('A certificate error has occurred');
120 event.preventDefault();
121 if (certificate.data) {
122 const id = ++this.pendingID;
123 console.log(`Raising a certificate error response with id: ${id}`);
124 this.pendingCallbacks.set(id, callback);
125 wrt.handleCertificateError(id, webContents, certificate.data, url, error);
127 console.log('Certificate could not be opened');
132 app.on('login', (event: any, webContents: any, request: any, authInfo: any, callback: any) => {
133 console.log(`Login info is required, isproxy: ${authInfo.isProxy}`);
134 event.preventDefault();
135 if (!this.profileDelegate.handleProxyInfo(authInfo, callback)) {
136 const id = ++this.pendingID;
137 console.log(`Raising a login info request with id: ${id}`);
138 this.pendingCallbacks.set(id, callback);
139 wrt.handleAuthRequest(id, webContents);
143 wrt.on('permission-response', (event: any, id: number, result: boolean) => {
144 console.log(`permission-response for ${id} is ${result}`);
145 let callback = this.pendingCallbacks.get(id);
146 if (typeof callback === 'function') {
147 console.log('calling permission response callback');
149 this.pendingCallbacks.delete(id);
153 wrt.on('auth-response', (event: any, id: number, submit: boolean, user: string, password: string) => {
154 let callback = this.pendingCallbacks.get(id);
155 if (typeof callback === 'function') {
156 console.log('calling auth response callback');
158 callback(user, password);
161 this.pendingCallbacks.delete(id);
166 private getWindowOption(options: RuntimeOption): NativeWRTjs.WRTWindowConstructorOptions {
169 backgroundColor: this.defaultBackgroundColor,
170 transparent: this.defaultTransparent,
173 nodeIntegration: options.isAddonAvailable,
174 nodeIntegrationInSubFrames: options.isAddonAvailable,
175 nodeIntegrationInWorker: false,
176 nativeWindowOpen: true,
178 webContents: WRTWebContents.create(),
182 hideSplashScreen(reason: string) {
184 case 'first-paint': {
185 if (wrt.hideSplashScreen(0) !== false)
190 if (wrt.hideSplashScreen(1) !== false)
195 if (wrt.hideSplashScreen(2) !== false)
199 case 'video-finished': {
208 private setupMainWindowEventListener() {
209 this.mainWindow.once('ready-to-show', () => {
210 console.log('mainWindow ready-to-show');
212 clearTimeout(this.showTimer);
214 if (this.splashShown)
215 this.hideSplashScreen('first-paint');
220 this.mainWindow.webContents.on('did-start-loading', () => {
221 console.log('webContents did-start-loading');
222 this.loadFinished = false;
225 this.mainWindow.webContents.on('did-finish-load', () => {
226 console.log('webContents did-finish-load');
227 this.loadFinished = true;
228 if (this.splashShown)
229 this.hideSplashScreen('complete');
231 addonManager.emit('contentDidFinishLoad', this.mainWindow.id);
232 if (wrt.isIMEWebApp()) {
233 this.activateIMEWebHelperClient();
235 this.profileDelegate.onDidFinishLoad();
240 private enableWindow() {
241 this.suspended = false;
242 // TODO: On 6.0, this causes a black screen on relaunch
244 clearTimeout(this.showTimer);
245 if (!this.backgroundRunnable())
246 this.mainWindow.setEnabled(true);
249 private initDisplayDelay() {
250 if (this.profileDelegate.isPreloading())
253 this.splashShown = wrt.showSplashScreen();
254 if (this.splashShown || !this.profileDelegate.needShowTimer())
257 this.showTimer = setTimeout(() => {
258 if (!this.suspended) {
259 console.log('FrameRendered not obtained from engine. To show window, timer fired');
260 this.mainWindow.emit('ready-to-show');
265 private backgroundRunnable() {
266 return this.backgroundSupport || this.profileDelegate.backgroundExecutable();
269 handleAppControlEvent(appControl: any) {
270 if (!this.profileDelegate.handleAppControlEvent(appControl)) {
274 let loadInfo = appControl.getLoadInfo();
275 let src = loadInfo.getSrc();
276 this.reload = loadInfo.getReload() || this.profileDelegate.needReload(src);
277 // handle http://tizen.org/appcontrol/operation/main operation specially.
278 // only menu-screen app can send launch request with main operation.
279 // in this case, web app should have to resume web app not reset.
280 if (this.reload && appControl.getOperation() == 'http://tizen.org/appcontrol/operation/main')
283 this.handleAppControlReload(src);
285 this.sendAppControlEvent();
288 private launchInspectorIfNeeded(appControl: NativeWRTjs.AppControl) {
289 console.log('launchInspectorIfNeeded');
290 let needInpectorGuide = this.profileDelegate.needInpectorGuide();
291 let hasAulDebug = (appControl.getData('__AUL_DEBUG__') === '1');
293 if (hasAulDebug || needInpectorGuide) {
294 let debugPort = wrt.startInspectorServer();
295 let data = { "port": [debugPort.toString()] };
296 this.debugPort = debugPort;
297 appControl.reply(data);
301 loadUrl(appControl: NativeWRTjs.AppControl) {
302 this.contentSrc = appControl.getLoadInfo().getSrc();
303 this.launchInspectorIfNeeded(appControl);
304 this.mainWindow.loadURL(this.contentSrc);
305 this.prelaunch(this.contentSrc);
307 this.mainWindow.emit('ready-to-show');
312 if (this.suspended || this.inQuit)
314 console.log('WebApplication : suspend');
315 this.suspended = true;
316 if (this.windowList.length > 0) {
317 addonManager.emit('lcSuspend', this.mainWindow.id);
318 this.windowList[this.windowList.length - 1].hide();
321 if (this.profileDelegate.canIgnoreSuspend())
323 if (!this.backgroundRunnable()) {
324 this.windowList.forEach((window) => window.setEnabled(false));
325 if (!this.multitaskingSupport) {
327 console.log('multitasking is not supported; quitting app')
335 console.log('WebApplication : resume');
336 this.suspended = false;
337 addonManager.emit('lcResume', this.mainWindow.id, this.reload);
340 if (!this.backgroundRunnable())
341 this.windowList.forEach((window) => window.setEnabled(true));
342 this.windowList[this.windowList.length - 1].show();
346 console.log('WebApplication : quit');
348 this.windowList.forEach((window) => {
349 window.removeAllListeners();
350 window.setEnabled(false);
356 console.log('WebApplication : beforeQuit');
357 this.profileDelegate.beforeQuit();
358 addonManager.emit('lcQuit', this.mainWindow.id);
359 if (this.debugPort) {
360 console.log('stop inspector server');
362 wrt.stopInspectorServer();
367 private handleAppControlReload(url: string) {
368 console.log('WebApplication : handleAppControlReload');
371 this.mainWindow.loadURL(url);
374 private flushData() {
375 console.log('WebApplication : FlushData');
376 session.defaultSession?.flushStorageData();
379 sendAppControlEvent() {
380 const kAppControlEventScript = `(function(){
381 var __event = document.createEvent("CustomEvent");
382 __event.initCustomEvent("appcontrol", true, true, null);
383 document.dispatchEvent(__event);
384 for (var i=0; i < window.frames.length; i++)
385 window.frames[i].document.dispatchEvent(__event);
387 wrt.executeJS(this.mainWindow.webContents, kAppControlEventScript);
390 private activateIMEWebHelperClient() {
391 console.log('webApplication : activateIMEWebHelperClient');
392 const kImeActivateFunctionCallScript =
393 '(function(){WebHelperClient.impl.activate();})()';
394 wrt.executeJS(this.mainWindow.webContents, kImeActivateFunctionCallScript);
398 if (this.profileDelegate.isPreloading()) {
401 console.log('WebApplication : show');
402 if (this.profileDelegate.backgroundExecutable()) {
403 console.log('skip showing while backgroundExecution mode');
404 } else if (!this.mainWindow.isVisible()) {
405 console.log(`show this.windowList.length : ${this.windowList.length}`);
406 this.mainWindow.show();
407 if (this.windowList.length > 1) {
408 this.windowList[this.windowList.length - 1].moveTop();
413 private closeWindows() {
414 this.profileDelegate.clearSuface(this.mainWindow.webContents);
415 this.windowList.slice().forEach((window) => {
416 if (window != this.mainWindow)
421 keyEvent(key: string) {
422 console.log(`WebApplication : keyEvent[${key}]`);
426 addonManager.emit('hwUpkey', this.mainWindow.id);
430 addonManager.emit('hwDownkey', this.mainWindow.id);
433 console.log('No handler for ' + key);
438 prelaunch(url: string) {
439 console.log('WebApplication : prelaunch');
440 addonManager.emit('lcPrelaunch', this.mainWindow.id, url);
444 this.profileDelegate.clearCache();
448 const kAmbientTickEventScript = `(function(){
449 var __event = document.createEvent("CustomEvent");
450 __event.initCustomEvent("timetick", true, true);
451 document.dispatchEvent(__event);
452 for (var i=0; i < window.frames.length; i++)
453 window.frames[i].document.dispatchEvent(__event);
455 wrt.executeJS(this.mainWindow.webContents, kAmbientTickEventScript);
458 ambientChanged(ambient_mode: boolean) {
459 const kAmbientChangedEventScript = `(function(){
460 var __event = document.createEvent(\"CustomEvent\");
462 __event.initCustomEvent(\"ambientmodechanged\",true,true,__detail);
463 __event.detail.ambientMode = ${ambient_mode ? 'true' : 'false'};
464 document.dispatchEvent(__event);
465 for (var i=0; i < window.frames.length; i++)
466 window.frames[i].document.dispatchEvent(__event);
468 wrt.executeJS(this.mainWindow.webContents, kAmbientChangedEventScript);