WKContactPicker.mm   [plain text]

/*
* Copyright (C) 2020 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/

#include "config.h"
#include "WKContactPicker.h"

#if HAVE(CONTACTSUI)

#import "ContactsUISPI.h"
#import <Contacts/Contacts.h>
#import <WebCore/ContactInfo.h>
#import <WebCore/ContactsRequestData.h>
#import <wtf/RetainPtr.h>
#import <wtf/SoftLinking.h>

#if PLATFORM(IOS_FAMILY)
#import "UIKitSPI.h"
#endif

SOFT_LINK_FRAMEWORK(Contacts)
SOFT_LINK_CLASS(Contacts, CNContactFormatter)
SOFT_LINK_CLASS(Contacts, CNLabeledValue)
SOFT_LINK_CLASS(Contacts, CNMutableContact)
SOFT_LINK_CLASS(Contacts, CNPhoneNumber)

#if HAVE(CNCONTACTPICKERVIEWCONTROLLER)
SOFT_LINK_FRAMEWORK(ContactsUI)
SOFT_LINK_CLASS(ContactsUI, CNContactPickerViewController)
#endif

#pragma mark - Delegate Wrappers

@interface WKCNContactPickerDelegate : NSObject<CNContactPickerDelegate> {
@protected
WeakObjCPtr<id<CNContactPickerDelegate>> _contactPickerDelegate;
}

- (instancetype)initWithContactPickerDelegate:(id<CNContactPickerDelegate>)contactPickerDelegate;

@end

@implementation WKCNContactPickerDelegate

- (instancetype)initWithContactPickerDelegate:(id<CNContactPickerDelegate>)contactPickerDelegate
{
if (!(self = [super init]))
return nil;

_contactPickerDelegate = contactPickerDelegate;

return self;
}

#if HAVE(CNCONTACTPICKERVIEWCONTROLLER)
- (void)contactPickerDidCancel:(CNContactPickerViewController *)picker
{
[_contactPickerDelegate contactPickerDidCancel:picker];
}
#endif

@end

@interface WKCNContactPickerSingleSelectDelegate : WKCNContactPickerDelegate
@end

@implementation WKCNContactPickerSingleSelectDelegate

#if HAVE(CNCONTACTPICKERVIEWCONTROLLER)
- (void)contactPicker:(CNContactPickerViewController *)picker didSelectContact:(CNContact *)contact
{
[_contactPickerDelegate contactPicker:picker didSelectContact:contact];
}
#endif

@end

@interface WKCNContactPickerMultiSelectDelegate : WKCNContactPickerDelegate
@end

@implementation WKCNContactPickerMultiSelectDelegate

#if HAVE(CNCONTACTPICKERVIEWCONTROLLER)
- (void)contactPicker:(CNContactPickerViewController *)picker didSelectContacts:(NSArray<CNContact*> *)contacts
{
[_contactPickerDelegate contactPicker:picker didSelectContacts:contacts];
}
#endif

@end

#pragma mark - WKContactPicker

@interface WKContactPicker () <CNContactPickerDelegate>
@end

@implementation WKContactPicker {
WeakObjCPtr<WKWebView> _webView;
WeakObjCPtr<id<WKContactPickerDelegate>> _delegate;

Vector<WebCore::ContactProperty> _properties;
WTF::CompletionHandler<void(Optional<Vector<WebCore::ContactInfo>>&&)> _completionHandler;

RetainPtr<WKCNContactPickerDelegate> _contactPickerDelegate;
#if HAVE(CNCONTACTPICKERVIEWCONTROLLER)
RetainPtr<CNContactPickerViewController> _contactPickerViewController;
#endif
}

- (id<WKContactPickerDelegate>)delegate
{
return _delegate.get().get();
}

- (void)setDelegate:(id<WKContactPickerDelegate>)delegate
{
_delegate = delegate;
}

- (instancetype)initWithView:(WKWebView *)view
{
if (!(self = [super init]))
return nil;

_webView = view;

return self;
}

- (void)presentWithRequestData:(const WebCore::ContactsRequestData&)requestData completionHandler:(WTF::CompletionHandler<void(Optional<Vector<WebCore::ContactInfo>>&&)>&&)completionHandler
{
_properties = requestData.properties;
_completionHandler = WTFMove(completionHandler);

if (requestData.multiple)
_contactPickerDelegate = adoptNS([[WKCNContactPickerMultiSelectDelegate alloc] initWithContactPickerDelegate:self]);
else
_contactPickerDelegate = adoptNS([[WKCNContactPickerSingleSelectDelegate alloc] initWithContactPickerDelegate:self]);

#if HAVE(CNCONTACTPICKERVIEWCONTROLLER)
_contactPickerViewController = adoptNS([allocCNContactPickerViewControllerInstance() init]);
[_contactPickerViewController setDelegate:_contactPickerDelegate.get()];
[_contactPickerViewController setPrompt:requestData.url];

UIViewController *presentationViewController = [UIViewController _viewControllerForFullScreenPresentationFromView:_webView.get().get()];
[presentationViewController presentViewController:_contactPickerViewController.get() animated:YES completion:[weakSelf = WeakObjCPtr<WKContactPicker>(self)] {
auto strongSelf = weakSelf.get();
if (!strongSelf)
return;

if ([[strongSelf delegate] respondsToSelector:@selector(contactPickerDidPresent:)])
[[strongSelf delegate] contactPickerDidPresent:strongSelf.get()];
}];
#endif
}

#pragma mark - Completion

#if HAVE(CNCONTACTPICKERVIEWCONTROLLER)

- (void)contactPickerDidCancel:(CNContactPickerViewController *)picker
{
Vector<WebCore::ContactInfo> info;
[self _contactPickerDidDismissWithContactInfo:WTFMove(info)];
}

- (void)contactPicker:(CNContactPickerViewController *)picker didSelectContact:(CNContact *)contact
{
Vector<WebCore::ContactInfo> info = { [self _contactInfoFromCNContact:contact] };
[self _contactPickerDidDismissWithContactInfo:WTFMove(info)];
}

- (void)contactPicker:(CNContactPickerViewController *)picker didSelectContacts:(NSArray<CNContact*> *)contacts
{
Vector<WebCore::ContactInfo> info;
info.reserveInitialCapacity(contacts.count);
for (CNContact *contact in contacts)
info.uncheckedAppend([self _contactInfoFromCNContact:contact]);
[self _contactPickerDidDismissWithContactInfo:WTFMove(info)];
}

#endif

- (void)_contactPickerDidDismissWithContactInfo:(Vector<WebCore::ContactInfo>&&)info
{
_completionHandler(WTFMove(info));

if ([_delegate respondsToSelector:@selector(contactPickerDidDismiss:)])
[_delegate contactPickerDidDismiss:self];
}

- (WebCore::ContactInfo)_contactInfoFromCNContact:(CNContact *)contact
{
WebCore::ContactInfo contactInfo;

if (_properties.contains(WebCore::ContactProperty::Name)) {
NSString *contactName = [getCNContactFormatterClass() stringFromContact:contact style:CNContactFormatterStyleFullName];
contactInfo.name = { contactName };
}

if (_properties.contains(WebCore::ContactProperty::Email)) {
for (CNLabeledValue<NSString *> *emailAddress in contact.emailAddresses)
contactInfo.email.append(emailAddress.value);
}

if (_properties.contains(WebCore::ContactProperty::Tel)) {
for (CNLabeledValue<CNPhoneNumber *> *phoneNumber in contact.phoneNumbers)
contactInfo.tel.append(phoneNumber.value.stringValue);
}

return contactInfo;
}

#pragma mark - Testing

- (void)dismissWithContacts:(NSArray *)contacts
{
#if HAVE(CNCONTACTPICKERVIEWCONTROLLER)
[_contactPickerViewController dismissViewControllerAnimated:NO completion:[self, weakSelf = WeakObjCPtr<WKContactPicker>(self), jsContacts = RetainPtr<NSArray>(contacts)] {
auto strongSelf = weakSelf.get();
if (!strongSelf)
return;

[strongSelf contactPicker:_contactPickerViewController.get() didSelectContacts:[strongSelf _contactsFromJSContacts:jsContacts.get()]];
}];
#endif
}

- (NSArray<CNContact*> *)_contactsFromJSContacts:(NSArray *)jsContacts
{
if (!jsContacts)
return [NSArray array];

NSMutableArray<CNContact*> *contacts = [NSMutableArray arrayWithCapacity:jsContacts.count];

NSPredicate *stringValuePredicate = [NSPredicate predicateWithFormat:@"self isKindOfClass: %@", [NSString class]];

for (id jsContact in jsContacts) {
if (![jsContact isKindOfClass:[NSDictionary class]])
continue;

CNMutableContact *contact = [[allocCNMutableContactInstance() init] autorelease];

id names = [(NSDictionary *)jsContact objectForKey:@"name"];
if ([names isKindOfClass:[NSArray class]]) {
for (NSString *name in [names filteredArrayUsingPredicate:stringValuePredicate]) {
contact.givenName = name;
break;
}
}

id emails = [(NSDictionary *)jsContact objectForKey:@"email"];
if ([emails isKindOfClass:[NSArray class]]) {
NSMutableArray<CNLabeledValue<NSString*>*> *emailAddresses = [NSMutableArray array];
for (NSString *email in [emails filteredArrayUsingPredicate:stringValuePredicate]) {
CNLabeledValue<NSString*> *labeledValue = [getCNLabeledValueClass() labeledValueWithLabel:nil value:email];
[emailAddresses addObject:labeledValue];
}
contact.emailAddresses = emailAddresses;
}

id phoneNumbers = [(NSDictionary *)jsContact objectForKey:@"tel"];
if ([phoneNumbers isKindOfClass:[NSArray class]]) {
NSMutableArray<CNLabeledValue<CNPhoneNumber*>*> *numbers = [NSMutableArray array];
for (NSString *phoneNumber in [phoneNumbers filteredArrayUsingPredicate:stringValuePredicate]) {
CNPhoneNumber *cnPhoneNumber = [getCNPhoneNumberClass() phoneNumberWithStringValue:phoneNumber];
CNLabeledValue<CNPhoneNumber*> *labeledValue = [getCNLabeledValueClass() labeledValueWithLabel:nil value:cnPhoneNumber];
[numbers addObject:labeledValue];
}
contact.phoneNumbers = numbers;
}

[contacts addObject:contact];
}

return contacts;
}

@end

#endif // HAVE(CONTACTSUI)