// SPDX-License-Identifier: MIT // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. import UIKit import MobileCoreServices import UserNotifications class TunnelsListTableViewController: UIViewController { var tunnelsManager: TunnelsManager? let tableView: UITableView = { let tableView = UITableView(frame: CGRect.zero, style: .plain) tableView.estimatedRowHeight = 60 tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none tableView.register(TunnelListCell.self) return tableView }() let centeredAddButton: BorderedTextButton = { let button = BorderedTextButton() button.title = tr("tunnelsListCenteredAddTunnelButtonTitle") button.isHidden = true return button }() let busyIndicator: UIActivityIndicatorView = { let busyIndicator = UIActivityIndicatorView(style: .gray) busyIndicator.hidesWhenStopped = true return busyIndicator }() var detailDisplayedTunnel: TunnelContainer? override func loadView() { view = UIView() view.backgroundColor = .white tableView.dataSource = self tableView.delegate = self view.addSubview(tableView) tableView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tableView.topAnchor.constraint(equalTo: view.topAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) view.addSubview(busyIndicator) busyIndicator.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ busyIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor), busyIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor) ]) view.addSubview(centeredAddButton) centeredAddButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ centeredAddButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), centeredAddButton.centerYAnchor.constraint(equalTo: view.centerYAnchor) ]) centeredAddButton.onTapped = { [weak self] in guard let self = self else { return } self.addButtonTapped(sender: self.centeredAddButton) } busyIndicator.startAnimating() } override func viewDidLoad() { super.viewDidLoad() title = tr("tunnelsListTitle") navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:))) navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSettingsButtonTitle"), style: .plain, target: self, action: #selector(settingsButtonTapped(sender:))) restorationIdentifier = "TunnelsListVC" } func setTunnelsManager(tunnelsManager: TunnelsManager) { self.tunnelsManager = tunnelsManager tunnelsManager.tunnelsListDelegate = self busyIndicator.stopAnimating() tableView.reloadData() centeredAddButton.isHidden = tunnelsManager.numberOfTunnels() > 0 } override func viewWillAppear(_: Bool) { if let selectedRowIndexPath = tableView.indexPathForSelectedRow { tableView.deselectRow(at: selectedRowIndexPath, animated: false) } } @objc func addButtonTapped(sender: AnyObject) { guard tunnelsManager != nil else { return } let alert = UIAlertController(title: "", message: tr("addTunnelMenuHeader"), preferredStyle: .actionSheet) let importFileAction = UIAlertAction(title: tr("addTunnelMenuImportFile"), style: .default) { [weak self] _ in self?.presentViewControllerForFileImport() } alert.addAction(importFileAction) let scanQRCodeAction = UIAlertAction(title: tr("addTunnelMenuQRCode"), style: .default) { [weak self] _ in self?.presentViewControllerForScanningQRCode() } alert.addAction(scanQRCodeAction) let createFromScratchAction = UIAlertAction(title: tr("addTunnelMenuFromScratch"), style: .default) { [weak self] _ in if let self = self, let tunnelsManager = self.tunnelsManager { self.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager) } } alert.addAction(createFromScratchAction) let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel) alert.addAction(cancelAction) if let sender = sender as? UIBarButtonItem { alert.popoverPresentationController?.barButtonItem = sender } else if let sender = sender as? UIView { alert.popoverPresentationController?.sourceView = sender alert.popoverPresentationController?.sourceRect = sender.bounds } present(alert, animated: true, completion: nil) } @objc func settingsButtonTapped(sender: UIBarButtonItem) { guard tunnelsManager != nil else { return } let settingsVC = SettingsTableViewController(tunnelsManager: tunnelsManager) let settingsNC = UINavigationController(rootViewController: settingsVC) settingsNC.modalPresentationStyle = .formSheet present(settingsNC, animated: true) } func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager) { let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager) let editNC = UINavigationController(rootViewController: editVC) editNC.modalPresentationStyle = .formSheet present(editNC, animated: true) } func presentViewControllerForFileImport() { let documentTypes = ["com.wireguard.config.quick", String(kUTTypeText), String(kUTTypeZipArchive)] let filePicker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import) filePicker.delegate = self present(filePicker, animated: true) } func presentViewControllerForScanningQRCode() { let scanQRCodeVC = QRScanViewController() scanQRCodeVC.delegate = self let scanQRCodeNC = UINavigationController(rootViewController: scanQRCodeVC) scanQRCodeNC.modalPresentationStyle = .fullScreen present(scanQRCodeNC, animated: true) } } extension TunnelsListTableViewController: UIDocumentPickerDelegate { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { guard let tunnelsManager = tunnelsManager else { return } urls.forEach { url in TunnelImporter.importFromFile(url: url, into: tunnelsManager, sourceVC: self, errorPresenterType: ErrorPresenter.self) } } } extension TunnelsListTableViewController: QRScanViewControllerDelegate { func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController, completionHandler: (() -> Void)?) { tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { result in if let error = result.error { ErrorPresenter.showErrorAlert(error: error, from: qrScanViewController, onDismissal: completionHandler) } else { completionHandler?() } } } } extension TunnelsListTableViewController: UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { return 1 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return (tunnelsManager?.numberOfTunnels() ?? 0) } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: TunnelListCell = tableView.dequeueReusableCell(for: indexPath) if let tunnelsManager = tunnelsManager { let tunnel = tunnelsManager.tunnel(at: indexPath.row) cell.tunnel = tunnel cell.onSwitchToggled = { [weak self] isOn in guard let self = self, let tunnelsManager = self.tunnelsManager else { return } if isOn { tunnelsManager.startActivation(of: tunnel) } else { tunnelsManager.startDeactivation(of: tunnel) } } } return cell } } extension TunnelsListTableViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let tunnelsManager = tunnelsManager else { return } let tunnel = tunnelsManager.tunnel(at: indexPath.row) let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel) let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC) tunnelDetailNC.restorationIdentifier = "DetailNC" showDetailViewController(tunnelDetailNC, sender: self) // Shall get propagated up to the split-vc detailDisplayedTunnel = tunnel } func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let deleteAction = UIContextualAction(style: .destructive, title: tr("tunnelsListSwipeDeleteButtonTitle")) { [weak self] _, _, completionHandler in guard let tunnelsManager = self?.tunnelsManager else { return } let tunnel = tunnelsManager.tunnel(at: indexPath.row) tunnelsManager.remove(tunnel: tunnel) { error in if error != nil { ErrorPresenter.showErrorAlert(error: error!, from: self) completionHandler(false) } else { completionHandler(true) } } } return UISwipeActionsConfiguration(actions: [deleteAction]) } } extension TunnelsListTableViewController: TunnelsManagerListDelegate { func tunnelAdded(at index: Int) { tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic) centeredAddButton.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0) } func tunnelModified(at index: Int) { tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic) } func tunnelMoved(from oldIndex: Int, to newIndex: Int) { tableView.moveRow(at: IndexPath(row: oldIndex, section: 0), to: IndexPath(row: newIndex, section: 0)) } func tunnelRemoved(at index: Int, tunnel: TunnelContainer) { tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) centeredAddButton.isHidden = tunnelsManager?.numberOfTunnels() ?? 0 > 0 if detailDisplayedTunnel == tunnel, let splitViewController = splitViewController { if splitViewController.isCollapsed != false { (splitViewController.viewControllers[0] as? UINavigationController)?.popToRootViewController(animated: false) } else { let detailVC = UIViewController() detailVC.view.backgroundColor = .white let detailNC = UINavigationController(rootViewController: detailVC) splitViewController.showDetailViewController(detailNC, sender: self) } detailDisplayedTunnel = nil if let presentedNavController = self.presentedViewController as? UINavigationController, presentedNavController.viewControllers.first is TunnelEditTableViewController { self.presentedViewController?.dismiss(animated: false, completion: nil) } } } }