import { FullScreenSplashController } from './fullScreenSplashController'; import { Logger } from '../common/logger'; import { FullScreenCapture } from './fullScreenCapture'; import { RouteEnum, TEACHER_CONFERENCE_TYPE_VIDEO } from '../common/defs'; import { Titlebar } from './titlebar'; import { LocalizeUtils } from '../common/localizeUtils'; import { ChatWindowBounds } from 'src/background/webViewWindow'; import { CallbackThrottle } from './callbackThrottle'; import { LogDownloadController } from './logDownload'; import { ConferenceAttendeeNamesEventModel, ConferenceDirectiveModel } from '@lenovo-software/lsa-clients-common/dist/models/conferencingModels'; // TODO: Logger ID? (Used to be [WebView]) interface StudentInstance { waitingForFullScreenSelect: boolean; isInChatMode: boolean; isInShowTeacherMode: boolean; } class WebView { private fullScreenSplashController: FullScreenSplashController; private logger: Logger; private fullScreenCapture: FullScreenCapture; private policy: any; private mainWnd: chrome.app.window.AppWindow; private mode: 'CHAT' | 'FULL' | 'MINI'; private chatWindowBounds: ChatWindowBounds | undefined; private toolbarButtonMessageToBackground: string; private ngUI: chrome.webviewTag | undefined; private ngUIPlsQueue: boolean; private ngUIMsgQueue: Array; private studentInstance: StudentInstance; private _thumbnailCaptureApproved: boolean; private callbackThrottler: CallbackThrottle; private logDownloadController: LogDownloadController; constructor() { this.logger = Logger.getInstance(); this.fullScreenSplashController = new FullScreenSplashController(); this.fullScreenCapture = new FullScreenCapture(); this.mode = 'CHAT'; this.ngUIPlsQueue = true; this.ngUIMsgQueue = []; this.studentInstance = { waitingForFullScreenSelect: false, isInChatMode: false, isInShowTeacherMode: false }; this._thumbnailCaptureApproved = false; this.toolbarButtonMessageToBackground = ''; this.callbackThrottler = new CallbackThrottle(100); this.logDownloadController = new LogDownloadController(this); this.mainWnd = chrome.app.window.current(); chrome.runtime.onMessage.addListener((request) => { switch (request.message) { case 'ToWebView': { this.sendToWebView(request.params, request.openWindow); break; } case 'MWClose': { if (this.mainWnd) this.mainWnd.close(); break; } case 'MWFocus': { if (this.mainWnd) { this.logger.logMessage('MWFocus'); this.mainWnd.focus(); } break; } case 'PolicyUpdate': { this.logger.logMessage('Webview received PolicyUpdate message'); this.policy = request.policy; this.fullScreenCapture.setPolicy(this.policy); this.enterChatMode(); break; } case 'LaunchChat': { this.showChatUI(); break; } case 'AudioRoute': { this.goToAudioPage(); break; } case 'ChatRoute': { this.goToChatPage(); break; } case 'ToolbarButton': { this.toolbarButtonMessageToBackground = request.toolbarButtonMessageToBackground; break; } case 'LaunchLegal': { window.open('https://lenovosoftware.com/legal/lanschool'); break; } case 'ChatWindowBounds': { this.chatWindowBounds = request.data; break; } case 'StartThumbnailCapture': { this.startThumbnailCaptureWithSource(this.policy, request.data.source); break; } case 'StopThumbnailCapture': { this.stopThumbnailCapture(); break; } case 'MainPageStartingConnection': { this.notifyBackground(); break; } case 'BeginViewBroadcast': { this.logger.logWarning('Received BeginViewBroadcast'); this.beginViewBroadcast(request.data); break; } case 'SetConferenceAttendeeNames': { this.logger.logMessage('Received SetConferenceAttendeeNames'); this.handleAttendeeNames(request.data); break; } case 'EndViewBroadcast': { this.endViewBroadcast(); this.addMyTitleBar(); break; } case 'FromUI_ConferenceParamsSet': { this.logger.logMessage('Received FromUI_ConferenceParamsSet'); this.joinConference(); break; } case 'FromUI_ConferenceDetachComplete': { this.logger.logMessage('Received FromUI_ConferenceDetachComplete'); this.addMyTitleBar(); this.onConferenceDetachComplete(); break; } } }); chrome.runtime.sendMessage({ message: 'ChatWebViewReady' }); } // TODO: any startThumbnailCaptureWithSource(policy: any, source: string) { if (!policy.student_privacy && source == 'Full Screen' && this._thumbnailCaptureApproved !== true) { this.startThumbnailCapture(); } else if (!policy.full_screen_thumbnail && !this.studentInstance.isInShowTeacherMode) { this.enterChatMode(); } } // TODO: any sendToWebView(message: any, openWindow?: boolean) { if (!this.ngUIPlsQueue && this.ngUI) { this.logger.logMessage('sendToWebView: Sending message direct (no queue): ' + JSON.stringify(message)); this.ngUI.contentWindow.postMessage(message, '/'); } else { this.logger.logMessage('sendToWebView: Queueing message: ' + JSON.stringify(message)); this.ngUIMsgQueue.push(message); } if (openWindow) this.showChatUI(); } showChatUI() { if (this.mainWnd) this.mainWnd.show(); } // TODO: Move to window controller forceShowWindow() { if (this.mainWnd && this.mainWnd.isAlwaysOnTop()) { this.logger.logMessage('Setting window to: (0, 0, ' + screen.width + ', ' + screen.height + ')'); this.mainWnd.setBounds({ left: 0, top: 0, width: screen.width, height: screen.height }); // Force the window to be visible and full screen. this.mainWnd.show(); this.fullScreenSplashController.fullscreen(this.mainWnd); this.notifyBackground(); // Call this method again in a couple seconds. setTimeout(this.forceShowWindow, 2000); } } minimize() { if (this.mainWnd) this.mainWnd.minimize(); } addSplashBackgroundImage(backgroundId: string, imageName: string) { if (document.getElementById(backgroundId)) return; var bg = document.createElement('div'); bg.setAttribute('id', backgroundId); var img = document.createElement('img'); img.setAttribute('id', 'background_image'); img.setAttribute('src', imageName); bg.appendChild(img); document.body.appendChild(bg); } removeSplashBackgroundImage(backgroundId: string) { var bg = document.getElementById(backgroundId); if (bg) document.body.removeChild(bg); } onConferenceDetachComplete() { this.goToChatPage(); // enterChatMode(); } joinConference() { this.logger.logMessage('Joining conference...'); this.sendToWebView({ message: 'UI_JoinConference' }); } beginViewBroadcast(data: ConferenceDirectiveModel) { this.addWebView(); if ( data.hasOwnProperty('teacherData') && data.windowed === false && data.conferenceType & TEACHER_CONFERENCE_TYPE_VIDEO ) { Titlebar.removeTitlebar('titlebar'); } else { if (this.mode !== 'MINI') { this.addMyTitleBar(); } else { Titlebar.removeTitlebar('titlebar'); } } this.sendToWebView({ message: 'UI_SetConferenceParams', data: data }); } handleAttendeeNames(data: ConferenceAttendeeNamesEventModel) { this.sendToWebView({ message: 'UI_SetConferenceAttendeeNames', data: data }); } buttonControl() { if (this.toolbarButtonMessageToBackground && this.toolbarButtonMessageToBackground.length > 0) { chrome.runtime.sendMessage({ message: this.toolbarButtonMessageToBackground }); } else { this.minimize(); } } addMyTitleBar() { Titlebar.addTitlebar( 'titlebar', LocalizeUtils.isLanguageRTL() ? 'titlebar-minimize-button-rtl' : 'titlebar-minimize-button', '../images/ic_minimize_client@1x.png', '../images/ic_minimize_client_hover@1x.png', () => this.buttonControl() ); } endViewBroadcast() { this.sendToWebView({ message: 'UI_LeaveConference' }); } goToChatPage() { this.logger.logMessage('goToChatPage(+)'); this.setRoute(RouteEnum.Chat); this.mode = 'CHAT'; if (!this.ngUIPlsQueue) { // webview is already loaded so this is safe to execute this.addMyTitleBar(); } } goToAudioPage() { this.logger.logMessage('goToAudioPage(+)'); this.setRoute(RouteEnum.Speaker); this.mode = 'MINI'; Titlebar.removeTitlebar('titlebar'); } // TODO: any setRoute(route: any) { this.callbackThrottler.queueCallback(() => { if (this.ngUI) { this.logger.logMessage('setRoute(): Setting route ' + route); this.ngUI.src = route; } }); } addWebView() { if (document.getElementById('webview')) return; var div = document.createElement('div'); div.setAttribute('id', 'webview'); var webview = document.createElement('webview') as chrome.webviewTag; webview.setAttribute('partition', 'trustedWebView'); webview.setAttribute('style', 'width: 100%; height: 100%;'); this.ngUI = webview; this.goToChatPage(); webview.addEventListener('contentload', () => { while (this.ngUIMsgQueue.length > 0) { var dequeued = this.ngUIMsgQueue.shift(); this.logger.logMessage('Sending message (msgQueue): ' + JSON.stringify(dequeued)); this.ngUI?.contentWindow.postMessage(dequeued, '/'); } this.ngUIPlsQueue = false; }); // TODO: any webview.addEventListener('permissionrequest', (e: any) => { this.logger.logMessage('Received permissionrequest:'); this.logger.logMessage('\tpermission: ' + JSON.stringify(e.permission)); this.logger.logMessage('\trequest: ' + JSON.stringify(e.request)); if (e.permission === 'media') { e.request.allow(); } }); div.appendChild(webview); document.body.appendChild(div); } enterChatMode() { this.logger.logMessage('enterChatMode() called'); if (this.studentInstance.waitingForFullScreenSelect === true) { return; } if (this.studentInstance.isInChatMode === true) { this.notifyBackground(); return; } if (!this.mainWnd) { this.logger.logError('enterChatMode: mainWnd is undefined'); return; } this.mainWnd.setAlwaysOnTop(false); if (this.mode !== 'CHAT') { // Call restore to end fullscreen mode. this.mainWnd.restore(); (document as AppDocument).webkitCancelFullScreen(); this.mainWnd.show(); this.mode = 'CHAT'; } // For some reason, restore is ending the fullscreen but is putting it into // minimized mode. Need it out of that before we can set bounds succesfully so // we'll call restore again just to be sure. No harm in calling it multiple times. this.mainWnd.restore(); if (this.chatWindowBounds) { this.mainWnd.innerBounds.setPosition(this.chatWindowBounds.left, this.chatWindowBounds.top); this.mainWnd.innerBounds.setSize(this.chatWindowBounds.width, this.chatWindowBounds.height); } this.removeSplashBackgroundImage('background_splash'); this.addWebView(); this.addMyTitleBar(); this.mainWnd.show(); this.minimize(); this.setChatMode(true); this.notifyBackground(); } setChatMode(bool: boolean) { this.studentInstance.isInChatMode = bool; } enterSplashMode(addBackground: boolean) { this.logger.logMessage('enterSplashMode() called'); this.setChatMode(false); Titlebar.removeTitlebar('titlebar'); if (addBackground) this.addSplashBackgroundImage('background_splash', 'images/BlankScreen.png'); if (!this.mainWnd) { this.logger.logError('enterSplashMode: mainWnd is undefined'); return; } this.logger.logMessage('Setting window to: (0, 0, ' + screen.width + ', ' + screen.height + ')'); this.mainWnd.setBounds({ left: 0, top: 0, width: screen.width, height: screen.height }); if (this.mode !== 'FULL') { this.mode = 'FULL'; this.mainWnd.show(); this.fullScreenSplashController.fullscreen(this.mainWnd); this.mainWnd.setAlwaysOnTop(true); setTimeout(this.forceShowWindow, 2000); } this.notifyBackground(); } notifyBackground() { chrome.runtime.sendMessage({ message: 'ChatWebViewState', data: { state: this.studentInstance } }); } startThumbnailCapture() { // already waiting if (this.studentInstance.waitingForFullScreenSelect === true) return; this.studentInstance.waitingForFullScreenSelect = true; this.enterSplashMode(true); this.fullScreenCapture.startCapture( () => this.thumbnailCaptureApproved(), () => this.thumbnailCaptureDenied(), () => this.onStreamEnded() ); // Need to update the screen privacy when capture starts or it can get // in a state where it thinks capture is happening even when it isn't. //TODO: Handle privacy // LStudent.updateScreenPrivacy(true); } stopThumbnailCapture() { this.logger.logMessage('Stopping thumbnail capture'); this.fullScreenCapture.stopCapture(); this._thumbnailCaptureApproved = false; } onStreamEnded() { this.logger.logMessage('Stream ended, restarting thumbnail capture'); this.startThumbnailCapture(); } thumbnailCaptureApproved() { this.logger.logMessage('Thumbnail Capture approved'); this.studentInstance.waitingForFullScreenSelect = false; this._thumbnailCaptureApproved = true; setTimeout(() => { this.enterChatMode(); }, 500); } thumbnailCaptureDenied() { this.logger.logMessage('Thumbnail Capture denied'); this.studentInstance.waitingForFullScreenSelect = false; this._thumbnailCaptureApproved = false; this.startThumbnailCapture(); } //console.log goes nowhere from the webview, so we must forward these logs over to the background page logMessage(message: string) { chrome.runtime.sendMessage({ message: 'WebViewLogMessage', data: message }); } } function main() { new WebView(); } document.addEventListener('DOMContentLoaded', main, false);