From 9f6ca70e122a32fd6c383b2466ab5495b7940a84 Mon Sep 17 00:00:00 2001 From: Zoltan Kis Date: Fri, 19 Sep 2014 22:18:02 +0300 Subject: [PATCH] W3C Telephony implementation Implements the latest W3C Telephony API specification found at http://telephony.sysapps.org/ Requires the oFono telephony middleware and the BlueZ Bluetooth middleware for working with paired phones. BUG=XWALK-1419 --- examples/telephony_test.html | 634 +++++++++++++ telephony/README.md | 70 ++ telephony/telephony.gyp | 29 + telephony/telephony_api.js | 641 +++++++++++++ telephony/telephony_backend_ofono.cc | 1637 ++++++++++++++++++++++++++++++++++ telephony/telephony_backend_ofono.h | 234 +++++ telephony/telephony_extension.cc | 25 + telephony/telephony_extension.h | 20 + telephony/telephony_instance.cc | 147 +++ telephony/telephony_instance.h | 45 + telephony/telephony_logging.h | 16 + tizen-wrt.gyp | 3 +- 12 files changed, 3500 insertions(+), 1 deletion(-) create mode 100644 examples/telephony_test.html create mode 100644 telephony/README.md create mode 100644 telephony/telephony.gyp create mode 100644 telephony/telephony_api.js create mode 100644 telephony/telephony_backend_ofono.cc create mode 100644 telephony/telephony_backend_ofono.h create mode 100644 telephony/telephony_extension.cc create mode 100644 telephony/telephony_extension.h create mode 100644 telephony/telephony_instance.cc create mode 100644 telephony/telephony_instance.h create mode 100644 telephony/telephony_logging.h diff --git a/examples/telephony_test.html b/examples/telephony_test.html new file mode 100644 index 0000000..b2bb182 --- /dev/null +++ b/examples/telephony_test.html @@ -0,0 +1,634 @@ + + + + +

Test W3C Telephony API

+ + + + + + +
+ + + + number: +
+ + + + call id: +
+ + + + +
+ + + to number: +
+ + + + +
+ + [0..9, A..F, p]: + + +
+ Service id: + + + + +
+ +
+ + + + + + + + diff --git a/telephony/README.md b/telephony/README.md new file mode 100644 index 0000000..29db9d6 --- /dev/null +++ b/telephony/README.md @@ -0,0 +1,70 @@ +## Introduction +This is the implementation of the W3C Telephony API for the Crosswalk runtime, +as an external Crosswalk extension. + +## Specification +The API specification is located at http://telephony.sysapps.org + +## Back-end +The back-end on Tizen IVI is oFono. Check documentation at +http://git.kernel.org/cgit/network/ofono/ofono.git/tree/doc/ +and source code at +http://git.kernel.org/cgit/network/ofono/ofono.git/. + +## Testing +On the test page, first add service and call listeners. +Then get services, and get default service. +Then make a call, check active call, list calls, hold/resume/disconnect etc. + +Hints for testing on a device supporting either built-in telephony modems +and/or Bluetooth HFP. + +Install packages needed for testing: +zypper in bluez-test ofono-test tizen-extensions-crosswalk-examples + +Also, install helpers: +zypper in glibc-devel glibc-devel-utils linux-glibc-devel gdb findutils-locate + +Unblock the rfkill on Bluetooth: +rfkill list +Identify the number for Bluetooth and unblock +rfkill unblock 0 + +Enable xwalk verbose logging, as root: +vi /usr/lib/systemd/user/xwalk.service +Modify xwalk.service to include "--enable-logging --v=1" + +Enable core dumps, as root: +chsmack -e System /lib/systemd/systemd-coredump + +If want to coredump into files instead of systemd-coredumpctl, as root: +sysctl kernel.core_pattern=core +sysctl kernel.core_uses_pid=1 + +Set limits for core, as app: +ulimit -c unlimited + +Set up environment (and proxy) as app: +export XDG_RUNTIME_DIR=/run/user/`id -u` + +systemctl --user daemon-reload + +Pair and connect a phone, as app: +bluetoothctl + list + agent on + discoverable on + pairable on + scan on + pair + connect + quit + +To stop crosswalk, as app: +systemctl --user stop xwalk + +To launch the test page: +xwalk-launcher file:///home/app/telephony_test.html + +To debug, attach gdb to the xwalk-launcher process (it becomes the extension +process for the app). diff --git a/telephony/telephony.gyp b/telephony/telephony.gyp new file mode 100644 index 0000000..66d00fb --- /dev/null +++ b/telephony/telephony.gyp @@ -0,0 +1,29 @@ +{ + 'includes':[ + '../common/common.gypi', + ], + 'targets': [ + { + 'target_name': 'tizen_telephony', + 'type': 'loadable_module', + 'variables': { + 'packages': [ + 'gio-2.0', + ], + }, + 'includes': [ + '../common/pkg-config.gypi', + ], + 'sources': [ + 'telephony_api.js', + 'telephony_backend_ofono.cc', + 'telephony_backend_ofono.h', + 'telephony_extension.cc', + 'telephony_extension.h', + 'telephony_instance.cc', + 'telephony_instance.h', + 'telephony_logging.h', + ], + }, + ], +} diff --git a/telephony/telephony_api.js b/telephony/telephony_api.js new file mode 100644 index 0000000..c5a142f --- /dev/null +++ b/telephony/telephony_api.js @@ -0,0 +1,641 @@ +// Copyright (c) 2014 Intel Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// The W3C Telephony API specification: +// http://www.w3.org/2012/sysapps/telephony/ + +// The native part of the extension may or may not cache call objects on their +// own, depending on the backend. The oFono backend does cache the calls which +// are handled by oFono as DBUS objects. Because the IPC overhead, call and +// service objects are also cached by this implementation, and the IPC protocol +// involves exchanging object id's - except when there is a mismatch, in which +// case a re-sync is done. + +/////////////////////////////////////////////////////////////////////////////// +// Utilities +/////////////////////////////////////////////////////////////////////////////// + +var v8tools = null; +if (typeof requireNative !== 'undefined') { + v8tools = requireNative('v8tools'); +} + +if (!v8tools) { + v8tools = function() {}; + v8tools.prototype.forceSetProperty = function(obj, prop, value) { + obj[prop] = value; + }; +} + +function addReadonlyProperty(obj, name, value) { + Object.defineProperty(obj, name, { + configurable: true, + enumerable: true, + writable: true, // TODO zolkis: fix v8tools.forceSetProperty issues + value: value + }); +} + +function forceSetProperty(obj, prop, value) { + v8tools.forceSetProperty(obj, prop, value); + //obj[prop] = value; +} + +function error(txt) { + var output = (txt instanceof Object) ? + '\n[Telephony JS] ' + toPrintableString(txt) : + '\n[Telephony JS] Error: ' + (txt || 'null'); + + console.log(output); +} + +function log(txt) { + console.log('\n[JS log] ' + txt instanceof Object ? + toPrintableString(txt) : (txt || 'null')); +} + +function toPrintableString(o) { + if (!(o instanceof Object) && !(o instanceof Array)) + return o; + var out = '{ '; + for (var i in o) { + out += i + ': ' + toPrintableString(o[i]) + ', '; + } + out += '}'; + return out; +} + +function checkRemoteParty(str) { + return str.match(/[+]?[0-9*#]{1,80}/g) == str; +} + +/////////////////////////////////////////////////////////////////////////////// +// exports +/////////////////////////////////////////////////////////////////////////////// + +function TelephonyManager() { + this.onserviceadded = null; + this.onserviceremoved = null; + this.ondefaultservicechanged = null; + this.oncalladded = null; + this.callchanged = null; + this.oncallremoved = null; + this.onactivecallchanged = null; + this.oncallstatechanged = null; + this.onemergencynumberschanged = null; + addReadonlyProperty(this, 'defaultServiceId', null); + addReadonlyProperty(this, 'activeCall', null); +} + +var _telephonyManager = new TelephonyManager(); +exports = _telephonyManager; + +/////////////////////////////////////////////////////////////////////////////// +// Local structures, event listeners +/////////////////////////////////////////////////////////////////////////////// + +var _next_promise_id = 1; +var _pending_promises = {}; + +var _listeners_count = 0; +var _listeners = {}; +_listeners.serviceadded = []; +_listeners.serviceremoved = []; +_listeners.defaultservicechanged = []; +_listeners.calladded = []; +_listeners.callremoved = []; +_listeners.callstatechanged = []; +_listeners.callchanged = []; +_listeners.activecallchanged = []; + +function addEventListener(kind, callback) { + if (typeof callback == 'function' && _listeners[kind] && + _listeners[kind].indexOf(callback) == -1) { + _listeners[kind].push(callback); + if (_listeners_count++ == 0) + enableBackendNotifications(); + } +} + +function removeEventListener(kind, callback) { + if (typeof callback == 'function' && _listeners[kind]) { + var i = _listeners[kind].indexOf(callback); + if (i >= 0) { + _listeners[kind].splice(i, 1); + if (--_listeners_count == 0) + disableBackendNotifications(); + } + } +} + +function dispatchEvent(event) { + if (typeof event == 'object' && _listeners[event.type]) { + _listeners[event.type].forEach(function(cb) { + if (typeof cb == 'function') { + cb(event); + } + }); + return true; + } + return false; +} + +TelephonyManager.prototype.addEventListener = addEventListener; +TelephonyManager.prototype.removeEventListener = removeEventListener; +TelephonyManager.prototype.dispatchEvent = dispatchEvent; + +/////////////////////////////////////////////////////////////////////////////// +// Message sending and Promises +/////////////////////////////////////////////////////////////////////////////// + +function PendingRequest(resolve, reject) { + this.resolve = resolve; + this.reject = reject; +} + +function sendRequest(msg, resolve, reject) { + msg.promiseId = _next_promise_id++; + _pending_promises[msg.promiseId] = new PendingRequest(resolve, reject); + var req = JSON.stringify(msg); + extension.postMessage(req); +} + +function resolvePromise(msg) { + var promise = msg.promiseId ? _pending_promises[msg.promiseId] : null; + if (promise && promise.resolve) + promise.resolve(msg.returnValue); +} + +function rejectPromise(msg) { + var promise = msg.promiseId ? _pending_promises[msg.promiseId] : null; + if (!promise || !promise.reject) + return; + // DOMError will be nuked, DOMException kludgy to create, so using Error + // with the properties of DOMException + // the W3C Telephony spec needs an update on this... + var e = new Error(msg.errorMessage); + e.code = msg.errorCode; // add a 'code' property + // optionally add a name property; e.name = msg.errorName; + promise.reject(e); +} + +// Handle replies and notifications from native extension code. +extension.setMessageListener(function(json) { + var msg = JSON.parse(json); + switch (msg.cmd) { + // Event notifications. + case 'activeCallChanged': + handleActiveCallChanged(msg); + break; + case 'callAdded': // incoming or waiting calls + handleCallAdded(msg); + break; + case 'callRemoved': // disconnected calls + handleCallRemoved(msg); + break; + case 'callStateChanged': + handleCallStateChanged(msg); + break; + case 'callChanged': + handleCallChanged(msg); + break; + case 'serviceAdded': + handleServiceAdded(msg); + break; + case 'serviceRemoved': + handleServiceRemoved(msg); + break; + case 'serviceChanged': + handleServiceChanged(msg); + break; + case 'defaultServiceChanged': + handleDefaultServiceChanged(msg); + break; + case 'emergencyNumbersChanged': + handleEmergencyNumbersChanged(msg); + break; + + // Replies needing postprocessing before resolving the Promise. + case 'getServices': + getServicesCallback(msg); + break; + case 'getCalls': + getCallsCallback(msg); + break; + case 'getParticipants': + getParticipantsCallback(msg); + break; + + // Replies needing no special treatment before resolving the Promise. + case 'setDefaultService': + case 'setServiceEnabled': + case 'dial': + case 'accept': + case 'disconnect': + case 'hold': + case 'resume': + case 'deflect': + case 'transfer': + case 'createConference': + case 'split': + case 'getEmergencyNumbers': + case 'emergencyDial': + case 'sendTones': + case 'startTone': + case 'stopTone': + msg.isError ? rejectPromise(msg) : resolvePromise(msg); + break; + default: + error('Invalid message from extension: ' + json); + } +}); + +function enableBackendNotifications() { + var msg = { + 'cmd': 'enableNotifications' + }; + return sendRequest(msg, null, null); // no Promise +} + +function disableBackendNotifications() { + var msg = { + 'cmd': 'disableNotifications' + }; + return sendRequest(msg, null, null); // no Promise +} + +/////////////////////////////////////////////////////////////////////////////// +// TelephonyCall +/////////////////////////////////////////////////////////////////////////////// + +function TelephonyCall(dict) { + addReadonlyProperty(this, 'callId', dict.callId || null); + addReadonlyProperty(this, 'conferenceId', dict.conferenceId || null); + addReadonlyProperty(this, 'remoteParty', dict.remoteParty || null); + addReadonlyProperty(this, 'serviceId', dict.serviceId || null); + addReadonlyProperty(this, 'state', dict.state || null); + addReadonlyProperty(this, 'stateReason', null); +} + +TelephonyCall.prototype.accept = function() { + var msg = { + 'cmd': 'accept', + 'callId': this.callId + }; + return new Promise(function(resolve, reject) { + sendRequest(msg, resolve, reject); + }); +}; + +TelephonyCall.prototype.disconnect = function() { + var msg = { + 'cmd': 'disconnect', + 'callId': this.callId + }; + return new Promise(function(resolve, reject) { + sendRequest(msg, resolve, reject); + }); +}; + +TelephonyCall.prototype.hold = function() { + var msg = { + 'cmd': 'hold', + 'callId': this.callId + }; + return new Promise(function(resolve, reject) { + sendRequest(msg, resolve, reject); + }); +}; + +TelephonyCall.prototype.resume = function() { + var msg = { + 'cmd': 'resume', + 'callId': this.callId + }; + return new Promise(function(resolve, reject) { + sendRequest(msg, resolve, reject); + }); +}; + +TelephonyCall.prototype.deflect = function(remoteParty) { + var msg = { + 'cmd': 'deflect', + 'callId': this.callId, + 'remoteParty': remoteParty + }; + return new Promise(function(resolve, reject) { + sendRequest(msg, resolve, reject); + }); +}; + +TelephonyCall.prototype.transfer = function(call) { + var msg = { + 'cmd': 'transfer' + }; + return new Promise(function(resolve, reject) { + sendRequest(msg, resolve, reject); + }); +}; + +TelephonyCall.prototype.split = function() { + var msg = { + 'cmd': 'split', + 'callId': this.callId + }; + return new Promise(function(resolve, reject) { + sendRequest(msg, resolve, reject); + }); +}; + +/////////////////////////////////////////////////////////////////////////////// +// Call Management +/////////////////////////////////////////////////////////////////////////////// + +function handleActiveCallChanged(msg) { + var call = msg.call ? new TelephonyCall(msg.call) : null; + forceSetProperty(_telephonyManager, 'activeCall', call); + var evt = new Event('activecallchanged'); + _telephonyManager.dispatchEvent(evt); + if (_telephonyManager.onactivecallchanged instanceof Function) + _telephonyManager.onactivecallchanged(evt); +} + +function handleCallAdded(msg) { + var evt = new CustomEvent('calladded'); + addReadonlyProperty(evt, 'call', new TelephonyCall(msg.call)); + _telephonyManager.dispatchEvent(evt); + if (_telephonyManager.oncalladded instanceof Function) + _telephonyManager.oncalladded(evt); +} + +function handleCallStateChanged(msg) { + var call = new TelephonyCall(msg.call); + if (call.state == 'disconnected' && !call.duration) { + // if the backend didn't calculate duration, do it now + call.duration = new Date() - new Date(call.startTime); // milliseconds + } + if (call.callId == _telephonyManager.activeCall.callId) + forceSetProperty(_telephonyManager, 'activeCall', call); + var evt = new CustomEvent('callstatechanged'); + addReadonlyProperty(evt, 'call', call); + _telephonyManager.dispatchEvent(evt); + if (_telephonyManager.oncallstatechanged instanceof Function) + _telephonyManager.oncallstatechanged(evt); +} + +// special things to handle: isConference is changed with conf calls +function handleCallChanged(msg) { + if (!msg.call || !msg.changedProperties) + return; + var call = new TelephonyCall(msg.call); + if (call.callId == _telephonyManager.activeCall.callId) + forceSetProperty(_telephonyManager, 'activeCall', call); + var evt = new CustomEvent('callchanged'); + addReadonlyProperty(evt, 'call', call); + addReadonlyProperty(evt, 'changedProperties', call.changedProperties || []); + _telephonyManager.dispatchEvent(evt); + if (_telephonyManager.oncallchanged instanceof Function) + _telephonyManager.oncallchanged(evt); +} + +function handleCallRemoved(msg) { + var evt = new CustomEvent('callremoved'); + addReadonlyProperty(evt, 'call', new TelephonyCall(msg.call)); + _telephonyManager.dispatchEvent(evt); + if (_telephonyManager.oncallremoved instanceof Function) + _telephonyManager.oncallremoved(evt); +} + +TelephonyManager.prototype.getCalls = function() { + var msg = { + 'cmd': 'getCalls' + }; + return new Promise(function(resolve, reject) { + sendRequest(msg, resolve, reject); + }); +}; + +function getCallsCallback(msg) { + if (msg.isError || !(msg.returnValue instanceof Array)) { + rejectPromise(msg); + return; + } + var res = []; + for (var i = 0; i < msg.returnValue.length; i++) { + res.push(new TelephonyCall(msg.returnValue[i])); + } + msg.returnValue = res; + resolvePromise(msg); +} + +TelephonyManager.prototype.dial = function(remoteParty, dialOptions) { + return new Promise(function(resolve, reject) { + if (!checkRemoteParty(remoteParty)) { + var err = new Error('InvalidCharacterError'); + err.message = 'Invalid remote party'; + reject(err); + return; + } + var msg = { + cmd: 'dial', + serviceId: dialOptions && dialOptions.serviceId || null, + remoteParty: remoteParty, + hideCallerId: dialOptions && dialOptions.hideCallerId || false + }; + sendRequest(msg, resolve, reject); + }); +}; + +TelephonyManager.prototype.createConference = function() { + var msg = { + 'cmd': 'createConference' + }; + return new Promise(function(resolve, reject) { + sendRequest(msg, resolve, reject); + }); +}; + +TelephonyManager.prototype.getParticipants = function(conferenceId) { + var msg = { + 'cmd': 'getParticipants', + 'conferenceId': conferenceId + }; + return new Promise(function(resolve, reject) { + sendRequest(msg, resolve, reject); + }); +}; + +function getParticipantsCallback(msg) { + if (msg.isError || !(msg.returnValue instanceof Array)) { + rejectPromise(msg); + return; + } + var res = []; + for (var i = 0; i < msg.returnValue.length; i++) + res.push(new TelephonyCall(msg.returnValue[i])); + msg.returnValue = res; + resolvePromise(msg); +} + +/////////////////////////////////////////////////////////////////////////////// +// Tone Management +/////////////////////////////////////////////////////////////////////////////// + +TelephonyManager.prototype.sendTones = function(tones, toneOptions) { + var msg = { + 'cmd': 'sendTones', + 'tones': tones, + 'serviceId': toneOptions && toneOptions.serviceId || null, + 'gap': toneOptions && toneOptions.gap || null, + 'duration': toneOptions && toneOptions.duration || null + }; + return new Promise(function(resolve, reject) { + sendRequest(msg, resolve, reject); + }); +}; + +TelephonyManager.prototype.startTone = function(tone, toneOptions) { + var msg = { + 'cmd': 'startTone', + 'tone': tone, + 'serviceId': toneOptions && toneOptions.serviceId || null, + 'gap': toneOptions && toneOptions.gap || '' + }; + return new Promise(function(resolve, reject) { + sendRequest(msg, resolve, reject); + }); +}; + +TelephonyManager.prototype.stopTone = function(serviceId, toneOptions) { + var msg = { + 'cmd': 'stopTone', + 'serviceId': toneOptions && toneOptions.serviceId || null + }; + return new Promise(function(resolve, reject) { + sendRequest(msg, resolve, reject); + }); +}; + +/////////////////////////////////////////////////////////////////////////////// +// Emergency Management +/////////////////////////////////////////////////////////////////////////////// + +TelephonyManager.prototype.getEmergencyNumbers = function() { + var msg = { + 'cmd': 'getEmergencyNumbers' + }; + return new Promise(function(resolve, reject) { + sendRequest(msg, resolve, reject); + }); +}; + +/////////////////////////////////////////////////////////////////////////////// +// TelephonyService +/////////////////////////////////////////////////////////////////////////////// + +function TelephonyService(dict) { + if (!dict) + return null; + addReadonlyProperty(this, 'serviceId', dict.id || null); + addReadonlyProperty(this, 'enabled', !!dict.enabled); + addReadonlyProperty(this, 'emergency', dict.emergency); + addReadonlyProperty(this, 'protocol', dict.protocol || ''); + addReadonlyProperty(this, 'serviceType', dict.type || ''); + addReadonlyProperty(this, 'provider', dict.provider || ''); + this.displayName = dict.name || ''; + return this; +} + +TelephonyService.prototype.setServiceEnabled = function(enabled) { + var msg = { + 'cmd': 'setServiceEnabled', + 'serviceId': this.serviceId, + 'enabled': enabled ? true : false + }; + return new Promise(function(resolve, reject) { + sendRequest(msg, resolve, reject); + }); +}; + +/////////////////////////////////////////////////////////////////////////////// +// Service Management +/////////////////////////////////////////////////////////////////////////////// + +function handleServiceAdded(msg) { + var service = new TelephonyService(msg.service); + var evt = new CustomEvent('serviceadded'); + addReadonlyProperty(evt, 'serviceId', service.serviceId); + _telephonyManager.dispatchEvent(evt); + if (typeof _telephonyManager.onserviceadded == 'function') + _telephonyManager.onserviceadded(evt); +} + +function handleServiceChanged(msg) { + var evt = new CustomEvent('servicechanged'); + addReadonlyProperty(evt, 'service', new TelephonyService(msg.service)); + addReadonlyProperty(evt, 'changedProperties', msg.changedProperties); + _telephonyManager.dispatchEvent(evt); + if (typeof _telephonyManager.onservicechanged == 'function') + _telephonyManager.onservicechanged(evt); +} + +function handleServiceRemoved(msg) { + var evt = new CustomEvent('serviceremoved'); + addReadonlyProperty(evt, 'service', msg.service); + _telephonyManager.dispatchEvent(evt); + if (typeof _telephonyManager.onserviceremoved == 'function') + _telephonyManager.onserviceremoved(evt); +} + +function notifyDefaultServiceChanged() { + var evt = new CustomEvent('defaultservicechanged'); + addReadonlyProperty(evt, 'service', msg.service); + _telephonyManager.dispatchEvent(evt); + if (typeof _telephonyManager.ondefaultservicechanged == 'function') { + _telephonyManager.ondefaultservicechanged(evt); + } +} + +function handleDefaultServiceChanged(msg) { + forceSetProperty(_telephonyManager, 'defaultServiceId', msg.service.serviceId); + notifyDefaultServiceChanged(); +} + +TelephonyManager.prototype.getServiceIds = function() { + var msg = { + 'cmd': 'getServices' + }; + return new Promise(function(resolve, reject) { + sendRequest(msg, resolve, reject); + }); +}; + +function getServicesCallback(msg) { + if (msg.isError) { + rejectPromise(msg); + return; + } + var arr = msg.returnValue; + var res = []; + for (var i = 0; i < arr.length; i++) { + var service = arr[i]; + if (!_telephonyManager.defaultServiceId) { + forceSetProperty(_telephonyManager, 'defaultServiceId', service.serviceId); + notifyDefaultServiceChanged(); + } + res.push(service.serviceId); + } + msg.returnValue = res; + resolvePromise(msg); +} + +TelephonyManager.prototype.setDefaultServiceId = function(serviceId) { + var msg = { + 'cmd': 'setDefaultServiceId', + 'serviceId': serviceId + }; + return new Promise(function(resolve, reject) { + sendRequest(msg, resolve, reject); + }); +}; diff --git a/telephony/telephony_backend_ofono.cc b/telephony/telephony_backend_ofono.cc new file mode 100644 index 0000000..e75389f --- /dev/null +++ b/telephony/telephony_backend_ofono.cc @@ -0,0 +1,1637 @@ +// Copyright (c) 2014 Intel Corporation. All rights reserved. +// Use of source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "telephony/telephony_backend_ofono.h" +#include "telephony/telephony_logging.h" + +#include +#include +#include + +#include +#include + +namespace { + +const char kDbusOfonoService[] = "org.ofono"; +const char kDbusOfonoModemManager[] = "org.ofono.Manager"; +const char kDbusOfonoModem[] = "org.ofono.Modem"; +const char kDbusOfonoVoiceCallManager[] = "org.ofono.VoiceCallManager"; +const char kDbusOfonoVoiceCall[] = "org.ofono.VoiceCall"; +const char kDbusOfonoSimManager[] = "org.ofono.SimManager"; + +const char kCallStateInit[] = "init"; +const char kCallStateActive[] = "active"; +const char kCallStateHeld[] = "held"; +const char kCallStateDialing[] = "dialing"; +const char kCallStateAlerting[] = "alerting"; +const char kCallStateIncoming[] = "incoming"; +const char kCallStateWaiting[] = "waiting"; +const char kCallStateDisconnected[] = "disconnected"; +const char kCallStateConference[] = "conference"; + +} // namespace + +////////////////////////////////////////////////////////////////////////////// +// TelephonyBackend, DBUS/signal handling +/////////////////////////////////////////////////////////////////////////////// + +TelephonyBackend::TelephonyBackend(TelephonyInstance* instance) + : instance_(instance), cancellable_(nullptr), default_service_(nullptr), + active_call_(nullptr) { +} + +TelephonyBackend::~TelephonyBackend() { + DisableSignalHandlers(); + for (auto i : calls_) + delete i; + for (auto i : services_) + delete i; + for (auto i : removed_calls_) + delete i; +} + +void TelephonyBackend::SetDBusSignalHandler(const gchar* iface, + const gchar* name, const gchar* obj, GDBusSignalCallback cb, + gpointer user_data) { + LOG_DBG("SetDBusSignalHandler: " << std::string(iface) << ":" << + std::string(name) << " [" << std::string(obj ? obj : "") << "]"); + + GDBusConnection* dbus_conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, + nullptr); + + guint handle = g_dbus_connection_signal_subscribe(dbus_conn, nullptr, + iface, name, obj, nullptr, G_DBUS_SIGNAL_FLAGS_NONE, cb, user_data, + nullptr); + + dbus_listeners_.push_back(handle); +} + +GVariant* TelephonyBackend::SyncDBusCall(const gchar* object, + const gchar* interface, const gchar* method, GVariant* parameters, + GError** error) { + LOG_DBG("SyncDBusCall: " << std::string(method)); + + GDBusConnection* dbus_conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, + nullptr); + + return g_dbus_connection_call_sync(dbus_conn, kDbusOfonoService, + object, interface, method, parameters, + nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, error); +} + +void TelephonyBackend::AsyncDBusCall(const gchar* object, + const gchar* interface, const gchar* method, GVariant* parameters, + GAsyncReadyCallback callback, gpointer user_data) { + + GDBusConnection* dbus_conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, + nullptr); + + g_dbus_connection_call(dbus_conn, kDbusOfonoService, + object, interface, method, parameters, + nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, callback, user_data); +} + +void TelephonyBackend::EnableSignalHandlers() { + SetDBusSignalHandler(kDbusOfonoModemManager, "ModemAdded", nullptr, + SignalHandler, this); + SetDBusSignalHandler(kDbusOfonoModemManager, "ModemRemoved", nullptr, + SignalHandler, this); + SetDBusSignalHandler(kDbusOfonoModem, "PropertyChanged", nullptr, + SignalHandler, this); + + SetDBusSignalHandler(kDbusOfonoVoiceCallManager, "CallAdded", nullptr, + SignalHandler, this); + SetDBusSignalHandler(kDbusOfonoVoiceCallManager, "CallRemoved", nullptr, + SignalHandler, this); + SetDBusSignalHandler(kDbusOfonoVoiceCallManager, "PropertyChanged", nullptr, + SignalHandler, this); + + SetDBusSignalHandler(kDbusOfonoVoiceCall, "PropertyChanged", nullptr, + SignalHandler, this); + SetDBusSignalHandler(kDbusOfonoVoiceCall, "DisconnectReason", nullptr, + SignalHandler, this); +} + +// Called when the last signal listener is removed. +void TelephonyBackend::DisableSignalHandlers() { + LOG_DBG("DisableSignalHandlers"); + GDBusConnection* dbus_conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, + nullptr); + for (int i = 0; i < dbus_listeners_.size(); i++) { + g_dbus_connection_signal_unsubscribe(dbus_conn, dbus_listeners_[i]); + } + dbus_listeners_.clear(); +} + +void TelephonyBackend::SignalHandler(GDBusConnection* connection, + const gchar* sender, const gchar* object, + const gchar* interface, const gchar* signal, + GVariant* parameters, gpointer user_data) { + + TelephonyBackend* backend = static_cast(user_data); + if (!backend) { + LOG_ERR("SignalHandler: failed to get backend"); + return; + } + LOG_DBG("SignalHandler: " << std::string(signal)); + + if (!strcmp(interface, kDbusOfonoModemManager)) { + if (!strcmp(signal, "ModemAdded")) { + backend->OnModemAdded(object, parameters); + } else if (!strcmp(signal, "ModemRemoved")) { + backend->OnModemRemoved(object, parameters); + } + return; + } + + if (!strcmp(interface, kDbusOfonoModem)) { + backend->OnModemChanged(object, parameters); + return; + } + + if (!strcmp(interface, kDbusOfonoVoiceCallManager)) { + if (!strcmp(signal, "CallAdded")) { + backend->OnCallAdded(object, parameters); + } else if (!strcmp(signal, "CallRemoved")) { + backend->OnCallRemoved(object, parameters); + } else if (!strcmp(signal, "PropertyChanged")) { + backend->OnCallManagerChanged(object, parameters); + } + return; + } + + if (!strcmp(interface, kDbusOfonoVoiceCall)) { + if (!strcmp(signal, "PropertyChanged")) { + backend->OnCallChanged(object, parameters); + } else if (!strcmp(signal, "DisconnectReason")) { + backend->OnCallDisconnectReason(object, parameters); + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +// TelephonyService +/////////////////////////////////////////////////////////////////////////////// + +picojson::object& TelephonyBackend::ToJson(TelephonyService* service, + picojson::object& out) { + out["serviceId"] = picojson::value(service->id); + out["name"] = picojson::value(service->name); + out["serviceType"] = picojson::value(service->type); + out["enabled"] = picojson::value(service->online? true : false); + out["provider"] = picojson::value(service->provider); + out["protocol"] = picojson::value(service->protocol); + out["emergency"] = picojson::value(service->emergency); + return out; +} + +void TelephonyBackend::LogService(TelephonyService* service) { + std::cout << "{\n\tserviceId: " << service->id; + std::cout << "\n\tname: " << service->name; + std::cout << "\n\tenabled: " << + (service->powered && service->online ? "true" : "false"); + std::cout << "\n\tserial: " << service->serial; // e.g. BT address comes here + std::cout << "\n}" << std::endl; +} + +// Return the affected JS property name +const char* TelephonyBackend::UpdateServiceProperty(TelephonyService* service, + const char* key, GVariant* value) { + if (!strcmp(key, "Powered")) { + service->powered = g_variant_get_boolean(value); + return "enabled"; + } + + if (!strcmp(key, "Online")) { + service->online = g_variant_get_boolean(value); + return "enabled"; + } + + if (!strcmp(key, "Lockdown")) { + service->lockdown = g_variant_get_boolean(value); + return nullptr; + } + + if (!strcmp(key, "Emergency")) { + service->emergency = g_variant_get_boolean(value); + return "emergency"; + } + + if (!strcmp(key, "Manufacturer")) { + service->provider = g_variant_get_string(value, nullptr); + return "name"; + } + + if (!strcmp(key, "Name")) { + service->name = g_variant_get_string(value, nullptr); + return "name"; + } + + if (!strcmp(key, "Model")) { + service->model = g_variant_get_string(value, nullptr); + return "name"; + } + + if (!strcmp(key, "Revision")) { + service->revision = g_variant_get_string(value, nullptr); + return "name"; + } + + if (!strcmp(key, "Serial")) { + service->serial = g_variant_get_string(value, nullptr); + return "name"; + } + + if (!strcmp(key, "Type")) { + const gchar* stype = g_variant_get_string(value, nullptr); + if (!strcmp(stype, "hardware")) + service->type = "hw"; + else if (!strcmp(stype, "hfp")) + service->type = "hfp"; + else if (!strcmp(stype, "sap")) + service->type = "sap"; + else + service->type = "unknown"; + return "type"; + } + + return nullptr; +} + +bool TelephonyBackend::InitService(TelephonyService* service, + const std::string& sid, GVariantIter* props) { + + if (sid.empty() || !props) { + return false; + } + + service->id = sid; + service->protocol = "gsm"; + + char* key = nullptr; + GVariant* value = nullptr; + while (g_variant_iter_next(props, "{sv}", &key, &value)) { + UpdateServiceProperty(service, key, value); + } + g_variant_unref(value); + + return true; +} + +void TelephonyBackend::UpdateService(TelephonyService* service) { + GError* err = nullptr; + GVariant* res = SyncDBusCall(IdToDbus(service->id).c_str(), + kDbusOfonoModemManager, "GetProperties", nullptr, &err); + + if (err) { + LOG_ERR("UpdateService: " << err->message); + return; + } + + GVariantIter* props = nullptr; + g_variant_get(res, "(a{sv})", &props); + InitService(service, service->id, props); + g_variant_iter_free(props); + g_variant_unref(res); +} + +/////////////////////////////////////////////////////////////////////////////// +// TelephonyCall +/////////////////////////////////////////////////////////////////////////////// + +picojson::object& TelephonyBackend::ToJson(TelephonyCall* call, + picojson::object& out) { + out["callId"] = picojson::value(call->id); + out["serviceId"] = picojson::value(call->service ? + call->service->id : + ""); + out["remoteParty"] = picojson::value(call->remote_party); + out["state"] = picojson::value(call->state); + out["stateReason"] = picojson::value(call->state_reason); + out["startTime"] = picojson::value(call->start_time); + out["duration"] = picojson::value(call->duration); + out["protocol"] = picojson::value(call->protocol); + out["emergency"] = picojson::value(call->emergency); + out["conferenceId"] = picojson::value(call->conference ? + call->conference->id : ""); + picojson::value::array p; + for (auto c : call->participants) { + if (c) + p.push_back(picojson::value(c->id)); + } + out["participants"] = picojson::value(p); + return out; +} + +void TelephonyBackend::LogCall(TelephonyCall* call) { + std::cout << "{\n\tcallId: " << call->id; + std::cout << "\n\tremoteParty: " << call->remote_party; + std::cout << "\n\tstate: " << call->state; + std::cout << "\n\tserviceId: " << call->service->id; + std::cout << "\n\tstartTime: " << call->start_time; + std::cout << "\n}" << std::endl; +} + +void TelephonyBackend::UpdateDuration(TelephonyCall* call) { + time_t endt = time(nullptr); + struct tm tm; + // time format from /src/voicecall.c + strptime(call->start_time.c_str(), "%Y-%m-%dT%H:%M:%S%z", &tm); + time_t start_time = mktime(&tm); + call->duration = difftime(endt, start_time) * 1000.0; // sec --> msec +} + +const char* TelephonyBackend::UpdateCallState(TelephonyCall* call, + std::string& state) { + if (state == kCallStateDisconnected) { // stamp end time + UpdateDuration(call); + call->state = state; + return "state"; + } + + TelephonyCall* conf = call->conference; + // Check on conference participants state change. + if (conf && call != conf) { + // Calls participating in a conference stay in 'conference' state, but + // the shadow state is updated. + // Note that with oFono objects, all connected calls participating in a + // multiparty call are in 'active' or 'held' state, however, the W3C spec + // treats them as 'conference' state. When the state of a participating + // call object is changed, it is tracked separately, and the conference + // call state inherits the shadow state of participating calls. + call->saved_state = state; + + if (state != conf->state) { + bool change_conf_state = false; + if (state == kCallStateActive) { // at least one active participant + change_conf_state = true; + } else if (state == kCallStateHeld) { + // all participating calls must be held in order to change + // the state of the conference calls + change_conf_state = true; + for (auto p : conf->participants) { + if (p && p->state != kCallStateHeld) + change_conf_state = false; + } + } + + if (change_conf_state) { + conf->state = state; + NotifyCallChanged("state", conf); + return nullptr; // no notification for shadow state change + } + } + } + + call->state = state; + return "state"; +} + +// Return the affected JS property name. +const char* TelephonyBackend::UpdateCallProperty(TelephonyCall* call, + const char* key, GVariant* value) { + + if (!strcmp(key, "State")) { + std::string state = g_variant_get_string(value, nullptr); + return UpdateCallState(call, state); + } + + if (!strcmp(key, "StartTime")) { + call->start_time = g_variant_get_string(value, nullptr); + return "startTime"; + } + + if (!strcmp(key, "LineIdentification")) { + call->remote_party = g_variant_get_string(value, nullptr); + return "remoteParty"; + } + + if (!strcmp(key, "Name")) { + call->name = g_variant_get_string(value, nullptr); + return "name"; + } + + if (!strcmp(key, "Multiparty")) { + // this is the only notification from oFono for conference participants + if (!g_variant_get_boolean(value) && call->conference) { + // the call goes back to normal, split from conference + call->conference = nullptr; + call->state = call->saved_state.empty() ? + kCallStateActive : call->saved_state; + call->saved_state.clear(); + } + // when 'value' is true, createConference() has already updated this call + // only need to trigger the state change notification from here + return "state"; + } + + if (!strcmp(key, "Emergency")) { + call->emergency = g_variant_get_boolean(value); + return "emergency"; + } +} + +bool TelephonyBackend::InitCall(TelephonyCall* call, const std::string& cid, + GVariantIter* props) { + + if (cid.empty() || !props) { + return false; + } + + call->id = cid; + call->protocol = "gsm"; + call->duration = 0; + call->state = kCallStateInit; + char* key = nullptr; + GVariant* value = nullptr; + while (g_variant_iter_next(props, "{sv}", &key, &value)) { + UpdateCallProperty(call, key, value); + } + g_variant_unref(value); + + if (!call->service) { + // extract service id from call id + // format is /voicecall + size_t i = cid.find("voicecall"); + if (i <= 0) + return 0; + + std::string sid = cid.substr(0, i - 1); + call->service = FindService(sid); + if (!call->service) { + if (!QueryServices()) + return false; + call->service = FindService(sid); + if (!call->service) + return false; + } + } + + return true; +} + +void TelephonyBackend::UpdateCall(TelephonyCall* call) { + GError* err = nullptr; + GVariant* res = SyncDBusCall(IdToDbus(call->id).c_str(), kDbusOfonoVoiceCall, + "GetProperties", nullptr, &err); + if (err) { + LOG_ERR("UpdateCall: " << err->message); + return; + } + + GVariantIter* props = nullptr; + g_variant_get(res, "(a{sv})", &props); + InitCall(call, call->id, props); + g_variant_iter_free(props); + g_variant_unref(res); +} + +void TelephonyBackend::LogServices() { + std::cout << "\nServices:"; + for (int i = 0; i < services_.size(); i++) + LogService(services_[i]); +} + +void TelephonyBackend::LogCalls() { + std::cout << "\nCalls:"; + for (int i = 0; i < calls_.size(); i++) + LogCall(calls_[i]); +} + + +////////////////////////////////////////////////////////////////////////////// +// TelephonyBackend, service related notifications +/////////////////////////////////////////////////////////////////////////////// + +// In most cases there is only one or two services. +TelephonyService* TelephonyBackend::FindService(const std::string& id) { + for (auto s : services_) { + if (s && s->id == id) { + return s; + } + } + return nullptr; +} + +void TelephonyBackend::OnModemAdded(const gchar* obj, GVariant* parameters) { + LOG_DBG("OnModemAdded: " << std::string(obj)); + const char* path = nullptr; + GVariantIter* props = nullptr; + g_variant_get(parameters, "(oa{sv})", &path, &props); + if (!path) { + LOG_ERR("OnModemAdded: invalid object."); + return; + } + + std::string sid = IdFromDbus(path); + RemoveService(FindService(sid)); + TelephonyService* service = new TelephonyService(); + InitService(service, sid, props); + + services_.push_back(service); + NotifyServiceAdded(service); + g_variant_iter_free(props); +} + +void TelephonyBackend::OnModemChanged(const gchar* obj, GVariant* parameters) { + LOG_DBG("OnModemChanged: " << std::string(obj)); + TelephonyService* service = FindService(IdFromDbus(obj)); + if (!service) { + LOG_ERR("OnModemChanged: could not find service."); + return; + } + + const gchar* key = nullptr; + GVariant* value = nullptr; + g_variant_get(parameters, "(sv)", &key, &value); + key = UpdateServiceProperty(service, key, value); + if (key) + NotifyServiceChanged(key, service); + g_variant_unref(value); +} + +void TelephonyBackend::OnModemRemoved(const gchar* obj, GVariant* parameters) { + LOG_DBG("OnModemRemoved: " << std::string(obj)); + char* path = nullptr; + g_variant_get(parameters, "(o)", &path); + TelephonyService* service = FindService(IdFromDbus(path)); + + if (!service) { + LOG_ERR("OnModemRemoved: could not find service."); + return; + } + + NotifyServiceRemoved(service); + RemoveService(service); +} + +void TelephonyBackend::NotifyDefaultServiceChanged() { + LOG_DBG("NotifyDefaultServiceChanged"); + picojson::object js, notif; + notif["cmd"] = picojson::value("defaultServiceChanged"); + notif["service"] = default_service_ ? + picojson::value(ToJson(default_service_, js)) : + picojson::value(); + instance_->SendNotification(picojson::value(notif)); +} + +void TelephonyBackend::NotifyServiceAdded(TelephonyService* service) { + LOG_DBG("NotifyServiceAdded"); + picojson::object js, notif; + notif["cmd"] = picojson::value("serviceAdded"); + notif["service"] = service ? + picojson::value(ToJson(service, js)) : + picojson::value(); + instance_->SendNotification(picojson::value(notif)); +} + +void TelephonyBackend::NotifyServiceChanged(const char* key, + TelephonyService* service) { + LOG_DBG("NotifyServiceChanged: " << std::string(key)); + picojson::array changedProps; + changedProps.push_back(picojson::value(key)); + picojson::object js, notif; + notif["cmd"] = picojson::value("serviceChanged"); + notif["changedProperties"] = picojson::value(changedProps); + notif["service"] = service ? + picojson::value(ToJson(service, js)) : + picojson::value(); + instance_->SendNotification(picojson::value(notif)); +} + +void TelephonyBackend::NotifyServiceRemoved(TelephonyService* service) { + LOG_DBG("NotifyServiceRemoved"); + picojson::object js, notif; + notif["cmd"] = picojson::value("serviceRemoved"); + notif["service"] = service ? + picojson::value(ToJson(service, js)) : + picojson::value(); + instance_->SendNotification(picojson::value(notif)); +} + +void TelephonyBackend::RemoveService(TelephonyService* service) { + for (auto iter = services_.begin(); iter != services_.end();) { + if (*iter == service) { + delete *iter; + iter = services_.erase(iter); + } + } +} + +void TelephonyBackend::OnCallManagerChanged(const gchar* obj, + GVariant* parameters) { + // 'parameters' contains emergency numbers list a(s) + LOG_DBG("OnCallManagerChanged"); + GVariantIter* prop_iter = nullptr; + GVariant* value = nullptr; + picojson::value::array results; + g_variant_get(parameters, "(as)", &prop_iter); + + while (g_variant_iter_next(prop_iter, "s", &value)) { + const gchar* num = g_variant_get_string(value, nullptr); + results.push_back(picojson::value(num)); + } + g_variant_iter_free(prop_iter); + + picojson::object notif; + notif["cmd"] = picojson::value("emergencyNumbersChanged"); + notif["returnValue"] = picojson::value(results); + instance_->SendNotification(picojson::value(notif)); +} + +////////////////////////////////////////////////////////////////////////////// +// TelephonyBackend, call related notifications +/////////////////////////////////////////////////////////////////////////////// + +// In most cases there is only one or two calls. +TelephonyCall* TelephonyBackend::FindCall(const std::string& id) { + for (auto call : calls_) { + if (call && (call->id == id)) + return call; + } + return nullptr; +} + +void TelephonyBackend::OnCallAdded(const gchar* obj, GVariant* parameters) { + std::string sid = IdFromDbus(obj); + TelephonyService* service = FindService(sid); + + if (!service) { + QueryServices(); + service = FindService(sid); + if (!service) { + LOG_ERR("OnCallAdded: invalid service id = " << sid); + return; + } + } + + char* path = nullptr; + GVariantIter* props = nullptr; + g_variant_get(parameters, "(oa{sv})", &path, &props); + if (!path) { + LOG_ERR("OnCallAdded: invalid object path."); + return; + } + + std::string cid = IdFromDbus(path); + TelephonyCall* call = FindCall(cid); + if (!call) { + call = new TelephonyCall(service); + InitCall(call, cid, props); + calls_.push_back(call); + } else { + LOG_ERR("OnCallAdded: duplicate call id found."); + call->service = service; + InitCall(call, cid, props); + } + + NotifyCallAdded(call); + LOG_DBG("OnCallAdded: " << call->id); + g_variant_iter_free(props); +} + +void TelephonyBackend::OnCallChanged(const gchar* obj, GVariant* parameters) { + TelephonyCall* call = FindCall(IdFromDbus(obj)); + if (!call) { + LOG_ERR("OnCallChanged: invalid object."); + return; + } + + const gchar* key = nullptr; + GVariant* value = nullptr; + g_variant_get(parameters, "(sv)", &key, &value); + key = UpdateCallProperty(call, key, value); + g_variant_unref(value); + if (!key) + return; + + NotifyCallChanged(key, call); + LOG_DBG("OnCallChanged: " << call->id << "[" << key << "]"); +} + +void TelephonyBackend::OnCallDisconnectReason(const gchar* obj, + GVariant* parameters) { + LOG_DBG("OnCallDisconnectReason: " << std::string(obj)); + TelephonyCall* call = FindCall(IdFromDbus(obj)); + + if (!call) { + LOG_ERR("OnCallDisconnectReason: invalid object."); + return; + } + + gchar* result = nullptr; + g_variant_get(parameters, "(s)", &result); + call->state_reason = result; + NotifyCallChanged("stateReason", call); + g_free(result); +} + +void TelephonyBackend::OnCallRemoved(const gchar* obj, GVariant* parameters) { + LOG_DBG("OnCallRemoved: " << std::string(obj)); + char* path = nullptr; + g_variant_get(parameters, "(o)", &path); + TelephonyCall* call = FindCall(IdFromDbus(path)); + + if (!call) { + LOG_ERR("OnCallRemoved: could not find call."); + return; + } + + RemoveCall(call); + // When the last call is removed, the conf call and all others are purged. + if (calls_.empty()) { + for (auto iter = removed_calls_.begin(); iter != removed_calls_.end();) { + delete *iter; + iter = removed_calls_.erase(iter); + } + } +} + +void TelephonyBackend::CheckActiveCall(TelephonyCall* call) { + if (!call) { // call has been removed and need to find the next active call + bool changed = false; + for (auto c : calls_) { + if (c && c->state == kCallStateActive && + (!c->conference || c->id == c->conference->id)) { + active_call_ = c; + changed = true; + } + } + + if (!changed) + active_call_ = nullptr; + } else if (active_call_ && call->id == active_call_->id && + call->state != kCallStateActive) { // stop being active + active_call_ = nullptr; + } else if (active_call_ == call || call->state != kCallStateActive || + call->conference && call->conference->id != call->id) { + // either no change, or not active, or participant in a conference + // participants in a conference won't be active_call_, only the conf call + return; + } else { // an active [conf] call different from the current active_call_ + active_call_ = call; + } + + LOG_DBG("CheckActiveCall: active call set to " << + (active_call_ ? active_call_->id : "none.")); + picojson::object js, notif; + notif["cmd"] = picojson::value("activeCallChanged"); + notif["call"] = active_call_ ? + picojson::value(ToJson(active_call_, js)) : + picojson::value(); + instance_->SendNotification(picojson::value(notif)); +} + +void TelephonyBackend::NotifyCallAdded(TelephonyCall* call) { + LOG_DBG("NotifyCallAdded: " << call->id); + picojson::object js, notif; + notif["cmd"] = picojson::value("callAdded"); + notif["call"] = call ? + picojson::value(ToJson(call, js)) : + picojson::value(); + instance_->SendNotification(picojson::value(notif)); +} + +void TelephonyBackend::NotifyCallChanged(const char* key, TelephonyCall* call) { + LOG_DBG("NotifyCallChanged: " << call->id << "[" << std::string(key) << "]"); + picojson::array changed_props; // for compatibility with other backends + changed_props.push_back(picojson::value(key)); + + picojson::object js, notif; + bool statechange = (key == "state"); + notif["cmd"] = statechange ? + picojson::value("callStateChanged") : + picojson::value("callChanged"); + notif["changedProperties"] = picojson::value(changed_props); + notif["call"] = call ? + picojson::value(ToJson(call, js)) : + picojson::value(); + + instance_->SendNotification(picojson::value(notif)); + + if (statechange) + CheckActiveCall(call); +} + +void TelephonyBackend::NotifyCallRemoved(TelephonyCall* call) { + LOG_DBG("NotifyCallRemoved: " << call->id); + picojson::object js, notif; + notif["cmd"] = picojson::value("callRemoved"); + notif["call"] = call ? + picojson::value(ToJson(call, js)) : + picojson::value(); + instance_->SendNotification(picojson::value(notif)); +} + +void TelephonyBackend::RemoveCall(TelephonyCall* call) { + if (!call) { + LOG_ERR(("RemoveCall: null call")); + return; + } + + LOG_DBG("RemoveCall: removing " << call->id); + // remove from the global call list, and references from other calls + for (auto iter = calls_.begin(); iter != calls_.end();) { + TelephonyCall* c = *iter; + if (c != call && c == call->conference) { + RemoveParticipant(c, call); + LOG_DBG("RemoveCall: removed from conference " << c->id); + call->conference = nullptr; + } + + if (c->id == call->id) { + LOG_DBG("RemoveCall: removed from call list: " << c->id); + iter = calls_.erase(iter); + removed_calls_.push_back(call); + NotifyCallRemoved(call); + CheckActiveCall(); + } else { + ++iter; + } + } +} + +////////////////////////////////////////////////////////////////////////////// +// TelephonyBackend, API calls +/////////////////////////////////////////////////////////////////////////////// + +void TelephonyBackend::GetServices(const picojson::value& msg) { + LOG_DBG("GetServices"); + if (!QueryServices()) { + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + picojson::value::array results; + for (int i = 0; i < services_.size(); i++) { + if (services_[i]) { + picojson::object js; + ToJson(services_[i], js); + results.push_back(picojson::value(js)); + } + } + + instance_->SendSuccessReply(msg, picojson::value(results)); +} + +bool TelephonyBackend::QueryServices() { + LOG_DBG("QueryServices"); + GError* err = nullptr; + GVariant* res = + SyncDBusCall("/", kDbusOfonoModemManager, "GetModems", nullptr, &err); + + if (err) { + LOG_ERR("QueryServices: " << err->message); + g_error_free(err); + return false; + } + + if (!res) + return true; // no services found, that's OK + + GVariantIter* modems; + g_variant_get(res, "(a(oa{sv}))", &modems); + char* path; + GVariantIter* props; + while (g_variant_iter_next(modems, "(oa{sv})", &path, &props)) { + std::string sid = IdFromDbus(path); + TelephonyService* service = FindService(sid); + bool found = !!service; + if (!service) + service = new TelephonyService(); + InitService(service, sid, props); + if (!found) + services_.push_back(service); + } + g_variant_iter_free(props); + g_variant_iter_free(modems); + g_variant_unref(res); + + // set a default service + if (!default_service_ && services_.size() > 0) { + default_service_ = services_[0]; + NotifyDefaultServiceChanged(); + } + + return true; +} + +void TelephonyBackend::SetServiceEnabled(const picojson::value& msg) { + TelephonyService* service = FindService(msg.get("serviceId").to_str()); + if (!service) { + LOG_ERR("TelephonyService not found."); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + if (SetModemEnabled(service, msg.get("enabled").get())) + instance_->SendSuccessReply(msg); + else + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); +} + +bool TelephonyBackend::SetModemEnabled(TelephonyService* service, + bool enabled) { + LOG_DBG("SetModemEnabled: " << service->id << " : " << enabled); + std::string path = IdToDbus(service->id); + + GError* err = nullptr; + SyncDBusCall(path.c_str(), kDbusOfonoModemManager, "SetProperty", + g_variant_new("(sv)", "Powered", g_variant_new("b", enabled)), &err); + + if (!err) + SyncDBusCall(path.c_str(), kDbusOfonoModemManager, "SetProperty", + g_variant_new("(sv)", "Online", g_variant_new("b", enabled)), &err); + + if (err) { + LOG_ERR("SetModemEnabled: " << err->message); + g_error_free(err); + return false; + } + + return true; +} + +void TelephonyBackend::SetDefaultService(const picojson::value& msg) { + TelephonyService* service = FindService(msg.get("serviceId").to_str()); + + if (!service) { + LOG_ERR("TelephonyService not found."); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + LOG_DBG("SetDefaultService: " << service->id); + if (!service->online) + SetModemEnabled(service, true); + + default_service_ = service; + // No need to also notify, the Promise will confirm the action. + instance_->SendSuccessReply(msg); +} + +void TelephonyBackend::GetDefaultService(const picojson::value& msg) { + std::string dsi = (default_service_ ? default_service_->id : ""); + instance_->SendSuccessReply(msg, picojson::value(dsi)); +} + +// Get the calls from all services. +void TelephonyBackend::GetCalls(const picojson::value& msg) { + LOG_DBG("GetCalls"); + for (int i = 0; i < services_.size(); i++) { + if (!services_[i]->online) + continue; + + std::string path = IdToDbus(services_[i]->id); + GError* err = nullptr; + GVariant* res = SyncDBusCall(path.c_str(), kDbusOfonoVoiceCallManager, + "GetCalls", nullptr, &err); + + if (err) { + LOG_ERR("GetCalls: " << err->message); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + g_error_free(err); + return; + } + + if (!res) { + LOG_ERR("Results from 'GetCalls' method is nullptr"); + instance_->SendSuccessReply(msg, picojson::value("")); + return; + } + + GVariantIter* calls; + g_variant_get(res, "(a(oa{sv}))", &calls); + + char* obj; + GVariantIter* props; + picojson::value::array results; + while (g_variant_iter_next(calls, "(oa{sv})", &obj, &props)) { + std::string cid = IdFromDbus(obj); + TelephonyCall* call = FindCall(cid); + if (!call) { + call = new TelephonyCall(services_[i]); + InitCall(call, cid, props); + calls_.push_back(call); + } + } + + for (auto c : calls_) { + picojson::object js; + ToJson(c, js); + results.push_back(picojson::value(js)); + } + + instance_->SendSuccessReply(msg, picojson::value(results)); + g_variant_iter_free(calls); + g_variant_unref(res); + } +} + +void TelephonyBackend::DialCall(const picojson::value& msg) { + TelephonyService* service = FindService(msg.get("serviceId").to_str()); + + if (!service) { + // the app has called withouth querying services, expecting default service + if (!default_service_) { + if (!QueryServices() && !default_service_) { // query affects default + LOG_ERR("Dial: unable to find telephony services."); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + } + service = default_service_; + } + + std::string remote = msg.get("remoteParty").to_str(); + if (!CheckRemoteParty(remote)) { + LOG_ERR("Dial: invalid remote party " << remote); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + LOG_DBG("Dial " << remote); + GError* err = nullptr; + std::string sid = IdToDbus(service->id); + GVariant* res = SyncDBusCall(sid.c_str(), + kDbusOfonoVoiceCallManager, + "Dial", + g_variant_new("(ss)", + remote.c_str(), + msg.get("hideCallerId").get() ? + "enabled" : ""), + &err); + + if (err) { + LOG_ERR("Dial: " << err->message); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + g_error_free(err); + return; + } + + if (!res) { + LOG_ERR("Dial: nullptr call object."); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + char* obj = nullptr; + g_variant_get(res, "(o)", &obj); + std::string cid = IdFromDbus(obj); + TelephonyCall* call = FindCall(cid); + + // The call should already be there, added by OnCallAdded + if (!call) { + call = new TelephonyCall(service); + call->id = cid; + UpdateCall(call); + } + + picojson::object js; + ToJson(call, js); + instance_->SendSuccessReply(msg, picojson::value(js)); + g_variant_unref(res); +} + +void TelephonyBackend::DeflectCall(const picojson::value& msg) { + std::string cid = msg.get("callId").to_str(); + TelephonyCall* call = FindCall(cid); + + if (!call) { + LOG_ERR("Accept: invalid call id " << cid); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + std::string remote = msg.get("remoteParty").to_str(); + LOG_DBG("Deflect " << remote); + + if (!CheckRemoteParty(remote)) { + LOG_ERR("Deflect: invalid remote party " << remote); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + if (call->state != kCallStateIncoming && + call->state != kCallStateWaiting) { + LOG_ERR("Deflect: invalid call state: " << call->state); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + GError* err = nullptr; + SyncDBusCall(IdToDbus(call->id).c_str(), kDbusOfonoVoiceCall, "Deflect", + g_variant_new("s", remote.c_str()), &err); + + if (err) { + LOG_ERR("Deflect: " << err->message); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + g_error_free(err); + return; + } + + instance_->SendSuccessReply(msg); +} + +void TelephonyBackend::AcceptCall(const picojson::value& msg) { + std::string cid = msg.get("callId").to_str(); + TelephonyCall* call = FindCall(cid); + + if (!call) { + LOG_ERR("Accept: invalid call id " << cid); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + LOG_DBG("Accept " << call->id); + GError* err = nullptr; + std::string path = IdToDbus(call->id); + SyncDBusCall(path.c_str(), kDbusOfonoVoiceCall, "Answer", nullptr, &err); + + if (err) { + LOG_ERR("Accept: " << err->message); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + g_error_free(err); + return; + } + + instance_->SendSuccessReply(msg); +} + +bool TelephonyBackend::HangupCall(std::string& call_id) { + LOG_DBG("Disconnect " << call_id); + GError* err = nullptr; + std::string path = IdToDbus(call_id); + SyncDBusCall(path.c_str(), kDbusOfonoVoiceCall, "Hangup", nullptr, &err); + + if (err) { + LOG_ERR("Disconnect: " << err->message); + g_error_free(err); + return false; + } + + return true; +} + +void TelephonyBackend::HangupAllCalls(std::string& service_id) { + LOG_DBG("Disconnect all calls on service " << service_id); + GError* err = nullptr; + std::string path = IdToDbus(service_id); + SyncDBusCall(path.c_str(), kDbusOfonoVoiceCallManager, "HangupAll", nullptr, + &err); + + if (err) { + LOG_ERR("Disconnect all: " << err->message); + g_error_free(err); + } +} + +void TelephonyBackend::DisconnectCall(const picojson::value& msg) { + std::string cid = msg.get("callId").to_str(); + TelephonyCall* call = FindCall(cid); + + if (!call) { + LOG_ERR("Disconnect: invalid call id " << cid); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + if (!call->conference) { + if (HangupCall(cid)) + instance_->SendSuccessReply(msg); + else + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + UpdateDuration(call); + call->state = kCallStateDisconnected; + call->state_reason = "local"; + + bool failed = false; + for (auto c : call->participants) { + if (!HangupCall(c->id)) // causes call state updates + failed = true; + } + + if (failed) { + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + HangupAllCalls(call->service->id); // avoid spurious calls + return; + } + + RemoveCall(call); + instance_->SendSuccessReply(msg); +} + +void TelephonyBackend::HoldCall(const picojson::value& msg) { + std::string cid = msg.get("callId").to_str(); + TelephonyCall* call = FindCall(cid); + + if (!call || !call->service) { + LOG_ERR("Hold: invalid call id " << cid); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + LOG_DBG("Hold " << call->id); + // oFono has SwapCalls, and HoldAndAnswer. Find out which one to use. + const gchar* method = nullptr; + // cannot hold held, dialing, alerting, and disconnected calls + if (call->state != kCallStateIncoming && + call->state != kCallStateActive && + call->state != kCallStateWaiting) { + LOG_ERR("Hold: invalid call state: " << call->state); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + if (call->state == kCallStateActive) { + // check if there are waiting calls + for (int i = 0; i < calls_.size(); i++) { + if (calls_[i]->state == kCallStateWaiting) { + method = "HoldAndAnswer"; + break; + } + } + } + + if (!method) + method = "SwapCalls"; + + GError* err = nullptr; + std::string path = IdToDbus(call->service->id); + SyncDBusCall(path.c_str(), kDbusOfonoVoiceCallManager, method, nullptr, &err); + + if (err) { + LOG_ERR("Hold: " << err->message); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + g_error_free(err); + return; + } + + instance_->SendSuccessReply(msg); +} + +void TelephonyBackend::ResumeCall(const picojson::value& msg) { + std::string cid = msg.get("callId").to_str(); + TelephonyCall* call = FindCall(cid); + + if (!call) { + LOG_ERR("Resume: invalid call id"); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + LOG_DBG("Resume " << call->id); + if (call->state != kCallStateHeld) { + LOG_ERR("Resume: invalid call state '" << call->state); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + GError* err = nullptr; + std::string path = IdToDbus(call->service->id); + SyncDBusCall(path.c_str(), kDbusOfonoVoiceCallManager, "SwapCalls", + nullptr, &err); + + if (err) { + LOG_ERR("Resume: " << err->message); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + g_error_free(err); + return; + } + + instance_->SendSuccessReply(msg); +} + +void TelephonyBackend::TransferCall(const picojson::value& msg) { + if (!default_service_) { + LOG_ERR("Transfer: no default service"); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + LOG_DBG("Transfer: " << default_service_->id); + GError* err = nullptr; + std::string path = IdToDbus(default_service_->id); + SyncDBusCall(path.c_str(), kDbusOfonoVoiceCallManager, "Transfer", + nullptr, &err); + + if (err) { + LOG_ERR("Transfer: " << err->message); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + g_error_free(err); + return; + } + + instance_->SendSuccessReply(msg); +} + +void TelephonyBackend::CreateConference(const picojson::value& msg) { + static uint64_t conference_id_; + TelephonyService* service = active_call_? + active_call_->service : + default_service_; + + if (!service) { + LOG_ERR("CreateConference: unable to find the telephony service."); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + LOG_DBG("CreateConference: " << service->id); + GError* err = nullptr; + std::string modem_path = IdToDbus(service->id); + GVariant* res = SyncDBusCall(modem_path.c_str(), kDbusOfonoVoiceCallManager, + "CreateMultiparty", nullptr, &err); + + const time_t start_time = time(nullptr); + if (err) { + LOG_ERR("CreateConference: " << err->message); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + g_error_free(err); + return; + } + + TelephonyCall* conf = new TelephonyCall(service); + + // oFono returns the new array with the participating call object paths. + // Conference control calls should not be logged, but play safe for clients + // and record start time for conference. + struct tm tm_s = {0}; + char timebuf[40]; + strftime(timebuf, sizeof(timebuf), "%Y-%m-%dT%H:%M:%S%z", + localtime_r(&start_time, &tm_s)); + conf->start_time = std::string(timebuf); + + conference_id_++; + std::stringstream ss; + ss << "conference-" << conference_id_; + conf->id = ss.str(); + + conf->conference = conf; + conf->duration = 0; + conf->state = kCallStateActive; + + GVariantIter* calls; + g_variant_get(res, "(ao)", &calls); + + char* obj = nullptr; + while (g_variant_iter_next(calls, "o", &obj)) { + std::string call_id = IdFromDbus(obj); + TelephonyCall* call = FindCall(call_id); + + if (!call) { // unlikely + LOG_ERR("CreateConference: could not find call with id" << call_id); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + g_variant_iter_free(calls); + RemoveCall(conf); // will not cause notification, just cleanup + return; + } + + call->conference = conf; + call->saved_state = call->state; + call->state = kCallStateConference; // hangup/hold/split can change state + conf->participants.push_back(call); + } + g_variant_iter_free(calls); + + calls_.push_back(conf); + LOG_DBG("Conference call added: " << conf->id); + + picojson::object js; + ToJson(conf, js); + // No need to NotifyCallAdded(conf), since the Promise returns it. + instance_->SendSuccessReply(msg, picojson::value(js)); + CheckActiveCall(conf); + // no need to send notification about the changed participating calls + // other than a state change event; + // oFono will send CallChanged event with 'Multiparty' property updated + // to each participating call object, handle that in UpdateCallProperty + for (auto c : conf->participants) + NotifyCallChanged("state", c); +} + +void TelephonyBackend::GetConferenceParticipants(const picojson::value& msg) { + // This can also be implemented purely in JavaScript. + TelephonyCall* conf = FindCall(msg.get("conferenceId").to_str()); + + if (!conf || !conf->conference) { + LOG_ERR("GetParticipants: unable to find the conference call."); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + LOG_DBG("GetParticipants: " << conf->id); + picojson::value::array array; + for (int i = 0; i < conf->participants.size(); i++) { + TelephonyCall* call = conf->participants[i]; + if (call) + array.push_back(picojson::value(call->id)); + } + + instance_->SendSuccessReply(msg, picojson::value(array)); +} + +void TelephonyBackend::SplitCall(const picojson::value& msg) { + std::string cid = msg.get("callId").to_str(); + TelephonyCall* call = FindCall(cid); + + if (!call) { + LOG_ERR("Split: invalid call id " << cid); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + if (!call->conference) { + LOG_ERR("Split: not in a multiparty call."); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + TelephonyCall* conf = call->conference; + if (!conf) { + LOG_ERR("Split: invalid conference id."); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + LOG_DBG("Split: " << call->id << " from: " << conf->id); + GError* err = nullptr; + std::string path = IdToDbus(call->service->id); + GVariant* res = SyncDBusCall(path.c_str(), kDbusOfonoVoiceCallManager, + "PrivateChat", g_variant_new("s", call->id.c_str()), &err); + + if (err) { + LOG_ERR("Split: " << err->message); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + g_error_free(err); + return; + } + + // oFono will notify the state changes to each (now held) participating call + // oFono returns the new array with the rest of calls, ignore it + RemoveParticipant(conf, call); + instance_->SendSuccessReply(msg); + g_variant_unref(res); +} + +void TelephonyBackend::RemoveParticipant(TelephonyCall* conf, + TelephonyCall* call) { + if (!conf || !call) + return; + + int size = conf->participants.size(); + for (auto iter = conf->participants.begin(); + iter != conf->participants.end();) { + if ((*iter)->id == call->id) { // don't delete, just erase + iter = conf->participants.erase(iter); + break; + } else { + ++iter; + } + } + + // If there have been only 2 calls in the conference before the split, + // then remove the conf call object since we'll have two normal calls. + if (size == 2) { + RemoveCall(conf); + } +} + +void TelephonyBackend::SendTones(const picojson::value& msg) { + // serviceId, tones + std::string sid = msg.get("serviceId").to_str(); + TelephonyService* service = !sid.empty() ? FindService(sid) : nullptr; + + if (!service && !default_service_) { + LOG_ERR("SendTones: unable to find telephony service."); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + service = default_service_; + + std::string tones = msg.get("tones").to_str(); + if (tones.empty()) { + LOG_ERR("SendTones: empty sequence."); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + LOG_DBG("SendTones: " << tones << " to: " << sid); + GError* err = nullptr; + std::string path = IdToDbus(default_service_->id); + SyncDBusCall(path.c_str(), kDbusOfonoVoiceCallManager, "SendTones", + g_variant_new("(s)", tones.c_str()), &err); + + if (err) { + LOG_ERR("SendTones: " << err->message); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + g_error_free(err); + return; + } + + instance_->SendSuccessReply(msg); +} + +void TelephonyBackend::StartTone(const picojson::value& msg) { + instance_->SendErrorReply(msg, NOT_SUPPORTED_ERR); +} + +void TelephonyBackend::StopTone(const picojson::value& msg) { + instance_->SendErrorReply(msg, NOT_SUPPORTED_ERR); +} + +void TelephonyBackend::GetEmergencyNumbers(const picojson::value& msg) { + if (!default_service_ && (!QueryServices() || !default_service_)) { + LOG_ERR("GetEmergencyNumbers: no service"); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + return; + } + + GError* err = nullptr; + std::string path = IdToDbus(default_service_->id); + GVariant* res = SyncDBusCall(path.c_str(), kDbusOfonoVoiceCallManager, + "GetProperties", nullptr, &err); + + if (err) { + LOG_ERR("GetEmergencyNumbers: " << err->message); + instance_->SendErrorReply(msg, NO_MODIFICATION_ALLOWED_ERR); + g_error_free(err); + return; + } + + if (!res) { + LOG_ERR("Results from 'GetEmergencyNumbers' method is nullptr"); + instance_->SendSuccessReply(msg, picojson::value("")); + return; + } + + GVariantIter* prop_iter = nullptr; + g_variant_get(res, "(a{sv})", &prop_iter); + + gchar* key = nullptr; + GVariant* value = nullptr; + picojson::value::array results; + while (g_variant_iter_next(prop_iter, "{sv}", &key, &value)) { + if (!strcmp(key, "EmergencyNumbers")) { + gchar* number = nullptr; + GVariantIter* num_iter = nullptr; + g_variant_get(value, "as", &num_iter); + while (g_variant_iter_loop(num_iter, "s", &number)) { + results.push_back(picojson::value(number)); + } + g_variant_iter_free(num_iter); + g_variant_unref(value); + } + } + + instance_->SendSuccessReply(msg, picojson::value(results)); + g_variant_iter_free(prop_iter); + g_variant_unref(res); +} + +void TelephonyBackend::EmergencyDial(const picojson::value& msg) { + // with ofono, need to make a regular call with the emergency number + instance_->SendErrorReply(msg, NOT_SUPPORTED_ERR); +} + +bool TelephonyBackend::CheckRemoteParty(const std::string& address) { + // using the regexp proposed in oFono ./doc/voicecallmanager-api.txt + // but it doesn't work with g++; leaving here for reference + // std::regex phoneNumberRegex("[+]?[0-9*#]{1,80}"); + // return std::regex_match(address, phoneNumberRegex); + char* p = const_cast(address.c_str()); + int len = 80; + if (!p || !*p) + return false; + if (*p == '+') + p++; + for (; *p && --len; p++) { + if ((*p < '0' || *p > '9') && *p != '*' && *p != '#') + return false; + } + return (len > 0); +} + +std::string TelephonyBackend::IdFromDbus(const char* id) { + std::string res(id); + for (int i = 0; i < res.size(); i++) { + if (res[i] == '/') + res[i] = '|'; + } + return res; +} + +std::string TelephonyBackend::IdToDbus(std::string& id) { + std::string res = id; + for (int i = 0; i < res.size(); i++) { + if (res[i] == '|') + res[i] = '/'; + } + return res; +} diff --git a/telephony/telephony_backend_ofono.h b/telephony/telephony_backend_ofono.h new file mode 100644 index 0000000..d14a88c --- /dev/null +++ b/telephony/telephony_backend_ofono.h @@ -0,0 +1,234 @@ +// Copyright (c) 2014 Intel Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef TELEPHONY_TELEPHONY_BACKEND_OFONO_H_ +#define TELEPHONY_TELEPHONY_BACKEND_OFONO_H_ + +#include +#include + +#include +#include + +#include "common/extension.h" +#include "common/picojson.h" +#include "common/utils.h" +#include "telephony/telephony_instance.h" +#include "tizen/tizen.h" + +#define DECL_DBUS_SIGNAL_HANDLER(name) \ + static void name(GDBusConnection* connection, \ + const gchar* sender_name, \ + const gchar* object_path, \ + const gchar* interface_name, \ + const gchar* signal_name, \ + GVariant* parameters, \ + gpointer user_data) + +#define DECL_SIGNAL_HANDLER(name) \ + void name(const gchar* object_path, GVariant* parameters) + +#define _TEL_DBUS_ASYNC 0 // not much gain for async mode in a worker process + +#if _TEL_DBUS_ASYNC + #define DECL_API_METHOD(name) \ + void name(const picojson::value& msg); \ + static void name ## Finished(GObject* source_object, \ + GAsyncResult* res, \ + gpointer user_data) +#else + #define DECL_API_METHOD(name) \ + void name(const picojson::value& msg) +#endif + +/* + * Implementation notes. + * + * Telephony services map to ofono Modem objects (plus subscriber identities). + * The type of Modem objects is "hfp" for phones connected via Bluetooth HFP, + * and "hardware" for internal modem. + * + * This extension identifies services/calls by modem/call DBUS object path. + * The JS shim would need to generate an opaque id out of that and maintain 1:1 + * association, i.e. avoid having service objects with different serviceId but + * referring to the same modem path. This implementation exposes the object + * paths, which in turn exposes Bluetooth addresses to the + * client in case of HFP connection. So telephony is in the same privacy class + * as Bluetooth functionality, from fingerprinting point of view. + * + * Since oFono handles conference calls as arrays of call objects, with no + * abstraction for a dedicated conference call object, this implementation + * emulates it, and manages call id's for conference calls in a separate + * namespace, i.e. they don't correspond to call object paths. Also, + * oFono keeps all participating calls in 'active' state and updates the + * 'Multiparty' property. When participating calls are disconnected until only + * 2 calls remain, the conference call may or may not ne downgraded to normal + * calls. This implementation will handle them as separate normal calls and + * remove the conference call object in this case. When oFono changes one + * participating call's state, the conf call object is checked/updated. + * This is one issue complicating the implementation. + * The other is managing "the active call", which from oFono perspective, + * is not defined. The telephony protocols define which call is active, oFono + * just follows up the states. The W3C spec requires the notion of active call, + * as the one connected to audio resources. + * oFono doesn't provide that information. In this implementation, the active + * call is the last (normal or conference) call which became active. + * + * Since oFono does not handle multiple modems simultaneously, only one modem + * is kept powered on at any given time, so API calls using a different service + * than the default one will first try to power up the other, corresponding + * modem, and then switch back to the default. + */ + +class TelephonyBackend; +class TelephonyService; + +struct TelephonyCall { + explicit TelephonyCall(TelephonyService* s = NULL) + : service(s), conference(NULL), duration(0) {} + ~TelephonyCall() {} + TelephonyService* service; // the service owning the call + std::string id; // call object path or conference call id + std::string remote_party; // same as line_id in oFono (CLIP/COLP) + std::string state; + std::string state_reason; // mainly for disconnect reason + std::string saved_state; // to maintain state during conf call + std::string name; + std::string start_time; + double duration; // computed, in milliseconds + bool emergency; + std::string protocol; // duplicated from TelephonyService + TelephonyCall* conference; // the conference this call is part of + std::vector participants; + // other information from oFono is not handled in this implementation +}; + +struct TelephonyService { + std::string id; // modem object path + std::string name; // displayable name + std::string type; // "hfp" or "hw" + std::string model; + std::string revision; + std::string serial; + std::string protocol; // "gsm" or "cdma" + std::string provider; + bool emergency; + bool powered; + bool online; + bool lockdown; + // for future: std::vector calls; +}; + +class TelephonyBackend { + public: + explicit TelephonyBackend(TelephonyInstance* instance); + ~TelephonyBackend(); + + // implementing the API methods + DECL_API_METHOD(GetServices); + DECL_API_METHOD(SetServiceEnabled); + DECL_API_METHOD(SetDefaultService); + DECL_API_METHOD(GetDefaultService); + DECL_API_METHOD(GetCalls); + DECL_API_METHOD(DialCall); + DECL_API_METHOD(DeflectCall); + DECL_API_METHOD(AcceptCall); + DECL_API_METHOD(DisconnectCall); + DECL_API_METHOD(HoldCall); + DECL_API_METHOD(ResumeCall); + DECL_API_METHOD(TransferCall); + DECL_API_METHOD(CreateConference); + DECL_API_METHOD(GetConferenceParticipants); + DECL_API_METHOD(SplitCall); + DECL_API_METHOD(SendTones); + DECL_API_METHOD(StartTone); + DECL_API_METHOD(StopTone); + DECL_API_METHOD(GetEmergencyNumbers); + DECL_API_METHOD(EmergencyDial); + + bool HangupCall(std::string& call_id); + void HangupAllCalls(std::string& service_id); + + GVariant* SyncDBusCall(const gchar* object, const gchar* interface, + const gchar* method, GVariant* parameters, GError **error); + + void AsyncDBusCall(const gchar* object, const gchar* interface, + const gchar* method, GVariant* parameters, GAsyncReadyCallback callback, + gpointer user_data); + + void SetDBusSignalHandler(const gchar* iface, const gchar* object, + const gchar* name, GDBusSignalCallback cb, void* user_data); + + void EnableSignalHandlers(); + void DisableSignalHandlers(); + + private: + // handling DBUS signals from oFono + DECL_DBUS_SIGNAL_HANDLER(SignalHandler); + DECL_SIGNAL_HANDLER(OnModemAdded); + DECL_SIGNAL_HANDLER(OnModemRemoved); + DECL_SIGNAL_HANDLER(OnModemChanged); + DECL_SIGNAL_HANDLER(OnCallAdded); + DECL_SIGNAL_HANDLER(OnCallRemoved); + DECL_SIGNAL_HANDLER(OnCallManagerChanged); + DECL_SIGNAL_HANDLER(OnCallChanged); + DECL_SIGNAL_HANDLER(OnCallDisconnectReason); + + void NotifyDefaultServiceChanged(); + void NotifyServiceAdded(TelephonyService* service); + void NotifyServiceChanged(const char* key, TelephonyService* service); + void NotifyServiceRemoved(TelephonyService* service); + void NotifyCallAdded(TelephonyCall* call); + void NotifyCallChanged(const char* key, TelephonyCall* call); + void NotifyCallRemoved(TelephonyCall* call); + + bool QueryServices(); + void RemoveService(TelephonyService* service); + bool SetModemEnabled(TelephonyService* service, bool on); + void RemoveCall(TelephonyCall* call); + bool CheckRemoteParty(const std::string& address); + void RemoveParticipant(TelephonyCall* conf, TelephonyCall* call); + + std::string IdFromDbus(const char* id); + std::string IdToDbus(std::string& id); + + TelephonyService* FindService(const std::string& id); + TelephonyCall* FindCall(const std::string& id); + + picojson::object& ToJson(TelephonyCall* call, picojson::object& js); + picojson::object& ToJson(TelephonyService* s, picojson::object& js); + + const char* UpdateCallProperty(TelephonyCall* call, const char* key, + GVariant* value); + const char* UpdateCallState(TelephonyCall* call, std::string& state); + const char* UpdateServiceProperty(TelephonyService* s, const char* key, + GVariant* value); + bool InitCall(TelephonyCall* call, const std::string& path, + GVariantIter* props); + bool InitService(TelephonyService* s, const std::string& path, + GVariantIter* props); + void UpdateCall(TelephonyCall* call); + void UpdateService(TelephonyService* s); + void UpdateDuration(TelephonyCall* call); + void LogCall(TelephonyCall* call); + void LogService(TelephonyService* s); + void LogCalls(); + void LogServices(); + + void CheckActiveCall(TelephonyCall* call = NULL); + + private: + TelephonyInstance* instance_; + std::vector dbus_listeners_; + std::vector services_; + TelephonyService* default_service_; + TelephonyCall* active_call_; // the one which has audio + std::vector calls_; + std::vector removed_calls_; + GCancellable* cancellable_; + + DISALLOW_COPY_AND_ASSIGN(TelephonyBackend); +}; + +#endif // TELEPHONY_TELEPHONY_BACKEND_OFONO_H_ diff --git a/telephony/telephony_extension.cc b/telephony/telephony_extension.cc new file mode 100644 index 0000000..37c7e20 --- /dev/null +++ b/telephony/telephony_extension.cc @@ -0,0 +1,25 @@ +// Copyright (c) 2014 Intel Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "telephony/telephony_extension.h" + +#include +#include "telephony/telephony_instance.h" + +common::Extension* CreateExtension() { + return new TelephonyExtension; +} + +extern const char kSource_telephony_api[]; + +TelephonyExtension::TelephonyExtension() { + SetExtensionName("tizen.telephony"); + SetJavaScriptAPI(kSource_telephony_api); +} + +TelephonyExtension::~TelephonyExtension() {} + +common::Instance* TelephonyExtension::CreateInstance() { + return new TelephonyInstance; +} diff --git a/telephony/telephony_extension.h b/telephony/telephony_extension.h new file mode 100644 index 0000000..b16239f --- /dev/null +++ b/telephony/telephony_extension.h @@ -0,0 +1,20 @@ +// Copyright (c) 2014 Intel Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef TELEPHONY_TELEPHONY_EXTENSION_H_ +#define TELEPHONY_TELEPHONY_EXTENSION_H_ + +#include "common/extension.h" + +class TelephonyExtension : public common::Extension { + public: + TelephonyExtension(); + virtual ~TelephonyExtension(); + + private: + // common::Extension implementation. + virtual common::Instance* CreateInstance(); +}; + +#endif // TELEPHONY_TELEPHONY_EXTENSION_H_ diff --git a/telephony/telephony_instance.cc b/telephony/telephony_instance.cc new file mode 100644 index 0000000..0cdc4ad --- /dev/null +++ b/telephony/telephony_instance.cc @@ -0,0 +1,147 @@ +// Copyright (c) 2014 Intel Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "telephony/telephony_instance.h" + +#include + +#include "common/picojson.h" +#include "telephony/telephony_backend_ofono.h" +#include "tizen/tizen.h" + +namespace { + +const char kCmdGetServices[] = "getServices"; +const char kCmdSetDefaultService[] = "setDefaultService"; +const char kCmdSetServiceEnabled[] = "setServiceEnabled"; +const char kCmdGetCalls[] = "getCalls"; +const char kCmdDial[] = "dial"; +const char kCmdAccept[] = "accept"; +const char kCmdDisconnect[] = "disconnect"; +const char kCmdHold[] = "hold"; +const char kCmdResume[] = "resume"; +const char kCmdDeflect[] = "deflect"; +const char kCmdTransfer[] = "transfer"; +const char kCmdSplit[] = "split"; +const char kCmdSendTones[] = "sendTones"; +const char kCmdStartTone[] = "startTone"; +const char kCmdStopTone[] = "stopTone"; +const char kCmdGetEmergencyNumbers[] = "getEmergencyNumbers"; +const char kCmdEmergencyDial[] = "emergencyDial"; +const char kCmdCreateConference[] = "createConference"; +const char kCmdGetParticipants[] = "getParticipants"; +const char kCmdEnableNotifications[] = "enableNotifications"; +const char kCmdDisableNotifications[] = "disableNotifications"; + +} // namespace + +TelephonyInstance::TelephonyInstance() : backend_(new TelephonyBackend(this)) { +} + +TelephonyInstance::~TelephonyInstance() { + delete backend_; +} + +void TelephonyInstance::HandleMessage(const char* msg) { + picojson::value v; + std::string err; + picojson::parse(v, msg, msg + strlen(msg), &err); + if (!err.empty()) { + std::cerr << "Error: ignoring empty message.\n"; + return; + } + + if (!backend_) { + SendErrorReply(v, NO_MODIFICATION_ALLOWED_ERR, + "Telephony backend not initialized."); + return; + } + + std::string cmd = v.get("cmd").to_str(); + if (cmd == kCmdGetServices) { + backend_->GetServices(v); + } else if (cmd == kCmdSetDefaultService) { + backend_->SetDefaultService(v); + } else if (cmd == kCmdSetServiceEnabled) { + backend_->SetServiceEnabled(v); + } else if (cmd == kCmdEnableNotifications) { + backend_->EnableSignalHandlers(); + } else if (cmd == kCmdDisableNotifications) { + backend_->DisableSignalHandlers(); + } else if (cmd == kCmdGetCalls) { + backend_->GetCalls(v); + } else if (cmd == kCmdDial) { + backend_->DialCall(v); + } else if (cmd == kCmdAccept) { + backend_->AcceptCall(v); + } else if (cmd == kCmdDisconnect) { + backend_->DisconnectCall(v); + } else if (cmd == kCmdHold) { + backend_->HoldCall(v); + } else if (cmd == kCmdResume) { + backend_->ResumeCall(v); + } else if (cmd == kCmdDeflect) { + backend_->DeflectCall(v); + } else if (cmd == kCmdTransfer) { + backend_->TransferCall(v); + } else if (cmd == kCmdSendTones) { + backend_->SendTones(v); + } else if (cmd == kCmdStartTone) { + backend_->StartTone(v); + } else if (cmd == kCmdStopTone) { + backend_->StopTone(v); + } else if (cmd == kCmdGetEmergencyNumbers) { + backend_->GetEmergencyNumbers(v); + } else if (cmd == kCmdEmergencyDial) { + backend_->EmergencyDial(v); + } else if (cmd == kCmdCreateConference) { + backend_->CreateConference(v); + } else if (cmd == kCmdGetParticipants) { + backend_->GetConferenceParticipants(v); + } else if (cmd == kCmdSplit) { + backend_->SplitCall(v); + } else { + std::cout << "Ignoring unknown command: " << cmd; + } +} + +void TelephonyInstance::HandleSyncMessage(const char* message) { +} + +void TelephonyInstance::SendErrorReply(const picojson::value& msg, + const int error_code, const char* error_msg) { + picojson::value::object reply; + reply["promiseId"] = msg.get("promiseId"); + reply["cmd"] = msg.get("cmd"); + reply["isError"] = picojson::value(true); + reply["errorCode"] = picojson::value(static_cast(error_code)); + reply["errorMessage"] = error_msg ? picojson::value(error_msg) : + msg.get("cmd"); + picojson::value v(reply); + PostMessage(v.serialize().c_str()); +} + +void TelephonyInstance::SendSuccessReply(const picojson::value& msg, + const picojson::value& value) { + picojson::value::object reply; + reply["promiseId"] = msg.get("promiseId"); + reply["cmd"] = msg.get("cmd"); + reply["isError"] = picojson::value(false); + reply["returnValue"] = value; + picojson::value v(reply); + PostMessage(v.serialize().c_str()); +} + +void TelephonyInstance::SendSuccessReply(const picojson::value& msg) { + picojson::value::object reply; + reply["promiseId"] = msg.get("promiseId"); + reply["cmd"] = msg.get("cmd"); + reply["isError"] = picojson::value(false); + picojson::value v(reply); + PostMessage(v.serialize().c_str()); +} + +void TelephonyInstance::SendNotification(const picojson::value& msg) { + PostMessage(msg.serialize().c_str()); +} diff --git a/telephony/telephony_instance.h b/telephony/telephony_instance.h new file mode 100644 index 0000000..c3fe5f7 --- /dev/null +++ b/telephony/telephony_instance.h @@ -0,0 +1,45 @@ +// Copyright (c) 2014 Intel Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef TELEPHONY_TELEPHONY_INSTANCE_H_ +#define TELEPHONY_TELEPHONY_INSTANCE_H_ + +#include +#include // NOLINT + +#include "common/extension.h" + +namespace picojson { + +class value; + +} + +class TelephonyBackend; + +class TelephonyInstance : public common::Instance { + friend class TelephonyBackend; + public: + TelephonyInstance(); + virtual ~TelephonyInstance(); + + private: + // common::Instance implementation. + virtual void HandleMessage(const char* msg); + virtual void HandleSyncMessage(const char* msg); + + void SendSuccessReply(const picojson::value& msg); + void SendSuccessReply(const picojson::value& msg, + const picojson::value& value); + void SendErrorReply(const picojson::value& msg, + const int error_code, const char* error_msg = NULL); + void SendNotification(const picojson::value& msg); + + private: + std::thread dbus_thread_; + GMainLoop* dbus_loop_; + TelephonyBackend* backend_; +}; + +#endif // TELEPHONY_TELEPHONY_INSTANCE_H_ diff --git a/telephony/telephony_logging.h b/telephony/telephony_logging.h new file mode 100644 index 0000000..4aeed41 --- /dev/null +++ b/telephony/telephony_logging.h @@ -0,0 +1,16 @@ +// Copyright (c) 2014 Intel Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef TELEPHONY_TELEPHONY_LOGGING_H_ +#define TELEPHONY_TELEPHONY_LOGGING_H_ + +#define LOG_ERR(x) do { std::cout << "[Error] " << x << std::endl; } while (0) + +#ifdef NDEBUG + #define LOG_DBG(x) do {} while (0) +#else + #define LOG_DBG(x) do { std::cout << "[DBG] " << x << std::endl; } while (0) +#endif + +#endif // TELEPHONY_TELEPHONY_LOGGING_H_ diff --git a/tizen-wrt.gyp b/tizen-wrt.gyp index 87bdc1a..4e90ed0 100644 --- a/tizen-wrt.gyp +++ b/tizen-wrt.gyp @@ -44,8 +44,9 @@ [ 'extension_host_os == "ivi"', { 'dependencies': [ 'audiosystem/audiosystem.gyp:*', - 'vehicle/vehicle.gyp:*', 'sso/sso.gyp:*', + 'telephony/telephony.gyp:*', + 'vehicle/vehicle.gyp:*', ], }], ], -- 2.7.4