diff --git a/KitchenSink.xcodeproj/project.pbxproj b/KitchenSink.xcodeproj/project.pbxproj index 65d3ec3..18fa7aa 100644 --- a/KitchenSink.xcodeproj/project.pbxproj +++ b/KitchenSink.xcodeproj/project.pbxproj @@ -1865,7 +1865,7 @@ "@executable_path/Frameworks", executable_path/Frameworks, ); - MARKETING_VERSION = 3.16.1; + MARKETING_VERSION = 3.16.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.webex.sdk.KitchenSinkv3.0; @@ -1921,7 +1921,7 @@ "@executable_path/Frameworks", executable_path/Frameworks, ); - MARKETING_VERSION = 3.16.1; + MARKETING_VERSION = 3.16.2; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.webex.sdk.KitchenSinkv3.0; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/KitchenSink/NewUI/Utils/Constants.swift b/KitchenSink/NewUI/Utils/Constants.swift index 815066d..f56d6ac 100644 --- a/KitchenSink/NewUI/Utils/Constants.swift +++ b/KitchenSink/NewUI/Utils/Constants.swift @@ -6,6 +6,7 @@ class Constants { public static let loginTypeKey = "loginType" public static let emailKey = "userEmail" public static let selfId = "selfId" + public static let crashAutoUploadEnabledKey = "crashAutoUploadEnabled" public enum loginTypeValue: String { case email = "auth" diff --git a/KitchenSink/NewUI/ViewModels/CallViewModel.swift b/KitchenSink/NewUI/ViewModels/CallViewModel.swift index 893b360..f261282 100644 --- a/KitchenSink/NewUI/ViewModels/CallViewModel.swift +++ b/KitchenSink/NewUI/ViewModels/CallViewModel.swift @@ -144,6 +144,7 @@ class CallViewModel: ObservableObject @Published var placeholderText1 = "" @Published var placeholderText2 = "" @Published var speechEnhancement = false + @Published var isRecordingAudioDump = false let renderModes: [Call.VideoRenderMode] = [.fit, .cropFill, .stretchFill] let flashModes: [Call.FlashMode] = [.on, .off, .auto] let torchModes: [Call.TorchMode] = [.on, .off, .auto] @@ -864,6 +865,7 @@ class CallViewModel: ObservableObject self?.isClosedCaptionAllowed = call.isClosedCaptionAllowed self?.isClosedCaptionEnabled = call.isClosedCaptionEnabled self?.speechEnhancement = call.isSpeechEnhancementEnabled + self?.isRecordingAudioDump = call.isRecordingAudioDump } self.updateNameLabels(connected: call.isConnected) } @@ -886,10 +888,66 @@ class CallViewModel: ObservableObject // Handles more Options. func handleMoreClickAction() { + refreshAudioDumpRecordingState() DispatchQueue.main.async { [weak self] in self?.showMoreOptions = true } } + + func refreshAudioDumpRecordingState() { + DispatchQueue.main.async { [weak self] in + self?.isRecordingAudioDump = self?.currentCall?.isRecordingAudioDump ?? false + } + } + + func handleAudioDumpAction() { + guard let call = currentCall else { + showError("Audio Dump Error", "Call not found") + return + } + + if call.isRecordingAudioDump { + stopRecordingAudioDump(call: call) + } else { + startRecordingAudioDump(call: call) + } + } + + private func startRecordingAudioDump(call: CallProtocol) { + call.canStartRecordingAudioDump { [weak self] error in + if let error = error { + self?.showSlideInMessage(message: "Start audio dump error \(String(describing: error))") + self?.refreshAudioDumpRecordingState() + return + } + + call.startRecordingAudioDump { [weak self] error in + if let error = error { + self?.showSlideInMessage(message: "Start audio dump error \(String(describing: error))") + } else { + self?.showSlideInMessage(message: "Started recording audio dump") + } + self?.refreshAudioDumpRecordingState() + } + } + } + + private func stopRecordingAudioDump(call: CallProtocol) { + guard call.isRecordingAudioDump else { + showSlideInMessage(message: "Stop audio dump error: Not currently recording") + refreshAudioDumpRecordingState() + return + } + + call.stopRecordingAudioDump { [weak self] error in + if let error = error { + self?.showSlideInMessage(message: "Stop audio dump error \(String(describing: error))") + } else { + self?.showSlideInMessage(message: "Stopped recording audio dump") + } + self?.refreshAudioDumpRecordingState() + } + } // Handles HoldCall Action. func handleHoldCallAction() { diff --git a/KitchenSink/NewUI/ViewModels/SettingsViewModel.swift b/KitchenSink/NewUI/ViewModels/SettingsViewModel.swift index 7646e8f..cf94e1b 100644 --- a/KitchenSink/NewUI/ViewModels/SettingsViewModel.swift +++ b/KitchenSink/NewUI/ViewModels/SettingsViewModel.swift @@ -19,6 +19,7 @@ class SettingsViewModel: ObservableObject { @Published var enable1080pVideo = false @Published var useLegacyNoiseRemoval = false @Published var enableSpeechEnhancement = false + @Published var isCrashAutoUploadEnabled: Bool @Published var showError: Bool = false @Published var error: String = "" @Published var videoStreamModeLabel = "" @@ -33,6 +34,7 @@ class SettingsViewModel: ObservableObject { self.profile = profile self.messagingViewModel = messagingVM self.mailVM = mailVM + self.isCrashAutoUploadEnabled = UserDefaults.standard.bool(forKey: Constants.crashAutoUploadEnabledKey) } /// Asynchronously displays an error message on the main queue. @@ -84,6 +86,31 @@ class SettingsViewModel: ObservableObject { } } } + + func uploadDiagnosticLogs(completion: @escaping (String, String) -> Void) { + guard let webex = webex else { + completion("Upload Diagnostic Logs Failure", "Webex instance is not available.") + return + } + + isLoading = true + webex.uploadDiagnosticLogs { [weak self] result in + DispatchQueue.main.async { + self?.isLoading = false + switch result { + case .success(let response): + switch response.result { + case .noError: + completion("Upload Diagnostic Logs Success", response.feedbackId ?? "") + default: + completion("Upload Diagnostic Logs Failure", response.result.rawValue) + } + case .failure(let error): + completion("Upload Diagnostic Logs Failure", error.localizedDescription) + } + } + } + } func updateToggles() { @@ -92,6 +119,7 @@ class SettingsViewModel: ObservableObject { enable1080pVideo = UserDefaults.standard.bool(forKey: "VideoRes1080p") enableBackgroundConnection = UserDefaults.standard.bool(forKey: "backgroundConnection") useLegacyNoiseRemoval = UserDefaults.standard.bool(forKey: "legacyNoiseRemoval") + isCrashAutoUploadEnabled = UserDefaults.standard.bool(forKey: Constants.crashAutoUploadEnabledKey) enableSpeechEnhancement = webexPhone.isSpeechEnhancementEnabled } @@ -150,4 +178,39 @@ class SettingsViewModel: ObservableObject { } }) } + + func setCrashAutoUploadEnabled(_ isEnabled: Bool) { + isCrashAutoUploadEnabled = isEnabled + UserDefaults.standard.set(isEnabled, forKey: Constants.crashAutoUploadEnabledKey) + UserDefaults.standard.synchronize() + webex?.isCrashReportingEnabled = isEnabled + } + + func triggerSDKCrashForTesting() { + webex?.triggerSDKCrashForTesting() + } + + func triggerNullDereferenceCrashForTesting() { + webex?.triggerNullDereferenceCrashForTesting() + } + + func triggerIllegalInstructionCrashForTesting() { + webex?.triggerIllegalInstructionCrashForTesting() + } + + func triggerStackOverflowCrashForTesting() { + webex?.triggerStackOverflowCrashForTesting() + } + + func triggerUncaughtExceptionForTesting() { + webex?.triggerUncaughtExceptionForTesting() + } + + func triggerSIGFPECrashForTesting() { + webex?.triggerSIGFPECrashForTesting() + } + + func triggerSIGBUSCrashForTesting() { + webex?.triggerSIGBUSCrashForTesting() + } } diff --git a/KitchenSink/NewUI/Views/MoreOptionsCallView.swift b/KitchenSink/NewUI/Views/MoreOptionsCallView.swift index e4ab6c2..dcc1726 100644 --- a/KitchenSink/NewUI/Views/MoreOptionsCallView.swift +++ b/KitchenSink/NewUI/Views/MoreOptionsCallView.swift @@ -82,6 +82,16 @@ struct MoreOptionsCallView: View { } .accessibilityIdentifier("speechEnhancementToggle") } + + Section(header: Text("Troubleshooting")) { + Button(action: { + callingVM.handleAudioDumpAction() + }) { + Text(callingVM.isRecordingAudioDump ? "Stop Audio Dump" : "Start Audio Dump") + .foregroundStyle(Color.primary) + } + .accessibilityIdentifier("audioDumpToggle") + } if callingVM.isCUCMOrWxcCall { Section(header: Text("WxC/CUCUM Calling")) { @@ -348,6 +358,9 @@ struct MoreOptionsCallView: View { } } } + .onAppear { + callingVM.refreshAudioDumpRecordingState() + } } private func updateSettings() { diff --git a/KitchenSink/NewUI/Views/SettingsView.swift b/KitchenSink/NewUI/Views/SettingsView.swift index f6b64f9..b4a04be 100644 --- a/KitchenSink/NewUI/Views/SettingsView.swift +++ b/KitchenSink/NewUI/Views/SettingsView.swift @@ -17,6 +17,7 @@ struct SettingsView: View { @State private var isPhoneServicesOn = false @State private var showSetupView = false @State private var isSpeechEnhancementEnabled: Bool = true + @State private var showSimulateCrashAlert = false @Environment(\.dismiss) var dismiss @@ -108,6 +109,15 @@ struct SettingsView: View { }.onTapGesture { model.updateBackgroundConnection() } + + Toggle( + "Crash Auto Upload", + isOn: Binding( + get: { model.isCrashAutoUploadEnabled }, + set: { model.setCrashAutoUploadEnabled($0) } + ) + ) + .accessibilityIdentifier("crashAutoUpload") Text("Incoming Call") .onTapGesture { @@ -117,6 +127,16 @@ struct SettingsView: View { .onTapGesture { getAccessToken() } + Text("Upload Diagnostic Logs") + .onTapGesture { + uploadDiagnosticLogs() + } + Text("Simulate Crash") + .foregroundColor(.red) + .onTapGesture { + showSimulateCrashAlert = true + } + .accessibilityIdentifier("simulateCrash") NavigationLink(destination: CameraSettingView(cameraSettingVM: CameraSettingViewModel())) { Text("Camera Settings") .accessibilityIdentifier("cameraSettings") @@ -210,6 +230,32 @@ struct SettingsView: View { } message: { Text("Token Copied") } + .confirmationDialog("Simulate Crash", isPresented: $showSimulateCrashAlert, titleVisibility: .visible) { + Button("SDK Exception", role: .destructive) { + model.triggerUncaughtExceptionForTesting() + } + Button("Crash SDK", role: .destructive) { + model.triggerSDKCrashForTesting() + } + Button("Null Deref (SIGSEGV)", role: .destructive) { + model.triggerNullDereferenceCrashForTesting() + } + Button("Illegal Instruction (SIGILL)", role: .destructive) { + model.triggerIllegalInstructionCrashForTesting() + } + Button("Stack Overflow", role: .destructive) { + model.triggerStackOverflowCrashForTesting() + } + Button("Div-by-Zero (SIGFPE)", role: .destructive) { + model.triggerSIGFPECrashForTesting() + } + Button("Bus Error (SIGBUS)", role: .destructive) { + model.triggerSIGBUSCrashForTesting() + } + Button("Cancel", role: .cancel) {} + } message: { + Text("Use these KitchenSink actions to verify crash detection and deferred log upload on next launch.") + } .sheet(isPresented: $model.mailVM.isShowing) { MailView(viewModel: model.mailVM) } @@ -236,6 +282,14 @@ struct SettingsView: View { isAlertPresented = true } } + + private func uploadDiagnosticLogs() { + model.uploadDiagnosticLogs { (title, message) in + alertTitle = title + alertMessage = message + isAlertPresented = true + } + } /// Configures the mail message to be sent private func configureMailMessage() { diff --git a/KitchenSink/NewUI/Webex/WebexCall.swift b/KitchenSink/NewUI/Webex/WebexCall.swift index 58ed20d..d683516 100644 --- a/KitchenSink/NewUI/Webex/WebexCall.swift +++ b/KitchenSink/NewUI/Webex/WebexCall.swift @@ -23,6 +23,7 @@ public protocol CallProtocol: AnyObject { var isClosedCaptionEnabled: Bool {get} var isClosedCaptionAllowed: Bool {get} var isSpeechEnhancementEnabled: Bool {get} + var isRecordingAudioDump: Bool {get} var videoRenderViews: (local: MediaRenderView?, remote: MediaRenderView?) {get set} var screenShareView: MediaRenderView? {get set} var mediaStream: MediaStream? {get set} @@ -122,6 +123,9 @@ public protocol CallProtocol: AnyObject { func enableWXA(isEnabled: Bool, callback:@escaping ((Bool)->Void)) -> Void func send(dtmfCode: String, completionHandler: ((Error?) -> Void)?) func enableSpeechEnhancement(shouldEnable: Bool, completionHandler: @escaping (Result) -> Void) + func canStartRecordingAudioDump(completionHandler: @escaping (Error?) -> Void) + func startRecordingAudioDump(completionHandler: @escaping (Error?) -> Void) + func stopRecordingAudioDump(completionHandler: @escaping (Error?) -> Void) } @available(iOS 16.0, *) @@ -282,6 +286,10 @@ class CallKS: CallProtocol } } + var isRecordingAudioDump: Bool { + return call?.isRecordingAudioDump ?? false + } + private var call: Call? init(call: WebexSDK.Call) { @@ -698,6 +706,18 @@ class CallKS: CallProtocol public func enableSpeechEnhancement(shouldEnable: Bool, completionHandler: @escaping (Result) -> Void) { call?.enableReceiverSpeechEnhancement(shouldEnable: shouldEnable, completionHandler: completionHandler) } + + public func canStartRecordingAudioDump(completionHandler: @escaping (Error?) -> Void) { + call?.canStartRecordingAudioDump(completionHandler: completionHandler) + } + + public func startRecordingAudioDump(completionHandler: @escaping (Error?) -> Void) { + call?.startRecordingAudioDump(completionHandler: completionHandler) + } + + public func stopRecordingAudioDump(completionHandler: @escaping (Error?) -> Void) { + call?.stopRecordingAudioDump(completionHandler: completionHandler) + } } extension Call.BreakoutSession: Hashable { diff --git a/KitchenSink/NewUI/Webex/WebexKS.swift b/KitchenSink/NewUI/Webex/WebexKS.swift index 97a3fae..9f22e11 100644 --- a/KitchenSink/NewUI/Webex/WebexKS.swift +++ b/KitchenSink/NewUI/Webex/WebexKS.swift @@ -145,15 +145,12 @@ class WebexManager { } func initializeWebex(completionHandler: @escaping (Bool) -> Void) { - if let webex = webex, webex.authenticator?.authorized == true { - completionHandler(true) - return - } if webex != nil { webex.enableConsoleLogger = true // Do not set this to true in production unless you want to print logs in prod webex.authDelegate = AppDelegate.shared webex.logLevel = .verbose + configureCrashReporting(for: webex) DispatchQueue.main.async { webex.initialize { success in print("webex.initialize: " + "\(success)") @@ -168,6 +165,11 @@ class WebexManager { } } + private func configureCrashReporting(for webex: Webex) { + let persistedValue = UserDefaults.standard.bool(forKey: Constants.crashAutoUploadEnabledKey) + webex.isCrashReportingEnabled = persistedValue + } + func initWebexUsingOauth() { guard let path = Bundle.main.path(forResource: "Secrets", ofType: "plist") else { return } guard let keys = NSDictionary(contentsOfFile: path) else { return } diff --git a/Podfile b/Podfile index b38ddfc..2e9e61f 100644 --- a/Podfile +++ b/Podfile @@ -7,10 +7,10 @@ target 'KitchenSink' do use_frameworks! # Pods for KitchenSink - pod 'WebexSDK','~> 3.16.1' - # pod 'WebexSDK/Meeting','~> 3.16.1' # Uncomment this line and comment the above line for Meeting-only SDK - # pod 'WebexSDK/Wxc','~> 3.16.1' # Uncomment this line and comment the above line for Calling-only SDK - # pod 'WebexSDK/Message','~> 3.16.1' # Uncomment this line and comment the above line for Message-only SDK + pod 'WebexSDK','~> 3.16.2' + # pod 'WebexSDK/Meeting','~> 3.16.2' # Uncomment this line and comment the above line for Meeting-only SDK + # pod 'WebexSDK/Wxc','~> 3.16.2' # Uncomment this line and comment the above line for Calling-only SDK + # pod 'WebexSDK/Message','~> 3.16.2' # Uncomment this line and comment the above line for Message-only SDK target 'KitchenSinkUITests' do @@ -24,7 +24,7 @@ target 'KitchenSinkBroadcastExtension' do use_frameworks! # Pods for KitchenSinkBroadcastExtension - pod 'WebexBroadcastExtensionKit','~> 3.16.1' + pod 'WebexBroadcastExtensionKit','~> 3.16.2' end