diff --git a/.gitignore b/.gitignore index 34087b64..8835f084 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ dist .DS_Store js/preload/_*.js .idea/ -coverage/ +coverage/ \ No newline at end of file diff --git a/js/activityDetection/activityDetection.js b/js/activityDetection/activityDetection.js new file mode 100644 index 00000000..e753b9e9 --- /dev/null +++ b/js/activityDetection/activityDetection.js @@ -0,0 +1,93 @@ +'use strict'; + +const systemIdleTime = require('@paulcbetts/system-idle-time'); +const throttle = require('../utils/throttle'); + +let maxIdleTime; +let activityWindow; +let intervalId; +let throttleActivity; + +/** + * Check if the user is idle + */ +function activityDetection() { + // Get system idle status and idle time from PaulCBetts package + if (systemIdleTime.getIdleTime() < maxIdleTime) { + return {isUserIdle: false, systemIdleTime: systemIdleTime.getIdleTime()}; + } + + // If idle for more than 4 mins, monitor system idle status every second + if (!intervalId) { + monitorUserActivity(); + } + return null; +} + +/** + * Start monitoring user activity status. + * Run every 4 mins to check user idle status + */ +function initiateActivityDetection() { + + if (!throttleActivity) { + throttleActivity = throttle(maxIdleTime, sendActivity); + setInterval(throttleActivity, maxIdleTime); + } + + sendActivity(); + +} + +/** + * Monitor system idle status every second + */ +function monitorUserActivity() { + intervalId = setInterval(monitor, 1000); + + function monitor() { + if (systemIdleTime.getIdleTime() < maxIdleTime) { + // If system is active, send an update to the app bridge and clear the timer + sendActivity(); + clearInterval(intervalId); + intervalId = undefined; + } + } + +} + +/** + * Send user activity status to the app bridge + * to be updated across all clients + */ +function sendActivity() { + let systemActivity = activityDetection(); + if (systemActivity && !systemActivity.isUserIdle && systemActivity.systemIdleTime) { + send({systemIdleTime: systemActivity.systemIdleTime}); + } +} + +/** + * Sends user activity status from main process to activity detection hosted by + * renderer process. Allows main process to use activity detection + * provided by JS. + * @param {object} data - data as object + */ +function send(data) { + if (activityWindow && data) { + activityWindow.send('activity', { + systemIdleTime: data.systemIdleTime + }); + } +} + +function setActivityWindow(period, win) { + maxIdleTime = period; + activityWindow = win; +} + +module.exports = { + send: send, + setActivityWindow: setActivityWindow, + initiateActivityDetection: initiateActivityDetection +}; diff --git a/js/enums/api.js b/js/enums/api.js index 932f2c5a..be1f1936 100644 --- a/js/enums/api.js +++ b/js/enums/api.js @@ -8,7 +8,8 @@ const cmds = keyMirror({ setBadgeCount: null, badgeDataUrl: null, activate: null, - registerBoundsChange: null + registerBoundsChange: null, + registerActivityDetection: null, }); module.exports = { diff --git a/js/mainApiMgr.js b/js/mainApiMgr.js index 20e70112..4f4a2454 100644 --- a/js/mainApiMgr.js +++ b/js/mainApiMgr.js @@ -8,6 +8,7 @@ const electron = require('electron'); const windowMgr = require('./windowMgr.js'); const log = require('./log.js'); +const activityDetection = require('./activityDetection/activityDetection'); const badgeCount = require('./badgeCount.js'); const apiEnums = require('./enums/api.js'); @@ -87,11 +88,16 @@ electron.ipcMain.on(apiName, (event, arg) => { // renderer window that has a registered logger from JS. log.setLogWindow(event.sender); } + + if (arg.cmd === apiCmds.registerActivityDetection) { + // renderer window that has a registered activity detection from JS. + activityDetection.setActivityWindow(arg.period, event.sender); + } }); // expose these methods primarily for testing... module.exports = { - shouldCheckValidWindow: function(shouldCheck) { + shouldCheckValidWindow: function (shouldCheck) { checkValidWindow = shouldCheck; } } diff --git a/js/preload/preloadMain.js b/js/preload/preloadMain.js index 1ad079e2..0644560c 100644 --- a/js/preload/preloadMain.js +++ b/js/preload/preloadMain.js @@ -123,6 +123,27 @@ function createAPI() { } }, + /** + * allows JS to register a activity detector that can be used by electron main process. + * @param {Object} activityDetection - function that can be called accepting + * @param {Object} period - minimum user idle time in millisecond + * object: { + * period: Number + * systemIdleTime: Number + * } + */ + registerActivityDetection: function(period, activityDetection) { + if (typeof activityDetection === 'function') { + local.activityDetection = activityDetection; + + // only main window can register + local.ipcRenderer.send(apiName, { + cmd: apiCmds.registerActivityDetection, + period: period + }); + } + }, + /** * Implements equivalent of desktopCapturer.getSources - that works in * a sandboxed renderer process. @@ -130,6 +151,7 @@ function createAPI() { * for interface: see documentation in desktopCapturer/getSources.js */ getMediaSources: getMediaSources + }; // add support for both ssf and SYM_API name-space. @@ -158,6 +180,13 @@ function createAPI() { } }); + // listen for user activity from main process + local.ipcRenderer.on('activity', (event, arg) => { + if (local.activityDetection && arg && arg.systemIdleTime) { + local.activityDetection(arg.systemIdleTime); + } + }); + /** * Use render process to create badge count img and send back to main process. * If number is greater than 99 then 99+ img is returned. diff --git a/js/windowMgr.js b/js/windowMgr.js index a7087456..0907a0ea 100644 --- a/js/windowMgr.js +++ b/js/windowMgr.js @@ -8,14 +8,17 @@ const querystring = require('querystring'); const menuTemplate = require('./menus/menuTemplate.js'); const loadErrors = require('./dialogs/showLoadError.js'); -const { isMac } = require('./utils/misc.js'); +const {isMac} = require('./utils/misc.js'); const isInDisplayBounds = require('./utils/isInDisplayBounds.js'); const getGuid = require('./utils/getGuid.js'); -const log = require('./log.js') +const log = require('./log.js'); const logLevels = require('./enums/logLevels.js'); const notify = require('./notify/electron-notify.js'); + +const activityDetection = require('./activityDetection/activityDetection.js'); + const throttle = require('./utils/throttle.js'); -const { getConfigField, updateConfigField } = require('./config.js'); +const {getConfigField, updateConfigField} = require('./config.js'); //context menu const contextMenu = require('./menus/contextMenu.js'); @@ -35,11 +38,11 @@ let boundsChangeWindow; const preloadMainScript = path.join(__dirname, 'preload/_preloadMain.js'); function addWindowKey(key, browserWin) { - windows[ key ] = browserWin; + windows[key] = browserWin; } function removeWindowKey(key) { - delete windows[ key ]; + delete windows[key]; } function getParsedUrl(url) { @@ -49,10 +52,10 @@ function getParsedUrl(url) { function createMainWindow(initialUrl) { getConfigField('mainWinPos').then( - function(bounds) { + function (bounds) { doCreateMainWindow(initialUrl, bounds); }, - function() { + function () { // failed, use default bounds doCreateMainWindow(initialUrl, null); } @@ -105,7 +108,7 @@ function doCreateMainWindow(initialUrl, initialBounds) { let throttledMainWinBoundsChange = throttle(5000, saveMainWinBounds); mainWindow.on('move', throttledMainWinBoundsChange); - mainWindow.on('resize',throttledMainWinBoundsChange); + mainWindow.on('resize', throttledMainWinBoundsChange); function retry() { if (!isOnline) { @@ -120,7 +123,7 @@ function doCreateMainWindow(initialUrl, initialBounds) { // content can be cached and will still finish load but // we might not have netowrk connectivity, so warn the user. - mainWindow.webContents.on('did-finish-load', function() { + mainWindow.webContents.on('did-finish-load', function () { url = mainWindow.webContents.getURL(); if (!isOnline) { @@ -129,11 +132,14 @@ function doCreateMainWindow(initialUrl, initialBounds) { // removes all existing notifications when main window reloads notify.reset(); log.send(logLevels.INFO, 'main window loaded url: ' + url); + + // Initiate activity detection to monitor user activity status + activityDetection.initiateActivityDetection(); } }); - mainWindow.webContents.on('did-fail-load', function(event, errorCode, - errorDesc, validatedURL) { + mainWindow.webContents.on('did-fail-load', function (event, errorCode, + errorDesc, validatedURL) { loadErrors.showLoadFailure(mainWindow, validatedURL, errorDesc, errorCode, retry); }); @@ -143,7 +149,7 @@ function doCreateMainWindow(initialUrl, initialBounds) { const menu = electron.Menu.buildFromTemplate(menuTemplate(app)); electron.Menu.setApplicationMenu(menu); - mainWindow.on('close', function(e) { + mainWindow.on('close', function (e) { if (willQuitApp) { destroyAllWindows(); return; @@ -157,7 +163,7 @@ function doCreateMainWindow(initialUrl, initialBounds) { function destroyAllWindows() { let keys = Object.keys(windows); - for(var i = 0, len = keys.length; i < len; i++) { + for (var i = 0, len = keys.length; i < len; i++) { let winKey = keys[i]; removeWindowKey(winKey); } @@ -168,8 +174,8 @@ function doCreateMainWindow(initialUrl, initialBounds) { mainWindow.on('closed', destroyAllWindows); // open external links in default browser - a tag, window.open - mainWindow.webContents.on('new-window', function(event, newWinUrl, - frameName, disposition, newWinOptions) { + mainWindow.webContents.on('new-window', function (event, newWinUrl, + frameName, disposition, newWinOptions) { let newWinParsedUrl = getParsedUrl(newWinUrl); let mainWinParsedUrl = getParsedUrl(url); @@ -201,7 +207,7 @@ function doCreateMainWindow(initialUrl, initialBounds) { let newX = Number.parseInt(query.x, 10); let newY = Number.parseInt(query.y, 10); - let newWinRect = { x: newX, y: newY, width, height }; + let newWinRect = {x: newX, y: newY, width, height}; // only accept if both are successfully parsed. if (Number.isInteger(newX) && Number.isInteger(newY) && @@ -214,9 +220,9 @@ function doCreateMainWindow(initialUrl, initialBounds) { } } else { // create new window at slight offset from main window. - ({ x, y } = getWindowSizeAndPosition(mainWindow)); - x+=50; - y+=50; + ({x, y} = getWindowSizeAndPosition(mainWindow)); + x += 50; + y += 50; } /* eslint-disable no-param-reassign */ @@ -232,13 +238,13 @@ function doCreateMainWindow(initialUrl, initialBounds) { let webContents = newWinOptions.webContents; - webContents.once('did-finish-load', function() { + webContents.once('did-finish-load', function () { let browserWin = electron.BrowserWindow.fromWebContents(webContents); if (browserWin) { browserWin.winName = frameName; - browserWin.once('closed', function() { + browserWin.once('closed', function () { removeWindowKey(newWinKey); browserWin.removeListener('move', throttledBoundsChange); browserWin.removeListener('resize', throttledBoundsChange); @@ -250,7 +256,7 @@ function doCreateMainWindow(initialUrl, initialBounds) { let throttledBoundsChange = throttle(1000, sendChildWinBoundsChange.bind(null, browserWin)); browserWin.on('move', throttledBoundsChange); - browserWin.on('resize',throttledBoundsChange); + browserWin.on('resize', throttledBoundsChange); } }); } @@ -259,7 +265,7 @@ function doCreateMainWindow(initialUrl, initialBounds) { contextMenu(mainWindow); } -app.on('before-quit', function() { +app.on('before-quit', function () { willQuitApp = true; }); @@ -280,7 +286,7 @@ function getWindowSizeAndPosition(window) { let newSize = window.getSize(); if (newPos && newPos.length === 2 && - newSize && newSize.length === 2 ) { + newSize && newSize.length === 2) { return { x: newPos[0], y: newPos[1], @@ -302,7 +308,7 @@ function isMainWindow(win) { function hasWindow(win, winKey) { if (win instanceof electron.BrowserWindow) { - let browserWin = windows[ winKey ]; + let browserWin = windows[winKey]; return browserWin && win === browserWin; } @@ -321,7 +327,7 @@ function setIsOnline(status) { */ function activate(windowName) { let keys = Object.keys(windows); - for(let i = 0, len = keys.length; i < len; i++) { + for (let i = 0, len = keys.length; i < len; i++) { let window = windows[keys[i]]; if (window && !window.isDestroyed() && window.winName === windowName) { if (window.isMinimized()) { diff --git a/package.json b/package.json index a9f50833..7acdece4 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,14 @@ "unpacked-win": "npm run prebuild && build --win --x64 --dir", "unpacked-win-x86": "npm run prebuild && build --win --ia32 --dir", "prebuild": "npm run lint && npm run test && npm run browserify-preload", + "rebuild": "npm install electron-rebuild && ./node_modules/.bin/electron-rebuild", "lint": "eslint --ext .js js/", "test": "jest --testPathPattern test", "browserify-preload": "browserify -o js/preload/_preloadMain.js -x electron --insert-global-vars=__filename,__dirname js/preload/preloadMain.js" }, "jest": { - "collectCoverage": true, - "transformIgnorePatterns": [] + "collectCoverage": true, + "transformIgnorePatterns": [] }, "build": { "files": [ @@ -82,13 +83,15 @@ "jest": "^19.0.2" }, "dependencies": { + "@paulcbetts/system-idle-time": "^1.0.4", "async": "^2.1.5", + "electron-context-menu": "^0.8.0", + "electron-rebuild": "^1.5.7", "electron-squirrel-startup": "^1.0.0", "keymirror": "0.1.1", - "electron-context-menu": "^0.8.0", "winreg": "^1.2.3" }, "optionalDependencies": { - "screen-snippet": "git+https://github.com/symphonyoss/ScreenSnippet.git#v1.0.1" + "screen-snippet": "git+https://github.com/symphonyoss/ScreenSnippet.git#v1.0.1" } }