Report group calls to CallKit like we do 1:1 calls
This commit is contained in:
parent
687779e80f
commit
aeecb19d2a
@ -964,12 +964,22 @@ static void uncaughtExceptionHandler(NSException *exception)
|
||||
return [CallKitIdStore threadForCallKitId:handle];
|
||||
}
|
||||
|
||||
NSData *_Nullable groupId = [CallKitCallManager decodeGroupIdFromIntentHandle:handle];
|
||||
if (groupId) {
|
||||
__block TSGroupThread *thread = nil;
|
||||
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
|
||||
thread = [TSGroupThread fetchWithGroupId:groupId transaction:transaction];
|
||||
}];
|
||||
return thread;
|
||||
}
|
||||
|
||||
for (PhoneNumber *phoneNumber in
|
||||
[PhoneNumber tryParsePhoneNumbersFromUserSpecifiedText:handle
|
||||
clientPhoneNumber:[TSAccountManager localNumber]]) {
|
||||
SignalServiceAddress *address = [[SignalServiceAddress alloc] initWithPhoneNumber:phoneNumber.toE164];
|
||||
return [TSContactThread getOrCreateThreadWithContactAddress:address];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
@ -451,6 +451,14 @@ public final class CallService: LightweightCallManager {
|
||||
}
|
||||
}
|
||||
|
||||
func handleLocalHangupCall(_ call: SignalCall) {
|
||||
if call.isIndividualCall {
|
||||
individualCallService.handleLocalHangupCall(call)
|
||||
} else {
|
||||
terminate(call: call)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up any existing call state and get ready to receive a new call.
|
||||
*/
|
||||
@ -564,13 +572,13 @@ public final class CallService: LightweightCallManager {
|
||||
func joinGroupCallIfNecessary(_ call: SignalCall) {
|
||||
owsAssertDebug(call.isGroupCall)
|
||||
|
||||
guard currentCall == nil || currentCall == call else {
|
||||
let currentCall = self.currentCall
|
||||
if currentCall == nil {
|
||||
self.currentCall = call
|
||||
} else if currentCall != call {
|
||||
return owsFailDebug("A call is already in progress")
|
||||
}
|
||||
|
||||
// The joined/joining call must always be the current call.
|
||||
currentCall = call
|
||||
|
||||
// If we're not yet connected, connect now. This may happen if, for
|
||||
// example, the call ended unexpectedly.
|
||||
if call.groupCall.localDeviceState.connectionState == .notConnected {
|
||||
@ -583,7 +591,14 @@ public final class CallService: LightweightCallManager {
|
||||
// If we're not yet joined, join now. In general, it's unexpected that
|
||||
// this method would be called when you're already joined, but it is
|
||||
// safe to do so.
|
||||
if call.groupCall.localDeviceState.joinState == .notJoined { call.groupCall.join() }
|
||||
if call.groupCall.localDeviceState.joinState == .notJoined {
|
||||
call.groupCall.join()
|
||||
// Group calls can get disconnected, but we don't count that as ending the call.
|
||||
// So this call may have already been reported.
|
||||
if call.systemState == .notReported {
|
||||
callUIAdapter.startOutgoingCall(call: call)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@ -842,7 +857,14 @@ extension CallService: CallObserver {
|
||||
}
|
||||
}
|
||||
|
||||
public func groupCallRemoteDeviceStatesChanged(_ call: SignalCall) {}
|
||||
public func groupCallRemoteDeviceStatesChanged(_ call: SignalCall) {
|
||||
if call.groupCallRingState == .ringing && !call.groupCall.remoteDeviceStates.isEmpty {
|
||||
// The first time someone joins after a ring, we need to mark the call accepted.
|
||||
// (But if we didn't ring, the call will have already been marked accepted.)
|
||||
callUIAdapter.recipientAcceptedCall(call)
|
||||
}
|
||||
}
|
||||
|
||||
public func groupCallPeekChanged(_ call: SignalCall) {
|
||||
guard let thread = call.thread as? TSGroupThread else {
|
||||
owsFailDebug("Invalid thread for call: \(call)")
|
||||
|
||||
@ -56,8 +56,8 @@ public class SignalCall: NSObject, CallManagerCallReference {
|
||||
|
||||
public let audioActivity: AudioActivity
|
||||
|
||||
private var systemState: SystemState = .notReported
|
||||
private enum SystemState {
|
||||
private(set) var systemState: SystemState = .notReported
|
||||
enum SystemState {
|
||||
case notReported
|
||||
case reported
|
||||
case removed
|
||||
@ -107,6 +107,13 @@ public class SignalCall: NSObject, CallManagerCallReference {
|
||||
}
|
||||
}
|
||||
|
||||
public var isOutgoingAudioMuted: Bool {
|
||||
switch mode {
|
||||
case .individual(let call): return call.isMuted
|
||||
case .group(let call): return call.isOutgoingAudioMuted
|
||||
}
|
||||
}
|
||||
|
||||
private(set) lazy var videoCaptureController = VideoCaptureController()
|
||||
|
||||
// Should be used only on the main thread
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
import Foundation
|
||||
import SignalRingRTC
|
||||
import UIKit
|
||||
|
||||
@objc
|
||||
protocol CallHeaderDelegate: AnyObject {
|
||||
@ -122,6 +123,13 @@ class CallHeader: UIView {
|
||||
callTitleLabel.textColor = UIColor.white
|
||||
addShadow(to: callTitleLabel)
|
||||
|
||||
#if TESTABLE_BUILD
|
||||
// For debugging purposes, make it easy to force the call to disconnect.
|
||||
callTitleLabel.addGestureRecognizer(UILongPressGestureRecognizer(target: self,
|
||||
action: #selector(injectDisconnect)))
|
||||
callTitleLabel.isUserInteractionEnabled = true
|
||||
#endif
|
||||
|
||||
vStack.addArrangedSubview(callTitleLabel)
|
||||
|
||||
// Make the title view as wide as possible, but don't overlap either button.
|
||||
@ -351,6 +359,12 @@ class CallHeader: UIView {
|
||||
groupMembersButton.updateMemberCount(remoteMemberCount + (isJoined ? 1 : 0))
|
||||
}
|
||||
|
||||
// For testing abnormal scenarios.
|
||||
@objc
|
||||
private func injectDisconnect() {
|
||||
call.groupCall.disconnect()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@ -21,6 +21,21 @@ final class CallKitCallManager: NSObject {
|
||||
|
||||
@objc
|
||||
static let kAnonymousCallHandlePrefix = "Signal:"
|
||||
static let kGroupCallHandlePrefix = "SignalGroup:"
|
||||
|
||||
@objc
|
||||
static func decodeGroupId(fromIntentHandle handle: String) -> Data? {
|
||||
let prefix = handle.prefix(kGroupCallHandlePrefix.count)
|
||||
guard prefix == kGroupCallHandlePrefix else {
|
||||
return nil
|
||||
}
|
||||
do {
|
||||
return try Data.data(fromBase64Url: String(handle[prefix.endIndex...]))
|
||||
} catch {
|
||||
// ignore the error
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
required init(showNamesOnCallScreen: Bool) {
|
||||
AssertIsOnMainThread()
|
||||
@ -39,12 +54,15 @@ final class CallKitCallManager: NSObject {
|
||||
if showNamesOnCallScreen {
|
||||
let type: CXHandle.HandleType
|
||||
let value: String
|
||||
if let phoneNumber = call.individualCall.remoteAddress.phoneNumber {
|
||||
if call.isGroupCall {
|
||||
type = .generic
|
||||
value = Self.kGroupCallHandlePrefix + call.thread.groupModelIfGroupThread!.groupId.asBase64Url
|
||||
} else if let phoneNumber = call.individualCall.remoteAddress.phoneNumber {
|
||||
type = .phoneNumber
|
||||
value = phoneNumber
|
||||
} else {
|
||||
type = .generic
|
||||
value = call.individualCall.remoteAddress.stringForDisplay
|
||||
value = call.individualCall.remoteAddress.uuidString!
|
||||
}
|
||||
handle = CXHandle(type: type, value: value)
|
||||
} else {
|
||||
@ -55,7 +73,14 @@ final class CallKitCallManager: NSObject {
|
||||
|
||||
let startCallAction = CXStartCallAction(call: call.localId, handle: handle)
|
||||
|
||||
startCallAction.isVideo = call.individualCall.offerMediaType == .video
|
||||
if call.isIndividualCall {
|
||||
startCallAction.isVideo = call.individualCall.offerMediaType == .video
|
||||
} else {
|
||||
// All group calls are video calls even if the local video is off,
|
||||
// but what we set here is how the call shows up in the system call log,
|
||||
// which controls what happens if the user starts another call from the system call log.
|
||||
startCallAction.isVideo = !call.groupCall.isOutgoingVideoMuted
|
||||
}
|
||||
|
||||
let transaction = CXTransaction()
|
||||
transaction.addAction(startCallAction)
|
||||
|
||||
@ -218,7 +218,7 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
|
||||
// to unmute the call. We can work around this
|
||||
// by ignoring the next "unmute" request from
|
||||
// CallKit after the call is answered.
|
||||
self.ignoreFirstUnmuteAfterRemoteAnswer = call.individualCall.isMuted
|
||||
self.ignoreFirstUnmuteAfterRemoteAnswer = call.isOutgoingAudioMuted
|
||||
|
||||
// Enable audio for remotely accepted calls after the session is configured.
|
||||
self.audioSession.isRTCAudioEnabled = true
|
||||
@ -235,6 +235,11 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
|
||||
AssertIsOnMainThread()
|
||||
Logger.info("")
|
||||
|
||||
guard call.systemState == .reported else {
|
||||
callService.handleLocalHangupCall(call)
|
||||
return
|
||||
}
|
||||
|
||||
Self.providerReadyFlag.runNowOrWhenDidBecomeReadySync {
|
||||
self.callManager.localHangup(call: call)
|
||||
}
|
||||
@ -346,7 +351,9 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
|
||||
// We can't wait for long before fulfilling the CXAction, else CallKit will show a "Failed Call". We don't
|
||||
// actually need to wait for the outcome of the handleOutgoingCall promise, because it handles any errors by
|
||||
// manually failing the call.
|
||||
self.callService.individualCallService.handleOutgoingCall(call)
|
||||
if call.isIndividualCall {
|
||||
self.callService.individualCallService.handleOutgoingCall(call)
|
||||
}
|
||||
|
||||
action.fulfill()
|
||||
provider.reportOutgoingCall(with: call.localId, startedConnectingAt: nil)
|
||||
@ -354,11 +361,28 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
|
||||
let update = CXCallUpdate()
|
||||
if showNamesOnCallScreen {
|
||||
update.localizedCallerName = contactsManager.displayNameWithSneakyTransaction(thread: call.thread)
|
||||
} else {
|
||||
} else if call.isIndividualCall {
|
||||
update.localizedCallerName = NSLocalizedString("CALLKIT_ANONYMOUS_CONTACT_NAME",
|
||||
comment: "The generic name used for calls if CallKit privacy is enabled")
|
||||
} else {
|
||||
update.localizedCallerName = NSLocalizedString("CALLKIT_ANONYMOUS_GROUP_NAME",
|
||||
comment: "The generic name used for group calls if CallKit privacy is enabled")
|
||||
}
|
||||
provider.reportCall(with: call.localId, updated: update)
|
||||
|
||||
if call.isGroupCall {
|
||||
switch call.groupCallRingState {
|
||||
case .shouldRing, .ringing:
|
||||
// Let CallService call recipientAcceptedCall when someone joins.
|
||||
break
|
||||
case .ringingEnded:
|
||||
Logger.warn("ringing ended before we even reported the call to CallKit (maybe our peek info was out of date)")
|
||||
fallthrough
|
||||
case .doNotRing:
|
||||
// Immediately consider ourselves connected.
|
||||
recipientAcceptedCall(call)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
||||
@ -394,7 +418,7 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
self.callService.individualCallService.handleLocalHangupCall(call)
|
||||
callService.handleLocalHangupCall(call)
|
||||
|
||||
// Signal to the system that the action has been successfully performed.
|
||||
action.fulfill()
|
||||
@ -487,7 +511,7 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
if call.individualCall.direction == .incoming {
|
||||
if call.isIndividualCall && call.individualCall.direction == .incoming {
|
||||
// Only enable audio upon activation for locally accepted calls.
|
||||
self.audioSession.isRTCAudioEnabled = true
|
||||
}
|
||||
|
||||
@ -112,6 +112,9 @@ public class CallUIAdapter: NSObject, CallServiceObserver {
|
||||
var defaultAdaptee: CallUIAdaptee { callKitAdaptee ?? nonCallKitAdaptee }
|
||||
|
||||
func adaptee(for call: SignalCall) -> CallUIAdaptee {
|
||||
guard call.isIndividualCall else {
|
||||
return defaultAdaptee
|
||||
}
|
||||
switch call.individualCall.callAdapterType {
|
||||
case .nonCallKit: return nonCallKitAdaptee
|
||||
case .default: return defaultAdaptee
|
||||
|
||||
@ -491,7 +491,7 @@ class GroupCallViewController: UIViewController {
|
||||
}
|
||||
|
||||
func dismissCall() {
|
||||
callService.terminate(call: call)
|
||||
callService.callUIAdapter.localHangupCall(call)
|
||||
|
||||
guard let splitViewSnapshot = SignalApp.shared().snapshotSplitViewController(afterScreenUpdates: false) else {
|
||||
OWSWindowManager.shared.endCall(self)
|
||||
|
||||
@ -29,7 +29,21 @@ class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee {
|
||||
let success = self.audioSession.startAudioActivity(call.audioActivity)
|
||||
assert(success)
|
||||
|
||||
self.callService.individualCallService.handleOutgoingCall(call)
|
||||
if call.isIndividualCall {
|
||||
self.callService.individualCallService.handleOutgoingCall(call)
|
||||
} else {
|
||||
switch call.groupCallRingState {
|
||||
case .shouldRing, .ringing:
|
||||
// Let CallService call recipientAcceptedCall when someone joins.
|
||||
break
|
||||
case .ringingEnded:
|
||||
owsFailDebug("ringing ended while we were starting the call")
|
||||
fallthrough
|
||||
case .doNotRing:
|
||||
// Immediately consider ourselves connected.
|
||||
recipientAcceptedCall(call)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reportIncomingCall(_ call: SignalCall, completion: @escaping (Error?) -> Void) {
|
||||
@ -129,7 +143,7 @@ class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee {
|
||||
return
|
||||
}
|
||||
|
||||
self.callService.individualCallService.handleLocalHangupCall(call)
|
||||
callService.handleLocalHangupCall(call)
|
||||
}
|
||||
|
||||
internal func remoteDidHangupCall(_ call: SignalCall) {
|
||||
|
||||
@ -781,6 +781,9 @@
|
||||
/* The generic name used for calls if CallKit privacy is enabled */
|
||||
"CALLKIT_ANONYMOUS_CONTACT_NAME" = "Signal User";
|
||||
|
||||
/* The generic name used for group calls if CallKit privacy is enabled */
|
||||
"CALLKIT_ANONYMOUS_GROUP_NAME" = "Signal Group";
|
||||
|
||||
/* Accessibility hint describing what you can do with the camera button */
|
||||
"CAMERA_BUTTON_HINT" = "Take a picture and then send it";
|
||||
|
||||
|
||||
@ -19,23 +19,39 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
return [[SDSKeyValueStore alloc] initWithCollection:@"TSStorageManagerCallKitIdToUUIDCollection"];
|
||||
}
|
||||
|
||||
+ (SDSKeyValueStore *)groupIdStore
|
||||
{
|
||||
return [[SDSKeyValueStore alloc] initWithCollection:@"TSStorageManagerCallKitIdToGroupId"];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
+ (void)setThread:(TSThread *)thread forCallKitId:(NSString *)callKitId
|
||||
{
|
||||
OWSAssertDebug(callKitId.length > 0);
|
||||
OWSAssertDebug([thread isKindOfClass:[TSContactThread class]]);
|
||||
|
||||
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
|
||||
SignalServiceAddress *address = [(TSContactThread *)thread contactAddress];
|
||||
NSString *uuidString = address.uuidString;
|
||||
if (uuidString) {
|
||||
[self.uuidStore setString:uuidString key:callKitId transaction:transaction];
|
||||
TSGroupModel *groupModel = [thread groupModelIfGroupThread];
|
||||
if (groupModel) {
|
||||
[self.groupIdStore setData:groupModel.groupId key:callKitId transaction:transaction];
|
||||
// This is probably overkill since we currently generate these IDs randomly,
|
||||
// but better futureproof than sorry.
|
||||
[self.uuidStore removeValueForKey:callKitId transaction:transaction];
|
||||
[self.phoneNumberStore removeValueForKey:callKitId transaction:transaction];
|
||||
} else {
|
||||
OWSFailDebug(@"making a call to an address with no UUID: %@", address.phoneNumber);
|
||||
[self.phoneNumberStore setString:address.phoneNumber key:callKitId transaction:transaction];
|
||||
[self.uuidStore removeValueForKey:callKitId transaction:transaction];
|
||||
OWSAssertDebug([thread isKindOfClass:[TSContactThread class]]);
|
||||
SignalServiceAddress *address = [(TSContactThread *)thread contactAddress];
|
||||
NSString *uuidString = address.uuidString;
|
||||
if (uuidString) {
|
||||
[self.uuidStore setString:uuidString key:callKitId transaction:transaction];
|
||||
[self.phoneNumberStore removeValueForKey:callKitId transaction:transaction];
|
||||
} else {
|
||||
OWSFailDebug(@"making a call to an address with no UUID: %@", address.phoneNumber);
|
||||
[self.phoneNumberStore setString:address.phoneNumber key:callKitId transaction:transaction];
|
||||
[self.uuidStore removeValueForKey:callKitId transaction:transaction];
|
||||
}
|
||||
|
||||
[self.groupIdStore removeValueForKey:callKitId transaction:transaction];
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -46,7 +62,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
__block TSThread *_Nullable result;
|
||||
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
|
||||
// Check for an ACI first, then phone numbers.
|
||||
// Most likely: modern 1:1 calls
|
||||
NSString *_Nullable uuidString = [self.uuidStore getString:callKitId transaction:transaction];
|
||||
if (uuidString) {
|
||||
SignalServiceAddress *address = [[SignalServiceAddress alloc] initWithUuidString:uuidString];
|
||||
@ -54,6 +70,14 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
return;
|
||||
}
|
||||
|
||||
// Next try group calls
|
||||
NSData *_Nullable groupId = [self.groupIdStore getData:callKitId transaction:transaction];
|
||||
if (groupId) {
|
||||
result = [TSGroupThread fetchWithGroupId:groupId transaction:transaction];
|
||||
return;
|
||||
}
|
||||
|
||||
// Finally check the phone number store, for very old 1:1 calls.
|
||||
NSString *_Nullable phoneNumber = [self.phoneNumberStore getString:callKitId transaction:transaction];
|
||||
if (phoneNumber) {
|
||||
SignalServiceAddress *address = [[SignalServiceAddress alloc] initWithPhoneNumber:phoneNumber];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user