You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by br...@apache.org on 2013/07/23 22:23:12 UTC
[1/6] [CB-4341] Adding a fix to make subdirectories work within a
local plugin dependency - Includes the integration of integration specs which
test installation of plugins with dependencies
Updated Branches:
refs/heads/master f2fa75132 -> 21b6d79b3
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/src/install.js
----------------------------------------------------------------------
diff --git a/src/install.js b/src/install.js
index 13aa9bb..fd0f9de 100644
--- a/src/install.js
+++ b/src/install.js
@@ -198,6 +198,8 @@ function runInstall(actions, platform, project_dir, plugin_dir, plugins_dir, opt
}
var dep_url = path.join(result.output.trim(), dep_subdir);
+ //Clear out the subdir since the url now contains it
+ dep_subdir = "";
shell.cd(old_pwd);
} else if (fetchdata.source.type === 'git') {
dep_url = fetchdata.source.url;
[6/6] git commit: [CB-4341] Adding a fix to make subdirectories work
within a local plugin dependency - Includes the integration of integration
specs which test installation of plugins with dependencies
Posted by br...@apache.org.
[CB-4341] Adding a fix to make subdirectories work within a local plugin dependency
- Includes the integration of integration specs which test installation
of plugins with dependencies
Project: http://git-wip-us.apache.org/repos/asf/cordova-plugman/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-plugman/commit/21b6d79b
Tree: http://git-wip-us.apache.org/repos/asf/cordova-plugman/tree/21b6d79b
Diff: http://git-wip-us.apache.org/repos/asf/cordova-plugman/diff/21b6d79b
Branch: refs/heads/master
Commit: 21b6d79b32a3dbce29e85d1b67f547351a1bf11b
Parents: f2fa751
Author: Jeffrey Heifetz <jh...@blackberry.com>
Authored: Mon Jul 22 15:23:46 2013 -0400
Committer: Jeffrey Heifetz <jh...@blackberry.com>
Committed: Tue Jul 23 14:53:43 2013 -0400
----------------------------------------------------------------------
.gitignore | 2 +
spec/install.spec.js | 8 +-
spec/integration.spec.js | 85 +
spec/plugins/Contacts/plugin.xml | 143 ++
.../Contacts/src/android/ContactAccessor.java | 198 ++
.../src/android/ContactAccessorSdk5.java | 2183 ++++++++++++++++++
.../Contacts/src/android/ContactManager.java | 122 +
.../src/blackberry10/ContactActivity.js | 26 +
.../Contacts/src/blackberry10/ContactAddress.js | 30 +
.../Contacts/src/blackberry10/ContactError.js | 30 +
.../Contacts/src/blackberry10/ContactField.js | 27 +
.../src/blackberry10/ContactFindOptions.js | 50 +
.../Contacts/src/blackberry10/ContactName.js | 39 +
.../Contacts/src/blackberry10/ContactNews.js | 26 +
.../src/blackberry10/ContactOrganization.js | 22 +
.../Contacts/src/blackberry10/ContactPhoto.js | 23 +
.../Contacts/src/blackberry10/contactConsts.js | 225 ++
.../Contacts/src/blackberry10/contactUtils.js | 223 ++
spec/plugins/Contacts/src/blackberry10/index.js | 374 +++
.../Contacts/src/blackberry10/plugin.xml | 41 +
spec/plugins/Contacts/src/ios/CDVContact.h | 136 ++
spec/plugins/Contacts/src/ios/CDVContact.m | 1752 ++++++++++++++
spec/plugins/Contacts/src/ios/CDVContacts.h | 151 ++
spec/plugins/Contacts/src/ios/CDVContacts.m | 593 +++++
spec/plugins/Contacts/src/wp/Contacts.cs | 664 ++++++
spec/plugins/Contacts/www/Contact.js | 177 ++
spec/plugins/Contacts/www/ContactAddress.js | 46 +
spec/plugins/Contacts/www/ContactError.js | 42 +
spec/plugins/Contacts/www/ContactField.js | 37 +
spec/plugins/Contacts/www/ContactFindOptions.js | 34 +
spec/plugins/Contacts/www/ContactName.js | 41 +
.../plugins/Contacts/www/ContactOrganization.js | 44 +
spec/plugins/Contacts/www/contacts.js | 76 +
spec/plugins/Contacts/www/ios/Contact.js | 51 +
spec/plugins/Contacts/www/ios/contacts.js | 62 +
spec/plugins/dependencies/B/plugin.xml | 8 +-
spec/plugins/dependencies/E/plugin.xml | 57 -
spec/plugins/dependencies/E/src/android/E.java | 0
.../dependencies/E/src/ios/EPluginCommand.h | 0
.../dependencies/E/src/ios/EPluginCommand.m | 0
spec/plugins/dependencies/E/www/plugin-e.js | 0
spec/plugins/dependencies/subdir/E/plugin.xml | 57 +
.../dependencies/subdir/E/src/android/E.java | 0
.../subdir/E/src/ios/EPluginCommand.h | 0
.../subdir/E/src/ios/EPluginCommand.m | 0
.../dependencies/subdir/E/www/plugin-e.js | 0
.../blackberry10/native/device/chrome/.gitkeep | 0
.../native/device/plugins/jnext/auth.txt | 3 +
.../native/simulator/chrome/.gitkeep | 0
.../native/simulator/plugins/jnext/auth.txt | 3 +
src/install.js | 2 +
51 files changed, 7849 insertions(+), 64 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index 9daa824..7dbb168 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
.DS_Store
node_modules
+.gitkeep
+.tmp
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/install.spec.js
----------------------------------------------------------------------
diff --git a/spec/install.spec.js b/spec/install.spec.js
index 33bfcc8..c48db9a 100644
--- a/spec/install.spec.js
+++ b/spec/install.spec.js
@@ -103,17 +103,19 @@ describe('install', function() {
expect(proc.calls.length).toEqual(3);
});
it('should fetch any dependent plugins if missing', function() {
- var s = spyOn(plugman, 'fetch').andCallFake(function(id, dir, opts, cb) {
+ var deps_dir = path.join(plugins_dir, 'dependencies'),
+ s = spyOn(plugman, 'fetch').andCallFake(function(id, dir, opts, cb) {
cb(false, path.join(dir, id));
});
exists.andReturn(false);
// Plugin A depends on C & D
- install('android', temp, 'A', path.join(plugins_dir, 'dependencies'), {});
+ install('android', temp, 'A', deps_dir, {});
+ expect(s).toHaveBeenCalledWith('C', deps_dir, { link: false, subdir: undefined, git_ref: undefined}, jasmine.any(Function));
expect(s.calls.length).toEqual(3);
});
});
});
-
+
describe('failure', function() {
it('should throw if platform is unrecognized', function() {
expect(function() {
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/integration.spec.js
----------------------------------------------------------------------
diff --git a/spec/integration.spec.js b/spec/integration.spec.js
new file mode 100644
index 0000000..54b578e
--- /dev/null
+++ b/spec/integration.spec.js
@@ -0,0 +1,85 @@
+var plugman = require('../plugman'),
+ path = require('path'),
+ fs = require('fs'),
+ shell = require('shelljs'),
+ temp = path.join(__dirname, "..", ".tmp"),
+ plugins_dir = path.join(temp, 'plugins');
+
+describe("integration", function () {
+ beforeEach(function () {
+ if ( !fs.existsSync(temp)) {
+ shell.mkdir("-p", temp);
+ }
+ });
+ describe("local non-root depdencies", function () {
+ var project_src = path.join(__dirname, "projects", "android_one", "*"),
+ plugin_src = path.join(__dirname, "plugins", "dependencies", "B"),
+ project_dir = path.join(temp, "android");
+
+ beforeEach(function () {
+ shell.cp("-rf", project_src, project_dir);
+ });
+
+ it("should install dependencies from github", function () {
+ var flag = false,
+ installData;
+
+ runs(function () {
+ plugman.install('android', project_dir, plugin_src, plugins_dir, {}, function (error) {
+ expect(error).not.toBeDefined();
+ flag = true;
+ });
+ });
+ waitsFor(function () { return flag; }, "plugman install to finish", 10000);
+ runs(function () {
+ installData = require(path.join(plugins_dir, "android.json"));
+ expect(installData.installed_plugins).toEqual({ 'B': { PACKAGE_NAME: 'com.alunny.childapp'}});
+ expect(installData.dependent_plugins).toEqual({
+ 'D' : { PACKAGE_NAME: 'com.alunny.childapp'},
+ 'E': { PACKAGE_NAME: 'com.alunny.childapp'}
+ });
+ });
+ //Cleanup
+ this.after(function () {
+ shell.rm("-rf", project_dir);
+ });
+ });
+ });
+
+ describe("blackberry10", function () {
+ var project_src = path.join(__dirname, "projects", "blackberry10", "*"),
+ plugin_src = path.join(__dirname, "plugins", "Contacts"),
+ project_dir = path.join(temp, "blackberry10");
+
+ beforeEach(function () {
+ shell.cp("-rf", project_src, project_dir);
+ });
+
+ it("should install dependencies from github", function () {
+ var flag = false,
+ installData;
+
+ runs(function () {
+ plugman.install('blackberry10', project_dir, plugin_src, plugins_dir, {}, function (error) {
+ expect(error).not.toBeDefined();
+ flag = true;
+ });
+ });
+ waitsFor(function () { return flag; }, "plugman install to finish", 10000);
+ runs(function () {
+ installData = require(path.join(plugins_dir, "blackberry10.json"));
+ expect(installData.installed_plugins).toEqual({ 'org.apache.cordova.core.contacts': { PACKAGE_NAME: 'cordovaExample'}});
+ expect(installData.dependent_plugins).toEqual({
+ 'com.blackberry.utils' : { PACKAGE_NAME: 'cordovaExample'},
+ 'org.apache.cordova.blackberry10.pimlib': { PACKAGE_NAME: 'cordovaExample'}
+ });
+ });
+ //Cleanup
+ this.after(function () {
+ shell.rm("-rf", project_dir);
+ shell.rm("-rf", temp);
+ });
+ });
+ });
+
+});
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/plugin.xml
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/plugin.xml b/spec/plugins/Contacts/plugin.xml
new file mode 100644
index 0000000..7328f1a
--- /dev/null
+++ b/spec/plugins/Contacts/plugin.xml
@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<plugin xmlns="http://cordova.apache.org/ns/plugins/1.0"
+ id="org.apache.cordova.core.contacts"
+ version="0.1.0">
+ <name>Contacts</name>
+
+
+ <js-module src="www/contacts.js" name="contacts">
+ <clobbers target="navigator.contacts" />
+ </js-module>
+
+ <js-module src="www/Contact.js" name="Contact">
+ <clobbers target="Contact" />
+ </js-module>
+
+ <js-module src="www/ContactAddress.js" name="ContactAddress">
+ <clobbers target="ContactAddress" />
+ </js-module>
+
+ <js-module src="www/ContactError.js" name="ContactError">
+ <clobbers target="ContactError" />
+ </js-module>
+
+ <js-module src="www/ContactField.js" name="ContactField">
+ <clobbers target="ContactField" />
+ </js-module>
+
+ <js-module src="www/ContactFindOptions.js" name="ContactFindOptions">
+ <clobbers target="ContactFindOptions" />
+ </js-module>
+
+ <js-module src="www/ContactName.js" name="ContactName">
+ <clobbers target="ContactName" />
+ </js-module>
+
+ <js-module src="www/ContactOrganization.js" name="ContactOrganization">
+ <clobbers target="ContactOrganization" />
+ </js-module>
+
+
+
+
+ <!-- android -->
+ <platform name="android">
+ <config-file target="res/xml/config.xml" parent="/*">
+ <feature name="Contacts">
+ <param name="android-package" value="org.apache.cordova.core.ContactManager"/>
+ </feature>
+ </config-file>
+
+ <config-file target="AndroidManifest.xml" parent="/*">
+ <uses-permission android:name="android.permission.READ_CONTACTS" />
+ <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+ </config-file>
+
+ <source-file src="src/android/ContactAccessor.java" target-dir="src/org/apache/cordova/core" />
+ <source-file src="src/android/ContactAccessorSdk5.java" target-dir="src/org/apache/cordova/core" />
+ <source-file src="src/android/ContactManager.java" target-dir="src/org/apache/cordova/core" />
+ </platform>
+
+ <!-- ios -->
+ <platform name="ios">
+ <config-file target="config.xml" parent="/*">
+ <feature name="Contacts">
+ <param name="ios-package" value="CDVContacts"/>
+ </feature>
+ </config-file>
+
+ <js-module src="www/ios/contacts.js" name="contacts">
+ <merges target="navigator.contacts" />
+ </js-module>
+
+ <js-module src="www/ios/Contact.js" name="Contact">
+ <merges target="Contact" />
+ </js-module>
+
+ <header-file src="src/ios/CDVContacts.h" />
+ <source-file src="src/ios/CDVContacts.m" />
+ <header-file src="src/ios/CDVContact.h" />
+ <source-file src="src/ios/CDVContact.m" />
+ </platform>
+
+ <!-- blackberry10 -->
+ <platform name="blackberry10">
+ <config-file target="www/config.xml" parent="/widget">
+ <feature name="Contacts" value="Contacts"/>
+ </config-file>
+ <config-file target="www/config.xml" parent="/widget">
+ <rim:permissions>
+ </rim:permissions>
+ </config-file>
+ <config-file target="www/config.xml" parent="/widget/rim:permissions">
+ <rim:permit>access_pimdomain_contacts</rim:permit>
+ </config-file>
+ <source-file src="src/blackberry10/index.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/ContactActivity.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/ContactAddress.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/contactConsts.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/ContactError.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/ContactField.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/ContactFindOptions.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/ContactName.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/ContactNews.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/ContactOrganization.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/ContactPhoto.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/contactUtils.js" target-dir="Contacts"></source-file>
+ <dependency id="com.blackberry.utils" url="https://github.com/blackberry/cordova-blackberry-plugins.git" commit="plugins" subdir="plugin/com.blackberry.utils"/>
+ <dependency id="org.apache.cordova.blackberry10.pimlib" url="https://github.com/blackberry/cordova-blackberry-plugins.git" commit="plugins" subdir="/plugin/org.apache.cordova.blackberry10.pimlib/"/>
+ </platform>
+
+ <!-- wp7 -->
+ <platform name="wp7">
+ <config-file target="config.xml" parent="/*">
+ <feature name="Contacts">
+ <param name="wp-package" value="Contacts"/>
+ </feature>
+ </config-file>
+
+ <config-file target="Properties/WMAppManifest.xml" parent="/Deployment/App/Capabilities">
+ <Capability Name="ID_CAP_CONTACTS" />
+ </config-file>
+
+ <source-file src="src/wp/Contacts.cs" />
+ </platform>
+
+ <!-- wp8 -->
+ <platform name="wp8">
+ <config-file target="config.xml" parent="/*">
+ <feature name="Contacts">
+ <param name="wp-package" value="Contacts"/>
+ </feature>
+ </config-file>
+
+ <config-file target="Properties/WMAppManifest.xml" parent="/Deployment/App/Capabilities">
+ <Capability Name="ID_CAP_CONTACTS" />
+ </config-file>
+
+ <source-file src="src/wp/Contacts.cs" />
+ </platform>
+
+</plugin>
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/android/ContactAccessor.java
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/android/ContactAccessor.java b/spec/plugins/Contacts/src/android/ContactAccessor.java
new file mode 100644
index 0000000..24ef9c6
--- /dev/null
+++ b/spec/plugins/Contacts/src/android/ContactAccessor.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cordova.core;
+
+import java.util.HashMap;
+
+import android.util.Log;
+import android.webkit.WebView;
+
+import org.apache.cordova.CordovaInterface;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * This abstract class defines SDK-independent API for communication with
+ * Contacts Provider. The actual implementation used by the application depends
+ * on the level of API available on the device. If the API level is Cupcake or
+ * Donut, we want to use the {@link ContactAccessorSdk3_4} class. If it is
+ * Eclair or higher, we want to use {@link ContactAccessorSdk5}.
+ */
+public abstract class ContactAccessor {
+
+ protected final String LOG_TAG = "ContactsAccessor";
+ protected CordovaInterface mApp;
+ protected WebView mView;
+
+ /**
+ * Check to see if the data associated with the key is required to
+ * be populated in the Contact object.
+ * @param key
+ * @param map created by running buildPopulationSet.
+ * @return true if the key data is required
+ */
+ protected boolean isRequired(String key, HashMap<String,Boolean> map) {
+ Boolean retVal = map.get(key);
+ return (retVal == null) ? false : retVal.booleanValue();
+ }
+
+ /**
+ * Create a hash map of what data needs to be populated in the Contact object
+ * @param fields the list of fields to populate
+ * @return the hash map of required data
+ */
+ protected HashMap<String,Boolean> buildPopulationSet(JSONArray fields) {
+ HashMap<String,Boolean> map = new HashMap<String,Boolean>();
+
+ String key;
+ try {
+ if (fields.length() == 1 && fields.getString(0).equals("*")) {
+ map.put("displayName", true);
+ map.put("name", true);
+ map.put("nickname", true);
+ map.put("phoneNumbers", true);
+ map.put("emails", true);
+ map.put("addresses", true);
+ map.put("ims", true);
+ map.put("organizations", true);
+ map.put("birthday", true);
+ map.put("note", true);
+ map.put("urls", true);
+ map.put("photos", true);
+ map.put("categories", true);
+ }
+ else {
+ for (int i=0; i<fields.length(); i++) {
+ key = fields.getString(i);
+ if (key.startsWith("displayName")) {
+ map.put("displayName", true);
+ }
+ else if (key.startsWith("name")) {
+ map.put("displayName", true);
+ map.put("name", true);
+ }
+ else if (key.startsWith("nickname")) {
+ map.put("nickname", true);
+ }
+ else if (key.startsWith("phoneNumbers")) {
+ map.put("phoneNumbers", true);
+ }
+ else if (key.startsWith("emails")) {
+ map.put("emails", true);
+ }
+ else if (key.startsWith("addresses")) {
+ map.put("addresses", true);
+ }
+ else if (key.startsWith("ims")) {
+ map.put("ims", true);
+ }
+ else if (key.startsWith("organizations")) {
+ map.put("organizations", true);
+ }
+ else if (key.startsWith("birthday")) {
+ map.put("birthday", true);
+ }
+ else if (key.startsWith("note")) {
+ map.put("note", true);
+ }
+ else if (key.startsWith("urls")) {
+ map.put("urls", true);
+ }
+ else if (key.startsWith("photos")) {
+ map.put("photos", true);
+ }
+ else if (key.startsWith("categories")) {
+ map.put("categories", true);
+ }
+ }
+ }
+ }
+ catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+ return map;
+ }
+
+ /**
+ * Convenience method to get a string from a JSON object. Saves a
+ * lot of try/catch writing.
+ * If the property is not found in the object null will be returned.
+ *
+ * @param obj contact object to search
+ * @param property to be looked up
+ * @return The value of the property
+ */
+ protected String getJsonString(JSONObject obj, String property) {
+ String value = null;
+ try {
+ if (obj != null) {
+ value = obj.getString(property);
+ if (value.equals("null")) {
+ Log.d(LOG_TAG, property + " is string called 'null'");
+ value = null;
+ }
+ }
+ }
+ catch (JSONException e) {
+ Log.d(LOG_TAG, "Could not get = " + e.getMessage());
+ }
+ return value;
+ }
+
+ /**
+ * Handles adding a JSON Contact object into the database.
+ * @return TODO
+ */
+ public abstract String save(JSONObject contact);
+
+ /**
+ * Handles searching through SDK-specific contacts API.
+ */
+ public abstract JSONArray search(JSONArray filter, JSONObject options);
+
+ /**
+ * Handles searching through SDK-specific contacts API.
+ * @throws JSONException
+ */
+ public abstract JSONObject getContactById(String id) throws JSONException;
+
+ /**
+ * Handles removing a contact from the database.
+ */
+ public abstract boolean remove(String id);
+
+ /**
+ * A class that represents the where clause to be used in the database query
+ */
+ class WhereOptions {
+ private String where;
+ private String[] whereArgs;
+ public void setWhere(String where) {
+ this.where = where;
+ }
+ public String getWhere() {
+ return where;
+ }
+ public void setWhereArgs(String[] whereArgs) {
+ this.whereArgs = whereArgs;
+ }
+ public String[] getWhereArgs() {
+ return whereArgs;
+ }
+ }
+}
[3/6] [CB-4341] Adding a fix to make subdirectories work within a
local plugin dependency - Includes the integration of integration specs which
test installation of plugins with dependencies
Posted by br...@apache.org.
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/ios/CDVContact.m
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/ios/CDVContact.m b/spec/plugins/Contacts/src/ios/CDVContact.m
new file mode 100644
index 0000000..82704ea
--- /dev/null
+++ b/spec/plugins/Contacts/src/ios/CDVContact.m
@@ -0,0 +1,1752 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ */
+
+#import "CDVContact.h"
+#import <Cordova/NSDictionary+Extensions.h>
+
+#define DATE_OR_NULL(dateObj) ((aDate != nil) ? (id)([aDate descriptionWithLocale:[NSLocale currentLocale]]) : (id)([NSNull null]))
+#define IS_VALID_VALUE(value) ((value != nil) && (![value isKindOfClass:[NSNull class]]))
+
+static NSDictionary* org_apache_cordova_contacts_W3CtoAB = nil;
+static NSDictionary* org_apache_cordova_contacts_ABtoW3C = nil;
+static NSSet* org_apache_cordova_contacts_W3CtoNull = nil;
+static NSDictionary* org_apache_cordova_contacts_objectAndProperties = nil;
+static NSDictionary* org_apache_cordova_contacts_defaultFields = nil;
+
+@implementation CDVContact : NSObject
+
+ @synthesize returnFields;
+
+- (id)init
+{
+ if ((self = [super init]) != nil) {
+ ABRecordRef rec = ABPersonCreate();
+ self.record = rec;
+ if (rec) {
+ CFRelease(rec);
+ }
+ }
+ return self;
+}
+
+- (id)initFromABRecord:(ABRecordRef)aRecord
+{
+ if ((self = [super init]) != nil) {
+ self.record = aRecord;
+ }
+ return self;
+}
+
+/* synthesize 'record' ourselves to have retain properties for CF types */
+
+- (void)setRecord:(ABRecordRef)aRecord
+{
+ if (record != NULL) {
+ CFRelease(record);
+ }
+ if (aRecord != NULL) {
+ record = CFRetain(aRecord);
+ }
+}
+
+- (ABRecordRef)record
+{
+ return record;
+}
+
+/* Rather than creating getters and setters for each AddressBook (AB) Property, generic methods are used to deal with
+ * simple properties, MultiValue properties( phone numbers and emails) and MultiValueDictionary properties (Ims and addresses).
+ * The dictionaries below are used to translate between the W3C identifiers and the AB properties. Using the dictionaries,
+ * allows looping through sets of properties to extract from or set into the W3C dictionary to/from the ABRecord.
+ */
+
+/* The two following dictionaries translate between W3C properties and AB properties. It currently mixes both
+ * Properties (kABPersonAddressProperty for example) and Strings (kABPersonAddressStreetKey) so users should be aware of
+ * what types of values are expected.
+ * a bit.
+*/
++ (NSDictionary*)defaultABtoW3C
+{
+ if (org_apache_cordova_contacts_ABtoW3C == nil) {
+ org_apache_cordova_contacts_ABtoW3C = [NSDictionary dictionaryWithObjectsAndKeys:
+ kW3ContactNickname, [NSNumber numberWithInt:kABPersonNicknameProperty],
+ kW3ContactGivenName, [NSNumber numberWithInt:kABPersonFirstNameProperty],
+ kW3ContactFamilyName, [NSNumber numberWithInt:kABPersonLastNameProperty],
+ kW3ContactMiddleName, [NSNumber numberWithInt:kABPersonMiddleNameProperty],
+ kW3ContactHonorificPrefix, [NSNumber numberWithInt:kABPersonPrefixProperty],
+ kW3ContactHonorificSuffix, [NSNumber numberWithInt:kABPersonSuffixProperty],
+ kW3ContactPhoneNumbers, [NSNumber numberWithInt:kABPersonPhoneProperty],
+ kW3ContactAddresses, [NSNumber numberWithInt:kABPersonAddressProperty],
+ kW3ContactStreetAddress, kABPersonAddressStreetKey,
+ kW3ContactLocality, kABPersonAddressCityKey,
+ kW3ContactRegion, kABPersonAddressStateKey,
+ kW3ContactPostalCode, kABPersonAddressZIPKey,
+ kW3ContactCountry, kABPersonAddressCountryKey,
+ kW3ContactEmails, [NSNumber numberWithInt:kABPersonEmailProperty],
+ kW3ContactIms, [NSNumber numberWithInt:kABPersonInstantMessageProperty],
+ kW3ContactOrganizations, [NSNumber numberWithInt:kABPersonOrganizationProperty],
+ kW3ContactOrganizationName, [NSNumber numberWithInt:kABPersonOrganizationProperty],
+ kW3ContactTitle, [NSNumber numberWithInt:kABPersonJobTitleProperty],
+ kW3ContactDepartment, [NSNumber numberWithInt:kABPersonDepartmentProperty],
+ kW3ContactBirthday, [NSNumber numberWithInt:kABPersonBirthdayProperty],
+ kW3ContactUrls, [NSNumber numberWithInt:kABPersonURLProperty],
+ kW3ContactNote, [NSNumber numberWithInt:kABPersonNoteProperty],
+ nil];
+ }
+
+ return org_apache_cordova_contacts_ABtoW3C;
+}
+
++ (NSDictionary*)defaultW3CtoAB
+{
+ if (org_apache_cordova_contacts_W3CtoAB == nil) {
+ org_apache_cordova_contacts_W3CtoAB = [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithInt:kABPersonNicknameProperty], kW3ContactNickname,
+ [NSNumber numberWithInt:kABPersonFirstNameProperty], kW3ContactGivenName,
+ [NSNumber numberWithInt:kABPersonLastNameProperty], kW3ContactFamilyName,
+ [NSNumber numberWithInt:kABPersonMiddleNameProperty], kW3ContactMiddleName,
+ [NSNumber numberWithInt:kABPersonPrefixProperty], kW3ContactHonorificPrefix,
+ [NSNumber numberWithInt:kABPersonSuffixProperty], kW3ContactHonorificSuffix,
+ [NSNumber numberWithInt:kABPersonPhoneProperty], kW3ContactPhoneNumbers,
+ [NSNumber numberWithInt:kABPersonAddressProperty], kW3ContactAddresses,
+ kABPersonAddressStreetKey, kW3ContactStreetAddress,
+ kABPersonAddressCityKey, kW3ContactLocality,
+ kABPersonAddressStateKey, kW3ContactRegion,
+ kABPersonAddressZIPKey, kW3ContactPostalCode,
+ kABPersonAddressCountryKey, kW3ContactCountry,
+ [NSNumber numberWithInt:kABPersonEmailProperty], kW3ContactEmails,
+ [NSNumber numberWithInt:kABPersonInstantMessageProperty], kW3ContactIms,
+ [NSNumber numberWithInt:kABPersonOrganizationProperty], kW3ContactOrganizations,
+ [NSNumber numberWithInt:kABPersonJobTitleProperty], kW3ContactTitle,
+ [NSNumber numberWithInt:kABPersonDepartmentProperty], kW3ContactDepartment,
+ [NSNumber numberWithInt:kABPersonBirthdayProperty], kW3ContactBirthday,
+ [NSNumber numberWithInt:kABPersonNoteProperty], kW3ContactNote,
+ [NSNumber numberWithInt:kABPersonURLProperty], kW3ContactUrls,
+ kABPersonInstantMessageUsernameKey, kW3ContactImValue,
+ kABPersonInstantMessageServiceKey, kW3ContactImType,
+ [NSNull null], kW3ContactFieldType, /* include entries in dictionary to indicate ContactField properties */
+ [NSNull null], kW3ContactFieldValue,
+ [NSNull null], kW3ContactFieldPrimary,
+ [NSNull null], kW3ContactFieldId,
+ [NSNumber numberWithInt:kABPersonOrganizationProperty], kW3ContactOrganizationName, /* careful, name is used multiple times*/
+ nil];
+ }
+ return org_apache_cordova_contacts_W3CtoAB;
+}
+
++ (NSSet*)defaultW3CtoNull
+{
+ // these are values that have no AddressBook Equivalent OR have not been implemented yet
+ if (org_apache_cordova_contacts_W3CtoNull == nil) {
+ org_apache_cordova_contacts_W3CtoNull = [NSSet setWithObjects:kW3ContactDisplayName,
+ kW3ContactCategories, kW3ContactFormattedName, nil];
+ }
+ return org_apache_cordova_contacts_W3CtoNull;
+}
+
+/*
+ * The objectAndProperties dictionary contains the all of the properties of the W3C Contact Objects specified by the key
+ * Used in calcReturnFields, and various extract<Property> methods
+ */
++ (NSDictionary*)defaultObjectAndProperties
+{
+ if (org_apache_cordova_contacts_objectAndProperties == nil) {
+ org_apache_cordova_contacts_objectAndProperties = [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSArray arrayWithObjects:kW3ContactGivenName, kW3ContactFamilyName,
+ kW3ContactMiddleName, kW3ContactHonorificPrefix, kW3ContactHonorificSuffix, kW3ContactFormattedName, nil], kW3ContactName,
+ [NSArray arrayWithObjects:kW3ContactStreetAddress, kW3ContactLocality, kW3ContactRegion,
+ kW3ContactPostalCode, kW3ContactCountry, /*kW3ContactAddressFormatted,*/ nil], kW3ContactAddresses,
+ [NSArray arrayWithObjects:kW3ContactOrganizationName, kW3ContactTitle, kW3ContactDepartment, nil], kW3ContactOrganizations,
+ [NSArray arrayWithObjects:kW3ContactFieldType, kW3ContactFieldValue, kW3ContactFieldPrimary, nil], kW3ContactPhoneNumbers,
+ [NSArray arrayWithObjects:kW3ContactFieldType, kW3ContactFieldValue, kW3ContactFieldPrimary, nil], kW3ContactEmails,
+ [NSArray arrayWithObjects:kW3ContactFieldType, kW3ContactFieldValue, kW3ContactFieldPrimary, nil], kW3ContactPhotos,
+ [NSArray arrayWithObjects:kW3ContactFieldType, kW3ContactFieldValue, kW3ContactFieldPrimary, nil], kW3ContactUrls,
+ [NSArray arrayWithObjects:kW3ContactImValue, kW3ContactImType, nil], kW3ContactIms,
+ nil];
+ }
+ return org_apache_cordova_contacts_objectAndProperties;
+}
+
++ (NSDictionary*)defaultFields
+{
+ if (org_apache_cordova_contacts_defaultFields == nil) {
+ org_apache_cordova_contacts_defaultFields = [NSDictionary dictionaryWithObjectsAndKeys:
+ [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactName], kW3ContactName,
+ [NSNull null], kW3ContactNickname,
+ [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactAddresses], kW3ContactAddresses,
+ [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactOrganizations], kW3ContactOrganizations,
+ [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactPhoneNumbers], kW3ContactPhoneNumbers,
+ [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactEmails], kW3ContactEmails,
+ [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactIms], kW3ContactIms,
+ [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactPhotos], kW3ContactPhotos,
+ [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactUrls], kW3ContactUrls,
+ [NSNull null], kW3ContactBirthday,
+ [NSNull null], kW3ContactNote,
+ nil];
+ }
+ return org_apache_cordova_contacts_defaultFields;
+}
+
+/* Translate W3C Contact data into ABRecordRef
+ *
+ * New contact information comes in as a NSMutableDictionary. All Null entries in Contact object are set
+ * as [NSNull null] in the dictionary when translating from the JSON input string of Contact data. However, if
+ * user did not set a value within a Contact object or sub-object (by not using the object constructor) some data
+ * may not exist.
+ * bUpdate = YES indicates this is a save of an existing record
+ */
+- (bool)setFromContactDict:(NSDictionary*)aContact asUpdate:(BOOL)bUpdate
+{
+ if (![aContact isKindOfClass:[NSDictionary class]]) {
+ return FALSE; // can't do anything if no dictionary!
+ }
+
+ ABRecordRef person = self.record;
+ bool bSuccess = TRUE;
+ CFErrorRef error;
+
+ // set name info
+ // iOS doesn't have displayName - might have to pull parts from it to create name
+ bool bName = false;
+ NSDictionary* dict = [aContact valueForKey:kW3ContactName];
+ if ([dict isKindOfClass:[NSDictionary class]]) {
+ bName = true;
+ NSArray* propArray = [[CDVContact defaultObjectAndProperties] objectForKey:kW3ContactName];
+
+ for (id i in propArray) {
+ if (![(NSString*)i isEqualToString : kW3ContactFormattedName]) { // kW3ContactFormattedName is generated from ABRecordCopyCompositeName() and can't be set
+ [self setValue:[dict valueForKey:i] forProperty:(ABPropertyID)[(NSNumber*)[[CDVContact defaultW3CtoAB] objectForKey:i] intValue]
+ inRecord:person asUpdate:bUpdate];
+ }
+ }
+ }
+
+ id nn = [aContact valueForKey:kW3ContactNickname];
+ if (![nn isKindOfClass:[NSNull class]]) {
+ bName = true;
+ [self setValue:nn forProperty:kABPersonNicknameProperty inRecord:person asUpdate:bUpdate];
+ }
+ if (!bName) {
+ // if no name or nickname - try and use displayName as W3Contact must have displayName or ContactName
+ [self setValue:[aContact valueForKey:kW3ContactDisplayName] forProperty:kABPersonNicknameProperty
+ inRecord:person asUpdate:bUpdate];
+ }
+
+ // set phoneNumbers
+ // NSLog(@"setting phoneNumbers");
+ NSArray* array = [aContact valueForKey:kW3ContactPhoneNumbers];
+ if ([array isKindOfClass:[NSArray class]]) {
+ [self setMultiValueStrings:array forProperty:kABPersonPhoneProperty inRecord:person asUpdate:bUpdate];
+ }
+ // set Emails
+ // NSLog(@"setting emails");
+ array = [aContact valueForKey:kW3ContactEmails];
+ if ([array isKindOfClass:[NSArray class]]) {
+ [self setMultiValueStrings:array forProperty:kABPersonEmailProperty inRecord:person asUpdate:bUpdate];
+ }
+ // set Urls
+ // NSLog(@"setting urls");
+ array = [aContact valueForKey:kW3ContactUrls];
+ if ([array isKindOfClass:[NSArray class]]) {
+ [self setMultiValueStrings:array forProperty:kABPersonURLProperty inRecord:person asUpdate:bUpdate];
+ }
+
+ // set multivalue dictionary properties
+ // set addresses: streetAddress, locality, region, postalCode, country
+ // set ims: value = username, type = servicetype
+ // iOS addresses and im are a MultiValue Properties with label, value=dictionary of info, and id
+ // NSLog(@"setting addresses");
+ error = nil;
+ array = [aContact valueForKey:kW3ContactAddresses];
+ if ([array isKindOfClass:[NSArray class]]) {
+ [self setMultiValueDictionary:array forProperty:kABPersonAddressProperty inRecord:person asUpdate:bUpdate];
+ }
+ // ims
+ // NSLog(@"setting ims");
+ array = [aContact valueForKey:kW3ContactIms];
+ if ([array isKindOfClass:[NSArray class]]) {
+ [self setMultiValueDictionary:array forProperty:kABPersonInstantMessageProperty inRecord:person asUpdate:bUpdate];
+ }
+
+ // organizations
+ // W3C ContactOrganization has pref, type, name, title, department
+ // iOS only supports name, title, department
+ // NSLog(@"setting organizations");
+ // TODO this may need work - should Organization information be removed when array is empty??
+ array = [aContact valueForKey:kW3ContactOrganizations]; // iOS only supports one organization - use first one
+ if ([array isKindOfClass:[NSArray class]]) {
+ BOOL bRemove = NO;
+ NSDictionary* dict = nil;
+ if ([array count] > 0) {
+ dict = [array objectAtIndex:0];
+ } else {
+ // remove the organization info entirely
+ bRemove = YES;
+ }
+ if ([dict isKindOfClass:[NSDictionary class]] || (bRemove == YES)) {
+ [self setValue:(bRemove ? @"" : [dict valueForKey:@"name"]) forProperty:kABPersonOrganizationProperty inRecord:person asUpdate:bUpdate];
+ [self setValue:(bRemove ? @"" : [dict valueForKey:kW3ContactTitle]) forProperty:kABPersonJobTitleProperty inRecord:person asUpdate:bUpdate];
+ [self setValue:(bRemove ? @"" : [dict valueForKey:kW3ContactDepartment]) forProperty:kABPersonDepartmentProperty inRecord:person asUpdate:bUpdate];
+ }
+ }
+ // add dates
+ // Dates come in as milliseconds in NSNumber Object
+ id ms = [aContact valueForKey:kW3ContactBirthday];
+ NSDate* aDate = nil;
+ if (ms && [ms isKindOfClass:[NSNumber class]]) {
+ double msValue = [ms doubleValue];
+ msValue = msValue / 1000;
+ aDate = [NSDate dateWithTimeIntervalSince1970:msValue];
+ }
+ if ((aDate != nil) || [ms isKindOfClass:[NSString class]]) {
+ [self setValue:aDate != nil ? aDate:ms forProperty:kABPersonBirthdayProperty inRecord:person asUpdate:bUpdate];
+ }
+ // don't update creation date
+ // modification date will get updated when save
+ // anniversary is removed from W3C Contact api Dec 9, 2010 spec - don't waste time on it yet
+
+ // kABPersonDateProperty
+
+ // kABPersonAnniversaryLabel
+
+ // iOS doesn't have gender - ignore
+ // note
+ [self setValue:[aContact valueForKey:kW3ContactNote] forProperty:kABPersonNoteProperty inRecord:person asUpdate:bUpdate];
+
+ // iOS doesn't have preferredName- ignore
+
+ // photo
+ array = [aContact valueForKey:kW3ContactPhotos];
+ if ([array isKindOfClass:[NSArray class]]) {
+ if (bUpdate && ([array count] == 0)) {
+ // remove photo
+ bSuccess = ABPersonRemoveImageData(person, &error);
+ } else if ([array count] > 0) {
+ NSDictionary* dict = [array objectAtIndex:0]; // currently only support one photo
+ if ([dict isKindOfClass:[NSDictionary class]]) {
+ id value = [dict objectForKey:kW3ContactFieldValue];
+ if ([value isKindOfClass:[NSString class]]) {
+ if (bUpdate && ([value length] == 0)) {
+ // remove the current image
+ bSuccess = ABPersonRemoveImageData(person, &error);
+ } else {
+ // use this image
+ // don't know if string is encoded or not so first unencode it then encode it again
+ NSString* cleanPath = [value stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+ NSURL* photoUrl = [NSURL URLWithString:[cleanPath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
+ // caller is responsible for checking for a connection, if no connection this will fail
+ NSError* err = nil;
+ NSData* data = nil;
+ if (photoUrl) {
+ data = [NSData dataWithContentsOfURL:photoUrl options:NSDataReadingUncached error:&err];
+ }
+ if (data && ([data length] > 0)) {
+ bSuccess = ABPersonSetImageData(person, (__bridge CFDataRef)data, &error);
+ }
+ if (!data || !bSuccess) {
+ NSLog(@"error setting contact image: %@", (err != nil ? [err localizedDescription] : @""));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // TODO WebURLs
+
+ // TODO timezone
+
+ return bSuccess;
+}
+
+/* Set item into an AddressBook Record for the specified property.
+ * aValue - the value to set into the address book (code checks for null or [NSNull null]
+ * aProperty - AddressBook property ID
+ * aRecord - the record to update
+ * bUpdate - whether this is a possible update vs a new entry
+ * RETURN
+ * true - property was set (or input value as null)
+ * false - property was not set
+ */
+- (bool)setValue:(id)aValue forProperty:(ABPropertyID)aProperty inRecord:(ABRecordRef)aRecord asUpdate:(BOOL)bUpdate
+{
+ bool bSuccess = true; // if property was null, just ignore and return success
+ CFErrorRef error;
+
+ if (aValue && ![aValue isKindOfClass:[NSNull class]]) {
+ if (bUpdate && ([aValue isKindOfClass:[NSString class]] && ([aValue length] == 0))) { // if updating, empty string means to delete
+ aValue = NULL;
+ } // really only need to set if different - more efficient to just update value or compare and only set if necessary???
+ bSuccess = ABRecordSetValue(aRecord, aProperty, (__bridge CFTypeRef)aValue, &error);
+ if (!bSuccess) {
+ NSLog(@"error setting %d property", aProperty);
+ }
+ }
+
+ return bSuccess;
+}
+
+- (bool)removeProperty:(ABPropertyID)aProperty inRecord:(ABRecordRef)aRecord
+{
+ CFErrorRef err;
+ bool bSuccess = ABRecordRemoveValue(aRecord, aProperty, &err);
+
+ if (!bSuccess) {
+ CFStringRef errDescription = CFErrorCopyDescription(err);
+ NSLog(@"Unable to remove property %d: %@", aProperty, errDescription);
+ CFRelease(errDescription);
+ }
+ return bSuccess;
+}
+
+- (bool)addToMultiValue:(ABMultiValueRef)multi fromDictionary:dict
+{
+ bool bSuccess = FALSE;
+ id value = [dict valueForKey:kW3ContactFieldValue];
+
+ if (IS_VALID_VALUE(value)) {
+ CFStringRef label = [CDVContact convertContactTypeToPropertyLabel:[dict valueForKey:kW3ContactFieldType]];
+ bSuccess = ABMultiValueAddValueAndLabel(multi, (__bridge CFTypeRef)value, label, NULL);
+ if (!bSuccess) {
+ NSLog(@"Error setting Value: %@ and label: %@", value, label);
+ }
+ }
+ return bSuccess;
+}
+
+- (ABMultiValueRef)allocStringMultiValueFromArray:array
+{
+ ABMutableMultiValueRef multi = ABMultiValueCreateMutable(kABMultiStringPropertyType);
+
+ for (NSDictionary* dict in array) {
+ [self addToMultiValue:multi fromDictionary:dict];
+ }
+
+ return multi; // caller is responsible for releasing multi
+}
+
+- (bool)setValue:(CFTypeRef)value forProperty:(ABPropertyID)prop inRecord:(ABRecordRef)person
+{
+ CFErrorRef error;
+ bool bSuccess = ABRecordSetValue(person, prop, value, &error);
+
+ if (!bSuccess) {
+ NSLog(@"Error setting value for property: %d", prop);
+ }
+ return bSuccess;
+}
+
+/* Set MultiValue string properties into Address Book Record.
+ * NSArray* fieldArray - array of dictionaries containing W3C properties to be set into record
+ * ABPropertyID prop - the property to be set (generally used for phones and emails)
+ * ABRecordRef person - the record to set values into
+ * BOOL bUpdate - whether or not to update date or set as new.
+ * When updating:
+ * empty array indicates to remove entire property
+ * empty string indicates to remove
+ * [NSNull null] do not modify (keep existing record value)
+ * RETURNS
+ * bool false indicates error
+ *
+ * used for phones and emails
+ */
+- (bool)setMultiValueStrings:(NSArray*)fieldArray forProperty:(ABPropertyID)prop inRecord:(ABRecordRef)person asUpdate:(BOOL)bUpdate
+{
+ bool bSuccess = TRUE;
+ ABMutableMultiValueRef multi = nil;
+
+ if (!bUpdate) {
+ multi = [self allocStringMultiValueFromArray:fieldArray];
+ bSuccess = [self setValue:multi forProperty:prop inRecord:person];
+ } else if (bUpdate && ([fieldArray count] == 0)) {
+ // remove entire property
+ bSuccess = [self removeProperty:prop inRecord:person];
+ } else { // check for and apply changes
+ ABMultiValueRef copy = ABRecordCopyValue(person, prop);
+ if (copy != nil) {
+ multi = ABMultiValueCreateMutableCopy(copy);
+ CFRelease(copy);
+
+ for (NSDictionary* dict in fieldArray) {
+ id val;
+ NSString* label = nil;
+ val = [dict valueForKey:kW3ContactFieldValue];
+ label = (__bridge NSString*)[CDVContact convertContactTypeToPropertyLabel:[dict valueForKey:kW3ContactFieldType]];
+ if (IS_VALID_VALUE(val)) {
+ // is an update, find index of entry with matching id, if values are different, update.
+ id idValue = [dict valueForKey:kW3ContactFieldId];
+ int identifier = [idValue isKindOfClass:[NSNumber class]] ? [idValue intValue] : -1;
+ CFIndex i = identifier >= 0 ? ABMultiValueGetIndexForIdentifier(multi, identifier) : kCFNotFound;
+ if (i != kCFNotFound) {
+ if ([val length] == 0) {
+ // remove both value and label
+ ABMultiValueRemoveValueAndLabelAtIndex(multi, i);
+ } else {
+ NSString* valueAB = (__bridge_transfer NSString*)ABMultiValueCopyValueAtIndex(multi, i);
+ NSString* labelAB = (__bridge_transfer NSString*)ABMultiValueCopyLabelAtIndex(multi, i);
+ if ((valueAB == nil) || ![val isEqualToString:valueAB]) {
+ ABMultiValueReplaceValueAtIndex(multi, (__bridge CFTypeRef)val, i);
+ }
+ if ((labelAB == nil) || ![label isEqualToString:labelAB]) {
+ ABMultiValueReplaceLabelAtIndex(multi, (__bridge CFStringRef)label, i);
+ }
+ }
+ } else {
+ // is a new value - insert
+ [self addToMultiValue:multi fromDictionary:dict];
+ }
+ } // end of if value
+ } // end of for
+ } else { // adding all new value(s)
+ multi = [self allocStringMultiValueFromArray:fieldArray];
+ }
+ // set the (updated) copy as the new value
+ bSuccess = [self setValue:multi forProperty:prop inRecord:person];
+ }
+
+ if (multi) {
+ CFRelease(multi);
+ }
+
+ return bSuccess;
+}
+
+// used for ims and addresses
+- (ABMultiValueRef)allocDictMultiValueFromArray:array forProperty:(ABPropertyID)prop
+{
+ ABMutableMultiValueRef multi = ABMultiValueCreateMutable(kABMultiDictionaryPropertyType);
+ NSMutableDictionary* newDict;
+ NSMutableDictionary* addDict;
+
+ for (NSDictionary* dict in array) {
+ newDict = [self translateW3Dict:dict forProperty:prop];
+ addDict = [NSMutableDictionary dictionaryWithCapacity:2];
+ if (newDict) { // create a new dictionary with a Label and Value, value is the dictionary previously created
+ // June, 2011 W3C Contact spec adds type into ContactAddress book
+ // get the type out of the original dictionary for address
+ NSString* addrType = (NSString*)[dict valueForKey:kW3ContactFieldType];
+ if (!addrType) {
+ addrType = (NSString*)kABOtherLabel;
+ }
+ NSObject* typeValue = ((prop == kABPersonInstantMessageProperty) ? (NSObject*)kABOtherLabel : addrType);
+ // NSLog(@"typeValue: %@", typeValue);
+ [addDict setObject:typeValue forKey:kW3ContactFieldType]; // im labels will be set as Other and address labels as type from dictionary
+ [addDict setObject:newDict forKey:kW3ContactFieldValue];
+ [self addToMultiValue:multi fromDictionary:addDict];
+ }
+ }
+
+ return multi; // caller is responsible for releasing
+}
+
+// used for ims and addresses to convert W3 dictionary of values to AB Dictionary
+// got messier when June, 2011 W3C Contact spec added type field into ContactAddress
+- (NSMutableDictionary*)translateW3Dict:(NSDictionary*)dict forProperty:(ABPropertyID)prop
+{
+ NSArray* propArray = [[CDVContact defaultObjectAndProperties] valueForKey:[[CDVContact defaultABtoW3C] objectForKey:[NSNumber numberWithInt:prop]]];
+
+ NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:1];
+ id value;
+
+ for (NSString* key in propArray) { // for each W3 Contact key get the value
+ if (((value = [dict valueForKey:key]) != nil) && ![value isKindOfClass:[NSNull class]]) {
+ // if necessary convert the W3 value to AB Property label
+ NSString* setValue = value;
+ if ([CDVContact needsConversion:key]) { // IM types must be converted
+ setValue = (NSString*)[CDVContact convertContactTypeToPropertyLabel:value];
+ // IMs must have a valid AB value!
+ if ((prop == kABPersonInstantMessageProperty) && [setValue isEqualToString:(NSString*)kABOtherLabel]) {
+ setValue = @""; // try empty string
+ }
+ }
+ // set the AB value into the dictionary
+ [newDict setObject:setValue forKey:(NSString*)[[CDVContact defaultW3CtoAB] valueForKey:(NSString*)key]];
+ }
+ }
+
+ if ([newDict count] == 0) {
+ newDict = nil; // no items added
+ }
+ return newDict;
+}
+
+/* set multivalue dictionary properties into an AddressBook Record
+ * NSArray* array - array of dictionaries containing the W3C properties to set into the record
+ * ABPropertyID prop - the property id for the multivalue dictionary (addresses and ims)
+ * ABRecordRef person - the record to set the values into
+ * BOOL bUpdate - YES if this is an update to an existing record
+ * When updating:
+ * empty array indicates to remove entire property
+ * value/label == "" indicates to remove
+ * value/label == [NSNull null] do not modify (keep existing record value)
+ * RETURN
+ * bool false indicates fatal error
+ *
+ * iOS addresses and im are a MultiValue Properties with label, value=dictionary of info, and id
+ * set addresses: streetAddress, locality, region, postalCode, country
+ * set ims: value = username, type = servicetype
+ * there are some special cases in here for ims - needs cleanup / simplification
+ *
+ */
+- (bool)setMultiValueDictionary:(NSArray*)array forProperty:(ABPropertyID)prop inRecord:(ABRecordRef)person asUpdate:(BOOL)bUpdate
+{
+ bool bSuccess = FALSE;
+ ABMutableMultiValueRef multi = nil;
+
+ if (!bUpdate) {
+ multi = [self allocDictMultiValueFromArray:array forProperty:prop];
+ bSuccess = [self setValue:multi forProperty:prop inRecord:person];
+ } else if (bUpdate && ([array count] == 0)) {
+ // remove property
+ bSuccess = [self removeProperty:prop inRecord:person];
+ } else { // check for and apply changes
+ ABMultiValueRef copy = ABRecordCopyValue(person, prop);
+ if (copy) {
+ multi = ABMultiValueCreateMutableCopy(copy);
+ CFRelease(copy);
+ // get the W3C values for this property
+ NSArray* propArray = [[CDVContact defaultObjectAndProperties] valueForKey:[[CDVContact defaultABtoW3C] objectForKey:[NSNumber numberWithInt:prop]]];
+ id value;
+ id valueAB;
+
+ for (NSDictionary* field in array) {
+ NSMutableDictionary* dict;
+ // find the index for the current property
+ id idValue = [field valueForKey:kW3ContactFieldId];
+ int identifier = [idValue isKindOfClass:[NSNumber class]] ? [idValue intValue] : -1;
+ CFIndex idx = identifier >= 0 ? ABMultiValueGetIndexForIdentifier(multi, identifier) : kCFNotFound;
+ BOOL bUpdateLabel = NO;
+ if (idx != kCFNotFound) {
+ dict = [NSMutableDictionary dictionaryWithCapacity:1];
+ // NSDictionary* existingDictionary = (NSDictionary*)ABMultiValueCopyValueAtIndex(multi, idx);
+ CFTypeRef existingDictionary = ABMultiValueCopyValueAtIndex(multi, idx);
+ NSString* existingABLabel = (__bridge_transfer NSString*)ABMultiValueCopyLabelAtIndex(multi, idx);
+ NSString* testLabel = [field valueForKey:kW3ContactFieldType];
+ // fixes cb-143 where setting empty label could cause address to not be removed
+ // (because empty label would become 'other' in convertContactTypeToPropertyLabel
+ // which may not have matched existing label thus resulting in an incorrect updating of the label
+ // and the address not getting removed at the end of the for loop)
+ if (testLabel && [testLabel isKindOfClass:[NSString class]] && ([testLabel length] > 0)) {
+ CFStringRef w3cLabel = [CDVContact convertContactTypeToPropertyLabel:testLabel];
+ if (w3cLabel && ![existingABLabel isEqualToString:(__bridge NSString*)w3cLabel]) {
+ // replace the label
+ ABMultiValueReplaceLabelAtIndex(multi, w3cLabel, idx);
+ bUpdateLabel = YES;
+ }
+ } // else was invalid or empty label string so do not update
+
+ for (id k in propArray) {
+ value = [field valueForKey:k];
+ bool bSet = (value != nil && ![value isKindOfClass:[NSNull class]] && ([value isKindOfClass:[NSString class]] && [value length] > 0));
+ // if there is a contact value, put it into dictionary
+ if (bSet) {
+ NSString* setValue = [CDVContact needsConversion:(NSString*)k] ? (NSString*)[CDVContact convertContactTypeToPropertyLabel:value] : value;
+ [dict setObject:setValue forKey:(NSString*)[[CDVContact defaultW3CtoAB] valueForKey:(NSString*)k]];
+ } else if ((value == nil) || ([value isKindOfClass:[NSString class]] && ([value length] != 0))) {
+ // value not provided in contact dictionary - if prop exists in AB dictionary, preserve it
+ valueAB = [(__bridge NSDictionary*)existingDictionary valueForKey : [[CDVContact defaultW3CtoAB] valueForKey:k]];
+ if (valueAB != nil) {
+ [dict setValue:valueAB forKey:[[CDVContact defaultW3CtoAB] valueForKey:k]];
+ }
+ } // else if value == "" it will not be added into updated dict and thus removed
+ } // end of for loop (moving here fixes cb-143, need to end for loop before replacing or removing multivalue)
+
+ if ([dict count] > 0) {
+ // something was added into new dict,
+ ABMultiValueReplaceValueAtIndex(multi, (__bridge CFTypeRef)dict, idx);
+ } else if (!bUpdateLabel) {
+ // nothing added into new dict and no label change so remove this property entry
+ ABMultiValueRemoveValueAndLabelAtIndex(multi, idx);
+ }
+
+ CFRelease(existingDictionary);
+ } else {
+ // not found in multivalue so add it
+ dict = [self translateW3Dict:field forProperty:prop];
+ if (dict) {
+ NSMutableDictionary* addDict = [NSMutableDictionary dictionaryWithCapacity:2];
+ // get the type out of the original dictionary for address
+ NSObject* typeValue = ((prop == kABPersonInstantMessageProperty) ? (NSObject*)kABOtherLabel : (NSString*)[field valueForKey:kW3ContactFieldType]);
+ // NSLog(@"typeValue: %@", typeValue);
+ [addDict setObject:typeValue forKey:kW3ContactFieldType]; // im labels will be set as Other and address labels as type from dictionary
+ [addDict setObject:dict forKey:kW3ContactFieldValue];
+ [self addToMultiValue:multi fromDictionary:addDict];
+ }
+ }
+ } // end of looping through dictionaries
+
+ // set the (updated) copy as the new value
+ bSuccess = [self setValue:multi forProperty:prop inRecord:person];
+ }
+ } // end of copy and apply changes
+ if (multi) {
+ CFRelease(multi);
+ }
+
+ return bSuccess;
+}
+
+/* Determine which W3C labels need to be converted
+ */
++ (BOOL)needsConversion:(NSString*)W3Label
+{
+ BOOL bConvert = NO;
+
+ if ([W3Label isEqualToString:kW3ContactFieldType] || [W3Label isEqualToString:kW3ContactImType]) {
+ bConvert = YES;
+ }
+ return bConvert;
+}
+
+/* Translation of property type labels contact API ---> iPhone
+ *
+ * phone: work, home, other, mobile, fax, pager -->
+ * kABWorkLabel, kABHomeLabel, kABOtherLabel, kABPersonPhoneMobileLabel, kABPersonHomeFAXLabel || kABPersonHomeFAXLabel, kABPersonPhonePagerLabel
+ * emails: work, home, other ---> kABWorkLabel, kABHomeLabel, kABOtherLabel
+ * ims: aim, gtalk, icq, xmpp, msn, skype, qq, yahoo --> kABPersonInstantMessageService + (AIM, ICG, MSN, Yahoo). No support for gtalk, xmpp, skype, qq
+ * addresses: work, home, other --> kABWorkLabel, kABHomeLabel, kABOtherLabel
+ *
+ *
+ */
++ (CFStringRef)convertContactTypeToPropertyLabel:(NSString*)label
+{
+ CFStringRef type;
+
+ if ([label isKindOfClass:[NSNull class]] || ![label isKindOfClass:[NSString class]]) {
+ type = NULL; // no label
+ } else if ([label caseInsensitiveCompare:kW3ContactWorkLabel] == NSOrderedSame) {
+ type = kABWorkLabel;
+ } else if ([label caseInsensitiveCompare:kW3ContactHomeLabel] == NSOrderedSame) {
+ type = kABHomeLabel;
+ } else if ([label caseInsensitiveCompare:kW3ContactOtherLabel] == NSOrderedSame) {
+ type = kABOtherLabel;
+ } else if ([label caseInsensitiveCompare:kW3ContactPhoneMobileLabel] == NSOrderedSame) {
+ type = kABPersonPhoneMobileLabel;
+ } else if ([label caseInsensitiveCompare:kW3ContactPhonePagerLabel] == NSOrderedSame) {
+ type = kABPersonPhonePagerLabel;
+ } else if ([label caseInsensitiveCompare:kW3ContactImAIMLabel] == NSOrderedSame) {
+ type = kABPersonInstantMessageServiceAIM;
+ } else if ([label caseInsensitiveCompare:kW3ContactImICQLabel] == NSOrderedSame) {
+ type = kABPersonInstantMessageServiceICQ;
+ } else if ([label caseInsensitiveCompare:kW3ContactImMSNLabel] == NSOrderedSame) {
+ type = kABPersonInstantMessageServiceMSN;
+ } else if ([label caseInsensitiveCompare:kW3ContactImYahooLabel] == NSOrderedSame) {
+ type = kABPersonInstantMessageServiceYahoo;
+ } else if ([label caseInsensitiveCompare:kW3ContactUrlProfile] == NSOrderedSame) {
+ type = kABPersonHomePageLabel;
+ } else {
+ type = kABOtherLabel;
+ }
+
+ return type;
+}
+
++ (NSString*)convertPropertyLabelToContactType:(NSString*)label
+{
+ NSString* type = nil;
+
+ if (label != nil) { // improve efficiency......
+ if ([label isEqualToString:(NSString*)kABPersonPhoneMobileLabel]) {
+ type = kW3ContactPhoneMobileLabel;
+ } else if ([label isEqualToString:(NSString*)kABPersonPhoneHomeFAXLabel] ||
+ [label isEqualToString:(NSString*)kABPersonPhoneWorkFAXLabel]) {
+ type = kW3ContactPhoneFaxLabel;
+ } else if ([label isEqualToString:(NSString*)kABPersonPhonePagerLabel]) {
+ type = kW3ContactPhonePagerLabel;
+ } else if ([label isEqualToString:(NSString*)kABHomeLabel]) {
+ type = kW3ContactHomeLabel;
+ } else if ([label isEqualToString:(NSString*)kABWorkLabel]) {
+ type = kW3ContactWorkLabel;
+ } else if ([label isEqualToString:(NSString*)kABOtherLabel]) {
+ type = kW3ContactOtherLabel;
+ } else if ([label isEqualToString:(NSString*)kABPersonInstantMessageServiceAIM]) {
+ type = kW3ContactImAIMLabel;
+ } else if ([label isEqualToString:(NSString*)kABPersonInstantMessageServiceICQ]) {
+ type = kW3ContactImICQLabel;
+ } else if ([label isEqualToString:(NSString*)kABPersonInstantMessageServiceJabber]) {
+ type = kW3ContactOtherLabel;
+ } else if ([label isEqualToString:(NSString*)kABPersonInstantMessageServiceMSN]) {
+ type = kW3ContactImMSNLabel;
+ } else if ([label isEqualToString:(NSString*)kABPersonInstantMessageServiceYahoo]) {
+ type = kW3ContactImYahooLabel;
+ } else if ([label isEqualToString:(NSString*)kABPersonHomePageLabel]) {
+ type = kW3ContactUrlProfile;
+ } else {
+ type = kW3ContactOtherLabel;
+ }
+ }
+ return type;
+}
+
+/* Check if the input label is a valid W3C ContactField.type. This is used when searching,
+ * only search field types if the search string is a valid type. If we converted any search
+ * string to a ABPropertyLabel it could convert to kABOtherLabel which is probably not want
+ * the user wanted to search for and could skew the results.
+ */
++ (BOOL)isValidW3ContactType:(NSString*)label
+{
+ BOOL isValid = NO;
+
+ if ([label isKindOfClass:[NSNull class]] || ![label isKindOfClass:[NSString class]]) {
+ isValid = NO; // no label
+ } else if ([label caseInsensitiveCompare:kW3ContactWorkLabel] == NSOrderedSame) {
+ isValid = YES;
+ } else if ([label caseInsensitiveCompare:kW3ContactHomeLabel] == NSOrderedSame) {
+ isValid = YES;
+ } else if ([label caseInsensitiveCompare:kW3ContactOtherLabel] == NSOrderedSame) {
+ isValid = YES;
+ } else if ([label caseInsensitiveCompare:kW3ContactPhoneMobileLabel] == NSOrderedSame) {
+ isValid = YES;
+ } else if ([label caseInsensitiveCompare:kW3ContactPhonePagerLabel] == NSOrderedSame) {
+ isValid = YES;
+ } else if ([label caseInsensitiveCompare:kW3ContactImAIMLabel] == NSOrderedSame) {
+ isValid = YES;
+ } else if ([label caseInsensitiveCompare:kW3ContactImICQLabel] == NSOrderedSame) {
+ isValid = YES;
+ } else if ([label caseInsensitiveCompare:kW3ContactImMSNLabel] == NSOrderedSame) {
+ isValid = YES;
+ } else if ([label caseInsensitiveCompare:kW3ContactImYahooLabel] == NSOrderedSame) {
+ isValid = YES;
+ } else {
+ isValid = NO;
+ }
+
+ return isValid;
+}
+
+/* Create a new Contact Dictionary object from an ABRecordRef that contains information in a format such that
+ * it can be returned to JavaScript callback as JSON object string.
+ * Uses:
+ * ABRecordRef set into Contact Object
+ * NSDictionary withFields indicates which fields to return from the AddressBook Record
+ *
+ * JavaScript Contact:
+ * @param {DOMString} id unique identifier
+ * @param {DOMString} displayName
+ * @param {ContactName} name
+ * @param {DOMString} nickname
+ * @param {ContactField[]} phoneNumbers array of phone numbers
+ * @param {ContactField[]} emails array of email addresses
+ * @param {ContactAddress[]} addresses array of addresses
+ * @param {ContactField[]} ims instant messaging user ids
+ * @param {ContactOrganization[]} organizations
+ * @param {DOMString} published date contact was first created
+ * @param {DOMString} updated date contact was last updated
+ * @param {DOMString} birthday contact's birthday
+ * @param (DOMString} anniversary contact's anniversary
+ * @param {DOMString} gender contact's gender
+ * @param {DOMString} note user notes about contact
+ * @param {DOMString} preferredUsername
+ * @param {ContactField[]} photos
+ * @param {ContactField[]} tags
+ * @param {ContactField[]} relationships
+ * @param {ContactField[]} urls contact's web sites
+ * @param {ContactAccounts[]} accounts contact's online accounts
+ * @param {DOMString} timezone UTC time zone offset
+ * @param {DOMString} connected
+ */
+
+- (NSDictionary*)toDictionary:(NSDictionary*)withFields
+{
+ // if not a person type record bail out for now
+ if (ABRecordGetRecordType(self.record) != kABPersonType) {
+ return NULL;
+ }
+ id value = nil;
+ self.returnFields = withFields;
+
+ NSMutableDictionary* nc = [NSMutableDictionary dictionaryWithCapacity:1]; // new contact dictionary to fill in from ABRecordRef
+ // id
+ [nc setObject:[NSNumber numberWithInt:ABRecordGetRecordID(self.record)] forKey:kW3ContactId];
+ if (self.returnFields == nil) {
+ // if no returnFields specified, W3C says to return empty contact (but Cordova will at least return id)
+ return nc;
+ }
+ if ([self.returnFields objectForKey:kW3ContactDisplayName]) {
+ // displayname requested - iOS doesn't have so return null
+ [nc setObject:[NSNull null] forKey:kW3ContactDisplayName];
+ // may overwrite below if requested ContactName and there are no values
+ }
+ // nickname
+ if ([self.returnFields valueForKey:kW3ContactNickname]) {
+ value = (__bridge_transfer NSString*)ABRecordCopyValue(self.record, kABPersonNicknameProperty);
+ [nc setObject:(value != nil) ? value:[NSNull null] forKey:kW3ContactNickname];
+ }
+
+ // name dictionary
+ // NSLog(@"getting name info");
+ NSObject* data = [self extractName];
+ if (data != nil) {
+ [nc setObject:data forKey:kW3ContactName];
+ }
+ if ([self.returnFields objectForKey:kW3ContactDisplayName] && ((data == nil) || ([(NSDictionary*)data objectForKey : kW3ContactFormattedName] == [NSNull null]))) {
+ // user asked for displayName which iOS doesn't support but there is no other name data being returned
+ // try and use Composite Name so some name is returned
+ id tryName = (__bridge_transfer NSString*)ABRecordCopyCompositeName(self.record);
+ if (tryName != nil) {
+ [nc setObject:tryName forKey:kW3ContactDisplayName];
+ } else {
+ // use nickname or empty string
+ value = (__bridge_transfer NSString*)ABRecordCopyValue(self.record, kABPersonNicknameProperty);
+ [nc setObject:(value != nil) ? value:@"" forKey:kW3ContactDisplayName];
+ }
+ }
+ // phoneNumbers array
+ // NSLog(@"getting phoneNumbers");
+ value = [self extractMultiValue:kW3ContactPhoneNumbers];
+ if (value != nil) {
+ [nc setObject:value forKey:kW3ContactPhoneNumbers];
+ }
+ // emails array
+ // NSLog(@"getting emails");
+ value = [self extractMultiValue:kW3ContactEmails];
+ if (value != nil) {
+ [nc setObject:value forKey:kW3ContactEmails];
+ }
+ // urls array
+ value = [self extractMultiValue:kW3ContactUrls];
+ if (value != nil) {
+ [nc setObject:value forKey:kW3ContactUrls];
+ }
+ // addresses array
+ // NSLog(@"getting addresses");
+ value = [self extractAddresses];
+ if (value != nil) {
+ [nc setObject:value forKey:kW3ContactAddresses];
+ }
+ // im array
+ // NSLog(@"getting ims");
+ value = [self extractIms];
+ if (value != nil) {
+ [nc setObject:value forKey:kW3ContactIms];
+ }
+ // organization array (only info for one organization in iOS)
+ // NSLog(@"getting organizations");
+ value = [self extractOrganizations];
+ if (value != nil) {
+ [nc setObject:value forKey:kW3ContactOrganizations];
+ }
+
+ // for simple properties, could make this a bit more efficient by storing all simple properties in a single
+ // array in the returnFields dictionary and setting them via a for loop through the array
+
+ // add dates
+ // NSLog(@"getting dates");
+ NSNumber* ms;
+
+ /** Contact Revision field removed from June 16, 2011 version of specification
+
+ if ([self.returnFields valueForKey:kW3ContactUpdated]){
+ ms = [self getDateAsNumber: kABPersonModificationDateProperty];
+ if (!ms){
+ // try and get published date
+ ms = [self getDateAsNumber: kABPersonCreationDateProperty];
+ }
+ if (ms){
+ [nc setObject: ms forKey:kW3ContactUpdated];
+ }
+
+ }
+ */
+
+ if ([self.returnFields valueForKey:kW3ContactBirthday]) {
+ ms = [self getDateAsNumber:kABPersonBirthdayProperty];
+ if (ms) {
+ [nc setObject:ms forKey:kW3ContactBirthday];
+ }
+ }
+
+ /* Anniversary removed from 12-09-2010 W3C Contacts api spec
+ if ([self.returnFields valueForKey:kW3ContactAnniversary]){
+ // Anniversary date is stored in a multivalue property
+ ABMultiValueRef multi = ABRecordCopyValue(self.record, kABPersonDateProperty);
+ if (multi){
+ CFStringRef label = nil;
+ CFIndex count = ABMultiValueGetCount(multi);
+ // see if contains an Anniversary date
+ for(CFIndex i=0; i<count; i++){
+ label = ABMultiValueCopyLabelAtIndex(multi, i);
+ if(label && [(NSString*)label isEqualToString:(NSString*)kABPersonAnniversaryLabel]){
+ CFDateRef aDate = ABMultiValueCopyValueAtIndex(multi, i);
+ if(aDate){
+ [nc setObject: (NSString*)aDate forKey: kW3ContactAnniversary];
+ CFRelease(aDate);
+ }
+ CFRelease(label);
+ break;
+ }
+ }
+ CFRelease(multi);
+ }
+ }*/
+
+ if ([self.returnFields valueForKey:kW3ContactNote]) {
+ // note
+ value = (__bridge_transfer NSString*)ABRecordCopyValue(self.record, kABPersonNoteProperty);
+ [nc setObject:(value != nil) ? value:[NSNull null] forKey:kW3ContactNote];
+ }
+
+ if ([self.returnFields valueForKey:kW3ContactPhotos]) {
+ value = [self extractPhotos];
+ [nc setObject:(value != nil) ? value:[NSNull null] forKey:kW3ContactPhotos];
+ }
+
+ /* TimeZone removed from June 16, 2011 Contacts spec
+ *
+ if ([self.returnFields valueForKey:kW3ContactTimezone]){
+ [NSTimeZone resetSystemTimeZone];
+ NSTimeZone* currentTZ = [NSTimeZone localTimeZone];
+ NSInteger seconds = [currentTZ secondsFromGMT];
+ NSString* tz = [NSString stringWithFormat:@"%2d:%02u", seconds/3600, seconds % 3600 ];
+ [nc setObject:tz forKey:kW3ContactTimezone];
+ }
+ */
+ // TODO WebURLs
+ // [nc setObject:[NSNull null] forKey:kW3ContactUrls];
+ // online accounts - not available on iOS
+
+ return nc;
+}
+
+- (NSNumber*)getDateAsNumber:(ABPropertyID)datePropId
+{
+ NSNumber* msDate = nil;
+ NSDate* aDate = nil;
+ CFTypeRef cfDate = ABRecordCopyValue(self.record, datePropId);
+
+ if (cfDate) {
+ aDate = (__bridge NSDate*)cfDate;
+ msDate = [NSNumber numberWithDouble:([aDate timeIntervalSince1970] * 1000)];
+ CFRelease(cfDate);
+ }
+ return msDate;
+}
+
+/* Create Dictionary to match JavaScript ContactName object:
+ * formatted - ABRecordCopyCompositeName
+ * familyName
+ * givenName
+ * middleName
+ * honorificPrefix
+ * honorificSuffix
+*/
+
+- (NSObject*)extractName
+{
+ NSArray* fields = [self.returnFields objectForKey:kW3ContactName];
+
+ if (fields == nil) { // no name fields requested
+ return nil;
+ }
+
+ NSMutableDictionary* newName = [NSMutableDictionary dictionaryWithCapacity:6];
+ id value;
+
+ for (NSString* i in fields) {
+ if ([i isEqualToString:kW3ContactFormattedName]) {
+ value = (__bridge_transfer NSString*)ABRecordCopyCompositeName(self.record);
+ [newName setObject:(value != nil) ? value:[NSNull null] forKey:kW3ContactFormattedName];
+ } else {
+ // W3CtoAB returns NSNumber for AB name properties, get intValue and cast to ABPropertyID)
+ value = (__bridge_transfer NSString*)ABRecordCopyValue(self.record, (ABPropertyID)[[[CDVContact defaultW3CtoAB] valueForKey:i] intValue]);
+ [newName setObject:(value != nil) ? value:[NSNull null] forKey:(NSString*)i];
+ }
+ }
+
+ return newName;
+}
+
+/* Create array of Dictionaries to match JavaScript ContactField object for simple multiValue properties phoneNumbers, emails
+ * Input: (NSString*) W3Contact Property name
+ * type
+ * for phoneNumbers type is one of (work,home,other, mobile, fax, pager)
+ * for emails type is one of (work,home, other)
+ * value - phone number or email address
+ * (bool) primary (not supported on iphone)
+ * id
+*/
+- (NSObject*)extractMultiValue:(NSString*)propertyId
+{
+ NSArray* fields = [self.returnFields objectForKey:propertyId];
+
+ if (fields == nil) {
+ return nil;
+ }
+ ABMultiValueRef multi = nil;
+ NSObject* valuesArray = nil;
+ NSNumber* propNumber = [[CDVContact defaultW3CtoAB] valueForKey:propertyId];
+ ABPropertyID propId = [propNumber intValue];
+ multi = ABRecordCopyValue(self.record, propId);
+ // multi = ABRecordCopyValue(self.record, (ABPropertyID)[[[Contact defaultW3CtoAB] valueForKey:propertyId] intValue]);
+ CFIndex count = multi != nil ? ABMultiValueGetCount(multi) : 0;
+ id value;
+ if (count) {
+ valuesArray = [NSMutableArray arrayWithCapacity:count];
+
+ for (CFIndex i = 0; i < count; i++) {
+ NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:4];
+ if ([fields containsObject:kW3ContactFieldType]) {
+ NSString* label = (__bridge_transfer NSString*)ABMultiValueCopyLabelAtIndex(multi, i);
+ value = [CDVContact convertPropertyLabelToContactType:label];
+ [newDict setObject:(value != nil) ? value:[NSNull null] forKey:kW3ContactFieldType];
+ }
+ if ([fields containsObject:kW3ContactFieldValue]) {
+ value = (__bridge_transfer NSString*)ABMultiValueCopyValueAtIndex(multi, i);
+ [newDict setObject:(value != nil) ? value:[NSNull null] forKey:kW3ContactFieldValue];
+ }
+ if ([fields containsObject:kW3ContactFieldPrimary]) {
+ [newDict setObject:[NSNumber numberWithBool:(BOOL)NO] forKey:kW3ContactFieldPrimary]; // iOS doesn't support primary so set all to false
+ }
+ // always set id
+ value = [NSNumber numberWithUnsignedInt:ABMultiValueGetIdentifierAtIndex(multi, i)];
+ [newDict setObject:(value != nil) ? value:[NSNull null] forKey:kW3ContactFieldId];
+ [(NSMutableArray*)valuesArray addObject : newDict];
+ }
+ } else {
+ valuesArray = [NSNull null];
+ }
+ if (multi) {
+ CFRelease(multi);
+ }
+
+ return valuesArray;
+}
+
+/* Create array of Dictionaries to match JavaScript ContactAddress object for addresses
+ * pref - not supported
+ * type - address type
+ * formatted - formatted for mailing label (what about localization?)
+ * streetAddress
+ * locality
+ * region;
+ * postalCode
+ * country
+ * id
+ *
+ * iOS addresses are a MultiValue Properties with label, value=dictionary of address info, and id
+ */
+- (NSObject*)extractAddresses
+{
+ NSArray* fields = [self.returnFields objectForKey:kW3ContactAddresses];
+
+ if (fields == nil) { // no name fields requested
+ return nil;
+ }
+ CFStringRef value;
+ NSObject* addresses;
+ ABMultiValueRef multi = ABRecordCopyValue(self.record, kABPersonAddressProperty);
+ CFIndex count = multi ? ABMultiValueGetCount(multi) : 0;
+ if (count) {
+ addresses = [NSMutableArray arrayWithCapacity:count];
+
+ for (CFIndex i = 0; i < count; i++) {
+ NSMutableDictionary* newAddress = [NSMutableDictionary dictionaryWithCapacity:7];
+ // if we got this far, at least some address info is being requested.
+
+ // Always set id
+ id identifier = [NSNumber numberWithUnsignedInt:ABMultiValueGetIdentifierAtIndex(multi, i)];
+ [newAddress setObject:(identifier != nil) ? identifier:[NSNull null] forKey:kW3ContactFieldId];
+ // set the type label
+ NSString* label = (__bridge_transfer NSString*)ABMultiValueCopyLabelAtIndex(multi, i);
+
+ [newAddress setObject:(label != nil) ? (NSObject*)[[CDVContact class] convertPropertyLabelToContactType:label]:[NSNull null] forKey:kW3ContactFieldType];
+ // set the pref - iOS doesn't support so set to default of false
+ [newAddress setObject:@"false" forKey:kW3ContactFieldPrimary];
+ // get dictionary of values for this address
+ CFDictionaryRef dict = (CFDictionaryRef)ABMultiValueCopyValueAtIndex(multi, i);
+
+ for (id k in fields) {
+ bool bFound;
+ id key = [[CDVContact defaultW3CtoAB] valueForKey:k];
+ if (key && ![k isKindOfClass:[NSNull class]]) {
+ bFound = CFDictionaryGetValueIfPresent(dict, (__bridge const void*)key, (void*)&value);
+ if (bFound && (value != NULL)) {
+ CFRetain(value);
+ [newAddress setObject:(__bridge id)value forKey:k];
+ CFRelease(value);
+ } else {
+ [newAddress setObject:[NSNull null] forKey:k];
+ }
+ } else {
+ // was a property that iPhone doesn't support
+ [newAddress setObject:[NSNull null] forKey:k];
+ }
+ }
+
+ if ([newAddress count] > 0) { // ?? this will always be true since we set id,label,primary field??
+ [(NSMutableArray*)addresses addObject : newAddress];
+ }
+ CFRelease(dict);
+ } // end of loop through addresses
+ } else {
+ addresses = [NSNull null];
+ }
+ if (multi) {
+ CFRelease(multi);
+ }
+
+ return addresses;
+}
+
+/* Create array of Dictionaries to match JavaScript ContactField object for ims
+ * type one of [aim, gtalk, icq, xmpp, msn, skype, qq, yahoo] needs other as well
+ * value
+ * (bool) primary
+ * id
+ *
+ * iOS IMs are a MultiValue Properties with label, value=dictionary of IM details (service, username), and id
+ */
+- (NSObject*)extractIms
+{
+ NSArray* fields = [self.returnFields objectForKey:kW3ContactIms];
+
+ if (fields == nil) { // no name fields requested
+ return nil;
+ }
+ NSObject* imArray;
+ ABMultiValueRef multi = ABRecordCopyValue(self.record, kABPersonInstantMessageProperty);
+ CFIndex count = multi ? ABMultiValueGetCount(multi) : 0;
+ if (count) {
+ imArray = [NSMutableArray arrayWithCapacity:count];
+
+ for (CFIndex i = 0; i < ABMultiValueGetCount(multi); i++) {
+ NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:3];
+ // iOS has label property (work, home, other) for each IM but W3C contact API doesn't use
+ CFDictionaryRef dict = (CFDictionaryRef)ABMultiValueCopyValueAtIndex(multi, i);
+ CFStringRef value; // all values should be CFStringRefs / NSString*
+ bool bFound;
+ if ([fields containsObject:kW3ContactFieldValue]) {
+ // value = user name
+ bFound = CFDictionaryGetValueIfPresent(dict, kABPersonInstantMessageUsernameKey, (void*)&value);
+ if (bFound && (value != NULL)) {
+ CFRetain(value);
+ [newDict setObject:(__bridge id)value forKey:kW3ContactFieldValue];
+ CFRelease(value);
+ } else {
+ [newDict setObject:[NSNull null] forKey:kW3ContactFieldValue];
+ }
+ }
+ if ([fields containsObject:kW3ContactFieldType]) {
+ bFound = CFDictionaryGetValueIfPresent(dict, kABPersonInstantMessageServiceKey, (void*)&value);
+ if (bFound && (value != NULL)) {
+ CFRetain(value);
+ [newDict setObject:(id)[[CDVContact class] convertPropertyLabelToContactType : (__bridge NSString*)value] forKey:kW3ContactFieldType];
+ CFRelease(value);
+ } else {
+ [newDict setObject:[NSNull null] forKey:kW3ContactFieldType];
+ }
+ }
+ // always set ID
+ id identifier = [NSNumber numberWithUnsignedInt:ABMultiValueGetIdentifierAtIndex(multi, i)];
+ [newDict setObject:(identifier != nil) ? identifier:[NSNull null] forKey:kW3ContactFieldId];
+
+ [(NSMutableArray*)imArray addObject : newDict];
+ CFRelease(dict);
+ }
+ } else {
+ imArray = [NSNull null];
+ }
+
+ if (multi) {
+ CFRelease(multi);
+ }
+ return imArray;
+}
+
+/* Create array of Dictionaries to match JavaScript ContactOrganization object
+ * pref - not supported in iOS
+ * type - not supported in iOS
+ * name
+ * department
+ * title
+ */
+
+- (NSObject*)extractOrganizations
+{
+ NSArray* fields = [self.returnFields objectForKey:kW3ContactOrganizations];
+
+ if (fields == nil) { // no name fields requested
+ return nil;
+ }
+ NSObject* array = nil;
+ NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:5];
+ id value;
+ int validValueCount = 0;
+
+ for (id i in fields) {
+ id key = [[CDVContact defaultW3CtoAB] valueForKey:i];
+ if (key && [key isKindOfClass:[NSNumber class]]) {
+ value = (__bridge_transfer NSString*)ABRecordCopyValue(self.record, (ABPropertyID)[[[CDVContact defaultW3CtoAB] valueForKey:i] intValue]);
+ if (value != nil) {
+ // if there are no organization values we should return null for organization
+ // this counter keeps indicates if any organization values have been set
+ validValueCount++;
+ }
+ [newDict setObject:(value != nil) ? value:[NSNull null] forKey:i];
+ } else { // not a key iOS supports, set to null
+ [newDict setObject:[NSNull null] forKey:i];
+ }
+ }
+
+ if (([newDict count] > 0) && (validValueCount > 0)) {
+ // add pref and type
+ // they are not supported by iOS and thus these values never change
+ [newDict setObject:@"false" forKey:kW3ContactFieldPrimary];
+ [newDict setObject:[NSNull null] forKey:kW3ContactFieldType];
+ array = [NSMutableArray arrayWithCapacity:1];
+ [(NSMutableArray*)array addObject : newDict];
+ } else {
+ array = [NSNull null];
+ }
+ return array;
+}
+
+// W3C Contacts expects an array of photos. Can return photos in more than one format, currently
+// just returning the default format
+// Save the photo data into tmp directory and return FileURI - temp directory is deleted upon application exit
+- (NSObject*)extractPhotos
+{
+ NSMutableArray* photos = nil;
+
+ if (ABPersonHasImageData(self.record)) {
+ CFDataRef photoData = ABPersonCopyImageData(self.record);
+ NSData* data = (__bridge NSData*)photoData;
+ // write to temp directory and store URI in photos array
+ // get the temp directory path
+ NSString* docsPath = [NSTemporaryDirectory()stringByStandardizingPath];
+ NSError* err = nil;
+ NSString* filePath = [NSString stringWithFormat:@"%@/photo_XXXXX", docsPath];
+ char template[filePath.length + 1];
+ strcpy(template, [filePath cStringUsingEncoding:NSASCIIStringEncoding]);
+ mkstemp(template);
+ filePath = [[NSFileManager defaultManager]
+ stringWithFileSystemRepresentation:template
+ length:strlen(template)];
+
+ // save file
+ if ([data writeToFile:filePath options:NSAtomicWrite error:&err]) {
+ photos = [NSMutableArray arrayWithCapacity:1];
+ NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:2];
+ [newDict setObject:filePath forKey:kW3ContactFieldValue];
+ [newDict setObject:@"url" forKey:kW3ContactFieldType];
+ [newDict setObject:@"false" forKey:kW3ContactFieldPrimary];
+ [photos addObject:newDict];
+ }
+
+ CFRelease(photoData);
+ }
+ return photos;
+}
+
+/**
+ * given an array of W3C Contact field names, create a dictionary of field names to extract
+ * if field name represents an object, return all properties for that object: "name" - returns all properties in ContactName
+ * if field name is an explicit property, return only those properties: "name.givenName - returns a ContactName with only ContactName.givenName
+ * if field contains ONLY ["*"] return all fields
+ * dictionary format:
+ * key is W3Contact #define
+ * value is NSMutableArray* for complex keys: name,addresses,organizations, phone, emails, ims
+ * value is [NSNull null] for simple keys
+*/
++ (NSDictionary*)calcReturnFields:(NSArray*)fieldsArray // NSLog(@"getting self.returnFields");
+{
+ NSMutableDictionary* d = [NSMutableDictionary dictionaryWithCapacity:1];
+
+ if ((fieldsArray != nil) && [fieldsArray isKindOfClass:[NSArray class]]) {
+ if (([fieldsArray count] == 1) && [[fieldsArray objectAtIndex:0] isEqualToString:@"*"]) {
+ return [CDVContact defaultFields]; // return all fields
+ }
+
+ for (id i in fieldsArray) {
+ NSMutableArray* keys = nil;
+ NSString* fieldStr = nil;
+ if ([i isKindOfClass:[NSNumber class]]) {
+ fieldStr = [i stringValue];
+ } else {
+ fieldStr = i;
+ }
+
+ // see if this is specific property request in object - object.property
+ NSArray* parts = [fieldStr componentsSeparatedByString:@"."]; // returns original string if no separator found
+ NSString* name = [parts objectAtIndex:0];
+ NSString* property = nil;
+ if ([parts count] > 1) {
+ property = [parts objectAtIndex:1];
+ }
+ // see if this is a complex field by looking for its array of properties in objectAndProperties dictionary
+ id fields = [[CDVContact defaultObjectAndProperties] objectForKey:name];
+
+ // if find complex name (name,addresses,organizations, phone, emails, ims) in fields, add name as key
+ // with array of associated properties as the value
+ if ((fields != nil) && (property == nil)) { // request was for full object
+ keys = [NSMutableArray arrayWithArray:fields];
+ if (keys != nil) {
+ [d setObject:keys forKey:name]; // will replace if prop array already exists
+ }
+ } else if ((fields != nil) && (property != nil)) {
+ // found an individual property request in form of name.property
+ // verify is real property name by using it as key in W3CtoAB
+ id abEquiv = [[CDVContact defaultW3CtoAB] objectForKey:property];
+ if (abEquiv || [[CDVContact defaultW3CtoNull] containsObject:property]) {
+ // if existing array add to it
+ if ((keys = [d objectForKey:name]) != nil) {
+ [keys addObject:property];
+ } else {
+ keys = [NSMutableArray arrayWithObject:property];
+ [d setObject:keys forKey:name];
+ }
+ } else {
+ NSLog(@"Contacts.find -- request for invalid property ignored: %@.%@", name, property);
+ }
+ } else { // is an individual property, verify is real property name by using it as key in W3CtoAB
+ id valid = [[CDVContact defaultW3CtoAB] objectForKey:name];
+ if (valid || [[CDVContact defaultW3CtoNull] containsObject:name]) {
+ [d setObject:[NSNull null] forKey:name];
+ }
+ }
+ }
+ }
+ if ([d count] == 0) {
+ // no array or nothing in the array. W3C spec says to return nothing
+ return nil; // [Contact defaultFields];
+ }
+ return d;
+}
+
+/*
+ * Search for the specified value in each of the fields specified in the searchFields dictionary.
+ * NSString* value - the string value to search for (need clarification from W3C on how to search for dates)
+ * NSDictionary* searchFields - a dictionary created via calcReturnFields where the key is the top level W3C
+ * object and the object is the array of specific fields within that object or null if it is a single property
+ * RETURNS
+ * YES as soon as a match is found in any of the fields
+ * NO - the specified value does not exist in any of the fields in this contact
+ *
+ * Note: I'm not a fan of returning in the middle of methods but have done it some in this method in order to
+ * keep the code simpler. bgibson
+ */
+- (BOOL)foundValue:(NSString*)testValue inFields:(NSDictionary*)searchFields
+{
+ BOOL bFound = NO;
+
+ if ((testValue == nil) || ![testValue isKindOfClass:[NSString class]] || ([testValue length] == 0)) {
+ // nothing to find so return NO
+ return NO;
+ }
+ NSInteger valueAsInt = [testValue integerValue];
+
+ // per W3C spec, always include id in search
+ int recordId = ABRecordGetRecordID(self.record);
+ if (valueAsInt && (recordId == valueAsInt)) {
+ return YES;
+ }
+
+ if (searchFields == nil) {
+ // no fields to search
+ return NO;
+ }
+
+ if ([searchFields valueForKey:kW3ContactNickname]) {
+ bFound = [self testStringValue:testValue forW3CProperty:kW3ContactNickname];
+ if (bFound == YES) {
+ return bFound;
+ }
+ }
+
+ if ([searchFields valueForKeyIsArray:kW3ContactName]) {
+ // test name fields. All are string properties obtained via ABRecordCopyValue except kW3ContactFormattedName
+ NSArray* fields = [searchFields valueForKey:kW3ContactName];
+
+ for (NSString* testItem in fields) {
+ if ([testItem isEqualToString:kW3ContactFormattedName]) {
+ NSString* propValue = (__bridge_transfer NSString*)ABRecordCopyCompositeName(self.record);
+ if ((propValue != nil) && ([propValue length] > 0)) {
+ NSRange range = [propValue rangeOfString:testValue options:NSCaseInsensitiveSearch];
+ bFound = (range.location != NSNotFound);
+ propValue = nil;
+ }
+ } else {
+ bFound = [self testStringValue:testValue forW3CProperty:testItem];
+ }
+
+ if (bFound) {
+ break;
+ }
+ }
+ }
+ if (!bFound && [searchFields valueForKeyIsArray:kW3ContactPhoneNumbers]) {
+ bFound = [self searchContactFields:(NSArray*)[searchFields valueForKey:kW3ContactPhoneNumbers]
+ forMVStringProperty:kABPersonPhoneProperty withValue:testValue];
+ }
+ if (!bFound && [searchFields valueForKeyIsArray:kW3ContactEmails]) {
+ bFound = [self searchContactFields:(NSArray*)[searchFields valueForKey:kW3ContactEmails]
+ forMVStringProperty:kABPersonEmailProperty withValue:testValue];
+ }
+
+ if (!bFound && [searchFields valueForKeyIsArray:kW3ContactAddresses]) {
+ bFound = [self searchContactFields:[searchFields valueForKey:kW3ContactAddresses]
+ forMVDictionaryProperty:kABPersonAddressProperty withValue:testValue];
+ }
+
+ if (!bFound && [searchFields valueForKeyIsArray:kW3ContactIms]) {
+ bFound = [self searchContactFields:[searchFields valueForKey:kW3ContactIms]
+ forMVDictionaryProperty:kABPersonInstantMessageProperty withValue:testValue];
+ }
+
+ if (!bFound && [searchFields valueForKeyIsArray:kW3ContactOrganizations]) {
+ NSArray* fields = [searchFields valueForKey:kW3ContactOrganizations];
+
+ for (NSString* testItem in fields) {
+ bFound = [self testStringValue:testValue forW3CProperty:testItem];
+ if (bFound == YES) {
+ break;
+ }
+ }
+ }
+ if (!bFound && [searchFields valueForKey:kW3ContactNote]) {
+ bFound = [self testStringValue:testValue forW3CProperty:kW3ContactNote];
+ }
+
+ // if searching for a date field is requested, get the date field as a localized string then look for match against testValue in date string
+ // searching for photos is not supported
+ if (!bFound && [searchFields valueForKey:kW3ContactBirthday]) {
+ bFound = [self testDateValue:testValue forW3CProperty:kW3ContactBirthday];
+ }
+ if (!bFound && [searchFields valueForKeyIsArray:kW3ContactUrls]) {
+ bFound = [self searchContactFields:(NSArray*)[searchFields valueForKey:kW3ContactUrls]
+ forMVStringProperty:kABPersonURLProperty withValue:testValue];
+ }
+
+ return bFound;
+}
+
+/*
+ * Test for the existence of a given string within the value of a ABPersonRecord string property based on the W3c property name.
+ *
+ * IN:
+ * NSString* testValue - the value to find - search is case insensitive
+ * NSString* property - the W3c property string
+ * OUT:
+ * BOOL YES if the given string was found within the property value
+ * NO if the testValue was not found, W3C property string was invalid or the AddressBook property was not a string
+ */
+- (BOOL)testStringValue:(NSString*)testValue forW3CProperty:(NSString*)property
+{
+ BOOL bFound = NO;
+
+ if ([[CDVContact defaultW3CtoAB] valueForKeyIsNumber:property]) {
+ ABPropertyID propId = [[[CDVContact defaultW3CtoAB] objectForKey:property] intValue];
+ if (ABPersonGetTypeOfProperty(propId) == kABStringPropertyType) {
+ NSString* propValue = (__bridge_transfer NSString*)ABRecordCopyValue(self.record, propId);
+ if ((propValue != nil) && ([propValue length] > 0)) {
+ NSPredicate* containPred = [NSPredicate predicateWithFormat:@"SELF contains[cd] %@", testValue];
+ bFound = [containPred evaluateWithObject:propValue];
+ // NSRange range = [propValue rangeOfString:testValue options: NSCaseInsensitiveSearch];
+ // bFound = (range.location != NSNotFound);
+ }
+ }
+ }
+ return bFound;
+}
+
+/*
+ * Test for the existence of a given Date string within the value of a ABPersonRecord datetime property based on the W3c property name.
+ *
+ * IN:
+ * NSString* testValue - the value to find - search is case insensitive
+ * NSString* property - the W3c property string
+ * OUT:
+ * BOOL YES if the given string was found within the localized date string value
+ * NO if the testValue was not found, W3C property string was invalid or the AddressBook property was not a DateTime
+ */
+- (BOOL)testDateValue:(NSString*)testValue forW3CProperty:(NSString*)property
+{
+ BOOL bFound = NO;
+
+ if ([[CDVContact defaultW3CtoAB] valueForKeyIsNumber:property]) {
+ ABPropertyID propId = [[[CDVContact defaultW3CtoAB] objectForKey:property] intValue];
+ if (ABPersonGetTypeOfProperty(propId) == kABDateTimePropertyType) {
+ NSDate* date = (__bridge_transfer NSDate*)ABRecordCopyValue(self.record, propId);
+ if (date != nil) {
+ NSString* dateString = [date descriptionWithLocale:[NSLocale currentLocale]];
+ NSPredicate* containPred = [NSPredicate predicateWithFormat:@"SELF contains[cd] %@", testValue];
+ bFound = [containPred evaluateWithObject:dateString];
+ }
+ }
+ }
+ return bFound;
+}
+
+/*
+ * Search the specified fields within an AddressBook multivalue string property for the specified test value.
+ * Used for phoneNumbers, emails and urls.
+ * IN:
+ * NSArray* fields - the fields to search for within the multistring property (value and/or type)
+ * ABPropertyID - the property to search
+ * NSString* testValue - the value to search for. Will convert between W3C types and AB types. Will only
+ * search for types if the testValue is a valid ContactField type.
+ * OUT:
+ * YES if the test value was found in one of the specified fields
+ * NO if the test value was not found
+ */
+- (BOOL)searchContactFields:(NSArray*)fields forMVStringProperty:(ABPropertyID)propId withValue:testValue
+{
+ BOOL bFound = NO;
+
+ for (NSString* type in fields) {
+ NSString* testString = nil;
+ if ([type isEqualToString:kW3ContactFieldType]) {
+ if ([CDVContact isValidW3ContactType:testValue]) {
+ // only search types if the filter string is a valid ContactField.type
+ testString = (NSString*)[CDVContact convertContactTypeToPropertyLabel:testValue];
+ }
+ } else {
+ testString = testValue;
+ }
+
+ if (testString != nil) {
+ bFound = [self testMultiValueStrings:testString forProperty:propId ofType:type];
+ }
+ if (bFound == YES) {
+ break;
+ }
+ }
+
+ return bFound;
+}
+
+/*
+ * Searches a multiString value of the specified type for the specified test value.
+ *
+ * IN:
+ * NSString* testValue - the value to test for
+ * ABPropertyID propId - the property id of the multivalue property to search
+ * NSString* type - the W3C contact type to search for (value or type)
+ * OUT:
+ * YES is the test value was found
+ * NO if the test value was not found
+ */
+- (BOOL)testMultiValueStrings:(NSString*)testValue forProperty:(ABPropertyID)propId ofType:(NSString*)type
+{
+ BOOL bFound = NO;
+
+ if (ABPersonGetTypeOfProperty(propId) == kABMultiStringPropertyType) {
+ NSArray* valueArray = nil;
+ if ([type isEqualToString:kW3ContactFieldType]) {
+ valueArray = [self labelsForProperty:propId inRecord:self.record];
+ } else if ([type isEqualToString:kW3ContactFieldValue]) {
+ valueArray = [self valuesForProperty:propId inRecord:self.record];
+ }
+ if (valueArray) {
+ NSString* valuesAsString = [valueArray componentsJoinedByString:@" "];
+ NSPredicate* containPred = [NSPredicate predicateWithFormat:@"SELF contains[cd] %@", testValue];
+ bFound = [containPred evaluateWithObject:valuesAsString];
+ }
+ }
+ return bFound;
+}
+
+/*
+ * Returns the array of values for a multivalue string property of the specified property id
+ */
+- (__autoreleasing NSArray*)valuesForProperty:(ABPropertyID)propId inRecord:(ABRecordRef)aRecord
+{
+ ABMultiValueRef multi = ABRecordCopyValue(aRecord, propId);
+ NSArray* values = (__bridge_transfer NSArray*)ABMultiValueCopyArrayOfAllValues(multi);
+
+ CFRelease(multi);
+ return values;
+}
+
+/*
+ * Returns the array of labels for a multivalue string property of the specified property id
+ */
+- (NSArray*)labelsForProperty:(ABPropertyID)propId inRecord:(ABRecordRef)aRecord
+{
+ ABMultiValueRef multi = ABRecordCopyValue(aRecord, propId);
+ CFIndex count = ABMultiValueGetCount(multi);
+ NSMutableArray* labels = [NSMutableArray arrayWithCapacity:count];
+
+ for (int i = 0; i < count; i++) {
+ NSString* label = (__bridge_transfer NSString*)ABMultiValueCopyLabelAtIndex(multi, i);
+ if (label) {
+ [labels addObject:label];
+ }
+ }
+
+ CFRelease(multi);
+ return labels;
+}
+
+/* search for values within MultiValue Dictionary properties Address or IM property
+ * IN:
+ * (NSArray*) fields - the array of W3C field names to search within
+ * (ABPropertyID) propId - the AddressBook property that returns a multivalue dictionary
+ * (NSString*) testValue - the string to search for within the specified fields
+ *
+ */
+- (BOOL)searchContactFields:(NSArray*)fields forMVDictionaryProperty:(ABPropertyID)propId withValue:(NSString*)testValue
+{
+ BOOL bFound = NO;
+
+ NSArray* values = [self valuesForProperty:propId inRecord:self.record]; // array of dictionaries (as CFDictionaryRef)
+ int dictCount = [values count];
+
+ // for ims dictionary contains with service (w3C type) and username (W3c value)
+ // for addresses dictionary contains street, city, state, zip, country
+ for (int i = 0; i < dictCount; i++) {
+ CFDictionaryRef dict = (__bridge CFDictionaryRef)[values objectAtIndex:i];
+
+ for (NSString* member in fields) {
+ NSString* abKey = [[CDVContact defaultW3CtoAB] valueForKey:member]; // im and address fields are all strings
+ CFStringRef abValue = nil;
+ if (abKey) {
+ NSString* testString = nil;
+ if ([member isEqualToString:kW3ContactImType]) {
+ if ([CDVContact isValidW3ContactType:testValue]) {
+ // only search service/types if the filter string is a valid ContactField.type
+ testString = (NSString*)[CDVContact convertContactTypeToPropertyLabel:testValue];
+ }
+ } else {
+ testString = testValue;
+ }
+ if (testString != nil) {
+ BOOL bExists = CFDictionaryGetValueIfPresent(dict, (__bridge const void*)abKey, (void*)&abValue);
+ if (bExists) {
+ CFRetain(abValue);
+ NSPredicate* containPred = [NSPredicate predicateWithFormat:@"SELF contains[cd] %@", testString];
+ bFound = [containPred evaluateWithObject:(__bridge id)abValue];
+ CFRelease(abValue);
+ }
+ }
+ }
+ if (bFound == YES) {
+ break;
+ }
+ } // end of for each member in fields
+
+ if (bFound == YES) {
+ break;
+ }
+ } // end of for each dictionary
+
+ return bFound;
+}
+
+@end
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/ios/CDVContacts.h
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/ios/CDVContacts.h b/spec/plugins/Contacts/src/ios/CDVContacts.h
new file mode 100644
index 0000000..e3deb21
--- /dev/null
+++ b/spec/plugins/Contacts/src/ios/CDVContacts.h
@@ -0,0 +1,151 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import <AddressBook/ABAddressBook.h>
+#import <AddressBookUI/AddressBookUI.h>
+#import <Cordova/CDVPlugin.h>
+#import "CDVContact.h"
+
+@interface CDVContacts : CDVPlugin <ABNewPersonViewControllerDelegate,
+ ABPersonViewControllerDelegate,
+ ABPeoplePickerNavigationControllerDelegate
+ >
+{
+ ABAddressBookRef addressBook;
+}
+
+/*
+ * newContact - create a new contact via the GUI
+ *
+ * arguments:
+ * 1: successCallback: this is the javascript function that will be called with the newly created contactId
+ */
+- (void)newContact:(CDVInvokedUrlCommand*)command;
+
+/*
+ * displayContact - IN PROGRESS
+ *
+ * arguments:
+ * 1: recordID of the contact to display in the iPhone contact display
+ * 2: successCallback - currently not used
+ * 3: error callback
+ * options:
+ * allowsEditing: set to true to allow the user to edit the contact - currently not supported
+ */
+- (void)displayContact:(CDVInvokedUrlCommand*)command;
+
+/*
+ * chooseContact
+ *
+ * arguments:
+ * 1: this is the javascript function that will be called with the contact data as a JSON object (as the first param)
+ * options:
+ * allowsEditing: set to true to not choose the contact, but to edit it in the iPhone contact editor
+ */
+- (void)chooseContact:(CDVInvokedUrlCommand*)command;
+
+- (void)newPersonViewController:(ABNewPersonViewController*)newPersonViewController didCompleteWithNewPerson:(ABRecordRef)person;
+- (BOOL)personViewController:(ABPersonViewController*)personViewController shouldPerformDefaultActionForPerson:(ABRecordRef)person
+ property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifierForValue;
+
+/*
+ * search - searches for contacts. Only person records are currently supported.
+ *
+ * arguments:
+ * 1: successcallback - this is the javascript function that will be called with the array of found contacts
+ * 2: errorCallback - optional javascript function to be called in the event of an error with an error code.
+ * options: dictionary containing ContactFields and ContactFindOptions
+ * fields - ContactFields array
+ * findOptions - ContactFindOptions object as dictionary
+ *
+ */
+- (void)search:(CDVInvokedUrlCommand*)command;
+
+/*
+ * save - saves a new contact or updates and existing contact
+ *
+ * arguments:
+ * 1: success callback - this is the javascript function that will be called with the JSON representation of the saved contact
+ * search calls a fixed navigator.service.contacts._findCallback which then calls the success callback stored before making the call into obj-c
+ */
+- (void)save:(CDVInvokedUrlCommand*)command;
+
+/*
+ * remove - removes a contact from the address book
+ *
+ * arguments:
+ * 1: 1: successcallback - this is the javascript function that will be called with a (now) empty contact object
+ *
+ * options: dictionary containing Contact object to remove
+ * contact - Contact object as dictionary
+ */
+- (void)remove:(CDVInvokedUrlCommand*)command;
+
+// - (void) dealloc;
+
+@end
+
+@interface CDVContactsPicker : ABPeoplePickerNavigationController
+{
+ BOOL allowsEditing;
+ NSString* callbackId;
+ NSDictionary* options;
+ NSDictionary* pickedContactDictionary;
+}
+
+@property BOOL allowsEditing;
+@property (copy) NSString* callbackId;
+@property (nonatomic, strong) NSDictionary* options;
+@property (nonatomic, strong) NSDictionary* pickedContactDictionary;
+
+@end
+
+@interface CDVNewContactsController : ABNewPersonViewController
+{
+ NSString* callbackId;
+}
+@property (copy) NSString* callbackId;
+@end
+
+/* ABPersonViewController does not have any UI to dismiss. Adding navigationItems to it does not work properly, the navigationItems are lost when the app goes into the background.
+ The solution was to create an empty NavController in front of the ABPersonViewController. This
+ causes the ABPersonViewController to have a back button. By subclassing the ABPersonViewController,
+ we can override viewWillDisappear and take down the entire NavigationController at that time.
+ */
+@interface CDVDisplayContactViewController : ABPersonViewController
+{}
+@property (nonatomic, strong) CDVPlugin* contactsPlugin;
+
+@end
+@interface CDVAddressBookAccessError : NSObject
+{}
+@property (assign) CDVContactError errorCode;
+- (CDVAddressBookAccessError*)initWithCode:(CDVContactError)code;
+@end
+
+typedef void (^ CDVAddressBookWorkerBlock)(
+ ABAddressBookRef addressBook,
+ CDVAddressBookAccessError* error
+ );
+@interface CDVAddressBookHelper : NSObject
+{}
+
+- (void)createAddressBook:(CDVAddressBookWorkerBlock)workerBlock;
+@end
[4/6] [CB-4341] Adding a fix to make subdirectories work within a
local plugin dependency - Includes the integration of integration specs which
test installation of plugins with dependencies
Posted by br...@apache.org.
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/android/ContactManager.java
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/android/ContactManager.java b/spec/plugins/Contacts/src/android/ContactManager.java
new file mode 100755
index 0000000..1c086e1
--- /dev/null
+++ b/spec/plugins/Contacts/src/android/ContactManager.java
@@ -0,0 +1,122 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+package org.apache.cordova.core;
+
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.PluginResult;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import android.util.Log;
+
+public class ContactManager extends CordovaPlugin {
+
+ private ContactAccessor contactAccessor;
+ private static final String LOG_TAG = "Contact Query";
+
+ public static final int UNKNOWN_ERROR = 0;
+ public static final int INVALID_ARGUMENT_ERROR = 1;
+ public static final int TIMEOUT_ERROR = 2;
+ public static final int PENDING_OPERATION_ERROR = 3;
+ public static final int IO_ERROR = 4;
+ public static final int NOT_SUPPORTED_ERROR = 5;
+ public static final int PERMISSION_DENIED_ERROR = 20;
+
+ /**
+ * Constructor.
+ */
+ public ContactManager() {
+ }
+
+ /**
+ * Executes the request and returns PluginResult.
+ *
+ * @param action The action to execute.
+ * @param args JSONArray of arguments for the plugin.
+ * @param callbackContext The callback context used when calling back into JavaScript.
+ * @return True if the action was valid, false otherwise.
+ */
+ public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
+ /**
+ * Check to see if we are on an Android 1.X device. If we are return an error as we
+ * do not support this as of Cordova 1.0.
+ */
+ if (android.os.Build.VERSION.RELEASE.startsWith("1.")) {
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, ContactManager.NOT_SUPPORTED_ERROR));
+ return true;
+ }
+
+ /**
+ * Only create the contactAccessor after we check the Android version or the program will crash
+ * older phones.
+ */
+ if (this.contactAccessor == null) {
+ this.contactAccessor = new ContactAccessorSdk5(this.webView, this.cordova);
+ }
+
+ if (action.equals("search")) {
+ final JSONArray filter = args.getJSONArray(0);
+ final JSONObject options = args.getJSONObject(1);
+ this.cordova.getThreadPool().execute(new Runnable() {
+ public void run() {
+ JSONArray res = contactAccessor.search(filter, options);
+ callbackContext.success(res);
+ }
+ });
+ }
+ else if (action.equals("save")) {
+ final JSONObject contact = args.getJSONObject(0);
+ this.cordova.getThreadPool().execute(new Runnable() {
+ public void run() {
+ JSONObject res = null;
+ String id = contactAccessor.save(contact);
+ if (id != null) {
+ try {
+ res = contactAccessor.getContactById(id);
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, "JSON fail.", e);
+ }
+ }
+ if (res != null) {
+ callbackContext.success(res);
+ } else {
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, UNKNOWN_ERROR));
+ }
+ }
+ });
+ }
+ else if (action.equals("remove")) {
+ final String contactId = args.getString(0);
+ this.cordova.getThreadPool().execute(new Runnable() {
+ public void run() {
+ if (contactAccessor.remove(contactId)) {
+ callbackContext.success();
+ } else {
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, UNKNOWN_ERROR));
+ }
+ }
+ });
+ }
+ else {
+ return false;
+ }
+ return true;
+ }
+}
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/blackberry10/ContactActivity.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/blackberry10/ContactActivity.js b/spec/plugins/Contacts/src/blackberry10/ContactActivity.js
new file mode 100644
index 0000000..f0f82b3
--- /dev/null
+++ b/spec/plugins/Contacts/src/blackberry10/ContactActivity.js
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2012 Research In Motion Limited.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+var ContactActivity = function (args) {
+ this.direction = args.direction || null;
+ this.description = args.description || "";
+ this.mimeType = args.mimeType || "";
+ this.timestamp = new Date(parseInt(args.timestamp, 10)) || null;
+};
+
+Object.defineProperty(ContactActivity, "INCOMING", {"value": true});
+Object.defineProperty(ContactActivity, "OUTGOING", {"value": false});
+
+module.exports = ContactActivity;
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/blackberry10/ContactAddress.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/blackberry10/ContactAddress.js b/spec/plugins/Contacts/src/blackberry10/ContactAddress.js
new file mode 100644
index 0000000..1ba9fe4
--- /dev/null
+++ b/spec/plugins/Contacts/src/blackberry10/ContactAddress.js
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2012 Research In Motion Limited.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+var ContactAddress = function (properties) {
+ this.type = properties && properties.type ? properties.type : "";
+ this.streetAddress = properties && properties.streetAddress ? properties.streetAddress : "";
+ this.streetOther = properties && properties.streetOther ? properties.streetOther : "";
+ this.locality = properties && properties.locality ? properties.locality : "";
+ this.region = properties && properties.region ? properties.region : "";
+ this.postalCode = properties && properties.postalCode ? properties.postalCode : "";
+ this.country = properties && properties.country ? properties.country : "";
+};
+
+Object.defineProperty(ContactAddress, "HOME", {"value": "home"});
+Object.defineProperty(ContactAddress, "WORK", {"value": "work"});
+Object.defineProperty(ContactAddress, "OTHER", {"value": "other"});
+
+module.exports = ContactAddress;
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/blackberry10/ContactError.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/blackberry10/ContactError.js b/spec/plugins/Contacts/src/blackberry10/ContactError.js
new file mode 100644
index 0000000..f20f85e
--- /dev/null
+++ b/spec/plugins/Contacts/src/blackberry10/ContactError.js
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2012 Research In Motion Limited.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+var ContactError = function (code, msg) {
+ this.code = code;
+ this.message = msg;
+};
+
+Object.defineProperty(ContactError, "UNKNOWN_ERROR", { "value": 0 });
+Object.defineProperty(ContactError, "INVALID_ARGUMENT_ERROR", { "value": 1 });
+Object.defineProperty(ContactError, "TIMEOUT_ERROR", { "value": 2 });
+Object.defineProperty(ContactError, "PENDING_OPERATION_ERROR", { "value": 3 });
+Object.defineProperty(ContactError, "IO_ERROR", { "value": 4 });
+Object.defineProperty(ContactError, "NOT_SUPPORTED_ERROR", { "value": 5 });
+Object.defineProperty(ContactError, "PERMISSION_DENIED_ERROR", { "value": 20 });
+
+module.exports = ContactError;
+
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/blackberry10/ContactField.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/blackberry10/ContactField.js b/spec/plugins/Contacts/src/blackberry10/ContactField.js
new file mode 100644
index 0000000..aad735c
--- /dev/null
+++ b/spec/plugins/Contacts/src/blackberry10/ContactField.js
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2012 Research In Motion Limited.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+var ContactField = function (type, value) {
+ this.type = type || "";
+ this.value = value || "";
+};
+
+Object.defineProperty(ContactField, "HOME", {"value": "home"});
+Object.defineProperty(ContactField, "WORK", {"value": "work"});
+Object.defineProperty(ContactField, "OTHER", {"value": "other"});
+Object.defineProperty(ContactField, "MOBILE", {"value": "mobile"});
+Object.defineProperty(ContactField, "DIRECT", {"value": "direct"});
+
+module.exports = ContactField;
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/blackberry10/ContactFindOptions.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/blackberry10/ContactFindOptions.js b/spec/plugins/Contacts/src/blackberry10/ContactFindOptions.js
new file mode 100644
index 0000000..8be830d
--- /dev/null
+++ b/spec/plugins/Contacts/src/blackberry10/ContactFindOptions.js
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2012 Research In Motion Limited.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * ContactFindOptions.
+ * @constructor
+ * @param filter search fields
+ * @param sort sort fields and order
+ * @param limit max number of contacts to return
+ * @param favorite if set, only favorite contacts will be returned
+ */
+
+var ContactFindOptions = function (filter, sort, limit, favorite) {
+ this.filter = filter || null;
+ this.sort = sort || null;
+ this.limit = limit || -1; // -1 for returning all results
+ this.favorite = favorite || false;
+ this.includeAccounts = [];
+ this.excludeAccounts = [];
+};
+
+Object.defineProperty(ContactFindOptions, "SEARCH_FIELD_GIVEN_NAME", { "value": 0 });
+Object.defineProperty(ContactFindOptions, "SEARCH_FIELD_FAMILY_NAME", { "value": 1 });
+Object.defineProperty(ContactFindOptions, "SEARCH_FIELD_ORGANIZATION_NAME", { "value": 2 });
+Object.defineProperty(ContactFindOptions, "SEARCH_FIELD_PHONE", { "value": 3 });
+Object.defineProperty(ContactFindOptions, "SEARCH_FIELD_EMAIL", { "value": 4 });
+Object.defineProperty(ContactFindOptions, "SEARCH_FIELD_BBMPIN", { "value": 5 });
+Object.defineProperty(ContactFindOptions, "SEARCH_FIELD_LINKEDIN", { "value": 6 });
+Object.defineProperty(ContactFindOptions, "SEARCH_FIELD_TWITTER", { "value": 7 });
+Object.defineProperty(ContactFindOptions, "SEARCH_FIELD_VIDEO_CHAT", { "value": 8 });
+
+Object.defineProperty(ContactFindOptions, "SORT_FIELD_GIVEN_NAME", { "value": 0 });
+Object.defineProperty(ContactFindOptions, "SORT_FIELD_FAMILY_NAME", { "value": 1 });
+Object.defineProperty(ContactFindOptions, "SORT_FIELD_ORGANIZATION_NAME", { "value": 2 });
+
+module.exports = ContactFindOptions;
+
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/blackberry10/ContactName.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/blackberry10/ContactName.js b/spec/plugins/Contacts/src/blackberry10/ContactName.js
new file mode 100644
index 0000000..9b74753
--- /dev/null
+++ b/spec/plugins/Contacts/src/blackberry10/ContactName.js
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2012 Research In Motion Limited.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+function toFormattedName(properties) {
+ var formatted = "";
+ if (properties && properties.givenName) {
+ formatted = properties.givenName;
+ if (properties && properties.familyName) {
+ formatted += " " + properties.familyName;
+ }
+ }
+ return formatted;
+}
+
+var ContactName = function (properties) {
+ this.familyName = properties && properties.familyName ? properties.familyName : "";
+ this.givenName = properties && properties.givenName ? properties.givenName : "";
+ this.formatted = toFormattedName(properties);
+ this.middleName = properties && properties.middleName ? properties.middleName : "";
+ this.honorificPrefix = properties && properties.honorificPrefix ? properties.honorificPrefix : "";
+ this.honorificSuffix = properties && properties.honorificSuffix ? properties.honorificSuffix : "";
+ this.phoneticFamilyName = properties && properties.phoneticFamilyName ? properties.phoneticFamilyName : "";
+ this.phoneticGivenName = properties && properties.phoneticGivenName ? properties.phoneticGivenName : "";
+};
+
+module.exports = ContactName;
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/blackberry10/ContactNews.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/blackberry10/ContactNews.js b/spec/plugins/Contacts/src/blackberry10/ContactNews.js
new file mode 100644
index 0000000..9fb86dc
--- /dev/null
+++ b/spec/plugins/Contacts/src/blackberry10/ContactNews.js
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2012 Research In Motion Limited.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+var ContactNews = function (args) {
+ this.title = args.title || "";
+ this.body = args.body || "";
+ this.articleSource = args.articleSource || "";
+ this.companies = args.companies || [];
+ this.publishedAt = new Date(parseInt(args.publishedAt, 10)) || null;
+ this.uri = args.uri || "";
+ this.type = args.type || "";
+};
+
+module.exports = ContactNews;
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/blackberry10/ContactOrganization.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/blackberry10/ContactOrganization.js b/spec/plugins/Contacts/src/blackberry10/ContactOrganization.js
new file mode 100644
index 0000000..987310f
--- /dev/null
+++ b/spec/plugins/Contacts/src/blackberry10/ContactOrganization.js
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2012 Research In Motion Limited.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+var ContactOrganization = function (properties) {
+ this.name = properties && properties.name ? properties.name : "";
+ this.department = properties && properties.department ? properties.department : "";
+ this.title = properties && properties.title ? properties.title : "";
+};
+
+module.exports = ContactOrganization;
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/blackberry10/ContactPhoto.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/blackberry10/ContactPhoto.js b/spec/plugins/Contacts/src/blackberry10/ContactPhoto.js
new file mode 100644
index 0000000..eeaa263
--- /dev/null
+++ b/spec/plugins/Contacts/src/blackberry10/ContactPhoto.js
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2012 Research In Motion Limited.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+var ContactPhoto = function (originalFilePath, pref) {
+ this.originalFilePath = originalFilePath || "";
+ this.pref = pref || false;
+ this.largeFilePath = "";
+ this.smallFilePath = "";
+};
+
+module.exports = ContactPhoto;
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/blackberry10/contactConsts.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/blackberry10/contactConsts.js b/spec/plugins/Contacts/src/blackberry10/contactConsts.js
new file mode 100644
index 0000000..ef25206
--- /dev/null
+++ b/spec/plugins/Contacts/src/blackberry10/contactConsts.js
@@ -0,0 +1,225 @@
+/*
+* Copyright 2012 Research In Motion Limited.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+var ATTRIBUTE_KIND,
+ ATTRIBUTE_SUBKIND,
+ kindAttributeMap = {},
+ subKindAttributeMap = {},
+ _TITLE = 26,
+ _START_DATE = 43,
+ _END_DATE = 44;
+
+function populateKindAttributeMap() {
+ ATTRIBUTE_KIND = {
+ Invalid: 0,
+ Phone: 1,
+ Fax: 2,
+ Pager: 3,
+ Email: 4,
+ Website: 5,
+ Feed: 6,
+ Profile: 7,
+ Family: 8,
+ Person: 9,
+ Date: 10,
+ Group: 11,
+ Name: 12,
+ StockSymbol: 13,
+ Ranking: 14,
+ OrganizationAffiliation: 15,
+ Education: 16,
+ Note: 17,
+ InstantMessaging: 18,
+ VideoChat: 19,
+ ConnectionCount: 20,
+ Hidden: 21,
+ Biography: 22,
+ Sound: 23,
+ Notification: 24,
+ MessageSound: 25,
+ MessageNotification: 26
+ };
+
+ kindAttributeMap[ATTRIBUTE_KIND.Phone] = "phoneNumbers";
+ kindAttributeMap[ATTRIBUTE_KIND.Fax] = "faxNumbers";
+ kindAttributeMap[ATTRIBUTE_KIND.Pager] = "pagerNumber";
+ kindAttributeMap[ATTRIBUTE_KIND.Email] = "emails";
+ kindAttributeMap[ATTRIBUTE_KIND.Website] = "urls";
+ kindAttributeMap[ATTRIBUTE_KIND.Profile] = "socialNetworks";
+ kindAttributeMap[ATTRIBUTE_KIND.OrganizationAffiliation] = "organizations";
+ kindAttributeMap[ATTRIBUTE_KIND.Education] = "education";
+ kindAttributeMap[ATTRIBUTE_KIND.Note] = "note";
+ kindAttributeMap[ATTRIBUTE_KIND.InstantMessaging] = "ims";
+ kindAttributeMap[ATTRIBUTE_KIND.VideoChat] = "videoChat";
+ kindAttributeMap[ATTRIBUTE_KIND.Sound] = "ringtone";
+}
+
+function populateSubKindAttributeMap() {
+ ATTRIBUTE_SUBKIND = {
+ Invalid: 0,
+ Other: 1,
+ Home: 2,
+ Work: 3,
+ PhoneMobile: 4,
+ FaxDirect: 5,
+ Blog: 6,
+ WebsiteResume: 7,
+ WebsitePortfolio: 8,
+ WebsitePersonal: 9,
+ WebsiteCompany: 10,
+ ProfileFacebook: 11,
+ ProfileTwitter: 12,
+ ProfileLinkedIn: 13,
+ ProfileGist: 14,
+ ProfileTungle: 15,
+ FamilySpouse: 16,
+ FamilyChild: 17,
+ FamilyParent: 18,
+ PersonManager: 19,
+ PersonAssistant: 20,
+ DateBirthday: 21,
+ DateAnniversary: 22,
+ GroupDepartment: 23,
+ NameGiven: 24,
+ NameSurname: 25,
+ Title: _TITLE,
+ NameSuffix: 27,
+ NameMiddle: 28,
+ NameNickname: 29,
+ NameAlias: 30,
+ NameDisplayName: 31,
+ NamePhoneticGiven: 32,
+ NamePhoneticSurname: 33,
+ StockSymbolNyse: 34,
+ StockSymbolNasdaq: 35,
+ StockSymbolTse: 36,
+ StockSymbolLse: 37,
+ StockSymbolTsx: 38,
+ RankingKlout: 39,
+ RankingTrstRank: 40,
+ OrganizationAffiliationName: 41,
+ OrganizationAffiliationPhoneticName: 42,
+ OrganizationAffiliationTitle: _TITLE,
+ StartDate: _START_DATE,
+ EndDate: _END_DATE,
+ OrganizationAffiliationDetails: 45,
+ EducationInstitutionName: 46,
+ EducationStartDate: _START_DATE,
+ EducationEndDate: _END_DATE,
+ EducationDegree: 47,
+ EducationConcentration: 48,
+ EducationActivities: 49,
+ EducationNotes: 50,
+ InstantMessagingBbmPin: 51,
+ InstantMessagingAim: 52,
+ InstantMessagingAliwangwang: 53,
+ InstantMessagingGoogleTalk: 54,
+ InstantMessagingSametime: 55,
+ InstantMessagingIcq: 56,
+ InstantMessagingIrc: 57,
+ InstantMessagingJabber: 58,
+ InstantMessagingMsLcs: 59,
+ InstantMessagingMsn: 60,
+ InstantMessagingQq: 61,
+ InstantMessagingSkype: 62,
+ InstantMessagingYahooMessenger: 63,
+ InstantMessagingYahooMessengerJapan: 64,
+ VideoChatBbPlaybook: 65,
+ HiddenLinkedIn: 66,
+ HiddenFacebook: 67,
+ HiddenTwitter: 68,
+ ConnectionCountLinkedIn: 69,
+ ConnectionCountFacebook: 70,
+ ConnectionCountTwitter: 71,
+ HiddenChecksum: 72,
+ HiddenSpeedDial: 73,
+ BiographyFacebook: 74,
+ BiographyTwitter: 75,
+ BiographyLinkedIn: 76,
+ SoundRingtone: 77,
+ SimContactType: 78,
+ EcoID: 79,
+ Personal: 80,
+ StockSymbolAll: 81,
+ NotificationVibration: 82,
+ NotificationLED: 83,
+ MessageNotificationVibration: 84,
+ MessageNotificationLED: 85,
+ MessageNotificationDuringCall: 86,
+ VideoChatPin: 87
+ };
+
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.Other] = "other";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.Home] = "home";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.Work] = "work";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.PhoneMobile] = "mobile";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.FaxDirect] = "direct";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.Blog] = "blog";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.WebsiteResume] = "resume";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.WebsitePortfolio] = "portfolio";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.WebsitePersonal] = "personal";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.WebsiteCompany] = "company";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.ProfileFacebook] = "facebook";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.ProfileTwitter] = "twitter";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.ProfileLinkedIn] = "linkedin";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.ProfileGist] = "gist";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.ProfileTungle] = "tungle";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.DateBirthday] = "birthday";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.DateAnniversary] = "anniversary";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.NameGiven] = "givenName";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.NameSurname] = "familyName";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.Title] = "honorificPrefix";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.NameSuffix] = "honorificSuffix";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.NameMiddle] = "middleName";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.NamePhoneticGiven] = "phoneticGivenName";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.NamePhoneticSurname] = "phoneticFamilyName";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.NameNickname] = "nickname";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.NameDisplayName] = "displayName";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.OrganizationAffiliationName] = "name";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.OrganizationAffiliationDetails] = "department";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.Title] = "title";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.InstantMessagingBbmPin] = "BbmPin";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.InstantMessagingAim] = "Aim";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.InstantMessagingAliwangwang] = "Aliwangwang";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.InstantMessagingGoogleTalk] = "GoogleTalk";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.InstantMessagingSametime] = "Sametime";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.InstantMessagingIcq] = "Icq";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.InstantMessagingJabber] = "Jabber";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.InstantMessagingMsLcs] = "MsLcs";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.InstantMessagingSkype] = "Skype";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.InstantMessagingYahooMessenger] = "YahooMessenger";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.InstantMessagingYahooMessengerJapan] = "YahooMessegerJapan";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.VideoChatBbPlaybook] = "BbPlaybook";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.SoundRingtone] = "ringtone";
+ subKindAttributeMap[ATTRIBUTE_SUBKIND.Personal] = "personal";
+}
+
+module.exports = {
+ getKindAttributeMap: function () {
+ if (!ATTRIBUTE_KIND) {
+ populateKindAttributeMap();
+ }
+
+ return kindAttributeMap;
+ },
+ getSubKindAttributeMap: function () {
+ if (!ATTRIBUTE_SUBKIND) {
+ populateSubKindAttributeMap();
+ }
+
+ return subKindAttributeMap;
+ }
+};
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/blackberry10/contactUtils.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/blackberry10/contactUtils.js b/spec/plugins/Contacts/src/blackberry10/contactUtils.js
new file mode 100644
index 0000000..cd022c4
--- /dev/null
+++ b/spec/plugins/Contacts/src/blackberry10/contactUtils.js
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2012 Research In Motion Limited.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var self,
+ ContactFindOptions = require("./ContactFindOptions"),
+ ContactError = require("./ContactError"),
+ ContactName = require("./ContactName"),
+ ContactOrganization = require("./ContactOrganization"),
+ ContactAddress = require("./ContactAddress"),
+ ContactField = require("./ContactField"),
+ contactConsts = require("./contactConsts"),
+ ContactPhoto = require("./ContactPhoto"),
+ ContactNews = require("./ContactNews"),
+ ContactActivity = require("./ContactActivity");
+
+function populateFieldArray(contactProps, field, ClassName) {
+ if (contactProps[field]) {
+ var list = [],
+ obj;
+
+ contactProps[field].forEach(function (args) {
+ if (ClassName === ContactField) {
+ list.push(new ClassName(args.type, args.value));
+ } else if (ClassName === ContactPhoto) {
+ obj = new ContactPhoto(args.originalFilePath, args.pref);
+ obj.largeFilePath = args.largeFilePath;
+ obj.smallFilePath = args.smallFilePath;
+ list.push(obj);
+ } else if (ClassName === ContactNews) {
+ obj = new ContactNews(args);
+ list.push(obj);
+ } else if (ClassName === ContactActivity) {
+ obj = new ContactActivity(args);
+ list.push(obj);
+ } else {
+ list.push(new ClassName(args));
+ }
+ });
+ contactProps[field] = list;
+ }
+}
+
+function populateDate(contactProps, field) {
+ if (contactProps[field]) {
+ contactProps[field] = new Date(contactProps[field]);
+ }
+}
+
+function validateFindArguments(findOptions) {
+ var error = false;
+
+ // findOptions is mandatory
+ if (!findOptions) {
+ error = true;
+ } else {
+ // findOptions.filter is optional
+ if (findOptions.filter) {
+ findOptions.filter.forEach(function (f) {
+ switch (f.fieldName) {
+ case ContactFindOptions.SEARCH_FIELD_GIVEN_NAME:
+ case ContactFindOptions.SEARCH_FIELD_FAMILY_NAME:
+ case ContactFindOptions.SEARCH_FIELD_ORGANIZATION_NAME:
+ case ContactFindOptions.SEARCH_FIELD_PHONE:
+ case ContactFindOptions.SEARCH_FIELD_EMAIL:
+ case ContactFindOptions.SEARCH_FIELD_BBMPIN:
+ case ContactFindOptions.SEARCH_FIELD_LINKEDIN:
+ case ContactFindOptions.SEARCH_FIELD_TWITTER:
+ case ContactFindOptions.SEARCH_FIELD_VIDEO_CHAT:
+ break;
+ default:
+ error = true;
+ }
+
+ if (!f.fieldValue) {
+ error = true;
+ }
+ });
+ }
+
+ //findOptions.limit is optional
+ if (findOptions.limit) {
+ if (typeof findOptions.limit !== "number") {
+ error = true;
+ }
+ }
+
+ //findOptions.favorite is optional
+ if (findOptions.favorite) {
+ if (typeof findOptions.favorite !== "boolean") {
+ error = true;
+ }
+ }
+
+ // findOptions.sort is optional
+ if (!error && findOptions.sort && Array.isArray(findOptions.sort)) {
+ findOptions.sort.forEach(function (s) {
+ switch (s.fieldName) {
+ case ContactFindOptions.SORT_FIELD_GIVEN_NAME:
+ case ContactFindOptions.SORT_FIELD_FAMILY_NAME:
+ case ContactFindOptions.SORT_FIELD_ORGANIZATION_NAME:
+ break;
+ default:
+ error = true;
+ }
+
+ if (s.desc === undefined || typeof s.desc !== "boolean") {
+ error = true;
+ }
+ });
+ }
+
+ if (!error && findOptions.includeAccounts) {
+ if (!Array.isArray(findOptions.includeAccounts)) {
+ error = true;
+ } else {
+ findOptions.includeAccounts.forEach(function (acct) {
+ if (!error && (!acct.id || window.isNaN(window.parseInt(acct.id, 10)))) {
+ error = true;
+ }
+ });
+ }
+ }
+
+ if (!error && findOptions.excludeAccounts) {
+ if (!Array.isArray(findOptions.excludeAccounts)) {
+ error = true;
+ } else {
+ findOptions.excludeAccounts.forEach(function (acct) {
+ if (!error && (!acct.id || window.isNaN(window.parseInt(acct.id, 10)))) {
+ error = true;
+ }
+ });
+ }
+ }
+ }
+ return !error;
+}
+
+function validateContactsPickerFilter(filter) {
+ var isValid = true,
+ availableFields = {};
+
+ if (typeof(filter) === "undefined") {
+ isValid = false;
+ } else {
+ if (filter && Array.isArray(filter)) {
+ availableFields = contactConsts.getKindAttributeMap();
+ filter.forEach(function (e) {
+ isValid = isValid && Object.getOwnPropertyNames(availableFields).reduce(
+ function (found, key) {
+ return found || availableFields[key] === e;
+ }, false);
+ });
+ }
+ }
+
+ return isValid;
+}
+
+function validateContactsPickerOptions(options) {
+ var isValid = false,
+ mode = options.mode;
+
+ if (typeof(options) === "undefined") {
+ isValid = false;
+ } else {
+ isValid = mode === ContactPickerOptions.MODE_SINGLE || mode === ContactPickerOptions.MODE_MULTIPLE || mode === ContactPickerOptions.MODE_ATTRIBUTE;
+
+ // if mode is attribute, fields must be defined
+ if (mode === ContactPickerOptions.MODE_ATTRIBUTE && !validateContactsPickerFilter(options.fields)) {
+ isValid = false;
+ }
+ }
+
+ return isValid;
+}
+
+self = module.exports = {
+ populateContact: function (contact) {
+ if (contact.name) {
+ contact.name = new ContactName(contact.name);
+ }
+
+ populateFieldArray(contact, "addresses", ContactAddress);
+ populateFieldArray(contact, "organizations", ContactOrganization);
+ populateFieldArray(contact, "emails", ContactField);
+ populateFieldArray(contact, "phoneNumbers", ContactField);
+ populateFieldArray(contact, "faxNumbers", ContactField);
+ populateFieldArray(contact, "pagerNumbers", ContactField);
+ populateFieldArray(contact, "ims", ContactField);
+ populateFieldArray(contact, "socialNetworks", ContactField);
+ populateFieldArray(contact, "urls", ContactField);
+ populateFieldArray(contact, "photos", ContactPhoto);
+ populateFieldArray(contact, "news", ContactNews);
+ populateFieldArray(contact, "activities", ContactActivity);
+ // TODO categories
+
+ populateDate(contact, "birthday");
+ populateDate(contact, "anniversary");
+ },
+ invokeErrorCallback: function (errorCallback, code) {
+ if (errorCallback) {
+ errorCallback(new ContactError(code));
+ }
+ },
+ validateFindArguments: validateFindArguments,
+ validateContactsPickerFilter: validateContactsPickerFilter,
+ validateContactsPickerOptions: validateContactsPickerOptions
+};
+
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/blackberry10/index.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/blackberry10/index.js b/spec/plugins/Contacts/src/blackberry10/index.js
new file mode 100644
index 0000000..09a4bd2
--- /dev/null
+++ b/spec/plugins/Contacts/src/blackberry10/index.js
@@ -0,0 +1,374 @@
+/*
+ * Copyright 2013 Research In Motion Limited.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+var pimContacts,
+ contactUtils = require("./contactUtils"),
+ contactConsts = require("./contactConsts"),
+ ContactError = require("./ContactError"),
+ ContactName = require("./ContactName"),
+ ContactFindOptions = require("./ContactFindOptions"),
+ noop = function () {};
+
+function getAccountFilters(options) {
+ if (options.includeAccounts) {
+ options.includeAccounts = options.includeAccounts.map(function (acct) {
+ return acct.id.toString();
+ });
+ }
+
+ if (options.excludeAccounts) {
+ options.excludeAccounts = options.excludeAccounts.map(function (acct) {
+ return acct.id.toString();
+ });
+ }
+}
+
+function populateSearchFields(fields) {
+ var i,
+ l,
+ key,
+ searchFieldsObject = {},
+ searchFields = [];
+
+ for (i = 0, l = fields.length; i < l; i++) {
+ if (fields[i] === "*") {
+ searchFieldsObject[ContactFindOptions.SEARCH_FIELD_GIVEN_NAME] = true;
+ searchFieldsObject[ContactFindOptions.SEARCH_FIELD_FAMILY_NAME] = true;
+ searchFieldsObject[ContactFindOptions.SEARCH_FIELD_PHONE] = true;
+ searchFieldsObject[ContactFindOptions.SEARCH_FIELD_EMAIL] = true;
+ searchFieldsObject[ContactFindOptions.SEARCH_FIELD_ORGANIZATION_NAME] = true;
+ } else if (fields[i] === "displayName" || fields[i] === "name") {
+ searchFieldsObject[ContactFindOptions.SEARCH_FIELD_GIVEN_NAME] = true;
+ searchFieldsObject[ContactFindOptions.SEARCH_FIELD_FAMILY_NAME] = true;
+ } else if (fields[i] === "nickname") {
+ // not supported by Cascades
+ } else if (fields[i] === "phoneNumbers") {
+ searchFieldsObject[ContactFindOptions.SEARCH_FIELD_PHONE] = true;
+ } else if (fields[i] === "emails") {
+ searchFieldsObject[ContactFindOptions.SEARCH_FIELD_EMAIL] = true;
+ } else if (field === "addresses") {
+ // not supported by Cascades
+ } else if (field === "ims") {
+ // not supported by Cascades
+ } else if (field === "organizations") {
+ searchFieldsObject[ContactFindOptions.SEARCH_FIELD_ORGANIZATION_NAME] = true;
+ } else if (field === "birthday") {
+ // not supported by Cascades
+ } else if (field === "note") {
+ // not supported by Cascades
+ } else if (field === "photos") {
+ // not supported by Cascades
+ } else if (field === "categories") {
+ // not supported by Cascades
+ } else if (field === "urls") {
+ // not supported by Cascades
+ }
+ }
+
+ for (key in searchFieldsObject) {
+ if (searchFieldsObject.hasOwnProperty(key)) {
+ searchFields.push(window.parseInt(key));
+ }
+ }
+
+ return searchFields;
+}
+
+function convertBirthday(birthday) {
+ //Convert date string from native to milliseconds since epoch for cordova-js
+ var birthdayInfo;
+ if (birthday) {
+ birthdayInfo = birthday.split("-");
+ return new Date(birthdayInfo[0], birthdayInfo[1] - 1, birthdayInfo[2]).getTime();
+ } else {
+ return null;
+ }
+}
+
+function processJnextSaveData(result, JnextData) {
+ var data = JnextData,
+ birthdayInfo;
+
+ if (data._success === true) {
+ data.birthday = convertBirthday(data.birthday);
+ result.callbackOk(data, false);
+ } else {
+ result.callbackError(data.code, false);
+ }
+}
+
+function processJnextRemoveData(result, JnextData) {
+ var data = JnextData;
+
+ if (data._success === true) {
+ result.callbackOk(data);
+ } else {
+ result.callbackError(ContactError.UNKNOWN_ERROR, false);
+ }
+}
+
+function processJnextFindData(eventId, eventHandler, JnextData) {
+ var data = JnextData,
+ i,
+ l,
+ more = false,
+ resultsObject = {},
+ birthdayInfo;
+
+ if (data.contacts) {
+ for (i = 0, l = data.contacts.length; i < l; i++) {
+ data.contacts[i].birthday = convertBirthday(data.contacts[i].birthday);
+ data.contacts[i].name = new ContactName(data.contacts[i].name);
+ }
+ } else {
+ data.contacts = []; // if JnextData.contacts return null, return an empty array
+ }
+
+ if (data._success === true) {
+ eventHandler.error = false;
+ }
+
+ if (eventHandler.multiple) {
+ // Concatenate results; do not add the same contacts
+ for (i = 0, l = eventHandler.searchResult.length; i < l; i++) {
+ resultsObject[eventHandler.searchResult[i].id] = true;
+ }
+
+ for (i = 0, l = data.contacts.length; i < l; i++) {
+ if (resultsObject[data.contacts[i].id]) {
+ // Already existing
+ } else {
+ eventHandler.searchResult.push(data.contacts[i]);
+ }
+ }
+
+ // check if more search is required
+ eventHandler.searchFieldIndex++;
+ if (eventHandler.searchFieldIndex < eventHandler.searchFields.length) {
+ more = true;
+ }
+ } else {
+ eventHandler.searchResult = data.contacts;
+ }
+
+ if (more) {
+ pimContacts.getInstance().invokeJnextSearch(eventId);
+ } else {
+ if (eventHandler.error) {
+ eventHandler.result.callbackError(data.code, false);
+ } else {
+ eventHandler.result.callbackOk(eventHandler.searchResult, false);
+ }
+ }
+}
+
+module.exports = {
+ search: function (successCb, failCb, args, env) {
+ var cordovaFindOptions = {},
+ result = new PluginResult(args, env),
+ key;
+
+ for (key in args) {
+ if (args.hasOwnProperty(key)) {
+ cordovaFindOptions[key] = JSON.parse(decodeURIComponent(args[key]));
+ }
+ }
+
+ pimContacts.getInstance().find(cordovaFindOptions, result, processJnextFindData);
+ result.noResult(true);
+ },
+ save: function (successCb, failCb, args, env) {
+ var attributes = {},
+ result = new PluginResult(args, env),
+ key,
+ nativeEmails = [];
+
+ attributes = JSON.parse(decodeURIComponent(args[0]));
+
+ //convert birthday format for our native .so file
+ if (attributes.birthday) {
+ attributes.birthday = new Date(attributes.birthday).toDateString();
+ }
+
+ if (attributes.emails) {
+ attributes.emails.forEach(function (email) {
+ if (email.value) {
+ if (email.type) {
+ nativeEmails.push({ "type" : email.type, "value" : email.value });
+ } else {
+ nativeEmails.push({ "type" : "home", "value" : email.value });
+ }
+ }
+ });
+ attributes.emails = nativeEmails;
+ }
+
+ if (attributes.id !== null) {
+ attributes.id = window.parseInt(attributes.id);
+ }
+
+ attributes._eventId = result.callbackId;
+ pimContacts.getInstance().save(attributes, result, processJnextSaveData);
+ result.noResult(true);
+ },
+ remove: function (successCb, failCb, args, env) {
+ var result = new PluginResult(args, env),
+ attributes = {
+ "contactId": window.parseInt(JSON.parse(decodeURIComponent(args[0]))),
+ "_eventId": result.callbackId
+ };
+
+ if (!window.isNaN(attributes.contactId)) {
+ pimContacts.getInstance().remove(attributes, result, processJnextRemoveData);
+ result.noResult(true);
+ } else {
+ result.error(ContactError.UNKNOWN_ERROR);
+ result.noResult(false);
+ }
+ }
+};
+
+///////////////////////////////////////////////////////////////////
+// JavaScript wrapper for JNEXT plugin
+///////////////////////////////////////////////////////////////////
+
+JNEXT.PimContacts = function ()
+{
+ var self = this,
+ hasInstance = false;
+
+ self.find = function (cordovaFindOptions, pluginResult, handler) {
+ //register find eventHandler for when JNEXT onEvent fires
+ self.eventHandlers[cordovaFindOptions.callbackId] = {
+ "result" : pluginResult,
+ "action" : "find",
+ "multiple" : cordovaFindOptions[1].filter ? true : false,
+ "fields" : cordovaFindOptions[0],
+ "searchFilter" : cordovaFindOptions[1].filter,
+ "searchFields" : cordovaFindOptions[1].filter ? populateSearchFields(cordovaFindOptions[0]) : null,
+ "searchFieldIndex" : 0,
+ "searchResult" : [],
+ "handler" : handler,
+ "error" : true
+ };
+
+ self.invokeJnextSearch(cordovaFindOptions.callbackId);
+ return "";
+ };
+
+ self.invokeJnextSearch = function(eventId) {
+ var jnextArgs = {},
+ findHandler = self.eventHandlers[eventId];
+
+ jnextArgs._eventId = eventId;
+ jnextArgs.fields = findHandler.fields;
+ jnextArgs.options = {};
+ jnextArgs.options.filter = [];
+
+ if (findHandler.multiple) {
+ jnextArgs.options.filter.push({
+ "fieldName" : findHandler.searchFields[findHandler.searchFieldIndex],
+ "fieldValue" : findHandler.searchFilter
+ });
+ //findHandler.searchFieldIndex++;
+ }
+
+ JNEXT.invoke(self.m_id, "find " + JSON.stringify(jnextArgs));
+ }
+
+ self.getContact = function (args) {
+ return JSON.parse(JNEXT.invoke(self.m_id, "getContact " + JSON.stringify(args)));
+ };
+
+ self.save = function (args, pluginResult, handler) {
+ //register save eventHandler for when JNEXT onEvent fires
+ self.eventHandlers[args._eventId] = {
+ "result" : pluginResult,
+ "action" : "save",
+ "handler" : handler
+ };
+ JNEXT.invoke(self.m_id, "save " + JSON.stringify(args));
+ return "";
+ };
+
+ self.remove = function (args, pluginResult, handler) {
+ //register remove eventHandler for when JNEXT onEvent fires
+ self.eventHandlers[args._eventId] = {
+ "result" : pluginResult,
+ "action" : "remove",
+ "handler" : handler
+ };
+ JNEXT.invoke(self.m_id, "remove " + JSON.stringify(args));
+ return "";
+ };
+
+ self.getId = function () {
+ return self.m_id;
+ };
+
+ self.getContactAccounts = function () {
+ var value = JNEXT.invoke(self.m_id, "getContactAccounts");
+ return JSON.parse(value);
+ };
+
+ self.init = function () {
+ if (!JNEXT.require("libpimcontacts")) {
+ return false;
+ }
+
+ self.m_id = JNEXT.createObject("libpimcontacts.PimContacts");
+
+ if (self.m_id === "") {
+ return false;
+ }
+
+ JNEXT.registerEvents(self);
+ };
+
+ // Handle data coming back from JNEXT native layer. Each async function registers a handler and a PluginResult object.
+ // When JNEXT fires onEvent we parse the result string back into JSON and trigger the appropriate handler (eventHandlers map
+ // uses callbackId as key), along with the actual data coming back from the native layer. Each function may have its own way of
+ // processing native data so we do not do any processing here.
+
+ self.onEvent = function (strData) {
+ var arData = strData.split(" "),
+ strEventDesc = arData[0],
+ eventHandler,
+ args = {};
+
+ if (strEventDesc === "result") {
+ args.result = escape(strData.split(" ").slice(2).join(" "));
+ eventHandler = self.eventHandlers[arData[1]];
+ if (eventHandler.action === "save" || eventHandler.action === "remove") {
+ eventHandler.handler(eventHandler.result, JSON.parse(decodeURIComponent(args.result)));
+ } else if (eventHandler.action === "find") {
+ eventHandler.handler(arData[1], eventHandler, JSON.parse(decodeURIComponent(args.result)));
+ }
+ }
+ };
+
+ self.m_id = "";
+ self.eventHandlers = {};
+
+ self.getInstance = function () {
+ if (!hasInstance) {
+ self.init();
+ hasInstance = true;
+ }
+ return self;
+ };
+};
+
+pimContacts = new JNEXT.PimContacts();
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/blackberry10/plugin.xml
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/blackberry10/plugin.xml b/spec/plugins/Contacts/src/blackberry10/plugin.xml
new file mode 100644
index 0000000..d163585
--- /dev/null
+++ b/spec/plugins/Contacts/src/blackberry10/plugin.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+-->
+
+<plugin xmlns="http://www.phonegap.com/ns/plugins/1.0"
+ id="org.apache.cordova.core.Contacts"
+ version="0.0.1">
+
+ <name>Contacts</name>
+
+ <platform name="blackberry10">
+ <config-file target="www/config.xml" parent="/widget">
+ <feature name="Contacts" value="Contacts"/>
+ </config-file>
+ <source-file src="src/blackberry10/index.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/ContactActivity.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/ContactAddress.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/contactConsts.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/ContactError.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/ContactField.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/ContactFindOptions.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/ContactName.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/ContactNews.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/ContactOrganization.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/ContactPhoto.js" target-dir="Contacts"></source-file>
+ <source-file src="src/blackberry10/contactUtils.js" target-dir="Contacts"></source-file>
+ </platform>
+</plugin>
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/ios/CDVContact.h
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/ios/CDVContact.h b/spec/plugins/Contacts/src/ios/CDVContact.h
new file mode 100644
index 0000000..5187efc
--- /dev/null
+++ b/spec/plugins/Contacts/src/ios/CDVContact.h
@@ -0,0 +1,136 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import <AddressBook/ABAddressBook.h>
+#import <AddressBookUI/AddressBookUI.h>
+
+enum CDVContactError {
+ UNKNOWN_ERROR = 0,
+ INVALID_ARGUMENT_ERROR = 1,
+ TIMEOUT_ERROR = 2,
+ PENDING_OPERATION_ERROR = 3,
+ IO_ERROR = 4,
+ NOT_SUPPORTED_ERROR = 5,
+ PERMISSION_DENIED_ERROR = 20
+};
+typedef NSUInteger CDVContactError;
+
+@interface CDVContact : NSObject {
+ ABRecordRef record; // the ABRecord associated with this contact
+ NSDictionary* returnFields; // dictionary of fields to return when performing search
+}
+
+@property (nonatomic, assign) ABRecordRef record;
+@property (nonatomic, strong) NSDictionary* returnFields;
+
++ (NSDictionary*)defaultABtoW3C;
++ (NSDictionary*)defaultW3CtoAB;
++ (NSSet*)defaultW3CtoNull;
++ (NSDictionary*)defaultObjectAndProperties;
++ (NSDictionary*)defaultFields;
+
++ (NSDictionary*)calcReturnFields:(NSArray*)fields;
+- (id)init;
+- (id)initFromABRecord:(ABRecordRef)aRecord;
+- (bool)setFromContactDict:(NSDictionary*)aContact asUpdate:(BOOL)bUpdate;
+
++ (BOOL)needsConversion:(NSString*)W3Label;
++ (CFStringRef)convertContactTypeToPropertyLabel:(NSString*)label;
++ (NSString*)convertPropertyLabelToContactType:(NSString*)label;
++ (BOOL)isValidW3ContactType:(NSString*)label;
+- (bool)setValue:(id)aValue forProperty:(ABPropertyID)aProperty inRecord:(ABRecordRef)aRecord asUpdate:(BOOL)bUpdate;
+
+- (NSDictionary*)toDictionary:(NSDictionary*)withFields;
+- (NSNumber*)getDateAsNumber:(ABPropertyID)datePropId;
+- (NSObject*)extractName;
+- (NSObject*)extractMultiValue:(NSString*)propertyId;
+- (NSObject*)extractAddresses;
+- (NSObject*)extractIms;
+- (NSObject*)extractOrganizations;
+- (NSObject*)extractPhotos;
+
+- (NSMutableDictionary*)translateW3Dict:(NSDictionary*)dict forProperty:(ABPropertyID)prop;
+- (bool)setMultiValueStrings:(NSArray*)fieldArray forProperty:(ABPropertyID)prop inRecord:(ABRecordRef)person asUpdate:(BOOL)bUpdate;
+- (bool)setMultiValueDictionary:(NSArray*)array forProperty:(ABPropertyID)prop inRecord:(ABRecordRef)person asUpdate:(BOOL)bUpdate;
+- (ABMultiValueRef)allocStringMultiValueFromArray:array;
+- (ABMultiValueRef)allocDictMultiValueFromArray:array forProperty:(ABPropertyID)prop;
+- (BOOL)foundValue:(NSString*)testValue inFields:(NSDictionary*)searchFields;
+- (BOOL)testStringValue:(NSString*)testValue forW3CProperty:(NSString*)property;
+- (BOOL)testDateValue:(NSString*)testValue forW3CProperty:(NSString*)property;
+- (BOOL)searchContactFields:(NSArray*)fields forMVStringProperty:(ABPropertyID)propId withValue:testValue;
+- (BOOL)testMultiValueStrings:(NSString*)testValue forProperty:(ABPropertyID)propId ofType:(NSString*)type;
+- (NSArray*)valuesForProperty:(ABPropertyID)propId inRecord:(ABRecordRef)aRecord;
+- (NSArray*)labelsForProperty:(ABPropertyID)propId inRecord:(ABRecordRef)aRecord;
+- (BOOL)searchContactFields:(NSArray*)fields forMVDictionaryProperty:(ABPropertyID)propId withValue:(NSString*)testValue;
+
+@end
+
+// generic ContactField types
+#define kW3ContactFieldType @"type"
+#define kW3ContactFieldValue @"value"
+#define kW3ContactFieldPrimary @"pref"
+// Various labels for ContactField types
+#define kW3ContactWorkLabel @"work"
+#define kW3ContactHomeLabel @"home"
+#define kW3ContactOtherLabel @"other"
+#define kW3ContactPhoneFaxLabel @"fax"
+#define kW3ContactPhoneMobileLabel @"mobile"
+#define kW3ContactPhonePagerLabel @"pager"
+#define kW3ContactUrlBlog @"blog"
+#define kW3ContactUrlProfile @"profile"
+#define kW3ContactImAIMLabel @"aim"
+#define kW3ContactImICQLabel @"icq"
+#define kW3ContactImMSNLabel @"msn"
+#define kW3ContactImYahooLabel @"yahoo"
+#define kW3ContactFieldId @"id"
+// special translation for IM field value and type
+#define kW3ContactImType @"type"
+#define kW3ContactImValue @"value"
+
+// Contact object
+#define kW3ContactId @"id"
+#define kW3ContactName @"name"
+#define kW3ContactFormattedName @"formatted"
+#define kW3ContactGivenName @"givenName"
+#define kW3ContactFamilyName @"familyName"
+#define kW3ContactMiddleName @"middleName"
+#define kW3ContactHonorificPrefix @"honorificPrefix"
+#define kW3ContactHonorificSuffix @"honorificSuffix"
+#define kW3ContactDisplayName @"displayName"
+#define kW3ContactNickname @"nickname"
+#define kW3ContactPhoneNumbers @"phoneNumbers"
+#define kW3ContactAddresses @"addresses"
+#define kW3ContactAddressFormatted @"formatted"
+#define kW3ContactStreetAddress @"streetAddress"
+#define kW3ContactLocality @"locality"
+#define kW3ContactRegion @"region"
+#define kW3ContactPostalCode @"postalCode"
+#define kW3ContactCountry @"country"
+#define kW3ContactEmails @"emails"
+#define kW3ContactIms @"ims"
+#define kW3ContactOrganizations @"organizations"
+#define kW3ContactOrganizationName @"name"
+#define kW3ContactTitle @"title"
+#define kW3ContactDepartment @"department"
+#define kW3ContactBirthday @"birthday"
+#define kW3ContactNote @"note"
+#define kW3ContactPhotos @"photos"
+#define kW3ContactCategories @"categories"
+#define kW3ContactUrls @"urls"
[5/6] [CB-4341] Adding a fix to make subdirectories work within a
local plugin dependency - Includes the integration of integration specs which
test installation of plugins with dependencies
Posted by br...@apache.org.
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/android/ContactAccessorSdk5.java
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/android/ContactAccessorSdk5.java b/spec/plugins/Contacts/src/android/ContactAccessorSdk5.java
new file mode 100644
index 0000000..46440ba
--- /dev/null
+++ b/spec/plugins/Contacts/src/android/ContactAccessorSdk5.java
@@ -0,0 +1,2183 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+package org.apache.cordova.core;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.util.Log;
+import android.webkit.WebView;
+
+import org.apache.cordova.CordovaInterface;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+//import android.app.Activity;
+//import android.content.Context;
+
+/**
+ * An implementation of {@link ContactAccessor} that uses current Contacts API.
+ * This class should be used on Eclair or beyond, but would not work on any earlier
+ * release of Android. As a matter of fact, it could not even be loaded.
+ * <p>
+ * This implementation has several advantages:
+ * <ul>
+ * <li>It sees contacts from multiple accounts.
+ * <li>It works with aggregated contacts. So for example, if the contact is the result
+ * of aggregation of two raw contacts from different accounts, it may return the name from
+ * one and the phone number from the other.
+ * <li>It is efficient because it uses the more efficient current API.
+ * <li>Not obvious in this particular example, but it has access to new kinds
+ * of data available exclusively through the new APIs. Exercise for the reader: add support
+ * for nickname (see {@link android.provider.ContactsContract.CommonDataKinds.Nickname}) or
+ * social status updates (see {@link android.provider.ContactsContract.StatusUpdates}).
+ * </ul>
+ */
+
+public class ContactAccessorSdk5 extends ContactAccessor {
+
+ /**
+ * Keep the photo size under the 1 MB blog limit.
+ */
+ private static final long MAX_PHOTO_SIZE = 1048576;
+
+ private static final String EMAIL_REGEXP = ".+@.+\\.+.+"; /* <anything>@<anything>.<anything>*/
+
+ /**
+ * A static map that converts the JavaScript property name to Android database column name.
+ */
+ private static final Map<String, String> dbMap = new HashMap<String, String>();
+ static {
+ dbMap.put("id", ContactsContract.Data.CONTACT_ID);
+ dbMap.put("displayName", ContactsContract.Contacts.DISPLAY_NAME);
+ dbMap.put("name", ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME);
+ dbMap.put("name.formatted", ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME);
+ dbMap.put("name.familyName", ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME);
+ dbMap.put("name.givenName", ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME);
+ dbMap.put("name.middleName", ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME);
+ dbMap.put("name.honorificPrefix", ContactsContract.CommonDataKinds.StructuredName.PREFIX);
+ dbMap.put("name.honorificSuffix", ContactsContract.CommonDataKinds.StructuredName.SUFFIX);
+ dbMap.put("nickname", ContactsContract.CommonDataKinds.Nickname.NAME);
+ dbMap.put("phoneNumbers", ContactsContract.CommonDataKinds.Phone.NUMBER);
+ dbMap.put("phoneNumbers.value", ContactsContract.CommonDataKinds.Phone.NUMBER);
+ dbMap.put("emails", ContactsContract.CommonDataKinds.Email.DATA);
+ dbMap.put("emails.value", ContactsContract.CommonDataKinds.Email.DATA);
+ dbMap.put("addresses", ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS);
+ dbMap.put("addresses.formatted", ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS);
+ dbMap.put("addresses.streetAddress", ContactsContract.CommonDataKinds.StructuredPostal.STREET);
+ dbMap.put("addresses.locality", ContactsContract.CommonDataKinds.StructuredPostal.CITY);
+ dbMap.put("addresses.region", ContactsContract.CommonDataKinds.StructuredPostal.REGION);
+ dbMap.put("addresses.postalCode", ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE);
+ dbMap.put("addresses.country", ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY);
+ dbMap.put("ims", ContactsContract.CommonDataKinds.Im.DATA);
+ dbMap.put("ims.value", ContactsContract.CommonDataKinds.Im.DATA);
+ dbMap.put("organizations", ContactsContract.CommonDataKinds.Organization.COMPANY);
+ dbMap.put("organizations.name", ContactsContract.CommonDataKinds.Organization.COMPANY);
+ dbMap.put("organizations.department", ContactsContract.CommonDataKinds.Organization.DEPARTMENT);
+ dbMap.put("organizations.title", ContactsContract.CommonDataKinds.Organization.TITLE);
+ dbMap.put("birthday", ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE);
+ dbMap.put("note", ContactsContract.CommonDataKinds.Note.NOTE);
+ dbMap.put("photos.value", ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE);
+ //dbMap.put("categories.value", null);
+ dbMap.put("urls", ContactsContract.CommonDataKinds.Website.URL);
+ dbMap.put("urls.value", ContactsContract.CommonDataKinds.Website.URL);
+ }
+
+ /**
+ * Create an contact accessor.
+ */
+ public ContactAccessorSdk5(WebView view, CordovaInterface context) {
+ mApp = context;
+ mView = view;
+ }
+
+ /**
+ * This method takes the fields required and search options in order to produce an
+ * array of contacts that matches the criteria provided.
+ * @param fields an array of items to be used as search criteria
+ * @param options that can be applied to contact searching
+ * @return an array of contacts
+ */
+ @Override
+ public JSONArray search(JSONArray fields, JSONObject options) {
+ // Get the find options
+ String searchTerm = "";
+ int limit = Integer.MAX_VALUE;
+ boolean multiple = true;
+
+ if (options != null) {
+ searchTerm = options.optString("filter");
+ if (searchTerm.length() == 0) {
+ searchTerm = "%";
+ }
+ else {
+ searchTerm = "%" + searchTerm + "%";
+ }
+
+ try {
+ multiple = options.getBoolean("multiple");
+ if (!multiple) {
+ limit = 1;
+ }
+ } catch (JSONException e) {
+ // Multiple was not specified so we assume the default is true.
+ }
+ }
+ else {
+ searchTerm = "%";
+ }
+
+
+ //Log.d(LOG_TAG, "Search Term = " + searchTerm);
+ //Log.d(LOG_TAG, "Field Length = " + fields.length());
+ //Log.d(LOG_TAG, "Fields = " + fields.toString());
+
+ // Loop through the fields the user provided to see what data should be returned.
+ HashMap<String, Boolean> populate = buildPopulationSet(fields);
+
+ // Build the ugly where clause and where arguments for one big query.
+ WhereOptions whereOptions = buildWhereClause(fields, searchTerm);
+
+ // Get all the id's where the search term matches the fields passed in.
+ Cursor idCursor = mApp.getActivity().getContentResolver().query(ContactsContract.Data.CONTENT_URI,
+ new String[] { ContactsContract.Data.CONTACT_ID },
+ whereOptions.getWhere(),
+ whereOptions.getWhereArgs(),
+ ContactsContract.Data.CONTACT_ID + " ASC");
+
+ // Create a set of unique ids
+ Set<String> contactIds = new HashSet<String>();
+ int idColumn = -1;
+ while (idCursor.moveToNext()) {
+ if (idColumn < 0) {
+ idColumn = idCursor.getColumnIndex(ContactsContract.Data.CONTACT_ID);
+ }
+ contactIds.add(idCursor.getString(idColumn));
+ }
+ idCursor.close();
+
+ // Build a query that only looks at ids
+ WhereOptions idOptions = buildIdClause(contactIds, searchTerm);
+
+ // Determine which columns we should be fetching.
+ HashSet<String> columnsToFetch = new HashSet<String>();
+ columnsToFetch.add(ContactsContract.Data.CONTACT_ID);
+ columnsToFetch.add(ContactsContract.Data.RAW_CONTACT_ID);
+ columnsToFetch.add(ContactsContract.Data.MIMETYPE);
+
+ if (isRequired("displayName", populate)) {
+ columnsToFetch.add(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME);
+ }
+ if (isRequired("name", populate)) {
+ columnsToFetch.add(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.StructuredName.PREFIX);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.StructuredName.SUFFIX);
+ }
+ if (isRequired("phoneNumbers", populate)) {
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Phone._ID);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Phone.NUMBER);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Phone.TYPE);
+ }
+ if (isRequired("emails", populate)) {
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Email._ID);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Email.DATA);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Email.TYPE);
+ }
+ if (isRequired("addresses", populate)) {
+ columnsToFetch.add(ContactsContract.CommonDataKinds.StructuredPostal._ID);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Organization.TYPE);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.StructuredPostal.STREET);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.StructuredPostal.CITY);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.StructuredPostal.REGION);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY);
+ }
+ if (isRequired("organizations", populate)) {
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Organization._ID);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Organization.TYPE);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Organization.DEPARTMENT);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Organization.COMPANY);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Organization.TITLE);
+ }
+ if (isRequired("ims", populate)) {
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Im._ID);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Im.DATA);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Im.TYPE);
+ }
+ if (isRequired("note", populate)) {
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Note.NOTE);
+ }
+ if (isRequired("nickname", populate)) {
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Nickname.NAME);
+ }
+ if (isRequired("urls", populate)) {
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Website._ID);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Website.URL);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Website.TYPE);
+ }
+ if (isRequired("birthday", populate)) {
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Event.START_DATE);
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Event.TYPE);
+ }
+ if (isRequired("photos", populate)) {
+ columnsToFetch.add(ContactsContract.CommonDataKinds.Photo._ID);
+ }
+
+ // Do the id query
+ Cursor c = mApp.getActivity().getContentResolver().query(ContactsContract.Data.CONTENT_URI,
+ columnsToFetch.toArray(new String[] {}),
+ idOptions.getWhere(),
+ idOptions.getWhereArgs(),
+ ContactsContract.Data.CONTACT_ID + " ASC");
+
+ JSONArray contacts = populateContactArray(limit, populate, c);
+ return contacts;
+ }
+
+ /**
+ * A special search that finds one contact by id
+ *
+ * @param id contact to find by id
+ * @return a JSONObject representing the contact
+ * @throws JSONException
+ */
+ public JSONObject getContactById(String id) throws JSONException {
+ // Do the id query
+ Cursor c = mApp.getActivity().getContentResolver().query(ContactsContract.Data.CONTENT_URI,
+ null,
+ ContactsContract.Data.CONTACT_ID + " = ? ",
+ new String[] { id },
+ ContactsContract.Data.CONTACT_ID + " ASC");
+
+ JSONArray fields = new JSONArray();
+ fields.put("*");
+
+ HashMap<String, Boolean> populate = buildPopulationSet(fields);
+
+ JSONArray contacts = populateContactArray(1, populate, c);
+
+ if (contacts.length() == 1) {
+ return contacts.getJSONObject(0);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Creates an array of contacts from the cursor you pass in
+ *
+ * @param limit max number of contacts for the array
+ * @param populate whether or not you should populate a certain value
+ * @param c the cursor
+ * @return a JSONArray of contacts
+ */
+ private JSONArray populateContactArray(int limit,
+ HashMap<String, Boolean> populate, Cursor c) {
+
+ String contactId = "";
+ String rawId = "";
+ String oldContactId = "";
+ boolean newContact = true;
+ String mimetype = "";
+
+ JSONArray contacts = new JSONArray();
+ JSONObject contact = new JSONObject();
+ JSONArray organizations = new JSONArray();
+ JSONArray addresses = new JSONArray();
+ JSONArray phones = new JSONArray();
+ JSONArray emails = new JSONArray();
+ JSONArray ims = new JSONArray();
+ JSONArray websites = new JSONArray();
+ JSONArray photos = new JSONArray();
+
+ // Column indices
+ int colContactId = c.getColumnIndex(ContactsContract.Data.CONTACT_ID);
+ int colRawContactId = c.getColumnIndex(ContactsContract.Data.RAW_CONTACT_ID);
+ int colMimetype = c.getColumnIndex(ContactsContract.Data.MIMETYPE);
+ int colDisplayName = c.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME);
+ int colNote = c.getColumnIndex(ContactsContract.CommonDataKinds.Note.NOTE);
+ int colNickname = c.getColumnIndex(ContactsContract.CommonDataKinds.Nickname.NAME);
+ int colBirthday = c.getColumnIndex(ContactsContract.CommonDataKinds.Event.START_DATE);
+ int colEventType = c.getColumnIndex(ContactsContract.CommonDataKinds.Event.TYPE);
+
+ if (c.getCount() > 0) {
+ while (c.moveToNext() && (contacts.length() <= (limit - 1))) {
+ try {
+ contactId = c.getString(colContactId);
+ rawId = c.getString(colRawContactId);
+
+ // If we are in the first row set the oldContactId
+ if (c.getPosition() == 0) {
+ oldContactId = contactId;
+ }
+
+ // When the contact ID changes we need to push the Contact object
+ // to the array of contacts and create new objects.
+ if (!oldContactId.equals(contactId)) {
+ // Populate the Contact object with it's arrays
+ // and push the contact into the contacts array
+ contacts.put(populateContact(contact, organizations, addresses, phones,
+ emails, ims, websites, photos));
+
+ // Clean up the objects
+ contact = new JSONObject();
+ organizations = new JSONArray();
+ addresses = new JSONArray();
+ phones = new JSONArray();
+ emails = new JSONArray();
+ ims = new JSONArray();
+ websites = new JSONArray();
+ photos = new JSONArray();
+
+ // Set newContact to true as we are starting to populate a new contact
+ newContact = true;
+ }
+
+ // When we detect a new contact set the ID and display name.
+ // These fields are available in every row in the result set returned.
+ if (newContact) {
+ newContact = false;
+ contact.put("id", contactId);
+ contact.put("rawId", rawId);
+ }
+
+ // Grab the mimetype of the current row as it will be used in a lot of comparisons
+ mimetype = c.getString(colMimetype);
+
+ if (mimetype.equals(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)) {
+ contact.put("displayName", c.getString(colDisplayName));
+ }
+
+ if (mimetype.equals(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
+ && isRequired("name", populate)) {
+ contact.put("name", nameQuery(c));
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
+ && isRequired("phoneNumbers", populate)) {
+ phones.put(phoneQuery(c));
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
+ && isRequired("emails", populate)) {
+ emails.put(emailQuery(c));
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)
+ && isRequired("addresses", populate)) {
+ addresses.put(addressQuery(c));
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE)
+ && isRequired("organizations", populate)) {
+ organizations.put(organizationQuery(c));
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE)
+ && isRequired("ims", populate)) {
+ ims.put(imQuery(c));
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE)
+ && isRequired("note", populate)) {
+ contact.put("note", c.getString(colNote));
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE)
+ && isRequired("nickname", populate)) {
+ contact.put("nickname", c.getString(colNickname));
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE)
+ && isRequired("urls", populate)) {
+ websites.put(websiteQuery(c));
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE)) {
+ if (isRequired("birthday", populate) &&
+ ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY == c.getInt(colEventType)) {
+ contact.put("birthday", c.getString(colBirthday));
+ }
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
+ && isRequired("photos", populate)) {
+ photos.put(photoQuery(c, contactId));
+ }
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+
+ // Set the old contact ID
+ oldContactId = contactId;
+
+ }
+
+ // Push the last contact into the contacts array
+ if (contacts.length() < limit) {
+ contacts.put(populateContact(contact, organizations, addresses, phones,
+ emails, ims, websites, photos));
+ }
+ }
+ c.close();
+ return contacts;
+ }
+
+ /**
+ * Builds a where clause all all the ids passed into the method
+ * @param contactIds a set of unique contact ids
+ * @param searchTerm what to search for
+ * @return an object containing the selection and selection args
+ */
+ private WhereOptions buildIdClause(Set<String> contactIds, String searchTerm) {
+ WhereOptions options = new WhereOptions();
+
+ // If the user is searching for every contact then short circuit the method
+ // and return a shorter where clause to be searched.
+ if (searchTerm.equals("%")) {
+ options.setWhere("(" + ContactsContract.Data.CONTACT_ID + " LIKE ? )");
+ options.setWhereArgs(new String[] { searchTerm });
+ return options;
+ }
+
+ // This clause means that there are specific ID's to be populated
+ Iterator<String> it = contactIds.iterator();
+ StringBuffer buffer = new StringBuffer("(");
+
+ while (it.hasNext()) {
+ buffer.append("'" + it.next() + "'");
+ if (it.hasNext()) {
+ buffer.append(",");
+ }
+ }
+ buffer.append(")");
+
+ options.setWhere(ContactsContract.Data.CONTACT_ID + " IN " + buffer.toString());
+ options.setWhereArgs(null);
+
+ return options;
+ }
+
+ /**
+ * Create a new contact using a JSONObject to hold all the data.
+ * @param contact
+ * @param organizations array of organizations
+ * @param addresses array of addresses
+ * @param phones array of phones
+ * @param emails array of emails
+ * @param ims array of instant messenger addresses
+ * @param websites array of websites
+ * @param photos
+ * @return
+ */
+ private JSONObject populateContact(JSONObject contact, JSONArray organizations,
+ JSONArray addresses, JSONArray phones, JSONArray emails,
+ JSONArray ims, JSONArray websites, JSONArray photos) {
+ try {
+ // Only return the array if it has at least one entry
+ if (organizations.length() > 0) {
+ contact.put("organizations", organizations);
+ }
+ if (addresses.length() > 0) {
+ contact.put("addresses", addresses);
+ }
+ if (phones.length() > 0) {
+ contact.put("phoneNumbers", phones);
+ }
+ if (emails.length() > 0) {
+ contact.put("emails", emails);
+ }
+ if (ims.length() > 0) {
+ contact.put("ims", ims);
+ }
+ if (websites.length() > 0) {
+ contact.put("urls", websites);
+ }
+ if (photos.length() > 0) {
+ contact.put("photos", photos);
+ }
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+ return contact;
+ }
+
+ /**
+ * Take the search criteria passed into the method and create a SQL WHERE clause.
+ * @param fields the properties to search against
+ * @param searchTerm the string to search for
+ * @return an object containing the selection and selection args
+ */
+ private WhereOptions buildWhereClause(JSONArray fields, String searchTerm) {
+
+ ArrayList<String> where = new ArrayList<String>();
+ ArrayList<String> whereArgs = new ArrayList<String>();
+
+ WhereOptions options = new WhereOptions();
+
+ /*
+ * Special case where the user wants all fields returned
+ */
+ if (isWildCardSearch(fields)) {
+ // Get all contacts with all properties
+ if ("%".equals(searchTerm)) {
+ options.setWhere("(" + ContactsContract.Contacts.DISPLAY_NAME + " LIKE ? )");
+ options.setWhereArgs(new String[] { searchTerm });
+ return options;
+ } else {
+ // Get all contacts that match the filter but return all properties
+ where.add("(" + dbMap.get("displayName") + " LIKE ? )");
+ whereArgs.add(searchTerm);
+ where.add("(" + dbMap.get("name") + " LIKE ? AND "
+ + ContactsContract.Data.MIMETYPE + " = ? )");
+ whereArgs.add(searchTerm);
+ whereArgs.add(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
+ where.add("(" + dbMap.get("nickname") + " LIKE ? AND "
+ + ContactsContract.Data.MIMETYPE + " = ? )");
+ whereArgs.add(searchTerm);
+ whereArgs.add(ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE);
+ where.add("(" + dbMap.get("phoneNumbers") + " LIKE ? AND "
+ + ContactsContract.Data.MIMETYPE + " = ? )");
+ whereArgs.add(searchTerm);
+ whereArgs.add(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
+ where.add("(" + dbMap.get("emails") + " LIKE ? AND "
+ + ContactsContract.Data.MIMETYPE + " = ? )");
+ whereArgs.add(searchTerm);
+ whereArgs.add(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE);
+ where.add("(" + dbMap.get("addresses") + " LIKE ? AND "
+ + ContactsContract.Data.MIMETYPE + " = ? )");
+ whereArgs.add(searchTerm);
+ whereArgs.add(ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE);
+ where.add("(" + dbMap.get("ims") + " LIKE ? AND "
+ + ContactsContract.Data.MIMETYPE + " = ? )");
+ whereArgs.add(searchTerm);
+ whereArgs.add(ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE);
+ where.add("(" + dbMap.get("organizations") + " LIKE ? AND "
+ + ContactsContract.Data.MIMETYPE + " = ? )");
+ whereArgs.add(searchTerm);
+ whereArgs.add(ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE);
+ where.add("(" + dbMap.get("note") + " LIKE ? AND "
+ + ContactsContract.Data.MIMETYPE + " = ? )");
+ whereArgs.add(searchTerm);
+ whereArgs.add(ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE);
+ where.add("(" + dbMap.get("urls") + " LIKE ? AND "
+ + ContactsContract.Data.MIMETYPE + " = ? )");
+ whereArgs.add(searchTerm);
+ whereArgs.add(ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE);
+ }
+ }
+
+ /*
+ * Special case for when the user wants all the contacts but
+ */
+ if ("%".equals(searchTerm)) {
+ options.setWhere("(" + ContactsContract.Contacts.DISPLAY_NAME + " LIKE ? )");
+ options.setWhereArgs(new String[] { searchTerm });
+ return options;
+ }
+
+ String key;
+ try {
+ //Log.d(LOG_TAG, "How many fields do we have = " + fields.length());
+ for (int i = 0; i < fields.length(); i++) {
+ key = fields.getString(i);
+
+ if (key.equals("id")) {
+ where.add("(" + dbMap.get(key) + " = ? )");
+ whereArgs.add(searchTerm.substring(1, searchTerm.length() - 1));
+ }
+ else if (key.startsWith("displayName")) {
+ where.add("(" + dbMap.get(key) + " LIKE ? )");
+ whereArgs.add(searchTerm);
+ }
+ else if (key.startsWith("name")) {
+ where.add("(" + dbMap.get(key) + " LIKE ? AND "
+ + ContactsContract.Data.MIMETYPE + " = ? )");
+ whereArgs.add(searchTerm);
+ whereArgs.add(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
+ }
+ else if (key.startsWith("nickname")) {
+ where.add("(" + dbMap.get(key) + " LIKE ? AND "
+ + ContactsContract.Data.MIMETYPE + " = ? )");
+ whereArgs.add(searchTerm);
+ whereArgs.add(ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE);
+ }
+ else if (key.startsWith("phoneNumbers")) {
+ where.add("(" + dbMap.get(key) + " LIKE ? AND "
+ + ContactsContract.Data.MIMETYPE + " = ? )");
+ whereArgs.add(searchTerm);
+ whereArgs.add(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
+ }
+ else if (key.startsWith("emails")) {
+ where.add("(" + dbMap.get(key) + " LIKE ? AND "
+ + ContactsContract.Data.MIMETYPE + " = ? )");
+ whereArgs.add(searchTerm);
+ whereArgs.add(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE);
+ }
+ else if (key.startsWith("addresses")) {
+ where.add("(" + dbMap.get(key) + " LIKE ? AND "
+ + ContactsContract.Data.MIMETYPE + " = ? )");
+ whereArgs.add(searchTerm);
+ whereArgs.add(ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE);
+ }
+ else if (key.startsWith("ims")) {
+ where.add("(" + dbMap.get(key) + " LIKE ? AND "
+ + ContactsContract.Data.MIMETYPE + " = ? )");
+ whereArgs.add(searchTerm);
+ whereArgs.add(ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE);
+ }
+ else if (key.startsWith("organizations")) {
+ where.add("(" + dbMap.get(key) + " LIKE ? AND "
+ + ContactsContract.Data.MIMETYPE + " = ? )");
+ whereArgs.add(searchTerm);
+ whereArgs.add(ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE);
+ }
+ // else if (key.startsWith("birthday")) {
+// where.add("(" + dbMap.get(key) + " LIKE ? AND "
+// + ContactsContract.Data.MIMETYPE + " = ? )");
+// }
+ else if (key.startsWith("note")) {
+ where.add("(" + dbMap.get(key) + " LIKE ? AND "
+ + ContactsContract.Data.MIMETYPE + " = ? )");
+ whereArgs.add(searchTerm);
+ whereArgs.add(ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE);
+ }
+ else if (key.startsWith("urls")) {
+ where.add("(" + dbMap.get(key) + " LIKE ? AND "
+ + ContactsContract.Data.MIMETYPE + " = ? )");
+ whereArgs.add(searchTerm);
+ whereArgs.add(ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE);
+ }
+ }
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+
+ // Creating the where string
+ StringBuffer selection = new StringBuffer();
+ for (int i = 0; i < where.size(); i++) {
+ selection.append(where.get(i));
+ if (i != (where.size() - 1)) {
+ selection.append(" OR ");
+ }
+ }
+ options.setWhere(selection.toString());
+
+ // Creating the where args array
+ String[] selectionArgs = new String[whereArgs.size()];
+ for (int i = 0; i < whereArgs.size(); i++) {
+ selectionArgs[i] = whereArgs.get(i);
+ }
+ options.setWhereArgs(selectionArgs);
+
+ return options;
+ }
+
+ /**
+ * If the user passes in the '*' wildcard character for search then they want all fields for each contact
+ *
+ * @param fields
+ * @return true if wildcard search requested, false otherwise
+ */
+ private boolean isWildCardSearch(JSONArray fields) {
+ // Only do a wildcard search if we are passed ["*"]
+ if (fields.length() == 1) {
+ try {
+ if ("*".equals(fields.getString(0))) {
+ return true;
+ }
+ } catch (JSONException e) {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Create a ContactOrganization JSONObject
+ * @param cursor the current database row
+ * @return a JSONObject representing a ContactOrganization
+ */
+ private JSONObject organizationQuery(Cursor cursor) {
+ JSONObject organization = new JSONObject();
+ try {
+ organization.put("id", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Organization._ID)));
+ organization.put("pref", false); // Android does not store pref attribute
+ organization.put("type", getOrgType(cursor.getInt(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Organization.TYPE))));
+ organization.put("department", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Organization.DEPARTMENT)));
+ organization.put("name", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Organization.COMPANY)));
+ organization.put("title", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Organization.TITLE)));
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+ return organization;
+ }
+
+ /**
+ * Create a ContactAddress JSONObject
+ * @param cursor the current database row
+ * @return a JSONObject representing a ContactAddress
+ */
+ private JSONObject addressQuery(Cursor cursor) {
+ JSONObject address = new JSONObject();
+ try {
+ address.put("id", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal._ID)));
+ address.put("pref", false); // Android does not store pref attribute
+ address.put("type", getAddressType(cursor.getInt(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Organization.TYPE))));
+ address.put("formatted", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS)));
+ address.put("streetAddress", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.STREET)));
+ address.put("locality", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.CITY)));
+ address.put("region", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.REGION)));
+ address.put("postalCode", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE)));
+ address.put("country", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY)));
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+ return address;
+ }
+
+ /**
+ * Create a ContactName JSONObject
+ * @param cursor the current database row
+ * @return a JSONObject representing a ContactName
+ */
+ private JSONObject nameQuery(Cursor cursor) {
+ JSONObject contactName = new JSONObject();
+ try {
+ String familyName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME));
+ String givenName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME));
+ String middleName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME));
+ String honorificPrefix = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.PREFIX));
+ String honorificSuffix = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.SUFFIX));
+
+ // Create the formatted name
+ StringBuffer formatted = new StringBuffer("");
+ if (honorificPrefix != null) {
+ formatted.append(honorificPrefix + " ");
+ }
+ if (givenName != null) {
+ formatted.append(givenName + " ");
+ }
+ if (middleName != null) {
+ formatted.append(middleName + " ");
+ }
+ if (familyName != null) {
+ formatted.append(familyName);
+ }
+ if (honorificSuffix != null) {
+ formatted.append(" " + honorificSuffix);
+ }
+
+ contactName.put("familyName", familyName);
+ contactName.put("givenName", givenName);
+ contactName.put("middleName", middleName);
+ contactName.put("honorificPrefix", honorificPrefix);
+ contactName.put("honorificSuffix", honorificSuffix);
+ contactName.put("formatted", formatted);
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+ return contactName;
+ }
+
+ /**
+ * Create a ContactField JSONObject
+ * @param cursor the current database row
+ * @return a JSONObject representing a ContactField
+ */
+ private JSONObject phoneQuery(Cursor cursor) {
+ JSONObject phoneNumber = new JSONObject();
+ try {
+ phoneNumber.put("id", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone._ID)));
+ phoneNumber.put("pref", false); // Android does not store pref attribute
+ phoneNumber.put("value", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)));
+ phoneNumber.put("type", getPhoneType(cursor.getInt(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE))));
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ } catch (Exception excp) {
+ Log.e(LOG_TAG, excp.getMessage(), excp);
+ }
+ return phoneNumber;
+ }
+
+ /**
+ * Create a ContactField JSONObject
+ * @param cursor the current database row
+ * @return a JSONObject representing a ContactField
+ */
+ private JSONObject emailQuery(Cursor cursor) {
+ JSONObject email = new JSONObject();
+ try {
+ email.put("id", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email._ID)));
+ email.put("pref", false); // Android does not store pref attribute
+ email.put("value", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA)));
+ email.put("type", getContactType(cursor.getInt(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.TYPE))));
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+ return email;
+ }
+
+ /**
+ * Create a ContactField JSONObject
+ * @param cursor the current database row
+ * @return a JSONObject representing a ContactField
+ */
+ private JSONObject imQuery(Cursor cursor) {
+ JSONObject im = new JSONObject();
+ try {
+ im.put("id", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Im._ID)));
+ im.put("pref", false); // Android does not store pref attribute
+ im.put("value", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA)));
+ im.put("type", getImType(cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Im.PROTOCOL))));
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+ return im;
+ }
+
+ /**
+ * Create a ContactField JSONObject
+ * @param cursor the current database row
+ * @return a JSONObject representing a ContactField
+ */
+ private JSONObject websiteQuery(Cursor cursor) {
+ JSONObject website = new JSONObject();
+ try {
+ website.put("id", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Website._ID)));
+ website.put("pref", false); // Android does not store pref attribute
+ website.put("value", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Website.URL)));
+ website.put("type", getContactType(cursor.getInt(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Website.TYPE))));
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+ return website;
+ }
+
+ /**
+ * Create a ContactField JSONObject
+ * @param contactId
+ * @return a JSONObject representing a ContactField
+ */
+ private JSONObject photoQuery(Cursor cursor, String contactId) {
+ JSONObject photo = new JSONObject();
+ try {
+ photo.put("id", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Photo._ID)));
+ photo.put("pref", false);
+ photo.put("type", "url");
+ Uri person = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, (new Long(contactId)));
+ Uri photoUri = Uri.withAppendedPath(person, ContactsContract.Contacts.Photo.CONTENT_DIRECTORY);
+ photo.put("value", photoUri.toString());
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+ return photo;
+ }
+
+ @Override
+ /**
+ * This method will save a contact object into the devices contacts database.
+ *
+ * @param contact the contact to be saved.
+ * @returns the id if the contact is successfully saved, null otherwise.
+ */
+ public String save(JSONObject contact) {
+ AccountManager mgr = AccountManager.get(mApp.getActivity());
+ Account[] accounts = mgr.getAccounts();
+ String accountName = null;
+ String accountType = null;
+
+ if (accounts.length == 1) {
+ accountName = accounts[0].name;
+ accountType = accounts[0].type;
+ }
+ else if (accounts.length > 1) {
+ for (Account a : accounts) {
+ if (a.type.contains("eas") && a.name.matches(EMAIL_REGEXP)) /*Exchange ActiveSync*/{
+ accountName = a.name;
+ accountType = a.type;
+ break;
+ }
+ }
+ if (accountName == null) {
+ for (Account a : accounts) {
+ if (a.type.contains("com.google") && a.name.matches(EMAIL_REGEXP)) /*Google sync provider*/{
+ accountName = a.name;
+ accountType = a.type;
+ break;
+ }
+ }
+ }
+ if (accountName == null) {
+ for (Account a : accounts) {
+ if (a.name.matches(EMAIL_REGEXP)) /*Last resort, just look for an email address...*/{
+ accountName = a.name;
+ accountType = a.type;
+ break;
+ }
+ }
+ }
+ }
+
+ String id = getJsonString(contact, "id");
+ if (id == null) {
+ // Create new contact
+ return createNewContact(contact, accountType, accountName);
+ } else {
+ // Modify existing contact
+ return modifyContact(id, contact, accountType, accountName);
+ }
+ }
+
+ /**
+ * Creates a new contact and stores it in the database
+ *
+ * @param id the raw contact id which is required for linking items to the contact
+ * @param contact the contact to be saved
+ * @param account the account to be saved under
+ */
+ private String modifyContact(String id, JSONObject contact, String accountType, String accountName) {
+ // Get the RAW_CONTACT_ID which is needed to insert new values in an already existing contact.
+ // But not needed to update existing values.
+ int rawId = (new Integer(getJsonString(contact, "rawId"))).intValue();
+
+ // Create a list of attributes to add to the contact database
+ ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+
+ //Add contact type
+ ops.add(ContentProviderOperation.newUpdate(ContactsContract.RawContacts.CONTENT_URI)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, accountName)
+ .build());
+
+ // Modify name
+ JSONObject name;
+ try {
+ String displayName = getJsonString(contact, "displayName");
+ name = contact.getJSONObject("name");
+ if (displayName != null || name != null) {
+ ContentProviderOperation.Builder builder = ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.Data.CONTACT_ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[] { id, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE });
+
+ if (displayName != null) {
+ builder.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName);
+ }
+
+ String familyName = getJsonString(name, "familyName");
+ if (familyName != null) {
+ builder.withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, familyName);
+ }
+ String middleName = getJsonString(name, "middleName");
+ if (middleName != null) {
+ builder.withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, middleName);
+ }
+ String givenName = getJsonString(name, "givenName");
+ if (givenName != null) {
+ builder.withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, givenName);
+ }
+ String honorificPrefix = getJsonString(name, "honorificPrefix");
+ if (honorificPrefix != null) {
+ builder.withValue(ContactsContract.CommonDataKinds.StructuredName.PREFIX, honorificPrefix);
+ }
+ String honorificSuffix = getJsonString(name, "honorificSuffix");
+ if (honorificSuffix != null) {
+ builder.withValue(ContactsContract.CommonDataKinds.StructuredName.SUFFIX, honorificSuffix);
+ }
+
+ ops.add(builder.build());
+ }
+ } catch (JSONException e1) {
+ Log.d(LOG_TAG, "Could not get name");
+ }
+
+ // Modify phone numbers
+ JSONArray phones = null;
+ try {
+ phones = contact.getJSONArray("phoneNumbers");
+ if (phones != null) {
+ // Delete all the phones
+ if (phones.length() == 0) {
+ ops.add(ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.Data.RAW_CONTACT_ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[] { "" + rawId, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE })
+ .build());
+ }
+ // Modify or add a phone
+ else {
+ for (int i = 0; i < phones.length(); i++) {
+ JSONObject phone = (JSONObject) phones.get(i);
+ String phoneId = getJsonString(phone, "id");
+ // This is a new phone so do a DB insert
+ if (phoneId == null) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(ContactsContract.Data.RAW_CONTACT_ID, rawId);
+ contentValues.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
+ contentValues.put(ContactsContract.CommonDataKinds.Phone.NUMBER, getJsonString(phone, "value"));
+ contentValues.put(ContactsContract.CommonDataKinds.Phone.TYPE, getPhoneType(getJsonString(phone, "type")));
+
+ ops.add(ContentProviderOperation.newInsert(
+ ContactsContract.Data.CONTENT_URI).withValues(contentValues).build());
+ }
+ // This is an existing phone so do a DB update
+ else {
+ ops.add(ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.CommonDataKinds.Phone._ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[] { phoneId, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE })
+ .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, getJsonString(phone, "value"))
+ .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, getPhoneType(getJsonString(phone, "type")))
+ .build());
+ }
+ }
+ }
+ }
+ } catch (JSONException e) {
+ Log.d(LOG_TAG, "Could not get phone numbers");
+ }
+
+ // Modify emails
+ JSONArray emails = null;
+ try {
+ emails = contact.getJSONArray("emails");
+ if (emails != null) {
+ // Delete all the emails
+ if (emails.length() == 0) {
+ ops.add(ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.Data.RAW_CONTACT_ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[] { "" + rawId, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE })
+ .build());
+ }
+ // Modify or add a email
+ else {
+ for (int i = 0; i < emails.length(); i++) {
+ JSONObject email = (JSONObject) emails.get(i);
+ String emailId = getJsonString(email, "id");
+ // This is a new email so do a DB insert
+ if (emailId == null) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(ContactsContract.Data.RAW_CONTACT_ID, rawId);
+ contentValues.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE);
+ contentValues.put(ContactsContract.CommonDataKinds.Email.DATA, getJsonString(email, "value"));
+ contentValues.put(ContactsContract.CommonDataKinds.Email.TYPE, getContactType(getJsonString(email, "type")));
+
+ ops.add(ContentProviderOperation.newInsert(
+ ContactsContract.Data.CONTENT_URI).withValues(contentValues).build());
+ }
+ // This is an existing email so do a DB update
+ else {
+ String emailValue=getJsonString(email, "value");
+ if(!emailValue.isEmpty()) {
+ ops.add(ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.CommonDataKinds.Email._ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[] { emailId, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE })
+ .withValue(ContactsContract.CommonDataKinds.Email.DATA, getJsonString(email, "value"))
+ .withValue(ContactsContract.CommonDataKinds.Email.TYPE, getContactType(getJsonString(email, "type")))
+ .build());
+ } else {
+ ops.add(ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.CommonDataKinds.Email._ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[] { emailId, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE })
+ .build());
+ }
+ }
+ }
+ }
+ }
+ } catch (JSONException e) {
+ Log.d(LOG_TAG, "Could not get emails");
+ }
+
+ // Modify addresses
+ JSONArray addresses = null;
+ try {
+ addresses = contact.getJSONArray("addresses");
+ if (addresses != null) {
+ // Delete all the addresses
+ if (addresses.length() == 0) {
+ ops.add(ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.Data.RAW_CONTACT_ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[] { "" + rawId, ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE })
+ .build());
+ }
+ // Modify or add a address
+ else {
+ for (int i = 0; i < addresses.length(); i++) {
+ JSONObject address = (JSONObject) addresses.get(i);
+ String addressId = getJsonString(address, "id");
+ // This is a new address so do a DB insert
+ if (addressId == null) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(ContactsContract.Data.RAW_CONTACT_ID, rawId);
+ contentValues.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE);
+ contentValues.put(ContactsContract.CommonDataKinds.StructuredPostal.TYPE, getAddressType(getJsonString(address, "type")));
+ contentValues.put(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS, getJsonString(address, "formatted"));
+ contentValues.put(ContactsContract.CommonDataKinds.StructuredPostal.STREET, getJsonString(address, "streetAddress"));
+ contentValues.put(ContactsContract.CommonDataKinds.StructuredPostal.CITY, getJsonString(address, "locality"));
+ contentValues.put(ContactsContract.CommonDataKinds.StructuredPostal.REGION, getJsonString(address, "region"));
+ contentValues.put(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE, getJsonString(address, "postalCode"));
+ contentValues.put(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY, getJsonString(address, "country"));
+
+ ops.add(ContentProviderOperation.newInsert(
+ ContactsContract.Data.CONTENT_URI).withValues(contentValues).build());
+ }
+ // This is an existing address so do a DB update
+ else {
+ ops.add(ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.CommonDataKinds.StructuredPostal._ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[] { addressId, ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE })
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.TYPE, getAddressType(getJsonString(address, "type")))
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS, getJsonString(address, "formatted"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.STREET, getJsonString(address, "streetAddress"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.CITY, getJsonString(address, "locality"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.REGION, getJsonString(address, "region"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE, getJsonString(address, "postalCode"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY, getJsonString(address, "country"))
+ .build());
+ }
+ }
+ }
+ }
+ } catch (JSONException e) {
+ Log.d(LOG_TAG, "Could not get addresses");
+ }
+
+ // Modify organizations
+ JSONArray organizations = null;
+ try {
+ organizations = contact.getJSONArray("organizations");
+ if (organizations != null) {
+ // Delete all the organizations
+ if (organizations.length() == 0) {
+ ops.add(ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.Data.RAW_CONTACT_ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[] { "" + rawId, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE })
+ .build());
+ }
+ // Modify or add a organization
+ else {
+ for (int i = 0; i < organizations.length(); i++) {
+ JSONObject org = (JSONObject) organizations.get(i);
+ String orgId = getJsonString(org, "id");
+ // This is a new organization so do a DB insert
+ if (orgId == null) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(ContactsContract.Data.RAW_CONTACT_ID, rawId);
+ contentValues.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE);
+ contentValues.put(ContactsContract.CommonDataKinds.Organization.TYPE, getOrgType(getJsonString(org, "type")));
+ contentValues.put(ContactsContract.CommonDataKinds.Organization.DEPARTMENT, getJsonString(org, "department"));
+ contentValues.put(ContactsContract.CommonDataKinds.Organization.COMPANY, getJsonString(org, "name"));
+ contentValues.put(ContactsContract.CommonDataKinds.Organization.TITLE, getJsonString(org, "title"));
+
+ ops.add(ContentProviderOperation.newInsert(
+ ContactsContract.Data.CONTENT_URI).withValues(contentValues).build());
+ }
+ // This is an existing organization so do a DB update
+ else {
+ ops.add(ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.CommonDataKinds.Organization._ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[] { orgId, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE })
+ .withValue(ContactsContract.CommonDataKinds.Organization.TYPE, getOrgType(getJsonString(org, "type")))
+ .withValue(ContactsContract.CommonDataKinds.Organization.DEPARTMENT, getJsonString(org, "department"))
+ .withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, getJsonString(org, "name"))
+ .withValue(ContactsContract.CommonDataKinds.Organization.TITLE, getJsonString(org, "title"))
+ .build());
+ }
+ }
+ }
+ }
+ } catch (JSONException e) {
+ Log.d(LOG_TAG, "Could not get organizations");
+ }
+
+ // Modify IMs
+ JSONArray ims = null;
+ try {
+ ims = contact.getJSONArray("ims");
+ if (ims != null) {
+ // Delete all the ims
+ if (ims.length() == 0) {
+ ops.add(ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.Data.RAW_CONTACT_ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[] { "" + rawId, ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE })
+ .build());
+ }
+ // Modify or add a im
+ else {
+ for (int i = 0; i < ims.length(); i++) {
+ JSONObject im = (JSONObject) ims.get(i);
+ String imId = getJsonString(im, "id");
+ // This is a new IM so do a DB insert
+ if (imId == null) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(ContactsContract.Data.RAW_CONTACT_ID, rawId);
+ contentValues.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE);
+ contentValues.put(ContactsContract.CommonDataKinds.Im.DATA, getJsonString(im, "value"));
+ contentValues.put(ContactsContract.CommonDataKinds.Im.TYPE, getImType(getJsonString(im, "type")));
+
+ ops.add(ContentProviderOperation.newInsert(
+ ContactsContract.Data.CONTENT_URI).withValues(contentValues).build());
+ }
+ // This is an existing IM so do a DB update
+ else {
+ ops.add(ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.CommonDataKinds.Im._ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[] { imId, ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE })
+ .withValue(ContactsContract.CommonDataKinds.Im.DATA, getJsonString(im, "value"))
+ .withValue(ContactsContract.CommonDataKinds.Im.TYPE, getContactType(getJsonString(im, "type")))
+ .build());
+ }
+ }
+ }
+ }
+ } catch (JSONException e) {
+ Log.d(LOG_TAG, "Could not get emails");
+ }
+
+ // Modify note
+ String note = getJsonString(contact, "note");
+ ops.add(ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.Data.CONTACT_ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[] { id, ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE })
+ .withValue(ContactsContract.CommonDataKinds.Note.NOTE, note)
+ .build());
+
+ // Modify nickname
+ String nickname = getJsonString(contact, "nickname");
+ if (nickname != null) {
+ ops.add(ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.Data.CONTACT_ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[] { id, ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE })
+ .withValue(ContactsContract.CommonDataKinds.Nickname.NAME, nickname)
+ .build());
+ }
+
+ // Modify urls
+ JSONArray websites = null;
+ try {
+ websites = contact.getJSONArray("urls");
+ if (websites != null) {
+ // Delete all the websites
+ if (websites.length() == 0) {
+ Log.d(LOG_TAG, "This means we should be deleting all the phone numbers.");
+ ops.add(ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.Data.RAW_CONTACT_ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[] { "" + rawId, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE })
+ .build());
+ }
+ // Modify or add a website
+ else {
+ for (int i = 0; i < websites.length(); i++) {
+ JSONObject website = (JSONObject) websites.get(i);
+ String websiteId = getJsonString(website, "id");
+ // This is a new website so do a DB insert
+ if (websiteId == null) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(ContactsContract.Data.RAW_CONTACT_ID, rawId);
+ contentValues.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE);
+ contentValues.put(ContactsContract.CommonDataKinds.Website.DATA, getJsonString(website, "value"));
+ contentValues.put(ContactsContract.CommonDataKinds.Website.TYPE, getContactType(getJsonString(website, "type")));
+
+ ops.add(ContentProviderOperation.newInsert(
+ ContactsContract.Data.CONTENT_URI).withValues(contentValues).build());
+ }
+ // This is an existing website so do a DB update
+ else {
+ ops.add(ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.CommonDataKinds.Website._ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[] { websiteId, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE })
+ .withValue(ContactsContract.CommonDataKinds.Website.DATA, getJsonString(website, "value"))
+ .withValue(ContactsContract.CommonDataKinds.Website.TYPE, getContactType(getJsonString(website, "type")))
+ .build());
+ }
+ }
+ }
+ }
+ } catch (JSONException e) {
+ Log.d(LOG_TAG, "Could not get websites");
+ }
+
+ // Modify birthday
+ String birthday = getJsonString(contact, "birthday");
+ if (birthday != null) {
+ ops.add(ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.Data.CONTACT_ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=? AND " +
+ ContactsContract.CommonDataKinds.Event.TYPE + "=?",
+ new String[] { id, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE, new String("" + ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY) })
+ .withValue(ContactsContract.CommonDataKinds.Event.TYPE, ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY)
+ .withValue(ContactsContract.CommonDataKinds.Event.START_DATE, birthday)
+ .build());
+ }
+
+ // Modify photos
+ JSONArray photos = null;
+ try {
+ photos = contact.getJSONArray("photos");
+ if (photos != null) {
+ // Delete all the photos
+ if (photos.length() == 0) {
+ ops.add(ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.Data.RAW_CONTACT_ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[] { "" + rawId, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE })
+ .build());
+ }
+ // Modify or add a photo
+ else {
+ for (int i = 0; i < photos.length(); i++) {
+ JSONObject photo = (JSONObject) photos.get(i);
+ String photoId = getJsonString(photo, "id");
+ byte[] bytes = getPhotoBytes(getJsonString(photo, "value"));
+ // This is a new photo so do a DB insert
+ if (photoId == null) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(ContactsContract.Data.RAW_CONTACT_ID, rawId);
+ contentValues.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE);
+ contentValues.put(ContactsContract.Data.IS_SUPER_PRIMARY, 1);
+ contentValues.put(ContactsContract.CommonDataKinds.Photo.PHOTO, bytes);
+
+ ops.add(ContentProviderOperation.newInsert(
+ ContactsContract.Data.CONTENT_URI).withValues(contentValues).build());
+ }
+ // This is an existing photo so do a DB update
+ else {
+ ops.add(ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.CommonDataKinds.Photo._ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[] { photoId, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE })
+ .withValue(ContactsContract.Data.IS_SUPER_PRIMARY, 1)
+ .withValue(ContactsContract.CommonDataKinds.Photo.PHOTO, bytes)
+ .build());
+ }
+ }
+ }
+ }
+ } catch (JSONException e) {
+ Log.d(LOG_TAG, "Could not get photos");
+ }
+
+ boolean retVal = true;
+
+ //Modify contact
+ try {
+ mApp.getActivity().getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ Log.e(LOG_TAG, Log.getStackTraceString(e), e);
+ retVal = false;
+ } catch (OperationApplicationException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ Log.e(LOG_TAG, Log.getStackTraceString(e), e);
+ retVal = false;
+ }
+
+ // if the save was a success return the contact ID
+ if (retVal) {
+ return id;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Add a website to a list of database actions to be performed
+ *
+ * @param ops the list of database actions
+ * @param website the item to be inserted
+ */
+ private void insertWebsite(ArrayList<ContentProviderOperation> ops,
+ JSONObject website) {
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Website.DATA, getJsonString(website, "value"))
+ .withValue(ContactsContract.CommonDataKinds.Website.TYPE, getContactType(getJsonString(website, "type")))
+ .build());
+ }
+
+ /**
+ * Add an im to a list of database actions to be performed
+ *
+ * @param ops the list of database actions
+ * @param im the item to be inserted
+ */
+ private void insertIm(ArrayList<ContentProviderOperation> ops, JSONObject im) {
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Im.DATA, getJsonString(im, "value"))
+ .withValue(ContactsContract.CommonDataKinds.Im.TYPE, getImType(getJsonString(im, "type")))
+ .build());
+ }
+
+ /**
+ * Add an organization to a list of database actions to be performed
+ *
+ * @param ops the list of database actions
+ * @param org the item to be inserted
+ */
+ private void insertOrganization(ArrayList<ContentProviderOperation> ops,
+ JSONObject org) {
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Organization.TYPE, getOrgType(getJsonString(org, "type")))
+ .withValue(ContactsContract.CommonDataKinds.Organization.DEPARTMENT, getJsonString(org, "department"))
+ .withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, getJsonString(org, "name"))
+ .withValue(ContactsContract.CommonDataKinds.Organization.TITLE, getJsonString(org, "title"))
+ .build());
+ }
+
+ /**
+ * Add an address to a list of database actions to be performed
+ *
+ * @param ops the list of database actions
+ * @param address the item to be inserted
+ */
+ private void insertAddress(ArrayList<ContentProviderOperation> ops,
+ JSONObject address) {
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.TYPE, getAddressType(getJsonString(address, "type")))
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS, getJsonString(address, "formatted"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.STREET, getJsonString(address, "streetAddress"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.CITY, getJsonString(address, "locality"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.REGION, getJsonString(address, "region"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE, getJsonString(address, "postalCode"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY, getJsonString(address, "country"))
+ .build());
+ }
+
+ /**
+ * Add an email to a list of database actions to be performed
+ *
+ * @param ops the list of database actions
+ * @param email the item to be inserted
+ */
+ private void insertEmail(ArrayList<ContentProviderOperation> ops,
+ JSONObject email) {
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Email.DATA, getJsonString(email, "value"))
+ .withValue(ContactsContract.CommonDataKinds.Email.TYPE, getContactType(getJsonString(email, "type")))
+ .build());
+ }
+
+ /**
+ * Add a phone to a list of database actions to be performed
+ *
+ * @param ops the list of database actions
+ * @param phone the item to be inserted
+ */
+ private void insertPhone(ArrayList<ContentProviderOperation> ops,
+ JSONObject phone) {
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, getJsonString(phone, "value"))
+ .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, getPhoneType(getJsonString(phone, "type")))
+ .build());
+ }
+
+ /**
+ * Add a phone to a list of database actions to be performed
+ *
+ * @param ops the list of database actions
+ * @param phone the item to be inserted
+ */
+ private void insertPhoto(ArrayList<ContentProviderOperation> ops,
+ JSONObject photo) {
+ byte[] bytes = getPhotoBytes(getJsonString(photo, "value"));
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.IS_SUPER_PRIMARY, 1)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Photo.PHOTO, bytes)
+ .build());
+ }
+
+ /**
+ * Gets the raw bytes from the supplied filename
+ *
+ * @param filename the file to read the bytes from
+ * @return a byte array
+ * @throws IOException
+ */
+ private byte[] getPhotoBytes(String filename) {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ try {
+ int bytesRead = 0;
+ long totalBytesRead = 0;
+ byte[] data = new byte[8192];
+ InputStream in = getPathFromUri(filename);
+
+ while ((bytesRead = in.read(data, 0, data.length)) != -1 && totalBytesRead <= MAX_PHOTO_SIZE) {
+ buffer.write(data, 0, bytesRead);
+ totalBytesRead += bytesRead;
+ }
+
+ in.close();
+ buffer.flush();
+ } catch (FileNotFoundException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ } catch (IOException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+ return buffer.toByteArray();
+ }
+
+ /**
+ * Get an input stream based on file path or uri content://, http://, file://
+ *
+ * @param path
+ * @return an input stream
+ * @throws IOException
+ */
+ private InputStream getPathFromUri(String path) throws IOException {
+ if (path.startsWith("content:")) {
+ Uri uri = Uri.parse(path);
+ return mApp.getActivity().getContentResolver().openInputStream(uri);
+ }
+ if (path.startsWith("http:") || path.startsWith("https:") || path.startsWith("file:")) {
+ URL url = new URL(path);
+ return url.openStream();
+ }
+ else {
+ return new FileInputStream(path);
+ }
+ }
+
+ /**
+ * Creates a new contact and stores it in the database
+ *
+ * @param contact the contact to be saved
+ * @param account the account to be saved under
+ */
+ private String createNewContact(JSONObject contact, String accountType, String accountName) {
+ // Create a list of attributes to add to the contact database
+ ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+
+ //Add contact type
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, accountName)
+ .build());
+
+ // Add name
+ try {
+ JSONObject name = contact.optJSONObject("name");
+ String displayName = contact.getString("displayName");
+ if (displayName != null || name != null) {
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName)
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, getJsonString(name, "familyName"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, getJsonString(name, "middleName"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, getJsonString(name, "givenName"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.PREFIX, getJsonString(name, "honorificPrefix"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.SUFFIX, getJsonString(name, "honorificSuffix"))
+ .build());
+ }
+ } catch (JSONException e) {
+ Log.d(LOG_TAG, "Could not get name object");
+ }
+
+ //Add phone numbers
+ JSONArray phones = null;
+ try {
+ phones = contact.getJSONArray("phoneNumbers");
+ if (phones != null) {
+ for (int i = 0; i < phones.length(); i++) {
+ JSONObject phone = (JSONObject) phones.get(i);
+ insertPhone(ops, phone);
+ }
+ }
+ } catch (JSONException e) {
+ Log.d(LOG_TAG, "Could not get phone numbers");
+ }
+
+ // Add emails
+ JSONArray emails = null;
+ try {
+ emails = contact.getJSONArray("emails");
+ if (emails != null) {
+ for (int i = 0; i < emails.length(); i++) {
+ JSONObject email = (JSONObject) emails.get(i);
+ insertEmail(ops, email);
+ }
+ }
+ } catch (JSONException e) {
+ Log.d(LOG_TAG, "Could not get emails");
+ }
+
+ // Add addresses
+ JSONArray addresses = null;
+ try {
+ addresses = contact.getJSONArray("addresses");
+ if (addresses != null) {
+ for (int i = 0; i < addresses.length(); i++) {
+ JSONObject address = (JSONObject) addresses.get(i);
+ insertAddress(ops, address);
+ }
+ }
+ } catch (JSONException e) {
+ Log.d(LOG_TAG, "Could not get addresses");
+ }
+
+ // Add organizations
+ JSONArray organizations = null;
+ try {
+ organizations = contact.getJSONArray("organizations");
+ if (organizations != null) {
+ for (int i = 0; i < organizations.length(); i++) {
+ JSONObject org = (JSONObject) organizations.get(i);
+ insertOrganization(ops, org);
+ }
+ }
+ } catch (JSONException e) {
+ Log.d(LOG_TAG, "Could not get organizations");
+ }
+
+ // Add IMs
+ JSONArray ims = null;
+ try {
+ ims = contact.getJSONArray("ims");
+ if (ims != null) {
+ for (int i = 0; i < ims.length(); i++) {
+ JSONObject im = (JSONObject) ims.get(i);
+ insertIm(ops, im);
+ }
+ }
+ } catch (JSONException e) {
+ Log.d(LOG_TAG, "Could not get emails");
+ }
+
+
<TRUNCATED>
[2/6] [CB-4341] Adding a fix to make subdirectories work within a
local plugin dependency - Includes the integration of integration specs which
test installation of plugins with dependencies
Posted by br...@apache.org.
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/ios/CDVContacts.m
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/ios/CDVContacts.m b/spec/plugins/Contacts/src/ios/CDVContacts.m
new file mode 100644
index 0000000..3ca3e81
--- /dev/null
+++ b/spec/plugins/Contacts/src/ios/CDVContacts.m
@@ -0,0 +1,593 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ */
+
+#import "CDVContacts.h"
+#import <UIKit/UIKit.h>
+#import <Cordova/NSArray+Comparisons.h>
+#import <Cordova/NSDictionary+Extensions.h>
+//#import "CDVNotification.h"
+
+@implementation CDVContactsPicker
+
+@synthesize allowsEditing;
+@synthesize callbackId;
+@synthesize options;
+@synthesize pickedContactDictionary;
+
+@end
+@implementation CDVNewContactsController
+
+@synthesize callbackId;
+
+@end
+
+@implementation CDVContacts
+
+// no longer used since code gets AddressBook for each operation.
+// If address book changes during save or remove operation, may get error but not much we can do about it
+// If address book changes during UI creation, display or edit, we don't control any saves so no need for callback
+
+/*void addressBookChanged(ABAddressBookRef addressBook, CFDictionaryRef info, void* context)
+{
+ // note that this function is only called when another AddressBook instance modifies
+ // the address book, not the current one. For example, through an OTA MobileMe sync
+ Contacts* contacts = (Contacts*)context;
+ [contacts addressBookDirty];
+ }*/
+
+- (CDVPlugin*)initWithWebView:(UIWebView*)theWebView
+{
+ self = (CDVContacts*)[super initWithWebView:(UIWebView*)theWebView];
+
+ /*if (self) {
+ addressBook = ABAddressBookCreate();
+ ABAddressBookRegisterExternalChangeCallback(addressBook, addressBookChanged, self);
+ }*/
+
+ return self;
+}
+
+// overridden to clean up Contact statics
+- (void)onAppTerminate
+{
+ // NSLog(@"Contacts::onAppTerminate");
+}
+
+// iPhone only method to create a new contact through the GUI
+- (void)newContact:(CDVInvokedUrlCommand*)command
+{
+ NSString* callbackId = command.callbackId;
+
+ CDVAddressBookHelper* abHelper = [[CDVAddressBookHelper alloc] init];
+ CDVContacts* __weak weakSelf = self; // play it safe to avoid retain cycles
+
+ [abHelper createAddressBook: ^(ABAddressBookRef addrBook, CDVAddressBookAccessError* errCode) {
+ if (addrBook == NULL) {
+ // permission was denied or other error just return (no error callback)
+ return;
+ }
+ CDVNewContactsController* npController = [[CDVNewContactsController alloc] init];
+ npController.addressBook = addrBook; // a CF retaining assign
+ CFRelease(addrBook);
+
+ npController.newPersonViewDelegate = self;
+ npController.callbackId = callbackId;
+
+ UINavigationController* navController = [[UINavigationController alloc] initWithRootViewController:npController];
+
+ if ([weakSelf.viewController respondsToSelector:@selector(presentViewController:::)]) {
+ [weakSelf.viewController presentViewController:navController animated:YES completion:nil];
+ } else {
+ [weakSelf.viewController presentModalViewController:navController animated:YES];
+ }
+ }];
+}
+
+- (void)newPersonViewController:(ABNewPersonViewController*)newPersonViewController didCompleteWithNewPerson:(ABRecordRef)person
+{
+ ABRecordID recordId = kABRecordInvalidID;
+ CDVNewContactsController* newCP = (CDVNewContactsController*)newPersonViewController;
+ NSString* callbackId = newCP.callbackId;
+
+ if (person != NULL) {
+ // return the contact id
+ recordId = ABRecordGetRecordID(person);
+ }
+
+ if ([newPersonViewController respondsToSelector:@selector(presentingViewController)]) {
+ [[newPersonViewController presentingViewController] dismissViewControllerAnimated:YES completion:nil];
+ } else {
+ [[newPersonViewController parentViewController] dismissModalViewControllerAnimated:YES];
+ }
+
+ CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:recordId];
+ [self.commandDelegate sendPluginResult:result callbackId:callbackId];
+}
+
+- (void)displayContact:(CDVInvokedUrlCommand*)command
+{
+ NSString* callbackId = command.callbackId;
+ ABRecordID recordID = [[command.arguments objectAtIndex:0] intValue];
+ NSDictionary* options = [command.arguments objectAtIndex:1 withDefault:[NSNull null]];
+ bool bEdit = [options isKindOfClass:[NSNull class]] ? false : [options existsValue:@"true" forKey:@"allowsEditing"];
+
+ CDVAddressBookHelper* abHelper = [[CDVAddressBookHelper alloc] init];
+ CDVContacts* __weak weakSelf = self; // play it safe to avoid retain cycles
+
+ [abHelper createAddressBook: ^(ABAddressBookRef addrBook, CDVAddressBookAccessError* errCode) {
+ if (addrBook == NULL) {
+ // permission was denied or other error - return error
+ CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:errCode ? errCode.errorCode:UNKNOWN_ERROR];
+ [weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId];
+ return;
+ }
+ ABRecordRef rec = ABAddressBookGetPersonWithRecordID(addrBook, recordID);
+
+ if (rec) {
+ CDVDisplayContactViewController* personController = [[CDVDisplayContactViewController alloc] init];
+ personController.displayedPerson = rec;
+ personController.personViewDelegate = self;
+ personController.allowsEditing = NO;
+
+ // create this so DisplayContactViewController will have a "back" button.
+ UIViewController* parentController = [[UIViewController alloc] init];
+ UINavigationController* navController = [[UINavigationController alloc] initWithRootViewController:parentController];
+
+ [navController pushViewController:personController animated:YES];
+
+ if ([self.viewController respondsToSelector:@selector(presentViewController:::)]) {
+ [self.viewController presentViewController:navController animated:YES completion:nil];
+ } else {
+ [self.viewController presentModalViewController:navController animated:YES];
+ }
+
+ if (bEdit) {
+ // create the editing controller and push it onto the stack
+ ABPersonViewController* editPersonController = [[ABPersonViewController alloc] init];
+ editPersonController.displayedPerson = rec;
+ editPersonController.personViewDelegate = self;
+ editPersonController.allowsEditing = YES;
+ [navController pushViewController:editPersonController animated:YES];
+ }
+ } else {
+ // no record, return error
+ CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:UNKNOWN_ERROR];
+ [weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId];
+ }
+ CFRelease(addrBook);
+ }];
+}
+
+- (BOOL)personViewController:(ABPersonViewController*)personViewController shouldPerformDefaultActionForPerson:(ABRecordRef)person
+ property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifierForValue
+{
+ return YES;
+}
+
+- (void)chooseContact:(CDVInvokedUrlCommand*)command
+{
+ NSString* callbackId = command.callbackId;
+ NSDictionary* options = [command.arguments objectAtIndex:0 withDefault:[NSNull null]];
+
+ CDVContactsPicker* pickerController = [[CDVContactsPicker alloc] init];
+
+ pickerController.peoplePickerDelegate = self;
+ pickerController.callbackId = callbackId;
+ pickerController.options = options;
+ pickerController.pickedContactDictionary = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:kABRecordInvalidID], kW3ContactId, nil];
+ pickerController.allowsEditing = (BOOL)[options existsValue : @"true" forKey : @"allowsEditing"];
+
+ if ([self.viewController respondsToSelector:@selector(presentViewController:::)]) {
+ [self.viewController presentViewController:pickerController animated:YES completion:nil];
+ } else {
+ [self.viewController presentModalViewController:pickerController animated:YES];
+ }
+}
+
+- (BOOL)peoplePickerNavigationController:(ABPeoplePickerNavigationController*)peoplePicker
+ shouldContinueAfterSelectingPerson:(ABRecordRef)person
+{
+ CDVContactsPicker* picker = (CDVContactsPicker*)peoplePicker;
+ NSNumber* pickedId = [NSNumber numberWithInt:ABRecordGetRecordID(person)];
+
+ if (picker.allowsEditing) {
+ ABPersonViewController* personController = [[ABPersonViewController alloc] init];
+ personController.displayedPerson = person;
+ personController.personViewDelegate = self;
+ personController.allowsEditing = picker.allowsEditing;
+ // store id so can get info in peoplePickerNavigationControllerDidCancel
+ picker.pickedContactDictionary = [NSDictionary dictionaryWithObjectsAndKeys:pickedId, kW3ContactId, nil];
+
+ [peoplePicker pushViewController:personController animated:YES];
+ } else {
+ // Retrieve and return pickedContact information
+ CDVContact* pickedContact = [[CDVContact alloc] initFromABRecord:(ABRecordRef)person];
+ NSArray* fields = [picker.options objectForKey:@"fields"];
+ NSDictionary* returnFields = [[CDVContact class] calcReturnFields:fields];
+ picker.pickedContactDictionary = [pickedContact toDictionary:returnFields];
+
+ CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:picker.pickedContactDictionary];
+ [self.commandDelegate sendPluginResult:result callbackId:picker.callbackId];
+
+ if ([picker respondsToSelector:@selector(presentingViewController)]) {
+ [[picker presentingViewController] dismissViewControllerAnimated:YES completion:nil];
+ } else {
+ [[picker parentViewController] dismissModalViewControllerAnimated:YES];
+ }
+ }
+ return NO;
+}
+
+- (BOOL)peoplePickerNavigationController:(ABPeoplePickerNavigationController*)peoplePicker
+ shouldContinueAfterSelectingPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier
+{
+ return YES;
+}
+
+- (void)peoplePickerNavigationControllerDidCancel:(ABPeoplePickerNavigationController*)peoplePicker
+{
+ // return contactId or invalid if none picked
+ CDVContactsPicker* picker = (CDVContactsPicker*)peoplePicker;
+
+ if (picker.allowsEditing) {
+ // get the info after possible edit
+ // if we got this far, user has already approved/ disapproved addressBook access
+ ABAddressBookRef addrBook = nil;
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000
+ if (&ABAddressBookCreateWithOptions != NULL) {
+ addrBook = ABAddressBookCreateWithOptions(NULL, NULL);
+ } else
+#endif
+ {
+ // iOS 4 & 5
+ addrBook = ABAddressBookCreate();
+ }
+ ABRecordRef person = ABAddressBookGetPersonWithRecordID(addrBook, [[picker.pickedContactDictionary objectForKey:kW3ContactId] integerValue]);
+ if (person) {
+ CDVContact* pickedContact = [[CDVContact alloc] initFromABRecord:(ABRecordRef)person];
+ NSArray* fields = [picker.options objectForKey:@"fields"];
+ NSDictionary* returnFields = [[CDVContact class] calcReturnFields:fields];
+ picker.pickedContactDictionary = [pickedContact toDictionary:returnFields];
+ }
+ CFRelease(addrBook);
+ }
+ CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:picker.pickedContactDictionary];
+ [self.commandDelegate sendPluginResult:result callbackId:picker.callbackId];
+
+ if ([peoplePicker respondsToSelector:@selector(presentingViewController)]) {
+ [[peoplePicker presentingViewController] dismissViewControllerAnimated:YES completion:nil];
+ } else {
+ [[peoplePicker parentViewController] dismissModalViewControllerAnimated:YES];
+ }
+}
+
+- (void)search:(CDVInvokedUrlCommand*)command
+{
+ NSString* callbackId = command.callbackId;
+ NSArray* fields = [command.arguments objectAtIndex:0];
+ NSDictionary* findOptions = [command.arguments objectAtIndex:1 withDefault:[NSNull null]];
+
+ [self.commandDelegate runInBackground:^{
+ // from Apple: Important You must ensure that an instance of ABAddressBookRef is used by only one thread.
+ // which is why address book is created within the dispatch queue.
+ // more details here: http: //blog.byadrian.net/2012/05/05/ios-addressbook-framework-and-gcd/
+ CDVAddressBookHelper* abHelper = [[CDVAddressBookHelper alloc] init];
+ CDVContacts* __weak weakSelf = self; // play it safe to avoid retain cycles
+ // it gets uglier, block within block.....
+ [abHelper createAddressBook: ^(ABAddressBookRef addrBook, CDVAddressBookAccessError* errCode) {
+ if (addrBook == NULL) {
+ // permission was denied or other error - return error
+ CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:errCode ? errCode.errorCode:UNKNOWN_ERROR];
+ [weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId];
+ return;
+ }
+
+ NSArray* foundRecords = nil;
+ // get the findOptions values
+ BOOL multiple = NO; // default is false
+ NSString* filter = nil;
+ if (![findOptions isKindOfClass:[NSNull class]]) {
+ id value = nil;
+ filter = (NSString*)[findOptions objectForKey:@"filter"];
+ value = [findOptions objectForKey:@"multiple"];
+ if ([value isKindOfClass:[NSNumber class]]) {
+ // multiple is a boolean that will come through as an NSNumber
+ multiple = [(NSNumber*)value boolValue];
+ // NSLog(@"multiple is: %d", multiple);
+ }
+ }
+
+ NSDictionary* returnFields = [[CDVContact class] calcReturnFields:fields];
+
+ NSMutableArray* matches = nil;
+ if (!filter || [filter isEqualToString:@""]) {
+ // get all records
+ foundRecords = (__bridge_transfer NSArray*)ABAddressBookCopyArrayOfAllPeople(addrBook);
+ if (foundRecords && ([foundRecords count] > 0)) {
+ // create Contacts and put into matches array
+ // doesn't make sense to ask for all records when multiple == NO but better check
+ int xferCount = multiple == YES ? [foundRecords count] : 1;
+ matches = [NSMutableArray arrayWithCapacity:xferCount];
+
+ for (int k = 0; k < xferCount; k++) {
+ CDVContact* xferContact = [[CDVContact alloc] initFromABRecord:(__bridge ABRecordRef)[foundRecords objectAtIndex:k]];
+ [matches addObject:xferContact];
+ xferContact = nil;
+ }
+ }
+ } else {
+ foundRecords = (__bridge_transfer NSArray*)ABAddressBookCopyArrayOfAllPeople(addrBook);
+ matches = [NSMutableArray arrayWithCapacity:1];
+ BOOL bFound = NO;
+ int testCount = [foundRecords count];
+
+ for (int j = 0; j < testCount; j++) {
+ CDVContact* testContact = [[CDVContact alloc] initFromABRecord:(__bridge ABRecordRef)[foundRecords objectAtIndex:j]];
+ if (testContact) {
+ bFound = [testContact foundValue:filter inFields:returnFields];
+ if (bFound) {
+ [matches addObject:testContact];
+ }
+ testContact = nil;
+ }
+ }
+ }
+ NSMutableArray* returnContacts = [NSMutableArray arrayWithCapacity:1];
+
+ if ((matches != nil) && ([matches count] > 0)) {
+ // convert to JS Contacts format and return in callback
+ // - returnFields determines what properties to return
+ @autoreleasepool {
+ int count = multiple == YES ? [matches count] : 1;
+
+ for (int i = 0; i < count; i++) {
+ CDVContact* newContact = [matches objectAtIndex:i];
+ NSDictionary* aContact = [newContact toDictionary:returnFields];
+ [returnContacts addObject:aContact];
+ }
+ }
+ }
+ // return found contacts (array is empty if no contacts found)
+ CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:returnContacts];
+ [weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId];
+ // NSLog(@"findCallback string: %@", jsString);
+
+ if (addrBook) {
+ CFRelease(addrBook);
+ }
+ }];
+ }]; // end of workQueue block
+
+ return;
+}
+
+- (void)save:(CDVInvokedUrlCommand*)command
+{
+ NSString* callbackId = command.callbackId;
+ NSDictionary* contactDict = [command.arguments objectAtIndex:0];
+
+ [self.commandDelegate runInBackground:^{
+ CDVAddressBookHelper* abHelper = [[CDVAddressBookHelper alloc] init];
+ CDVContacts* __weak weakSelf = self; // play it safe to avoid retain cycles
+
+ [abHelper createAddressBook: ^(ABAddressBookRef addrBook, CDVAddressBookAccessError* errorCode) {
+ CDVPluginResult* result = nil;
+ if (addrBook == NULL) {
+ // permission was denied or other error - return error
+ result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:errorCode ? errorCode.errorCode:UNKNOWN_ERROR];
+ [weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId];
+ return;
+ }
+
+ bool bIsError = FALSE, bSuccess = FALSE;
+ BOOL bUpdate = NO;
+ CDVContactError errCode = UNKNOWN_ERROR;
+ CFErrorRef error;
+ NSNumber* cId = [contactDict valueForKey:kW3ContactId];
+ CDVContact* aContact = nil;
+ ABRecordRef rec = nil;
+ if (cId && ![cId isKindOfClass:[NSNull class]]) {
+ rec = ABAddressBookGetPersonWithRecordID(addrBook, [cId intValue]);
+ if (rec) {
+ aContact = [[CDVContact alloc] initFromABRecord:rec];
+ bUpdate = YES;
+ }
+ }
+ if (!aContact) {
+ aContact = [[CDVContact alloc] init];
+ }
+
+ bSuccess = [aContact setFromContactDict:contactDict asUpdate:bUpdate];
+ if (bSuccess) {
+ if (!bUpdate) {
+ bSuccess = ABAddressBookAddRecord(addrBook, [aContact record], &error);
+ }
+ if (bSuccess) {
+ bSuccess = ABAddressBookSave(addrBook, &error);
+ }
+ if (!bSuccess) { // need to provide error codes
+ bIsError = TRUE;
+ errCode = IO_ERROR;
+ } else {
+ // give original dictionary back? If generate dictionary from saved contact, have no returnFields specified
+ // so would give back all fields (which W3C spec. indicates is not desired)
+ // for now (while testing) give back saved, full contact
+ NSDictionary* newContact = [aContact toDictionary:[CDVContact defaultFields]];
+ // NSString* contactStr = [newContact JSONRepresentation];
+ result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:newContact];
+ }
+ } else {
+ bIsError = TRUE;
+ errCode = IO_ERROR;
+ }
+ CFRelease(addrBook);
+
+ if (bIsError) {
+ result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:errCode];
+ }
+
+ if (result) {
+ [weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId];
+ }
+ }];
+ }]; // end of queue
+}
+
+- (void)remove:(CDVInvokedUrlCommand*)command
+{
+ NSString* callbackId = command.callbackId;
+ NSNumber* cId = [command.arguments objectAtIndex:0];
+
+ CDVAddressBookHelper* abHelper = [[CDVAddressBookHelper alloc] init];
+ CDVContacts* __weak weakSelf = self; // play it safe to avoid retain cycles
+
+ [abHelper createAddressBook: ^(ABAddressBookRef addrBook, CDVAddressBookAccessError* errorCode) {
+ CDVPluginResult* result = nil;
+ if (addrBook == NULL) {
+ // permission was denied or other error - return error
+ result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:errorCode ? errorCode.errorCode:UNKNOWN_ERROR];
+ [weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId];
+ return;
+ }
+
+ bool bIsError = FALSE, bSuccess = FALSE;
+ CDVContactError errCode = UNKNOWN_ERROR;
+ CFErrorRef error;
+ ABRecordRef rec = nil;
+ if (cId && ![cId isKindOfClass:[NSNull class]] && ([cId intValue] != kABRecordInvalidID)) {
+ rec = ABAddressBookGetPersonWithRecordID(addrBook, [cId intValue]);
+ if (rec) {
+ bSuccess = ABAddressBookRemoveRecord(addrBook, rec, &error);
+ if (!bSuccess) {
+ bIsError = TRUE;
+ errCode = IO_ERROR;
+ } else {
+ bSuccess = ABAddressBookSave(addrBook, &error);
+ if (!bSuccess) {
+ bIsError = TRUE;
+ errCode = IO_ERROR;
+ } else {
+ // set id to null
+ // [contactDict setObject:[NSNull null] forKey:kW3ContactId];
+ // result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary: contactDict];
+ result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
+ // NSString* contactStr = [contactDict JSONRepresentation];
+ }
+ }
+ } else {
+ // no record found return error
+ bIsError = TRUE;
+ errCode = UNKNOWN_ERROR;
+ }
+ } else {
+ // invalid contact id provided
+ bIsError = TRUE;
+ errCode = INVALID_ARGUMENT_ERROR;
+ }
+
+ if (addrBook) {
+ CFRelease(addrBook);
+ }
+ if (bIsError) {
+ result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsInt:errCode];
+ }
+ if (result) {
+ [weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId];
+ }
+ }];
+ return;
+}
+
+@end
+
+/* ABPersonViewController does not have any UI to dismiss. Adding navigationItems to it does not work properly
+ * The navigationItems are lost when the app goes into the background. The solution was to create an empty
+ * NavController in front of the ABPersonViewController. This will cause the ABPersonViewController to have a back button. By subclassing the ABPersonViewController, we can override viewDidDisappear and take down the entire NavigationController.
+ */
+@implementation CDVDisplayContactViewController
+@synthesize contactsPlugin;
+
+- (void)viewWillDisappear:(BOOL)animated
+{
+ [super viewWillDisappear:animated];
+
+ if ([self respondsToSelector:@selector(presentingViewController)]) {
+ [[self presentingViewController] dismissViewControllerAnimated:YES completion:nil];
+ } else {
+ [[self parentViewController] dismissModalViewControllerAnimated:YES];
+ }
+}
+
+@end
+@implementation CDVAddressBookAccessError
+
+@synthesize errorCode;
+
+- (CDVAddressBookAccessError*)initWithCode:(CDVContactError)code
+{
+ self = [super init];
+ if (self) {
+ self.errorCode = code;
+ }
+ return self;
+}
+
+@end
+
+@implementation CDVAddressBookHelper
+
+/**
+ * NOTE: workerBlock is responsible for releasing the addressBook that is passed to it
+ */
+- (void)createAddressBook:(CDVAddressBookWorkerBlock)workerBlock
+{
+ // TODO: this probably should be reworked - seems like the workerBlock can just create and release its own AddressBook,
+ // and also this important warning from (http://developer.apple.com/library/ios/#documentation/ContactData/Conceptual/AddressBookProgrammingGuideforiPhone/Chapters/BasicObjects.html):
+ // "Important: Instances of ABAddressBookRef cannot be used by multiple threads. Each thread must make its own instance."
+ ABAddressBookRef addressBook;
+
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000
+ if (&ABAddressBookCreateWithOptions != NULL) {
+ CFErrorRef error = nil;
+ // CFIndex status = ABAddressBookGetAuthorizationStatus();
+ addressBook = ABAddressBookCreateWithOptions(NULL, &error);
+ // NSLog(@"addressBook access: %lu", status);
+ ABAddressBookRequestAccessWithCompletion(addressBook, ^(bool granted, CFErrorRef error) {
+ // callback can occur in background, address book must be accessed on thread it was created on
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ if (error) {
+ workerBlock(NULL, [[CDVAddressBookAccessError alloc] initWithCode:UNKNOWN_ERROR]);
+ } else if (!granted) {
+ workerBlock(NULL, [[CDVAddressBookAccessError alloc] initWithCode:PERMISSION_DENIED_ERROR]);
+ } else {
+ // access granted
+ workerBlock(addressBook, [[CDVAddressBookAccessError alloc] initWithCode:UNKNOWN_ERROR]);
+ }
+ });
+ });
+ } else
+#endif
+ {
+ // iOS 4 or 5 no checks needed
+ addressBook = ABAddressBookCreate();
+ workerBlock(addressBook, NULL);
+ }
+}
+
+@end
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/src/wp/Contacts.cs
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/src/wp/Contacts.cs b/spec/plugins/Contacts/src/wp/Contacts.cs
new file mode 100644
index 0000000..af78942
--- /dev/null
+++ b/spec/plugins/Contacts/src/wp/Contacts.cs
@@ -0,0 +1,664 @@
+/*
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+using Microsoft.Phone.Tasks;
+using Microsoft.Phone.UserData;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Windows;
+using DeviceContacts = Microsoft.Phone.UserData.Contacts;
+
+
+namespace WPCordovaClassLib.Cordova.Commands
+{
+ [DataContract]
+ public class SearchOptions
+ {
+ [DataMember]
+ public string filter { get; set; }
+ [DataMember]
+ public bool multiple { get; set; }
+ }
+
+ [DataContract]
+ public class ContactSearchParams
+ {
+ [DataMember]
+ public string[] fields { get; set; }
+ [DataMember]
+ public SearchOptions options { get; set; }
+ }
+
+ [DataContract]
+ public class JSONContactAddress
+ {
+ [DataMember]
+ public string formatted { get; set; }
+ [DataMember]
+ public string type { get; set; }
+ [DataMember]
+ public string streetAddress { get; set; }
+ [DataMember]
+ public string locality { get; set; }
+ [DataMember]
+ public string region { get; set; }
+ [DataMember]
+ public string postalCode { get; set; }
+ [DataMember]
+ public string country { get; set; }
+ [DataMember]
+ public bool pref { get; set; }
+ }
+
+ [DataContract]
+ public class JSONContactName
+ {
+ [DataMember]
+ public string formatted { get; set; }
+ [DataMember]
+ public string familyName { get; set; }
+ [DataMember]
+ public string givenName { get; set; }
+ [DataMember]
+ public string middleName { get; set; }
+ [DataMember]
+ public string honorificPrefix { get; set; }
+ [DataMember]
+ public string honorificSuffix { get; set; }
+ }
+
+ [DataContract]
+ public class JSONContactField
+ {
+ [DataMember]
+ public string type { get; set; }
+ [DataMember]
+ public string value { get; set; }
+ [DataMember]
+ public bool pref { get; set; }
+ }
+
+ [DataContract]
+ public class JSONContactOrganization
+ {
+ [DataMember]
+ public string type { get; set; }
+ [DataMember]
+ public string name { get; set; }
+ [DataMember]
+ public bool pref { get; set; }
+ [DataMember]
+ public string department { get; set; }
+ [DataMember]
+ public string title { get; set; }
+ }
+
+ [DataContract]
+ public class JSONContact
+ {
+ [DataMember]
+ public string id { get; set; }
+ [DataMember]
+ public string rawId { get; set; }
+ [DataMember]
+ public string displayName { get; set; }
+ [DataMember]
+ public string nickname { get; set; }
+ [DataMember]
+ public string note { get; set; }
+
+ [DataMember]
+ public JSONContactName name { get; set; }
+
+ [DataMember]
+ public JSONContactField[] emails { get; set; }
+
+ [DataMember]
+ public JSONContactField[] phoneNumbers { get; set; }
+
+ [DataMember]
+ public JSONContactField[] ims { get; set; }
+
+ [DataMember]
+ public JSONContactField[] photos { get; set; }
+
+ [DataMember]
+ public JSONContactField[] categories { get; set; }
+
+ [DataMember]
+ public JSONContactField[] urls { get; set; }
+
+ [DataMember]
+ public JSONContactOrganization[] organizations { get; set; }
+
+ [DataMember]
+ public JSONContactAddress[] addresses { get; set; }
+ }
+
+
+ public class Contacts : BaseCommand
+ {
+
+ public const int UNKNOWN_ERROR = 0;
+ public const int INVALID_ARGUMENT_ERROR = 1;
+ public const int TIMEOUT_ERROR = 2;
+ public const int PENDING_OPERATION_ERROR = 3;
+ public const int IO_ERROR = 4;
+ public const int NOT_SUPPORTED_ERROR = 5;
+ public const int PERMISSION_DENIED_ERROR = 20;
+ public const int SYNTAX_ERR = 8;
+
+ public Contacts()
+ {
+
+ }
+
+ // refer here for contact properties we can access: http://msdn.microsoft.com/en-us/library/microsoft.phone.tasks.savecontacttask_members%28v=VS.92%29.aspx
+ public void save(string jsonContact)
+ {
+
+ // jsonContact is actually an array of 1 {contact}
+ string[] args = JSON.JsonHelper.Deserialize<string[]>(jsonContact);
+
+
+ JSONContact contact = JSON.JsonHelper.Deserialize<JSONContact>(args[0]);
+
+ SaveContactTask contactTask = new SaveContactTask();
+
+ if (contact.nickname != null)
+ {
+ contactTask.Nickname = contact.nickname;
+ }
+ if (contact.urls != null && contact.urls.Length > 0)
+ {
+ contactTask.Website = contact.urls[0].value;
+ }
+ if (contact.note != null)
+ {
+ contactTask.Notes = contact.note;
+ }
+
+ #region contact.name
+ if (contact.name != null)
+ {
+ if (contact.name.givenName != null)
+ contactTask.FirstName = contact.name.givenName;
+ if (contact.name.familyName != null)
+ contactTask.LastName = contact.name.familyName;
+ if (contact.name.middleName != null)
+ contactTask.MiddleName = contact.name.middleName;
+ if (contact.name.honorificSuffix != null)
+ contactTask.Suffix = contact.name.honorificSuffix;
+ if (contact.name.honorificPrefix != null)
+ contactTask.Title = contact.name.honorificPrefix;
+ }
+ #endregion
+
+ #region contact.org
+ if (contact.organizations != null && contact.organizations.Count() > 0)
+ {
+ contactTask.Company = contact.organizations[0].name;
+ contactTask.JobTitle = contact.organizations[0].title;
+ }
+ #endregion
+
+ #region contact.phoneNumbers
+ if (contact.phoneNumbers != null && contact.phoneNumbers.Length > 0)
+ {
+ foreach (JSONContactField field in contact.phoneNumbers)
+ {
+ string fieldType = field.type.ToLower();
+ if (fieldType == "work")
+ {
+ contactTask.WorkPhone = field.value;
+ }
+ else if (fieldType == "home")
+ {
+ contactTask.HomePhone = field.value;
+ }
+ else if (fieldType == "mobile")
+ {
+ contactTask.MobilePhone = field.value;
+ }
+ }
+ }
+ #endregion
+
+ #region contact.emails
+
+ if (contact.emails != null && contact.emails.Length > 0)
+ {
+
+ // set up different email types if they are not explicitly defined
+ foreach (string type in new string[] { "personal", "work", "other" })
+ {
+ foreach (JSONContactField field in contact.emails)
+ {
+ if (field != null && String.IsNullOrEmpty(field.type))
+ {
+ field.type = type;
+ break;
+ }
+ }
+ }
+
+ foreach (JSONContactField field in contact.emails)
+ {
+ if (field != null)
+ {
+ if (field.type != null && field.type != "other")
+ {
+ string fieldType = field.type.ToLower();
+ if (fieldType == "work")
+ {
+ contactTask.WorkEmail = field.value;
+ }
+ else if (fieldType == "home" || fieldType == "personal")
+ {
+ contactTask.PersonalEmail = field.value;
+ }
+ }
+ else
+ {
+ contactTask.OtherEmail = field.value;
+ }
+ }
+
+ }
+ }
+ #endregion
+
+ if (contact.note != null && contact.note.Length > 0)
+ {
+ contactTask.Notes = contact.note;
+ }
+
+ #region contact.addresses
+ if (contact.addresses != null && contact.addresses.Length > 0)
+ {
+ foreach (JSONContactAddress address in contact.addresses)
+ {
+ if (address.type == null)
+ {
+ address.type = "home"; // set a default
+ }
+ string fieldType = address.type.ToLower();
+ if (fieldType == "work")
+ {
+ contactTask.WorkAddressCity = address.locality;
+ contactTask.WorkAddressCountry = address.country;
+ contactTask.WorkAddressState = address.region;
+ contactTask.WorkAddressStreet = address.streetAddress;
+ contactTask.WorkAddressZipCode = address.postalCode;
+ }
+ else if (fieldType == "home" || fieldType == "personal")
+ {
+ contactTask.HomeAddressCity = address.locality;
+ contactTask.HomeAddressCountry = address.country;
+ contactTask.HomeAddressState = address.region;
+ contactTask.HomeAddressStreet = address.streetAddress;
+ contactTask.HomeAddressZipCode = address.postalCode;
+ }
+ else
+ {
+ // no other address fields available ...
+ Debug.WriteLine("Creating contact with unsupported address type :: " + address.type);
+ }
+ }
+ }
+ #endregion
+
+
+ contactTask.Completed += new EventHandler<SaveContactResult>(ContactSaveTaskCompleted);
+ contactTask.Show();
+ }
+
+ void ContactSaveTaskCompleted(object sender, SaveContactResult e)
+ {
+ SaveContactTask task = sender as SaveContactTask;
+
+ if (e.TaskResult == TaskResult.OK)
+ {
+
+ Deployment.Current.Dispatcher.BeginInvoke(() =>
+ {
+ DeviceContacts deviceContacts = new DeviceContacts();
+ deviceContacts.SearchCompleted += new EventHandler<ContactsSearchEventArgs>(postAdd_SearchCompleted);
+
+ string displayName = String.Format("{0}{2}{1}", task.FirstName, task.LastName, String.IsNullOrEmpty(task.FirstName) ? "" : " ");
+
+ deviceContacts.SearchAsync(displayName, FilterKind.DisplayName, task);
+ });
+
+
+ }
+ else if (e.TaskResult == TaskResult.Cancel)
+ {
+ DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Operation cancelled."));
+ }
+ }
+
+ void postAdd_SearchCompleted(object sender, ContactsSearchEventArgs e)
+ {
+ if (e.Results.Count() > 0)
+ {
+ List<Contact> foundContacts = new List<Contact>();
+
+ int n = (from Contact contact in e.Results select contact.GetHashCode()).Max();
+ Contact newContact = (from Contact contact in e.Results
+ where contact.GetHashCode() == n
+ select contact).First();
+
+ DispatchCommandResult(new PluginResult(PluginResult.Status.OK, FormatJSONContact(newContact, null)));
+ }
+ else
+ {
+ DispatchCommandResult(new PluginResult(PluginResult.Status.NO_RESULT));
+ }
+ }
+
+
+
+ public void remove(string id)
+ {
+ // note id is wrapped in [] and always has exactly one string ...
+ DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "{\"code\":" + NOT_SUPPORTED_ERROR + "}"));
+ }
+
+ public void search(string searchCriteria)
+ {
+ string[] args = JSON.JsonHelper.Deserialize<string[]>(searchCriteria);
+
+ ContactSearchParams searchParams = new ContactSearchParams();
+ try
+ {
+ searchParams.fields = JSON.JsonHelper.Deserialize<string[]>(args[0]);
+ searchParams.options = JSON.JsonHelper.Deserialize<SearchOptions>(args[1]);
+ }
+ catch (Exception)
+ {
+ DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, INVALID_ARGUMENT_ERROR));
+ return;
+ }
+
+ if (searchParams.options == null)
+ {
+ searchParams.options = new SearchOptions();
+ searchParams.options.filter = "";
+ searchParams.options.multiple = true;
+ }
+
+ DeviceContacts deviceContacts = new DeviceContacts();
+ deviceContacts.SearchCompleted += new EventHandler<ContactsSearchEventArgs>(contacts_SearchCompleted);
+
+ // default is to search all fields
+ FilterKind filterKind = FilterKind.None;
+ // if only one field is specified, we will try the 3 available DeviceContact search filters
+ if (searchParams.fields.Count() == 1)
+ {
+ if (searchParams.fields.Contains("name"))
+ {
+ filterKind = FilterKind.DisplayName;
+ }
+ else if (searchParams.fields.Contains("emails"))
+ {
+ filterKind = FilterKind.EmailAddress;
+ }
+ else if (searchParams.fields.Contains("phoneNumbers"))
+ {
+ filterKind = FilterKind.PhoneNumber;
+ }
+ }
+
+ try
+ {
+
+ deviceContacts.SearchAsync(searchParams.options.filter, filterKind, searchParams);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine("search contacts exception :: " + ex.Message);
+ }
+ }
+
+ private void contacts_SearchCompleted(object sender, ContactsSearchEventArgs e)
+ {
+ ContactSearchParams searchParams = (ContactSearchParams)e.State;
+
+ List<Contact> foundContacts = null;
+
+ // if we have multiple search fields
+ if (searchParams.options.filter.Length > 0 && searchParams.fields.Count() > 1)
+ {
+ foundContacts = new List<Contact>();
+ if (searchParams.fields.Contains("emails"))
+ {
+ foundContacts.AddRange(from Contact con in e.Results
+ from ContactEmailAddress a in con.EmailAddresses
+ where a.EmailAddress.Contains(searchParams.options.filter)
+ select con);
+ }
+ if (searchParams.fields.Contains("displayName"))
+ {
+ foundContacts.AddRange(from Contact con in e.Results
+ where con.DisplayName.Contains(searchParams.options.filter)
+ select con);
+ }
+ if (searchParams.fields.Contains("name"))
+ {
+ foundContacts.AddRange(from Contact con in e.Results
+ where con.CompleteName != null && con.CompleteName.ToString().Contains(searchParams.options.filter)
+ select con);
+ }
+ if (searchParams.fields.Contains("phoneNumbers"))
+ {
+ foundContacts.AddRange(from Contact con in e.Results
+ from ContactPhoneNumber a in con.PhoneNumbers
+ where a.PhoneNumber.Contains(searchParams.options.filter)
+ select con);
+ }
+ if (searchParams.fields.Contains("urls"))
+ {
+ foundContacts.AddRange(from Contact con in e.Results
+ from string a in con.Websites
+ where a.Contains(searchParams.options.filter)
+ select con);
+ }
+ }
+ else
+ {
+ foundContacts = new List<Contact>(e.Results);
+ }
+
+ //List<string> contactList = new List<string>();
+
+ string strResult = "";
+
+ IEnumerable<Contact> distinctContacts = foundContacts.Distinct();
+
+ foreach (Contact contact in distinctContacts)
+ {
+ strResult += FormatJSONContact(contact, null) + ",";
+ //contactList.Add(FormatJSONContact(contact, null));
+ if (!searchParams.options.multiple)
+ {
+ break; // just return the first item
+ }
+ }
+ PluginResult result = new PluginResult(PluginResult.Status.OK);
+ result.Message = "[" + strResult.TrimEnd(',') + "]";
+ DispatchCommandResult(result);
+
+ }
+
+ private string FormatJSONPhoneNumbers(Contact con)
+ {
+ string retVal = "";
+ string contactFieldFormat = "\"type\":\"{0}\",\"value\":\"{1}\",\"pref\":\"false\"";
+ foreach (ContactPhoneNumber number in con.PhoneNumbers)
+ {
+
+ string contactField = string.Format(contactFieldFormat,
+ number.Kind.ToString(),
+ number.PhoneNumber);
+
+ retVal += "{" + contactField + "},";
+ }
+ return retVal.TrimEnd(',');
+ }
+
+ private string FormatJSONEmails(Contact con)
+ {
+ string retVal = "";
+ string contactFieldFormat = "\"type\":\"{0}\",\"value\":\"{1}\",\"pref\":\"false\"";
+ foreach (ContactEmailAddress address in con.EmailAddresses)
+ {
+ string contactField = string.Format(contactFieldFormat,
+ address.Kind.ToString(),
+ address.EmailAddress);
+
+ retVal += "{" + contactField + "},";
+ }
+ return retVal.TrimEnd(',');
+ }
+
+ private string getFormattedJSONAddress(ContactAddress address, bool isPrefered)
+ {
+
+ string addressFormatString = "\"pref\":{0}," + // bool
+ "\"type\":\"{1}\"," +
+ "\"formatted\":\"{2}\"," +
+ "\"streetAddress\":\"{3}\"," +
+ "\"locality\":\"{4}\"," +
+ "\"region\":\"{5}\"," +
+ "\"postalCode\":\"{6}\"," +
+ "\"country\":\"{7}\"";
+
+ string formattedAddress = address.PhysicalAddress.AddressLine1 + " "
+ + address.PhysicalAddress.AddressLine2 + " "
+ + address.PhysicalAddress.City + " "
+ + address.PhysicalAddress.StateProvince + " "
+ + address.PhysicalAddress.CountryRegion + " "
+ + address.PhysicalAddress.PostalCode;
+
+ string jsonAddress = string.Format(addressFormatString,
+ isPrefered ? "\"true\"" : "\"false\"",
+ address.Kind.ToString(),
+ formattedAddress,
+ address.PhysicalAddress.AddressLine1 + " " + address.PhysicalAddress.AddressLine2,
+ address.PhysicalAddress.City,
+ address.PhysicalAddress.StateProvince,
+ address.PhysicalAddress.PostalCode,
+ address.PhysicalAddress.CountryRegion);
+
+ //Debug.WriteLine("getFormattedJSONAddress returning :: " + jsonAddress);
+
+ return "{" + jsonAddress + "}";
+ }
+
+ private string FormatJSONAddresses(Contact con)
+ {
+ string retVal = "";
+ foreach (ContactAddress address in con.Addresses)
+ {
+ retVal += this.getFormattedJSONAddress(address, false) + ",";
+ }
+
+ //Debug.WriteLine("FormatJSONAddresses returning :: " + retVal);
+ return retVal.TrimEnd(',');
+ }
+
+ private string FormatJSONWebsites(Contact con)
+ {
+ string retVal = "";
+ foreach (string website in con.Websites)
+ {
+ retVal += "\"" + website + "\",";
+ }
+ return retVal.TrimEnd(',');
+ }
+
+ /*
+ * formatted: The complete name of the contact. (DOMString)
+ familyName: The contacts family name. (DOMString)
+ givenName: The contacts given name. (DOMString)
+ middleName: The contacts middle name. (DOMString)
+ honorificPrefix: The contacts prefix (example Mr. or Dr.) (DOMString)
+ honorificSuffix: The contacts suffix (example Esq.). (DOMString)
+ */
+ private string FormatJSONName(Contact con)
+ {
+ string retVal = "";
+ string formatStr = "\"formatted\":\"{0}\"," +
+ "\"familyName\":\"{1}\"," +
+ "\"givenName\":\"{2}\"," +
+ "\"middleName\":\"{3}\"," +
+ "\"honorificPrefix\":\"{4}\"," +
+ "\"honorificSuffix\":\"{5}\"";
+
+ if (con.CompleteName != null)
+ {
+ retVal = string.Format(formatStr,
+ con.CompleteName.FirstName + " " + con.CompleteName.LastName, // TODO: does this need suffix? middlename?
+ con.CompleteName.LastName,
+ con.CompleteName.FirstName,
+ con.CompleteName.MiddleName,
+ con.CompleteName.Title,
+ con.CompleteName.Suffix);
+ }
+ else
+ {
+ retVal = string.Format(formatStr,"","","","","","");
+ }
+
+ return "{" + retVal + "}";
+ }
+
+ private string FormatJSONContact(Contact con, string[] fields)
+ {
+
+ string contactFormatStr = "\"id\":\"{0}\"," +
+ "\"displayName\":\"{1}\"," +
+ "\"nickname\":\"{2}\"," +
+ "\"phoneNumbers\":[{3}]," +
+ "\"emails\":[{4}]," +
+ "\"addresses\":[{5}]," +
+ "\"urls\":[{6}]," +
+ "\"name\":{7}," +
+ "\"note\":\"{8}\"," +
+ "\"birthday\":\"{9}\"";
+
+
+ string jsonContact = String.Format(contactFormatStr,
+ con.GetHashCode(),
+ con.DisplayName,
+ con.CompleteName != null ? con.CompleteName.Nickname : "",
+ FormatJSONPhoneNumbers(con),
+ FormatJSONEmails(con),
+ FormatJSONAddresses(con),
+ FormatJSONWebsites(con),
+ FormatJSONName(con),
+ con.Notes.FirstOrDefault(),
+ con.Birthdays.FirstOrDefault());
+
+ //Debug.WriteLine("jsonContact = " + jsonContact);
+ // JSON requires new line characters be escaped
+ return "{" + jsonContact.Replace("\n", "\\n") + "}";
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/www/Contact.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/www/Contact.js b/spec/plugins/Contacts/www/Contact.js
new file mode 100644
index 0000000..9c46a0c
--- /dev/null
+++ b/spec/plugins/Contacts/www/Contact.js
@@ -0,0 +1,177 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+*/
+
+var argscheck = require('cordova/argscheck'),
+ exec = require('cordova/exec'),
+ ContactError = require('./ContactError'),
+ utils = require('cordova/utils');
+
+/**
+* Converts primitives into Complex Object
+* Currently only used for Date fields
+*/
+function convertIn(contact) {
+ var value = contact.birthday;
+ try {
+ contact.birthday = new Date(parseFloat(value));
+ } catch (exception){
+ console.log("Cordova Contact convertIn error: exception creating date.");
+ }
+ return contact;
+}
+
+/**
+* Converts Complex objects into primitives
+* Only conversion at present is for Dates.
+**/
+
+function convertOut(contact) {
+ var value = contact.birthday;
+ if (value !== null) {
+ // try to make it a Date object if it is not already
+ if (!utils.isDate(value)){
+ try {
+ value = new Date(value);
+ } catch(exception){
+ value = null;
+ }
+ }
+ if (utils.isDate(value)){
+ value = value.valueOf(); // convert to milliseconds
+ }
+ contact.birthday = value;
+ }
+ return contact;
+}
+
+/**
+* Contains information about a single contact.
+* @constructor
+* @param {DOMString} id unique identifier
+* @param {DOMString} displayName
+* @param {ContactName} name
+* @param {DOMString} nickname
+* @param {Array.<ContactField>} phoneNumbers array of phone numbers
+* @param {Array.<ContactField>} emails array of email addresses
+* @param {Array.<ContactAddress>} addresses array of addresses
+* @param {Array.<ContactField>} ims instant messaging user ids
+* @param {Array.<ContactOrganization>} organizations
+* @param {DOMString} birthday contact's birthday
+* @param {DOMString} note user notes about contact
+* @param {Array.<ContactField>} photos
+* @param {Array.<ContactField>} categories
+* @param {Array.<ContactField>} urls contact's web sites
+*/
+var Contact = function (id, displayName, name, nickname, phoneNumbers, emails, addresses,
+ ims, organizations, birthday, note, photos, categories, urls) {
+ this.id = id || null;
+ this.rawId = null;
+ this.displayName = displayName || null;
+ this.name = name || null; // ContactName
+ this.nickname = nickname || null;
+ this.phoneNumbers = phoneNumbers || null; // ContactField[]
+ this.emails = emails || null; // ContactField[]
+ this.addresses = addresses || null; // ContactAddress[]
+ this.ims = ims || null; // ContactField[]
+ this.organizations = organizations || null; // ContactOrganization[]
+ this.birthday = birthday || null;
+ this.note = note || null;
+ this.photos = photos || null; // ContactField[]
+ this.categories = categories || null; // ContactField[]
+ this.urls = urls || null; // ContactField[]
+};
+
+/**
+* Removes contact from device storage.
+* @param successCB success callback
+* @param errorCB error callback
+*/
+Contact.prototype.remove = function(successCB, errorCB) {
+ argscheck.checkArgs('FF', 'Contact.remove', arguments);
+ var fail = errorCB && function(code) {
+ errorCB(new ContactError(code));
+ };
+ if (this.id === null) {
+ fail(ContactError.UNKNOWN_ERROR);
+ }
+ else {
+ exec(successCB, fail, "Contacts", "remove", [this.id]);
+ }
+};
+
+/**
+* Creates a deep copy of this Contact.
+* With the contact ID set to null.
+* @return copy of this Contact
+*/
+Contact.prototype.clone = function() {
+ var clonedContact = utils.clone(this);
+ clonedContact.id = null;
+ clonedContact.rawId = null;
+
+ function nullIds(arr) {
+ if (arr) {
+ for (var i = 0; i < arr.length; ++i) {
+ arr[i].id = null;
+ }
+ }
+ }
+
+ // Loop through and clear out any id's in phones, emails, etc.
+ nullIds(clonedContact.phoneNumbers);
+ nullIds(clonedContact.emails);
+ nullIds(clonedContact.addresses);
+ nullIds(clonedContact.ims);
+ nullIds(clonedContact.organizations);
+ nullIds(clonedContact.categories);
+ nullIds(clonedContact.photos);
+ nullIds(clonedContact.urls);
+ return clonedContact;
+};
+
+/**
+* Persists contact to device storage.
+* @param successCB success callback
+* @param errorCB error callback
+*/
+Contact.prototype.save = function(successCB, errorCB) {
+ argscheck.checkArgs('FFO', 'Contact.save', arguments);
+ var fail = errorCB && function(code) {
+ errorCB(new ContactError(code));
+ };
+ var success = function(result) {
+ if (result) {
+ if (successCB) {
+ var fullContact = require('./contacts').create(result);
+ successCB(convertIn(fullContact));
+ }
+ }
+ else {
+ // no Entry object returned
+ fail(ContactError.UNKNOWN_ERROR);
+ }
+ };
+ var dupContact = convertOut(utils.clone(this));
+ exec(success, fail, "Contacts", "save", [dupContact]);
+};
+
+
+module.exports = Contact;
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/www/ContactAddress.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/www/ContactAddress.js b/spec/plugins/Contacts/www/ContactAddress.js
new file mode 100644
index 0000000..3d39086
--- /dev/null
+++ b/spec/plugins/Contacts/www/ContactAddress.js
@@ -0,0 +1,46 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+*/
+
+/**
+* Contact address.
+* @constructor
+* @param {DOMString} id unique identifier, should only be set by native code
+* @param formatted // NOTE: not a W3C standard
+* @param streetAddress
+* @param locality
+* @param region
+* @param postalCode
+* @param country
+*/
+
+var ContactAddress = function(pref, type, formatted, streetAddress, locality, region, postalCode, country) {
+ this.id = null;
+ this.pref = (typeof pref != 'undefined' ? pref : false);
+ this.type = type || null;
+ this.formatted = formatted || null;
+ this.streetAddress = streetAddress || null;
+ this.locality = locality || null;
+ this.region = region || null;
+ this.postalCode = postalCode || null;
+ this.country = country || null;
+};
+
+module.exports = ContactAddress;
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/www/ContactError.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/www/ContactError.js b/spec/plugins/Contacts/www/ContactError.js
new file mode 100644
index 0000000..01b229a
--- /dev/null
+++ b/spec/plugins/Contacts/www/ContactError.js
@@ -0,0 +1,42 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+*/
+
+/**
+ * ContactError.
+ * An error code assigned by an implementation when an error has occurred
+ * @constructor
+ */
+var ContactError = function(err) {
+ this.code = (typeof err != 'undefined' ? err : null);
+};
+
+/**
+ * Error codes
+ */
+ContactError.UNKNOWN_ERROR = 0;
+ContactError.INVALID_ARGUMENT_ERROR = 1;
+ContactError.TIMEOUT_ERROR = 2;
+ContactError.PENDING_OPERATION_ERROR = 3;
+ContactError.IO_ERROR = 4;
+ContactError.NOT_SUPPORTED_ERROR = 5;
+ContactError.PERMISSION_DENIED_ERROR = 20;
+
+module.exports = ContactError;
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/www/ContactField.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/www/ContactField.js b/spec/plugins/Contacts/www/ContactField.js
new file mode 100644
index 0000000..e84107a
--- /dev/null
+++ b/spec/plugins/Contacts/www/ContactField.js
@@ -0,0 +1,37 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+*/
+
+/**
+* Generic contact field.
+* @constructor
+* @param {DOMString} id unique identifier, should only be set by native code // NOTE: not a W3C standard
+* @param type
+* @param value
+* @param pref
+*/
+var ContactField = function(type, value, pref) {
+ this.id = null;
+ this.type = (type && type.toString()) || null;
+ this.value = (value && value.toString()) || null;
+ this.pref = (typeof pref != 'undefined' ? pref : false);
+};
+
+module.exports = ContactField;
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/www/ContactFindOptions.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/www/ContactFindOptions.js b/spec/plugins/Contacts/www/ContactFindOptions.js
new file mode 100644
index 0000000..bd8bf35
--- /dev/null
+++ b/spec/plugins/Contacts/www/ContactFindOptions.js
@@ -0,0 +1,34 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+*/
+
+/**
+ * ContactFindOptions.
+ * @constructor
+ * @param filter used to match contacts against
+ * @param multiple boolean used to determine if more than one contact should be returned
+ */
+
+var ContactFindOptions = function(filter, multiple) {
+ this.filter = filter || '';
+ this.multiple = (typeof multiple != 'undefined' ? multiple : false);
+};
+
+module.exports = ContactFindOptions;
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/www/ContactName.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/www/ContactName.js b/spec/plugins/Contacts/www/ContactName.js
new file mode 100644
index 0000000..15cf60b
--- /dev/null
+++ b/spec/plugins/Contacts/www/ContactName.js
@@ -0,0 +1,41 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+*/
+
+/**
+* Contact name.
+* @constructor
+* @param formatted // NOTE: not part of W3C standard
+* @param familyName
+* @param givenName
+* @param middle
+* @param prefix
+* @param suffix
+*/
+var ContactName = function(formatted, familyName, givenName, middle, prefix, suffix) {
+ this.formatted = formatted || null;
+ this.familyName = familyName || null;
+ this.givenName = givenName || null;
+ this.middleName = middle || null;
+ this.honorificPrefix = prefix || null;
+ this.honorificSuffix = suffix || null;
+};
+
+module.exports = ContactName;
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/www/ContactOrganization.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/www/ContactOrganization.js b/spec/plugins/Contacts/www/ContactOrganization.js
new file mode 100644
index 0000000..5dd242b
--- /dev/null
+++ b/spec/plugins/Contacts/www/ContactOrganization.js
@@ -0,0 +1,44 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+*/
+
+/**
+* Contact organization.
+* @constructor
+* @param {DOMString} id unique identifier, should only be set by native code // NOTE: not a W3C standard
+* @param name
+* @param dept
+* @param title
+* @param startDate
+* @param endDate
+* @param location
+* @param desc
+*/
+
+var ContactOrganization = function(pref, type, name, dept, title) {
+ this.id = null;
+ this.pref = (typeof pref != 'undefined' ? pref : false);
+ this.type = type || null;
+ this.name = name || null;
+ this.department = dept || null;
+ this.title = title || null;
+};
+
+module.exports = ContactOrganization;
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/www/contacts.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/www/contacts.js b/spec/plugins/Contacts/www/contacts.js
new file mode 100644
index 0000000..5e6b4db
--- /dev/null
+++ b/spec/plugins/Contacts/www/contacts.js
@@ -0,0 +1,76 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+*/
+
+var argscheck = require('cordova/argscheck'),
+ exec = require('cordova/exec'),
+ ContactError = require('./ContactError'),
+ utils = require('cordova/utils'),
+ Contact = require('./Contact');
+
+/**
+* Represents a group of Contacts.
+* @constructor
+*/
+var contacts = {
+ /**
+ * Returns an array of Contacts matching the search criteria.
+ * @param fields that should be searched
+ * @param successCB success callback
+ * @param errorCB error callback
+ * @param {ContactFindOptions} options that can be applied to contact searching
+ * @return array of Contacts matching search criteria
+ */
+ find:function(fields, successCB, errorCB, options) {
+ argscheck.checkArgs('afFO', 'contacts.find', arguments);
+ if (!fields.length) {
+ errorCB && errorCB(new ContactError(ContactError.INVALID_ARGUMENT_ERROR));
+ } else {
+ var win = function(result) {
+ var cs = [];
+ for (var i = 0, l = result.length; i < l; i++) {
+ cs.push(contacts.create(result[i]));
+ }
+ successCB(cs);
+ };
+ exec(win, errorCB, "Contacts", "search", [fields, options]);
+ }
+ },
+
+ /**
+ * This function creates a new contact, but it does not persist the contact
+ * to device storage. To persist the contact to device storage, invoke
+ * contact.save().
+ * @param properties an object whose properties will be examined to create a new Contact
+ * @returns new Contact object
+ */
+ create:function(properties) {
+ argscheck.checkArgs('O', 'contacts.create', arguments);
+ var contact = new Contact();
+ for (var i in properties) {
+ if (typeof contact[i] !== 'undefined' && properties.hasOwnProperty(i)) {
+ contact[i] = properties[i];
+ }
+ }
+ return contact;
+ }
+};
+
+module.exports = contacts;
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/www/ios/Contact.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/www/ios/Contact.js b/spec/plugins/Contacts/www/ios/Contact.js
new file mode 100644
index 0000000..b40c41a
--- /dev/null
+++ b/spec/plugins/Contacts/www/ios/Contact.js
@@ -0,0 +1,51 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+*/
+
+var exec = require('cordova/exec'),
+ ContactError = require('./ContactError');
+
+/**
+ * Provides iOS Contact.display API.
+ */
+module.exports = {
+ display : function(errorCB, options) {
+ /*
+ * Display a contact using the iOS Contact Picker UI
+ * NOT part of W3C spec so no official documentation
+ *
+ * @param errorCB error callback
+ * @param options object
+ * allowsEditing: boolean AS STRING
+ * "true" to allow editing the contact
+ * "false" (default) display contact
+ */
+
+ if (this.id === null) {
+ if (typeof errorCB === "function") {
+ var errorObj = new ContactError(ContactError.UNKNOWN_ERROR);
+ errorCB(errorObj);
+ }
+ }
+ else {
+ exec(null, errorCB, "Contacts","displayContact", [this.id, options]);
+ }
+ }
+};
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/Contacts/www/ios/contacts.js
----------------------------------------------------------------------
diff --git a/spec/plugins/Contacts/www/ios/contacts.js b/spec/plugins/Contacts/www/ios/contacts.js
new file mode 100644
index 0000000..67cf421
--- /dev/null
+++ b/spec/plugins/Contacts/www/ios/contacts.js
@@ -0,0 +1,62 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+*/
+
+var exec = require('cordova/exec');
+
+/**
+ * Provides iOS enhanced contacts API.
+ */
+module.exports = {
+ newContactUI : function(successCallback) {
+ /*
+ * Create a contact using the iOS Contact Picker UI
+ * NOT part of W3C spec so no official documentation
+ *
+ * returns: the id of the created contact as param to successCallback
+ */
+ exec(successCallback, null, "Contacts","newContact", []);
+ },
+ chooseContact : function(successCallback, options) {
+ /*
+ * Select a contact using the iOS Contact Picker UI
+ * NOT part of W3C spec so no official documentation
+ *
+ * @param errorCB error callback
+ * @param options object
+ * allowsEditing: boolean AS STRING
+ * "true" to allow editing the contact
+ * "false" (default) display contact
+ * fields: array of fields to return in contact object (see ContactOptions.fields)
+ *
+ * @returns
+ * id of contact selected
+ * ContactObject
+ * if no fields provided contact contains just id information
+ * if fields provided contact object contains information for the specified fields
+ *
+ */
+ var win = function(result) {
+ var fullContact = require('./contacts').create(result);
+ successCallback(fullContact.id, fullContact);
+ };
+ exec(win, null, "Contacts","chooseContact", [options]);
+ }
+};
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/dependencies/B/plugin.xml
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/B/plugin.xml b/spec/plugins/dependencies/B/plugin.xml
index 4258857..16847a5 100644
--- a/spec/plugins/dependencies/B/plugin.xml
+++ b/spec/plugins/dependencies/B/plugin.xml
@@ -25,15 +25,15 @@
<name>Plugin B</name>
- <dependency id="D" />
- <dependency id="E" />
+ <dependency id="D" url="." subdir="spec/plugins/dependencies/D"/>
+ <dependency id="E" url="." subdir="spec/plugins/dependencies/subdir/E"/>
<asset src="www/plugin-b.js" target="plugin-b.js" />
<config-file target="config.xml" parent="/*">
<access origin="build.phonegap.com" />
</config-file>
-
+
<!-- android -->
<platform name="android">
<config-file target="res/xml/config.xml" parent="plugins">
@@ -45,7 +45,7 @@
target-dir="src/com/phonegap/B" />
</platform>
-
+
<!-- ios -->
<platform name="ios">
<!-- CDV 2.5+ -->
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/dependencies/E/plugin.xml
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/E/plugin.xml b/spec/plugins/dependencies/E/plugin.xml
deleted file mode 100644
index c7a2098..0000000
--- a/spec/plugins/dependencies/E/plugin.xml
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-
- Copyright 2013 Anis Kadri
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing,
- software distributed under the License is distributed on an
- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- KIND, either express or implied. See the License for the
- specific language governing permissions and limitations
- under the License.
-
--->
-
-<plugin xmlns="http://cordova.apache.org/ns/plugins/1.0"
- xmlns:android="http://schemas.android.com/apk/res/android"
- id="E"
- version="0.6.0">
-
- <name>Plugin E</name>
-
- <asset src="www/plugin-e.js" target="plugin-e.js" />
-
- <config-file target="config.xml" parent="/*">
- <access origin="build.phonegap.com" />
- </config-file>
-
- <!-- android -->
- <platform name="android">
- <config-file target="res/xml/config.xml" parent="plugins">
- <plugin name="E"
- value="com.phonegap.E.E"/>
- </config-file>
-
- <source-file src="src/android/E.java"
- target-dir="src/com/phonegap/E" />
- </platform>
-
-
- <!-- ios -->
- <platform name="ios">
- <!-- CDV 2.5+ -->
- <config-file target="config.xml" parent="plugins">
- <plugin name="E"
- value="EPluginCommand"/>
- </config-file>
-
- <header-file src="src/ios/EPluginCommand.h" />
- <source-file src="src/ios/EPluginCommand.m"/>
- </platform>
-</plugin>
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/dependencies/E/src/android/E.java
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/E/src/android/E.java b/spec/plugins/dependencies/E/src/android/E.java
deleted file mode 100644
index e69de29..0000000
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/dependencies/E/src/ios/EPluginCommand.h
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/E/src/ios/EPluginCommand.h b/spec/plugins/dependencies/E/src/ios/EPluginCommand.h
deleted file mode 100644
index e69de29..0000000
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/dependencies/E/src/ios/EPluginCommand.m
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/E/src/ios/EPluginCommand.m b/spec/plugins/dependencies/E/src/ios/EPluginCommand.m
deleted file mode 100644
index e69de29..0000000
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/dependencies/E/www/plugin-e.js
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/E/www/plugin-e.js b/spec/plugins/dependencies/E/www/plugin-e.js
deleted file mode 100644
index e69de29..0000000
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/dependencies/subdir/E/plugin.xml
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/subdir/E/plugin.xml b/spec/plugins/dependencies/subdir/E/plugin.xml
new file mode 100644
index 0000000..c7a2098
--- /dev/null
+++ b/spec/plugins/dependencies/subdir/E/plugin.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Copyright 2013 Anis Kadri
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+-->
+
+<plugin xmlns="http://cordova.apache.org/ns/plugins/1.0"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ id="E"
+ version="0.6.0">
+
+ <name>Plugin E</name>
+
+ <asset src="www/plugin-e.js" target="plugin-e.js" />
+
+ <config-file target="config.xml" parent="/*">
+ <access origin="build.phonegap.com" />
+ </config-file>
+
+ <!-- android -->
+ <platform name="android">
+ <config-file target="res/xml/config.xml" parent="plugins">
+ <plugin name="E"
+ value="com.phonegap.E.E"/>
+ </config-file>
+
+ <source-file src="src/android/E.java"
+ target-dir="src/com/phonegap/E" />
+ </platform>
+
+
+ <!-- ios -->
+ <platform name="ios">
+ <!-- CDV 2.5+ -->
+ <config-file target="config.xml" parent="plugins">
+ <plugin name="E"
+ value="EPluginCommand"/>
+ </config-file>
+
+ <header-file src="src/ios/EPluginCommand.h" />
+ <source-file src="src/ios/EPluginCommand.m"/>
+ </platform>
+</plugin>
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/dependencies/subdir/E/src/android/E.java
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/subdir/E/src/android/E.java b/spec/plugins/dependencies/subdir/E/src/android/E.java
new file mode 100644
index 0000000..e69de29
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/dependencies/subdir/E/src/ios/EPluginCommand.h
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/subdir/E/src/ios/EPluginCommand.h b/spec/plugins/dependencies/subdir/E/src/ios/EPluginCommand.h
new file mode 100644
index 0000000..e69de29
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/dependencies/subdir/E/src/ios/EPluginCommand.m
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/subdir/E/src/ios/EPluginCommand.m b/spec/plugins/dependencies/subdir/E/src/ios/EPluginCommand.m
new file mode 100644
index 0000000..e69de29
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/plugins/dependencies/subdir/E/www/plugin-e.js
----------------------------------------------------------------------
diff --git a/spec/plugins/dependencies/subdir/E/www/plugin-e.js b/spec/plugins/dependencies/subdir/E/www/plugin-e.js
new file mode 100644
index 0000000..e69de29
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/projects/blackberry10/native/device/chrome/.gitkeep
----------------------------------------------------------------------
diff --git a/spec/projects/blackberry10/native/device/chrome/.gitkeep b/spec/projects/blackberry10/native/device/chrome/.gitkeep
new file mode 100644
index 0000000..e69de29
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/projects/blackberry10/native/device/plugins/jnext/auth.txt
----------------------------------------------------------------------
diff --git a/spec/projects/blackberry10/native/device/plugins/jnext/auth.txt b/spec/projects/blackberry10/native/device/plugins/jnext/auth.txt
new file mode 100644
index 0000000..0983f4f
--- /dev/null
+++ b/spec/projects/blackberry10/native/device/plugins/jnext/auth.txt
@@ -0,0 +1,3 @@
+local:/// *
+file:// *
+http:// *
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/projects/blackberry10/native/simulator/chrome/.gitkeep
----------------------------------------------------------------------
diff --git a/spec/projects/blackberry10/native/simulator/chrome/.gitkeep b/spec/projects/blackberry10/native/simulator/chrome/.gitkeep
new file mode 100644
index 0000000..e69de29
http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/21b6d79b/spec/projects/blackberry10/native/simulator/plugins/jnext/auth.txt
----------------------------------------------------------------------
diff --git a/spec/projects/blackberry10/native/simulator/plugins/jnext/auth.txt b/spec/projects/blackberry10/native/simulator/plugins/jnext/auth.txt
new file mode 100644
index 0000000..0983f4f
--- /dev/null
+++ b/spec/projects/blackberry10/native/simulator/plugins/jnext/auth.txt
@@ -0,0 +1,3 @@
+local:/// *
+file:// *
+http:// *
\ No newline at end of file