From cb051f695db44e7a52e3f423fa27de00c493a9ac Mon Sep 17 00:00:00 2001 From: Eric Kuck Date: Fri, 14 Dec 2018 17:27:11 -0600 Subject: Reorganized project structure Signed-off-by: Eric Kuck --- .../SettingsTableViewController.swift | 200 +++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 WireGuard/WireGuard/UI/iOS/ViewController/SettingsTableViewController.swift (limited to 'WireGuard/WireGuard/UI/iOS/ViewController/SettingsTableViewController.swift') diff --git a/WireGuard/WireGuard/UI/iOS/ViewController/SettingsTableViewController.swift b/WireGuard/WireGuard/UI/iOS/ViewController/SettingsTableViewController.swift new file mode 100644 index 0000000..b583c5b --- /dev/null +++ b/WireGuard/WireGuard/UI/iOS/ViewController/SettingsTableViewController.swift @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018 WireGuard LLC. All Rights Reserved. + +import UIKit +import os.log + +class SettingsTableViewController: UITableViewController { + + enum SettingsFields: String { + case iosAppVersion = "WireGuard for iOS" + case goBackendVersion = "WireGuard Go Backend" + case exportZipArchive = "Export zip archive" + case exportLogFile = "Export log file" + } + + let settingsFieldsBySection: [[SettingsFields]] = [ + [.iosAppVersion, .goBackendVersion], + [.exportZipArchive], + [.exportLogFile] + ] + + let tunnelsManager: TunnelsManager? + var wireguardCaptionedImage: (view: UIView, size: CGSize)? + + init(tunnelsManager: TunnelsManager?) { + self.tunnelsManager = tunnelsManager + super.init(style: .grouped) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + title = "Settings" + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped)) + + tableView.estimatedRowHeight = 44 + tableView.rowHeight = UITableView.automaticDimension + tableView.allowsSelection = false + + tableView.register(KeyValueCell.self) + tableView.register(ButtonCell.self) + + tableView.tableFooterView = UIImageView(image: UIImage(named: "wireguard.pdf")) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + guard let logo = tableView.tableFooterView else { return } + + let bottomPadding = max(tableView.layoutMargins.bottom, 10) + let fullHeight = max(tableView.contentSize.height, tableView.bounds.size.height - tableView.layoutMargins.top - bottomPadding) + + let imageAspectRatio = logo.intrinsicContentSize.width / logo.intrinsicContentSize.height + + var height = tableView.estimatedRowHeight * 1.5 + var width = height * imageAspectRatio + let maxWidth = view.bounds.size.width - max(tableView.layoutMargins.left + tableView.layoutMargins.right, 20) + if width > maxWidth { + width = maxWidth + height = width / imageAspectRatio + } + + let needsReload = height != logo.frame.height + + logo.frame = CGRect(x: (view.bounds.size.width - width) / 2, y: fullHeight - height, width: width, height: height) + + if needsReload { + tableView.tableFooterView = logo + } + } + + @objc func doneTapped() { + dismiss(animated: true, completion: nil) + } + + func exportConfigurationsAsZipFile(sourceView: UIView) { + guard let tunnelsManager = tunnelsManager else { return } + guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } + + let destinationURL = destinationDir.appendingPathComponent("wireguard-export.zip") + _ = FileManager.deleteFile(at: destinationURL) + + let count = tunnelsManager.numberOfTunnels() + let tunnelConfigurations = (0 ..< count).compactMap { tunnelsManager.tunnel(at: $0).tunnelConfiguration() } + ZipExporter.exportConfigFiles(tunnelConfigurations: tunnelConfigurations, to: destinationURL) { [weak self] error in + if let error = error { + ErrorPresenter.showErrorAlert(error: error, from: self) + return + } + + let fileExportVC = UIDocumentPickerViewController(url: destinationURL, in: .exportToService) + self?.present(fileExportVC, animated: true, completion: nil) + } + } + + func exportLogForLastActivatedTunnel(sourceView: UIView) { + guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } + + let dateFormatter = ISO8601DateFormatter() + dateFormatter.formatOptions = [.withFullDate, .withTime, .withTimeZone] // Avoid ':' in the filename + let timeStampString = dateFormatter.string(from: Date()) + let destinationURL = destinationDir.appendingPathComponent("wireguard-log-\(timeStampString).txt") + + DispatchQueue.global(qos: .userInitiated).async { + + if FileManager.default.fileExists(atPath: destinationURL.path) { + let isDeleted = FileManager.deleteFile(at: destinationURL) + if !isDeleted { + ErrorPresenter.showErrorAlert(title: "Log export failed", message: "The pre-existing log could not be cleared", from: self) + return + } + } + + guard let networkExtensionLogFilePath = FileManager.networkExtensionLogFileURL?.path else { + ErrorPresenter.showErrorAlert(title: "Log export failed", message: "Unable to determine extension log path", from: self) + return + } + + let isWritten = Logger.global?.writeLog(called: "APP", mergedWith: networkExtensionLogFilePath, called: "NET", to: destinationURL.path) ?? false + guard isWritten else { + ErrorPresenter.showErrorAlert(title: "Log export failed", message: "Unable to write logs to file", from: self) + return + } + + DispatchQueue.main.async { + let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil) + // popoverPresentationController shall be non-nil on the iPad + activityVC.popoverPresentationController?.sourceView = sourceView + activityVC.popoverPresentationController?.sourceRect = sourceView.bounds + activityVC.completionWithItemsHandler = { _, _, _, _ in + // Remove the exported log file after the activity has completed + _ = FileManager.deleteFile(at: destinationURL) + } + self.present(activityVC, animated: true) + } + } + } +} + +// MARK: UITableViewDataSource + +extension SettingsTableViewController { + override func numberOfSections(in tableView: UITableView) -> Int { + return settingsFieldsBySection.count + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return settingsFieldsBySection[section].count + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch section { + case 0: + return "About" + case 1: + return "Export configurations" + case 2: + return "Tunnel log" + default: + return nil + } + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let field = settingsFieldsBySection[indexPath.section][indexPath.row] + if field == .iosAppVersion || field == .goBackendVersion { + let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) + cell.copyableGesture = false + cell.key = field.rawValue + if field == .iosAppVersion { + var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version" + if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { + appVersion += " (\(appBuild))" + } + cell.value = appVersion + } else if field == .goBackendVersion { + cell.value = WIREGUARD_GO_VERSION + } + return cell + } else if field == .exportZipArchive { + let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) + cell.buttonText = field.rawValue + cell.onTapped = { [weak self] in + self?.exportConfigurationsAsZipFile(sourceView: cell.button) + } + return cell + } else { + assert(field == .exportLogFile) + let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) + cell.buttonText = field.rawValue + cell.onTapped = { [weak self] in + self?.exportLogForLastActivatedTunnel(sourceView: cell.button) + } + return cell + } + } +} -- cgit v1.2.3-59-g8ed1b