mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-22 02:01:08 +07:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ef72fee1d | |||
| d77e71d7cd |
+2
-1
@@ -1,7 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(AMNEZIAVPN_VERSION 4.8.14.5)
|
||||
set(AMNEZIAVPN_VERSION 4.8.14.6)
|
||||
|
||||
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
||||
DESCRIPTION "AmneziaVPN"
|
||||
@@ -12,6 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
|
||||
set(RELEASE_DATE "${CURRENT_DATE}")
|
||||
|
||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||
|
||||
set(APP_ANDROID_VERSION_CODE 2118)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
|
||||
@@ -109,16 +109,6 @@ void AmneziaApplication::init()
|
||||
// install filter on main window
|
||||
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
|
||||
win->installEventFilter(this);
|
||||
#ifdef Q_OS_ANDROID
|
||||
QObject::connect(win, &QQuickWindow::sceneGraphError,
|
||||
[](QQuickWindow::SceneGraphError, const QString &msg) {
|
||||
qWarning() << "Scene graph error (suppressed):" << msg;
|
||||
});
|
||||
// Keep graphics context alive across hide/show cycles to avoid
|
||||
// eglSwapBuffers/makeCurrent being called on a context Android has reclaimed.
|
||||
win->setPersistentSceneGraph(true);
|
||||
win->setPersistentGraphics(true);
|
||||
#endif
|
||||
win->show();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -122,4 +122,5 @@ dependencies {
|
||||
implementation(libs.google.mlkit)
|
||||
implementation(libs.androidx.datastore)
|
||||
implementation(libs.androidx.biometric)
|
||||
implementation(libs.google.play.review)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ androidx-datastore = "1.1.1"
|
||||
kotlinx-coroutines = "1.8.1"
|
||||
kotlinx-serialization = "1.6.3"
|
||||
google-mlkit = "17.3.0"
|
||||
google-play-review = "2.0.2"
|
||||
|
||||
[libraries]
|
||||
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
||||
@@ -28,6 +29,7 @@ androidx-datastore = { module = "androidx.datastore:datastore-preferences", vers
|
||||
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
|
||||
kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinx-serialization" }
|
||||
google-mlkit = { module = "com.google.mlkit:barcode-scanning", version.ref = "google-mlkit" }
|
||||
google-play-review = { module = "com.google.android.play:review-ktx", version.ref = "google-play-review" }
|
||||
|
||||
[bundles]
|
||||
androidx-camera = [
|
||||
|
||||
@@ -297,7 +297,10 @@ class AmneziaActivity : QtActivity() {
|
||||
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
|
||||
|
||||
// Cancel pending operations if window loses focus
|
||||
if (!hasFocus) {
|
||||
if (hasFocus) {
|
||||
ReviewManager.onWindowFocusGained(this, mainScope)
|
||||
} else {
|
||||
// Cancel pending operations if window loses focus
|
||||
resumeHandler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
}
|
||||
@@ -350,6 +353,8 @@ class AmneziaActivity : QtActivity() {
|
||||
isActivityResumed = true
|
||||
Log.d(TAG, "Resume Amnezia activity")
|
||||
|
||||
ReviewManager.onActivityResumed(mainScope)
|
||||
|
||||
if (pendingOpenFileUri != null && !openFileDeliveryScheduled) {
|
||||
val uri = pendingOpenFileUri!!
|
||||
openFileDeliveryScheduled = true
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.app.Activity
|
||||
import com.google.android.play.core.review.ReviewManagerFactory
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.amnezia.vpn.util.Log
|
||||
import org.amnezia.vpn.util.Prefs
|
||||
|
||||
private const val TAG = "ReviewManager"
|
||||
|
||||
private const val PREFS_REVIEW_OPEN_COUNT = "REVIEW_OPEN_COUNT"
|
||||
private const val REVIEW_REQUEST_OPEN_INTERVAL = 20
|
||||
|
||||
object ReviewManager {
|
||||
|
||||
@Volatile
|
||||
private var shouldRequestReviewOnFocus = false
|
||||
|
||||
/**
|
||||
* Call from onResume
|
||||
* Increments the open count and arms the flag if the interval is hit.
|
||||
* I/O runs on the IO dispatcher to avoid blocking the main thread.
|
||||
*/
|
||||
fun onActivityResumed(scope: CoroutineScope) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val openCount = Prefs.load<Int>(PREFS_REVIEW_OPEN_COUNT) + 1
|
||||
val shouldRequest = openCount >= REVIEW_REQUEST_OPEN_INTERVAL
|
||||
Prefs.save(PREFS_REVIEW_OPEN_COUNT, if (shouldRequest) 0 else openCount)
|
||||
|
||||
shouldRequestReviewOnFocus = shouldRequest
|
||||
Log.i(TAG, "onActivityResumed: openCount=$openCount, shouldRequestReview=$shouldRequestReviewOnFocus")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call from onWindowFocusChanged (hasFocus=true)
|
||||
*/
|
||||
fun onWindowFocusGained(activity: Activity, scope: CoroutineScope) {
|
||||
if (!shouldRequestReviewOnFocus) return
|
||||
shouldRequestReviewOnFocus = false
|
||||
|
||||
scope.launch(Dispatchers.Main) {
|
||||
requestReviewFlow(activity)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestReviewFlow(activity: Activity) {
|
||||
val reviewManager = ReviewManagerFactory.create(activity)
|
||||
reviewManager.requestReviewFlow().addOnCompleteListener { request ->
|
||||
if (request.isSuccessful) {
|
||||
reviewManager.launchReviewFlow(activity, request.result)
|
||||
} else {
|
||||
Log.w(TAG, "Review flow request failed: ${request.exception}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,13 +126,8 @@ extension PacketTunnelProvider {
|
||||
}
|
||||
|
||||
vpnReachability.startTracking { [weak self] status in
|
||||
switch status {
|
||||
case .reachableViaWiFi, .reachableViaWWAN:
|
||||
ovpnLog(.info, message: "Reachability changed, reconnecting OpenVPN session")
|
||||
self?.ovpnAdapter?.reconnect(afterTimeInterval: 1)
|
||||
default:
|
||||
break
|
||||
}
|
||||
guard status == .reachableViaWiFi else { return }
|
||||
self?.ovpnAdapter?.reconnect(afterTimeInterval: 5)
|
||||
}
|
||||
|
||||
startHandler = completionHandler
|
||||
|
||||
@@ -41,13 +41,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
var ovpnAdapter: OpenVPNAdapter?
|
||||
private lazy var openVPNPacketFlowAdapter = PacketTunnelFlowAdapter(flow: packetFlow)
|
||||
private let pathMonitorQueue = DispatchQueue(label: Constants.processQueueName + ".path-monitor")
|
||||
private let networkChangeQueue = DispatchQueue(label: Constants.processQueueName + ".network-change")
|
||||
private let pathMonitor = NWPathMonitor()
|
||||
private var didReceiveInitialPathUpdate = false
|
||||
private var currentPath: Network.NWPath?
|
||||
private var currentPathSignature: String?
|
||||
private var pendingNetworkChangeWorkItem: DispatchWorkItem?
|
||||
private var isApplyingNetworkChange = false
|
||||
|
||||
var splitTunnelType: Int?
|
||||
var splitTunnelSites: [String]?
|
||||
@@ -81,13 +78,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
guard hasMeaningfulChange, let proto = self.protoType else { return }
|
||||
|
||||
// OpenVPN and WireGuard/AWG handle network changes internally.
|
||||
// Restarting them here can race their own reconnect logic and break tunnel setup.
|
||||
if proto == .wireguard || proto == .openvpn {
|
||||
// WireGuard/AWG manages network changes internally; avoid restarting the tunnel here.
|
||||
if proto == .wireguard {
|
||||
return
|
||||
}
|
||||
|
||||
self.scheduleNetworkChangeHandling(for: proto, path: path)
|
||||
DispatchQueue.main.async {
|
||||
self.handle(networkChange: path) { _ in }
|
||||
}
|
||||
}
|
||||
pathMonitor.start(queue: pathMonitorQueue)
|
||||
|
||||
@@ -261,47 +259,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
}
|
||||
|
||||
private func handle(networkChange changePath: Network.NWPath, completion: @escaping (Error?) -> Void) {
|
||||
guard protoType == .xray else {
|
||||
updateActiveInterfaceIndex(for: changePath)
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
updateActiveInterfaceIndex(for: changePath)
|
||||
reasserting = true
|
||||
xrayLog(.info, message: "Applying network change to xray tunnel")
|
||||
stopXray { }
|
||||
startXray { [weak self] error in
|
||||
self?.reasserting = false
|
||||
completion(error)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleNetworkChangeHandling(for proto: TunnelProtoType, path: Network.NWPath) {
|
||||
guard proto == .xray else { return }
|
||||
|
||||
pendingNetworkChangeWorkItem?.cancel()
|
||||
|
||||
let workItem = DispatchWorkItem { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
if self.isApplyingNetworkChange {
|
||||
xrayLog(.debug, message: "Skipping network change while restart is already in progress")
|
||||
return
|
||||
}
|
||||
|
||||
self.isApplyingNetworkChange = true
|
||||
DispatchQueue.main.async {
|
||||
self.handle(networkChange: path) { [weak self] _ in
|
||||
self?.networkChangeQueue.async {
|
||||
self?.isApplyingNetworkChange = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pendingNetworkChangeWorkItem = workItem
|
||||
networkChangeQueue.asyncAfter(deadline: .now() + 1.0, execute: workItem)
|
||||
wg_log(.info, message: "Tunnel restarted.")
|
||||
startTunnel(options: nil, completionHandler: completion)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,14 +21,10 @@ Window {
|
||||
function onStateChanged() {
|
||||
if (Qt.platform.os === "android") {
|
||||
if (Qt.application.state === Qt.ApplicationActive) {
|
||||
root.visible = true
|
||||
refreshTimer.restart()
|
||||
} else if (Qt.application.state === Qt.ApplicationSuspended) {
|
||||
// Hide window to stop the Qt render loop and prevent
|
||||
// eglSwapBuffers from being called on a lost EGL context.
|
||||
// NOTE: Do NOT hide on ApplicationInactive — that fires on any
|
||||
// focus change (IME, notifications) and would blank the screen.
|
||||
root.visible = false
|
||||
} else if (Qt.application.state === Qt.ApplicationSuspended ||
|
||||
Qt.application.state === Qt.ApplicationInactive) {
|
||||
console.log("QML: Application going to background, state:", Qt.application.state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user