{"action":"created","comment":{"url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/pulls/comments/84708003","pull_request_review_id":5468326,"id":84708003,"diff_hunk":"@@ -0,0 +1,653 @@\n+//\n+// Wire\n+// Copyright (C) 2016 Wire Swiss GmbH\n+//\n+// This program is free software: you can redistribute it and/or modify\n+// it under the terms of the GNU General Public License as published by\n+// the Free Software Foundation, either version 3 of the License, or\n+// (at your option) any later version.\n+//\n+// This program is distributed in the hope that it will be useful,\n+// but WITHOUT ANY WARRANTY; without even the implied warranty of\n+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n+// GNU General Public License for more details.\n+//\n+// You should have received a copy of the GNU General Public License\n+// along with this program. If not, see http://www.gnu.org/licenses/.\n+//\n+\n+@import ZMCDataModel;\n+@import CallKit;\n+@import UIKit;\n+@import ZMCSystem;\n+@import Intents;\n+#import \"ZMCallKitDelegate.h\"\n+#import \"ZMUserSession.h\"\n+#import \"ZMVoiceChannel+CallFlow.h\"\n+#import \"ZMVoiceChannel+CallFlowPrivate.h\"\n+#import \"AVSFlowManager.h\"\n+#import \"AVSMediaManager.h\"\n+#import \"AVSMediaManager+Client.h\"\n+#import <zmessaging/zmessaging-Swift.h>\n+\n+/**\n+ @c ZMCallKitDelegate is the main logical part of CallKit integration of wire-ios-sync-engine. \n+ CallKit is the iOS VoIP calling integration framework that allows to place VoIP calls with the same priority level\n+ as other (GSM) calls.\n+ \n+ CallKit is supported from iOS 10. On the devices that do not support it we still show the notifications.\n+ \n+ It is possible to disable CallKit and use old-style notifications logic: `+[ZMUserSession setUseCallKit:]`.\n+ \n+ CallKit integration consists of 2 parts:\n+ I.  Showing iOS calling UI when call happens\n+     I.a Receive calls\n+     I.b Place calls\n+ II. Interaction with Phone app and redialing existing calls from there\n+ \n+ Using CallKit means asking iOS to start and end the call, so it can show the proper calling UI and play the ringtone.\n+ \n+ Flow for receieving the call (I.a):\n+ 1. BE sends the new call state in conversation via push\n+ 2. @c ZMCallStateTranscoder decode the payload and update the conversation/ZMVoiceChannel fields\n+ 3. @c ZMVoiceChannel observer sends the update to @c ZMCallKitDelegate -[ZMCallKitDelegate voiceChannelStateDidChange:]\n+ 4. @c ZMCallKitDelegate indicates the call to CallKit's @c CXProvider in -[ZMCallKitDelegate indicateIncomingCallInConversation:]\n+ 5. @c CXProvider approves the call and informs @c ZMCallKitDelegate that call is possible in -[ZMCallKitDelegate provider:performStartCallAction:]\n+ 6. @c ZMCallKitDelegate joins the call with -[ZMVoiceChannel join]\n+ \n+ Flow for sending the call (I.b):\n+ 1. API consumer (UI app) calls -[ZMVoiceChannel joinInUserSession:]. This call is forwarded to -[ZMCallKitDelegate requestStartCallInConversation:]\n+ 2. @c ZMCallKitDelegate indicates the call to CallKit's @c CXCallController, that asks @c CXProvider if call is possible. \n+ 3. @c CXProvider is indicating the call is possible with the callback -[ZMCallKitDelegate provider:performStartCallAction:]\n+ 4. @c ZMCallKitDelegate joins the call with -[ZMVoiceChannel join]\n+ \n+ Flow for interaction with last calls / Phone app:\n+ 1. The app is launched or brought to foreground\n+ 2. @c UIApplicationDelegate receives the call for continuing the User Activity that is forwarded to @c ZMUserSession\n+ 3. @c ZMUserSession forwards the call to -[ZMCallKitDelegate continueUserActivity:]\n+ 4. @c ZMCallKitDelegate looks up the caller from the NSUserActivity and starts the call\n+ \n+ */\n+\n+\n+static NSString * const ZMCallKitDelegateCallStartedInGroup = @\"callkit.call.started\";\n+\n+static char* const ZMLogTag ZM_UNUSED = \"CallKit\";\n+\n+NS_ASSUME_NONNULL_BEGIN\n+\n+@implementation CXProvider (TypeConformance)\n+@end\n+\n+@interface ZMVoiceChannel (NonIdleStates)\n+/// Returns the list of @c ZMVoiceChannelState that are considered as an active call.\n++ (NSSet *)nonIdleStates;\n+/// Checks if current state of voice channel is one of the active ones.\n+- (BOOL)inNonIdleState;\n+@end\n+\n+@interface ZMCallKitDelegate ()\n+@property (nonatomic) id<CallKitProviderType> provider;\n+@property (nonatomic) id<CallKitCallController> callController;\n+@property (nonatomic) ZMOnDemandFlowManager *onDemandFlowManager;\n+@property (nonatomic, weak) ZMUserSession *userSession;\n+@property (nonatomic, weak) AVSMediaManager *mediaManager;\n+@property (nonatomic) NSMutableDictionary <NSString *, NSNumber *> *lastConversationsState;\n+@end\n+\n+@interface ZMCallKitDelegate (VoiceChannelObserver)\n+- (void)conversationVoiceChannelStateDidChange:(ZMConversation *)conversation;\n+@end\n+\n+NS_ASSUME_NONNULL_END\n+\n+@implementation ZMUser (Handle)\n+\n+- (CXHandle *)callKitHandle\n+{\n+    if (0 != self.phoneNumber.length) {\n+        return [[CXHandle alloc] initWithType:CXHandleTypePhoneNumber value:self.phoneNumber];\n+    }\n+    else if (0 != self.emailAddress.length) {\n+        return [[CXHandle alloc] initWithType:CXHandleTypeEmailAddress value:self.emailAddress];\n+    }\n+    else {\n+        RequireString(1, \"Cannot create CXHandle: user has neither email nor phone number\");\n+    }\n+    return nil;\n+}\n+\n+@end\n+\n+@implementation ZMConversation (Handle)\n+\n+- (CXHandle *)callKitHandle\n+{\n+    switch (self.conversationType) {\n+        case ZMConversationTypeOneOnOne:\n+        case ZMConversationTypeConnection:\n+            return self.connectedUser.callKitHandle;\n+            break;\n+        case ZMConversationTypeGroup:\n+            return [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:self.remoteIdentifier.transportString];\n+            break;\n+        default:\n+            RequireString(1, \"Cannot create CXHandle: conversation type is invalid\");\n+            return nil;\n+            break;\n+    }\n+    \n+    return nil;\n+}\n+\n++ (nullable instancetype)resolveConversationForPersons:(NSArray<INPerson *> *)persons\n+                                             inContext:(NSManagedObjectContext *)context\n+{\n+    if (1 != persons.count) {\n+        ZMLogError(@\"CallKit: Cannot resolve call conversation for %lu participants\", (unsigned long)persons.count);\n+        return nil;\n+    }\n+    \n+    INPerson *person = persons.firstObject;\n+    \n+    switch (person.personHandle.type) {\n+        case INPersonHandleTypeUnknown:\n+        {\n+            NSUUID *personHandle = [NSUUID uuidWithTransportString:person.personHandle.value];\n+            ZMConversation *result = [ZMConversation conversationWithRemoteID:personHandle\n+                                                               createIfNeeded:NO\n+                                                                    inContext:context];\n+            return result;\n+        }\n+            break;\n+        case INPersonHandleTypePhoneNumber:\n+        {\n+            ZMUser *user = [ZMUser userWithPhoneNumber:person.personHandle.value inContext:context];\n+            if (nil == user) {\n+                // Due to iOS bug the email caller is indicated as one with the INPersonHandleTypePhoneNumber in iOS 10.0.2\n+                user = [ZMUser userWithEmailAddress:person.personHandle.value inContext:context];\n+                \n+                if (nil == user) {\n+                    NSUUID *personHandle = [NSUUID uuidWithTransportString:person.personHandle.value];\n+                    ZMConversation *result = [ZMConversation conversationWithRemoteID:personHandle\n+                                                                       createIfNeeded:NO\n+                                                                            inContext:context];\n+                    return result;\n+                }\n+                else {\n+                    return user.oneToOneConversation;\n+                }\n+            }\n+            else {\n+                return user.oneToOneConversation;\n+            }\n+        }\n+            break;\n+        case INPersonHandleTypeEmailAddress:\n+        {\n+            ZMUser *user = [ZMUser userWithEmailAddress:person.personHandle.value inContext:context];\n+            if (nil == user) {\n+                return nil;\n+            }\n+            else {\n+                return user.oneToOneConversation;\n+            }\n+        }\n+            break;\n+    }\n+}\n+\n+@end\n+\n+\n+@implementation CXCallAction (Conversation)\n+\n+- (ZMConversation *)conversationInContext:(NSManagedObjectContext *)context\n+{\n+    ZMConversation *result = [ZMConversation conversationWithRemoteID:self.callUUID\n+                                                       createIfNeeded:NO\n+                                                            inContext:context];\n+    return result;\n+}\n+\n+@end\n+\n+@implementation ZMVoiceChannel (NonIdleStates)\n+\n++ (NSSet *)nonIdleStates {\n+    return [NSSet setWithObjects:@(ZMVoiceChannelStateOutgoingCall),\n+            @(ZMVoiceChannelStateOutgoingCallInactive),\n+            @(ZMVoiceChannelStateSelfIsJoiningActiveChannel),\n+            @(ZMVoiceChannelStateSelfConnectedToActiveChannel),\n+            @(ZMVoiceChannelStateIncomingCall),\n+            @(ZMVoiceChannelStateIncomingCallInactive), nil];\n+}\n+\n+- (BOOL)inNonIdleState\n+{\n+    return [self.class.nonIdleStates containsObject:@(self.state)];\n+}\n+\n+@end\n+\n+\n+@interface ZMCallKitDelegate (ProviderDelegate) <CXProviderDelegate>\n+@end\n+\n+@implementation ZMCallKitDelegate\n+\n+- (void)dealloc\n+{\n+    [[NSNotificationCenter defaultCenter] removeObserver:self];\n+}\n+\n+- (instancetype)initWithCallKitProvider:(id<CallKitProviderType>)callKitProvider\n+                         callController:(id<CallKitCallController>)callController\n+                    onDemandFlowManager:(ZMOnDemandFlowManager *)onDemandFlowManager\n+                            userSession:(ZMUserSession *)userSession\n+                           mediaManager:(AVSMediaManager *)mediaManager\n+\n+{\n+    self = [super init];\n+    if (nil != self) {\n+        Require(callKitProvider);\n+        Require(callController);\n+        Require(userSession);\n+\n+        self.provider = callKitProvider;\n+        self.callController = callController;\n+        [self.provider setDelegate:self queue:nil];\n+        self.userSession = userSession;\n+        self.mediaManager = mediaManager;\n+        self.onDemandFlowManager = onDemandFlowManager;\n+        \n+        self.lastConversationsState = [[NSMutableDictionary alloc] init];\n+\n+        [[NSNotificationCenter defaultCenter] addObserver:self\n+                                                 selector:@selector(managedObjectsDidChange:)\n+                                                     name:NSManagedObjectContextObjectsDidChangeNotification\n+                                                   object:userSession.managedObjectContext];\n+        \n+        [[NSNotificationCenter defaultCenter] addObserver:self\n+                                                 selector:@selector(applicationDidBecomeActive:)\n+                                                     name:UIApplicationDidBecomeActiveNotification\n+                                                   object:nil];\n+        \n+        for (ZMConversation *conversation in self.activeCallConversations) {\n+            [self conversationVoiceChannelStateDidChange:conversation];\n+        }\n+    }\n+    return self;\n+}\n+\n++ (CXProviderConfiguration *)providerConfiguration\n+{\n+    NSString* localizedName = [NSBundle mainBundle].infoDictionary[@\"CFBundleName\"];\n+    if (localizedName == nil) {\n+        localizedName = @\"Wire\";\n+    }\n+    CXProviderConfiguration* providerConfiguration = [[CXProviderConfiguration alloc] initWithLocalizedName:localizedName];\n+\n+    providerConfiguration.supportsVideo = YES;\n+    providerConfiguration.maximumCallsPerCallGroup = 1;\n+    providerConfiguration.supportedHandleTypes = [NSSet setWithObjects:\n+                                                  @(CXHandleTypePhoneNumber),\n+                                                  @(CXHandleTypeEmailAddress),\n+                                                  @(CXHandleTypeGeneric), nil];\n+    \n+    providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation([UIImage imageNamed:@\"logo\"]);\n+    NSString *ringtoneSound = [ZMCustomSound notificationRingingSoundName];\n+    providerConfiguration.ringtoneSound = ringtoneSound;\n+\n+    return providerConfiguration;\n+}\n+\n+- (void)requestStartCallInConversation:(ZMConversation *)conversation videoCall:(BOOL)video\n+{\n+    ZMUser *selfUser = [ZMUser selfUserInUserSession:self.userSession];\n+    \n+    CXStartCallAction *startCallAction = [[CXStartCallAction alloc] initWithCallUUID:conversation.remoteIdentifier\n+                                                                              handle:selfUser.callKitHandle];\n+    startCallAction.video = video;\n+    \n+    CXTransaction *startCallTransaction = [[CXTransaction alloc] initWithAction:startCallAction];\n+    \n+    [self.callController requestTransaction:startCallTransaction completion:^(NSError * _Nullable error) {\n+        if (nil != error) {\n+            ZMLogError(@\"Cannot start call: %@\", error);\n+        }\n+    }];\n+}\n+\n+- (void)requestEndCallInConversation:(ZMConversation *)conversation\n+{\n+    CXEndCallAction *endCallAction = [[CXEndCallAction alloc] initWithCallUUID:conversation.remoteIdentifier];\n+    \n+    CXTransaction *endCallTransaction = [[CXTransaction alloc] initWithAction:endCallAction];\n+    \n+    [self.callController requestTransaction:endCallTransaction completion:^(NSError * _Nullable error) {\n+        if (nil != error) {\n+            ZMLogError(@\"Cannot end call: %@\", error);\n+        }\n+        [conversation.voiceChannel leave];\n+    }];\n+}\n+\n+- (NSArray<ZMConversation *> *)activeCallConversations\n+{\n+    ZMConversationList *nonIdleConversations = [ZMConversationList conversationsInUserSession:self.userSession];\n+    \n+    return [nonIdleConversations filterWithBlock:^BOOL(ZMConversation *conversation) {\n+        return conversation.voiceChannel.inNonIdleState;\n+    }];\n+}\n+\n+- (void)indicateIncomingCallInConversation:(ZMConversation *)conversation\n+{\n+    // Construct a CXCallUpdate describing the incoming call, including the caller.\n+    CXCallUpdate* update = [[CXCallUpdate alloc] init];\n+    \n+    ZMUser *caller = [conversation.voiceChannel.participants firstObject];\n+    \n+    if (conversation.conversationType == ZMConversationTypeGroup) {\n+        update.localizedCallerName = [ZMCallKitDelegateCallStartedInGroup localizedStringWithUser:caller\n+                                                                                     conversation:conversation\n+                                                                                            count:0];\n+    }\n+    else {\n+        update.localizedCallerName = caller.displayName;\n+    }\n+    \n+    update.remoteHandle = conversation.callKitHandle;\n+    update.hasVideo = conversation.isVideoCall;\n+    [self.provider reportNewIncomingCallWithUUID:conversation.remoteIdentifier\n+                                          update:update\n+                                      completion:^(NSError * _Nullable error) {\n+                                          if (nil != error) {\n+                                              [conversation.voiceChannel leave];\n+                                              ZMLogError(@\"Cannot report incoming call: %@\", error);\n+                                          }\n+                                      }];\n+}\n+\n+- (void)leaveAllActiveCalls\n+{\n+    [self.userSession enqueueChanges:^{\n+        for (ZMConversation *conversation in [ZMConversationList nonIdleVoiceChannelConversationsInUserSession:self.userSession]) {\n+            if (conversation.voiceChannel.state == ZMVoiceChannelStateIncomingCall) {\n+                [conversation.voiceChannel ignoreIncomingCall];\n+            }\n+            else if (conversation.voiceChannel.inNonIdleState) {\n+                [conversation.voiceChannel leave];\n+            }\n+        }\n+    }];\n+}\n+\n+- (void)configureAudioSession\n+{\n+    AVAudioSession *sessionInstance = [AVAudioSession sharedInstance];\n+    \n+    // we are going to play and record so we pick that category\n+    NSError *error = nil;\n+    [sessionInstance setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];\n+    if (error.code != 0) {\n+        ZMLogError(@\"couldn't set session's audio category: %ld\", (long)error.code);\n+    }\n+    \n+    // set the mode to voice chat\n+    [sessionInstance setMode:AVAudioSessionModeVoiceChat error:&error];\n+    if (error.code != 0) {\n+        ZMLogError(@\"couldn't set session's audio mode: %ld\", (long)error.code);\n+    }\n+    \n+    // set the session's sample rate\n+    [sessionInstance setPreferredSampleRate:16000 error:&error];\n+    if (error.code != 0) {\n+        ZMLogError(@\"couldn't set session's preferred sample rate: %ld\", (long)error.code);\n+    }\n+}\n+\n+- (BOOL)continueUserActivity:(NSUserActivity *)userActivity\n+{\n+    ZMUserSession *userSession = self.userSession;\n+    INInteraction* interaction = userActivity.interaction;\n+    if (interaction == nil) {\n+        return NO;\n+    }\n+    INIntent *intent = interaction.intent;\n+    \n+    NSArray<INPerson *> *contacts = nil;\n+    BOOL isVideo = NO;\n+    \n+    if ([intent isKindOfClass:[INStartAudioCallIntent class]]) {\n+        INStartAudioCallIntent *startAudioCallIntent = (INStartAudioCallIntent *)intent;\n+        contacts = startAudioCallIntent.contacts;\n+        isVideo = NO;\n+    }\n+    else if ([intent isKindOfClass:[INStartVideoCallIntent class]]) {\n+        INStartVideoCallIntent *startVideoCallIntent = (INStartVideoCallIntent *)intent;\n+        contacts = startVideoCallIntent.contacts;\n+        isVideo = YES;\n+    }\n+    \n+    if (1 == contacts.count) {\n+        ZMConversation *callConversation = [ZMConversation resolveConversationForPersons:contacts\n+                                                                               inContext:userSession.managedObjectContext];\n+        if (nil != callConversation) {\n+            if (isVideo) {\n+                NSError *joinError = nil;\n+                [callConversation.voiceChannel joinVideoCall:&joinError inUserSession:userSession];\n+                if (nil != joinError) {\n+                    ZMLogError(@\"Cannot start the video call: %@\", joinError);\n+                }\n+            }\n+            else {\n+                [callConversation.voiceChannel joinInUserSession:userSession];\n+            }\n+            \n+            return YES;\n+        }\n+        else {\n+            return NO;\n+        }\n+    }\n+    \n+    return YES;\n+}\n+\n+@end\n+\n+@implementation ZMCallKitDelegate (VoiceChannelObserver)\n+\n+- (void)managedObjectsDidChange:(NSNotification *)notification\n+{\n+    NSSet<ZMManagedObject *> *updateSet   = notification.userInfo[NSUpdatedObjectsKey];\n+    NSSet<ZMManagedObject *> *refreshSet  = notification.userInfo[NSRefreshedObjectsKey];\n+    NSSet<ZMManagedObject *> *insertedSet = notification.userInfo[NSInsertedObjectsKey];\n+\n+    void (^handleChangeset)(NSSet <ZMManagedObject *> *changeset) = ^(NSSet <ZMManagedObject *> *changeset) {\n+        for (id object in changeset) {\n+            if ([object isKindOfClass:[ZMConversation class]]) {\n+                [self updateConversationIfNeeded:object];\n+            }\n+        }\n+    };\n+    \n+    handleChangeset(updateSet);\n+    handleChangeset(refreshSet);\n+    handleChangeset(insertedSet);\n+}\n+\n+- (void)updateConversationIfNeeded:(ZMConversation *)conversation\n+{\n+    ZMVoiceChannelState newState = conversation.voiceChannel.state;\n+    NSNumber *knownStateNumber = self.lastConversationsState[conversation.remoteIdentifier.transportString];\n+    ZMVoiceChannelState knownState = (ZMVoiceChannelState)[knownStateNumber integerValue];\n+    if (knownStateNumber == nil || knownState != newState) {\n+        [self conversationVoiceChannelStateDidChange:conversation];\n+    }\n+    \n+    self.lastConversationsState[conversation.remoteIdentifier.transportString] = @(newState);\n+}\n+\n+- (void)conversationVoiceChannelStateDidChange:(ZMConversation *)conversation\n+{\n+    ZMLogInfo(@\"Call state %d in %@\", conversation.voiceChannel.state, conversation.displayName);\n+    \n+    switch (conversation.voiceChannel.state) {\n+    case ZMVoiceChannelStateIncomingCall:\n+            [self indicateIncomingCallInConversation:conversation];\n+        break;\n+    case ZMVoiceChannelStateSelfIsJoiningActiveChannel:\n+            [self.provider reportOutgoingCallWithUUID:conversation.remoteIdentifier\n+                              startedConnectingAtDate:[NSDate date]];\n+\n+        break;\n+    case ZMVoiceChannelStateSelfConnectedToActiveChannel:\n+            [self.provider reportOutgoingCallWithUUID:conversation.remoteIdentifier\n+                                      connectedAtDate:[NSDate date]];\n+            conversation.voiceChannel.callStartDate = [NSDate date];\n+        break;\n+    case ZMVoiceChannelStateNoActiveUsers:\n+        {\n+            CXEndCallAction *endCallAction = [[CXEndCallAction alloc] initWithCallUUID:conversation.remoteIdentifier];\n+            \n+            CXTransaction *endCallTransaction = [[CXTransaction alloc] initWithAction:endCallAction];","path":"Source/Calling/ZMCallKitDelegate.m","position":516,"original_position":516,"commit_id":"f6175ecee50044e4dfbe6f3be47fd4bec817cb93","original_commit_id":"1dd1ca22c4854a5417606d4f61494e780a592ffb","user":{"login":"basine","id":1119259,"avatar_url":"https://avatars.githubusercontent.com/u/1119259?v=3","gravatar_id":"","url":"https://api.github.com/users/basine","html_url":"https://github.com/basine","followers_url":"https://api.github.com/users/basine/followers","following_url":"https://api.github.com/users/basine/following{/other_user}","gists_url":"https://api.github.com/users/basine/gists{/gist_id}","starred_url":"https://api.github.com/users/basine/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/basine/subscriptions","organizations_url":"https://api.github.com/users/basine/orgs","repos_url":"https://api.github.com/users/basine/repos","events_url":"https://api.github.com/users/basine/events{/privacy}","received_events_url":"https://api.github.com/users/basine/received_events","type":"User","site_admin":false},"body":"Careful: voiceChannel is a transient property which will be recreated when the conversation is faulted and unfaulted. This might have unexpected side-effects for variables stored along with it.","created_at":"2016-10-24T15:04:37Z","updated_at":"2016-10-24T15:04:37Z","html_url":"https://github.com/wireapp/wire-ios-sync-engine/pull/99#discussion_r84708003","pull_request_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/pulls/99","_links":{"self":{"href":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/pulls/comments/84708003"},"html":{"href":"https://github.com/wireapp/wire-ios-sync-engine/pull/99#discussion_r84708003"},"pull_request":{"href":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/pulls/99"}}},"pull_request":{"url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/pulls/99","id":90030929,"html_url":"https://github.com/wireapp/wire-ios-sync-engine/pull/99","diff_url":"https://github.com/wireapp/wire-ios-sync-engine/pull/99.diff","patch_url":"https://github.com/wireapp/wire-ios-sync-engine/pull/99.patch","issue_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/issues/99","number":99,"state":"open","locked":false,"title":"CallKit","user":{"login":"mikeger","id":715129,"avatar_url":"https://avatars.githubusercontent.com/u/715129?v=3","gravatar_id":"","url":"https://api.github.com/users/mikeger","html_url":"https://github.com/mikeger","followers_url":"https://api.github.com/users/mikeger/followers","following_url":"https://api.github.com/users/mikeger/following{/other_user}","gists_url":"https://api.github.com/users/mikeger/gists{/gist_id}","starred_url":"https://api.github.com/users/mikeger/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mikeger/subscriptions","organizations_url":"https://api.github.com/users/mikeger/orgs","repos_url":"https://api.github.com/users/mikeger/repos","events_url":"https://api.github.com/users/mikeger/events{/privacy}","received_events_url":"https://api.github.com/users/mikeger/received_events","type":"User","site_admin":false},"body":"# Purpose\r\n\r\nAdds support for CallKit calling screens and Phone app integration.\r\n\r\n# Details\r\n\r\nZMCallKitDelegate is the main logical part of CallKit integration of wire-ios-sync-engine. CallKit is the iOS VoIP calling integration framework that allows to place VoIP calls with the same priority level as other (GSM) calls.\r\n \r\nCallKit is supported from iOS 10. On the devices that do not support it we still show the notifications.\r\n \r\nIt is possible to disable CallKit and use old-style notifications logic: `+[ZMUserSession setUseCallKit:]`.\r\n \r\nCallKit integration consists of 2 parts:\r\n\r\n I.  Showing iOS calling UI when call happens\r\n     I.a Receive calls\r\n     I.b Place calls\r\nII. Interaction with Phone app and redialing existing calls from there\r\n \r\nUsing CallKit means asking iOS to start and end the call, so it can show the proper calling UI and play the ringtone.\r\n \r\n\r\n Flow for receieving the call (I.a):\r\n![img_4952](https://cloud.githubusercontent.com/assets/715129/19639245/ee80981c-99d7-11e6-9448-dc4915288cd9.JPG)\r\n\r\n 1. BE sends the new call state in conversation via push\r\n 2. ZMCallStateTranscoder decode the payload and update the conversation/ZMVoiceChannel fields\r\n 3. ZMVoiceChannel observer sends the update to ZMCallKitDelegate -[ZMCallKitDelegate voiceChannelStateDidChange:]\r\n 4. ZMCallKitDelegate indicates the call to CallKit's CXProvider in -[ZMCallKitDelegate indicateIncomingCallInConversation:]\r\n 5. CXProvider approves the call and informs ZMCallKitDelegate that call is possible in -[ZMCallKitDelegate provider:performStartCallAction:]\r\n 6. ZMCallKitDelegate joins the call with -[ZMVoiceChannel join]\r\n \r\n\r\n\r\nFlow for sending the call (I.b):\r\n\r\n![img_4953](https://cloud.githubusercontent.com/assets/715129/19639262/06dc6954-99d8-11e6-8b7a-d1b1d7169b83.JPG)\r\n\r\n1. API consumer (UI app) calls -[ZMVoiceChannel joinInUserSession:]. This call is forwarded to -[ZMCallKitDelegate requestStartCallInConversation:]\r\n2. ZMCallKitDelegate indicates the call to CallKit's CXCallController, that asks CXProvider if call is possible. \r\n3. CXProvider is indicating the call is possible with the callback -[ZMCallKitDelegate provider:performStartCallAction:]\r\n4. ZMCallKitDelegate joins the call with -[ZMVoiceChannel join]\r\n \r\n\r\nFlow for interaction with last calls / Phone app:\r\n\r\n1. The app is launched or brought to foreground\r\n2. UIApplicationDelegate receives the call for continuing the User Activity that is forwarded to ZMUserSession\r\n3. ZMUserSession forwards the call to -[ZMCallKitDelegate continueUserActivity:]\r\n4. ZMCallKitDelegate looks up the caller from the NSUserActivity and starts the call\r\n \r\n# Requires\r\n[ ] https://github.com/wireapp/wire-ios-data-model/pull/63","created_at":"2016-10-19T15:16:24Z","updated_at":"2016-10-24T15:04:37Z","closed_at":null,"merged_at":null,"merge_commit_sha":"9070b3ca0b7bd062464df4b4a8f78ac4b8aab5e8","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/pulls/99/commits","review_comments_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/pulls/99/comments","review_comment_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/pulls/comments{/number}","comments_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/issues/99/comments","statuses_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/statuses/f6175ecee50044e4dfbe6f3be47fd4bec817cb93","head":{"label":"wireapp:feature/callkit","ref":"feature/callkit","sha":"f6175ecee50044e4dfbe6f3be47fd4bec817cb93","user":{"login":"wireapp","id":16047324,"avatar_url":"https://avatars.githubusercontent.com/u/16047324?v=3","gravatar_id":"","url":"https://api.github.com/users/wireapp","html_url":"https://github.com/wireapp","followers_url":"https://api.github.com/users/wireapp/followers","following_url":"https://api.github.com/users/wireapp/following{/other_user}","gists_url":"https://api.github.com/users/wireapp/gists{/gist_id}","starred_url":"https://api.github.com/users/wireapp/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wireapp/subscriptions","organizations_url":"https://api.github.com/users/wireapp/orgs","repos_url":"https://api.github.com/users/wireapp/repos","events_url":"https://api.github.com/users/wireapp/events{/privacy}","received_events_url":"https://api.github.com/users/wireapp/received_events","type":"Organization","site_admin":false},"repo":{"id":53574338,"name":"wire-ios-sync-engine","full_name":"wireapp/wire-ios-sync-engine","owner":{"login":"wireapp","id":16047324,"avatar_url":"https://avatars.githubusercontent.com/u/16047324?v=3","gravatar_id":"","url":"https://api.github.com/users/wireapp","html_url":"https://github.com/wireapp","followers_url":"https://api.github.com/users/wireapp/followers","following_url":"https://api.github.com/users/wireapp/following{/other_user}","gists_url":"https://api.github.com/users/wireapp/gists{/gist_id}","starred_url":"https://api.github.com/users/wireapp/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wireapp/subscriptions","organizations_url":"https://api.github.com/users/wireapp/orgs","repos_url":"https://api.github.com/users/wireapp/repos","events_url":"https://api.github.com/users/wireapp/events{/privacy}","received_events_url":"https://api.github.com/users/wireapp/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/wireapp/wire-ios-sync-engine","description":"iOS synchronization library for Wire","fork":false,"url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine","forks_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/forks","keys_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/keys{/key_id}","collaborators_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/teams","hooks_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/hooks","issue_events_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/issues/events{/number}","events_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/events","assignees_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/assignees{/user}","branches_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/branches{/branch}","tags_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/tags","blobs_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/git/refs{/sha}","trees_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/git/trees{/sha}","statuses_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/statuses/{sha}","languages_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/languages","stargazers_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/stargazers","contributors_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/contributors","subscribers_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/subscribers","subscription_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/subscription","commits_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/commits{/sha}","git_commits_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/git/commits{/sha}","comments_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/comments{/number}","issue_comment_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/issues/comments{/number}","contents_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/contents/{+path}","compare_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/compare/{base}...{head}","merges_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/merges","archive_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/downloads","issues_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/issues{/number}","pulls_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/pulls{/number}","milestones_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/milestones{/number}","notifications_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/labels{/name}","releases_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/releases{/id}","deployments_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/deployments","created_at":"2016-03-10T10:02:14Z","updated_at":"2016-10-24T14:33:54Z","pushed_at":"2016-10-24T15:01:59Z","git_url":"git://github.com/wireapp/wire-ios-sync-engine.git","ssh_url":"git@github.com:wireapp/wire-ios-sync-engine.git","clone_url":"https://github.com/wireapp/wire-ios-sync-engine.git","svn_url":"https://github.com/wireapp/wire-ios-sync-engine","homepage":"https://wire.com","size":7209,"stargazers_count":93,"watchers_count":93,"language":"Objective-C","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":8,"mirror_url":null,"open_issues_count":2,"forks":8,"open_issues":2,"watchers":93,"default_branch":"develop"}},"base":{"label":"wireapp:develop","ref":"develop","sha":"ce238a4dafbe9bf465b92e19ae655185bc73d956","user":{"login":"wireapp","id":16047324,"avatar_url":"https://avatars.githubusercontent.com/u/16047324?v=3","gravatar_id":"","url":"https://api.github.com/users/wireapp","html_url":"https://github.com/wireapp","followers_url":"https://api.github.com/users/wireapp/followers","following_url":"https://api.github.com/users/wireapp/following{/other_user}","gists_url":"https://api.github.com/users/wireapp/gists{/gist_id}","starred_url":"https://api.github.com/users/wireapp/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wireapp/subscriptions","organizations_url":"https://api.github.com/users/wireapp/orgs","repos_url":"https://api.github.com/users/wireapp/repos","events_url":"https://api.github.com/users/wireapp/events{/privacy}","received_events_url":"https://api.github.com/users/wireapp/received_events","type":"Organization","site_admin":false},"repo":{"id":53574338,"name":"wire-ios-sync-engine","full_name":"wireapp/wire-ios-sync-engine","owner":{"login":"wireapp","id":16047324,"avatar_url":"https://avatars.githubusercontent.com/u/16047324?v=3","gravatar_id":"","url":"https://api.github.com/users/wireapp","html_url":"https://github.com/wireapp","followers_url":"https://api.github.com/users/wireapp/followers","following_url":"https://api.github.com/users/wireapp/following{/other_user}","gists_url":"https://api.github.com/users/wireapp/gists{/gist_id}","starred_url":"https://api.github.com/users/wireapp/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wireapp/subscriptions","organizations_url":"https://api.github.com/users/wireapp/orgs","repos_url":"https://api.github.com/users/wireapp/repos","events_url":"https://api.github.com/users/wireapp/events{/privacy}","received_events_url":"https://api.github.com/users/wireapp/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/wireapp/wire-ios-sync-engine","description":"iOS synchronization library for Wire","fork":false,"url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine","forks_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/forks","keys_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/keys{/key_id}","collaborators_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/teams","hooks_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/hooks","issue_events_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/issues/events{/number}","events_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/events","assignees_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/assignees{/user}","branches_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/branches{/branch}","tags_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/tags","blobs_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/git/refs{/sha}","trees_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/git/trees{/sha}","statuses_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/statuses/{sha}","languages_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/languages","stargazers_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/stargazers","contributors_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/contributors","subscribers_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/subscribers","subscription_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/subscription","commits_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/commits{/sha}","git_commits_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/git/commits{/sha}","comments_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/comments{/number}","issue_comment_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/issues/comments{/number}","contents_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/contents/{+path}","compare_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/compare/{base}...{head}","merges_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/merges","archive_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/downloads","issues_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/issues{/number}","pulls_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/pulls{/number}","milestones_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/milestones{/number}","notifications_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/labels{/name}","releases_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/releases{/id}","deployments_url":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/deployments","created_at":"2016-03-10T10:02:14Z","updated_at":"2016-10-24T14:33:54Z","pushed_at":"2016-10-24T15:01:59Z","git_url":"git://github.com/wireapp/wire-ios-sync-engine.git","ssh_url":"git@github.com:wireapp/wire-ios-sync-engine.git","clone_url":"https://github.com/wireapp/wire-ios-sync-engine.git","svn_url":"https://github.com/wireapp/wire-ios-sync-engine","homepage":"https://wire.com","size":7209,"stargazers_count":93,"watchers_count":93,"language":"Objective-C","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":8,"mirror_url":null,"open_issues_count":2,"forks":8,"open_issues":2,"watchers":93,"default_branch":"develop"}},"_links":{"self":{"href":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/pulls/99"},"html":{"href":"https://github.com/wireapp/wire-ios-sync-engine/pull/99"},"issue":{"href":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/issues/99"},"comments":{"href":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/issues/99/comments"},"review_comments":{"href":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/pulls/99/comments"},"review_comment":{"href":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/pulls/99/commits"},"statuses":{"href":"https://api.github.com/repos/wireapp/wire-ios-sync-engine/statuses/f6175ecee50044e4dfbe6f3be47fd4bec817cb93"}}}}
{"actor":{"display_login":"basine"}}