--- /dev/null
+// 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());
+}
--- /dev/null
+{
+ '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',
+ ],
+ }],
+ ],
+ },
+ ],
+}
--- /dev/null
+// 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_
--- /dev/null
+// 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);});
+ }
+};
--- /dev/null
+// 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;
+}
--- /dev/null
+<!--
+// 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>
<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>
# 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)
# 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)
[ 'extension_host_os == "mobile"', {
'dependencies': [
'application/application.gyp:*',
+ 'callhistory/callhistory.gyp:*',
'download/download.gyp:*',
'bookmark/bookmark.gyp:*',
'messageport/messageport.gyp:*',
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_
// 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.
'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) {
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