mirror of
https://gitlab.linphone.org/BC/public/linphone-iphone.git
synced 2026-01-17 11:08:06 +00:00
Add ZRTP Popup
This commit is contained in:
parent
99b4868f7e
commit
9ef28d00f6
9 changed files with 478 additions and 95 deletions
|
|
@ -55,6 +55,7 @@
|
|||
D74C9CFF2ACAEC5E0021626A /* PopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74C9CFE2ACAEC5E0021626A /* PopupView.swift */; };
|
||||
D74C9D012ACB098C0021626A /* PermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74C9D002ACB098C0021626A /* PermissionManager.swift */; };
|
||||
D750D3392AD3E6EE00EC99C5 /* PopupLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D750D3382AD3E6EE00EC99C5 /* PopupLoadingView.swift */; };
|
||||
D75759322B56D40900E7AC10 /* ZRTPPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D75759312B56D40900E7AC10 /* ZRTPPopup.swift */; };
|
||||
D76005F62B0798B00054B79A /* IntExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76005F52B0798B00054B79A /* IntExtension.swift */; };
|
||||
D7702EF22AC7205000557C00 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7702EF12AC7205000557C00 /* WelcomeView.swift */; };
|
||||
D777DBB32AE12C5900565A99 /* ContactsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D777DBB22AE12C5900565A99 /* ContactsManager.swift */; };
|
||||
|
|
@ -148,6 +149,7 @@
|
|||
D74C9CFE2ACAEC5E0021626A /* PopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupView.swift; sourceTree = "<group>"; };
|
||||
D74C9D002ACB098C0021626A /* PermissionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionManager.swift; sourceTree = "<group>"; };
|
||||
D750D3382AD3E6EE00EC99C5 /* PopupLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupLoadingView.swift; sourceTree = "<group>"; };
|
||||
D75759312B56D40900E7AC10 /* ZRTPPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZRTPPopup.swift; sourceTree = "<group>"; };
|
||||
D76005F52B0798B00054B79A /* IntExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntExtension.swift; sourceTree = "<group>"; };
|
||||
D7702EF12AC7205000557C00 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = "<group>"; };
|
||||
D777DBB22AE12C5900565A99 /* ContactsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsManager.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -401,6 +403,14 @@
|
|||
path = Fragments;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D75759302B56D3CE00E7AC10 /* Fragments */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D75759312B56D40900E7AC10 /* ZRTPPopup.swift */,
|
||||
);
|
||||
path = Fragments;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D7702EF02AC7200600557C00 /* Welcome */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -490,6 +500,7 @@
|
|||
D7B5678C2B28883700DE63EB /* Call */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D75759302B56D3CE00E7AC10 /* Fragments */,
|
||||
D7B99E972B29B37F00BE7BF2 /* ViewModel */,
|
||||
D7B5678D2B28888F00DE63EB /* CallView.swift */,
|
||||
);
|
||||
|
|
@ -702,6 +713,7 @@
|
|||
D726E4392B16440C0083C415 /* ContactAvatarModel.swift in Sources */,
|
||||
D76005F62B0798B00054B79A /* IntExtension.swift in Sources */,
|
||||
D7E6D0512AEBDBD500A57AAF /* ContactsListBottomSheet.swift in Sources */,
|
||||
D75759322B56D40900E7AC10 /* ZRTPPopup.swift in Sources */,
|
||||
D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */,
|
||||
D7A03FC62ACC458A0081A588 /* SplashScreen.swift in Sources */,
|
||||
D7A03FC02ACC2E390081A588 /* HistoryView.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ final class CoreContext: ObservableObject {
|
|||
|
||||
Factory.Instance.logCollectionPath = configDir
|
||||
Factory.Instance.enableLogCollection(state: LogCollectionState.Enabled)
|
||||
|
||||
|
||||
Log.info("Initialising core")
|
||||
let url = NSURL(fileURLWithPath: configDir)
|
||||
if let pathComponent = url.appendingPathComponent("linphonerc") {
|
||||
|
|
@ -102,6 +102,9 @@ final class CoreContext: ObservableObject {
|
|||
|
||||
self.mCore.setUserAgent(name: "Linphone iOS 6.0 Beta (\(UIDevice.current.localizedModel)) - Linphone SDK : \(self.coreVersion)", version: "6.0")
|
||||
|
||||
self.mCore.videoCaptureEnabled = true
|
||||
self.mCore.videoDisplayEnabled = true
|
||||
|
||||
self.mCoreSuscriptions.insert(self.mCore.publisher?.onGlobalStateChanged?.postOnMainQueue { (cbVal: (core: Core, state: GlobalState, message: String)) in
|
||||
if cbVal.state == GlobalState.On {
|
||||
self.defaultAccount = self.mCore.defaultAccount
|
||||
|
|
@ -138,9 +141,6 @@ final class CoreContext: ObservableObject {
|
|||
}
|
||||
})
|
||||
|
||||
self.mCore.videoCaptureEnabled = true
|
||||
self.mCore.videoDisplayEnabled = true
|
||||
|
||||
// Create a Core listener to listen for the callback we need
|
||||
// In this case, we want to know about the account registration status
|
||||
self.mCoreSuscriptions.insert(self.mCore.publisher?.onConfiguringStatus?.postOnMainQueue { (cbVal: (core: Core, status: Config.ConfiguringState, message: String)) in
|
||||
|
|
@ -161,7 +161,7 @@ final class CoreContext: ObservableObject {
|
|||
// If account has been configured correctly, we will go through Progress and Ok states
|
||||
// Otherwise, we will be Failed.
|
||||
Log.info("New registration state is \(cbVal.state) for user id " +
|
||||
"\( String(describing: cbVal.account.params?.identityAddress?.asString())) = \(cbVal.message)\n")
|
||||
"\( String(describing: cbVal.account.params?.identityAddress?.asString())) = \(cbVal.message)\n")
|
||||
if cbVal.state == .Ok {
|
||||
self.loggingInProgress = false
|
||||
self.loggedIn = true
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
"**Micro** : Pour permettre à vos correspondants de vous entendre." : {
|
||||
|
||||
},
|
||||
"**Notifications** : Pour vous informé quand vous recevez un message ou un appel." : {
|
||||
"**Notifications** : Pour vous informer quand vous recevez un message ou un appel." : {
|
||||
|
||||
},
|
||||
"#" : {
|
||||
|
|
@ -358,6 +358,9 @@
|
|||
},
|
||||
"Last name" : {
|
||||
|
||||
},
|
||||
"Letters don't match!" : {
|
||||
|
||||
},
|
||||
"Linphone" : {
|
||||
|
||||
|
|
@ -474,6 +477,9 @@
|
|||
},
|
||||
"Remove picture" : {
|
||||
|
||||
},
|
||||
"Say %@ and click on the letters given by your correspondent:" : {
|
||||
|
||||
},
|
||||
"Scan QR code" : {
|
||||
|
||||
|
|
@ -528,6 +534,9 @@
|
|||
},
|
||||
"The user name or password is incorrects" : {
|
||||
|
||||
},
|
||||
"This call is completely securised" : {
|
||||
|
||||
},
|
||||
"This contact will be deleted definitively." : {
|
||||
|
||||
|
|
@ -581,6 +590,9 @@
|
|||
},
|
||||
"Username error" : {
|
||||
|
||||
},
|
||||
"Validate the device" : {
|
||||
|
||||
},
|
||||
"Video Call" : {
|
||||
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ class TelecomManager: ObservableObject {
|
|||
@Published var callStarted: Bool = false
|
||||
@Published var outgoingCallStarted: Bool = false
|
||||
@Published var remoteVideo: Bool = false
|
||||
@Published var isRecordingByRemote: Bool = false
|
||||
@Published var isPausedByRemote: Bool = false
|
||||
@Published var isRecordingByRemote: Bool = false
|
||||
@Published var isPausedByRemote: Bool = false
|
||||
|
||||
var actionToFulFill: CXCallAction?
|
||||
var callkitAudioSessionActivated: Bool?
|
||||
|
|
@ -130,7 +130,7 @@ class TelecomManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
private func makeRecordFilePath() -> String{
|
||||
private func makeRecordFilePath() -> String {
|
||||
var filePath = "recording_"
|
||||
let now = Date()
|
||||
let dateFormat = DateFormatter()
|
||||
|
|
@ -408,11 +408,10 @@ class TelecomManager: ObservableObject {
|
|||
case .IncomingReceived:
|
||||
let addr = call.remoteAddress
|
||||
let displayName = incomingDisplayName(call: call)
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
DispatchQueue.main.async {
|
||||
self.outgoingCallStarted = false
|
||||
self.callStarted = true
|
||||
self.callStarted = false
|
||||
if self.callInProgress == false {
|
||||
withAnimation {
|
||||
self.callInProgress = true
|
||||
|
|
|
|||
|
|
@ -49,23 +49,32 @@ struct CallView: View {
|
|||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
if #available(iOS 16.0, *), idiom != .pad {
|
||||
innerView(geometry: geo)
|
||||
.sheet(isPresented: $audioRouteSheet, onDismiss: {
|
||||
audioRouteSheet = false
|
||||
hideButtonsSheet = false
|
||||
}) {
|
||||
innerBottomSheet()
|
||||
.presentationDetents([.fraction(0.3)])
|
||||
}
|
||||
} else {
|
||||
innerView(geometry: geo)
|
||||
.halfSheet(showSheet: $audioRouteSheet) {
|
||||
innerBottomSheet()
|
||||
} onDismiss: {
|
||||
audioRouteSheet = false
|
||||
hideButtonsSheet = false
|
||||
}
|
||||
ZStack {
|
||||
if #available(iOS 16.0, *), idiom != .pad {
|
||||
innerView(geometry: geo)
|
||||
.sheet(isPresented: $audioRouteSheet, onDismiss: {
|
||||
audioRouteSheet = false
|
||||
hideButtonsSheet = false
|
||||
}) {
|
||||
innerBottomSheet()
|
||||
.presentationDetents([.fraction(0.3)])
|
||||
}
|
||||
} else {
|
||||
innerView(geometry: geo)
|
||||
.halfSheet(showSheet: $audioRouteSheet) {
|
||||
innerBottomSheet()
|
||||
} onDismiss: {
|
||||
audioRouteSheet = false
|
||||
hideButtonsSheet = false
|
||||
}
|
||||
}
|
||||
if callViewModel.zrtpPopupDisplayed == true {
|
||||
ZRTPPopup(callViewModel: callViewModel)
|
||||
.background(.black.opacity(0.65))
|
||||
.onTapGesture {
|
||||
callViewModel.zrtpPopupDisplayed = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -243,6 +252,17 @@ struct CallView: View {
|
|||
|
||||
Spacer()
|
||||
|
||||
if callViewModel.isMediaEncrypted {
|
||||
Button {
|
||||
callViewModel.showZrtpSasDialogIfPossible()
|
||||
} label: {
|
||||
Image(callViewModel.isZrtpPq ? "media-encryption-zrtp-pq" : "media-encryption-srtp")
|
||||
.resizable()
|
||||
.frame(width: 30, height: 30)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
if telecomManager.remoteVideo {
|
||||
Button {
|
||||
callViewModel.switchCamera()
|
||||
|
|
@ -263,50 +283,71 @@ struct CallView: View {
|
|||
ZStack {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
if callViewModel.remoteAddress != nil {
|
||||
let addressFriend = contactsManager.getFriendWithAddress(address: callViewModel.remoteAddress!)
|
||||
ZStack {
|
||||
|
||||
let contactAvatarModel = addressFriend != nil
|
||||
? ContactsManager.shared.avatarListModel.first(where: {
|
||||
($0.friend!.consolidatedPresence == .Online || $0.friend!.consolidatedPresence == .Busy)
|
||||
&& $0.friend!.name == addressFriend!.name
|
||||
&& $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly()
|
||||
})
|
||||
: ContactAvatarModel(friend: nil, withPresence: false)
|
||||
if callViewModel.isRemoteDeviceTrusted {
|
||||
Circle()
|
||||
.fill(Color.blueInfo500)
|
||||
.frame(width: 105, height: 105)
|
||||
}
|
||||
|
||||
if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
|
||||
if contactAvatarModel != nil {
|
||||
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 100, hidePresence: true)
|
||||
if callViewModel.remoteAddress != nil {
|
||||
let addressFriend = contactsManager.getFriendWithAddress(address: callViewModel.remoteAddress!)
|
||||
|
||||
let contactAvatarModel = addressFriend != nil
|
||||
? ContactsManager.shared.avatarListModel.first(where: {
|
||||
($0.friend!.consolidatedPresence == .Online || $0.friend!.consolidatedPresence == .Busy)
|
||||
&& $0.friend!.name == addressFriend!.name
|
||||
&& $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly()
|
||||
})
|
||||
: ContactAvatarModel(friend: nil, withPresence: false)
|
||||
|
||||
if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
|
||||
if contactAvatarModel != nil {
|
||||
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 100, hidePresence: true)
|
||||
}
|
||||
} else {
|
||||
if callViewModel.remoteAddress!.displayName != nil {
|
||||
Image(uiImage: contactsManager.textToImage(
|
||||
firstName: callViewModel.remoteAddress!.displayName!,
|
||||
lastName: callViewModel.remoteAddress!.displayName!.components(separatedBy: " ").count > 1
|
||||
? callViewModel.remoteAddress!.displayName!.components(separatedBy: " ")[1]
|
||||
: ""))
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.clipShape(Circle())
|
||||
|
||||
} else {
|
||||
Image(uiImage: contactsManager.textToImage(
|
||||
firstName: callViewModel.remoteAddress!.username ?? "Username Error",
|
||||
lastName: callViewModel.remoteAddress!.username!.components(separatedBy: " ").count > 1
|
||||
? callViewModel.remoteAddress!.username!.components(separatedBy: " ")[1]
|
||||
: ""))
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
if callViewModel.remoteAddress!.displayName != nil {
|
||||
Image(uiImage: contactsManager.textToImage(
|
||||
firstName: callViewModel.remoteAddress!.displayName!,
|
||||
lastName: callViewModel.remoteAddress!.displayName!.components(separatedBy: " ").count > 1
|
||||
? callViewModel.remoteAddress!.displayName!.components(separatedBy: " ")[1]
|
||||
: ""))
|
||||
Image("profil-picture-default")
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.clipShape(Circle())
|
||||
|
||||
} else {
|
||||
Image(uiImage: contactsManager.textToImage(
|
||||
firstName: callViewModel.remoteAddress!.username ?? "Username Error",
|
||||
lastName: callViewModel.remoteAddress!.username!.components(separatedBy: " ").count > 1
|
||||
? callViewModel.remoteAddress!.username!.components(separatedBy: " ")[1]
|
||||
: ""))
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
Image("profil-picture-default")
|
||||
.resizable()
|
||||
|
||||
if callViewModel.isRemoteDeviceTrusted {
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Image("trusted")
|
||||
.resizable()
|
||||
.frame(width: 25, height: 25)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.frame(width: 100, height: 100)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
|
||||
Text(callViewModel.displayName)
|
||||
|
|
|
|||
180
Linphone/UI/Call/Fragments/ZRTPPopup.swift
Normal file
180
Linphone/UI/Call/Fragments/ZRTPPopup.swift
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-iphone
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
struct ZRTPPopup: View {
|
||||
|
||||
@ObservedObject private var telecomManager = TelecomManager.shared
|
||||
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
|
||||
|
||||
@ObservedObject var callViewModel: CallViewModel
|
||||
|
||||
@State private var letters1: String = "AA"
|
||||
@State private var letters2: String = "BB"
|
||||
@State private var letters3: String = "CC"
|
||||
@State private var letters4: String = "DD"
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
VStack(alignment: .leading) {
|
||||
Text("Validate the device")
|
||||
.default_text_style_600(styleSize: 20)
|
||||
|
||||
Text("Say \(callViewModel.upperCaseAuthTokenToRead) and click on the letters given by your correspondent:")
|
||||
.default_text_style(styleSize: 15)
|
||||
.padding(.bottom, 20)
|
||||
|
||||
HStack(spacing: 25) {
|
||||
Spacer()
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Text(letters1)
|
||||
.default_text_style(styleSize: 30)
|
||||
.frame(width: 60, height: 60)
|
||||
}
|
||||
.padding(10)
|
||||
.background(Color.grayMain2c200)
|
||||
.cornerRadius(40)
|
||||
.onTapGesture {
|
||||
callViewModel.lettersClicked(letters: letters1)
|
||||
callViewModel.zrtpPopupDisplayed = false
|
||||
}
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Text(letters2)
|
||||
.default_text_style(styleSize: 30)
|
||||
.frame(width: 60, height: 60)
|
||||
}
|
||||
.padding(10)
|
||||
.background(Color.grayMain2c200)
|
||||
.cornerRadius(40)
|
||||
.onTapGesture {
|
||||
callViewModel.lettersClicked(letters: letters2)
|
||||
callViewModel.zrtpPopupDisplayed = false
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.bottom, 20)
|
||||
|
||||
HStack(spacing: 25) {
|
||||
Spacer()
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Text(letters3)
|
||||
.default_text_style(styleSize: 30)
|
||||
.frame(width: 60, height: 60)
|
||||
}
|
||||
.padding(10)
|
||||
.background(Color.grayMain2c200)
|
||||
.cornerRadius(40)
|
||||
.onTapGesture {
|
||||
callViewModel.lettersClicked(letters: letters3)
|
||||
callViewModel.zrtpPopupDisplayed = false
|
||||
}
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Text(letters4)
|
||||
.default_text_style(styleSize: 30)
|
||||
.frame(width: 60, height: 60)
|
||||
}
|
||||
.padding(10)
|
||||
.background(Color.grayMain2c200)
|
||||
.cornerRadius(40)
|
||||
.onTapGesture {
|
||||
callViewModel.lettersClicked(letters: letters4)
|
||||
callViewModel.zrtpPopupDisplayed = false
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.bottom, 20)
|
||||
|
||||
HStack {
|
||||
Text("Skip")
|
||||
.underline()
|
||||
.tint(Color.grayMain2c600)
|
||||
.default_text_style_600(styleSize: 15)
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.bottom, 30)
|
||||
.onTapGesture {
|
||||
callViewModel.zrtpPopupDisplayed = false
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
callViewModel.zrtpPopupDisplayed = false
|
||||
}, label: {
|
||||
Text("Letters don't match!")
|
||||
.default_text_style_orange_600(styleSize: 20)
|
||||
.frame(height: 35)
|
||||
.frame(maxWidth: .infinity)
|
||||
})
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 10)
|
||||
.cornerRadius(60)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 60)
|
||||
.inset(by: 0.5)
|
||||
.stroke(Color.orangeMain500, lineWidth: 1)
|
||||
)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 20)
|
||||
.background(.white)
|
||||
.cornerRadius(20)
|
||||
.padding(.horizontal)
|
||||
.frame(maxHeight: .infinity)
|
||||
.shadow(color: Color.orangeMain500, radius: 0, x: 0, y: 2)
|
||||
.frame(maxWidth: sharedMainViewModel.maxWidth)
|
||||
.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
|
||||
.onAppear {
|
||||
|
||||
var random = SystemRandomNumberGenerator()
|
||||
let correctLetters = Int(random.next(upperBound: UInt32(4)))
|
||||
|
||||
letters1 = (correctLetters == 0) ? callViewModel.upperCaseAuthTokenToListen : self.randomAlphanumericString(2)
|
||||
letters2 = (correctLetters == 1) ? callViewModel.upperCaseAuthTokenToListen : self.randomAlphanumericString(2)
|
||||
letters3 = (correctLetters == 2) ? callViewModel.upperCaseAuthTokenToListen : self.randomAlphanumericString(2)
|
||||
letters4 = (correctLetters == 3) ? callViewModel.upperCaseAuthTokenToListen : self.randomAlphanumericString(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func randomAlphanumericString(_ length: Int) -> String {
|
||||
let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
let len = UInt32(letters.count)
|
||||
var random = SystemRandomNumberGenerator()
|
||||
var randomString = ""
|
||||
for _ in 0..<length {
|
||||
let randomIndex = Int(random.next(upperBound: len))
|
||||
let randomCharacter = letters[letters.index(letters.startIndex, offsetBy: randomIndex)]
|
||||
randomString.append(randomCharacter)
|
||||
}
|
||||
return randomString
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ZRTPPopup(callViewModel: CallViewModel())
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
import SwiftUI
|
||||
import linphonesw
|
||||
import AVFAudio
|
||||
import Combine
|
||||
|
||||
class CallViewModel: ObservableObject {
|
||||
|
||||
|
|
@ -36,11 +37,19 @@ class CallViewModel: ObservableObject {
|
|||
@Published var isRemoteRecording: Bool = false
|
||||
@Published var isPaused: Bool = false
|
||||
@Published var timeElapsed: Int = 0
|
||||
@Published var zrtpPopupDisplayed: Bool = false
|
||||
@Published var upperCaseAuthTokenToRead = ""
|
||||
@Published var upperCaseAuthTokenToListen = ""
|
||||
@Published var isMediaEncrypted: Bool = false
|
||||
@Published var isZrtpPq: Bool = false
|
||||
@Published var isRemoteDeviceTrusted: Bool = false
|
||||
|
||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
|
||||
var currentCall: Call?
|
||||
|
||||
private var callSuscriptions = Set<AnyCancellable?>()
|
||||
|
||||
init() {
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: .allowBluetooth)
|
||||
|
|
@ -78,6 +87,10 @@ class CallViewModel: ObservableObject {
|
|||
self.isPaused = self.isCallPaused()
|
||||
self.timeElapsed = 0
|
||||
}
|
||||
|
||||
self.callSuscriptions.insert(self.currentCall!.publisher?.onEncryptionChanged?.postOnMainQueue {(cbVal: (call: Call, on: Bool, authenticationToken: String?)) in
|
||||
_ = self.updateEncryption()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -268,4 +281,105 @@ class CallViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lettersClicked(letters: String) {
|
||||
let verified = letters == self.upperCaseAuthTokenToListen
|
||||
Log.info(
|
||||
"[ZRTPPopup] User clicked on \(verified ? "right" : "wrong") letters"
|
||||
)
|
||||
|
||||
if verified {
|
||||
coreContext.doOnCoreQueue { core in
|
||||
if core.currentCall != nil {
|
||||
core.currentCall!.authenticationTokenVerified = verified
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateEncryption() -> Bool {
|
||||
if currentCall != nil && currentCall!.currentParams != nil {
|
||||
switch currentCall!.currentParams!.mediaEncryption {
|
||||
case MediaEncryption.ZRTP:
|
||||
let authToken = currentCall!.authenticationToken
|
||||
let isDeviceTrusted = currentCall!.authenticationTokenVerified && authToken != nil
|
||||
|
||||
Log.info(
|
||||
"[CallViewModel] Current call media encryption is ZRTP, auth token is \(isDeviceTrusted ? "trusted" : "not trusted yet")"
|
||||
)
|
||||
|
||||
isRemoteDeviceTrusted = isDeviceTrusted
|
||||
|
||||
if isDeviceTrusted {
|
||||
ToastViewModel.shared.toastMessage = "Info_call_securised"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
}
|
||||
|
||||
/*
|
||||
let securityLevel = isDeviceTrusted ? SecurityLevel.Safe : SecurityLevel.Encrypted
|
||||
let avatarModel = contact
|
||||
if (avatarModel != nil) {
|
||||
avatarModel.trust.postValue(securityLevel)
|
||||
contact.postValue(avatarModel!!)
|
||||
} else {
|
||||
Log.error("$TAG No avatar model found!")
|
||||
}
|
||||
*/
|
||||
|
||||
isMediaEncrypted = true
|
||||
// When Post Quantum is available, ZRTP is Post Quantum
|
||||
isZrtpPq = Core.getPostQuantumAvailable
|
||||
|
||||
if !isDeviceTrusted && authToken != nil && !authToken!.isEmpty {
|
||||
Log.info("[CallViewModel] Showing ZRTP SAS confirmation dialog")
|
||||
showZrtpSasDialog(authToken: authToken!)
|
||||
}
|
||||
|
||||
return isDeviceTrusted
|
||||
case MediaEncryption.SRTP, MediaEncryption.DTLS:
|
||||
isMediaEncrypted = true
|
||||
isZrtpPq = false
|
||||
return false
|
||||
default:
|
||||
isMediaEncrypted = false
|
||||
isZrtpPq = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func showZrtpSasDialogIfPossible() {
|
||||
if currentCall != nil && currentCall!.currentParams != nil && currentCall!.currentParams!.mediaEncryption == MediaEncryption.ZRTP {
|
||||
let authToken = currentCall!.authenticationToken
|
||||
let isDeviceTrusted = currentCall!.authenticationTokenVerified && authToken != nil
|
||||
Log.info(
|
||||
"[CallViewModel] Current call media encryption is ZRTP, auth token is \(isDeviceTrusted ? "trusted" : "not trusted yet")"
|
||||
)
|
||||
if (authToken != nil && !authToken!.isEmpty) {
|
||||
showZrtpSasDialog(authToken: authToken!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showZrtpSasDialog(authToken: String) {
|
||||
if self.currentCall != nil {
|
||||
let upperCaseAuthToken = authToken.localizedUppercase
|
||||
|
||||
let mySubstringPrefix = upperCaseAuthToken.prefix(2)
|
||||
|
||||
let mySubstringSuffix = upperCaseAuthToken.suffix(2)
|
||||
|
||||
switch self.currentCall!.dir {
|
||||
case Call.Dir.Incoming:
|
||||
self.upperCaseAuthTokenToRead = String(mySubstringPrefix)
|
||||
self.upperCaseAuthTokenToListen = String(mySubstringSuffix)
|
||||
default:
|
||||
self.upperCaseAuthTokenToRead = String(mySubstringSuffix)
|
||||
self.upperCaseAuthTokenToListen = String(mySubstringPrefix)
|
||||
}
|
||||
|
||||
self.zrtpPopupDisplayed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,11 +27,17 @@ struct ToastView: View {
|
|||
VStack {
|
||||
if toastViewModel.displayToast {
|
||||
HStack {
|
||||
Image(toastViewModel.toastMessage.contains("Success") ? "check" : "warning-circle")
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.foregroundStyle(toastViewModel.toastMessage.contains("Success") ? Color.greenSuccess500 : Color.redDanger500)
|
||||
if toastViewModel.toastMessage.contains("Info_") {
|
||||
Image("trusted")
|
||||
.resizable()
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
} else {
|
||||
Image(toastViewModel.toastMessage.contains("Success") ? "check" : "warning-circle")
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.foregroundStyle(toastViewModel.toastMessage.contains("Success") ? Color.greenSuccess500 : Color.redDanger500)
|
||||
}
|
||||
|
||||
switch toastViewModel.toastMessage {
|
||||
case "Successful":
|
||||
|
|
@ -68,7 +74,14 @@ struct ToastView: View {
|
|||
.foregroundStyle(Color.greenSuccess500)
|
||||
.default_text_style(styleSize: 15)
|
||||
.padding(8)
|
||||
|
||||
|
||||
case "Info_call_securised":
|
||||
Text("This call is completely securised")
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(Color.blueInfo500)
|
||||
.default_text_style(styleSize: 15)
|
||||
.padding(8)
|
||||
|
||||
case let str where str.contains("is recording"):
|
||||
Text(toastViewModel.toastMessage)
|
||||
.multilineTextAlignment(.center)
|
||||
|
|
@ -111,7 +124,7 @@ struct ToastView: View {
|
|||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 50)
|
||||
.inset(by: 0.5)
|
||||
.stroke(toastViewModel.toastMessage.contains("Success") ? Color.greenSuccess500 : Color.redDanger500, lineWidth: 1)
|
||||
.stroke(toastViewModel.toastMessage.contains("Success") ? Color.greenSuccess500 : (toastViewModel.toastMessage.contains("Info_") ? Color.blueInfo500 : Color.redDanger500), lineWidth: 1)
|
||||
)
|
||||
.onTapGesture {
|
||||
if !toastViewModel.toastMessage.contains("is recording") {
|
||||
|
|
|
|||
|
|
@ -1,33 +1,45 @@
|
|||
//
|
||||
// ActivityIndicator.swift
|
||||
// Linphone
|
||||
//
|
||||
// Created by Martins Benoît on 13/12/2023.
|
||||
//
|
||||
/*
|
||||
* Copyright (c) 2010-2024 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-iphone
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ActivityIndicator: View {
|
||||
|
||||
let style = StrokeStyle(lineWidth: 3, lineCap: .round)
|
||||
@State var animate = false
|
||||
let color1 = Color.white
|
||||
let color2 = Color.white.opacity(0.5)
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.trim(from: 0, to: 0.7)
|
||||
.stroke(
|
||||
AngularGradient(gradient: .init(colors: [color1, color2]), center: .center), style: style)
|
||||
.rotationEffect(Angle(degrees: animate ? 360: 0))
|
||||
.animation(Animation.linear(duration: 0.7).repeatForever(autoreverses: false), value: UUID())
|
||||
}.onAppear {
|
||||
self.animate.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
let style = StrokeStyle(lineWidth: 3, lineCap: .round)
|
||||
@State var animate = false
|
||||
let color1 = Color.white
|
||||
let color2 = Color.white.opacity(0.5)
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.trim(from: 0, to: 0.7)
|
||||
.stroke(
|
||||
AngularGradient(gradient: .init(colors: [color1, color2]), center: .center), style: style)
|
||||
.rotationEffect(Angle(degrees: animate ? 360: 0))
|
||||
.animation(Animation.linear(duration: 0.7).repeatForever(autoreverses: false), value: UUID())
|
||||
}.onAppear {
|
||||
self.animate.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ActivityIndicator()
|
||||
ActivityIndicator()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue