TM-SGNL-iOS/Signal/DeviceTransfer/OutgoingDeviceTransferQRScanningViewController.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

275 lines
11 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import MultipeerConnectivity
import SignalServiceKit
import SignalUI
class OutgoingDeviceTransferQRScanningViewController: DeviceTransferBaseViewController {
private let qrCodeScanViewController = QRCodeScanViewController(appearance: .unadorned)
lazy var activityIndicator: UIActivityIndicatorView = {
let activityIndicator = UIActivityIndicatorView(style: .medium)
activityIndicator.color = Theme.primaryIconColor
return activityIndicator
}()
lazy var label: UILabel = {
let label = UILabel()
label.font = .dynamicTypeBody2
label.textColor = Theme.primaryTextColor
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.textAlignment = .center
return label
}()
lazy var hStack: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 6
stackView.setContentHuggingVerticalLow()
stackView.setCompressionResistanceVerticalHigh()
let leadingSpacer = UIView.hStretchingSpacer()
stackView.addArrangedSubview(leadingSpacer)
stackView.addArrangedSubview(activityIndicator)
stackView.addArrangedSubview(label)
label.setCompressionResistanceHorizontalHigh()
label.setCompressionResistanceVerticalHigh()
let trailingSpacer = UIView.hStretchingSpacer()
stackView.addArrangedSubview(trailingSpacer)
leadingSpacer.autoMatch(.width, to: .width, of: trailingSpacer)
return stackView
}()
lazy var captureContainerView: UIView = {
let view = UIView()
let qrView = qrCodeScanViewController.view!
view.addSubview(qrView)
view.addSubview(maskingView)
maskingView.autoPinHeightToSuperview()
maskingView.autoHCenterInSuperview()
qrView.autoPinEdges(toEdgesOf: maskingView)
view.addSubview(cameraToggleButton)
cameraToggleButton.autoSetDimensions(to: .square(32))
cameraToggleButton.autoPinEdge(.trailing, to: .trailing, of: maskingView)
cameraToggleButton.autoPinEdge(.bottom, to: .bottom, of: view)
return view
}()
lazy var maskingView: UIView = {
let maskingView = BezierPathView { layer, bounds in
let path = UIBezierPath(rect: bounds)
let circlePath = UIBezierPath(roundedRect: bounds, cornerRadius: bounds.size.height * 0.5)
path.append(circlePath)
path.usesEvenOddFillRule = true
layer.path = path.cgPath
layer.fillRule = .evenOdd
layer.fillColor = Theme.actionSheetBackgroundColor.cgColor
}
maskingView.autoPinToSquareAspectRatio()
maskingView.autoSetDimension(.height, toSize: 256)
return maskingView
}()
private lazy var cameraToggleButton: UIButton = {
let button = UIButton()
button.setImage(UIImage(named: "switch-camera-28"), animated: false)
button.tintColor = Theme.primaryIconColor
button.addTarget(
self,
action: #selector(didTapCameraToggle),
for: .touchUpInside
)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
let titleLabel = self.titleLabel(
text: OWSLocalizedString("DEVICE_TRANSFER_SCANNING_TITLE",
comment: "The title for the action sheet asking the user to scan the QR code to transfer")
)
contentView.addArrangedSubview(titleLabel)
contentView.addArrangedSubview(.spacer(withHeight: 12))
let explanationLabel = self.explanationLabel(
explanationText: OWSLocalizedString("DEVICE_TRANSFER_SCANNING_EXPLANATION",
comment: "The explanation for the action sheet asking the user to scan the QR code to transfer")
)
contentView.addArrangedSubview(explanationLabel)
let topSpacer = UIView.vStretchingSpacer()
contentView.addArrangedSubview(topSpacer)
contentView.addArrangedSubview(captureContainerView)
contentView.addArrangedSubview(hStack)
hStack.isHidden = true
let bottomSpacer = UIView.vStretchingSpacer()
contentView.addArrangedSubview(bottomSpacer)
topSpacer.autoMatch(.height, to: .height, of: bottomSpacer)
topSpacer.autoSetDimension(.height, toSize: 25, relation: .greaterThanOrEqual)
qrCodeScanViewController.delegate = self
addChild(qrCodeScanViewController)
}
@objc
private func didTapCameraToggle() {
qrCodeScanViewController.prefersFrontFacingCamera =
!qrCodeScanViewController.prefersFrontFacingCamera
}
}
// MARK: -
extension OutgoingDeviceTransferQRScanningViewController: QRCodeScanDelegate {
func qrCodeScanViewDismiss(_ qrCodeScanViewController: QRCodeScanViewController) {
AssertIsOnMainThread()
navigationController?.popViewController(animated: true)
}
func qrCodeScanViewScanned(
qrCodeData: Data?,
qrCodeString: String?
) -> QRCodeScanOutcome {
AssertIsOnMainThread()
guard let qrCodeString = qrCodeString else {
// TODO: The user has probably scanned the wrong QR code.
// We could show an error alert to help them resolve the issue.
// For now, ignore the QR code and continue scanning.
owsFailDebug("QR code does not have a valid string payload.")
return .continueScanning
}
guard let scannedURL = URL(string: qrCodeString) else {
// TODO: The user has probably scanned the wrong QR code.
// We could show an error alert to help them resolve the issue.
// For now, ignore the QR code and continue scanning.
owsFailDebug("QR code does not have a valid URL payload: \(qrCodeString).")
return .continueScanning
}
// Ignore if a non-signal url was scanned
guard scannedURL.scheme == UrlOpener.Constants.sgnlPrefix else {
// TODO: The user has probably scanned the wrong QR code.
// We could show an error alert to help them resolve the issue.
// For now, ignore the QR code and continue scanning.
owsFailDebug("QR code does not have a transfer URL payload: \(qrCodeString).")
return .continueScanning
}
showConnecting()
DispatchQueue.global().async {
do {
let (peerId, certificateHash) = try AppEnvironment.shared.deviceTransferServiceRef.parseTransferURL(scannedURL)
AppEnvironment.shared.deviceTransferServiceRef.addObserver(self)
try AppEnvironment.shared.deviceTransferServiceRef.transferAccountToNewDevice(with: peerId, certificateHash: certificateHash)
} catch {
owsFailDebug("Something went wrong \(error)")
if let error = error as? DeviceTransferService.Error {
switch error {
case .unsupportedVersion:
self.showError(
text: OWSLocalizedString("DEVICE_TRANSFER_ERROR_UNSUPPORTED_VERSION",
comment: "An error indicating the user must update their device before trying to transfer.")
)
return
case .modeMismatch:
let desiredMode: DeviceTransferService.TransferMode =
DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isPrimaryDevice ?? true
? .linked : .primary
switch desiredMode {
case .linked:
self.showError(
text: OWSLocalizedString("DEVICE_TRANSFER_ERROR_MODE_MISMATCH_LINKED",
comment: "An error indicating the user must scan this code with a linked device to transfer.")
)
case .primary:
self.showError(
text: OWSLocalizedString("DEVICE_TRANSFER_ERROR_MODE_MISMATCH_PRIMARY",
comment: "An error indicating the user must scan this code with a primary device to transfer.")
)
}
return
default:
break
}
}
self.showError(
text: OWSLocalizedString("DEVICE_TRANSFER_ERROR_GENERIC",
comment: "An error indicating that something went wrong with the transfer and it could not complete")
)
}
}
return .stopScanning
}
func showConnecting() {
captureContainerView.isHidden = true
hStack.isHidden = false
label.text = OWSLocalizedString("DEVICE_TRANSFER_SCANNING_CONNECTING",
comment: "Text indicating that we are connecting to the scanned device")
label.textColor = Theme.primaryTextColor
activityIndicator.isHidden = false
activityIndicator.startAnimating()
}
func showError(text: String) {
DispatchMainThreadSafe {
self.captureContainerView.isHidden = true
self.hStack.isHidden = false
self.label.text = text
self.label.textColor = .ows_accentRed
self.activityIndicator.stopAnimating()
self.activityIndicator.isHidden = true
}
}
}
// MARK: -
extension OutgoingDeviceTransferQRScanningViewController: DeviceTransferServiceObserver {
func deviceTransferServiceDiscoveredNewDevice(peerId: MCPeerID, discoveryInfo: [String: String]?) {}
func deviceTransferServiceDidStartTransfer(progress: Progress) {
AppEnvironment.shared.deviceTransferServiceRef.removeObserver(self)
let vc = OutgoingDeviceTransferProgressViewController(progress: progress)
navigationController?.pushViewController(vc, animated: true)
}
func deviceTransferServiceDidEndTransfer(error: DeviceTransferService.Error?) {
if error != nil {
showError(
text: OWSLocalizedString("DEVICE_TRANSFER_ERROR_GENERIC",
comment: "An error indicating that something went wrong with the transfer and it could not complete")
)
}
}
func deviceTransferServiceDidRequestAppRelaunch() {
owsFail("Relaunch not supported for outgoing transfer; only on the receiving device during transfer")
}
}