From ab454da6878ff640db61afab21dac31c4169537c Mon Sep 17 00:00:00 2001 From: Elaine <138257830+elaine-signal@users.noreply.github.com> Date: Wed, 27 May 2026 16:22:31 -0400 Subject: [PATCH] Add debug log preview --- Signal/AppLaunch/AppDelegate.swift | 10 +- Signal/Calls/CallQualitySurvey.swift | 9 +- ...lQualitySurveyDebugLogViewController.swift | 13 +- .../Survey/CallQualitySurveyNavigation.swift | 7 +- .../Debugging/ContactSupportActionSheet.swift | 3 +- Signal/Debugging/DebugLogs.swift | 301 +++++++++++------- .../NotificationActionHandler.swift | 10 +- .../ProvisioningController.swift | 6 +- .../RegistrationNavigationController.swift | 3 +- .../ComposeSupportEmailOperation.swift | 33 +- .../ContactSupportViewController.swift | 6 +- .../DebugLogPreviewViewController.swift | 45 ++- .../AppSettings/HelpViewController.swift | 5 +- .../InternalSettingsViewController.swift | 6 +- .../DatabaseRecoveryViewController.swift | 10 +- .../ChatListFYISheetCoordinator.swift | 5 +- .../translations/en.lproj/Localizable.strings | 3 + 17 files changed, 299 insertions(+), 176 deletions(-) diff --git a/Signal/AppLaunch/AppDelegate.swift b/Signal/AppLaunch/AppDelegate.swift index d8df275646..34dfa2c12b 100644 --- a/Signal/AppLaunch/AppDelegate.swift +++ b/Signal/AppLaunch/AppDelegate.swift @@ -1250,14 +1250,20 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { switch action { case .submitDebugLogsAndCrash: addSubmitDebugLogsAction { - DebugLogs.submitLogs(supportTag: supportTag, dumper: logDumper) { + DebugLogs(dumper: logDumper).promptToSubmitLogs( + from: viewController, + supportTag: supportTag, + ) { owsFail("Exiting after submitting debug logs") } } case .submitDebugLogsAndLaunchApp(let window, let launchContext): addSubmitDebugLogsAction { [unowned window] in - DebugLogs.submitLogs(supportTag: supportTag, dumper: logDumper) { + DebugLogs(dumper: logDumper).promptToSubmitLogs( + from: viewController, + supportTag: supportTag, + ) { ignoreErrorAndLaunchApp(in: window, launchContext: launchContext) } } diff --git a/Signal/Calls/CallQualitySurvey.swift b/Signal/Calls/CallQualitySurvey.swift index a0179cdc74..7d67ed8568 100644 --- a/Signal/Calls/CallQualitySurvey.swift +++ b/Signal/Calls/CallQualitySurvey.swift @@ -192,13 +192,16 @@ class CallQualitySurveyManager { return proto } - func submit(rating: CallQualitySurvey.Rating, shouldSubmitDebugLogs: Bool) { + func submit( + rating: CallQualitySurvey.Rating, + logsToSubmit logs: DebugLogs?, + ) { var proto = buildProto(rating: rating) Task { - if shouldSubmitDebugLogs { + if let logs { do { - let debugLogURL = try await DebugLogs.uploadLogs(dumper: .fromGlobals()) + let debugLogURL = try await logs.uploadLogs() proto.debugLogURL = debugLogURL.absoluteString } catch { logger.error("Failed to submit debug logs: \(error)") diff --git a/Signal/Calls/UserInterface/Survey/CallQualitySurveyDebugLogViewController.swift b/Signal/Calls/UserInterface/Survey/CallQualitySurveyDebugLogViewController.swift index 77f96b49b7..6012c265ad 100644 --- a/Signal/Calls/UserInterface/Survey/CallQualitySurveyDebugLogViewController.swift +++ b/Signal/Calls/UserInterface/Survey/CallQualitySurveyDebugLogViewController.swift @@ -16,10 +16,12 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController { private let tableViewController = OWSTableViewController2() private var shouldSubmitDebugLogs = false + private var logs: DebugLogs private let rating: CallQualitySurvey.Rating init(rating: CallQualitySurvey.Rating) { + self.logs = DebugLogs(dumper: .fromGlobals()) self.rating = rating super.init(nibName: nil, bundle: nil) } @@ -129,7 +131,8 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController { let container = UIView() let textView = LinkingTextView { [weak self] in - self?.showDebugLogPreview() + guard let self else { return } + self.logs.showPreview(from: self) } textView.attributedText = .composed(of: [ OWSLocalizedString( @@ -193,12 +196,6 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController { present(nav, animated: true) } - private func showDebugLogPreview() { - let vc = DebugLogPreviewViewController() - let nav = OWSNavigationController(rootViewController: vc) - present(nav, animated: true) - } - override func customSheetHeight() -> CGFloat? { let headerHeight = headerContainer.height let collectionViewHeight = tableViewController.tableView.contentSize.height + tableViewController.tableView.contentInset.totalHeight @@ -209,7 +206,7 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController { private func submit() { sheetNav?.submit( rating: self.rating, - shouldSubmitDebugLogs: self.shouldSubmitDebugLogs, + logsToSubmit: shouldSubmitDebugLogs ? logs : nil, ) } } diff --git a/Signal/Calls/UserInterface/Survey/CallQualitySurveyNavigation.swift b/Signal/Calls/UserInterface/Survey/CallQualitySurveyNavigation.swift index 8e40e88378..b79c95f578 100644 --- a/Signal/Calls/UserInterface/Survey/CallQualitySurveyNavigation.swift +++ b/Signal/Calls/UserInterface/Survey/CallQualitySurveyNavigation.swift @@ -82,10 +82,13 @@ final class CallQualitySurveyNavigationController: UINavigationController { pushViewController(vc, animated: false) } - func submit(rating: CallQualitySurvey.Rating, shouldSubmitDebugLogs: Bool) { + func submit( + rating: CallQualitySurvey.Rating, + logsToSubmit: DebugLogs?, + ) { callQualitySurveyManager.submit( rating: rating, - shouldSubmitDebugLogs: shouldSubmitDebugLogs, + logsToSubmit: logsToSubmit, ) let host = presentingViewController dismiss(animated: true) { diff --git a/Signal/Debugging/ContactSupportActionSheet.swift b/Signal/Debugging/ContactSupportActionSheet.swift index c432b9abab..1e83fc0e7b 100644 --- a/Signal/Debugging/ContactSupportActionSheet.swift +++ b/Signal/Debugging/ContactSupportActionSheet.swift @@ -47,11 +47,12 @@ enum ContactSupportActionSheet { let submitWithLogAction = ActionSheetAction(title: submitWithLogTitle, style: .default) { [weak fromViewController] _ in guard let fromViewController else { return } + let logs = DebugLogs(dumper: logDumper) let emailRequest = SupportEmailModel( userDescription: nil, emojiMood: nil, supportFilter: emailFilter.asString, - debugLogPolicy: .requireUpload(logDumper), + debugLogPolicy: .requireUpload(logs), hasRecentChallenge: logDumper.challengeReceivedRecently(), ) diff --git a/Signal/Debugging/DebugLogs.swift b/Signal/Debugging/DebugLogs.swift index 6f9f93d36c..258a87ef73 100644 --- a/Signal/Debugging/DebugLogs.swift +++ b/Signal/Debugging/DebugLogs.swift @@ -8,7 +8,7 @@ import SignalServiceKit import SignalUI import zlib -public struct DebugLogDumper { +struct DebugLogDumper { fileprivate var accountManager: (any TSAccountManager)? fileprivate var appVersion: any AppVersion fileprivate var db: (any DB)? @@ -25,7 +25,7 @@ public struct DebugLogDumper { ) } - public func challengeReceivedRecently() -> Bool { + func challengeReceivedRecently() -> Bool { guard let db else { return false } @@ -57,34 +57,134 @@ public struct DebugLogDumper { } } -enum DebugLogs { +final class DebugLogs { + private let dumper: DebugLogDumper + private var logsDirPath: String? + init(dumper: DebugLogDumper) { + self.dumper = dumper + self.logsDirPath = DebugLogs.collectAndFlushLogs(dumper: dumper) + } + + deinit { + if let logsDirPath { + OWSFileSystem.deleteFile(logsDirPath) + } + } + + func showPreview( + from viewController: UIViewController, + onSubmit: (() -> Void)? = nil, + onCancel: (() -> Void)? = nil, + ) { + guard let logsDirPath else { + Logger.error("No logs path found for preview") + handleError(error: .noLogs, viewController: viewController) + onCancel?() + return + } + let logFilePaths = ((try? FileManager.default.contentsOfDirectory(atPath: logsDirPath)) ?? []).map { + URL(fileURLWithPath: logsDirPath).appendingPathComponent($0).path + } + let previewVC = DebugLogPreviewViewController(logFilePaths: logFilePaths, onSubmit: onSubmit, onCancel: onCancel) + let nav = OWSNavigationController(rootViewController: previewVC) + viewController.present(nav, animated: true) + } + + /// Presents a log preview with an option to submit. Completion is only + /// called if the user submits, after the submission is completed. @MainActor - static func submitLogs(supportTag: String? = nil, dumper: DebugLogDumper, completion: (() -> Void)? = nil) { - let submitLogsCompletion = { - if let completion { - // Wait a moment. If the user opens a URL, it needs a moment to complete. - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + func promptToSubmitLogs( + from viewController: UIViewController, + supportTag: String? = nil, + completion: (() -> Void)? = nil, + ) { + showPreview(from: viewController, onSubmit: { + Task { + await viewController.awaitableDismiss(animated: true) + await self.submitLogs(supportTag: supportTag) + if let completion { + try? await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC) completion() } } - } + }) + } + @MainActor + func promptToSubmitLogs( + from viewController: UIViewController, + supportTag: String? = nil, + ) async { + let didSubmit = await withCheckedContinuation { continuation in + showPreview( + from: viewController, + onSubmit: { + continuation.resume(returning: true) + }, + onCancel: { + continuation.resume(returning: false) + }, + ) + } + if didSubmit { + await viewController.awaitableDismiss(animated: true) + await submitLogs(supportTag: supportTag) + } + } + + enum DebugLogsError: LocalizedError { + case noLogs + case couldNotPackageLogs + case uploadError(zipFilePath: String) + + var errorDescription: String? { localizedErrorMessage } + var localizedErrorMessage: String { + switch self { + case .noLogs: + OWSLocalizedString( + "DEBUG_LOG_ALERT_NO_LOGS", + comment: "Error indicating that no debug logs could be found.", + ) + case .couldNotPackageLogs: + OWSLocalizedString( + "DEBUG_LOG_ALERT_COULD_NOT_PACKAGE_LOGS", + comment: "Error indicating that the debug logs could not be packaged.", + ) + case .uploadError: + OWSLocalizedString( + "DEBUG_LOG_ALERT_ERROR_UPLOADING_LOG", + comment: "Error indicating that a debug log could not be uploaded.", + ) + } + } + } + + @MainActor + private func submitLogs(supportTag: String?) async { var supportFilter = "Signal - iOS Debug Log" if let supportTag { supportFilter += " - \(supportTag)" } guard let frontmostViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts else { - submitLogsCompletion() return } - uploadLogsUsingViewController(frontmostViewController, dumper: dumper) { url in - guard let presentingViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts else { - submitLogsCompletion() - return - } + let url: URL? + do { + url = try await uploadLogsWithUI(from: frontmostViewController) + } catch { + self.handleError(error: error, viewController: frontmostViewController) + return + } + guard let url else { return } + + guard let presentingViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts else { + return + } + + await withCheckedContinuation { (continuation: CheckedContinuation) in let alert = ActionSheetController( title: NSLocalizedString("DEBUG_LOG_ALERT_TITLE", comment: "Title of the debug log alert."), message: NSLocalizedString("DEBUG_LOG_ALERT_MESSAGE", comment: "Message of the debug log alert."), @@ -102,10 +202,10 @@ enum DebugLogs { await ComposeSupportEmailOperation.sendEmailWithDefaultErrorHandling( supportFilter: supportFilter, logUrl: url, - hasRecentChallenge: dumper.challengeReceivedRecently(), + hasRecentChallenge: self.dumper.challengeReceivedRecently(), ) } - submitLogsCompletion() + continuation.resume() }, )) } @@ -118,7 +218,7 @@ enum DebugLogs { handler: { _ in UIPasteboard.general.string = url.absoluteString presentingViewController.presentToast(text: CommonStrings.copiedToClipboardToast, image: .copy) - submitLogsCompletion() + continuation.resume() }, )) alert.addAction(ActionSheetAction( @@ -131,67 +231,39 @@ enum DebugLogs { AttachmentSharing.showShareUI( for: url.absoluteString, sender: nil, - completion: submitLogsCompletion, + completion: { continuation.resume() }, ) }, )) alert.addAction(ActionSheetAction( title: CommonStrings.cancelButton, style: .cancel, - handler: { _ in submitLogsCompletion() }, + handler: { _ in continuation.resume() }, )) presentingViewController.presentActionSheet(alert) } } @MainActor - private static func uploadLogsUsingViewController(_ viewController: UIViewController, dumper: DebugLogDumper, completion: @escaping (URL) -> Void) { - AssertIsOnMainThread() - - ModalActivityIndicatorViewController.present( - fromViewController: viewController, + private func uploadLogsWithUI(from viewController: UIViewController) async throws(DebugLogsError) -> URL? { + return try await ModalActivityIndicatorViewController.presentAndPropagateResult( + from: viewController, canCancel: true, - asyncBlock: { await _uploadLogs(dumper: dumper, modalActivityIndicator: $0, completion: completion) }, - ) - } - - @MainActor - private static func _uploadLogs(dumper: DebugLogDumper, modalActivityIndicator: ModalActivityIndicatorViewController, completion: @escaping (URL) -> Void) async { - do { - let url = try await uploadLogs(dumper: dumper) - guard !modalActivityIndicator.wasCancelled else { return } - modalActivityIndicator.dismiss { - completion(url) - } - } catch { - guard !modalActivityIndicator.wasCancelled else { - if let logArchiveOrDirectoryPath = error.logArchiveOrDirectoryPath { - OWSFileSystem.deleteFile(logArchiveOrDirectoryPath) + ) { () throws(DebugLogsError) -> URL? in + do throws(DebugLogsError) { + return try await self.uploadLogs() + } catch { + if Task.isCancelled { + return nil } - return - } - - modalActivityIndicator.dismiss { - DebugLogs.showFailureAlert( - with: error.localizedErrorMessage, - logArchiveOrDirectoryPath: error.logArchiveOrDirectoryPath, - ) + throw error } } } // MARK: - Collecting & uploading - private struct NoLogsError: Error { - var errorString: String { - OWSLocalizedString( - "DEBUG_LOG_ALERT_NO_LOGS", - comment: "Error indicating that no debug logs could be found.", - ) - } - } - - private static func collectLogs() -> Result { + private static func collectLogs() -> String? { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy.MM.dd hh.mm.ss" let dateString = dateFormatter.string(from: Date()) @@ -203,7 +275,7 @@ enum DebugLogs { let logFilePaths = DebugLogger.shared.allLogFilePaths if logFilePaths.isEmpty { - return .failure(NoLogsError()) + return nil } for logFilePath in logFilePaths { @@ -219,50 +291,44 @@ enum DebugLogs { OWSFileSystem.protectFileOrFolder(atPath: copyFilePath) } - return .success(zipDirPath) + return zipDirPath } - static func exportLogs() { + func exportLogs(viewController: UIViewController) { AssertIsOnMainThread() - switch collectLogs() { - case let .success(logsDirPath): - AttachmentSharing.showShareUI(for: URL(fileURLWithPath: logsDirPath), sender: nil) { - OWSFileSystem.deleteFile(logsDirPath) - } - case let .failure(error): - Self.showFailureAlert(with: error.errorString, logArchiveOrDirectoryPath: nil) - return + guard let logsDirPath else { + return handleError( + error: .noLogs, + viewController: viewController, + ) + } + AttachmentSharing.showShareUI(for: URL(fileURLWithPath: logsDirPath), sender: nil) { + OWSFileSystem.deleteFile(logsDirPath) } } - struct UploadDebugLogError: Error { - var localizedErrorMessage: String - var logArchiveOrDirectoryPath: String? - } - - /// - Note: Various dependencies might not be initialized yet when this - /// method is called from the database recovery flow. Notably, the database - /// isn't available in that flow. - static func uploadLogs(dumper: DebugLogDumper) async throws(UploadDebugLogError) -> URL { - // Phase 1: Dump any additional details that are relevant. + private static func collectAndFlushLogs( + dumper: DebugLogDumper, + ) -> String? { + // Dump any additional details that are relevant. dumper.dump() Logger.info("About to zip debug logs") - // Phase 2: Flush pending logs to disk. + // Flush pending logs to disk. Logger.flush() - // Phase 3: Make a local copy of all of the log files. - let zipDirPath: String - switch collectLogs() { - case let .success(logsDirPath): - zipDirPath = logsDirPath - case let .failure(error): - throw UploadDebugLogError(localizedErrorMessage: error.errorString) + // Make a local copy of all of the log files. + return collectLogs() + } + + func uploadLogs() async throws(DebugLogsError) -> URL { + guard let logsDirPath else { + throw DebugLogsError.noLogs } - // Phase 4: Zip up the log files. - let zipDirUrl = URL(fileURLWithPath: zipDirPath) - let zipFileUrl = URL(fileURLWithPath: (zipDirPath as NSString).appendingPathExtension("zip")!) + // Zip up the log files. + let zipDirUrl = URL(fileURLWithPath: logsDirPath) + let zipFileUrl = URL(fileURLWithPath: (logsDirPath as NSString).appendingPathExtension("zip")!) let fileCoordinator = NSFileCoordinator() var zipError: NSError? fileCoordinator.coordinate(readingItemAt: zipDirUrl, options: [.forUploading], error: &zipError) { temporaryFileUrl in @@ -273,38 +339,44 @@ enum DebugLogs { } } if zipError != nil || !OWSFileSystem.fileOrFolderExists(url: zipFileUrl) { - let errorMessage = OWSLocalizedString( - "DEBUG_LOG_ALERT_COULD_NOT_PACKAGE_LOGS", - comment: "Error indicating that the debug logs could not be packaged.", - ) - throw UploadDebugLogError(localizedErrorMessage: errorMessage, logArchiveOrDirectoryPath: zipDirPath) + throw DebugLogsError.couldNotPackageLogs } OWSFileSystem.protectFileOrFolder(atPath: zipFileUrl.path) - OWSFileSystem.deleteFile(zipDirPath) - // Phase 5: Upload the log files. + // Upload the log files. do { let url = try await DebugLogUploader.uploadFile(fileUrl: zipFileUrl, mimeType: MimeType.applicationZip.rawValue) try OWSFileSystem.deleteFile(url: zipFileUrl) return url } catch { - let errorMessage = OWSLocalizedString( - "DEBUG_LOG_ALERT_ERROR_UPLOADING_LOG", - comment: "Error indicating that a debug log could not be uploaded.", - ) - throw UploadDebugLogError(localizedErrorMessage: errorMessage, logArchiveOrDirectoryPath: zipFileUrl.path) + throw DebugLogsError.uploadError(zipFilePath: zipFileUrl.path) } } - private static func showFailureAlert(with message: String, logArchiveOrDirectoryPath: String?) { - let deleteArchive: (String) -> Void = { filePath in - OWSFileSystem.deleteFile(filePath) + private func handleError( + error: DebugLogsError, + viewController: UIViewController, + ) { + let logsPath: String? + let completion: (() -> Void)? + switch error { + case .noLogs: + logsPath = nil + completion = nil + case .couldNotPackageLogs: + logsPath = self.logsDirPath + completion = nil + case .uploadError(let zipFilePath): + logsPath = zipFilePath + completion = { + OWSFileSystem.deleteFile(zipFilePath) + } } - let alert = ActionSheetController(title: nil, message: message) + let alert = ActionSheetController(message: error.localizedErrorMessage) - if let logArchiveOrDirectoryPath { + if let logsPath { alert.addAction(.init( title: OWSLocalizedString( "DEBUG_LOG_ALERT_OPTION_EXPORT_LOG_ARCHIVE", @@ -312,23 +384,18 @@ enum DebugLogs { ), ) { _ in AttachmentSharing.showShareUI( - for: URL(fileURLWithPath: logArchiveOrDirectoryPath), + for: URL(fileURLWithPath: logsPath), sender: nil, - completion: { - deleteArchive(logArchiveOrDirectoryPath) - }, + completion: completion, ) }) } alert.addAction(.init(title: CommonStrings.okButton) { _ in - if let logArchiveOrDirectoryPath { - deleteArchive(logArchiveOrDirectoryPath) - } + completion?() }) - let presentingViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts - presentingViewController?.presentActionSheet(alert) + viewController.presentActionSheet(alert) } } diff --git a/Signal/Notifications/NotificationActionHandler.swift b/Signal/Notifications/NotificationActionHandler.swift index 7d001438e0..00683200ce 100644 --- a/Signal/Notifications/NotificationActionHandler.swift +++ b/Signal/Notifications/NotificationActionHandler.swift @@ -366,11 +366,13 @@ public class NotificationActionHandler { @MainActor private class func submitDebugLogs(supportTag: String?) async { - await withCheckedContinuation { continuation in - DebugLogs.submitLogs(supportTag: supportTag, dumper: .fromGlobals()) { - continuation.resume() - } + guard let viewController = CurrentAppContext().frontmostViewController() else { + return } + await DebugLogs(dumper: .fromGlobals()).promptToSubmitLogs( + from: viewController, + supportTag: supportTag, + ) } @MainActor diff --git a/Signal/Provisioning/UserInterface/ProvisioningController.swift b/Signal/Provisioning/UserInterface/ProvisioningController.swift index 009bef8774..65552e4aa5 100644 --- a/Signal/Provisioning/UserInterface/ProvisioningController.swift +++ b/Signal/Provisioning/UserInterface/ProvisioningController.swift @@ -148,7 +148,11 @@ class ProvisioningController: NSObject { @objc @MainActor private func submitLogs() { - DebugLogs.submitLogs(supportTag: "Onboarding", dumper: .fromGlobals()) + guard let viewController = CurrentAppContext().frontmostViewController() else { + return + } + let logs = DebugLogs(dumper: .fromGlobals()) + logs.promptToSubmitLogs(from: viewController, supportTag: "Onboarding") } // MARK: - Transitions diff --git a/Signal/Registration/UserInterface/RegistrationNavigationController.swift b/Signal/Registration/UserInterface/RegistrationNavigationController.swift index c9f2044573..0be6535703 100644 --- a/Signal/Registration/UserInterface/RegistrationNavigationController.swift +++ b/Signal/Registration/UserInterface/RegistrationNavigationController.swift @@ -500,7 +500,8 @@ public class RegistrationNavigationController: OWSNavigationController { )) self.present(navVc, animated: true) } else { - DebugLogs.submitLogs(supportTag: "Registration", dumper: .fromGlobals()) + let logs = DebugLogs(dumper: .fromGlobals()) + logs.promptToSubmitLogs(from: self, supportTag: "Registration") } } } diff --git a/Signal/src/ViewControllers/AppSettings/ComposeSupportEmailOperation.swift b/Signal/src/ViewControllers/AppSettings/ComposeSupportEmailOperation.swift index d020558ea3..c5d6bcc6d2 100644 --- a/Signal/src/ViewControllers/AppSettings/ComposeSupportEmailOperation.swift +++ b/Signal/src/ViewControllers/AppSettings/ComposeSupportEmailOperation.swift @@ -12,10 +12,10 @@ struct SupportEmailModel { enum LogPolicy { /// Attempt to upload the logs and include the resulting URL in the email body /// If the upload fails for one reason or another, continue anyway - case attemptUpload(DebugLogDumper) + case attemptUpload(DebugLogs) /// Upload the logs. If they fail to upload, fail the operation - case requireUpload(DebugLogDumper) + case requireUpload(DebugLogs) /// Don't upload new logs, instead use the provided link case link(URL) @@ -157,15 +157,15 @@ final class ComposeSupportEmailOperation: NSObject { debugUrlString = nil case .link(let url): debugUrlString = url.absoluteString - case .attemptUpload(let dumper): + case .attemptUpload(let logs): do { - debugUrlString = try await uploadDebugLogWithTimeout(dumper: dumper).absoluteString + debugUrlString = try await uploadDebugLogWithTimeout(logs: logs).absoluteString } catch { debugUrlString = "[Support note: Log upload failed — \(error.userErrorDescription)]" } - case .requireUpload(let dumper): + case .requireUpload(let logs): do { - debugUrlString = try await uploadDebugLogWithTimeout(dumper: dumper).absoluteString + debugUrlString = try await uploadDebugLogWithTimeout(logs: logs).absoluteString } catch { throw EmailError.logUploadFailure(underlyingError: (error as? LocalizedError)) } @@ -187,17 +187,16 @@ final class ComposeSupportEmailOperation: NSObject { } } - private func uploadDebugLogWithTimeout(dumper: DebugLogDumper) async throws -> URL { + private func uploadDebugLogWithTimeout(logs: DebugLogs) async throws -> URL { do { return try await withCooperativeTimeout(seconds: 60) { - do throws(DebugLogs.UploadDebugLogError) { - return try await DebugLogs.uploadLogs(dumper: dumper) + do throws(DebugLogs.DebugLogsError) { + return try await logs.uploadLogs() } catch { - // FIXME: Should we do something with the local log file? - if let logArchiveOrDirectoryPath = error.logArchiveOrDirectoryPath { - _ = OWSFileSystem.deleteFile(logArchiveOrDirectoryPath) + if case .uploadError(zipFilePath: let zipPath) = error { + OWSFileSystem.deleteFile(zipPath) } - throw DebugLogsUploadError(localizedDescription: error.localizedErrorMessage) + throw error } } } catch is CooperativeTimeoutError { @@ -286,11 +285,3 @@ final class ComposeSupportEmailOperation: NSObject { .joined(separator: "\r\n") } } - -struct DebugLogsUploadError: Error, LocalizedError, UserErrorDescriptionProvider { - let localizedDescription: String - - var errorDescription: String? { - localizedDescription - } -} diff --git a/Signal/src/ViewControllers/AppSettings/ContactSupportViewController.swift b/Signal/src/ViewControllers/AppSettings/ContactSupportViewController.swift index 1df3b21d39..ed19b6e025 100644 --- a/Signal/src/ViewControllers/AppSettings/ContactSupportViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/ContactSupportViewController.swift @@ -214,11 +214,15 @@ final class ContactSupportViewController: OWSTableViewController2, TextViewWithP private func didTapNext() { let logDumper = DebugLogDumper.fromGlobals() + var logPolicy: SupportEmailModel.LogPolicy? + if debugSwitch.isOn { + logPolicy = .attemptUpload(DebugLogs(dumper: logDumper)) + } let emailRequest = SupportEmailModel( userDescription: descriptionField.text, emojiMood: emojiPicker.selectedMood, supportFilter: selectedFilter.map { "iOS \($0.emailFilterString)" }, - debugLogPolicy: debugSwitch.isOn ? .attemptUpload(logDumper) : nil, + debugLogPolicy: logPolicy, hasRecentChallenge: logDumper.challengeReceivedRecently(), ) diff --git a/Signal/src/ViewControllers/AppSettings/DebugLogPreviewViewController.swift b/Signal/src/ViewControllers/AppSettings/DebugLogPreviewViewController.swift index e6b19f1959..b62bd89e9c 100644 --- a/Signal/src/ViewControllers/AppSettings/DebugLogPreviewViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/DebugLogPreviewViewController.swift @@ -9,6 +9,16 @@ import SignalUI final class DebugLogPreviewViewController: OWSViewController { private let textView = UITextView() + private let logFilePaths: [String] + private let onSubmit: (() -> Void)? + private let onCancel: (() -> Void)? + + init(logFilePaths: [String], onSubmit: (() -> Void)? = nil, onCancel: (() -> Void)? = nil) { + self.logFilePaths = logFilePaths + self.onSubmit = onSubmit + self.onCancel = onCancel + super.init() + } override func viewDidLoad() { super.viewDidLoad() @@ -22,7 +32,7 @@ final class DebugLogPreviewViewController: OWSViewController { view.backgroundColor = .Signal.groupedBackground - navigationItem.rightBarButtonItem = .cancelButton(dismissingFrom: self) + navigationItem.rightBarButtonItem = .cancelButton(dismissingFrom: self, completion: onCancel) // UITableView does not have the text-rendering optimizations that // UITextView with scrolling enabled has, and the app freezes when @@ -64,12 +74,31 @@ final class DebugLogPreviewViewController: OWSViewController { textView.textContainerInset = .init(margin: 20) textView.textContainer.lineFragmentPadding = 0 textView.verticalScrollIndicatorInsets = .init(hMargin: 0, vMargin: OWSTableViewController2.cellRounding / 2) + + if let onSubmit { + cellContainer.setContentHuggingPriority(.defaultLow, for: .vertical) + + let submitButton = UIButton( + configuration: .largePrimary(title: OWSLocalizedString( + "DEBUG_LOG_PREVIEW_SUBMIT_BUTTON", + comment: "Button below the debug log preview which continues the submission flow", + )), + primaryAction: UIAction { _ in + onSubmit() + }, + ) + stackView.addArrangedSubview(submitButton) + submitButton.autoPinWidthToSuperviewMargins() + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.presentationController?.delegate = self } private func loadLogs() { - Logger.flush() - - self.textView.text = DebugLogger.shared.allLogFilePaths.reduce( + self.textView.text = self.logFilePaths.reduce( into: "", ) { partialResult, logFilePath in do { @@ -89,3 +118,11 @@ final class DebugLogPreviewViewController: OWSViewController { } } + +// MARK: - UIAdaptivePresentationControllerDelegate + +extension DebugLogPreviewViewController: UIAdaptivePresentationControllerDelegate { + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + onCancel?() + } +} diff --git a/Signal/src/ViewControllers/AppSettings/HelpViewController.swift b/Signal/src/ViewControllers/AppSettings/HelpViewController.swift index 87f2b6b4dc..b2ca57a89d 100644 --- a/Signal/src/ViewControllers/AppSettings/HelpViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/HelpViewController.swift @@ -69,8 +69,9 @@ final class HelpViewController: OWSTableViewController2 { loggingSection.add(.item( name: OWSLocalizedString("SETTINGS_ADVANCED_SUBMIT_DEBUGLOG", comment: ""), accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "submit_debug_log"), - actionBlock: { - DebugLogs.submitLogs(dumper: .fromGlobals()) + actionBlock: { [weak self] in + guard let self else { return } + DebugLogs(dumper: .fromGlobals()).promptToSubmitLogs(from: self) }, )) contents.add(loggingSection) diff --git a/Signal/src/ViewControllers/AppSettings/Internal/InternalSettingsViewController.swift b/Signal/src/ViewControllers/AppSettings/Internal/InternalSettingsViewController.swift index c3ad383b34..cc1c336698 100644 --- a/Signal/src/ViewControllers/AppSettings/Internal/InternalSettingsViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/Internal/InternalSettingsViewController.swift @@ -88,8 +88,10 @@ class InternalSettingsViewController: OWSTableViewController2 { )) if mode == .registration { - debugSection.add(.actionItem(withText: "Submit debug logs") { - DebugLogs.submitLogs(supportTag: "Registration", dumper: .fromGlobals()) + debugSection.add(.actionItem(withText: "Submit debug logs") { [weak self] in + guard let self else { return } + let logs = DebugLogs(dumper: .fromGlobals()) + logs.promptToSubmitLogs(from: self, supportTag: "Registration") }) } diff --git a/Signal/src/ViewControllers/DatabaseRecoveryViewController.swift b/Signal/src/ViewControllers/DatabaseRecoveryViewController.swift index 36b8f07031..380b0ec098 100644 --- a/Signal/src/ViewControllers/DatabaseRecoveryViewController.swift +++ b/Signal/src/ViewControllers/DatabaseRecoveryViewController.swift @@ -210,12 +210,10 @@ class DatabaseRecoveryViewController: OWSViewController { @objc private func didRequestToSubmitDebugLogs() { - self.dismiss(animated: true) { - DebugLogs.submitLogs( - supportTag: "LaunchFailure_DatabaseRecoveryFailed", - dumper: .preLaunch(), - ) - } + DebugLogs(dumper: .preLaunch()).promptToSubmitLogs( + from: self, + supportTag: "LaunchFailure_DatabaseRecoveryFailed", + ) } private func attemptRecovery() { diff --git a/Signal/src/ViewControllers/HomeView/Chat List/ChatListFYISheetCoordinator.swift b/Signal/src/ViewControllers/HomeView/Chat List/ChatListFYISheetCoordinator.swift index 85db37d5b0..8e02eb7dbb 100644 --- a/Signal/src/ViewControllers/HomeView/Chat List/ChatListFYISheetCoordinator.swift +++ b/Signal/src/ViewControllers/HomeView/Chat List/ChatListFYISheetCoordinator.swift @@ -676,7 +676,10 @@ private final class BackupArchiveErrorHeroSheet: HeroSheetViewController { title: "Submit debug log", action: { sheet in sheet.dismiss(animated: true) { - DebugLogs.submitLogs(supportTag: "BackupArchive", dumper: .fromGlobals()) + DebugLogs(dumper: .fromGlobals()).promptToSubmitLogs( + from: fromViewController, + supportTag: "BackupArchive", + ) } }, ), diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 02df85a7c4..3e199d8785 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -2749,6 +2749,9 @@ /* Header text displayed above the debug log preview */ "DEBUG_LOG_PREVIEW_HEADER" = "When submitted, your log will be posted online for 30 days at a unique, unpublished URL."; +/* Button below the debug log preview which continues the submission flow */ +"DEBUG_LOG_PREVIEW_SUBMIT_BUTTON" = "Submit"; + /* Title for the debug log preview screen */ "DEBUG_LOG_PREVIEW_TITLE" = "Debug Log";