Browse Source

add BackupProviderViewController

B. Petersen 2 years ago
parent
commit
4f4d102c3e

+ 30 - 0
DcCore/DcCore/DC/Wrapper.swift

@@ -1414,6 +1414,36 @@ public class DcLot {
     }
 }
 
+public class DcBackupProvider {
+    private var dcBackupProviderPointer: OpaquePointer
+
+    public init(_ dcContext: DcContext) {
+        dcBackupProviderPointer = dc_backup_provider_new(dcContext.contextPointer)
+    }
+
+    deinit {
+        dc_backup_provider_unref(dcBackupProviderPointer)
+    }
+
+    public func getQr() -> String? {
+        guard let cString = dc_backup_provider_get_qr(dcBackupProviderPointer) else { return nil }
+        let swiftString = String(cString: cString)
+        dc_str_unref(cString)
+        return swiftString
+    }
+
+    public func getQrSvg() -> String? {
+        guard let cString = dc_backup_provider_get_qr_svg(dcBackupProviderPointer) else { return nil }
+        let swiftString = String(cString: cString)
+        dc_str_unref(cString)
+        return swiftString
+    }
+
+    public func wait() {
+        dc_backup_provider_wait(dcBackupProviderPointer)
+    }
+ }
+
 public class DcProvider {
     private var dcProviderPointer: OpaquePointer?
 

+ 4 - 0
deltachat-ios.xcodeproj/project.pbxproj

@@ -204,6 +204,7 @@
 		B20462E62440C99600367A57 /* AutodelOptionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B20462E52440C99600367A57 /* AutodelOptionsViewController.swift */; };
 		B21005DB23383664004C70C5 /* EmailOptionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21005DA23383664004C70C5 /* EmailOptionsViewController.swift */; };
 		B2172F3C29C125F2002C289E /* AdvancedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2172F3B29C125F2002C289E /* AdvancedViewController.swift */; };
+		B259D64329B771D5008FB706 /* BackupTransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B259D64229B771D5008FB706 /* BackupTransferViewController.swift */; };
 		B26B3BC7236DC3DC008ED35A /* SwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B26B3BC6236DC3DC008ED35A /* SwitchCell.swift */; };
 		B2C42570265C325C00B95377 /* MultilineLabelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C4256F265C325C00B95377 /* MultilineLabelCell.swift */; };
 		B2D4B63B29C38D1900B47DA8 /* ChatsAndMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2D4B63A29C38D1900B47DA8 /* ChatsAndMediaViewController.swift */; };
@@ -514,6 +515,7 @@
 		B2591B1F24106BA400C38152 /* eo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eo; path = eo.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		B2591B2024106BA400C38152 /* eo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eo; path = eo.lproj/Localizable.strings; sourceTree = "<group>"; };
 		B2591B2124106BA400C38152 /* eo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = eo; path = eo.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
+		B259D64229B771D5008FB706 /* BackupTransferViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupTransferViewController.swift; sourceTree = "<group>"; };
 		B25FD2642952387200E79E00 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		B25FD2652952387200E79E00 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = "<group>"; };
 		B25FD2662952387200E79E00 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = el; path = el.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -944,6 +946,7 @@
 			children = (
 				B28D25882913CE8600B9067F /* Settings */,
 				AE18F28B228C17630007B1BE /* AccountSetup */,
+				B259D64229B771D5008FB706 /* BackupTransferViewController.swift */,
 				30DDCBEA28FCA21800465D22 /* AccountSwitchViewController.swift */,
 				30DDCBE828FCA1F900465D22 /* PartialScreenPresentationController.swift */,
 				3015634323A003BA00E9DEF4 /* AudioRecorderController.swift */,
@@ -1501,6 +1504,7 @@
 				303492952565AABC00A523D0 /* DraftModel.swift in Sources */,
 				78E45E3A21D3CFBC00D4B15E /* SettingsViewController.swift in Sources */,
 				3080A021277DE09900E74565 /* InputStackView.swift in Sources */,
+				B259D64329B771D5008FB706 /* BackupTransferViewController.swift in Sources */,
 				AE8519EA2272FDCA00ED86F0 /* DeviceContactsHandler.swift in Sources */,
 				302E592426A5CF4800DD4F58 /* ConnectivityViewController.swift in Sources */,
 				78ED838321D5379000243125 /* TextFieldCell.swift in Sources */,

+ 204 - 0
deltachat-ios/Controller/BackupTransferViewController.swift

@@ -0,0 +1,204 @@
+import Foundation
+import UIKit
+import DcCore
+import SDWebImageSVGKitPlugin
+
+class BackupTransferViewController: UIViewController {
+
+    private let dcContext: DcContext
+    private let dcAccounts: DcAccounts
+    private var dcBackupProvider: DcBackupProvider?
+
+    private lazy var qrContentView: UIImageView = {
+        let view = UIImageView()
+        view.contentMode = .scaleAspectFit
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.accessibilityHint = String.localized("scan_to_transfer")
+        return view
+    }()
+
+    private lazy var progressContainer: UIView = {
+        let view = UIView()
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.layer.cornerRadius = 20
+        view.clipsToBounds = true
+        view.layer.borderColor = DcColors.grey50.cgColor
+        view.layer.borderWidth = 1
+        view.backgroundColor = DcColors.defaultInverseColor.withAlphaComponent(0.5)
+        return view
+    }()
+
+    private lazy var progress: UIActivityIndicatorView = {
+        let progress = UIActivityIndicatorView(style: .white)
+        progress.translatesAutoresizingMaskIntoConstraints = false
+        return progress
+    }()
+
+    private lazy var blurView: UIVisualEffectView = {
+        let blurEffect = UIBlurEffect(style: .light)
+        let view = UIVisualEffectView(effect: blurEffect)
+        view.translatesAutoresizingMaskIntoConstraints = false
+        return view
+    }()
+
+    private var progressObserver: NSObjectProtocol?
+
+    init(dcAccounts: DcAccounts) {
+        self.dcAccounts = dcAccounts
+        self.dcContext = dcAccounts.getSelected()
+        super.init(nibName: nil, bundle: nil)
+        hidesBottomBarWhenPushed = true
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    // MARK: - lifecycle
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        title = String.localized("add_another_device")
+        setupSubviews()
+        // TODO: add some more hints about what is going on
+
+        DispatchQueue.global(qos: .userInitiated).async { [weak self] in
+            guard let self = self else { return }
+            self.dcAccounts.stopIo()
+            self.dcBackupProvider = DcBackupProvider(self.dcContext)
+            DispatchQueue.main.async {
+                let image = self.getQrImage(svg: self.dcBackupProvider?.getQrSvg())
+                self.qrContentView.image = image
+                self.progress.stopAnimating()
+                self.progressContainer.isHidden = true
+                DispatchQueue.global(qos: .userInitiated).async { [weak self] in
+                    guard let self = self else { return }
+                    self.dcBackupProvider?.wait()
+                    // TODO: track events and show transfer progress
+                    // TODO: once the QR code is scanned, it can disappear from the screen
+                }
+            }
+        }
+    }
+
+    override func viewDidDisappear(_ animated: Bool) {
+        // TODO: this is too harsh, aborting should be done only when the user actively quits the viewController
+        if dcBackupProvider != nil {
+            dcContext.stopOngoingProcess()
+            dcBackupProvider = nil
+        }
+        dcAccounts.startIo()
+    }
+
+    // MARK: - setup
+    private func setupSubviews() {
+        view.addSubview(qrContentView)
+        view.addSubview(progressContainer)
+        progressContainer.addSubview(blurView)
+        progressContainer.addSubview(progress)
+        let qrDefaultWidth = qrContentView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor, multiplier: 0.75)
+        qrDefaultWidth.priority = UILayoutPriority(500)
+        qrDefaultWidth.isActive = true
+        let qrMinWidth = qrContentView.widthAnchor.constraint(lessThanOrEqualToConstant: 260)
+        qrMinWidth.priority = UILayoutPriority(999)
+        qrMinWidth.isActive = true
+        view.addConstraints([
+            qrContentView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 1.05),
+            qrContentView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
+            qrContentView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
+            progressContainer.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
+            progressContainer.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
+            progressContainer.constraintHeightTo(100),
+            progressContainer.constraintWidthTo(100),
+            progress.constraintCenterXTo(progressContainer),
+            progress.constraintCenterYTo(progressContainer)
+        ])
+        blurView.fillSuperview()
+        progressContainer.isHidden = false
+        progress.startAnimating()
+        view.backgroundColor = DcColors.defaultBackgroundColor
+    }
+    
+    private func getQrImage(svg: String?) -> UIImage? {
+        if let svg = svg {
+            let svgData = svg.data(using: .utf8)
+            return SDImageSVGKCoder.shared.decodedImage(with: svgData, options: [:])
+        }
+        return nil
+    }
+
+}
+
+
+/// Does a best effort attempt to trigger the local network privacy alert.
+///
+/// It works by sending a UDP datagram to the discard service (port 9) of every
+/// IP address associated with a broadcast-capable interface. This should
+/// trigger the local network privacy alert, assuming the alert hasn’t already
+/// been displayed for this app.
+///
+/// This code takes a ‘best effort’. It handles errors by ignoring them. As
+/// such, there’s guarantee that it’ll actually trigger the alert.
+///
+/// - note: iOS devices don’t actually run the discard service. I’m using it
+/// here because I need a port to send the UDP datagram to and port 9 is
+/// always going to be safe (either the discard service is running, in which
+/// case it will discard the datagram, or it’s not, in which case the TCP/IP
+/// stack will discard it).
+///
+/// There should be a proper API for this (r. 69157424).
+///
+/// For more background on this, see [Triggering the Local Network Privacy Alert](https://developer.apple.com/forums/thread/663768).
+/// [via https://developer.apple.com/forums/thread/663768 ]
+func triggerLocalNetworkPrivacyAlert() {
+    let sock4 = socket(AF_INET, SOCK_DGRAM, 0)
+    guard sock4 >= 0 else { return }
+    defer { close(sock4) }
+    let sock6 = socket(AF_INET6, SOCK_DGRAM, 0)
+    guard sock6 >= 0 else { return }
+    defer { close(sock6) }
+
+    let addresses = addressesOfDiscardServiceOnBroadcastCapableInterfaces()
+    var message = [UInt8]("!".utf8)
+    for address in addresses {
+        address.withUnsafeBytes { buf in
+            let sa = buf.baseAddress!.assumingMemoryBound(to: sockaddr.self)
+            let saLen = socklen_t(buf.count)
+            let sock = sa.pointee.sa_family == AF_INET ? sock4 : sock6
+            _ = sendto(sock, &message, message.count, MSG_DONTWAIT, sa, saLen)
+        }
+    }
+}
+
+/// Returns the addresses of the discard service (port 9) on every
+/// broadcast-capable interface.
+///
+/// Each array entry is contains either a `sockaddr_in` or `sockaddr_in6`.
+private func addressesOfDiscardServiceOnBroadcastCapableInterfaces() -> [Data] {
+    var addrList: UnsafeMutablePointer<ifaddrs>?
+    let err = getifaddrs(&addrList)
+    guard err == 0, let start = addrList else { return [] }
+    defer { freeifaddrs(start) }
+    return sequence(first: start, next: { $0.pointee.ifa_next })
+        .compactMap { i -> Data? in
+            guard
+                (i.pointee.ifa_flags & UInt32(bitPattern: IFF_BROADCAST)) != 0,
+                let sa = i.pointee.ifa_addr
+            else { return nil }
+            var result = Data(UnsafeRawBufferPointer(start: sa, count: Int(sa.pointee.sa_len)))
+            switch CInt(sa.pointee.sa_family) {
+            case AF_INET:
+                result.withUnsafeMutableBytes { buf in
+                    let sin = buf.baseAddress!.assumingMemoryBound(to: sockaddr_in.self)
+                    sin.pointee.sin_port = UInt16(9).bigEndian
+                }
+            case AF_INET6:
+                result.withUnsafeMutableBytes { buf in
+                    let sin6 = buf.baseAddress!.assumingMemoryBound(to: sockaddr_in6.self)
+                    sin6.pointee.sin6_port = UInt16(9).bigEndian
+                }
+            default:
+                return nil
+            }
+            return result
+        }
+}

+ 19 - 1
deltachat-ios/Controller/Settings/SettingsViewController.swift

@@ -13,6 +13,7 @@ internal final class SettingsViewController: UITableViewController {
     private enum CellTags: Int {
         case profile
         case chatsAndMedia
+        case addAnotherDevice
         case notifications
         case selectBackground
         case advanced
@@ -65,6 +66,17 @@ internal final class SettingsViewController: UITableViewController {
         return cell
     }()
 
+    private lazy var addAnotherDeviceCell: UITableViewCell = {
+        let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
+        cell.tag = CellTags.addAnotherDevice.rawValue
+        cell.textLabel?.text = String.localized("add_another_device")
+        if #available(iOS 16.0, *) {
+            cell.imageView?.image = UIImage(systemName: "macbook.and.iphone") // added in ios16
+        }
+        cell.accessoryType = .disclosureIndicator
+        return cell
+    }()
+
     private lazy var advancedCell: UITableViewCell = {
         let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
         cell.tag = CellTags.advanced.rawValue
@@ -122,7 +134,7 @@ internal final class SettingsViewController: UITableViewController {
         let preferencesSection = SectionConfigs(
             headerTitle: nil,
             footerTitle: nil,
-            cells: [chatsAndMediaCell, notificationCell, selectBackgroundCell, connectivityCell, advancedCell]
+            cells: [chatsAndMediaCell, notificationCell, selectBackgroundCell, addAnotherDeviceCell, connectivityCell, advancedCell]
         )
         let helpSection = SectionConfigs(
             headerTitle: nil,
@@ -207,6 +219,7 @@ internal final class SettingsViewController: UITableViewController {
         switch cellTag {
         case .profile: showEditSettingsController()
         case .chatsAndMedia: showChatsAndMedia()
+        case .addAnotherDevice: showBackupProviderViewController()
         case .notifications: break
         case .advanced: showAdvanced()
         case .help: showHelp()
@@ -254,6 +267,11 @@ internal final class SettingsViewController: UITableViewController {
         navigationController?.pushViewController(ChatsAndMediaViewController(dcAccounts: dcAccounts), animated: true)
     }
 
+    private func showBackupProviderViewController() {
+         let backupProviderViewController = BackupTransferViewController(dcAccounts: dcAccounts)
+         navigationController?.pushViewController(backupProviderViewController, animated: true)
+    }
+
     private func showAdvanced() {
         navigationController?.pushViewController(AdvancedViewController(dcAccounts: dcAccounts), animated: true)
     }

+ 2 - 0
deltachat-ios/en.lproj/Localizable.strings

@@ -992,3 +992,5 @@
 "add_encrypted_account" = "Add encrypted account";
 "backup_successful" = "Backup successful";
 "backup_successful_explain_ios" = "You can find the backup in the \"Delta Chat\" folder using the \"Files\" app.\n\nMove the backup out of this folder to keep it when deleting Delta Chat.";
+"add_another_device" = "Add Another Device";
+"add_another_device_explain" = "Add another device for this account";

+ 2 - 0
scripts/untranslated.xml

@@ -7,4 +7,6 @@
     <string name="add_encrypted_account">Add encrypted account</string>
     <string name="backup_successful">Backup successful</string>
     <string name="backup_successful_explain_ios">You can find the backup in the \"Delta Chat\" folder using the \"Files\" app.\n\nMove the backup out of this folder to keep it when deleting Delta Chat.</string>
+    <string name="add_another_device">Add Another Device</string>
+    <string name="add_another_device_explain">Add another device for this account</string>
 </resources>