diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp
index 0831b1a2..1ac179fd 100644
--- a/client/amnezia_application.cpp
+++ b/client/amnezia_application.cpp
@@ -95,7 +95,14 @@ void AmneziaApplication::init()
qFatal("Android logging initialization failed");
}
AndroidController::instance()->setSaveLogs(m_settings->isSaveLogs());
- connect(m_settings.get(), &Settings::saveLogsChanged, AndroidController::instance(), &AndroidController::setSaveLogs);
+ connect(m_settings.get(), &Settings::saveLogsChanged,
+ AndroidController::instance(), &AndroidController::setSaveLogs);
+
+ connect(m_settings.get(), &Settings::serverRemoved,
+ AndroidController::instance(), &AndroidController::resetLastServer);
+
+ connect(m_settings.get(), &Settings::settingsCleared,
+ [](){ AndroidController::instance()->resetLastServer(-1); });
connect(AndroidController::instance(), &AndroidController::initConnectionState, this,
[this](Vpn::ConnectionState state) {
diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml
index 22eed003..548c9bb9 100644
--- a/client/android/AndroidManifest.xml
+++ b/client/android/AndroidManifest.xml
@@ -56,6 +56,10 @@
+
+
+
+
@@ -146,6 +150,22 @@
+
+
+
+
+
+
+
+
+
+
+ Подключение
+ Отключение
+
\ No newline at end of file
diff --git a/client/android/res/values/strings.xml b/client/android/res/values/strings.xml
new file mode 100644
index 00000000..3fdd9844
--- /dev/null
+++ b/client/android/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+
+ Connecting
+ Disconnecting
+
\ 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 9a813626..f01e8df6 100644
--- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt
+++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt
@@ -26,9 +26,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
-import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
-import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.getStatistics
import org.amnezia.vpn.protocol.getStatus
import org.amnezia.vpn.qt.QtAndroidController
@@ -36,11 +34,11 @@ import org.amnezia.vpn.util.Log
import org.qtproject.qt.android.bindings.QtActivity
private const val TAG = "AmneziaActivity"
+const val ACTIVITY_MESSENGER_NAME = "Activity"
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 BIND_SERVICE_TIMEOUT = 1000L
class AmneziaActivity : QtActivity() {
@@ -58,25 +56,17 @@ class AmneziaActivity : QtActivity() {
val event = msg.extractIpcMessage()
Log.d(TAG, "Handle event: $event")
when (event) {
- ServiceEvent.CONNECTED -> {
- QtAndroidController.onVpnConnected()
- }
-
- ServiceEvent.DISCONNECTED -> {
- QtAndroidController.onVpnDisconnected()
- doUnbindService()
- }
-
- ServiceEvent.RECONNECTING -> {
- QtAndroidController.onVpnReconnecting()
+ ServiceEvent.STATUS_CHANGED -> {
+ msg.data?.getStatus()?.let { (state) ->
+ Log.d(TAG, "Handle protocol state: $state")
+ QtAndroidController.onVpnStateChanged(state.ordinal)
+ }
}
ServiceEvent.STATUS -> {
if (isWaitingStatus) {
isWaitingStatus = false
- msg.data?.getStatus()?.let { (state) ->
- QtAndroidController.onStatus(state.ordinal)
- }
+ msg.data?.getStatus()?.let { QtAndroidController.onStatus(it) }
}
}
@@ -87,7 +77,7 @@ class AmneziaActivity : QtActivity() {
}
ServiceEvent.ERROR -> {
- msg.data?.getString(ERROR_MSG)?.let { error ->
+ msg.data?.getString(MSG_ERROR)?.let { error ->
Log.e(TAG, "From VpnService: $error")
}
// todo: add error reporting to Qt
@@ -109,14 +99,15 @@ class AmneziaActivity : QtActivity() {
// get a messenger from the service to send actions to the service
vpnServiceMessenger.set(Messenger(service))
// send a messenger to the service to process service events
- vpnServiceMessenger.send {
- Action.REGISTER_CLIENT.packToMessage().apply {
- replyTo = activityMessenger
- }
- }
+ vpnServiceMessenger.send(
+ Action.REGISTER_CLIENT.packToMessage {
+ putString(MSG_CLIENT_NAME, ACTIVITY_MESSENGER_NAME)
+ },
+ replyTo = activityMessenger
+ )
isServiceConnected = true
if (isWaitingStatus) {
- vpnServiceMessenger.send(Action.REQUEST_STATUS)
+ vpnServiceMessenger.send(Action.REQUEST_STATUS, replyTo = activityMessenger)
}
}
@@ -126,6 +117,7 @@ class AmneziaActivity : QtActivity() {
vpnServiceMessenger.reset()
isWaitingStatus = true
QtAndroidController.onServiceDisconnected()
+ doBindService()
}
override fun onBindingDied(name: ComponentName?) {
@@ -148,8 +140,11 @@ class AmneziaActivity : QtActivity() {
Log.d(TAG, "Create Amnezia activity: $intent")
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
vpnServiceMessenger = IpcMessenger(
- onDeadObjectException = ::doUnbindService,
- messengerName = "VpnService"
+ "VpnService",
+ onDeadObjectException = {
+ doUnbindService()
+ doBindService()
+ }
)
intent?.let(::processIntent)
}
@@ -244,10 +239,9 @@ class AmneziaActivity : QtActivity() {
private fun doBindService() {
Log.d(TAG, "Bind service")
Intent(this, AmneziaVpnService::class.java).also {
- bindService(it, serviceConnection, BIND_ABOVE_CLIENT)
+ bindService(it, serviceConnection, BIND_ABOVE_CLIENT and BIND_AUTO_CREATE)
}
isInBoundState = true
- handleBindTimeout()
}
@MainThread
@@ -256,26 +250,14 @@ class AmneziaActivity : QtActivity() {
Log.d(TAG, "Unbind service")
isWaitingStatus = true
QtAndroidController.onServiceDisconnected()
- vpnServiceMessenger.reset()
isServiceConnected = false
+ vpnServiceMessenger.send(Action.UNREGISTER_CLIENT, activityMessenger)
+ vpnServiceMessenger.reset()
isInBoundState = false
unbindService(serviceConnection)
}
}
- private fun handleBindTimeout() {
- mainScope.launch {
- if (isWaitingStatus) {
- delay(BIND_SERVICE_TIMEOUT)
- if (isWaitingStatus && !isServiceConnected) {
- Log.d(TAG, "Bind timeout, reset connection status")
- isWaitingStatus = false
- QtAndroidController.onStatus(ProtocolState.DISCONNECTED.ordinal)
- }
- }
- }
- }
-
/**
* Methods of starting and stopping VpnService
*/
@@ -312,7 +294,7 @@ class AmneziaActivity : QtActivity() {
Log.d(TAG, "Connect to VPN")
vpnServiceMessenger.send {
Action.CONNECT.packToMessage {
- putString(VPN_CONFIG, vpnConfig)
+ putString(MSG_VPN_CONFIG, vpnConfig)
}
}
}
@@ -320,7 +302,7 @@ class AmneziaActivity : QtActivity() {
private fun startVpnService(vpnConfig: String) {
Log.d(TAG, "Start VPN service")
Intent(this, AmneziaVpnService::class.java).apply {
- putExtra(VPN_CONFIG, vpnConfig)
+ putExtra(MSG_VPN_CONFIG, vpnConfig)
}.also {
ContextCompat.startForegroundService(this, it)
}
@@ -369,6 +351,22 @@ class AmneziaActivity : QtActivity() {
}
}
+ @Suppress("unused")
+ fun resetLastServer(index: Int) {
+ Log.v(TAG, "Reset server: $index")
+ mainScope.launch {
+ VpnStateStore.store {
+ if (index == -1 || it.serverIndex == index) {
+ VpnState.defaultState
+ } else if (it.serverIndex > index) {
+ it.copy(serverIndex = it.serverIndex - 1)
+ } else {
+ it
+ }
+ }
+ }
+ }
+
@Suppress("unused")
fun saveFile(fileName: String, data: String) {
Log.d(TAG, "Save file $fileName")
@@ -438,7 +436,7 @@ class AmneziaActivity : QtActivity() {
Log.saveLogs = enabled
vpnServiceMessenger.send {
Action.SET_SAVE_LOGS.packToMessage {
- putBoolean(SAVE_LOGS, enabled)
+ putBoolean(MSG_SAVE_LOGS, enabled)
}
}
}
diff --git a/client/android/src/org/amnezia/vpn/AmneziaApplication.kt b/client/android/src/org/amnezia/vpn/AmneziaApplication.kt
index 33182887..d8c87bd6 100644
--- a/client/android/src/org/amnezia/vpn/AmneziaApplication.kt
+++ b/client/android/src/org/amnezia/vpn/AmneziaApplication.kt
@@ -18,6 +18,7 @@ class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
super.onCreate()
Prefs.init(this)
Log.init(this)
+ VpnStateStore.init(this)
Log.d(TAG, "Create Amnezia application")
createNotificationChannel()
}
diff --git a/client/android/src/org/amnezia/vpn/AmneziaTileService.kt b/client/android/src/org/amnezia/vpn/AmneziaTileService.kt
new file mode 100644
index 00000000..5ad872a0
--- /dev/null
+++ b/client/android/src/org/amnezia/vpn/AmneziaTileService.kt
@@ -0,0 +1,272 @@
+package org.amnezia.vpn
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.content.ComponentName
+import android.content.Intent
+import android.content.ServiceConnection
+import android.net.VpnService
+import android.os.Build
+import android.os.IBinder
+import android.os.Messenger
+import android.service.quicksettings.Tile
+import android.service.quicksettings.TileService
+import androidx.core.content.ContextCompat
+import kotlin.LazyThreadSafetyMode.NONE
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import org.amnezia.vpn.protocol.ProtocolState
+import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
+import org.amnezia.vpn.protocol.ProtocolState.CONNECTING
+import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
+import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTING
+import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING
+import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN
+import org.amnezia.vpn.util.Log
+
+private const val TAG = "AmneziaTileService"
+private const val DEFAULT_TILE_LABEL = "AmneziaVPN"
+
+class AmneziaTileService : TileService() {
+
+ private lateinit var scope: CoroutineScope
+ private var vpnStateListeningJob: Job? = null
+ private lateinit var vpnServiceMessenger: IpcMessenger
+
+ @Volatile
+ private var isServiceConnected = false
+ private var isInBoundState = false
+ @Volatile
+ private var isVpnConfigExists = false
+
+ private val serviceConnection: ServiceConnection by lazy(NONE) {
+ object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+ Log.d(TAG, "Service ${name?.flattenToString()} was connected")
+ vpnServiceMessenger.set(Messenger(service))
+ isServiceConnected = true
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ Log.w(TAG, "Service ${name?.flattenToString()} was unexpectedly disconnected")
+ isServiceConnected = false
+ vpnServiceMessenger.reset()
+ updateVpnState(DISCONNECTED)
+ }
+
+ override fun onBindingDied(name: ComponentName?) {
+ Log.w(TAG, "Binding to the ${name?.flattenToString()} unexpectedly died")
+ doUnbindService()
+ doBindService()
+ }
+ }
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ Log.d(TAG, "Create Amnezia Tile Service")
+ scope = CoroutineScope(SupervisorJob())
+ vpnServiceMessenger = IpcMessenger(
+ "VpnService",
+ onDeadObjectException = ::doUnbindService
+ )
+ }
+
+ override fun onDestroy() {
+ Log.d(TAG, "Destroy Amnezia Tile Service")
+ doUnbindService()
+ scope.cancel()
+ super.onDestroy()
+ }
+
+ // Workaround for some bugs
+ override fun onBind(intent: Intent?): IBinder? =
+ try {
+ super.onBind(intent)
+ } catch (e: Throwable) {
+ Log.e(TAG, "Failed to bind AmneziaTileService: $e")
+ null
+ }
+
+ override fun onStartListening() {
+ super.onStartListening()
+ Log.d(TAG, "Start listening")
+ if (AmneziaVpnService.isRunning(applicationContext)) {
+ Log.d(TAG, "Vpn service is running")
+ doBindService()
+ } else {
+ Log.d(TAG, "Vpn service is not running")
+ isServiceConnected = false
+ updateVpnState(DISCONNECTED)
+ }
+ vpnStateListeningJob = launchVpnStateListening()
+ }
+
+ override fun onStopListening() {
+ Log.d(TAG, "Stop listening")
+ vpnStateListeningJob?.cancel()
+ vpnStateListeningJob = null
+ doUnbindService()
+ super.onStopListening()
+ }
+
+ override fun onClick() {
+ Log.d(TAG, "onClick")
+ if (isLocked) {
+ unlockAndRun { onClickInternal() }
+ } else {
+ onClickInternal()
+ }
+ }
+
+ private fun onClickInternal() {
+ if (isVpnConfigExists) {
+ Log.d(TAG, "Change VPN state")
+ if (qsTile.state == Tile.STATE_INACTIVE) {
+ Log.d(TAG, "Start VPN")
+ updateVpnState(CONNECTING)
+ startVpn()
+ } else if (qsTile.state == Tile.STATE_ACTIVE) {
+ Log.d(TAG, "Stop vpn")
+ updateVpnState(DISCONNECTING)
+ stopVpn()
+ }
+ } else {
+ Log.d(TAG, "Start Activity")
+ Intent(this, AmneziaActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }.also {
+ startActivityAndCollapseCompat(it)
+ }
+ }
+ }
+
+ private fun doBindService() {
+ Log.d(TAG, "Bind service")
+ Intent(this, AmneziaVpnService::class.java).also {
+ bindService(it, serviceConnection, BIND_ABOVE_CLIENT)
+ }
+ isInBoundState = true
+ }
+
+ private fun doUnbindService() {
+ if (isInBoundState) {
+ Log.d(TAG, "Unbind service")
+ isServiceConnected = false
+ vpnServiceMessenger.reset()
+ isInBoundState = false
+ unbindService(serviceConnection)
+ }
+ }
+
+ private fun startVpn() {
+ if (isServiceConnected) {
+ connectToVpn()
+ } else {
+ if (checkPermission()) {
+ startVpnService()
+ doBindService()
+ } else {
+ updateVpnState(DISCONNECTED)
+ }
+ }
+ }
+
+ private fun checkPermission() =
+ if (VpnService.prepare(applicationContext) != null) {
+ Intent(this, VpnRequestActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }.also {
+ startActivityAndCollapseCompat(it)
+ }
+ false
+ } else {
+ true
+ }
+
+ private fun startVpnService() =
+ ContextCompat.startForegroundService(
+ applicationContext,
+ Intent(this, AmneziaVpnService::class.java)
+ )
+
+ private fun connectToVpn() = vpnServiceMessenger.send(Action.CONNECT)
+
+ private fun stopVpn() = vpnServiceMessenger.send(Action.DISCONNECT)
+
+ @SuppressLint("StartActivityAndCollapseDeprecated")
+ private fun startActivityAndCollapseCompat(intent: Intent) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ startActivityAndCollapse(
+ PendingIntent.getActivity(
+ applicationContext,
+ 0,
+ intent,
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ startActivityAndCollapse(intent)
+ }
+ }
+
+ private fun updateVpnState(state: ProtocolState) {
+ scope.launch {
+ VpnStateStore.store { it.copy(protocolState = state) }
+ }
+ }
+
+ private fun launchVpnStateListening() =
+ scope.launch { VpnStateStore.dataFlow().collectLatest(::updateTile) }
+
+ private fun updateTile(vpnState: VpnState) {
+ Log.d(TAG, "Update tile: $vpnState")
+ isVpnConfigExists = vpnState.serverName != null
+ val tile = qsTile ?: return
+ tile.apply {
+ label = vpnState.serverName ?: DEFAULT_TILE_LABEL
+ when (vpnState.protocolState) {
+ CONNECTED -> {
+ state = Tile.STATE_ACTIVE
+ subtitleCompat = null
+ }
+
+ DISCONNECTED, UNKNOWN -> {
+ state = Tile.STATE_INACTIVE
+ subtitleCompat = null
+ }
+
+ CONNECTING, RECONNECTING -> {
+ state = Tile.STATE_UNAVAILABLE
+ subtitleCompat = resources.getString(R.string.connecting)
+ }
+
+ DISCONNECTING -> {
+ state = Tile.STATE_UNAVAILABLE
+ subtitleCompat = resources.getString(R.string.disconnecting)
+ }
+ }
+ updateTile()
+ }
+ // double update to fix weird visual glitches
+ tile.updateTile()
+ }
+
+ private var Tile.subtitleCompat: CharSequence?
+ set(value) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ this.subtitle = value
+ }
+ }
+ get() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ return this.subtitle
+ }
+ return null
+ }
+}
diff --git a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt
index 78f89ab8..094383e8 100644
--- a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt
+++ b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt
@@ -1,7 +1,10 @@
package org.amnezia.vpn
+import android.app.ActivityManager
+import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
import android.app.Notification
import android.app.PendingIntent
+import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
@@ -16,6 +19,7 @@ import android.os.Process
import androidx.annotation.MainThread
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
+import java.util.concurrent.ConcurrentHashMap
import kotlin.LazyThreadSafetyMode.NONE
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
@@ -26,6 +30,7 @@ import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -39,14 +44,11 @@ import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTING
import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING
import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN
-import org.amnezia.vpn.protocol.Statistics
-import org.amnezia.vpn.protocol.Status
import org.amnezia.vpn.protocol.VpnException
import org.amnezia.vpn.protocol.VpnStartException
import org.amnezia.vpn.protocol.awg.Awg
import org.amnezia.vpn.protocol.cloak.Cloak
import org.amnezia.vpn.protocol.openvpn.OpenVpn
-import org.amnezia.vpn.protocol.putStatistics
import org.amnezia.vpn.protocol.putStatus
import org.amnezia.vpn.protocol.wireguard.Wireguard
import org.amnezia.vpn.util.Log
@@ -57,12 +59,16 @@ import org.json.JSONObject
private const val TAG = "AmneziaVpnService"
-const val VPN_CONFIG = "VPN_CONFIG"
-const val ERROR_MSG = "ERROR_MSG"
-const val SAVE_LOGS = "SAVE_LOGS"
+const val MSG_VPN_CONFIG = "VPN_CONFIG"
+const val MSG_ERROR = "ERROR"
+const val MSG_SAVE_LOGS = "SAVE_LOGS"
+const val MSG_CLIENT_NAME = "CLIENT_NAME"
const val AFTER_PERMISSION_CHECK = "AFTER_PERMISSION_CHECK"
private const val PREFS_CONFIG_KEY = "LAST_CONF"
+private const val PREFS_SERVER_NAME = "LAST_SERVER_NAME"
+private const val PREFS_SERVER_INDEX = "LAST_SERVER_INDEX"
+private const val PROCESS_NAME = "org.amnezia.vpn:amneziaVpnService"
private const val NOTIFICATION_ID = 1337
private const val STATISTICS_SENDING_TIMEOUT = 1000L
private const val DISCONNECT_TIMEOUT = 5000L
@@ -76,6 +82,8 @@ class AmneziaVpnService : VpnService() {
private var protocol: Protocol? = null
private val protocolCache = mutableMapOf()
private var protocolState = MutableStateFlow(UNKNOWN)
+ private var serverName: String? = null
+ private var serverIndex: Int = -1
private val isConnected
get() = protocolState.value == CONNECTED
@@ -89,8 +97,11 @@ class AmneziaVpnService : VpnService() {
private var connectionJob: Job? = null
private var disconnectionJob: Job? = null
private var statisticsSendingJob: Job? = null
- private lateinit var clientMessenger: IpcMessenger
private lateinit var networkState: NetworkState
+ private val clientMessengers = ConcurrentHashMap()
+
+ private val isActivityConnected
+ get() = clientMessengers.any { it.value.name == ACTIVITY_MESSENGER_NAME }
private val connectionExceptionHandler = CoroutineExceptionHandler { _, e ->
protocolState.value = DISCONNECTED
@@ -116,13 +127,22 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "Handle action: $action")
when (action) {
Action.REGISTER_CLIENT -> {
- clientMessenger.set(msg.replyTo)
+ val clientName = msg.data.getString(MSG_CLIENT_NAME)
+ val messenger = IpcMessenger(msg.replyTo, clientName)
+ clientMessengers[msg.replyTo] = messenger
+ Log.d(TAG, "Messenger client '$clientName' was registered")
+ if (clientName == ACTIVITY_MESSENGER_NAME && isConnected) launchSendingStatistics()
+ }
+
+ Action.UNREGISTER_CLIENT -> {
+ clientMessengers.remove(msg.replyTo)?.let {
+ Log.d(TAG, "Messenger client '${it.name}' was unregistered")
+ if (it.name == ACTIVITY_MESSENGER_NAME) stopSendingStatistics()
+ }
}
Action.CONNECT -> {
- val vpnConfig = msg.data.getString(VPN_CONFIG)
- Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
- connect(vpnConfig)
+ connect(msg.data.getString(MSG_VPN_CONFIG))
}
Action.DISCONNECT -> {
@@ -130,17 +150,17 @@ class AmneziaVpnService : VpnService() {
}
Action.REQUEST_STATUS -> {
- clientMessenger.send {
- ServiceEvent.STATUS.packToMessage {
- putStatus(Status.build {
- setState(this@AmneziaVpnService.protocolState.value)
- })
+ clientMessengers[msg.replyTo]?.let { clientMessenger ->
+ clientMessenger.send {
+ ServiceEvent.STATUS.packToMessage {
+ putStatus(this@AmneziaVpnService.protocolState.value)
+ }
}
}
}
Action.SET_SAVE_LOGS -> {
- Log.saveLogs = msg.data.getBoolean(SAVE_LOGS)
+ Log.saveLogs = msg.data.getBoolean(MSG_SAVE_LOGS)
}
}
}
@@ -189,7 +209,7 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "Create Amnezia VPN service")
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
connectionScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + connectionExceptionHandler)
- clientMessenger = IpcMessenger(messengerName = "Client")
+ loadServerData()
launchProtocolStateHandler()
networkState = NetworkState(this, ::reconnect)
}
@@ -201,15 +221,13 @@ class AmneziaVpnService : VpnService() {
if (isAlwaysOnCompat) {
Log.d(TAG, "Start service via Always-on")
- connect(Prefs.load(PREFS_CONFIG_KEY))
+ connect()
} else if (intent?.getBooleanExtra(AFTER_PERMISSION_CHECK, false) == true) {
Log.d(TAG, "Start service after permission check")
- connect(Prefs.load(PREFS_CONFIG_KEY))
+ connect()
} else {
Log.d(TAG, "Start service")
- val vpnConfig = intent?.getStringExtra(VPN_CONFIG)
- Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
- connect(vpnConfig)
+ connect(intent?.getStringExtra(MSG_VPN_CONFIG))
}
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, foregroundServiceTypeCompat)
return START_REDELIVER_INTENT
@@ -219,17 +237,16 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "onBind by $intent")
if (intent?.action == SERVICE_INTERFACE) return super.onBind(intent)
isServiceBound = true
- if (isConnected) launchSendingStatistics()
return vpnServiceMessenger.binder
}
override fun onUnbind(intent: Intent?): Boolean {
Log.d(TAG, "onUnbind by $intent")
if (intent?.action != SERVICE_INTERFACE) {
- isServiceBound = false
- stopSendingStatistics()
- clientMessenger.reset()
- if (isUnknown || isDisconnected) stopService()
+ if (clientMessengers.isEmpty()) {
+ isServiceBound = false
+ if (isUnknown || isDisconnected) stopService()
+ }
}
return true
}
@@ -238,7 +255,6 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "onRebind by $intent")
if (intent?.action != SERVICE_INTERFACE) {
isServiceBound = true
- if (isConnected) launchSendingStatistics()
}
super.onRebind(intent)
}
@@ -278,17 +294,16 @@ class AmneziaVpnService : VpnService() {
*/
private fun launchProtocolStateHandler() {
mainScope.launch {
- protocolState.collect { protocolState ->
+ // drop first default UNKNOWN state
+ protocolState.drop(1).collect { protocolState ->
Log.d(TAG, "Protocol state changed: $protocolState")
when (protocolState) {
CONNECTED -> {
- clientMessenger.send(ServiceEvent.CONNECTED)
networkState.bindNetworkListener()
- if (isServiceBound) launchSendingStatistics()
+ if (isActivityConnected) launchSendingStatistics()
}
DISCONNECTED -> {
- clientMessenger.send(ServiceEvent.DISCONNECTED)
networkState.unbindNetworkListener()
stopSendingStatistics()
if (!isServiceBound) stopService()
@@ -300,12 +315,19 @@ class AmneziaVpnService : VpnService() {
}
RECONNECTING -> {
- clientMessenger.send(ServiceEvent.RECONNECTING)
stopSendingStatistics()
}
CONNECTING, UNKNOWN -> {}
}
+
+ clientMessengers.send {
+ ServiceEvent.STATUS_CHANGED.packToMessage {
+ putStatus(protocolState)
+ }
+ }
+
+ VpnStateStore.store { VpnState(protocolState, serverName, serverIndex) }
}
}
}
@@ -332,7 +354,17 @@ class AmneziaVpnService : VpnService() {
}
@MainThread
- private fun connect(vpnConfig: String?) {
+ private fun connect(vpnConfig: String? = null) {
+ if (vpnConfig == null) {
+ connectToVpn(Prefs.load(PREFS_CONFIG_KEY))
+ } else {
+ Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
+ connectToVpn(vpnConfig)
+ }
+ }
+
+ @MainThread
+ private fun connectToVpn(vpnConfig: String) {
if (isConnected || protocolState.value == CONNECTING) return
Log.d(TAG, "Start VPN connection")
@@ -340,6 +372,7 @@ class AmneziaVpnService : VpnService() {
protocolState.value = CONNECTING
val config = parseConfigToJson(vpnConfig)
+ saveServerData(config)
if (config == null) {
onError("Invalid VPN config")
protocolState.value = DISCONNECTED
@@ -417,24 +450,38 @@ class AmneziaVpnService : VpnService() {
private fun onError(msg: String) {
Log.e(TAG, msg)
mainScope.launch {
- clientMessenger.send {
+ clientMessengers.send {
ServiceEvent.ERROR.packToMessage {
- putString(ERROR_MSG, msg)
+ putString(MSG_ERROR, msg)
}
}
}
}
- private fun parseConfigToJson(vpnConfig: String?): JSONObject? =
- try {
- vpnConfig?.let {
- JSONObject(it)
- }
- } catch (e: JSONException) {
- onError("Invalid VPN config json format: ${e.message}")
+ private fun parseConfigToJson(vpnConfig: String): JSONObject? =
+ if (vpnConfig.isBlank()) {
null
+ } else {
+ try {
+ JSONObject(vpnConfig)
+ } catch (e: JSONException) {
+ onError("Invalid VPN config json format: ${e.message}")
+ null
+ }
}
+ private fun saveServerData(config: JSONObject?) {
+ serverName = config?.opt("description") as String?
+ serverIndex = config?.opt("serverIndex") as Int? ?: -1
+ Prefs.save(PREFS_SERVER_NAME, serverName)
+ Prefs.save(PREFS_SERVER_INDEX, serverIndex)
+ }
+
+ private fun loadServerData() {
+ serverName = Prefs.load(PREFS_SERVER_NAME).ifBlank { null }
+ if (serverName != null) serverIndex = Prefs.load(PREFS_SERVER_INDEX)
+ }
+
private fun checkPermission(): Boolean =
if (prepare(applicationContext) != null) {
Intent(this, VpnRequestActivity::class.java).apply {
@@ -446,4 +493,12 @@ class AmneziaVpnService : VpnService() {
} else {
true
}
+
+ companion object {
+ fun isRunning(context: Context): Boolean =
+ (context.getSystemService(ACTIVITY_SERVICE) as ActivityManager)
+ .runningAppProcesses.any {
+ it.processName == PROCESS_NAME && it.importance <= IMPORTANCE_FOREGROUND_SERVICE
+ }
+ }
}
diff --git a/client/android/src/org/amnezia/vpn/IpcMessage.kt b/client/android/src/org/amnezia/vpn/IpcMessage.kt
index 26c3b9de..2ddff4ef 100644
--- a/client/android/src/org/amnezia/vpn/IpcMessage.kt
+++ b/client/android/src/org/amnezia/vpn/IpcMessage.kt
@@ -20,9 +20,7 @@ sealed interface IpcMessage {
}
enum class ServiceEvent : IpcMessage {
- CONNECTED,
- DISCONNECTED,
- RECONNECTING,
+ STATUS_CHANGED,
STATUS,
STATISTICS_UPDATE,
ERROR
@@ -30,6 +28,7 @@ enum class ServiceEvent : IpcMessage {
enum class Action : IpcMessage {
REGISTER_CLIENT,
+ UNREGISTER_CLIENT,
CONNECT,
DISCONNECT,
REQUEST_STATUS,
diff --git a/client/android/src/org/amnezia/vpn/IpcMessenger.kt b/client/android/src/org/amnezia/vpn/IpcMessenger.kt
index 218a165b..58baf31a 100644
--- a/client/android/src/org/amnezia/vpn/IpcMessenger.kt
+++ b/client/android/src/org/amnezia/vpn/IpcMessenger.kt
@@ -9,11 +9,21 @@ import org.amnezia.vpn.util.Log
private const val TAG = "IpcMessenger"
class IpcMessenger(
+ messengerName: String? = null,
private val onDeadObjectException: () -> Unit = {},
- private val onRemoteException: () -> Unit = {},
- private val messengerName: String = "Unknown"
+ private val onRemoteException: () -> Unit = {}
) {
private var messenger: Messenger? = null
+ val name = messengerName ?: "Unknown"
+
+ constructor(
+ messenger: Messenger,
+ messengerName: String? = null,
+ onDeadObjectException: () -> Unit = {},
+ onRemoteException: () -> Unit = {}
+ ) : this(messengerName, onDeadObjectException, onRemoteException) {
+ this.messenger = messenger
+ }
fun set(messenger: Messenger) {
this.messenger = messenger
@@ -25,19 +35,29 @@ class IpcMessenger(
fun send(msg: () -> Message) = messenger?.sendMsg(msg())
+ fun send(msg: Message, replyTo: Messenger) = messenger?.sendMsg(msg.apply { this.replyTo = replyTo })
+
fun send(msg: T)
where T : Enum, T : IpcMessage = messenger?.sendMsg(msg.packToMessage())
+ fun send(msg: T, replyTo: Messenger)
+ where T : Enum, T : IpcMessage = messenger?.sendMsg(msg.packToMessage().apply { this.replyTo = replyTo })
+
private fun Messenger.sendMsg(msg: Message) {
try {
send(msg)
} catch (e: DeadObjectException) {
- Log.w(TAG, "$messengerName messenger is dead")
+ Log.w(TAG, "$name messenger is dead")
messenger = null
onDeadObjectException()
} catch (e: RemoteException) {
- Log.w(TAG, "Sending a message to the $messengerName messenger failed: ${e.message}")
+ Log.w(TAG, "Sending a message to the $name messenger failed: ${e.message}")
onRemoteException()
}
}
}
+
+fun Map.send(msg: () -> Message) = this.values.forEach { it.send(msg) }
+
+fun Map.send(msg: T)
+ where T : Enum, T : IpcMessage = this.values.forEach { it.send(msg) }
diff --git a/client/android/src/org/amnezia/vpn/VpnState.kt b/client/android/src/org/amnezia/vpn/VpnState.kt
new file mode 100644
index 00000000..4d5e9c99
--- /dev/null
+++ b/client/android/src/org/amnezia/vpn/VpnState.kt
@@ -0,0 +1,75 @@
+package org.amnezia.vpn
+
+import android.app.Application
+import androidx.datastore.core.MultiProcessDataStoreFactory
+import androidx.datastore.core.Serializer
+import androidx.datastore.dataStoreFile
+import java.io.InputStream
+import java.io.ObjectInputStream
+import java.io.ObjectOutputStream
+import java.io.OutputStream
+import java.io.Serializable
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.withContext
+import org.amnezia.vpn.protocol.ProtocolState
+import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
+import org.amnezia.vpn.util.Log
+
+private const val TAG = "VpnState"
+private const val STORE_FILE_NAME = "vpnState"
+
+data class VpnState(
+ val protocolState: ProtocolState,
+ val serverName: String? = null,
+ val serverIndex: Int = -1
+) : Serializable {
+ companion object {
+ private const val serialVersionUID: Long = -1760654961004181606
+ val defaultState: VpnState = VpnState(DISCONNECTED)
+ }
+}
+
+object VpnStateStore {
+ private lateinit var app: Application
+
+ private val dataStore = MultiProcessDataStoreFactory.create(
+ serializer = VpnStateSerializer(),
+ produceFile = { app.dataStoreFile(STORE_FILE_NAME) }
+ )
+
+ fun init(app: Application) {
+ Log.v(TAG, "Init VpnStateStore")
+ this.app = app
+ }
+
+ fun dataFlow(): Flow = dataStore.data
+
+ suspend fun store(f: (vpnState: VpnState) -> VpnState) {
+ try {
+ dataStore.updateData(f)
+ } catch (e : Exception) {
+ Log.e(TAG, "Failed to store VpnState: $e")
+ }
+ }
+}
+
+private class VpnStateSerializer : Serializer {
+ override val defaultValue: VpnState = VpnState.defaultState
+
+ override suspend fun readFrom(input: InputStream): VpnState {
+ return withContext(Dispatchers.IO) {
+ ObjectInputStream(input).use {
+ it.readObject() as VpnState
+ }
+ }
+ }
+
+ override suspend fun writeTo(t: VpnState, output: OutputStream) {
+ withContext(Dispatchers.IO) {
+ ObjectOutputStream(output).use {
+ it.writeObject(t)
+ }
+ }
+ }
+}
diff --git a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt
index cab810a7..537d9925 100644
--- a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt
+++ b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt
@@ -1,18 +1,23 @@
package org.amnezia.vpn.qt
+import org.amnezia.vpn.protocol.ProtocolState
+import org.amnezia.vpn.protocol.Status
+
/**
* JNI functions of the AndroidController class from android_controller.cpp,
* called by events in the Android part of the client
*/
object QtAndroidController {
+
+ fun onStatus(status: Status) = onStatus(status.state)
+ fun onStatus(protocolState: ProtocolState) = onStatus(protocolState.ordinal)
+
external fun onStatus(stateCode: Int)
external fun onServiceDisconnected()
external fun onServiceError()
external fun onVpnPermissionRejected()
- external fun onVpnConnected()
- external fun onVpnDisconnected()
- external fun onVpnReconnecting()
+ external fun onVpnStateChanged(stateCode: Int)
external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long)
external fun onFileOpened(uri: String)
diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp
index 767004fc..b789f0e0 100644
--- a/client/platforms/android/android_controller.cpp
+++ b/client/platforms/android/android_controller.cpp
@@ -56,26 +56,10 @@ AndroidController::AndroidController() : QObject()
Qt::QueuedConnection);
connect(
- this, &AndroidController::vpnConnected, this,
- [this]() {
- qDebug() << "Android event: VPN connected";
- emit connectionStateChanged(Vpn::ConnectionState::Connected);
- },
- Qt::QueuedConnection);
-
- connect(
- this, &AndroidController::vpnDisconnected, this,
- [this]() {
- qDebug() << "Android event: VPN disconnected";
- emit connectionStateChanged(Vpn::ConnectionState::Disconnected);
- },
- Qt::QueuedConnection);
-
- connect(
- this, &AndroidController::vpnReconnecting, this,
- [this]() {
- qDebug() << "Android event: VPN reconnecting";
- emit connectionStateChanged(Vpn::ConnectionState::Reconnecting);
+ this, &AndroidController::vpnStateChanged, this,
+ [this](AndroidController::ConnectionState state) {
+ qDebug() << "Android event: VPN state changed:" << textConnectionState(state);
+ emit connectionStateChanged(convertState(state));
},
Qt::QueuedConnection);
@@ -106,9 +90,7 @@ bool AndroidController::initialize()
{"onServiceDisconnected", "()V", reinterpret_cast(onServiceDisconnected)},
{"onServiceError", "()V", reinterpret_cast(onServiceError)},
{"onVpnPermissionRejected", "()V", reinterpret_cast(onVpnPermissionRejected)},
- {"onVpnConnected", "()V", reinterpret_cast(onVpnConnected)},
- {"onVpnDisconnected", "()V", reinterpret_cast(onVpnDisconnected)},
- {"onVpnReconnecting", "()V", reinterpret_cast(onVpnReconnecting)},
+ {"onVpnStateChanged", "(I)V", reinterpret_cast(onVpnStateChanged)},
{"onStatisticsUpdate", "(JJ)V", reinterpret_cast(onStatisticsUpdate)},
{"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast(onFileOpened)},
{"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast(onConfigImported)},
@@ -158,6 +140,11 @@ void AndroidController::stop()
callActivityMethod("stop", "()V");
}
+void AndroidController::resetLastServer(int serverIndex)
+{
+ callActivityMethod("resetLastServer", "(I)V", serverIndex);
+}
+
void AndroidController::saveFile(const QString &fileName, const QString &data)
{
callActivityMethod("saveFile", "(Ljava/lang/String;Ljava/lang/String;)V",
@@ -370,30 +357,14 @@ void AndroidController::onVpnPermissionRejected(JNIEnv *env, jobject thiz)
}
// static
-void AndroidController::onVpnConnected(JNIEnv *env, jobject thiz)
+void AndroidController::onVpnStateChanged(JNIEnv *env, jobject thiz, jint stateCode)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
- emit AndroidController::instance()->vpnConnected();
-}
+ auto state = ConnectionState(stateCode);
-// static
-void AndroidController::onVpnDisconnected(JNIEnv *env, jobject thiz)
-{
- Q_UNUSED(env);
- Q_UNUSED(thiz);
-
- emit AndroidController::instance()->vpnDisconnected();
-}
-
-// static
-void AndroidController::onVpnReconnecting(JNIEnv *env, jobject thiz)
-{
- Q_UNUSED(env);
- Q_UNUSED(thiz);
-
- emit AndroidController::instance()->vpnReconnecting();
+ emit AndroidController::instance()->vpnStateChanged(state);
}
// static
diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h
index 86b117f7..3491d837 100644
--- a/client/platforms/android/android_controller.h
+++ b/client/platforms/android/android_controller.h
@@ -20,9 +20,9 @@ public:
// keep synchronized with org.amnezia.vpn.protocol.ProtocolState
enum class ConnectionState
{
+ DISCONNECTED,
CONNECTED,
CONNECTING,
- DISCONNECTED,
DISCONNECTING,
RECONNECTING,
UNKNOWN
@@ -30,6 +30,7 @@ public:
ErrorCode start(const QJsonObject &vpnConfig);
void stop();
+ void resetLastServer(int serverIndex);
void setNotificationText(const QString &title, const QString &message, int timerSec);
void saveFile(const QString &fileName, const QString &data);
QString openFile(const QString &filter);
@@ -48,9 +49,7 @@ signals:
void serviceDisconnected();
void serviceError();
void vpnPermissionRejected();
- void vpnConnected();
- void vpnDisconnected();
- void vpnReconnecting();
+ void vpnStateChanged(ConnectionState state);
void statisticsUpdated(quint64 rxBytes, quint64 txBytes);
void fileOpened(QString uri);
void configImported(QString config);
@@ -77,9 +76,7 @@ private:
static void onServiceDisconnected(JNIEnv *env, jobject thiz);
static void onServiceError(JNIEnv *env, jobject thiz);
static void onVpnPermissionRejected(JNIEnv *env, jobject thiz);
- static void onVpnConnected(JNIEnv *env, jobject thiz);
- static void onVpnDisconnected(JNIEnv *env, jobject thiz);
- static void onVpnReconnecting(JNIEnv *env, jobject thiz);
+ static void onVpnStateChanged(JNIEnv *env, jobject thiz, jint stateCode);
static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes);
static void onConfigImported(JNIEnv *env, jobject thiz, jstring data);
static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri);
diff --git a/client/protocols/protocols_defs.h b/client/protocols/protocols_defs.h
index f83a0067..8ab5594f 100644
--- a/client/protocols/protocols_defs.h
+++ b/client/protocols/protocols_defs.h
@@ -20,6 +20,7 @@ namespace amnezia
constexpr char dns1[] = "dns1";
constexpr char dns2[] = "dns2";
+ constexpr char serverIndex[] = "serverIndex";
constexpr char description[] = "description";
constexpr char name[] = "name";
constexpr char cert[] = "cert";
diff --git a/client/settings.cpp b/client/settings.cpp
index 475c52e7..223719e2 100644
--- a/client/settings.cpp
+++ b/client/settings.cpp
@@ -68,6 +68,7 @@ void Settings::removeServer(int index)
servers.removeAt(index);
setServersArray(servers);
+ emit serverRemoved(index);
}
bool Settings::editServer(int index, const QJsonObject &server)
@@ -338,6 +339,7 @@ QString Settings::secondaryDns() const
void Settings::clearSettings()
{
m_settings.clearSettings();
+ emit settingsCleared();
}
ServerCredentials Settings::defaultServerCredentials() const
diff --git a/client/settings.h b/client/settings.h
index ed302653..613d567b 100644
--- a/client/settings.h
+++ b/client/settings.h
@@ -191,6 +191,8 @@ public:
signals:
void saveLogsChanged(bool enabled);
+ void serverRemoved(int serverIndex);
+ void settingsCleared();
private:
QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const;
diff --git a/client/vpnconnection.cpp b/client/vpnconnection.cpp
index d267584a..c3719562 100644
--- a/client/vpnconnection.cpp
+++ b/client/vpnconnection.cpp
@@ -270,6 +270,7 @@ QJsonObject VpnConnection::createVpnConfiguration(int serverIndex, const ServerC
ErrorCode *errorCode)
{
QJsonObject vpnConfiguration;
+ vpnConfiguration[config_key::serverIndex] = serverIndex;
for (ProtocolEnumNS::Proto proto : ContainerProps::protocolsForContainer(container)) {
auto s = m_settings->server(serverIndex);
@@ -471,10 +472,15 @@ void VpnConnection::disconnectFromVpn()
#ifdef Q_OS_ANDROID
if (m_vpnProtocol && m_vpnProtocol.data()) {
- connect(AndroidController::instance(), &AndroidController::vpnDisconnected, this,
- [this]() {
- onConnectionStateChanged(Vpn::ConnectionState::Disconnected);
- }, Qt::SingleShotConnection);
+ auto *const connection = new QMetaObject::Connection;
+ *connection = connect(AndroidController::instance(), &AndroidController::vpnStateChanged, this,
+ [this, connection](AndroidController::ConnectionState state) {
+ if (state == AndroidController::ConnectionState::DISCONNECTED) {
+ onConnectionStateChanged(Vpn::ConnectionState::Disconnected);
+ disconnect(*connection);
+ delete connection;
+ }
+ });
m_vpnProtocol.data()->stop();
}
#endif