From 0197b881db103ecc16fcf98cab299f72f624e7db Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn Date: Fri, 14 Jun 2024 21:00:32 +0400 Subject: [PATCH] [ios] implement logging to the file - get logs from core - log them in the default os_log to see in the Console app - write to file Signed-off-by: Kiryl Kaveryn --- iphone/CoreApi/CoreApi/Logger/Logger.h | 5 +- iphone/CoreApi/CoreApi/Logger/Logger.mm | 243 +++++++++++++++++- iphone/Maps/Common/Common.swift | 31 +-- iphone/Maps/Core/Settings/MWMSettings.h | 4 + iphone/Maps/Core/Settings/MWMSettings.mm | 20 +- iphone/Maps/Maps.xcodeproj/project.pbxproj | 4 + .../AboutController/AboutController.swift | 54 +++- .../SettingsTableViewDetailedSwitchCell.swift | 20 ++ .../SettingsTableViewiCloudSwitchCell.swift | 14 +- .../UI/Settings/MWMSettingsViewController.mm | 28 +- iphone/Maps/UI/Storyboard/Settings.storyboard | 50 +++- iphone/Maps/main.mm | 3 +- 12 files changed, 395 insertions(+), 81 deletions(-) create mode 100644 iphone/Maps/UI/Settings/Cells/SettingsTableViewDetailedSwitchCell.swift diff --git a/iphone/CoreApi/CoreApi/Logger/Logger.h b/iphone/CoreApi/CoreApi/Logger/Logger.h index be94ad8aad..0c5e899fa0 100644 --- a/iphone/CoreApi/CoreApi/Logger/Logger.h +++ b/iphone/CoreApi/CoreApi/Logger/Logger.h @@ -12,8 +12,11 @@ typedef NS_ENUM(NSUInteger, LogLevel) { @interface Logger : NSObject -+ (void)log:(LogLevel)level message:(NSString*)message; ++ (void)log:(LogLevel)level message:(NSString *)message; + (BOOL)canLog:(LogLevel)level; ++ (void)setFileLoggingEnabled:(BOOL)fileLoggingEnabled; ++ (nullable NSURL *)getLogFileURL; ++ (uint64_t)getLogFileSize; @end diff --git a/iphone/CoreApi/CoreApi/Logger/Logger.mm b/iphone/CoreApi/CoreApi/Logger/Logger.mm index 09f4e1482c..a64143c995 100644 --- a/iphone/CoreApi/CoreApi/Logger/Logger.mm +++ b/iphone/CoreApi/CoreApi/Logger/Logger.mm @@ -1,32 +1,249 @@ #import "Logger.h" -#import "base/logging.hpp" +#import + +#include "base/logging.hpp" +#include "coding/zip_creator.hpp" @interface Logger () + +@property (nullable, nonatomic) NSFileHandle * fileHandle; +@property (nonnull, nonatomic) os_log_t osLogger; +/// This property is introduced to avoid the CoreApi => Maps target dependency and stores the MWMSettings.isFileLoggingEnabled value. +@property (class, nonatomic) BOOL fileLoggingEnabled; +@property (class, readonly, nonatomic) dispatch_queue_t fileLoggingQueue; + ++ (Logger *)logger; ++ (void)enableFileLogging; ++ (void)disableFileLogging; ++ (void)logMessageWithLevel:(base::LogLevel)level src:(base::SrcPoint const &)src message:(std::string const &)message; ++ (NSURL *)getZippedLogFile:(NSString *)logFilePath; ++ (void)removeFileAtPath:(NSString *)filePath; + (base::LogLevel)baseLevel:(LogLevel)level; + @end +// Subsystem and category are used for the OSLog. +NSString * const kLoggerSubsystem = [[NSBundle mainBundle] bundleIdentifier]; +NSString * const kLoggerCategory = @"OM"; +NSString * const kLogFileName = @"log.txt"; +NSString * const kZipLogFileExtension = @"zip"; +NSString * const kLogFilePath = [[NSFileManager.defaultManager temporaryDirectory] URLByAppendingPathComponent:kLogFileName].path; +// TODO: (KK) Review and change this limit after some testing. +NSUInteger const kMaxLogFileSize = 1024 * 1024 * 100; // 100 MB; + @implementation Logger -+ (void)log:(LogLevel)level message:(NSString*)message { - LOG_SHORT([Logger baseLevel:level], (message.UTF8String)); +static BOOL _fileLoggingEnabled = NO; + ++ (void)initialize +{ + if (self == [Logger class]) { + SetLogMessageFn(&LogMessage); + SetAssertFunction(&AssertMessage); + } +} + ++ (Logger *)logger { + static Logger * logger = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + logger = [[self alloc] init]; + }); + return logger; +} + ++ (dispatch_queue_t)fileLoggingQueue { + static dispatch_queue_t fileLoggingQueue = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0); + fileLoggingQueue = dispatch_queue_create("app.organicmaps.fileLoggingQueue", attributes); + }); + return fileLoggingQueue; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _osLogger = os_log_create(kLoggerSubsystem.UTF8String, kLoggerCategory.UTF8String); + } + return self; +} + +// MARK: - Public + ++ (void)setFileLoggingEnabled:(BOOL)fileLoggingEnabled { + fileLoggingEnabled ? [self enableFileLogging] : [self disableFileLogging]; +} + ++ (BOOL)fileLoggingEnabled { + return _fileLoggingEnabled; +} + ++ (void)log:(LogLevel)level message:(NSString *)message { + LOG_SHORT([self baseLevel:level], (message.UTF8String)); } + (BOOL)canLog:(LogLevel)level { return [Logger baseLevel:level] >= base::g_LogLevel; } ++ (nullable NSURL *)getLogFileURL { + if ([self fileLoggingEnabled]) { + if (![NSFileManager.defaultManager fileExistsAtPath:kLogFilePath]) { + LOG(LERROR, ("Log file doesn't exist while file logging is enabled:", kLogFilePath.UTF8String)); + return nil; + } + return [self getZippedLogFile:kLogFilePath]; + } else { + // Fetch logs from the OSLog store. + if (@available(iOS 15.0, *)) { + NSError * error; + OSLogStore * store = [OSLogStore storeWithScope:OSLogStoreCurrentProcessIdentifier error:&error]; + + if (error) { + LOG(LERROR, (error.localizedDescription.UTF8String)); + return nil; + } + + NSPredicate * predicate = [NSPredicate predicateWithFormat:@"subsystem == %@", kLoggerSubsystem]; + OSLogEnumerator * enumerator = [store entriesEnumeratorWithOptions:{} position:nil predicate:predicate error:&error]; + + if (error) { + LOG(LERROR, (error.localizedDescription.UTF8String)); + return nil; + } + + NSMutableString * logString = [NSMutableString string]; + NSString * kNewLineStr = @"\n"; + + id object; + while (object = [enumerator nextObject]) { + if ([object isMemberOfClass:[OSLogEntryLog class]]) { + [logString appendString:[object composedMessage]]; + [logString appendString:kNewLineStr]; + } + } + + if (logString.length == 0) { + LOG(LINFO, ("OSLog entry is empty.")); + return nil; + } + + [NSFileManager.defaultManager createFileAtPath:kLogFilePath contents:[logString dataUsingEncoding:NSUTF8StringEncoding] attributes:nil]; + return [self getZippedLogFile:kLogFilePath]; + } else { + return nil; + } + } +} + ++ (uint64_t)getLogFileSize { + Logger * logger = [self logger]; + return logger.fileHandle != nil ? [logger.fileHandle offsetInFile] : 0; +} + +// MARK: - C++ injection + +void LogMessage(base::LogLevel level, base::SrcPoint const & src, std::string const & message) +{ + [Logger logMessageWithLevel:level src:src message:message]; + CHECK_LESS(level, base::g_LogAbortLevel, ("Abort. Log level is too serious", level)); +} + +bool AssertMessage(base::SrcPoint const & src, std::string const & message) +{ + [Logger logMessageWithLevel:base::LCRITICAL src:src message:message]; + return true; +} + +// MARK: - Private + ++ (void)enableFileLogging { + Logger * logger = [self logger]; + NSFileManager * fileManager = [NSFileManager defaultManager]; + + // Create a log file if it doesn't exist and setup file handle for writing. + if (![fileManager fileExistsAtPath:kLogFilePath]) + [fileManager createFileAtPath:kLogFilePath contents:nil attributes:nil]; + NSFileHandle * fileHandle = [NSFileHandle fileHandleForWritingAtPath:kLogFilePath]; + if (fileHandle == nil) { + LOG(LERROR, ("Failed to open log file for writing", kLogFilePath.UTF8String)); + [self disableFileLogging]; + return; + } + // Clean up the file if it exceeds the maximum size. + if ([fileManager contentsAtPath:kLogFilePath].length > kMaxLogFileSize) + [fileHandle truncateFileAtOffset:0]; + + logger.fileHandle = fileHandle; + + _fileLoggingEnabled = YES; + LOG(LINFO, ("File logging is enabled")); +} + ++ (void)disableFileLogging { + Logger * logger = [self logger]; + + [logger.fileHandle closeFile]; + logger.fileHandle = nil; + [self removeFileAtPath:kLogFilePath]; + + _fileLoggingEnabled = NO; + LOG(LINFO, ("File logging is disabled")); +} + ++ (void)logMessageWithLevel:(base::LogLevel)level src:(base::SrcPoint const &)src message:(std::string const &)message { + // Build the log message string. + auto & logHelper = base::LogHelper::Instance(); + std::ostringstream output; + // TODO: (KK) Either guard this call, or refactor thread ids in logHelper. + logHelper.WriteProlog(output, level); + logHelper.WriteLog(output, src, message); + + auto const logString = output.str(); + + Logger * logger = [self logger]; + // Log the message into the system log. + os_log(logger.osLogger, "%{public}s", logString.c_str()); + + dispatch_async([self fileLoggingQueue], ^{ + // Write the log message into the file. + NSFileHandle * fileHandle = logger.fileHandle; + if (fileHandle != nil) { + [fileHandle seekToEndOfFile]; + [fileHandle writeData:[NSData dataWithBytes:logString.c_str() length:logString.length()]]; + } + }); +} + ++ (NSURL *)getZippedLogFile:(NSString *)logFilePath { + NSString * zipFilePath = [[logFilePath stringByDeletingPathExtension] stringByAppendingPathExtension:kZipLogFileExtension]; + auto const success = CreateZipFromFiles({logFilePath.UTF8String}, zipFilePath.UTF8String); + if (!success) { + LOG(LERROR, ("Failed to zip log file:", kLogFilePath.UTF8String, ". The original file will be returned.")); + return [NSURL fileURLWithPath:logFilePath]; + } + [self removeFileAtPath:kLogFilePath]; + return [NSURL fileURLWithPath:zipFilePath]; +} + ++ (void)removeFileAtPath:(NSString *)filePath { + if ([NSFileManager.defaultManager fileExistsAtPath:filePath]) { + NSError * error; + [NSFileManager.defaultManager removeItemAtPath:filePath error:&error]; + if (error) + LOG(LERROR, (error.localizedDescription.UTF8String)); + } +} + + (base::LogLevel)baseLevel:(LogLevel)level { switch (level) { - case LogLevelDebug: - return LDEBUG; - case LogLevelInfo: - return LINFO; - case LogLevelWarning: - return LWARNING; - case LogLevelError: - return LERROR; - case LogLevelCritical: - return LCRITICAL; + case LogLevelDebug: return LDEBUG; + case LogLevelInfo: return LINFO; + case LogLevelWarning: return LWARNING; + case LogLevelError: return LERROR; + case LogLevelCritical: return LCRITICAL; } } diff --git a/iphone/Maps/Common/Common.swift b/iphone/Maps/Common/Common.swift index 7c04fa4907..abe3f3e044 100644 --- a/iphone/Maps/Common/Common.swift +++ b/iphone/Maps/Common/Common.swift @@ -1,5 +1,4 @@ import Foundation -import OSLog private func IPAD() -> Bool { return UI_USER_INTERFACE_IDIOM() == .pad } @@ -28,42 +27,18 @@ func statusBarHeight() -> CGFloat { return min(statusBarSize.height, statusBarSize.width) } -private let enableLoggingInRelease = true - func LOG(_ level: LogLevel, _ message: @autoclosure () -> Any, functionName: StaticString = #function, fileName: StaticString = #file, lineNumber: UInt = #line) { - - let shortFileName = URL(string: "\(fileName)")?.lastPathComponent ?? "" - let formattedMessage = "\(shortFileName):\(lineNumber) \(functionName): \(message())" - - if #available(iOS 14.0, *), enableLoggingInRelease { - os.Logger.logger.log(level: OSLogLevelFromLogLevel(level), "\(formattedMessage, privacy: .public)") - } else if Logger.canLog(level) { + if (Logger.canLog(level)) { + let shortFileName = URL(string: "\(fileName)")?.lastPathComponent ?? "" + let formattedMessage = "\(shortFileName):\(lineNumber) \(functionName): \(message())" Logger.log(level, message: formattedMessage) } } -private func OSLogLevelFromLogLevel(_ level: LogLevel) -> OSLogType { - switch level { - case .error: return .error - case .info: return .info - case .debug: return .debug - case .critical: return .fault - case .warning: return .default - @unknown default: - fatalError() - } -} - struct Weak where T: AnyObject { weak var value: T? } - -@available(iOS 14.0, *) -private extension os.Logger { - static let subsystem = Bundle.main.bundleIdentifier! - static let logger = Logger(subsystem: subsystem, category: "OM") -} diff --git a/iphone/Maps/Core/Settings/MWMSettings.h b/iphone/Maps/Core/Settings/MWMSettings.h index 7d8ceef789..2e7751f05a 100644 --- a/iphone/Maps/Core/Settings/MWMSettings.h +++ b/iphone/Maps/Core/Settings/MWMSettings.h @@ -37,4 +37,8 @@ NS_SWIFT_NAME(Settings) + (BOOL)iCLoudSynchronizationEnabled; + (void)setICLoudSynchronizationEnabled:(BOOL)iCLoudSyncEnabled; ++ (void)initializeLogging; ++ (BOOL)isFileLoggingEnabled; ++ (void)setFileLoggingEnabled:(BOOL)fileLoggingEnabled; + @end diff --git a/iphone/Maps/Core/Settings/MWMSettings.mm b/iphone/Maps/Core/Settings/MWMSettings.mm index 8d93508b13..28bc2b835c 100644 --- a/iphone/Maps/Core/Settings/MWMSettings.mm +++ b/iphone/Maps/Core/Settings/MWMSettings.mm @@ -3,8 +3,8 @@ #import "MWMMapViewControlsManager.h" #import "SwiftBridge.h" - #include +#include namespace { @@ -19,6 +19,7 @@ NSString * const kThemeMode = @"ThemeMode"; NSString * const kSpotlightLocaleLanguageId = @"SpotlightLocaleLanguageId"; NSString * const kUDTrackWarningAlertWasShown = @"TrackWarningAlertWasShown"; NSString * const kiCLoudSynchronizationEnabledKey = @"iCLoudSynchronizationEnabled"; +NSString * const kUDFileLoggingEnabledKey = @"FileLoggingEnabledKey"; } // namespace @implementation MWMSettings @@ -167,4 +168,21 @@ NSString * const kiCLoudSynchronizationEnabledKey = @"iCLoudSynchronizationEnabl [NSUserDefaults.standardUserDefaults setBool:iCLoudSyncEnabled forKey:kiCLoudSynchronizationEnabledKey]; [NSNotificationCenter.defaultCenter postNotificationName:NSNotification.iCloudSynchronizationDidChangeEnabledState object:nil]; } + ++ (void)initializeLogging { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self setFileLoggingEnabled:[self isFileLoggingEnabled]]; + }); +} + ++ (BOOL)isFileLoggingEnabled { + return [NSUserDefaults.standardUserDefaults boolForKey:kUDFileLoggingEnabledKey]; +} + ++ (void)setFileLoggingEnabled:(BOOL)fileLoggingEnabled { + [NSUserDefaults.standardUserDefaults setBool:fileLoggingEnabled forKey:kUDFileLoggingEnabledKey]; + [Logger setFileLoggingEnabled:fileLoggingEnabled]; +} + @end diff --git a/iphone/Maps/Maps.xcodeproj/project.pbxproj b/iphone/Maps/Maps.xcodeproj/project.pbxproj index 3f504e4cd4..fb473654b1 100644 --- a/iphone/Maps/Maps.xcodeproj/project.pbxproj +++ b/iphone/Maps/Maps.xcodeproj/project.pbxproj @@ -482,6 +482,7 @@ ED79A5D72BDF8D6100952D1F /* SynchronizationStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5CF2BDF8D6100952D1F /* SynchronizationStateManager.swift */; }; ED79A5D82BDF8D6100952D1F /* DefaultLocalDirectoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5D02BDF8D6100952D1F /* DefaultLocalDirectoryMonitor.swift */; }; ED7CCC4F2C1362E300E2A737 /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED7CCC4E2C1362E300E2A737 /* FileType.swift */; }; + ED8270F02C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8270EF2C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift */; }; ED9966802B94FBC20083CE55 /* ColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED99667D2B94FBC20083CE55 /* ColorPicker.swift */; }; EDBD68072B625724005DD151 /* LocationServicesDisabledAlert.xib in Resources */ = {isa = PBXBuildFile; fileRef = EDBD68062B625724005DD151 /* LocationServicesDisabledAlert.xib */; }; EDBD680B2B62572E005DD151 /* LocationServicesDisabledAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDBD680A2B62572E005DD151 /* LocationServicesDisabledAlert.swift */; }; @@ -1394,6 +1395,7 @@ ED79A5CF2BDF8D6100952D1F /* SynchronizationStateManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizationStateManager.swift; sourceTree = ""; }; ED79A5D02BDF8D6100952D1F /* DefaultLocalDirectoryMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultLocalDirectoryMonitor.swift; sourceTree = ""; }; ED7CCC4E2C1362E300E2A737 /* FileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileType.swift; sourceTree = ""; }; + ED8270EF2C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTableViewDetailedSwitchCell.swift; sourceTree = ""; }; ED99667D2B94FBC20083CE55 /* ColorPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorPicker.swift; sourceTree = ""; }; EDBD68062B625724005DD151 /* LocationServicesDisabledAlert.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LocationServicesDisabledAlert.xib; sourceTree = ""; }; EDBD680A2B62572E005DD151 /* LocationServicesDisabledAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationServicesDisabledAlert.swift; sourceTree = ""; }; @@ -3726,6 +3728,7 @@ F6E2FD381E097BA00083EBEC /* SettingsTableViewLinkCell.swift */, F6E2FD391E097BA00083EBEC /* SettingsTableViewSelectableCell.swift */, F6E2FD3A1E097BA00083EBEC /* SettingsTableViewSwitchCell.swift */, + ED8270EF2C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift */, ); path = Cells; sourceTree = ""; @@ -4445,6 +4448,7 @@ 3454D7C21E07F045004AF2AD /* NSString+Categories.m in Sources */, 34E7761F1F14DB48003040B3 /* PlacePageArea.swift in Sources */, ED79A5D82BDF8D6100952D1F /* DefaultLocalDirectoryMonitor.swift in Sources */, + ED8270F02C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift in Sources */, 4728F69322CF89A400E00028 /* GradientView.swift in Sources */, F6381BF61CD12045004CA943 /* LocaleTranslator.mm in Sources */, 9917D17F2397B1D600A7E06E /* IPadModalPresentationController.swift in Sources */, diff --git a/iphone/Maps/UI/Help/AboutController/AboutController.swift b/iphone/Maps/UI/Help/AboutController/AboutController.swift index 15c5ce7af5..59fd59ccf6 100644 --- a/iphone/Maps/UI/Help/AboutController/AboutController.swift +++ b/iphone/Maps/UI/Help/AboutController/AboutController.swift @@ -1,3 +1,5 @@ +import OSLog + final class AboutController: MWMViewController { fileprivate struct AboutInfoTableViewCellModel { @@ -276,7 +278,13 @@ private extension AboutController { self?.navigationController?.pushViewController(FaqController(), animated: true) case .reportABug: guard let link = aboutInfo.link else { fatalError("The recipient link should be provided to report a bug.") } - self?.sendEmailWith(header: "Organic Maps Bugreport", toRecipients: [link]) + UIApplication.shared.showLoadingOverlay { + let logFileURL = Logger.getLogFileURL() + UIApplication.shared.hideLoadingOverlay { + guard let self else { return } + self.sendEmailWith(header: "Organic Maps Bugreport", toRecipients: [link], attachmentFileURL: logFileURL) + } + } case .reportMapDataProblem, .volunteer, .news: self?.openUrl(aboutInfo.link) case .rateTheApp: @@ -436,7 +444,7 @@ extension AboutController: UICollectionViewDelegateFlowLayout { // MARK: - Mail Composing private extension AboutController { - func sendEmailWith(header: String, toRecipients: [String]) { + func sendEmailWith(header: String, toRecipients: [String], attachmentFileURL: URL? = nil) { func emailSubject(subject: String) -> String { let appInfo = AppInfo.shared() return String(format:"[%@-%@ iOS] %@", appInfo.bundleVersion, appInfo.buildNumber, subject) @@ -496,9 +504,34 @@ private extension AboutController { return false } + func showMailComposingAlert() { + let text = String(format:L("email_error_body"), toRecipients.joined(separator: ";")) + let alert = UIAlertController(title: L("email_error_title"), message: text, preferredStyle: .alert) + let action = UIAlertAction(title: L("ok"), style: .default, handler: nil) + alert.addAction(action) + present(alert, animated: true, completion: nil) + } + let subject = emailSubject(subject: header) let body = emailBody() + // If the attachment file path is provided, the default mail composer should be used. + if let attachmentFileURL { + if MWMMailViewController.canSendMail(), let attachmentData = try? Data(contentsOf: attachmentFileURL) { + let mailViewController = MWMMailViewController() + mailViewController.mailComposeDelegate = self + mailViewController.setSubject(subject) + mailViewController.setMessageBody(body, isHTML:false) + mailViewController.setToRecipients(toRecipients) + mailViewController.addAttachmentData(attachmentData, mimeType: "application/zip", fileName: attachmentFileURL.lastPathComponent) + + self.present(mailViewController, animated: true, completion:nil) + } else { + showMailComposingAlert() + } + return + } + // Before iOS 14, try to open alternate email apps first, assuming that if users installed them, they're using them. let os = ProcessInfo().operatingSystemVersion if (os.majorVersion < 14 && (openGmail(subject: subject, body: body, recipients: toRecipients) || @@ -507,14 +540,8 @@ private extension AboutController { } // From iOS 14, it is possible to change the default mail app, and mailto should open a default mail app. - if openDefaultMailApp(subject: subject, body: body, recipients: toRecipients) { - return - } else { - let text = String(format:L("email_error_body"), toRecipients.joined(separator: ";")) - let alert = UIAlertController(title: L("email_error_title"), message: text, preferredStyle: .alert) - let action = UIAlertAction(title: L("ok"), style: .default, handler: nil) - alert.addAction(action) - present(alert, animated: true, completion: nil) + if !openDefaultMailApp(subject: subject, body: body, recipients: toRecipients) { + showMailComposingAlert() } } } @@ -537,3 +564,10 @@ private extension UIStackView { addArrangedSubview(view) } } + +// MARK: - MFMailComposeViewControllerDelegate +extension AboutController: MFMailComposeViewControllerDelegate { + func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + dismiss(animated: true, completion: nil) + } +} diff --git a/iphone/Maps/UI/Settings/Cells/SettingsTableViewDetailedSwitchCell.swift b/iphone/Maps/UI/Settings/Cells/SettingsTableViewDetailedSwitchCell.swift new file mode 100644 index 0000000000..4227207116 --- /dev/null +++ b/iphone/Maps/UI/Settings/Cells/SettingsTableViewDetailedSwitchCell.swift @@ -0,0 +1,20 @@ +class SettingsTableViewDetailedSwitchCell: SettingsTableViewSwitchCell { + + override func awakeFromNib() { + super.awakeFromNib() + styleDetail() + } + + @objc + func setDetail(_ text: String?) { + detailTextLabel?.text = text + setNeedsLayout() + } + + private func styleDetail() { + let detailTextLabelStyle = "regular12:blackSecondaryText" + detailTextLabel?.setStyleAndApply(detailTextLabelStyle) + detailTextLabel?.numberOfLines = 0 + detailTextLabel?.lineBreakMode = .byWordWrapping + } +} diff --git a/iphone/Maps/UI/Settings/Cells/SettingsTableViewiCloudSwitchCell.swift b/iphone/Maps/UI/Settings/Cells/SettingsTableViewiCloudSwitchCell.swift index 7371c6e5ec..a9c211a5f8 100644 --- a/iphone/Maps/UI/Settings/Cells/SettingsTableViewiCloudSwitchCell.swift +++ b/iphone/Maps/UI/Settings/Cells/SettingsTableViewiCloudSwitchCell.swift @@ -1,9 +1,4 @@ -final class SettingsTableViewiCloudSwitchCell: SettingsTableViewSwitchCell { - - override func awakeFromNib() { - super.awakeFromNib() - styleDetail() - } +final class SettingsTableViewiCloudSwitchCell: SettingsTableViewDetailedSwitchCell { @objc func updateWithError(_ error: NSError?) { @@ -24,11 +19,4 @@ final class SettingsTableViewiCloudSwitchCell: SettingsTableViewSwitchCell { } setNeedsLayout() } - - private func styleDetail() { - let detailTextLabelStyle = "regular12:blackSecondaryText" - detailTextLabel?.setStyleAndApply(detailTextLabelStyle) - detailTextLabel?.numberOfLines = 0 - detailTextLabel?.lineBreakMode = .byWordWrapping - } } diff --git a/iphone/Maps/UI/Settings/MWMSettingsViewController.mm b/iphone/Maps/UI/Settings/MWMSettingsViewController.mm index ee9a79d606..83efc3cd22 100644 --- a/iphone/Maps/UI/Settings/MWMSettingsViewController.mm +++ b/iphone/Maps/UI/Settings/MWMSettingsViewController.mm @@ -33,7 +33,7 @@ static NSString * const kUDDidShowICloudSynchronizationEnablingAlert = @"kUDDidS @property(weak, nonatomic) IBOutlet SettingsTableViewLinkCell *voiceInstructionsCell; @property(weak, nonatomic) IBOutlet SettingsTableViewLinkCell *drivingOptionsCell; @property(weak, nonatomic) IBOutlet SettingsTableViewiCloudSwitchCell *iCloudSynchronizationCell; - +@property(weak, nonatomic) IBOutlet SettingsTableViewDetailedSwitchCell *enableLoggingCell; @end @@ -189,9 +189,21 @@ static NSString * const kUDDidShowICloudSynchronizationEnablingAlert = @"kUDDidS [self.iCloudSynchronizationCell configWithDelegate:self title:@"iCloud Synchronization (Beta)" isOn:isICLoudSynchronizationEnabled]; + + __weak __typeof(self) weakSelf = self; [CloudStorageManager.shared addObserver:self onErrorCompletionHandler:^(NSError * _Nullable error) { - [self.iCloudSynchronizationCell updateWithError:error]; + __strong auto strongSelf = weakSelf; + [strongSelf.iCloudSynchronizationCell updateWithError:error]; }]; + + [self.enableLoggingCell configWithDelegate:self title:L(@"enable_logging") isOn:MWMSettings.isFileLoggingEnabled]; + [self updateLogFileSize]; +} + +- (void)updateLogFileSize { + uint64_t logFileSize = [Logger getLogFileSize]; + NSString * detailString = logFileSize == 0 ? nil : [NSString stringWithFormat:L(@"log_file_size"), formattedSize(logFileSize)]; + [self.enableLoggingCell setDetail:detailString]; } - (void)show3dBuildingsAlert:(UITapGestureRecognizer *)recognizer { @@ -324,6 +336,9 @@ static NSString * const kUDDidShowICloudSynchronizationEnablingAlert = @"kUDDidS } else { [MWMSettings setICLoudSynchronizationEnabled:value]; } + } else if (cell == self.enableLoggingCell) { + [MWMSettings setFileLoggingEnabled:value]; + [self updateLogFileSize]; } } @@ -373,4 +388,13 @@ static NSString * const kUDDidShowICloudSynchronizationEnablingAlert = @"kUDDidS } } +- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section { + switch (section) { + case 1: + return L(@"enable_logging_warning_message"); + default: + return nil; + } +} + @end diff --git a/iphone/Maps/UI/Storyboard/Settings.storyboard b/iphone/Maps/UI/Storyboard/Settings.storyboard index f2fd3cc6be..dc2993590b 100644 --- a/iphone/Maps/UI/Storyboard/Settings.storyboard +++ b/iphone/Maps/UI/Storyboard/Settings.storyboard @@ -1,9 +1,9 @@ - + - + @@ -292,7 +292,7 @@ + + + + + + + + + + + + - + @@ -332,7 +357,7 @@ - + @@ -350,7 +375,7 @@ - + @@ -376,22 +401,22 @@ - + - + -