Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions KitchenSink.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)";
Expand Down
1 change: 1 addition & 0 deletions KitchenSink/NewUI/Utils/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
58 changes: 58 additions & 0 deletions KitchenSink/NewUI/ViewModels/CallViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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)
}
Expand All @@ -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() {
Expand Down
63 changes: 63 additions & 0 deletions KitchenSink/NewUI/ViewModels/SettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
Expand All @@ -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.
Expand Down Expand Up @@ -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()
{
Expand All @@ -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
}

Expand Down Expand Up @@ -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()
}
}
13 changes: 13 additions & 0 deletions KitchenSink/NewUI/Views/MoreOptionsCallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand Down Expand Up @@ -348,6 +358,9 @@ struct MoreOptionsCallView: View {
}
}
}
.onAppear {
callingVM.refreshAudioDumpRecordingState()
}
}

private func updateSettings() {
Expand Down
54 changes: 54 additions & 0 deletions KitchenSink/NewUI/Views/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand All @@ -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")
Expand Down Expand Up @@ -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)
}
Expand All @@ -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() {
Expand Down
20 changes: 20 additions & 0 deletions KitchenSink/NewUI/Webex/WebexCall.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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>) -> Void)
func canStartRecordingAudioDump(completionHandler: @escaping (Error?) -> Void)
func startRecordingAudioDump(completionHandler: @escaping (Error?) -> Void)
func stopRecordingAudioDump(completionHandler: @escaping (Error?) -> Void)
}

@available(iOS 16.0, *)
Expand Down Expand Up @@ -282,6 +286,10 @@ class CallKS: CallProtocol
}
}

var isRecordingAudioDump: Bool {
return call?.isRecordingAudioDump ?? false
}

private var call: Call?

init(call: WebexSDK.Call) {
Expand Down Expand Up @@ -698,6 +706,18 @@ class CallKS: CallProtocol
public func enableSpeechEnhancement(shouldEnable: Bool, completionHandler: @escaping (Result<Void>) -> 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 {
Expand Down
10 changes: 6 additions & 4 deletions KitchenSink/NewUI/Webex/WebexKS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand All @@ -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 }
Expand Down
Loading