Added Tizen filters and Call History extension using now reentrant versions of localt...
authorZoltan Kis <zoltan.kis@intel.com>
Fri, 31 Jan 2014 20:50:10 +0000 (22:50 +0200)
committerZoltan Kis <zoltan.kis@intel.com>
Mon, 3 Mar 2014 13:53:38 +0000 (15:53 +0200)
callhistory: merged js and html

callhistory: fix coding style

callhistory: fix code style

callhistory: fix coding style

callhistory: fixed comments

callhistory: styled constants

callhistory: added scoping to C variables, changed constants

callhistory: removed debug statements

callhistory/callhistory.cc [new file with mode: 0644]
callhistory/callhistory.gyp [new file with mode: 0644]
callhistory/callhistory.h [new file with mode: 0644]
callhistory/callhistory_api.js [new file with mode: 0644]
callhistory/callhistory_mobile.cc [new file with mode: 0644]
examples/callhistory.html [new file with mode: 0644]
examples/index.html
packaging/tizen-extensions-crosswalk.spec
tizen-wrt.gyp
tizen/tizen.h
tizen/tizen_api.js

diff --git a/callhistory/callhistory.cc b/callhistory/callhistory.cc
new file mode 100644 (file)
index 0000000..d843419
--- /dev/null
@@ -0,0 +1,141 @@
+// 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.
+
+// This file contains the IPC implementation between the extension and runtime,
+// and the glue code to Tizen specific backend.
+
+#include "callhistory/callhistory.h"
+
+const char kEntryID[] = "uid";
+const char kServiceID[] = "serviceId";
+const char kCallType[] = "type";
+const char kCallFeatures[] = "features";
+const char kRemoteParties[] = "remoteParties";
+const char kForwardedFrom[] = "forwardedFrom";
+const char kStartTime[] = "startTime";
+const char kCallDuration[] = "duration";
+const char kCallEndReason[] = "endReason";
+const char kCallDirection[] = "direction";
+const char kCallRecording[] = "recording";
+const char kCallCost[] = "cost";
+const char kCallCurrency[] = "currency";
+
+const char kRemoteParty[] = "remoteParty";
+const char kPersonID[] = "personId";
+const char kExtRemoteParty[] = "remoteParties.remoteParty";
+const char kExtPersonID[] = "remoteParties.personId";
+
+const char kTizenTEL[] = "TEL";
+const char kTizenXMPP[] = "XMPP";
+const char kTizenSIP[] = "SIP";
+
+const char kAnyCall[] = "CALL";
+const char kVoiceCall[] = "VOICECALL";
+const char kVideoCall[] = "VIDEOCALL";
+const char kEmergencyCall[] = "EMERGENCYCALL";
+
+const char kDialedCall[] = "DIALED";
+const char kReceivedCall[] = "RECEIVED";
+const char kUnseenMissedCall[] = "MISSEDNEW";
+const char kMissedCall[] = "MISSED";
+const char kRejectedCall[] = "REJECTED";
+const char kBlockedCall[] = "BLOCKED";
+
+namespace {
+
+#define LOG_ERR(msg) std::cerr << "\n[Error] " << msg
+
+static const unsigned int kInstanceMagic = 0xACDCBEEF;
+
+}  // namespace
+
+common::Extension* CreateExtension() {
+  return new CallHistoryExtension;
+}
+
+// This will be generated from callhistory_api.js.
+extern const char kSource_callhistory_api[];
+
+CallHistoryExtension::CallHistoryExtension() {
+  const char* entry_points[] = { NULL };
+  SetExtraJSEntryPoints(entry_points);
+  SetExtensionName("tizen.callhistory");
+  SetJavaScriptAPI(kSource_callhistory_api);
+}
+
+CallHistoryExtension::~CallHistoryExtension() {}
+
+common::Instance* CallHistoryExtension::CreateInstance() {
+  return new CallHistoryInstance;
+}
+
+CallHistoryInstance::CallHistoryInstance()
+    : backendConnected_(false),
+      listenerCount_(0),
+      instanceCheck_(kInstanceMagic) {
+}
+
+CallHistoryInstance::~CallHistoryInstance() {
+  UnregisterListener();
+  ReleaseBackend();
+}
+
+bool CallHistoryInstance::IsValid() const {
+  return (instanceCheck_ == kInstanceMagic);
+}
+
+void CallHistoryInstance::HandleSyncMessage(const char* msg) {
+  LOG_ERR("Sync API not supported; message ignored '" << msg << "'\n");
+}
+
+void CallHistoryInstance::HandleMessage(const char* msg) {
+  picojson::value js_cmd;
+  picojson::value::object js_reply;
+  std::string js_err;
+  int err = UNKNOWN_ERR;
+
+  js_reply["cmd"] = picojson::value("reply");
+
+  picojson::parse(js_cmd, msg, msg + strlen(msg), &js_err);
+  if (!js_err.empty()) {
+    LOG_ERR("Error parsing JSON:" + js_err + " ['" + msg + "']\n");
+    js_reply["errorCode"] = picojson::value(static_cast<double>(err));
+    SendReply(js_reply);
+    return;
+  }
+
+  js_reply["reply_id"] = js_cmd.get("reply_id");
+
+  if (!CheckBackend()) {
+    err = DATABASE_ERR;
+    LOG_ERR("Could not connect to backend\n");
+    js_reply["errorCode"] = picojson::value(static_cast<double>(err));
+    SendReply(js_reply);
+    return;
+  }
+
+  std::string cmd = js_cmd.get("cmd").to_str();
+  if (cmd == "find")
+    err = HandleFind(js_cmd, js_reply);  // returns results in js_reply
+  else if (cmd == "remove")
+    err = HandleRemove(js_cmd);  // only success/error
+  else if (cmd == "removeBatch")
+    err = HandleRemoveBatch(js_cmd);  // only success/error
+  else if (cmd == "removeAll")
+    err = HandleRemoveAll(js_cmd);  // only success/error
+  else if (cmd == "addListener")
+    err = HandleAddListener();
+  else if (cmd == "removeListener")
+    err = HandleRemoveListener();
+  else
+    err = INVALID_STATE_ERR;
+
+  js_reply["errorCode"] = picojson::value(static_cast<double>(err));
+  SendReply(js_reply);
+}
+
+void CallHistoryInstance::SendReply(picojson::value::object& js_reply) {
+  picojson::value v(js_reply);
+  PostMessage(v.serialize().c_str());
+}
diff --git a/callhistory/callhistory.gyp b/callhistory/callhistory.gyp
new file mode 100644 (file)
index 0000000..1e2b33d
--- /dev/null
@@ -0,0 +1,33 @@
+{
+  'includes':[
+    '../common/common.gypi',
+  ],
+  'targets': [
+    {
+      'target_name': 'tizen_callhistory',
+      'type': 'loadable_module',
+
+      'conditions': [
+        [ 'extension_host_os == "mobile"', {
+          'variables': {
+            'packages': ['contacts-service2', 'libpcrecpp',]
+          },
+
+         'includes': [
+           '../common/pkg-config.gypi',
+          ],
+
+          'sources': [
+            'callhistory_api.js',
+            'callhistory.cc',
+            'callhistory.h',
+            'callhistory_mobile.cc',
+            'callhistory_props.h',
+            '../common/extension.cc',
+            '../common/extension.h',
+          ],
+        }],
+      ],
+    },
+  ],
+}
diff --git a/callhistory/callhistory.h b/callhistory/callhistory.h
new file mode 100644 (file)
index 0000000..6972313
--- /dev/null
@@ -0,0 +1,108 @@
+// 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 CALLHISTORY_CALLHISTORY_H_
+#define CALLHISTORY_CALLHISTORY_H_
+
+#include <time.h>
+#include <string>
+#include <iostream>
+#include "common/extension.h"
+#include "common/picojson.h"
+#include "tizen/tizen.h"  // for errors and filter definitions
+
+// For Tizen Call History API specification, see
+// https://developer.tizen.org/dev-guide/2.2.1/org.tizen.web.device.apireference/tizen/callhistory.html
+
+/*
+ * Extension
+ */
+class CallHistoryExtension : public common::Extension {
+ public:
+  CallHistoryExtension();
+  virtual ~CallHistoryExtension();
+ private:
+  // common::Extension implementation.
+  virtual common::Instance* CreateInstance();
+};
+
+/*
+ * Instance: interface for Tizen Mobile and Tizen IVI implementations
+ */
+class CallHistoryInstance : public common::Instance {
+ public:
+  CallHistoryInstance();
+  virtual ~CallHistoryInstance();
+  virtual bool IsValid() const;
+
+ private:
+  // common::Instance implementation.
+  void HandleMessage(const char* msg);
+  void HandleSyncMessage(const char* msg);
+
+  void SendReply(picojson::value::object& jsreply);
+
+  // Tizen API backend-specific call handlers
+  int HandleFind(const picojson::value& msg,
+                 picojson::value::object& reply);
+  int HandleRemove(const picojson::value& msg);
+  int HandleRemoveBatch(const picojson::value& msg);
+  int HandleRemoveAll(const picojson::value& msg);
+  int HandleAddListener();
+  int HandleRemoveListener();
+
+  int UnregisterListener();
+  bool ReleaseBackend();
+  bool CheckBackend();
+
+  bool backendConnected_;
+  unsigned int listenerCount_;
+  unsigned int instanceCheck_;
+};
+
+// property names used in the JS API, for CallHistoryEntry
+// used in the profile-specific implementations
+// definitions in callhistory.cc
+extern const char kEntryID[];
+extern const char kServiceID[];
+extern const char kCallType[];
+extern const char kCallFeatures[];
+extern const char kRemoteParties[];
+extern const char kForwardedFrom[];
+extern const char kStartTime[];
+extern const char kCallDuration[];
+extern const char kCallEndReason[];
+extern const char kCallDirection[];
+extern const char kCallRecording[];
+extern const char kCallCost[];
+extern const char kCallCurrency[];
+
+// property names used in the JS API, for RemoteParty
+extern const char kRemoteParty[];
+extern const char kPersonID[];
+
+// additional remote party specifiers accepted by filters
+extern const char kExtRemoteParty[];
+extern const char kExtPersonID[];
+
+// call type values
+extern const char kTizenTEL[];
+extern const char kTizenXMPP[];
+extern const char kTizenSIP[];
+
+// call feature values
+extern const char kAnyCall[];
+extern const char kVoiceCall[];
+extern const char kVideoCall[];
+extern const char kEmergencyCall[];
+
+// call direction values
+extern const char kDialedCall[];
+extern const char kReceivedCall[];
+extern const char kUnseenMissedCall[];
+extern const char kMissedCall[];
+extern const char kRejectedCall[];
+extern const char kBlockedCall[];
+
+#endif  // CALLHISTORY_CALLHISTORY_H_
diff --git a/callhistory/callhistory_api.js b/callhistory/callhistory_api.js
new file mode 100644 (file)
index 0000000..e37b44f
--- /dev/null
@@ -0,0 +1,267 @@
+// 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.
+
+// CallHistory WebIDL specification
+// https://developer.tizen.org/dev-guide/2.2.1/org.tizen.web.device.apireference/tizen/callhistory.html
+
+function error(txt) {
+  var text = txt instanceof Object ? toPrintableString(txt) : txt;
+  console.log('\n[CallHist JS] Error: ' + txt);
+}
+
+// print an Object into a string; used for logging
+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 isValidString(value) {
+  return typeof(value) === 'string' || value instanceof String;
+}
+
+function isValidInt(value) {
+  return isFinite(value) && !isNaN(parseInt(value));
+}
+
+function isValidFunction(value) {
+  return (value && (value instanceof Function));
+}
+
+function throwTizenTypeMismatch() {
+  throw new tizen.WebAPIException(tizen.WebAPIException.TYPE_MISMATCH_ERR);
+}
+
+function throwTizenInvalidValue() {
+  throw new tizen.WebAPIException(tizen.WebAPIException.INVALID_VALUES_ERR);
+}
+
+function throwTizenNotFound() {
+  throw new tizen.WebAPIException(tizen.WebAPIException.NOT_FOUND_ERR);
+}
+
+function throwTizenUnknown() {
+  throw new tizen.WebAPIException(tizen.WebAPIException.UNKNOWN_ERR);
+}
+
+function throwTizenException(e) {
+  if (e instanceof TypeError)
+    throwTizenTypeMismatch();
+  else if (e instanceof RangeError)
+    throwTizenInvalidValue();
+  else
+    throwTizenUnknownError();
+}
+
+var callh_listeners = {};
+var callh_listener_id = 0;
+var callh_listeners_count = 0;
+
+var callh_onsuccess = {};
+var callh_onerror = {};
+var callh_next_reply_id = 0;
+
+var getNextReplyId = function() {
+  return callh_next_reply_id++;
+};
+
+// send a JSON message to the native extension code
+function postMessage(msg, onsuccess, onerror) {
+  var reply_id = getNextReplyId();
+  msg.reply_id = reply_id;
+  callh_onsuccess[reply_id] = onsuccess;
+  if (isValidFunction(onerror))
+    callh_onerror[reply_id] = onerror;
+  var sm = JSON.stringify(msg);
+  extension.postMessage(sm);
+}
+
+function handleReply(msg) {
+  // reply_id can be 0
+  if (!msg || msg.reply_id == null || msg.reply_id == undefined) {
+    error('Listener error for reply, called with: \n' + msg);
+    throwTizenUnknown();
+  }
+  if (msg.errorCode != tizen.WebAPIError.NO_ERROR) {
+    var onerror = callh_onerror[msg.reply_id];
+    if (isValidFunction(onerror)) {
+      onerror(new tizen.WebAPIError(msg.errorCode));
+      delete callh_onerror[msg.reply_id];
+    } else {
+      error('Error: error callback is not a function');
+    }
+    return;
+  }
+  var onsuccess = callh_onsuccess[msg.reply_id];
+  if (isValidFunction(onsuccess)) {
+    onsuccess(msg.result);
+    delete callh_onsuccess[msg.reply_id];
+  } else {
+    error('Error: success callback is not a function');
+  }
+}
+
+function handleNotification(msg) {
+  if (msg.errorCode != tizen.WebAPIError.NO_ERROR) {
+    error('Error code in listener callback');
+    return;
+  }
+  for (var key in callh_listeners) {
+    var cb = callh_listeners[key];
+    if (!cb) {
+      error('No listener object found for handle ' + key);
+      return;
+    }
+
+    if (cb.onadded && msg.added && msg.added.length > 0)
+      cb.onadded(msg.added);
+
+    if (cb.onchanged && msg.changed && msg.changed.length > 0)
+      cb.onchanged(msg.changed);
+
+    if (cb.onremoved && msg.deleted && msg.deleted.length > 0)
+      cb.onremoved(msg.deleted);
+  }
+}
+
+// handle the JSON messages sent from the native extension code to JS
+// including replies and change notifications
+extension.setMessageListener(function(json) {
+  var msg = JSON.parse(json);
+  if (!msg || !msg.errorCode || !msg.cmd) {
+    error('Listener error, called with: \n' + json);
+    return;
+  }
+
+  if (msg.cmd == 'reply') {
+    handleReply(msg);
+  } else if (msg.cmd == 'notif') {
+    handleNotification(msg);
+  } else {
+    error('invalid JSON message from extension: ' + json);
+  }
+});
+
+function isValidFilter(f) {
+  return (f instanceof tizen.AttributeFilter) ||
+         (f instanceof tizen.AttributeRangeFilter) ||
+         (f instanceof tizen.CompositeFilter);
+}
+
+exports.find = function(successCallback, errorCallback, filter, sortMode, limit, offset) {
+  if (!isValidFunction(successCallback))
+    throwTizenTypeMismatch();
+
+  if (arguments.length > 1 && errorCallback && !(errorCallback instanceof Function))
+    throwTizenTypeMismatch();
+
+  if (arguments.length > 2 && filter && !isValidFilter(filter))
+    throwTizenTypeMismatch();
+
+  if (arguments.length > 3 && sortMode && !(sortMode instanceof tizen.SortMode))
+    throwTizenTypeMismatch();
+
+  if (arguments.length > 4 && limit && !isValidInt(limit))
+    throwTizenTypeMismatch();
+
+  if (arguments.length > 5 && offset && !isValidInt(offset))
+    throwTizenTypeMismatch();
+
+  var cmd = {
+    cmd: 'find',
+    filter: filter,
+    sortMode: sortMode,
+    limit: limit,
+    offset: offset
+  };
+  postMessage(cmd, successCallback, errorCallback);
+};
+
+exports.remove = function(callEntry) {
+  var uid = callEntry.uid;
+  if (callEntry.uid == undefined)
+    throwTizenUnknown();
+
+  postMessage({ cmd: 'remove', uid: uid },
+              function() { print('Entry removed: uid = ' + uid); },
+              function(err) { error(err.name + ': ' + err.message);});
+};
+
+exports.removeBatch = function(callEntryList, successCallback, errorCallback) {
+  if (!callEntryList || callEntryList.length == 0)
+    throwTizenInvalidValue();
+
+  if (!successCallback && !(successCallback instanceof Function))
+    throwTizenTypeMismatch();
+
+  if (!errorCallback && (!successCallback || !(errorCallback instanceof Function)))
+    throwTizenTypeMismatch();
+
+  var uids = [];  // collect uid's from all entries
+  for (var i = 0; i < callEntryList.length; i++) {
+    if (callEntryList[i].uid == undefined)
+      throwTizenTypeMismatch();
+    uids.push(callEntryList[i].uid);
+  }
+
+  postMessage({ cmd: 'removeBatch', uids: uids }, successCallback, errorCallback);
+};
+
+exports.removeAll = function(successCallback, errorCallback) {
+  if (!successCallback && !(successCallback instanceof Function))
+    throwTizenTypeMismatch();
+
+  if (!errorCallback && (!successCallback || !(errorCallback instanceof Function)))
+    throwTizenTypeMismatch();
+
+  postMessage({ cmd: 'removeAll' }, successCallback, errorCallback);
+};
+
+exports.addChangeListener = function(obs) {
+  if (!obs || !(isValidFunction(obs.onadded) ||
+      isValidFunction(obs.onchanged) || isValidFunction(obs.onremoved)))
+    throwTizenTypeMismatch();
+
+  for (var key in callh_listeners) {  // if the same object was registered
+    if (callh_listeners[key] == obs &&
+        callh_listeners[key].onadded == obs.onadded &&
+        callh_listeners[key].onremoved == obs.onremoved &&
+        callh_listeners[key].onchanged == obs.onchanged) {
+      return key;
+    }
+  }
+
+  callh_listeners[++callh_listener_id] = obs;
+  callh_listeners_count++;
+  if (callh_listeners_count == 1) {
+    postMessage({ cmd: 'addListener' },
+                function() { print('Listener registered'); },
+                function(err) { error(err.name + ': ' + err.message);});
+  }
+  return callh_listener_id;
+};
+
+exports.removeChangeListener = function(handle) {
+  if (!isValidInt(handle)) {
+    throwTizenTypeMismatch();
+  }
+
+  var obs = callh_listeners[handle];
+  if (!obs) {
+    throwTizenInvalidValue();
+  }
+
+  delete callh_listeners[handle];
+  callh_listeners_count--;
+  if (callh_listeners_count == 0) {
+    postMessage({ cmd: 'removeListener'},
+                function() { print('Listener unregistered'); },
+                function(err) { error(err.name + ': ' + err.message);});
+  }
+};
diff --git a/callhistory/callhistory_mobile.cc b/callhistory/callhistory_mobile.cc
new file mode 100644 (file)
index 0000000..cf74f51
--- /dev/null
@@ -0,0 +1,849 @@
+// 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.
+
+// This file contains the IPC implementation between the extension and runtime,
+// and the glue code to Tizen specific backend. Specification:
+// https://developer.tizen.org/dev-guide/2.2.1/org.tizen.web.device.apireference/tizen/callhistory.html
+
+#include "callhistory/callhistory.h"
+
+#include <contacts.h>
+
+#include <sstream>
+
+namespace {
+
+// Wrapper for logging; currently cout/cerr is used in Tizen extensions.
+#define LOG_ERR(msg) std::cerr << "\n[Error] " << msg
+
+// Global call log view URI variable used from Contacts.
+#define CALLH_VIEW_URI        _contacts_phone_log._uri
+
+// CallHistoryEntry attribute id's from Contacts global call log view var.
+#define CALLH_ATTR_UID        _contacts_phone_log.id
+#define CALLH_ATTR_PERSONID   _contacts_phone_log.person_id
+#define CALLH_ATTR_DIRECTION  _contacts_phone_log.log_type
+#define CALLH_ATTR_ADDRESS    _contacts_phone_log.address
+#define CALLH_ATTR_STARTTIME  _contacts_phone_log.log_time
+#define CALLH_ATTR_DURATION   _contacts_phone_log.extra_data1
+
+#define CALLH_FILTER_NONE    -1
+#define CALLH_FILTER_AND      CONTACTS_FILTER_OPERATOR_AND
+#define CALLH_FILTER_OR       CONTACTS_FILTER_OPERATOR_OR
+
+inline bool check(int err) {
+  return err == CONTACTS_ERROR_NONE;
+}
+
+// Map contacts errors to JS errors.
+int MapContactErrors(int err) {
+  //             Tizen Contacts error code          Tizen WRT error code.
+  return (err == CONTACTS_ERROR_NONE              ? NO_ERROR          :
+          err == CONTACTS_ERROR_INVALID_PARAMETER ? VALIDATION_ERR    :
+          err == CONTACTS_ERROR_DB                ? DATABASE_ERR      :
+          err == CONTACTS_ERROR_INTERNAL          ? NOT_SUPPORTED_ERR :
+          err == CONTACTS_ERROR_NO_DATA           ? NOT_FOUND_ERR     :
+          err == CONTACTS_ERROR_PERMISSION_DENIED ? SECURITY_ERR      :
+          err == CONTACTS_ERROR_IPC_NOT_AVALIABLE ? TIMEOUT_ERR       :
+          err == CONTACTS_ERROR_IPC               ? TIMEOUT_ERR       :
+                                                    UNKNOWN_ERR);
+}
+
+// Converting strings from API queries to contacts view identifiers.
+// See contacts_views.h ( _contacts_phone_log ).
+int MapAttrName(std::string &att) {
+  // the order matters, more frequent query attributes come first
+  if (att == kCallDirection)
+    return CALLH_ATTR_DIRECTION;
+
+  if (att == kExtRemoteParty || att == kRemoteParty)
+    return CALLH_ATTR_ADDRESS;
+
+  if (att == kStartTime)
+    return CALLH_ATTR_STARTTIME;
+
+  if (att == kEntryID)
+    return CALLH_ATTR_UID;
+
+  if (att == kCallDuration)
+    return CALLH_ATTR_DURATION;
+
+  return 0;
+}
+
+// Helper class for scope destruction of Contact types (opaque pointers).
+template <typename T>
+class ScopeGuard {
+ public:
+  ScopeGuard() { var_ = reinterpret_cast<T>(NULL); }
+  ~ScopeGuard() { LOG_ERR("ScopeGuard: type not supported"); }
+  T* operator&() { return &var_; }  // NOLINT "unary & is dangerous"
+  void operator=(T& var) { var_ = var; }
+ private:
+  T var_;
+};
+
+// Specialized destructors for all supported Contact types.
+template<>
+inline ScopeGuard<contacts_filter_h>::~ScopeGuard() {
+  if (var_ && !check(contacts_filter_destroy(var_)))
+    LOG_ERR("ScopeGuard: failed to destroy contacts_filter_h");
+}
+
+template<>
+inline ScopeGuard<contacts_list_h>::~ScopeGuard() {
+  if (var_ && !check(contacts_list_destroy(var_, true)))
+    LOG_ERR("ScopeGuard: failed to destroy contacts_list_h");
+}
+
+template<>
+inline ScopeGuard<contacts_record_h>::~ScopeGuard() {
+  if (var_ && !check(contacts_record_destroy(var_, true)))
+    LOG_ERR("ScopeGuard: failed to destroy contacts_record_h");
+}
+
+template<>
+inline ScopeGuard<contacts_query_h>::~ScopeGuard() {
+  if (var_ && !check(contacts_query_destroy(var_)))
+    LOG_ERR("ScopeGuard: failed to destroy contacts_query_h");
+}
+
+// Macros interfacing with C code from Contacts API.
+#define CHK(fnc) do { int _er = (fnc); \
+  if (!check(_er)) { LOG_ERR(#fnc " failed"); return _er;} } while (0)
+
+#define CHK_MAP(fnc) do { int _er = (fnc); \
+  if (!check(_er)) { LOG_ERR(#fnc " failed"); \
+    return MapContactErrors(_er); } } while (0)
+
+
+picojson::value JsonFromInt(int val) {
+  return picojson::value(static_cast<double>(val));
+}
+
+picojson::value JsonFromTime(time_t val) {
+  char timestr[40];
+  // Instead "struct tm* tms = localtime(&val);" use the reentrant version.
+  time_t tme = time(NULL);
+  struct tm tm_s = {0};
+  localtime_r(&tme, &tm_s);
+  struct tm* tms = &tm_s;
+
+  strftime(timestr, sizeof(timestr), "%Y-%m-%d %H:%M:%S.%06u GMT%z", tms);
+  return picojson::value(std::string(timestr));
+}
+
+picojson::value JsonTimeFromInt(int val) {
+  time_t tval = static_cast<time_t>(val);
+  return JsonFromTime(tval);
+}
+
+// Needed because v.get() asserts if type is wrong, this one fails gracefully.
+bool IntFromJson(const picojson::value& v, int* result) {
+  if (!result || !v.is<double>())
+    return false;
+  *result = static_cast<int>(v.get<double>());
+  return true;
+}
+
+// Read Contacts record, and prepare JSON array element for the "result"
+// property used in setMessageListener() JS function in callhistory_api.js.
+int SerializeEntry(contacts_record_h record, picojson::value::object& o) {
+  int int_val;
+  char* string_val;
+
+  o["type"] = picojson::value("TEL");  // for now, only "TEL" is supported
+
+  CHK(contacts_record_get_int(record, CALLH_ATTR_UID, &int_val));
+  o["uid"] = JsonFromInt(int_val);
+
+  CHK(contacts_record_get_int(record, CALLH_ATTR_DURATION, &int_val));
+  o["duration"] = JsonFromInt(int_val);
+
+  CHK(contacts_record_get_int(record, CALLH_ATTR_STARTTIME, &int_val));
+  o["startTime"] = JsonTimeFromInt(int_val);
+
+  picojson::value::array features;
+  features.push_back(picojson::value("CALL"));  // common to all
+
+  CHK(contacts_record_get_int(record, CALLH_ATTR_DIRECTION, &int_val));
+  switch (int_val) {
+    case CONTACTS_PLOG_TYPE_VIDEO_INCOMMING:
+      features.push_back(picojson::value("VIDEOCALL"));
+      o["direction"] = picojson::value("RECEIVED");
+      break;
+    case CONTACTS_PLOG_TYPE_VOICE_INCOMMING:
+      features.push_back(picojson::value("VOICECALL"));
+      o["direction"] = picojson::value("RECEIVED");
+      break;
+    case CONTACTS_PLOG_TYPE_VIDEO_OUTGOING:
+      features.push_back(picojson::value("VIDEOCALL"));
+      o["direction"] = picojson::value("DIALED");
+      break;
+    case CONTACTS_PLOG_TYPE_VOICE_OUTGOING:
+      features.push_back(picojson::value("VOICECALL"));
+      o["direction"] = picojson::value("DIALED");
+      break;
+    case CONTACTS_PLOG_TYPE_VIDEO_INCOMMING_UNSEEN:
+      features.push_back(picojson::value("VIDEOCALL"));
+      o["direction"] = picojson::value("MISSEDNEW");
+      break;
+    case CONTACTS_PLOG_TYPE_VOICE_INCOMMING_UNSEEN:
+      features.push_back(picojson::value("VOICECALL"));
+      o["direction"] = picojson::value("MISSEDNEW");
+      break;
+    case CONTACTS_PLOG_TYPE_VIDEO_INCOMMING_SEEN:
+      features.push_back(picojson::value("VIDEOCALL"));
+      o["direction"] = picojson::value("MISSED");
+      break;
+    case CONTACTS_PLOG_TYPE_VOICE_INCOMMING_SEEN:
+      features.push_back(picojson::value("VOICECALL"));
+      o["direction"] = picojson::value("MISSED");
+      break;
+    case CONTACTS_PLOG_TYPE_VIDEO_REJECT:
+      features.push_back(picojson::value("VIDEOCALL"));
+      o["direction"] = picojson::value("REJECTED");
+      break;
+    case CONTACTS_PLOG_TYPE_VOICE_REJECT:
+      features.push_back(picojson::value("VOICECALL"));
+      o["direction"] = picojson::value("REJECTED");
+      break;
+    case CONTACTS_PLOG_TYPE_VIDEO_BLOCKED:
+      features.push_back(picojson::value("VIDEOCALL"));
+      o["direction"] = picojson::value("BLOCKED");
+      break;
+    case CONTACTS_PLOG_TYPE_VOICE_BLOCKED:
+      features.push_back(picojson::value("VOICECALL"));
+      o["direction"] = picojson::value("BLOCKED");
+      break;
+    default:
+      LOG_ERR("SerializeEntry(): invalid 'direction'");
+      break;
+  }
+
+  o["features"] = picojson::value(features);
+
+  picojson::value::array rp_list;
+  picojson::value::object r_party;
+  CHK(contacts_record_get_str_p(record, CALLH_ATTR_ADDRESS, &string_val));
+  r_party["remoteParty"] = picojson::value(string_val);
+
+  CHK(contacts_record_get_int(record, CALLH_ATTR_PERSONID, &int_val));
+  r_party["personId"] = JsonFromInt(int_val);
+
+  rp_list.push_back(picojson::value(r_party));
+  o["remoteParties"] = picojson::value(rp_list);
+
+  return CONTACTS_ERROR_NONE;
+}
+
+/*
+Setting up native filters
+3 levels of methods are used, which all add partial filters to a common
+contacts filter:
+- ParseFilter() for dispatching,
+- add[Attribute|Range|Composite]Filter, for parsing and integrating,
+- map[Attribute|Range]Filter to map to contacts filters.
+
+Examples for full JSON commands:
+{ "cmd":"find",
+  "filter": {
+    "attributeName":"direction",
+    "matchFlag":"EXACTLY",
+    "matchValue":"DIALED"
+  },
+  "sortMode": {
+    "attributeName":"startTime",
+    "order":"DESC"
+  },
+  "limit":null,
+  "offset":null,
+  "reply_id":3
+}
+
+{ "cmd":"find",
+  "filter": {
+    "attributeName":"startTime",
+    "initialValue": "2013-12-30T15:18:22.077Z",
+    "endValue":     "2012-12-30T15:18:22.077Z"
+  },
+  "sortMode":{
+   "attributeName":"startTime",
+   "order":"DESC"
+  },
+  "limit":null,
+  "offset":null,
+  "reply_id":4
+}
+
+{ "cmd":"find",
+  "filter": {
+    "type":"INTERSECTION",
+    "filters":[
+      { "attributeName":"remoteParties",
+        "matchFlag":"CONTAINS",
+        "matchValue":"John"
+      },
+      { "attributeName":"direction",
+        "matchFlag":"EXACTLY",
+        "matchValue":"RECEIVED"
+      }
+    ]},
+  "sortMode": {
+    "attributeName":"startTime",
+    "order":"DESC"
+  },
+  "limit":100,
+  "offset":0,
+  "reply_id":1
+}
+*/
+
+// Map an attribute filter to an existing contacts filter.
+int MapAttributeFilter(contacts_filter_h& filter,
+                       const std::string& attr,
+                       const std::string& match_flag,
+                       const picojson::value& value) {
+  unsigned int prop_id;
+  // Valid match flags: "EXACTLY",    "FULLSTRING", "CONTAINS",
+  //                    "STARTSWITH", "ENDSWITH",   "EXISTS".
+  contacts_match_str_flag_e mflag = CONTACTS_MATCH_EXACTLY;
+
+  // More frequently used attributes are in the front.
+  if (attr == kCallDirection) {
+    // Valid matchflags: EXACTLY, EXISTS.
+    // Values: "DIALED","RECEIVED","MISSEDNEW","MISSED","BLOCKED","REJECTED"
+    prop_id = CALLH_ATTR_DIRECTION;
+
+    if (match_flag == STR_MATCH_EXISTS)
+      return CONTACTS_ERROR_NONE;  // no extra filter is set up
+    else if (match_flag != STR_MATCH_EXACTLY)
+      return CONTACTS_ERROR_DB;  // error
+
+    int voice = 0, video = 0;
+    std::string dir = value.to_str();
+
+    if (dir == kDialedCall) {
+      voice = CONTACTS_PLOG_TYPE_VOICE_OUTGOING;  // should be DIALED
+      video = CONTACTS_PLOG_TYPE_VIDEO_OUTGOING;  // here, too
+    } else if (dir == kReceivedCall) {
+      voice = CONTACTS_PLOG_TYPE_VOICE_INCOMMING;  // should be INCOMING
+      video = CONTACTS_PLOG_TYPE_VIDEO_INCOMMING;  // here, too
+    } else if (dir == kUnseenMissedCall) {
+      voice = CONTACTS_PLOG_TYPE_VOICE_INCOMMING_UNSEEN;  // here, too
+      video = CONTACTS_PLOG_TYPE_VIDEO_INCOMMING_UNSEEN;  // here, too
+    } else if (dir == kMissedCall) {
+      voice = CONTACTS_PLOG_TYPE_VOICE_INCOMMING_SEEN;  // here, too
+      video = CONTACTS_PLOG_TYPE_VIDEO_INCOMMING_SEEN;  // here, too
+    } else if (dir == kRejectedCall) {
+      voice = CONTACTS_PLOG_TYPE_VOICE_REJECT;  // should be REJECTED
+      video = CONTACTS_PLOG_TYPE_VIDEO_REJECT;  // here, too
+    } else if (dir == kBlockedCall) {
+      voice = CONTACTS_PLOG_TYPE_VOICE_BLOCKED;
+      video = CONTACTS_PLOG_TYPE_VIDEO_BLOCKED;
+    }
+
+    CHK(contacts_filter_add_int(filter, prop_id, CONTACTS_MATCH_EQUAL, voice));
+    CHK(contacts_filter_add_operator(filter, CONTACTS_FILTER_OPERATOR_OR));
+    CHK(contacts_filter_add_int(filter, prop_id, CONTACTS_MATCH_EQUAL, video));
+  } else if (attr == kCallFeatures) {
+    // Valid matchflags: EXACTLY, EXISTS.
+    // values: "CALL", "VOICECALL", "VIDEOCALL", "EMERGENCYCALL"
+    return CONTACTS_ERROR_INTERNAL;  // not supported now
+  } else if (attr == kEntryID) {
+    // matchflags: EXACTLY, EXISTS, map from string to int
+    prop_id = CALLH_ATTR_UID;
+
+    if (match_flag != STR_MATCH_EXACTLY)
+      return CONTACTS_ERROR_INVALID_PARAMETER;
+
+    int val;
+    if (!IntFromJson(value, &val))
+      return CONTACTS_ERROR_DB;
+
+    CHK(contacts_filter_add_int(filter, prop_id, CONTACTS_MATCH_EQUAL, val));
+  } else if (attr == kCallType) {
+    // Valid matchflags: EXACTLY, EXISTS.
+    // values: "TEL", "XMPP", "SIP"
+    // the implementation of contacts doesn't support it
+    return CONTACTS_ERROR_INTERNAL;
+  } else if (attr == kExtRemoteParty) {  // only for exact match
+    // Valid matchflags: EXACTLY.
+    prop_id = CALLH_ATTR_ADDRESS;
+
+    if (match_flag != STR_MATCH_EXACTLY)
+      return CONTACTS_ERROR_INVALID_PARAMETER;
+
+    const char* val = value.to_str().c_str();
+    CHK(contacts_filter_add_str(filter, prop_id, mflag, val));
+  } else if (attr == kRemoteParties || attr == kRemoteParty) {
+    // Valid matchflags: all flags.
+    prop_id = CALLH_ATTR_ADDRESS;
+
+    if (match_flag == STR_MATCH_EXACTLY)
+      mflag = CONTACTS_MATCH_EXACTLY;
+    else if (match_flag == STR_MATCH_FULLSTRING)
+      mflag = CONTACTS_MATCH_FULLSTRING;
+    else if (match_flag == STR_MATCH_CONTAINS)
+      mflag = CONTACTS_MATCH_CONTAINS;
+    else if (match_flag == STR_MATCH_STARTSWITH)
+      mflag = CONTACTS_MATCH_STARTSWITH;
+    else if (match_flag == STR_MATCH_ENDSWITH)
+      mflag = CONTACTS_MATCH_ENDSWITH;
+    else if (match_flag == STR_MATCH_EXISTS)
+      mflag = CONTACTS_MATCH_EXISTS;
+
+    const char* val = value.to_str().c_str();
+    CHK(contacts_filter_add_str(filter, prop_id, mflag, val));
+  } else {
+    LOG_ERR("MapAttributeFilter " << attr << " not supported");
+    return CONTACTS_ERROR_INTERNAL;
+  }
+
+  return CONTACTS_ERROR_NONE;
+}
+
+// map an attribute range filter to an existing contacts filter
+int MapRangeFilter(contacts_filter_h& filter,
+                   const std::string& attr,
+                   const picojson::value& start_value,
+                   const picojson::value& end_value) {
+  unsigned int prop_id = 0;
+  int sv, ev;
+  bool is_start, is_end;
+
+  is_start = start_value.is<picojson::null>();
+  is_end = end_value.is<picojson::null>();
+
+  if (attr == kStartTime) {
+    prop_id = CALLH_ATTR_STARTTIME;
+  } else if (attr == kCallDuration) {
+    prop_id = CALLH_ATTR_DURATION;
+  }
+  // No other attribute supports range filtering.
+
+  if (is_start) {
+    if (!IntFromJson(start_value, &sv))
+      return CONTACTS_ERROR_DB;
+
+    CHK(contacts_filter_add_int(filter, prop_id, \
+      CONTACTS_MATCH_GREATER_THAN_OR_EQUAL, sv));
+  }
+
+  if (is_end) {
+    if (!IntFromJson(end_value, &ev))
+      return CONTACTS_ERROR_DB;
+
+    if (is_start)
+      CHK(contacts_filter_add_operator(filter, CONTACTS_FILTER_OPERATOR_AND));
+
+    CHK(contacts_filter_add_int(filter, prop_id, \
+      CONTACTS_MATCH_LESS_THAN_OR_EQUAL, ev));
+  }
+
+  return  CONTACTS_ERROR_NONE;
+}
+
+int ParseFilter(contacts_filter_h& filter,
+                const picojson::value& js_filter,
+                int filter_op,
+                bool& is_empty);
+
+// Parse a composite filter and add to the existing contacts filter.
+int AddCompositeFilter(contacts_filter_h& filter,
+                       const picojson::value& jsf,
+                       int filter_op,
+                       bool& is_empty) {
+  std::string comp_filt_type = jsf.get("type").to_str();
+  contacts_filter_operator_e inner_op;
+  if (comp_filt_type == "INTERSECTION")
+    inner_op = CALLH_FILTER_AND;
+  else if (comp_filt_type == "UNION")
+    inner_op = CALLH_FILTER_OR;
+  else
+    return CONTACTS_ERROR_INVALID_PARAMETER;
+
+  ScopeGuard<contacts_filter_h> filt;
+  contacts_filter_h* pfilt = NULL;
+  if (filter_op == CALLH_FILTER_NONE || is_empty) {
+    pfilt = &filter;
+  } else if (filter_op == CALLH_FILTER_OR || filter_op == CALLH_FILTER_AND) {
+    CHK(contacts_filter_create(CALLH_VIEW_URI, &filt));
+    pfilt = &filt;
+  }
+
+  // For each filter in 'filters', add it to '*pfilt'.
+  picojson::array a = jsf.get("filters").get<picojson::array>();
+  for (picojson::array::iterator it = a.begin(); it != a.end(); ++it) {
+    CHK(ParseFilter(*pfilt, *it, inner_op, is_empty));
+  }
+
+  // Add the composite filter to 'filter'.
+  if (pfilt != &filter && !is_empty) {
+    contacts_filter_operator_e fop = (contacts_filter_operator_e) filter_op;
+    CHK(contacts_filter_add_operator(filter, fop));
+    CHK(contacts_filter_add_filter(filter, *pfilt));
+  }
+  return CONTACTS_ERROR_NONE;
+}
+
+// Parse an attribute filter and add to the existing contacts filter.
+int AddAttributeFilter(contacts_filter_h& filter,
+                       const picojson::value& jsf,
+                       int filter_op,
+                       bool& is_empty) {
+  ScopeGuard<contacts_filter_h> filt;
+  contacts_filter_h* pfilt = NULL;
+  if (filter_op == CALLH_FILTER_NONE || is_empty) {
+    pfilt = &filter;
+  } else if (filter_op == CALLH_FILTER_OR || filter_op == CALLH_FILTER_AND) {
+    CHK(contacts_filter_create(CALLH_VIEW_URI, &filt));
+    pfilt = &filt;
+  }
+
+  std::string attr = jsf.get("attributeName").to_str();
+  std::string mflag = jsf.get("matchFlag").to_str();
+  picojson::value mvalue = jsf.get("matchValue");
+
+  CHK(MapAttributeFilter(*pfilt, attr, mflag, mvalue));
+
+  if (pfilt != &filter && !is_empty) {
+    contacts_filter_operator_e fop = (contacts_filter_operator_e) filter_op;
+    CHK(contacts_filter_add_operator(filter, fop));
+    CHK(contacts_filter_add_filter(filter, *pfilt));
+  }
+
+  is_empty = false;  // 'filter' was changed, from now on add to it
+  return CONTACTS_ERROR_NONE;
+}
+
+// Parse an attribute range filter and add to the existing contacts filter.
+int AddRangeFilter(contacts_filter_h& filter,
+                   const picojson::value& jsf,
+                   int filter_op,
+                   bool& is_empty) {
+  ScopeGuard<contacts_filter_h> filt;
+  contacts_filter_h* pfilt = NULL;
+  if (filter_op == CALLH_FILTER_NONE || is_empty) {
+    pfilt = &filter;
+  } else if (filter_op == CALLH_FILTER_OR || filter_op == CALLH_FILTER_AND) {
+    CHK(contacts_filter_create(CALLH_VIEW_URI, &filt));
+    pfilt = &filt;
+  }
+
+  std::string attr = jsf.get("attributeName").to_str();
+  picojson::value start_val = jsf.get("startValue");
+  picojson::value end_val = jsf.get("endValue");
+
+  CHK(MapRangeFilter(*pfilt, attr, start_val, end_val));
+
+  if (pfilt != &filter) {
+    contacts_filter_operator_e fop = (contacts_filter_operator_e) filter_op;
+    CHK(contacts_filter_add_operator(filter, fop));
+    CHK(contacts_filter_add_filter(filter, *pfilt));
+  }
+
+  is_empty = false;  // 'filter' was changed, from now on add to it
+  return CONTACTS_ERROR_NONE;
+}
+
+// Determine the type of a JSON filter and dispatch to the right method.
+int ParseFilter(contacts_filter_h& filter,
+                const picojson::value& js_filter,
+                int filter_op,
+                bool& is_empty) {
+  if (!js_filter.is<picojson::null>()) {  // this check is redundant
+    if (js_filter.contains("type")) {
+      return AddCompositeFilter(filter, js_filter, filter_op, is_empty);
+    } else if (js_filter.contains("matchFlag")) {
+      return AddAttributeFilter(filter, js_filter, filter_op, is_empty);
+    } else if (js_filter.contains("initialValue")) {
+      return AddRangeFilter(filter, js_filter, filter_op, is_empty);
+    }
+  }
+  return CONTACTS_ERROR_NONE;
+}
+
+// Prepare JSON for setMessageListener() JS function in callhistory_api.js.
+int HandleFindResults(contacts_list_h list, picojson::value::object& resp) {
+  int err = CONTACTS_ERROR_DB;
+  unsigned int total = 0;
+
+  contacts_list_get_count(list, &total);
+  picojson::value::array result;
+
+  for (unsigned int i = 0; i < total; i++) {
+    contacts_record_h record = NULL;
+    CHK(contacts_list_get_current_record_p(list, &record));
+    if (record != NULL) {  // read the fields and create JSON attributes
+      picojson::value::object o;
+      CHK(SerializeEntry(record, o));
+      result.push_back(picojson::value(o));
+    }
+
+    err = contacts_list_next(list);  // move the current record
+    if (err != CONTACTS_ERROR_NONE && err != CONTACTS_ERROR_NO_DATA) {
+      LOG_ERR("HandleFindResults: iterator error");
+      return CONTACTS_ERROR_DB;
+    }
+  }
+  resp["result"] = picojson::value(result);  // as used in callhistory_api.js
+  return CONTACTS_ERROR_NONE;
+}
+
+// Handling database notifications through Contacts API;
+// 'changes' is a string, and yes, we need to PARSE it...
+void NotifyDatabaseChange(const char* view, char* changes, void* user_data) {
+  picojson::value::object out;  // output JSON object
+  picojson::value::array added;  // full records
+  picojson::value::array changed;  // full records
+  picojson::value::array deleted;  // only id's
+
+  char  delim[] = ",:";
+  char* rest;
+  char* chtype  = NULL;
+  char* chid    = NULL;
+  int changetype, uid, err = NO_ERROR;
+  bool ins = false;
+  contacts_record_h record = NULL;
+
+  chtype = strtok_r(changes, delim, &rest);
+  while (chtype) {
+    changetype = atoi((const char*)chtype);
+    chid = strtok_r(NULL, delim, &rest);
+    uid = atoi((const char*)chid);
+    switch (changetype) {
+      case CONTACTS_CHANGE_INSERTED:
+        ins = true;
+      case CONTACTS_CHANGE_UPDATED:
+        if (!ins)
+        if (check(contacts_db_get_record(CALLH_VIEW_URI, uid, &record))) {
+          picojson::value::object val;
+          if (check(SerializeEntry(record, val))) {
+            if (ins)
+              added.push_back(picojson::value(val));
+            else
+              changed.push_back(picojson::value(val));
+          }
+          contacts_record_destroy(record, true);
+        }
+        break;
+      case CONTACTS_CHANGE_DELETED:
+        deleted.push_back(JsonFromInt(uid));
+        break;
+      default:
+        LOG_ERR("CallHistory: invalid database change: " << chtype);
+        err = DATABASE_ERR;
+        break;
+    }
+    chtype = strtok_r(NULL, delim, &rest);
+  }
+
+  out["cmd"] = picojson::value("notif");
+  out["errorCode"] = picojson::value(static_cast<double>(err));
+  out["added"]   = picojson::value(added);
+  out["changed"] = picojson::value(changed);
+  out["deleted"] = picojson::value(deleted);
+  picojson::value v(out);
+
+  CallHistoryInstance* chi = static_cast<CallHistoryInstance*>(user_data);
+  if (chi->IsValid())
+    chi->PostMessage(v.serialize().c_str());
+  else
+    LOG_ERR("CallHistory: invalid notification callback");
+}
+
+}  // namespace
+
+
+bool CallHistoryInstance::CheckBackend() {
+  if (backendConnected_)
+    return true;
+  if (check(contacts_connect2())) {
+    backendConnected_ = true;
+    return true;
+  }
+  return false;
+}
+
+bool CallHistoryInstance::ReleaseBackend() {
+  if (!backendConnected_)
+    return true;  // Already disconnected.
+  if (check(contacts_disconnect2()))
+    backendConnected_ = false;
+  else
+    LOG_ERR("ReleaseBackend(): could not close DB");
+  return !backendConnected_;
+}
+
+// Register a single native listener for all JS ones; dispatch at JS side.
+int CallHistoryInstance::HandleAddListener() {
+  if (listenerCount_ == 0) {  // Do actual registration only on first request.
+    void* user_data = reinterpret_cast<void*>(this);
+    int err = contacts_db_add_changed_cb_with_info(CALLH_VIEW_URI,
+                                                   NotifyDatabaseChange,
+                                                   user_data);
+    if (check(err))
+      listenerCount_++;
+    return MapContactErrors(err);
+  }
+  listenerCount_++;
+  return NO_ERROR;
+}
+
+int CallHistoryInstance::HandleRemoveListener() {
+  if (listenerCount_ == 0) {
+    return UNKNOWN_ERR;
+  } else if (--listenerCount_ == 0) {
+    return UnregisterListener();
+  }
+  return NO_ERROR;
+}
+
+int CallHistoryInstance::UnregisterListener() {
+  void* user_data = reinterpret_cast<void*>(this);
+  int err = contacts_db_remove_changed_cb_with_info(CALLH_VIEW_URI,
+                                                    NotifyDatabaseChange,
+                                                    user_data);
+  return MapContactErrors(err);
+}
+
+// Take a JSON query, translate to contacts query, collect the results, and
+// return a JSON string via the callback.
+int CallHistoryInstance::HandleFind(const picojson::value& input,
+                                    picojson::value::object& reply) {
+  int limit = 0;
+  IntFromJson(input.get("limit"), &limit);  // no change on error
+
+  int offset = 0;
+  IntFromJson(input.get("offset"), &offset);
+
+  // check sort mode
+  bool asc = false;
+  std::string sortAttr(kStartTime);
+
+  picojson::value sortmode = input.get("sortMode");
+  if (!sortmode.is<picojson::null>()) {
+    picojson::value sa = sortmode.get("attributeName");
+    if (!sa.is<picojson::null>())
+      sortAttr = sa.to_str();
+    sa = sortmode.get("order");
+    std::string sortorder;
+    if (!sa.is<picojson::null>() && (sa.to_str() == STR_SORT_ASC))
+      asc = true;
+  }
+
+  // set up filter
+  ScopeGuard<contacts_filter_h> filter;
+  ScopeGuard<contacts_query_h> query;
+  ScopeGuard<contacts_list_h> list;
+  picojson::value js_filter = input.get("filter");  // object or null
+  if (js_filter.is<picojson::null>()) {
+    CHK_MAP(contacts_db_get_all_records(CALLH_VIEW_URI, offset, \
+                                          limit, &list));
+  } else {
+    bool filter_empty = true;
+    CHK_MAP(contacts_filter_create(CALLH_VIEW_URI, &filter));
+    contacts_filter_h* pfilt = &filter;
+    CHK_MAP(ParseFilter(*pfilt, js_filter, CALLH_FILTER_NONE, filter_empty));
+    CHK_MAP(contacts_query_create(CALLH_VIEW_URI, &query));
+    contacts_query_h* pquery = &query;
+    CHK_MAP(contacts_query_set_filter(*pquery, *pfilt));
+    CHK_MAP(contacts_query_set_sort(*pquery, MapAttrName(sortAttr), asc));
+    CHK_MAP(contacts_db_get_records_with_query(*pquery, offset, limit, &list));
+  }
+  contacts_list_h* plist = &list;
+  CHK_MAP(HandleFindResults(*plist, reply));
+  return NO_ERROR;
+}
+
+int CallHistoryInstance::HandleRemove(const picojson::value& msg) {
+  int uid = -1;
+
+  picojson::value v = msg.get("uid");
+  if (!IntFromJson(v, &uid)) {
+    return TYPE_MISMATCH_ERR;
+  }
+
+  return MapContactErrors(contacts_db_delete_record(CALLH_VIEW_URI, uid));
+}
+
+int CallHistoryInstance::HandleRemoveBatch(const picojson::value& msg) {
+  int err = TYPE_MISMATCH_ERR;
+  picojson::value arr = msg.get("uids");
+  if (!arr.is<picojson::array>()) {
+    return err;
+  }
+
+  picojson::array json_arr = arr.get<picojson::array>();
+  int id;
+  int count = json_arr.size();
+  int* ids = new int[count];
+  #if !NODEBUG
+    std::ostringstream uidlist;
+    uidlist.str("");
+  #endif
+
+  for (int i = 0, j = 0; i < count; i++) {
+      picojson::value v = json_arr[i];
+      if (IntFromJson(v, &id)) {
+        ids[j++] = id;
+        #if !NODEBUG
+          uidlist << id << ", ";
+        #endif
+      } else {
+        return err;
+      }
+  }
+
+  err = contacts_db_delete_records(CALLH_VIEW_URI, ids, count);
+  delete[] ids;
+  return MapContactErrors(err);
+}
+
+// Tizen Contacts server API doesn't expose any method for removing all
+// elements in one operation. Need to fetch all the records, fetch id's
+// and remove them one by one, or in batches
+int CallHistoryInstance::HandleRemoveAll(const picojson::value& msg) {
+  contacts_record_h rec  = NULL;  // should not be destroyed by us;
+  ScopeGuard<contacts_list_h> list;
+  contacts_list_h *plist;
+  int limit = 200;  // initial batch size
+  unsigned int count, i, j;
+  #if !NODEBUG
+    std::ostringstream uidlist;
+    uidlist.str("");
+  #endif
+
+  do {  // remove batches until done
+    CHK_MAP(contacts_list_create(&list));
+    CHK_MAP(contacts_db_get_all_records(CALLH_VIEW_URI, 0, limit, &list));
+    plist = &list;
+    CHK_MAP(contacts_list_get_count(*plist, &count));
+    if (count == 0) {
+      return NO_ERROR;
+    }
+
+    // now we have an array of id's to be removed
+    int* ids = new int(count);
+    CHK_MAP(contacts_record_create(CALLH_VIEW_URI, &rec));
+    CHK_MAP(contacts_list_first(*plist));
+
+    for (i = 0, j = 0; i < count; i++) {
+      CHK_MAP(contacts_list_get_current_record_p(*plist, &rec));
+      int id = -1;  // fetch the id from each record
+      CHK_MAP(contacts_record_get_int(rec, CALLH_ATTR_UID , &id));
+      if (id >= 0) {
+        ids[j++] = id;
+        #if !NODEBUG
+          uidlist << id << ", ";
+        #endif
+      }
+      if (!check(contacts_list_next(*plist)))
+        break;
+    }
+    CHK_MAP(contacts_db_delete_records(CALLH_VIEW_URI, ids, j));
+  } while (count == static_cast<unsigned int>(limit));
+  return NO_ERROR;
+}
diff --git a/examples/callhistory.html b/examples/callhistory.html
new file mode 100644 (file)
index 0000000..28406db
--- /dev/null
@@ -0,0 +1,405 @@
+<!--
+// 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>
+<h1>Test Tizen Call History API</h1>
+<body>
+  <button id="all_btn"       onclick="readAll()"      >All      </button>
+  <button id="dialed_btn"    onclick="readDialed()"   >Dialed   </button>
+  <button id="received_btn"  onclick="readReceived()" >Received </button>
+  <button id="missed_btn"    onclick="readMissed()"   >Missed   </button>
+  <button id="missednew_btn" onclick="readMissedNew()">MissedNew</button>
+  <button id="blocked_btn"   onclick="readBlocked()"  >Blocked  </button>
+  <button id="rejected_btn"  onclick="readRejected()" >Rejected </button>
+  <button id="goback_btn"    onclick="goBack()"       >Back     </button>
+  <br>
+  <form>
+    <input type="checkbox" id="limit_chk">Limit:
+      <input type="text" size=5 id="limit_text">
+    <input type="checkbox" id="offset_chk">Offset:
+      <input type="text" size=5 id="offset_text">
+    <input type="checkbox" id="rparty_chk">With:
+      <input type="text" size=25 id="rparty_text">
+  </form>
+  <button id="clearscr_btn" onclick="clearScreen()" >Clear Screen</button>
+  <button id="removeall_btn" onclick="removeAll()"  >Remove All</button>
+  <button id="remove_btn" onclick="removeEntries()"  >Remove:</button>
+      <input type="text" size=5 id="remove_text">
+  <button id="addlistener_btn" onclick="addListener()" >Add listener</button>
+  <button id="removelistener_btn" onclick="removeListener()" >Remove listener
+  </button>
+  <br>
+  <textarea cols=80 rows=30 id="output"></textarea>
+</body>
+
+<!--
+///////////////////////////////////////////////////////////////////////////////
+// simulation of tizen environment on desktop
+- ->
+<script>
+var extension = function() {};
+extension.messageListeners = [];
+extension.message = "";
+extension.postMessage = function(msg) {
+    console.log("extension.postMessage(" + msg + ")");
+}
+extension.setMessageListener = function(fun) {
+    console.log("extension.setMessageListener(" + fun.toString() + ")");
+    extension.messageListeners.push(fun);
+}
+var exports = new Object();
+var tizen = new Object();
+
+tizen.WebAPIException = {};
+tizen.WebAPIError = {};
+
+</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]
+    });
+</script>
+
+<script type="text/javascript" src="../callhistory/callhistory_api.js">
+</script>
+
+<script>
+tizen.callhistory = function() {};
+tizen.callhistory.find = exports.find;
+tizen.callhistory.remove = exports.remove;
+tizen.callhistory.removeBatch = exports.removeBatch;
+tizen.callhistory.removeAll = exports.removeAll;
+tizen.callhistory.addChangeListener = exports.addChangeListener;
+tizen.callhistory.removeChangeListener = exports.removeChangeListener;
+tizen.callhistory.SortMode = exports.SortMode;
+</script>
+<!- -
+///////////////////////////////////////////////////////////////////////////////
+// end of simulation of tizen environment
+-->
+
+<script>
+  var output = document.getElementById('output');
+  var filter = null;
+  var sortMode = null;
+  var limit = null;
+  var offset = null;
+  var lastSeenList = null;
+
+  // definitions for call direction
+  // temporary; the API should provide it
+  var str_dialed = 'DIALED';
+  var str_received = 'RECEIVED';
+  var str_missed = 'MISSED';
+  var str_missed_new = 'MISSEDNEW';
+  var str_rejected = 'REJECTED';
+  var str_blocked = 'BLOCKED';
+
+  function goBack() {
+    window.history.back();
+  }
+
+  function clearScreen() {
+    output.value = '';
+  }
+
+  function onException(error, text) {
+    var t = text == undefined ? '' : text;
+    print('\nException: ' + error.name +
+          '; ' + error.message + '; ' + t);
+  }
+
+  function onError(error) {
+    print('\nError: ' + error.message);
+  }
+
+  function printVal(thing, depth) {
+    var out, key;
+    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) {
+        out += '\n' + tabs + '\t' + key + ': ' +
+               printVal(thing[key], depth + 1);
+      }
+      out += '\n' + tabs + '}';
+    } else {
+      out = thing;
+    }
+    return out;
+  }
+
+  function print(o) {
+    var output = document.getElementById('output');
+    if (output)
+      output.value += '\n' + printVal(o, 0);
+  }
+
+  function displayEntryList(array) {
+    var output = document.getElementById('output');
+    if (!output)
+      return;
+    output.value += '\nResults (count = ' + array.length + '): ' +
+                    printVal(array, 0);
+    if (array.length > 0)
+      lastSeenList = array;
+  }
+
+  function is_integer(value) {
+    return isFinite(value) && !isNaN(parseInt(value));
+  }
+  function getIntVal(name) {
+    var text = document.getElementById(name + '_text');
+    var count = parseInt(text.value);
+    if (isFinite(count) && !isNaN(count) && count >= 0)
+      return count;
+    return null;
+  }
+
+  function testLimit() {
+    limit = null;
+    var chkbox = document.getElementById('limit_chk');
+    if (chkbox.checked)
+      limit = getIntVal('limit');
+    if (limit)
+      print(', limit=' + limit);
+  }
+
+  function testOffset() {
+    offset = null;
+    var chkbox = document.getElementById('offset_chk');
+    if (chkbox.checked)
+      offset = getIntVal('offset');
+    if (offset)
+      print(', offset=' + offset);
+  }
+
+  function testRemotePartyFilter(dir) {
+    var rpartyValue = (document.getElementById('rparty_chk').checked ?
+                       document.getElementById('rparty_text').value : null);
+    filter = null;
+    var direction = '';
+    try {
+      if (rpartyValue) {
+        print(', with: ' + rpartyValue);
+        filter =
+            new tizen.AttributeFilter('remoteParties', 'ENDSWITH', rpartyValue);
+      }
+      if (dir) {
+        var dirFilter = new tizen.AttributeFilter('direction', 'EXACTLY', dir);
+        filter = filter ? new tizen.CompositeFilter('INTERSECTION',
+            [filter, dirFilter]) : dirFilter;
+      }
+    } catch (err) {
+      onException(err, 'failed to set up filters');
+    }
+  }
+
+  function setFilters(dir) {
+    testLimit();
+    testOffset();
+    testRemotePartyFilter(dir);
+
+    sortMode = null;
+    try {
+      sortMode = new tizen.SortMode('startTime', 'DESC');
+    } catch (err) {
+      onException(err, 'tizen.SortMode');
+    }
+  }
+
+  function query_and_display() {
+    try {
+      tizen.callhistory.find(displayEntryList,
+                             onError,
+                             filter,
+                             sortMode,
+                             limit,
+                             offset);
+    } catch (err) {
+      onException(err, 'tizen.callhistory.find');
+    }
+  }
+
+  function readAll()
+  {
+    clearScreen();
+    print('Reading all call history');
+    setFilters();
+    query_and_display();
+  }
+
+  function readDialed()
+  {
+    clearScreen();
+    print('Reading dialed calls');
+    setFilters(str_dialed);
+    query_and_display();
+  }
+
+  function readReceived()
+  {
+    clearScreen();
+    print('Reading received calls');
+    setFilters(str_received);
+    query_and_display();
+  }
+
+  function readMissed()
+  {
+    clearScreen();
+    print('Reading missed calls');
+    setFilters(str_missed);
+    query_and_display();
+  }
+
+  function readMissedNew()
+  {
+    clearScreen();
+    print('Reading missed calls');
+    setFilters(str_missed_new);
+    query_and_display();
+  }
+
+  function readBlocked()
+  {
+    clearScreen();
+    print('Reading blocked calls');
+    setFilters(str_blocked);
+    query_and_display();
+  }
+
+  function readRejected()
+  {
+    clearScreen();
+    print('Reading rejected calls');
+    setFilters(str_rejected);
+    query_and_display();
+  }
+
+  function removeAll() {
+    clearScreen();
+    print('Removing all call history');
+    try {
+      tizen.callhistory.removeAll(
+          function() { print('removeAll successful'); }, onError);
+    } catch (err) {
+      onException(err, 'tizen.callhistory.removeAll failed');
+    }
+  }
+
+  function removeEntries() {
+    clearScreen();
+    var count = getIntVal('remove');
+
+    try {
+      setFilters();
+      if (count == 1) {
+        print('Removing first entry from last results list');
+        if (!lastSeenList || lastSeenList.length < count) {
+          print('Empty results list; please press e.g. "All" first');
+          return;
+        }
+        for (var key in lastSeenList) {
+          if (count-- > 0) {
+            print('Invoking tizen.callhistory.remove()');
+            print('removing ' + lastSeenList[key]);
+            tizen.callhistory.remove(lastSeenList[key]);
+          }
+        }
+      } else if (count > 1) {
+        print('Removing ' + count + ' entries');
+        if (!lastSeenList || lastSeenList.length < count) {
+          print('Not enough records in last seen list; try pressing "All"');
+          return;
+        }
+        var batch = [];
+        for (var key in lastSeenList) {
+          if (count-- > 0)
+            batch.push(lastSeenList[key]);
+        }
+        print('Invoking tizen.callhistory.removeBatch()');
+        tizen.callhistory.removeBatch(batch,
+            function() { print('removeBatch successful'); }, onError);
+      } else
+        print('Invalid count, nothing removed');
+    } catch (err) {
+      onException(err);
+    }
+  }
+
+  function Listener() {}
+  Listener.prototype = {
+    onadded: function(list) {
+      print('[Event] entries added');
+      displayEntryList(list);
+    },
+    onchanged: function(list) {
+      print('[Event] entries changed');
+      displayEntryList(list);
+    },
+    onremoved: function(list) {
+      print('[Event] entries removed');
+      displayEntryList(list);
+    }
+  };
+
+  var listenerHandles = [];
+
+  function addListener() {
+    print('Adding listener...');
+    if (listenerHandles.length > 4) {
+      print('Max 4 listeners allowed');
+      return;
+    }
+    var obs = new Listener();
+    try {
+      var handle = tizen.callhistory.addChangeListener(obs);
+      listenerHandles.push(handle);
+      print('Added listener ' + handle);
+    } catch (err) {
+      onException(err, 'tizen.callhistory.addChangeListener not available');
+    }
+  }
+
+  function removeListener() {
+    print('Removing most recently added listener...');
+    if (listenerHandles.length == 0) {
+      print('No listeners.');
+      return;
+    }
+    try {
+      var handle = listenerHandles.pop(listenerHandles.length - 1);
+      tizen.callhistory.removeChangeListener(handle);
+      print('Removed listener ' + handle);
+    } catch (err) {
+      onException(err, 'tizen.callhistory.removeChangeListener not available');
+    }
+  }
+
+  print('Call History Test\n' + '\nNotes:\n' +
+      '  * Remove: before testing removing items, press "All" ' +
+      '  * Remove: testing with count = 1 uses tizen.callhistory.remove()\n' +
+      '  * Remove: testing with count > 1 uses tizen.callhistory.removeBatch()\n' +
+      '  * Use "Clear Screen" manually before adding/removing listeners'
+  );
+</script>
+</html>
index e7ff5c8..5be6692 100644 (file)
@@ -30,5 +30,6 @@ div.block {
 <a href="download.html"><div class="block">download</div></a>
 <a href="filesystem.html"><div class="block">filesystem</div></a>
 <a href="application.html"><div class="block">application</div></a>
+<a href="callhistory.html"><div class="block">Call History</div></a>
 </body>
 </html>
index c44ab09..72f6a62 100644 (file)
@@ -35,7 +35,10 @@ BuildRequires: pkgconfig(capi-system-system-settings)
 # For IVI, it doesn't need sim package.
 %bcond_with ivi
 %if !%{with ivi}
-BuildRequires:  pkgconfig(capi-telephony-sim)
+BuildRequires: pkgconfig(capi-telephony-sim)
+BuildRequires: pkgconfig(tapi)
+BuildRequires: pkgconfig(contacts-service2)
+BuildRequires: pkgconfig(libpcrecpp)
 %endif
 BuildRequires: pkgconfig(capi-web-favorites)
 BuildRequires: pkgconfig(capi-web-url-download)
@@ -43,7 +46,6 @@ BuildRequires: pkgconfig(dbus-glib-1)
 # Evas.h is required by capi-web-favorites.
 BuildRequires: pkgconfig(evas)
 BuildRequires: pkgconfig(glib-2.0)
-BuildRequires: pkgconfig(tapi)
 BuildRequires: pkgconfig(libudev)
 BuildRequires: pkgconfig(message-port)
 BuildRequires: pkgconfig(notification)
index b5dc7d9..a04112b 100644 (file)
@@ -22,6 +22,7 @@
         [ 'extension_host_os == "mobile"', {
           'dependencies': [
             'application/application.gyp:*',
+            'callhistory/callhistory.gyp:*',
             'download/download.gyp:*',
             'bookmark/bookmark.gyp:*',
             'messageport/messageport.gyp:*',
index e38a5c3..ba805e9 100644 (file)
@@ -46,6 +46,20 @@ enum WebApiAPIErrors {
   IO_ERR = 101,
   PERMISSION_DENIED_ERR = 102,
   SERVICE_NOT_AVAILABLE_ERR = 103,
+  DATABASE_ERR = 104,
 };
 
+#define STR_MATCH_EXACTLY        "EXACTLY"
+#define STR_MATCH_FULLSTRING     "FULLSTRING"
+#define STR_MATCH_CONTAINS       "CONTAINS"
+#define STR_MATCH_STARTSWITH     "STARTSWITH"
+#define STR_MATCH_ENDSWITH       "ENDSWITH"
+#define STR_MATCH_EXISTS         "EXISTS"
+
+#define STR_SORT_ASC             "ASC"
+#define STR_SORT_DESC            "DESC"
+
+#define STR_FILTEROP_OR          "UNION"
+#define STR_FILTEROP_AND         "INTERSECTION"
+
 #endif  // TIZEN_TIZEN_H_
index f9ae8b2..a9ce83e 100644 (file)
@@ -2,6 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+// Tizen API Specification:
+// https://developer.tizen.org/dev-guide/2.2.1/org.tizen.web.device.apireference/tizen/tizen.html
+
+
 // WARNING! This list should be in sync with the equivalent enum
 // located at tizen.h. Remember to update tizen.h if you change
 // something here.
@@ -39,7 +43,8 @@ var errors = {
   '100': { type: 'INVALID_VALUES_ERR', name: 'InvalidValuesError', message: '' },
   '101': { type: 'IO_ERR', name: 'IOError', message: 'IOError' },
   '102': { type: 'PERMISSION_DENIED_ERR', name: 'Permission_deniedError', message: '' },
-  '103': { type: 'SERVICE_NOT_AVAILABLE_ERR', name: 'ServiceNotAvailableError', message: '' }
+  '103': { type: 'SERVICE_NOT_AVAILABLE_ERR', name: 'ServiceNotAvailableError', message: '' },
+  '104': { type: 'DATABASE_ERR', name: 'DATABASE_ERR', message: '' }
 };
 
 exports.WebAPIException = function(code, message, name) {
@@ -116,3 +121,133 @@ exports.ApplicationControl = function(operation, uri, mime, category, data) {
   this.category = category;
   this.data = data || [];
 };
+
+// Tizen Filters
+
+// either AttributeFilter, AttributeRangeFilter, or CompositeFilter
+function is_tizen_filter(f) {
+  return (f instanceof tizen.AttributeFilter) ||
+         (f instanceof tizen.AttributeRangeFilter) ||
+         (f instanceof tizen.CompositeFilter);
+}
+
+// AbstractFilter (abstract base class)
+exports.AbstractFilter = function() {};
+
+// SortMode
+// [Constructor(DOMString attributeName, optional SortModeOrder? order)]
+// interface SortMode {
+//   attribute DOMString attributeName;
+//   attribute SortModeOrder order;
+// };
+exports.SortMode = function(attrName, order) {
+  if (!(typeof(attrName) === 'string' || attrname instanceof String) ||
+      order && (order != 'DESC' && order != 'ASC'))
+    throw new exports.WebAPIException(exports.WebAPIException.TYPE_MISMATCH_ERR);
+
+  Object.defineProperties(this, {
+    'attributeName': { writable: false, enumerable: true, value: attrName },
+    'order': { writable: false, enumerable: true, value: order || 'ASC' }
+  });
+};
+exports.SortMode.prototype.constructor = exports.SortMode;
+
+// AttributeFilter
+// [Constructor(DOMString attributeName, optional FilterMatchFlag? matchFlag,
+//              optional any matchValue)]
+// interface AttributeFilter : AbstractFilter {
+//   attribute DOMString attributeName;
+//   attribute FilterMatchFlag matchFlag;
+//   attribute any matchValue;
+// };
+
+var FilterMatchFlag = {
+  EXACTLY: 0,
+  FULLSTRING: 1,
+  CONTAINS: 2,
+  STARTSWITH: 3,
+  ENDSWITH: 4,
+  EXISTS: 5
+};
+
+exports.AttributeFilter = function(attrName, matchFlag, matchValue) {
+  if (this && this.constructor == exports.AttributeFilter &&
+      (typeof(attrName) === 'string' || attrname instanceof String) &&
+      matchFlag && matchFlag in FilterMatchFlag) {
+    Object.defineProperties(this, {
+      'attributeName': { writable: false, enumerable: true, value: attrName },
+      'matchFlag': {
+        writable: false,
+        enumerable: true,
+        value: matchValue !== undefined ? (matchFlag ? matchFlag : 'EXACTLY') : 'EXISTS'
+      },
+      'matchValue': {
+        writable: false,
+        enumerable: true,
+        value: matchValue === undefined ? null : matchValue
+      }
+    });
+  } else {
+    throw new exports.WebAPIException(exports.WebAPIException.TYPE_MISMATCH_ERR);
+  }
+};
+exports.AttributeFilter.prototype = new exports.AbstractFilter();
+exports.AttributeFilter.prototype.constructor = exports.AttributeFilter;
+
+
+// AttributeRangeFilter
+// [Constructor(DOMString attributeName, optional any initialValue,
+//              optional any endValue)]
+// interface AttributeRangeFilter : AbstractFilter {
+//   attribute DOMString attributeName;
+//   attribute any initialValue;
+//   attribute any endValue;
+// };
+exports.AttributeRangeFilter = function(attrName, start, end) {
+  if (!this || this.constructor != exports.AttributeRangeFilter ||
+      !(typeof(attrName) === 'string' || attrname instanceof String)) {
+    throw new exports.WebAPIException(exports.WebAPIException.TYPE_MISMATCH_ERR);
+  }
+
+  Object.defineProperties(this, {
+    'attributeName': { writable: true, enumerable: true, value: attrName },
+    'initialValue': {
+      writable: true,
+      enumerable: true,
+      value: start === undefined ? null : start },
+    'endValue': { writable: true, enumerable: true, value: end === undefined ? null : end }
+  });
+};
+exports.AttributeRangeFilter.prototype = new exports.AbstractFilter();
+exports.AttributeRangeFilter.prototype.constructor = exports.AttributeRangeFilter;
+
+
+// CompositeFilter
+// [Constructor(CompositeFilterType type, optional AbstractFilter[]? filters)]
+// interface CompositeFilter : AbstractFilter {
+//   attribute CompositeFilterType type;
+//   attribute AbstractFilter[] filters;
+// };
+
+var CompositeFilterType = { UNION: 0, INTERSECTION: 1 };
+
+exports.CompositeFilter = function(type, filters) {
+  if (!this || this.constructor != exports.CompositeFilter ||
+      !(type in CompositeFilterType) ||
+      filters && !(filters instanceof Array)) {
+    throw new exports.WebAPIException(exports.WebAPIException.TYPE_MISMATCH_ERR);
+  }
+
+  Object.defineProperties(this, {
+    'type': { writable: false, enumerable: true, value: type },
+    'filters': {
+      writable: false,
+      enumerable: true,
+      value: filters === undefined ? null : filters
+    }
+  });
+};
+exports.CompositeFilter.prototype = new exports.AbstractFilter();
+exports.CompositeFilter.prototype.constructor = exports.CompositeFilter;
+
+// end of Tizen filters