Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  ContactStore.m   Sprache: Unknown

 

Spracherkennung für: .m vermutete Sprache: Abap {ABAP[36] Lisp[34] Masm[34]} [Methode: maximale Elemente, drei Dimensionen]

#import <AddressBook/AddressBook.h>
#import <Contacts/Contacts.h>
#import "ContactStore.h"
#import "NSString+Hex.h"
#import "ServerAPIConnector.h"
#import "ServerConnector.h"
#import "MyIdentityStore.h"
#import "ProtocolDefines.h"
#import "ThreemaFramework/ThreemaFramework-Swift.h"
#import "ThreemaError.h"
#import "AppGroup.h"

#import "IdentityInfoFetcher.h"
#import "CryptoUtils.h"
#import "LicenseStore.h"

#define MIN_CHECK_INTERVAL 5*60

#ifdef DEBUG
static const DDLogLevel ddLogLevel = DDLogLevelAll;
#else
static const DDLogLevel ddLogLevel = DDLogLevelNotice;
#endif

static const uint8_t emailHashKey[] = {0x30,0xa5,0x50,0x0f,0xed,0x97,0x01,0xfa,0x6d,0xef,0xdb,0x61,0x08,0x41,0x90,0x0f,0xeb,0xb8,0xe4,0x30,0x88,0x1f,0x7a,0xd8,0x16,0x82,0x62,0x64,0xec,0x09,0xba,0xd7};
static const uint8_t mobileNoHashKey[] = {0x85,0xad,0xf8,0x22,0x69,0x53,0xf3,0xd9,0x6c,0xfd,0x5d,0x09,0xbf,0x29,0x55,0x5e,0xb9,0x55,0xfc,0xd8,0xaa,0x5e,0xc4,0xf9,0xfc,0xd8,0x69,0xe2,0x58,0x37,0x07,0x23};

static const NSTimeInterval minimumSyncInterval = 30;   /* avoid multiple concurrent syncs, e.g. triggered by interval timer + incoming message from unknown user */

@implementation ContactStore {
    NSDate *lastMaxModificationDate;
    NSDate *lastFullSyncDate;
    NSTimer *checkStatusTimer;

    id<UserSettingsProtocol> userSettings;
    EntityManager *entityManager;
    // Workaround to prevent too many active synchronizations at the same time (IOS-4791)
    BOOL addressBookChangeSynchronizationActive;
}

+ (ContactStore*)sharedContactStore {
    static ContactStore *instance;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [ContactStore new];
    });

    return instance;
}

- (instancetype)init
{
    return [self initWithUserSettings:[UserSettings sharedUserSettings] entityManager:[self entityManager]];
}

- (instancetype)initWithUserSettings:(id<UserSettingsProtocol>)userSettingsProtocol entityManager:(NSObject *)entityManagerObject {
    NSAssert([entityManagerObject isKindOfClass:[EntityManager class]], @"Parameter entityManagerObject should be type of EntityManager");

    self = [super init];
    if (self) {
        _syncContactsQueue = dispatch_queue_create("ch.threema.ContactStore.syncContactsQueue", DISPATCH_QUEUE_SERIAL);
        dispatch_set_target_queue(_syncContactsQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));

        /* register a callback to get information about address book changes */
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(addressBookChangeDetected:) name:CNContactStoreDidChangeNotification object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orderChanged:) name:@"ThreemaContactsOrderChanged" object:nil];

        userSettings = userSettingsProtocol;
        entityManager = (EntityManager *)entityManagerObject;

        /* update display/sort order prefs to match system */
        BOOL sortOrder = [[CNContactsUserDefaults sharedDefaults] sortOrder] == CNContactSortOrderGivenName;
        [userSettings setSortOrderFirstName:sortOrder];
        
        addressBookChangeSynchronizationActive = false;
    }
    return self;
}

- (void)dealloc {
    [checkStatusTimer invalidate];
}

- (void)resetEntityManager {
    self->entityManager = [self entityManager];
}

- (void)addressBookChangeDetected:(NSNotification *)notification {
    DDLogNotice(@"Address book change detected");
    
    if (addressBookChangeSynchronizationActive) {
        DDLogNotice(@"Skip address book change synchronization, because one is already running...");
        return;
    }
    
    addressBookChangeSynchronizationActive = true;
    [self synchronizeAddressBookForceFullSync:NO onCompletion:^(BOOL addressBookAccessGranted) {
        [self updateAllContacts];
        [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationAddressbookSyncronized object:self userInfo:nil];
        addressBookChangeSynchronizationActive = false;
    } onError:^(NSError *error) {
        [self updateAllContacts];
        [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationAddressbookSyncronized object:self userInfo:nil];
        addressBookChangeSynchronizationActive = false;
    }];
}

- (NSObject *)contactForIdentity:(NSString *)identity {
    /* check in local DB first */
    ContactEntity *contact = [entityManager.entityFetcher contactEntityFor: identity];
    return contact;
}

- (void)addContactWithIdentity:(NSString *)identity verificationLevel:(int32_t)verificationLevel onCompletion:(nonnull void(^)(NSObject * _Nullable, BOOL))onCompletion onError:(void(^)(NSError *error))onError {

    /* check in local DB first */
    ContactEntity *contact = [entityManager.entityFetcher contactEntityFor:identity];
    if (contact) {
        [entityManager performAndWaitSave:^{
            if (contact.isHidden) {
                MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
                
                contact.isHidden = NO;
                [mediatorSyncableContacts updateAcquaintanceLevelWithIdentity:contact.identity value:[NSNumber numberWithInteger:ContactAcquaintanceLevelDirect]];
                
                // Check if this is a trusted contact (like *THREEMA)
                if ([PredefinedContactsObjC isPredefinedContactWithIdentity:contact.identity publicKey:contact.publicKey]) {
                    contact.contactVerificationLevel = ContactVerificationLevelFullyVerified;
                    [mediatorSyncableContacts updateVerificationLevelWithIdentity:identity value:[NSNumber numberWithInteger: contact.contactVerificationLevel]];
                }

                [mediatorSyncableContacts syncAsync];
            }
        }];

        onCompletion(contact, YES);
        return;
    }
    
    /* not found - request from server */
    ServerAPIConnector *apiConnector = [[ServerAPIConnector alloc] init];
    [apiConnector fetchIdentityInfo:identity onCompletion:^(NSData *publicKey, NSNumber *state, NSNumber *type, NSNumber *featureMask) {
        
        /* save new contact */
        dispatch_async(dispatch_get_main_queue(), ^{
            /* save new contact */
            MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];

            __weak typeof(self) weakSelf = self;

            [self addContactWithIdentity:identity publicKey:publicKey cnContactId:nil verificationLevel:verificationLevel state:state type:type featureMask:featureMask acquaintanceLevel:ContactAcquaintanceLevelDirect alerts:YES contactSyncer:mediatorSyncableContacts onCompletion:^(NSObject *contactObject){
                NSAssert(contactObject == nil || [contactObject isKindOfClass:[ContactEntity class]], @"Parameter senderObject must be type of ContactEntity");

                [mediatorSyncableContacts syncObjcWithCompletionHandler:^(NSError * _Nullable error) {
                    if (error == nil) {
                        /* force synchronisation */
                        [weakSelf synchronizeAddressBookForceFullSync:YES onCompletion:nil onError:nil];
                        WorkDataFetcherObjCBridge* fetcher = [[WorkDataFetcherObjCBridge alloc] initWithEntityManager:entityManager contactStore:self];
                        [fetcher updateWorkDataWithForce:YES completionHandler:^(NSError * _Nullable error) {
                            if (error == nil) {
                                [[NSNotificationCenter defaultCenter] postNotificationName:kSafeBackupTrigger object:nil];
                                onCompletion(contactObject, NO);
                            }
                            else {
                                DDLogError(@"WorkDataFetch failed: %@", [error localizedDescription]);
                                onCompletion(nil, NO);
                            }
                        }];
                    }
                    else {
                        DDLogError(@"Contact multi device sync failed: %@", [error localizedDescription]);
                        onCompletion(nil, NO);
                    }
                }];
            }];
        });
    } onError:^(NSError *error) {
        if (onError != nil) {
            onError(error);
        }
    }];
}

- (void)addContactWithIdentity:(nullable NSString *)identity publicKey:(nullable NSData *)publicKey cnContactId:(nullable NSString *)cnContactId verificationLevel:(int32_t)verificationLevel state:(nullable NSNumber *)state type:(nullable NSNumber *)type featureMask:(nullable NSNumber *)featureMask acquaintanceLevel:(ContactAcquaintanceLevel)acquaintanceLevel alerts:(BOOL)alerts onCompletion:(nonnull void(^)(NSObject * nullable))onCompletion {

    MediatorSyncableContacts *mediatorSyncableContacts = [MediatorSyncableContacts new];
    [self addContactWithIdentity:identity publicKey:publicKey cnContactId:cnContactId verificationLevel:verificationLevel state:state type:type featureMask:featureMask acquaintanceLevel:acquaintanceLevel alerts:alerts contactSyncer:mediatorSyncableContacts onCompletion:^(NSObject * contactObject) {
        NSAssert(contactObject == nil || [contactObject isKindOfClass:[ContactEntity class]], @"Parameter senderObject must be type of ContactEntity");

        [mediatorSyncableContacts syncObjcWithCompletionHandler:^(NSError * _Nullable error) {
            if (error == nil) {
                onCompletion(contactObject);
            }
            else {
                onCompletion(nil);
            }
        }];
    }];
}

/**
 Add or update contact.

 @param identity Identity of the contact (will be not validated)
 @param publicKey Public key it corresponds with the identity (will be not validated)
 @param cnContactId Is not null contact will be linked to this address book contact
 @param verificationLevel Verification level of the identity
 @param state State of the identity, will only be changed if is not null
 @param type Type of the identity, will only be changed if is not null
 @param featureMask Feature mask of the identity
 @param acquaintanceLevel Is `group` contact will be marked as hidden
 @param alerts Is `YES` an notification will be displayed, if public key of already existing contact differs to given public key
 @param mediatorSyncableContacts Contact syncer for multi device
 @param onCompletion With parameter of type `ContactEntity` as result of added/updated contact
 */

- (void)addContactWithIdentity:(nonnull NSString*)identity publicKey:(nonnull NSData*)publicKey cnContactId:(nullable NSString *)cnContactId verificationLevel:(int32_t)verificationLevel state:(nullable NSNumber *)state type:(nullable NSNumber *)type featureMask:(nullable NSNumber *)featureMask acquaintanceLevel:(ContactAcquaintanceLevel)acquaintanceLevel alerts:(BOOL)alerts contactSyncer:(nullable MediatorSyncableContacts *)mediatorSyncableContacts onCompletion:(nonnull void(^)(NSObject * nullable))onCompletion {

    /* Make sure this is not our own identity */
    if ([MyIdentityStore sharedMyIdentityStore].isValidIdentity && [identity isEqualToString:[MyIdentityStore sharedMyIdentityStore].identity]) {
        DDLogInfo(@"Ignoring attempt to add own identity");
        onCompletion(nil);
        return;
    }

    /* Check if we already have a contact with this identity */
    [entityManager performAndWaitSave:^{
        __block BOOL added = NO;

        void (^linkingFinished)(ContactEntity *) = ^(ContactEntity *contact){
            if (added) {
                __block NSString *identity = contact.identity;
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                    [mediatorSyncableContacts updateAllWithIdentity:identity added:added];
                    onCompletion(contact);
                });
                return;
            }
            onCompletion(contact);
        };

        ContactEntity *contact = [entityManager.entityFetcher contactEntityFor: identity];
        if (contact) {
            DDLogInfo(@"Found existing contact with identity %@", identity);
            if (![publicKey isEqualToData:contact.publicKey]) {
                DDLogError(@"Public key doesn't match for existing identity %@!", identity);
                
                if (alerts) {
                    [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationErrorPublicKeyMismatch object:nil userInfo:nil];
                }

                onCompletion(contact);
                return;
            }

            if (contact.isHidden && acquaintanceLevel == ContactAcquaintanceLevelDirect) {
                contact.isHidden = NO;
                [mediatorSyncableContacts updateAcquaintanceLevelWithIdentity:contact.identity value:[NSNumber numberWithInteger:acquaintanceLevel]];
            }
        } else {
            added = YES;

            NSError *createContactError;
            contact = [entityManager getOrCreateContactWithIdentity:identity publicKey:publicKey sortOrderFirstName:userSettings.sortOrderFirstName error:&createContactError];
            if (createContactError) {
                DDLogError(@"New contact could not be created: %@", createContactError);
                onCompletion(nil);
                return;
            }

            [contact setFeatureMaskTo: featureMask.intValue];
            
            if (state != nil) {
                contact.contactState = (ContactState)state.integerValue;
            }
            
            if (type != nil) {
                if ([type isEqualToNumber:@1]) {
                    [self addAsWorkWithIdentities:[[NSOrderedSet alloc] initWithArray:@[contact.identity]] contactSyncer:mediatorSyncableContacts];
                }
            }
            contact.isHidden = acquaintanceLevel == ContactAcquaintanceLevelGroupOrDeleted ? YES : NO;
            [self addProfilePictureRequest:identity];
        }

        // Update verification level
        ContactVerificationLevel currentVerificationLevel = contact.contactVerificationLevel;

        // Check if this is a trusted contact (like *THREEMA) and verification level is not fully verified
        if ([PredefinedContactsObjC isPredefinedContactWithIdentity:identity publicKey:publicKey] && contact.contactVerificationLevel != ContactVerificationLevelFullyVerified) {
            contact.contactVerificationLevel = ContactVerificationLevelFullyVerified;
        }
        else {
            // Check if verification level increases to server or fully verified
            if (contact.contactVerificationLevel < verificationLevel) {
                contact.contactVerificationLevel = verificationLevel;
            }
        }

        [self fetchGatewayProfilePictureFor:identity];

        if (contact.contactVerificationLevel != currentVerificationLevel) {
            [mediatorSyncableContacts updateVerificationLevelWithIdentity:identity value: [NSNumber numberWithInteger: contact.contactVerificationLevel]];
        }

        // Set contact as none work contact
        if (contact.workContact == nil) {
            contact.workContact = @NO;
            [mediatorSyncableContacts updateWorkVerificationLevelWithIdentity:contact.identity value:contact.workContact];
        }
        
        if (cnContactId) {
            if (contact.cnContactId != nil) {
                if (![contact.cnContactId isEqualToString:cnContactId]) {
                    /* contact is already linked to a different CNContactID - check if the name matches;
                     if so, the CNContactID may have changed and we need to re-link */

                    CNContactStore *cnAddressBook = [CNContactStore new];
                    
                    [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
                        if (granted == YES) {
                            NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[cnContactId]];
                            NSError *error;
                            NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
                            if (error) {
                                DDLogError(@"error fetching contacts %@", error);
                            }
                            else {
                                if (cnContacts.count == 1) {
                                    CNContact *foundContact = cnContacts.firstObject;
                                    NSString *firstName = foundContact.givenName;
                                    NSString *lastName = foundContact.familyName;
                                    
                                    if (contact.firstName != nil && contact.firstName.length > 0 && contact.lastName != nil && contact.lastName.length > 0) {
                                        if ([firstName isEqualToString:contact.firstName] && [lastName isEqualToString:contact.lastName]) {
                                            DDLogInfo(@"Address book record ID has changed for %@ %@ (%@ -> %@) - relinking", firstName, lastName, contact.cnContactId, cnContactId);
                                            [self linkContact:contact toCnContactId:cnContactId contactSyncer:mediatorSyncableContacts onCompletion:^{
                                                linkingFinished(contact);
                                            }];
                                            return;
                                        }
                                    }
                                    else if (contact.firstName != nil && contact.firstName.length > 0) {
                                        if ([firstName isEqualToString:contact.firstName]) {
                                            DDLogInfo(@"Address book record ID has changed for %@ %@ (%@ -> %@) - relinking", firstName, lastName, contact.cnContactId, cnContactId);
                                            [self linkContact:contact toCnContactId:cnContactId contactSyncer:mediatorSyncableContacts onCompletion:^{
                                                linkingFinished(contact);
                                            }];
                                            return;
                                        }
                                    }
                                    else if (contact.lastName != nil && contact.lastName.length > 0) {
                                        if ([lastName isEqualToString:contact.lastName]) {
                                            DDLogInfo(@"Address book record ID has changed for %@ %@ (%@ -> %@) - relinking", firstName, lastName, contact.cnContactId, cnContactId);
                                            [self linkContact:contact toCnContactId:cnContactId contactSyncer:mediatorSyncableContacts onCompletion:^{
                                                linkingFinished(contact);
                                            }];
                                            return;
                                        }
                                    }
                                    else {
                                        // No name for the contact to compare, replace the cncontactid
                                        DDLogInfo(@"Address book record ID has changed for %@ %@ (%@ -> %@) - relinking", firstName, lastName, contact.cnContactId, cnContactId);
                                        [self linkContact:contact toCnContactId:cnContactId contactSyncer:mediatorSyncableContacts onCompletion:^{
                                            linkingFinished(contact);
                                        }];
                                        return;
                                    }
                                } // if (cnContacts.count == 1)
                            }
                        } // if (granted == YES)
                        
                        linkingFinished(contact);
                    }];
                    return;
                } // if (![contact.cnContactId isEqualToString:cnContactId])
            } // if (contact.cnContactId != nil)
            else {
                [self linkContact:contact toCnContactId:cnContactId contactSyncer:mediatorSyncableContacts onCompletion:^{
                    linkingFinished(contact);
                }];
                return;
            }
        } // if (cnContactId)

        linkingFinished(contact);
    }];
}

/**
 Add or update all linked address book contacts.

 @param identities: Identities to add and update
 @param emailHashes: Email hashes with contact id of address book
 @param mobileHashes: Mobile hashes with contact id of address book
 @param contactSyncer: Contact syncer for multi device
 @param onCompletion: Completion handler
 */

- (void)addContactsWithIdentities:(NSArray * _Nonnull)identities emailHashes:(NSDictionary * _Nonnull)emailHashToCnContactId mobileNoHashes:(NSDictionary * _Nonnull)mobileNoHashToCnContactId contactSyncer:(nullable MediatorSyncableContacts *)mediatorSyncableContacts onCompletion:(nonnull void(^)(void))onCompletion
{
    NSSet *excludedIds = [NSSet setWithArray:userSettings.syncExclusionList];
    NSMutableArray *allIdentities = [NSMutableArray new];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        dispatch_group_t dispatchGroup = dispatch_group_create();

        for (NSDictionary *identityData in identities) {
            NSString *identity = [identityData objectForKey:@"identity"];

            /* ignore this ID? */
            if ([excludedIds containsObject:identity])
                continue;

            NSString *cnContactId = [emailHashToCnContactId objectForKey:[identityData objectForKey:@"emailHash"]];
            if (cnContactId == nil) {
                cnContactId = [mobileNoHashToCnContactId objectForKey:[identityData objectForKey:@"mobileNoHash"]];
            }
            if (cnContactId == nil) {
                continue;
            }

            DDLogVerbose(@"Adding identity %@ to contacts", identity);
            [allIdentities addObject:identity];

            dispatch_group_enter(dispatchGroup);

            dispatch_async(dispatch_get_main_queue(), ^{
                [self addContactWithIdentity:identity publicKey:[[NSData alloc] initWithBase64EncodedString:[identityData objectForKey:@"publicKey"] options:0] cnContactId:cnContactId verificationLevel:ContactVerificationLevelServerVerified state:nil type:nil featureMask:nil acquaintanceLevel:ContactAcquaintanceLevelDirect alerts:NO contactSyncer:mediatorSyncableContacts onCompletion:^(NSObject * __unused contactObject) {
                    dispatch_group_leave(dispatchGroup);
                }];
            });
        }
        DDLogNotice(@"[ContactSync] Found %lu new address book contacts", (unsigned long)allIdentities.count);

        dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER);

        onCompletion();
    });
}

- (void)resetImportedStatus {
    [entityManager performAndWaitSave:^{
        NSArray *contacts = [entityManager.entityFetcher contactEntities];
        for (ContactEntity *contact in contacts) {
            contact.contactImportStatus = ContactImportStatusInitial;
        }
    }];
}

- (void)addWorkContactAndUpdateFeatureMaskWithIdentity:(nonnull NSString *)identity
                                             publicKey:(nonnull NSData *)publicKey
                                             firstname:(nullable NSString *)firstname
                                              lastname:(nullable NSString *)lastname
                                                   csi:(nullable NSString *)csi
                                              jobTitle:(nullable NSString *)jobTitle
                                            department:(nullable NSString *)department
                                     acquaintanceLevel:(ContactAcquaintanceLevel)acquaintanceLevel
                                workAvailabilityStatus:(nullable WorkAvailabilityStatus *) workAvailabilityStatus
                                          onCompletion:(nonnull void(^)(NSObject * _Nonnull))onCompletion
                                               onError:(nonnull void(^)(NSError * _Nonnull))onError {

    [FeatureMask getFeatureMaskFor:@[identity] completionHandler:^(NSDictionary<NSString *,NSNumber *> * _Nullable featureMasks, NSError * _Nullable error) {
        if (error) {
            onError(error);
            return;
        }

        NSNumber *featureMask = featureMasks[identity];

        // Sync create of the contact first for MD, if it successful than store it and sync update of the contact
        MediatorSyncableContacts *createContactSync = [MediatorSyncableContacts new];
        [createContactSync createWithDefaultValuesWithIdentity:identity publicKey:publicKey featureMask:featureMask.intValue isWork:YES];
        [createContactSync syncAsSubTaskWithCompletionHandler:^(NSError * _Nullable error) {
            if (error) {
                onError(error);
                return;
            }

            MediatorSyncableContacts *updateContactSync = [MediatorSyncableContacts new];

            __block NSString *contactIdentity = [self addWorkContactWithIdentity:identity
                                                                       publicKey:publicKey
                                                                       firstname:firstname
                                                                        lastname:lastname
                                                                             csi:csi
                                                                        jobTitle:jobTitle
                                                                      department:department
                                                                     featureMask:featureMasks[identity]
                                                               acquaintanceLevel:acquaintanceLevel
                                                          workAvailabilityStatus: workAvailabilityStatus
                                                                   entityManager:entityManager
                                                                   contactSyncer:updateContactSync];
            if (contactIdentity) {
                // Set sync action to update, because sync action create was used above before contact is stored in DB
                [updateContactSync setSyncActionToUpdateWithIdentity:identity];
                [updateContactSync syncAsSubTaskWithCompletionHandler:^(NSError * _Nullable error) {
                    if (error == nil) {
                        __block ContactEntity *contactEntity;
                        [entityManager performAndWait:^{
                            contactEntity = [[entityManager entityFetcher] contactEntityFor:contactIdentity];
                        }];
                        onCompletion(contactEntity);
                    }
                    else {
                        onError(error);
                    }
                }];
            }
            else {
                onError([ThreemaError threemaError:[NSString stringWithFormat:@"Add or update work contact for %@ failed", identity]]);
            }
        }];
    }];
}

/**
 Add or update work contact, if multi device activated contact will be synced.

 @param identity: Identity of the contact (will be not validated)
 @param publicKey: Public key it corresponds with the identity (will be not validated)
 @param firstname: First name of the contact
 @param lastname: Last name of the contact
 @param csi: CSI of the contact
 @param jobTitle: Job title of the contact
 @param department: Department of the contact
 @param featureMask: Feature mask of contact (will only updated if not nil)
 @param acquaintanceLevel: Is `group` contact will be marked as hidden
 @param workAvailabilityStatus: Optional `WorkAvailabilityStatus` of the contact
 @param entityManagerObject: Used `EntityManager`
 @param contactSyncer: Used `MediatorSyncableContacts` to sync changes when MD is enabled
 @returns: Added/updated identity of contact or nil if public key of already existing contact differs to given public key
 */

- (nullable NSString *)addWorkContactWithIdentity:(nonnull NSString *)identity
                                        publicKey:(nonnull NSData *)publicKey
                                        firstname:(NSString *)firstname
                                         lastname:(NSString *)lastname
                                              csi:(NSString *)csi
                                         jobTitle:(NSString *)jobTitle
                                       department:(NSString *)department
                                      featureMask:(nullable NSNumber *)featureMask
                                acquaintanceLevel:(ContactAcquaintanceLevel)acquaintanceLevel
                           workAvailabilityStatus:(nullable WorkAvailabilityStatus *)workAvailabilityStatus
                                    entityManager:(NSObject * _Nonnull)entityManagerObject
                                    contactSyncer:(MediatorSyncableContacts*)mediatorSyncableContacts {
    NSAssert([entityManagerObject isKindOfClass:[EntityManager class]], @"Parameter entityManagerObject should be type of EntityManager");
    EntityManager *em = (EntityManager *)entityManagerObject;

    /* Make sure this is not our own identity */
    if ([MyIdentityStore sharedMyIdentityStore].isValidIdentity && [identity isEqualToString:[MyIdentityStore sharedMyIdentityStore].identity]) {
        DDLogInfo(@"Ignoring attempt to add own identity");
        return nil;
    }

    __block NSString *contactIdentity;
    __block BOOL added = NO;

    [em performAndWaitSave:^{
        /* Check if we already have a contact with this identity */
        ContactEntity *contact;

        contact = [em.entityFetcher contactEntityFor: identity];
        if (contact) {
            DDLogInfo(@"Found existing contact with identity %@", identity);
            if (![publicKey isEqualToData:contact.publicKey]) {
                DDLogError(@"Public key doesn't match for existing identity %@!", identity);
                contact = nil;
                return;
            }
            else {
                if (contact.isHidden && acquaintanceLevel == ContactAcquaintanceLevelDirect) {
                    contact.isHidden = NO;
                    [mediatorSyncableContacts updateAcquaintanceLevelWithIdentity:contact.identity value:[NSNumber numberWithInteger:acquaintanceLevel]];
                }
            }
        } else {
            DDLogNotice(@"New work contact added");
            added = YES;

            NSError *createContactError;
            contact = [em getOrCreateContactWithIdentity:identity publicKey:publicKey sortOrderFirstName:userSettings.sortOrderFirstName error:&createContactError];
            if (createContactError) {
                DDLogError(@"New contact could not be created: %@", createContactError);
                return;
            }

            contact.isHidden = acquaintanceLevel == ContactAcquaintanceLevelGroupOrDeleted ? YES : NO;
            [self addAsWorkWithIdentities:[[NSOrderedSet alloc] initWithArray:@[contact.identity]] contactSyncer:mediatorSyncableContacts];
            [self addProfilePictureRequest:identity];
        }

        if (firstname != nil) {
            if (firstname.length > 0) {
                if (![contact.firstName isEqualToString:firstname]) {
                    [contact setFirstNameTo:firstname sortOrderFirstName:userSettings.sortOrderFirstName];
                    [mediatorSyncableContacts updateFirstNameWithIdentity:contact.identity value:contact.firstName];
                }
            }
        }

        if (lastname != nil) {
            if (lastname.length > 0) {
                if (![contact.lastName isEqualToString:lastname]) {
                    [contact setLastNameTo:lastname sortOrderFirstName:userSettings.sortOrderFirstName];
                    [mediatorSyncableContacts updateLastNameWithIdentity:contact.identity value:contact.lastName];
                }
            }
        }

        // TODO: (IOS-4895) MD support Job Title and Department
        if ((csi || contact.csi) && ![csi isEqualToString:contact.csi]) {
            contact.csi = csi;
        }
        
        if ((jobTitle || contact.jobTitle) && ![jobTitle isEqualToString:contact.jobTitle]) {
            contact.jobTitle = jobTitle;
        }
        if ((department || contact.department) && ![department isEqualToString:contact.department]) {
            contact.department = department;
        }

        if (contact.contactVerificationLevel != ContactVerificationLevelFullyVerified) {
            if(contact.contactVerificationLevel != ContactVerificationLevelServerVerified) {
                contact.contactVerificationLevel = ContactVerificationLevelServerVerified;
                [mediatorSyncableContacts updateVerificationLevelWithIdentity:contact.identity value:[NSNumber numberWithInteger: contact.contactVerificationLevel]];
            }
        }

        if (![contact.workContact isEqualToNumber:@YES]) {
            contact.workContact = @YES;
            [mediatorSyncableContacts updateWorkVerificationLevelWithIdentity:contact.identity value:contact.workContact];
        }

        if ((featureMask || contact.featureMask) && ![featureMask isEqual:contact.featureMask]) {
            [contact setFeatureMaskTo: featureMask.intValue];
        }
                
        if(workAvailabilityStatus) {
            // Check if contact currently has status
            if (contact.workAvailabilityStatus) {
                // Only update if it differs from current
                if ([self workAvailabilityStatusChangedWithCurrent:contact.workAvailabilityStatus new:workAvailabilityStatus]) {
                    [em.entityDestroyer deleteWithWorkAvailabilityStatus:contact.workAvailabilityStatus];
                    [em.entityCreator workAvailabilityStatusEntityWithValue:workAvailabilityStatus.categoryRawValue text:workAvailabilityStatus.text contact:contact];
                }
            }
            // Contact has no status, we create a new one
            else {
                [em.entityCreator workAvailabilityStatusEntityWithValue:workAvailabilityStatus.categoryRawValue text:workAvailabilityStatus.text contact:contact];
            }
        }
        // We did not receive a status, if contact has one, we remove it
        else if (contact.workAvailabilityStatus) {
            [em.entityDestroyer deleteWithWorkAvailabilityStatus:contact.workAvailabilityStatus];
        }
        
        // We always sync the status, even if is not set
        [mediatorSyncableContacts updateWorkAvailabilityStatusWithIdentity:contact.identity status:workAvailabilityStatus != nil ? workAvailabilityStatus : [[WorkAvailabilityStatus alloc] initWithValue:0 text:nil]];

        contactIdentity = contact.identity;
    }];

    if (added) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [mediatorSyncableContacts updateAllWithIdentity:contactIdentity added:added];
        });
    }

    return contactIdentity;
}

- (void)updateFromAddressBookWithContact:(nonnull ContactEntity*)contact contactSyncer:(nullable MediatorSyncableContacts *)mediatorSyncableContacts forceImport:(BOOL)forceImport onCompletion:(nullable void(^)(void))onCompletion {

    if ([contact contactImportStatus] != ContactImportStatusInitial && !forceImport) {
        DDLogInfo(@"Contact already imported. Do not import again.");
        if (onCompletion) {
            onCompletion();
        }
        return;
    }
    
    CNContactStore *cnAddressBook = [CNContactStore new];
    
    [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
        if (granted == YES) {
            NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]];
            NSError *error;
            NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys  error:&error];
            if (error) {
                NSLog(@"error fetching contacts %@", error);
            } else {
                CNContact *foundContact = cnContacts.firstObject;
                if (foundContact != nil) {
                    [self _updateContact:contact withCnContact:foundContact forceImport:forceImport contactSyncer:mediatorSyncableContacts];
                }
            }
        }
        if (onCompletion) onCompletion();
    }];
}

- (void)_updateContact:(nonnull ContactEntity *)contact withCnContact:(nonnull CNContact *)cnContact forceImport:(BOOL)forceImport contactSyncer:(nullable MediatorSyncableContacts *)mediatorSyncableContacts {
    if (cnContact == nil) {
        DDLogError(@"Cannot update contact from nil cnContact");
        return;
    }
    
    // Refrain from updating the contact information if the multi-device feature is enabled and the import status is not set to `.initial`
    if (userSettings.enableMultiDevice && [contact contactImportStatus] != ContactImportStatusInitial && !forceImport) {
        DDLogInfo(@"Contact already imported. Do not import again.");
        return;
    }
    
    NSString *newFirstName = cnContact.givenName;
    NSString *newLastName = cnContact.familyName;
    
    /* no name? try company name and e-mail address (Outlook auto-generated contacts etc.) */
    if (newFirstName.length == 0 && newLastName.length == 0) {
        NSString *companyName = cnContact.organizationName;
        if (companyName.length > 0) {
            newLastName = companyName;
        } else {
            /* no name? try e-mail address (Outlook auto-generated contacts etc.) */
            if (cnContact.emailAddresses.count > 0) {
                newLastName = ((CNLabeledValue *)cnContact.emailAddresses.firstObject).value;
            }
        }
    }
    
    if (newFirstName != contact.firstName && ![newFirstName isEqual:contact.firstName]) {
        [contact setFirstNameTo:newFirstName sortOrderFirstName:userSettings.sortOrderFirstName];
        [mediatorSyncableContacts updateFirstNameWithIdentity:contact.identity value:newFirstName];
    }
    
    if (newLastName != contact.lastName && ![newLastName isEqual:contact.lastName]) {
        [contact setLastNameTo:newLastName sortOrderFirstName:userSettings.sortOrderFirstName];
        [mediatorSyncableContacts updateLastNameWithIdentity:contact.identity value:newLastName];
    }

    
    /* get image, if any */
    NSData *newImageData = nil;
    if (cnContact.imageDataAvailable) {
        newImageData = cnContact.thumbnailImageData;
    }
    
    if (newImageData != contact.imageData && ![newImageData isEqualToData:contact.imageData]) {
        contact.imageData = newImageData;

        [mediatorSyncableContacts setProfileUpdateTypeWithIdentity:contact.identity value:contact.imageData != nil ? DeltaUpdateTypeUpdated : DeltaUpdateTypeRemoved];
    }

    ContactImportStatus importStatus = userSettings.enableMultiDevice ? ContactImportStatusImported : ContactImportStatusInitial;
    if (contact.contactImportStatus != importStatus) {
        contact.contactImportStatus = importStatus;
    }

    DDLogVerbose(@"Updated contact %@ to %@ %@", contact.identity, contact.firstName, contact.lastName);
}

- (void)updateAllContacts {
    MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
    
    NSArray *allContacts = [entityManager.entityFetcher contactEntities];
    if (allContacts == nil || allContacts.count == 0) {
        return;
    }
    
    // Migration of deprecated and removed verification level kVerificationLevelWorkVerified (3) and kVerificationLevelWorkFullyVerified (4) to flag workContact
    [entityManager performAndWaitSave:^{
        for (ContactEntity *contact in allContacts) {
            if (contact.workContact == nil) {
                if ([contact getVerificationLevelForMigration].intValue == 3) {
                    contact.contactVerificationLevel = ContactVerificationLevelServerVerified;
                    [mediatorSyncableContacts updateVerificationLevelWithIdentity:contact.identity value:[NSNumber numberWithInteger: contact.contactVerificationLevel]];
                    contact.workContact = @YES;
                } else if ([contact getVerificationLevelForMigration].intValue == 4) {
                    contact.contactVerificationLevel = ContactVerificationLevelFullyVerified;
                    [mediatorSyncableContacts updateVerificationLevelWithIdentity:contact.identity value:[NSNumber numberWithInteger: contact.contactVerificationLevel]];
                    contact.workContact = @YES;
                } else {
                    contact.workContact = @NO;
                }
                [mediatorSyncableContacts updateWorkVerificationLevelWithIdentity:contact.identity value:contact.workContact];
            }
        }
    }];
    
    __block NSArray *linkedContacts;
    [entityManager performAndWait:^{
        linkedContacts = [allContacts filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id  _Nullable evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
            ContactEntity *contact = (ContactEntity *)evaluatedObject;
            return contact.cnContactId != nil;
        }]];
    }];
    if (linkedContacts == nil || linkedContacts.count == 0) {
        [mediatorSyncableContacts syncAsync];
        return;
    }
    
    CNContactStore *cnAddressBook = [CNContactStore new];
    [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
        if (granted == YES) {
            // Go through all contacts and resync with address book; only create
            // address book ref when encountering the first contact that is linked
            __block int updated = 0;
                
            for (ContactEntity *contact in linkedContacts) {
                [entityManager performAndWaitSave:^{
                    NSString *cnContactID = [contact.cnContactId copy];
                    if (cnContactID == nil) {
                        DDLogNotice(@"CNContactID of linked contact was nil.");
                        return;
                    }
                    
                    NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[cnContactID]];
                    NSError *error;
                    NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
                    if (error) {
                        DDLogError(@"Fetching address book contacts failed %@", error);
                    } else {
                        if (cnContacts != nil && cnContacts.count > 0) {
                            CNContact *foundContact = cnContacts.firstObject;
                            [self _updateContact:contact withCnContact:foundContact forceImport:NO contactSyncer:mediatorSyncableContacts];
                            updated++;
                        }
                    }
                }];
            }
            DDLogInfo(@"Updated %d contacts", updated);
            
            [self updateStatusForAllContactsIgnoreInterval:NO contactSyncer:mediatorSyncableContacts onCompletion:^{
                [mediatorSyncableContacts syncObjcWithCompletionHandler:^(NSError * _Nullable error) {
                    if (error) {
                        DDLogError(@"Contact sync failed: %@", [error localizedDescription]);
                    }
                }];
            } onError:nil]; // To keep the existing behavior we silently discard errors. // TODO: (IOS-4425) This should probably be changed
        }
    }];
}

/**
 Update contact if attributes has changed and sync is multi device activated.

 @param identity: Identity of the contact to changed
 @param avatar: Image data of the contact avatar
 @param firstName: First name of the contact
 @param lastName: Last name of the contact
 */

- (void)updateContactWithIdentity:(NSString * _Nonnull)identity avatar:(NSData * _Nullable)avatar firstName:(NSString * _Nullable)firstName lastName:(NSString * _Nullable)lastName {

    [entityManager performAndWaitSave:^{
        ContactEntity *contact = [[entityManager entityFetcher] contactEntityFor:identity];
        if (contact) {
            MediatorSyncableContacts *mediatorSyncableContacts = [MediatorSyncableContacts new];

            if (contact.imageData != avatar) {
                contact.imageData = avatar;
                [mediatorSyncableContacts setProfileUpdateTypeWithIdentity:identity value:contact.imageData ? 2 : 1];
            }

            if (contact.firstName != firstName) {
                [contact setFirstNameTo:firstName sortOrderFirstName:userSettings.sortOrderFirstName];
                [mediatorSyncableContacts updateFirstNameWithIdentity:identity value:contact.firstName];
            }

            if (contact.lastName != lastName) {
                [contact setLastNameTo:lastName sortOrderFirstName:userSettings.sortOrderFirstName];
                [mediatorSyncableContacts updateLastNameWithIdentity:identity value:contact.lastName];
            }

            [mediatorSyncableContacts syncAsync];
        }
        else {
            DDLogError(@"Missing contact %@ to update", identity);
        }
    }];
}

/**
 Mark contact as deleted if no 1:1-conversation exists.
 
 If Multi Device is activated the contact will be reflected.

 @param identity: Identity of the contact to mark as deleted
 @param entityManagerObject: EntityManager on which the deletion will de executed
 */

- (void)markContactAsDeletedWithIdentity:(nonnull NSString *)identity entityManagerObject:(nonnull NSObject *)entityManagerObject {
    NSAssert([entityManagerObject isKindOfClass:[EntityManager class]], @"Parameter entityManagerObject should be type of EntityManager");
    EntityManager *em = (EntityManager *)entityManagerObject;

    [em performAndWait:^{
        ContactEntity *contact = [[em entityFetcher] contactEntityFor:identity];
        if (contact) {
            if (!contact.isHidden) {
                // Prevent deletion if has contact a 1:1 conversation
                ConversationEntity *conversation = [[em entityFetcher] conversationEntityFor:contact.identity];
                if (conversation) {
                    DDLogWarn(@"Contact %@ can't be deleted because has a 1:1 conversation", contact.identity);
                    return;
                }

                // Mark contact as deleted
                [em performAndWaitSave:^{
                    contact.isHidden = YES;

                }];

                MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
                [mediatorSyncableContacts updateAcquaintanceLevelWithIdentity:contact.identity value:[[NSNumber alloc] initWithInt:ContactAcquaintanceLevelGroupOrDeleted]];
                [mediatorSyncableContacts syncObjcWithCompletionHandler:^(NSError * _Nullable error) {
                    if (error) {
                        DDLogError(@"Sync contact %@ mark as deleted failed: %@", contact.identity, [error localizedDescription]);
                    }
                }];
            }
        }
    }];
}

- (void)linkContact:(nonnull NSObject *)contactObject toCnContactId:(NSString *)cnContactId {
    NSAssert([contactObject isKindOfClass:[ContactEntity class]], @"Parameter contactObject must be type of ContactEntity");
    ContactEntity *contact = (ContactEntity*)contactObject;

    MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
    [self linkContact:contact toCnContactId:cnContactId contactSyncer:mediatorSyncableContacts forceImport:YES onCompletion:^{
        [mediatorSyncableContacts syncObjcWithCompletionHandler:^(NSError * _Nullable error) {
            if (error) {
                DDLogError(@"Contact multi device sync failed: %@", [error localizedDescription]);
            }
        }];
    }];
}

- (void)linkContact:(ContactEntity*)contact toCnContactId:(NSString *)cnContactId contactSyncer:(MediatorSyncableContacts *)mediatorSyncableContacts onCompletion:(void(^)(void))onCompletion {
    [self linkContact:contact toCnContactId:cnContactId contactSyncer:mediatorSyncableContacts forceImport:NO onCompletion:onCompletion];
}

- (void)linkContact:(ContactEntity*)contact toCnContactId:(NSString *)cnContactId contactSyncer:(MediatorSyncableContacts *)mediatorSyncableContacts forceImport:(BOOL)forceImport onCompletion:(void(^)(void))onCompletion {
    /* obtain first/last name from address book */
    [entityManager performAndWaitSave:^{
        contact.cnContactId = cnContactId;
        [self updateFromAddressBookWithContact:contact contactSyncer:mediatorSyncableContacts forceImport:forceImport onCompletion:onCompletion];
    }];
}

- (void)unlinkContact:(nonnull NSObject*)contactObject {
    __block MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];

    [entityManager performAndWaitSave:^{
        NSAssert([contactObject isKindOfClass:[ContactEntity class]], @"Parameter contactObject must be type of ContactEntity");
        ContactEntity *contact = (ContactEntity*)contactObject;

        contact.cnContactId = nil;
        if (contact.firstName) {
            [contact setFirstNameTo:nil sortOrderFirstName:userSettings.sortOrderFirstName];
            [mediatorSyncableContacts updateFirstNameWithIdentity:contact.identity value:@""];
        }
        if (contact.lastName) {
            [contact setLastNameTo:nil sortOrderFirstName:userSettings.sortOrderFirstName];
            [mediatorSyncableContacts updateLastNameWithIdentity:contact.identity value:@""];
        }
        if (contact.imageData) {
            contact.imageData = nil;
            [mediatorSyncableContacts setProfileUpdateTypeWithIdentity:contact.identity value:DeltaUpdateTypeRemoved];
        }
    }];
    
    [mediatorSyncableContacts syncAsync];
}

#pragma mark - Fetch contact

/**
 Fetch public key for identity, the completion handler will be executed in background thread.

 Warning: Please note that a newly added contact is synchronized as subtask (i.e. executed immediately)

 @param identity: Contact identity
 @param acquaintanceLevel: If contact new and acquaintance level is `ContactAcquaintanceLevelGroup`, than the created contact is marked as hidden
 @param entityManagerObject: Must be type of `EntityManager`, is needed to run DB on main or background context
 @param onCompletion: Executed on background thread
 @param onError: Executed on arbitrary thread
 */

- (void)fetchPublicKeyForIdentity:(NSString*)identity acquaintanceLevel:(ContactAcquaintanceLevel)acquaintanceLevel entityManager:(NSObject*)entityManagerObject ignoreBlockUnknown:(BOOL)ignoreBlockUnknown onCompletion:(void(^)(NSData *publicKey))onCompletion onError:(void(^)(NSError *error))onError {

    // Don't store special contacts, return builtin public key
    if ([PredefinedContactsObjC isSpecialContactWithIdentity:identity]) {
        onCompletion([PredefinedContactsObjC publicKeyFor:identity]);
        return;
    }

    NSAssert([entityManagerObject isKindOfClass:[EntityManager class]], @"Parameter entityManagerObject should be type of EntityManager");
    EntityManager *em = (EntityManager *)entityManagerObject;
    BusinessInjector *businessInjector = [[BusinessInjector alloc] initWithEntityManager:em];
    GroupManager *groupManager = [businessInjector groupManagerObjC];

    [em perform:^{
        // check in local DB first
        ContactEntity *contact = [em.entityFetcher contactEntityFor:identity];
        if (contact.publicKey) {
            if (userSettings.blockUnknown && ignoreBlockUnknown == false) {
                if (contact.isHidden == false) {
                    onCompletion(contact.publicKey);
                    return;
                }
                else if (contact.groupConversations.count > 0) {
                    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"identity == %@", contact.identity];
                    for (ConversationEntity *groupConversation in contact.groupConversations) {
                        Group *group = [groupManager getGroupWithConversation:groupConversation];
                        if (group.isSelfMember && [group.members filteredSetUsingPredicate:predicate].count > 0) {
                            onCompletion(contact.publicKey);
                            return;
                        }
                    }
                }
                
                DDLogVerbose(@"Block unknown contacts is on - discarding message");
                onError([ThreemaError threemaError:@"Message received from unknown contact and block contacts is on" withCode:ThreemaProtocolErrorBlockUnknownContact]);
            } else {
                onCompletion(contact.publicKey);
            }
        } else {
            
            if (TargetManagerObjC.isBusinessApp) {
                [self fetchWorkPublicKeyForIdentity:identity acquaintanceLevel:acquaintanceLevel entityManager:em ignoreBlockUnknown:ignoreBlockUnknown onCompletion:onCompletion onError:onError];
            } else {
                // Block message if user is not in our subscription
                if (userSettings.blockUnknown && ignoreBlockUnknown == false) {
                    DDLogVerbose(@"Block unknown contacts is on - discarding message");
                    onError([ThreemaError threemaError:@"Message received from unknown contact and block contacts is on" withCode:ThreemaProtocolErrorBlockUnknownContact]);
                } else {
                    [self fetchAddContactAndSyncWithIdentity:identity acquaintanceLevel:acquaintanceLevel entityManager:em onCompletion:onCompletion onError:onError];
                }
            }
        }
    }];
}

/// Fetch public key for identity in the work subscription to know if this identity should be blocked or not.
/// If identity was not found and block unknown is disabled, contact will be fetched and added in fetchAddContactAndSyncWithIdentity.
/// The completion handler will be executed in background thread
/// @param identity Contact identity
/// @param acquaintanceLevel If contact new and acquaintance level is `ContactAcquaintanceLevelGroup`, than the created contact is marked as hidden
/// @param entityManagerObject Must be type of `EntityManager`, is needed to run DB on main or background context
/// @param onCompletion Executed on background thread
/// @param onError Executed on arbitrary thread
- (void)fetchWorkPublicKeyForIdentity:(nonnull NSString*)identity acquaintanceLevel:(ContactAcquaintanceLevel)acquaintanceLevel entityManager:(nonnull NSObject*)entityManagerObject ignoreBlockUnknown:(BOOL)ignoreBlockUnknown onCompletion:(void(^)(NSData *publicKey))onCompletion onError:(void(^)(NSError *error))onError {
    EntityManager *em = (EntityManager *)entityManagerObject;
    
    [self fetchWorkIdentities:@[identity] onCompletion:^(NSArray *foundIdentities) {
        // First, check in local DB again, as it may have already been saved in the meantime (in case of parallel requests)
        if ([em existingContactWith:identity]) {
            __block NSData *publicKey;
            [em performAndWait:^{
                publicKey = [em.entityFetcher contactEntityFor:identity].publicKey;
            }];
            
            if (publicKey != nil) {
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                    onCompletion(publicKey);
                });
                return;
            }
        }
        
        dispatch_async(dispatch_get_main_queue(), ^{
            if (foundIdentities.count > 0) {
                for (NSDictionary *foundIdentity in foundIdentities) {
                    if ([foundIdentity[@"id"] isEqualToString:identity]) {
                        // Save new contact. Do it on main queue to ensure that it's done by the time we signal completion.
                        NSData *publicKey = [[NSData alloc] initWithBase64EncodedString:foundIdentity[@"pk"] options:0];
                        if (!publicKey) {
                            continue;
                        }
                        NSString *firstName = nil;
                        NSString *lastName = nil;
                        NSString *csi = nil;
                        NSString *jobTitle = nil;
                        NSString *department = nil;
                        WorkAvailabilityStatus *workAvailabilityStatus = nil;

                        if (![foundIdentity[@"first"] isEqual:[NSNull null]]) {
                            firstName = foundIdentity[@"first"];
                        }
                        if (![foundIdentity[@"last"] isEqual:[NSNull null]]) {
                            lastName = foundIdentity[@"last"];
                        }
                        if (![foundIdentity[@"csi"] isEqual:[NSNull null]]) {
                            csi = foundIdentity[@"csi"];
                        }
                        if (![foundIdentity[@"jobTitle"] isEqual:[NSNull null]]) {
                            jobTitle = foundIdentity[@"jobTitle"];
                        }
                        if (![foundIdentity[@"department"] isEqual:[NSNull null]]) {
                            department = foundIdentity[@"department"];
                        }
                        if (![foundIdentity[@"availability"] isEqual:[NSNull null]]) {
                            workAvailabilityStatus = [WorkAvailabilityStatus fromEncodedString:foundIdentity[@"availability"]];
                        }
                                                
                        [self addWorkContactAndUpdateFeatureMaskWithIdentity:identity
                                                                   publicKey:publicKey
                                                                   firstname:firstName
                                                                    lastname:lastName csi:csi
                                                                    jobTitle:jobTitle
                                                                  department:department
                                                           acquaintanceLevel:acquaintanceLevel
                                                      workAvailabilityStatus:workAvailabilityStatus
                                                                onCompletion:^(NSObject * _Nonnull __unused contactObject) {
                            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                                onCompletion(publicKey);
                            });
                        } onError:^(NSError * _Nonnull error) {
                            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                                DDLogError(@"Add work contact failed: %@", error.localizedDescription);
                                onError(error);
                            });
                        }];
                        return;
                    }
                }
            }
            
            // Block message if user is not in our subscription
            if (userSettings.blockUnknown && ignoreBlockUnknown == false) {
                DDLogVerbose(@"Block unknown contacts is on and contact not found in work list - discarding message");
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                    onError([ThreemaError threemaError:@"Message received from unknown contact and block contacts is on" withCode:ThreemaProtocolErrorBlockUnknownContact]);
                });
            } else {
                [self fetchAddContactAndSyncWithIdentity:identity acquaintanceLevel:acquaintanceLevel entityManager:em onCompletion:onCompletion onError:onError];
            }
        });
    } onError:^(NSError *error) {
        onError(error);
    }];
}

/// Fetch public key for identity and add it to the contactlist. The completion handler will be executed in background thread.
/// @param identity Contact identity
/// @param acquaintanceLevel If contact new and acquaintance level is `ContactAcquaintanceLevelGroup`, than the created contact is marked as hidden
/// @param entityManagerObject Must be type of `EntityManager`, is needed to run DB on main or background context
/// @param onCompletion Executed on background thread
/// @param onError Executed on arbitrary thread
- (void)fetchAddContactAndSyncWithIdentity:(nonnull NSString*)identity acquaintanceLevel:(ContactAcquaintanceLevel)acquaintanceLevel entityManager:(nonnull NSObject*)entityManagerObject onCompletion:(void(^)(NSData *publicKey))onCompletion onError:(void(^)(NSError *error))onError {
    
    EntityManager *em = (EntityManager *)entityManagerObject;

    [[IdentityInfoFetcher sharedIdentityInfoFetcher] fetchIdentityInfoFor:identity onCompletion:^(NSData *publicKey, NSNumber *state, NSNumber *type, NSNumber *featureMask) {
        // First, check in local DB again, as it may have already been saved in the meantime (in case of parallel requests)
        ContactEntity *contact = [em.entityFetcher contactEntityFor:identity];
        if (contact.publicKey) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                onCompletion(contact.publicKey);
            });
            return;
        }

        // Sync create of the contact first for MD, if it successful than store it and sync update of the contact
        MediatorSyncableContacts *createContactSync = [[MediatorSyncableContacts alloc] init];
        [createContactSync createWithDefaultValuesWithIdentity:identity publicKey:publicKey featureMask:featureMask.intValue isWork:NO];
        [createContactSync syncAsSubTaskWithCompletionHandler:^(NSError * _Nullable error) {
            if (error == nil) {
                // Save new contact. Do it on main queue to ensure that it's done by the time we signal completion.
                dispatch_async(dispatch_get_main_queue(), ^{
                    __weak typeof(self) weakSelf = self;
                    MediatorSyncableContacts *updateContactSync = [[MediatorSyncableContacts alloc] init];
                    [self addContactWithIdentity:identity publicKey:publicKey cnContactId:nil verificationLevel:ContactVerificationLevelUnverified state:state type:type featureMask:featureMask acquaintanceLevel:acquaintanceLevel alerts:NO contactSyncer:updateContactSync onCompletion:^(NSObject * __unused contactObject) {
                        // Set sync action to update, because sync action create was used above before contact is stored in DB
                        [updateContactSync setSyncActionToUpdateWithIdentity:identity];
                        [updateContactSync syncAsSubTaskWithCompletionHandler:^(NSError * _Nullable error) {
                            if (error == nil) {
                                [weakSelf synchronizeAddressBookForceFullSync:YES onCompletion:nil onError:nil];

                                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                                    onCompletion(publicKey);
                                });
                            }
                            else {
                                onError(error);
                            }
                        }];
                    }];
                });
            }
            else {
                onError(error);
            }
        }];

    } onError:^(NSError * _Nonnull error) {
        onError(error);
    }];
}

- (void)prefetchIdentityInfo:(NSSet*)identities onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError {
    NSMutableSet *identitiesToFetch = [NSMutableSet set];
    
    // Skip identities that we already have a contact for
    for (NSString *identity in identities) {
        ContactEntity *contact = [entityManager.entityFetcher contactEntityFor:identity];
        if (!contact) {
            [identitiesToFetch addObject:identity];
        }
    }
    
    if ([identitiesToFetch count] == 0) {
        onCompletion();
        return;
    }
    
    [[IdentityInfoFetcher sharedIdentityInfoFetcher] prefetchIdentityInfo:identitiesToFetch onCompletion:onCompletion onError:onError];
}

- (void)fetchWorkIdentities:(NSArray *)identities onCompletion:(void(^)(NSArray *foundIdentities))onCompletion onError:(void(^)(NSError *error))onError {
    [[IdentityInfoFetcher sharedIdentityInfoFetcher] fetchWorkIdentitiesInfo:identities onCompletion:onCompletion onError:onError];
}

- (void)upgradeContact:(nonnull NSObject*)contactObject toVerificationLevel:(int32_t)verificationLevel {
    MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];

    [entityManager performAndWaitSave:^{
        NSAssert([contactObject isKindOfClass:[ContactEntity class]], @"Parameter contactObject must be type of ContactEntity");
        ContactEntity *contact = (ContactEntity*)contactObject;
        
        if ((contact.contactVerificationLevel < verificationLevel && contact.contactVerificationLevel != ContactVerificationLevelFullyVerified) || verificationLevel == ContactVerificationLevelFullyVerified) {

            contact.contactVerificationLevel = verificationLevel;
            contact.isHidden = NO;
            [mediatorSyncableContacts updateVerificationLevelWithIdentity:contact.identity value:[NSNumber numberWithInteger: contact.contactVerificationLevel]];
            [mediatorSyncableContacts updateAcquaintanceLevelWithIdentity:contact.identity value:[NSNumber numberWithInteger:ContactAcquaintanceLevelDirect]];
        }
    }];

    [mediatorSyncableContacts syncAsync];
}

- (void)addAsWorkWithIdentities:(NSOrderedSet *)identities contactSyncer:(nullable MediatorSyncableContacts *)mediatorSyncableContacts {
    BOOL hasChanged = NO;
    NSMutableOrderedSet *currentWorkIdentities = [[NSMutableOrderedSet alloc] initWithOrderedSet:[userSettings workIdentities]];
    for (NSString *identity in identities) {
        if (![currentWorkIdentities containsObject:identity]) {
            [currentWorkIdentities addObject:identity];
            [mediatorSyncableContacts updateIdentityTypeWithIdentity:identity value:@YES];
            hasChanged = YES;
        }
    }
    if (hasChanged) {
        userSettings.workIdentities = currentWorkIdentities;
    }
}

#pragma mark - Nickname

/**
 Update nickname if is necessary.

 @param identity: Identity of contact to change nickname
 @param nickname: Nickname to update
 */

- (void)updateNickname:(nonnull NSString *)identity nickname:(NSString *)nickname {
    MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];

    [entityManager performAndWaitSave:^{
        ContactEntity *contact = [entityManager.entityFetcher contactEntityFor:identity];

        if (contact) {
            BOOL hasChanged = NO;

            if (nickname && nickname.length > 0 && ![contact.publicNickname isEqualToString:nickname]) {
                if ([nickname isEqualToString:identity]) {
                    DDLogNotice(@"[Nickname] Set new nickname (ID) for %@", identity);
                }
                else {
                    DDLogNotice(@"[Nickname] Set new nickname for %@", identity);
                }
                contact.publicNickname = nickname;
                hasChanged = YES;
            }
            else if (nickname == nil && ![contact.publicNickname isEqualToString:contact.identity]) {
                DDLogNotice(@"[Nickname] Set ID as nickname for %@", identity);
                contact.publicNickname = contact.identity;
                hasChanged = YES;
            }

            if (hasChanged) {
                [mediatorSyncableContacts updateNicknameWithIdentity:contact.identity value:contact.publicNickname];
            }
        }
    }];

    if ([userSettings enableMultiDevice]) {
        [mediatorSyncableContacts syncAsync];
    }
}

#pragma mark - Profile Picture

- (void)updateProfilePicture:(nullable NSString *)identity imageData:(NSData *)imageData shouldReflect:(BOOL)shouldReflect blobID:(nullable NSData *)blobID encryptionKey:(nullable NSData *)encryptionKey didFailWithError:(NSError * _Nullable __autoreleasing * _Nullable)error {
    UIImage *image = [UIImage imageWithData:imageData];
    if (image == nil) {
        *error = [ThreemaError threemaError:@"Image decoding failed"];
        return;
    }

    __block ContactEntity *contact;

    [entityManager performAndWaitSave:^{
        contact = [entityManager.entityFetcher contactEntityFor:identity];
        if (contact) {
            ImageDataEntity *dbImage = [entityManager.entityCreator imageDataEntityWithData:imageData size:image.size message:nil];
            contact.contactImage = dbImage;
        }
    }];

    if (!contact) {
        *error = [ThreemaError threemaError:@"Contact not found"];
        return;
    }

    [self removeProfilePictureRequest:identity];
    
    if ([userSettings enableMultiDevice] && shouldReflect) {
        MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
        [mediatorSyncableContacts setContactProfileUpdateTypeWithIdentity:identity value:DeltaUpdateTypeUpdated blobID:blobID encryptionKey:encryptionKey];
        [mediatorSyncableContacts syncAsync];
    }
}

- (void)deleteProfilePicture:(nullable NSString *)identity shouldReflect:(BOOL)shouldReflect {
    __block ContactEntity *contact;

    [entityManager performAndWaitSave:^{
        contact = [entityManager.entityFetcher contactEntityFor:identity];
        if (contact) {
            contact.contactImage = nil;
        }
    }];

    if (!contact) {
        return;
    }

    [self removeProfilePictureRequest:identity];
    
    if ([userSettings enableMultiDevice] && shouldReflect) {
        MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
        [mediatorSyncableContacts setContactProfileUpdateTypeWithIdentity:identity value:DeltaUpdateTypeRemoved blobID:nil encryptionKey:nil];
        [mediatorSyncableContacts syncAsync];
    }
}

- (void)removeProfilePictureFlagForAllContacts {
    [entityManager performSave:^{
        NSArray *allContacts = [entityManager.entityFetcher contactEntities];
        
        if (allContacts != nil) {
            for (ContactEntity *contact in allContacts) {
                contact.profilePictureSent = NO;
                contact.profilePictureBlobID = nil;
            }
        }
    }];
}

- (void)removeProfilePictureFlagForIdentity:(NSString *)identity {
    [entityManager performAndWaitSave:^{
        ContactEntity *contact = [entityManager.entityFetcher contactEntityFor:identity];
        if (contact) {
            contact.profilePictureSent = NO;
            contact.profilePictureBlobID = nil;
        }
    }];
}

- (BOOL)existsProfilePictureRequestForIdentity:(NSString *)identity {
    @synchronized (self) {
        return [[userSettings profilePictureRequestList] containsObject:identity];
    }
}

- (void)removeProfilePictureRequest:(NSString *)identity {
    @synchronized (self) {
        if ([self existsProfilePictureRequestForIdentity:identity]) {
            NSMutableSet *profilePictureRequestList = [NSMutableSet setWithArray:userSettings.profilePictureRequestList];
            [profilePictureRequestList removeObject:identity];
            userSettings.profilePictureRequestList = profilePictureRequestList.allObjects;
        }
    }
}

- (void)addProfilePictureRequest:(NSString *)identity {
    @synchronized (self) {
        if (![self existsProfilePictureRequestForIdentity:identity]) {
            NSMutableSet *profilePictureRequestList = [NSMutableSet setWithArray:userSettings.profilePictureRequestList];
            [profilePictureRequestList addObject:identity];
            userSettings.profilePictureRequestList = profilePictureRequestList.allObjects;
        }
    }
}

#pragma mark - Sync Address Book

- (void)synchronizeAddressBookForceFullSync:(BOOL)forceFullSync onCompletion:(void(^)(BOOL addressBookAccessGranted))onCompletion onError:(void(^)(NSError *error))onError {
    [self synchronizeAddressBookForceFullSync:forceFullSync ignoreMinimumInterval:NO onCompletion:onCompletion onError:onError];
}

- (void)synchronizeAddressBookForceFullSync:(BOOL)forceFullSync ignoreMinimumInterval:(BOOL)ignoreMinimumInterval onCompletion:(void(^)(BOOL addressBookAccessGranted))onCompletion onError:(void(^)(NSError *error))onError {
    if (ProcessInfoHelper.isRunningForScreenshots)  {
        return;
    }
    
    /* Get all entries from the user's address book, hash the e-mail addresses
     and phone numbers and send to the server. */

    if (!userSettings.syncContacts) {
        DDLogInfo(@"Contact sync is disabled");
        [self processStatusUpdateOnlyWithFullServerSync:forceFullSync ignoreMinimumInterval:ignoreMinimumInterval onCompletion:onCompletion onError:onError];
        return;
    }

    CNContactStore *cnAddressBook = [CNContactStore new];
    if (cnAddressBook == nil) {
        DDLogInfo(@"Address book not found");
        [self processStatusUpdateOnlyWithFullServerSync:forceFullSync ignoreMinimumInterval:ignoreMinimumInterval onCompletion:onCompletion onError:onError];
        return;
    }

    CNAuthorizationStatus currentStatus = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts];
    if (currentStatus != CNAuthorizationStatusAuthorized && currentStatus != CNAuthorizationStatusLimited) {
        [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
            if (granted == YES) {
                [self synchronizeAddressBookForceFullSync:forceFullSync onCompletion:onCompletion onError:onError];
            } else {
                DDLogInfo(@"Address book access has NOT been granted: %@", error);
                [self processStatusUpdateOnlyWithFullServerSync:forceFullSync ignoreMinimumInterval:ignoreMinimumInterval onCompletion:onCompletion onError:onError];
            }
        }];
        return;
    }

    dispatch_async(_syncContactsQueue, ^{
        NSUserDefaults *defaults = [AppGroup userDefaults];
        NSDate *lastServerCheck = [defaults objectForKey:@"ContactsSyncLastCheck"];
        NSInteger lastServerCheckInterval = [defaults integerForKey:@"ContactsSyncLastCheckInterval"];
        BOOL fullServerSync = YES;
        
        /* calculate earliest date for next server check */
        if (lastServerCheck != nil) {
            if (-[lastServerCheck timeIntervalSinceNow] < lastServerCheckInterval) {
                DDLogInfo(@"Last server contacts sync less than %ld seconds ago", (long)lastServerCheckInterval);
                if (forceFullSync) {
                    DDLogInfo(@"Forcing full sync");
                } else {
                    fullServerSync = NO;
                }
            }
        }
        
        /* check if we are within the minimum interval */
        if (fullServerSync) {
            if (!ignoreMinimumInterval && lastFullSyncDate != nil && -[lastFullSyncDate timeIntervalSinceNow] < minimumSyncInterval) {
                DDLogInfo(@"Still within minimum interval - not syncing");
                if (onCompletion != nil)
                    dispatch_async(dispatch_get_main_queue(), ^{
                        onCompletion(YES);
                    });
                return;
            }
        }
        
        DDLogNotice(@"[ContactSync] Build all e-mail and phone number hashes");
        /* extract all e-mail and phone number hashes from the user's address book */
        [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
            if (granted == YES) {
                NSError *error;
                NSMutableArray *allCNContacts = [NSMutableArray new];
                
                NSArray *containers = [cnAddressBook containersMatchingPredicate:nil error:&error];
                for (CNContainer *container in containers) {
                    NSPredicate *predicate = [CNContact predicateForContactsInContainerWithIdentifier:container.identifier];
                    NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
                    if (cnContacts != nil) {
                        [allCNContacts addObjectsFromArray:cnContacts];
                    }
                }
                
                [self processAddressBookContacts:allCNContacts fullServerSync:fullServerSync ignoreMinimumInterval:ignoreMinimumInterval onCompletion:onCompletion onError:onError];
            }
        }];
    });
}

/**
 Process status request/update to all contacts.

 @param fullServerSync: If true do MD sync of all contacts
 @param ignoreMinimumInterval: True contact status request/update will be called anyway
 @param onCompletion: Completion handler
 @param onError: Error handler
 */

- (void)processStatusUpdateOnlyWithFullServerSync:(BOOL)fullServerSync ignoreMinimumInterval:(BOOL)ignoreMinimumInterval onCompletion:(void(^)(BOOL addressBookAccessGranted))onCompletion onError:(void(^)(NSError * _Nonnull))onError {

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{
        MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
        
        [self updateStatusForAllContactsIgnoreInterval:ignoreMinimumInterval contactSyncer:mediatorSyncableContacts onCompletion:^{
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{
                [mediatorSyncableContacts syncObjcWithCompletionHandler:^(NSError * _Nullable error) {
                    if (error == nil) {
                        if (onCompletion != nil) {
                            dispatch_async(dispatch_get_main_queue(), ^{
                                onCompletion(NO);
                            });
                        }
                    }
                    else {
                        DDLogError(@"[ContactSync] Contact multi device sync failed: %@", [error localizedDescription]);
                        if (onError != nil) {
                            onError(error);
                        }
                    }
                }];
            });
        } onError:nil]; // To keep the existing behavior we silently discard errors. // TODO: (IOS-4425) This should probably be changed
    });
}

/**
 Process address book contacts and status request/update to all contacts.

 @param contacts: Address book contacts to add or update as Threema contact
 @param fullServerSync: True sync all address book contacts otherwise just the new ones (If true do MD sync of all contacts)
 @param ignoreMinimumInterval: True contact status request/update will be called anyway
 @param onCompletion: Completion handler
 @param onError: Error handler
 */

- (void)processAddressBookContacts:(NSArray*)contacts fullServerSync:(BOOL)fullServerSync ignoreMinimumInterval:(BOOL)ignoreMinimumInterval onCompletion:(void(^)(BOOL addressBookAccessGranted))onCompletion onError:(void(^)(NSError *error))onError {
    
    NSUserDefaults *defaults = [AppGroup userDefaults];
    PhoneNumberNormalizer *normalizer = [PhoneNumberNormalizer new];

    NSSet *emailLastCheck = [NSSet setWithArray:[defaults objectForKey:@"ContactsSyncLastEmailHashes"]];
    NSSet *mobileNoLastCheck = [NSSet setWithArray:[defaults objectForKey:@"ContactsSyncLastMobileNoHashes"]];
    
    NSString *countryCode = [normalizer userRegion];
    DDLogInfo(@"Current country code: %@", countryCode);
    
    NSMutableSet *emailHashesBase64 = [NSMutableSet set];
    NSMutableSet *mobileNoHashesBase64 = [NSMutableSet set];
    
    NSMutableDictionary *emailHashToCnContactId = [NSMutableDictionary dictionary];
    NSMutableDictionary *mobileNoHashToCnContactId = [NSMutableDictionary dictionary];
    
    for (CNContact *person in contacts) {
        NSString *cnContactId = person.identifier;
        NSString *name = [CNContactFormatter stringFromContact:person style:CNContactFormatterStyleFullName];
        
        for (CNLabeledValue *label in person.emailAddresses) {
            NSString *email = label.value;
            if (email.length > 0) {
                NSString *emailNormalized = [[email lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
                NSString *emailHashBase64 = [ContactStore hashEmailBase64:emailNormalized];
                [emailHashToCnContactId setObject:cnContactId forKey:emailHashBase64];
                [emailHashesBase64 addObject:emailHashBase64];
                
                /* Gmail address? If so, hash with the other domain as well */
                NSString *emailNormalizedAlt = nil;
                if ([emailNormalized hasSuffix:@"@gmail.com"])
                    emailNormalizedAlt = [emailNormalized stringByReplacingOccurrencesOfString:@"@gmail.com" withString:@"@googlemail.com"];
                else if ([emailNormalized hasSuffix:@"@googlemail.com"])
                    emailNormalizedAlt = [emailNormalized stringByReplacingOccurrencesOfString:@"@googlemail.com" withString:@"@gmail.com"];
                
                if (emailNormalizedAlt != nil) {
                    NSString *emailHashAltBase64 = [ContactStore hashEmailBase64:emailNormalizedAlt];
                    [emailHashToCnContactId setObject:cnContactId forKey:emailHashAltBase64];
                    [emailHashesBase64 addObject:emailHashAltBase64];
                }
                
                DDLogVerbose(@"%@ (%@): %@", name, cnContactId, emailNormalized);
            }
        }
        
        for (CNLabeledValue *label in person.phoneNumbers) {
            NSString *phone = [label.value stringValue];
            if (phone.length > 0) {
                /* normalize phone number first */
                NSString *mobileNoNormalized = [normalizer e164FormatFrom:phone defaultRegion:countryCode];
                if (mobileNoNormalized == nil)
                    continue;
                NSString *mobileNoHashBase64 = [ContactStore hashMobileNoBase64:mobileNoNormalized];
                [mobileNoHashToCnContactId setObject:cnContactId forKey:mobileNoHashBase64];
                [mobileNoHashesBase64 addObject:mobileNoHashBase64];
                DDLogVerbose(@"%@ (%@): %@", name, cnContactId, mobileNoNormalized);
            }
        }
    }
    
    if (!fullServerSync) {
        /* a full server sync is not scheduled right now, so remove any hashes that we checked last time from the list */
        for (NSString *emailHash in emailLastCheck) {
            [emailHashesBase64 removeObject:emailHash];
        }
        for (NSString *mobileNoHash in mobileNoLastCheck) {
            [mobileNoHashesBase64 removeObject:mobileNoHash];
        }
    }
    
    if (emailHashesBase64.count == 0 && mobileNoHashesBase64.count == 0) {
        DDLogInfo(@"No new contacts to synchronize");
        if (onCompletion != nil)
            dispatch_async(dispatch_get_main_queue(), ^{
                onCompletion(YES);
            });
        return;
    }
    DDLogNotice(@"ContactSync: Start request %lu emails, %lu phonenumbers", (unsigned long)emailHashesBase64.count, (unsigned long)mobileNoHashesBase64.count);
    ServerAPIConnector *conn = [[ServerAPIConnector alloc] init];
    [conn matchIdentitiesWithEmailHashes:[emailHashesBase64 allObjects] mobileNoHashes:[mobileNoHashesBase64 allObjects] includeInactive:NO onCompletion:^(NSArray *identities, int checkInterval) {
        
        if (fullServerSync) {
            [defaults setObject:[emailHashesBase64 allObjects] forKey:@"ContactsSyncLastEmailHashes"];
            [defaults setObject:[mobileNoHashesBase64 allObjects] forKey:@"ContactsSyncLastMobileNoHashes"];
            [defaults setObject:[NSDate date] forKey:@"ContactsSyncLastCheck"];
        } else {
            /* add the hashes we just checked to the full list */
            NSMutableArray *prevEmailHashes = [NSMutableArray arrayWithArray:[defaults objectForKey:@"ContactsSyncLastEmailHashes"]];
            NSMutableArray *prevMobileNoHashes = [NSMutableArray arrayWithArray:[defaults objectForKey:@"ContactsSyncLastMobileNoHashes"]];
            [prevEmailHashes addObjectsFromArray:[emailHashesBase64 allObjects]];
            [prevMobileNoHashes addObjectsFromArray:[mobileNoHashesBase64 allObjects]];
            [defaults setObject:prevEmailHashes forKey:@"ContactsSyncLastEmailHashes"];
            [defaults setObject:prevMobileNoHashes forKey:@"ContactsSyncLastMobileNoHashes"];
        }
        [defaults setInteger:checkInterval forKey:@"ContactsSyncLastCheckInterval"];
        [defaults synchronize];

        DDLogNotice(@"[ContactSync] Start Core Data stuff");

        MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
        [self addContactsWithIdentities:identities emailHashes:emailHashToCnContactId mobileNoHashes:mobileNoHashToCnContactId contactSyncer:mediatorSyncableContacts onCompletion:^{
            // trigger updating of status for identities
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{
                DDLogNotice(@"[ContactSync] Update status and featuremask for all contacts");
                [self updateStatusForAllContactsIgnoreInterval:ignoreMinimumInterval contactSyncer:mediatorSyncableContacts onCompletion:^{
                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{
                        [mediatorSyncableContacts syncObjcWithCompletionHandler:^(NSError * _Nullable error) {
                            if (error == nil) {
                                if (fullServerSync) {
                                    lastFullSyncDate = [NSDate date];
                                }
                                
                                DDLogNotice(@"[ContactSync] Address book sync finished");
                                if (onCompletion != nil) {
                                    dispatch_async(dispatch_get_main_queue(), ^{
                                        onCompletion(YES);
                                    });
                                }
                            }
                            else {
                                DDLogError(@"[ContactSync] Contact multi device sync failed: %@", [error localizedDescription]);
                                if (onCompletion) onCompletion(YES);
                            }
                        }];
                    });
                } onError:nil]; // To keep the existing behavior we silently discard errors. // TODO: (IOS-4425) This should probably be changed
            });
        }];
    } onError:^(NSError *error) {
        DDLogError(@"[ContactSync] Synchronize address book failed: %@", error);
        if (onError != nil) {
            dispatch_async(dispatch_get_main_queue(), ^{
                onError(error);
            });
        }
    }];
}

+ (void)linkedIdentitiesForEmail:(NSString *)email AndMobileNo:(NSString *)mobileNo onCompletion:(void(^)(NSArray *identities))onCompletion {
    
    NSArray<NSString *> *emailHashesBase64 = [NSArray array];
    NSArray<NSString *> *mobileNoHashesBase64 = [NSArray array];
    
    if (email.length > 0) {
        NSString *emailNormalized = [[email lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
        
        NSString *emailHashBase64 = [ContactStore hashEmailBase64:emailNormalized];
        emailHashesBase64 = @[emailHashBase64];
    }
    
    if (mobileNo.length > 0) {
        /* normalize phone number first */
        PhoneNumberNormalizer *normalizer = [PhoneNumberNormalizer new];
        NSString *countryCode = [normalizer userRegion];
        NSString *mobileNoNormalized = [normalizer e164FormatFrom:mobileNo defaultRegion:countryCode];
        if (mobileNoNormalized != nil) {
            NSString *mobileNoHashBase64 = [ContactStore hashMobileNoBase64:mobileNoNormalized];
            
            mobileNoHashesBase64 = @[mobileNoHashBase64];
        }
    }
    
    if (emailHashesBase64.count > 0 || mobileNoHashesBase64.count > 0) {
        ServerAPIConnector *conn = [[ServerAPIConnector alloc] init];
        [conn matchIdentitiesWithEmailHashes:emailHashesBase64 mobileNoHashes:mobileNoHashesBase64 includeInactive:YES onCompletion:^(NSArray *identities, int checkInterval) {
            if (identities == nil) {
                NSArray *emptyArray = [NSArray array];
                onCompletion(emptyArray);
            } else {
                onCompletion(identities);
            }
        } onError:^(NSError *error) {
            DDLogError(@"Linked identities failed: %@", error);
            NSArray *emptyArray = [NSArray array];
            onCompletion(emptyArray);
        }];
    } else {
        NSArray *emptyArray = [NSArray array];
        onCompletion(emptyArray);
    }
}

- (NSArray *)allIdentities {
    NSSet *identitiesSet = [entityManager.entityFetcher contactIdentities];
    return [identitiesSet allObjects];
}

- (NSArray<NSString *> *)contactsWithFeatureMaskNil {
    __block NSArray<NSString *> *identities;
    [entityManager performAndWait:^{
        identities = [self identitiesForContacts:[entityManager.entityFetcher contactEntitiesWithFeatureMaskNil]];
    }];
    return identities;
}

- (NSArray *)allContacts {
    return [entityManager.entityFetcher contactEntities];
}

- (void)orderChanged:(NSNotification*)notification {
    [entityManager performSave:^{
        /* update display name and sort index of all contacts */
        NSArray *allContacts = [entityManager.entityFetcher contactEntities];
        
        if (allContacts != nil) {
            for (ContactEntity *contact in allContacts) {
                /* set last name again to trigger update of display name and sort index */
                [contact setLastNameTo:contact.lastName sortOrderFirstName:userSettings.sortOrderFirstName];
            }
        }
    }];
}

- (NSArray<NSString *> *)identitiesForContacts:(NSArray<ContactEntity *> *)contacts {
    NSMutableArray *identities = [NSMutableArray arrayWithCapacity:contacts.count];
    for (ContactEntity *contact in contacts) {
        [identities addObject:contact.identity];
    }
    return identities;
}

- (NSArray *)validIdentities {
    NSMutableArray *identities = [[NSMutableArray alloc] init];
    
    EntityManager *backgroundEntityManager = [self backgroundEntityManager];
    [backgroundEntityManager performAndWait:^{
        NSArray *contacts = [backgroundEntityManager.entityFetcher contactEntities];
        
        for (ContactEntity *contact in contacts) {
            if (contact.contactState != ContactStateInvalid) {
                [identities addObject:contact.identity];
            }
        }
    }];
    
    return identities;
}

- (BOOL)needCheckStatus:(BOOL)ignoreInterval {
    if (ignoreInterval) {
        return YES;
    }
    
    NSUserDefaults *defaults = [AppGroup userDefaults];
    NSDate *dateLastCheck = [defaults objectForKey:@"DateLastCheckStatus"];
    if (dateLastCheck == nil) {
        return true;
    }
    
    NSInteger checkInterval = [self getCheckStatusInterval];
    NSDate *dateOfNextCheck = [dateLastCheck dateByAddingTimeInterval:checkInterval];
    NSDate *now = [NSDate date];
    return [now timeIntervalSinceDate:dateOfNextCheck] > 0;
}

- (void)setupCheckStatusTimer {
    NSUserDefaults *defaults = [AppGroup userDefaults];
    
    NSDate *now = [NSDate date];
    [defaults setObject:now forKey:@"DateLastCheckStatus"];
    [defaults synchronize];
    
    NSInteger checkInterval = [self getCheckStatusInterval];
    checkStatusTimer = [NSTimer scheduledTimerWithTimeInterval:checkInterval target:self selector:@selector(updateStatusForAllContacts) userInfo:nil repeats:NO];
}

- (NSInteger) getCheckStatusInterval {
    NSUserDefaults *defaults = [AppGroup userDefaults];
    NSInteger checkInterval = [defaults integerForKey:@"CheckStatusInterval"];

    return MAX(checkInterval, MIN_CHECK_INTERVAL);
}

- (void)updateStatusForAllContacts {
    [self updateStatusForAllContactsIgnoreInterval:NO onCompletion:^{
        DDLogNotice(@"Successfully updated status for all contacts");
    } onError:^(NSError * _Nonnull error) {
        DDLogError(@"Contact multi device sync failed: %@", [error localizedDescription]);
    }];
}

- (void)updateStatusForAllContactsIgnoreInterval:(BOOL)ignoreInterval onCompletion:(nonnull void(^)(void))onCompletion onError:(nonnull void(^)(NSError * _Nonnull error))onError {
    MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
    [self updateStatusForAllContactsIgnoreInterval:ignoreInterval contactSyncer:mediatorSyncableContacts onCompletion:^{
        [mediatorSyncableContacts syncObjcWithCompletionHandler:^(NSError * _Nullable error) {
            if (error) {
                onError(error);
            }
            else {
                onCompletion();
            }
        }];
    } onError:^(NSError *error) {
        if (error != nil) {
            onError(error);
        }
        // This should hardly ever happen so we just call the completion handler.
        else {
            DDLogWarn(@"Update status for all contact failed, but error is nil.");
            onCompletion();
        }
    }];
}

- (void)updateStatusForAllContactsIgnoreInterval:(BOOL)ignoreInterval contactSyncer:(MediatorSyncableContacts *)mediatorSyncableContacts onCompletion:(void(^)(void))onCompletion onError:(nullable void(^)(NSError *error))onError {
    if (ProcessInfoHelper.isRunningForScreenshots)  {
        // do nothing
    } else {
        if ([self needCheckStatus:ignoreInterval] == NO) {
            DDLogNotice(@"[ContactSync] Do not update status and featuremasks");
            if (onCompletion) {
                onCompletion();
            }
            return;
        }
        
        [self updateStatusWithContactSyncer:mediatorSyncableContacts onCompletion:^() {
            [self setupCheckStatusTimer];
            DDLogNotice(@"[ContactSync] Update status and featuremasks finished");
            if (onCompletion) {
                onCompletion();
            }
        } onError:^(NSError *error){
            [self setupCheckStatusTimer];
            DDLogError(@"[ContactSync] Update status featuremasks finished with error: %@", error);
            // For backwards compatibility the error handler is optional
            // TODO: (IOS-4425) This should always return an error if it fails such that it can be handled by a caller if needed.
            if (onError) {
                onError(error);
            } else if (onCompletion) {
                onCompletion();
            }
        }];
    }
}

- (void)updateStatusWithContactSyncer:(MediatorSyncableContacts *)mediatorSyncableContacts onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError  {
    NSArray *identities = [self validIdentities];
    ServerAPIConnector *conn = [[ServerAPIConnector alloc] init];
    [conn checkStatusOfIdentities:identities onCompletion:^(NSArray *states, NSArray *types, NSArray *featureMasks, int checkInterval) {
        [entityManager performAndWaitSave:^{
            NSMutableOrderedSet *workIdentities = [NSMutableOrderedSet new];
            for (NSInteger i=0; i<[identities count]; i++) {
                NSNumber *state = [states objectAtIndex: i];
                NSNumber *type = [types objectAtIndex:i];
                NSNumber *featureMask = [featureMasks objectAtIndex:i];
                
                NSString *identityString = [identities objectAtIndex:i];
                ContactEntity *contact = [entityManager.entityFetcher contactEntityFor: identityString];
                if ([ProcessInfoHelper isRunningForScreenshots]) {
                    // do not update active/inactive state for screenshots
                }
                else {
                    if (contact.contactState != state.intValue) {
                        contact.contactState = (ContactState)state.intValue;
                        [mediatorSyncableContacts updateStateWithIdentity:contact.identity value:[NSNumber numberWithInteger: contact.contactState]];
                    }
                }
                
                if ([type isEqualToNumber:@1]) {
                    [workIdentities addObject:contact.identity];
                }
                
                if (![featureMask isEqual:[NSNull null]]) {
                    NSNumber *oldFeatureMask = contact.featureMask;
                    [contact setFeatureMaskTo:featureMask.integerValue]; // Always update feature mask of local contact
                    if (![oldFeatureMask isEqualToNumber:featureMask]) {
                        [mediatorSyncableContacts updateFeatureMaskWithIdentity:contact.identity value:contact.featureMask];
                    }
                }
            }
            if (![[NSUserDefaults standardUserDefaults] boolForKey:@"FASTLANE_SNAPSHOT"]) {
                [self addAsWorkWithIdentities:workIdentities contactSyncer:mediatorSyncableContacts];
            }
        }];
        
        NSUserDefaults *defaults = [AppGroup userDefaults];
        [defaults setInteger:checkInterval forKey:@"CheckStatusInterval"];
        [defaults synchronize];
        
        onCompletion();
    } onError:^(NSError *error) {
        onError(error);
    }];
}

- (NSArray<NSDictionary<NSString *, NSString *> *> *)cnContactEmailsForContact:(NSObject *)contactObject {
    NSAssert(contactObject == nil || [contactObject isKindOfClass:[ContactEntity class]], @"Parameter contactObject must be type of ContactEntity");
    ContactEntity *contact = (ContactEntity*)contactObject;

    if (contact.cnContactId == nil)
        return nil;
    
    __block NSArray *cnContacts;
    CNContactStore *cnAddressBook = [CNContactStore new];
    
    NSError *error;
    NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]];
    NSArray *tmpCnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
    if (error) {
        NSLog(@"error fetching contacts %@", error);
        return nil;
    } else {
        cnContacts = tmpCnContacts;
        
        NSMutableArray<NSDictionary<NSString *, NSString *> *> *emails = [NSMutableArray new];
        if (cnContacts.count == 1) {
            for (CNContact *person in cnContacts) {
                for (CNLabeledValue<NSString *> *label in person.emailAddresses) {
                    NSMutableDictionary<NSString *, NSString *> *dict = [NSMutableDictionary new];
                    NSString *emailLabel = label.label;
                    NSString *email = label.value;
                    if (email.length > 0) {
                        [dict setValue:[CNLabeledValue localizedStringForLabel:emailLabel] forKey:@"label"];
                        [dict setValue:email forKey:@"address"];
                        [emails addObject:dict];
                    }
                }
            }
        }
        return emails;
    }
}

- (NSArray<NSDictionary<NSString *, NSString *> *> *)cnContactPhoneNumbersForContact:(NSObject *)contactObject {
    NSAssert(contactObject == nil || [contactObject isKindOfClass:[ContactEntity class]], @"Parameter contactObject must be type of ContactEntity");
    ContactEntity *contact = (ContactEntity*)contactObject;

    if (contact.cnContactId == nil)
        return nil;
    
    __block NSArray *cnContacts;
    CNContactStore *cnAddressBook = [CNContactStore new];
    
    NSError *error;
    NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]];
    NSArray *tmpCnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
    if (error) {
        NSLog(@"error fetching contacts %@", error);
        return nil;
    } else {
        cnContacts = tmpCnContacts;
        
        NSMutableArray<NSDictionary<NSString *, NSString *> *> *phoneNumbers = [NSMutableArray new];
        if (cnContacts.count == 1) {
            for (CNContact *person in cnContacts) {
                for (CNLabeledValue<CNPhoneNumber *> *label in person.phoneNumbers) {
                    NSMutableDictionary<NSString *, NSString *> *dict = [NSMutableDictionary new];
                    NSString *phoneLabel = label.label;
                    NSString *phone = [label.value stringValue];
                    if (phone.length > 0) {
                        [dict setValue:[CNLabeledValue localizedStringForLabel:phoneLabel] forKey:@"label"];
                        [dict setValue:phone forKey:@"number"];
                        [phoneNumbers addObject:dict];
                    }
                }
            }
        }
        return phoneNumbers;
    }
}

+ (NSString*)hashEmailBase64:(NSString*)email {
    NSData *emailHashKeyData = [NSData dataWithBytes:emailHashKey length:sizeof(emailHashKey)];
    return [[CryptoUtils hmacSha256ForData:[email dataUsingEncoding:NSASCIIStringEncoding] key:emailHashKeyData] base64EncodedStringWithOptions:0];
}

+ (NSString*)hashMobileNoBase64:(NSString*)mobileNo {
    NSData *mobileNoHashKeyData = [NSData dataWithBytes:mobileNoHashKey length:sizeof(mobileNoHashKey)];
    return [[CryptoUtils hmacSha256ForData:[mobileNo dataUsingEncoding:NSASCIIStringEncoding] key:mobileNoHashKeyData] base64EncodedStringWithOptions:0];
}

#pragma mark - Read receipts

- (void)resetCustomReadReceipts {
    MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];

    [entityManager performSave:^{
        NSArray *contactsWithCustomReadReceipts = [entityManager.entityFetcher contactEntitiesWithCustomReadReceipt];
        
        if (contactsWithCustomReadReceipts != nil) {
            for (ContactEntity *contact in contactsWithCustomReadReceipts) {
                contact.readReceipt = ReadReceiptDefault;
                [mediatorSyncableContacts updateReadReceiptWithIdentity:contact.identity value:ReadReceiptDefault];
            }
        }
    }];
    
    [mediatorSyncableContacts syncAsync];
}

#pragma mark - Multi Device Sync

- (void)reflectContact:(NSString *)identity {
    MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
    [mediatorSyncableContacts updateAllWithIdentity:identity added:NO];
    [mediatorSyncableContacts syncAsync];
}

@end

¤ Dauer der Verarbeitung: 0.25 Sekunden  (vorverarbeitet am  2026-11-04) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Statistik
#Sources=1104850
#Domains=416977