W3C Telephony implementation
authorZoltan Kis <zoltan.kis@intel.com>
Fri, 19 Sep 2014 19:18:02 +0000 (22:18 +0300)
committerZoltan Kis <zoltan.kis@intel.com>
Thu, 2 Oct 2014 18:21:13 +0000 (21:21 +0300)
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

12 files changed:
examples/telephony_test.html [new file with mode: 0644]
telephony/README.md [new file with mode: 0644]
telephony/telephony.gyp [new file with mode: 0644]
telephony/telephony_api.js [new file with mode: 0644]
telephony/telephony_backend_ofono.cc [new file with mode: 0644]
telephony/telephony_backend_ofono.h [new file with mode: 0644]
telephony/telephony_extension.cc [new file with mode: 0644]
telephony/telephony_extension.h [new file with mode: 0644]
telephony/telephony_instance.cc [new file with mode: 0644]
telephony/telephony_instance.h [new file with mode: 0644]
telephony/telephony_logging.h [new file with mode: 0644]
tizen-wrt.gyp

diff --git a/examples/telephony_test.html b/examples/telephony_test.html
new file mode 100644 (file)
index 0000000..b2bb182
--- /dev/null
@@ -0,0 +1,634 @@
+<!--
+// 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.
+-->
+
+<html>
+<style>
+body {
+    color:black;
+    font-family:verdana;
+    font-size:80%
+}
+h1 {
+    color:blue;
+    font-family:verdana;
+    font-size:150%;
+}
+p {
+    color:black;
+    font-family:verdana;
+    font-size:80%
+}
+</style>
+<h1>Test W3C Telephony API</h1>
+<body>
+  <button id="clearconsole_btn" onclick="clearConsole()">Clear Console Output</button>
+  <button id="add_service_listener_btn" onclick="addServiceListeners()" >
+      Add service listeners</button>
+  <button id="add_call_listeners_btn" onclick="addCallListeners()" >
+      Add call listeners</button>
+  <button id="services_btn"  onclick="getServiceIds()" >Get services</button>
+  <button id="get_dservice_btn" onclick="getDefaultService()">
+      Get default service</button>
+  <br>
+  <button id="active_call_btn"  onclick="getActiveCall()" >
+      Get active call</button>
+  <button id="get_calls_btn"  onclick="getCalls()" >Get all calls </button>
+  <button id="dial_btn"  onclick="dial()" >Dial</button>
+   number: <input type="text" size=20 id="dial_input">
+  <br>
+  <button id="conf_btn"  onclick="createConference()" >
+      Create conference</button>
+  <button id="conf_parties_btn"  onclick="getParticipants()" >
+      Get conference participants</button>
+  <button id="split_btn" onclick="split()">Split</button>
+   call id: <input type="text" size=25 id="split_input">
+  <br>
+  <button id="disconnect_btn" onclick="disconnect()">
+      Disconnect active call</button>
+  <button id="disconnect_all_btn" onclick="hangupAllCalls()">
+      Disconnect all calls</button>
+  <button id="hold_btn" onclick="hold()">Hold active call</button>
+  <button id="resume_btn" onclick="resume()">Resume held call</button>
+  <br>
+  <button id="accept_btn" onclick="accept()">
+      Accept incoming/waiting call</button>
+  <button id="deflect_btn" onclick="deflect()">Deflect incoming/waiting call</button>
+   to number: <input type="text" size=25 id="deflect_input">
+  <br>
+  <button id="transfer_btn" onclick="transfer()">Transfer incoming/waiting call </button>
+  <button id="emerg_nr_btn" onclick="getEmergencyNumbers()">
+      Get emergency numbers</button>
+  <button id="remove_call_listeners_btn" onclick="removeCallListeners()" >
+      Remove call listeners</button>
+  <button id="remove_service_listeners_btn" onclick="removeServiceListeners()" >
+      Remove service listeners</button>
+  <br>
+  <button id="sendtones_btn" onclick="sendTones()">Send Tones</button>
+   [0..9, A..F, p]: <input type="text" size=15 id="tones_input">
+  <button id="starttone_btn" onclick="startTone()">Start Tone</button>
+  <button id="endtone_btn" onclick="endTone()">End Tone</button>
+  <br>
+  Service id: <input type="text" size=30 id="service_id_input">
+  <button id="get_service_btn" onclick="getService()">
+      Show</button>
+  <button id="set_dservice_btn" onclick="setDefaultService()">
+      Set as default</button>
+  <button id="enable_service_btn" onclick="enableService()">
+      Enable</button>
+  <button id="disable_service_btn" onclick="disableService()">
+      Disable</button>
+  <br>
+  <textarea cols=90 rows=22 id="consolelog"></textarea>
+  <br>
+</body>
+
+<script>
+  var output = document.getElementById("consolelog");
+
+  function clearConsole() {
+    output.value = '';
+  }
+
+  function onException(error, text) {  // there should be no exceptions raised
+    var t = text === undefined ? '' : '[' + text + ']';
+    var en = error === undefined ? "" : ':'+ error.name;
+    var em = error === undefined ? "" : ';' + error.message;
+    print('Exception' + t + en + em);
+    return null;
+  }
+
+  function onError(error, text) {
+    var t, en, em;
+    if (error) {
+      en = ':' + error.name;
+      em = ';' + error.message;
+    } else {
+      em = en = '';
+    }
+    if (text)
+      t = '[' + text + ']';
+    else
+      t = null;
+    print('Error' + t + en + em);
+    return null;
+  }
+
+  function printVal(thing, depth) {
+    var out, key;
+    if (thing instanceof Function) {
+      out += '{}';
+    } else if (thing instanceof Array) {
+      out = '[ ';
+      for (key in thing)
+        out += printVal(thing[key], depth + 1) + ', ';
+      out += ']';
+    } else if (thing instanceof Object) {
+      var tabs = '';
+      for (var i = 0; i < depth; i++)
+        tabs += '\t';
+      out = '\n' + tabs + '{';
+      for (key in thing) {
+        if (!(thing[key] instanceof Function))
+          out += '\n' + tabs + '\t' + key + ': ' +
+                 printVal(thing[key], depth + 1);
+      }
+      out += '\n' + tabs + '}';
+    } else {
+      out = thing;
+    }
+    return out;
+  }
+
+  function print(o) {
+    output.value += '\n' + printVal(o, 0);
+    output.scrollTop = output.scrollHeight;
+  }
+
+  function displayEntryList(array) {
+    output.value += '\nList count = ' + array.length + '; ' +
+        printVal(array, 0);
+  }
+
+  function readStringField(name) {
+    var input = document.getElementById(name);
+    if (input)
+      return input.value || null;
+    return onError(null, "Invalid " + name + ": " + id);
+  }
+</script>
+
+<!--
+///////////////////////////////////////////////////////////////////////////////
+// simulation of tizen environment on desktop
+- ->
+<script>
+var extension = function() {};
+extension.messageListeners = [];
+extension.message = "";
+
+extension.postMessage = function(msg) {
+  print("extension.postMessage(" + msg + ")");
+}
+
+extension.setMessageListener = function(fun) {
+  print("extension.setMessageListener(" + fun.toString() + ")");
+  extension.messageListeners.push(fun);
+}
+
+var exports = new Object();
+var tizen = new Object();
+</script>
+
+<script type="text/javascript" src="../tizen/tizen_api.js">
+</script>
+
+<script>
+  for(key in exports) {
+   if (!tizen[key])
+    Object.defineProperty(tizen, key, {
+      configurable: false,
+      writable: false,
+      value: exports[key]
+    });
+  }
+  exports = new Object();
+</script>
+
+<script type="text/javascript" src="telephony_api.js">
+</script>
+
+<script type="text/javascript">
+tizen.telephony = exports;
+</script>
+<!- -
+///////////////////////////////////////////////////////////////////////////////
+// end of simulation of tizen environment
+-->
+
+<script>
+  var telephony = tizen.telephony;
+
+  function getServiceIds() {
+    print("Getting telephony service id's...");
+    tizen.telephony.getServiceIds().then(
+      function(list) {
+        displayEntryList(list);
+      },
+      function(err) {
+        onError(err, 'getServiceIds');
+      });
+  }
+
+  function readService() {
+    var id = readStringField('service_id_input');
+    var s = tizen.telephony.getService(id);
+    if (!s)
+      return onError(null, "No service for id: " + id);
+    return s;
+  }
+
+  function setDefaultService() {
+    var id = readStringField('service_id_input');
+    tizen.telephony.setDefaultServiceId(id).then(
+      function() {
+        print('Default telephony service id set to ' + id);
+      },
+      function(err) {
+        onError(err, 'setDefaultService');
+      });
+  }
+
+  function getDefaultService() {
+    print("Default service id: " + tizen.telephony.defaultServiceId);
+  }
+
+  function getService() {
+    var service = readService();
+    if (service)
+      print('service[' + id + ']: ' + printVal(service));
+  }
+
+  function enableService() {
+    var service = readService();
+    if (service) {
+      service.setEnabled(true).then(
+        function() {
+          print('Enabled service id ' + service.serviceId);
+        },
+        function(err) {
+          onError(err, 'enableService');
+        });
+    }
+  }
+
+  function disableService() {
+    var service = readService();
+    if (service) {
+      service.setEnabled(false).then(
+        function() {
+          print('Disabled service id ' + service.serviceId);
+        },
+        function(err) {
+          onError(err, 'disableService');
+        });
+    }
+  }
+
+  function onServiceAdded(evt) {
+    print("onServiceAdded: " + printVal(evt.service));
+  }
+
+  function onServiceRemoved(evt) {
+    print("onServiceRemoved: " + printVal(evt.service));
+  }
+
+  function onDefaultServiceChanged(evt) {
+    print("onDefaultServiceChanged: " + printVal(evt.service));
+  }
+
+  function onCallAdded(evt) {
+    print("onCallAdded: " + printVal(evt.call));
+  }
+
+  function onCallRemoved(evt) {
+    print("onCallRemoved: " + printVal(evt.call));
+  }
+
+  function onActiveCallChanged(evt) {
+    print("onActiveCallChanged to: " + evt.call ? evt.call.callId : "null");
+  }
+
+  function onCallStateChanged(evt) {
+    print("onCallStateChanged: " + printVal(evt.call));
+  }
+
+  function addServiceListeners() {
+    if (!telephony) {
+      return onError(null, "telephony not supported");
+    }
+    tizen.telephony.addEventListener('serviceadded', onServiceAdded, false);
+    tizen.telephony.addEventListener('serviceremoved', onServiceRemoved, false);
+    tizen.telephony.addEventListener('defaultservicechanged', onDefaultServiceChanged, false);
+    print("Event listeners added for 'serviceadded', 'serviceremoved', 'defaultservicechanged'.");
+  }
+
+  function removeServiceListeners() {
+    if (!telephony) {
+      return onError(null, "telephony not supported");
+    }
+    tizen.telephony.removeEventListener('serviceadded', onServiceAdded, false);
+    tizen.telephony.removeEventListener('serviceremoved', onServiceRemoved, false);
+    tizen.telephony.removeEventListener('defaultservicechanged', onDefaultServiceChanged, false);
+    print("Event listeners removed for 'serviceadded', 'serviceremoved', 'defaultservicechanged'.");
+  }
+
+  function addCallListeners() {
+    if (!telephony) {
+      return onError(null, "telephony not supported");
+    }
+    tizen.telephony.addEventListener('calladded', onCallAdded, false);
+    tizen.telephony.addEventListener('callremoved', onCallRemoved, false);
+    tizen.telephony.addEventListener('activecallchanged', onActiveCallChanged, false);
+    tizen.telephony.addEventListener('callstatechanged', onCallStateChanged, false);
+    print("Event listeners added for 'calladded', 'callremoved', 'activecallchanged', 'callstatechanged'.");
+  }
+
+  function removeCallListeners() {
+    if (!telephony) {
+      return onError(null, "telephony not supported");
+    }
+    tizen.telephony.removeEventListener('calladded', onCallAdded, false);
+    tizen.telephony.removeEventListener('callremoved', onCallRemoved, false);
+    tizen.telephony.removeEventListener('activecallchanged', onActiveCallChanged, false);
+    tizen.telephony.removeEventListener('callstatechanged', onCallStateChanged, false);
+    print("Event listeners added for 'calladded', 'callremoved', 'activecallchanged', 'callstatechanged'.");
+  }
+
+  function getActiveCall() {
+    var ac = tizen.telephony.activeCall;
+    if (!ac)
+      print("No active call");
+    else
+      print("Active call: " + printVal(ac));
+  }
+
+  function getCalls() {
+    print('Getting telephony calls...');
+    tizen.telephony.getCalls().then(
+      function(list) {
+        displayEntryList(list);
+      },
+      function(err) {
+        onError(err, 'getCalls');
+      });
+  }
+
+  function hangupAllCalls() {
+    print("Disconnecting all calls...");
+    tizen.telephony.getCalls().then(
+      function(list) {
+        var found = false;
+        list.forEach(function(call) {
+          if (call.state == 'held' || call.state == 'active') {
+            found = true;
+            call.disconnect().then(
+              function(){
+                print("Call " + call.callId + " disconnected.");
+              },
+              function(err) {
+                onError(err, 'disconnect');
+              });
+          }
+        });
+        if (!found)
+          print('No calls.');
+      },
+      function(err) {
+        onError(err, 'getCalls');
+      });
+  }
+
+  function createConference() {
+    print("Creating conference call...");
+    if (!tizen.telephony.activeCall) {
+      print("No active call.");
+      return;
+    }
+    tizen.telephony.createConference().then(
+      function(confCall) {
+        print('Conference call created: ' + printVal(confCall));
+      },
+      function(err) {
+        onError(err, 'createConference');
+      });
+  }
+
+  function getParticipants() {
+    print("Getting participants of active conference call...");
+    var ac = tizen.telephony.activeCall;
+    if (!ac) {
+      print("No active call.");
+    } else if (!ac.conferenceId) {
+      print("Active call not a conference. Remote party: " + ac.remoteParty);
+      return;
+    }
+    tizen.telephony.getParticipants(id).then(
+      function(list) {
+        print('Conference participant calls: ');
+        displayEntryList(list);
+      },
+      function(err) {
+        onError(err, 'getParticipants');
+      });
+  }
+
+  function split() {
+    var id = readStringField('split_input');
+    print("Splitting call id " + id + ' from its conference call');
+    tizen.telephony.split(id).then(
+      function() {
+        print('Call id ' + id + ' split from conference and activated');
+      },
+      function(err) {
+        onError(err, 'split');
+      });
+  }
+
+  function dial() {
+    var number = readStringField('dial_input');
+    if (number) {
+      print('Dialing ' + number);
+      tizen.telephony.dial(number).then(
+        function() {
+          print('Dialing ' + number + ' successful.');
+        },
+        function(err) {
+          onError(err, 'dial');
+        });
+    }
+  }
+
+  function accept() {
+    print("Accepting incoming/waiting call...");
+    tizen.telephony.getCalls().then(
+      function(list) {
+        var found = false;
+        list.forEach(function(call) {
+          if (call.state == 'incoming' || call.state == 'waiting') {
+            var state = call.state;
+            found = true;
+            call.accept().then(
+              function(){
+                print("Accepted " + state + " call: ");
+                print(call);
+              },
+              function(err) {
+                onError(err, 'accept');
+              });
+          }
+        });
+        if (!found)
+          print("No incoming or waiting calls")
+      },
+      function(err) {
+        onError(err, 'getCalls');
+      });
+  }
+
+  function disconnect() {
+    print("Disconnecting active call...");
+    if (!tizen.telephony.activeCall) {
+      print("No active calls");
+      return;
+    }
+    var id = tizen.telephony.activeCall.callId;
+    tizen.telephony.activeCall.disconnect().then(
+      function() {
+        print('Disconnected call id ' + id);
+      },
+      function(err) {
+        onError(err, 'disconnect');
+      });
+  }
+
+  function hold() {
+    print("Holding active call...");
+    if (!tizen.telephony.activeCall) {
+      print("No active calls");
+      return;
+    }
+    var call = tizen.telephony.activeCall;
+    call.hold().then(
+      function() {
+        print('Held call id ' + call.callId);
+      },
+      function(err) {
+        onError(err, 'hold');
+      });
+  }
+
+  function resume() {
+    print("Resuming held call...");
+    tizen.telephony.getCalls().then(
+      function(list) {
+        var found = false;
+        list.forEach(function(call) {
+          if (call.state == 'held') {
+            found = true;
+            call.resume().then(
+              function(){
+                print("Resumed call: " + call.callId);
+              },
+              function(err) {
+                onError(err, 'resume');
+              });
+          }
+        });
+        if (!found)
+          print("No held calls")
+      },
+      function(err) {
+        onError(err, 'getCalls');
+      });
+  }
+
+  function deflect() {
+    print("Deflecting incoming/waiting call...");
+    var number = readStringField('deflect_input');
+    if (!number)
+      return;
+    tizen.telephony.getCalls().then(
+      function(list) {
+        var found = false;
+        list.forEach(function(call) {
+          if (call.state == 'incoming' || call.state == 'waiting') {
+            var state = call.state;
+            found = true;
+            call.deflect(number).then(
+              function(){
+                print("Deflected " + state + " call: " + call.callId);
+              },
+              function(err) {
+                onError(err, 'deflect');
+              });
+          }
+        });
+        if (!found)
+          print("No incoming or waiting calls")
+      },
+      function(err) {
+        onError(err, 'getCalls');
+      });
+  }
+
+  function transfer() {
+    print("Transfer: joining the active and held calls, then disconnect...");
+    if (!tizen.telephony.activeCall) {
+      print("No active call");
+      return;
+    }
+    // not checking the held calls now, the system will signal error anyway
+    tizen.telephony.transfer().then(
+        function(){
+          print("Transferred " + state + " call: " + call.callId);
+        },
+        function(err) {
+          onError(err, 'transfer');
+        });
+  }
+
+  function sendTones() {
+    var tones = readStringField('tones_input');
+    if (!tones)
+      return;
+    tizen.telephony.sendTones(tones).then(
+      function() {
+        print('Tones sent: ' + tones);
+      },
+      function(err) {
+        onError(err, 'sendTones');
+      });
+  }
+
+  function startTone() {
+    var tones = readStringField('tones_input');
+    if (!tones)
+      return;
+    tizen.telephony.startTone(tones).then(
+      function() {
+        print('Tone started: ' + tones);
+      },
+      function(err) {
+        onError(err, 'startTone');
+      });
+  }
+
+  function stopTone() {
+    var tones = readStringField('tones_input');
+    if (!tones)
+      return;
+    tizen.telephony.stopTone(tones).then(
+      function() {
+        print('Tone stopped: ' + tones);
+      },
+      function(err) {
+        onError(err, 'stopTone');
+      });
+  }
+
+  function getEmergencyNumbers() {
+    tizen.telephony.getEmergencyNumbers().then(
+      function(list) {
+        print('Emergency number list: ');
+        displayEntryList(list);
+      },
+      function(err) {
+        onError(err, 'getEmergencyNumbers');
+      });
+  }
+
+</script>
+</html>
diff --git a/telephony/README.md b/telephony/README.md
new file mode 100644 (file)
index 0000000..29db9d6
--- /dev/null
@@ -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 <device>
+ connect <device>
+ 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 (file)
index 0000000..66d00fb
--- /dev/null
@@ -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 (file)
index 0000000..c5a142f
--- /dev/null
@@ -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 (file)
index 0000000..e75389f
--- /dev/null
@@ -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 <gio/gio.h>
+#include <time.h>
+#include <uuid/uuid.h>
+
+#include <string>
+#include <sstream>
+
+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<TelephonyBackend*>(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 <ofono source>/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 <service_id>/voicecall<xx>
+    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<bool>()))
+    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<bool>() ?
+                                       "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<char*>(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 (file)
index 0000000..d14a88c
--- /dev/null
@@ -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 <gio/gio.h>
+#include <stdint.h>
+
+#include <string>
+#include <vector>
+
+#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<TelephonyCall*> 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<TelephonyCall*> 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<guint> dbus_listeners_;
+  std::vector<TelephonyService*> services_;
+  TelephonyService* default_service_;
+  TelephonyCall* active_call_;  // the one which has audio
+  std::vector<TelephonyCall*> calls_;
+  std::vector<TelephonyCall*> 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 (file)
index 0000000..37c7e20
--- /dev/null
@@ -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 <glib-object.h>
+#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 (file)
index 0000000..b16239f
--- /dev/null
@@ -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 (file)
index 0000000..0cdc4ad
--- /dev/null
@@ -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 <string>
+
+#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<double>(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 (file)
index 0000000..c3fe5f7
--- /dev/null
@@ -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 <glib.h>
+#include <thread>  // 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 (file)
index 0000000..4aeed41
--- /dev/null
@@ -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_
index 87bdc1a..4e90ed0 100644 (file)
@@ -44,8 +44,9 @@
         [ 'extension_host_os == "ivi"', {
           'dependencies': [
             'audiosystem/audiosystem.gyp:*',
-            'vehicle/vehicle.gyp:*',
             'sso/sso.gyp:*',
+            'telephony/telephony.gyp:*',
+            'vehicle/vehicle.gyp:*',
           ],
         }],
       ],