mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-23 02:00:20 +07:00
add check access camera
This commit is contained in:
@@ -24,5 +24,9 @@
|
|||||||
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string>
|
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string>
|
||||||
<string name="openNotificationSettings">Open notification settings</string>
|
<string name="openNotificationSettings">Open notification settings</string>
|
||||||
|
|
||||||
|
<string name="cameraPermissionDialogTitle">Camera access</string>
|
||||||
|
<string name="cameraPermissionDialogMessage">To scan a QR code for device pairing, Amnezia VPN needs access to the camera.</string>
|
||||||
|
<string name="cameraPermissionContinue">Continue</string>
|
||||||
|
|
||||||
<string name="tvNoFileBrowser">Please install a file management utility to browse files</string>
|
<string name="tvNoFileBrowser">Please install a file management utility to browse files</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -73,6 +73,7 @@ private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1
|
|||||||
private const val CREATE_FILE_ACTION_CODE = 2
|
private const val CREATE_FILE_ACTION_CODE = 2
|
||||||
private const val OPEN_FILE_ACTION_CODE = 3
|
private const val OPEN_FILE_ACTION_CODE = 3
|
||||||
private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
|
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 PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
|
||||||
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
|
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
|
||||||
@@ -880,6 +881,66 @@ class AmneziaActivity : QtActivity() {
|
|||||||
@SuppressLint("UnsupportedChromeOsCameraSystemFeature")
|
@SuppressLint("UnsupportedChromeOsCameraSystemFeature")
|
||||||
fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
|
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")
|
@Suppress("unused")
|
||||||
fun isOnTv(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
fun isOnTv(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||||
|
|
||||||
@@ -1179,6 +1240,7 @@ class AmneziaActivity : QtActivity() {
|
|||||||
CREATE_FILE_ACTION_CODE -> "CREATE_FILE"
|
CREATE_FILE_ACTION_CODE -> "CREATE_FILE"
|
||||||
OPEN_FILE_ACTION_CODE -> "OPEN_FILE"
|
OPEN_FILE_ACTION_CODE -> "OPEN_FILE"
|
||||||
CHECK_NOTIFICATION_PERMISSION_ACTION_CODE -> "CHECK_NOTIFICATION_PERMISSION"
|
CHECK_NOTIFICATION_PERMISSION_ACTION_CODE -> "CHECK_NOTIFICATION_PERMISSION"
|
||||||
|
CHECK_CAMERA_PERMISSION_ACTION_CODE -> "CHECK_CAMERA_PERMISSION"
|
||||||
else -> actionCode.toString()
|
else -> actionCode.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,4 +34,6 @@ object QtAndroidController {
|
|||||||
|
|
||||||
external fun onActivityPaused()
|
external fun onActivityPaused()
|
||||||
external fun onActivityResumed()
|
external fun onActivityResumed()
|
||||||
|
|
||||||
|
external fun onCameraPermissionResult(granted: Boolean)
|
||||||
}
|
}
|
||||||
@@ -44,6 +44,7 @@ set(SOURCES ${SOURCES}
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.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/QtAppDelegate.mm
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ set(SOURCES ${SOURCES}
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.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)
|
set(ICON_FILE ${CMAKE_CURRENT_SOURCE_DIR}/images/app.icns)
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ set(HEADERS ${HEADERS}
|
|||||||
${CLIENT_ROOT_DIR}/core/utils/utilities.h
|
${CLIENT_ROOT_DIR}/core/utils/utilities.h
|
||||||
${CLIENT_ROOT_DIR}/core/utils/managementServer.h
|
${CLIENT_ROOT_DIR}/core/utils/managementServer.h
|
||||||
${CLIENT_ROOT_DIR}/core/utils/constants.h
|
${CLIENT_ROOT_DIR}/core/utils/constants.h
|
||||||
|
${CLIENT_ROOT_DIR}/platforms/ios/iosPairingCameraAccess.h
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mozilla headres
|
# Mozilla headres
|
||||||
@@ -155,6 +156,7 @@ set(SOURCES ${SOURCES}
|
|||||||
if(NOT IOS AND NOT MACOS_NE)
|
if(NOT IOS AND NOT MACOS_NE)
|
||||||
set(SOURCES ${SOURCES}
|
set(SOURCES ${SOURCES}
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.cpp
|
${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.cpp
|
||||||
|
${CLIENT_ROOT_DIR}/platforms/ios/iosPairingCameraAccess_stub.cpp
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,8 @@ bool AndroidController::initialize()
|
|||||||
{"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)},
|
{"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)},
|
||||||
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)},
|
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)},
|
||||||
{"onActivityPaused", "()V", reinterpret_cast<void *>(onActivityPaused)},
|
{"onActivityPaused", "()V", reinterpret_cast<void *>(onActivityPaused)},
|
||||||
{"onActivityResumed", "()V", reinterpret_cast<void *>(onActivityResumed)}
|
{"onActivityResumed", "()V", reinterpret_cast<void *>(onActivityResumed)},
|
||||||
|
{"onCameraPermissionResult", "(Z)V", reinterpret_cast<void *>(onCameraPermissionResult)}
|
||||||
};
|
};
|
||||||
|
|
||||||
QJniEnvironment env;
|
QJniEnvironment env;
|
||||||
@@ -202,6 +203,21 @@ bool AndroidController::isCameraPresent()
|
|||||||
return callActivityMethod<jboolean>("isCameraPresent", "()Z");
|
return callActivityMethod<jboolean>("isCameraPresent", "()Z");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool AndroidController::isCameraPermissionGranted()
|
||||||
|
{
|
||||||
|
return callActivityMethod<jboolean>("isCameraPermissionGranted", "()Z");
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidController::requestCameraPermissionForQrPairing()
|
||||||
|
{
|
||||||
|
callActivityMethod("requestCameraPermissionForQrPairing", "()V");
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidController::openApplicationDetailsSettings()
|
||||||
|
{
|
||||||
|
callActivityMethod("openApplicationDetailsSettings", "()V");
|
||||||
|
}
|
||||||
|
|
||||||
bool AndroidController::isOnTv()
|
bool AndroidController::isOnTv()
|
||||||
{
|
{
|
||||||
return callActivityMethod<jboolean>("isOnTv", "()Z");
|
return callActivityMethod<jboolean>("isOnTv", "()Z");
|
||||||
@@ -583,4 +599,13 @@ void AndroidController::onActivityResumed(JNIEnv *env, jobject thiz)
|
|||||||
emit AndroidController::instance()->activityResumed();
|
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<bool>(granted));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ public:
|
|||||||
void closeFd();
|
void closeFd();
|
||||||
QString getFileName(const QString &uri);
|
QString getFileName(const QString &uri);
|
||||||
bool isCameraPresent();
|
bool isCameraPresent();
|
||||||
|
bool isCameraPermissionGranted();
|
||||||
|
void requestCameraPermissionForQrPairing();
|
||||||
|
void openApplicationDetailsSettings();
|
||||||
bool isOnTv();
|
bool isOnTv();
|
||||||
bool isEdgeToEdgeEnabled();
|
bool isEdgeToEdgeEnabled();
|
||||||
int getStatusBarHeight();
|
int getStatusBarHeight();
|
||||||
@@ -77,6 +80,7 @@ signals:
|
|||||||
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
|
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
|
||||||
void activityPaused();
|
void activityPaused();
|
||||||
void activityResumed();
|
void activityResumed();
|
||||||
|
void cameraPermissionResult(bool granted);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool isWaitingStatus = true;
|
bool isWaitingStatus = true;
|
||||||
@@ -109,6 +113,7 @@ private:
|
|||||||
static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp);
|
static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp);
|
||||||
static void onActivityPaused(JNIEnv *env, jobject thiz);
|
static void onActivityPaused(JNIEnv *env, jobject thiz);
|
||||||
static void onActivityResumed(JNIEnv *env, jobject thiz);
|
static void onActivityResumed(JNIEnv *env, jobject thiz);
|
||||||
|
static void onCameraPermissionResult(JNIEnv *env, jobject thiz, jboolean granted);
|
||||||
|
|
||||||
template <typename Ret, typename ...Args>
|
template <typename Ret, typename ...Args>
|
||||||
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);
|
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#ifndef IOS_PAIRING_CAMERA_ACCESS_H
|
||||||
|
#define IOS_PAIRING_CAMERA_ACCESS_H
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
bool amneziaIosPairingCameraAccessGranted();
|
||||||
|
void amneziaIosRequestPairingCameraAccess(const std::function<void(bool)> &onDone);
|
||||||
|
void amneziaIosOpenApplicationSettings();
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#include "platforms/ios/iosPairingCameraAccess.h"
|
||||||
|
|
||||||
|
#import <AVFoundation/AVFoundation.h>
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
bool amneziaIosPairingCameraAccessGranted()
|
||||||
|
{
|
||||||
|
const AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
|
||||||
|
return status == AVAuthorizationStatusAuthorized;
|
||||||
|
}
|
||||||
|
|
||||||
|
void amneziaIosRequestPairingCameraAccess(const std::function<void(bool)> &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<bool>(granted));
|
||||||
|
});
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
void amneziaIosOpenApplicationSettings()
|
||||||
|
{
|
||||||
|
NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
|
||||||
|
if (url != nil) {
|
||||||
|
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#include "platforms/ios/iosPairingCameraAccess.h"
|
||||||
|
|
||||||
|
bool amneziaIosPairingCameraAccessGranted()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void amneziaIosRequestPairingCameraAccess(const std::function<void(bool)> &onDone)
|
||||||
|
{
|
||||||
|
onDone(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void amneziaIosOpenApplicationSettings() {}
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
|
#include "platforms/ios/iosPairingCameraAccess.h"
|
||||||
|
|
||||||
#if defined(Q_OS_ANDROID)
|
#if defined(Q_OS_ANDROID)
|
||||||
#include "platforms/android/android_controller.h"
|
#include "platforms/android/android_controller.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -109,6 +111,8 @@ PairingUiController::PairingUiController(PairingController *pairingController, S
|
|||||||
{
|
{
|
||||||
#if defined(Q_OS_ANDROID)
|
#if defined(Q_OS_ANDROID)
|
||||||
g_pairingUiForAndroidQr = this;
|
g_pairingUiForAndroidQr = this;
|
||||||
|
connect(AndroidController::instance(), &AndroidController::cameraPermissionResult, this,
|
||||||
|
[this](bool granted) { emit pairingCameraAccessFinished(granted); });
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +161,40 @@ void PairingUiController::openPairingQrScanner()
|
|||||||
#endif
|
#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)
|
bool PairingUiController::applyScannedTextAsPairingUuid(const QString &raw)
|
||||||
{
|
{
|
||||||
const QString t = raw.trimmed();
|
const QString t = raw.trimmed();
|
||||||
|
|||||||
@@ -74,6 +74,13 @@ public slots:
|
|||||||
/** Android: system camera activity. iOS: toggle camera from QML. */
|
/** 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;
|
||||||
|
/** 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. */
|
/** If \a raw contains a session UUID (not vpn://), emits pairingUuidFromScan and returns true. */
|
||||||
bool applyScannedTextAsPairingUuid(const QString &raw);
|
bool applyScannedTextAsPairingUuid(const QString &raw);
|
||||||
|
|
||||||
@@ -97,6 +104,8 @@ 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);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setTvBusy(bool busy);
|
void setTvBusy(bool busy);
|
||||||
|
|||||||
@@ -19,19 +19,103 @@ import "../Components"
|
|||||||
PageType {
|
PageType {
|
||||||
id: root
|
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() {
|
function openAddDeviceViaQr() {
|
||||||
const maxC = ApiAccountInfoModel.data("maxDeviceCount")
|
const maxC = ApiAccountInfoModel.data("maxDeviceCount")
|
||||||
const activeC = ApiAccountInfoModel.data("activeDeviceCount")
|
const activeC = ApiAccountInfoModel.data("activeDeviceCount")
|
||||||
if (maxC > 0 && activeC >= maxC) {
|
if (maxC > 0 && activeC >= maxC) {
|
||||||
PageController.goToPage(PageEnum.PageSettingsApiDeviceLimit)
|
PageController.goToPage(PageEnum.PageSettingsApiDeviceLimit)
|
||||||
} else {
|
return
|
||||||
|
}
|
||||||
|
if (Qt.platform.os !== "android" && Qt.platform.os !== "ios") {
|
||||||
PageController.goToPage(PageEnum.PageSettingsApiQrPairingSend)
|
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 {
|
Connections {
|
||||||
target: PairingUiController
|
target: PairingUiController
|
||||||
|
|
||||||
|
function onPairingCameraAccessFinished(granted) {
|
||||||
|
if (!root.pendingOpenQrPageAfterCamera) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
root.pendingOpenQrPageAfterCamera = false
|
||||||
|
if (granted) {
|
||||||
|
root.proceedOpenQrPairingPage()
|
||||||
|
} else {
|
||||||
|
root.waitingSettingsReturnForQrPage = true
|
||||||
|
root.showCameraDeniedDrawer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onPhonePairingSucceeded() {
|
function onPhonePairingSucceeded() {
|
||||||
const serverIndex = ServersUiController.getProcessedServerIndex()
|
const serverIndex = ServersUiController.getProcessedServerIndex()
|
||||||
if (serverIndex < 0) {
|
if (serverIndex < 0) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import QRCodeReader 1.0
|
|||||||
import PageEnum 1.0
|
import PageEnum 1.0
|
||||||
import Style 1.0
|
import Style 1.0
|
||||||
|
|
||||||
|
import "./"
|
||||||
import "../Controls2"
|
import "../Controls2"
|
||||||
import "../Controls2/TextTypes"
|
import "../Controls2/TextTypes"
|
||||||
import "../Config"
|
import "../Config"
|
||||||
@@ -23,6 +24,10 @@ PageType {
|
|||||||
property int lastInvalidPairingQrToastClockMs: 0
|
property int lastInvalidPairingQrToastClockMs: 0
|
||||||
/** iOS may deliver many QR frames; guard duplicate step transitions. */
|
/** iOS may deliver many QR frames; guard duplicate step transitions. */
|
||||||
property bool addDeviceConfirmNavigationScheduled: false
|
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 {
|
Timer {
|
||||||
id: pairingCameraKickTimer
|
id: pairingCameraKickTimer
|
||||||
@@ -31,6 +36,38 @@ PageType {
|
|||||||
onTriggered: root.restartPairingIosCamera()
|
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() {
|
function restartPairingIosCamera() {
|
||||||
if (Qt.platform.os !== "ios" || !root.pairingCameraOpen) {
|
if (Qt.platform.os !== "ios" || !root.pairingCameraOpen) {
|
||||||
return
|
return
|
||||||
@@ -63,6 +100,7 @@ PageType {
|
|||||||
pairingQrReader.stopReading()
|
pairingQrReader.stopReading()
|
||||||
root.pairingCameraOpen = false
|
root.pairingCameraOpen = false
|
||||||
root.pairingWizardStep = 0
|
root.pairingWizardStep = 0
|
||||||
|
root.waitingSettingsReturnForScan = false
|
||||||
if (!root.keepPhonePairingInBackgroundOnClose && !PairingUiController.phonePairingBusy) {
|
if (!root.keepPhonePairingInBackgroundOnClose && !PairingUiController.phonePairingBusy) {
|
||||||
PairingUiController.cancelAllPairingActivity()
|
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 {
|
Connections {
|
||||||
target: root
|
target: root
|
||||||
function onPairingCameraOpenChanged() {
|
function onPairingCameraOpenChanged() {
|
||||||
@@ -161,6 +220,11 @@ PageType {
|
|||||||
}
|
}
|
||||||
enabled: !PairingUiController.phonePairingBusy
|
enabled: !PairingUiController.phonePairingBusy
|
||||||
clickedFunc: function() {
|
clickedFunc: function() {
|
||||||
|
if (!PairingUiController.isPairingCameraAccessGranted()) {
|
||||||
|
root.awaitingCameraPermissionForScan = true
|
||||||
|
PairingUiController.requestPairingCameraAccess()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (Qt.platform.os === "android") {
|
if (Qt.platform.os === "android") {
|
||||||
PairingUiController.openPairingQrScanner()
|
PairingUiController.openPairingQrScanner()
|
||||||
} else {
|
} else {
|
||||||
@@ -238,14 +302,6 @@ PageType {
|
|||||||
wrapMode: Text.Wrap
|
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 {
|
BasicButtonType {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
@@ -296,6 +352,19 @@ PageType {
|
|||||||
Connections {
|
Connections {
|
||||||
target: PairingUiController
|
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) {
|
function onPairingUuidFromScan(uuid) {
|
||||||
if (root.addDeviceConfirmNavigationScheduled) {
|
if (root.addDeviceConfirmNavigationScheduled) {
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user