Replaced the 'ZXingObjC' dependency with a native implementation for scanning QR Codes

pull/751/head
Morgan Pretty 2 years ago
parent f623db678e
commit c7f6b5a94e

@ -22,7 +22,6 @@ abstract_target 'GlobalDependencies' do
pod 'PureLayout', '~> 3.1.8'
pod 'NVActivityIndicatorView'
pod 'YYImage/libwebp', git: 'https://github.com/signalapp/YYImage'
pod 'ZXingObjC'
pod 'DifferenceKit'
target 'SessionTests' do

@ -108,9 +108,6 @@ PODS:
- YYImage/libwebp (1.0.4):
- libwebp
- YYImage/Core
- ZXingObjC (3.6.5):
- ZXingObjC/All (= 3.6.5)
- ZXingObjC/All (3.6.5)
DEPENDENCIES:
- Curve25519Kit (from `https://github.com/oxen-io/session-ios-curve-25519-kit.git`, branch `session-version`)
@ -129,7 +126,6 @@ DEPENDENCIES:
- WebRTC-lib
- YapDatabase/SQLCipher (from `https://github.com/oxen-io/session-ios-yap-database.git`, branch `signal-release`)
- YYImage/libwebp (from `https://github.com/signalapp/YYImage`)
- ZXingObjC
SPEC REPOS:
https://github.com/CocoaPods/Specs.git:
@ -147,7 +143,6 @@ SPEC REPOS:
- SQLCipher
- SwiftProtobuf
- WebRTC-lib
- ZXingObjC
EXTERNAL SOURCES:
Curve25519Kit:
@ -202,8 +197,7 @@ SPEC CHECKSUMS:
WebRTC-lib: d83df8976fa608b980f1d85796b3de66d60a1953
YapDatabase: b418a4baa6906e8028748938f9159807fd039af4
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 68799237a4dc046f5ac25c573af03b559f5b10c4
PODFILE CHECKSUM: 4705728e69454d50805c70272479a7d4a04209d5
COCOAPODS: 1.12.1

@ -165,12 +165,12 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle
dismiss(animated: true, completion: nil)
}
func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String) {
func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String, onError: (() -> ())?) {
let hexEncodedPublicKey = string
startNewDMIfPossible(with: hexEncodedPublicKey)
startNewDMIfPossible(with: hexEncodedPublicKey, onError: onError)
}
fileprivate func startNewDMIfPossible(with onsNameOrPublicKey: String) {
fileprivate func startNewDMIfPossible(with onsNameOrPublicKey: String, onError: (() -> ())?) {
let maybeSessionId: SessionId? = SessionId(from: onsNameOrPublicKey)
if KeyPair.isValidHexEncodedPublicKey(candidate: onsNameOrPublicKey) {
@ -185,7 +185,8 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle
title: "ALERT_ERROR_TITLE".localized(),
body: .text("DM_ERROR_DIRECT_BLINDED_ID".localized()),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
cancelStyle: .alert_text,
afterClosed: onError
)
)
self.present(modal, animated: true)
@ -197,7 +198,8 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle
title: "ALERT_ERROR_TITLE".localized(),
body: .text("DM_ERROR_INVALID".localized()),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
cancelStyle: .alert_text,
afterClosed: onError
)
)
self.present(modal, animated: true)
@ -243,7 +245,8 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle
title: "ALERT_ERROR_TITLE".localized(),
body: .text(message),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
cancelStyle: .alert_text,
afterClosed: onError
)
)
self?.present(modal, animated: true)
@ -663,7 +666,7 @@ private final class EnterPublicKeyVC: UIViewController {
@objc fileprivate func startNewDMIfPossible() {
let text = publicKeyTextView.text?.trimmingCharacters(in: .whitespaces) ?? ""
NewDMVC.startNewDMIfPossible(with: text)
NewDMVC.startNewDMIfPossible(with: text, onError: nil)
}
}

@ -134,12 +134,12 @@ final class LinkDeviceVC: BaseVC, UIPageViewControllerDataSource, UIPageViewCont
dismiss(animated: true, completion: nil)
}
func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String) {
func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String, onError: (() -> ())?) {
let seed = Data(hex: string)
continueWithSeed(seed)
continueWithSeed(seed, onError: onError)
}
func continueWithSeed(_ seed: Data) {
func continueWithSeed(_ seed: Data, onError: (() -> ())?) {
if (seed.count != 16) {
let modal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
@ -147,9 +147,7 @@ final class LinkDeviceVC: BaseVC, UIPageViewControllerDataSource, UIPageViewCont
body: .text("INVALID_RECOVERY_PHRASE_MESSAGE".localized()),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text,
afterClosed: { [weak self] in
self?.scanQRCodeWrapperVC.startCapture()
}
afterClosed: onError
)
)
present(modal, animated: true)
@ -319,7 +317,7 @@ private final class RecoveryPhraseVC: UIViewController {
let hexEncodedSeed = try Mnemonic.decode(mnemonic: mnemonic)
let seed = Data(hex: hexEncodedSeed)
mnemonicTextView.resignFirstResponder()
linkDeviceVC.continueWithSeed(seed)
linkDeviceVC.continueWithSeed(seed, onError: nil)
} catch let error {
let error = error as? Mnemonic.DecodingError ?? Mnemonic.DecodingError.generic
showError(title: error.errorDescription!)

@ -144,25 +144,26 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC
dismiss(animated: true, completion: nil)
}
func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String) {
joinOpenGroup(with: string)
func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String, onError: (() -> ())?) {
joinOpenGroup(with: string, onError: onError)
}
fileprivate func joinOpenGroup(with urlString: String) {
fileprivate func joinOpenGroup(with urlString: String, onError: (() -> ())?) {
// A V2 open group URL will look like: <optional scheme> + <host> + <optional port> + <room> + <public key>
// The host doesn't parse if no explicit scheme is provided
guard let (room, server, publicKey) = SessionUtil.parseCommunity(url: urlString) else {
showError(
title: "invalid_url".localized(),
message: "COMMUNITY_ERROR_INVALID_URL".localized()
message: "COMMUNITY_ERROR_INVALID_URL".localized(),
onError: onError
)
return
}
joinOpenGroup(roomToken: room, server: server, publicKey: publicKey, shouldOpenCommunity: true)
joinOpenGroup(roomToken: room, server: server, publicKey: publicKey, shouldOpenCommunity: true, onError: onError)
}
fileprivate func joinOpenGroup(roomToken: String, server: String, publicKey: String, shouldOpenCommunity: Bool) {
fileprivate func joinOpenGroup(roomToken: String, server: String, publicKey: String, shouldOpenCommunity: Bool, onError: (() -> ())?) {
guard !isJoining, let navigationController: UINavigationController = navigationController else { return }
isJoining = true
@ -209,7 +210,8 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC
self?.dismiss(animated: true) { // Dismiss the loader
self?.showError(
title: "COMMUNITY_ERROR_GENERIC".localized(),
message: error.localizedDescription
message: error.localizedDescription,
onError: onError
)
}
@ -232,13 +234,14 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC
// MARK: - Convenience
private func showError(title: String, message: String = "") {
private func showError(title: String, message: String = "", onError: (() -> ())?) {
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: title,
body: .text(message),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
cancelStyle: .alert_text,
afterClosed: onError
)
)
self.navigationController?.present(confirmationModal, animated: true, completion: nil)
@ -399,13 +402,14 @@ private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, O
roomToken: room.token,
server: OpenGroupAPI.defaultServer,
publicKey: OpenGroupAPI.defaultServerPublicKey,
shouldOpenCommunity: true
shouldOpenCommunity: true,
onError: nil
)
}
@objc private func joinOpenGroup() {
let url = urlTextView.text?.trimmingCharacters(in: .whitespaces) ?? ""
joinOpenGroupVC?.joinOpenGroup(with: url)
joinOpenGroupVC?.joinOpenGroup(with: url, onError: nil)
}
// MARK: - Updating

@ -119,12 +119,12 @@ final class QRCodeVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControl
dismiss(animated: true, completion: nil)
}
func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String) {
func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String, onError: (() -> ())?) {
let hexEncodedPublicKey = string
startNewPrivateChatIfPossible(with: hexEncodedPublicKey)
startNewPrivateChatIfPossible(with: hexEncodedPublicKey, onError: onError)
}
fileprivate func startNewPrivateChatIfPossible(with hexEncodedPublicKey: String) {
fileprivate func startNewPrivateChatIfPossible(with hexEncodedPublicKey: String, onError: (() -> ())?) {
if !KeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) {
let modal: ConfirmationModal = ConfirmationModal(
targetView: self.view,
@ -132,7 +132,8 @@ final class QRCodeVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControl
title: "invalid_session_id".localized(),
body: .text("INVALID_SESSION_ID_MESSAGE".localized()),
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text
cancelStyle: .alert_text,
afterClosed: onError
)
)
self.present(modal, animated: true)

@ -2,55 +2,36 @@
import UIKit
import AVFoundation
import ZXingObjC
import SessionUIKit
import SessionUtilitiesKit
protocol QRScannerDelegate: AnyObject {
func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String)
func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String, onError: (() -> ())?)
}
class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate, ZXCaptureDelegate {
class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
public weak var scanDelegate: QRScannerDelegate?
private let captureQueue: DispatchQueue = DispatchQueue.global(qos: .default)
private var capture: ZXCapture?
private var capture: AVCaptureSession?
private var captureLayer: AVCaptureVideoPreviewLayer?
private var captureEnabled: Bool = false
// MARK: - Initialization
deinit {
self.capture?.layer.removeFromSuperlayer()
self.captureLayer?.removeFromSuperlayer()
}
// MARK: - Components
private let maskingView: UIView = {
let result: OWSBezierPathView = OWSBezierPathView()
result.configureShapeLayerBlock = { layer, bounds in
// Add a circular mask
let path: UIBezierPath = UIBezierPath(rect: bounds)
let margin: CGFloat = ScaleFromIPhone5To7Plus(24, 48)
let radius: CGFloat = ((min(bounds.size.width, bounds.size.height) * 0.5) - margin)
// Center the circle's bounding rectangle
let circleRect: CGRect = CGRect(
x: ((bounds.size.width * 0.5) - radius),
y: ((bounds.size.height * 0.5) - radius),
width: (radius * 2),
height: (radius * 2)
)
let circlePath: UIBezierPath = UIBezierPath.init(
roundedRect: circleRect,
cornerRadius: 16
)
path.append(circlePath)
path.usesEvenOddFillRule = true
layer.path = path.cgPath
layer.fillRule = .evenOdd
layer.themeFillColor = .black
layer.opacity = 0.32
}
private let maskingView: UIView = UIView()
private lazy var maskLayer: CAShapeLayer = {
let result: CAShapeLayer = CAShapeLayer()
result.fillRule = .evenOdd
result.themeFillColor = .black
result.opacity = 0.32
return result
}()
@ -61,7 +42,8 @@ class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObj
super.loadView()
self.view.addSubview(maskingView)
maskingView.pin(to: self.view)
maskingView.layer.addSublayer(maskLayer)
}
override func viewWillAppear(_ animated: Bool) {
@ -81,11 +63,28 @@ class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObj
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
// Note: When accessing 'capture.layer' if the setup hasn't been completed it
// will result in a layout being triggered which creates an infinite loop, this
// check prevents that case
if let capture: ZXCapture = self.capture {
capture.layer.frame = self.view.bounds
captureLayer?.frame = self.view.bounds
if maskingView.frame != self.view.bounds {
// Add a circular mask
let path: UIBezierPath = UIBezierPath(rect: self.view.bounds)
let radius: CGFloat = ((min(self.view.bounds.size.width, self.view.bounds.size.height) * 0.5) - Values.largeSpacing)
// Center the circle's bounding rectangle
let circleRect: CGRect = CGRect(
x: ((self.view.bounds.size.width * 0.5) - radius),
y: ((self.view.bounds.size.height * 0.5) - radius),
width: (radius * 2),
height: (radius * 2)
)
let clippingPath: UIBezierPath = UIBezierPath.init(
roundedRect: circleRect,
cornerRadius: 16
)
path.append(clippingPath)
path.usesEvenOddFillRule = true
maskLayer.path = path.cgPath
}
}
@ -101,31 +100,76 @@ class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObj
#else
if self.capture == nil {
self.captureQueue.async { [weak self] in
let capture: ZXCapture = ZXCapture()
capture.camera = capture.back()
capture.focusMode = .autoFocus
capture.delegate = self
capture.start()
let maybeDevice: AVCaptureDevice? = {
if let result = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back) {
return result
}
return AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)
}()
// Set the input device to autoFocus (since we don't have the interaction setup for
// doing it manually)
maybeDevice?.focusMode = .continuousAutoFocus
// Device input
guard
let device: AVCaptureDevice = maybeDevice,
let input: AVCaptureInput = try? AVCaptureDeviceInput(device: device)
else {
return SNLog("Failed to retrieve the device for enabling the QRCode scanning camera")
}
// Image output
let output: AVCaptureVideoDataOutput = AVCaptureVideoDataOutput()
output.alwaysDiscardsLateVideoFrames = true
// Metadata output the session
let metadataOutput: AVCaptureMetadataOutput = AVCaptureMetadataOutput()
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
// Note: When accessing the 'layer' for the first time it will create
// an instance of 'AVCaptureVideoPreviewLayer', this can hang a little
// so we do this on the background thread first
if capture.layer != nil {}
let capture: AVCaptureSession = AVCaptureSession()
capture.beginConfiguration()
if capture.canAddInput(input) { capture.addInput(input) }
if capture.canAddOutput(output) { capture.addOutput(output) }
if capture.canAddOutput(metadataOutput) { capture.addOutput(metadataOutput) }
guard !capture.inputs.isEmpty && capture.outputs.count == 2 else {
return SNLog("Failed to attach the input/output to the capture session")
}
guard metadataOutput.availableMetadataObjectTypes.contains(.qr) else {
return SNLog("The output is unable to process QR codes")
}
// Specify that we want to capture QR Codes (Needs to be done after being added
// to the session, 'availableMetadataObjectTypes' is empty beforehand)
metadataOutput.metadataObjectTypes = [.qr]
capture.commitConfiguration()
// Create the layer for rendering the camera video
let layer: AVCaptureVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: capture)
layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
// Start running the capture session
capture.startRunning()
DispatchQueue.main.async {
capture.layer.frame = (self?.view.bounds ?? .zero)
self?.view.layer.addSublayer(capture.layer)
layer.frame = (self?.view.bounds ?? .zero)
self?.view.layer.addSublayer(layer)
if let maskingView: UIView = self?.maskingView {
self?.view.bringSubviewToFront(maskingView)
}
self?.capture = capture
self?.captureLayer = layer
}
}
}
else {
self.capture?.start()
self.capture?.startRunning()
}
#endif
}
@ -133,18 +177,25 @@ class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObj
private func stopCapture() {
self.captureEnabled = false
self.captureQueue.async { [weak self] in
self?.capture?.stop()
self?.capture?.stopRunning()
}
}
internal func captureResult(_ capture: ZXCapture, result: ZXResult) {
guard self.captureEnabled else { return }
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
guard
self.captureEnabled,
let metadata: AVMetadataObject = metadataObjects.first(where: { ($0 as? AVMetadataMachineReadableCodeObject)?.type == .qr }),
let qrCodeInfo: AVMetadataMachineReadableCodeObject = metadata as? AVMetadataMachineReadableCodeObject,
let qrCode: String = qrCodeInfo.stringValue
else { return }
self.stopCapture()
// Vibrate
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
self.scanDelegate?.controller(self, didDetectQRCodeWith: result.text)
self.scanDelegate?.controller(self, didDetectQRCodeWith: qrCode) { [weak self] in
self?.startCapture()
}
}
}

Loading…
Cancel
Save