You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by an...@apache.org on 2013/07/25 00:39:47 UTC

[07/18] [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

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