BackupTransferViewController.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import Foundation
  2. import UIKit
  3. import DcCore
  4. import SDWebImageSVGKitPlugin
  5. class BackupTransferViewController: UIViewController {
  6. private let dcContext: DcContext
  7. private let dcAccounts: DcAccounts
  8. private var dcBackupProvider: DcBackupProvider?
  9. private var imexObserver: NSObjectProtocol?
  10. private lazy var qrContentView: UIImageView = {
  11. let view = UIImageView()
  12. view.contentMode = .scaleAspectFit
  13. view.translatesAutoresizingMaskIntoConstraints = false
  14. view.accessibilityHint = String.localized("scan_to_transfer")
  15. return view
  16. }()
  17. private lazy var progressContainer: UIView = {
  18. let view = UIView()
  19. view.translatesAutoresizingMaskIntoConstraints = false
  20. view.layer.cornerRadius = 20
  21. view.clipsToBounds = true
  22. view.layer.borderColor = DcColors.grey50.cgColor
  23. view.layer.borderWidth = 1
  24. view.backgroundColor = DcColors.defaultInverseColor.withAlphaComponent(0.5)
  25. return view
  26. }()
  27. private lazy var progress: UIActivityIndicatorView = {
  28. let progress = UIActivityIndicatorView(style: .white)
  29. progress.translatesAutoresizingMaskIntoConstraints = false
  30. return progress
  31. }()
  32. init(dcAccounts: DcAccounts) {
  33. self.dcAccounts = dcAccounts
  34. self.dcContext = dcAccounts.getSelected()
  35. super.init(nibName: nil, bundle: nil)
  36. hidesBottomBarWhenPushed = true
  37. }
  38. required init?(coder _: NSCoder) {
  39. fatalError("init(coder:) has not been implemented")
  40. }
  41. // MARK: - lifecycle
  42. override func viewDidLoad() {
  43. super.viewDidLoad()
  44. title = String.localized("add_another_device")
  45. setupSubviews()
  46. // TODO: add some more hints about what is going on
  47. DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  48. guard let self = self else { return }
  49. self.dcAccounts.stopIo()
  50. self.dcBackupProvider = DcBackupProvider(self.dcContext)
  51. DispatchQueue.main.async {
  52. if !(self.dcBackupProvider?.isOk() ?? false) {
  53. self.showLastErrorAlert("Cannot create backup provider; try over in a minute")
  54. return
  55. }
  56. let image = self.getQrImage(svg: self.dcBackupProvider?.getQrSvg())
  57. self.qrContentView.image = image
  58. self.progress.stopAnimating()
  59. self.progressContainer.isHidden = true
  60. DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  61. guard let self = self else { return }
  62. self.dcBackupProvider?.wait()
  63. }
  64. }
  65. }
  66. }
  67. override func didMove(toParent parent: UIViewController?) {
  68. let isRemoved = parent == nil
  69. if isRemoved {
  70. if let imexObserver = self.imexObserver {
  71. NotificationCenter.default.removeObserver(imexObserver)
  72. }
  73. if dcBackupProvider != nil {
  74. dcContext.stopOngoingProcess()
  75. dcBackupProvider?.unref()
  76. dcBackupProvider = nil
  77. }
  78. dcAccounts.startIo()
  79. UIApplication.shared.isIdleTimerDisabled = false
  80. } else {
  81. UIApplication.shared.isIdleTimerDisabled = true
  82. imexObserver = NotificationCenter.default.addObserver(forName: dcNotificationImexProgress, object: nil, queue: nil) { [weak self] notification in
  83. guard let self = self, let ui = notification.userInfo, let permille = ui["progress"] as? Int else { return }
  84. var statusLineText = ""
  85. var hideQrCode = false
  86. if permille == 0 {
  87. self.showLastErrorAlert("Error")
  88. hideQrCode = true
  89. } else if permille <= 100 {
  90. statusLineText = "Exporting database..."
  91. } else if permille <= 300 {
  92. statusLineText = "Creating collection..."
  93. } else if permille <= 350 {
  94. statusLineText = "Collection created."
  95. } else if permille <= 400 {
  96. statusLineText = "Waiting for receiver..."
  97. } else if permille <= 450 {
  98. statusLineText = "Receiver connected..."
  99. hideQrCode = true
  100. } else if permille < 1000 {
  101. let percent = (permille-450)/5
  102. statusLineText = "Transfer... \(percent)%"
  103. hideQrCode = true
  104. } else if permille == 1000 {
  105. statusLineText = "Done."
  106. hideQrCode = true
  107. }
  108. self.title = statusLineText // TODO: this should be a dedicated view
  109. if hideQrCode && !self.qrContentView.isHidden {
  110. self.qrContentView.isHidden = true
  111. }
  112. }
  113. }
  114. }
  115. // MARK: - setup
  116. private func setupSubviews() {
  117. view.addSubview(qrContentView)
  118. view.addSubview(progressContainer)
  119. progressContainer.addSubview(progress)
  120. let qrDefaultWidth = qrContentView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor, multiplier: 0.75)
  121. qrDefaultWidth.priority = UILayoutPriority(500)
  122. qrDefaultWidth.isActive = true
  123. let qrMinWidth = qrContentView.widthAnchor.constraint(lessThanOrEqualToConstant: 260)
  124. qrMinWidth.priority = UILayoutPriority(999)
  125. qrMinWidth.isActive = true
  126. view.addConstraints([
  127. qrContentView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 1.05),
  128. qrContentView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
  129. qrContentView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
  130. progressContainer.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
  131. progressContainer.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
  132. progressContainer.constraintHeightTo(100),
  133. progressContainer.constraintWidthTo(100),
  134. progress.constraintCenterXTo(progressContainer),
  135. progress.constraintCenterYTo(progressContainer)
  136. ])
  137. progressContainer.isHidden = false
  138. progress.startAnimating()
  139. view.backgroundColor = DcColors.defaultBackgroundColor
  140. }
  141. private func getQrImage(svg: String?) -> UIImage? {
  142. if let svg = svg {
  143. let svgData = svg.data(using: .utf8)
  144. return SDImageSVGKCoder.shared.decodedImage(with: svgData, options: [:])
  145. }
  146. return nil
  147. }
  148. private func showLastErrorAlert(_ errorContext: String) {
  149. var lastError = dcContext.lastErrorString
  150. if lastError.isEmpty {
  151. lastError = "<last error not set>"
  152. }
  153. let error = errorContext + " (" + lastError + ")"
  154. let alert = UIAlertController(title: String.localized("Add Another Account"), message: error, preferredStyle: .alert)
  155. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: nil))
  156. navigationController?.present(alert, animated: true, completion: nil)
  157. }
  158. }
  159. /// Does a best effort attempt to trigger the local network privacy alert.
  160. ///
  161. /// It works by sending a UDP datagram to the discard service (port 9) of every
  162. /// IP address associated with a broadcast-capable interface. This should
  163. /// trigger the local network privacy alert, assuming the alert hasn’t already
  164. /// been displayed for this app.
  165. ///
  166. /// This code takes a ‘best effort’. It handles errors by ignoring them. As
  167. /// such, there’s guarantee that it’ll actually trigger the alert.
  168. ///
  169. /// - note: iOS devices don’t actually run the discard service. I’m using it
  170. /// here because I need a port to send the UDP datagram to and port 9 is
  171. /// always going to be safe (either the discard service is running, in which
  172. /// case it will discard the datagram, or it’s not, in which case the TCP/IP
  173. /// stack will discard it).
  174. ///
  175. /// There should be a proper API for this (r. 69157424).
  176. ///
  177. /// For more background on this, see [Triggering the Local Network Privacy Alert](https://developer.apple.com/forums/thread/663768).
  178. /// [via https://developer.apple.com/forums/thread/663768 ]
  179. func triggerLocalNetworkPrivacyAlert() {
  180. let sock4 = socket(AF_INET, SOCK_DGRAM, 0)
  181. guard sock4 >= 0 else { return }
  182. defer { close(sock4) }
  183. let sock6 = socket(AF_INET6, SOCK_DGRAM, 0)
  184. guard sock6 >= 0 else { return }
  185. defer { close(sock6) }
  186. let addresses = addressesOfDiscardServiceOnBroadcastCapableInterfaces()
  187. var message = [UInt8]("!".utf8)
  188. for address in addresses {
  189. address.withUnsafeBytes { buf in
  190. let sa = buf.baseAddress!.assumingMemoryBound(to: sockaddr.self)
  191. let saLen = socklen_t(buf.count)
  192. let sock = sa.pointee.sa_family == AF_INET ? sock4 : sock6
  193. _ = sendto(sock, &message, message.count, MSG_DONTWAIT, sa, saLen)
  194. }
  195. }
  196. }
  197. /// Returns the addresses of the discard service (port 9) on every
  198. /// broadcast-capable interface.
  199. ///
  200. /// Each array entry is contains either a `sockaddr_in` or `sockaddr_in6`.
  201. private func addressesOfDiscardServiceOnBroadcastCapableInterfaces() -> [Data] {
  202. var addrList: UnsafeMutablePointer<ifaddrs>?
  203. let err = getifaddrs(&addrList)
  204. guard err == 0, let start = addrList else { return [] }
  205. defer { freeifaddrs(start) }
  206. return sequence(first: start, next: { $0.pointee.ifa_next })
  207. .compactMap { i -> Data? in
  208. guard
  209. (i.pointee.ifa_flags & UInt32(bitPattern: IFF_BROADCAST)) != 0,
  210. let sa = i.pointee.ifa_addr
  211. else { return nil }
  212. var result = Data(UnsafeRawBufferPointer(start: sa, count: Int(sa.pointee.sa_len)))
  213. switch CInt(sa.pointee.sa_family) {
  214. case AF_INET:
  215. result.withUnsafeMutableBytes { buf in
  216. let sin = buf.baseAddress!.assumingMemoryBound(to: sockaddr_in.self)
  217. sin.pointee.sin_port = UInt16(9).bigEndian
  218. }
  219. case AF_INET6:
  220. result.withUnsafeMutableBytes { buf in
  221. let sin6 = buf.baseAddress!.assumingMemoryBound(to: sockaddr_in6.self)
  222. sin6.pointee.sin6_port = UInt16(9).bigEndian
  223. }
  224. default:
  225. return nil
  226. }
  227. return result
  228. }
  229. }