From 6fc65dba8acaa0c5cb204c0c8f5af42bc92d12bc Mon Sep 17 00:00:00 2001 From: dranik Date: Thu, 7 May 2026 22:50:14 +0300 Subject: [PATCH] fixed iOS QRCodeReader --- client/core/controllers/gatewayController.cpp | 18 ++++-- client/platforms/ios/QRCodeReaderBase.cpp | 1 + client/platforms/ios/QRCodeReaderBase.h | 2 + client/platforms/ios/QRCodeReaderBase.mm | 49 +++++++++++++--- .../controllers/api/pairingUiController.cpp | 13 +++++ .../api/subscriptionUiController.cpp | 4 ++ .../Pages2/PageSettingsApiQrPairingDev.qml | 56 +++++++++++++++++-- .../Pages2/PageSettingsApiQrPairingSend.qml | 56 +++++++++++++++++-- 8 files changed, 176 insertions(+), 23 deletions(-) diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index 31f194a2d..b09a28047 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -33,6 +33,16 @@ 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 errorResponsePattern2("No non-revoked public key found for"); constexpr QLatin1String errorResponsePattern3("Account not found."); @@ -189,7 +199,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api QList sslErrors; connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); - wait.exec(QEventLoop::ExcludeUserInputEvents); + execNetworkWaitLoop(wait); QByteArray encryptedResponseBody = reply->readAll(); 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::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); - wait.exec(QEventLoop::ExcludeUserInputEvents); + execNetworkWaitLoop(wait); if (reply->error() == QNetworkReply::NetworkError::NoError) { 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); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); - wait.exec(QEventLoop::ExcludeUserInputEvents); + execNetworkWaitLoop(wait); auto result = replyProcessingFunction(reply, sslErrors); 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::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); - wait.exec(QEventLoop::ExcludeUserInputEvents); + execNetworkWaitLoop(wait); if (reply->error() == QNetworkReply::NetworkError::NoError) { reply->deleteLater(); diff --git a/client/platforms/ios/QRCodeReaderBase.cpp b/client/platforms/ios/QRCodeReaderBase.cpp index c3148a854..7e7c2dbd8 100644 --- a/client/platforms/ios/QRCodeReaderBase.cpp +++ b/client/platforms/ios/QRCodeReaderBase.cpp @@ -12,3 +12,4 @@ QRect QRCodeReader::cameraSize() { void QRCodeReader::startReading() {} void QRCodeReader::stopReading() {} void QRCodeReader::setCameraSize(QRect) {} +void QRCodeReader::notifyCodeRead(const QString &) {} diff --git a/client/platforms/ios/QRCodeReaderBase.h b/client/platforms/ios/QRCodeReaderBase.h index 29a4946d8..6dc712f4a 100644 --- a/client/platforms/ios/QRCodeReaderBase.h +++ b/client/platforms/ios/QRCodeReaderBase.h @@ -16,6 +16,8 @@ public slots: void startReading(); void stopReading(); void setCameraSize(QRect value); + /// Called from AVFoundation delegate on the main queue; emits codeReaded. + void notifyCodeRead(const QString &code); signals: void codeReaded(QString code); diff --git a/client/platforms/ios/QRCodeReaderBase.mm b/client/platforms/ios/QRCodeReaderBase.mm index 963c35a85..e865b9c16 100644 --- a/client/platforms/ios/QRCodeReaderBase.mm +++ b/client/platforms/ios/QRCodeReaderBase.mm @@ -1,6 +1,9 @@ #if !MACOS_NE #include "QRCodeReaderBase.h" +#include +#include + #import #import @@ -27,13 +30,15 @@ } - (BOOL)startReading { - NSError *error; + [self stopReading]; + + NSError *error = nil; AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo]; AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice: captureDevice error: &error]; - if(!deviceInput) { - NSLog(@"Error %@", error.localizedDescription); + if (!deviceInput) { + NSLog(@"[QRCodeReader] deviceInput failed: %@", error.localizedDescription); return NO; } @@ -66,14 +71,21 @@ [_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; } - (void)stopReading { - [_captureSession stopRunning]; - _captureSession = nil; - - [_videoPreviewPlayer removeFromSuperlayer]; + if (_captureSession) { + [_captureSession stopRunning]; + _captureSession = nil; + } + if (_videoPreviewPlayer) { + [_videoPreviewPlayer removeFromSuperlayer]; + _videoPreviewPlayer = nil; + } } - (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection { @@ -82,7 +94,16 @@ AVMetadataMachineReadableCodeObject *metadataObject = [metadataObjects objectAtIndex:0]; 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(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) { m_cameraSize = value; + qInfo() << "[QRCodeReader] setCameraSize" << value; } void QRCodeReader::startReading() { - [m_qrCodeReader startReading]; + const BOOL ok = [m_qrCodeReader startReading]; + if (!ok) { + qWarning() << "[QRCodeReader] startReading failed (see NSLogs)"; + } } void QRCodeReader::stopReading() { [m_qrCodeReader stopReading]; + qInfo() << "[QRCodeReader] stopReading"; +} + +void QRCodeReader::notifyCodeRead(const QString &code) { + emit codeReaded(code); } #else #include "QRCodeReaderBase.h" @@ -124,4 +154,5 @@ QRect QRCodeReader::cameraSize() { void QRCodeReader::startReading() {} void QRCodeReader::stopReading() {} void QRCodeReader::setCameraSize(QRect) {} +void QRCodeReader::notifyCodeRead(const QString &) {} #endif diff --git a/client/ui/controllers/api/pairingUiController.cpp b/client/ui/controllers/api/pairingUiController.cpp index d057e057e..35aeea4cf 100644 --- a/client/ui/controllers/api/pairingUiController.cpp +++ b/client/ui/controllers/api/pairingUiController.cpp @@ -1,5 +1,6 @@ #include "pairingUiController.h" +#include #include #include #include @@ -82,6 +83,7 @@ void PairingUiController::setTvPairingUiPhase(int phase) void PairingUiController::openPairingQrScanner() { #if defined(Q_OS_ANDROID) + qInfo() << "[PairingUi] openPairingQrScanner (Android native activity)"; AndroidController::instance()->startQrReaderActivity(); #endif } @@ -89,16 +91,20 @@ void PairingUiController::openPairingQrScanner() bool PairingUiController::applyScannedTextAsPairingUuid(const QString &raw) { const QString t = raw.trimmed(); + qInfo() << "[PairingUi] scan raw len=" << t.size(); if (t.startsWith(QStringLiteral("vpn://"), Qt::CaseInsensitive)) { + qInfo() << "[PairingUi] scan rejected: looks like vpn:// bundle, not session UUID"; return false; } 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}")); const QRegularExpressionMatch m = re.match(t); if (!m.hasMatch()) { + qInfo() << "[PairingUi] scan rejected: no UUID v4 pattern in payload"; return false; } const QString uuid = m.captured(0); + qInfo() << "[PairingUi] scan accepted uuid=" << uuid.left(13) << "..."; emit pairingUuidFromScan(uuid); return true; } @@ -367,24 +373,29 @@ void PairingUiController::submitPhonePairing(const QString &qrUuid, int serverIn } const QString trimmedUuid = qrUuid.trimmed(); + qInfo() << "[PairingUi] submitPhonePairing serverIndex=" << serverIndex << "uuidLen=" << trimmedUuid.size(); if (trimmedUuid.isEmpty()) { + qWarning() << "[PairingUi] submitPhonePairing aborted: empty UUID (paste or scan first)"; emit errorOccurred(ErrorCode::ApiConfigEmptyError); return; } if (serverIndex < 0 || serverIndex >= m_serversController->getServersCount()) { + qWarning() << "[PairingUi] submitPhonePairing invalid serverIndex"; emit errorOccurred(ErrorCode::InternalError); return; } const ServerConfig serverConfig = m_serversController->getServerConfig(serverIndex); if (!serverConfig.isApiV2()) { + qWarning() << "[PairingUi] submitPhonePairing server is not API v2"; emit errorOccurred(ErrorCode::InternalError); return; } const ApiV2ServerConfig *apiV2 = serverConfig.as(); if (!apiV2) { + qWarning() << "[PairingUi] submitPhonePairing cast to ApiV2ServerConfig failed"; emit errorOccurred(ErrorCode::InternalError); return; } @@ -392,6 +403,7 @@ void PairingUiController::submitPhonePairing(const QString &qrUuid, int serverIn QString vpnKey; const ErrorCode keyErr = m_subscriptionController->prepareVpnKeyExport(serverIndex, vpnKey); if (keyErr != ErrorCode::NoError) { + qWarning() << "[PairingUi] prepareVpnKeyExport failed" << static_cast(keyErr); emit errorOccurred(keyErr); return; } @@ -400,6 +412,7 @@ void PairingUiController::submitPhonePairing(const QString &qrUuid, int serverIn const QJsonArray supportedProtocols = apiV2->apiConfig.supportedProtocols; const QString apiKey = apiV2->authData.apiKey; if (apiKey.isEmpty()) { + qWarning() << "[PairingUi] submitPhonePairing aborted: empty API key on server card"; emit errorOccurred(ErrorCode::ApiConfigEmptyError); return; } diff --git a/client/ui/controllers/api/subscriptionUiController.cpp b/client/ui/controllers/api/subscriptionUiController.cpp index a8a73ff35..58c404f18 100644 --- a/client/ui/controllers/api/subscriptionUiController.cpp +++ b/client/ui/controllers/api/subscriptionUiController.cpp @@ -435,7 +435,11 @@ bool SubscriptionUiController::getAccountInfo(int serverIndex, bool reload) if (reload) { QEventLoop wait; QTimer::singleShot(1000, &wait, &QEventLoop::quit); +#ifdef Q_OS_IOS + wait.exec(); +#else wait.exec(QEventLoop::ExcludeUserInputEvents); +#endif } QJsonObject accountInfo; ErrorCode errorCode = m_subscriptionController->getAccountInfo(serverIndex, accountInfo); diff --git a/client/ui/qml/Pages2/PageSettingsApiQrPairingDev.qml b/client/ui/qml/Pages2/PageSettingsApiQrPairingDev.qml index a8af8b4bb..fc0a68544 100644 --- a/client/ui/qml/Pages2/PageSettingsApiQrPairingDev.qml +++ b/client/ui/qml/Pages2/PageSettingsApiQrPairingDev.qml @@ -16,10 +16,34 @@ PageType { property int qrImageIndex: 0 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 { target: root function onVisibleChanged() { if (!root.visible) { + pairingCameraKickTimer.stop() pairingQrReader.stopReading() root.pairingCameraOpen = false 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 { anchors.fill: parent contentHeight: layout.implicitHeight @@ -189,6 +238,7 @@ PageType { QRCodeReader { id: pairingQrReader + anchors.fill: parent onCodeReaded: function(code) { if (PairingUiController.applyScannedTextAsPairingUuid(code)) { @@ -205,11 +255,7 @@ PageType { return } if (Qt.platform.os === "ios") { - Qt.callLater(function() { - var p = cameraSlot.mapToItem(root, 0, 0) - pairingQrReader.setCameraSize(Qt.rect(p.x, p.y, cameraSlot.width, cameraSlot.height)) - pairingQrReader.startReading() - }) + pairingCameraKickTimer.restart() } } } diff --git a/client/ui/qml/Pages2/PageSettingsApiQrPairingSend.qml b/client/ui/qml/Pages2/PageSettingsApiQrPairingSend.qml index 18cdb0ca4..05966d1c3 100644 --- a/client/ui/qml/Pages2/PageSettingsApiQrPairingSend.qml +++ b/client/ui/qml/Pages2/PageSettingsApiQrPairingSend.qml @@ -15,10 +15,34 @@ PageType { 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 { target: root function onVisibleChanged() { if (!root.visible) { + pairingCameraKickTimer.stop() pairingQrReader.stopReading() root.pairingCameraOpen = false 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 { anchors.fill: parent contentHeight: layout.implicitHeight @@ -111,6 +160,7 @@ PageType { QRCodeReader { id: pairingQrReader + anchors.fill: parent onCodeReaded: function(code) { if (PairingUiController.applyScannedTextAsPairingUuid(code)) { @@ -127,11 +177,7 @@ PageType { return } if (Qt.platform.os === "ios") { - Qt.callLater(function() { - var p = cameraSlot.mapToItem(root, 0, 0) - pairingQrReader.setCameraSize(Qt.rect(p.x, p.y, cameraSlot.width, cameraSlot.height)) - pairingQrReader.startReading() - }) + pairingCameraKickTimer.restart() } } }