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