mirror of
https://github.com/flyandi/mazda-custom-application-sdk
synced 2025-01-09 17:53:24 +00:00
More rework on the CMU node process
This commit is contained in:
parent
b0eadb2bf0
commit
2fbbddc878
6 changed files with 1153 additions and 689 deletions
652
src/framework/js/CustomApplicationOverride.js
Normal file
652
src/framework/js/CustomApplicationOverride.js
Normal file
|
@ -0,0 +1,652 @@
|
||||||
|
/**
|
||||||
|
* Custom Applications SDK for Mazda Connect Infotainment System
|
||||||
|
*
|
||||||
|
* A mini framework that allows to write custom applications for the Mazda Connect Infotainment System
|
||||||
|
* that includes an easy to use abstraction layer to the JCI system.
|
||||||
|
*
|
||||||
|
* Written by Andreas Schwarz (http://github.com/flyandi/mazda-custom-applications-sdk)
|
||||||
|
* Copyright (c) 2016. All rights reserved.
|
||||||
|
*
|
||||||
|
* WARNING: The installation of this application requires modifications to your Mazda Connect system.
|
||||||
|
* If you don't feel comfortable performing these changes, please do not attempt to install this. You might
|
||||||
|
* be ending up with an unusuable system that requires reset by your Dealer. You were warned!
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
||||||
|
* License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with this program.
|
||||||
|
* If not, see http://www.gnu.org/licenses/
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the main system file that manages everything between the CMU backend server and the frontend.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (Logger)
|
||||||
|
*/
|
||||||
|
|
||||||
|
window.Logger = {
|
||||||
|
|
||||||
|
levels: {
|
||||||
|
debug: 'DEBUG',
|
||||||
|
info: 'INFO',
|
||||||
|
error: 'ERROR',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscriptions
|
||||||
|
* @array
|
||||||
|
*/
|
||||||
|
subscriptions: [],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (debug) debug message
|
||||||
|
*/
|
||||||
|
|
||||||
|
debug: function() {
|
||||||
|
this.__message(this.levels.debug, "#006600", Array.apply(null, arguments));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (error) error message
|
||||||
|
*/
|
||||||
|
|
||||||
|
error: function() {
|
||||||
|
this.__message(this.levels.error, "#FF0000", Array.apply(null, arguments));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (info) info message
|
||||||
|
*/
|
||||||
|
|
||||||
|
info: function() {
|
||||||
|
this.__message(this.levels.info, "#0000FF", Array.apply(null, arguments));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe
|
||||||
|
* @return {[type]} [description]
|
||||||
|
*/
|
||||||
|
subscribe: function(callback) {
|
||||||
|
|
||||||
|
if(typeof(callback) == "function") {
|
||||||
|
this.subscriptions.push(callback);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [__message description]
|
||||||
|
* @param {[type]} level [description]
|
||||||
|
* @param {[type]} color [description]
|
||||||
|
* @param {[type]} values [description]
|
||||||
|
* @return {[type]} [description]
|
||||||
|
*/
|
||||||
|
__message: function(level, color, values) {
|
||||||
|
|
||||||
|
var msg = [];
|
||||||
|
|
||||||
|
if(values.length > 1) {
|
||||||
|
values.forEach(function(value, index) {
|
||||||
|
|
||||||
|
if(index > 0) {
|
||||||
|
|
||||||
|
if(typeof(value) == "object") {
|
||||||
|
|
||||||
|
var keys = value, o = false;
|
||||||
|
|
||||||
|
if(Object.prototype.toString.call(value) == "[object Object]") {
|
||||||
|
var keys = Object.keys(value),
|
||||||
|
o = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(keys.forEach) {
|
||||||
|
|
||||||
|
keys.forEach(function(v, index) {
|
||||||
|
msg.push(o ? '[' + v + '=' + value[v]+ ']' : '[' + v + ']');
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
msg.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
msg = msg.join(" ");
|
||||||
|
|
||||||
|
this.subscriptions.forEach(function(subscription) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
subscription(level, values[0], msg, color);
|
||||||
|
} catch(e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (CustomApplications)
|
||||||
|
*
|
||||||
|
* Registers itself between the JCI system and CustomApplication framework.
|
||||||
|
*/
|
||||||
|
|
||||||
|
window.CustomApplications = {
|
||||||
|
|
||||||
|
ID: 'system',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (locals)
|
||||||
|
*/
|
||||||
|
debug: false,
|
||||||
|
bootstrapped: false,
|
||||||
|
|
||||||
|
systemAppId: 'system',
|
||||||
|
systemAppCategory: 'Applications',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overwrites
|
||||||
|
*/
|
||||||
|
proxyAppName: 'vdt',
|
||||||
|
proxyAppContext: 'DriveChartDetails',
|
||||||
|
proxyMmuiEvent: 'SelectDriveRecord',
|
||||||
|
|
||||||
|
targetAppName: 'custom',
|
||||||
|
targetAppContext: 'Surface',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration
|
||||||
|
*/
|
||||||
|
configuration: {
|
||||||
|
|
||||||
|
networkHost: '127.0.0.1',
|
||||||
|
networkPort: 9700,
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commands
|
||||||
|
*/
|
||||||
|
commands: {
|
||||||
|
|
||||||
|
REQUEST_PING: 'ping',
|
||||||
|
REQUEST_APPDRIVE: 'appdrive',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Results
|
||||||
|
*/
|
||||||
|
results: {
|
||||||
|
|
||||||
|
RESULT_OK: 200,
|
||||||
|
RESULT_PONG: 201,
|
||||||
|
RESULT_NOTFOUND: 404,
|
||||||
|
RESULT_ERROR: 500,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the proxy
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
initialize: function(callback) {
|
||||||
|
|
||||||
|
if(!this.initialized) {
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
|
||||||
|
this.requests = {};
|
||||||
|
|
||||||
|
this.obtainConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback ? callback() : true;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Establishes a connection between the front and backend
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
obtainConnection: function() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
this.client = new WebSocket('ws://' + this.configuration.networkHost + ':' + this.configuration.networkPort);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ping
|
||||||
|
*/
|
||||||
|
this.client.ping = function() {
|
||||||
|
|
||||||
|
this.request(this.commands.REQUEST_PING, {
|
||||||
|
inboundStamp: (new Date()).getTime()
|
||||||
|
}, function(error, result) {
|
||||||
|
|
||||||
|
Logger.info(this.ID, "ping", {
|
||||||
|
lost: error,
|
||||||
|
time: !error ? result.outboundStamp - result.inboundStamp : 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onOpen
|
||||||
|
* @event
|
||||||
|
*/
|
||||||
|
this.client.onopen = function() {
|
||||||
|
|
||||||
|
Logger.info(this.ID, "connection open");
|
||||||
|
|
||||||
|
this.client.ping();
|
||||||
|
|
||||||
|
this.requestAppDrive();
|
||||||
|
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onMessage
|
||||||
|
* @event
|
||||||
|
*/
|
||||||
|
this.client.onmessage = function(message) {
|
||||||
|
|
||||||
|
this.handleReturnRequest(message);
|
||||||
|
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onError
|
||||||
|
* @event
|
||||||
|
*/
|
||||||
|
this.client.onerror = function(error) {
|
||||||
|
|
||||||
|
Logger.error(CustomApplications.ID, 'ClientError', error);
|
||||||
|
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onClose
|
||||||
|
* @event
|
||||||
|
*/
|
||||||
|
this.client.onclose = function(event) {
|
||||||
|
|
||||||
|
this.client = null;
|
||||||
|
|
||||||
|
if(event.code == 3110) {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
|
||||||
|
CustomApplications.obtainConnection();
|
||||||
|
|
||||||
|
}, 5000); // retry later
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
|
||||||
|
this.client = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [request description]
|
||||||
|
* @return {[type]} [description]
|
||||||
|
*/
|
||||||
|
request: function(request, payload, callback) {
|
||||||
|
|
||||||
|
// check connection state
|
||||||
|
if(!this.client || this.client.readyState != 1) return callback(true, {});
|
||||||
|
|
||||||
|
// prepare id
|
||||||
|
var id = false;
|
||||||
|
while(!id || this.requests[id]) {
|
||||||
|
id = (new Date()).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
// register request
|
||||||
|
this.requests[id] = callback;
|
||||||
|
|
||||||
|
// sanity check
|
||||||
|
payload = payload || {};
|
||||||
|
|
||||||
|
// add request id
|
||||||
|
payload.requestId = id;
|
||||||
|
|
||||||
|
payload.request = request;
|
||||||
|
|
||||||
|
// execute
|
||||||
|
return this.client.send(JSON.stringify(payload));
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a request
|
||||||
|
* @param {[type]} data [description]
|
||||||
|
* @return {[type]} [description]
|
||||||
|
*/
|
||||||
|
handleReturnRequest: function(message) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
// parse message
|
||||||
|
var payload = JSON.parse(message.data);
|
||||||
|
|
||||||
|
// check against active requests
|
||||||
|
if(payload.requestId && this.requests[payload.requestId]) {
|
||||||
|
|
||||||
|
var callback = this.requests[payload.requestId];
|
||||||
|
|
||||||
|
if(typeof(callback) == "function") {
|
||||||
|
|
||||||
|
callback(payload.result == this.results.RESULT_ERROR, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.requests[payload.requestId];
|
||||||
|
|
||||||
|
return; // all done
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
|
||||||
|
Logger.error(CustomApplications.ID, 'handleReturnRequest', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trys to load the AppDrive
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
requestAppDrive: function() {
|
||||||
|
|
||||||
|
if(typeof(CustomApplicationsHandler) != "undefined") return false;
|
||||||
|
|
||||||
|
if(!this.request(this.commands.REQUEST_APPDRIVE, false, function(error, result) {
|
||||||
|
|
||||||
|
if(error || !result.appdrive) {
|
||||||
|
return setTimeout(function() {
|
||||||
|
|
||||||
|
this.requestAppDrive();
|
||||||
|
|
||||||
|
}.bind(this), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// load appdrive
|
||||||
|
this.loadAppDrive(result.appdrive, function() {
|
||||||
|
|
||||||
|
// boot strap system
|
||||||
|
this.bootstrap();
|
||||||
|
|
||||||
|
// update applications
|
||||||
|
this.updateApplications();
|
||||||
|
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
}.bind(this)));
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to load the AppDrive
|
||||||
|
* @param object appdrive The AppDrive object
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
loadAppDrive: function(appdrive, callback) {
|
||||||
|
|
||||||
|
this.appdrive = appdrive;
|
||||||
|
|
||||||
|
if(this.appdrive) {
|
||||||
|
|
||||||
|
// load css
|
||||||
|
this.loadCSS(this.appdrive.css);
|
||||||
|
|
||||||
|
// load js
|
||||||
|
this.loadJS(this.appdrive.js, callback);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (bootstrap)
|
||||||
|
*
|
||||||
|
* Bootstraps the JCI system
|
||||||
|
*/
|
||||||
|
|
||||||
|
bootstrap: function() {
|
||||||
|
|
||||||
|
// verify that core objects are available
|
||||||
|
if(typeof framework === 'object' && framework._currentAppUiaId === this.systemAppId && this.bootstrapped === false) {
|
||||||
|
|
||||||
|
// retrieve system app
|
||||||
|
var systemApp = framework.getAppInstance(this.systemAppId);
|
||||||
|
|
||||||
|
// verify bootstrapping - yeah long name
|
||||||
|
if(systemApp) {
|
||||||
|
|
||||||
|
// set to strap - if everything fails - no harm is done :-)
|
||||||
|
this.bootstrapped = true;
|
||||||
|
|
||||||
|
// let's boostrap
|
||||||
|
try {
|
||||||
|
|
||||||
|
// overwrite list2 handler
|
||||||
|
systemApp._contextTable[this.systemAppCategory].controlProperties.List2Ctrl.selectCallback = this.menuItemSelectCallback.bind(systemApp);
|
||||||
|
|
||||||
|
// for usb changes - we do this right now but might
|
||||||
|
if(typeof(systemApp.overwriteStatusMenuUSBAudioMsgHandler) == "undefined") {
|
||||||
|
systemApp.overwriteStatusMenuUSBAudioMsgHandler = systemApp._StatusMenuUSBAudioMsgHandler;
|
||||||
|
systemApp._StatusMenuUSBAudioMsgHandler = this.StatusMenuUSBAudioMsgHandler.bind(systemApp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// overwrite framework route handler
|
||||||
|
if(typeof(framework.overwriteRouteMmmuiMsg) == "undefined") {
|
||||||
|
framework.overwriteRouteMmmuiMsg = framework.routeMmuiMsg;
|
||||||
|
framework.routeMmuiMsg = this.routeMmuiMsg.bind(framework);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ovewrite framework MMUI sender
|
||||||
|
if(typeof(framework.overwriteSendEventToMmui) == "undefined") {
|
||||||
|
framework.overwriteSendEventToMmui = framework.sendEventToMmui;
|
||||||
|
framework.sendEventToMmui = this.sendEventToMmui.bind(framework);
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign template transition
|
||||||
|
framework.transitionsObj._genObj._TEMPLATE_CATEGORIES_TABLE.SurfaceTmplt = 'Detail with UMP';
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
// bootstrapping process failed - we just leave it here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (Overwrite) menuItemSelectCallback
|
||||||
|
*/
|
||||||
|
|
||||||
|
menuItemSelectCallback: function(listCtrlObj, appData, params) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
if(appData.mmuiEvent == "SelectCustomApplication") {
|
||||||
|
|
||||||
|
// exit if handler is not available
|
||||||
|
if(typeof(CustomApplicationsHandler) != "undefined") {
|
||||||
|
|
||||||
|
// launch app
|
||||||
|
if(CustomApplicationsHandler.launch(appData)) {
|
||||||
|
|
||||||
|
// clone app data
|
||||||
|
try {
|
||||||
|
appData = JSON.parse(JSON.stringify(appData));
|
||||||
|
|
||||||
|
// set app data
|
||||||
|
appData.appName = CustomApplicationsProxy.proxyAppName;
|
||||||
|
appData.mmuiEvent = CustomApplicationsProxy.proxyMmuiEvent;
|
||||||
|
} catch(e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass to original handler
|
||||||
|
this._menuItemSelectCallback(listCtrlObj, appData, params);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (Overwrite) sendEventToMmui
|
||||||
|
*/
|
||||||
|
|
||||||
|
sendEventToMmui: function(uiaId, eventId, params, fromVui) {
|
||||||
|
|
||||||
|
var currentUiaId = this.getCurrentApp(),
|
||||||
|
currentContextId = this.getCurrCtxtId();
|
||||||
|
|
||||||
|
// proxy overwrites
|
||||||
|
if(typeof(CustomApplicationsHandler) === 'object' && currentUiaId == CustomApplicationsProxy.targetAppName) {
|
||||||
|
currentUiaId = CustomApplicationsProxy.proxyAppName;
|
||||||
|
currentContextId = CustomApplicationsProxy.proxyAppContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass to original handler
|
||||||
|
framework.overwriteSendEventToMmui(uiaId, eventId, params, fromVui, currentUiaId, currentContextId);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (Overwrite) routeMmuiMsg
|
||||||
|
*/
|
||||||
|
|
||||||
|
routeMmuiMsg: function(jsObject) {
|
||||||
|
|
||||||
|
if(typeof(CustomApplicationsHandler) === 'object') {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
var proxy = CustomApplications;
|
||||||
|
|
||||||
|
// validate routing message
|
||||||
|
switch(jsObject.msgType) {
|
||||||
|
|
||||||
|
// magic switch
|
||||||
|
case 'ctxtChg':
|
||||||
|
if(jsObject.uiaId == proxy.proxyAppName) {
|
||||||
|
jsObject.uiaId = proxy.targetAppName;
|
||||||
|
jsObject.ctxtId = proxy.targetAppContext;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// check if our proxy app is in the focus stack
|
||||||
|
case 'focusStack':
|
||||||
|
|
||||||
|
if(jsObject.appIdList && jsObject.appIdList.length) {
|
||||||
|
for(var i = 0; i < jsObject.appIdList.length; i++) {
|
||||||
|
var appId = jsObject.appIdList[i];
|
||||||
|
if(appId.id == proxy.proxyAppName) {
|
||||||
|
appId.id = proxy.targetAppName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'msg':
|
||||||
|
case 'alert':
|
||||||
|
|
||||||
|
if(jsObject.uiaId == proxy.proxyAppName) {
|
||||||
|
jsObject.uiaId = proxy.targetAppName;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// do nothing
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass to framework
|
||||||
|
framework.overwriteRouteMmmuiMsg(jsObject);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (Overwrite) StatusMenuUSBAudioMsgHandler
|
||||||
|
*/
|
||||||
|
|
||||||
|
StatusMenuUSBAudioMsgHandler: function(msg) {
|
||||||
|
|
||||||
|
// pass to original handler
|
||||||
|
this.overwriteStatusMenuUSBAudioMsgHandler(msg);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (updateApplications)
|
||||||
|
*/
|
||||||
|
|
||||||
|
updateApplications: function() {
|
||||||
|
// extend with custom applications
|
||||||
|
try {
|
||||||
|
if(typeof(CustomApplicationsHandler) != "undefined") {
|
||||||
|
|
||||||
|
CustomApplicationsHandler.retrieve(function(items) {
|
||||||
|
|
||||||
|
var systemApp = framework.getAppInstance(this.systemAppId);
|
||||||
|
|
||||||
|
items.forEach(function(item) {
|
||||||
|
|
||||||
|
systemApp._masterApplicationDataList.items.push(item);
|
||||||
|
|
||||||
|
framework.localize._appDicts[this.systemAppId][item.appData.appName.replace(".", "_")] = item.title;
|
||||||
|
|
||||||
|
framework.common._contextCategory._contextCategoryTable[item.appData.appName + '.*'] = 'Applications';
|
||||||
|
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
// failed to register applications
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime Caller
|
||||||
|
*/
|
||||||
|
|
||||||
|
if(window.opera) {
|
||||||
|
window.opera.addEventListener('AfterEvent.load', function (e) {
|
||||||
|
CustomApplications.initialize(function() {
|
||||||
|
CustomApplications.bootstrap();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** EOF **/
|
|
@ -73,6 +73,9 @@ const SYSTEM_CUSTOM_PATH = "custom/";
|
||||||
const JCI_MOUNT_PATH = "/tmp/mnt/data_persist/appdrive/"; // we link our resources in here
|
const JCI_MOUNT_PATH = "/tmp/mnt/data_persist/appdrive/"; // we link our resources in here
|
||||||
const JCI_APP_PATH = "custom";
|
const JCI_APP_PATH = "custom";
|
||||||
|
|
||||||
|
const COMMAND_LOAD_JS = "loadjs";
|
||||||
|
const COMMAND_LOAD_CSS = "loadcss";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the CMU that is compiled into the node binary and runs the actual link between the
|
* This is the CMU that is compiled into the node binary and runs the actual link between the
|
||||||
* custom applications and the CMU.
|
* custom applications and the CMU.
|
||||||
|
@ -117,6 +120,10 @@ cmu.prototype = {
|
||||||
*/
|
*/
|
||||||
__construct: function()
|
__construct: function()
|
||||||
{
|
{
|
||||||
|
// initial app drive
|
||||||
|
this.findAppDrive();
|
||||||
|
|
||||||
|
// create webserver
|
||||||
this.__socket = new _webSocketServer({
|
this.__socket = new _webSocketServer({
|
||||||
port: this.network.port
|
port: this.network.port
|
||||||
});
|
});
|
||||||
|
@ -127,7 +134,7 @@ cmu.prototype = {
|
||||||
|
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
this.findAppDrive();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
@ -138,21 +145,73 @@ cmu.prototype = {
|
||||||
*/
|
*/
|
||||||
attachClient: function(client) {
|
attachClient: function(client) {
|
||||||
|
|
||||||
client.on('message', function(message) {
|
// we only allow one client to be connected
|
||||||
|
this.client = client;
|
||||||
|
|
||||||
|
// assing
|
||||||
|
this.client.on('message', function(message) {
|
||||||
|
|
||||||
this.handleClientData(client, message);
|
this.handleClientData(client, message);
|
||||||
|
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
client.on('close', function() {
|
this.client.on('close', function() {
|
||||||
|
|
||||||
// do nothing
|
// do nothing
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('error', function(e) {
|
this.client.on('error', function(e) {
|
||||||
|
|
||||||
// do nothing
|
// do nothing
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// let's try this
|
||||||
|
this.requestLoadJavascript(['test.js']);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [requestLoadJavascript description]
|
||||||
|
* @param {[type]} files [description]
|
||||||
|
* @param {[type]} path [description]
|
||||||
|
* @return {[type]} [description]
|
||||||
|
*/
|
||||||
|
requestLoadJavascript: function(files, path) {
|
||||||
|
|
||||||
|
return this.sendCommand(COMMAND_LOAD_JS, {
|
||||||
|
filenames: files,
|
||||||
|
path: path
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [requestLoadCSS description]
|
||||||
|
* @param {[type]} files [description]
|
||||||
|
* @param {[type]} path [description]
|
||||||
|
* @return {[type]} [description]
|
||||||
|
*/
|
||||||
|
requestLoadCSS: function(files, path) {
|
||||||
|
|
||||||
|
return this.sendCommand(COMMAND_LOAD_CSS, {
|
||||||
|
filenames: files,
|
||||||
|
path: path
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a command to the client
|
||||||
|
* @param {[type]} command [description]
|
||||||
|
* @param {[type]} payload [description]
|
||||||
|
* @param {[type]} client [description]
|
||||||
|
* @return {[type]} [description]
|
||||||
|
*/
|
||||||
|
sendCommand: function(command, attributes, client) {
|
||||||
|
|
||||||
|
this.sendFromPayload(client || this.client, {
|
||||||
|
command: command,
|
||||||
|
attributes: attributes,
|
||||||
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -199,14 +258,9 @@ cmu.prototype = {
|
||||||
*/
|
*/
|
||||||
case REQUEST_APPDRIVE:
|
case REQUEST_APPDRIVE:
|
||||||
|
|
||||||
// find applications
|
this.sendFromPayload(client, payload, {
|
||||||
this.findAppDrive(function(appdrive) {
|
appdrive: this.appdrive
|
||||||
|
});
|
||||||
this.sendFromPayload(client, payload, {
|
|
||||||
appdrive
|
|
||||||
});
|
|
||||||
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -242,6 +296,7 @@ cmu.prototype = {
|
||||||
client.send(final);
|
client.send(final);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the version
|
* Returns the version
|
||||||
* @getter
|
* @getter
|
||||||
|
@ -421,10 +476,9 @@ cmu.prototype = {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Exports
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports = new cmu();
|
exports = new cmu();
|
||||||
|
|
||||||
/**
|
|
||||||
/** eof */
|
/** eof */
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<script src="../../system/CustomApplications.js"></script>
|
<script src="../../system/cmu.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
CustomApplications.initialize();
|
CMU.initialize();
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
Logger.subscribe(function(level, name, msg, color) {
|
Logger.subscribe(function(level, name, msg, color) {
|
||||||
console.log(
|
console.log(
|
||||||
'%c[' + level + '] %c[' + name + '] ' + msg,
|
'%c[' + level + '] %c[' + name + '] ' + msg,
|
||||||
'color:' + color,
|
'color:' + color,
|
||||||
'color:black'
|
'color:black'
|
||||||
);
|
);
|
||||||
});
|
});*/
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
3
src/node-cmu/test/test.js
Normal file
3
src/node-cmu/test/test.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
|
alert('loaded');
|
|
@ -1,672 +0,0 @@
|
||||||
/**
|
|
||||||
* Custom Applications SDK for Mazda Connect Infotainment System
|
|
||||||
*
|
|
||||||
* A mini framework that allows to write custom applications for the Mazda Connect Infotainment System
|
|
||||||
* that includes an easy to use abstraction layer to the JCI system.
|
|
||||||
*
|
|
||||||
* Written by Andreas Schwarz (http://github.com/flyandi/mazda-custom-applications-sdk)
|
|
||||||
* Copyright (c) 2016. All rights reserved.
|
|
||||||
*
|
|
||||||
* WARNING: The installation of this application requires modifications to your Mazda Connect system.
|
|
||||||
* If you don't feel comfortable performing these changes, please do not attempt to install this. You might
|
|
||||||
* be ending up with an unusuable system that requires reset by your Dealer. You were warned!
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify it under the terms of the
|
|
||||||
* GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
|
||||||
* License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with this program.
|
|
||||||
* If not, see http://www.gnu.org/licenses/
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the main system file that manages everything between the CMU backend server and the frontend.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (Logger)
|
|
||||||
*/
|
|
||||||
|
|
||||||
window.Logger = {
|
|
||||||
|
|
||||||
levels: {
|
|
||||||
debug: 'DEBUG',
|
|
||||||
info: 'INFO',
|
|
||||||
error: 'ERROR',
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscriptions
|
|
||||||
* @array
|
|
||||||
*/
|
|
||||||
subscriptions: [],
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (debug) debug message
|
|
||||||
*/
|
|
||||||
|
|
||||||
debug: function() {
|
|
||||||
this.__message(this.levels.debug, "#006600", Array.apply(null, arguments));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (error) error message
|
|
||||||
*/
|
|
||||||
|
|
||||||
error: function() {
|
|
||||||
this.__message(this.levels.error, "#FF0000", Array.apply(null, arguments));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (info) info message
|
|
||||||
*/
|
|
||||||
|
|
||||||
info: function() {
|
|
||||||
this.__message(this.levels.info, "#0000FF", Array.apply(null, arguments));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe
|
|
||||||
* @return {[type]} [description]
|
|
||||||
*/
|
|
||||||
subscribe: function(callback) {
|
|
||||||
|
|
||||||
if(typeof(callback) == "function") {
|
|
||||||
this.subscriptions.push(callback);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [__message description]
|
|
||||||
* @param {[type]} level [description]
|
|
||||||
* @param {[type]} color [description]
|
|
||||||
* @param {[type]} values [description]
|
|
||||||
* @return {[type]} [description]
|
|
||||||
*/
|
|
||||||
__message: function(level, color, values) {
|
|
||||||
|
|
||||||
var msg = [];
|
|
||||||
|
|
||||||
if(values.length > 1) {
|
|
||||||
values.forEach(function(value, index) {
|
|
||||||
|
|
||||||
if(index > 0) {
|
|
||||||
|
|
||||||
if(typeof(value) == "object") {
|
|
||||||
|
|
||||||
var keys = value, o = false;
|
|
||||||
|
|
||||||
if(Object.prototype.toString.call(value) == "[object Object]") {
|
|
||||||
var keys = Object.keys(value),
|
|
||||||
o = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
keys.forEach(function(v, index) {
|
|
||||||
msg.push(o ? '[' + v + '=' + value[v]+ ']' : '[' + v + ']');
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
msg.push(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
msg = msg.join(" ");
|
|
||||||
|
|
||||||
this.subscriptions.forEach(function(subscription) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
subscription(level, values[0], msg, color);
|
|
||||||
} catch(e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (CustomApplications)
|
|
||||||
*
|
|
||||||
* Registers itself between the JCI system and CustomApplication framework.
|
|
||||||
*/
|
|
||||||
|
|
||||||
window.CustomApplications = {
|
|
||||||
|
|
||||||
ID: 'system',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (locals)
|
|
||||||
*/
|
|
||||||
debug: false,
|
|
||||||
bootstrapped: false,
|
|
||||||
|
|
||||||
systemAppId: 'system',
|
|
||||||
systemAppCategory: 'Applications',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overwrites
|
|
||||||
*/
|
|
||||||
proxyAppName: 'vdt',
|
|
||||||
proxyAppContext: 'DriveChartDetails',
|
|
||||||
proxyMmuiEvent: 'SelectDriveRecord',
|
|
||||||
|
|
||||||
targetAppName: 'custom',
|
|
||||||
targetAppContext: 'Surface',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration
|
|
||||||
*/
|
|
||||||
configuration: {
|
|
||||||
|
|
||||||
networkHost: '127.0.0.1',
|
|
||||||
networkPort: 9700,
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Commands
|
|
||||||
*/
|
|
||||||
commands: {
|
|
||||||
|
|
||||||
REQUEST_PING: 'ping',
|
|
||||||
REQUEST_APPDRIVE: 'appdrive',
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Results
|
|
||||||
*/
|
|
||||||
results: {
|
|
||||||
|
|
||||||
RESULT_OK: 200,
|
|
||||||
RESULT_PONG: 201,
|
|
||||||
RESULT_NOTFOUND: 404,
|
|
||||||
RESULT_ERROR: 500,
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the proxy
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
initialize: function(callback) {
|
|
||||||
|
|
||||||
if(!this.initialized) {
|
|
||||||
|
|
||||||
this.initialized = true;
|
|
||||||
|
|
||||||
this.requests = {};
|
|
||||||
|
|
||||||
this.obtainConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
return callback ? callback() : true;
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Establishes a connection between the front and backend
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
obtainConnection: function() {
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
this.client = new WebSocket('ws://' + this.configuration.networkHost + ':' + this.configuration.networkPort);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ping
|
|
||||||
*/
|
|
||||||
this.client.ping = function() {
|
|
||||||
|
|
||||||
this.request(this.commands.REQUEST_PING, {
|
|
||||||
inboundStamp: (new Date()).getTime()
|
|
||||||
}, function(error, result) {
|
|
||||||
|
|
||||||
Logger.info(this.ID, "ping", {
|
|
||||||
lost: error,
|
|
||||||
time: !error ? result.outboundStamp - result.inboundStamp : 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
}.bind(this);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onOpen
|
|
||||||
* @event
|
|
||||||
*/
|
|
||||||
this.client.onopen = function() {
|
|
||||||
|
|
||||||
Logger.info(this.ID, "connection open");
|
|
||||||
|
|
||||||
this.client.ping();
|
|
||||||
|
|
||||||
this.requestAppDrive();
|
|
||||||
|
|
||||||
}.bind(this);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onMessage
|
|
||||||
* @event
|
|
||||||
*/
|
|
||||||
this.client.onmessage = function(message) {
|
|
||||||
|
|
||||||
this.handleReturnRequest(message);
|
|
||||||
|
|
||||||
}.bind(this);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onError
|
|
||||||
* @event
|
|
||||||
*/
|
|
||||||
this.client.onerror = function(error) {
|
|
||||||
|
|
||||||
Logger.error(CustomApplications.ID, 'ClientError', error);
|
|
||||||
|
|
||||||
}.bind(this);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onClose
|
|
||||||
* @event
|
|
||||||
*/
|
|
||||||
this.client.onclose = function(event) {
|
|
||||||
|
|
||||||
this.client = null;
|
|
||||||
|
|
||||||
if(event.code == 3110) {
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
|
|
||||||
CustomApplications.obtainConnection();
|
|
||||||
|
|
||||||
}, 5000); // retry later
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}.bind(this);
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
|
|
||||||
this.client = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [request description]
|
|
||||||
* @return {[type]} [description]
|
|
||||||
*/
|
|
||||||
request: function(request, payload, callback) {
|
|
||||||
|
|
||||||
// check connection state
|
|
||||||
if(!this.client || this.client.readyState != 1) return callback(true, {});
|
|
||||||
|
|
||||||
// prepare id
|
|
||||||
var id = false;
|
|
||||||
while(!id || this.requests[id]) {
|
|
||||||
id = (new Date()).getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
// register request
|
|
||||||
this.requests[id] = callback;
|
|
||||||
|
|
||||||
// sanity check
|
|
||||||
payload = payload || {};
|
|
||||||
|
|
||||||
// add request id
|
|
||||||
payload.requestId = id;
|
|
||||||
|
|
||||||
payload.request = request;
|
|
||||||
|
|
||||||
// execute
|
|
||||||
return this.client.send(JSON.stringify(payload));
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes a request
|
|
||||||
* @param {[type]} data [description]
|
|
||||||
* @return {[type]} [description]
|
|
||||||
*/
|
|
||||||
handleReturnRequest: function(message) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
// parse message
|
|
||||||
var payload = JSON.parse(message.data);
|
|
||||||
|
|
||||||
// check against active requests
|
|
||||||
if(payload.requestId && this.requests[payload.requestId]) {
|
|
||||||
|
|
||||||
var callback = this.requests[payload.requestId];
|
|
||||||
|
|
||||||
if(typeof(callback) == "function") {
|
|
||||||
|
|
||||||
callback(payload.result == this.results.RESULT_ERROR, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete this.requests[payload.requestId];
|
|
||||||
|
|
||||||
return; // all done
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
|
|
||||||
Logger.error(CustomApplications.ID, 'handleReturnRequest', e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trys to load the AppDrive
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
requestAppDrive: function() {
|
|
||||||
|
|
||||||
if(typeof(CustomApplicationsHandler) != "undefined") return false;
|
|
||||||
|
|
||||||
if(!this.request(this.commands.REQUEST_APPDRIVE, false, function(error, result) {
|
|
||||||
|
|
||||||
if(error) {
|
|
||||||
return setTimeout(function() {
|
|
||||||
|
|
||||||
this.requestAppDrive();
|
|
||||||
|
|
||||||
}.bind(this), 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// boot strap system
|
|
||||||
this.bootstrap();
|
|
||||||
|
|
||||||
}.bind(this)));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (bootstrap)
|
|
||||||
*
|
|
||||||
* Bootstraps the JCI system
|
|
||||||
*/
|
|
||||||
|
|
||||||
bootstrap: function() {
|
|
||||||
|
|
||||||
// verify that core objects are available
|
|
||||||
if(typeof framework === 'object' && framework._currentAppUiaId === this.systemAppId && this.bootstrapped === false) {
|
|
||||||
|
|
||||||
// retrieve system app
|
|
||||||
var systemApp = framework.getAppInstance(this.systemAppId);
|
|
||||||
|
|
||||||
// verify bootstrapping - yeah long name
|
|
||||||
if(systemApp) {
|
|
||||||
|
|
||||||
// set to strap - if everything fails - no harm is done :-)
|
|
||||||
this.bootstrapped = true;
|
|
||||||
|
|
||||||
// let's boostrap
|
|
||||||
try {
|
|
||||||
|
|
||||||
// overwrite list2 handler
|
|
||||||
systemApp._contextTable[this.systemAppCategory].controlProperties.List2Ctrl.selectCallback = this.menuItemSelectCallback.bind(systemApp);
|
|
||||||
|
|
||||||
// for usb changes
|
|
||||||
if(typeof(systemApp.overwriteStatusMenuUSBAudioMsgHandler) == "undefined") {
|
|
||||||
systemApp.overwriteStatusMenuUSBAudioMsgHandler = systemApp._StatusMenuUSBAudioMsgHandler;
|
|
||||||
systemApp._StatusMenuUSBAudioMsgHandler = this.StatusMenuUSBAudioMsgHandler.bind(systemApp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// overwrite framework route handler
|
|
||||||
if(typeof(framework.overwriteRouteMmmuiMsg) == "undefined") {
|
|
||||||
framework.overwriteRouteMmmuiMsg = framework.routeMmuiMsg;
|
|
||||||
framework.routeMmuiMsg = this.routeMmuiMsg.bind(framework);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ovewrite framework MMUI sender
|
|
||||||
if(typeof(framework.overwriteSendEventToMmui) == "undefined") {
|
|
||||||
framework.overwriteSendEventToMmui = framework.sendEventToMmui;
|
|
||||||
framework.sendEventToMmui = this.sendEventToMmui.bind(framework);
|
|
||||||
}
|
|
||||||
|
|
||||||
// assign template transition
|
|
||||||
framework.transitionsObj._genObj._TEMPLATE_CATEGORIES_TABLE.SurfaceTmplt = 'Detail with UMP';
|
|
||||||
|
|
||||||
// kick off loader
|
|
||||||
this.prepareCustomApplications();
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
// bootstrapping process failed - we just leave it here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (Overwrite) menuItemSelectCallback
|
|
||||||
*/
|
|
||||||
|
|
||||||
menuItemSelectCallback: function(listCtrlObj, appData, params) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
if(appData.mmuiEvent == "SelectCustomApplication") {
|
|
||||||
|
|
||||||
// exit if handler is not available
|
|
||||||
if(typeof(CustomApplicationsHandler) != "undefined") {
|
|
||||||
|
|
||||||
// launch app
|
|
||||||
if(CustomApplicationsHandler.launch(appData)) {
|
|
||||||
|
|
||||||
// clone app data
|
|
||||||
try {
|
|
||||||
appData = JSON.parse(JSON.stringify(appData));
|
|
||||||
|
|
||||||
// set app data
|
|
||||||
appData.appName = CustomApplicationsProxy.proxyAppName;
|
|
||||||
appData.mmuiEvent = CustomApplicationsProxy.proxyMmuiEvent;
|
|
||||||
} catch(e) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
// pass to original handler
|
|
||||||
this._menuItemSelectCallback(listCtrlObj, appData, params);
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (Overwrite) sendEventToMmui
|
|
||||||
*/
|
|
||||||
|
|
||||||
sendEventToMmui: function(uiaId, eventId, params, fromVui) {
|
|
||||||
|
|
||||||
var currentUiaId = this.getCurrentApp(),
|
|
||||||
currentContextId = this.getCurrCtxtId();
|
|
||||||
|
|
||||||
// proxy overwrites
|
|
||||||
if(typeof(CustomApplicationsHandler) === 'object' && currentUiaId == CustomApplicationsProxy.targetAppName) {
|
|
||||||
currentUiaId = CustomApplicationsProxy.proxyAppName;
|
|
||||||
currentContextId = CustomApplicationsProxy.proxyAppContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
// pass to original handler
|
|
||||||
framework.overwriteSendEventToMmui(uiaId, eventId, params, fromVui, currentUiaId, currentContextId);
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (Overwrite) routeMmuiMsg
|
|
||||||
*/
|
|
||||||
|
|
||||||
routeMmuiMsg: function(jsObject) {
|
|
||||||
|
|
||||||
if(typeof(CustomApplicationsHandler) === 'object') {
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
var proxy = CustomApplicationsProxy;
|
|
||||||
|
|
||||||
// validate routing message
|
|
||||||
switch(jsObject.msgType) {
|
|
||||||
|
|
||||||
// magic switch
|
|
||||||
case 'ctxtChg':
|
|
||||||
if(jsObject.uiaId == proxy.proxyAppName) {
|
|
||||||
jsObject.uiaId = proxy.targetAppName;
|
|
||||||
jsObject.ctxtId = proxy.targetAppContext;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// check if our proxy app is in the focus stack
|
|
||||||
case 'focusStack':
|
|
||||||
|
|
||||||
if(jsObject.appIdList && jsObject.appIdList.length) {
|
|
||||||
for(var i = 0; i < jsObject.appIdList.length; i++) {
|
|
||||||
var appId = jsObject.appIdList[i];
|
|
||||||
if(appId.id == proxy.proxyAppName) {
|
|
||||||
appId.id = proxy.targetAppName;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'msg':
|
|
||||||
case 'alert':
|
|
||||||
|
|
||||||
if(jsObject.uiaId == proxy.proxyAppName) {
|
|
||||||
jsObject.uiaId = proxy.targetAppName;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// do nothing
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pass to framework
|
|
||||||
framework.overwriteRouteMmmuiMsg(jsObject);
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (Overwrite) StatusMenuUSBAudioMsgHandler
|
|
||||||
*/
|
|
||||||
|
|
||||||
StatusMenuUSBAudioMsgHandler: function(msg) {
|
|
||||||
|
|
||||||
// pass to original handler
|
|
||||||
this.overwriteStatusMenuUSBAudioMsgHandler(msg);
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (loadCustomApplications)
|
|
||||||
*/
|
|
||||||
|
|
||||||
loadCustomApplications: function() {
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
if(typeof(CustomApplicationsHandler) === 'undefined') {
|
|
||||||
|
|
||||||
// clear
|
|
||||||
clearTimeout(this.loadTimer);
|
|
||||||
|
|
||||||
// try to load the script
|
|
||||||
utility.loadScript("apps/custom/runtime/runtime.js", false, function() {
|
|
||||||
|
|
||||||
clearTimeout(this.loadTimer);
|
|
||||||
|
|
||||||
this.initCustomApplicationsDataList();
|
|
||||||
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
// safety timer
|
|
||||||
this.loadTimer = setTimeout(function() {
|
|
||||||
|
|
||||||
if(typeof(CustomApplicationsHandler) == "undefined") {
|
|
||||||
|
|
||||||
this.loadCount = this.loadCount + 1;
|
|
||||||
|
|
||||||
// 20 attempts or we forget it - that's almost 3min
|
|
||||||
if(this.loadCount < 20) {
|
|
||||||
|
|
||||||
this.loadCustomApplications();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}.bind(this), 10000);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
// if this fails, we won't attempt again because there could be issues with the actual handler
|
|
||||||
setTimeout(function() {
|
|
||||||
|
|
||||||
this.loadCustomApplications();
|
|
||||||
|
|
||||||
}.bind(this), 10000);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (initCustomApplicationsDataList)
|
|
||||||
*/
|
|
||||||
|
|
||||||
initCustomApplicationsDataList: function() {
|
|
||||||
// extend with custom applications
|
|
||||||
try {
|
|
||||||
if(typeof(CustomApplicationsHandler) != "undefined") {
|
|
||||||
|
|
||||||
CustomApplicationsHandler.retrieve(function(items) {
|
|
||||||
|
|
||||||
var systemApp = framework.getAppInstance(this.systemAppId);
|
|
||||||
|
|
||||||
items.forEach(function(item) {
|
|
||||||
|
|
||||||
systemApp._masterApplicationDataList.items.push(item);
|
|
||||||
|
|
||||||
framework.localize._appDicts[this.systemAppId][item.appData.appName.replace(".", "_")] = item.title;
|
|
||||||
|
|
||||||
framework.common._contextCategory._contextCategoryTable[item.appData.appName + '.*'] = 'Applications';
|
|
||||||
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
}.bind(this));
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
// failed to register applications
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runtime Caller
|
|
||||||
*/
|
|
||||||
|
|
||||||
if(window.opera) {
|
|
||||||
window.opera.addEventListener('AfterEvent.load', function (e) {
|
|
||||||
CustomApplications.initialize(function() {
|
|
||||||
CustomApplications.bootstrap();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** EOF **/
|
|
426
src/system/cmu.js
Normal file
426
src/system/cmu.js
Normal file
|
@ -0,0 +1,426 @@
|
||||||
|
/**
|
||||||
|
* Custom Applications SDK for Mazda Connect Infotainment System
|
||||||
|
*
|
||||||
|
* A mini framework that allows to write custom applications for the Mazda Connect Infotainment System
|
||||||
|
* that includes an easy to use abstraction layer to the JCI system.
|
||||||
|
*
|
||||||
|
* Written by Andreas Schwarz (http://github.com/flyandi/mazda-custom-applications-sdk)
|
||||||
|
* Copyright (c) 2016. All rights reserved.
|
||||||
|
*
|
||||||
|
* WARNING: The installation of this application requires modifications to your Mazda Connect system.
|
||||||
|
* If you don't feel comfortable performing these changes, please do not attempt to install this. You might
|
||||||
|
* be ending up with an unusuable system that requires reset by your Dealer. You were warned!
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
||||||
|
* License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with this program.
|
||||||
|
* If not, see http://www.gnu.org/licenses/
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CMU - Lightweight Communication Management Module
|
||||||
|
*/
|
||||||
|
|
||||||
|
window.CMU = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration
|
||||||
|
*/
|
||||||
|
configuration: {
|
||||||
|
|
||||||
|
networkHost: '127.0.0.1',
|
||||||
|
networkPort: 9700,
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests
|
||||||
|
*/
|
||||||
|
requests: {
|
||||||
|
|
||||||
|
REQUEST_PING: 'ping',
|
||||||
|
REQUEST_APPDRIVE: 'appdrive',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commands
|
||||||
|
*/
|
||||||
|
|
||||||
|
commands: {
|
||||||
|
|
||||||
|
LOAD_JS: 'loadjs',
|
||||||
|
LOAD_CSS: 'loadcss',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Results
|
||||||
|
*/
|
||||||
|
results: {
|
||||||
|
|
||||||
|
RESULT_OK: 200,
|
||||||
|
RESULT_PONG: 201,
|
||||||
|
RESULT_NOTFOUND: 404,
|
||||||
|
RESULT_ERROR: 500,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the proxy
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
initialize: function(callback) {
|
||||||
|
|
||||||
|
if(!this.initialized) {
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
|
||||||
|
this.requests = {};
|
||||||
|
|
||||||
|
this.obtainConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback ? callback() : true;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Establishes a connection between the front and backend
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
obtainConnection: function() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
this.client = new WebSocket('ws://' + this.configuration.networkHost + ':' + this.configuration.networkPort);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ping
|
||||||
|
*/
|
||||||
|
this.client.ping = function() {
|
||||||
|
|
||||||
|
this.request(this.requests.REQUEST_PING, {
|
||||||
|
inboundStamp: (new Date()).getTime()
|
||||||
|
}, function(error, result) {
|
||||||
|
|
||||||
|
this.__log("ping", {
|
||||||
|
lost: error,
|
||||||
|
time: !error ? result.outboundStamp - result.inboundStamp : 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onOpen
|
||||||
|
* @event
|
||||||
|
*/
|
||||||
|
this.client.onopen = function() {
|
||||||
|
|
||||||
|
this.__log("connection open");
|
||||||
|
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onMessage
|
||||||
|
* @event
|
||||||
|
*/
|
||||||
|
this.client.onmessage = function(message) {
|
||||||
|
|
||||||
|
this.handleReturnRequest(message);
|
||||||
|
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onError
|
||||||
|
* @event
|
||||||
|
*/
|
||||||
|
this.client.onerror = function(error) {
|
||||||
|
|
||||||
|
this.__error('error', error);
|
||||||
|
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onClose
|
||||||
|
* @event
|
||||||
|
*/
|
||||||
|
this.client.onclose = function(event) {
|
||||||
|
|
||||||
|
this.client = null;
|
||||||
|
|
||||||
|
if(event.code == 3110) {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
|
||||||
|
CMU.obtainConnection();
|
||||||
|
|
||||||
|
}, 5000); // retry later
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
|
||||||
|
this.client = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [request description]
|
||||||
|
* @return {[type]} [description]
|
||||||
|
*/
|
||||||
|
request: function(request, payload, callback) {
|
||||||
|
|
||||||
|
// check connection state
|
||||||
|
if(!this.client || this.client.readyState != 1) return callback(true, {});
|
||||||
|
|
||||||
|
// prepare id
|
||||||
|
var id = false;
|
||||||
|
while(!id || this.requests[id]) {
|
||||||
|
id = (new Date()).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
// register request
|
||||||
|
this.requests[id] = callback;
|
||||||
|
|
||||||
|
// sanity check
|
||||||
|
payload = payload || {};
|
||||||
|
|
||||||
|
// add request id
|
||||||
|
payload.requestId = id;
|
||||||
|
|
||||||
|
payload.request = request;
|
||||||
|
|
||||||
|
// execute
|
||||||
|
return this.client.send(JSON.stringify(payload));
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a request
|
||||||
|
* @param {[type]} data [description]
|
||||||
|
* @return {[type]} [description]
|
||||||
|
*/
|
||||||
|
handleReturnRequest: function(message) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
// parse message
|
||||||
|
var payload = JSON.parse(message.data);
|
||||||
|
|
||||||
|
// payload
|
||||||
|
switch(true) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests (Command from CMU)
|
||||||
|
*/
|
||||||
|
case typeof(payload.command) != "undefined":
|
||||||
|
|
||||||
|
this.handleCommand(payload.command, payload.attributes);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Everything else
|
||||||
|
*/
|
||||||
|
default:
|
||||||
|
|
||||||
|
// check against active requests
|
||||||
|
if(payload.requestId && this.requests[payload.requestId]) {
|
||||||
|
|
||||||
|
var callback = this.requests[payload.requestId];
|
||||||
|
|
||||||
|
if(typeof(callback) == "function") {
|
||||||
|
|
||||||
|
callback(payload.result == this.results.RESULT_ERROR, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.requests[payload.requestId];
|
||||||
|
|
||||||
|
return; // all done
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(error) {
|
||||||
|
|
||||||
|
this.__error('handleReturnRequest', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (handleCommand)
|
||||||
|
*/
|
||||||
|
|
||||||
|
handleCommand: function(command, attributes) {
|
||||||
|
|
||||||
|
switch(command) {
|
||||||
|
|
||||||
|
/** @type {LOADJS} [description] */
|
||||||
|
case this.commands.LOAD_JS:
|
||||||
|
|
||||||
|
this.loadJavascript(attributes.filenames, attributes.path);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (loadJavascript)
|
||||||
|
*/
|
||||||
|
|
||||||
|
loadJavascript: function(scripts, path, callback, options) {
|
||||||
|
|
||||||
|
this.__loadInvoker(scripts, path, function(filename, next) {
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.type = 'text/javascript';
|
||||||
|
script.src = filename;
|
||||||
|
script.onload = next;
|
||||||
|
document.body.appendChild(script);
|
||||||
|
}, callback, options);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (loadCSS)
|
||||||
|
*/
|
||||||
|
|
||||||
|
loadCSS: function(css, path, callback, options) {
|
||||||
|
|
||||||
|
this.__loadInvoker(css, path, function(filename, next) {
|
||||||
|
var css = document.createElement('link');
|
||||||
|
css.rel = "stylesheet";
|
||||||
|
css.type = "text/css";
|
||||||
|
css.href = filename
|
||||||
|
css.onload = async ? callback : next;
|
||||||
|
document.body.appendChild(css);
|
||||||
|
}, callback, options);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (__loadInvoker)
|
||||||
|
*/
|
||||||
|
|
||||||
|
__loadInvoker: function(input, path, build, callback, options) {
|
||||||
|
|
||||||
|
// sanity checks
|
||||||
|
if(typeof(build) != "function") return false;
|
||||||
|
|
||||||
|
// initialize
|
||||||
|
var ids = false, result = false, options = options ? options : {timeout: 1000}, timeout = false;
|
||||||
|
|
||||||
|
// items need to be an array
|
||||||
|
var items = input instanceof Array ? input : function() {
|
||||||
|
|
||||||
|
var newArray = [];
|
||||||
|
|
||||||
|
newArray.push(input);
|
||||||
|
|
||||||
|
return newArray;
|
||||||
|
|
||||||
|
}.call();
|
||||||
|
|
||||||
|
// loaded handler
|
||||||
|
var loaded = 0, next = function(failure) {
|
||||||
|
loaded++;
|
||||||
|
if(loaded >= items.length) {
|
||||||
|
if(typeof(callback) == "function") {
|
||||||
|
callback(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// process items
|
||||||
|
items.forEach(function(filename, index) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
filename = (path ? path : "") + filename;
|
||||||
|
|
||||||
|
if(options.timeout) {
|
||||||
|
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(function() {
|
||||||
|
|
||||||
|
this.__error("__loadInvoker:timeout", {filename: filename});
|
||||||
|
|
||||||
|
// just do the next one
|
||||||
|
next(true);
|
||||||
|
|
||||||
|
}.bind(this), options.timeout);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
build(filename, function(resource) {
|
||||||
|
|
||||||
|
next();
|
||||||
|
|
||||||
|
}.bind(this), ids ? ids[index] : false);
|
||||||
|
|
||||||
|
} catch(error) {
|
||||||
|
next(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(error) {
|
||||||
|
this.__error("__loadInvoker:loaderror", {error: error, filename: filename});
|
||||||
|
}
|
||||||
|
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple proxy to the logger
|
||||||
|
* @param string message The message
|
||||||
|
* @param array params The parameters
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
__log: function(message, params) {
|
||||||
|
if(typeof(Logger) != "undefined") {
|
||||||
|
Logger.info(this.IDs, message, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(message, params);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple proxy to the logger
|
||||||
|
* @param string message The message
|
||||||
|
* @param array params The parameters
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
__error: function(message, params) {
|
||||||
|
if(typeof(Logger) != "undefined") {
|
||||||
|
Logger.error(this.ID, message, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(message, params);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime Caller
|
||||||
|
*/
|
||||||
|
|
||||||
|
if(window.opera) {
|
||||||
|
window.opera.addEventListener('AfterEvent.load', function (e) {
|
||||||
|
CMU.initialize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** EOF **/
|
Loading…
Reference in a new issue