fixed iOS QRCodeReader

This commit is contained in:
dranik
2026-05-07 22:50:14 +03:00
parent f65fd4a8c5
commit 6fc65dba8a
8 changed files with 176 additions and 23 deletions

View File

@@ -33,6 +33,16 @@
namespace namespace
{ {
void execNetworkWaitLoop(QEventLoop &wait)
{
#ifdef Q_OS_IOS
// QEventLoop::ExcludeUserInputEvents is not supported on iOS (Qt warns; can break nested UI).
wait.exec();
#else
wait.exec(QEventLoop::ExcludeUserInputEvents);
#endif
}
constexpr QLatin1String errorResponsePattern1("No active configuration found for"); constexpr QLatin1String errorResponsePattern1("No active configuration found for");
constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for"); constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for");
constexpr QLatin1String errorResponsePattern3("Account not found."); constexpr QLatin1String errorResponsePattern3("Account not found.");
@@ -189,7 +199,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
QList<QSslError> sslErrors; QList<QSslError> sslErrors;
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; }); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(QEventLoop::ExcludeUserInputEvents); execNetworkWaitLoop(wait);
QByteArray encryptedResponseBody = reply->readAll(); QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString(); QString replyErrorString = reply->errorString();
@@ -431,7 +441,7 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; }); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(QEventLoop::ExcludeUserInputEvents); execNetworkWaitLoop(wait);
if (reply->error() == QNetworkReply::NetworkError::NoError) { if (reply->error() == QNetworkReply::NetworkError::NoError) {
auto encryptedResponseBody = reply->readAll(); auto encryptedResponseBody = reply->readAll();
@@ -564,7 +574,7 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; }); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(QEventLoop::ExcludeUserInputEvents); execNetworkWaitLoop(wait);
auto result = replyProcessingFunction(reply, sslErrors); auto result = replyProcessingFunction(reply, sslErrors);
reply->deleteLater(); reply->deleteLater();
@@ -586,7 +596,7 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; }); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(QEventLoop::ExcludeUserInputEvents); execNetworkWaitLoop(wait);
if (reply->error() == QNetworkReply::NetworkError::NoError) { if (reply->error() == QNetworkReply::NetworkError::NoError) {
reply->deleteLater(); reply->deleteLater();

View File

@@ -12,3 +12,4 @@ QRect QRCodeReader::cameraSize() {
void QRCodeReader::startReading() {} void QRCodeReader::startReading() {}
void QRCodeReader::stopReading() {} void QRCodeReader::stopReading() {}
void QRCodeReader::setCameraSize(QRect) {} void QRCodeReader::setCameraSize(QRect) {}
void QRCodeReader::notifyCodeRead(const QString &) {}

View File

@@ -16,6 +16,8 @@ public slots:
void startReading(); void startReading();
void stopReading(); void stopReading();
void setCameraSize(QRect value); void setCameraSize(QRect value);
/// Called from AVFoundation delegate on the main queue; emits codeReaded.
void notifyCodeRead(const QString &code);
signals: signals:
void codeReaded(QString code); void codeReaded(QString code);

View File

@@ -1,6 +1,9 @@
#if !MACOS_NE #if !MACOS_NE
#include "QRCodeReaderBase.h" #include "QRCodeReaderBase.h"
#include <QByteArray>
#include <QDebug>
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h> #import <AVFoundation/AVFoundation.h>
@@ -27,13 +30,15 @@
} }
- (BOOL)startReading { - (BOOL)startReading {
NSError *error; [self stopReading];
NSError *error = nil;
AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo]; AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo];
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice: captureDevice error: &error]; AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice: captureDevice error: &error];
if(!deviceInput) { if (!deviceInput) {
NSLog(@"Error %@", error.localizedDescription); NSLog(@"[QRCodeReader] deviceInput failed: %@", error.localizedDescription);
return NO; return NO;
} }
@@ -66,14 +71,21 @@
[_captureSession startRunning]; [_captureSession startRunning];
NSLog(@"[QRCodeReader] startReading OK frame=(%.1f,%.1f,%.1f,%.1f) statusBar=%.1f",
cameraCGRect.origin.x, cameraCGRect.origin.y, cameraCGRect.size.width, cameraCGRect.size.height, statusBarHeight);
return YES; return YES;
} }
- (void)stopReading { - (void)stopReading {
[_captureSession stopRunning]; if (_captureSession) {
_captureSession = nil; [_captureSession stopRunning];
_captureSession = nil;
[_videoPreviewPlayer removeFromSuperlayer]; }
if (_videoPreviewPlayer) {
[_videoPreviewPlayer removeFromSuperlayer];
_videoPreviewPlayer = nil;
}
} }
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection { - (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
@@ -82,7 +94,16 @@
AVMetadataMachineReadableCodeObject *metadataObject = [metadataObjects objectAtIndex:0]; AVMetadataMachineReadableCodeObject *metadataObject = [metadataObjects objectAtIndex:0];
if ([[metadataObject type] isEqualToString: AVMetadataObjectTypeQRCode]) { if ([[metadataObject type] isEqualToString: AVMetadataObjectTypeQRCode]) {
_qrCodeReader->emit codeReaded([metadataObject stringValue].UTF8String); NSString *value = [metadataObject stringValue];
if (value.length == 0) {
return;
}
NSLog(@"[QRCodeReader] metadata QR len=%lu", static_cast<unsigned long>(value.length));
QRCodeReader *cpp = _qrCodeReader;
const QByteArray utf8([value UTF8String]);
dispatch_async(dispatch_get_main_queue(), ^{
cpp->notifyCodeRead(QString::fromUtf8(utf8));
});
} }
} }
} }
@@ -100,14 +121,23 @@ QRect QRCodeReader::cameraSize() {
void QRCodeReader::setCameraSize(QRect value) { void QRCodeReader::setCameraSize(QRect value) {
m_cameraSize = value; m_cameraSize = value;
qInfo() << "[QRCodeReader] setCameraSize" << value;
} }
void QRCodeReader::startReading() { void QRCodeReader::startReading() {
[m_qrCodeReader startReading]; const BOOL ok = [m_qrCodeReader startReading];
if (!ok) {
qWarning() << "[QRCodeReader] startReading failed (see NSLogs)";
}
} }
void QRCodeReader::stopReading() { void QRCodeReader::stopReading() {
[m_qrCodeReader stopReading]; [m_qrCodeReader stopReading];
qInfo() << "[QRCodeReader] stopReading";
}
void QRCodeReader::notifyCodeRead(const QString &code) {
emit codeReaded(code);
} }
#else #else
#include "QRCodeReaderBase.h" #include "QRCodeReaderBase.h"
@@ -124,4 +154,5 @@ QRect QRCodeReader::cameraSize() {
void QRCodeReader::startReading() {} void QRCodeReader::startReading() {}
void QRCodeReader::stopReading() {} void QRCodeReader::stopReading() {}
void QRCodeReader::setCameraSize(QRect) {} void QRCodeReader::setCameraSize(QRect) {}
void QRCodeReader::notifyCodeRead(const QString &) {}
#endif #endif

View File

@@ -1,5 +1,6 @@
#include "pairingUiController.h" #include "pairingUiController.h"
#include <QDebug>
#include <QRegularExpression> #include <QRegularExpression>
#include <QTimer> #include <QTimer>
#include <QUuid> #include <QUuid>
@@ -82,6 +83,7 @@ void PairingUiController::setTvPairingUiPhase(int phase)
void PairingUiController::openPairingQrScanner() void PairingUiController::openPairingQrScanner()
{ {
#if defined(Q_OS_ANDROID) #if defined(Q_OS_ANDROID)
qInfo() << "[PairingUi] openPairingQrScanner (Android native activity)";
AndroidController::instance()->startQrReaderActivity(); AndroidController::instance()->startQrReaderActivity();
#endif #endif
} }
@@ -89,16 +91,20 @@ void PairingUiController::openPairingQrScanner()
bool PairingUiController::applyScannedTextAsPairingUuid(const QString &raw) bool PairingUiController::applyScannedTextAsPairingUuid(const QString &raw)
{ {
const QString t = raw.trimmed(); const QString t = raw.trimmed();
qInfo() << "[PairingUi] scan raw len=" << t.size();
if (t.startsWith(QStringLiteral("vpn://"), Qt::CaseInsensitive)) { if (t.startsWith(QStringLiteral("vpn://"), Qt::CaseInsensitive)) {
qInfo() << "[PairingUi] scan rejected: looks like vpn:// bundle, not session UUID";
return false; return false;
} }
static const QRegularExpression re(QStringLiteral( static const QRegularExpression re(QStringLiteral(
"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}")); "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}"));
const QRegularExpressionMatch m = re.match(t); const QRegularExpressionMatch m = re.match(t);
if (!m.hasMatch()) { if (!m.hasMatch()) {
qInfo() << "[PairingUi] scan rejected: no UUID v4 pattern in payload";
return false; return false;
} }
const QString uuid = m.captured(0); const QString uuid = m.captured(0);
qInfo() << "[PairingUi] scan accepted uuid=" << uuid.left(13) << "...";
emit pairingUuidFromScan(uuid); emit pairingUuidFromScan(uuid);
return true; return true;
} }
@@ -367,24 +373,29 @@ void PairingUiController::submitPhonePairing(const QString &qrUuid, int serverIn
} }
const QString trimmedUuid = qrUuid.trimmed(); const QString trimmedUuid = qrUuid.trimmed();
qInfo() << "[PairingUi] submitPhonePairing serverIndex=" << serverIndex << "uuidLen=" << trimmedUuid.size();
if (trimmedUuid.isEmpty()) { if (trimmedUuid.isEmpty()) {
qWarning() << "[PairingUi] submitPhonePairing aborted: empty UUID (paste or scan first)";
emit errorOccurred(ErrorCode::ApiConfigEmptyError); emit errorOccurred(ErrorCode::ApiConfigEmptyError);
return; return;
} }
if (serverIndex < 0 || serverIndex >= m_serversController->getServersCount()) { if (serverIndex < 0 || serverIndex >= m_serversController->getServersCount()) {
qWarning() << "[PairingUi] submitPhonePairing invalid serverIndex";
emit errorOccurred(ErrorCode::InternalError); emit errorOccurred(ErrorCode::InternalError);
return; return;
} }
const ServerConfig serverConfig = m_serversController->getServerConfig(serverIndex); const ServerConfig serverConfig = m_serversController->getServerConfig(serverIndex);
if (!serverConfig.isApiV2()) { if (!serverConfig.isApiV2()) {
qWarning() << "[PairingUi] submitPhonePairing server is not API v2";
emit errorOccurred(ErrorCode::InternalError); emit errorOccurred(ErrorCode::InternalError);
return; return;
} }
const ApiV2ServerConfig *apiV2 = serverConfig.as<ApiV2ServerConfig>(); const ApiV2ServerConfig *apiV2 = serverConfig.as<ApiV2ServerConfig>();
if (!apiV2) { if (!apiV2) {
qWarning() << "[PairingUi] submitPhonePairing cast to ApiV2ServerConfig failed";
emit errorOccurred(ErrorCode::InternalError); emit errorOccurred(ErrorCode::InternalError);
return; return;
} }
@@ -392,6 +403,7 @@ void PairingUiController::submitPhonePairing(const QString &qrUuid, int serverIn
QString vpnKey; QString vpnKey;
const ErrorCode keyErr = m_subscriptionController->prepareVpnKeyExport(serverIndex, vpnKey); const ErrorCode keyErr = m_subscriptionController->prepareVpnKeyExport(serverIndex, vpnKey);
if (keyErr != ErrorCode::NoError) { if (keyErr != ErrorCode::NoError) {
qWarning() << "[PairingUi] prepareVpnKeyExport failed" << static_cast<int>(keyErr);
emit errorOccurred(keyErr); emit errorOccurred(keyErr);
return; return;
} }
@@ -400,6 +412,7 @@ void PairingUiController::submitPhonePairing(const QString &qrUuid, int serverIn
const QJsonArray supportedProtocols = apiV2->apiConfig.supportedProtocols; const QJsonArray supportedProtocols = apiV2->apiConfig.supportedProtocols;
const QString apiKey = apiV2->authData.apiKey; const QString apiKey = apiV2->authData.apiKey;
if (apiKey.isEmpty()) { if (apiKey.isEmpty()) {
qWarning() << "[PairingUi] submitPhonePairing aborted: empty API key on server card";
emit errorOccurred(ErrorCode::ApiConfigEmptyError); emit errorOccurred(ErrorCode::ApiConfigEmptyError);
return; return;
} }

View File

@@ -435,7 +435,11 @@ bool SubscriptionUiController::getAccountInfo(int serverIndex, bool reload)
if (reload) { if (reload) {
QEventLoop wait; QEventLoop wait;
QTimer::singleShot(1000, &wait, &QEventLoop::quit); QTimer::singleShot(1000, &wait, &QEventLoop::quit);
#ifdef Q_OS_IOS
wait.exec();
#else
wait.exec(QEventLoop::ExcludeUserInputEvents); wait.exec(QEventLoop::ExcludeUserInputEvents);
#endif
} }
QJsonObject accountInfo; QJsonObject accountInfo;
ErrorCode errorCode = m_subscriptionController->getAccountInfo(serverIndex, accountInfo); ErrorCode errorCode = m_subscriptionController->getAccountInfo(serverIndex, accountInfo);

View File

@@ -16,10 +16,34 @@ PageType {
property int qrImageIndex: 0 property int qrImageIndex: 0
property bool pairingCameraOpen: false property bool pairingCameraOpen: false
Timer {
id: pairingCameraKickTimer
interval: 180
repeat: false
onTriggered: root.restartPairingIosCamera()
}
function restartPairingIosCamera() {
if (Qt.platform.os !== "ios" || !root.pairingCameraOpen) {
return
}
if (cameraSlot.width < 32 || cameraSlot.height < 32) {
console.info("[PairingQr] cameraSlot too small wxh=", cameraSlot.width, cameraSlot.height, "retry")
pairingCameraKickTimer.restart()
return
}
var p = cameraSlot.mapToItem(root, 0, 0)
console.info("[PairingQr] start preview frame", p.x, p.y, cameraSlot.width, cameraSlot.height)
pairingQrReader.stopReading()
pairingQrReader.setCameraSize(Qt.rect(Math.round(p.x), Math.round(p.y), Math.round(cameraSlot.width), Math.round(cameraSlot.height)))
pairingQrReader.startReading()
}
Connections { Connections {
target: root target: root
function onVisibleChanged() { function onVisibleChanged() {
if (!root.visible) { if (!root.visible) {
pairingCameraKickTimer.stop()
pairingQrReader.stopReading() pairingQrReader.stopReading()
root.pairingCameraOpen = false root.pairingCameraOpen = false
PairingUiController.cancelAllPairingActivity() PairingUiController.cancelAllPairingActivity()
@@ -27,6 +51,31 @@ PageType {
} }
} }
Connections {
target: root
function onPairingCameraOpenChanged() {
if (!root.pairingCameraOpen) {
pairingCameraKickTimer.stop()
pairingQrReader.stopReading()
return
}
if (Qt.platform.os === "ios") {
pairingCameraKickTimer.restart()
}
}
}
Connections {
target: cameraSlot
enabled: Qt.platform.os === "ios" && root.pairingCameraOpen
function onWidthChanged() {
pairingCameraKickTimer.restart()
}
function onHeightChanged() {
pairingCameraKickTimer.restart()
}
}
FlickableType { FlickableType {
anchors.fill: parent anchors.fill: parent
contentHeight: layout.implicitHeight contentHeight: layout.implicitHeight
@@ -189,6 +238,7 @@ PageType {
QRCodeReader { QRCodeReader {
id: pairingQrReader id: pairingQrReader
anchors.fill: parent
onCodeReaded: function(code) { onCodeReaded: function(code) {
if (PairingUiController.applyScannedTextAsPairingUuid(code)) { if (PairingUiController.applyScannedTextAsPairingUuid(code)) {
@@ -205,11 +255,7 @@ PageType {
return return
} }
if (Qt.platform.os === "ios") { if (Qt.platform.os === "ios") {
Qt.callLater(function() { pairingCameraKickTimer.restart()
var p = cameraSlot.mapToItem(root, 0, 0)
pairingQrReader.setCameraSize(Qt.rect(p.x, p.y, cameraSlot.width, cameraSlot.height))
pairingQrReader.startReading()
})
} }
} }
} }

View File

@@ -15,10 +15,34 @@ PageType {
property bool pairingCameraOpen: false property bool pairingCameraOpen: false
Timer {
id: pairingCameraKickTimer
interval: 180
repeat: false
onTriggered: root.restartPairingIosCamera()
}
function restartPairingIosCamera() {
if (Qt.platform.os !== "ios" || !root.pairingCameraOpen) {
return
}
if (cameraSlot.width < 32 || cameraSlot.height < 32) {
console.info("[PairingQr] cameraSlot too small wxh=", cameraSlot.width, cameraSlot.height, "retry")
pairingCameraKickTimer.restart()
return
}
var p = cameraSlot.mapToItem(root, 0, 0)
console.info("[PairingQr] start preview frame", p.x, p.y, cameraSlot.width, cameraSlot.height)
pairingQrReader.stopReading()
pairingQrReader.setCameraSize(Qt.rect(Math.round(p.x), Math.round(p.y), Math.round(cameraSlot.width), Math.round(cameraSlot.height)))
pairingQrReader.startReading()
}
Connections { Connections {
target: root target: root
function onVisibleChanged() { function onVisibleChanged() {
if (!root.visible) { if (!root.visible) {
pairingCameraKickTimer.stop()
pairingQrReader.stopReading() pairingQrReader.stopReading()
root.pairingCameraOpen = false root.pairingCameraOpen = false
PairingUiController.cancelAllPairingActivity() PairingUiController.cancelAllPairingActivity()
@@ -26,6 +50,31 @@ PageType {
} }
} }
Connections {
target: root
function onPairingCameraOpenChanged() {
if (!root.pairingCameraOpen) {
pairingCameraKickTimer.stop()
pairingQrReader.stopReading()
return
}
if (Qt.platform.os === "ios") {
pairingCameraKickTimer.restart()
}
}
}
Connections {
target: cameraSlot
enabled: Qt.platform.os === "ios" && root.pairingCameraOpen
function onWidthChanged() {
pairingCameraKickTimer.restart()
}
function onHeightChanged() {
pairingCameraKickTimer.restart()
}
}
FlickableType { FlickableType {
anchors.fill: parent anchors.fill: parent
contentHeight: layout.implicitHeight contentHeight: layout.implicitHeight
@@ -111,6 +160,7 @@ PageType {
QRCodeReader { QRCodeReader {
id: pairingQrReader id: pairingQrReader
anchors.fill: parent
onCodeReaded: function(code) { onCodeReaded: function(code) {
if (PairingUiController.applyScannedTextAsPairingUuid(code)) { if (PairingUiController.applyScannedTextAsPairingUuid(code)) {
@@ -127,11 +177,7 @@ PageType {
return return
} }
if (Qt.platform.os === "ios") { if (Qt.platform.os === "ios") {
Qt.callLater(function() { pairingCameraKickTimer.restart()
var p = cameraSlot.mapToItem(root, 0, 0)
pairingQrReader.setCameraSize(Qt.rect(p.x, p.y, cameraSlot.width, cameraSlot.height))
pairingQrReader.startReading()
})
} }
} }
} }