From bb56008c3d80fd049a9165d7e4615bfd83ea3cc4 Mon Sep 17 00:00:00 2001 From: dranik Date: Fri, 8 May 2026 16:57:35 +0300 Subject: [PATCH] add check access camera --- client/android/res/values/strings.xml | 4 + .../src/org/amnezia/vpn/AmneziaActivity.kt | 62 +++++++++++++ .../org/amnezia/vpn/qt/QtAndroidController.kt | 2 + client/cmake/ios.cmake | 1 + client/cmake/macos_ne.cmake | 1 + client/cmake/sources.cmake | 2 + .../platforms/android/android_controller.cpp | 27 +++++- client/platforms/android/android_controller.h | 5 ++ client/platforms/ios/iosPairingCameraAccess.h | 10 +++ .../platforms/ios/iosPairingCameraAccess.mm | 37 ++++++++ .../ios/iosPairingCameraAccess_stub.cpp | 13 +++ .../controllers/api/pairingUiController.cpp | 38 ++++++++ .../ui/controllers/api/pairingUiController.h | 9 ++ .../ui/qml/Pages2/PageSettingsApiDevices.qml | 86 ++++++++++++++++++- .../Pages2/PageSettingsApiQrPairingSend.qml | 85 ++++++++++++++++-- 15 files changed, 372 insertions(+), 10 deletions(-) create mode 100644 client/platforms/ios/iosPairingCameraAccess.h create mode 100644 client/platforms/ios/iosPairingCameraAccess.mm create mode 100644 client/platforms/ios/iosPairingCameraAccess_stub.cpp diff --git a/client/android/res/values/strings.xml b/client/android/res/values/strings.xml index bf8d76d1d..fcdc94dad 100644 --- a/client/android/res/values/strings.xml +++ b/client/android/res/values/strings.xml @@ -24,5 +24,9 @@ To show notifications, you must enable notifications in the system settings Open notification settings + Camera access + To scan a QR code for device pairing, Amnezia VPN needs access to the camera. + Continue + Please install a file management utility to browse files \ No newline at end of file diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index dca70ee5c..417702102 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -73,6 +73,7 @@ private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1 private const val CREATE_FILE_ACTION_CODE = 2 private const val OPEN_FILE_ACTION_CODE = 3 private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4 +private const val CHECK_CAMERA_PERMISSION_ACTION_CODE = 5 private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED" private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L @@ -880,6 +881,66 @@ class AmneziaActivity : QtActivity() { @SuppressLint("UnsupportedChromeOsCameraSystemFeature") fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA) + @Suppress("unused") + fun isCameraPermissionGranted(): Boolean = + ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED + + @Suppress("unused") + fun requestCameraPermissionForQrPairing() { + if (isCameraPermissionGranted()) { + mainScope.launch { + qtInitialized.await() + QtAndroidController.onCameraPermissionResult(true) + } + return + } + runOnUiThread { + AlertDialog.Builder(this) + .setTitle(R.string.cameraPermissionDialogTitle) + .setMessage(R.string.cameraPermissionDialogMessage) + .setNegativeButton(R.string.cancel) { _, _ -> + mainScope.launch { + qtInitialized.await() + QtAndroidController.onCameraPermissionResult(false) + } + } + .setPositiveButton(R.string.cameraPermissionContinue) { _, _ -> + requestPermission( + Manifest.permission.CAMERA, + CHECK_CAMERA_PERMISSION_ACTION_CODE, + PermissionRequestHandler( + onSuccess = { + mainScope.launch { + qtInitialized.await() + QtAndroidController.onCameraPermissionResult(true) + } + }, + onFail = { + mainScope.launch { + qtInitialized.await() + QtAndroidController.onCameraPermissionResult(false) + } + }, + onAny = {} + ) + ) + } + .show() + } + } + + @Suppress("unused") + fun openApplicationDetailsSettings() { + try { + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", packageName, null) + startActivity(this) + } + } catch (e: ActivityNotFoundException) { + Log.e(TAG, "openApplicationDetailsSettings: $e") + } + } + @Suppress("unused") fun isOnTv(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) @@ -1179,6 +1240,7 @@ class AmneziaActivity : QtActivity() { CREATE_FILE_ACTION_CODE -> "CREATE_FILE" OPEN_FILE_ACTION_CODE -> "OPEN_FILE" CHECK_NOTIFICATION_PERMISSION_ACTION_CODE -> "CHECK_NOTIFICATION_PERMISSION" + CHECK_CAMERA_PERMISSION_ACTION_CODE -> "CHECK_CAMERA_PERMISSION" else -> actionCode.toString() } } diff --git a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt index ec143635d..73f69437b 100644 --- a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt +++ b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt @@ -34,4 +34,6 @@ object QtAndroidController { external fun onActivityPaused() external fun onActivityResumed() + + external fun onCameraPermissionResult(granted: Boolean) } \ No newline at end of file diff --git a/client/cmake/ios.cmake b/client/cmake/ios.cmake index 86df23d25..b07848721 100644 --- a/client/cmake/ios.cmake +++ b/client/cmake/ios.cmake @@ -44,6 +44,7 @@ set(SOURCES ${SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm + ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosPairingCameraAccess.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm diff --git a/client/cmake/macos_ne.cmake b/client/cmake/macos_ne.cmake index ac1796ad0..f9c22382f 100644 --- a/client/cmake/macos_ne.cmake +++ b/client/cmake/macos_ne.cmake @@ -49,6 +49,7 @@ set(SOURCES ${SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm + ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosPairingCameraAccess_stub.cpp ) set(ICON_FILE ${CMAKE_CURRENT_SOURCE_DIR}/images/app.icns) diff --git a/client/cmake/sources.cmake b/client/cmake/sources.cmake index 3ae725d63..8d904f6dd 100644 --- a/client/cmake/sources.cmake +++ b/client/cmake/sources.cmake @@ -65,6 +65,7 @@ set(HEADERS ${HEADERS} ${CLIENT_ROOT_DIR}/core/utils/utilities.h ${CLIENT_ROOT_DIR}/core/utils/managementServer.h ${CLIENT_ROOT_DIR}/core/utils/constants.h + ${CLIENT_ROOT_DIR}/platforms/ios/iosPairingCameraAccess.h ) # Mozilla headres @@ -155,6 +156,7 @@ set(SOURCES ${SOURCES} if(NOT IOS AND NOT MACOS_NE) set(SOURCES ${SOURCES} ${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.cpp + ${CLIENT_ROOT_DIR}/platforms/ios/iosPairingCameraAccess_stub.cpp ) endif() diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp index 5184754e5..4401a00b9 100644 --- a/client/platforms/android/android_controller.cpp +++ b/client/platforms/android/android_controller.cpp @@ -104,7 +104,8 @@ bool AndroidController::initialize() {"onImeInsetsChanged", "(I)V", reinterpret_cast(onImeInsetsChanged)}, {"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast(onSystemBarsInsetsChanged)}, {"onActivityPaused", "()V", reinterpret_cast(onActivityPaused)}, - {"onActivityResumed", "()V", reinterpret_cast(onActivityResumed)} + {"onActivityResumed", "()V", reinterpret_cast(onActivityResumed)}, + {"onCameraPermissionResult", "(Z)V", reinterpret_cast(onCameraPermissionResult)} }; QJniEnvironment env; @@ -202,6 +203,21 @@ bool AndroidController::isCameraPresent() return callActivityMethod("isCameraPresent", "()Z"); } +bool AndroidController::isCameraPermissionGranted() +{ + return callActivityMethod("isCameraPermissionGranted", "()Z"); +} + +void AndroidController::requestCameraPermissionForQrPairing() +{ + callActivityMethod("requestCameraPermissionForQrPairing", "()V"); +} + +void AndroidController::openApplicationDetailsSettings() +{ + callActivityMethod("openApplicationDetailsSettings", "()V"); +} + bool AndroidController::isOnTv() { return callActivityMethod("isOnTv", "()Z"); @@ -583,4 +599,13 @@ void AndroidController::onActivityResumed(JNIEnv *env, jobject thiz) emit AndroidController::instance()->activityResumed(); } +// static +void AndroidController::onCameraPermissionResult(JNIEnv *env, jobject thiz, jboolean granted) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + + emit AndroidController::instance()->cameraPermissionResult(static_cast(granted)); +} + diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h index 2294cc78b..1a2ddae9d 100644 --- a/client/platforms/android/android_controller.h +++ b/client/platforms/android/android_controller.h @@ -38,6 +38,9 @@ public: void closeFd(); QString getFileName(const QString &uri); bool isCameraPresent(); + bool isCameraPermissionGranted(); + void requestCameraPermissionForQrPairing(); + void openApplicationDetailsSettings(); bool isOnTv(); bool isEdgeToEdgeEnabled(); int getStatusBarHeight(); @@ -77,6 +80,7 @@ signals: void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp); void activityPaused(); void activityResumed(); + void cameraPermissionResult(bool granted); private: bool isWaitingStatus = true; @@ -109,6 +113,7 @@ private: static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp); static void onActivityPaused(JNIEnv *env, jobject thiz); static void onActivityResumed(JNIEnv *env, jobject thiz); + static void onCameraPermissionResult(JNIEnv *env, jobject thiz, jboolean granted); template static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args); diff --git a/client/platforms/ios/iosPairingCameraAccess.h b/client/platforms/ios/iosPairingCameraAccess.h new file mode 100644 index 000000000..a6af9a89d --- /dev/null +++ b/client/platforms/ios/iosPairingCameraAccess.h @@ -0,0 +1,10 @@ +#ifndef IOS_PAIRING_CAMERA_ACCESS_H +#define IOS_PAIRING_CAMERA_ACCESS_H + +#include + +bool amneziaIosPairingCameraAccessGranted(); +void amneziaIosRequestPairingCameraAccess(const std::function &onDone); +void amneziaIosOpenApplicationSettings(); + +#endif diff --git a/client/platforms/ios/iosPairingCameraAccess.mm b/client/platforms/ios/iosPairingCameraAccess.mm new file mode 100644 index 000000000..6f937c193 --- /dev/null +++ b/client/platforms/ios/iosPairingCameraAccess.mm @@ -0,0 +1,37 @@ +#include "platforms/ios/iosPairingCameraAccess.h" + +#import +#import + +bool amneziaIosPairingCameraAccessGranted() +{ + const AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; + return status == AVAuthorizationStatusAuthorized; +} + +void amneziaIosRequestPairingCameraAccess(const std::function &onDone) +{ + const AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; + if (status == AVAuthorizationStatusAuthorized) { + onDone(true); + return; + } + if (status == AVAuthorizationStatusDenied || status == AVAuthorizationStatusRestricted) { + onDone(false); + return; + } + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo + completionHandler:^(BOOL granted) { + dispatch_async(dispatch_get_main_queue(), ^{ + onDone(static_cast(granted)); + }); + }]; +} + +void amneziaIosOpenApplicationSettings() +{ + NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; + if (url != nil) { + [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; + } +} diff --git a/client/platforms/ios/iosPairingCameraAccess_stub.cpp b/client/platforms/ios/iosPairingCameraAccess_stub.cpp new file mode 100644 index 000000000..e331914d6 --- /dev/null +++ b/client/platforms/ios/iosPairingCameraAccess_stub.cpp @@ -0,0 +1,13 @@ +#include "platforms/ios/iosPairingCameraAccess.h" + +bool amneziaIosPairingCameraAccessGranted() +{ + return true; +} + +void amneziaIosRequestPairingCameraAccess(const std::function &onDone) +{ + onDone(true); +} + +void amneziaIosOpenApplicationSettings() {} diff --git a/client/ui/controllers/api/pairingUiController.cpp b/client/ui/controllers/api/pairingUiController.cpp index 753f2298f..0b46009a2 100644 --- a/client/ui/controllers/api/pairingUiController.cpp +++ b/client/ui/controllers/api/pairingUiController.cpp @@ -9,6 +9,8 @@ #include #include +#include "platforms/ios/iosPairingCameraAccess.h" + #if defined(Q_OS_ANDROID) #include "platforms/android/android_controller.h" #endif @@ -109,6 +111,8 @@ PairingUiController::PairingUiController(PairingController *pairingController, S { #if defined(Q_OS_ANDROID) g_pairingUiForAndroidQr = this; + connect(AndroidController::instance(), &AndroidController::cameraPermissionResult, this, + [this](bool granted) { emit pairingCameraAccessFinished(granted); }); #endif } @@ -157,6 +161,40 @@ void PairingUiController::openPairingQrScanner() #endif } +bool PairingUiController::isPairingCameraAccessGranted() const +{ +#if defined(Q_OS_ANDROID) + return AndroidController::instance()->isCameraPermissionGranted(); +#elif defined(Q_OS_IOS) + return amneziaIosPairingCameraAccessGranted(); +#else + return true; +#endif +} + +void PairingUiController::requestPairingCameraAccess() +{ +#if defined(Q_OS_ANDROID) + AndroidController::instance()->requestCameraPermissionForQrPairing(); +#elif defined(Q_OS_IOS) + amneziaIosRequestPairingCameraAccess([this](bool granted) { + QMetaObject::invokeMethod( + this, [this, granted]() { emit pairingCameraAccessFinished(granted); }, Qt::QueuedConnection); + }); +#else + emit pairingCameraAccessFinished(true); +#endif +} + +void PairingUiController::openPairingCameraAppSettings() +{ +#if defined(Q_OS_ANDROID) + AndroidController::instance()->openApplicationDetailsSettings(); +#elif defined(Q_OS_IOS) + amneziaIosOpenApplicationSettings(); +#endif +} + bool PairingUiController::applyScannedTextAsPairingUuid(const QString &raw) { const QString t = raw.trimmed(); diff --git a/client/ui/controllers/api/pairingUiController.h b/client/ui/controllers/api/pairingUiController.h index 778a26650..7f2abf15e 100644 --- a/client/ui/controllers/api/pairingUiController.h +++ b/client/ui/controllers/api/pairingUiController.h @@ -74,6 +74,13 @@ public slots: /** Android: system camera activity. iOS: toggle camera from QML. */ void openPairingQrScanner(); + /** Mobile: whether the app may use the camera for QR pairing (OS permission). Desktop: true. */ + Q_INVOKABLE bool isPairingCameraAccessGranted() const; + /** Mobile: show rationale / system camera permission UI; emits pairingCameraAccessFinished. Desktop: emits granted. */ + Q_INVOKABLE void requestPairingCameraAccess(); + /** Open system settings for this app (camera can be enabled there). No-op on desktop. */ + Q_INVOKABLE void openPairingCameraAppSettings(); + /** If \a raw contains a session UUID (not vpn://), emits pairingUuidFromScan and returns true. */ bool applyScannedTextAsPairingUuid(const QString &raw); @@ -97,6 +104,8 @@ signals: void pairingUuidFromScan(const QString &uuid); void tvPairingUiPhaseChanged(); + /** After requestPairingCameraAccess(): true if OS granted camera access. */ + void pairingCameraAccessFinished(bool granted); private: void setTvBusy(bool busy); diff --git a/client/ui/qml/Pages2/PageSettingsApiDevices.qml b/client/ui/qml/Pages2/PageSettingsApiDevices.qml index 365adb9ce..df4b72372 100644 --- a/client/ui/qml/Pages2/PageSettingsApiDevices.qml +++ b/client/ui/qml/Pages2/PageSettingsApiDevices.qml @@ -19,19 +19,103 @@ import "../Components" PageType { id: root + /** True after "Add Device via QR" until permission result or navigation. */ + property bool pendingOpenQrPageAfterCamera: false + /** True after denial: user may enable camera in system settings; resume opens QR page when granted. */ + property bool waitingSettingsReturnForQrPage: false + + function proceedOpenQrPairingPage() { + PageController.goToPage(PageEnum.PageSettingsApiQrPairingSend) + pendingOpenQrPageAfterCamera = false + waitingSettingsReturnForQrPage = false + } + + function showCameraDeniedDrawer() { + showQuestionDrawer( + qsTr("Camera access is required"), + qsTr("Allow camera access to scan the pairing QR code. You can enable it in the system settings for Amnezia VPN."), + qsTr("Open settings"), + qsTr("Cancel"), + function() { + PairingUiController.openPairingCameraAppSettings() + }, + function() { + waitingSettingsReturnForQrPage = false + }) + } + + function tryResumeQrPageAfterCameraSettings() { + if (!waitingSettingsReturnForQrPage || !root.visible) { + return + } + if (PairingUiController.isPairingCameraAccessGranted()) { + proceedOpenQrPairingPage() + } + } + function openAddDeviceViaQr() { const maxC = ApiAccountInfoModel.data("maxDeviceCount") const activeC = ApiAccountInfoModel.data("activeDeviceCount") if (maxC > 0 && activeC >= maxC) { PageController.goToPage(PageEnum.PageSettingsApiDeviceLimit) - } else { + return + } + if (Qt.platform.os !== "android" && Qt.platform.os !== "ios") { PageController.goToPage(PageEnum.PageSettingsApiQrPairingSend) + return + } + if (PairingUiController.isPairingCameraAccessGranted()) { + proceedOpenQrPairingPage() + return + } + pendingOpenQrPageAfterCamera = true + PairingUiController.requestPairingCameraAccess() + } + + onVisibleChanged: { + if (!visible) { + pendingOpenQrPageAfterCamera = false + waitingSettingsReturnForQrPage = false + } + } + + Connections { + target: Qt.application + + function onStateChanged() { + if (Qt.application.state !== Qt.ApplicationActive) { + return + } + root.tryResumeQrPageAfterCameraSettings() + } + } + + Connections { + target: SettingsController + + enabled: Qt.platform.os === "android" + + function onActivityResumed() { + root.tryResumeQrPageAfterCameraSettings() } } Connections { target: PairingUiController + function onPairingCameraAccessFinished(granted) { + if (!root.pendingOpenQrPageAfterCamera) { + return + } + root.pendingOpenQrPageAfterCamera = false + if (granted) { + root.proceedOpenQrPairingPage() + } else { + root.waitingSettingsReturnForQrPage = true + root.showCameraDeniedDrawer() + } + } + function onPhonePairingSucceeded() { const serverIndex = ServersUiController.getProcessedServerIndex() if (serverIndex < 0) { diff --git a/client/ui/qml/Pages2/PageSettingsApiQrPairingSend.qml b/client/ui/qml/Pages2/PageSettingsApiQrPairingSend.qml index 6b46ab3d8..e223fae0b 100644 --- a/client/ui/qml/Pages2/PageSettingsApiQrPairingSend.qml +++ b/client/ui/qml/Pages2/PageSettingsApiQrPairingSend.qml @@ -6,6 +6,7 @@ import QRCodeReader 1.0 import PageEnum 1.0 import Style 1.0 +import "./" import "../Controls2" import "../Controls2/TextTypes" import "../Config" @@ -23,6 +24,10 @@ PageType { property int lastInvalidPairingQrToastClockMs: 0 /** iOS may deliver many QR frames; guard duplicate step transitions. */ property bool addDeviceConfirmNavigationScheduled: false + /** Mobile: waiting for camera permission before starting scan UI / Android scanner. */ + property bool awaitingCameraPermissionForScan: false + /** After denial on scan screen: user may enable camera in settings. */ + property bool waitingSettingsReturnForScan: false Timer { id: pairingCameraKickTimer @@ -31,6 +36,38 @@ PageType { onTriggered: root.restartPairingIosCamera() } + function startPairingScanAfterPermission() { + if (Qt.platform.os === "android") { + PairingUiController.openPairingQrScanner() + } else if (Qt.platform.os === "ios") { + root.pairingCameraOpen = true + } + } + + function showScanCameraDeniedDrawer() { + showQuestionDrawer( + qsTr("Camera access is required"), + qsTr("Allow camera access to scan the pairing QR code. You can enable it in the system settings for Amnezia VPN."), + qsTr("Open settings"), + qsTr("Cancel"), + function() { + PairingUiController.openPairingCameraAppSettings() + }, + function() { + root.waitingSettingsReturnForScan = false + }) + } + + function tryResumeScanAfterCameraSettings() { + if (!root.waitingSettingsReturnForScan || !root.visible || root.pairingWizardStep !== 0) { + return + } + if (PairingUiController.isPairingCameraAccessGranted()) { + root.waitingSettingsReturnForScan = false + root.startPairingScanAfterPermission() + } + } + function restartPairingIosCamera() { if (Qt.platform.os !== "ios" || !root.pairingCameraOpen) { return @@ -63,6 +100,7 @@ PageType { pairingQrReader.stopReading() root.pairingCameraOpen = false root.pairingWizardStep = 0 + root.waitingSettingsReturnForScan = false if (!root.keepPhonePairingInBackgroundOnClose && !PairingUiController.phonePairingBusy) { PairingUiController.cancelAllPairingActivity() } @@ -70,6 +108,27 @@ PageType { } } + Connections { + target: Qt.application + + function onStateChanged() { + if (Qt.application.state !== Qt.ApplicationActive) { + return + } + root.tryResumeScanAfterCameraSettings() + } + } + + Connections { + target: SettingsController + + enabled: Qt.platform.os === "android" + + function onActivityResumed() { + root.tryResumeScanAfterCameraSettings() + } + } + Connections { target: root function onPairingCameraOpenChanged() { @@ -161,6 +220,11 @@ PageType { } enabled: !PairingUiController.phonePairingBusy clickedFunc: function() { + if (!PairingUiController.isPairingCameraAccessGranted()) { + root.awaitingCameraPermissionForScan = true + PairingUiController.requestPairingCameraAccess() + return + } if (Qt.platform.os === "android") { PairingUiController.openPairingQrScanner() } else { @@ -238,14 +302,6 @@ PageType { wrapMode: Text.Wrap } - ParagraphTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - text: qsTr("Devices available with Amnezia Premium: %1").arg(ApiAccountInfoModel.data("availableDeviceSlots")) - wrapMode: Text.Wrap - } - BasicButtonType { Layout.fillWidth: true Layout.leftMargin: 16 @@ -296,6 +352,19 @@ PageType { Connections { target: PairingUiController + function onPairingCameraAccessFinished(granted) { + if (!root.awaitingCameraPermissionForScan) { + return + } + root.awaitingCameraPermissionForScan = false + if (granted) { + root.startPairingScanAfterPermission() + } else { + root.waitingSettingsReturnForScan = true + root.showScanCameraDeniedDrawer() + } + } + function onPairingUuidFromScan(uuid) { if (root.addDeviceConfirmNavigationScheduled) { return