another.im-ios/Monal/Classes/MLQRCodeScanner.swift
2024-11-18 15:53:52 +01:00

285 lines
9.7 KiB
Swift

//
// MLQRCodeScanner.swift
// Monal
//
// Created by Friedrich Altheide on 20.11.20.
// Copyright © 2020 Monal.im. All rights reserved.
//
import SafariServices
@objc protocol MLLQRCodeScannerAccountLoginDelegate : AnyObject
{
func MLQRCodeAccountLoginScanned(jid: String, password: String)
func closeQRCodeScanner()
}
struct XMPPLoginQRCode : Codable
{
let usedProtocol:String
let address:String
let password:String
private enum CodingKeys: String, CodingKey
{
case usedProtocol = "protocol", address, password
}
}
@objc class MLQRCodeScannerController: UIViewController, AVCaptureMetadataOutputObjectsDelegate
{
@objc weak var loginDelegate : MLLQRCodeScannerAccountLoginDelegate?
var videoPreviewLayer: AVCaptureVideoPreviewLayer!;
var captureSession: AVCaptureSession!;
override func viewDidLoad()
{
super.viewDidLoad()
self.title = NSLocalizedString("QR-Code Scanner", comment: "")
view.backgroundColor = UIColor.black
switch AVCaptureDevice.authorizationStatus(for: .video)
{
case .authorized:
self.setupCaptureSession()
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { granted in
if granted {
DispatchQueue.main.async {
self.setupCaptureSession()
}
}
}
case .denied:
return
case .restricted:
return
@unknown default:
return;
}
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
if (captureSession?.isRunning == false)
{
captureSession.startRunning()
}
}
override func viewWillDisappear(_ animated: Bool)
{
if (captureSession?.isRunning == true)
{
captureSession.stopRunning()
}
super.viewWillDisappear(animated)
}
func setupCaptureSession()
{
// init capture session
captureSession = AVCaptureSession()
guard let captureDevice = AVCaptureDevice.default(for: .video)
else
{
errorMsg(title: NSLocalizedString("QR-Code video error", comment: "QR-Code-Scanner"), msg: NSLocalizedString("Could not get default capture device", comment: "QR-Code-Scanner"))
return;
}
let videoInput: AVCaptureDeviceInput
do
{
videoInput = try AVCaptureDeviceInput(device: captureDevice)
} catch
{
errorMsg(title: NSLocalizedString("QR-Code video error", comment: "QR-Code-Scanner"), msg: NSLocalizedString("Could not init video session", comment: "QR-Code-Scanner"))
return
}
if(captureSession.canAddInput(videoInput))
{
captureSession.addInput(videoInput)
}
else
{
errorMsgNoCameraFound()
return;
}
let metadataOutput = AVCaptureMetadataOutput()
if (captureSession.canAddOutput(metadataOutput)) {
captureSession.addOutput(metadataOutput)
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
metadataOutput.metadataObjectTypes = [.qr]
} else {
errorMsgNoCameraFound()
return
}
videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
videoPreviewLayer.frame = view.layer.bounds
videoPreviewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(videoPreviewLayer)
captureSession.startRunning()
}
func errorMsgNoCameraFound()
{
captureSession = nil
errorMsg(title: NSLocalizedString("Could not access camera", comment: "QR-Code-Scanner: camera not found"), msg: NSLocalizedString("It does not seem as your device has a camera. Please use a device with a camera for scanning", comment: "QR-Code-Scanner: Camera not found"))
}
override var prefersStatusBarHidden: Bool
{
return false
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask
{
return .portrait
}
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
captureSession.stopRunning()
if let metadataObject = metadataObjects.first {
guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
guard let qrCodeAsString = readableObject.stringValue else {
return handleQRCodeError()
}
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
//open https?:// urls in safari view controller just as they would if the qrcode was scanned using the camera app
if qrCodeAsString.hasPrefix("https://") || qrCodeAsString.hasPrefix("http://") {
if let url = URL(string:qrCodeAsString) {
let vc = SFSafariViewController(url:url, configuration:SFSafariViewController.Configuration())
present(vc, animated: true)
}
//let our app delegate handle all xmpp: urls
} else if qrCodeAsString.hasPrefix("xmpp:") {
guard let url = URL(string:qrCodeAsString) else {
return handleQRCodeError()
}
return (UIApplication.shared.delegate as! MonalAppDelegate).handleXMPPURL(url)
//if none of the above: handle json provisioning qrcodes, see: https://github.com/iNPUTmice/Conversations/issues/3796
} else {
// check if we have a json object
guard let qrCodeData = qrCodeAsString.data(using:.utf8) else {
return handleQRCodeError()
}
let jsonDecoder = JSONDecoder()
do {
let loginData = try jsonDecoder.decode(XMPPLoginQRCode.self, from:qrCodeData)
handleAccountLogin(loginData:loginData)
} catch {
handleQRCodeError()
}
return
}
}
}
func errorMsg(title: String, msg: String, startCaptureOnClose: Bool = false)
{
let ac = UIAlertController(title: title, message: msg, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: NSLocalizedString("Close", comment: ""), style: .default)
{
action -> Void in
// start capture again after invalid qr code
if(startCaptureOnClose == true)
{
self.captureSession.startRunning()
}
else if (self.loginDelegate != nil) {
self.loginDelegate?.closeQRCodeScanner()
}
}
)
DispatchQueue.main.async{
self.present(ac, animated: true)
}
}
func handleAccountLogin(loginData: XMPPLoginQRCode)
{
if(loginData.usedProtocol == "xmpp")
{
if(self.loginDelegate != nil)
{
self.navigationController?.popViewController(animated: true)
self.loginDelegate?.MLQRCodeAccountLoginScanned(jid: loginData.address, password: loginData.password)
}
else
{
errorMsg(title: NSLocalizedString("Wrong menu", comment: "QR-Code-Scanner: account scan wrong menu"), msg: NSLocalizedString("The qrcode contains login credentials for an acount. Go to settings -> new account and rescan the qrcode", comment: "QR-Code-Scanner: account scan wrong menu"), startCaptureOnClose: true)
}
}
}
func handleQRCodeError()
{
errorMsg(title: NSLocalizedString("Invalid format", comment: "QR-Code-Scanner: invalid format"), msg: NSLocalizedString("We could not find a xmpp related QR-Code", comment: "QR-Code-Scanner: invalid format"), startCaptureOnClose: true)
}
}
struct MLQRCodeScanner : UIViewControllerRepresentable {
let handleLogin: ((String, String) -> Void)?
let handleClose: (() -> Void)
class Coordinator: NSObject, MLLQRCodeScannerAccountLoginDelegate {
let handleLogin: ((String, String) -> Void)?
let handleClose: (() -> Void)
func MLQRCodeAccountLoginScanned(jid: String, password: String) {
if(self.handleLogin != nil) {
self.handleLogin!(jid, password)
}
}
func closeQRCodeScanner() {
self.handleClose()
}
init(handleLogin: ((String, String) -> Void)?, handleClose: @escaping () -> Void) {
self.handleLogin = handleLogin
self.handleClose = handleClose
}
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MLQRCodeScanner>) -> MLQRCodeScannerController {
let qrCodeScannerViewController = MLQRCodeScannerController()
if(self.handleLogin != nil) {
qrCodeScannerViewController.loginDelegate = context.coordinator
}
return qrCodeScannerViewController
}
func updateUIViewController(_ uiViewController: MLQRCodeScannerController, context: UIViewControllerRepresentableContext<MLQRCodeScanner>) {
}
func makeCoordinator() -> MLQRCodeScanner.Coordinator {
Coordinator(handleLogin: self.handleLogin, handleClose: self.handleClose);
}
init(handleClose: @escaping () -> Void) {
self.handleLogin = nil
self.handleClose = handleClose
}
init(handleLogin: @escaping (String, String) -> Void, handleClose: @escaping () -> Void) {
self.handleLogin = handleLogin
self.handleClose = handleClose
}
}