Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Threema/ThreemaFramework/Business/   (Social Medium ©)  Datei vom 26.9.2026 mit Größe 101 kB image not shown  

Quelle  ContactStore.m  Sprache: unbekannt

 
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.61 Sekunden]