remove dead code

This commit is contained in:
dranik
2026-05-13 11:56:58 +03:00
parent e226fadb07
commit 1baa2d85bd
9 changed files with 1 additions and 347 deletions
-4
View File
@@ -211,10 +211,6 @@ if(AMNEZIA_QR_PAIRING_ALLOW)
target_compile_definitions(${PROJECT} PRIVATE AMNEZIA_QR_PAIRING_ALLOW) target_compile_definitions(${PROJECT} PRIVATE AMNEZIA_QR_PAIRING_ALLOW)
endif() endif()
if(AMNEZIA_QR_PAIRING_ALLOW_DUPLICATE_VPN_KEY)
target_compile_definitions(${PROJECT} PRIVATE AMNEZIA_QR_PAIRING_ALLOW_DUPLICATE_VPN_KEY)
endif()
target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC}) target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC})
# Finalize the executable so Qt can gather/deploy QML modules and plugins correctly (Android needs this). # Finalize the executable so Qt can gather/deploy QML modules and plugins correctly (Android needs this).
-4
View File
@@ -254,10 +254,6 @@ bool AmneziaApplication::parseCommands()
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
void AmneziaApplication::startLocalServer() { void AmneziaApplication::startLocalServer() {
#ifdef AMNEZIA_QR_PAIRING_ALLOW
return;
#endif
const QString serverName("AmneziaVPNInstance"); const QString serverName("AmneziaVPNInstance");
QLocalServer::removeServer(serverName); QLocalServer::removeServer(serverName);
@@ -7,40 +7,7 @@ import kotlin.math.floor
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
/**
* Same proportions as [PageSettingsApiQrPairingSend.qml] (iOS embedded scan): sq = 0.72 * min(w,h),
* vertical bias -0.06 * height (square shifted up slightly).
*/
object PairingQrScanGeometry { object PairingQrScanGeometry {
const val SQ_FRACTION = 0.72f
const val VERTICAL_BIAS = 0.06f
fun holeRectF(viewW: Int, viewH: Int): RectF {
val w = viewW.toFloat()
val h = viewH.toFloat()
val side = min(w, h) * SQ_FRACTION
val left = (w - side) / 2f
var top = (h - side) / 2f - h * VERTICAL_BIAS
top = max(0f, top)
var bottom = top + side
if (bottom > h) {
bottom = h
}
val adjSide = bottom - top
return RectF(left, top, left + adjSide, bottom)
}
/** ML Kit [Barcode] box is in [InputImage] pixel space (same as analysis frame WxH). */
fun barcodeCenterInPairingHole(imageW: Int, imageH: Int, barcode: Barcode): Boolean {
val box = barcode.boundingBox ?: return true
val r = holeRectF(imageW, imageH)
return r.contains(box.centerX().toFloat(), box.centerY().toFloat())
}
/**
* Maps a rectangle in [PreviewView] / overlay pixel space to [InputImage] pixel space,
* assuming the preview uses default FILL_CENTER scaling (same as typical CameraX [PreviewView]).
*/
fun viewRectToInputImageRectFillCenter( fun viewRectToInputImageRectFillCenter(
viewW: Int, viewW: Int,
viewH: Int, viewH: Int,
@@ -59,16 +26,6 @@ object PairingQrScanGeometry {
) )
} }
/** Pairing hole (same geometry as overlay) expressed in ML Kit / [InputImage] coordinates. */
fun pairingHoleInImageCoords(viewW: Int, viewH: Int, imageW: Int, imageH: Int): RectF {
val holeView = holeRectF(viewW, viewH)
return viewRectToInputImageRectFillCenter(viewW, viewH, imageW, imageH, holeView)
}
/**
* Rounded scan window corner radius — same formula as iOS `layoutScanOverlayGeometry` (`holeR`).
* [sidePx] is the scan square side in pixels.
*/
fun pairingIosStyleHoleCornerRadiusPx(sidePx: Float, density: Float): Float { fun pairingIosStyleHoleCornerRadiusPx(sidePx: Float, density: Float): Float {
val d = density val d = density
var holeR = min(28f * d, max(10f * d, sidePx * 0.056f)) var holeR = min(28f * d, max(10f * d, sidePx * 0.056f))
@@ -77,7 +34,6 @@ object PairingQrScanGeometry {
return max(holeR, 1f) return max(holeR, 1f)
} }
/** Area(roi ∩ box) / area(box); 0 if disjoint. */
fun barcodeBoxOverlapFraction(roi: RectF, box: Rect): Float { fun barcodeBoxOverlapFraction(roi: RectF, box: Rect): Float {
val bf = RectF(box) val bf = RectF(box)
val inter = RectF(roi) val inter = RectF(roi)
@@ -87,40 +43,6 @@ object PairingQrScanGeometry {
return if (boxArea <= 0f) 0f else interArea / boxArea return if (boxArea <= 0f) 0f else interArea / boxArea
} }
/**
* Accept only codes whose bounding box overlaps the on-screen pairing square by at least
* [minOverlapFraction] when that square is mapped into image space (preview FILL_CENTER model).
*/
fun barcodeMostlyInsidePairingHole(
viewW: Int,
viewH: Int,
imageW: Int,
imageH: Int,
barcode: Barcode,
minOverlapFraction: Float = 0.82f
): Boolean {
val box = barcode.boundingBox ?: return true
if (viewW <= 0 || viewH <= 0 || imageW <= 0 || imageH <= 0) {
return barcodeCenterInPairingHole(imageW, imageH, barcode)
}
val roi = pairingHoleInImageCoords(viewW, viewH, imageW, imageH)
val inset = 0.02f * min(imageW, imageH)
roi.inset(inset, inset)
if (roi.width() <= 0f || roi.height() <= 0f) {
return barcodeCenterInPairingHole(imageW, imageH, barcode)
}
return barcodeBoxOverlapFraction(roi, box) >= minOverlapFraction
}
/**
* Pairing send: accept only if the QR lies fully inside the on-screen square.
* [roiInImageSpace] is [holeRectF] in [PreviewView] coords mapped into the same space as ML Kit
* geometry ([CoordinateTransform] in [CameraActivity]).
*
* When [Barcode.getCornerPoints] is present (typical for QR), all corners must lie inside the ROI —
* tighter than [BoundingBox], which is often padded.
* Otherwise falls back to bbox center inside ROI plus [minOverlapFraction] of bbox area inside ROI.
*/
fun barcodeMatchesPairingHole( fun barcodeMatchesPairingHole(
roiInImageSpace: RectF, roiInImageSpace: RectF,
imageW: Int, imageW: Int,
@@ -161,14 +83,8 @@ object PairingQrScanGeometry {
return barcodeBoxOverlapFraction(roi, box) >= minOverlapFraction return barcodeBoxOverlapFraction(roi, box) >= minOverlapFraction
} }
/** Bbox-only fallback when corner points are missing (unusual for QR). */
private const val PAIRING_SEND_MIN_OVERLAP_BBOX_FALLBACK = 0.72f private const val PAIRING_SEND_MIN_OVERLAP_BBOX_FALLBACK = 0.72f
/**
* Native pairing scan hole — same rules as iOS `layoutScanOverlayGeometry` in
* `iosPairingQrOverlayWindow.mm` (0.72 × min side, header / bottom band clamps).
* [headerBottomPx] is the bottom edge of the chrome row in this views coordinate system.
*/
fun pairingIosStyleHoleRectF( fun pairingIosStyleHoleRectF(
viewW: Int, viewW: Int,
viewH: Int, viewH: Int,
@@ -201,9 +117,6 @@ object PairingQrScanGeometry {
return RectF(sqX, sqY, sqX + sqSz, sqY + sqSz) return RectF(sqX, sqY, sqX + sqSz, sqY + sqSz)
} }
/**
* Vertical center of the torch control in px (same math as iOS `torchCenterYConstraint` update).
*/
fun pairingIosStyleTorchCenterYPx( fun pairingIosStyleTorchCenterYPx(
holeBottomPx: Float, holeBottomPx: Float,
bandBottomPx: Float, bandBottomPx: Float,
@@ -224,7 +137,6 @@ object PairingQrScanGeometry {
return max(torchCy, hdr) return max(torchCy, hdr)
} }
/** [pairingIosStyleHoleRectF] mapped with the legacy FILL_CENTER preview model (transform fallback). */
fun pairingIosStyleHoleInImageCoords( fun pairingIosStyleHoleInImageCoords(
viewW: Int, viewW: Int,
viewH: Int, viewH: Int,
@@ -2,7 +2,6 @@
#include <QJsonDocument> #include <QJsonDocument>
#include <QSysInfo> #include <QSysInfo>
#include "core/controllers/gatewayController.h"
#include "core/repositories/secureAppSettingsRepository.h" #include "core/repositories/secureAppSettingsRepository.h"
#include "core/utils/api/apiUtils.h" #include "core/utils/api/apiUtils.h"
#include "core/utils/constants/apiConstants.h" #include "core/utils/constants/apiConstants.h"
@@ -13,8 +12,6 @@ using namespace amnezia;
namespace namespace
{ {
constexpr auto kGenerateQrEndpoint = "%1api/v1/generate_qr";
constexpr auto kScanQrEndpoint = "%1api/v1/scan_qr";
constexpr qsizetype kPairingMaxQrUuidChars = 128; constexpr qsizetype kPairingMaxQrUuidChars = 128;
constexpr qsizetype kPairingMaxVpnConfigChars = 256 * 1024; constexpr qsizetype kPairingMaxVpnConfigChars = 256 * 1024;
constexpr qsizetype kPairingMaxApiKeyChars = 8192; constexpr qsizetype kPairingMaxApiKeyChars = 8192;
@@ -186,50 +183,3 @@ QJsonObject PairingController::buildScanQrPayload(const QString &qrUuid, const Q
o[apiDefs::key::osVersion] = QSysInfo::productType(); o[apiDefs::key::osVersion] = QSysInfo::productType();
return o; return o;
} }
ErrorCode PairingController::startPairing(const QString &qrUuid, QrPairingConfigPayload &outPayload)
{
outPayload = QrPairingConfigPayload {};
if (qrUuid.isEmpty()) {
return ErrorCode::ApiConfigEmptyError;
}
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(), m_appSettingsRepository->isDevGatewayEnv(),
pairingLongPollTimeoutMsecs(), m_appSettingsRepository->isStrictKillSwitchEnabled());
QByteArray responseBody;
const ErrorCode transportError = gatewayController.post(QString::fromLatin1(kGenerateQrEndpoint), buildGenerateQrPayload(qrUuid), responseBody);
if (transportError != ErrorCode::NoError) {
return transportError;
}
const QJsonObject obj = QJsonDocument::fromJson(responseBody).object();
return interpretGenerateQrJson(obj, outPayload);
}
ErrorCode PairingController::completePairing(const QString &qrUuid, const QString &vpnConfig, const QJsonObject &serviceInfo,
const QJsonArray &supportedProtocols, const QString &apiKey)
{
if (qrUuid.isEmpty() || vpnConfig.isEmpty() || apiKey.isEmpty()) {
return ErrorCode::ApiConfigEmptyError;
}
const ErrorCode fieldErr = validatePairingScanFields(qrUuid, vpnConfig, apiKey);
if (fieldErr != ErrorCode::NoError) {
return fieldErr;
}
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(), m_appSettingsRepository->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs, m_appSettingsRepository->isStrictKillSwitchEnabled());
QByteArray responseBody;
const ErrorCode transportError =
gatewayController.post(QString::fromLatin1(kScanQrEndpoint),
buildScanQrPayload(qrUuid, vpnConfig, serviceInfo, supportedProtocols, apiKey), responseBody);
if (transportError != ErrorCode::NoError) {
return transportError;
}
const QJsonObject obj = QJsonDocument::fromJson(responseBody).object();
return interpretScanQrJson(obj);
}
@@ -37,10 +37,6 @@ public:
/** Length bounds before `scan_qr` (avoids huge JSON / abuse). */ /** Length bounds before `scan_qr` (avoids huge JSON / abuse). */
static amnezia::ErrorCode validatePairingScanFields(const QString &qrUuid, const QString &vpnConfig, const QString &apiKey); static amnezia::ErrorCode validatePairingScanFields(const QString &qrUuid, const QString &vpnConfig, const QString &apiKey);
amnezia::ErrorCode startPairing(const QString &qrUuid, QrPairingConfigPayload &outPayload);
amnezia::ErrorCode completePairing(const QString &qrUuid, const QString &vpnConfig, const QJsonObject &serviceInfo,
const QJsonArray &supportedProtocols, const QString &apiKey);
private: private:
SecureAppSettingsRepository *m_appSettingsRepository; SecureAppSettingsRepository *m_appSettingsRepository;
}; };
@@ -55,7 +55,6 @@ int pairingRetryDelayMs(int zeroBasedAttempt)
return baseMs * (1 << zeroBasedAttempt); return baseMs * (1 << zeroBasedAttempt);
} }
/** Legacy TV QR: generateQrCodeImageSeries base64url-wrapped QDataStream chunk (see qrCodeUtils.cpp). */
bool tryDecodeLegacyChunkedPairingQrPayload(const QString &t, QString *outUuid) bool tryDecodeLegacyChunkedPairingQrPayload(const QString &t, QString *outUuid)
{ {
static const QRegularExpression binUrlSafe(QStringLiteral("^[A-Za-z0-9_-]+$")); static const QRegularExpression binUrlSafe(QStringLiteral("^[A-Za-z0-9_-]+$"));
@@ -99,10 +98,6 @@ bool tryDecodeLegacyChunkedPairingQrPayload(const QString &t, QString *outUuid)
return true; return true;
} }
/**
* Extract a pairing session UUID from raw QR text without touching QObject / signals.
* Safe from CameraX / JNI threads while AmneziaActivity is stopped (Qt event loop may not run).
*/
QString extractPairingSessionUuidFromScanText(const QString &raw) QString extractPairingSessionUuidFromScanText(const QString &raw)
{ {
const QString t = raw.trimmed(); const QString t = raw.trimmed();
@@ -24,7 +24,6 @@ class PairingUiController : public QObject
Q_PROPERTY(QString tvSessionUuid READ tvSessionUuid NOTIFY tvSessionUuidChanged) Q_PROPERTY(QString tvSessionUuid READ tvSessionUuid NOTIFY tvSessionUuidChanged)
Q_PROPERTY(bool tvPairingBusy READ tvPairingBusy NOTIFY tvPairingBusyChanged) Q_PROPERTY(bool tvPairingBusy READ tvPairingBusy NOTIFY tvPairingBusyChanged)
Q_PROPERTY(QString tvStatusMessage READ tvStatusMessage NOTIFY tvStatusMessageChanged) Q_PROPERTY(QString tvStatusMessage READ tvStatusMessage NOTIFY tvStatusMessageChanged)
/** Long-poll window for generate_qr (seconds), for receive UI countdown. */
Q_PROPERTY(int tvPairingWaitWindowSeconds READ tvPairingWaitWindowSeconds NOTIFY tvQrCodesChanged) Q_PROPERTY(int tvPairingWaitWindowSeconds READ tvPairingWaitWindowSeconds NOTIFY tvQrCodesChanged)
Q_PROPERTY(bool phonePairingBusy READ phonePairingBusy NOTIFY phonePairingBusyChanged) Q_PROPERTY(bool phonePairingBusy READ phonePairingBusy NOTIFY phonePairingBusyChanged)
@@ -33,19 +32,11 @@ class PairingUiController : public QObject
pendingPhonePairingUuidChanged) pendingPhonePairingUuidChanged)
Q_PROPERTY(QString lastSuccessfulPhonePairingDisplayName READ lastSuccessfulPhonePairingDisplayName NOTIFY Q_PROPERTY(QString lastSuccessfulPhonePairingDisplayName READ lastSuccessfulPhonePairingDisplayName NOTIFY
lastSuccessfulPhonePairingDisplayNameChanged) lastSuccessfulPhonePairingDisplayNameChanged)
/** TV flow for QA: 0=idle, 1=waitingForPeer, 2=error, 3=sessionExpired */
Q_PROPERTY(int tvPairingUiPhase READ tvPairingUiPhase NOTIFY tvPairingUiPhaseChanged) Q_PROPERTY(int tvPairingUiPhase READ tvPairingUiPhase NOTIFY tvPairingUiPhaseChanged)
/** Full-screen pairing QR camera under QML (mobile); drives translucent main window. */
Q_PROPERTY(bool embeddedPairingQrCameraActive READ embeddedPairingQrCameraActive WRITE setEmbeddedPairingQrCameraActive NOTIFY Q_PROPERTY(bool embeddedPairingQrCameraActive READ embeddedPairingQrCameraActive WRITE setEmbeddedPairingQrCameraActive NOTIFY
embeddedPairingQrCameraActiveChanged) embeddedPairingQrCameraActiveChanged)
/** True only on iOS builds: use native UIWindow QR overlay (not Qt.platform.os, which can differ). */
Q_PROPERTY(bool iosNativePairingQrOverlayBuild READ iosNativePairingQrOverlayBuild CONSTANT) Q_PROPERTY(bool iosNativePairingQrOverlayBuild READ iosNativePairingQrOverlayBuild CONSTANT)
/** True only on Android builds: full-screen CameraActivity pairing scanner; QML hides duplicate scan chrome. */
Q_PROPERTY(bool androidNativePairingQrOverlayBuild READ androidNativePairingQrOverlayBuild CONSTANT) Q_PROPERTY(bool androidNativePairingQrOverlayBuild READ androidNativePairingQrOverlayBuild CONSTANT)
/**
* Epoch ms until which QML should not call openPairingQrScanner again (after native CameraActivity closes).
* Android pairing flow only; always 0 on other platforms.
*/
Q_PROPERTY(qint64 androidPairingReaderCooldownUntilEpochMs READ androidPairingReaderCooldownUntilEpochMs NOTIFY Q_PROPERTY(qint64 androidPairingReaderCooldownUntilEpochMs READ androidPairingReaderCooldownUntilEpochMs NOTIFY
androidPairingReaderCooldownUntilEpochMsChanged) androidPairingReaderCooldownUntilEpochMsChanged)
@@ -72,23 +63,12 @@ public:
bool iosNativePairingQrOverlayBuild() const; bool iosNativePairingQrOverlayBuild() const;
bool androidNativePairingQrOverlayBuild() const; bool androidNativePairingQrOverlayBuild() const;
Q_INVOKABLE void setEmbeddedPairingQrCameraActive(bool active); Q_INVOKABLE void setEmbeddedPairingQrCameraActive(bool active);
/** iOS: native dim strip height uses safe bottom + extraPt (see PageSettingsApiQrPairingSend scanDimBleedBottom). No-op elsewhere. */
Q_INVOKABLE void syncIosEmbeddedPairingQrNativeBottomExtra(int extraPt); Q_INVOKABLE void syncIosEmbeddedPairingQrNativeBottomExtra(int extraPt);
/**
* iOS: reapply UIView transparency + safe-area dim strips when embedded pairing is already active.
* Needed after multitask resume: setEmbeddedPairingQrCameraActive(true) is a no-op if the flag stayed true,
* but QUIMetalView / hierarchy may have been rebuilt opaque so the camera only shows in the status bar band.
*/
Q_INVOKABLE void refreshIosEmbeddedPairingQrChrome(); Q_INVOKABLE void refreshIosEmbeddedPairingQrChrome();
qint64 androidPairingReaderCooldownUntilEpochMs() const { return m_androidPairingReaderCooldownUntilEpochMs; } qint64 androidPairingReaderCooldownUntilEpochMs() const { return m_androidPairingReaderCooldownUntilEpochMs; }
/** Lengthens androidPairingReaderCooldownUntilEpochMs to at least now + ms (Android pairing; no-op elsewhere). */
Q_INVOKABLE void suppressAndroidNativePairingReaderStarts(int ms); Q_INVOKABLE void suppressAndroidNativePairingReaderStarts(int ms);
/**
* iOS: UIKit UIWindow QR scanner (see iosPairingQrOverlayWindow). Pass translated title/subtitle for native chrome.
* No-op on other platforms.
*/
Q_INVOKABLE void presentIosPairingQrNativeOverlayScanner(const QString &title = QString(), Q_INVOKABLE void presentIosPairingQrNativeOverlayScanner(const QString &title = QString(),
const QString &subtitle = QString()); const QString &subtitle = QString());
Q_INVOKABLE void dismissIosPairingQrNativeOverlayScanner(); Q_INVOKABLE void dismissIosPairingQrNativeOverlayScanner();
@@ -96,36 +76,25 @@ public:
#if defined(Q_OS_ANDROID) #if defined(Q_OS_ANDROID)
static bool tryConsumeAndroidQrScan(const QString &code); static bool tryConsumeAndroidQrScan(const QString &code);
/** JNI from CameraActivity onDestroy: avoid reopening native reader while camera HAL is still releasing. */
static void notifyAndroidPairingQrCameraClosed(); static void notifyAndroidPairingQrCameraClosed();
/** JNI before CameraActivity finish when user pressed back — Qt should reopen native scan (QML shell has no preview). */
static void notifyAndroidPairingQrCameraUserDismissed(); static void notifyAndroidPairingQrCameraUserDismissed();
#endif #endif
public slots: public slots:
/** Fast preflight before opening receive QR page; emits errorOccurred on failure. */
bool canOpenTvQrPairingPage(); bool canOpenTvQrPairingPage();
void startTvQrSession(); void startTvQrSession();
void cancelTvQrSession(); void cancelTvQrSession();
/** TV receive + phone send: call when leaving QR pairing (back / pop) so long-poll state does not stick. */
void cancelAllPairingActivity(); void cancelAllPairingActivity();
/** Sends the current premium/free API config from \a serverIndex to the gateway for the given \a qrUuid. */
void submitPhonePairing(const QString &qrUuid, int serverIndex); void submitPhonePairing(const QString &qrUuid, int serverIndex);
/** Android: system camera activity. iOS: toggle camera from QML. */
void openPairingQrScanner(); void openPairingQrScanner();
/** Mobile: whether the app may use the camera for QR pairing (OS permission). Desktop: true. */
Q_INVOKABLE bool isPairingCameraAccessGranted() const; Q_INVOKABLE bool isPairingCameraAccessGranted() const;
/** Mobile: show rationale / system camera permission UI; emits pairingCameraAccessFinished. Desktop: emits granted. */
Q_INVOKABLE void requestPairingCameraAccess(); Q_INVOKABLE void requestPairingCameraAccess();
/** Open system settings for this app (camera can be enabled there). No-op on desktop. */
Q_INVOKABLE void openPairingCameraAppSettings(); Q_INVOKABLE void openPairingCameraAppSettings();
/** Android: torch for embedded pairing camera. No-op elsewhere. */
Q_INVOKABLE void setPairingQrTorchEnabled(bool enabled); Q_INVOKABLE void setPairingQrTorchEnabled(bool enabled);
/** If \a raw contains a session UUID (not vpn://), emits pairingUuidFromScan and returns true. */
bool applyScannedTextAsPairingUuid(const QString &raw); bool applyScannedTextAsPairingUuid(const QString &raw);
Q_INVOKABLE void clearPendingPhonePairingUuid(); Q_INVOKABLE void clearPendingPhonePairingUuid();
@@ -146,15 +115,11 @@ signals:
void pairingUuidFromScan(const QString &uuid); void pairingUuidFromScan(const QString &uuid);
void tvPairingUiPhaseChanged(); void tvPairingUiPhaseChanged();
/** After requestPairingCameraAccess(): true if OS granted camera access. */
void pairingCameraAccessFinished(bool granted); void pairingCameraAccessFinished(bool granted);
void embeddedPairingQrCameraActiveChanged(); void embeddedPairingQrCameraActiveChanged();
void androidPairingReaderCooldownUntilEpochMsChanged(); void androidPairingReaderCooldownUntilEpochMsChanged();
/** iOS native overlay scanner: payload was not a pairing session UUID (toast in QML). */
void pairingSendQrScanRejectedInvalidPayload(); void pairingSendQrScanRejectedInvalidPayload();
/** Native overlay back chevron tapped — dismiss scanner and close page from QML. */
void pairingIosNativeQrOverlayBackRequested(); void pairingIosNativeQrOverlayBackRequested();
/** Android CameraActivity: user pressed back — QML should exit pairing send (e.g. closePage), not reopen camera. */
void pairingAndroidNativeQrScannerUserDismissed(); void pairingAndroidNativeQrScannerUserDismissed();
private: private:
@@ -16,31 +16,24 @@ import "../Components"
PageType { PageType {
id: root id: root
/** Loud dim colors when true (red/blue/cyan/orange regions). Sync with PageStart.pairingQrChromeDebug. */
property bool pairingQrChromeDebug: false property bool pairingQrChromeDebug: false
/** iOS (and any non-Android mobile): native QRCodeReader; Qt may not always report os === "ios". */
readonly property bool useIosStyleNativeQrReader: GC.isMobile() && Qt.platform.os !== "android" readonly property bool useIosStyleNativeQrReader: GC.isMobile() && Qt.platform.os !== "android"
/** iOS-only: full-screen UIKit UIWindow scanner (PairingUiController.iosNativePairingQrOverlayBuild — not Qt.platform.os). */
readonly property bool useIosNativePairingQrOverlay: PairingUiController.iosNativePairingQrOverlayBuild readonly property bool useIosNativePairingQrOverlay: PairingUiController.iosNativePairingQrOverlayBuild
/** Android: full-screen CameraActivity — Qt cannot reliably composite CameraX under QML on some OEMs (e.g. Samsung). */
readonly property bool useAndroidNativePairingQrScanner: GC.isMobile() && Qt.platform.os === "android" readonly property bool useAndroidNativePairingQrScanner: GC.isMobile() && Qt.platform.os === "android"
/** Android: iOS-like flow — titles and camera preview only in CameraActivity; QML hides duplicate scan chrome. */
readonly property bool useAndroidNativePairingQrOverlay: PairingUiController.androidNativePairingQrOverlayBuild readonly property bool useAndroidNativePairingQrOverlay: PairingUiController.androidNativePairingQrOverlayBuild
&& GC.isMobile() && GC.isMobile()
&& Qt.platform.os === "android" && Qt.platform.os === "android"
/** Let dimming draw into window chrome (status bar + tab bar) when camera underlay is active. */
readonly property bool extendScanDimToScreenEdges: GC.isMobile() && pairingWizardStep === 0 readonly property bool extendScanDimToScreenEdges: GC.isMobile() && pairingWizardStep === 0
&& PairingUiController.embeddedPairingQrCameraActive && PairingUiController.embeddedPairingQrCameraActive
&& !root.useAndroidNativePairingQrOverlay && !root.useAndroidNativePairingQrOverlay
clip: !extendScanDimToScreenEdges clip: !extendScanDimToScreenEdges
/** QQuickWindow (not Item); do not type as Item breaks binding on Qt 6. */ /** QQuickWindow as var — typing as Item breaks bindings on Qt 6. */
readonly property var appWindow: Window.window readonly property var appWindow: Window.window
/** Pixels of window above this page (status bar / safe area gap). */
readonly property real scanDimBleedTop: { readonly property real scanDimBleedTop: {
if (!extendScanDimToScreenEdges || !appWindow || !appWindow.contentItem) if (!extendScanDimToScreenEdges || !appWindow || !appWindow.contentItem)
return 0 return 0
@@ -49,7 +42,6 @@ PageType {
bleed = Math.max(bleed, PageController.safeAreaTopMargin) bleed = Math.max(bleed, PageController.safeAreaTopMargin)
return bleed return bleed
} }
/** Pixels of window below this page (tab bar + home indicator). */
readonly property real scanDimBleedBottom: { readonly property real scanDimBleedBottom: {
if (!extendScanDimToScreenEdges || !appWindow || !appWindow.contentItem) if (!extendScanDimToScreenEdges || !appWindow || !appWindow.contentItem)
return 0 return 0
@@ -63,15 +55,9 @@ PageType {
return bleed return bleed
} }
/**
* Bottom bleed for dimLayer only. On iOS embedded native QR, keep dim inside the page — semi-opaque dim
* extended into the tab stack seam composites badly with opaque tab chrome (persistent hairline).
* Native bottom mask still uses scanDimBleedBottom via pushIosNativeBottomBleedSync().
*/
readonly property real scanDimBleedBottomForDimLayer: (root.useIosStyleNativeQrReader readonly property real scanDimBleedBottomForDimLayer: (root.useIosStyleNativeQrReader
&& PairingUiController.embeddedPairingQrCameraActive) ? 0 : scanDimBleedBottom && PairingUiController.embeddedPairingQrCameraActive) ? 0 : scanDimBleedBottom
/** iOS: extend UIKit bottom dim under QML tab bar (see iosPairingCameraAccess + PairingUiController). */
function pushIosNativeBottomBleedSync() { function pushIosNativeBottomBleedSync() {
if (!root.useIosStyleNativeQrReader || !PairingUiController.embeddedPairingQrCameraActive) { if (!root.useIosStyleNativeQrReader || !PairingUiController.embeddedPairingQrCameraActive) {
return return
@@ -85,61 +71,16 @@ PageType {
} }
} }
Timer {
id: pairingScanLayoutLogTimer
interval: 50
repeat: false
onTriggered: root.logPairingScanLayout("timer")
}
Connections { Connections {
target: PairingUiController target: PairingUiController
function onEmbeddedPairingQrCameraActiveChanged() { function onEmbeddedPairingQrCameraActiveChanged() {
if (PairingUiController.embeddedPairingQrCameraActive) { if (PairingUiController.embeddedPairingQrCameraActive) {
pairingScanLayoutLogTimer.restart()
Qt.callLater(root.pushIosNativeBottomBleedSync) Qt.callLater(root.pushIosNativeBottomBleedSync)
} }
} }
} }
function logPairingScanLayout(tag) {
const w = Window.window
const ci = w && w.contentItem ? w.contentItem : null
let m00 = null
let m0h = null
let scanBot = null
let dimTL = null
let dimBR = null
let dimHoleB = null
if (ci) {
m00 = root.mapToItem(ci, 0, 0)
m0h = root.mapToItem(ci, 0, root.height)
scanBot = scanStep.mapToItem(ci, 0, scanStep.height)
dimTL = dimLayer.mapToItem(ci, 0, 0)
dimBR = dimLayer.mapToItem(ci, dimLayer.width, dimLayer.height)
dimHoleB = dimLayer.holeBottom
}
console.warn("[PairingQrLayout]", tag,
"extend=", extendScanDimToScreenEdges,
"clip=", clip,
"root=", root.width, "x", root.height,
"win=", w ? w.width : -1, "x", w ? w.height : -1,
"contentItem=", ci ? ci.width : -1, "x", ci ? ci.height : -1,
"bleedT/B=", scanDimBleedTop, scanDimBleedBottom,
"dimLayerBleedB=", root.scanDimBleedBottomForDimLayer,
"safeT/B=", PageController.safeAreaTopMargin, PageController.safeAreaBottomMargin,
"map00=", m00 ? m00.x + "," + m00.y : "n/a",
"map0h=", m0h ? m0h.x + "," + m0h.y : "n/a",
"ci.scanStepBot=", scanBot ? scanBot.x.toFixed(1) + "," + scanBot.y.toFixed(1) : "n/a",
"ci.dimTL/BR=", dimTL ? dimTL.x.toFixed(1) + "," + dimTL.y.toFixed(1) : "n/a",
dimBR ? dimBR.x.toFixed(1) + "," + dimBR.y.toFixed(1) : "n/a",
"dimHoleB=", dimHoleB !== null ? dimHoleB.toFixed(1) : "n/a",
"win.screen=", w && w.screen ? w.screen.width + "x" + w.screen.height : "n/a",
"dimLayer wh=", dimLayer.width, "x", dimLayer.height)
}
/** 0 = scan QR, 1 = confirm before sending subscription */
property int pairingWizardStep: 0 property int pairingWizardStep: 0
property bool keepPhonePairingInBackgroundOnClose: false property bool keepPhonePairingInBackgroundOnClose: false
@@ -188,11 +129,6 @@ PageType {
if (addDeviceConfirmNavigationScheduled) { if (addDeviceConfirmNavigationScheduled) {
return return
} }
console.warn("[PairingQrSend] startMobileScanner Qt.platform.os=", Qt.platform.os,
"iosNativePairingQrOverlayBuild=", PairingUiController.iosNativePairingQrOverlayBuild,
"useIosNativePairingQrOverlay=", root.useIosNativePairingQrOverlay,
"androidNativePairingQrOverlayBuild=", PairingUiController.androidNativePairingQrOverlayBuild,
"useAndroidNativePairingQrOverlay=", root.useAndroidNativePairingQrOverlay)
if (!PairingUiController.isPairingCameraAccessGranted()) { if (!PairingUiController.isPairingCameraAccessGranted()) {
awaitingCameraPermissionForScan = true awaitingCameraPermissionForScan = true
PairingUiController.requestPairingCameraAccess() PairingUiController.requestPairingCameraAccess()
@@ -208,14 +144,10 @@ PageType {
if (root.useAndroidNativePairingQrScanner) { if (root.useAndroidNativePairingQrScanner) {
const coolUntil = PairingUiController.androidPairingReaderCooldownUntilEpochMs const coolUntil = PairingUiController.androidPairingReaderCooldownUntilEpochMs
if (Date.now() < coolUntil) { if (Date.now() < coolUntil) {
console.warn("[PairingQrSend] startMobileScanner: skip (native camera cooldown), ms left=",
(coolUntil - Date.now()))
return return
} }
const now = Date.now() const now = Date.now()
if (now - _androidPairingReaderLastStartMs < 700) { if (now - _androidPairingReaderLastStartMs < 700) {
console.warn("[PairingQrSend] startMobileScanner: skip duplicate Android CameraActivity within",
(now - _androidPairingReaderLastStartMs), "ms")
return return
} }
_androidPairingReaderLastStartMs = now _androidPairingReaderLastStartMs = now
@@ -314,14 +246,8 @@ PageType {
} }
} }
/**
* StackView often instantiates the page already visible — onVisibleChanged(true) may never run, so
* startMobileScanner (and native overlay present) would be skipped; only stop/dismiss runs. Same pattern as
* PageSetupWizardApiQrPairingReceive (Component.onCompleted + visible).
*/
Component.onCompleted: { Component.onCompleted: {
if (GC.isMobile() && root.visible && pairingWizardStep === 0) { if (GC.isMobile() && root.visible && pairingWizardStep === 0) {
console.warn("[PairingQrSend] Component.onCompleted: schedule startMobileScanner (page created visible)")
Qt.callLater(startMobileScanner) Qt.callLater(startMobileScanner)
} }
} }
@@ -347,29 +273,21 @@ PageType {
}) })
return return
} }
/**
* No fixed ms delay: f1 reapply UIView transparency; f2 restart AVCapture; f3 refresh again —
* after restartPairingIosCamera, QUIMetalView / render thread often rebuilds opaque layers (same bug
* as status-bar-only camera) until underlay is reapplied once more.
*/
Qt.callLater(function () { Qt.callLater(function () {
if (!root.visible || root.pairingWizardStep !== 0 || !GC.isMobile()) { if (!root.visible || root.pairingWizardStep !== 0 || !GC.isMobile()) {
return return
} }
console.warn("[PairingQrResume] ApplicationActive f1 underlay")
PairingUiController.embeddedPairingQrCameraActive = true PairingUiController.embeddedPairingQrCameraActive = true
PairingUiController.refreshIosEmbeddedPairingQrChrome() PairingUiController.refreshIosEmbeddedPairingQrChrome()
Qt.callLater(function () { Qt.callLater(function () {
if (!root.visible || root.pairingWizardStep !== 0) { if (!root.visible || root.pairingWizardStep !== 0) {
return return
} }
console.warn("[PairingQrResume] ApplicationActive f2 restart camera")
root.restartPairingIosCamera() root.restartPairingIosCamera()
Qt.callLater(function () { Qt.callLater(function () {
if (!root.visible || root.pairingWizardStep !== 0) { if (!root.visible || root.pairingWizardStep !== 0) {
return return
} }
console.warn("[PairingQrResume] ApplicationActive f3 underlay post-camera")
PairingUiController.refreshIosEmbeddedPairingQrChrome() PairingUiController.refreshIosEmbeddedPairingQrChrome()
}) })
}) })
-74
View File
@@ -19,87 +19,15 @@ PageType {
property bool isControlsDisabled: false property bool isControlsDisabled: false
property bool isTabBarDisabled: false property bool isTabBarDisabled: false
/** Loud colors (tab bar base green, extra overlap) when true — pair with PageSettingsApiQrPairingSend.pairingQrChromeDebug. */
property bool pairingQrChromeDebug: false property bool pairingQrChromeDebug: false
/** Opaque extension of tab bar background upward (iOS embedded QR); see PairingTabChrome deltaY vs stack bottom. */
readonly property int tabBarChromeOverlapUp: (PairingUiController.embeddedPairingQrCameraActive && GC.isMobile() readonly property int tabBarChromeOverlapUp: (PairingUiController.embeddedPairingQrCameraActive && GC.isMobile()
&& Qt.platform.os !== "android") && Qt.platform.os !== "android")
? (root.pairingQrChromeDebug ? 24 : 18) : 0 ? (root.pairingQrChromeDebug ? 24 : 18) : 0
/** Pull stack under tab chrome so TabBar.background overlap fully covers stack bottom pixels. */
readonly property int tabStackPairingUnderlapDown: (PairingUiController.embeddedPairingQrCameraActive && GC.isMobile() readonly property int tabStackPairingUnderlapDown: (PairingUiController.embeddedPairingQrCameraActive && GC.isMobile()
&& Qt.platform.os !== "android") ? 8 : 0 && Qt.platform.os !== "android") ? 8 : 0
readonly property bool pairingTabChromeLogActive: PairingUiController.embeddedPairingQrCameraActive && GC.isMobile()
&& Qt.platform.os !== "android"
function logPairingTabChromeLayout(tag) {
if (!root.pairingTabChromeLogActive) {
return
}
const w = Window.window
const ci = w && w.contentItem ? w.contentItem : null
let msg = "[PairingTabChrome] " + tag
msg += " PageStart=" + Math.round(root.width) + "x" + Math.round(root.height)
msg += " tabBar=" + Math.round(tabBar.width) + "x" + Math.round(tabBar.height) + " y=" + tabBar.y.toFixed(2)
msg += " imeBM=" + PageController.imeHeight
msg += " stack=" + Math.round(tabBarStackView.width) + "x" + Math.round(tabBarStackView.height)
msg += " overlapUp=" + tabBarChromeOverlapUp + " stackUnderlap=" + tabStackPairingUnderlapDown
msg += " tabBgRootH=" + (tabBarBackgroundRoot ? tabBarBackgroundRoot.height.toFixed(2) : "n/a")
if (ci) {
const tabOrigin = tabBar.mapToItem(ci, 0, 0)
const tabBandTop = tabBar.mapToItem(ci, 0, -tabBarChromeOverlapUp)
const stackOrigin = tabBarStackView.mapToItem(ci, 0, 0)
const stackBottomMid = tabBarStackView.mapToItem(ci, tabBarStackView.width * 0.5, tabBarStackView.height)
const bgTopLeft = tabBarBackgroundRoot.mapToItem(ci, 0, 0)
msg += " ci.tab(0,0)=" + tabOrigin.x.toFixed(1) + "," + tabOrigin.y.toFixed(1)
msg += " ci.tab(0,-ov)=" + tabBandTop.x.toFixed(1) + "," + tabBandTop.y.toFixed(1)
msg += " ci.stack(0,0)=" + stackOrigin.x.toFixed(1) + "," + stackOrigin.y.toFixed(1)
msg += " ci.stackMidBot=" + stackBottomMid.x.toFixed(1) + "," + stackBottomMid.y.toFixed(1)
msg += " ci.tabBgRoot(0,0)=" + bgTopLeft.x.toFixed(1) + "," + bgTopLeft.y.toFixed(1)
msg += " deltaY_tabBandTop_minus_stackMidBot=" + (tabBandTop.y - stackBottomMid.y).toFixed(2)
} else {
msg += " ci=missing"
}
console.warn(msg)
}
Timer {
id: pairingTabChromeLogTimer50
interval: 50
repeat: false
onTriggered: root.logPairingTabChromeLayout("t50")
}
Timer {
id: pairingTabChromeLogTimer350
interval: 350
repeat: false
onTriggered: root.logPairingTabChromeLayout("t350")
}
Connections {
target: PairingUiController
function onEmbeddedPairingQrCameraActiveChanged() {
if (PairingUiController.embeddedPairingQrCameraActive && GC.isMobile() && Qt.platform.os !== "android") {
pairingTabChromeLogTimer50.restart()
pairingTabChromeLogTimer350.restart()
}
}
}
onWidthChanged: {
if (root.pairingTabChromeLogActive) {
pairingTabChromeLogTimer50.restart()
}
}
onHeightChanged: {
if (root.pairingTabChromeLogActive) {
pairingTabChromeLogTimer50.restart()
}
}
Connections { Connections {
objectName: "pageControllerConnection" objectName: "pageControllerConnection"
@@ -413,12 +341,10 @@ PageType {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
height: parent.height + root.tabBarChromeOverlapUp height: parent.height + root.tabBarChromeOverlapUp
/** Opaque base: Shape alone can show the window-layer camera through anti-aliased edges when the window is clear (iOS QR pairing). */
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: root.pairingQrChromeDebug ? "#00ff66" : AmneziaStyle.color.onyxBlack color: root.pairingQrChromeDebug ? "#00ff66" : AmneziaStyle.color.onyxBlack
} }
/** Stroke around tab row; hidden during iOS embedded QR overlap — top horizontal slateGray reads as a hairline “strip” above tabs. */
Shape { Shape {
id: tabBarChromeShape id: tabBarChromeShape
objectName: "backgroundShape" objectName: "backgroundShape"