Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
2f6714e
feat/Implement QR code generation and scanning
yp-amnezia May 7, 2026
2cb7b30
add test server
yp-amnezia May 7, 2026
5583c0a
fixed open Qr QML & add check error code & add test
yp-amnezia May 7, 2026
5beae95
add test macros AMNEZIA_QR_PAIRING_ALLOW_DUPLICATE_VPN_KEY & disable …
yp-amnezia May 7, 2026
2cb12c5
add qml QR Code
yp-amnezia May 7, 2026
c877e1e
fix build iOS
yp-amnezia May 7, 2026
f65fd4a
fixed server go
yp-amnezia May 7, 2026
6fc65db
fixed iOS QRCodeReader
yp-amnezia May 7, 2026
5a192ce
fixed QR scaner
yp-amnezia May 7, 2026
ab12a0b
update screen QR Code
yp-amnezia May 8, 2026
433ecb4
fixed scanner phone & fix UI/UX
yp-amnezia May 8, 2026
a53db6e
fixed open QR code screen & fix iOS scanner
yp-amnezia May 8, 2026
bb56008
add check access camera
yp-amnezia May 8, 2026
b7e2847
fixed UI scanner iOS
yp-amnezia May 8, 2026
d2d3545
add test scaner ios
yp-amnezia May 8, 2026
0268269
fixed iOS UI scanner
yp-amnezia May 8, 2026
d3347e6
fixed AVCaptureMetadataOutput rectOfInterest
yp-amnezia May 8, 2026
14c7aab
fixed icon back
yp-amnezia May 8, 2026
1ee0a6c
fixed addArc scanner
yp-amnezia May 8, 2026
2fa0ec8
remove qml limit device
yp-amnezia May 8, 2026
f781bf6
fixed scaner QR Android
yp-amnezia May 9, 2026
bf4bf99
fixed line box
yp-amnezia May 9, 2026
e226fad
fixed PR code
yp-amnezia May 12, 2026
1baa2d8
remove dead code
yp-amnezia May 13, 2026
8a29b49
update updated_spec.yaml
yp-amnezia May 13, 2026
d0a9f6e
add ui Configuration Files
yp-amnezia May 13, 2026
81b8cd0
fix build
yp-amnezia May 13, 2026
b46a9e3
remove old file
yp-amnezia May 13, 2026
5eab5fc
fixed 404, 1100, 1109 - fixed crash app (add server)
yp-amnezia May 18, 2026
d866874
remove mock & temp var AMNEZIA_QR_PAIRING_ALLOW
yp-amnezia May 18, 2026
d6c34b3
remove comment
yp-amnezia May 18, 2026
29ad1f0
merge dev & fix conf
yp-amnezia May 18, 2026
9851b4b
remove old code
yp-amnezia May 18, 2026
d483345
fixed serviceType|userCountryCode
yp-amnezia May 19, 2026
e554e9b
remove log & remove temp code
yp-amnezia May 19, 2026
614973a
remove old code
yp-amnezia May 19, 2026
22de0c2
remove comment
yp-amnezia May 19, 2026
eba2097
remove temp code
yp-amnezia May 20, 2026
9c9e170
fix 1103 error (update qr code)
yp-amnezia May 20, 2026
c327d3e
add error 1123 & fix update qr code & fix hide qr code
yp-amnezia May 20, 2026
b09a4ec
reset files
yp-amnezia May 20, 2026
d670bca
chore(qr-pairing): drop unrelated PageStart churn and trim overlay logs
yp-amnezia May 20, 2026
3d540b2
remove doc
yp-amnezia May 20, 2026
a4b2b3e
reset files
yp-amnezia May 20, 2026
67dcadc
add rus translate
yp-amnezia May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.15.4)
set(AMNEZIAVPN_VERSION 4.9.0.2)

set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
Expand Down
3 changes: 3 additions & 0 deletions client/amneziaApplication.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ void AmneziaApplication::init()
// install filter on main window
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
win->installEventFilter(this);
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
win->setDefaultAlphaBuffer(true);
#endif
#ifdef Q_OS_ANDROID
QObject::connect(win, &QQuickWindow::sceneGraphError,
[](QQuickWindow::SceneGraphError, const QString &msg) {
Expand Down
10 changes: 10 additions & 0 deletions client/android/res/drawable/ic_pairing_back.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFE8E8EC"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
</vector>
5 changes: 5 additions & 0 deletions client/android/res/drawable/torch_fab_bg.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#38FFFFFF" />
</shape>
71 changes: 71 additions & 0 deletions client/android/res/layout/camera_preview.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,75 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />

<org.amnezia.vpn.PairingQrScanOverlayView
android:id="@+id/pairingScanOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />

<LinearLayout
android:id="@+id/pairingChrome"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:background="@android:color/transparent"
android:orientation="horizontal"
android:paddingStart="8dp"
android:paddingTop="28dp"
android:paddingEnd="16dp"
android:paddingBottom="12dp"
android:visibility="gone">

<ImageButton
android:id="@+id/pairingBack"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="top"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/pairing_qr_camera_back"
android:padding="12dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_pairing_back" />

<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:orientation="vertical">

<TextView
android:id="@+id/pairingTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pairing_qr_camera_title"
android:textColor="#FFE8E8EC"
android:textSize="20sp"
android:textStyle="bold" />

<TextView
android:id="@+id/pairingSubtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/pairing_qr_camera_subtitle"
android:textColor="#FFB8B8C0"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>

<TextView
android:id="@+id/torchButton"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="32dp"
android:background="@drawable/torch_fab_bg"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:text="🔦"
android:textSize="26sp"
android:contentDescription="@string/camera_torch" />

</FrameLayout>
8 changes: 8 additions & 0 deletions client/android/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,13 @@
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
<string name="openNotificationSettings">Открыть настройки уведомлений</string>

<string name="cameraPermissionDialogTitle">Доступ к камере</string>
<string name="cameraPermissionDialogMessage">Чтобы отсканировать QR-код для добавления устройства, Amnezia VPN нужен доступ к камере.</string>
<string name="cameraPermissionContinue">Продолжить</string>
<string name="camera_torch">Фонарик</string>
<string name="pairing_qr_camera_title">Добавить устройство по QR</string>
<string name="pairing_qr_camera_subtitle">Отсканируйте QR сессии на устройстве, которое хотите добавить. Перед отправкой подписки будет подтверждение.</string>
<string name="pairing_qr_camera_back">Назад</string>

<string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string>
</resources>
8 changes: 8 additions & 0 deletions client/android/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,13 @@
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system 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="camera_torch">Flashlight</string>
<string name="pairing_qr_camera_title">Add device via QR</string>
<string name="pairing_qr_camera_subtitle">Scan the session QR shown on the device you want to add. You will confirm before the subscription is sent.</string>
<string name="pairing_qr_camera_back">Back</string>

<string name="tvNoFileBrowser">Please install a file management utility to browse files</string>
</resources>
93 changes: 92 additions & 1 deletion client/android/src/org/amnezia/vpn/AmneziaActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import java.io.IOException
import kotlin.LazyThreadSafetyMode.NONE
import kotlin.coroutines.CoroutineContext
Expand Down Expand Up @@ -73,12 +76,18 @@ 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
private const val KEY_PENDING_OPEN_FILE_URI = "pending_open_file_uri"

class AmneziaActivity : QtActivity() {
class AmneziaActivity : QtActivity(), LifecycleOwner {

private val lifecycleRegistry = LifecycleRegistry(this)

override val lifecycle: Lifecycle
get() = lifecycleRegistry

private lateinit var mainScope: CoroutineScope
private val qtInitialized = CompletableDeferred<Unit>()
Expand All @@ -99,6 +108,8 @@ class AmneziaActivity : QtActivity() {
private var pendingOpenFileUri: String? = null
private var openFileDeliveryScheduled = false

private var lastPairingQrReaderStartUptimeMs: Long = 0L

private val vpnServiceEventHandler: Handler by lazy(NONE) {
object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
Expand Down Expand Up @@ -205,6 +216,7 @@ class AmneziaActivity : QtActivity() {
registerBroadcastReceivers()
intent?.let(::processIntent)
runBlocking { vpnProto = proto.await() }
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}

override fun onSaveInstanceState(outState: Bundle) {
Expand Down Expand Up @@ -262,6 +274,7 @@ class AmneziaActivity : QtActivity() {

override fun onStart() {
super.onStart()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Log.d(TAG, "Start Amnezia activity")
mainScope.launch {
qtInitialized.await()
Expand All @@ -285,6 +298,7 @@ class AmneziaActivity : QtActivity() {
qtInitialized.await()
QtAndroidController.onServiceDisconnected()
}
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
super.onStop()
}

Expand Down Expand Up @@ -357,6 +371,7 @@ class AmneziaActivity : QtActivity() {
if (qtInitialized.isCompleted) {
QtAndroidController.onActivityPaused()
}
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
super.onPause()
isActivityResumed = false
// Cancel all pending operations when activity pauses
Expand All @@ -367,6 +382,7 @@ class AmneziaActivity : QtActivity() {

override fun onResume() {
super.onResume()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
isActivityResumed = true
Log.d(TAG, "Resume Amnezia activity")
if (qtInitialized.isCompleted) {
Expand Down Expand Up @@ -483,6 +499,7 @@ class AmneziaActivity : QtActivity() {
unregisterBroadcastReceiver(notificationStateReceiver)
notificationStateReceiver = null
mainScope.cancel()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
super.onDestroy()
}

Expand Down Expand Up @@ -880,6 +897,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)

Expand Down Expand Up @@ -928,6 +1005,19 @@ class AmneziaActivity : QtActivity() {
}
}

@Suppress("unused")
fun startPairingQrCodeReader() {
val now = SystemClock.uptimeMillis()
if (now - lastPairingQrReaderStartUptimeMs < 1200L) {
return
}
lastPairingQrReaderStartUptimeMs = now
Intent(this, CameraActivity::class.java).also {
it.putExtra(CameraActivity.EXTRA_PAIRING_QR_CAMERA, true)
startActivity(it)
}
}

@Suppress("unused")
fun setSaveLogs(enabled: Boolean) {
Log.v(TAG, "Set save logs: $enabled")
Expand Down Expand Up @@ -1179,6 +1269,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()
}
}
Expand Down
Loading
Loading