Electron 24 - protocol handler (#85)

* added idea and coverage directories under gitignore

* electron-24: implemented handlers to process protocol actions

* electron-17: implemented use case for opening app if it is not open and handle the protocol url

* electron-24: added code and documentation comments

* electron-24: added unit tests for the protocol handler

* added npm-debug log to gitignore

* electron-24: added protocol handler support for windows

* electron-24: made changes as per comments on the PR

* electron-16: added more comments and further refactoring
This commit is contained in:
Vishwas Shashidhar 2017-05-13 23:53:44 +05:30 committed by Lynn
parent 4694e5206a
commit cda34b1d70
9 changed files with 307 additions and 41 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ dist
js/preload/_*.js
.idea/
coverage/
npm-debug.log

View File

@ -9,10 +9,12 @@ const cmds = keyMirror({
badgeDataUrl: null,
activate: null,
registerBoundsChange: null,
registerProtocolHandler: null,
checkProtocolAction: null,
registerActivityDetection: null,
});
module.exports = {
cmds: cmds,
apiName: 'symphony-api'
}
};

View File

@ -5,8 +5,13 @@ const app = electron.app;
const nodeURL = require('url');
const squirrelStartup = require('electron-squirrel-startup');
const AutoLaunch = require('auto-launch');
const { getConfigField } = require('./config.js');
const { isMac, isDevEnv } = require('./utils/misc.js');
const urlParser = require('url');
const {getConfigField} = require('./config.js');
const {isMac, isDevEnv} = require('./utils/misc.js');
const protocolHandler = require('./protocolHandler');
// used to check if a url was opened when the app was already open
let isAppAlreadyOpen = false;
// exit early for squirrel installer
if (squirrelStartup) {
@ -21,15 +26,17 @@ require('./memoryMonitor.js');
const windowMgr = require('./windowMgr.js');
// only allow a single instance of app.
const shouldQuit = app.makeSingleInstance(() => {
const shouldQuit = app.makeSingleInstance((argv) => {
// Someone tried to run a second instance, we should focus our window.
let mainWin = windowMgr.getMainWindow();
if (mainWin) {
isAppAlreadyOpen = true;
if (mainWin.isMinimized()) {
mainWin.restore();
}
mainWin.focus();
}
processProtocolAction(argv);
});
// quit if another instance is already running
@ -50,52 +57,59 @@ var symphonyAutoLauncher = new AutoLaunch({
app.on('ready', getUrlAndOpenMainWindow);
function getUrlAndOpenMainWindow() {
processProtocolAction(process.argv);
isAppAlreadyOpen = true;
// for dev env allow passing url argument
let installMode = false;
process.argv.some((val) => {
let flag = '--install';
if (val === flag) {
installMode = true;
getConfigField('launchOnStartup')
.then(setStartup);
.then(setStartup);
}
return false;
});
if (installMode === false){
if (installMode === false) {
openMainWindow();
}
}
function setStartup(lStartup){
if (lStartup === true){
function setStartup(lStartup) {
if (lStartup === true) {
symphonyAutoLauncher.isEnabled()
.then(function(isEnabled){
if(isEnabled){
app.quit();
}
symphonyAutoLauncher.enable()
.then(function (){
app.quit();
});
})
} else{
symphonyAutoLauncher.isEnabled()
.then(function(isEnabled){
if(isEnabled){
symphonyAutoLauncher.disable()
.then(function (){
.then(function (isEnabled) {
if (isEnabled) {
app.quit();
});
} else{
app.quit();
}
})
}
symphonyAutoLauncher.enable()
.then(function () {
app.quit();
});
})
} else {
symphonyAutoLauncher.isEnabled()
.then(function (isEnabled) {
if (isEnabled) {
symphonyAutoLauncher.disable()
.then(function () {
app.quit();
});
} else {
app.quit();
}
})
}
}
function openMainWindow(){
function openMainWindow() {
if (isDevEnv) {
let url;
process.argv.forEach((val) => {
@ -131,6 +145,53 @@ function createWin(urlFromConfig) {
windowMgr.createMainWindow(url);
}
/**
* processes protocol action for windows clients
* @param argv {Array} an array of command line arguments
*/
function processProtocolAction(argv) {
// In case of windows, we need to handle protocol handler
// manually because electron doesn't emit
// 'open-url' event on windows
if (!(process.platform === 'win32')) {
return;
}
let protocolUri;
for (let i = 0; i < argv.length; i++) {
if (argv[i].startsWith("symphony://")) {
protocolUri = argv[i];
break;
}
}
if (protocolUri) {
const parsedURL = urlParser.parse(protocolUri);
if (!parsedURL.protocol || !parsedURL.slashes) {
return;
}
handleProtocolAction(protocolUri);
}
}
function handleProtocolAction(uri) {
if (!isAppAlreadyOpen) {
// app is opened by the protocol url, cache the protocol url to be used later
protocolHandler.setProtocolUrl(uri);
} else {
// app is already open, so, just trigger the protocol action method
protocolHandler.processProtocolAction(uri);
}
}
app.on('window-all-closed', function () {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
@ -146,3 +207,15 @@ app.on('activate', function () {
windowMgr.showMainWindow();
}
});
// adds 'symphony' as a protocol
// in the system. plist file in macOS
// and registry keys in windows
app.setAsDefaultProtocolClient('symphony');
// This event is emitted only on macOS
// at this moment, support for windows
// is in pipeline (https://github.com/electron/electron/pull/8052)
app.on('open-url', function (event, url) {
handleProtocolAction(url);
});

View File

@ -10,6 +10,7 @@ const windowMgr = require('./windowMgr.js');
const log = require('./log.js');
const activityDetection = require('./activityDetection/activityDetection');
const badgeCount = require('./badgeCount.js');
const protocolHandler = require('./protocolHandler');
const apiEnums = require('./enums/api.js');
const apiCmds = apiEnums.cmds;
@ -69,6 +70,15 @@ electron.ipcMain.on(apiName, (event, arg) => {
return;
}
if (arg.cmd === apiCmds.checkProtocolAction) {
protocolHandler.checkProtocolAction();
return;
}
if (arg.cmd === apiCmds.registerProtocolHandler) {
protocolHandler.setProtocolWindow(event.sender);
}
if (arg.cmd === apiCmds.badgeDataUrl && typeof arg.dataUrl === 'string' &&
typeof arg.count === 'number') {
badgeCount.setDataUrl(arg.dataUrl, arg.count);
@ -100,4 +110,4 @@ module.exports = {
shouldCheckValidWindow: function (shouldCheck) {
checkValidWindow = shouldCheck;
}
}
};

View File

@ -32,6 +32,13 @@ const throttledSetBadgeCount = throttle(1000, function(count) {
});
});
// check to see if the app was opened via a url
const checkProtocolAction = function () {
local.ipcRenderer.send(apiName, {
cmd: apiCmds.checkProtocolAction
});
};
createAPI();
// creates API exposed from electron.
@ -65,6 +72,13 @@ function createAPI() {
throttledSetBadgeCount(count);
},
/**
* checks to see if the app was opened from a url.
*/
checkProtocolAction: function () {
checkProtocolAction();
},
/**
* provides api similar to html5 Notification, see details
* in notify/notifyImpl.js
@ -123,6 +137,24 @@ function createAPI() {
}
},
/**
* allows JS to register a protocol handler that can be used by the electron main process.
* @param protocolHandler {Object} protocolHandler a callback to register the protocol handler
*/
registerProtocolHandler: function (protocolHandler) {
if (typeof protocolHandler === 'function') {
local.processProtocolAction = protocolHandler;
local.ipcRenderer.send(apiName, {
cmd: apiCmds.registerProtocolHandler
});
}
},
/**
* allows JS to register a activity detector that can be used by electron main process.
* @param {Object} activityDetection - function that can be called accepting
@ -235,6 +267,18 @@ function createAPI() {
});
});
/**
* an event triggered by the main process for processing protocol urls
* @type {String} arg - the protocol url
*/
local.ipcRenderer.on('protocol-action', (event, arg) => {
if (local.processProtocolAction && arg) {
local.processProtocolAction(arg);
}
});
function updateOnlineStatus() {
local.ipcRenderer.send(apiName, {
cmd: apiCmds.isOnline,

View File

@ -0,0 +1,52 @@
'use strict';
let protocolWindow;
let protocolUrl;
/**
* processes a protocol uri
* @param {String} uri - the uri opened in the format 'symphony://...'
*/
function processProtocolAction(uri) {
if (protocolWindow && uri && uri.startsWith("symphony://")) {
protocolWindow.send('protocol-action', uri);
}
}
/**
* sets the protocol window
* @param {Object} win - the renderer window
*/
function setProtocolWindow(win) {
protocolWindow = win;
}
/**
* checks to see if the app was opened by a uri
*/
function checkProtocolAction() {
if (protocolUrl) {
processProtocolAction(protocolUrl);
protocolUrl = undefined;
}
}
/**
* caches the protocol uri
* @param {String} uri - the uri opened in the format 'symphony://...'
*/
function setProtocolUrl(uri) {
protocolUrl = uri;
}
function getProtocolUrl() {
return protocolUrl;
}
module.exports = {
processProtocolAction: processProtocolAction,
setProtocolWindow: setProtocolWindow,
checkProtocolAction: checkProtocolAction,
setProtocolUrl: setProtocolUrl,
getProtocolUrl: getProtocolUrl
};

View File

@ -49,8 +49,7 @@ function removeWindowKey(key) {
}
function getParsedUrl(url) {
let parsedUrl = nodeURL.parse(url);
return parsedUrl;
return nodeURL.parse(url);
}
function createMainWindow(initialUrl) {
@ -82,7 +81,7 @@ function doCreateMainWindow(initialUrl, initialBounds) {
}
};
// set size and postion
// set size and position
let bounds = initialBounds;
// if bounds if not fully contained in some display then use default size
@ -127,7 +126,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.
// we might not have network connectivity, so warn the user.
mainWindow.webContents.on('did-finish-load', function () {
url = mainWindow.webContents.getURL();

View File

@ -0,0 +1,61 @@
/**
* Created by vishwas on 10/05/17.
*/
const protocolHandler = require('../js/protocolHandler');
const electron = require('./__mocks__/electron');
describe('protocol handler', function () {
const url = 'symphony://?userId=100001';
const mainProcess = electron.ipcMain;
const protocolWindow = electron.ipcRenderer;
beforeAll(function () {
protocolHandler.setProtocolWindow(protocolWindow);
});
it('process a protocol action', function (done) {
const spy = jest.spyOn(protocolHandler, 'processProtocolAction');
protocolHandler.processProtocolAction(url);
expect(spy).toHaveBeenCalledWith(url);
done();
});
it('protocol url should be undefined by default', function (done) {
expect(protocolHandler.getProtocolUrl()).toBeUndefined();
done();
});
it('protocol handler open url should be called', function (done) {
const spy = jest.spyOn(mainProcess, 'send');
mainProcess.send('open-url', url);
expect(spy).toHaveBeenCalled();
done();
});
it('check protocol action should be called', function (done) {
const spy = jest.spyOn(protocolHandler, 'checkProtocolAction');
const setSpy = jest.spyOn(protocolHandler, 'setProtocolUrl');
protocolHandler.setProtocolUrl(url);
expect(setSpy).toHaveBeenCalledWith(url);
protocolHandler.checkProtocolAction();
expect(spy).toHaveBeenCalled();
expect(protocolHandler.getProtocolUrl()).toBeUndefined();
done();
});
});

View File

@ -8,20 +8,44 @@ function pathToConfigDir() {
return path.join(__dirname, '/../fixtures');
}
// electron app mock...
const app = {
getAppPath: pathToConfigDir,
getPath: function(type) {
if (type === 'exe') {
return path.join(pathToConfigDir(), '/Symphony.exe');
}
return pathToConfigDir();
},
on: function() {
// no-op
}
};
// simple ipc mocks for render and main process ipc using
// nodes' EventEmitter
const ipcMain = {
on: function(event, cb) {
ipcEmitter.on(event, cb);
}
}
},
send: function (event, args) {
var senderEvent = {
sender: {
send: function (event, arg) {
ipcEmitter.emit(event, arg);
}
}
};
ipcEmitter.emit(event, senderEvent, args);
},
};
const ipcRenderer = {
sendSync: function(event, args) {
let listeners = ipcEmitter.listeners(event);
if (listeners.length > 0) {
let listener = listeners[0];
var eventArg = {}
var eventArg = {};
listener(eventArg, args);
return eventArg.returnValue;
}
@ -34,7 +58,7 @@ const ipcRenderer = {
ipcEmitter.emit(event, arg);
}
}
}
};
ipcEmitter.emit(event, senderEvent, args);
},
on: function(eventName, cb) {
@ -43,7 +67,7 @@ const ipcRenderer = {
removeListener: function(eventName, cb) {
ipcEmitter.removeListener(eventName, cb);
}
}
};
module.exports = {
require: jest.genMockFunction(),