// // Copyright © 2018 WireGuard LLC. All rights reserved. // import UIKit import os.log import CoreData import BNRCoreDataStack import NetworkExtension protocol TunnelsTableViewControllerDelegate: class { func addProvider(tunnelsTableViewController: TunnelsTableViewController) func connect(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) func disconnect(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) func info(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) func delete(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) func status(for tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) -> NEVPNStatus func showSettings() } class TunnelsTableViewController: UITableViewController { weak var delegate: TunnelsTableViewControllerDelegate? var viewContext: NSManagedObjectContext! @IBOutlet var settingsButton: UIBarButtonItem! @IBOutlet var editButton: UIBarButtonItem! @IBOutlet var doneButton: UIBarButtonItem! private lazy var fetchedResultsController: FetchedResultsController = { let fetchRequest = NSFetchRequest() fetchRequest.entity = Tunnel.entity() fetchRequest.sortDescriptors = [NSSortDescriptor(key: "title", ascending: true)] let frc = FetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: viewContext) frc.setDelegate(self.frcDelegate) return frc }() public func updateStatus(for tunnelIdentifier: String) { viewContext.perform { do { let tunnel = try Tunnel.findFirstInContext(self.viewContext, predicate: NSPredicate(format: "tunnelIdentifier == %@", tunnelIdentifier)) if let tunnel = tunnel { if let indexPath = self.fetchedResultsController.indexPathForObject(tunnel) { self.tableView.reloadRows(at: [indexPath], with: .none) } } } catch { os_log("Unable to load tunnel for tunnel identifier: %{public}@", log: Log.general, type: .error, error.localizedDescription) } } } private lazy var frcDelegate: TunnelFetchedResultsControllerDelegate = { // swiftlint:disable:this weak_delegate return TunnelFetchedResultsControllerDelegate(tableView: self.tableView) }() override func viewDidLoad() { super.viewDidLoad() do { try fetchedResultsController.performFetch() } catch { print("Failed to fetch objects: \(error)") } // Get rid of seperator lines in table. tableView.tableFooterView = UIView(frame: CGRect.zero) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) updateBarButtons() } @IBAction func editTunnels(_ sender: Any) { tableView.setEditing(!tableView.isEditing, animated: true) updateBarButtons() } private func updateBarButtons() { navigationController?.setToolbarHidden(tableView.isEditing, animated: true) if tableView.isEditing { self.navigationItem.setRightBarButtonItems([doneButton], animated: true) } else { self.navigationItem.setRightBarButtonItems([settingsButton, editButton], animated: true) } } @IBAction func showSettings(_ sender: Any) { delegate?.showSettings() } @IBAction func addProvider(_ sender: UIBarButtonItem) { delegate?.addProvider(tunnelsTableViewController: self) } override func numberOfSections(in tableView: UITableView) -> Int { return 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return fetchedResultsController.sections?[0].objects.count ?? 0 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(type: TunnelTableViewCell.self, for: indexPath) cell.delegate = self guard let sections = fetchedResultsController.sections else { fatalError("FetchedResultsController \(fetchedResultsController) should have sections, but found nil") } let section = sections[indexPath.section] let tunnel = section.objects[indexPath.row] cell.configure(tunnel: tunnel, status: delegate?.status(for: tunnel, tunnelsTableViewController: self) ?? .invalid) return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let sections = fetchedResultsController.sections else { fatalError("FetchedResultsController \(fetchedResultsController) should have sections, but found nil") } let section = sections[indexPath.section] let tunnel = section.objects[indexPath.row] delegate?.info(tunnel: tunnel, tunnelsTableViewController: self) tableView.deselectRow(at: indexPath, animated: true) } override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) { guard let sections = fetchedResultsController.sections else { fatalError("FetchedResultsController \(fetchedResultsController) should have sections, but found nil") } let section = sections[indexPath.section] let tunnel = section.objects[indexPath.row] delegate?.info(tunnel: tunnel, tunnelsTableViewController: self) } override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { return true } override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { guard let sections = fetchedResultsController.sections else { fatalError("FetchedResultsController \(fetchedResultsController) should have sections, but found nil") } let section = sections[indexPath.section] let tunnel = section.objects[indexPath.row] delegate?.delete(tunnel: tunnel, tunnelsTableViewController: self) } } } extension TunnelsTableViewController: TunnelTableViewCellDelegate { func connect(tunnelIdentifier: String) { let tunnel = try? Tunnel.findFirstInContext(self.viewContext, predicate: NSPredicate(format: "tunnelIdentifier == %@", tunnelIdentifier)) if let tunnel = tunnel { self.delegate?.connect(tunnel: tunnel!, tunnelsTableViewController: self) } } func disconnect(tunnelIdentifier: String) { let tunnel = try? Tunnel.findFirstInContext(self.viewContext, predicate: NSPredicate(format: "tunnelIdentifier == %@", tunnelIdentifier)) if let tunnel = tunnel { self.delegate?.disconnect(tunnel: tunnel!, tunnelsTableViewController: self) } } } extension TunnelsTableViewController: Identifyable {} class TunnelFetchedResultsControllerDelegate: NSObject, FetchedResultsControllerDelegate { private weak var tableView: UITableView? // MARK: - Lifecycle init(tableView: UITableView) { self.tableView = tableView } func fetchedResultsControllerDidPerformFetch(_ controller: FetchedResultsController) { tableView?.reloadData() } func fetchedResultsControllerWillChangeContent(_ controller: FetchedResultsController) { tableView?.beginUpdates() } func fetchedResultsControllerDidChangeContent(_ controller: FetchedResultsController) { tableView?.endUpdates() } func fetchedResultsController(_ controller: FetchedResultsController, didChangeObject change: FetchedResultsObjectChange) { guard let tableView = tableView else { return } switch change { case let .insert(_, indexPath): tableView.insertRows(at: [indexPath], with: .automatic) case let .delete(_, indexPath): tableView.deleteRows(at: [indexPath], with: .automatic) case let .move(_, fromIndexPath, toIndexPath): tableView.moveRow(at: fromIndexPath, to: toIndexPath) case let .update(_, indexPath): tableView.reloadRows(at: [indexPath], with: .automatic) } } func fetchedResultsController(_ controller: FetchedResultsController, didChangeSection change: FetchedResultsSectionChange) { guard let tableView = tableView else { return } switch change { case let .insert(_, index): tableView.insertSections(IndexSet(integer: index), with: .automatic) case let .delete(_, index): tableView.deleteSections(IndexSet(integer: index), with: .automatic) } } } protocol TunnelTableViewCellDelegate: class { func connect(tunnelIdentifier: String) func disconnect(tunnelIdentifier: String) } class TunnelTableViewCell: UITableViewCell { @IBOutlet weak var tunnelTitleLabel: UILabel! @IBOutlet weak var activityIndicator: UIActivityIndicatorView! @IBOutlet weak var tunnelSwitch: UISwitch! weak var delegate: TunnelTableViewCellDelegate? private var tunnelIdentifier: String? @IBAction func tunnelSwitchChanged(_ sender: Any) { tunnelSwitch.isEnabled = false guard let tunnelIdentifier = tunnelIdentifier else { return } if tunnelSwitch.isOn { delegate?.connect(tunnelIdentifier: tunnelIdentifier) } else { delegate?.disconnect(tunnelIdentifier: tunnelIdentifier) } } func configure(tunnel: Tunnel, status: NEVPNStatus) { self.tunnelTitleLabel?.text = tunnel.title tunnelIdentifier = tunnel.tunnelIdentifier if status == .connecting || status == .disconnecting || status == .reasserting { activityIndicator.startAnimating() tunnelSwitch.isHidden = true } else { activityIndicator.stopAnimating() tunnelSwitch.isHidden = false } tunnelSwitch.isOn = status == .connected tunnelSwitch.onTintColor = status == .invalid || status == .reasserting ? .gray : .green tunnelSwitch.isEnabled = true } override func setEditing(_ editing: Bool, animated: Bool) { super.setEditing(editing, animated: animated) tunnelSwitch.isHidden = editing } } extension TunnelTableViewCell: Identifyable {}